T3X
A Minimum Procedural Language

Release 6.8
 
Copyright © 1996-2000 Nils M Holm
 
 
mail: nmh@t3x.org
home: http://www.t3x.org/
 

Table Of Contents

  1. The History of T
    1. The T Family Tree
  2. A Tour through the Language T3X
    1. The Input Alphabet
    2. Comments
    3. Naming Conventions
    4. Data Declarations
      1. Atomic Variables
      2. Constants
      3. Vectors
      4. Structures
    5. Factors
      1. Signed and Unsigned Values
    6. Expressions
      1. Procedure Calls and Subscripts
      2. Unary Operators
      3. Term Operators
      4. Sum Operators
      5. Bit Operators
      6. Relational Operators
      7. Conjunctions and Disjunctions
      8. Conditional Expressions
      9. Constant Expressions
      10. Order of Evaluation
    7. Statements
      1. Assignments
      2. Procedure Calls
      3. Conditional Statements
      4. Loop Statements
      5. Branch Statements
      6. Compound Statements
      7. Local Symbols
      8. Empty Statements
    8. Procedures
      1. Recursive Procedures
      2. Mutually Recursive Procedures
      3. Variadic Procedures
      4. Interface Procedures
    9. The Object System
      1. Classes
      2. Objects
      3. Messages
      4. Class Relations
      5. Class Constants
    10. Modules
    11. Scoping Rules
      1. Conflicts
    12. Type Checking
    13. Meta Commands
    14. The Main Program
  3. The Runtime Environment
    1. The Core Routines
    2. Character Functions
    3. I/O-Streams
    4. Dynamic Memory Management
    5. String Functions
    6. The Tcode class
    7. The System Interface
    8. Video Terminal Control
    9. External Memory Access
    10. Release 5 Compatibility
  4. The Virtual Tcode Machine
    1. The Architecture
    2. Calling Conventions
    3. Instance Contexts
    4. Instruction Cycles
    5. Startup Conditions
    6. Conventions Used in the Following Sections
    7. Declarations
    8. Context Manipulation
    9. Stack Manipulation
    10. Arithmetic Instructions
    11. Predicates
    12. Load and Store Instructions
    13. Flow Control
    14. External Linkage
    15. Source Level Debugging
    16. Loading Tcode
  5. Quick Reference
    1. Language Overview
      1. Declarations
      2. Statements
      3. Operators
      4. Meta Commands
      5. The Formal Syntax
    2. Runtime Support Routines
      1. T3X
      2. Char
      3. IOStream
      4. Memory
      5. String
      6. System
      7. TTYCtl
      8. XMem
      9. R5C
    3. Miscellaneous
      1. Escape Sequnces
      2. The Tcode Instruction Set
  6. License

1. The History of T

In the mid-90's, I was looking for a language providing the following properties (again):

  1. Simple and straight-forward syntax and semantics.
  2. Very high portability.
  3. A small and simple implementation, preferrably in written in itself.
  4. Suitability for both, interpretation and native code generation.

One might think that there must have been quite a few languages providing these features, but obviously my search did not lead to any satisfactory result, or I would not have invented T. Point (3) turned out to be particuraly hard to match. The language which came closest to my requirements was BCPL. The typeless approach which has been very consistently implemented in this language leads to clear, simple, and flexible semantics. The language is portable, its implementation is small and can easily be done in BCPL itself. The compiler provided by Martin Richards, the inventor of BCPL, generates code which is aimed at interpretation (for the purpose of porting the compiler), but may be translated into native code, as well.

Unfortunately, the syntax of BCPL reflects its otherwise overwhelming elegance only to a limited degree. The precedences of some operators have been chosen in a not very intuitive way in my opinion and the syntax of the language is hard to parse by a pure recursive descent parser -- there are precedence rules for statements, for example, which make parsing and understanding BCPL programs unnecessarily hard. An RD parser always had been a must for my language of choice, because they are very easy to implement. [AT this point I have to note that Richard's BCPL compiler is small, elegant and easy to understand, although it uses syntax trees and a bottom up parsing technique.]

Even if BCPL did not match my ideas exactly, it came pretty close and studying the compiler sources has influenced the design of T a lot. Without BCPL, T would not be the language it is today.

The most important thing when designing a programming language is -- in my opinion -- to define its main purpose. The design goal of T was to create a portable, simple, and easy to understand notation for the description of algorithms. T was never aimed at industrial software development. Its purpose is to support the programmer in the process of reasoning about problems. It should be a productivity tool in the sense that it provides a playground for new ideas and allows the creator of these ideas to share it with others using a formal notation. Such a notation, of course, has to be clear, simple, easy to learn, and it would be a great advantage, if a compiler for this notation would be available in many different environments.

Naturally, my interpretation of `productivity' is not exactly the same as in the rather profit-oriented `real world' and the design of the language T reflects this intention very well. T is not suitable for writing large scale `application programs', nor for `rapid application development'. It is more a notation than a programming language. Because it is simple and straight-forward, it does not force its user to pay too much attention to the language itself. Instead, it provides some very basic `building-stones' which may be used to build a formal solution for a given problem.

There are many popular programming languages which provide functions to create `popup menus', `radio buttons', database queries, `event handling' and tons of features which are very helpful when creating graphical user interfaces and such things. But all these features do not really help the programmer solve a problem -- unless the problem is to create an application program. When I am talking about a `problem', however, I usually mean the search for an algorithm. Frequently, people say things like

`A' cannot be done in language `B'.

Normally, such statements are the result of too little reasoning. Basically, any algorithm can be implemented in any language. The only difference is in the amount of work one has to perform to solve the same problem in different languages. So the correct form of the above statement would be

Problem `A' is inconvenient to solve in language `B'.

T provides only a very basic set of building-stones, but it turns out that this set is suitable to solve a variety of different problems in a convenient way -- including, for example, the creation of a compiler for the language itself.

Since release 6.0, the things have changed a little. Even when creating very simple programs, one has to 're-invent the wheel' too frequently. Therefore, some object oriented extensions have been added to the core language to ease the re-use of existing code modules. The new language (which is of course compatible to prior releases) supports an object oriented programming style, but uses object orientation primarily at the module level and not su much for code abstraction. Even in a typical object oriented T3X program, the 'object' data types are not so frequently seen as in other OO languages.

1.1 The T Family Tree

Since the original design, many different versions of the language T have been created. The development of the most versions has been frozen at some point and only T3X is currently being avtively developed. The following figure provides an overview over the existing T and T3X versions.

        T 1
  (just an idea...)
         |
         |
        T 2 
(C version, 386, 8086)
         |
         |
        T 3 ----+---- T 3r0c
         |      |     (386)
         |      |
         |      |
         |      +---- T 3r0t
         |      (386, 8086, symbolic)
         |              |
         |            T 3r1t
         |      (386, 8086, symbolic)
         |              |
         |            T 3r2t -------------+---- T3Mr0
         |      (386, 8086, symbolic)     |     (6502)
         |              |                 |
         |            T 3r3               |
         |      (386, 8088, symbolic)     +---- T3Xr{0-3}
         |                                 (Tcode, 386, 8086,
         |                                  synthesizing TXCG,
         |                                  VM in C)
        T 4 ---- T4r0                             |
             (386, 8086,                        T3Xr4
            synthesizing CG,             (Tcode, 386, 8086,
            Modules, serapate             TXCG, TXOPT, include
              compilation)                files via TXPP)
                                                  |
                                                T3Xr5
                                         (Tcode2, 386, 8086,
                                          TXCG, TXOPT, TXPP,
                                          debugger support)
                                                  |
                                                T3Xr6
                                         (Tcode3, 386, new
                                          RT system, object
                                          extension)
Fig.1 The T Family Tree

The original T (version 1) has never been fully implemented. It was merely an idea -- something I was playing with.

The first real implementation was T2. It was written in C using very conventional RD parsing techniques. There were assembly code generators for 8086 and 386-based machines. These generators are still available in all T3 and T4 packages, but not in T3M and T3X.

T3 is the root of a large subfamily of T versions. T3r0t (T version 3, release 0, T implementation) has been implemented first. The techniques used in the T3 implementation were quite different from the techniques used in the T2 translator. Since the T3 code generators were rather dumb at that time, efficiency became very important, because the compiler was used to compile itself. (At least, I wanted a reasonably fast compiler). The C version has been written at a later time to allow the installation of T3 on machines providing a C compiler. Basically, it was a modified T2 compiler. The C version is included in all T3r? packages.

The language definition of T3 differed from T2 only by the addition of some built-in runtime procedures, namely OPEN(), CLOSE(), and ERASE().

Subsequent releases of T3 were mainly bug-fix releases. For a long time, T3 was one of the most stable and usable branches of the language. Nowadays, it is no longer maintained, because it has been entirely superseded by T3X. T3r3 was an attempt to make T3 more T3X-compatible under some aspects. It has been created after the first T3X version

The next step in the development of T was T4. T4 features separate compilation, modules, and synthesizing code generators for 8086- and 386-based platforms. There were many other additions to the language itself which made T4 more convenient to use than T3. However, T4 remains an experiment. There has never been a real release of T4. Most enhancements to the language itself have been integrated into T3X in the course of time and some experiences gained under its development turned out to be valuable when creating the module system of T3X release 6.0.

T3M was an experimental port of T3 to 6502-based systems. It has never been finished.

The flagship of the T3 family -- and in fact, of the entire T family -- is T3X (T3 eXtended). Most important features of all other branches have been integrated into T3X and a totally different approach to portability has been chosen at this time. Instead of defining a procedure call interface to the code generators, Tcode -- a simple intermediate language -- has been created to pass information from the compiler to the code generator. This way, the translator and the code generator have been conceptionally separated. Another goal of this approach was the creation of a language which is suitable for the efficient interpretation by a virtual machine. In fact, the first `back end' for the T3X compiler was a Tcode interpreter.

Of course, the use of a simple virtual machine for porting a compiler to new platforms is not new. A very good lecture covering this topic is still "BCPL - the language and its compiler", Richards & Whitby-Strevens, Cambridge U Press, 1980.

Since release 6.0, the language T3X contains some extensions which support an object oriented programming style, modules, separate compilation, and a new and different runtime support system. Some new keywords and statement types have been added to the language and the object has been introduced as a new data type. Notice, though, that T3X will basically remain a typeless language. Great effort has been put on integrating the OO concept into the typeless design. Therefore, the only type-checks which have been added are related to sending messages to objects and accessing members of modules.

According to the T3X approach, the object oriented concept has been reduced to a bare minimum while adapting it to the language. Many semantic ambiguities have been removed and the whole concept has been highly simplified. One could say that T3X with OO extensions is to Object-Pascal what purely procedural T3X is to Pascal.

2. A Tour through the Language T3X

T3X is a typeless, block-structured, procedural, object oriented programming language. Programs, classes, procedures, statements, and expressions form a hierarchy: Programs consist of classes, procedures, and statements, classes contain procedures and statements, procedures usually contain statements, and statements mostly contain expressions. Variables may be atoms (ordinal) or vectors (one-dimensional arrays). Since there are no different types, composed data types - called structures - are basically equal to vectors. Constants may be used to represent frequently used or tuneable values.

2.1 The Input Alphabet

The T3X compiler expects its input in the form of an ASCII file (a sequence of octets where the least significant seven bits contain the ASCII code of one character and the high bit is set to zero). The following characters will be treated as white space (The C-style 0x-notation is used to represent hexa-decimal numbers):

White space characters delimit tokens, but will be otherwise ignored by the compiler.

Valid input characters are the upper and lower case alphabetic characters A-Z, a-z, the decimal digits 0-9, and the following special characters:

! " # % & ( ) * + , - . / : ; < = > @ [ \ ] ^ _ | ~

Characters which are not contained in this alphabet may occur only in string literals, character literals, and comments. Otherwise they will cause an error.

2.2 Comments

A comment may be introduced at almost any point in a T3X program using an exclamation point (!). It extends up to but not including the end of the line. Therefore, a comment is treated the same way as a single white space character, and consequently,

wh! this is a comment
ile(1) ;

is equal to

wh ile(1) ;

and not to

while(1) ;

Consequently, comments may not occur inside of a single token, but only between two tokens. This is particularly valid for string literals and character literals which are single tokens as well. A !-character inside one of these literals is treated as an ordinary character.

2.3 Naming Conventions

Symbolic names may contain alphabetic characters, the underscore character (_), and decimal digits, where the first character must be alphabetic or an underscore. Upper case and lower case characters will be treated equal. Therefore, the names

abc abC aBc aBC Abc AbC ABc ABC

would all refer to the same symbol in a T3X program. The T3X compiler always uses all characters contained in two symbols to distinguish them, so

very_very_very_long_symbol_number_one

and

very_very_very_long_symbol_number_two

are guaranteed to be different. The maximum length of symbol names may be limited by other factors like the maximum token length, however.

2.4 Data Declarations

In fact, T3X is not a totally typeless language. I am calling the language 'typeless' anyway, because even BCPL (which pushes the typeless concept quite to its limit) has at least two types (variables and MANIFEST constants) which require different handling at compile time. In T3X, the following types exist:

Vectors and structures are basically the same and there is no big difference between methods and procedures, either. Atomic variables are used to hold small numeric values or single ASCII characters (which are represented by numbers). Constants are used to provide symbolic names for immutable numeric values. Vectors are sequences of atomic variables. A structure is a set of constants which is used to give names to single members of a vector. Procedures process parameters and return values just like mathematical functions. Since T3X is an imperative language, they usually have side effects, too. A method is a procedure which is used to alter the state of an object. An object is an instance of a class (which is not a type of its own). Classes will be discussed in detail in the section about object oriented programming.

2.4.1 Atomic Variables

Each (atomic) T3X variable allocates exactly one machine word. When talking about variables in the remainder of this document, the attribute atomic is always implied. Vectors will be implicitly referred to as vectors or arrays.

Variables are defined using a VAR statement. Any number of names may be defined in a single statement:

VAR x_coord, y_coord, depth;

Although, it is recommended to define only logically connected variables in a single statement.

All types of values may be stored in a variable: numeric values, pointers to strings, pointers to vectors, pointers to structures, pointers to objects, or single characters. The range of numeric values which may be stored in a variable actually depends upon the implementation. The Tcode engine uses only 16 bits to represent a cell -- independently from the underlying platform. Therefore, programs which use values not in the range -32767...32767 may be considered machine-dependent.

When a variable is placed in an expression (frequently also called a righthand side value, it evaluates to its value. When it is placed on the lefthand side of an assignment, however, it evaluates to its address (which will be dereferenced immediately by the assignment operator, of course).

2.4.2 Constants

Constants are variables which exist only at compile time. Instead of an automatically assigned address, they are initialized with an explicitly specified value when they are declared. Since they are compile time entities, the values of constants may not change at run time. Any number of constants may be declared in a single CONST statement:

CONST READ=1, WRITE=2, RDWR=READ|WRITE;

Each constant name must be followed by an equal sign (=) and a constant expression which evaluates to the value of the constant. Constant expressions will be explained in a later section.

Constants may occur only in righthand side expressions, where they evaluate to their values.

2.4.3 Vectors

Vectors, like constants, are compile time variables. When they are declared, they will be initialized with the address of an array of subsequent machine words, the so-called vector members or vector elements. The address of a vector is equal to the address of its first member. Any number of vectors may be defined in a single VAR statement. Declarations of vectors and atomic variables may be mixed in one and the same statement:

VAR RingBuffer[1000], Head, Tail;

Vector declarations differ from atomic variable declarations by the trailing square brackets containing a constant expression which specifies the size of the vector in machine words. The first member of a vector has the index value 0 and the last one has the index vectorsize-1 (999 in the above example). The size of a vector may range from 1 to 16383 elements.

Since vector addresses are stored in compile time variables, they may not change at run time. Naturally, it is legal to change the values of vector members, though. When occurring in righthand side expressions, vector names evaluate to the addresses of their associated arrays.

Single members of a vector may be addressed using the subscript operator []. The expression

v[5]

for example, evaluates to the fifth member of the vector v. Subscripted vectors may occur on the left sides of expressions, as well. The assignment

v[i] := 99;

would change the i'th member of v to 99. Like atomic variables, the members of vectors may be used to store any data type, even pointers to vectors. See the description of the []-operator for details about nested vectors.

A special case of the vector is the byte vector. Like `ordinary' vectors, they are declared in VAR statements:

VAR Input::256, Output::256;

The only difference between a vector and a byte vector is the computation of the required size. The size value after the ::-operator specifies the number of characters required. The amount of memory actually allocated depends on the size of a machine word on the target machine, which is returned by the core class procedure T.BPW(). For all Tcode programs, T.BPW()=2 is valid. The size of a byte vector is computed using this formula:

vectorsize + T.BPW() - 1
------------------------
         T.BPW()

which allocates enough space for at least vectorsize characters. No further type information is stored in vector entries. Therefore, it is valid to access byte vectors using [] and word vectors using ::. However, this is discouraged, because the actual vector sizes might depend on a specific implementation.

A byte vector may not be larger than 32766 bytes (16383 machine words on the Tcode machine).

2.4.4 Structures

A structure is a composed data object. Only one structure may be defined in a single STRUCT statement:

STRUCT POINT = PT_x, PT_y;

Note that this statement does not actually create a new data object, but only the `layout' of a point structure. To create a point data object, an additional VAR statement is required.

VAR point_a[POINT], point_b[POINT];

creates two point entities, point_a and point_b. The members of such an entity can be addressed using the subscript operator: point_a[PT_x] and point_a[PT_y].

Structures do not really have an own type. As the declaration and member access syntax already suggests, they are ordinary arrays and the member names are constants. In fact, the statement

STRUCT s = a, b, c;

is perfectly equal to

CONST s=3, a=0, b=1, c=2;

The STRUCT statement only defines symbolic names for accessing vector members with a fixed position and known meaning. The structure name is another constant which holds the number of constants used to name the members (and therefore the size of the entire structure).

2.5 Factors

This section describes the most basic elements of each T3X program, the factors which may be used in expressions. This chapter is written in bottom-up order, so that factors already have been explained when expressions will be discussed, and expressions already have been explained in the section about statements.

There are many different kinds of factors: symbols, numeric literals, character constants, string literals, tables, and procedure calls. A factor may occur only in expressions and a single factor is the minimum form of an expression. Factors may be prefixed by unary operators or they may be combined using binary or ternary operators. Basically, all sorts of factors are exchangable: where one of them may occur, all others are allowed, too. The only exception is the symbol which has some additional properties which make it special. For example, symbols may be subscripted and it is possible to compute their addresses. These operations are limited to symbols. All other operations may be applied to any kind of factor, even it it makes little sense, like the multiplication of two strings (which will lead to highly environment-dependent results):

"Hello" * "World"

The evaluation of a symbol depends on its type. Variables and constants evaluate to their values, vectors and objects evaluate to their addresses. Structure names and structure member names are treated like constants.

Numeric literals are written in decimal or hexa-decimal notation and represent their own values. A percent sign may be used to negate a number:

%123 = -123.

The difference between %123 and -123 is that %123 is a factor while -123 is an expression (`minus' applied to a numeric factor). In fact, the percent sign has little meaning in T3X, since the compiler evaluates ordinary minus prefixes in constant expression contexts, too. In T3, constant expressions were limited to single factors and therefore, the percent sign was required to define negative constant values. The %-prefix has been kept for compatibility reasons. An optimizing compiler might turn -n into %n, if n is a constant numeric factor.

The hexa-decimal notation may be used to represent a numeric value when prefixing the literal with the characters '0x' or '0X' (null, X). No space is allowed between the prefix and the hexa-decimal digits. The number 4095, for example, also can be written as '0xfff' or '0xFFF'. The characters 'A' through 'F' (alternativey 'a'...'f') are used to represent the hexa-decimal digits with the values '10' to '15'. No difference is made between upper and lower case characters. The literals

0x1f 0X1f 0x1F 0X1F

all express the value 31. The percent prefix may be combined with hexa-decimal factors, as well.

Character constants are single characters or escape sequences enclosed by single quote characters like

'a' '0' '\s' ''' '\\'

A character constant evaluates to the ASCII code of the enclosed character. An escape sequence may be used to include certain unprintable or special characters. The backslash character is used to introduce the sequence. The '\' and the following character will be removed and replaced with the associated character. Note that no escape sequence is required to represent an apostrophy: '''. Besides most C-style sequences, the following translations will be performed: \e->ESC, \q->", and \s->blank. The latter has been included for readability reasons. Unlike C, T3X accepts uppercase sequences as well: \e and \E both evaluate to ESC. The escape character may be used to escape itself. Thereby, it loses its special meaning and '\\' evaluates to a single literal backslash. A summary of all escape sequences is listed in the quick reference section.

String literals are sequences of characters delimited by double quotes ("):

"Hello, World!\N"

Each character either represents itself or is part of an escape sequence as described above. A full machine word will be allocated per character, but currently, only the least significant eight bits will be filled with the ASCII code of the character. In later extensions, the entire space may be used to represent Unicode characters, for example. Each string literal is terminated with a NUL character, so n+1 machine words are required to store a string of the length n.

Since a string is an array of subsequent machine words, the []-operator may be used to access its single characters.

When a string is prefixed with the keyword PACKED, a byte instead of a full word is used to store each of its characters, and enough NUL characters are appended to pad the literal to the next word boundary. This way, the proper alignment of subsequent data objects is guaranteed. The byte operator :: may be used to access single characters of packed strings. The meta command #packstrings; may be used to automatically pack all strings in a program

At runtime, either form of the string literal evaluates to the address of its first character.

A more general form of a literal vector is the table. A table is a static initialized vector and a generalization of BCPL-style TABLEs. Syntactically, it is a list of table elements delimited by square brackets:

[ 7, "MOD", @modulo ]

Each table member occupies exactly one machine word. A string, for example, is represented by a pointer, while the string literal itself is placed outside of the table. Therefore, table members can be accessed using the subscript operator []:

[ 77,88,99 ] [2]

evaluates to 99. The square brackets have been chosen for delimiting tables because of the connection between vectors and the subscript operator.

The class of each table member may be any out of the following list:

Constant expressions include everyting which has a value that may be computed at compile time (like numeric literals). The inclusion of strings has been explained above. Addresses of global variables and procedures are represented by a symbol name prefixed with the address operator @.

What makes tables particulary flexible is the possibility to nest them:

[ [ 2, 9, 4 ],
  [ 7, 5, 3 ],
  [ 6, 1, 8 ] ]

Like strings, embedded tables are stored outside of the surrounding table and included as pointers. If, for example, the above table is assigned to the symbol v, the following conditions hold:

v[0] = [ 2, 9, 4 ]
v[1] = [ 7, 5, 3 ]
v[2] = [ 6, 1, 8 ]

Since the result of applying a subscript operator to a table containig tables results in a vector again, the subscript operator may be applied one more time, and consequently,

v[1][1]

would result in 5:

v       = [ [2,4,9], [7,5,3], [6,1,8] ]
v[1]    = [7,5,3]
v[1][1] = 5

(Remember that the first element of a vector has the index 0.)

A table which contains at least one non-constant expression is called a dynamic table. Non-constant expressions must be put in parentheses when they are to be included in a table:

v := [ "a * b = ", (a*b) ];

Embedded (non-constant) expressions are computed freshly each time the flow of the program passes the table they are contained in (each time the table is evaluated). Therefore, the values of table members computed by embedded expressions may be different each time the table is evaluated. This is why such a table is called 'dynamic'. The parentheses indicate to the compiler that an expression is non-constant and make it generate additional code to fill in the value of the expression whenever the table is encountered. Therefore, static (constant) expressions should never be parenthesized in tables, because inefficient code could be the result.

v := [ "5 * 7 = ", (5*7) ];

works, but computes 5*7 each time the table is evaluated. (Note: Even if an optimizing compiler would fold 5*7 to 35, the value would have to be stored in the table each time it is passed.)

On the other hand, including dynamic expressions in a table without any parentheses will lead to an error:

v := [ "a * b = ", a*b ];

will not work unless both, a and b are constant.

Like strings, tables may be prefixed with the keyword PACKED. Packed tables may contain only byte-values. Therefore, their members are limited to constant expressions with bit patterns where only the least significant 8 bits may contain values other than 0. Expressed in numbers, this is the range from -128 to 255.

A strings may be considered a special form of a table. Consequently, each string may be written as a table as well. For example,

"T3X"

is equal to

[ 'T', '3', 'X', 0 ]

(Note the trailing zero in the vector literal.) A similar relation exists between packed strings and packed tables. Like packed strings, packed tables will be padded with zeroes up to the next word boundary.

The maximum number of members per table may be limited, but at least 128 elements per table are always allowed. The elements contained in nested tables do not count, but the entire embedded table counts as a single member. The same limit may exist for packed tables and string literals.

Procedure calls are represented by the procedure name followed by a parentheses-enclosed list of zero or more comma-separated arguments:

sys.write(1, "Hello, World!\n", 14);

Each argument may be any valid expression. When a procedure expects zero arguments, the parentheses still must be supplied: P(). A procedure call evaluates to the return value of the called procedure.

In T3X, only procedures may be called. Calls to absolute addresses and computed calls -- like in BCPL -- are not allowed. There is a mechanism to perform indirect calls, though. The CALL operator allows to call a procedure whose address is stored in a variable:

p := @printpacked;
CALL p(packed "Hello, World!\n");

For several reasons, the CALL operator should be treated with special care: The argument count will not be checked. Due to the T3X calling conventions, a wrong number of arguments will in most cases lead to an undefined result. The result is also undefined, if the variable in the CALL statement does not point to a valid procedure. The only way to obtain a valid procedure pointer is the application of the address operator @ to a procedure symbol.

When the symbol following the keyword CALL references a procedure rather than a variable, the CALL operator will be ignored.

More detailed information on procedure calls can be found in a later section.

2.5.1 Signed and Unsigned Values

Numeric entities usually carry a 'sign' in T3X. This means that a part of a bit pattern representing a number is reserved to indicate the number's sign (positive or negative). On two's-complement machines, the most significant bit (high bit) contains the sign flag. If this bit is set, the number is negative and otherwise it is positive. Therefore, the numeric range on the Tcode machine includes the values -32767 to 32767 (in bit patterns 0xffff to 0x7fff).

Under some circumstances, it is desirable to interpret a number as an unsigned entity instead (for example when comparing pointers). In this document, a leading dot is used to indicate an unsigned number. In T3X itself, no such notation exists, but some operators may be modified with a leading dot to turn them into 'unsigned' operators. Unsigned operators treat the sign bit as a part of the value. Therefore, the range of these operators is from 0 to 65535 (on the Tcode machine).

For example, the 'less than' operator '<' evaluates the sign bits of its operands and consequently,

-1 < 1

is true. The 'modified less than' operator '.<' does not evaluate any signs and therefore

-1 .< 1

is false. Of course, it would be preferrable to write

65535 .< 1

in this case. Since the modified operators operate on raw 'bit patterns', -1 and 65535 represent the same value for them: 0xffff. To avoid confusion, however, signed and unsigned operators should only be applied to the follwowing ranges:

Range Operators
-32767...32767 signed (* / < > <= >=)
0...65535 unsigned (.* ./ .< .> .<= .>=)

2.6 Expressions

In expressions, operators may be used to modify or combine factors in various ways. Most operators may be applied to any kind of operand, even if the resulting operation does not evaluate to any meaningful value.

There are different kinds of operators and like procedures, they are classified by the number of their arguments (which are called operands in this context). There are unary (prefix and postfix) operators, binary (infix) operators, and there is one ternary operator and one variadic operator.

Operators also may be classified by their precedences. The higher the precedence of an operator is, the stronger it binds its operands. For example, the term operators (multiplication, division, modulo) bind stronger than the sum operators (addition, subtraction). Therefore,

a * b + c * d

is equal to

(a*b) + (c*d)

Like in math expressions, parentheses may be used to override these default bindings. The precedence rules are simple in T3X:

  1. Postfix operators bind strongest.
  2. The less operands an operator has, the stronger it binds.
  3. The ascending order of precedence for binary operators is as follows: disjunction (1), conjunction (2), equational (3), relational (4), bit (5), sum (6), and term (7) operators.

The precedence rules in 3. are similar to the rules used in the evaluation of algebraic math expressions.

Another property of an operator is its associativity. An operator associates to the left when a sequence of identical operations is evaluated from the left to the right:

Associativity Expression Meaning
lefta - b - c (a - b) - c
righta - b - c a - (b - c)

In T3X, all binary operations with the sole exception of :: are left-associative. The byte operator is right-associative. Unary operators are always right-associative.

In the remainder of this section, all availabe operators will be explained. The appearance is ordered by descending precedence.

2.6.1 Procedure Calls and Subscripts

The operators (), [], and :: are the only postfix operators. They always bind to primary factors in the form of symbols and this dependency cannot be overridden using parentheses. Subscripts and call operators may be considered a part of a factor rather then an operator applied to a factor.

The ()-operator is the only variadic operator. Given the procedure call

P(a1, ..., aN)

its arity is N+1 (P plus N arguments). The meaning of the operator is the application of the procedure P to the (optional) arguments a1 through aN. If P does not have any formal arguments, the syntax is

P()

The value of the operation depends on the semantics of P. See the description of the RETURN statement for further details.

() may be applied only to symbols of the type `procedure'. The procedure must have been declared before its first application using either a procedure definition or declaration. The number of arguments to a procedure call will be checked against the arity of the called procedure. If the numbers do not match, an error will be signalled.

Each argument to a procedure call may be any valid expression itself which includes, of course, procedure calls. Given the binary function P2, the following expression is perfectly valid:

P2( P2(1, 2), P2( 3, P2(4, 5) ) )

An indirect procedure call may be performed using the CALL operator which is in fact an extension to the () operator. The expression

CALL PP(a1, ..., aN)

like the previous example, evaluates to the result of the application of PP to a1...aN, but in this case, PP is a procedure pointer instead of an actual procedure. A procedure pointer is an ordinary variable which has been assigned the address of a procedure using the address operator:

PP := @P;

In indirect procedure calls, no type checking as described above will be performed. If PP is the name of a procedure instead of a variable, the keyword CALL will be ignored.

In direct and indirect calls, the calling scheme is call by value. This means that the arguments to the call will be evaluated before the call actually takes place and the value of each parametric expression will be transported to the procedure.

Note: Since vectors evaluate to their addresses, passing a vector by value will actually pass a reference to the array associated with the vector symbol. Therefore, vectors are always passed by reference: Instead of passing the entire vector, only the address of its first member is transported to the called procedure. There, the address will be stored in an (atomic) parameter variable. Since parameters are always atomic and therefore evaluate to their values and vectors evaluate to their addresses, both the actual vector and the parameter will reference the same memory location and the parameter may be used as a vector.

Procedure parameters are guranteed to be evaluated in the order of occurance (from the left to the right). For example, in the expression

P( Q(), R() );

the programmer may rely on the fact that Q() will be called before R().

The subscript operator [] may be applied to vectors as well as to atomic variables. In expression of the form

symbol [subscript]

subscript may be any valid expression. If symbol is a vector, the subscript operation

a[b]

evaluates to the b'th member of a. If a is an atomic variable, the operation evaluates to the b'th member of the vector, a points to. This means that both subscripts in the following example would evaluate to the same value:

var     v[100], pv;
var     a1, a2;

pv := v;
a1 := v[25];
a2 := pv[25];

The reason is simple. Since vectors evaluate to their addresses, the assignment

pv := v;

stores the address of v in pv. Atomic variables, on the other hand, evaluate to their values and therefore, pv evaluates to the address of v which has been previously stored in it. Consequently, v and pv both evaluate to the address of v in the above example. Hence, a variable which holds the address of a vector may be used in place of that vector. For this reason, the subscript operator may be applied to atomic variables, as well.

Since there is no nesting limit for vectors, any number of subscript operators may follow a single symbol. Assuming that v5 holds a vector containing five levels of nested vectors, the expression

v4[i1][i2][i3][i4][i5]

may be used to access single elements at the deepest nesting level. Such chains of subscripts evaluate from the left to the right.

The byte subscript operator :: differs from the ordinary (word) subscript operator in several ways. First, it addresses bytes in (byte) vectors and second, it is right-associative. The expression

a::b

evaluates to the b'th byte contained in the vector a. Therefore, :: is mostly used to access characters in packed strings. Since the results of ::-operations are always limited to byte-width, they cannot be assumed to always return valid addresses. For this reason, byte subscripts are right-associative (the result may be a valid subscript). If the expression

a :: b :: c

would evaluate from the left to the right (a::b :: c) the result of a::b would probably not be a valid address, since it is limited to eight bits. In this case, however, the following subscript would reference the position c of a non-vector -- which is certainly not the desired result. If the expression evaluates from the right to the left, on the other hand (a :: b::c) the subexpression b::c is evaluated first and will probably return a valid subscript. This subscript is then applied to the (also valid) vector a.

Finally, the :: operator differs from [] in the point that there is no righthand delimiter for the subscript expression. Therefore, the righthand side of :: is always a single factor and expressions like

a::b+c

actually evaluate to

(a::b)+c

since :: has the highest precedence. To address the b+c'th byte in the array a, the subscript must be parenthesized:

a::(b+c)

Note that in either form of the subscript operator, it is impossible to change the associativity using parentheses.

2.6.2 Unary Operators

All unary operators have a high precedence and bind to single factors. Unless explicitly specified using parentheses, they never affect subexpressions containing other operators except for postfix operators which have an even higher precedence. The suffix operators must bind stronger than the prefix operators, because this order leads to much more sensible semantics. For example

-P(a,b)

means `negate the result of applying P to a and b' and

~v[j]

means `evaluate to the inverse value of the j'th member of v'. If the order of precedence would be reverse, the meaning of the first example would be `apply whatever is at the negative address of P to a and b' and the second one would mean `evaluate to the j'th member of the vector located at the address expressed by the inverse value of v'. Unless someone convinces me of the advantages of the second variant, I will stick to the first version: postfix operators are evaluated before prefix operators.

Altogether, there are four prefix operators. The minus sign - (which exists as a binary operator, too) evaluates to the negative value of its operand. Like in math expressions, any even number of minus signs has no effect. The unary minus sign is distinguished from the binary '-' by its context. When the sign occurs between two operands, it is binary. If it occurs at the place of a factor, it is unary and the factor itself follows.

The tilde operator ~ results in the value of its operand with all bits inverted. Since inverting a bit twice always yields the original value, even numbers of ~-operators have no effect, either.

The backslash \ represents the logical NOT (while ~ represents the bitwise NOT). This operator evaluates to true (-1), if its operand is false (0) and vice versa. Only the value zero represents `false' in T3X and all non-zero values represent logical truth. The normal form of the `true' value is -1. Two (or any other even number of) subsequent logical NOT operators may be used to create the normal form of a truth value.

The address operator @ evaluates to the address of its operand. Therefore, it may be applied only to symbol names. The addresses of constants, structure members, and classes may not be computed using @, because such entities have no addresses. Since the subscript operators bind stronger than the address operator, @ can be used to compute addresses of vector and structure members, and even the addresses of members of nested tables:

@v[i][j]

computes the address of the j'th member of the embedded vector v[i]. Of course, the address operator might be combined with byte subscipts, as well:

@s::i

yields the address of the i'th byte in s.

2.6.3 Term Operators

The operation A*B evaluates to the product of A and B. If AB does not fit in a machine word, the result is undefined.

A/B results in the integral part of the quotient of A and B. The result is undefined, if B is zero.

A MOD B evaluates to the difference between A and A./B.*B where A./B is an unsigned integer division and .* is an unsigned multiplication. Therefore, A MOD B is the division remainder of A./B. Like /, MOD leads to an undefined result, if B=0.

All term operators respect the signs of both of their operands. Two equally signed operands yield a positive result and operands with different signs lead to a negative result.

As of version T3XR5, the language also provides some modified operators which work on absolute values. Modified versions of the multiplication and division operator exist. Like all modified operators, they are prefixed with a dot `.':

The operation A.*B evaluates to the product of the unsigned values .A and .B. A./B results in the integral part of the quotient of .A and .B.
The notation .X is used to denote the unsigned value of X.

2.6.4 Sum Operators

A+B evaluates to the sum of A and B and A-B evaluates to their difference.

2.6.5 Bit Operators

In T3X, all bit operations have the same precedence. The grouping of such operations must be specified explicitly using parentheses. Otherwise, the evaluation is performed from the left to the right, as usual.

The operation A&B in results the bitwise AND of A and B. Each bit is the result of computing the logical product of one bit in A with the bit at the same position in B.

A|B yields the result of performing a bitwise OR on A and B. Each bit in the result is a logical sum of a bit in A and the bit at the same position in B.

A^B performs a bitwise exclusive OR (XOR). In this case, the computation of a single bit is done by combining bits at the same positions in A and B using a logical negative equivalence operation.

See the following table for the results of applying logical operations to pairs of bits.

A B AND,* OR,+ XOR,\=
0 0 0 0 0
0 1 0 1 1
1 0 0 1 1
1 1 1 1 0

A<<B evaluates to the value of A with all bits shifted to the left by B positions. This is the same as an unsigned multiplication with the B'th power of 2:

             b
a<<b = a .* 2

After such an operation, the sign of the result must be considered undefined. This is not relevant, of course, if A is used as a bit field where each bit represents a binary state.

A>>B yields the result of shifting the bits in A to the right by B positions. This is basically equal to the computation of the quotient

      b
a ./ 2

Like in left-shift operations, the sign must be regarded as undefined after right-shift operations.

Technically speaking, one might say that the shift operators in T3X perform bitwise instead of arithmetic shift operations. This implementation has been chosen, because it is in some cases hard to manipulate bit fields using arithmetic shift operators.

2.6.6 Relational Operators

Relational operators are used to compare two operands. The relation between the operands is expressed as a truth value: all these operators return true, if their meaning applies to their operands and false otherwise. The following relational operations exist (.X denotes the unsigned value of X):

Operator Description
A < B A is less than B
A > B A is greater than B
A <= B A is less than or equal to B
A >= B A is greater than or equal to B
A .< B .A is less than .B
A .> B .A is greater than .B
A .<= B .A is less than or equal to .B
A .>= B .A is greater than or equal to .B
A = B A is equal to B
A \= B A is not equal to B

Note: the operators expressing equivalence (=, \=) have a lower precedence than operators expressing ordering (> , <, >=, <=, .<, .>, .<=, .>=). For example,

A < B = C < D

is equal to

(A<B) = (C<D)

Consequently, the equation sign may be interpreted as `logical equivalence' when used between comparisons: the above expression evaluates to true, if either

(A<B) AND (C<D)

or

\(A<B) AND \(C<D)

applies. Since the inequation operator \= has the same precedence as =, it may be used as a negative logical equivalence operator (aka an Exclusive OR):

A<0 \= B<0

becomes true, if either A or B is negative. If the truth values of the comparisons A<0 and B<0 are equal, the expression yields the result `false'.

Notice again that any value may be considered a truth value in T3X. Everything but the value zero is interpreted as `truth', and only 0 may be used to express the `false' value.

2.6.7 Conjunctions and Disjunctions

The operators A/\B and A\/B represent the logical conjunction (AND) and disjunction (OR). Generally, the expression

A /\ B

evaluates to some `true' value, only if A AND B evaluate to a `true' value and

A \/ B

yields a `true' result if either A OR B (or both of them) evaluate to a `true' value.

More specifically, /\ and \/ are so-called short circuit operators. Since the expression A/\B can lead to a `true' result only if all its operands are `true', there is no actual need to evaluate B, if A already has evaluated to a `false' value. Therefore, the second operand of a conjunction never will be evaluated by a T3X program, if the first one already is false. The result will be zero in this case. If, on the other hand, the first value is `true', the result of the entire conjunctional expression will be the value of the second operand. Therefore, the result of

A /\ B

can be specified more precisely as follows:

if A=0, the result is 0.
if A\=0, the result is B.

The meaning is just a more general form of the logical AND.

Similiarly, the expression A\/B can never become `false', if A already has been found out to be true. Therefore, no T3X program will ever evaluate B in such a case, and the result of the disjunction

A \/ B

can be explained more precisely as

A, if A\=0.
B, if A=0.

Like in ordinary algebra, conjunctions bind stronger than disjunctions:

A /\ B \/ C /\ D

equals

(A/\B) \/ (C/\D)

In chains of equal logical operations, the order of evaluation is from the left to the right, as for all binary operators. This means that chains of conjunctions will be evaluated up to the first `false' occurrence and chains of disjunctions will be processed up to the first `true' occurrence. In either case, the result of the entire chain is the value of the last processed operand.

There exists a connection between the logical operators and conditional statements: Because of the short circuit evaluation, logical operators may be used to implement some flow control within expressions. The expression

A /\ B()

has almost the same meaning as

IF (A) B();

The only difference is that the expression yields a value, while the statement only has a side effect. Likewise, the expression

A \/ B()

has a meaning similar to

IF (\A) B();

The IF-statement will be explained in a later section.

2.6.8 Conditional Expressions

The ternary conditional operator has the least precedence. Therefore, it may be used to combine any kind of expressions without using parentheses. The following expression, for example, implements the minimum function:

a<b -> a : b

Since the operator has three operands, it consists of two parts: -> and :. The meaning of the conditional operator is as follows: Given the expression

A-> B: C

if the operand A (the condition) evaluates to some `true' value, B will be evaluated and otherwise, C will be evaluated. If A is evaluated, B will not be evaluated and vice versa. The result of the expression is equal to the value of the last evaluated operand.

Like the logical operators /\ and \/, the conditional operator has a connection to conditional statements:

A-> B(): C()

is equivalent to

IE (A) B(); ELSE C();

except for the fact, of course, that the expression has a value, while the statement only has a side effect. (IE means If/Else and introduces a conditional statement with an alternative). The IE-statement will be discussed in a later section.

2.6.9 Constant Expressions

Constant expressions are used wherever a value must be known at compile time. Only a limited set of operators is allowed in constant expressions and the order of evaluation is always from the left to the right. There are no precedence or associativity rules.

L+1*10

evaluates to (L+1)*10 and not to L+(1*10), like it would in ordinary expressions. The resons for this decision were 1) ease of implementation and 2) the fact that most conditional expressions contain only a single operator or none at all.

The following operators are recognized inside of constant expressions:

2.6.10 Order of Evaluation

While the associativity and precedence rules specify, which operation is to be performed first, the order of evaluation determins, what factor is to be evaluated first. For example, in the expression

A * B

A may be evaluated before B or vice versa. The order of evaluation becomes important, if both, A and B have side effects. If A had the side effect of printing 'A' on the terminal screen and B would print 'B', the terminal output of above expression may be "AB" as well as "BA".

In fact, the order of evaluation is generally undefined in T3X, but there are exceptions. One exception is formed by operators whose semantics are defined by the order of evaluation, like the short circuit boolean operators and conditional operators. For example, the order of evaluation of the expression

A() /\ B()

is exactly defined. The lefthand side is always evaluated first and the righthand side is only evaluated, if the value of the lefthand side is non-zero. Given the above side effects, this expression would print "AB", if A() is non-zero and "A", if it is zero. It would under no circumstances print "BA".

The other exception is the order of evaluation of procedure call arguments and nested procedure calls. Procedure call arguments are guaranteed to be evalauted from the left to the right and nested calls are evaluated inside-out. In combination, they are evaluated inside-out and then left-right. The expression

P( A(), B() )

would be invariably evaluated in the order A, B, P. For example, it is safe to format a string in a procedure call argument in T3X and compute the length of the formatted string in a following argument. The statement of the form

t.write(1, str.format(buf, packed"%S/.txrc", [(path)]), str.length(buf));

would correctly print the string formatted in str.format.

Notice: Never rely on any ordering not explicitly specified in this subsection! Even if precedence rules may suggest a specific order of evaluation, it may in fact be different and, even worse, it may change without breaking any rules, for example when turning on/off optimizations or using a different compiler version.

2.7 Statements

Statements are the basic building stones of T3X programs. While expressions just have a value, statements are used to `tell the program to do something'. Therefore, T3X is a so-called imperative language. Each program is a list of `commands' which is executed in sequence. Each command is also called a statement in the terminology of imperative programming.

There are different kinds of statements: assignments, procedure calls, conditional statements, loop statements, branch statements, and compound statements. The assignment is an essential part of every imperative language. It is frequently even used to characterize the imperative approach. Compound statemements do not have an own meaning, but they are used to group statements to form the bodies of loops, conditionals, and procedures. All other statement types serve the control of the program's flow.

In T3X, all statements have to be terminated with a semicolon. This means that a semicolon must follow every statement in a program, except for compound statements which are delimited by the keywords DO and END. In other procedural languages (like BCPL and Pascal), statements are separated rather than terminated. In such languages, a delimiter has to be used only, if two statements are written in sequence -- there may not be any delimiter after the last statement. The separation rules in some languages are rather complex and the saving in delimiters is usually not worth the extra expense of having to remember these rules. Therefore, the simplest form of combining statements has been chosen in T3X: Each simple (non-compound) statement has to be terminated.

2.7.1 Assignments

An assignment is used to transfer the value of an expression to a specific storage location. For example, the statement

A := B;

copies the value of B to A. After the assignment, both variables will have the same value. The previous value of B gets thereby lost.

The righthand side of an assigment may be any valid expression as described in the previous section. The lefthand side is restricted to a subset of expressions which is frequently referred to as lvalues (lefthand side values). In T3X, each lvalue may be one of the following:

(Of course, vector members and structure members are basically the same.) Assignments to vector members are in no way limited; addressing elements of multiply nested vectors is perfectly legal. In the section about factors, the evaluation of variables on lefthand and righthand sides of assignments has been explained: On righthand sides, variables evaluate to their values and on lefthand sides, they evaluate to their addresses. The assignment operator := first evaluates the expression on its left side and remembers the resulting address. Then, it evaluates the expression to its right and stores the result at the memorized address.

A generalization of the evaluation of lefthand sides is the following: All but the last reference on a lefthand side of an assignment evaluates to its value. Only the last reference evaluates to its address. Some examples:

A := B;

The symbol A references a specific storage location. Since it is the only reference in the lvalue, it evaluates to its address. In the statement

A[i] := B;

A is not the last reference and hence, it yields its value (which is its address in case A is a vector). The operation [i] references the i'th member of A. Since it is the last reference on the left side, it evaluates to the address of A[i] instead of its value. Consequently, the following assignment operator stores B at this address: the i'th member of A. The same is valid for the access of vector elements at any nesting level. The statement

A[i1][i2][i3][i4] := B;

for example, stores B in the i4'th element of A[i1][i2][i3].

Accessing byte vectors works in the same way:

A::i := B;

stores the least significant eight bits of B in the i'th byte of A. Since :: associates to the right, the last evaluated reference is the leftmost one in chains of byte operators like

A::B::i := C;

Because B::i will be evaluated first in this example, it will yield its value. Then, the address of A::(B::i) is computed. Since no more references are following after A::, the (least significant eight bits of the) value of C will be stored in the (B::i)'th byte of A.

Note: Although the assignment symbol := looks like an operator (and is also frequently referred to as such), it may not be used inside of expressions. The occurrence of this operator automatically turns an expression into a statement.

2.7.2 Procedure Calls

The application of a procedure may form a complete statement:

memfill(a, 'X', 10);

In this case, the return value of the called procedure will be discarded and only the side effects of the procedure will actually take effect. The side effect of the above statement, for example, is to fill the first 10 characters of the vector a with the character 'X'.

Each procedure -- no matter whether it returns a specific value or not -- may be used in a standalone procedure call. Like in procedure calls in expression contexts, each argument may be any valid expression. For details, see the the section on factors in an earlier part of this manual.

2.7.3 Conditional Statements

There are two forms of the conditional statement. The first one is the IF statement which is avaliable in most procedural languages. Its general syntax is

IF (expression) statement

where expression may be any expression and statement may be any statement. The IF statement itself does not have to be terminated with a semicolon, since its body, which is a statement, too, already supplies the terminating semicolon. The statement which forms the body of the IF statement will be executed, only if expression evaluates to some `true' (non-zero) value. The following statement turns a into its absolute value:

IF (a < 0) a := -a;

If A is less than zero, then a will be filled with -a, thereby changing its sign. Since the body a := -a is executed only, if a < 0 applies, this conditional statement always leaves a positive value in a.

Notice that the semicolon in the above example belongs to the assignment.

The second form of the conditional statement is the conditional with an alternative:

IE (expression) statement-T ELSE statement-F

Like in IF statements, any valid expression/statement may be used in the places of expression and statement-{T,F}.

The meaning of the IE statement is the same as of IF statements, as long as the expression becomes `true'. In this case, the first statement (statement-T) will be executed. If the expression evaluates to `false', however, statement-F will be executed, while an IF statement without an alternative would not have any effect in this case. Therefore

IE (expr) stmt ELSE ;

is equal to

IF (expr) stmt

IE is an abbreviation for If/Else. In most languages, the IF statement may or may not have an alternative. In T3X, there is a separate type of statement for each version. The reason for this choice is the `dangling else' problem which cannot arise when these statement types are separated. If no further information is supplied, the following program written in a language which allows optional alternatives would be ambiguous:

IF (condition1)
        IF (condition2)
                statement1
ELSE
        statement2

The problem is to decide which IF the ELSE branch belongs to: is it the alternative of IF (condition1) or IF (condition2)? The indentation in this example suggests that it belongs to the first IF. In fact, however, most languages will bind it to the most recently opened IF -- the second one in this example. In T3X, such an ambiguity does not exist:

IE (condition1)
        IF (condition2) statement1
ELSE
        statement2

Since the IF statement cannot have an alternative, the ELSE branch must belong to IE (condition1).

2.7.4 Loop Statements

There are two kinds of loops: `while' loops and `for' loops which represent two classes of problems: those which are computable by algorithms with a known upper limit of iterations (FOR-computable or primitive recursive functions) and problems which cannot be computed by algorithms with a fixed number of iterations (WHILE-computable or general recursive functions). Since the FOR-computable functions are a subset of the WHILE-computable ones, FOR statements may be regarded as a special case of WHILE statements and in fact, it is possible to express a FOR loop using WHILE, but not vice versa. (In T3X, it is possible, but theoretically, it's not.)

There is a third kind of loop in many other languages, the repeating loop, but it turns ouf to be a special case of the WHILE loop. Repeating loops are not very frequently needed and if they are, they can easily be constructed using WHILE and IF in T3X.

The WHILE loop has the following general form:

WHILE (expression) statement

where expression may be any expression and statement may be any statement. The body consisting of the statement will be executed while the test expression in parentheses evaluates to some `true' value. Hence the name of this loop. If the expression becomes `false' before the statement has been passed the first time, it will never be executed. However, a loop which tests its exit condition at the end of the statement may be constructed using WHILE, IF, and a compound statement (which will be explained later in this chapter):

WHILE (-1) DO    ! -1 = true
        statement
        IF (\condition) LEAVE;
END

In this case, statement will be executed at least once, because the exit condition -1 is a `true' constant. In the following IF statement, the loop will be left if condition does not apply. LEAVE is used to branch out of the loop. It will be explained later, too.

The FOR loop exists in two forms: an explicit form and a short form. The explicit form looks as follows:

FOR (var=start, limit, step) statement

Var is an atomic variable which must have been declared before. Unlike in BCPL, it will not be declared implicitly by the FOR statement. Start and limit are expressions and step is a constant expression. The FOR loop works this way: First, var is initialized with the value of start. Then, var is compared against limit. If either the condition

var<limit /\ step>=0

or

var>limit /\ step<0

holds, the loop is passed. Otherwise the loop is left and statement will not be executed any more. If either of the above consitions holds, the loop statement is executed, step is added to var, and the loop will be repeated from the point where the exit condition is checked. Like in a WHILE loop, the statement will never be executed, if the exit condition already becomes true (which is the case, if both of the above conditions become false) at the first time it is checked.

The following examples prints the numbers from 0 to 9 using the procedure print which is defined in the first example. (Print uses some routines of the classes t3x and string which will be explained in a following chapter.)

print(n) DO VAR b::10;
        t.write(1, str.format(b, packed"%D\N", [(n)]),
                str.length(b));
END

FOR (i=0, 10, 1) print(i);

This example counts down from 9 to 0:

FOR (i=9, -1, -1) print(i);

Particularly notice the limits of the FOR loops in these examples. They always specify the first value which will not be applied to the statement. Another way to write the second example would be the following one, where the FOR loop has been replaced by a WHILE loop:

i := 9; WHILE (i > -1) DO
        print(i);
        i := i-1;
END

The meaning of this program fragment is completely equal to the one employing a FOR loop, but the syntax of the FOR statement is more compact and expresses the purpose of the statement clearer. Hence, the FOR statement has been included in the T3X language.

The step value is optional in FOR statements. In the short form of the statement, it is omitted. If only two operands are specified with FOR, the step width defaults to one. Therefore, the statements

FOR (j=0, 100, 1) p(i);

and

FOR (j=0, 100) p(i);

have exactly the same meaning.

2.7.5 Branch Statements

A `branch' passes control to a specific point in a program. Typical destinations for branch commands are the beginnings or the ends of loops or the ends of procedures or programs. There is no branch command with a freely definable destination like Goto in BCPL.

The LEAVE comman causes the immediate termination of the innermost WHILE or FOR loop. There are no operands to LEAVE.

The following code compares the characters in two strings A and B. The loop is left at the first position where the strings differ, but in any case after 100 steps:

FOR (i=0, 100) IF (a[i] \= b[i]) LEAVE;

The loop is set up for 100 passes and the LEAVE command makes the loop terminate, if a mismatch has been found.

The LOOP command transfers control to the beginning of the innermost loop. Like LEAVE, it has no operands. If LOOP is used inside of a FOR loop, it branches to the increment part where the value of the index variable is modified. In WHILE loops, it branches directly to the point where the exit condition is checked.

To leave a procedure, a RETURN statement may be used. It has the general forms

RETURN expression;

and

RETURN ;

The statement evaluates the specified expression, if any, and prepares its value for passing it back to the calling procedure. Then, it performs a branch to the end of the procedure where local storage is released and the procedure is left. The value received by the calling procedure is the value of expression:

P(x) RETURN x*x;

Q() DO VAR y;
        y := P(5);
END

In this short example program, 5 is passed as an argument to the procedure P. The procedure computes the square of its argument and returns it to Q where the result will be stord in y.

When no expression is specified after RETURN, a zero value will be assumed. Therefore, RETURN; is a short form of RETURN 0;

All the above branch statements take care of locally allocated storage. If local symbols are defined in the bodies of loops, for example, LOOP and LEAVE will release this storage before branching to their respective destinations. This allows the use of these commands in any loop context, even if local symbols are present.

The HALT statement with the general form

HALT constant-expression ;

performs a branch to the end of the entire program, thereby terminating it. If necessary, the command cleans up the runtime environment of the program so that it can terminate gracefully. The value of the specified constant expression is returned to the calling process. Only the least significant eight bits are guaranteed to be returned to the caller.

The argument of HALT may be omitted. In this case, zero will be delivered to the caller.

2.7.6 Compound Statements

A compound statement (sometimes also called a block statement) is a group of statements which is treated like a single statement under some aspects. For example, a compound statement may occur at any place where an ordinary statement is expected, like in

IF (expression) statement

In such situations, a compound statement can be used to extend the scope of the conditional statement so that it is applied to a group of statements instead of a single statement:

IF (a < '0' \/ a > '9') DO VAR b::3;
        t.write(1, "Not a valid digit. Code=", 25);
        t.write(1, str.format(b, packed"%X\N", [(a & 0xff)]),
                str.length(b));
END

In this example, both t.write() messages will be sent only, if the IF-condition applies. (Sending a message is basically the same as calling a procedure. The concept will be explained in detail in the chapter about object oriented T3X.) The keywords DO and END are used to delimit statement blocks. There is no terminating semicolon after a compound statement. The line

DO p(); q(); END ;

would be recognized as a compound statement containing the procedure calls P() and Q() and an empty statement consisting only of a single semicolon.

In T3X, compound statements are ordinary statements and they may occur at any place where a statement is expected. Even statements like

DO DO END DO END END

are perfectly valid. The use of compound statements in sequences becomes clear in the next sections where the allocation of local storage in compound statements is explained.

2.7.7 Local Symbols

Besides the grouping of commands, compound statements provide a mechanism for the definition of local symbols and the allocation of dynamic storage. Declaration statements already have been explained in a previous section. All data objects which can be created in T3X also may be declared locally inside of compound statements by placing their declarations at the beginning of the statement block. Any number of declarations will be accepted after the keyword DO, which introduces the block. The declaration statements themselves do not change in this case. Only the positioning inside of a statement block makes the declared symbols local to that block. The statement

DO VAR i; FOR (i=0, 10) p(i); END

for example, applies the procedure p to the sequence 0...9. The index variable is declared inside of a compound statement which also contains the FOR loop which generates the sequence. The variable i does not exist before the compound statement is entered. It will be created automatically at the point of its declaration and it will cease to exist at the end of the block it has been declared in. Therefore, variables which are local to compound statements are sometimes also called automatic variables.

Besides atomic variables and vectors, structures, constants, and objects may be declared locally, too. Unlike BCPL, T3X does not support nested procedure definitions. In case of atomic variables and vectors, the storage required by the variables is allocated when the symbol becomes valid and released when the variable is destroyed again. In most environments, automatic storage will be allocated on the runtime stack. The main purpose of local symbols is the definition of local variables in procedures.

To illustrate another application of local storage allocation, imagine the following situation:

P() DO
        VAR     big_V[LARGE_1];
        VAR     big_W[LARGE_2]; ! Too big

        task1(big_V);
        task2(big_W);
END

In this procedure, two tasks requiring large amounts of storage shall be run sequentially, but not enough memory for both arrays is available. On solution would be the creation of two procedures where each one creates local storage only for one task. Another one would be to share the vector, but both solutions only work at the cost of readablility and maintainability. T3X provides another solution, since the compiler guarantees that local storage is allocated exactly at the point of its declaration and released immediately at the point of the destruction of the associated symbol:

P() DO
        DO VAR big_V[LARGE_1];
                task1(big_V);
        END ! big_V gets released here

        DO VAR big_W[LARGE_2];
                task2(big_W);
        END ! big_W gets released here
END

Since compound statements may be nested, naming conflicts may occur in many languages, like the following example (in BCPL) illustrates:

${ LET I
        I := 123
        ${ LET I
                I := 456
        $}
        WRITEF("%N*N", I);
$}

The variable I which is defined in the outer compound statement is redefined in the inner block. Inside of the inner block, the variable I is assigned the value 456. Clearly, the assignment I := 123 in the outer block references the variable defined in the outer block, but which one is referenced in the inner scope? BCPL -- like most other procedural languages -- resolves this ambiguity by always giving preference to the innermost definition. Therefore, the example program fragment would print 123. When this method is used, the symbol I defined in the outer scope becomes inaccessible in the embedded scope. This effect is called shadowing: The inner definition `shadows' the outer one which thereby becomes temporarily invisible to the compiler.

T3X uses more strict scoping rules than most other languages: Symbols generally may not be redefined in T3X programs. This also applies to global symbols (symbols which have been declared at the top level -- outside of procedure definitions, classes, or statement blocks). This way, shadowing can never happen. The flexibility of local symbols remains, though, since names can be reused as soon as a local data object is getting destroyed:

F(x,y) DO VAR i, j;
        ! ...
END

G(x,y) DO VAR i, j;
        ! ...
END

As shown in this example, symbol names may be reused in procedure definitions (for formal argument names) as well as in subsequent compound statements. Since the variables i and j will be destroyed at the end of the compound statement forming the body of F, they can be reused in G. The same is valid for the argument names x and y.

The following example shows some local and global symbols and their scopes.

+++ VAR     GX, GY;
 |
+++ CLASS A()
 |          STRUCT C=R,G,B;                        +++
 |                                                  |
 |          P(x, y) DO                        +++  +++
 |                  VAR x1, x2;               +++   |
 |          END                               ---   |
 |  END                                            ---
 |
+++ P(x, y) DO VAR x1, y1;                    +++
 |                                             |
 |          STRUCT  PT=PX,PY;                 +++
 |                                             |
 |          DO VAR i, j;                 +++   |
 |                  DO VAR x2, y2;  +++   |    |
 |                  END             ---   |    |
 |                                        |    |
 |                  DO VAR x2, y2;  +++   |    |
 |                  END             ---   |    |
 |          END                          ---   |
 |                                             |
 |          DO CONST t=%1, f=~t;         +++   |
 |                  DO VAR x2, y2;  +++   |    |
 |                  END             ---   |    |
 |          END                          ---   |
 |  END                                       ---
Fig.2 Scopes

Like all other symbols, the global variables GX and GY are valid from the point of their declaration, but unlike locally declared names, they remain existant up to the end of the program. Their scope is the entire program (beginning at their declaration). The scopes of all symbols in the example are illustrated using vertical bars. Plus signs indicate the point where a symbol name becomes valid and its storage is allocated, and minus signs mark the point of its destruction.

Especially notice that the names x2 and y2 which are used in three different scopes denote three different variables. A value stored in x1 within the first scope, for example, cannot be retrieved in the second or the third scope from x1, because the two names reference different locations. The variable which is created at the beginning of the first scope containing x1 is deleted at the end of this scope and the value stored in that variable gets lost. Assignments to local variables remain valid only between the according +++ and --- indicators.

All symbols which are defined in a so-called class context (between the keywords CLASS and the matching scope terminator END) are valid only inside of this context. Both, the structure C and the procedure P defined in the class A are only valid inside of the class context of A. There exists no conflict between A.P (the method P of A and the procedure P which is defined at the top level. Class context will be discussed in detail in the section on object oriented T3X.) later in this document.

2.7.8 Empty Statements

There are two forms of the empty statement (aka null statement) in T3X. The first form is the single semicolon

;

Compound statements may be empty, too:

DO END

Both null statements have absolutely no effect. Their only purpose is to fill a gap where a statement is required, but nothing is to do. They are useful to negate the meanings of complex conditions, for example. Instead of negating the condition at the cost of making it harder to understand, one might turn

IF (complex-condition) statement

into

IE (complex-condition)
        ;
ELSE
        statement

2.8 Procedures

Each procedure may be considered a separate small program. It communicates with other procedures using parameters and return values or through global variables. Each procedure has access to all global data objects which have been declared before it. Generally, it is considered `good style' in procedural languages to keep procedures self-contained and use global storage as little as possible, but when an data has to be shared between a big number of different procedures, the use of top-level definitions is very common.

The definition of a procedure has only one single form in T3X. Since there is no support for nested routines, all procedure declarations and definitions must occur at the top level -- the space between the other global declarations. Procedures declared in class contexts are called methods. They will be explained later.

The only form of the procedure definition is

P(a1, ... aN) statement

where P is the name of the procedure, a1...aN are the names of the formal arguments, and statement is the body of the procedure -- the part which describes its meaning.

The procedure name may be any valid symbol and it is declared in the global context. Therefore, procedure names never may be reused. (An advantage of T3X's strict scoping rules is that procedures cannot get shadowed.) The arguments a1...aN are local to the procedure. Their names will cease to exist after the statement has been accepted. Hence, they may be reused after the procedure definition, but not inside of it. The parentheses around the argument list must always be specified, even if the list is empty:

Q() statement

The number of arguments specified in the definition of a procedure determines the type of the procedure. The type of a procedure is notated as a single number which represents the number of the routine's arguments. In T3X, the argument counts of all procedure calls will be checked. The compiler will not allow calls with a wrong number of parameters. This has to be done because of T3X's calling conventions: Parameters are passed in reverse order and therefore, each procedure relies on a correct number of arguments. BCPL uses a totally different approach in which it is easy to compensate for missing or superflous procedure parameters. The only real advantage of this approach, however, is the possibility of defining real variadic procedures (procedures with a variable number of arguments). The transport of variable argument lists also can be realized in T3X, but using a different mechanism which will be explained later.

When a procedure is called, it may receive data through its arguments. This works in the following way. Given a procedure

P(x, a, b, c) RETURN a*x*x + b*x + c;

and a procedure call

y := P(n, i, j, k);

the caller places the values of the actual arguments n, i, j, and k in a temporary storage location (usually on the runtime stack), saves the address of the following operation (the assignment in this case) and then transfers control to the procedure P. In P, the formal arguments x, a, b, and c reference storage locations which exactly match the temporary locations of the values passed to the routine, so that x=n, a=i, b=j, and c=k.

The procedure P computes a*x*x+b*x+c and returns it to the caller. Each procedure returns automatically, if its body has been processed completely or if an explicit RETURN statement is executed. In the above example, both happens at the same time. It is not unusual to specify a RETURN statement at the end of a procedure, since only RETURN may pass an explicit value back to the caller. Procedures which do not return through RETURN (or a RETURN; statement without a value) have an implicit zero return value. In the example, however, the value of P is explicitly specified. After passing control back to the caller, the assignment takes place, and the result of the procedure call is stored in y. Between the procedure return and the assignment, the temporary storage where the actual arguments were held is released again.

The most frequently used form of the procedure has a body consisting of a compound statement:

fib(n) DO
        VAR     f, i, j, k;

        f := 1;
        j := 1;
        FOR (i=1, n) DO
                k := f;
                f := j;
                j := j+k;
        END
        RETURN f;
END

Note that the variables declared at the beginning of the procedure

VAR f, i, j, k;

belong to the compound statement rather than to the procedure. Like in conditional statements and loops, the statement block is used to extend the scope of the procedure: not only a single statement, but a group of statements forms the body of the routine.

2.8.1 Recursive Procedures

It is perfectly safe for a procedure to call itself. Since the declaration of a procedure takes place while parsing its head (consisting of its name and its argument list), the declaration is already valid when the compiler processes the body. Therefore, the procedure may recurse into itself:

fac(n) RETURN n=0-> 1: fac(n-1)*n;

This small example computes n!. For the trivial case n=0, it simply returns 1. To compute n! for n>0, it first computes (n-1)! and then multiplies it with n. To compute the factorial of n-1, it applies itself. Since the argument of the recursive call is decremented by one at each level of recursion, it will finally reach 0 and the procedure will start returning.

Recursion is safe in T3X, because local variables (which include formal arguments) are created freshly each time the according declaration is passed. Therefore, the symbol n in the above example denotes different variables at each level of recursion. To see how recursion works, the following modified example of the factorial function is recommended:

fac(n) DO VAR f, b::10;
        f := n=0-> 1: fac(n-1)*n;
        t.write(1, str.format(b, packed"%D\N", [(f)]),
                str.length(b));
        RETURN f;
END

Of course, the usual restrictions concerning the use of global memory and other shared resources in recursive procedures apply in T3X, as well.

2.8.2 Mutually Recursive Procedures

Recursive procedures which depend on each other are called `mutually recursive'. Such a configuration introduces the following problem: Given the procedures

A() DO !...
        B();
END

B() DO !...
        A();
END

which depend on each others, it does not matter which one is declared first -- one will always be inaccessible from within the other. In the above configuration, B is undefined in A because it will be declared after A. When swapping the definitions, A will become undefined in B.

The problem is solved by introducing procedure declarations which may occur before the matching definition. A declaration makes a symbol known to the compiler, but does not associate any meaning with the declared symbol. In case of procedures, the meaning may be `subsequently delivered' in a later definition. To declare a procedure, the DECL statement is used:

DECL name(type);

Analogous to VAR statements, any number of comma-separated declarations may be included in a single DECL statement. Name is the name of the procedure to declare and type is a constant expression specifying the number of formal arguments of that procedure. This value is required to type check forward calls to the procedure. The number of formal arguments in a subsequent definition must exactly match the type specified in the declaration. Otherwise, a redefinition error will be signalled.

DECL reserves the given names for later procedure definitions. Therefore, each of these names may be reused only in one subsequent procedure definition. Declaring a procedure without defining it later is an error, since this may leave forward references to the declared procedure unresolved.

To correct the above program containing the mutually recursive procedures A and B, the declaration

DECL B(0);

has to be inserted before the definition of A. Like procedure definitions, DECL statements are allowed only at the top level and in class contexts, but not inside of local scopes.

2.8.3 Variadic Procedures

All procedures have fixed numbers of arguments in T3X. It is possible, however, to pass a variable number of arguments to a procedure using a vector. The following simple example computes the average of n values stored in the vector v:

average(n, v) DO
        VAR     i, t;

        t := 0;
        FOR (i=0, n) t := t+v[i];
        RETURN t/n;
END

Since vectors are first-class objects in T3X, it is possible to inline vectors in procedure applications, thereby forming an elegant way of passing constant vectors with variable sizes to a procedure:

average(5, [ 2, 3, 5, 7, 11 ]);
average(3, [ 123, 456, 789 ]);

Since release 4 of the T3X language, tables may contain embedded expressions. Therefore, dynamically computed values may be included in tables as well, making the use of tables for variadic procedure calls even more flexible. Given a T3X implementation of a subset of of the variadic standard C library procedure printf() and the fib() procedure which has been defined earlier in this chapter, it would be possible to write the following program which prints the line

fib(n) = m

for each n=1...10 and m=fib(n):

DO VAR i;
        FOR (i=1, 10)
                printf("fib(%d) = %d\n", [ (i), (fib(i)) ]);
END

printf() replaces each %d with the readable representation of the value of one of the arguments following the string. Each time, a %d is processed, the procedure advances to the next argument. The C version uses a variable number of arguments while the T3X version uses a vector to transport the arguments.
BTW: printf() uses the number of %-patterns to determine the number of arguments passed to it. The programmer is responsible for supplying a the correct number of them.

2.8.4 Interface Procedures

Note: INTERFACE definitions are obsolete and should no longer be used in T3X-R6-0 and later releases. They have been superseded by the R6 runtime classes. The newer compilers still accept interface statements for compatibility reasons, but support is limited to the T3X-R5 standard procedures. See the description of the Release 5 Compatibility class for details.

An INTERFACE statement is used to access routines which are not part of the program which contains the statement. The original purpose of INTERFACE statements is to provide an extensible way to access routines provided by the runtime environment -- either the Tcode interpreter, if a bytecode program is run or the runtime library, if the program has been compiled to native code. The general syntax of this statement is

INTERFACE name(type) = slot;

Any number of interfaces may be specfied in a comma-separated list and the part containing ' = slot' is optional. The meaning of the first part of the interface description (containing 'name(type)') is exactly equal to a DECL statement: it declares a procedure called name with type formal arguments. The only difference to a procedure declaration is the type of the declared object. While DECL declares a user-level procedure, INTERFACE declares an interface procedure.

Interface procedures are called in a way which slightly differs from ordinary procedure calls. (This difference is totally transparent to the programmer, though.) While a user-level procedure simply has an address to jump to, an interface procedure is part of another program which provides the necessary runtime support for the T3X program in the form of procedures. Since these procedures are not defined in the same source program, an `interface' for accessing them has to be created. The internal form of this interface is a jump table which simply contains the addresses of the available runtime support procedures.

When a program starts up, there are some predefined interfaces which will be created automatically by the RT interpreter or library. These interfaces allow access to the procedures which were `built into' the original T3. They provide some T3 compatibility. Additional procedures require an explit INTERFACE declaration. For example, T3X (R5) provides a standard routine for copying memory regions. To use it, it must be declared first, using the statement

INTERFACE memcopy(3) = 15;

The routine has three arguments and occupies slot number 15 in the above described jump table. The slot number is assigned by the runtime library -- it cannot be freely chosen by the programmer. Memcopy() is always located at slot 15. The INTERFACE statement declares an interface to a predefined slot. It assigns slot numbers to names and not to procedures!

When no slot number is specified, the compiler automatically advances to the next free slot number. Therefore, it is sufficient to specify an explicit slot number with the first declaration, if a set of interfaces with subsequent slot numbers has to be declared. For example,

INTERFACE readpacked(3) = 11, writepacked(3), reposition(4);

will create the interface procedure readpacked() to reference slot 11, writepacked() to use slot 12, and reposition() to access slot 13.

Each slot number may be used only by a single procedure. The attempt to use the same slot number twice will lead to an error.

The slot numbers of the T3X (R5) standard procedures can be found in the chapter explaining release 5 compatibility.

2.9 The Object System

Since release 6.0, the T3X language contains some object oriented extensions which have been primarily introduced to provide separately compiled, reusable modules and to define a clean and to the runtime environment. The information in this chapter is related to T3X-R6 and later releases.

The T3X object system is based upon classes, objects, and messages. Classes are used to describe the properties of objects, objects are used to instantiate classes, and messages are used to communicate with objects.

2.9.1 Classes

Each class declaration may contain any of the following entities:

The scope of variables defined inside of a class is the class they have been defined in. The scope of (non-public) constants, structures, and procedures is the same as for variables. The scope of a forward declaration is always the class context. There exist no public forward declarations across class boundaries. Objects are treated like variables in class contexts.

Procedures which have been declared public inside of a class are so-called methods. These procedures can be called from the outside by sending a message with the same name to an instance of the class.

Public constants (which include public structures) belong to the T3X module system rather than to the object system. They will be discussed later.

Given the above rules, a class can be outlined as follows:

+--------------------------------------------+
|                                            |
|               M e t h o d s                |
|                                            |
|              +--------------+              |
|              |  Variables   |              |
|              +--------------+              |
|              |  Constants   |              |
|              +--------------+              |
|--------------|  Structures  |--------------|
|              +--------------+              |
|              |  Procedures  |              |
|              +--------------+              |
|              |  Objects     |              |
|              +--------------+              |
|                                            |
|      P u b l i c   C o n s t a n t s       |
|                                            |
+--------------------------------------------+
Fig.3 Encapsulation

Since the scopes of the entities contained in the inner core of a class is the class itself, there is no way to access them from the outside. Only the methods defined by the class (which are visible outside of the class) and the procedures in the core may modify data declared by the class. To change the state of the class from the outside, a method must be applied to an instance of the class. This principle is called encapsulation. The advantage of hiding data is that the internal structure of a class may be changed while keeping its interface as it is. This way, an improved class may replace a previous version without having to modify code which depends on that class.

Since constants may not be changed, it is safe to declare them public, too.

A CLASS declaration creates a new class context. It has the following general forms:

CLASS classname () declarations END
CLASS classname (required, ...) declarations END

Class declarations may not be nested. Therefore, they are only valid at the global top level. Classname specifies the name of the new class. All declarations between the class header (CLASS ...) and the keyword END will be created in the newly created class context. All types of declarations except for classes and modules are permitted inside of classes. All declarations in class contexts are valid from the point of their declaration up to the end of the class they have been declared in.

A list of required classes may be specified between the parentheses in the class header. A dependent class may instantiate objects of each required class and access its class constants.

When a class declaration is prefixed with the keyword PUBLIC, the class will be exported so that other modules can require it. Each public class must itself be contained in a module. See the section on modules for more details.

When used as a factor in an expression, a class name evaluates to the size of an object of that class, which is the sum of the sizes of all its instance variables.

A public procedure (also called method) is created using an ordinary procedure definition prefixed with the keyword PUBLIC:

PUBLIC methodname () statement
PUBLIC methodname (arg1, ... argN) statement

Naturally, method definitions are only valid inside of class contexts. The only syntactical difference between a method and a procedure defintion is the keyword PUBLIC. Procedures may be defined inside of classes, but methods may not be defined outside of classes.

The keyword combination PUBLIC CONST introduces the definition of some class constants:

PUBLIC CONST constname = value, ... ;

This type of statement is only valid in class contexts. It creates a number of constants and assigns values to them, like an ordinary CONST statement. Constants defined this way are equal to ordinary constants inside of the context, they have been declared in, but additionally, they may be accessed by classes or modules requiring their class.

The combination PUBLIC STRUCT is used to introduce a public structure definition:

PUBLIC STRUCT structname = member1, ..., memberN;

Like public constant defintions, this statement type is only valid in class contexts. Inside of the defining class, the structure may be used like an ordinary structure, but it may also be used by classes or modules requiring the class defining it.

2.9.2 Objects

An object is used to instantiate a class. A class declaration is only the description of an object. It does not create a an object itself. In programming terms, this means that it does not allocate any space for an object's data. (Of course, it does allocate space for the methods used to manipulate instances of the class.)

A class may have any number of instances. Each additional instance duplicates all the data described in its class, but all instances share the same methods. When a method is applied to an object (by sending a message to that object), only the data contained in that specific object may change. When all static data used by an object is defined at class level, the states of all objects are entirely independent. (This is not the case, if the object refers to global data, of course.)

+---------------+                             +---------------+
|  Object a[A]  |                             |  Object b[A]  |
+---------------+    +-------------------+    +---------------+
         \           |      Methods      |           /
          \          |  +-------------+  |          /
           '-------> |  |  Layout of  |  | <-------'
                     |  |  private    |  |
           ,-------> |  |  entities   |  | <-------,
          /          |  +-------------+  |          \
         /           |      Methods      |           \
+---------------+    +-------------------+    +---------------+
|  Object c[A]  |           Class A           |  Object d[A]  |
+---------------+                             +---------------+
Fig.4 Instances of a Class

Variables defined at the top level inside of a class declaration are called instance variables, because space for them will be allocated first when an instance of the describing class is created.

An OBJECT statement instantiates a number of classes, thereby defining some new objects. It has the following general form:

OBJECT objectname[classname], ... ;

Objectname is the name of a new object, and classname determines its class. Objects of different classes may be defined in the same statement.

Objects may be defined in global, class, and local contexts. The class of each object must be required by the module or class creating the object. See the section on modules for further details.

The name of an object may be used as a factor in an expression:

objectname
@objectname

It evaluates to the address of the specified object. The notation '@objectname' is equal to 'objectname'.

2.9.3 Messages

The only way of manipulating an object is to send it a message. A message is comparable to a procedure call, but it performs some additional tasks. Like a procedure call, a message may carry some arguments to a method and like a procedure, a method delivers a return value.

When a method is applied to an object, the instance context is changed to the object the message is sent to before the method executes and the previous context is restored before the method returns. This is comparable to the local context of a procedure. Each procedure creates a local context when it is entered and restores the caller's context when it returns. The instance context of a method, however, is persistent. This means that it will not be set up when a method is entered and neither will it be destroyed, when the method is left again. Instead, an already existing instance context is made the current context upon entry, and the context which was previously in effect is restored upon exit. Like a stack frame, the caller's instance context is saved on the stack (or some other appropriate temporary storage).

Shifting the instance context to the object which a message is sent to assures that each object manipulates only its own data. Methods use the instance context to find out which object's data are to be processed.

+------------+
|  Class A   |
|------------|             +--------------------+
|  Method m  | ----------> |  Instance Context  |
|------------|             +--------------------+
|    ...     |                    |      |
+------------+           ,--------'      '--------,
                         |                        |
                         V                        V
                 +---------------+         +---------------+
                 |  Object x[A]  |         |  Object y[A]  |
                 +---------------+         +---------------+
Fig.5 Multiplexing Method Applications

The instance context (which is frequently implemented as a pointer) works as a multiplexer which allows different objects to be manipulated by one and the same method.

Each message consists of three parts: the name of the destination object whose data form the new instance context, the name of the method to be applied, and a list of arguments to be passed to the method (which may be empty). Like in procedure calls, the number of arguments passed to a method must match the number of formal arguments of that method.

Since methods change the instance context, they require that an object's address be passed to them. Therefore, methods cannot be called like ordinary procedures. When a method desires to apply another method of its own class without changing the instance context, it must also use message passing - the object defining the current instance context must send a message to itself. For this purpose, the instance context is bound to the symbol self. All messages sent to self will be directed to the object whose instance context currently is in effect.

The following notations are used for sending a message to an object:

objectname.methodname()
objectname.methodname(arg1, ... argN)
objectname.methodname();
objectname.methodname(arg1, ... argN);

Objectname is the name of the destination object and methodname is the name of a public procedure which shall answer the message. It must be defined in the class of objectname. The number of arguments passed to the method must match the number of formal arguments specified in its procedure header. This sentence exists as a standalone statement as well as a factor. When used as a factor in an expression, it evaluates to the return value of the applied method. When used as a statement (with a terminating semicolon), it is limited to its side-effects (like changing the state of the destination object).

The SEND operator allows to send a message to an unknown object:

SEND (variable, classname, methodname(...) ) ;

It applies the public procedure classname.methodname (the method methodname of the class classname) to the object, variable points to. Naturally, the object pointed to should be an instance of classname. Semantically,

object xa[A];
x := xa.b();

is perfectly equal to

object xa[A];
pa := @xa;
x := SEND(pa, A, b());

2.9.4 Class Relations

A class A may depend on another class B. If A, for example, defines objects of the class B, A depends upon B.

To resolve dependencies between classes contained in separate files, T3X defines so-called modules. This concept allows the description of class relations without derivation and inheritance. When defining a new class, all required classes must be specified in the class header. Classes which are not specified in this header will not be accessible in the new class. This way, the compiler can keep track of class relations and check method applications even on a cross-module basis.

It is possible for a procedural T3X program to depend on a number of classes, but no class may ever depend on a procedural program. Procedural programs must be able to require classes, because otherwise no object could be created ever, since the entry point of each program is a procedure (and not a class).

For details upon modules and separate compilation, check the section on modules later in this document.

2.9.5 Class Constants

Classes may define public constants and structures (which is basically the same in T3X). Such entities may be used to evaluate to values and/or build structured arrays in dependent classes or programs. This extension does not weaken the encapsulation principle, because constants are immutable and there is no way of changing the state of an object by accessing a constant.

Public constants belong to the class and they do not get instantiated when an object is created. To access a class constant, a special form of a message (without any parameters) must be sent to the class (not an instance of the class!) defining the desired constant:

classname.constname

Semantically, such a term is equal to an ordinary constant, structure, or structure member (which all evaluate to numeric values).

2.10 Modules

Modules have been introduced for the following reasons:

  1. To provide an interface between procedural programs and classes.
  2. To provide an interface to the runtime support routines.
  3. To support separate compilation

(1) No program can instantiate a class without depending on it. So without modules, classes could not be instantiated ever, because the entry point of each T3X program is a procedure.
(2) The INTERFACE method used in T3X up to release 5 is not easily extendable by the user and slot number collisions may occur, when different parties develop extensions. Therefore, runtime support has been changed to a class-oriented system.
(3) Having different classes in different modules increases the usability of each class. Therefore, modules may be compiled separately and then linked together. This was also a prerequisite for (2), of course.

A module begins with a module header of the form

MODULE modulename (requiredclass, ...) ;

and extends up to the end of the file. The list of the required classes may be empty. When a class is required in a MODULE statement, its public entities will be embedded in the global context. Therefore, a class contained in a module can use all classes required by the module without requiring them itself. However, this method is not recommended! Instead, all class dependencies should be explicitly specified without respect to the module context, like in the following example.

MODULE main(A,B,C);

CLASS D(C) ... END

CLASS E(B) ... END

In a module, all declaration types used in ordinary programs are allowed plus so-called public classes. A public class is a normal class declaration prefixed with the keyword PUBLIC:

PUBLIC CLASS classname (required-class, ...)
...
END

By declaring a class 'public', it will be made 'publically accessible' so that it can be required by classes or modules in other files which will be compiled at a different time or even on a different machine. All public entities defined in a public class can be made available to a class or module in a different file by requiring the public class there. There is no difference in requiring an internal class (defined in the same file) or an external class (defined in a different file). Although, an implementation of the compiler may require that the module containing the required class be compiled first.

No procedure, which is not contained in a module, can ever instantiate a class. The following example outlines how a message is sent to a class by a procedural program.

CLASS A()
 PUBLIC m() DO
        writes("A: received m."); newline();
 END
END

MODULE main(A);

DO OBJECT xa[A];
        xa.m();
END

2.11 Scoping Rules

In T3X, there exist only very few simple scoping rules, which define in which contexts symbols are valid. In total, there are four different contexts:

  1. The global context which covers the entire file or module and whose declarations are located in the space between procedures and class definitions.
  2. The class context which contains all symbols which belong to a specific class. They are delimited by a class header (CLASS(...)) and the keyword END.
  3. The procedural context which is a procedure header plus the associated procedure body.
  4. The block context which begins with the keyword DO and ends with the keyword END and which may be nested.

Global symbols are valid from the point of the declaration up to the end of the file (they are also valid in class contexts and local contexts). Class-level symbols are valid from the points of their declarations up to the end of the class the have been declared in. Each class context may itself contain local contexts which are equal to local contexts at the global level. Procedural symbols are defined in procedure headers and they are valid inside the entire procedure, including all block contexts contained in its body. Block-local symbols are declared at the beginning of as block (directly after DO, and they remain valid up to the innermnost occurence of END.

The following rules apply:

2.11.1 Conflicts

At the end of a class context, all names contained in that class context as well as the classname itself will be removed from the global context. However, the class name will be memorized at a different location and may never be reused. This is a little inconsistency which has to do with the module system. Imagine the following situation:

CLASS A() ... END

A() DO ... END

CLASS B(A) ... END

In this case, the class B would depend upon the procedure A which would be semantically incorrect. On the other hand, if the name A would persist, the following code would be correct:

CLASS A() ... END

CLASS B()
 OBJECT XA[A];
END

In this case, class B could use class A without being dependent on it, which would also be semantically incorrect, because the module extension requires that B be dependent on A in this case. Therefore, the (admittedly brute force) solution of not permitting the reuse of (deleted) class names has been chosen.

2.12 Type Checking

In T3X, there exist only very basic type checking mechanisms to detect things like assignments to constants, calls of non-procedures, and the like. In a truly object oriented language with first class objects, however, more extensive type checking is required.

When, for example, an object is passed to or returned by a function, the passed value becomes a first class value which may be assigned to a variable. No type information would be associated with such a variable in T3X. Therefore, the compiler could not decide which messages this object can answer and which not. This problem has been solved by not allowing to send messages to any type of data object other than the 'object'. The SEND operator may be used to circumvent this restriction. Like the CALL operator, SEND shall be used with special care.

Together with the newly introduced type object, T3X has now six different 'types' on which only specific operations are allowed. Type-related semantic checks carried through by the compiler are summarized in the following list:

This table contains an overview over the operations which may be applied to each type of entity:

Type Evaluate Change Call Send to
Constant + - - -
Variable + + (+) (*)
Vector (#) + - - -
Procedure + - + -
Class + - - -
Object + - - +

(+) Using the CALL operator
(*) Using the SEND operator
(#) A structure is a combination of a vector and a set of constants

In this context, evaluating an entity means to compute its value. This is the value assigned to a constant, the content of an atomic variable, the size of a class or structure, and the address of each other kind of entity (vector, packed vector, procedure, object).
To change an entity means to assign a new value to it. This can be done only with atomic variables.
Calling an entity denotes a procedure call, and excludes sending a message. Only procedures can be called directly. An indirect call can be performed through a variable holding the address of a procedure.
To send a message means to apply a method to an object.

2.13 Meta Commands

Meta commands are used to modify the behaviour of the compiler. No code will be generated for meta command, but the code generation process may be controlled by meta commands. Meta command do not belong to the T3X language itself and different implementations may provide different sets of meta commands. The commands described in this section should be present, though.

All meta commands begin with a # sign and like all other statements, they are terminated with a semicolon. They might occur at any place where a statement or a declaration (either local or global) is expected, but not inside of statements or declarations. The following meta command exist:

#CLASSPATH "path";

Specify an alternative path for searching class files. Normally, the translator searches class files in the current working directory and in some compiled-in paths. When bootstrapping the compiler or running it in some other non-standard environment, it may be necessary to specify the class path explicitly using this command. Only one path may be specified. When using multiple #classpath commands, only the last one will take effect. This solution is a makeshift and will probably be superseded by a more flexible technique in the future (environment variables, command line arguments, etc).

#DEBUG;

Turn on the emission of debug information, like source code line numbers and variable names and addresses. When the debug switch is turned on, the T3X translator will generate a LINE instruction at the beginning of each statement and an LSYM instruction for each local and a GSYM instruction for each global variable.
Debug information is intended to be used by a source level debugger.

#L line "name" ;

Re-set input line number and file name. This command should be generated by preprocessors when changing the order of input lines (for example when inserting code by including a file). #L sets the internal line counter of TXTRN to 'line' (which must be specified as a decimal number) and the input file name to 'name'. When reporting errors, TXTRN will use the provided line number and file name. The command
#L line "" ;
should be used to indicate that the following text belongs to the main program and has not been included from some other file.

#PACKSTRINGS;

Assume that the keyword PACKED be present before all string literals. Consequently, all strings in a program which are defined after #PACKSTRINGS; will be packed regardless of the actual presence of PACKED. Tables will not be affected. Unpacked strings can no longer be expressed using double quotes when this meta command is in effect. Tables must be used instead.

#R5C;

Turn on limited release 5 compatibility. This command may be used only once per program and should be used at its beginning. It generated internal prototypes of the T3XR5 runtime support procedures which are no longer present in later releases. The extended runtime procedures located in the slots up to 19 also will be declared. When #R5; is used, the resulting program must be linked against the release 5 compatilibity class. The the section on runtime support for a description of the R5 standard procedures.

2.14 The Main Program

Each program has an initial entry point where the execution begins at run time. In T3X, the entry point is a compound statement at the top level which does not belong to any procedure definition. This compound statement is mandatory and it always must be the last definition in the entire program. Consequently, the minimum valid T3X program is

DO END

The main procedure, like any other compound statement, may declare its own local symbols. Since it has no name, it cannot recurse, however. Also, RETURN may not be used, because there is no procedure to return to.

The T3X compiler automatically inserts the statement

HALT;

at the end of the main program so that the entire program will terminate when the end of the statement block has been reached.

3. The Runtime Environment

Most of this Chapter has been automatically generated from the structured document 'classes.sd' which is contained in the T3X Release 6 distribution. The runtime classes are still under construction. More classes and more methods may be added in the future. Although, the goal is to keep the runtime system as simple as possible.

There are three different types of classes: The core class, some native classes and some dynamically loadable classes. To the programmer, there is no difference between these class types, but when implementing additional runtime classes, it is important to know the differences.

The core class T3X is written in a system-level language like C or assember. Calling functions of this class is done through an internal jump table. The core class cannot be extended or modified. If available, this class also contains the interface to the dynamic linker (see T.SHLOPEN(), etc).

The native class is the most common type. Such classes are written in T3X themselves using the techniques described in the section about the object system. The major part of the runtime system is implemented this way. There is no difference between a native class and a user-defined module. Native classes are linked together using a Tcode linker.

Dynamically loadable classes (DLCs) allow to add low-level (LL) functions to a program. The LL functions themselves are written in some suitable system-level language and a shared object or dynamic link library (DLL) is generated from the LL modules. Additionally, a native class must be defined which loads and unloads the shared object and translates T3X procedures calls into LL calls. Dynamic linker support must be present in the core class to use DLCs.

DLCs have been chosen as the only method for extending the runtime environment with system level functions, because this method allows one and the same method to be used by a compiled as well as an interpreted program, since the Tcode interpreter uses the same dynamic linker interface as the T3X library code.

A runtime class is linked into a program by requiring it either at class level or at module level. For example,

class foo(t3x, iostream)

would be the header of a class requiring the T3X core class and the iostream class, and the statement

module bar(t3x, char, string);

would require the core class plus the char and string classes. The only exception is the R5C class which never must be required. To do so, the #R5; meta command should be used instead.

Currently, the following runtime classes exist:

Name Type Description
t3x core basic routines
char native character manipulation
iostream native buffered I/O-streams
memory native dynamic memory management
string native string manipulation
system dlc (mostly) portable system calls
ttyctl dlc terminal control

These classes will be explained in detail in the following sections.

3.1 The Core Routines

OBJECT T[T3X];

The T3X class contains some procedures which provide access to the most common operating system services, like opening, reading, writing, and erasing files, copying and comparing memory regions, receiving arguments, etc. It also contains the dynamic loder interface, if available. The class requires no explicit initialization or shutdown.

The T3X class does not contain any variables. Therefore, it is sufficient to create a single instance per module.

In the following sections, 'Str' indicates a PACKED STRING.

3.1.1 BPW

T.BPW() ! => Num

Return the number of Bytes Per Word on the host machine. When running a Tcode program, this value will always be 2, regardless of the host environment. When called by a native machine program, the procedure will return the actual word size of the target machine.

3.1.2 CLOSE

T.CLOSE(fdesc) ! Fdesc => Num

Close the file descriptor 'fdesc'.

T.CLOSE returns 0 on success and a negative value in case of an error.

See also: OPEN, SYS.DUP, SYS.DUP2, SYS.PIPE, READ, WRITE

3.1.3 CVALIST

T.CVALIST(n, bmap, ilist, olist) => 0

Convert a Tcode argument list into a native argument list. Since the Tcode machine is a 16-bit architecture, argument lists may need to be extended before passing them to machine code procedures in 32-bit environments.

Extending an argument list from 16 to 32 bits (or whatever is appropriate on the host system) is done by zero-extending all values in the vector representing the argument list to the size of a generic pointer on the host. Additionally, the offset of the Tcode machine's data area will be added to pointer type arguments. The bitmap 'bmap' specifies the type of each argument.

Argument lists may not be longer than 16 elements (plus the trailing null).

'N' specifies the number of elements in the argument list 'ilist'. If a trailing null is required, it must be counted, too. 'Ilist' is a vector containing the arguments. 'Bmap' is a bit field where a bit is set when the argument with the according offset is a pointer. If bit #0 is set, (bmap & 1), ilist[0] is a pointer, if bit #1 is set (bmap & 2), ilist[1] is a pointer, and so on. 'Olist' will be filled with the extended argument list. It must provide up to 17 times the size of a generic pointer in bytes (which is usually equal to 17 machine words on the host system).

When a negative count is supplied, the effect of T.CVALIST is reversed. In this case, each member of 'olist' will be copied to 'ilist' and truncated to 16-bits. No pointers may processed this way. The 'Bmap' argument is ignored when converting argument lists in this direction.

T.CVALIST is used to prepare argument lists for passing them to dynamically loaded procedures in Tcode programs. When a T3X program is run in an environment where the size of a pointer is equal to the size of a T3X machine word, T.CVALIST simply copies the argument vector.

T.SHLCALL and SYS.SPAWN use T.CVALIST internally. Most programs will not require its use.

See also: T.SHLCALL, SYS.SPAWN

3.1.4 GETARG

T.GETARG(n, buffer, size) ! Num,Str,Num => Num

Retrieve the 'n'th command line argument and store its first 'size'-1 characters in 'buffer'. If the length K of the the requested argument is less than 'size'-1, copy only K characters. In any case, append a NUL character.

T.GETARG returns the number of characters copied. A return code of -1 indicates that a non-existing argument has been requested ('n' is too big).

See also: GETENV

3.1.5 GETENV

T.GETENV(name, buffer, size) ! Str,Str,Num => Num

Retrieve the value of the environment variable 'name' and store up to 'size'-1 chararacters of its value in 'buffer'. Append a NUL character to the value bytes.

T.GETENV returns the number of characters copied. A return code of -1 indicates that a non-existing variable name has been specified.

See also: GETARG

3.1.6 MEMCOMP

T.MEMCOMP(r1, r2, len) ! Bvec,Bvec,Num => Num

Compare up to 'len' bytes of the regions 'r1' and 'r2'. When a mismatch is found during the comparison, the procedure returns

r1::p - r2::p

where 'p' is the position of the mismatch. When 'len' bytes have been compared without encountering a mismatch, zero is returned.

See also: MEMCOPY, MEMFILL, MEMSCAN

3.1.7 MEMCOPY

T.MEMCOPY(dest, src, len) ! Bvec,Bvec,Num => 0

Copy 'len' bytes from region 'src' to region 'dest'. The regions may overlap.

See also: MEMCOMP, MEMFILL, MEMSCAN

3.1.8 MEMFILL

T.MEMFILL(region, val, len) ! Bvec,Num,Num => 0

Fill the first 'len' bytes of 'region' with the value of the least significant byte of 'val'.

See also: MEMCOMP, MEMCOPY, MEMSCAN

3.1.9 MEMSCAN

T.MEMSCAN(region, val, len) ! Bvec,Num,Num => Num

Scan the first 'len' bytes of 'region' for 'val'. If the scanned region contains 'val', return its offset (0...len-1) and otherwise return -1.

See also: MEMCOMP, MEMCOPY, MEMFILL

3.1.10 NEWLINE

T.NEWLINE(s) ! Str => Str

Write a system-dependent newline sequence into the string 's'. The sequence will move the cursor to the beginning of a new line when sent to terminal screens (in cooked mode).

T.NEWLINE returns a pointer to 's'.

See also: WRITE, TTY.MODE

3.1.11 OPEN

T.OPEN(path, mode) ! Str,Num => Fdesc

Open the file whose path is specified in 'path' in the given 'mode'. The exact format of 'path' depends on the operating system. The following mode constants exist:

Mode ReadOK WriteOK Create
T3X.OREAD Yes No No
T3X.OWRITE No Yes Yes
T3X.ORDWR Yes Yes No
T3X.OAPPND Yes Yes No

When OWRITE is specified and a file with the given name already exists, it will be deleted first.

OAPPND is like ORDWR, but the file pointer will be positioned at the end of the file so that T.WRITE will append its output to the file.

T.OPEN returns a file descriptor for accessing 'path' on success and a negative number in case of an error.

When a T3X program starts up, there are some already open file descriptors which are by default connected to the user's terminal:

T3X.SYSIN standard input read-only
T3X.SYSOUT standard output write-only
T3X.SYSERR standard error write-only

See also: CLOSE, READ, WRITE, SEEK

3.1.12 PACK

T.PACK(vec1, vec2) ! Ustr, Str => Num

Copy the least significant byte of each machine word of the vector 'v1' into a byte of 'v2' until a null word is found in 'vec1'. This way, the unpacked string contained in 'vec1' is converted into a packed string. A terminating NUL character will be placed at the end of the string in 'vec2'.

'Vec1' and 'vec2' may be the same vector. In this case, the string will be packed in situ and the unpacked string will be overwritten.

T.PACK returns the number of machines words required to store the packed string including the terminating NUL byte.

See also: UNPACK

3.1.13 READ

T.READ(fdesc, buffer, count) ! Fdesc,Vec,Num => Num

Read up to 'count' characters from the file descriptor 'fdesc' into 'buffer'. Return the number of characters read.

A return value less than zero indicates a severe error. A return value which is less than 'count' usually indicates that the end of the input has been reached.

When reading line oriented devices, such as terminals, a return value below 'count' may indicate the end of the line. In this case, a zero value means that the input stream is exhausted.

For a summary of standard descriptors (system input and output), see T.OPEN().

See also: OPEN, CLOSE, SYS.PIPE, WRITE

3.1.14 REMOVE

T.REMOVE(path) ! Str => Num

Remove the directory entry specified in 'path'. The exact format of 'path' depends on the operating system.

On system supporting multiple links (names) for a single file, this procedure will only remove the specified link. On such systems, other links to the file may still be used to access the file. Only when the last link is removed, the file will become inaccessible. On other systems, T.REMOVE deletes the given file immediately.

T.REMOVE returns zero, if the directory entry could be successfully deleted and otherwise a negative value.

See also: SYS.OPENDIR, RENAME

3.1.15 RENAME

T.RENAME(old, new) ! Str,Str => Num

Rename the directory entry whose path is specified in 'old' to 'new'. 'Old' and 'new' may describe names contained in different pathes. In this case, the directory entry will be 'moved' to the directory specified in 'new'. Both, 'old' and 'new' must contain file names (not directory names). The old and the new directory entry name must both reside on the same physical device.

T.RENAME returns zero upon success and a negative value in case of an error.

See also: REMOVE

3.1.16 SEEK

T.SEEK(fdesc, where, origin) ! Fdesc,Num,Num => Num

Move the file pointer associated with the file descriptor 'fdesc' to a new position. 'Where' specifies the desired position and 'origin' specifies where the motion shall start. The following origins are possible:

Constant Origin Distance
T3X.SEEK_SET Beginning of the file +where
T3X.SEEK_FWD Current position +where
T3X.SEEK_END End of the file -where
T3X.SEEK_BCK Current position -where

SEEK_SET and SEEK_FWD move the file pointer forward, SEEK_END and SEEK_BCK move it backward. In either case, 'where' is an unsigned value so that offsets may range from 0 to 65535 bytes.

T.SEEK returns zero upon success and -1 in case of an error.

SEEK may be undefined on some devices and pipes.

See also: OPEN, CLOSE, READ, WRITE

3.1.17 SHLCALL

T.SHLCALL(sym, args) ! Fndesc,Vec => X

Call the shared library procedure 'sym' passing the arguments contained in 'args' to it. 'Sym' is a function descriptor which must be obtained using SHLSYM. The argument vector 'args' contains the following members:

args[0] the number of actual arguments
args[1] the argument type bitmap
args[2]...[N] the arguments to be passed to 'sym'

No more than 16 arguments may be passed to 'sym'. The bitmap in args[1] works as follows: when transporting vectors to a shared library procedure, vector addresses must be converted from Tcode machine relative to absolute addresses, But numeric value must be passed unchanged. Therefore, a bit in args[1] has to be set for each string argument. The first argument is associated with the least significant bit (2^0), the second argument with 2^1, and so on.

T.SHLCALL returns the return value of the called procedure. When passing an invalid argument vector to it, it returns -1, which cannot be distinguished from a procedure return value.

See also: SHLOPEN, SHLSYM, SHLCLOSE, CVALIST

3.1.18 SHLCLOSE

T.SHLCLOSE(shldesc); ! Shldesc => Num

Close the shared library descriptor 'shldesc'. Return zero on success and a negative value in case of an error.

See also: SHLOPEN, SHLSYM, SHLCALL

3.1.19 SHLOPEN

T.SHLOPEN(path) ! Str => Shldesc | -1

Open the shared library whose location is specified in 'path'. The exact format of path depends on the operating system. If SHLOPEN fails to open the specified shared object, it might attempt to open the same file in the directory

$T3XDIR/classes/'path'

where $T3XDIR is an environment variable containing the location of the T3X base directory (usually /usr/local/t3x).

When the libary could be opened successfully, SHLOPEN returns a shared library descriptor. Otherwise, it returns -1.

See also: SHLSYM, SHLCALL, SHLCLOSE

3.1.20 SHLSYM

T.SHLSYM(shldesc, name) ! Shldesc,Str => Fndesc | -1

Lookup the specified 'name' in the shared library referenced through 'shldesc'. When 'name' could be resolved in 'shldesc', return a function descriptor which may be used to call the procedure 'name'. If no object with the given name exists in the shared object, return -1.

NOTE: only procedures should be looked up in the shared object, because calling non-procedures will lead to unpredictable results.

See also: SHLOPEN, SHLCALL, SHLCLOSE

3.1.21 UNPACK

T.UNPACK(vec1, vec2) ! Str, Ustr => Num

Copy each byte of the vector 'v1' into a separate machine word of 'v2' until a null character is found in 'vec1'. Each value will be zero-extended when transferred. This way, the packed string contained in 'vec1' is converted into an unpacked string. A terminating NUL word will be placed at the end of the string in 'vec2'.

'Vec1' and 'vec2' may not be the same vector. In this case, the string would be destroyed.

T.UNPACK returns the number of machines words required to store the unpacked string including the terminating NUL word.

See also: PACK

3.1.22 WRITE

T.WRITE(fdesc, buffer, count) ! Fdesc,Vec,Num => Num

Write 'count' characters from 'buffer' to the file descriptor 'fdesc'. Return the number of characters actually written.

A return value which is less than 'count' indicates a severe error (such as insufficient space left on a device).

For a summary of standard descriptors (system input and output), see T.OPEN().

See also: OPEN, CLOSE, SYS.PIPE, READ

3.2 Character Functions

OBJECT CHR[CHAR];
CHR.INIT();

The CHAR class contains functions for determining character types and converting characters. They all operate on ASCII values.

This class must be initialized by calling CHR.INIT before it can be used. An explicit shutdown is not required.

The CHAR class does not contain any variables. Therefore, it is sufficient to create a single instance per module.

3.2.1 INIT

CHR.INIT() ! => 0

Initialize the character class by loading an internal pointer with the character type map.

See also: CHR.MAP

3.2.2 ALPHA

CHR.ALPHA(c) ! Char => Num

Return TRUE (-1), if 'c' is an alphabetic character (in the range 'a'...'z' or 'A'...'Z'). Otherwise return FALSE (0).

3.2.3 ASCII

CHR.ASCII(c) ! Char => Num

Return TRUE (-1), if 'c' is a valid ASCII value (in the range 0...127). Otherwise return FALSE (0).

3.2.4 CNTRL

CHR.CNTRL(c) ! Char => Num

Return TRUE (-1), if 'c' is a control character (in the range 0...31 or equal to 127). Otherwise return FALSE (0).

3.2.5 DIGIT

CHR.DIGIT(c) ! Char => Num

Return TRUE (-1), if 'c' is a decimal digit (in the range '0'...'9'). Otherwise return FALSE (0).

3.2.6 LCASE

CHR.LCASE(c) ! Char => Char

If the character 'c' is an upper case character (see CHR.UPPER), convert it to lower case and return it. Otherwise, return it unchanged.

See also: CHR.UCASE

3.2.7 LOWER

CHR.LOWER(c) ! Char => Num

Return TRUE (-1), if 'c' is a lower case letter (in the range 'a'...'z'). Otherwise return FALSE (0).

3.2.8 MAP

CHR.MAP() ! => Vec

Return the character description map used internally. This map is a vector of the length 128 and contains a set of flags for describing each ASCII character. It can be used to implement faster character checks. For example,

IF (CHR.LOWER(c)) ...

can be written as

chrmap := CHR.MAP();
...
IF (chrmap[c] & CHAR.C_UPPER = 0) ...

which saves a procedure call each time a character is tested for being lower case.

The following public constants are defined in the CHAR class and can be used for testing character flags:

C_ALPHA alphabetic
C_UPPER upper case
C_DIGIT decimal digit
C_SPACE white space
C_CNTRL control character

3.2.9 SPACE

CHR.SPACE(c) ! Char => Num

Return TRUE (-1), if 'c' is a space character (HT(9), LF(10), VT(11), FF(12), CR(13)). Otherwise return FALSE (0).

3.2.10 UCASE

CHR.UCASE(c) ! Char => Char

If the character 'c' is a lower case character (see CHR.LOWER), convert it to upper case and return it. Otherwise, return it unchanged.

See also: CHR.UCASE

3.2.11 UPPER

CHR.UPPER(c) ! Char => Num

Return TRUE (-1), if 'c' is a upper case letter (in the range 'A'...'Z'). Otherwise return FALSE (0).

3.3 I/O-Streams

OBJECT IOS[IOSTREAM];

The IOSTREAM class implements fully buffered I/O streams.

I/O streams provide a string/character-oriented interface to the programmer while performing block-oriented I/O to the file or device associated with a stream. This way, they combine the speed of block-I/O with the flexibility of character-based I/O.

This class contains the I/O stream data structure and procedures for creating, opening, closing, reading, and writing streams.

A separate IOSTREAM object must be defined for each stream to use in a program.

In this section 'Str' indicates a PACKED string.

3.3.1 CLOSE

ios.CLOSE() ! => Num

Shutdown the I/O stream 'ios' by first flushing its buffer an then closing the file associated with the stream. Flushing a buffer means to write pending input (if the stream has been written to) ond to discard any pending input (if the stream is to be read from).

IOS.CLOSE returns zero, if the stream could be closed and otherwise -1. After sucessfully sending CLOSE, the receiving stream becomes invalid immediately and should no longer be accessed.

See also: CREATE, OPEN, FLUSH

3.3.2 CREATE

ios.CREATE(fd, buffer, len, mode) ! Fdesc,Bvec,Num,Num => 0

Initialize the iostream 'ios' with the given parameters. 'Fd' is an open file descriptor which will be associated with the stream. 'Buffer' will be used for buffering read/write operations on the stream. 'Len' specifies the size of 'buffer' in characters. 'Mode' controls the operations allowed on 'ios'. The following flags may be used to build the mode value:

Mode ReadOK WriteOK LF>CRLF CRLF>LF
IOSTREAM.FREAD Yes No - -
IOSTREAM.FWRITE No Yes - -
IOSTREAM.FRDWR Yes Yes - -
IOSTREAM.FKILLCR - - - Yes
IOSTREAM.FADDCR - - Yes -
IOSTREAM.FTRANS - - Yes Yes

CRLF>LF denotes that each CR character found in an input stream will be silently discarded. This is useful when reading DOS-style ASCII text files. LF>CRLF means that a CR character will be added before each LF in the output stream. Since FADDCR has no effect on input and FKILLCR has no effect on output, FTRANS may be used safely on input as well as output streams.

IOS.CREATE only initializes an IOSTREAM object with some data. It cannot fail and therefore, it returns always 0.

When using IOS.CREATE to create a stream for accessing standard file descriptors (such as stdin and stdout), these stream should never be closed. IOS.FLUSH may be used for synchronizing them.

See also: OPEN, CLOSE, FLUSH

3.3.3 EOF

ios.EOF() ! => Num

Return a flag indicating whether input has been exhausted on the stream 'ios'. When IOS.EOF returns TRUE (-1), no more input can be read from 'ios'. This may be because the end of the associated input file has been reached or because an EOF character has been typed on a terminal, for example.

See also: READ, READS, RDCH, RESET

3.3.4 FLUSH

ios.FLUSH() ! => Num

Flush the stream 'ios' and return a value indicating whether the operation was successful. Zero means success, -1 means failure.

Flushing an output stream means to write all pending data to the associated file, flushing an input stream means to discard all pending input. The operation performed on a combined input/output stream depends on the last operation performed before (reading or writing).

See also: OPEN, CLOSE, READ, WRITE

3.3.5 MOVE

ios.MOVE(offset, origin) ! Num,Num => Num

Move the file pointer of the file descriptor associated with 'ios' to a new position. The position is computed using the given 'offset' and 'origin'. 'Offset' is the number of bytes to move and 'origin' specifies where the motion shall begin. The following origin values are available:

Mode Origin
IOSTREAM.SEEK_SET the beginning of the file
IOSTREAM.SEEK_FWD the current position (move forward)
IOSTREAM.SEEK_END the end of the file
IOSTREAM.SEEK_BCK the end of the file (move backward)

SEEK_SET and SEEK_FWD move the file pointer forward, SEEK_END and SEEK_BCK move it backward. In either case, 'where' is an unsigned value so that offsets may range from 0 to 65535 bytes.

IOS.MOVE always flushes the stream buffer before changing the file pointer.

It returns zero upon success and -1 in case of an error.

See also: FLUSH, T.SEEK

3.3.6 OPEN

ios.OPEN(path, buffer, len, mode) ! Str,Vec,Num,Num => Num

Open the file specified in 'path' and initialize 'ios' with the resulting file descriptor and the arguments 'buffer', 'len', and 'mode'. See IOS.CREATE for details. The exact format of 'Path' depends on the operating system. The follwing open modes ('mode') exist:

Mode ReadOK WriteOK Create
IOSTREAM.FREAD Yes No No
IOSTREAM.FWRITE No Yes Yes
IOSTREAM.FRDWR Yes Yes No

When creating a file, any existing file with the same name will be deleted.

IOS.OPEN returns zero upon success and -1 in case of an error.

See also: CREATE, CLOSE, FLUSH, T.OPEN

3.3.7 RDCH

ios.RDCH() ! => Char

Read a single character from 'ios' and return it. When the EOF condition is true on 'ios', return -1 (which cannot be a valid character).

See also: READ, READS, WRCH, EOF

3.3.8 READ

ios.READ(buffer, len) ! Vec,Num => Num

Read up to 'len' characters from 'ios' into 'buffer'. Return the number of characters actually read. A return value less than 'len' may indicate the end of input or a newline on a terminal. A return value of zero always indicates the EOF. A value below zero indicates a severe error.

See also: RDCH, READS, WRITE, EOF

3.3.9 READS

ios.READS(buffer, len) ! Vec,Num => Num

Read up to 'len'-1 characters from 'ios' into 'buffer'. Return the number of characters actually read. A return value of zero indicates that the EOF has been reached. A value below zero indicates a severe error.

Unlike READ, READS stops reading when it encounters a line separator (LF).

See also: RDCH, READ, WRITE, EOF

4.10 RESET

ios.RESET() ! => 0

Reset the error flag of the given iostream 'ios'. Resetting the error flag is necessary to access a stream after an error has occurred (for example after reading beyond the EOF).

See also: EOF, READ

3.3.11 WRCH

ios.WRCH(c) ! Char => Char|Num

Write the character 'c' to the stream 'ios'. If the character could be written, return it and otherwise return -1.

See also: WRITE, WRITES, FLUSH

3.3.12 WRITE

ios.WRITE(buffer, len) ! Vec,Num => Num

Write 'len' characters from 'buffer' to 'ios'. Return the number of characters actually written. A return value less than 'len' indicates a severe error (such as no space left on the target device).

See also: WRITES, WRCH, FLUSH

3.3.13 WRITES

ios.WRITES(str) ! Str => Num

Write the packed string 'str' to 'ios'. Return the number of characters actually written. A return value less than 'len' indicates a severe error (such as no space left on the target device).

See also: WRITE, WRCH, FLUSH

3.4 Dynamic Memory Management

OBJECT MEM[MEMORY];

The MEMORY class implements dynamic memory pools. When initialized, the address of a static data area is passed to a MEMORY object. This area (called a 'pool') will be managed by the MEMORY object. Vectors can be allocated from the pool and released it again, when they are no longer required.

A first-match algorithm is used to allocate memory in a pool. The algorithm is optimized for sequential allocation. The pool is defragmented when releasing memory, but no garbage collection is performed.

MEMORY objects are ineffective when allocating a large number of small vectors, since the free list is kept inside of the pool.

Multiple memory pools may be defined using the MEMORY class.

3.4.1 ALLOC

mem.ALLOC(size) ! Num => Vec

Allocate 'size' bytes from the memory pool 'mem' and return a pointer to the allocated vector. If the request could not be satsified due to insufficient memory, return 0.

Up to 32767 bytes may be allocated in a single request.

See also: FREE

3.4.2 FREE

mem.FREE(vec) ! Vec => 0

Release the memory occupied by the vector 'vec' to 'mem' and defragment 'mem'. Thereby, the size of 'vec' will be added to the amount of free memory in 'mem'.

'Vec' must be the address of a vector which has been previously allocated from 'mem'. Otherwise, the calling program may be terminated with an error message ("mem_free(): bad block").

Accessing a freed vector is undefined.

See also: ALLOC

3.4.3 INIT

mem.INIT(pool, size) ! Vec,Num => 0

Initialize the memory pool 'mem' by adding 'size' bytes to the internal freelist. 'Pool' must have a size of at least 'size' bytes. Size may not be larger than 32767.

MEM.INIT always returns 0.

3.4.4 WALK

mem.WALK(vec, sizep, statp) ! Vec,Vec,Vec => Vec

Traverse the list of vectors in 'mem'. This list contains both, allocated and free vectors. Traversing the list works as follows:

When MEM.WALK is called the first time, 'vec' is set to zero:

v := mem.walk(0, @p, @s);

This call will return a pointer to the first vector in 'mem'. The returned vector can be passed to MEM.WALK in a subsequent call to retrieve a pointer to the next vector:

v := mem.walk(v, @p, @s);

When the 'vec' argument finally points to the last vector in 'mem', MEM.WALK will return zero, thereby indicating the end of the list.

The argument 'sizep' is a one-word vector which will be filled with the size of the returned vector. 'Statp' is also a one- word argument which will be filled with the status of the vector (1=free, 0=allocated). If zero is passed to MEM.WALK instead of a vector for either argument, it will not be filled.

3.5 String Functions

OBJECT STR[STRING];

The STRING class contains procedures for manipulating ASCIIZ string (NUL-terminated sequences of ASCII characters). All procedures operate on PACKED strings. Therefore, 'Str' indicates a packed string in this section.

The STRING class does not contain any data and it does not require any explicit initialization or shutdown procedures.

3.5.1 COMP

STR.COMP(a, b) ! Str,Str => Num

Compare each character in 'a' with the character at the same position in 'b' and return the difference

a::i - b::i

of the characters at the first mismatching position 'i'. When no mismatch is encountered, the difference between the terminating NUL characters (0) is returned. Consequently, the return value of STR.COMP can be interpreted as follows:

>0 'a' is lexially greater than 'b'
<0 'a' is lexially less than 'b'
=0 'a' is equal to 'b'

See also: FIND, SCAN, RSCAN

3.5.2 COPY

STR.COPY(a, b) ! Str,Str => 0

Copy the string stored at the location 'b' to the location 'a'. Return zero.

See also: FORMAT

3.5.3 FIND

STR.FIND(a, b) ! Str,Str => Num

Find the first occurrence of the string 'b' in the string 'a'. Return the offset of the string found, if any. Return -1, if 'a' does not contain 'b'.

See also: COMP, SCAN, RSCAN

3.5.4 FORMAT

STR.FORMAT(buf, tmpl, list) ! => Str,Str,Vec => Str

Format the arguments contained in 'list' according to the template 'tmpl' and store the resulting string in 'buf'.

'Tmpl' is a string containing literal characters as well as 'format defintions'. A format definition is a substring which begins with a percent sign and ends with one of the characters in {C,D,S,X,%}. When 'tmpl' does not contain any format definitions, it will be copied to 'buf' and 'list' will be ignored. When format defintions exist, each definition will be used to format one element of 'list'. Instead of the definition itself, the result of formatting the next member of 'list' according to the definition will be inserted into 'buf'.

A format definition has the following syntax:

%[max][:F][U][{LR}]{CDSX%}

([x] indicates an optional element, {xyz} indicates 'one out of x,y,z'.)

- The percent sign '%' starts the definition.

- When a decimal number 'max' is specified after '%', this number will be the minimum field length for the current argument. If formatting the current argument yields a result shorter than 'max', the field will be filled with blanks.

- ':F' denotes that the character F should be used for filling fields instead of blank characters.

- When 'U' is specifed together with 'D', an unsigned numeric string will be generated (a signed representation will be generated by default).

- 'L' instructs FORMAT to left-adjust the current field, 'R' instructs it to right-adjust it. The default is to left- adjust string and to right-adjust numbers.

- The last character specifies the type of the current argument in 'list'.

The following types exist (i denotes the index of the current argument):

Type Insert list[i] as
C character
D decimal numeric literal
S string
X hexa-decimal numeric literal

The form '%%' may be used to include a literal percent sign.

Examples (all strings are packed):

Template Argument list Result
"%D%% of %10:*D = %D" [10,200,20] 10% of *******200 = 20
"'%C' = 0X%X = %D" ['A','A','A'] 'A' = 0X41 = 65
"%:-9LS%:+9RS" ["XXX","YYY"] XXX------++++++YYY

STR.FORMAT returns the address of 'buf'.

See also: PARSE, COPY

3.5.5 LENGTH

STR.LENGTH(a) ! Str => Num

Return the number of characters contained in 'a' (exluding the terminating NUL character).

3.5.6 NUMTOSTR

STR.NUMTOSTR(buf, n, radix) ! Str,Num,Num => Str

Convert a number 'n' into a string representing that number with respect to a given 'radix'. The resulting numeric literal will be strored in 'buf'. If 'radix' is negative, a leading minus sign will be generated, if 'n' is also negative. If 'radix' is positive, an unsigned literal will be generated.

'Buf' must provide enough space for the resulting literal.

Valid values for radix range from '2' (binary) to '16' (hexa- decimal) and from '-2' (signed binary) to '-16' (signed hexa- decimal).

STRTONUM returns the address of the first character of the resulting literal.

See also: STRTONUM, FORMAT

3.5.7 PARSE

STR.PARSE(source, tmpl, list) ! Str,Str,Vec => Num

Extract patterns described in 'tmpl' from 'source' and store the extracted objects in 'list'. Patterns used in 'tmpl' are similar to format descriptions as used by STR.FORMAT. Characters not belonging to patterns are matched literally.

STR.PARSE compares each character contained in 'tmpl' with a character in 'source' (like STR.COMP) unless it finds a '%'-character in 'tmpl'. A percent character indicates the beginning of a pattern. Patterns match specific classes of characters. Instead of matching the pattern description, the character class described by the pattern is matched.

Some patterns store the matched substring in an element of 'list' and some do not. Each pattern may consists of the following parts:

%[len][:D]{CDSWX%}

([x] indicates an optional element, {xyz} indicates 'one out of x,y,z'.)

The special form %[c0...cN] may be used to match any character 'c0'...'c1'.

- If a length 'len' is specified, UP TO 'len' characters will be matched. Typically, this option is used together with %S.

- ':D' instructs STR.PARSE to recognize the character 'D' as a delimiter (default = none).

The following pattern types exist:

Type Store as Matches
C character any single character
D number a signed decimal number (+)
S string a string (*)
W - space: any number of '\s' or '\t'
X number a signed hexa-decimal number (+)
% - percent sign

(+) These leading prefixes are accepted: {+-%}
(*) When no length is specified, %S matches the entire resr of 'source'. ':D' or a length may be specified to match a substring.

Numbers and characters are stored in 'list[i][0]' (where 'i' is the index of the current member of 'list') and strings are copied to the location pointed to by 'list[i]'.

Example:

VAR name::50, speed, unit::10;
STR.PARSE(packed"HAL9000 @ 500 MHz", packed"%:@S@ %D%W%S",
 [ name, @speed, unit]);

will store

"HAL9000 " in 'name'
500        in 'speed'
"MHz"      in 'unit'

STR.PARSE returns the number of patterns stored.

See also: FORMAT, COMP

3.5.8 RSCAN

STR.RSCAN(s, c) ! Str,Num => Num

Find the rightmost occurrence of the character 'c' in the string 's' and return the offset (position) of the character found. If 'c' is not contained in 's', return -1.

See also: SCAN, FIND, COMP

3.5.9 SCAN

STR.SCAN(s, c) ! Str,Num => Num

Find the first occurrence of the character 'c' in the string 's' and return the offset (position) of the character found. If 'c' is not contained in 's', return -1.

See also: RSCAN, FIND, COMP

3.5.10 STRTONUM

STR.STRTONUM(s, radix, lastp) ! Str,Num,Vec => Num

Compute the value represented by the numeric literal stored in the string 's'. 'Radix' specifies the base of the literal in 's'. It may range from '2' (binary) to '16' (hexa-decimal).

STR.STRTONUM performs the following steps:

- Leading TAB ('\t') and space ('\s') characters in 's' are skipped.

- A plus (+) or minus (-,%) sign is recognized.

- Characters belonging to the specified literal class (based upon 'radix) are collected and converted into a numeric value.

The following characters may represent the digits from 0 to 15: "0123456789ABCDEF".

When the argument 'lastp' is nonzero, it will be filled with the number of characters processed. Consequently, it points to the first non-numeric character in 's' when STR.STRTONUM returns.

STR.STRTONUM returns the computed value.

No overflow checking is performed.

See also: NUMTOSTR, PARSE

3.5.11 XLATE

STR.XLATE(s, old, new) ! Str,Num,Num => Str

Replace each occurrence of the character 'old' in the string 's' with 'new'. Return the address of 's'.

See also: SCAN, RSCAN

3.6 The Tcode Class

MODULE MODNAME(TCODE);
CLASS CLASSNAME(TCODE) ... END

The TCODE class contains a set of public constants describing the instruction set of the Tcode machine. There is no need to instantiate this class, since it does not contain any state or methods. To access the opcode of a specific Tcode instruction use the class constant notation

tcode.IINSTRUCTION

For example, to load the variable I with the the opcode of the JUMP instruction, use

I := tcode.IJUMP;

The special constant IENDOFSET contains a value which is one above the highest value used to form Tcode instructions. To check if a variable I contains a valid instruction, the following code may be used:

IF (I < 0 \/ (I & 0x7F) > tcode.IENDOFSET)
        ; ! Call your illegal instruction handler here

3.7 The System Interface

OBJECT SYS[SYSTEM];
SYS.INIT();

The SYSTEM class contains some procedures which form a portable interface to the operating system. Most procedures have the same names and functions as Unix system calls. Some functions may be unavailable when running the virtual Tcode machine on non-Unix systems.

This class is implemented as a shared object. Therefore, it must be initialized by SYS.INIT before its use and it may be unloaded by calling SYS.FINI.

SYSTEM does not contain any variables. Therefore, it is sufficient to create a single instance per module.

In the following sections, 'Str' indicates a PACKED STRING.

3.7.1 INIT

SYS.INIT() ! => 0

Initialize the operating system interface. Basically, this routine loads the shared object containing the actual interface procedures. SYS.INIT will fail, if the shared object could not be opened. In this case, the calling program will be halted.

See also: FINI

3.7.2 CHDIR

SYS.CHDIR(path) ! Str => Num

Change the current working directory to 'path'. 'Path' contains the operating system dependent representation of a path.

SYS.CHDIR returns 0 on success and a negative value in case of an error.

See also: MKDIR, RMDIR, OPENDIR

3.7.3 CLOSEDIR

SYS.CLOSEDIR(ddesc) ! Ddesc => Num

Close the directory descriptor 'Ddesc'.

SYS.CLOSEDIR returns 0 on success and a negative value in case of an error.

See also: OPENDIR, READDIR, STAT

3.7.4 DUP

SYS.DUP(oldfd) ! Fdesc => Fdesc | -1

Duplicate the file descriptor 'oldfd' and return a new descriptor which will reference the same file. Since the old and the new descriptor reference the same file, all operations performed on one of them will also affect the other.

SYS.DUP returns a new descriptor on success and a negative number in case of an error.

See also: DUP2, T.OPEN, T.CLOSE, PIPE, FORK

3.7.5 DUP2

SYS.DUP2(oldfd, newfd) ! Fdesc,Fdesc => Num

Duplicate the file descriptor 'oldfd' and make 'newfd' reference the same file. Therefore, all operations performed on one of them will also affect the other. If 'newfd' already references a valid descriptor, it will first be closed using T.CLOSE.

SYS.DUP2 returns zero upon success, and a negative number in case of an error.

See also: DUP, T.OPEN, T.CLOSE, PIPE, FORK

3.7.6 FINI

SYS.FINI() ! => 0

Shutdown the operating system interface and unload the shared object containing the interface routines. After calling SYS.FINI, the SYSTEM services become unavailable immediately.

See also: INIT

3.7.7 FORK

SYS.FORK()

Duplicate the calling process. The new process -- called the child process -- will start running exactly at the point where SYS.FORK returns. Each processes has an own data segment and an own set of file descriptors. Descriptors, which where open when SYS.FORK was called, will reference the same files in both processes, however.

After the successful creation of the new process, SYS.FORK returns 0 to the child process and the process ID of the child to the parent process.

In case of an error, it returns -1.

See also: KILL, SPAWN, T.OPEN

3.7.8 GETDIR

SYS.GETDIR(buf, len) ! Str,Num => Num

Store the fully qualified path name of the current working directory in 'buf'. Do not store more than the first 'len'-1 characters. Append a trailing NUL character. 'Buf' must be at least 'len' characters in length, and it may not be smaller than 65 characters.

If 'len' is less than 65 or the function fails, -1 is returned. Upon success, the number of stored characters is returned.

See also: MKDIR, RMDIR, OPENDIR

3.7.9 KILL

SYS.KILL(pid, sig) ! Num,Num => Num

Send a signal to the process with the process ID 'pid'. The following constants may be used in the place of 'sig' to specify which signal to send to the process:

Constant Action
SYSTEM.SIGTEST Test process
SYSTEM.SIGTERM Request termination
SYSTEM.SIGKILL Force termination

SYS.KILL returns zero, if the signal could be delivered sucessfully, and a negative number in case of an error.

Delivering SIGTEST does not have any effect. Therefore, it can be used to check whether the process with a given PID exists.

SIGTERM may be caught by the receiving process to initiate a clean shutdown.

SIGKILL terminates the receiving process immediately.

See also: FORK, SPAWN

3.7.10 MKDIR

SYS.MKDIR(path) ! Str => Num

Create a directory with the name 'path'. 'Path' is an operating system dependent path name.

SYS.MKDIR returns zero, if the directort could be created and otherwise a negative value.

See also: CHDIR, RMDIR, OPENDIR

3.7.11 OPENDIR

SYS.OPENDIR(path) ! Str => Ddesc | -1

Open the directory specified in 'path'. The exact format of 'path' depends on the underlying operating system.

Upon success, SYS.OPENDIR returns a directory descriptor and in case of an error, it returns -1.

See also: READDIR, CLOSEDIR, STAT

3.7.12 PIPE

SYS.PIPE(vec) ! Vec => Num

Create a pipe (a FIFO structure) and fill the vector 'vec' with two descriptors which can be used for accessing the pipe. Each element of 'vec' will be filled with an ordinary file descriptor as returned by T.OPEN. Therefore, the usual I/O operations can be used to read and write a pipe.

After the successful creation of a pipe, 'Vec[0]' will contain the output descriptor (which is read-only) and 'Vec[1]' will contain the input descriptor (which is write-only).

Data written to 'vec[1]' can be read from 'vec[0]'. Write requests will block, if the pipe is full and read requests will block, if the pipe is empty. The size of the pipe depends on the operating system.

SYS.PIPE returns zero when a pipe could be created and a negative value in case of an error.

See also: T.OPEN, T.CLOSE, T.READ, T.WRITE

3.7.13 RDCHK

SYS.RDCHK(fdesc) ! Fdesc => Num

Check whether input is available from the file descriptor 'fdesc' (whether a read operation on 'fdesc' would NOT block).

SYS.RDCHK returns a non-zero value, if the operation would succeed without blocking and zero, if the read request would block.

See also: T.READ

3.7.14 READDIR

SYS.READDIR(ddesc, buffer, lim) ! Ddesc,Str,Num => Num

Read the next directory entry from the directory descriptor 'ddesc' and fill 'buffer' with the name of that entry. If the name is longer than 'lim'-1 characters, truncate it to 'lim'-1 characters. In any case, terminate the name with a NUL character.

SYS.READDIR returns the length of the name read upon success, and -1 in case of an error. Reading beyond the end of the directory will also return -1.

See also: OPENDIR, CLOSEDIR, STAT

3.7.15 RMDIR

SYS.RMDIR(path) ! Str => Num

Remove the directory specified in 'path'. 'Path' is an operating system dependent path name.

SYS.RMDIR returns zero, if the directort could be removed and otherwise a negative value.

See also: CHDIR, MKDIR, OPENDIR

3.7.16 SPAWN

SYS.SPAWN(prog, args, mode) ! Str,Vec,Num => Num

Create a new process by running the program 'prog' with the command line options stored in 'args'. 'Prog' contains the path of the executable in a operating system dependent format. 'Args' is a vector of packed strings where each one contains one command line argument. The last vector element must be null. 'Mode' controls whether execution of the calling process will be suspended until the spawned process exits. The following modes exist:

SYSTEM.SPAWN_NOWAIT Execute the new process concurrently.
SYSTEM.SPAWN_WAIT Suspend the caller until the new process terminates.

NOTE: some operating systems may restrict the space which can be used for passing command line arguments.

NOTE2: on non-multitasking systems, SPAWN_NOWAIT may be unimplemented.

SYS.SPAWN returns the exit code of the subprocess when called with SPAWN_WAIT and zero when called with SPAWN_NOWAIT. In case of an error, it will return -1.

See also: FORK, WAIT

3.7.17 STAT

SYS.STAT(path, sb) ! Str,Statbuf => Num

Retrieve some information about the file specified in 'path'. The path is in a system dependent format. The retrieved information will be strored in a 'Statbuf' structure which has the following format:

Member Description
ST_DEV device ID
ST_INO inode number
ST_MODE access bits
ST_NLINK number of links
ST_UID user ID of owner
ST_GID group ID of owner
ST_RDEV device type
ST_SIZE file size in bytes
ST_EXT64 file size in 64K blocks
ST_MTIME date of last modification (8 bytes)
  Format: CYMDHMSh, see SYS.TIME().
ST_MT_2 \
ST_MT_3  > Buffer for ST_MTIME
ST_MT_4 /

Depending on the operating system, some fields will be filled with more or less meaning full standard values. For example, systems not supporting multiple links will fill the ST_NLINK field with 1.

The access field ST_MODE may have the following flags set:

Flag Description
FM_RDOK file is readable
FM_WROK file is writeable
FM_EXOK file is executable (*)
FM_ISDIR file is a directory

(*) DOS-files do not have an executable flag. Therefore, x|FM_EXOK is always zero on DOS systems.

SYS.STAT returns zero upon success and otherwise a negative value.

See also: OPENDIR, READDIR, CLOSEDIR

3.7.18 TIME

SYS.TIME(tbuf) ! Bvec => 0

Fill the buffer 'tbuf' with the current system time. 'Tbuf' must provide eight bytes of space which will be filled as follows:

Field Value Range
tbuf[0] year / 100 -
tbuf[1] year mod 100 -
tbuf[2] month 1...12
tbuf[3] day 1...31
tbuf[4] hour 0...23
tbuf[5] minute 0...59
tbuf[6] second 0...59
tbuf[7] second/100 0...99

SYS.TIME never fails and always returns 0. It might return an incorrect time, though, and on systems without a clock, it may fill 'tbuf' with the same values each time it is called.

3.7.19 WAIT

SYS.WAIT() ! => Num

Wait for a subprocess to terminate and returns its exit code.

See also: SPAWN, FORK

3.7 Video Terminal Control

OBJECT TTY[TTYCTL];

The TTYCTL class implements a set of routines for controlling character-based video terminals and reading keyboards. Procedures contained in this class include writes to the terminal screen, cursor movement, clearing and srolling screen regions, setting display colors (where available), and decoding keyboard input.

The TTYCTL routines must be initialized by a call to TTY.INIT and shut down by calling TTY.FINI.

This class does not contain any data.

All string arguments are packed.

3.8.1 INIT

TTY.INIT() ! => 0

Initialize the TTY control structures. This routine must be called before any other procedure of this class can be used. It performs the following steps (depending on the used operating system, some of these steps may be skipped):

+ Open the shared library TTYMOD.SO containing the low level TTY control routines.

+ Determine the user's terminal type (by evaluating the $TERM variable).

+ Check the terminal's color capability (by checking the existance of the $ANSICOLOR variable).

+ Extract some required properties, control strings, and keycodes from the /etc/termcap database.

+ Link the symbols contained in TTYMOD.SO.

TTY.INIT may fail for any of the following reasons:

+ TTYMOD.SO could not be opened.

+ $TERM is not set or its value is not a known TTY type.

+ One out of the following termcap entries is undefined: {co,li,ce,cl,cm,cs,sf,sr,se,so}.

+ A required procedure from TTYMOD.SO could not be linked.

In any of the above cases, an appropriate message will be printed and the program will be terminated.

See also: MODE

3.8.2 CLEAR

TTY.CLEAR() ! => 0

Clear the terminal screen using the currently selected color.

See also: CLREOL, COLOR

3.8.3 CLREOL

TTY.CLREOL() ! => 0

Clear all character right of the cursor in the current line using the currenly selected color.

See also: CLEAR, COLOR

3.8.4 COLOR

TTY.COLOR(color) ! Num => 0

Select a new foreground and background color. 'Color' is created by OR'ing together a foreground and a background color value. The following values exist (F_ indicates 'foreground' and B_ indicates 'background'):

F_BLACK, F_BLUE, F_GREEN, F_CYAN, F_RED, F_MAGENTA, F_YELLOW, F_GREY,
B_BLACK, B_BLUE, B_GREEN, B_CYAN, B_RED, B_MAGENTA, B_YELLOW, B_GREY

The special value F_BRIGHT may be OR'ed in to increase the intensity of the foreground color. For example,

tty.color(F_CYAN | B_BLUE | F_BRIGHT)

selects bright cyan color on blue background.

On monochrome terminals, only the color values

F_GREY|B_BLACK  and  F_BLACK|B_GREY

should be considered defined.

See also: SCREENTYPE

3.8.5 FINI

TTY.FINI() ! => 0

Shutdown the TTYCTL class. If the class is implemented using a shared module, unload this module.

See also: INIT

3.8.6 MODE

TTY.MODE(rawflag) ! Num => 0

Switch the terminal to 'raw mode'. Some terminals (especially in the Unix world) must be in 'raw mode' to allow to read single characters from them. In non-raw ('cooked') mode, reading a TTY device first returns when CR (aka ENTER,NL) is pressed on the terminal's keyboard. To make the read call return immedialtely after a key has been pressed, the TTY driver must be in raw mode.

TTY.MODE(1) selects raw mode and TTY.MODE(0) selects cooked mode.

These calls may have no effect on other platforms, but when switching a TTY driver to raw mode, it should be swicched back to cooked mode before terminating the program. Otherwise, the TTY driver may be left in an undesired state and make the TTY inaccessable.

On some systems, cooked mode may not be implemented. In this case, READC will always return after the first keypress.

See also: READC

3.8.7 MOVE

TTY.MOVE(x, y) ! Num,Num => 0

Move the cursor to the specified location (column 'x', row 'y'). If the specified coordinates do not exist on the used TTY, the result is undefined. Coordinates start at (0,0).

3.8.8 QUERY

TTY.QUERY() ! => Num

Check whether there are characters in the keyboard input buffer. If there are characters, TTY.READC would return when called at that moment. Otherwise, it would block.

TTY.QUERY returns -1, if there are characters in the buffer and otherwise 0.

See also: READC

3.8.9 READC

TTY.READC() ! => Num

Read a single character from the terminal's keyboard and return its keycode. For keys generating ASCII characters, the ASCII code of the key will be returned. 'Special' keys like the arrow keys, PREVIOS PAGE, NEXT PAGE, INSERT, DELETE, and the function keys return values above 255. The following symbols exist for matching special key codes:

Keycode Label(s)
K_HOME 'Home'
K_LEFT Left arrow
K_RGHT Right arrow
K_END 'End'
K_BKSP Backspace, <--, <X]
K_DEL 'Del', 'Delete', 'Remove'
K_KILL Control + 'U', Control + Backspace
K_INS 'Ins', 'Insert'
K_CR 'CR', 'Enter', 'Return', <-'
K_UP Up arrow
K_DOWN Down arrow
K_ESC 'ESC', 'Escape'
K_PREV 'Prev', 'PgUp', 'PageUp'
K_PGUP equal to K_PREV
K_NEXT 'Next', 'PgDn', 'PageDn'
K_PGDN = K_NEXT
K_F1 'F1'
K_F2 'F2'
K_F3 'F3'
K_F4 'F4'
K_F5 'F5'
K_F6 'F6'
K_F7 'F7'
K_F8 'F8'
K_F9 'F9'
K_F10 'F10'

Some systems may require to switch the TTY driver to raw mode before single characters can be read from a terminal.

See also: MODE, QUERY, WRITEC

3.8.10 RSCROLL

TTY.RSCROLL(top, bottom) ! Num,Num => 0

Scroll the screen region from 'top' to 'bottom' down one line. At the top of the region a blank line will be inserted using the currently selected color. Line numbers start at 0.

See also: SCROLL, SCREENTYPE

3.8.11 SCREENTYPE

TTY.SCREENTYPE(scr) ! Vec => Num

Determine the size and color capability of the connectec terminal screen. The retrieved information will be stored id a SCRN structure which has the following members:

Member Description
SCRN_X Number of columns per line
SCRN_Y Number of lines
SCRN_C nonzero = terminal supports ANSI color

SCREENTYPE returns zero upon success and a negative value, if the screen type could not be determined.

See also: COLOR, MOVE

3.8.12 SCROLL

TTY.SCROLL(top, bottom) ! Num,Num => 0

Scroll the screen region from 'top' to 'bottom' up one line. At the bottom of the region a blank line will be inserted using the currently selected color. Line numbers start at 0.

See also: RSCROLL, SCREENTYPE

3.8.13 WRITEC

TTY.WRITEC(c) ! Num => Num

Write the character 'c' to the terminal screen and return it. The character will be output at the current position of the cursor. Writing a character advances the cursor. When the cursor is in the rightmost column when writing a character, the cursor position is undefined after the output operation.

See also: READC, WRITES

3.8.14 WRITES

TTY.WRITES(string) ! Str => 0

Write a string on the terminal screen as if each character of the string had been written using TTY.WRITEC. However, TTY.WRITES is usually faster than the character-oriented WRITEC method.

See also: WRITEC, READC

3.9 External Memory Access

3.9.1 USAGE

OBJECT XM[XMEM];
XM.INIT();

The XMEM class provides access to external memory blocks. An external memory block is a continous region of memory not contained in the T3X data area. XM blocks are byte addressed. Bytes in XM blocks can only be read and written through the procedures XM.GET and XM.PUT defined by this class.

The XMEM class does have a state which is implemented in the shared object part. Therefore, only one single instance of the class may be loaded.

Since each external memory block must be completely adressable using Tcode machine words, their sizes may not exceed 65536 bytes.

This class is implemented as a shared object. Therefore, it must be initialized by XM.INIT before its use and it may be unloaded by calling XM.FINI.

3.9.2 INIT

XM.INIT() ! => 0

Initialize the external memory interface. Basically, this routine loads the shared object containing the actual interface procedures. XM.INIT will fail, if the shared object could not be opened. In this case, the calling program will be halted.

See also: FINI

3.9.3 ALLOC

XM.ALLOC(length) ! => id | -1

Allocate a block of external memory with a size of LENGTH bytes. Upon success, return an identifier which may be used in subseuqent XM operations to access the block. In case of an error (out of memory / out of IDs), return -1.

See also: FREE

3.9.4 FREE

XM.FREE(id) ! => 0 | -1

Release a previously allocated external memory block. ID is an identifier returned by XM.ALLOC().

See also: ALLOC

3.9.5 GET

XM.GET(id, index) ! => value

Return the byte stored at address INDEX of the external memory block referenced through ID. INDEX may not exceed X-1 where X is the size of the block as specified at allocation time.

See also: ALLOC, PUT.

3.9.6 PUT

XM.PUT(id, index, value) ! => value

Replace value of the byte stored at address INDEX of the external memory block referenced through ID with VALUE. INDEX may not exceed X-1 where X is the size of the block as specified at allocation time. All but the least significant 8 bits of VALUE will be discarded.

See also: ALLOC, GET.

3.10 Release 5 Compatibility

#R5;

The R5C class is an internal class providing some compatibility with T3X releases prior to R6. Release 5 (and earlier) programs used a different interface for accessing runtime support procedures and therefore, a special wrapper class is necessary to access the R5 runtime routines.

In R5, runtime procedures are always predefined in the global context while they are contained in classes (and must be explicitly required) in R6. When including the meta command #R5; in a program, the T3XR6 compiler will create some R5-style internal declarations, thereby allowing to use for example the procedure WRITEPACKED() instead of the T3X class method T.WRITE().

When compiling a program with R5 support enabled, it must be linked against the R5C class.

The R5C class *cannot be required*, since no class directory for it exists. The class file contains a special wrapper which translates R5-style procedure calls into R6-style messages. See the documentation of the MKR5LIB utility for details.

The supported procedures include the T3-compliant ('built-in') procedures and the BASIC T3X extensions. However, they do not include the video terminal (VIO) or vector graphics (GRAPHICS) extensions. Programs using the VIO routines must be rewritten to use the TTYCTL class (which should not be too hard). Currently, a graphics class for T3XR6 does not exist.

3.10.1 Limitations

Using the R5C compatibility mechanism, it is not possible to compile pre-R6 program which depend on extended versions of the Tcode interpreter using custom slot assignments.

The Release 6 Compiler does not evaluate interface statements. However, it does parse them and reports assignments to slot numbers above 20. Therefore, pre-R6 program using the basic extentions may be compiled safely. Support for these extensions is contained in the R5C class.

When including a #R5 command, the basic extension procedures will be available as if the following INTERFACE statement was present:

interface readpacked(3) = 11,
 writepacked(3),
 reposition(4),
 rename(2),
 memcopy(3),
 memcomp(3);

3.10.2 Compatibility Procedures

This is a summary of all supported release 5 compability procedures supported by the R5C class. Basically, this is an excerpt from the T3X-R5 manual. It has been included to facilitate the conversion of programs to the new class oriented runtime system, if necessary. The described procedures are obsolete and using them in future projects is not recommended. A pointer to an R6 procedure, which should be used instead, can be found in each of the following descriptions.

READS(Buffer, Count) -- String, Number => Number
Superseded by: T.READ()

This procedure reads up to count characters from the currently selected input stream and unpacks the string read into buffer so that each character will occupy one machine word. It will also append a trailing NUL word to properly terminate the string. Because READS() uses an internal buffer, count may not be larger than 1024. The procedure returns the number of characters actually read. When reading from a terminal, entering a newline character will terminate the READS() call on most systems. In this case, the length of the line read will be returned. A return code of zero usually indicates the end of the input file or the input of an EOF character. A result less than zero indicates general failure.

READS() may read binary data containing NUL characters. Therefore, measuring the length of the string in buffer to determine the number of characters received is not guaranteed to work.

At startup time, the input port will be connected to the terminal of the user who has started the program, so that READS() will read the user's keyboard.

WRITES(String) -- String => Number
Superseded by: T.WRITE()

WRITES() packs string into an internal buffer and then writes the packed string to the currently selected output stream. All but the least significant 8 bits of each word in string will get truncated. WRITES() determines the number of characters to write by counting the words in string up to a delimiting NUL word. Since it checks for a NUL word, WRITES() can be used to write binary data: Setting bit #8 (by an | operation med 256) makes a NUL character a non-delimiter. Since the 8th bit will be truncated, however, a NUL-character will be written.

WRITES() returns the number of characters actually written. Any value below the length of string indicates general failure. Due to the use of an internal buffer, WRITES() may not write strings with a length of more than 1024 characters.

At startup time, the output port will be connected to the terminal of the user who has started the program, so that WRITES() will write to the user's screen.

NEWLINE() => 0
Superseded by: T.NEWLINE()

This procedure writes a system-dependent newline sequence to the currently selected output stream ("\n" on Unix and Plan9 and "\r\n" on DOS, for example).

SELECT(Port, Fd) -- Number, Descriptor => Descriptor
Superseded by: N/A (use T.OPEN(), T.CLOSE(), etc)

This routine selects a new input or output stream. If port is zero, it selects an input stream and if port is non-zero, it selects an output stream. Fd is the file descriptor of the new I/O stream. A valid file descriptor may be obtained by an OPEN() call. Alternatively, one of the standard desciptors 0, 1, 2 may be used. 0 denotes `standard input' (the user's keyboard), 1 denotes `standard output' (the user's screen), and 2 describes the `standard error' port which is usually associated with the terminal screen, too. SELECT() replaces the currently selected input or output port with fd without checking whether it is a valid descriptor. It returns the descriptor which was in effect before the call so that it may be restored later.

OPEN(Path, Mode) -- String, Number => Descriptor
Superseded by: T.OPEN()

OPEN() opens the file whose path name is specified in the unpacked string path in the given mode. Mode is a numeric value specifiying how to open the file and what operations will be allowed on it. The following table summarizes the possible values of mode:

Mode Allow write Allow read If file
nonexistant
Append to file
0 No Yes Fail No
1 Yes No Create No
2 Yes Yes Fail No
3 Yes No Fail Yes

If the OPEN() call succeeds, the procedure will return a file descriptor for the opened or newly created file. This file descriptor can be passed to SELECT() or one of the new T3X I/O procedures. When the descriptor is no longer used, it should be destroyed using CLOSE().

If OPEN() fails for some reason, it will return the value -1 which is not a valid descriptor.

CLOSE(Fd) -- Descriptor => Number

Superseded by: T.CLOSE()

This procedure deletes a file descriptor created by OPEN(). It returns zero upon success and a negative value, if an invalid descriptor has been passed to it. A closed descriptor becomes invalid immediately and may no longer be used. All pending I/O operations will be performed on the file associated with fd before the descriptor is deletd. CLOSE() may not be used to close one of the standard descriptors 0, 1, and 2.

ERASE(Path) -- String => Number
Superseded by: T.REMOVE()

This routine deletes the file whose path name is specified in the unpacked string path. It returns zero on success and a negative value, if the file does not exist or the user has no permission to delete it. On systems providing multiple links per file (like Unix and its descendants), ERASE() only deletes the specified link.

ATON(Numstr) -- String => Number
Superseded by: STR.STRTONUM()

ATON() parses the unpacked string numstr and converts it into a numeric value. First, it skips all leading white space characters (\f, \n, \r, \s, \t). Then, it checks for a leading minus sign. Either - or % is recognized. Only a single sign may occur. Finally, the procedure collects all decimal digits following the optional spaces and sign and computes the value represented by the string. It returns the computed value. If a non-numeric string is passed to ATON(), it returns zero. Note: the return value zero may stand for both, non-numeric input or any valid string containing a zero value (like "0").

NTOA(Value, Width) -- Number, Number => String
Superseded by: STR.NUMTOSTR()

This procedure creates a readable representation of the numeric object value in an internal buffer and returns a pointer to the generated string. The representation of the number will be unpacked. If width is larger than the number of characters required by the string representing value, the string will be filled to a length of width using blank characters (\s). If value is negative, the numeric string will be prefixed with an ordinary minus sign (-).

Because of the use of an internal buffer, the maximum value for width is limited to 255. Also, the procedure is not reentrant. Therefore, the statememt

concat(ntoa(a,0), ntoa(b,0));

will pass two times ntoa(b,0) to the -- fictious -- procedure concat, and not ntoa(a,0) and ntoa(b,0). If multiple values computed by NTOA() shall be used in one expression, the results must be saved in user-supplied buffers before the evaluation of the expression takes place.

PACK(S, P) -- String, Pstring => Number
Superseded by: T.PACK()

PACK() packs the unpacked string S into P by copying each machine word from S into a byte in P. Of course, all but the least significant 8 bits of each machine word get lost in P. S and P may denote the same location. In this case, S will be packed `in situ' and its original content will be overwritten. PACK() returns the number of machine words required by the packed string P (including the terminating NUL character and padding bytes). P must provide enough space to hold the packed string. Any value which is greater than half the length of S is safe. The exact length of the packed string P is

length of S + BPW - 1
---------------------
         BPW

where BPW is the number of bytes per machine word on the target machine. For Tcode programs, BPW=2 applies on all platforms.

UNPACK(P, S) -- Pstring, String => Number
Superseded by: T.UNPACK()

This procedure unpacks the packed string P into S by copying each byte from P into a separate machine word in S. Each character will be extended by filling all but its least significant eight bits with zeroes. Because unpacking a string expands its size, P and S may not reference the same location. If they do, the result of UNPACK() is undefined. The procedure returns the number of machine words required to store the unpacked string including the terminating NUL word. S must provide enough space to hold the unpacked string. Generally, four times the length of P plus 1 is safe on 16- and 32-bit systems. To compute the exact amount of required storage, reverse the formula in the description of PACK().

INTERFACE WRITEPACKED(3) = 12;
WRITEPACKED(Fd, Buffer, Len) -- Fdesc, Vec, Num => Num

Superseded by: T.WRITE()

This routine writes len bytes from buffer to the file (or device) associated with the file descriptor fd. A valid descriptor may be obtained from OPEN(). The standard descriptors may be used, too (see SELECT()). Unlike in the T3 I/O procedures, the file descriptor is specified explicitly here. SELECT() has no effect on the extended I/O procedures. WRITEPACKED() returns the number of bytes actually written. Any return value which is less than len indicates failure.

INTERFACE READPACKED(3) = 11;
READPACKED(Fd, Buffer, Len) -- Fdesc, Vec, Num => Num

Superseded by: T.READ()

This procedure reads up to len bytes from the file (or device) associated with the file descriptor fd into buffer. A file descriptor may be obtained from OPEN(). The standard descriptors explained in the SELECT() entry may be used, too. Like in all extended I/O procedures, the file descriptor is specified explicitly and therefore, SELECT() has no effect on this procedure. READPACKED() returns the number of bytes actually read. When reading from a terminal device, the return value may be smaller than len, since data is transferred line by line and READPACKED() may return when a newline character is encountered. A zero return value indicates that the end of the input file has been reached or an EOF character has been typed. A negative return value indicates general failure.

INTERFACE REPOSITION(4) = 13;
REPOSITION(Fd, PosH, PosL, How) -- Fdesc, Num, Num, Num => Num

Superseded by: T.SEEK()

REPOSITION() moves the file pointer of the file descriptor fd to a new position. The file pointer always points to the offset in the file where the next read/write operation will take place. PosH and posL contain the new position. PosH specifies its high word and posL its low word. Consequently, the new position will be

PosH .* 65536 + PosL

The value of how determines the method of moving the pointer:

How Method
0 From the beginning of the file
1 Relative to current position
2 From the end of the file

When moving from the beginning of the file, the position must be positive and when moving from the end, it must be negative. Note that negative values must be 32-bit values. To move back 1024 bytes, for example, use

REPOSITION(fd, %1, %1024, 1);

The procedure returns zero on success and a negative value in case of failure.

INTERFACE RENAME(2) = 14;
RENAME(Old, New) -- String, String => Number

Superseded by: T.RENAME()

Rename changes the name of the directory entry stored in old to new. Both strings are unpacked. When a path is specified in old, only the last part of the path -- the file name -- will be changed to new. New may not contain any path separators. RENAME() returns zero upon success and a negative value in case of failure.

INTERFACE MEMCOPY(3) = 15;
MEMCOPY(D, S, L) -- Vector, Vector, Number => 0

Superseded by: T.MEMCOPY()

MEMCOPY() copies L bytes from the source vector S to the destination vector D. Since it cannot fail under normal circumstances, it does not return a meaningful value. Of course, MEMCOPY() could be easily implemented as a T3X program itself, but the runtime support routine is written in assembly language for higher efficiency. The regions A and B may overlap.

INTERFACE MEMCOMP(3) = 16;
MEMCOMP(R1, R2, L) -- Vector, Vector, Number => Number

Superseded by: T.MEMCOMP()

This routine compares up to L bytes at corresponding position in R1 and R2. When L positions have been compared, the both memory regions are considered equal and MEMCOMP() returns zero. When two different bytes are found during the comparison, the procedure stops comparing and returns the difference between the two values

R1::p - R2::p

where p is the current position. Therefore, MEMCOMP() can be used for lexically sorting strings, for example.

Like MEMCOPY(), MEMCOMP() has been implemented in assembly language for efficiency reasons.

4. The Virtual Tcode Machine

The Tcode machine is the target of the reference implementation of T3X. Tcode may be interpreted as well as transformed into native code. It also provides mechanisms for static linking so that multiple Tcode modules can be linked together forming one single program. Since version 3, support for object oriented programming is built into the virtual Tcode machine. This chapter describes Tcode3 and its virtual machine in detail.

4.1 The Architecture

The Tcode machine is a virtual 16-bit machine basically consisting of the following parts:

+---------------+FFFFh                   FFFFh+---------------+
|               |                             |     Stack     |
|               |                             |      and      |
|  U n u s e d  |       +-------------+  +--->|    Dynamic    |
|               |   +---|     IP      |  |    |    Storage    |
|               |   |   +-------------+  | +->|- - - - - - - -|
|- - - - - - - -|   |   |     RR      |  | |  |               |
|               |   |   +-------------+  | |  |    F r e e    |
|               |<--+   |     FP      |--+ |  |               |
| Tcode Program |       +-------------+    |  |  M e m o r y  |
|               |       |     SP      |----+  |               |
| Instructions  |       +-------------+       |- - - - - - - -|
|               |          REGISTERS          |    Static     |
|               |                             |     Data      |
|               |                             |               |
+---------------+0000h                   0000h+---------------+
   CODE ARRAY                                    DATA ARRAY
Fig.6 The Architecture of the Tcode Machine

There are two byte-addressable memory regions called the code array and the data array. The code array holds the Tcode program which is to be executed and the data array is used to hold the data used by the program. Each cell in one of the arrays is completely addressable using a 16-bit pointer. Therefore, the maximum capacity per array is 65536 bytes.

Machine words -- which are always 2 bytes wide -- are stored with the least significant byte in the cell with the higher address: 1234h = 34h 12h (little endian byte ordering).

The Tcode machine has five 16-bit wide special purpose registers which are outlined in the following overview.

FP, the Frame Pointer.

The frame pointer always points to the stack frame (aka context) of the currently running procedure. FP is implicitly used by the instructions LDL, LDLV, SAVL, INCL which address local objects. FP is modified only by HDR and END instructions. See also calling conventions.

IP, the Instruction Pointer.

This register always points to the instruction which will be interpreted next. IP is interpreted as an offset into the code array. It cannot be accessed directly, but only through jump, call, and branch instructions.

RR, the Return Register.

This register is used to transport procedure results back to the caller. The return register is loaded by a POP instruction and saved by CLEAN. See also calling conventions.

SELF, the class context pointer.

SELF points to the instance context which is currently in effect. This is equal to the first byte of the data space of the object which is currently receiving a message. Instance contexts are static. They are established using CCTX and released using ECTX. The SELF register is used by the LDI and LDIV instructions to compute the addresses of instance variables. See also instance contexts.

SP, the Stack Pointer.

The stack pointer points to the object most recently placed on the stack. Moving an object onto the stack implicitly decreases SP by 1 machine word. Removing an object increases it by 1 machine word. SP may be explicitly modified using the STACK instruction.

The Tcode machine instructions can be divided into the following nine groups:

Declarations, external linkage, and debug instructions will be processed only once (therefore, they may be resolved in a preprocessing step). This means that an instruction like

STR 5 H e l l o

will not create a new string literal each time it is interpreted, but only at the first time. (One also might think of this behaviour as creating the same object each time a declaration executed).

Arithmetic instructions and predicates expect their arguments on the runtime stack and also place their results there. Since there are no general purpose registers, most operations are performed on stack elements.

4.2 Calling Conventions

A procdure should always begin with a HDR instruction which saves the caller's context and creates a new stack frame and end with an END instruction which restores the saved stack frame and jumps back to the caller.

A procedure call

P(a, b, c)

where a, b and c be global variables, is coded as follows:

LDG a LDG b LDG c CALL LP CLEAN 3

Each LDG instruction loads the value of a global variable onto the stack. CALL performs the procedure call which returns with its result in the return register RR. LP denotes the label tagging the procedure P. The final CLEAN instruction removes the three arguments from the stack and replaces them with the value returned in RR so that the top stack element finally holds the procedure return value.

Each called procedure may expect the following stack configuration:

FP+M Argument #1
FP+3 Argument #N-1
FP+2 Argument #N
FP+1 Return Address (saved by CALL or CALR)
FP+0 Old SP (saved by HDR)
FP-1 Local Variable #1
FP-2 Local Variable #2
FP-J Local Variable #K
SP ( Free Memory below )

Note that the arguments are passed to the procedure in reverse order with the first argument at the highest address. Both, arguments and local variables may be accessed using LDL instructions. Given the above context,

LDL -M

would access the first argument.

LDL -2

always loads the value of the last argument, if any. Local storage is accessed using positive offets:

NUM 25 SAVL 2

would load the second local variable with the value 25.

4.3 Instance Contexts

A method is a procedure used for manipulating the data of an object. In addition to the usual procedure frame as described in the previous section, it should contain instructions to shift and restore the instance context which is held in the SELF register. Since CCTX (create class context) expects a new context at FP+2, it must be preceeded by HDR and therefore, a method frame looks as follows:

CLAB procedure-label
HDR
CCTX
... code ...
CLAB exit-label
ECTX
END

Passing a message m with three arguments to a global object O

O.m(1,2,3);

would be coded as follows:

NUM 1 NUM 2 NUM 3 LDGV LO CALL Lm CLEAN 4

Each called method may expect the following stack configuration:

FP+M Argument #1
FP+4 Argument #N-1
FP+3 Argument #N
FP+2 Receiver's Address
FP+1 Return Address (saved by CALL or CALR)
FP+0 Old SP (saved by HDR)
FP-1 Sender's Address (Old Instance Context, saved by CCTX)
FP-2 Local Variable #1
FP-3 Local Variable #2
FP-J Local Variable #K
SP ( Free Memory below )

4.4 Instruction Cycles

A cycle is the set of operations which is required to execute one single Tcode instruction. Each cycle consists of the following steps:

  1. Load the instruction pointed to by IP. Increment IP by 1.
  2. If the instruction has an operand (indicated by bit #7 set), load the machine word pointed to by IP into an internal register and increment IP by 2.
  3. If the loaded instruction is an INCG, INCI, INCL, or INIT command, load another machine word (pointed to by IP) into another internal register and increment IP by 2.
  4. If the loaded instruction is valid, execute it, otherwise signal an error and halt the machine.

These steps are repeated until the Tcode machine is halted by executing HALT.

If an instruction modifies stack elements, first all its operands are removed from the stack, then the operation denoted by the instruction is performed, and finally the result is placed back on the stack.

Declaration instructions may have more than two arguments. See the section about declarations for details.

4.5 Startup Conditions

This sections describes the state of the Tcode machine when it is started.

4.6 Conventions Used in the Following Sections

Symbol Size
(bits)
Description
M N 16 a generic numeric value.
.N 16 an unsigned numeric value.
L 16 a label tagging a data word or a procedure.
E 16 a labels referencing an external procedure.
C 8 a character.
X1...XN var a vector containing N elements of the type X.
memory[X] 16 the content of the X'th machine word in the data array.
memory::X 8 the content of the X'th byte in the data array.
S0...SN 16 the N+1 elements most recently pushed onto the stack.

Annotations Normally, the most significant bit of each machine word is interpreted as a sign flag (1 indicates a negative number). The leading dot notation .N indicates that the MSB of N should be treated as a part of the value instead of a sign indicator.

An address is an offset into the code or data array where the base (code or data array) is implicitly determined by the associated instruction. Adresses are 16 bits wide.

There is a relation between labels (L) and addresses (A). When a label Lx tags a specific instruction at the address Ay, then Lx and Ay are exchangeable. In the following sections, L will be used to denote the creation of or the reference to a label while the notation A will be used to denote a reference to a location tagged by a label.

External labels are used to create a connection between the name of an external procedure and a reference to such a procedure. External label IDs are 16 bits wide.

S0 denotes the element most recently pushed onto the stack. When popping elements from a stack holding N+1 elements, S0 will be removed first and SN will be removed last.

4.7 Declarations

81 CLAB L Code LABel.
Define a label identified by the value L which tags the following procedure.

85 CREF L Code REFerence.
Define a word-size storage location holding the address of the procedure tagged by the label L.

84 DATA N DATA definition.
Define a word-size storage location containing the value N.

83 DECL N vector DECLaration.
Define a vector with a length of N machine words and undefined content.

82 DLAB L Data LABel.
Define a label identified by the value L which tags the following data object.

86 DREF L Data REFerence.
Define a word-size storage location holding the address of the data object tagged by the label L.

89 INIT N L INITialize.
Originally used to initialize the Tcode environment -- hence its name. Each program must begin with this instruction. The argument N specifies the Tcode version the program complies to. This document describes version 3 of the Tcode language. L is a code label tagging the initial entry point of the Tcode module containing the instruction. This label may be evaluated by a Tcode linker.

88 PSTR N C1 ... CN define Packed STRing.
Define a vector with a length of

(N+2) / 2

machine words containing the characters C1 through CN. Each character is stored in a separate byte, not machine word. All trailing bytes of the vector are filled with zeroes so that a properly terminated packed string is created.

87 STR N C1 ... CN define STRing.
Define a vector with a length of N+1 machine words containing the values C1 through CN. The last vector member always holds the value zero. The primary use of this instruction is the creation of string literals.
Note: Although the created string holds one character per machine word, the initialization data of STR is packed and contains one character per byte.

4.8 Context Manipulation

0C CCTX Create class ConTeXt.
Push the context of the sending method or procedure (SELF) and then change the instance context by loading SELF with the machine word pointed to by FP+2 (the first argument passed to the answering method).

0D ECTX End class ConTeXt.
Load SELF with the value previously saved on the stack by a CCTX instruction, thereby restoring the instance context of the sender. The sender's context will be removed from the stack.

0B END END procedure.
Remove two elements S0 and S1. Restore the context of the calling procedure by loading FP with S0 and then perform a branch to S1. S1 is usually a return address which has been saved by a CALL or CALR instruction.

0A HDR HeaDeR.
Push the context of the calling procedure (FP) and create a fresh procedure context by loading FP with SP.

4.9 Stack Manipulation

92 CLEAN N CLEAN up arguments.
Remove N procedure arguments from the stack:

SP := SP + N*2

and then push the content of RR, the return register.

0F DUP DUPlicate
Push the current top of the stack (S0), thereby duplicating it.

0E POP
Pop the top element S0 and load it into the return register RR.

91 STACK N
Move the stack pointer SP by N machine words:

SP := SP - N*2.

Moving the stack pointer `down' (N>=1) allocates space on the stack, moving it `up' (N<=-1) deallocates space. STACK is primarily used to allocate and release dynamic memory in procedures.

10 SWAP
Exchange the values of S0 and S1.

4.10 Arithmetic Instructions

1E ADD
Remove two elements S0 and S1 and push their sum: S1+S0.

20 BAND Bitwise AND.
Remove two elements S0 and S1, perform a bitwise AND on them and push the result: S1 & S0.

15 BNOT Bitwise NOT.
Invert each bit in the top element: ~S0.

21 BOR Bitwise OR.
Remove two elements S0 and S1, perform a bitwise OR on them and push the result: S1 | S0.

23 BSHL Bitwise SHift Left.
Remove two elements S0 and S1, shift the bits of S1 to the left by S0 positions and push the result: S1 << S0.

24 BSHR Bitwise SHift Right.
Remove two elements S0 and S1, shift the bits of S1 to the right by S0 positions and push the result: S1 >> S0.

22 BXOR Bitwise eXclusive OR.
Remove two elements S0 and S1, perform a bitwise XOR on them and push the result: S1 ^ S0.

1A DIV integer DIVide.
Remove two elements S0 and S1, compute the (signed) integer part of their quotient and push it: S1 / S0. If S0=0, signal a fatal error and halt.

97 INCG A N INCrement Global.
Add the value N to the value of the memory cell located at the address A. This is exactly the same as

LDG A NUM N ADD SAVG A

but more efficient.

98 INCI M N INCrement Instance variable.
Add the value N to the value of the memory cell whose absolute address is SELF + M*2. INCI M N is equal to

LDI M NUM N ADD SAVI M

but more efficient.

99 INCL M N INCrement Local.
Add the value N to the value of the memory cell whose absolute address is FP - M*2. INCL M N is equal to

LDL M NUM N ADD SAVL M

but more efficient.

14 LNOT Logical NOT.
If the top element is equal to zero, replace it with -1, otherwise with zero: S0=0-> -1: 0.

1D MOD MODulo.
Remove two elements S0 and S1, compute their division remainder and push it: S1 MOD S0. S1 MOD S0 is defined as S1 - S1/S0*S0 where `/' denotes an integer division. If S0=0, signal a fatal error and halt.

1B MUL MULtiply.
Remove two elements S0 and S1, compute their (signed) product and push it: S1 * S0. Do not perform any overflow checking.

13 NEG NEGate.
Negate the top element: -S0.

16 NOP NO Operation.
Rest for a cycle.

1F SUB SUBtract.
Remove two elements S0 and S1 and push their difference: S1-S0.

1C UDIV Unsigned integer DIVide.
Remove two elements S0 and S1, compute the unsigned integer part of their quotient and push it: .S1 / .S0. If S0=0, signal a fatal error and halt.

1B UMUL Unsigned MULtiply.
Remove two elements S0 and S1, compute their unsigned product and push it: .S1 * .S0. Do not perform any overflow checking.

4.11 Predicates

25 EQU EQUal.
Remove two elements S0 and S1. Push true, if they are equal and otherwise false: S1=S0-> -1: 0.

28 GRTR GReaTeR than.
Remove two elements S0 and S1. Push true, if S1 is greater than S0 and otherwise false: S1>S0-> -1: 0. S0 and S1 are both signed.

2A GTEQ Greater Than or EQual to.
Remove two elements S0 and S1. Push true, if S1 is greater than or equal to S0 and otherwise false: S1>=S0-> -1: 0. S0 and S1 are both signed.

27 LESS LESS than
Remove two elements S0 and S1. Push true, if S1 is less than S0 and otherwise false: S1<S0-> -1: 0. S0 and S1 are both signed.

29 LTEQ Less Than or EQual to.
Remove two elements S0 and S1. Push true, if S1 is less than or equal to S0 and otherwise false: S1<=S0-> -1: 0. S0 and S1 are both signed.

26 NEQU Not EQUal.
Remove two elements S0 and S1. Push true, if they are not equal and otherwise false: S1\=S0-> -1: 0.

2C UGRTR Unsigned GReaTeR than.
Remove two elements S0 and S1. Push true, if .S1 is greater than .S0 and otherwise false: .S1>.S0-> -1: 0.

2E UGTEQ Unsigned Greater Than or EQual to.
Remove two elements S0 and S1. Push true, if .S1 is greater than or equal to .S0 and otherwise false: .S1>=.S0-> -1: 0.

2B ULESS Unsigned LESS than
Remove two elements S0 and S1. Push true, if .S1 is less than .S0 and otherwise false: .S1<.S0-> -1: 0.

2D ULTEQ Unsigned Less Than or EQual to.
Remove two elements S0 and S1. Push true, if .S1 is less than or equal to .S0 and otherwise false: .S1<=.S0-^gt; -1: 0.

4.12 Load and Store Instructions

3A DEREF DEREFerence.
Remove two elements S0 and S1 and compute the absolute address of the S0'th member of the vector pointed to by S1: S0*2 + S1. Push the computed address. When used together with IND, this instruction dereferences the S0'th member of S1. Hence the name of this instruction.

3B DREFB DeREFerence Byte.
Remove two elements S0 and S1 and compute the absolute address of the S0'th byte of the vector pointed to by S1 (S0+S1). Push the computed address. See also DEREF.

38 IND INDirect access.
Replace the top element with the value stored in the storage cell pointed to by the top element: S0 := memory[S0/2].

39 INDB INDirect Byte access.
Replace the top element with a machine word which contains the zero-extended byte located at the address contained in the top element: S0 := memory::S0.

B0 LDG A LoaD Global.
Push the value stored in the memory cell with the address A: memory[A/2].

B1 LDGV A LoaD Global Vector.
Push the address A.

B4 LDI N LoaD Instance variable.
Push the value located at memory[(SELF/2)+N]. This instruction is used to load the content of an instance variable. N specifies the offset of the variable relative to the beginning of the object's data area.

B5 LDIV N LoaD Instance Vector.
Push the address SELF+N*2. This instruction is used to load the address of an instance variable. N specifies the offset of the variable relative to the beginning of the currently addressed object's data area.

B2 LDL N LoaD Local.
Push the value stored at the N'th position `below' the stack frame base. The address of the cell is computed as follows:

FP - N*2

Consequently, negative values of N may be used to access locations `above' the frame base.

B6 LDLAB A LoaD LABel.
Push the address A. This instruction is similar to LDGV, but it is also used to reference anonymous vectors and procedures.

B3 LDLV N LoaD Local Vector.
Push the absolute address of a local object. N specifies the offset of the object to the stack frame base. The absolute address is computed using the formula FP - N*2.

AE NUM N load NUMber.
Push the value N.

BD SAVG A SAVe Global.
Pop one element and save it in the memory cell with the address A: memory[A/2] := S0.

BF SAVI N SAVe Instance variable.
Pop one element and save it in the memory cell

memory[(SELF/2)+N]

This instruction is used to alter the state of an instance variable. N specifies the offset of the variable relative to the beginning of the currrently addressed object's data area.

BE SAVL N SAVe Local.
Pop one element and save it in the storage cell with the address FP - N*2. See also LDL.

37 SELF SELF reference.
Push the content of the SELF register onto the stack.

40 STORB STORe Byte.
Pop two elements S0 and S1 and store the least significant 8 bits of S0 in the byte pointed to by S1: memory::S1 := S0.

3F STORE
Pop two elements S0 and S1 and store the value S0 in the memory cell pointed to by S1: memory[S1/2] := S0.

4.13 Flow Control

C6 BRF A BRanch on False.
Remove the element S0 and branch to the address A, if S0 is false.

C7 BRT A BRanch on True.
Remove the element S0 and branch to the address A, if S0 is true.

C2 CALL A procedure CALL.
Push the current value of the instruction pointer IP and then perform a branch to the code array location A.

42 CALR CALl through Register.
Push the current value of the instruction pointer IP and then remove one element and perform a branch to the location it points to. The destination is implicitly located in the code array of the program.

CB DNEXT A Downward NEXT.
Remove two elements S0 and S1 and branch to the address A, if S1 <= S0. (*)

C4 SYS N SYStem call.
Execute the system procedure associated with the index value N. The index value is removed and a call-dependent number of arguments is passed to the respective system procedure. The system procedure may return a machine word size return value in the return register RR. The stack must me cleaned up using CLEAN after calling a system procedure. System procedures are implemented as methods of the class system. Therefore, they should be invoked only by sending them as a message to an instance of the system class. The exact semantics of SYS depend on the called procedure. The index values are not compatible with the EXEC codes of previous Tcode versions.

CC HALT N
Instantly halt the Tcode machine. The least significant eight bits of N will be delivered to the invoker of the program as a return code.

C9 JUMP A
Unconditionally jump to the address A.

C7 NBRF A Nondestructive BRanch on False.
Branch to the address A, if S0 is false. Do not remove S0.

C8 NBRT A Nondestructive BRanch on True.
Branch to the address A, if S0 is true. Do not remove S0.

CA UNEXT A Upward NEXT.
Remove two elements S0 and S1 and branch to the address A, if S1 >= S0. (*)

(*) The instructions UNEXT and DNEXT have been designed for use in counting loops (FOR-NEXT loops in BASIC). The idea is as follows: At the end of the loop, the current loop index and the loop limit are both pushed onto the stack. UNEXT compares the values and branches out of the loop, if index>=limit. DNEXT branches, if index<=limit. Therefore, UNEXT is used in upward counting loops and DNEXT is used in countdown loops.

4.14 External Linkage

CF CALX E CALl eXternal procedure.
Call a procedure contained in a different module. There must exist an EXT record defining the external label E in the same module and a PUB record with the same name in another module, so that the external reference can be resolved.

CE EXT E N C1 ... CN EXTernal reference.
Create an external reference to the symbol represented by the characters C1 through CN. E is a so-called external label. Such labels are used to reference external symbols in CALX instructions. See the section on separate compilation for details. Notice: When interpreting a Tcode program, this instruction should lead to an error.

CD PUB L N C1 ... CN PUBlic reference.
Signal a Tcode linker that the procedure tagged by the label L is public and can be referenced externally using the name formed by the characters C1 through CN. Again, see the section on separate compilation for details. When interpreting Tcode programs, this instruction type may be ignored safely.

4.15 Source Level Debugging Support

54 CKSTK ChecK for STacK overflow.
Check whether the stack pointer SP points to a location with an address lower than the highest location occupied by a static data object (a location 'below' the end of the static data area). In this case, a stack overflow has occured and the Tcode machine will be halted and an appropriate message will be displayed.

D1 GSYM L N C1...CN Global SYMbol.
Name a global symbol. C1 through CN contain the single characters of the symbol name. L is the ID of the label which marks the named symbol. GSYM instructions should be generated only for global variable names.

D3 ISYM M N C1...CN Instance SYMbol.
Name an instance variable. C1 through CN contain the single characters of the symbol name. M is the offset in machine words (into the classes data space) of the data object named by the symbol.

D0 LINE N LINE number.
The following instructions have been created from line N in the source code this Tcode program has been created from.

D2 LSYM M N C1...CN Local SYMbol.
Name a local symbol. C1 through CN contain the single characters of the symbol name. M is a signed number holding the position of the variable relative to the value of FP.

4.16 Loading Tcode

The Tcode3 definition provides a set of instructions for binding together modules which have been separately compiled to Tcode. External references are limited to procedure calls. This means that a module can call procedures defined in an external module, but it cannot access an external module's data.

To call an external procedure, the label which tags the entry point of the routine must be declared public (using a PUB instruction) in the module containing the called routine. In the module of the caller, it must be declared extern (using EXT). The 'extern' declaration creates a so-called external label which may be referenced by calls to external procedures (CALX instructions).

The instruction PUB provides a symbolic name for a procedure. This symbolic name may be referenced by EXT in a different module. CALX is used to reference an EXT instruction defined in the same module. An external reference is resolved in four steps:

  1. The external label, which is the operand of a CALX instruction, is looked up in the external symbol table (the table holding the EXT records).
  2. The name contained in the matching EXT record is looked up in the public symbol table (the table holding the PUB records).
  3. The label contained in the matching PUB record replaces the external label in the CALX instruction.
  4. CALX is replaced with CALL.

The following figure illustrates the principle of external references.

+----------------------------+      +----------------------------+
|                            |      |                            |
|    ,--> EXT E 3 _p_ >=================> PUB L 3 _p_            |
|    |                       |      |     CLAB L HDR ... END     |
|    '-------.-.-----------, |      |                            |
|            | |           | |      |                            |
|  CALX E >--' |  CALX E >-' |      |                            |
|              |             |      |                            |
|     CALX E >-'             |      |                            |
|                            |      |                            |
+----------------------------+      +----------------------------+
       Caller's Module                     Callee's Module
Fig.7 External References

Additional Notes

Since labels are represented by integers in Tcode, label collisions will occur when binding two (or more) Tcode modules together. Therefore, labels must be renamed in this case. When a module A has been loaded and a module B is to be loaded, the highest label ID used in A should be added to each (non-external) label in B.

Two or more EXT records with the same name may exist, because the same symbol may be associated with different external labels in different modules.

The existance of two PUB records with the same name is considered an error (redefinition error).

There must exist a matching PUB record to each EXT record. Otherwise, an error must be signalled (unresolved external).

5. Quick Reference

5.1 Language Overview

5.1.1 Declarations

Statement Description
VAR name, ... ; Define atomic variables
VAR name[cexpr], ... ; Define vectors with size = cexpr
VAR name::cexpr, ... ; Define byte vectors with size = cexpr
VAR name[structname], ...; Define structured vectors
CONST name = cexpr, ... ; Define constants
PUBLIC CONST ... Define class constants
STRUCT name = m1, ... mN ; Define structure name with members m1...mN
PUBLIC STRUCT ... Define class constants
CLASS name(req-module, ...)
class-declarations
END
Define a class and its dependencies
PUBLIC CLASS ... Define a public class
OBJECT name[classname], ... ; Define instance of class classname
DECL name(cexpr), ... ; Declare procedures (type = cexpr)
name(a1, ..., aN) stmt Define a procedure
PUBLIC name(a1, ..., aN) stmt Define a public procedure (method)
INTERFACE name(cexpr) = slot,
name(cexpr), ...
Define some interface procedures (obsolete, do not use)
MODULE name(req-module, ...) Name a module and define dependencies

5.1.2 Statements

Statement Description
IF (expr) stmt Run stmt, if expr is true
IE (expr) stmt-1 ELSE stmt-2 Run stmt-1, if expr is true and stmt-2, if expr is false
WHILE (expr) stmt Run stmt while expr is true
FOR (var=start, limit, cexpr)
stmt
Count from start to limit-cexpr using cexpr increments. If cexpr is negative, count to limit+cexpr. Run stmt in each step. start and limit are expressions.
FOR (var=start, limit) stmt Short for FOR (var=start, limit, 1) ...
LEAVE; Leave the innermost WHILE or FOR loop
LOOP; Restart the innermost WHILE or FOR loop.
In FOR loops, go to the increment step.
RETURN expr; Return expr to the calling procedure.
RETURN; Short for RETURN 0;
HALT cexpr; Terminate program with eit code cexpr
HALT; Short for HALT 0;
lvalue := expr; Assign value of expr to lvalue
name(a1, ..., aN); Call procedure name
CALL ptr(a1, ..., aN); Indirect procedure call through ptr
SEND(ptr, classname,
name(a1, ..., aN));
Send message name to object of class classname pointed
to by ptr
DO decl ...; stmt ... END Compund statement. Declaration and statements are both
optional.
; Empty statement

5.1.3 Operators

Operator Prec Assoc Description
(expr) 0 - Give a (sub)expression the highest possible precedence
name(a1, ..., aN) 0 L The value of applying procedure name to the arguments
a1 through aN
name[expr] 0 L The expr'th element of the vector name. [expr] may be repeated.
name::expr 0 R The expr'th byte of the vector name.
@lvalue 1 R The address of lvalue.
~X 2 R The bitwise complement of X
\X 2 R The logical complement of X
-X 2 R The negative value of X
X * Y 3 L The product of X and Y
X / Y 3 L The quotient of X and Y
X MOD Y 3 L The division remainder of .X and .Y
X .* Y 3 L The unsigned product of .X and .Y
X ./ Y 3 L The unsigned division remainder of .X and .Y
X + Y 4 L The sum of X and Y
X - Y 4 L The difference between X and Y
X & Y 5 L The bitwise logical product X AND Y
X | Y 5 L The bitwise logical sum X AND Y
X ^ Y 5 L The bitwise logical neg. equiv. X XOR Y
X << Y 5 L X shifted to the left by Y bits
X >> Y 5 L X shifted to the right by Y bits
X < Y 6 L True, if X is less than Y
X <= Y 6 L True, if X is less than or equal to Y
X > Y 6 L True, if X is greater than Y
X >= Y 6 L True, if X is greater than or equal to Y
X .< Y 6 L True, if .X is less than .Y
X .<= Y 6 L True, if .X is less than or equal to .Y
X .> Y 6 L True, if .X is greater than .Y
X .>= Y 6 L True, if .X is greater than or equal to .Y
X = Y 7 L True, if X is equal to Y
X \= Y 7 L True, if X is not equal to Y
X /\ Y 8 L Short circuit log. AND: 0 if X=0; Y, if X\=0
X \/ Y 9 L Short circuit log. OR: X if X\=0; Y, if X=0
X -> Y : Z 10 L Conditional expr: Y, if X\=0; Z, if X=0

5.1.4 Meta Commands

Meta command Description
#L number "filename"; Set internal line counter to number and input file
name to filename. For preprocessors.
#CLASSPATH "path"; Specify an alternative location for searching class files.
#DEBUG; Turn on emission of debug information.
#PACKSTRINGS; Automatically pack all strings.
#R5; Turn on T3X Release 5 compatibility.

5.1.5 The Formal Syntax

%token SYMBOL, NUMBER, CHARACTER, STRING

%%
Program:
	Declarations
	CompoundStmt
	;

Declarations:
	Declaration
	| Declaration Declarations
	;

Declaration:
	'VAR' VarList ';'
	| 'CONST' ConstList ';'
	| 'PUBLIC' 'CONST' ConstList ';'
	| 'DECL' ProtoList ';'
	| 'EXTERN' 'DECL' ProtoList ';'
	| 'INTERFACE' InterfaceList ';'
	| 'STRUCT' SYMBOL '=' SymList ';'
	| 'PUBLIC' 'STRUCT' SYMBOL '=' SymList ';'
	| FunctionDecl
	| ClassDecl
	| 'PUBLIC' ClassDecl
	| 'OBJECT' ObjDeclList ';'
	| 'MODULE' SYMBOL '(' SymList ')' ';'
	| 'MODULE' SYMBOL '(' ')' ';'
	;

VarList:
	VarDecl
	| VarList ',' VarDecl
	;

VarDecl:
	SYMBOL
	| SYMBOL '[' ConstValue ']'
	| SYMBOL '::' ConstValue
	;

ConstList:
	ConstDef
	| ConstDef ',' ConstList
	;

ConstDef:
	SYMBOL '=' ConstValue
	;

SymList:
	SYMBOL
	| SymList ',' SYMBOL
	;

ClassDecl:
	'CLASS' SYMBOL '(' SymList ')' InstanceDecls 'END'
	'CLASS' SYMBOL '(' ')' InstanceDecls 'END'
	;

InstanceDecls:
	InstanceDecl
	| InstanceDecl InstanceDecls
	;

InstanceDecl:
	'VAR' VarList ';'
	| 'CONST' ConstList ';'
	| 'DECL' ProtoList ';'
	| 'EXTERN' 'DECL' ProtoList ';'
	| 'INTERFACE' InterfaceList ';'
	| 'STRUCT' SYMBOL '=' SymList ';'
	| FunctionDecl
	| 'PUBLIC' FunctionDecl
	| 'OBJECT' ObjDeclList ';'
	;

ObjDeclList:
	SYMBOL '[' SYMBOL ']'
	| SYMBOL '[' SYMBOL ']' ',' ObjDeclList
	;

InterfaceList:
	InterfaceDef
	| InterfaceDef ',' InterfaceList
	;

InterfaceDef:
	ProtoDef
	| ProtoDef '=' ConstValue
	;

ProtoList:
	ProtoDef
	| ProtoDef ',' ProtoList
	;

ProtoDef:
	SYMBOL '(' ConstValue ')'
	;

FunctionDecl:
	SYMBOL '(' OptFormalArgs ')' Statement
	;

OptFormalArgs:
	ArgumentList
	|
	;

ArgumentList:
	SYMBOL
	| SYMBOL ',' ArgumentList
	;

Statement:
	CompoundStmt
	| SYMBOL OptSubscripts ':=' Expression ';'
	| SYMBOL '::' Factor ':=' Expression ';'
	| 'CALL' SYMBOL FunctionCallApp ';'
	| SYMBOL '.' SYMBOL FunctionCallApp
	| 'SEND' '(' SYMBOL ',' SYMBOL ',' SYMBOL FunctionCallApp ')'
	| 'IF' '(' Expression ')' Statement
	| 'IE' '(' Expression ')' Statement 'ELSE' Statement
	| 'WHILE' '(' Expression ')' Statement
	| 'FOR' '(' SYMBOL '=' Expression ',' Expression ')' Statement
	| 'FOR' '(' SYMBOL '=' Expression ',' Expression ',' ConstValue ')'
		Statement
	| 'LEAVE' ';'
	| 'LOOP' ';'
	| 'RETURN' ';'
	| 'RETURN' Expression ';'
	| 'HALT' ';'
	| 'HALT' ConstValue ';'
	| ';'
	;

CompoundStmt:
	'DO' 'END'
	| 'DO' LocalDecls 'END'
	| 'DO' StatementList 'END'
	| 'DO' LocalDecls StatementList 'END'
	;

LocalDecls:
	LocalDecl
	| LocalDecl LocalDecls
	;

LocalDecl:
	'VAR' VarList ';'
	| 'CONST' ConstList ';'
	| 'STRUCT' SYMBOL '=' SymList ';'
	| 'OBJECT' ObjDeclList ';'
	;

StatementList:
	Statement
	| Statement StatementList
	;

ExprList:
	Expression
	| Expression ',' ExprList
	;

Expression:
	Disjunction
	| Disjunction '->' Expression ':' Expression
	;

Disjunction:
	Conjunction
	| Disjunction '\/' Conjunction
	;

Conjunction:
	Equation
	| Conjunction '/\' Equation
	;

Equation:
	Relation
	| Equation '=' Relation
	| Equation '\=' Relation
	;

Relation:
	BitOperation
	| Relation '<' BitOperation
	| Relation '>' BitOperation
	| Relation '<=' BitOperation
	| Relation '>=' BitOperation
	| Relation '.<' BitOperation
	| Relation '.>' BitOperation
	| Relation '.<=' BitOperation
	| Relation '.>=' BitOperation
	;

BitOperation:
	Sum
	| BitOperation '&' Sum
	| BitOperation '|' Sum
	| BitOperation '^' Sum
	| BitOperation '<<' Sum
	| BitOperation '>>' Sum
	;

Sum:
	Term
	| Sum '+' Term
	| Sum '-' Term
	;

Term:
	Factor
	| Term '*' Factor
	| Term '/' Factor
	| Term '.*' Factor
	| Term './' Factor
	| Term 'MOD' Factor
	;

Factor:
	NUMBER
	| STRING
	| Table
	| 'PACKED' STRING
	| 'PACKED' PackedTable
	| SYMBOL FunctionCallApp
	| 'CALL' SYMBOL FunctionCallApp
	| 'SEND' '(' SYMBOL ',' SYMBOL ',' SYMBOL FunctionCallApp ')'
	| SYMBOL OptSubscripts
	| SYMBOL '::' Factor
	| SYMBOL '.' SYMBOL
	| SYMBOL '.' SYMBOL FunctionCallApp
	| '@' SYMBOL
	| '-' Factor
	| '\' Factor
	| '~' Factor
	| '(' Expression ')'
	;

OptSubscripts:
	| Subscripts
	;

Subscripts:
	'[' Expression ']'
	| '[' Expression ']' Subscripts
	;
	
Table:
	'[' MemberList ']'
	| '[' ']'
	;

MemberList:
	TableMember
	| TableMember ',' MemberList
	;

TableMember:
	ConstValue
	| STRING
	| Table
	| '@' SYMBOL
	| '(' Expression ')'
	;

PackedTable:
	'[' PackedTableMembers ']'
	| '[' ']'
	;

PackedTableMembers:
	PackedTableMember
	| PackedTableMember ',' PackedTableMembers
	;

PackedTableMember:
	SYMBOL
	| Number
	;

FunctionCallApp:
	'(' ')'
	| '(' ExprList ')'
	;

ConstValue:
	SimpleConst
	| ConstValue '+' SimpleConst
	| ConstValue '*' SimpleConst
	| ConstValue '|' SimpleConst
	;

SimpleConst:
	SYMBOL
	| Number
	| '-' SimpleConst
	| '~' SimpleConst
	;

Number:
	NUMBER
	| CHARACTER
	;
%%

5.2 Runtime Support Routines

5.2.1 T3X

Procedure Description
T.BPW() Return the number of bytes per machine word on the
host machine
T.CLOSE(fd) Close file descriptor FD
T.CVALIST(n, bmap, in, out) Convert argument list (internal)
T.GETARG(n, buf, max) Copy MAX characters of command line argument #N into BUF
T.GETENV(name, buf, max) Copy MAX characters of the value of the environment
variable NAME to BUF
T.MEMCOMP(r1, r2, len) Compare LEN bytes at the region R1 and R2
T.MEMCOPY(dest, src, len) Copy LEN bytes from SRC to DEST
T.MEMFILL(dest, char, len) Fill LEN bytes at DEST with CHAR
T.MEMSCAN(src, char, lim) Search LIM bytes at SRC for CHAR
T.NEWLINE(buf) Fill BUF with a system-depdant newline sequence
T.OPEN(path, mode) Open file PATH in the given MODE
T3X.OAPPND
T3X.ORDWR
T3X.OREAD
T3X.OWRITE
append mode (write-only)
read/write mode
read-only mode
create/overwrite mode (write-only)
T.PACK(vector, string) Pack an unpacked string or vector
T.READ(fd, buf, len) Read LEN bytes from FD into BUF
T.REMOVE(path) Delete (a link to) the file PATH
T.RENAME(old, new) Rename the file OLD to NEW
T.SEEK(fd, pos, org) Move the file pointer of FD by POS bytes starting at ORG.
T3X.SEEK_END
T3X.SEEK_REL
T3X.SEEK_SET
origin: end of file
origin: current position
origin: beginning of file
T.SHLCALL(sym, alist) Call dynamically loaded procedure SYM with argument list ALIST
T.SHLCLOSE(shld) Close a shared library descriptor
T.SHLOPEN(path) Open the shared library with the given PATH
T.SHLSYM(shld, name) Extract symbol NAME from a shared library
T.UNPACK(string, vector) Unpack a packed string or byte vector
T.WRITE(fd, buf, len) Write LEN characters from BUF to FD

5.2.2 Char

Procedure Description
CHR.INIT() Initialize the CHAR class
CHAR.C_ALPHA
CHAR.C_CNTRL
CHAR.C_DIGIT
CHAR.LOWER
CHAR.C_SPACE
CHAR.C_UPPER
property: alphabetic
property: control character
property: decimal digit
property: lower case character
property: while space character
property: upper case character
CHR.ALPHA(char) Alphabetic character test
CHR.ASCII(char) ASCII character test
CHR.CNTRL(char) Control character test
CHR.DIGIT(char) Numeric character test
CHR.LCASE(char) Lower case character test
CHR.MAP() Return map of character properties
CHR.SPACE(char) White space character test
CHR.UCASE(char) Convert CHAR to upper case, if it is lower case
CHR.UPPER(char) Convert CHAR to lower case, if it is upper case

5.2.3 IOStream

Procedure Description
IOS.CLOSE() Close a stream.
IOS.CREATE(fd, buf, len, mode) Create a stream using the buffer BUF with the given length LEN and connect it to the file descriptor FD. MODEs are described under IOS.OPEN.
IOS.EOF() Return true, if the input is exhausted.
IOS.FLUSH() Write buffered data to the associated descriptor.
IOS.MOVE(offset, origin) Move the stream pointer to the given OFFSET starting at ORIGIN.
IOSTREAM.SEEK_END
IOSTREAM.SEEK_REL
IOSTREAM.SEEK_SET
origin: end of file
origin: current position
origin: beginning of file
IOS.OPEN(path, buf, len, mode) Like IOS.CREATE(), but open PATH instead of connecting the stream to an existing descriptor. The following open MODEs exist:
IOSTREAM.FADDCR
IOSTREAM.FKILLCR
IOSTREAM.FRDWR
IOSTREAM.FREAD
IOSTREAM.FTRANS
IOSTREAM.FWRITE
mode: Add CR before LF on output
mode: Kill CRs on input
mode: Open file in read/write mode
mode: Open file read-only
mode: Apply both FADDCR and FKILLCR
mode: Create file in write-only mode
IOS.RDCH() Read and return a single character.
IOS.READ(buf, len) Read up to LEN characters into BUF.
IOS.READS(buf, len) Read a line with up to LEN-1 characters into BUF.
IOS.WRCH(char) Write a single character.
IOS.WRITE(buf, len) Write LEN characters from BUF.
IOS.WRITES(str) Write a string.

5.2.4 Memory

Procedure Description
MEM.INIT(pool, size) Initialize a new memory POOL. A pool of the specified SIZE must be supplied.
MEM.WALK(block, sizep, statp) Walk the block list starting at BLOCK (0=beginning of pool). The vectors SIZEP and STATP will be filled with size and status.
MEM.ALLOC(size) Allocate a block of the given SIZE
MEM.FREE(block) Free a previously allocated BLOCK

5.2.5 String

Procedure Description
STR.COMP(s1, s2) Compare strings
STR.COPY(dest, src) Copy SRC to DEST
STR.FIND(str, pat) Find position of PAT in STR
STR.FORMAT(buf, tmpl, list) Format BUF according to TMPL using the arguments in LIST
STR.LENGTH(str) Compute the length of a string
STRING.MAXLEN The maximum string length
STR.NUMTOSTR(buf, n, radix) Create a string representing N using the given RADIX. Store the result in BUF
STR.PARSE(str, tmpl, list) Extract data described in TMPL from STR. Store results in the elements pointed to by LIST.
STR.RSCAN(str, char) Find the rightmost occurence of CHAR in STR
STR.SCAN(str, char) Find the first occurence of CHAR in STR
STR.STRTONUM(s, radix, lp) Convert the string S which contains a numeric string with the given RADIX into a value. Fill LP with the position of the first non-digit.
STR.XLATE(str, old, new) Replace each character OLD in STR with NEW

5.2.6 System

Procedure Description
SYS.INIT() Initialize the system interface.
SYS.FINI() Shutdown the system interface.
SYS.CHDIR(path) Change the curreent working dir to PATH.
SYS.CLOSEDIR(dirfd) Close a directory file descriptor.
SYS.DUP(fd) Duplicate file descriptor FD.
SYS.DUP2(old, new) Duplicate file descriptor OLD to NEW. If NEW already is open, close it first.
SYS.FORK() Duplicate the calling process.
SYS.KILL(pid, sig) Send signal SIG to process PID.
SYSTEM.SIGKILL
SYSTEM.SIGTERM
SYSTEM.SIGTEST
signal: kill process
signal: terminate process
signal: test process id
SYS.MKDIR(path) Create new directory.
SYS.OPENDIR(path) Open a directory file descriptor.
SYS.PIPE(fdvec) Create a pipe. FDVEC[0] is filled with input end and FDVEC[1] with the input descriptor.
SYS.RDCHK(fd) Check FD for pending characters.
SYS.READDIR(dirfd, buf, max) Read the first MAX characters of the next directory entry (file name) from DIRFD into BUF.
SYS.RMDIR(path) Remove the given directory.
SYS.SPAWN(prog, args, mode) Spawn program PROG as a subprocess passing the arguments point to by ARGS to it. MODE may be one of the following:
SYSTEM.SPAWN_NOWAIT
SYSTEM.SPAWN_WAIT
mode: create background process
mode: wait for process termination
SYS.STAT(path, sb) Retrieve statistics for file PATH.
SYSTEM.STATBUF Structure for file statistics (SB).
SYSTEM.ST_DEV
SYSTEM.ST_EXT
SYSTEM.ST_GID
SYSTEM.ST_INO
SYSTEM.ST_MODE
SYSTEM.ST_MTIME
SYSTEM.ST_MT_2
SYSTEM.ST_MT_3
SYSTEM.ST_MT_4
SYSTEM.ST_NLINK
SYSTEM.ST_RDEV
SYSTEM.ST_SIZE
SYSTEM.ST_UID
SB: device ID
SB: size of file in full 64K blocks
SB: group ID of owner
SB: inode number
SB: permission flags
SB: modification time: YYMDHMSh
SB: 8-byte MTIME buffer
SB: 8-byte MTIME buffer
SB: 8-byte MTIME buffer
SB: number of links
SB: device type
SB: size mod 64K
SB: user ID of owner
SYS.TIME(tbuf) fill TBUF with system time (YYMDHMSh).
SYS.WAIT() Wait for subprocess termination.

5.2.7 TTYCtl

Procedure Description
TTY.INIT() Initialize the TTY interface.
TTY.FINI() Shutdown the TTY interface.
TTY.CLEAR() Clear the terminal screen.
TTY.CLREOL() Clear the current line starting at the cursor position.
TTY.COLOR(color) Select COLOR for TTY writes.
TTYCTL.B_BLACK TTYCTL.B_BLUE
TTYCTL.B_CYAN
TTYCTL.B_GREEN
TTYCTL.B_GREY
TTYCTL.B_MAGENTA
TTYCTL.B_RED
TTYCTL.B_YELLOW
background colors
TTYCTL.F_BLACK
TTYCTL.F_BLUE
TTYCTL.F_CYAN
TTYCTL.F_GREEN
TTYCTL.F_GREY
TTYCTL.F_MAGENTA
TTYCTL.F_RED
TTYCTL.F_YELLOW
foreground colors
TTYCTL.F_BRIGHT Foreground intensity flag
TTY.MODE(raw) Select raw or cooked mode (where available).
TTY.MOVE(x, y) Move the cursor to the given coordinates.
TTY.QUERY() Check for pending characters in the keyboard buffer.
TTY.READC() Read a single character from the keyboard. Special keys return special codes listed here:
TTYCTL.K_BKSP
TTYCTL.K_CR
TTYCTL.K_DEL
TTYCTL.K_DOWN
TTYCTL.K_END
TTYCTL.K_ESC
TTYCTL.K_F1
TTYCTL.K_F2
TTYCTL.K_F3
TTYCTL.K_F4
TTYCTL.K_F5
TTYCTL.K_F6
TTYCTL.K_F7
TTYCTL.K_F8
TTYCTL.K_F9
TTYCTL.K_F10
TTYCTL.K_HOME
TTYCTL.K_INS
TTYCTL.K_KILL
TTYCTL.K_LEFT
TTYCTL.K_NEXT
TTYCTL.K_PGDN
TTYCTL.K_PGUP
TTYCTL.K_PREV
TTYCTL.K_RIGHT
TTYCTL.K_UP
Backspace
Enter / CR / Return
Delete
Down arrow
End
Escape
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
Home
Insert
Kill, Erase line
Left arrow
Next, Page down
Next, Page down
Prev, Page up
Prev, Page up
Right arrow
Up arrow
TTY.RSCROLL(top, bot) Scroll down the lines from TOP to BOT.
TTY.SCREENTYPE(scrn) Determine the screen type. Return a SCRN struct:
TTYCTL.SCRN SCRN struct
TTYCTL.SCRN_C
TTYCTL.SCRN_X
TTYCTL.SCRN_Y
SCRN: terminal has color
SCRN: number of columns
SCRN: number of lines
TTY.SCROLL(top, bot) Scroll up the lines from TOP to BOT.
TTY.WRITEC(char) Write a single character to the screen.
TTY.WRITES(string) Write a string of characters to the screen.

5.2.8 XMem

Procedure Description
XMEM.INIT() Initialize the XMEM interface.
XMEM.FINI() Shutdown the XMEM interface.
XMEM.ALLOC(size) Allocate an external memory block of SIZE bytes.
XMEM.FREE(id) Release an allocated block.
XMEM.GET(id, index) Read a byte from address INDEX of the block referenced by ID.
XMEM.PUT(id, index, value) Change the INDEX'th byte of block ID to VALUE.

5.2.9 R5C

Procedure Description
ATON(string) Convert numeric string to value.
CLOSE(fd) Close file descriptor.
ERASE(path) Erase disk file.
MEMCOMP(r1, r2, len) Compare LEN bytes of memory regions R1 and R2.
MEMCOPY(dest, src, len) Copy LEN bytes from SRC to DEST.
NEWLINE() Emit a system-dependent newline sequence.
NTOA(expr, width) Create a decimal string representing EXPR and pad to WIDTH with blanks.
OPEN(file,mode) Open FILE in given MODE. 0=read, 1=write, 2=read/write 3=1+append.
PACK(vector, string) Pack a vector or unpacked string.
READPACKED(fd, buf, len) Read up to LEN bytes from FD into BUF.
READS(string, max) Read up to MAX characters from the current input port and store them in STRING.
RENAME(old, new) Rename file OLD to NEW.
REPOSITION(fd, ph, pl, org) Move the file pointer of FD to PH*65536+PL starting at ORG.
SELECT(port, fd) Select a new input (PORT=0) or output (PORT=1) port.
UNPACK(string, vector) Unpack a packed string or byte vector.
WRITEPACKED(fd, buf, len) Write LEN characters from BUF to FD.
WRITES(string) Write string to the current output port.

5.3 Miscellaneous

5.3.1 Escape Sequences

Escape
Seqences
ASCII
Code
Name Description
\a \A 0x07 BEL Ring terminal bell
\b \B 0x08 BS Backspace
\e \E 0x1B ESC Introduce control sequence
\f \F 0x12 FF Form feed
\n \N 0x0A LF Line feed
\q \Q \" 0x22 - Literal quote character
\r \R 0x0D CR Carriage return
\t \T 0x09 HT Horizontal tabulator
\v \V 0x0B VT Vertical tabulator
\\ 0x5C - Literal backslash

5.3.2 The Tcode Instruction Set

  00 10 20 30 40 50
00 - SWAP SUB LDG* STORE LINE*
01 CLAB* STACK* BAND LDGV* STORB GSYM**
02 DLAB* CLEAN* BOR LDL* CALL* LSYM**
03 DECL* NEG BXOR LDLV* CALR ISYM**
04 DATA* LNOT BSHL LDI* SYS* CKSTK
05 CREF* BNOT BSHR LDIV* BRF* -
06 DREF* NOP EQU LDLAB* BRT* -
07 STR** INCG*2 NEQU NUM* NBRF* -
08 PSTR** INCI*2 LESS SELF NBRT* -
09 INIT*2 INCL*2 GRTR IND JUMP* -
0A HDR MUL LTEQ INDB UNEXT* -
0B END DIV GTEQ DEREF DNEXT* -
0C CCTX UMUL ULESS DREFB HALT* -
0D ECTX UDIV UGRTR SAVG* PUB** -
0E POP MOD ULTEQ SAVL* EXT** -
0F DUP ADD UGTEQ SAVI* CALX* -

* instruction has 1 argument.
*2 instruction has 2 arguments.
** instruction has 2 arguments and a string attached.
In any of these cases, add 0x80 to the opcode.

6. License

This manual is part of the T3X compiler package which is distributed under the following terms.

T3X -- A Compiler for the Minimum Procedural Language T3X
Copyright (C) 1996-2000 Nils M Holm. All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.