TINT is a library implementation of the ancient TRAC programming language ("Text Reckoning And Compiling") by Calvin Mooers from 1959.
TINT is a macro language suitable for embedding in text. This OCaml package includes:
cat(1)
that evaluates TINT expressions in its inputSee the documentation for the TINT primitive functions.
You'll have to do some hunting to learn how to program in TINT; this description of TRAC is your starting point:
Calvin N. Mooers: "TRAC, A Procedure-Describing Language for the Reactive Typewriter", Communications of the ACM 9:3, pp. 215-219 (March 1966).
Let's implement a minimal version of the command-line program tint(1)
included with this package. It reads standard input (only) and evaluates TINT expressions line-by-line (only).
In main
we initialize the TINT state using Eval.init
, and then fold over the input lines and the state accumulator with the eval
function.
eval
passes each line
to Eval.eval
, printing the evaluated line. If there was an error, we print a message to standard error.
It's usually important, when looping or folding over the state, to call Eval.reset
to reset the active and neutral strings in the state.
let eval state line = match Tint.Eval.eval (Tint.Eval.reset state) line with | Error err -> Tint.Types.string_of_error err |> prerr_endline; state | Ok (state, result) -> print_endline result; state let main () = let each_inline () = match In_channel.input_line stdin with | None -> None | Some line -> Some (line, ()) in match Tint.Eval.init Tint.Eval.prims (Tint.Eval.Forms.forms ["os",Sys.os_type]) with | Error err -> prerr_endline err; exit 1 | Ok state -> Seq.unfold each_inline () |> Seq.fold_left eval state |> ignore let () = if !Sys.interactive then () else main ()
Here's a trivial demonstration:
$ echo '#(upcase,foo)' | ./demo1 FOO
TINT has global variables (called forms) and you can define them at run-time with the #(ds)
("define string") function, and then call (retrieve) their values with the #(cl)
function. This is why we need to fold over the input lines, rather then just iterating: so that the state modified in an earlier line is available in later lines.
When we initialized the state in main
above, we provided one predefined form, binding "os"
to the value of Sys.os_type
.
Here we bind the form x
to the string foo
in the first line, and we call x
's value in the second line.
$ (echo '#(ds,x,foo)hey'; echo '#(upcase,#(cl,x)) ##(cl,os)') #(ds,x,foo)hey #(upcase,#(cl,x)) ##(cl,os)
Feeding this input to our program, we get this output:
$ (echo '#(ds,x,foo)hey'; echo '#(upcase,#(cl,x)) ##(cl,os)') | ./demo1 hey FOO Unix
Since the ds
function evaluates to the empty string, the first line of output is just hey
. The second line up-cases the value of x
and also calls the predefined form os
.
In order for #(cl,x)
to succeed in the second line, we need to fold over the state.
We need to reset the state because the result of evaluation of the first line (hey
) remains in TINT's neutral string; it would also appear in the second line if we didn't call Tint.Eval.reset
.
N.B. In general, TINT expressions can span multiple lines, so we usually would process the entire input at once, or, to reduce memory usage, make sure to read in all of each expression by counting parens. But in the above, I wanted to demonstrate the need to reset the state.
It's easy to add new primitives to the TINT evaluator. A TINT primitive is an OCaml function of type Types.prim
, but the Eval.Prims
module has helper functions that let us just write a simpler function of type:
Types.state -> string list -> string
Most primitives ignore the state. The string list is the arguments given to the new TINT function, and the string is the result.
Here's valid new primitive:
let f _state _args = "the result"
Evaluating #(f)
returns "the result"
; so does evaluating f
with any number of arguments of any type like #(f,1,,foo)
.
Here's a poorly implemented primitive:
let g _state args = List.hd args
g
is poorly written in that it doesn't do any error handling. If invoked with no args, the interpreter will blow up with:
Exception: Failure "hd".
How do we hook a new primitive into the evaluator? The easiest way is to first feed each new primitive to Eval.Prims.make
along with a name, yielding a function with the true type of a TINT primitive Types.prim
. I usually do it like this:
let firstarg = let f _state args = List.hd args in Tint.Eval.Prims.make "firstarg" f
Then we add the result to Eval.Prims.prims
with our base set of TINT primitives (usually Eval.Prims.prims
like so:
Tint.Eval.Prims.prims Tint.Eval.prims [firstarg]
We give this enhanced set of primitives to Eval.init
to initialize a state that includes our new primitive:
let state = let enhanced = Tint.Eval.Prims.prims Tint.Eval.prims [firstarg] in Tint.Eval.init enhanced Tint.Eval.forms
and now we can pass this state to Eval.eval
.
Here's a new version of our program above that defines an actual useful primitive, rand
, that returns a random integer. It has an optional argument which is the maximum (exclusive) bound, and does error checking on its input; note that eval
and main
are the same as above:
(* #(rand), #(rand,100) *) let rand = let f = let () = Random.self_init () in fun _state -> function | [n] -> begin match int_of_string n with | exception _ -> Tint.Eval.Prims.error "not a number" | n' -> Random.int n' |> string_of_int end | args -> Tint.Eval.Prims.error "usage" in Tint.Eval.Prims.make ~defaults:[pred (int_of_float (2.**30.)) |> string_of_int] "rand" f let eval state line = match Tint.Eval.eval (Tint.Eval.reset state) line with | Error err -> Tint.Types.string_of_error err |> prerr_endline; state | Ok (state, result) -> print_endline result; state let main () = let each_inline () = match In_channel.input_line stdin with | None -> None | Some line -> Some (line, ()) in match Tint.Eval.init Tint.Eval.prims (Tint.Eval.Forms.forms ["os",Sys.os_type]) with | Error err -> prerr_endline err; exit 1 | Ok state -> Seq.unfold each_inline () |> Seq.fold_left eval state |> ignore let () = if !Sys.interactive then () else main ()