Playing TRAC

Richard Suchenwirth 2004-04-25 - More programming language archeology: here are some experiments to re-live aspects of the venerable TRAC language ("Text Reckoning and Compilation"), dating back to about 1964, in Tcl. Some quotes from "The Beginner's Manual for TRAC Language" (1972):

  • "TRAC language is characterized as being an interactive, macrogenerator, evaluative string language"
  • "The language employs a single data type, namely the type 'string'"
  • Extension with application packages "written in FORTRAN, COBOL, PL/I or in any other language" was at least discussed.
  • "A collection of expressions or commands in TRAC language is called a 'script'"

One big difference to Tcl is that substitutions of "forms" (the TRAC unification for both variables and commands) happen not only once, but as long as substitution changes something. This trampoline effect can be had in Tcl with calling subst in a loop - I added some trace support:

 if ![info exists Trace] {set Trace 0}
 proc TN {} {set ::Trace 1}
 proc TF {} {set ::Trace 0}

 proc trac form {
    while 1 {
      set new [subst $form]
       if {$new eq $form} break
       if $::Trace {puts $form->$new}
       set form $new
    }
    set form
 }

To test this multiple substitution, here is an integer range generator that does not use recursion (because the inner call to itself is escaped with a backslash, and hence returned to its caller - who in turn re-substitutes it). Such iteration avoids the interp recursionlimit, yet allows to write quasi-recursive code.

 proc range n {
    if $n {return "\[range [expr $n-1]] $n"}
 }

... And it works as expected:

   % range 5
   [range 4] 5
   % trac {foo [range 5] bar}
   foo  1 2 3 4 5 bar

Note that foo is not a command here - TRAC leaves its input untouched, except for substitutions.

TRAC is visually quite different from Tcl. The test above would, in real TRAC (classic T-64), look like

 foo #(CL,range,5) bar'

where CL is the built-in "primitive" to call a form, the command and its arguments are separated by commas, and enclosed in #(...) to indicate it shall be substituted, just like brackets in Tcl. The meta-character apostrophe (') terminates the input and has it evaluated.

Below I play with some primitives that are named with two-letter uppercases as in TRAC, but the syntax is still Tcl-ish, so that I can use our parser - the TRAC assignment of a "form" to a variable, Define String,

 #(DS,foo,42)

is in this "Tractcl" dialect written as

 [DS foo 42]

and it's evident that an alias for set does the job for now:}

 interp alias {} DS {} set
 interp alias {} DD {} unset

Forms are not only for variables, but for procedures as well. As discussed in half-lambda, I save the SS (Segment String) step by directly using numeric argument names ($1, $2, ...), so a single body string can be evaluated with arguments. (But see below why SS is still desirable...) The range generator comes out like this:

 DS rg {[GR $1 0 "\[CL rg [SU $1 1]] $1" {}]}
#-- using one of the two TRAC conditionals:
 proc GR {1 2 3 4} {
    expr {$1 > $2?[trac $3]:[trac $4]}
 }
 proc EQ {1 2 3 4} {
    expr {$1 eq $2?[trac $3]:[trac $4]}
 }

The CalL primitive assigns the arguments, and a few extra ones, to numeric variables, and substitutes the named form:

 proc CL {formVar args} {
    if [info exists ::$formVar] {
       set i 0
       foreach a $args {set [incr i] $a}
       foreach a {. . . .} {set [incr i] ""}
       subst [set ::$formVar]
    } ;# else do nothing
 }

Here are TRAC's binary arithmetic (strictly Polish, as in Lisp - #(AD,5,10) is "5+10") as well as boolean operators (the latter used unmarked octals for both input and output), and I/O routines. TRAC-64 numbers were very special, as they allowed a non-digit prefix, which is ignored in computation, but added before the result, e.g.

 #(AD,cats 11,dogs-3)'cats 8

A number with all prefix and no digits defaults to 0. I wrote a generic proc that takes the operator and two operands:

 proc trac-arith {op 1 2} {
    set 1 [trac $1]
    regexp {(.+\D)??([-+]?\d*)$} $1 -> pr 1
    if {$1 eq ""} {set 1 0}
    set 2 [trac $2]
    regexp {(.+\D)??([-+]?\d*)$} $2 -> - 2
     if {$2 eq ""} {set 2 0}
     return $pr[expr $1 $op $2]
 }
#-- The specific "primitives" are just curried:
 foreach {prim op} {AD + SU - ML * DV /} {
    interp alias {} $prim {} trac-arith $op
 }
#-- Testing arithmetics with factorial:
 DS fac {[GR $1 1 "\[ML $1 \[CL fac [SU $1 1]]]" 1]}
#--- Boolean (bitwise) operations:
 proc BU {1 2} {
    format %o [expr 0$1 | 0$2]
 }
#-- Input (RS, Read String) and output (PS, Print String)
 proc PS form {
    if [info exists ::$form] {
       puts [set ::$form]
    } else {
       puts [trac $form]
    }
 }
 proc RS {}   {gets stdin}

Re-reading the TRAC book convinced me that the SS primitive ("segment string") isn't so weird after all - besides argument numbering, it can also be used, together with CL, for substring substitution. So here it is - each argument is replaced with a Tcl variable reference ${1}, ${2}, ..., which are in turn re-substituted by CL - see the example at bottom:

 proc SS {formVar args} {
    upvar #0 $formVar form
    set i 0
    foreach a $args {
       set form [string map [list $a "\${[incr i]}"] $form]
    }
 }
#-- Testing:
 PS {foo [CL rg 5] bar}
 PS [AD "cats 11" dogs-8]
 PS [BU 403 1526]
 PS [CL fac 5]
 DS try {This is a bad example.}
 SS try bad
 puts $try ;# see it modified
 PS [CL try good]

produces on stdout:

 foo 1 2 3 4 5 bar
 cats 3
 1527
 120
 This is a ${1} example.
 This is a good example.

Boy, I'm beginning to like TRAC - and Tcl even more, as it allows to experiment with "foreign languages" pretty effortlessly... But TRAC has its drawbacks, too:

  • forms are always global
  • segmentation loses the original substrings
  • often forms have to be referred to by name, not as pure values