Motivation
While developing EML/Suite, the debugging process amounted to:
- ask an LLM to propose updates to the
Suite.p47ufile - call
makemanually to produce the P47 form usingrejig - pull up the simulator
- remove the prior version of EML/Suite via
š¦ĀCLRĀš¦ĀDELETEĀDELP - hit Esc twice to get back to the
M.SAV+ribbon for theREADPfunction - navigate to the directory where the fresh P47 is
- load it with a double-click
XEQthe changed subroutine- copy the result in X to the clipboard
- paste it into the LLMās chat window
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:
--scriptto pass the script by name--exec/eto give it directly on the command line--headlessto suppress the startup of the GTK GUI
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:
catfn: wrapsš§ĀCATĀFCNSto call a named built-in function via the catalogflag: access a global calculator flag by name or numbernim: overwrites the X register via the NIM mechanism, faking numeric keyboard inputspress: accepts one or moreGDK_KEY_*constants to simulate UI keypressesreadp: wraps theREADPfunction to read a P47 program into the simulatorreg: access a global calculator register by letter or numbersavest: save the simulatorās state to an.s47file orloadstfrom onesnap: take a BMP snapshot of the calculatorās screen; also writes a TSV side-car file giving the stack register contentstsvfn: override the default TSV file naming rules used by the stats functions, graphing, etc.var: access a global calculator variablexeq: wraps the function of the calculatorāsXEQkey to execute a program by label
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:
- Tcl does it that way, likely because it allowsā¦
- 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]]ā - 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:
ENTER: injects a āReturnā keypress5 which the simulator interprets as RPNāsENTERR/S: injects a backslash, which Tclās string parser would otherwise treat specially
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:
- advanced networking7
- IPv6 support
- syslog
- TLS
- database access
- SQLite
- Redis
- graphics/GUI
- SDL
- Win32 API
- programming-in-the-large
- event loop
- OO extension
- namespaces
- sub-interpreters
treedata structure
- POSIX extensions8
- REPL (including command history)
- zlib
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
- ^ 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.
- ^
Or symlink, or hard-link⦠Better still, use the new
make t47target. - ^
Prime example:
ARCTANvsartanh. - ^
Not strictly a function, but we use that term here to draw a parallel between
catfnandš§ĀCATĀFCNS. - ^ 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.
- ^ 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.
- ^
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! 𤯠- ^
The Jim
posixcommand gives only thefork,gethostname,getids, anduptimesub-commands. The key elements of POSIX are wrapped by included Tcl facilities likeopenin service of cross-platform compatibility.