The TADS 3 language's expression syntax and operators are essentially the same as they were in TADS 2. Some changes are:
Here is a summary of the TADS 3 operators. The operators are listed in order of decreasing precedence, with related operators grouped. Note that some operators have equal precedence to operators that are unrelated, and thus not shown grouped in the table; to clarify which groups have equal precedence to one another, the "Prec" column gives the relative precedence of each operator. A higher number indicates higher precedence, but the number otherwise has no meaning.
Operator
|
Example
|
Prec |
Operands |
Associativity |
Description |
new |
new Thing(x) |
16 |
1 or more |
Not applicable |
The first operand must be a class or object name; an optional argument list can appear in parentheses after the class name, in which case they're evaluated from right to left and passed to the class's constructor; yields the reference to the newly constructed instance of the given class; the keyword "transient" can appear immediately after "new" to create a transient instance |
inherited |
inherited(x) |
16 |
0 or more |
Not applicable |
A superclass name can be explicitly specified immediately after the "inherited" keyword, or can be omitted to inherit from the next class in override order; an optional argument list can appear in parentheses after the superclass name (or immediately after the "inherited" keyword if no superclass name is present), in which case the arguments are evaluated from right to left and passed to the inherited method; yields the return value from the inherited method |
delegated |
delegated
Thing(x) |
16 |
1 or more |
Not applicable |
See details below on
this operator. |
& |
&prop |
16 |
1 |
Not applicable |
Valid with a property name operand only; yields a
property "pointer" to the named property, which can be stored in a
variable and used to invoke a property/method via the "." operator |
++ (post) -- (post) |
i++ |
15 |
1 |
Not applicable |
The operand must be an lvalue; increments (++) or decrements
(--) the lvalue's numeric value by 1, assigns the new value back to the
lvalue, then yields the original value from before the
increment/decrement |
[] |
lst[3] |
15 |
2 |
Not applicable |
Evaluates the left operand, then evaluates the index value, then indexes the collection value on the left by the index value within the brackets and yields the result |
. |
obj.prop |
15 |
2 |
Left-to-right |
Evaluates the left operand, then evaluates the right operand, then invokes the property or method identified by the right operand on the object value of the left operand, and yields the return value of the property or method |
() |
func(7) |
15 |
1 or more |
Not applicable |
Evaluates the argument list within the parentheses (an
empty list is valid), starting with the rightmost argument, then evaluates
the left operand, then calls the method or function and yields the return
value, if any |
++ (pre) -- (pre) |
++x |
14 |
1 |
Not applicable |
The operand must be an lvalue; evaluates the operand, increments (++) or decrements (--) its numeric value by 1, assigns the result back in the lvalue, and yields the incremented value |
! ~ |
!x |
14 |
1 |
Not applicable |
Evaluates the operand, then yields logical negation (!) or bit-wise negation (~) |
+ - |
-x |
14 |
1 |
Not applicable |
(Single-operand prefix-operator form) Evaluates the operand, then yields the same value (+) or the arithmetic negative (-) |
* / % |
x * y |
13 |
2 |
Left-to-right |
Evaluates left operand then right operand, and yields the product/quotient/modulo |
+ - |
x + y |
12 |
2 |
Left-to-right |
(Two-operand form) Evaluates left operand then right operand, and yields the sum/difference |
<< >> |
X <<
3 |
11 |
2 |
Left-to-right |
Evaluates left operand then right operand, and yields left side shifted left/right by the number of bits specified by the right side |
> < >= <= |
x >=
7 |
10 |
2 |
Left-to-right |
Evaluates left operand then right operand, and yields true if the comparison holds, nil if not |
is in not in |
x is
in(3,4) |
9 |
2 or more |
Left-to-right |
Evalutes left operand, then evaluates each element of the parenthesized, comma-delimited list on the right-hand side in turn until encountering an element with the same value as the left operand (see details below); elements of the right-operand list are assignment expressions |
== != |
x ==
y |
9 |
2 |
Left-to-right |
Evaluates left operand then right operand, and yields true if the results are equal/unequal, nil otherwise |
& |
x &
0x7f |
8 |
2 |
Left-to-right |
Evaluates left operand then right operand, and yields bit-wise AND of the values |
^ |
x ^
0x04 |
7 |
2 |
Left-to-right |
Evaluates left operand then right operand, and yields the bit-wise XOR of the values |
| |
x |
0x08 |
6 |
2 |
Left-to-right |
Evaluates left operand then right operand, and yields the bit-wise OR of the values |
&& |
x &&
y |
5 |
2 |
Left-to-right |
Evaluates left operand; if zero or nil, yields nil, otherwise evaluates right operand, and yields nil if zero or nil, true otherwise |
|| |
x ||
y |
4 |
2 |
Left-to-right |
Evaluates left operand; if true, the result is true; otherwise evaluates right operand, and yields nil if zero or nil, true otherwise |
? : |
x ?
y : z |
3 |
3 |
Right-to-left |
Evaluates first operand; if true, evaluates second operand, otherwise third |
, |
x, y |
2 |
2 |
Left-to-right |
Evaluates left side, then right side |
= += -= *= /= %= &= |= ^= >>= <<= |
x +=
7 |
1 |
2 |
Right-to-left |
Except for =, evaluates the left operand first; then evaluates right operand, and assigns its value to the left operand (=) or combines its value with the left operand's value and assigns the result to the left operand, which must be an "lvalue" of some kind (a local variable, an indexed list, or an object property) |
It's frequently necessary to test a value for equality against several possible alternatives. The traditional way to write this kind of test is with several "==" expressions joined by "||" operators:
if (cmd == 'q' || cmd == 'quit' || cmd == 'exit')
// etc
This kind of expression is especially tedious when the common expression you're testing is more than a simple variable. For example, if you wanted to make the above comparison insensitive to case, you'd have to do something like this:
if (cmd.toLower() == 'q' || cmd.toLower() = 'quit'
|| cmd.toLower() == 'exit')
// etc
In addition to being tedious, this is inefficient: the compiler has to evaluate the call to toLower() again for each "==" test, in case the method call had any side effects. You could always evaluate the toLower() call once and store the result in another local variable, but that requires extra typing for the variable declaration (although the expression would become simpler to type, which sometimes makes up the difference).
The "is in" operator makes it easier and more efficient to write this kind of comparison. This operator takes a list of expressions, enclosed in parentheses and separated by commas, to compare to a given value. We could rewrite the test above using "is in" like this:
if (cmd.toLower() is in ('q', 'quit', 'exit'))
// etc
This is much less work to type, and it's easier to read. It also has the benefit that the expression on the left of the "is in" operator is evaluated only once.
The result of the "is in" operator is true if the value on the left is found in the parenthesized list of values, nil if not.
The entries in the list don't have to be constants, so you could write something like this:
if (cmd.toLower() is in (global.quitCommand, global.exitCommand))
// etc
The "is in" operator has "short-circuit" behavior, just like the || and && operators. This means that the "is in" operator only evaluates as many entries in the comparison list as are necessary to determine if the list contains a match. The operator first evaluates the left operand, then evaluates the items in the list, one at a time, in left-to-right order. If the first element matches the left operand, the operator stops and yields true as the result (or, in the case of "not in", nil). If the first element doesn't match, the operator evaluates the second list element; if this element matches the left operand, the operator stops and yields true (or nil for "not in"). If the operator makes it to the end of the list without finding a match, it yields nil (true for "not in"). Thus, the operator evaluates the list elements only until it finds one that matches, and evaluates all of the elements if there are no matches.
This short-circuit behavior is important when the expressions in the list have side effects. Consider this example:
f1(x)
{
"this is f1: x = <<x>>\n";
return x;
}
// elsewhere...
if (3 is in (f1(1), f2(2), f1(3), f1(4), f1(5))) // ...
The "if" statement will result in the following display:
this is f1: x = 1
this is f1: x = 2
this is f1: x = 3
The "is in" operator will stop there – it won't call f1(4) or f1(5), because it finds the value it's looking for after it calls f1(3).
Another operator, "not in" lets you perform the opposite test: this operator yields true if a value is not found in a list of values:
if (x not in (a, b, c)) // ...
The "not in" operator has the same short-circuit behavior as the "is in" operator: the operator only evaluates as many of the list elements as necessary to determine whether or not the value is in the list. So, the operator stops as soon as it finds the left operand value in the list.
The "is in" and "not in" operators have the same precedence and associativity as the "==" and "!=" operators (these operators all associate left-to-right).
It is sometimes desirable to be able to circumvent the normal inheritance relationships between objects, and call a method in an unrelated object as though it were inherited from a base class of the current object. For example, you might want to create an object that sometimes acts as though it were derived from one base class, and sometimes acts as though it were derived from another class, based on some dynamic state in the object. Or, you might wish to create a specialized set of inheritance relationships that don't fit into the usual class tree model.
The delegated keyword can be useful for these situations. This keyword is similar to the inherited keyword, in that it allows you to invoke a method in another object while retaining the same "self" object as the caller. delegated differs from inherited, though, in that you can delegate a call to any object, whether or not the object is related to "self." In addition, you can use an object expression with delegated, whereas inherited requires a compile-time constant object.
The syntax of delegated is similar to that of inherited:
return_value = delegated object_expression.property optional_argument_list
For example:
book: Item
handler = Readable
doTake(actor) {
return delegated handler.doTake(actor); }
;
In this example, the doTake method delegates its processing to the doTake method of the object given by the "handler" property of the "self" object, which in this case is the Readable object. When Readable.doTake executes, its "self" object will be the same as it was in book.doTake, because delegated preserves the "self" object in the delegatee.
In the delegatee, the targetobj pseudo-variable contains the object that was the target of the delegated expression.
It is legal to omit the property name or expression in an inherited or delegated expression. When the property name or expression is omitted, the property inherited or delegated to is implicitly the same as the current target property. For example, consider this code:
myObj: myClass
myMethod(a, b)
{
inherited(a*2, b*2);
}
;
This invokes the inherited myMethod(), as though we had instead written inherited.myMethod(a*2, b*2). Because the current method is myMethod when the inherited expression is evaluated, myMethod is the implied property of the inherited expression.
The new pseudo-variable targetprop provides access at run-time to the current target property, which is the property that was invoked to reach the current method. This complements self, which gives the object whose property was invoked.
The targetprop pseudo-variable can be used in expressions as though it were a normal local variable containing a property pointer value, except that targetprop cannot be assigned a new value explicitly. You can use targetprop only in contexts where self is valid.
The new pseudo-variable targetobj provides access at run-time to the original target object of the current method. This is the object that was specified in the method call that reached the current method. Note that the target object remains unchanged when you use inherited to inherit a superclass method, because the method is still executing in the context of the original call to the inheriting method.
The targetobj value is the same as self in normal method calls, but not in calls initiated with the delegated keyword. When delegated is used, the value of self stays the same as it was in the delegating method, and targetobj gives the target of the delegated.
You can use this variable only in contexts where self is valid.
This new pseudo-variable provides access at run-time to the current method definer. This is the object that actually defines the method currently executing; in most cases, this is the object that defined the current method code in the source code of the program.
You can use this variable only in contexts where self is valid.
Unlike C and Java, a string in TADS 3 doesn't have to fit all on one line. You can instead write a string that spans several lines:
"This is a string
that spans several
lines of source code. ";
The compiler essentially ignores the line breaks; it converts each line break into a single space character. If a line break is immediately followed by one or more whitespace characters (spaces, tabs, and so on), the compiler removes all of those spaces; so each line break turns into a single space, no matter how many leading spaces are on each line. This is important because most people like to use indentation to make their source code easier to read; ignoring the leading whitespace on each extra line within a string means that you don't have to worry about indentation affecting the appearance of a string.
In some languages, it's undesirable to have any extra whitespace in a string. For example, Chinese is usually written without using any spaces between words. To accommodate languages where extra spacing is unwanted, the compiler has a directive, #pragma newline_spacing(), that lets you control how the compiler treats newlines in strings. By default, newline spacing is on:
#pragma newline_spacing(on)
This tells the compiler to use the behavior described above: each line break within a string is converted to a single space character, and all whitespace characters immediately following a line break are removed. This default is suitable for most Western languages, where spaces are used to separate adjacent words.
For languages where extra whitespace is unwanted, you can turn off newline spacing:
#pragma newline_spacing(off)
This tells the compiler to simply remove each line break entirely, including any whitespace immediately following a line break. In this mode, the compiler doesn't convert a line break to a space, but simply removes the line break entirely.
This pragma only affects strings following the pragma in the source file, so you can change the mode at any time within the same source file. Whenever you change the mode, the new mode is in effect until the next mode change, or until the end of the current source file. Note that if you change the mode within a header file, the mode will revert to its previous setting at the end of the header file – this means that you can #include a file without worrying about any mode changes, because even if the included file changes the mode, the mode will revert back once the compiler finishes with the included file.