Enter RPN

R47: The Simulator DSL
Login

R47: The Simulator DSL

Motivation

While developing EML/Suite, the debugging process amounted to:

That’s one honkin’ big OODA loop!

With this new domain-specific scripting language — called T47 — one can point a coding assistant at this documentation page, from which it will then learn of the existence of the C47/R47 simulator and rejig. Those tooling foundations will allow any decent coding LLM to be able to generate both the changed P47 file and a T47 script to drive that test run, producing the results it needs for making a decision about what to try next. This removes all the manual steps above, further shrinking the OODA loop, allowing iterations at LLM speed.

Command Line Interface

This feature extends the C47 and R47 simulators with the ability to accept programs written in a sub-dialect1 of Tcl.

There are three new command line flags in support of this:

The first option will accept - to mean stdin, allowing this command to work:

./r47 --script - < my-script.t47

The second is better for brief explorations:

./r47 --exec 'puts "Hello, RPN world!"'

A third option is that the simulator now allows you to rename2 the executable to t47 to imply --headless. Since that then leaves only a script as a possible input source, this then requires either an explicit --script flag or a fallback to the implicit --script -, allowing this shorter command form:

./t47 < my-script.t47

There is nothing special about the file name extension .t47. You might instead name your scripts as .tcl to gain syntax highlighting and LSP support in a programmer’s text editor. Be careful to keep a rein on it, though, because T47 is far from full-fat Tcl.

Commands

The present implementation adds the following commands to the Jim Tcl dialect:

In addition, every built-in op that is also a valid Tcl identifier is a valid T47 command. That then allows:

nim 1.2345
tan

The first command overwrites the X register with a new number, and the second calls the calculator’s TAN op directly.

State Management

The simulator immediately exits when it reaches the end of one of these scripts, without saving its state first. This is very much on purpose because we want a given script to do the same thing when run twice in succession. We software developers call this property ā€œidempotency,ā€ though in our particular jargon, the definition differs from that of mathematicians.

At the same time, do realize that if you have a prior backup*.cfg file for your simulator, that will be loaded before the script runs. Whether this is a feature or a problem is dependent on your needs and situation. One wishing a clean slate might move that out of the way before running a script, or one might instead carefully set it up ahead of time in support of running your scripts against a known setup.

This then plays into the proper interpretation of the prior example: without a prior configuration backup, the 1.2345 NIM entry will be taken as degrees, but otherwise…? šŸ¤·ā€ā™‚ļø

Direct Ops

Calculator ops that happen to be named in accord with Tcl identifier rules are exported as T47 commands. These are all functionally equivalent:

arctan
xeq ARCTAN
catfn ARCTAN

The first line is not merely shorter and easier to type, the call dispatches faster because it comes down to a constant-time (O(1)) table lookup. The longer alternatives will cost you a linear table scan, O(n)), tolerable only when the referent is in the catalog but under a name that causes us to exclude it from the T47 command set. Jim Tcl is flexible enough that these rules only exclude about 0.4% of the possible ops, primarily those with various types of brackets meaningful to the language parser. This includes, for instance, the Riemann zeta function op — ζ(x) — due to the parentheses used for array indexing.

As a counterexample, consider the #b (bit count) op, which you might think confusable with the Jim/Tcl comment character. It would be if the language lacked strong quoting mechanisms to get around it:

 ./t47 -e 'nim 5; {#b}'

That correctly reports that there are 2 bits set. Without the quoting curly brackets, the parser would eat the second part of that script as a comment, giving the script’s ā€œ5ā€ input back as its result.

The simplest way to get a catalog of legal calculator op mappings to DSL commands is:

$ ./t47 --dslcommands
$ tail -1 t47-op-commands.txt
Registered 961 of 1004 possible catalog functions.

Now consider this instead:

$ echo 'puts [lsort -unique [info commands]]' | 
  ./t47 | grep varmnu | tr ' ' '\n' |
  tee t47-all-commands.txt | wc -l
1078

The reported command counts differ because the latter includes those added statically plus about a hundred built-ins from our particular configuration of Jim itself.

These files serve as handy lookup tables; hint!

Case Sensitivity

T47 lowercases its direct calculator op wrappers both to unify certain historical irregularities3 in the C47 op naming scheme and to comply with Tcl programming conventions.

At the same time, realize that this does not affect the arguments taken by catfn and xeq. They continue to match catalog ops by exact name, including case, same as occurs when calling them on-device. XEQ ā€˜a’ and XEQ ā€˜A’ mean different things, a distinction we must maintain in T47.

A direct result of this policy is that these each cause an interpretation error:

ARCTAN
catfn arctan

I refer you again to the lookup tables produced by the commands above for help in sorting this type of thing out.

Argument Parsing

ā€œEverything is a stringā€ is a Tcl mantra, and that applies to command arguments. This code…

nim 8
xeq x!
sto 00
dropx
nim 42
xeq RCL+ 00

…computes `x! + 42, x=8` as you would expect, but it does so by parsing each of those arguments in accord with the calculator’s own operations metadata. It knows that the STO op backing our sto command takes a register or variable as an argument, for instance, which then informs how it parses the string ā€œ00ā€.

There are limits. One biggie is that T47 does not presently attempt to handle indirect accesses. While we could in principle fix this, one has to ask whether it isn’t better to just write a .p47u file, compile it with rejig, readp it in via T47, and xeq its top label.

Values

Calculator flags, registers, and variables can be looked up within the calculator and used in T47 code directly:

set r [reg 00]
puts "R00 = $r"

This feature builds atop the same argument parsing rules as above. If you can refer to a thing in a command, you can refer to it via the appropriate one of flag, reg, or var.

The two-argument (set) form of these return their passed values for three key reasons:

  1. Tcl does it that way, likely because it allows…
  2. Mathematically-sound chaining. In the same way that you can say `a = b = c = 42`, T47 lets you say ā€œvar a [var b [var c 42]]ā€
  3. Parser round-tripping. Because everything goes through string processing, this scheme lets you verify that it’s working properly without a separate ā€œgetā€ action:
puts "Set calculator variable v = ā€˜[var v foo]’ (should be ā€˜foo’)"

Note the Tcl command substitution to avoid the need for the explicit variable in the initial example above.

Types

Jim Tcl offers a single scalar data type, strings, while the R47 supports a plethora of data types. How do we cope?

Reading values out of the calculator is easy: we simply stringify them, much as is done for display on the LCD.

The tricky part is how T47 takes a command’s argument string and produces the expected in-calculator data type. Our rule is simple: try everything!

Okay, not literally everything, but each of those options in that order. This reduces ambiguity by trying the most recognizable types first, ruling out the more generic types below it on success. If there are no date and time separators, it can’t be either of those. If the value is numeric but doesn’t fit in a short and doesn’t have a base affix, it might be a long. If it isn’t a long but we discern addition of two reals with the second multiplied by š‘–, it’s complex. If there’s only one real part, treat it as such. Only if all else fails does the parser punt by treating the value as a string literal.

T47 tries to adhere to the P47 program format’s ā€œstringā€ representations for literals, where that makes sense. Because this family of formats were designed to be easy and reliable to parse, any seasoned programmer should find the basics obvious, but there are a few details worth calling out as potentially surprising…

Date

Because the P47 STRING_DATE format is based on Julian days, which are not recognizable purely by format, T47 requires you to use YYYY-MM-DD format to have an argument value data-typed as such.

Time

The P47 STRING_TIME format can include a fractional part — HH:MM:SS.mmm — but when rendering it back to a string, T47 currently rounds to the nearest second for display purposes. This means T47 can send a fractional time to the calculator, but if you want to see the full precision, reading it back out with T47 and printing the resulting string is not the right way.

Short Integer

The P47 STRING_SHORT_INTEGER format isn’t well-suited for writing in a T47 program, so instead we use something closer to what you see on-screen: NNN#bb where NNN is anything representable as a 64-bit integer (signed or otherwise) and bb is a required base affix from 2-16 for interpreting the NNN digits. A value not matching all three criteria falls through to…

Long Integer

Same rules as for the C47/R47 generally: up to 1000 digits, positive and negative. Without the base affix, an integer is considered ā€œlongā€ even if it is as short as ā€œ1ā€.

Complex Number

The P47 STRING_COMPLEX34 format is:

a + ix b

…where a is the real part, b is the imaginary, and ix means ā€œmultiplied by š‘–.ā€ The spaces are all optional, and the signs can all flip as needed so that ā€œ-5-ix3ā€ is parsed as `-5-3i`.

When you want more expressive complex number parsing, it’s time to switch to rejig. Use it to produce a .p47 file from UTF-8 input text, then readp that into the calculator with T47 and xeq it.

Real Number

Same rules as for the C47/R47 generally: a number with up to 34 digits, distinguished from a long integer by presence of a radix dot, an ā€œeā€ to mark it as scientific notation, or both.

String

Anything not matching one of the other data types.

Command Reference

catfn

Call a built-in operation4 by exact name match, including case.

Prefer using direct ops where possible.

flag

Access a calculator global flag by name or number:

flag CPXRES $v
set f [flag 42]

The first command sets the content of named system flag ā€˜CPXRES’ to the value of T47 variable v, and the second retrieves a numbered global flag’s value into script variable f.

No attempt is made to support local flags for the same reason reg does not.

Notice that flag names are not lowercased in the way that direct op commands are. You must spell them exactly as given in the catalog.

nim

This provides a programmatic interface to a useful subset of the calculator’s numeric input mode. This not only gives a more compact alternative to press, it works in --headless mode.

press

Because the GDK key constant scheme overlaps ASCII where possible, the simple form of the call is:

press 4

That injects a fake 4 button keypress into the simulator’s event loop.

The more complicated form is:

press [list 4 3 2 1]

Here, [list … is a Tcl command substitution block which returns its parameters as a Tcl list, which press then consumes in order.

Hover your mouse cursor over the simulator’s keys to learn which keystrokes each one responds to. For instance, sending a Q will execute the š‘„Ā² function.

Two additional words are presently understood via this interface:

We have no immediate plans to add additional special words, though it may happen by and by.

Because this command is incompatible with --headless mode,6 you should prefer nim for numeric inputs. For built-in calculator ops, prefer the DSL command forms where available and catfn/xeq where not.

readp

Reads a named P47 program file into the calculator:

readp my-program.p47

If you give the name of a file without a path component and no file is found in the current working directory by that name, it prepends the simulator’s PROGRAMS/ subdirectory and tries again.

Relative paths are taken from the CWD of the running simulator.

reg

Access a global calculator register by letter or number:

reg 42 $v
set r [reg 42]

The first command sets the content of R42 to the value of T47 variable v, and the second retrieves it into script variable r.

No attempt is made to support local registers (LocR, R.š‘„š‘¦) since that requires a call frame, and T47 operates outside one.

savest

This command wraps the calculator’s built-in SAVEST operation:

savest my-state.s47

That gives you a way around the purposeful default state management rule of exiting before any state is saved. Since we also provide the inverse loadst command, you might choose to move backup*.cfg out of the way before running a script but then load a curated state file in before doing work.

If you leave off the optional file name parameter, the simulator decides on a file name.

snap

While this command may appear to offer nothing more than a wrapper around the built-in SNAP operation, it’s more subtle than that.

By default, the calculator’s SNAP op timestamps its output, which is good for avoiding collisions when run interactively, but bad for scripting. Therefore, our snap command differs by taking an optional file base name parameter, to which is appended a ā€œ.bmpā€ extension, overriding this behavior and allowing subsequent code to find the resulting screenshot without needing to replicate the time-stamping algorithm or to scan the working directory for the newest *.bmp file.

The underlying calculator operation also writes out a TSV file containing the stack register contents to avoid the need to OCR that BMP file. This also uses the basename parameter, if given. Calling…

snap foo

…produces foo.bmp and foo.REGS.TSV.

tsvfn

This exposes the tail end of the snap file naming scheme for use in other TSV file writing cases. The most useful of these may be the printing functions, since it gives a T47 script a way to export computed results in text form without taking on the BMP file overhead of using snap merely to get that TSV side-car file out. The same mechanism is shared by the calculator’s graphing and statistics functions.

Calling tsvfn ahead of snap has no effect; your given value will be overwritten.

var

Access a global calculator variable:

var foo {Hello, world!}
set v [var foo]

The first command sets the content of ā€˜foo’ to an alpha string, and the second retrieves it into T47 variable $v.

When setting a calculator variable, the T47 interpreter applies heuristics to the passed string to decide which data type to convert the value to.

When getting one, everything is converted to a string per Tcl norms.

xeq

Primarily, this is a thin wrapper around the calculator’s XEQ button handler, taking a label of a subroutine to execute. Paired with readp, this gives the language a large part of its utility, because it means an outside entity — whether human, robot overlord, or space alien — can inject an RPN program into the simulator and then start it running. The other DSL routines largely serve to modify this process and record its results.

This command is similar to catfn in that it will fall back to a linear scan over the built-in function catalog if the label doesn’t exist. While this extends the cost of the failure path, when neither the label nor the built-in function exist, it faithfully replicates the actual XEQ button capabilities: if tapped a second time, XEQ gives that key’s shifted šŸŸ§Ā É‘ key function for inputting the argument, which can be either a label or an op name.

Beware: The same case-sensitivity issue applies to xeq as to catfn!

Underpinnings

The Jim Tcl basis for T47 is a stripped-down reimplementation of full-fat Tcl 8.5 with select features backported from Tcl 8.6 and 8.7. It is missing all of the Tcl 9 additions, plus major add-ons like Tk.

T47 further strips out several Jim Tcl features deemed either unnecessary in this context or too costly:

We also leave out Jim’s math library, not only because the R47’s builtins stomp it flat but because it’d end up shadowed by the calculator ops we expose as DSL commands. At the same time, we left basic Tcl arithmetic expression handling alone so that you can use it purely on the scripting side. This means +, -, *, /, and % are not calculator ops exposed as argless (nullary) commands but instead the Jim Tcl binary commands taking their operands as arguments:

$ ./t47 -e 'puts [+ 2 3]'
5

The equivalent using the full might and majesty of the C47/R47 calculator engine would be:

./t47 -e 'nim 2; nim 3; xeq +; puts [reg X]'

The explicit output of the X register isn't strictly required since the current implementation prints that automatically after the script ends.

History

Tcl started out in the late 1980s as a purposefully minimalist language. It rode a popularity wave produced by the groundbreaking Tk GUI toolkit, but once the other platforms either caught up or were themselves subsumed by the web app wave, Tcl fell back into its prior status as a niche language. Today, its ecosystem remains far smaller than the big-name computer programming languages.

The thing is, none of that is a problem for T47. We looked at several other major alternatives and remain confident that this is the correct foundation. A key design element is that Tcl’s minimalist nature allows the DSL proper to shine through.

Going Beyond

If all you do is treat T47 scripts as simple lists of commands in the RPN spirit, that’s perfectly fine. Do realize, however, that if you later find yourself needing loops, conditionals, procedures, lambdas, string manipulation, list processing…this and more are there waiting for you. Combine that with the R47’s advanced implementation of RPN, and one can produce wonders.

(You may now wish to return to my R47 article index.)

License

This work is Ā© 2026 by Warren Young and is licensed under CC BY-NC-SA 4.0


  1. ^ The core of the T47 language is a subset the full power of Jim Tcl, but it then extends it with nearly 1000 new commands. Jim in turn is a subset/superset hybrid of full Tcl.
  2. ^ Or symlink, or hard-link… Better still, use the new make t47 target.
  3. ^ Prime example: ARCTAN vs artanh.
  4. ^ Not strictly a function, but we use that term here to draw a parallel between catfn and 🟧 CAT FCNS.
  5. ^ The GDK key code for this is not ā€œ13ā€ as you might guess. This is our workaround for the high 16-bit value it does use.
  6. ^ The reason should be obvious, but in case it is not, it is that without a GUI running, there is no queue into which to inject key-up events, or at least no consumer of same.
  7. ^ We still have the basics via aio, which we cannot exclude. That also pulls in package support, which we’d leave out if we could; if your T47 programs get big enough to justify the package abstraction, we’ll want to hear about it, because…wow! 🤯
  8. ^ The Jim posix command gives only the fork, gethostname, getids, and uptime sub-commands. The key elements of POSIX are wrapped by included Tcl facilities like open in service of cross-platform compatibility.