Statements

TADS 3 has a number of extensions to the TADS 2 language.  The new language features provide greater functionality and bring the language syntactically closer to Java and C.  Some changes from TADS 2:

 

External declarations:  In most cases, the compiler will automatically create the appropriate external symbol definitions for you.  When you compile a group of modules together with t3make, the compiler automatically constructs symbol files for all of the modules, and then reads the symbol files as it compiles each module.  This allows all of the symbols from all of the modules to be available when compiling each individual module.  When you're constructing a library, though, you obviously won't have access to user-written game modules – you want your library to work with any user's game module, after all.  The extern statement addresses this situation by allowing you to explicitly declare the functions, objects, and classes that you require the user-written code to provide.  The syntax is:

 

    extern function function_name(arg1, arg2, arg3);
    extern object object_name;
    extern class class_name;

 

Function definition syntax:  The function syntax used in TADS 2 is now obsolete; you should use the new syntax, which more closely resembles the syntax that Java and C use.

 
    function_name(arg1, arg2, arg3)
    {
        function_body;
    }

 

Modifying functions:  The modify keyword can be used in a function definition.  Modifying a function is just like replacing it (using the replace keyword), except that the new definition of the function can invoke the old definition of the function (i.e., the definition that's being replaced).  This allows the program to apply incremental changes to a function, such as adding new special cases, without the need to copy the full text of the original function.

 

To invoke the previous definition of the function, use the replaced keyword.  This keyword is syntactically like the name of a function, so you can put a parenthesized argument list after it to invoke the past function, and you can simply use the replaced keyword by itself to obtain a pointer to the old function.  Here's an example.

 

    getName(val)
    {
      switch(dataType(val))
      {
      case TypeObject:
        return val.name;
 
      default:
        return 'unknown';
    }
 
    // later, or in a separate source module
    modify getName(val)
    {
      if (dataType(val) == TypeSString)
        return '\'' + val + '\'';
      else
        return replaced(val);
    }

 

Note how the modified function refers back to the original version: we add handling for string values, which the original definition didn't provide, but simply invoke the original version of the function for any other type.  The call to replaced(val) invokes the previous definition of the function, which we're replacing.

 

Once a function is redefined using modify, it's no longer possible to invoke the old definition of the function directly by name.  The only way to reach the old definition is via the replaced keyword, and that can only be used within the new definition of the function.  However, note that you can obtain a pointer to the old function, and then invoke the old function through that pointer outside the bounds of the redefinition.

 

Explicit property declarations:  A program can now explicitly declare property name symbols using the property statement:

 

    property prop1 [, prop2 [...]] ;

 

This statement declares each listed symbol as a property.  Programs are not required to declare properties names, but it is occasionally useful to be able to do so.  This is especially useful in libraries, because it is common in library code to call out to a method that the library requires the user code to provide in one of its own objects but which the library does not itself define.  In such cases, when the library is compiled on its own (by the library author, not yet linking to user code), the compiler will generate a warning message about an undefined property if the property is not defined anywhere in the library code.  This new syntax allows the library programmer to declare the symbols explicitly, which not only suppresses the warning message from the compiler, but also makes the meaning of the symbols clear to readers of the library source code.

 

Object method definition syntax:  The syntax for defining methods in objects has changed slightly from TADS 2.  For more consistency with Java and C++ method declarations, the equals sign ("=") that TADS 2 used to separate the method's argument list from the method body has been eliminated.  The equals sign is still used for a simple property with a value.  Here's an example:

 

    MyObject: object
        sdesc = "my object"    // use '=' for property values
        doInspect(actor)       // but not for methods
        {
            "It's a pretty ordinary-looking object.";
        }
        doOpen() { "Okay."; }  // no '=' even with no arguments
        doClose { "Done."; }   // same here
    ;

 

Note that calls to doOpen and doClose in this example would allow empty parentheses after the method name but don't require them.  The language doesn't distinguish between an empty argument list and no argument list in this situation.  So, you could make a call to either MyObject.doOpen or MyObject.doOpen(), and both would behave the same way.

 

For more details, refer to the Object Definitions section.

 

Locals in a for initializer:  You can define new local variables in the initializer part of a for statement by using the local keyword in the initializer.  For example:

 

    for (i = 1, local j = 3, local k = 4, l = 5 ; i < 5 ; ++i) // ...

 

This declares two new local variables, j and k, and uses the existing variables i and l.  Note that l is not a new local, even though it comes after the local k definition, because each local keyword in a for initializer defines only one variable.  Note also that an initial value assignment is required for each new local declared.

 

The new locals declared in a for initializer are local in scope to the for statement and its body (this is the same rule that Java uses, although note that it differs from the (undesirable) way C++ works).  The effect is exactly as though an extra open brace ("{") followed by a local statement for each new local appeared immediately before the for statement, and an extra close brace ("}") appeared immediately after the end of the body of the loop.

 

Note that this new feature changes the formal syntax of the for initializer in a subtle way:

 

for_statement: for ( for_init ; for_condition ; for_reinit ) body
 
for_init:  for_init_item , for_init
        |  for_init_item
 
for_init_item:  assignment_expr
             |  local symbol = assignment_expr

 

The subtle change is that a for initializer (for_init) was, in TADS 2, simply a comma expression, but it is now a series of one or more initializer items separated by commas, where each initializer item is either a local variable declaration or an assignment expression.  Apart from the new ability to declare new local variables, this syntax change has no practical effect, since a series of assignment expressions separated by commas is identical to a comma expression.

 

Labeled break and continue:  The break and continue statements can optionally specify a target label.  When a label is used with one of these statements, it must refer to a statement that encloses the break or continue.  In the case of continue, the label must refer directly to a loop statement: a for, while, or do-while statement.  The target of a break may be any enclosing statement.

 

When a label is used with break, the statement transfers control to the statement immediately following the labeled statement.  If the target statement is a loop, control transfers to the statement following the loop body.  If the target is a compound statement (a group of statements enclosed in braces), control transfers to the next statement after the block's closing brace.  Targeted break statements are especially useful when you want to break out of a loop from within a switch statement:

 

scanLoop:
    for (i = 1 ; i < 10 ; ++i)
    {
        switch(val[i])
        {
        case '+':
            ++sum;
            break;
 
        case '-':
            --sum;
            break;
 
        case 'eof':
            break scanLoop;
        }
    }

 

Targeted break statements are also useful for breaking out of nested loops:

 

matchLoop:
    for (i = 1 ; i <= val.length() ; ++i)
    {
        for (j = 1 ; j < i ; ++j)
        {
            if (val[i] == val[j])
                break matchLoop;
        }
    }

 

Varying arguments as lists:  New syntax allows a function that takes a varying number of arguments to receive the varying part of the argument list as a named list parameter, rather than using the traditional getarg() mechanism.  To declare a function or method taking varying arguments as a list of values, replace the ellipsis with the list parameter name enclosed in square brackets:

 

// old way: formatStr(fmt, ...)
// new way:
formatStr(fmt, [vals]) { /* function body */ }

 

This declares a function that takes one or more arguments.  The first argument is named fmt, and all of the remaining arguments are placed into a list named vals.  If the function is called with a single argument, vals will contain an empty list, because there are no arguments after fmt.  If the function is called with two arguments, vals will be a list containing the second argument value.  If the function is called with four arguments, vals will be a list whose elements are (in order) the second, third, and fourth argument values.

 

This new syntax works the same as the traditional getarg() mechanism, and in fact you can still use getarg() to retrieve the argument values even if the new syntax is used to declare a function.  The new syntax is simply an alternative notation that in many cases facilitates more readable code.

 

Of course, the old ellipsis notation is still supported, so you can still write variable-argument functions using the traditional notation if you prefer.

 

Lists as variable arguments:  In addition to being able to receive varying arguments as a list, you can pass a list value as though it were a list of individual argument values.  To do this, place an ellipsis after the list argument value in the function or method call's argument list:

 

   local lst = [1, 2, 3];
   formatStr('x[%d, %d] = %d', lst...);

 

Rather than passing two arguments to formatStr() (i.e., a string and a four-element list), this passes four arguments (a string, the integer 1, the integer 2, and the integer 3), as though all four had been passed as separate arguments – in other words, the call is identical to this:

 

   formatStr('x[%d, %d] = %d', 1, 2, 3);

 

This notation allows you to call a function taking a variable argument list given a list value.  This makes it possible to layer calls to functions and methods with variable argument lists, since an intermediate function can itself take a variable argument list and later pass the same arguments to another variable argument function.  This type of layering was not possible in TADS 2, since there was no way for code to pass a variable argument list obtained from its caller when calling a function or method.

 

The foreach statement:  The new statement foreach provides a convenient syntax for writing a loop over the contents of a collection, such as a list or a Vector.

 

The syntax of the foreach statement is:

 

   foreach ( foreach_lvalue in expression ) body

 

The foreach_lvalue specifies a local variable or other "lvalue" expression which serves as the looping variable.  This can be any lvalue (any expression that can be used on the left-hand side of an assignment operator), or it can be the keyword local followed by the name of a new local variable; if local is used, a new local variable is created with scope local to the foreach statement and its body.

 

The expression is any expression that evaluates to a Collection object, such as a list or Vector value.

 

The statement loops over the elements of the collection.  For each element, the statement assigns the current element to the lvalue, then executes the body.

 

Here's an example that displays the elements of a list.

 

   local lst = [1, 2, 3, 4, 5];
   foreach (local x in lst)
      "<<x>>\n";

 

The foreach statement uses the Collection intrinsic class's createIterator() method to create an Iterator object for the collection expression, then uses the Iterator object's getNext() method to traverse the elements of the collection.  Therefore, the order in which the statement visits the elements of the collection is exactly the same as the order used by the Iterator for the collection.

 

Note that, because the foreach statement uses the Collection.createIterator() method to create the iterator, the iteration uses a "snapshot" of the collection created at the start of the loop.  Because the iterator uses this frozen snapshot, changes made to the collection during the loop will not affect the iteration.  For example, consider the following:

 

   local i = 1;
   local vec = new Vector(10).fillValue(nil, 1, 10).applyAll({x: i++});
 
   foreach (local x in vec)
   {
      vec.applyAll({v: v+1});
      "<<x>>\n";

   }

 

The first two lines create a vector and initialize its elements to the integers 1 through 10, using the applyAll method of the vector.  The foreach body modifies the entire vector – adding 1 to every element – then prints out the current value.  At first glance, we might expect the values displayed to be something like this:  1, 3, 5, 7, 9…; we might expect this because of the applyAll() call updates every element of the vector on every iteration of the loop.  This isn't what happens, though: because the foreach statement iterates over a frozen snapshot of the vector, we actually print out the original contents of the vector:  1, 2, 3, 4, and so on.  After we're finished with the iteration, though, if we look at the vector, we'll find it modified as we'd expect.  In addition, even within the loop, if we were to refer directly to the vector through the variable vec, we'd find it modified as we'd expect – the snapshot pertains only the iteration variable, and doesn't "freeze" the vector itself.  To see this, consider this more complex example:

 

   local i = 1;
   local vec = new Vector(10).fillValue(0, 1, 10).applyAll({x: i++});
 
   i = 1;
   foreach (local x in vec)
   {
      vec.applyAll({v: v+1});
      "x = <<x>>, vec[<<i>>] = <<vec[i]>>\n";
      ++i;

   }

 

This would display the following, showing that the vector has been modified – and the modifications are visible within the loop – even though the modifications are not visible to the iteration variable:

 

   x = 1, vec[1] = 2
   x = 2, vec[2] = 4
   x = 3, vec[3] = 6
   x = 4, vec[4] = 8
   x = 5, vec[5] = 10
   x = 6, vec[6] = 12
   x = 7, vec[7] = 14
   x = 8, vec[8] = 16
   x = 9, vec[9] = 18
   x = 10, vec[10] = 20

 

Although we've belabored this snapshot behavior as though it were some pitfall you must take care to avoid, it should be emphasized that, in most practical cases, this feature relieves you from having to worry about how the iteration will proceed.  Even if you're making changes to the contents of the collection during the loop, you can be confident that they'll have no effect on the iteration.  The snapshot feature makes it easy to iterate over collections without having to worry about the details of how changes would affect the order of the elements, or the number of elements, or anything else.