Files
equi/equi.md
T
2022-08-06 00:08:17 +03:00

15 KiB

Equi

Equi is a general-purpose 16-bit stack-based platform (and a programming language/VM named the same) aimed at low-cost, low-energy computing. It was inspired by Forth, Uxn, VTL-2, SIMPL and some other similar projects.

The name Equi comes from the fact each source code instruction is equivalent to a machine instruction. No, it isn't mapped to one machine instruction. It is the machine instruction. All the instructions and data in Equi are represented with printable ASCII characters only. This allows to bootstrap Equi code directly from the keyboard (any standard keyboard/keypad that allows serial input) using a tiny interpreter stored, for instance, in the hardware ROM.

Specification

Main features of an Equi machine:

  • Instruction bus: 8-bit;
  • Data bus: 16-bit;
  • Address bus: 16-bit;
  • Two 256-byte (128-word) stacks, main and return;
  • One 32-byte literal stack;
  • 16-bit program counter (PC);
  • 16-bit compilation buffer pointer (CBP);
  • 16-bit compilation lookup table pointer (CLTP);
  • Interpretation mode (IM) flag;
  • Compilation mode (CM) flag;
  • Instruction ignore (II) flag;
  • 42752 bytes of main RAM (0x0000-0xa6ff), 41984 bytes of which are available for the program and data and 768 bytes hold the three stacks and necessary service registers listed above;
  • Up to 4GB flat persistent storage (tape, disk, flash etc);
  • Serial terminal input and output;
  • Up to 65535 peripheral extension ports.

The default Equi RAM layout is:

Address range Size (bytes) Purpose
0x0000-0x00ff 256 Main stack
0x0100-0x01ff 256 Return stack
0x0200-0x021f 32 Literal stack
0x0220-0x0221 2 PC
0x0222-0x0223 2 CBP
0x0224-0x0225 2 CLTP
0x0226 1 Flags (II/CM/IM)
0x0227-0x0228 2 GPD area start (set to 0x2300 by default)
0x0229-0x022a 2 Command buffer start (set to 0x5000 by default)
0x022b-0x022c 2 Command buffer size (set to 0x5700 by default)
0x0227-0x02ff 211 Reserved for internal usage by the interpreter and future extensions
0x0300-0x22ff 8192 Compilation lookup table (CLT) - up to 1024 compiled words (6 bytes for literal + 2 bytes for CBP address each) by default
0x2300-0x4fff 11520 General purpose data (GPD) area
0x5000-0xa6ff 22272 Command buffer area

This layout has been carefully chosen to achieve maximum portability: so that full a Equi system could fit inside the user area of Apple II's space between (physical) 0x0800 and 0xbf00 addresses, while still leaving 4096 bytes for the interpreter itself with all necessary peripheral drivers if required so. As such, Apple II equipped with full 48K RAM is supposedly the lowest spec possible target system Equi could run on with the default virtual RAM configuration.

To preserve runtime integrity, Equi implementations are allowed to (but not required to) restrict all writes to the addresses below the GPD area start (0x2300 by default). On systems with less/more RAM where CLT and/or GPD and/or command buffer areas are smaller/larger, the values at 0x0227, 0x0229 and 0x022b must be populated accordingly. You don't need to fill in the sizes of CLT and GPD areas since they are calculated automatically from the specified offsets, you only need to specify how big your command buffer is to avoid any writes outside the user area.

Equi is strictly case-sensitive: all uppercase basic Latin letters, as well as a number of special characters, are reserved for machine instructions, and all custom words must be defined in lowercase only (additionally, _ character is allowed in the identifiers). Within comments (see below), any characters can be used.

The interpreter can run in one of the four modes: command (default), interpretation (IM), compilation (CM) and instruction ignore (II) mode. An Equi machine always starts in the command mode. The latter three are triggered by certain instructions that set the corresponding flags. The semantics of the compilation mode is similar to that of Forth, and will be covered in detail here later on.

In the command mode, the interpreter doesn't perform any instruction execution and doesn't manipulate program counter (PC). Instead, it accumulates all characters typed from the standard input into the so-called command buffer. The only instruction Equi must react to in this mode is CR, the carriage return character, that sets PC to the command buffer start, sets the IM flag, clears the CLT and starts execution in the interpretation mode. Note that this also means that every Equi program file, even when run in a non-interactive environment, must end with a CR character, and as long as it does and every program has a halting Q instruction, you can safely concatenate several Equi programs in a single file to be executed sequentially.

In the interpretation mode (IM flag set), when the interpreter encounters any of the following characters - _0-9A-Fa-z (not including -) - it pushes their ASCII values bytewise onto the literal stack (32-byte long, the overflown bytes are discarded). When any other character (except :, " or ') is encountered when the literal stack is not empty, the # instruction logic (see below) is performed automatically. If : is encountered, compilation mode logic is performed instead. If a Q instruction or a on-printable character is encountered, Equi returns to the command mode immediately.

In the compilation mode, all instructions except ; are skipped while the CM flag is set. When the interpreter encounters ; instruction, it performs the finalizing logic to save the compiled word into CLT (see below) and returns to the interpretation mode.

In the instruction ignore more (II flag set), all instruction except ) (that unsets the II flag), are skipped and discarded. PC, however, does increase as usual in this mode.

Note that II flag has the precedence over IM and CM flags and CM flag has the precedence over IM flag. I.e. you cannot exit the interpretation mode while being in the compilation mode, and you can't exit any other mode while being in the II mode. And surely enough you can't exit the command mode (interpreter shell itself) unless all three mode flags are unset.

Equi's core instruction set is:

Op Stack state Meaning
# ( -- ) Literal: pop all characters from the literal stack, discard all _a-z characters, leave the top 4 characters and push the 16-bit value from them (in the order they were pushed) onto the main stack
" ( -- lit1 lit2 ... ) Pop all the values from the literal stack and push them onto the main stack as 16-bit values
( -- ) No operation: whitespace can be used in the code for clarity and not affect anything except PC
( ( -- ) Set the II flag: when it is set, the interpreter must ignore all instructions except ), used for writing comments
) ( -- ) Unset the II flag, returning to the normal interpretation or compilation mode
: ( -- ) Compilation mode start: set CM flag and set CBP to PC+1 value
; ( -- ) Compilation mode end: replace this instruction in-memory with R instruction, pop all characters from the literal stack, append the lookup table with the first 6 chars and CBP value, unset the CM flag and increase CLTP value by 8
' ( -- ) Call the compiled word: pop all characters from the literal stack, leave the first 6, look them up in CLT for a CBP value, set PC to CBP if found, error out if not, then push PC to return stack and set PC to the CBP value
CR ( -- ) In the command mode, output a line break and switch to the interpretation mode (see above); in all other modes, identical to whitespace
R ( -- ) Return: pop and assign the PC value from the return stack
] ( a -- ) Pop the value from main stack and push onto return stack
[ ( -- a ) Pop the value from return stack and push onto main stack
L ( addr -- a ) Load a 16-bit value from addr
S ( a addr -- ) Store a 16-bit value into addr
W ( a addr -- ) Write a 8-bit value into addr (note that both value and address still must be 16-bit, the higher byte of the value is discarded)
! ( a -- ) Drop the top value from the stack
$ ( a -- a a ) Duplicate the top value on the stack
% ( a b -- b a ) Swap two top values on the stack
@ ( a b c -- b c a ) Rotate three top values on the stack
\ ( a b -- a b a ) Copy over the second value on the stack
J ( rel -- ) Jump: increase or decrease PC according to the relative value (treated as signed, from -32768 to 32767)
I ( cond rel -- ) Pop relative value and condition. If the condition value is not zero, J to the relative value
X ( -- pc ) Locate eXecution point: push PC+1 value onto the main stack
> ( a b -- a>b ) Push 1 onto the stack if the second popped value is greater than the first, 0 otherwise
< ( a b -- a>b ) Push 1 onto the stack if the second popped value is less than the first, 0 otherwise
= ( a b -- a==b ) Push 1 onto the stack if the two popped values are equal, 0 otherwise
+ ( a b -- a+b ) Sum
- ( a b -- a-b ) Difference
* ( a b -- a*b ) Product
/ ( a b -- a/b rem ) Integer division (with remainder)
N ( a -- -a ) Single-instruction negation (complement to 65536)
~ ( a -- ~a ) Bitwise NOT
& ( a b -- a&b ) Bitwise AND
` ` `( a b -- a
^ ( a b -- a^b ) Bitwise XOR
. ( a -- ) Output a character by the ASCII (or Unicode, if supported) value into the standard terminal
, ( -- a ) Non-blocking key input of an ASCII (or Unicode, if supported) value from the standard terminal
? ( -- a ) Blocking key input of an ASCII (or Unicode, if supported) value from the standard terminal
P ( p1 p2 port -- status r1 r2 ) Port I/O: pass two 16-bit parameters to the port and read the operation status and results into the words on the stack top
} ( adh adl len maddr -- status) Persistent storage write operation. Stack parameters: persistent address high 2 bytes, low 2 bytes, data length, RAM address
{ ( adh adl len maddr -- status) Persistent storage read operation. Stack parameters: persistent address high 2 bytes, low 2 bytes, data length, RAM address
Q ( -- ) Quit the interpretation mode (unset IM flag if set), or the interpreter shell itself if in command mode (halt the machine when it's nowhere to exit to)

This would be an example of loading a 15200-byte long Equi program from the persistent storage at address 0x12345678 into the RAM at the next free address and running it (discarding the load status for brevity):

1234#5678#3b60X0007+{!(CR)

The logic of this loader snippet is as follows. After the X instruction, the top of the stack contains the address occupied by the following byte (0) in this case. Then we add the amount of bytes needed to shift loading to the end of the snippet, in our case 7 (additionally skipping 0, 0, 7, +, { and !) and use the result as the target loading address in memory. Since this loader snippet itself ends at 0x5021 (with the default configuration, the command buffer starts at 0x5000) and the status is already popped from the stack, the execution will start from 0x5022 immediately. Please also notice how we don't need # after 3b60 (15200 in hex) and 0007 since all valid instructions except :, ' and " trigger # automatically before execution when the literal stack is not empty.

This should be enough to get started with any program, which also means once a boot ROM with an Equi shell is up, it can also be used as a stand-alone OS, like a Forth system or ROM-BASIC. This way, already loaded Equi programs can also load and call each other as long as the command buffer RAM permits and their exact locations of in the persistent storage are known. And until the moment the Equi machine is put back into the command mode, all these programs can share word definitions with each other and reuse them as necessary.

Note that, due to the dynamic nature of word allocation and ability to reconfigure the runtime environment for different offsets depending on the target, absolute jumps are not directly supported in Equi and generally not recommended, although one can easily do them with ]R sequence and/or calculate absolute positions using X instruction.

Please also note that Equi doesn't specify any graphical or sound output capabilities. If such support is required, it generally must be implemented, as with any other peripheral, via the port I/O interface (P) instruction specific to a particular hardware/software implementation. Same goes for how standard serial terminal input/output is processed: Equi specification doesn't enforce any particular way. On the desktop/laptop PCs, however, it is advised, especially for software-based implementations/VMs, that the terminal I/O should be VT100-compatible, including, for instance, control character support and the output of an audiovisual bell for ASCII 0x07 (\a or ^G). Depending on the target, these features may already be supported by the underlying OS's terminal emulator or may be implemented as a part of the VM itself.

Reference implementation

Being a purely PC-oriented low-level runtime/programming environment, Equi has the reference implementation emulator/VM written in C (ANSI C89 standard), equi.c, compilable and runnable on all the systems supporting standard I/O. Note that this emulator:

  • doesn't fully implement P instruction but instead outputs its parameters to the standard error stream and puts three 0x0000 values back onto the stack,
  • sandboxes the { and } operations using the file you supply in the command-line parameter. If you don't supply the file, these operations will effectively do nothing except putting 0x0000 (success status) onto the stack.

The source code file should compile using any mainstream C compiler with C89 support, like GCC/DJGPP, Clang, TCC etc. However, it is also being developed to be compilable with CC65 compiler for targets like Apple II or Atari 800. All the machine/target specific configuration is done at compile time, using compiler command-line switches. Here are the instructions to build Equi using different known C compilers: