This is a list of changes to the TADS 3 core system: the language,
the built-in classes and functions, the compiler and related build
tools, the debugger, and the interpreter virtual machine. The change
history is organized by release, with the most recent release first.
(This page only covers changes to the core TADS language and
Virtual Machine engine. There are separate release notes for most
operating system install packages - HTML TADS, FrobTADS, CocoaTADS,
etc. For changes to the Adv3 library, see
Recent Library Changes.)
- New syntax lets you create constant regular expression values:
R"pattern" and R'pattern' are
equivalent, and define static, pre-compiled RexPattern objects. The
compiler converts a string of the form R"..." or
R'...' into a static RexPattern object, which you can
then use in any context where a pattern is required. For example:
local str = 'test string';
local title = str.findReplace(R'%<%w', {x: x.toTitleCase()});
To define a regular expression literal, the R must be capitalized,
and there must be no spaces between the R and the open quote. "R"
strings can't use embedded expressions (the << >> syntax).
The triple-quote syntax can be used with R strings, as in
R"""...""".
In the past, you could achieve a similar effect by defining a
static property and setting it to a new RexPattern()
expression. You can still use that approach, of course, but the new
syntax is more compact and eliminates the need to define an extra
property just to cache the RexPattern object. Using the new syntax
also generates slightly more efficient code, since it doesn't require
a property lookup to retrieve the RexPattern object. (There might be
other reasons to use the property approach in specific cases, though;
for example, in a library or extension, defining the pattern via a
property provides a clean way for users of the module to override the
pattern if they need to replace it with a customized version.)
- The basic integer arithmetic operators (+, -, *, /, and the
corresponding combination operators such as ++ and +=) now check for
overflow, and automatically promote the result to BigNumber when an
overflow occurs. In the past, overflows were simply truncated to fit
the 32-bit integer type. It was very difficult to detect such cases
or to do anything sensible with the results; you really had to just be
careful to avoid overflows, which isn't easy when working with
external or user-entered data. The new treatment ensures that the
results of the arithmetic operators will always be arithmetically
correct, even when they exceed the capacity of the basic integer type.
This is more in keeping with the TADS philosophy of providing
a high-level environment where you don't have to worry about
hardware-level details such as how many bits can be stored in an
integer.
This change should be transparent to existing programs, since (a)
BigNumbers can for the most part be used interchangeably with
integers, and (b) any program that encountered an integer overflow in
the past probably misbehaved when it did, since there was no good way
to detect or handle overflows. The effect on most existing programs
should thus be to allow them to correctly handle a wider range of
inputs. In some cases, the effect will be to flag a run-time error
for a case where a value really does have to fit in an integer, where
in the past the code would have failed somewhat more mysteriously due
to the truncated arithmetic result. The new handling should be an
improvement even in error cases, since the source of the error will
be more immediately apparent than in the past.
There's a slight impact on execution speed because of the extra
checks required for arithmetic operations, although typical TADS
programs aren't arithmetically intensive enough to notice any
difference. What's more, the VM-level checking eliminates the need
for extra program code to do bounds checking, so this could end up
being a performance enhancement in situations where overflows are a
concern.
- The compiler now automatically promotes any integer constant value
that overflows the ordinary integer type to BigNumber. This applies
to values that are explicitly stated (e.g., "x = 3000000000;") as well
as to constant expressions (e.g., "x = 1000000000 * 3;"). The
compiler shows a warning message each time it applies such a
promotion; BigNumber values aren't necessarily allowed in all contexts
where integers are normally used, such as for some built-in function
and method arguments, so the compiler wants you to be aware when it
substitutes a BigNumber for a value you stated as an integer. You can
remove the warning on a case-by-case basis by explicitly stating the
value as a BigNumber constant in the first place, by including a
decimal point in the number (e.g., "x = 3000000000.;").
Integers specified in hex or octal notation (e.g., 0x80000000 or
040000000000) aren't promoted if they can fit within a 32-bit
unsigned integer. Hex and octal are frequently used to enter
numbers with specific bit patterns, so the compiler assumes that's
your intention with these formats. A hex or octal number that's over
the 32-bit unsigned limit of 4294967295 will be promoted, though,
since there's no way to store such a large value in a 32-bit integer
regardless of its signedness.
- The compiler now pre-calculates the results of arithmetic
expressions involving BigNumber constant values. (This is known
as "constant folding".) If an expression contains only constant
numeric values, with any combination of integers and BigNumber values,
the compiler will pre-calculate the results for the ordinary
arithmetic operators (+ - * / %) and the comparison operators (== !=
< > <= >=). In the past, the compiler deferred
calculations involving BigNumber values until run-time; constant
folding improves the execution speed of affected expressions for
obvious reasons.
- You can now use sprintf
format codes directly within embedded expressions. To do this, start
the expression with a "%" code immediately after the angle brackets,
with no intervening spaces. For example, "x in octal is
<<%o x>>" will display x's contents in octal
(base 8). The compiler simply converts this sort of expression into a
sprintf() call with the given format code, so you can use the entire
range of format code syntax that sprintf() accepts.
- The new sprintf format
codes %r and %R generate Roman numerals for integer values, in lower-
and upper-case.
- The String methods find() and
findReplace() now accept a
RexPattern object as the search target. An ordinary string can also
still be used, of course. This essentially makes String.find() and
String.findReplace() replicate the functionality of
rexSearch() and
rexReplace(),
respectively; the main benefit is that the syntax for the String
methods is a little cleaner and more intuitive, since the subject
string is moved out of the parameter list.
This is especially convenient with the new R'...' syntax
for creating static RexPattern objects. For example,
str.find(R'%w+') finds the first word in a string.
- The String method
findReplace() and the
rexReplace() function each
now take an optional additional argument, limit, specifying the
maximum number of replacements to perform. If the limit
argument is omitted, the ReplaceOnce and ReplaceAll flags determine
the limit; if limit is included in the arguments, the
ReplaceOnce and ReplaceAll flags are ignored, and the limit
value takes precedence. limit can be nil to specify that all
occurrences are to be replaced, or an integer to set a limit count.
Zero means that no replacements are performed, in which case
the original subject string is returned unchanged.
- The new String function
findAll() searches a string
for all occurrences of a given substring or regular expression, and
returns a list of the matches.
- Two new functions implement reverse searches in strings:
rexSearchLast() and
String.findLast() These
functions search a string from right to left, allowing you to find the
last (rightmost) match for a substring or regular expression pattern.
- The rexGroup() function
can now be used to get information on the entire match, by passing 0
for the group number. rexGroup(0) returns the same format as the
other groups, but contains the text and location of the entire match
rather than of a parenthesized group within the match.
- TADS now has more complete support for Unicode case conversions.
Unicode defines two levels of case conversions; the older, simpler
level provides one-to-one character mappings between upper- and
lower-case letters, while the newer level allows for characters that
expand into multiple replacement characters when converting case. The
canonical example is the German sharp S character, ß, which
changes to "SS" when capitalized - there's no such thing as a capital
sharp S in standard German typography. For proper case conversion of
a string containing an ß, then, each ß character expands to
the two-character sequence "SS". There are similar examples in other
languages, some involving other ligatures and some involving accented
characters that don't have upper-case equivalents.
In past versions of TADS, only the one-to-one conversions were
supported. Characters such as ß that required more complex
handling were generally left unchanged in case conversions. TADS now
supports the full one-to-N mappings. This won't affect most text,
since most characters have simple single-character replacements when
converting in upper or lower case.
TADS also now supports Unicode "case folding", which is a separate
mapping for case-insensitive string comparisons. In the past, TADS
generally approached case-insensitive comparisons by converting each
character to be compared to a common case (upper or lower), according
to which character was the "reference" character in the comparison.
Now, TADS uses the Unicode case folding tables instead, and converts
each character to its "folded" form for comparison. The folded form
of each character is defined individually in the Unicode character
database tables, but in nearly all cases it's the same as converting
the character to upper case and then back to lower case.
The new case conversion and case-folding support affects several areas:
- Regular expressions: when the
<nocase> flag is specified, the matcher uses case folding to
match each contiguous string of literals. (In past versions,
characters were compared by converting to the pattern character's
case using the one-to-one conversions only.)
- The String methods
toUpper() and
toLower() use the new
case conversion tables. In the past, these used the older
one-to-one case conversion tables.
- The new String methods
toTitleCase() and
toFoldedCase()
use the new tables.
- The new String method
compareIgnoreCase()
uses the full case folding tables to perform a case-insensitive
comparison.
- The StringComparator class
now uses full case folding when the comparator isn't case-sensitive.
In the past, caseless comparisons were done by converting each
input character to match the case of the corresponding dictionary
character, using the one-to-one conversions only.
The new support only includes the unconditional case mappings. The
Unicode tables define a number of case mappings that are conditional,
some on the language in use and some on string context. TADS doesn't
currently support any of the conditional mappings.
- The new String method
toTitleCase() converts
each character in the string to "title case". This is the same as
upper case for most characters, but varies for some characters. For
example, a character representing a ligature (e.g., the 'ffi' ligature
character, U+FB03) is converted to the corresponding series of
separate letters with only the first letter capitalized (so U+FB03
becomes the three separate letters F, f, i).
- The new String method
toFoldedCase() converts
each character in the string to its case-folded equivalent, as defined
in the Unicode standard. The point of case folding is to erase case
differences between strings, to allow for case-insensitive
comparisons. For most strings, the case-folded version is the same as
the lower-case version, although not always; characters that don't
have exact equivalents in the opposite case (e.g., the German sharp S,
ß) are generally handled as though they were first mapped to upper
case and then back to lower, so the result will sometimes expand one
character to two or more characters in the folded version (e.g., ß
turns into ss, so that 'WEISS' will match 'weiß' in a case-insensitive
comparison).
- The new String method
compareTo() compares the
target string to another string, returning a negative number if the
target string sorts before the second string, 0 if they're identical,
or a positive number i the target string sorts after the other string.
C/C++ programmers will recognize this as the standard strcmp()
behavior. You can get the same information using comparison
operators, but compareTo() is more efficient for things like sorting
callbacks because it determines the relative order in one operation.
For example:
lst = lst.sort(SortAsc, {a, b: a < b ? -1 : a > b ? 1 : 0});
lst = lst.sort(SortAsc, {a, b: a.compareTo(b)});
The two callbacks have the same effect, but the second is a little
more efficient, since it always does just one string comparison per
callback invocation.
- The new String method
compareIgnoreCase()
compares the target string to another string, using the case-folded
version of each string. It returns the same type of result as
compareTo() - negative if the target string sorts before the other
string, zero if they're equal, and positive if the target sorts after
the other string, in all cases ignoring case differences. This is
equivalent to calling compareTo() using the results of calling
toFoldedCase() on each string, but compareIgnoreCase() is more
efficient since it never constructs the full case-folded versions of
the two strings (it does the case folding character by character as it
compares the strings).
- The new function concat()
returns a string with the concatenation of the argument values. This
is essentially the same as using the "+" operator to concatenate a
series of strings, but it's more efficient when combining three or
more values, since the "+" operator is applied successively in
pairs and so has to build and copy an intermediate result string at
each step.
- The new function abs()
returns the absolute value of an integer or BigNumber value.
- The new function sgn()
returns the SGN (sign) of an integer or BigNumber value. The SGN is 1
for a positive argument, 0 for zero, and -1 for a negative argument.
- The new t3make option -FC automatically creates the project's
output directories. If this option is specified, the compiler creates
the directories specified in the -Fy, -Fs, and -o options, if they
don't already exist. This makes it simpler to move a project to a new
directory or onto a new machine, since -FC makes it unnecessary to
create the output directories manually.
- HTTPRequest now recognizes GIF image files when sending a reply
body. When the caller lets HTTPRequest auto-detect the MIME type in
sendReply() and related methods, the class will now use "image/gif"
when it detects a GIF file. As with other binary file types, the
class recognizes GIF files by looking for the format's standard
signature near the start of the reply body data.
(bugdb.tads.org #0000139)
- The new HTTPRequest method
sendReplyAsync() lets
you send the reply to an HTTP request asynchronously, in a background
thread, so that the main program thread can continue to service other
requests while the reply is sent. This is useful when the reply
contains a large content body, such as a large image or audio file.
Most browsers use background threads on the client side to download
large media objects, so that the UI remains responsive to user input
while the objects are downloaded; with the TADS Web UI, this means
that the browser can generate new XML requests while image or audio
downloads are still in progress. The HTTPRequest sendReply() method
is synchronous, meaning that it doesn't return until the entire data
transfer has been completed. This means that the program can't
service any new XML requests that the browser sends during the
download until after the download has completed and sendReply()
returns, which makes the UI unresponsive for the duration of the
download. sendReplyAsync() addresses this by letting you initiate a
reply and then immediately return to servicing other requests, without
waiting for the reply data transfer to finish.
The Web UI library uses the new method to send replies to requests
for resource files, since these files are often images, sounds, and
other media objects that can be large enough to take noticeable time
to transfer across a network. Resource files are the mechanism that
most games use to handle their HTML media objects, so most game
authors shouldn't have to use sendReplyAsync() directly.
- The new class FileName provides
a portable way to manipulate file names and directory paths, and
methods to operate on the corresponding file system objects named.
Each operating system has its own file path syntax, so it's always
been difficult to use ordinary strings to construct and parse
filenames that involve directory paths. It's too easy to make
assumptions that tie the program to a single operating system; the
alternative has been to write a bunch of special-case code to handle
the syntax for each OS that you want to support. The FileName class
helps by providing methods for common filename construction and
parsing operations, which are implemented appropriately for each
operating system where TADS runs. TADS has always had many of these
portability functions internally for its own use, mainly for the
compiler and other tools; the FileName class makes them available to
TADS programs. These new features will probably be of little direct
interest to game authors, but could be useful to library and tool
developers.
In addition to building and parsing filenames, FileName
provides access to a much more complete set of file system functions
than was previously available. The new class lets you create and
delete directories, list directory contents, retrieve file system
metadata (file sizes, types, modification dates), and move and
rename files. As with the previously existing file access functions,
the new functions are subject to the file safety restrictions to
reduce the risk of malicious use and give the user control over
the scope of a program's file system access.
- The inputFile() function
now returns a FileName object to
represent the file chosen by the user, rather than a string. Existing
code shouldn't be affected unless it's unusually dependent upon the
result being a string, since the FileName object can be passed to any
of the functions that open files (including the File.openXxx methods,
saveGame(), etc) in place of a string, and is automatically
converted to a string containing the file name in most contexts
where a string is required.
A FileName returned by inputFile() has an internal attribute that
marks it as a user selection, which grants special permission to use
the file even if it wouldn't normally be accessible under the file
safety settings. A manual selection via an inputFile() dialog
overrides the safety settings because of the user's direct
involvement; the user directly expresses an intention to use the file
in the manner proposed by the dialog, which is an implicit grant of
permission.
- Several of the system-level functions that access files are now
subject to file safety restrictions:
saveGame(),
restoreGame(),
setLogFile(),
setScriptFile(), and
logConsoleCreate()
now enforce the appropriate read or write permissions according to the
file safety settings.
In the past, these functions didn't enforce file safety settings,
mostly for practical reasons: these functions are all used by the Adv3
library to operate on files that are normally selected by the user, so
it would have been confusing to deny access in cases where the user
happened to choose a file outside the sandbox. This was balanced
against the lower inherent risk with these functions, as compared to
the general-purpose File methods. The game program can't use these
functions for arbitrary read/write operations; the actual data content
they read/write is largely under the control of the system, so there's
probably no way to use them to do something like planting a virus or
stealing private data. However, since some of them create new files,
they could still be used for certain types of mischief, such as
overwriting system files or destroying user data.
The thing that's changed - that allows us to bring these functions
into the file safety mechanism - is the new ability of
inputFile() to mark its
filename result as coming from a manual user selection, and the
corresponding file safety enhancement that grants access permissions
to such manually selected files. This ensures that user file
selections for Save, Restore, etc. will still work properly, even when
they're outside the sandbox.
- The new Date built-in class provides
extensive functionality for parsing, formatting, and doing arithmetic
with calendar dates and times; it works with the new
TimeZone class to convert between
universal time and local time anywhere in the world, correctly
accounting for historical changes in time zone definitions and
daylight savings time. This should be especially useful to authors
writing games involving time travel, or set during the early morning
hours on certain Sundays in March or November.
- The new function makeList()
constructs a list consisting of a repeated value.
- The system library's main startup code (in lib/_main.t) now allows
the main() function to omit the argument list parameter. The library
now simply calls main() with as many arguments as it requires,
providing the standard "args" parameter if needed and otherwise
omitting it. For little stand-alone programs that don't use the Adv3
library, this simplifies the code a little by letting you omit the
argument list parameter if you don't need it.
- Lists and vectors can now be converted to strings, explicitly with
toString() as well as in
contexts where non-string values are automatically coerced to strings,
such as on the right-hand side of a "+" operator where the left-hand
side is a string value. The string representation of a list or vector
is the concatenation of its elements, each first converted to a string
itself if necessary, with commas separating elements. For example,
toString([1, 2, 3]) is the string '1,2,3'.
- In implicit string conversions, the value true is now
acceptable, and is converted to the string 'true'. In the past,
true worked this way with the
toString function, but not
in implicit string conversions (such as when a value is used on the
right-hand side of a "+" operator when the left-hand side is a
string: 'x=' + true caused a run-time error in the past,
but now returns 'x=true').
- The toString function
and implicit string conversions now accept properties, function
pointers, pointers to built-in functions, and enum values. If the
reflection services module (reflect.t) is included in the build, these
types will be passed to reflectionServices.valToSymbol() so that they
can be translated to symbols when possible. If reflect.t isn't
included in the build, or if valToSymbol() doesn't return a string
value, these types will be represented using a default format that
indicates the value's type and an internal numeric identifier for the
value, such as "property#23" or "enum#17". Any object type that
doesn't have a specialized string conversion defined by the built-in
object type is now handled the same way, so an object without special
formatting is represented either by its symbolic name (if it has one
and reflect.t is included in the build) or a generic "object#" format.
- The List method sublist()
now accepts negative length values, for better consistency with
similar methods (e.g., String.substr). A negative length essentially
states the length relative to the end of the list, in that it gives
the number of elements to omit from the end of the result list.
- The byte packing language now lets
you specify that a square-bracketed group is to be packed
from/unpacked into subgroups per iteration for a repeated item, rather
than using a single sublist for the whole group. The "!" modifier
makes this change. For example, fp.unpackBytes('[L S C]5!')
returns (when successful) a list containing five sublists, each of
which contains the three unpacked elements from one group iteration (a
long int, a short int, and an 8-bit int).
- The byte packer now allows up-to counts (e.g., 'a10*') for packing
(not just unpacking). When packing, for a group or a non-string item,
an up-to count packs up to the numeric limit, or up to the actual
number of arguments; for a string, an up-to count packs up to the
actual string length or up to the limit, truncating the string at the
limit if it's longer.
- The regular expression language accepts several new shorthand
character classes: %s for a space character, %S for a non-space
character, %d for a digit, %D for a non-digit, %v for a vertical
space, %V for a non vertical space. (These correspond to backslash
sequences - \s, \D, etc - that are fairly standard these days in other
languages with regex parsers, such as Javascript and php. There were
already <xxx> character classes that do the same things as these
new % codes, but these particular classes tend to be used often enough
that it's nice to have shorthand versions.)
- The new __objref() operator
lets you test for the existence of a particular object symbol,
optionally generating a warning or error message if the symbol isn't
defined or is defined as something other than an object. This is
similar to the defined()
operator, but is specialized for object references.
- The randomize()
built-in function can do several new tricks. First, it allows you to
select from three different RNG algorithms to use in
rand(): the default ISAAC
algorithm (the original TADS 3 RNG), a Linear Congruential Generator
(or LCG, the long-time de facto standard for computer RNGs), and the
Mersenne Twister (a newer algorithm that's become popular in other
modern interpreted languages). ISAAC is still a good general-purpose
choice, but the new options are there in case you have some reason to
prefer the properties of one of the other generators. Second, you can
now set a fixed seed value. This allows you to override the automatic
startup randomization that was added in TADS 3.1, and further lets you
start a new fixed sequence at any time. Third, you can now save and
restore the state of the RNG, so that you can make the RNG repeat the
same sequence of results it produced from the time of the saved state.
- When the interpreter is launched, any command-line arguments that
follow the .t3 file name are passed to the program as string arguments
to the main() function. In the past, these arguments were passed as-is,
without any character set translation, which caused unpredictable
results if they contained any non-ASCII characters. The interpreter
now translates these strings from the local character set to Unicode,
ensuring that any accented letters or other non-ASCII characters are
interpreted properly.
(Related to bugdb.tads.org #0000109)
- The new interpreter command-line option
-d specifies the default
directory for file input/output. This is the directory that the File
object uses to open files whose names are specified with relative
paths. If -d isn't specified, the default is the folder containing
the .t3 file. (In past versions, there wasn't any way to set the
working directory, which was always the .t3 file's folder. This means
the behavior in the absence of a -d option is the same as in the past.)
The new option -sd lets
you separately specify the "sandbox" directory for the file safety
feature. In the absence of an -sd setting, the sandbox directory is
the same as -d setting, or simply the .t3 file's containing folder if
there's no -d option.
(The -d option was added in part to address
bugdb.tads.org #0000120)
- A bug in the dynamic compiler caused 'if' statements in
dynamically compiled code (e.g., using new DynamicFunc()) to
use the 'then' branch code for both true and false outcomes. This has
been fixed.
(bugdb.tads.org #0000117)
- A bug in the dynamic compiler sometimes caused a run-time error when
accessing a local variable when the enclosing function also defined an
anonymous function. This is now fixed.
(bugdb.tads.org #0000118)
- A bug in the BigNumber class sporadically gave incorrect results
for additions. (Specifically, results were sporadically off by one in
the last digit.) This has been corrected.
- toInteger() caused a crash when used with values below 0.1. This
has been corrected.
(bugdb.tads.org #0000127)
- The compiler reported an unhelpful internal error message
("unsplicing invalid line") if a file ended in an unterminated string
literal. The message is now the more explanatory "unterminated string
literal".
- Consider this macro definition and usage:
#define ERROR(msg) tadsSay(#@msg)
ERROR({)
In the past, the compiler treated the ERROR({) line as have a
missing close paren. This is because the compiler previously tried to
balance open and close curly braces and square brackets within macro
arguments, and treated any parentheses found nested within
braces or brackets as being part of the macro argument, rather than
terminating the macro argument. This no longer occurs; parentheses
are now treated independently of braces and brackets within macro
arguments, so a close paren within a macro argument that doesn't match
an earlier open paren in the same argument now ends the argument. The
example above thus now compiles without error, and expands to
tadsSay('{'). The balancing act for braces and brackets does
still apply to commas, though: a comma within a pair of braces or
brackets is still considered part of the argument. This is important
for macro arguments that contain things like statement blocks or
anonymous function definitions.
- The File unpackBytes() method incorrectly threw an error if an
"up-to" format was used (e.g., 'H20*') and the file had zero bytes
left to read. This has been corrected; unpackBytes() now succeeds
and returns a zero-length result for the format item.
- String.split() incorrectly returned a one-element list (consisting
of an empty string) when used on an empty string. This now correctly
returns an empty list.
- A bug in String.split() caused sporadic crashes when splitting
at a delimiter and the result list had more than 10 elements. The
bug was related to garbage collection timing, so it was unpredictable.
This is now fixed.
(bugdb.tads.org #0000156)
- A bug introduced in 3.1.0 caused exceptions to be caught in the
wrong handlers under certain rare conditions. The problem happened
with exceptions thrown from within method calls when the caller had a
new "try" block starting immediately after the expression containing
the method call, with no other VM instructions between the call and
the start of the "try" block (this means, for example, that the return
value from the method call was discarded and no other computations
were performed as part of the same expression after the method call).
When all of these conditions were met, the exception was incorrectly
handled by the "catch" part of the "try" block that started just after
the call; this was incorrect because the "try" block didn't contain
the call and so its "catch" block shouldn't have been involved in
handling an exception that occurred within the call. The correct
behavior has been restored.
- A bug in the regular expression parser randomly caused bad
behavior, including crashes, if the last character of an expression
string was outside of the ASCII range (Unicode code points 0 to 127).
The bug was only triggered when certain byte values happened to follow
the string in memory, so it only showed up rarely even for expression
strings matching the description. (It was also possible to trigger
the same bug with a non-ASCII character within five characters of the
last position if the string ended with an incomplete <Xxx>
character class name, lacking the final ">", but this might have
been too improbable to have ever been observed in the wild.) This has
been fixed.
- A compiler bug introduced in 3.1.0 made it impossible to assign
to an indexed "self" element in a modifier method for an intrinsic
class such as List or Vector. This has been corrected.
(bugdb.tads.org #0000128)
- In the past, the compiler attempted to pre-evaluate any indexing
expression it encountered ("a[b]") where the index value and the value
being indexed were both constants. In such cases, it only recognized
list indexing, which was the only valid constant indexing expression
before operator overloading made it possible to define indexing on
custom object classes. This made the compiler generate error messages
for (potentially) valid code involving constant index values applied
to object names. This has been corrected; the compiler now treats
such expressions as valid, and defers their evaluation until run-time,
so that any operator overloading can be properly applied.
(bugdb.tads.org #0000142)
- The standard main window layout code in the Web UI library now
loads its Flash helper object (TADS.SWF) dynamically rather than
statically, and only does so when it detects that Flash support is
already present in the browser. In the past, the TADS.SWF object was
embedded statically on the page, which means that the browser saw the
object declaration whether or not Flash support was installed. Some
browsers attempt to be helpful in this situation by popping up a
dialog or prompt pointing out that the page depends on Flash and
offering to download and install a Flash plug-in. For users who
intentionally omit Flash from their browser configurations, though,
this "helpful" prompt is an annoyance, since it comes up every time a
page with a Flash embedding is loaded or reloaded and the answer is
always No. The library's new approach avoids the superfluous prompt
by creating the TADS.SWF object embedding only after determining that
Flash is already installed.
(There's a trade-off, of course, in that the browsers that display
the prompt do so for good reason. Without it, users who
unintentionally omitted Flash from their configurations will
never know the page makes use of it. This seems like a small price to
pay, though, in that (a) most modern browsers include Flash support
out of the box anyway (excluding those on iOS, where Flash simply
isn't available), so practically everyone who hasn't gone out of their
way to remove Flash already has it (or is running on iOS, where
there's no need for a prompt since there's no way to install Flash at
all); and (b) even if there's anyone left over after considering (a)
who could benefit from the prompt, the Web UI only uses Flash as a
very minor enhancement (specifically, to obtain a list of installed
fonts for the Preferences dialog), so these presumably rare users
won't suffer any really significant loss of functionality from the
lack of Flash that we're preventing the browser from alerting them
to.)
(bugdb.tads.org #0000145)
- The compiler didn't generate the full string list properly when
the -Os option was used; only strings for the first source module in
the build were included, rather than strings for all modules being
compiled. (What's more, when the build included many modules, the
underlying bug sometimes caused internal memory corruptions within the
compiler that generated spurious error messages or other unpredictable
results.) This has been corrected.
(bugdb.tads.org #0000150)
This update has three main themes: dynamic coding, greater
convenience, and network support. On the dynamic code side, it's now
possible to compile new code at run-time, and new reflection features
provide more thorough run-time access to the program's own internal
structure. These dynamic features will be especially interesting to
library and extension authors, as they open new opportunities for
creating miniature languages within the language. The convenience
enhancements involve numerous, mostly small changes that make common
tasks easier and frequent coding patterns more compact. As for
the new network features, the original motivation and initial
application is to run TADS games in a client/server configuration,
where the player uses an ordinary browser to access the game, with no
need to install TADS or even download a game file. This is just one
application of the new technology, though; the network features are
actually much more generalized and extensive than this first use might
suggest. In effect, TADS is now capable of acting as a fairly
complete (if small scale) Web server programming environment. This
opens many new possibilities for networked user interfaces and
multi-player games. It also has an interesting bonus benefit, which
is that it lets you tap into the full power of the browser to create
your TADS user interfaces. Modern browsers provide a vastly more
powerful user interface platform than HTML TADS (or any other existing
IF runtime), and all of that power is now directly available to TADS
games.
Note that before this version was released, it was sometimes
referred to as 3.0.19 (e.g., in the TADS bug database and the T3
blog). If you're looking for 3.0.19 based on something you read
elsewhere, this is it. We finally bumped the name up to 3.1 because
of the substantial new functionality it contains.
- Compatibility Alerts: The following changes might affect
existing code that was originally created with an earlier version of
TADS 3.
- operator is now a reserved word, due to the new operator
overloading feature. This means that the word operator can't
used as a symbol name, such as for the name of an object, function, or
local variable.
- method is now a reserved word, due to the new "floating"
method definition syntax.
- invokee is now a reserved word.
- defined is now a reserved word.
- << >> sequences are now meaningful in single-quoted
strings, since these strings can now contain expression embeddings.
This means that formerly inert << sequences in single-quoted
strings will now be interpreted as embeddings. This should
be a compatibility issue, but as it happens a fortuitous
compiler bug in older versions made it virtually impossible to use
<< in single-quoted strings anyway, so this shouldn't affect
any existing code.
- Complex expressions involving compound assignment operators
with side effects in their lvalues are now compiled with different
(better)
behavior. This probably won't affect any existing code,
because (a) it only applies to fairly unusual expressions, and (b) anyone
who encountered the old behavior probably would have thought it was
a bug and changed their code to avoid it.
The new behavior is much more predictable and much more what you'd
expect, but it's possible that there's existing code that accidentally
relies on the old behavior.
Details below.
- The regular expression character class <space> now
explicitly matches only horizontal whitespace characters,
not vertical separators (\n, \r, \b, and a few others).
This is actually more likely to fix problems than create new ones,
since the old behavior was inconsistent with what most people
expect from other regular expression implementations.
- The rand() function now evaluates only one of its arguments
when it's called with multiple arguments. In the past, rand()
evaluated all of its argument values first, then randomly selected
one of the values as the result. Now, rand() makes the random selection
first, then evaluates only the selected item, and returns the result.
This means that side effects are only triggered for the selected
argument, not for all arguments as in the past. This could conceivably
affect existing code that relied on all of the arguments' side effects
being executed, although such code tends to be tricky enough that
most people avoid it, so the practical impact should be minimal to
non-existent. Also, importantly, this is a compiler
change only, so it only affects newly compiled code; existing .t3
files already in distribution won't be affected.
- It's now possible for a TADS game to be a Web server. This is
accomplished with the new intrinsic classes HTTPServer, which handles the low-level
networking required to receive and parse HTTP requests from Web
clients, and HTTPRequest, which
represents an incoming HTTP request from a client; and the new
intrinsic function set tads-net,
which contains additional support functions for networking operations.
The new HTTP server support classes are designed to automatically
handle all of the low-level necessities of a network service, while
still giving the game program full control over how requests are
actually processed. This makes it possible to create a wide variety
of effects with the network server. Initially, it will be used to
present the traditional single-player user interface in a networked
configuration, where the game runs on a server and the client needs
only an ordinary Web browser. This eliminates the need for clients to
install the TADS software, while also greatly expanding the UI
capabilities of TADS games by letting you use the full power of HTML
DOM and Javascript. Over time, it opens up lots of other
possibilities, such as collaborative gaming and multi-player games.
- In addition to the new ability to act as a Web server, TADS games
can now also make HTTP requests as clients, via the new function
sendNetRequest().
This lets a game send information to and receive information from
remote Internet servers during play.
- The interpreter has a new command-line option, -ns##, for
setting the "network safety level". This is analogous to the file
safety level, and controls the program's ability to access the network
functions. The "##" part is two digits, the first giving the safety
level for client functions, and the second giving the level for
server functions. The client level controls outgoing
connections from the program to external network services; the server
level controls the program's ability to accept connections from
external client programs (such as Web browsers). There are three
possible values for each component: 0 means "no safety", which allows
all network access without restrictions; 1 sets local access only,
which only allows connections to or from other applications running on
the same computer; and 2 is "maximum safety", meaning no network
access is allowed at all. For example, -ns02 gives the
program full access as a client to any external network service
anywhere on the network, but at the same time prohibits the program
from setting up any network services of its own or accepting any
connections from other processes or computers. The default safety
level is -ns11, which allows the program to connect to and
accept connections from other programs on the same machine only.
- The compiler now accepts "triple-quoted" strings. This
isn't a third type of string beyond single- and double-quoted;
it's just a new way of writing those two existing string types. A few
other C-like languages have adopted this as a nicer syntax for writing
strings that contain quote marks as part of their literal text. This
is a particularly common need in TADS, since so many strings are
part of the story text.
A triple-quoted string starts and ends with three copies of
the quote mark. What you gain is that you can then freely use the
quote mark character within the string without worrying about
"escaping" it with a backslash. The traditional C backslash syntax
for embedded quotes is awkward to type and hard to read. Triple
quotes make strings more readable by letting you use quotes directly
in a string without any escape characters.
desc = """The sign reads "Beware of Backslash!""""
There are a couple of subtleties you should be aware of, so
take a look at the System
Manual for details.
- Single-quoted strings can now use << >> expression
embeddings (including all of the new embedding features, such as
<<if>>). The single-quoted version produces a
result that's equivalent to concatenating the embedded expressions
to the surrounding string fragments. For example, 'one
<<two>> three <<four>> five' is
equivalent to 'one ' + two + ' three ' + four + ' five'. See
String Literals in the System
Manual for more details.
- A new string embedding
template syntax lets you create custom keywords and phrases
for use inside embedded
expressions. The compiler translates each custom template invocation
into a function call, so this is merely a syntactic convenience, but
it can make embeddings in strings more readable by avoiding
expression-like syntax. For example, you could create
a template that lets you write a string like "You currently have
<<score in words>>" points", rather than the
more techy looking "You currently have <<spellNum(score)>>
points".
- There's a powerful new string embedding syntax for writing
passages that vary according to run-time conditions. Traditionally,
conditions were embedded in strings using the ?: operator, as in:
desc = "The door is <<isOpen ? 'open' : 'closed'>>."
That's fine for substituting a word or two based on a simple
true/false condition, but for anything more complex it can be
hard to read. The new syntax improves the situation
by making the varying text part
of the string, rather than part of the expression. The conditions
become almost like markups interposed within the text:
desc = "The door is <<if isOpen>>open<<else>>closed<<end>>. "
The improved readability of the new syntax is more obvious with
longer passages:
desc = "A massive iron door, bristling with rivets and bolts
across its surface. <<if isOpen>>It's open a crack, leaving
enough room for a mouse or perhaps a small hare to slip
through, but probably not quite enough for a burly
adventurer. "
Another benefit of the new syntax is that, because the varying text
is part of the string rather than part of an embedding, you can freely
use additional embeddings within the then/else parts. (That's not
possible with ?: embeddings, since strings inside embedded
expressions can't themselves contain any embeddings.) You can even
nest <<if>> structures for more complex conditions.
As with other embedding syntax, <<if>> can be used
in single-quoted strings as well.
Full details are in the
System Manual.
- Another new embedding syntax,
<<one of>>, makes it
easier to create lists of alternative messages to be chosen randomly
or in a cycle (or a combination of the two).
<<one of>> variations are defined
for simple random selection, shuffling, cycling, "stop" lists,
and for various combinations of these, such as going through a
list once in sequence and then shuffling it.
The traditional way to create random or sequential messages
was via the Adv3 EventList class, which of course
still works. <<one of>> is much
more concise and readable for simple message variations, though, since
it doesn't require a separate object declaration for the event list.
The new syntax also has the advantage of being nestable inside
other <<one of>> structures and <<if>>
structures.
- One more new special embedding:
<<first time>>
shows a message the first time the enclosing string is displayed,
and omits it after that. This is really just a special case of
<<one of>> (and, in fact, the compiler actually
rewrites it that way), but it's such a common motif in IF authoring
that the custom syntax seems justified.
- Speaking of string embedding, the compiler now respects
parentheses (and square brackets and curly braces) within an embedded
expression when determining where it ends. The compiler previously
assumed an embedding ended at the very first >>, regardless
of context, but this
was problematic if you wanted to use the >> bit-shift operator within an
expression. Now, the compiler counts parentheses, brackets, and
braces, and treats >> as a bit-shift operator if it
appears within a bracketed group. This means you can use the >>
operator in an embedded expression simply by parenthesizing the expression.
- The new TadsObject methods getMethod() and setMethod() give you
more dynamic control over objects and classes by letting you add new
methods to an existing object. This makes it possible to perform
almost any sort of transformation on an object. See the
TadsObject documentation for details.
- The new method keyword lets you define a "floating"
method. This is a routine that's not associated with an object, but
which nonetheless has access to the method context variables
(targetprop, targetobj, definingobj, and self), as well as
inherited. This is meant specifically for use with
TadsObject.setMethod(): it lets you create a method that's not
intially part of any object, and then plug it in as an actual method
of selected objects at run-time. Syntactically, a floating method
definition looks just like an ordinary function definition, except
that whole thing is preceded by the keyword method instead of
the optional function keyword. See the System Manual for more details.
The method keyword can also be used to create anonymous
methods. These look and act almost the same as anonymous functions.
The difference is that an anonymous method doesn't share its
method context variables (self, definingobj, targetobj, targetprop)
with its enclosing lexical scope. Instead, an anonymous method takes
on the "live" values for those variables each time it's called.
As with named floating methods, anonymous methods are designed for
attaching to objects via setMethod().
Details are in the
System Manual.
- The new intrinsic class DynamicFunc makes it possible for a
running program to extend itself by creating new code on the fly. New
code is created by compiling a string that contains source code text,
just as you'd use in the main program source code. This type of
facility is common in modern interpreted languages, especially
scripting languages (e.g., Javascript, PHP), where people have found
all sorts of uses for it. It's especially interesting in TADS because
it opens the door to new string and message processing capabilities.
DynamicFunc values behave very much like ordinary function pointers.
You can call a DynamicFunc as though it were a function pointer, and
you can use a DynamicFunc in TadsObject.setMethod() to create a new
method for an object.
- GrammarProd objects can now be
dynamically created, and the grammar rules for an existing GrammarProd
can be modified at run-time. The new methods deleteAlt() and
clearAlts() remove existing alternatives from a GrammarProd, and the
new method addAlt() adds new alternatives. New rules are specified
using the same syntax as the regular "grammar" statement.
- It's now possible to retrieve information on the preprocessor
macros defined by the compiled program. This is handled through the
existing function t3GetGlobalSymbols(),
which now takes an optional argument value that selects which type of
symbol information to retrieve. The argument is one of the following
constant values: T3GlobalSymbols, to retrieve the global symbol table;
or T3PreprocMacros, to retrieve the macro symbol table. If you don't
include an argument, the global symbol table is retrieved as in the
past, so existing code will work unchanged. As with the symbol table,
the macro table is available only during preinit, or during normal
run-time if the program is compiled with debugging information.
- It's now possible to get a pointer to an intrinsic (built-in)
function. Use the "&" operator, just like with a property name.
For example, &tadsSay yields a pointer to the tadsSay()
intrinsic. These pointers operate just like ordinary function
pointers: you can use them to make calls, and you can even use them
in TadsObject.setMethod().
- For syntactic consistency, the "&" operator can now be used to
get a pointer to an ordinary function.
(In the past this wasn't allowed, but only for nit-picky
reasons. It was felt that, because there wasn't a need
for an explicit "pointer-to" operator for functions, "&" shouldn't
even be allowed for functions, making its function clearer by virtue
of being single-purpose. This was seen as worthwhile because "pointer
to property" seems to be a particularly subtle idea for new users, as
it's rather abstract and not common in other languages. However, now
that we have pointers to built-in functions, we do need an
explicit "pointer to" operator for those, and "&" is certainly the
right choice: it's the right parallel to C++, on which our syntax was
originally modeled, and it's the right analogy to the existing TADS
usage of "&" with properties. With the addition of the
built-in function pointer type, given that "&property" means
"pointer to property", and "&built-in" means
"pointer to built-in," it would be confusing if
"&function" didn't mean "pointer to function" as well. The
only thing left out, really is "&object", but that would be
going too far. The analogy with C++ would make "&object"
confusing for experienced C++ users because object and
&object have quite different meanings in that language.)
- The new "defined" operator tests at compile time to determine
whether a symbol is defined or not. The syntax is
defined(symbol); this yields the constant
value true if symbol is defined at the global
level within the program (as a function, object, or property name),
nil if not. You can use defined() in any
expression context, such as in the condition of an if statement.
This operator is particularly useful in libraries, since it makes it
possible to write code that conditionally references objects only
when they're actually part of the program. See the
System Manual for details.
Note that "defined" still has its separate meaning within
#if preprocessor expressions. There, the operator determines if
the symbol is defined as a preprocessor (#define) symbol.
When used outside of #if expressions, "defined" has the
new meaning of testing the symbol's presence in the compiler
global symbols rather than the preprocessor macro symbols.
- The "new" keyword is no longer required (although it's still
allowed) in the definition of a long-form anonymous function. For
example, it's now valid to write code like lst.mapAll(function(x)
{ return x+1; }). The presence or absence of the "new" makes no
difference to the meaning.
The original rationale for requiring "new" was that anonymous
functions are actually objects that are newly created on each
evaluation, so it was felt that this should be made explicit in the
syntax. The rationale for now relaxing the "new" requirement is that
"closures" (as they're more technically known) have become common in
other mainstream languages, and no one else seems to think the syntax
should mimic object construction. (For that matter, TADS's own
short-form syntax never had the "new".) There's also a good
pragmatic reason: with the new Web UI, TADS programmers might find
themselves switching back and forth between TADS and Javascript,
since the Web UI incorporates a substantial Javascript front end.
Javascript's anonymous function syntax is almost exactly like TADS's
long-form syntax, except that Javascript doesn't have the "new". Such
close-but-not-perfect similarities are particularly vexing
when switching between languages, so it seemed best to eliminate the
unnecessary difference.
- Short-form anonymous functions can now define their own local
variables. Use the local keyword as usual, at the beginning
of the function's expression. The syntax is analogous to defining
locals within a for statement's initializer clause. See Anonymous Functions in
the System Manual for details and examples.
This is convenient because it lets you use the short-form
syntax even if the function requires a local variable or two. In the
past, you had to switch to the long-form syntax to define a local,
even if the rest of the function simply returned an expression value.
- The syntax of the for loop has been extended with
three new features:
- for..in::
for (x in collection) is now synonymous with the
existing foreach syntax. This makes the syntax more
uniform, which makes the language easier to use by eliminating
the need to remember to use distinct keywords for the two kinds
of loops.
- Hybrid for and for..in loops: The "in" syntax newly
allowed in for loops can also be combined with the
conventional three-part init/condition/update syntax. This
allows you to add other looping variables to an "in" iteration.
One of the drawbacks of the foreach syntax is that any
other looping variables have to be initialized before the foreach
and updated and/or tested within the loop body. for loops,
in contrast, can manage multiple variables as part of explicit
loop structure within the for statement itself, which
is more compact and often clearer. For example, to add a counter
to an "in" loop, and stop after 10 iterations even if more
elements are in the list, you could write
for (local i = 1, local ele in list ; i < 10 ; ++i) { }.
- for..in range loops: By far the most common type of
for loop is a simple iteration over a range of integers,
such as over the index range for a list. There's now a custom
syntax for this kind of loop: for (var in from
.. to). This syntax steps the variable var
from the from expression's value to the to
expression's value, inclusive of the limits. For example, to
step through the index values for a list, you can write
for (local i in 1..list.length()). This new syntax
makes simple integer loops easier to write and clearer. It
also makes them more reliable, since a common coding error
is using < instead of <= (or vice
versa) for a loop condition. The range syntax is more intuitive
because it explicitly states the endpoints rather than expressing
the loop condition as a value comparison. The new syntax also
allows specifying the increment: for (i in 0..20 step 2)
steps through even numbers, and for (i in 10..1 step -1)
steps down from 10 to 1. As with collection loops, you can mix
this syntax with the full three-part for syntax:
for (local i in 1..20, sum = 0 ; lst[i] != nil ; sum += lst[i]).
The new syntax is described in more detail in the System Manual.
- A new language feature lets you pass argument values to functions
and methods using explicit argument names. This has several important
benefits. First, callees can retrieve argument values from callers
without regard to the order of the values in the calling expression.
Second, callees are free to ignore named parameters entirely, which
allows a caller to pass extra, optional context information without
burdening every callee's method definition syntax with unneeded extra
parameter declarations. Third, nested callees can retrieve named
arguments passed indirectly from a caller several levels removed, so
intermediate functions that don't care about context information can
ignore it without preventing nested callees from accessing it. This
doesn't replace the traditional positional argument system, but rather
extends it. Some other languages use named arguments primarily for
code clarity reasons, but the TADS version is designed to address a
particular coding problem that comes up time and again in IF library
design. The details take a little work to explain; the new System
Manual chapter on Named Arguments
has the full story.
- New syntax makes it easier to define functions and methods that
take optional arguments. It's always been possible to do something
similar using the "..." syntax, but "..." is really intended for cases
where the number of additional arguments is unpredictable and has no
fixed upper limit. The new syntax, in contrast, is for cases where
you have a specific number of arguments, but where you wish to make
one or more of the arguments optional, so that callers can omit them
for the most common case where a default value would apply. This
makes the calling syntax more convenient for the common case, while
still letting callers specify the full details when needed. "..."
isn't ideal for this use because it doesn't provide error checking for
too many parameters, and because it requires fairly tedious
extra syntax in the callee to check for the presence of the extra
arguments and retrieve their values. The new syntax is described in
detail in the new System Manual chapter on optional parameters.
- The new invokee
pseudo-variable retrieves a pointer to the function currently executing.
This is most useful for anonymous functions, since it provides a way
for an anonymous function to invoke itself recursively.
- The t3GetStackTrace() function can now retrieve information on the
local variables in effect at each stack level. The locals are
available via the locals_ property of the T3StackInfo object that
represents a stack level.
By default, locals are omitted, since they take additional time to
retrieve. To include local variable information, supply the new
flags argument with the value T3GetStackLocals. (This is a bit
value; it's possible that future bit values will be added, in which
case this will be combinable with other bit flags via the '|'
operator.)
The locals in each stack frame are provided as a LookupTable. Each
element of the table is keyed by a string giving the name of the
variable, and each corresponding table value is the current value of
the local. The table is merely a copy of the locals in the stack,
a value in the table won't have any effect on the local variables
themselves.
- The t3GetStackTrace() function now includes information on the
named arguments passed to each stack frame. This is available via
the namedArgs_ property of the T3StackInfo object that represents
each stack level. This property is nil for a stack level that
doesn't have named arguments.
Each named argument list is provided as a LookupTable. Each element
of the table is keyed by a string giving the name of the argument,
and each corresponding table value is the argument's value.
- The t3GetStackTrace() function now includes full information on
"native" calls in the stack - that is, intrinsic functions and
intrinsic class methods. In the past, native callers were recognizable
by their complete lack of information, in that they had a nil value
for both the calling function and object/property values.
This change comes into play when a native routine calls bytecode
via a function pointer you passed into the native routine, such as
when List.forEach() invokes its callback. When the native caller is a
built-in function, the function pointer element of the stack level
object will contain a built-in function pointer value; when it's an
intrinsic class method, it will contain suitable object and property
values describing the native method. For obvious reasons, there's no
source file information for a native routine in the stack trace.
- The new intrinsic class StackFrameDesc provides read and write
access to the local variables in a stack frame. You obtain a
StackFrameDesc object via t3GetStackTrace() by including the
T3GetStackDesc flag. The object provides methods to get and set the
values of local variables, and to retrieve the method context
variables (self, targetobj, targetprop, definingobj). See the System
Manual chapter for more information.
- A new language and VM feature make it possible to "overload"
operators. This means that you can define a method on an object or
class that's invoked using one of the algebraic operator symbols, such
as "+" or "*", rather than via the normal method call syntax.
Operator overloading has many potential uses, but the two main uses
are (1) to create an especially compact syntax for common operations
on specialized objects, and (2) to create a custom object that mimics
the low-level interface of one of the built-in types or classes. The
details are described more fully in a new chapter in the system manual.
Operator overloading has three significant limitations. First, you
can't override operators pre-defined by intrinsic classes. For
example, you can't redefine the indexing operator "[]" for a List, or
the concatenation operator "+" for a String. You can, however,
add operators to intrinsic classes: it's legal to use "modify"
to define an operator on an intrinsic class as long as that operator
isn't already defined by the intrinsic class itself. Second, there's
no way to overload operators at all for a primitive type like integer,
whether or not the type defines it: you can't change the meaning of
"+" when applied to integers, or add a meaning for "true + nil".
Third, not all operators are overloadable; notably, none of the
comparison operators (==, !=, <, <=, >, >=) are
overloadable. All of these limitations are due to performance
considerations; with these restrictions in place, this new feature has
no performance cost to programs that don't use it.
- Using operator overloading, it's now possible to create
"list-like" objects. An object is considered list-like if it has an
overload for operator[] and it provides a
length() method that takes zero arguments. If the object
provides this interface, the length() method must
return an integer value.
The following built-in functions that formerly only accepted
regular lists and/or vectors will now also accept list-like objects:
inputDialog(), makeString(), rand(), Dictionary.addWord(),
Dictionary.removeWord(), List.intersect(), List.appendUnique(), new
LookupTable(), GrammarProd.parseTokens(), new StringComparator(),
TadsObject.setSuperclassList(), new Vector(), Vector.appendAll(),
Vector.appendUnique(), Vector.copyFrom(). Similarly, the "..."
varying argument expansion operator can be applied to a list-like
object as though it were a true list; and List and Vector comparisons
with "==" and "!=" will compare element-by-element against a list-like
value on the right-hand side; and the List and Vector "+" and "-"
operators will treat right-hand operands as lists if they're list-like
objects.
- New syntax lets you create a LookupTable directly from a set of
Key/Value pairs. The syntax is similar to a list expression: write
the list of Key->Value pairs in square brackets, with an arrow
symbol '->' between each key and value. For example, x =
['one'->1, 'two'->2, 'three'->3] creates a LookupTable
with keys 'one', 'two', and 'three', corresponding to values 1, 2, and
3, so x['one'] yields 1, and so on. This syntax is equivalent to
calling new LookupTable() and then filling in the keyed
values, so this kind of expression creates a new LookupTable object
each time it's evaluated.
- The LookupTable class now lets you specify the value to be returned
when you index a table with a key that doesn't exist in the table.
This is called the "default value" for the table; in the past, this
was always nil. The new setDefaultValue() method lets you
set a different default. You can retrieve the default value
previously set for a table with the new getDefaultValue()
method. The initial default value for a new table is nil, so the
behavior is the same as in prior versions if you don't use the new
method. You can also specify the default value when creating a table
with the new shorthand syntax, by writing
"*->value" as the last element of the list. (The
asterisk is meant to suggest a "wildcard" matching any key not
specifically entered in the table.)
- The Dictionary class has new built-in infrastructure support for
spelling correctors. The new function correctSpelling() retrieves a
list of words in the dictionary that are within a specified "edit
distance" of a given string. This new function isn't a full-fledged
spelling corrector, but provides a very fast version of a key
infrastructure element for building one. Refer to the Dictionary class
documentation for details.
- The new intrinsic class StringBuffer is a mutable version of the
character string object. Unlike regular String objects, a
StringBuffer's text contents can be modified in place, without
creating new String objects. StringBuffer is designed especially for
situations where it takes many incremental steps to build a string.
It's much more efficient to use StringBuffer for these cases than it
is to use regular string concatenation, because the latter makes a new
copy of each concatenated combination. StringBuffer provides methods
to edit the contents of the buffer: you can insert, append, delete,
and replace parts of the text. When you've finished the build steps
for a string buffer, you can convert it to a regular string using
toString(). You can also extract a substring of the buffer using the
substr() method, just like for a regular string.
- The rexReplace()
function has several new features that make it more powerful
and more convenient to use:
- You can now specify a function to determine the replacement
text to use for each match. If you supply a function (regular or
anonymous) in place of the regular replacement text argument,
rexReplace() calls the function for each match, passing as arguments
the matched text, the index of the match within the overall subject
string, and the original subject string. Your callback function
returns a string value giving the text to use as the replacement.
This allows for much more complex string manipulations, since you can
test conditions that are beyond what can be encoded in a regular
expression, and you can apply arbitrary transformations to the match
string to produce the replacement text.
- You can now specify a list of patterns to match, instead of
just a single pattern, and each pattern can have a separate
replacement value (which can be a string or a callback function, per
the new callback feature above). By default, when you supply a list
of patterns, rexReplace() searches for all of the patterns at once.
This is similar to combining the patterns with '|' to make a single
pattern, but it's more powerful because it lets you specify a
different replacement string for each pattern. If you include
ReplaceSerial in the flags, rexReplace() instead searches "serially"
for the patterns: it replaces each occurrence of the first pattern
throughout the entire string, and then re-scans the updated string to
replace all occurrences of the second pattern, and so on. The effect
is the same as calling rexReplace() sequentially with each individual
pattern, but it's more compact to write it this way.
- The "flags" argument is now optional. If it's omitted,
the default is ReplaceAll. This makes the function more convenient
to use for the most common case. Note that if you need to specify
the "index" argument, you must also include a "flags" value, since
the arguments are positional.
- The new flag ReplaceIgnoreCase makes the search insensitive to
case, by default. A <case> or <case> directive in the
regular expression overrides the ReplaceIgnoreCase setting. The main
reason this flag is provided at all (given that it's largely redundant
with the <case> or <case> directives) is for uniformity
with String.findReplace(), but it can occasionally be useful in that
lets you reuse an expression for both case-sensitive and
case-insensitive searches.
- The new flag ReplaceFollowCase makes the replacement follow the
capitalization pattern of the matched text. Lower-case letters in the
replacement text are capitalized (or not) as follows: if all of the
alphabetic characters in the matched text are capitals, the entire
replacement text is capitalized; if all of the letters in the match
are lower-case, the replacement text is left in lower-case; if the
match has both capitals and lower-case letters, the first alphabetic
character of the replacement text is capitalized. This only applies
to lower-case letters in a replacement string, and only to literal
text: "%" group substitutions aren't affected, since they already copy
text directly from the match anyway. The flag also has no effect when
the replacement is a callback function rather than a string; we have
to assume the function returns exactly what it wants, since it can
perform similar case manipulations of its own.
- The String method findReplace() has a few new
features:
- You can now specify a list of search strings, and a list of
corresponding replacements. This makes it possible to perform a whole
series of replacements with a single call.
- You can now pass a function in place of a string as the
replacement argument. For each match, findReplace() invokes the
function, passing in the matched text and other information;
the function returns a string giving the replacement text. This
makes it possible to vary the replacement according to the actual
matched text, its position in the subject string, or other factors.
- The "flags" argument is now optional. If it's omitted, the
default is ReplaceAll. This makes the most common usage more
convenient. Note that if you need to specify an "index" value
(for the starting position), you'll need to include a "flags"
value, since the arguments are positional.
- The new flag ReplaceIgnoreCase makes the search insensitive
to capitalization.
- The new flag ReplaceFollowCase makes the replacement text
follow the case of the matched text, mimicking its capitalization
pattern.
- The functions rexMatch(), rexSearch(), and rexReplace(), and the
String methods toUnicode(), find(), and findReplace() now accept a
negative values for the "index" argument. This is taken as an index
from the end of the string: -1 is the last character, -2 the second to
last, and so on. For the search and replace functions, a negative
index doesn't change the left-to-right order of the search; it's
simply a convenience for specifying the starting point. Some other
string methods already accepted this notation, so these additions make
the API more consistent.
- The regular expression matcher now matches <space> only to
horizontal whitespace characters. In the past, <space>
matched some vertical whitespace characters as well, but this was
inconsistent with the usual matching rules in most other regular
expression implementations, and was usually undesirable. The new
character type <vspace> explicitly matches vertical whitespace:
'\n', '\r', '\b', '\u2028', '\u2029', and a few ASCII control
characters that the Unicode standard defines as line break characters
('\u0085', '\u001C', '\u001D', '\u001E'). To create a character
class matcher that matches all whitespace, the way <space>
did in the past, use <space|vspace>.
- Look-back assertions are
now supported in the regular expression language. A look-back
assertion tests a sub-pattern against the characters preceding
the current match point, which makes it possible to apply conditions
to the preceding context where a potential match appears. TADS follows
the widely-used Perl-style syntax for the new assertions.
- In the past, the regular expression compiler explicitly ignored
any closure operator (*, +, {}) applied to an assertion, because such
constructs are essentially meaningless and are susceptible to infinite
loops. The compiler no longer takes this approach; instead, it does a
thorough check for meaningless loops in the regular expression, and
deletes them. This is an improvement because it detects all loops, no
matter how complex, whereas the old approach only caught this one
superficial case. This change creates one behavior difference, which
is that it corrects the effect of the * operator applied to an
assertion: in the past, the * was simply ignored, whereas it now
correctly requires that the assertion is true zero or more times.
This is the same as removing the assertion entirely, since a condition
that has to be true zero or more times is really no condition at all.
- The new function sprintf() creates formatted text
from data values according to a format template string. This is
similar to the sprintf() function in many C-like languages. This
style of string formatting is sometimes more compact and convenient
than the alternatives. sprintf() is also particularly useful for
formatting numbers, since it has several style options for integers
and floating-point values that are tedious to code by hand.
- The new String method splice()
lets you delete a portion of a string, insert new text into a string,
or both at the same time. splice(idx, del, ins) deletes del
characters starting at index idx, and then inserts the string
ins in their place. The ins string is optional, so you
can omit it if you just want to delete a segment of the string.
- The String method substr() now accepts a negative value for the
length argument, which specifies a number of characters to discard
from the end of the string.
- The new String method split()
divides a string into substrings at a given delimiter, which can be
given as either a string or a RexPattern (regular expression pattern).
It can alternatively split a string into substrings of a fixed length.
This method comes in handy for surprisingly many simple string parsing jobs.
- The new List method join()
concatenates the elements of the list together into a string.
Vector has this same new method.
- The new String method
specialsToHtml() converts special TADS characters (such as \n
and \b sequences) to standard HTML equivalents. This is designed
specifically for the Web UI, to make it easier to port games between
the traditional console UI and the Web UI by providing support in
the Web UI for the traditional (pre-HTML) TADS formatting codes.
- Another new String method, specialsToText(), is
similar to specialsToHtml(), but converts the string to plain text.
Special TADS characters are converted to their plain text equivalents,
the basic HTML "&" entities are converted to their character
equivalents, a few basic tags (<BR>, <P>, and a few
others) are converted to suitable plain-text equivalents, and most
other tags are stripped out. This is designed for situations where
you need a plain-text rendering of the way a TADS string would look
as displayed on the regular output console.
- The new string methods urlEncode()
and urlDecode() simplify
encoding and decoding URL parameter strings, for use in HTTP network
requests. urlEncode() converts special characters to "%" encodings;
urlDecode() reverses the effect, translating "%" encodings back to
the character equivalents.
- Two new String methods, sha256()
and digestMD5(), calculate
standard hash values for the string's contents. sha256() calculates
the 256-bit SHA-2 (Secure Hash Algorithm 2) hash, and digestMD5()
calculates the MD5 message digest. The same methods are available on
ByteArray to hash the byte array's contents, and on File to hash
bytes from the file.
- It's now easier to convert between strings and ByteArray objects.
First, a ByteArray can now be converted to a string via toString(), or
via implicit conversions, such as a "<< >>" embedding in a
string. The bytes in the array are simply treated as Unicode
character codes. Second, ByteArray.mapToString() can now be called
without a character set argument, or with nil for the character set;
this performs the same conversion as toString(). Third, the ByteArray
constructor has two new formats: new ByteArray('string')
creates an array containing the string, treating each character as a
Latin-1 character; and new ByteArray('string',
charmap) is equivalent to
'string'.mapToByteArray(charmap), but is a little more
intuitive syntactically. Similarly, String.mapToByteArray() can now
be called without the character mapper argument, which is equivalent
to new ByteArray('string').
- ByteArray.mapToString(), the ByteArray constructor, and
String.mapToByteArray() now accept a string giving the name of a
character set in place of a CharacterSet object. The methods simply
create a CharacterSet object for the given name automatically. This
is more convenient for one-off conversions, but if you're using a
character set repeatedly keep in mind that it's more efficient for you
to create the object once and reuse it.
- Concatenating nil to a string with the "+" operator now simply
yields the original string. That is, nil is treated as equivalent to
an empty string for this operator. In the past, the literal string
"nil" was appended instead. This was almost never useful, whereas
it's often a convenience to have nil treated as an empty string in
this situation.
- The compiler now recognizes the "\r" escape code in string
literals. \r represents a Carriage Return character, ASCII 13, which
is the same meaning this code has in C, C++, Java, and Javascript. \r
wasn't part of the TADS escape set historically (although you could
always code it numerically as \015 or \u000D), mostly because there
was never much need for it in practice. TADS tries to smooth out
newline differences among platforms by representing all newline
sequences as \n characters, so it's rare for a \r to find its way into
a TADS string in the first place. We've added \r mostly for the sake
of completeness and familiarity for C/Java programmers.
- The built-in function makeString() now returns an empty string if
the repeat count argument is zero, and throws an error if the count is
less than zero. In the past, all repeat counts less than 1 were
treated as though 1 were specified.
- The new List method splice()
lets you delete a portion of a List, insert new elements into the
list, or both at the same time. splice(idx, del, ins1, ins2, ...)
deletes del elements of the list starting at index idx,
and then inserts the new elements ins1, ins2, etc., in
their place. The new elements are optional; if they're omitted, the
method only does the deletion. If del is 0, the method only
does the insertion. The equivalent method is also now available
for Vector.
- The new static List method generate() creates a list with a
given number of elements by invoking a callback function to generate
each element's value. This is similar to mapAll(), but rather than
transforming an existing list, generate() constructs a new list from a
formula. For example, for a list of the first ten even integers, we
can write List.generate({i: i*2}, 10).
Vector.generate() works the same way to generate a new Vector.
- The Vector constructor now allows you to omit the initial
allocation argument in most cases. You can call new Vector()
without any arguments to use a default initial allocation (currently
10 elements). new Vector(source), where source
is a list or another Vector, creates a Vector copy of the source
object using the source object's length as the initial allocation
size. (This change is meant to make the Vector programming interface
more consistent with the spirit of the class as a high-level,
automatic collection manager. The old requirement to specify what
amounts to an optimization parameter for every Vector was rather out
of character with this spirit.)
- Most of the List and Vector methods that take an index value
arguments now accept negative index values to count backwards from the
last element: -1 is the last element, -2 is the second to last, and so
on. For methods that insert elements, 0 generally counts as one past
the last element, to insert after the last existing element. See the
individual List and Vector method descriptions for details. Note that
this works only with method calls, not with the [ ] subscript
operator.
- The built-in functions max() and min() now accept a single list,
vector, or other list-like object value as the argument. The result
is the highest or lowest value in the list.
- List has four new methods for finding minimum and maximum
elements, optionally applying a mapping function to the element values
to be minimized or maximized. indexOfMin() returns the index of the
element with the minimum value; minVal() returns the minimum element
value; indexOfMax() returns the index of the element with the maximum
value; maxVal() returns the maximum element value. With no
arguments, these methods all simply compare the element values
directly, and minVal() and maxVal() return the lowest/highest
element value. These methods can all optionally take one argument
giving a function pointer. If the function argument is supplied, the
methods call the function for each element in the list, passing the
element's value as the argument to the callback function, and the
methods all use the return value of the function in place of the
element value. For example, if lst is a list of string values,
lst.maxVal({x: x.length()}) returns the length of the longest string
in the list.
Vector has the same four new methods.
- The File object has a new pair of methods, packBytes() and
unpackBytes(), that make it much easier to work with raw binary files,
especially files in third-party or standard formats such as JPEG or
MP3. The new methods convert between bytes in a file and TADS
datatypes in your program, using a mini-language that can express
complex data structures very compactly. This is based on the similar
facility in Perl and php, so if you're familiar with one of those
you'll already know the basics. ByteArray and String have their own
versions of the methods as well, for times when you want to prepare
byte structures in memory rather than in a file. For details, see Byte Packing in the System Manual.
- A new static (class-level) File method, File.getRootName(),
extracts the "root" portion of a filename string. This is the portion
of the filename that excludes any directory or folder path prefix.
For example, given a string 'a/b/c.txt' while running on a Unix
machine, the function returns 'c.txt'. The function uses the correct
local naming rules for the OS that the program is actually running on.
- In File.openTextFile() and File.openTextResource(), if the
character set isn't specified (because the parameter is missing or
nil), the default is now the system's default local character set for
file contents. This is the same character that
getLocalCharSet(CharsetFileCont) returns. In the past, the default
was always "us-ascii". Another small enhancement is that the
character set parameter can now be given as nil, which explicitly
selects the default (in the past, if you wanted the default, you had
to omit the parameter entirely).
- A new static (class-level) File method, File.deleteFile(), lets
you delete files. The file safety level for write mode applies to
deletions, so you can only delete a file that you could also
overwrite.
- File.writeBytes() now accepts a File object as the source of
the data to write to the file. This makes it easy to copy a portion
of one file to another file. When the source is a File object,
the start parameter specifies a seek location in the source
file; if start is omitted or nil, the default starting location
is the current seek location in the file.
- A new File method, setMode(), lets you change the data mode of an
open file.
- In the past, File.getPos() wasn't reliable when reading from text
files. The File object internally buffers text read from the file so
that it can perform character set conversions, and getPos() was
incorrectly returning the position of the last byte read into the
internal buffer, rather than the read position within the buffer.
This made it impossible to reliably seek back to a starting location
in the middle of a series of text reads. This is now fixed.
- Reading from a file opened in one of the read/write access modes
(FileAccessReadWriteKeep, FileAccessReadWriteTrunc) didn't work in
past versions; it could cause a crash. This has been corrected.
- The File methods that open resource files are now affected by the
file safety level. If a resource isn't bundled into the .t3 file, the
open-resource methods traditionally looked for the resource as a
separate, unbundled file within the image file's folder. They now do
this only if the file safety level would allow access to the
same file through a regular open-file method. This makes it
especially important for you to explicitly bundle any resources
directly into the .t3 file, since bundled resources are always
accessible, regardless of file safety settings.
- The new intrinsic class TemporaryFile provides support for
temporary files in the local file system. A temporary file is a file
that only exists for the duration of the program's execution, and is
automatically deleted when the program exits. You can manipulate
temporary files even when the file safety level prohibits access to
the local file system, because their inherent limitations prevent
misuse by malicious programs.
Use new TemporaryFile() to create a TemporaryFile object.
The system automatically assigns the new object a unique filename in
the local file system, typically in a special system directory
designated by the operating system. You don't specify the name of a
temporary file, since the system chooses the name for you to ensure
uniqueness. Creating the TemporaryFile object doesn't actually create
a file on disk; it merely generates a filename that you can use. You
can then create, read, write, and otherwise manipulate the actual file
using the File object. Pass the TemporaryFile object in place of the
filename string to open the file. You can also use TemporaryFile
objects in most other system functions that operate on files
(saveGame(), restoreGame(), setLogFile(), scriptScriptFile(), and
logConsoleCreate()).
- The file safety level is now separated into two components, one
for reading files and the other for writing files. The safety levels
have the same meanings as in past versions, but you can now select the
read and write levels separately by specifying two digits in the
-s option. The first digit is the read level, and the second
is the write level. For example, -s04 allows all read
access, but blocks all write access. If you specify only one digit,
the given level is applied to both read and write operations, so
the option works the same way it used to if you use the old syntax.
The default is still -s2, which allows read and write access
to the directory containing the image file, and denies all other file
access.
- The file safety levels that restrict access to the image file
directory now consider files within subdirectories of that directory
to be within that directory. In the past this was ambiguous, although
most systems considered only allowed access to files directly in the
image file directory. It makes more sense to allow subdirectory
access than to forbid it: subdirectories are conceptually contained
within their parent directory, so access within subdirectories is
still effectively sandboxed to the containing directory.
- Saved game files can now include an optional "metadata" table,
containing game-defined descriptive information about the state of the
game at the time of creating the save file. This can include any
information the game wishes to include, such as the current room
location, score, number of turns, chapter number, etc. The
interpreter and other tools can extract this information and display
it to the user when browsing a collection of saved game files, to help
jog the user's memory about the game position saved in the file. See
saveGame() in the System
Manual for details.
- The setLogFile() and setScriptFile() built-in functions now return
values, to indicate success or failure. On success, the functions
return true; on failure, they return nil. A failure result from
setLogFile() means that the specified file can't be created, and from
setScriptFile() it means the file doesn't exist or can't be opened.
- The new IntrinsicClass static method isIntrinsicClass() lets you
determine if a given object is an IntrinsicClass instance. This isn't
possible using ofKind() or getSuperclassList(), because those methods
work within the inheritance class tree for the intrinsic types.
This new method is required to make this determination.
IntrinsicClass is only used for the representation of an
intrinsic class, and isn't involved in the inheritance structure.
For more information, see the IntrinsicClass
documentation.
- You can now enter expressions containing anonymous functions
interactively in the debugger (such as in the "Watch" list, breakpoint
conditions, or the expression evaluation dialog). This wasn't
allowed in the past.
- The debugger now shows the contents of a Vector in-line when you
inspect its value, just like a list. This saves you the trouble of
using the "+" box in the watch window every time you want to look at a
small vector's contents. You can still use the "+" box as usual, but
when a vector only has a few elements, the in-line display is usually
all you'll need.
- The debugger now makes it easy to view the contents of a
LookupTable. First, when a LookupTable value is displayed in a
tooltip or in a watch window, the list of keys and values is displayed
using the new [key->value] list notation. Second, when
inspecting a lookup table object in a watch window, you can now click
the "+" symbol to inspect the list of key/value pairs stored in the
table.
- The debugger now shows the original pattern string in-line
when inspecting a variable containing a RexPattern object.
- When rand() is used with multiple arguments, it now only evaluates
the one randomly chosen argument value. This means that side effects
will only be triggered for the chosen argument, and not for any of the
other arguments. This is useful because it means that you can use
rand() to intentionally trigger a randomly selected side effect.
For example: rand("one", "two", "three", "four", "five") prints
out a randomly selected number from one to five, and
rand(f(x), g(x), h(x)) randomly calls one of the functions
f(), g(), or h().
- The rand() function can now generate a random string based on a
template string. When rand() is called with a single string argument,
the function returns a string of random characters chosen according to
the template. See the rand()
documentation for details.
- The interpreter now automatically seeds the random number
generator at the start of the run. In the past, it was up to the
program to do this by explicitly calling the randomize() function.
The reason for changing the default is that defaults in general should
be the settings most people would want most of the time, and in this
case that's clearly automatic randomizing. The only time you'd want
not to randomize is during regression testing, when you want to
verify that the program runs exactly the same way every time. When
you do want a repeatable random number sequence, specify the new
-norand interpreter option: t3run -norand mygame.t3
- A new BigNumber method, numType(), returns information on the type
of value represented by the BigNumber. This allows you to identify
the special distinguished values "Not a Number" (NaN) and positive and
negative infinity.
- A few new BigNumber.formatString() flags have been added:
BignumCompact (use the more compact of the regular format or
scientific notation); BignumMaxSigDigits (count only significant
digits against the maxDigits limit, not leading zeros);
BignumKeepTrailingZeros (keep trailing zeros after the decimal point
to fill out the result to maxDigits in length).
- toString() now respects the radix argument for BigNumber values
that are whole integers.
- toString() accepts a new "isSigned" argument that lets you control
whether a signed or unsigned interpretation should be used for integer
values. In the past, this was tied to radix - decimal treated values
as signed, any other radix as unsigned. The default is the same
as before, but you can now override it to get an unsigned decimal
representation, or a signed hex representation, for example.
- toInteger() now accepts any radix from 2 to 36. The letters A
through Z represent digit values 10 through 35 for bases above 10, in
analogy to hexadecimal notation. For better consistency with its
name, the function also now converts the strings "true" and "nil"
respectively to 1 and 0 (instead of the boolean true and nil values
that it formerly returned), and converts the boolean values true and
nil to 1 and 0. It's also a little more liberal about parsing strings
containing the text "true" or "nil", in that it now ignores leading
and trailing spaces.
- The new function toNumber() is similar to
toInteger(), but also parses strings representing BigNumber values.
If the input is a string representing a floating point value (i.e., it
has a decimal point or uses the 'E' exponential format), or an
integer that's too large for an ordinary 32-bit TADS integer, the
value is returned as a BigNumber. If it's a whole number that fits in
a 32-bit integer, it's returned as an integer. This routine can also
be used to parse a BigNumber integer value in a non-decimal radix.
- The compiler now pays attention to resource file timestamps in
determining whether it needs to rebuild the image file. In the past,
merely updating a resource file didn't trigger a relink, so the image
wouldn't be updated with a new resource until it was relinked for some
other reason.
- The new "if-nil" operator ??
checks an expression to
see if the value is nil, and if so yields a default value.
a ?? b yields a if a is not nil, otherwise
it yields b. This is a concise and efficient (in terms
of code size and execution time) way to write a default value
substitution, which is a common situation when using argument
values from callers, property values set elsewhere in the code,
and function and method call results.
- The new operator >>> performs a logical right
shift. Furthermore, the existing >> operator is now
explicitly defined as performing an arithmetic right shift. In the
past, it wasn't specified whether >> performed an
arithmetic or logical shift, although in practice all existing
implementations (as far as we know) performed an arithmetic shift, so
existing programs shouldn't see any change.
The difference between the arithmetic and logical right shift is
the treatment of the vacated high-order bits. A logical right shift
fills all vacated bits with zeros, whereas an arithmetic right shift
fills the vacated bits with the original high-order bit of the left
operand. Arithmetic shifts are so named because copying the
high-order bit preserves the sign of the original value.
The >>> operator isn't purely a TADS invention.
Java and Javascript define it the same way TADS does, and likewise
specify that >> performs an arithmetic shift.
- Many of the standard bundled character sets now recognize a number
of name variations, based on naming conventions used in various other
programming languages and applications. They're meant to make the
character mapper a little easier to use by letting you use names that
you might be accustomed to using from other systems, rather than
having to remember a separate naming scheme for TADS. The new
variations (which are all insensitive to upper/lower case) are:
- Latin-X: In the past, these had to be called "isoN",
as in "iso2" for Latin-2. The mapper now also accepts
Latin2, Latin-2, ISO-2, 8859-2, ISO8859-2, ISO-8859-2, ISO_8859-2,
ISO_8859_2, and L2. (Likewise for Latin-3, Latin-4, etc.)
- Windows and DOS code pages: The old name was "cpXXX", where
XXX is the code page number, as in "cp1252". The mapper now also
accepts just plain 1252, as well as win1252, win-1252, windows1252,
windows-1252, dos1252, and dos-1252.
- UCS-2LE: The little-endian 16-bit Unicode character sets can also
be called UCS2LE, UTF-16LE, UTF16LE, UTF_16LE, UnicodeL, Unicode-L,
and Unicode-LE.
- UCS-2BE: The big-endian 16-bit Unicode character sets can also
be called UCS2BE, UTF-16BE, UTF16BE, UTF_16BE, UnicodeB, Unicode-B,
and Unicode-BE.
- BigNumber now uses the standard "round to nearest, ties to even"
rule whenever rounding occurs, including calculation results and
explicit rounding requests. This is the rounding method that most
computer floating point systems use, because it's considered the least
statistically biased.
The new rule is to round to the nearest digit, or to round to the
nearest even digit when exactly halfway between two digits.
For example, rounding 104.5 to integer yields 104: the value is
exactly halfway between 104 and 105, so we round to the nearest even
digit for a result of 104. Similarly, rounding 1.2345 to four digits
yields 1.234, since 1.2345 is exactly halfway between 1.234 and 1.235,
and the nearest even digit in last place is 4.
The old algorithm was almost identical: it was "round
to nearest, ties away from zero", which rounded to the next higher
absolute value when exactly halfway between digits. For example,
rounding 104.5 to integer formerly yielded 105, since we formerly
always rounded up from the halfway point. There's no change for
values that aren't exactly halfway between digits: rounding 102.5001
or 103.4999 to integer both yield 103 under both the new and old
rules.
- The console output formatter now renders embedded null characters
(Unicode value U+0000) as spaces. In the past, the formatter
considered a null to be the end of a string, which isn't consistent
with TADS string semantics.
- The compiler is less conservative than it used to be about a
certain optimization involving anonymous functions. In the past,
every anonymous function expression triggered the creation of an
AnonFuncPtr object. This object contains the anonymous function's
context: the local variables and "self" from its enclosing code block.
However, many anonymous functions are entirely self-contained, meaning
they don't make any reference to their enclosing contexts. These
don't actually need a context object, since they have no dependency on
anything that would go in it. There's a slight performance cost to
creating the context object, so the compiler now creates one only when
it's actually needed.
This change should be almost invisible to game code. It's
possible to detect if you're looking for it by checking the dataType()
of an anonymous function value: it was formerly always TypeObject, but
now it can also be TypeFuncPtr, for cases where no context object is
needed. Other than this (and a slight speed improvement), the change
should be transparent.
- It's now possible to break out of an infinite loop if you should
happen to get stuck in one evaluating an expression within the
debugger. The debugger already let you break into normal program
execution that was stuck looping, using a platform-defined keystroke
or other UI action (on Windows, for example, the Workbench debugger
uses the key combination Ctrl+Break). The same thing now works if you
evaluate a debugger expression that gets stuck in a loop. (In the
past, the debugger ignored the "break" command while it already had
control. It now interrupts the expression by throwing an error.)
(bugdb.tads.org #0000093)
- In past versions, the StringComparator object didn't save its list
of character equivalence mappings properly to the image file or to
saved game files. This caused character mappings to be lost
(corrupted, actually, but for practical purposes lost) when they were
made during preinit, or when restoring a game where a StringComparator
with mappings had been created dynamically. This has been corrected.
(bugdb.tads.org #0000085)
- The multi-method inherited() operator didn't work properly if used
within a function that was replaced by another function using the
"modify" statement. This is now fixed.
- A bug in the compiler sometimes caused a crash in a rare situation
involving a link-time symbol conflict (in particular, the same symbol
defined as a grammar rule in one module and a different type
in another module). This has been corrected.
- A bug in the regular expression search functions prevented
matching zero-length expressions at the end of the subject string.
For example, searching for 'x*$' correctly matched 'uvwx'
(with a match length of 1), but not 'uvw' (which should match
with a length of 0). This has been fixed.
- A regular expression bug caused incorrect results to be returned
for capturing groups for certain complex expression types. This
was triggered by an expression with two wildcard closures preceding
a capturing group preceding some fixed text. This is now fixed.
(bugdb.tads.org #0000088)
- In past versions of the text-only interpreters, if an
<ABOUTBOX> section contained an <IMG> tag with an ALT
attribute, the ALT text was displayed. It shouldn't have been,
because the text-only systems aren't supposed to display
anything that's within an <ABOUTBOX> section. This has
been corrected.
(bugdb.tads.org #0000063)
- The text-only interpreters now parse and translate HTML
entity markups ("&" sequences) within <TITLE> tags when
setting the window title. (In past versions, the text-only
interpreters simply displayed the literal text of the &
sequences. The HTML interpreters didn't have this problem,
so this change doesn't affect them.)
(bugdb.tads.org #0000062)
- The bug fix in 3.0.18 for newline translation in UTF-16 text files
introduced another bug, which corrupted output text when calling
writeFile() with a string containing embedded newline ('\n') characters.
This has been corrected.
(bugdb.tads.org #0000065)
- The compiler formerly
generated code that was arguably incorrect for a compound assignment
expression containing side effects in the lvalue. In the past,
the compiler effectively expanded an expression of the form
lvalue op= rvalue into lvalue = lvalue op rvalue. For
example, "a += b" became "a = a + b". In most cases this is fine, but
when the lvalue contains side effects, it results in two evaluations
of the side effects. For example, "lst[i++] += 1" incremented "i"
twice. For a really complex expression like "lst[i++][j++][k++] +=
2", the "i++" side effect was triggered even more times, because
it contains multiple implied lvalues.
The correct behavior is to evaluate each subexpression in the
lvalue only once. The compiler now generates code that does just this.
The old behavior was only arguably wrong, in that the
correct behavior wasn't really specified anywhere. But to the extent
that TADS doesn't fully specify its expression behavior,
it's reasonable to expect
that TADS should behave like C++. C++ does have well-defined
semantics for this situation, which the new behavior matches.
Plus, the new behavior is in line
with the obvious reading of an op= expression: the lvalue is
only mentioned once in such an expression, suggesting that it should
only be evaluated once.
- A bug in the debugger caused a crash under rare circumstances.
The problem happened when a run-time error occurred within a callback
function being invoked from certain built-in functions or methods, and
you then terminated the program via the debugger UI while execution
was still suspended at the error location. This is now fixed.
- A bug in the Vector intrinsic class caused rare, random crashes
under certain conditions when calling mapAll(). The crashes were most
likely with very large vectors, and only when the callback function
could trigger garbage collection (such as by creating a new object).
This has been fixed.
- A bug in the BigNumber class caused inaccurate results for expE(),
raiseToPower(), sinh(), cosh(), and tanh() for certain values. The
problem only affected a limited (but difficult to characterize) range
of input values. For affected values, the results were still correct
to about 17 decimal places; the inaccuracy appeared starting at about
the 18th decimal place, so it would only have been noticeable in
applications requiring relatively high precisions. For example, a
program that would have been happy with the standard C "double" type
wouldn't have noticed the bug, because the typical C double provides
only about 15 decimal digits of precision. The problem is now fixed.
- In past versions, the ByteArray object didn't save undo
information for File.readBytes() or ByteArray.writeInt() operations.
It now saves the undo.
- The compiler incorrectly reported internal errors for assignments
to objects, functions, and other global symbol names. The compiler now
shows a regular error message instead for this type of error.
(bugdb.tads.org #0000071)
The compiler has a new feature called "multi-methods." This
implements a relatively new object-oriented programming technique
known as multiple dispatch, in which the types of multiple
arguments can be used to determine which of several possible functions
to call. The traditional TADS method call uses a
single-dispatch system: when you write "x.foo(3)", you're
invoking the method foo as defined on the object x, or as inherited
from the nearest superclass of x that defines that method. This is
known as single dispatch because a single value (x) controls the
selection of which definition of foo will be invoked. With multiple
dispatch, this notion is extended so that multiple values can be
considered when selecting which method to invoke. For example, you
could write one version of a function "putIn()" that operates on a
Thing and a Container, and another version of the same function that
operates on a Liquid and a Vessel, and the system will automatically
choose the correct version at run-time based on the types of
both arguments. This new system is described more fully
in the System Manual.
The compiler can now generate information in each object about the
source file where the object was defined. This new information is
stored in a new property, sourceTextGroup; this supplements the
existing sourceTextOrder property. Since the new information takes up
extra space in the compiled file, it's not generated by default. To
generate it, include the new compiler option "-Gstg" in your makefile
or command line, or place the directive #pragma
sourceTextGroup(on) directive in each source module where you
wish to generate the information. The #pragma lets you selectively
generate the information in specific source files, or even in specific
portions of specific source files: the corresponding directive
#pragma sourceTextGroup(off) lets you turn off the
information in a section where you don't need it.
When you turn on sourceTextGroup generation, the compiler
automatically adds a sourceTextGroup property to each object, just as
it automatically adds a sourceTextOrder value. The sourceTextGroup
value is a reference to an anonymous object that the compiler
automatically creates. One such object is generated per source module
for which sourceTextGroup generation is activated. This object
contains two properties: sourceTextGroupName is a string giving the
name of the module, as it appeared in the compiler command line,
makefile (.t3m), or library (.tl) file; sourceTextGroupOrder is an
integer giving the relative order of the module among the list of
modules comprising the overall program.
sourceTextGroup is useful in cases where you want to establish the
order of appearance in source files among objects in multiple modules.
For a given object x,
x.sourceTextGroup.sourceTextGroupOrder gives you the relative
order within the overall build of the module where x was
defined, and x.sourceTextOrder gives you the relative order
of x among the objects defined within that module. Between
the two values, you can establish a linear ordering for all of the
statically defined objects in the entire program.
The compiler now stores "links" to resource files in the .t3 file
when compiling in Debug mode. This makes it easier to test a game
that uses multimedia resources, by making the Build environment match
the Release environment more precisely.
In the past, when building in Debug
mode, the compiler completely ignored multimedia resource files listed
in the "-res" section of the command line or makefile. This was by
design; the reasoning was that when you're compiling in Debug mode,
you'll only be running that copy on your development machine, within
your build environment, where all of the resource files are available
as local files; and since the files are all available as separate
local files anyway, we can make the compilation go faster by skipping
the step where we copy those files into the .t3 file.
This worked
fine most of the time, but it didn't work in the occasional
case where the resource name is different from the corresponding local
file's name in the file system. One example is the Cover Art file,
which is always stored in the game under the resource name
".system/CoverArt.jpg", which is usually not the same name the
file has locally. This meant that if you tried to refer to such a
resource via HTML (such as with an <IMG> tag), the resource was
reported as missing when running the debug build.
The new arrangement
fixes this problem without giving up the time savings. With the new
scheme, the compiler stores a link for each resource in the .t3
file: this simply records the mapping from resource name to local
file name. The HTML renderer can then look up the appropriate local
file for each resource, including any resources that have special
names. As before, the actual contents of the resources are not
copied.
Note that none of this affects Release builds. The compiler has
always copied the full contents of all resources into the .t3 file
when doing a Release build, and continues to do so. Release builds
continue to be fully self-contained, so that you only have to
distribute the .t3 file to players, not the individual
graphics and sound files it refers to.
The compiler's status output didn't add a line break after each
resource files being added to the project, resulting in poorly formatted
displays. This has been corrected.