Inspired over the holidays by reading Crafting Interpreters by Robert Nystrom, I designed a custom high level language and assembly language and built a compiler, assembler, virtual machine, and disassembler. Compatible with WebAssembly (WASM) so it runs in the browser (VM compiled to WASM). All implemented in low level C. The goal of this project was to do a very difficult software engineering task using GitHub Copilot Agent Mode.
I say inspired by Nystom's book, but of course I am old school and learned compiler design from the Red Dragon book. The assembler code I learned back in the day on Z80 and Motorola 68k. Implementing a VM with byte-code was new experience for me, and actually the VM is considerably more sophisticated than the high level language.
https://nickzinn.github.io/zmanLang/
Language, ZmanLang
A C-style, implicitly typed, sort-of toy language but relatively complete with functions, arrays, and strings. Canonical Example:
print("Hello, World!\n");
Sort Example:
func bubbleSort(arr[]) {
let n := length(arr);
let i := 0;
while (i < n) {
let j := 0;
while (j < n - 1) {
if (arr[j] > arr[j + 1]) {
let tmp := arr[j];
arr[j] := arr[j +1];
arr[j+1] := tmp;
}
j := j + 1;
}
i := i + 1;
}
}
let data := {-1,5,4, -3, 8, 7};
println(data);
bubbleSort(data);
println(data);
I classify it as a toy language because it doesn't implement floating point math (only integer), data structures and garbage collection. Currently dynamic memory allocation just grows the heap via a bump allocator, which is fast and suitable for small scripts.
Assembly Language, StackVM-32 ASM
32-bit, stack-based assembly language with support for directives and labels. Picked a small instruction set of 50 opcodes (and one for tail recursion). Easy to handcode and very capable. Also added 9 syscalls for IO and dynamic memory. Example:
.code
.entry main
main:
PUSHI str_lit_0
PUSHI 13
SYSCALL 4
HALT
.data
str_lit_0:
.ascii "Hello world!\n"
.end
The v0 reference compiler, zmc, compiles ZmanLang source (.zm) to StackVM-32 assembly (.asm) (see spec-lang.md and examples/web/README.md).
The assembler parses StackVM-32 assembly (.asm) and produces a runnable ZVM container (.zvm) (see spec-assembler.md and spec-vm.md).
The StackVM-32 virtual machine loads and executes .zvm containers (instruction set + container format: spec-vm.md).
The disassembler turns a .zvm container back into readable assembly and auto-labels branch/call targets (see spec-vm.md).
- Required: a C compiler (
cc,clang, orgcc) andmake. - Optional (static analysis):
clang-tidy(preferred) orclangformake lint. - Optional (web/WASM): Emscripten (
emcc) formake web. - Optional (web harness run): Python 3 for
python3 -m http.server.
Common commands:
make release # build optimized binaries into bin/
make debug # debug build
make test # run golden tests (also builds release)
make run PROG=examples/test1.asm
make disasm ZVM=program.zvm
make web # build WebAssembly bundles (requires emcc)This repo deploys the web harness in examples/web to GitHub Pages using a GitHub Actions workflow.
- One-time setup (repo settings): Settings → Pages → Source: GitHub Actions
- Deploys automatically on push to
main(or you can manually run the workflow from the Actions tab) - The workflow builds the WASM bundle (Emscripten) and publishes the contents of
examples/web
Local build (optional):
make web
cd examples/web && python3 -m http.server 8000