Tiny JVM
A compact virtual machine that runs the same program two ways: as WebAssembly in this page, and as firmware on a microcontroller. The program is written in a small language of its own and turned into bytecode by a toolchain written in Java.
The idea
"Java" and "embedded" usually sit in two different projects. This one fuses them into a single artifact. A tiny stack-based interpreter (a few hundred lines of portable C) executes a fixed bytecode. Because the interpreter is just C with no OS calls, the exact same source compiles to two targets: a WebAssembly module that runs in the browser, and a firmware image that runs on an ESP32 in the Wokwi simulator.
The bytecode is produced by a small compiler written in Java: a lexer, a recursive-descent parser, and a code generator. Writing that toolchain is the Java half of the project; keeping the VM small enough to fit a microcontroller is the embedded half.
How it works
Five stages, from source text to a running program:
The same bytecode file is byte-for-byte identical on both targets. The over-temp-alarm sample below reads a sensor and drives an output pin; run it in the browser and the pin writes go to a fake console, flash it to the Wokwi board and they light a real LED in the simulation.
Instruction set
One-byte opcodes, a value stack of 32-bit integers, up to 256 local-variable slots. This table is the contract the C VM and the Java code generator both target.
| Opcode | Name | Operand | Effect |
|---|---|---|---|
| 0x01 | PUSH | int32 (LE) | push a constant onto the stack |
| 0x02 | POP | -- | discard the top of stack |
| 0x03 | DUP | -- | duplicate the top of stack |
| 0x10 | ADD | -- | a, b -> a + b |
| 0x11 | SUB | -- | a, b -> a - b |
| 0x12 | MUL | -- | a, b -> a * b |
| 0x13 | DIV | -- | a, b -> a / b (trap on 0) |
| 0x14 | LT | -- | a, b -> (a < b) ? 1 : 0 |
| 0x20 | LOAD | uint8 slot | push local variable #slot |
| 0x21 | STORE | uint8 slot | pop into local variable #slot |
| 0x30 | JMP | int16 offset | unconditional branch |
| 0x31 | JZ | int16 offset | branch if top of stack == 0 |
| 0x32 | JNZ | int16 offset | branch if top of stack != 0 |
| 0x40 | CALL | uint16 addr | call function at bytecode addr |
| 0x41 | RET | -- | return from function |
| 0x50 | -- | pop and emit to the output console | |
| 0x51 | PINMODE | -- | pin, mode -> configure a GPIO pin |
| 0x52 | DWRITE | -- | pin, value -> drive a GPIO pin |
| 0x53 | DREAD | -- | pin -> push the pin's current level |
| 0xFF | HALT | -- | stop the VM |
Try it
Pick a sample (already compiled to bytecode -- tlc runs server-side
during the build, not in your browser) and step the VM one instruction at a time,
watching the value stack, locals, and console update live.
Value stack
-
Locals (slots 0-7)
-
Console (PRINT output)
GPIO -- relevant to the over-temp alarm sample
Sample source
Written in the project's own language -- what actually got compiled into the bytecode the demo above is running.
Fibonacci
Shows: loops, locals, arithmetic
let a = 0;
let b = 1;
let i = 0;
while (i < 15) {
let t = a + b;
a = b;
b = t;
i = i + 1;
}
print a;
Factorial (recursive)
Shows: function calls, the call stack, RET
fn fact(n) {
if (n == 0) { return 1; }
return n * fact(n - 1);
}
print fact(6);
Over-temp alarm
Shows: the same bytecode driving a GPIO on the Wokwi board
# reads a simulated sensor on pin 34, lights pin 2 when it's hot
pinMode(2, OUTPUT);
while (1) {
let c = digitalRead(34);
digitalWrite(2, c > 28);
}
What's built so far
- The C VM: interpreter loop, stack, locals, calls, GPIO hooks, and a full set of traps (divide-by-zero, stack overflow, bad bytecode) instead of undefined behavior -- 8/8 hand-assembled test cases pass, including the adversarial ones.
- The Java toolchain: lexer, recursive-descent parser with panic-mode error recovery, a scope-resolving pass, and a code generator with full back-patching for control flow and function calls -- 57 JUnit tests pass.
- The WebAssembly build and the interactive stepper above, running the exact samples the Java compiler produced.
Still to come: the Wokwi firmware view running the same bytecode on a simulated ESP32, and a GitHub repo/README for the standalone VM and toolchain.