Cortex-M0 Stack Machine
This implements a rough bare-metal stack machine that works on Cortex-M0 devices (or possibly anything that supports Thumb2, but I've only tried it on M0).
Building
Currently the only target is the micro:bit as emulated by QEMU. There's a good chance it might work on hardware but I don't have one and I haven't tried it.
If you have an ARM toolchain installed (i.e. GNU binutils that start
with arm-none-eabi-), you should be able to just do:
$ make
And you'll get a vm.elf.
Running
You will need qemu-system-arm, which should be included with any
reasonable copy of qemu. You can run vm.elf like so:
$ qemu-system-arm -machine microbit -serial stdio -device loader,file=vm.elf
You should get a > prompt. Control-C will kill qemu.
Theory of Operation
The data stack is native 32-bit words, stored at the top of memory using the hardware stack and hardware push/pop instructions. This means it grows downward toward the bottom of RAM.
At the bottom of RAM is the input buffer, the control stack, a pointer to the next compile location ("scratch space"), then the environment. On startup, built-in words are copied into RAM and the REPL loop begins.
The REPL input parses space-separated "words" and compiles them into native code in the scratch space. There are three kinds of words - literal values, regular words, and definitions.
A literal value is a $ followed by hex digits defining up to 32 bits of
data. This value is pushed to the stack.
> $31
A regular word is any sequence of characters not starting with a $ or
:. It names a function, which is called. See below for built-in
words.
> $31 putch
0
A definition word is a : followed by any sequence of letters not
containing a space. This collects all prior words on the input line and
defines them as a new word with the given name. The collection of
currently defined words is called the "environment".
> $31 putch :print_one
> print_one
1
The stack is maintained between input lines.
> $31
> putch
1
Popping too many values will cause a hardware fault accessing out-of-bounds memory and call the fault handler, which will attempt to point you at the word that failed. It will then reset the CPU.
> pop
^halted!
>
Unfortunately, the environment is reset on startup, so mistakes can make progress somewhat difficult. 😅
This is obviously very FORTH-inspired, as pretty much all interactive stack machines are.
Built-in words
A good stack machine has a lot more words than this. But this was enough to test with.
pop - ( v -- )
Discards the top of the stack.
dup - ( v -- v v )
Duplicates the top item on the stack.
add - ( a b -- a+b )
Adds the two values on the top of the stack and pushes the result to the top of the stack.
do - ( -- )
Pushes the location after the do onto the control stack.
while - ( v -- )
Pops the value on the top of the stack. If it is non-zero, control resumes at the location on the top of the control stack. If it is zero, the location on the top of the control stack is popped and control continues as normal.
eq - ( a b -- a==b )
Pops two values off of the stack and compares them for equality. If they
are equal, a 1 is pushed to the stack. Otherwise, a 0 is pushed.
ne - ( a b -- a!=b )
Like eq but tests for inequality.
(This is actually implemented with a subtract so the "true" values it
produces are non-zero, but are not definitely 1)
putch - ( v -- )
Pops the value on the top of the stack, truncates it to a byte, and writes it to the output.
getch - ( -- v )
Reads one byte from the input and pushes it onto the stack.
if - ( v -- )
Pops the value on the top of the stack. If it is zero, execution
continues after the next else or endif.
endif - ( -- )
Does nothing. Serves as a location target for a prior if or else.
else - ( -- )
Continues execution after the next else or endif.
halt
Halts execution.