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

!

~

&

+

-

++ (pre)

-- (pre)

1

Not applicable

Evaluates operand, then yields logical negation/bit-wise negation/address/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

 

This is especially inefficient because the compiler would have evaluate the call to toLower() 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 this is even more verbose.

 

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 in, is easier to read, and 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 list, 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.

Implicit inherit and delegate properties

It is legal to omit the property name or expression in an inherit or delegate 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.

 

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.