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 interactive demo isn't wired up yet. This page is the concept, the architecture, and the instruction set the VM will run. The full build plan lives in docs/build-spec-tiny-jvm.md.

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:

1
Source: a small C/BASIC-like language
2
Java toolchain: lex, parse, generate
3
Bytecode: one flat array of bytes
4
C VM: fetch / decode / execute loop
5
Two targets: WASM in the page, firmware on Wokwi

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.

OpcodeNameOperandEffect
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)
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 PRINT -- pop and emit to the output console
0x51 PINMODE -- mode, pin -> configure a GPIO pin
0x52 DWRITE -- value, pin -> drive a GPIO pin
0x53 DREAD -- pin -> push the pin's current level
0xFF HALT -- stop the VM

Sample programs

Written in the project's own language. The demo will let you pick one, compile it in the browser, and step the VM one instruction at a time with the stack visible.

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

This pass is scaffolding: the project's page, its listing, and the written specification. Still to come, in build order:

  • The C VM: interpreter loop, stack, locals, calls, GPIO hooks.
  • The Java toolchain: lexer, parser, code generator, a small CLI.
  • The WebAssembly build and the interactive stepper on this page.
  • The Wokwi project and an embedded view of it running the same bytecode.