Reflection

The term "Reflection" refers to a set of mechanisms in a programming language that let a running program dynamically inspect its own structure.  Reflection features vary by language; static languages such as C and C++ offer essentially no reflection features, while interpreted languages often let the program do almost anything the interpreter itself could.

 

TADS 3 offers a number of reflection features, described below.

Determining a Value's Type

The TADS language does not require (or allow) you to specify at compile-time the type of data that a variable or property can hold.  When a program is running, a variable or property can hold any kind of value, and you can change the type it stores at any time simply by assigning it a new value.  In this sense, the TADS language is not "statically typed": there is no compile-time type associated with a variable or property value.

 

However, the language is strongly typed, but at run-time rather than at compile-time: each value has a specific type, and the type of a value itself can never change (even though its container can change type when you store a new value in it).  In addition, the system does not allow you to perform arbitrary conversions between types; you can never use an integer as though it were an object reference, for example.

 

You can access the type of any value using the dataType() intrinsic function.  This function returns a code that indicates the "primitive" (built-in) type of the value.  The primitive types are the basic types built in to the language: true, nil, integer, string, list, object reference, function pointer, property, enumerator.

 

If the primitive type of a value is object reference, you can learn more about the type by inspecting the class relationships of the object.  At the simplest level, you can determine if the object is related by inheritance to a particular class using the ofKind(baseClass) method on the object.  If you need more general information, you can use the getSuperclassList() method to obtain a list of all of the direct superclasses of the object.

Determining a Property's Definition

The dataType() function can be used with any value, but sometimes it is useful to obtain the type of a particular property definition for a particular object without evaluating the property.  If an object's property is defined as a method, evaluating the property will call the method; sometimes it is necessary to learn whether or not the property is defined as a method without actually calling the method.  For these cases, you can use the propType(&prop) method, which obtains the type of data defined for a property without actually evaluating the property.

 

You can determine if an object defines a property at all using the propDefined(prop, flags) method.  This method also lets you determine whether the object defines the method directly or inherits it from a superclass, and when the method is inherited, to identify the superclass from which the object inherits the method.

Enumerating Active Objects

You can enumerate all of the objects in the running program using the firstObj() and nextObj() intrinsic functions.  These functions let you iterate through the set of all objects in the program, including objects statically defined in the compiler and those dynamically allocated during execution.

Accessing Compiler Symbols: the reflectionServices object

TADS 3 lets a running program access the global symbols that were defined during compilation.  This powerful capability makes it possible to interpret strings into object references, properties, and function references, so that you can do things such as call a property of an object given the name of the property as a string.

 

The standard TADS 3 library provides an optional module that you can include in your program for a simple interface to the compiler symbols.  To use these services, simply include the module reflect.t in your build.  Note that this is designed to be a separately-compiled module, so do not #include it from your source modules - instead, simply add it to the t3make command line when you build your program:

 

   t3make myProg.t reflect.t

 

Note that, by default, reflect.t does not include support for the BigNumber intrinsic class.  However, you can enable BigNumber support by defining the symbol REFLECT_BIGNUM on the compiler command line:

 

   t3make -DREFLECT_BIGNUM myProg.t reflect.

 

The reflectionServices object provides the high-level compiler symbols interface.  The methods of this object are discussed below.

 

formatStackFrame(fr, includeSourcePos) - returns a string with a formatted representation of the stack frame fr, which must be an object of class T3StackInfo (the type of object in the list returned by the intrinsic function t3GetStackTrace()).  If includeSourcePos is true, and source information is available for the frame, the return value includes a printable representation of the source position's filename and line number.  The return values look like this:

 

                    myObj.prop1('abc', 123) myProg.t, line 52

 

valToSymbol(val) - converts the value val to a symbolic or string representation, as appropriate.  If the value is an integer, string, BigNumber, list, true, or nil, the return value is a string representation of the value appropriate to the type (in the case of true and nil, the strings 'true' and 'nil' are returned, respectively).  If the value is an object, property pointer, function pointer, or enumerator, the return value is a string giving the symbolic name of the value, if available, or a string showing the type without a symbol (such as "(obj)" or "(prop)").

Low-Level Compiler Symbol Services

This section discusses the low-level compiler symbol services.  These services are built into the VM.  In most cases, you should use the high-level services provided by the reflectionServices object, since that interface is easier to use and provides substantially the same capabilities.

 

The interpreter provides the program with the global symbols via a LookupTable object.  Each entry in the table has a compiler symbol as its key, and the symbol's definition as its value.  For example, each named object defined in the program has an entry in the table with the compile-time name of the object as the key, and a reference to the object as its value.  In addition, the table includes all of the properties, functions, enumerators, and intrinsic classes.

 

To obtain a reference to the symbol table, use the t3GetGlobalSymbols() intrinsic function.  This function returns the LookupTable object, if it's available, or nil if not.


Note that the global symbol table is available from t3GetGlobalSymbols() only under certain conditions:

 

 

At other times – specifically, during normal execution of a program compiled for release, with no debugging information – the symbol table is not available, so t3GetGlobalSymbols() will return nil.

 

Note that you can use the global symbol table during normal execution of a program compiled for release, if you want.  To do this, simply obtain a reference to the symbol table in pre-initialization code, and then store the reference in a property of a statically-defined object.  When the compiler builds the final image file, it will automatically keep the symbol table because of the reference stored in the program.  Here's an example:

 

  #include <t3.h>
  #include <lookup.h>
 
  symtabObj: PreinitObject
    execute()
    {
      // stash a reference to the symbol table in
      // my 'symtab' property, so that it will
      // remain available at run-time
      symtab = t3GetGlobalSymbols();
    }
    symtab = nil
  ;

 

To reference the symbol table at run-time, you would get it from symtabObj.symtab.  Note that even though you stored a reference to the table, t3GetGlobalSymbols() will still return nil at run-time if the program wasn't compiled for debugging; the reference you saved is to the table that was created during pre-initialization, which at run-time is just an ordinary LookupTable object loaded from the image file.

 

If you don't store a reference to the symbol table during pre-initialization, the garbage collector will detect that the table is unreachable, and will automatically discard the object.  This saves space in the image file (and at run-time) for programs that don't need access to the information during normal execution.

 

Here's an example of using the symbol table to call a method by name.  This example asks the user to type in the name of a method, then looks up the name in the symbol table and calls the property, if it's found.

 

 

  callMethod(obj)
  {
    local methodName;
    local prop;
 
    // ask for the name of a method to call
    "Enter a method name: ";
    methodName = inputLine();
 
    // look up the symbol
    prop = symtabObj.symtab[methodName];
 
    // make sure we found a property
    if (prop == nil)
      "Undefined symbol";
    else if (dataType(prop) != TypeProp)
      "Not a property";
    else
    {
      // be sure to catch any errors in the call
      try
      {
        // call the property with no arguments
        obj.(prop)();
      }
      catch (Exception exc)
      {
        // show the error
        "Error calling method: ";
        exc.displayException();
      }
    }
  }