zeptoforth is an inlining native-code-compiling Forth, unlike many Forths which are ‘indirect-threaded’ or ‘direct-threaded’, i.e. it compiles words directly into native ARM Thumb instructions and, when directed to do so by defining words with [inlined] in them which do not call any non-inlined/non-constant words or, on the RP2040, do not make any use of constant pools, can inline words into them. It also attempts to inline constants as much as it can. This does have the effect that there is no good way to list words after they have been defined except as ARM Thumb assembly language.
zeptoforth, being a Forth, has distinct interpretation and compilation modes. It can compile to both the main task’s RAM dictionary and to the flash dictionary; this is controlled by specifying compile-to-ram and compile-to-flash, where compile-to-ram is the default on boot.
Code that is compiled to RAM is transient, can be forgotten in the reverse order of being compiled at runtime by defining a marker or cornerstone and then executing it or by executing forget. Code that is compiled to flash is semi-permanent and is available on boot (it should be noted that variables, values, and buffer:s compiled to flash must be followed by a reboot before they are actually accessed or else undefined behavior and likely a crash will result). Code compiled to flash cannot be arbitrarily forgotten but can be erased with the use of markers and cornerstones compiled to flash, but unlike markers and cornerstones compiled to RAM these are very often quite wasteful of flash space because they must align the current flash dictionary here address to a flash erase page boundary.
One important gotcha with zeptoforth is that trying to create a buffer with create when compiling to flash will not do what you want it to do, because the buffer will be compiled to flash and not to RAM, and thus will not be modifiable; however, create is still useful when compiling to flash for creating constant arrays in combination with ,, h,, and c,. In most cases when you want to create a statically-allocated buffer use buffer: instead.
If you specify a control structure while interpreting, it will unlike many Forths temporarily switch to compilation mode, compile to RAM, then when the outermost control structure is ended automatically execute the code compiled and then promptly forget it. However, this has the side effect that if you attempt to modify the RAM dictionary within this code, your changes will be forgotten as well.
I hope this is a short summary of some basic concepts related to zeptoforth; if you have any additional questions, just go ahead and ask!
Edit:
Some additional important points is that, as zeptoforth supports exceptions, you may want to attempt to execute code and clean up after it even if an exception has been raised, and then re-raise the exception. A good example is as follows:
: hex. ( x -- )
base @ { old-base }
[: hex u. ;] try
old-base base !
?raise
;
In this case this is a word prints a 32-bit value as hexadecimal, first saving the base, then afterwards restoring the base even if an exception was raised, and then re-raising the exception.
It should be remembered that try always returns either 0 or the exception caught, but beneath that on the stack, if an exception was raised, it restores the data stack pointer to its original location. As a result the contents of the data stack aside from its top cannot necessarily be trusted. Therefore, we use a local variable, which is on the return stack internally, to save the base.
As for ?raise, it pops the top of the stack, if it is not 0 it treats it as an exception and raises it, or otherwise it continues executing onwards. This behavior is very useful for conditionally re-raising exceptions.
(Note that in this context [: … ;] defines a quotation, a sort of nested anonymous word; once it is done being defined it will push, at runtime, its execution token onto the data stack.)
Another useful note is the existence of with-allot ( bytes xt – ) and with-aligned-allot ( bytes xt – ); these take a number of bytes for a temporary buffer on the top of the current task’s RAM dictionary and an execution token to execute. It will first allot the buffer, then call the execution token, passing it the base address of the buffer. Once the called execution token returns or an exception is raised, it un-allots the buffer and, if an exception had been raised, it re-raises it. Note that with-allot differs from with-aligned-allot in that the former does not care about alignment while the latter guarantees that the buffer is cell (i.e. 4 byte)-aligned.
An example of the use of this is:
: stupid-with-allot-example ( -- )
s" Hello, world!" dup [: { addr bytes buffer }
addr buffer bytes move
buffer bytes type
;] with-allot
;
Another useful thing which you will see a lot of the use of in the code is local variables. This is a feature missing from many more traditional Forths, but I find them very useful. E.g. to define local variables x, y, and z with the values 0, 1, and 2, compile:
0 1 2 { x y z }
To set z to 3 then compile:
3 to z
To add 1 to x you can either compile:
x 1+ to x`
or
1 +to x
Note that the default kind of local variable is a single-cell local variable that, when referenced, pushes its value, but there are also double-cell local variables, and both single-cell and double-cell local variables have variants which push their addresses rather than values (which is often very useful for defining very small temporary buffers on the return stack).
You can see this with the following:
0 1 2. 3. { W: x W^ y D: z D^ w }
where x is a normal cell local variable that pushes its value (the W: is just for consistency with the other examples; it is unnecessary), y is a cell local variable that pushes its address, z is a double-cell local variable that pushes its value (mind you 2. is a double-cell integral value), and w is a double-cell local variable that pushes its address (mind you 3. is a double-cell integral value).
I should note that local variables in zeptoforth can be defined just about anywhere in a word and can be freely intermixed with do … loops, but are not arbitrarily intermixable with accesses to the return stack with >r, r>, r@, rdrop, etc. because they live internally on the return stack at runtime.