Expressions and Operators

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, shown in order of precedence (each operator has higher precedence than the operators that follow it in the table).

 

Operator

Operands

Associativity

Description

[]

.

 

2

Not applicable

Evaluates the left operand, then evaluates the index value/right operand, then yields the indexed value/property or method value

&

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

!

~

+

-

++ (pre)

-- (pre)

1

Not applicable

Evaluates operand, then yields logical negation/bit-wise negation/arithmetic identity/arithmetic negative, or increments/decrements the lvalue's contents, then yields the incremented/decremented value.

()

2

Not applicable

Evaluates the argument list within the parentheses, starting with the rightmost argument, then evaluates the left operand, then calls the method or function and yields the return value, if any

++ (post)

-- (post)

1

Not applicable

Increments/decrements the lvalue's contents, then yields the original value prior to the increment/decrement

*

/

%

2

Left-to-right

Evaluates left operand then right operand, and yields the product/quotient/modulo

+

-

2

Left-to-right

Evaluates left operand then right operand, and yields the sum/difference

<<

>>

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

>

<

>=

<=

2

Left-to-right

Evaluates left operand then right operand, and yields true if the comparison holds, nil if not

is in

not in

==

!=

2

Left-to-right

Evaluates left operand then right operand, and yields true if the results are equal/unequal, nil otherwise (see the additional details on "is in" and "not in" below)

&

2

Left-to-right

Evaluates left operand then right operand, and yields bit-wise AND of the values

^

2

Left-to-right

Evaluates left operand then right operand, and yields the bit-wise XOR of the values

|

2

Left-to-right

Evaluates left operand then right operand, and yields the bit-wise OR of the values

&&

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

||

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

? :

3

Right-to-left

Evaluates first operand; if true, evaluates second operand, otherwise third

,

2

Left-to-right

Evaluates left side, then right side

=

+=

-=

*=

/=

%=

&=

|=

^=

>>=

<<=

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)

 

The "is in" and "not in" Operators

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.  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.  Thus, the operator evaluates list elements only until it finds one that 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).

The delegated keyword

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.

Implicit inherited and delegated properties

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 targetprop pseudo-variable

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 targetobj pseudo-variable

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.

The definingobj pseudo-variable

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.

Controlling string newline spacing: #pragma newline_spacing()

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.