Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

neon_vm

demo license: MIT

A stack-based virtual machine written from scratch in C. It executes densely packed binary opcodes through its own dispatch loop, the same shape of machine that sits under runtimes like the JVM or the CPython interpreter.

Step through it in your browser → The VM is compiled to WebAssembly and driven one instruction at a time, with the bytecode, the instruction pointer, and the stack drawn after every step.

Written to understand what a bytecode interpreter is actually doing between "here is a byte" and "here is a result".

How it works

  • Bytecode chunks. A Chunk is a growable uint8_t array of instructions plus a parallel pool of double constants. Instructions that need a literal store a one-byte index into that pool rather than the value itself, which keeps the instruction stream compact.
  • A stack machine, not a register machine. There are no registers. Operands are pushed onto a fixed 256-slot stack, and each arithmetic opcode pops what it needs and pushes the result back. 1.2 + 3.4 * 5.6 becomes three pushes, a multiply, and an add.
  • The dispatch loop. vmStep() in src/vm.c reads one instruction, switches on it, and returns. interpret() is a loop around it; the web demo calls it once per click. The instruction pointer is a raw uint8_t* walking the chunk directly.
  • Bytecode is untrusted input. Every step checks what it is about to touch: stack overflow and underflow, a constant index past the pool, an OP_CONSTANT with its operand cut off, an unknown opcode, and running off the end of the chunk without an OP_RETURN. Each stops the VM with a runtime error rather than reading or writing memory it does not own.
  • Instruction set. OP_CONSTANT, OP_ADD, OP_SUBTRACT, OP_MULTIPLY, OP_DIVIDE, OP_RETURN. That is the whole ISA.

Build & run

git clone https://github.com/apollo-2006/neon_vm.git
cd neon_vm

make
./neon_vm

src/main.c hand-assembles the expression 1.2 + 3.4 * 5.6 and runs it:

--- Neon VM Booting ---

          [ 1.2 ]
          [ 1.2 ][ 3.4 ]
          [ 1.2 ][ 3.4 ][ 5.6 ]
          [ 1.2 ][ 19.04 ]
          [ 20.24 ]
Program Execution Finished. Result: 20.24

The stack dump before each instruction comes from DEBUG_TRACE_EXECUTION in include/common.h. Build with -DNEON_NO_TRACE for a silent run.

Tests

make test

tests/test_vm.c runs well-formed programs and every class of malformed one (underflow, overflow at 257 pushes, a missing OP_RETURN, a truncated operand, a bad constant index, an unknown opcode) and checks the VM stops in the expected state. It builds with AddressSanitizer and UndefinedBehaviorSanitizer, so an out-of-bounds read fails the run even if the result happens to look right.

Web demo

web/ holds the browser build: neon_web.c exposes the Chunk and VM to JavaScript, and web/build.sh compiles it with the unmodified src/ files using Emscripten. GitHub Actions runs the tests, builds the demo, and publishes it to Pages on every push to main, so the live page always runs the code in this repository.

web/build.sh                          # needs emcc on PATH
python3 -m http.server -d web/dist    # then open http://localhost:8000

The demo includes a small infix-to-bytecode compiler in web/app.js so you can type an expression instead of assembling by hand. It is part of the page, not the VM.

Layout

include/chunk.h   Chunk: bytecode array + constant pool
include/vm.h      VM state, interpret(), vmLoad(), vmStep()
src/chunk.c       growable arrays
src/vm.c          checked single-step dispatch
src/main.c        hand-assembled example
tests/test_vm.c   well-formed and malformed programs
web/              WebAssembly bindings and the demo page

Known limits

  • There is no compiler. Bytecode is written by hand in main.c; there is no lexer, parser, or source language in front of the VM.
  • One value type. Everything on the stack is a double. No strings, booleans, objects, or type tags.
  • No control flow. No jumps, branches, loops, or calls, so the ISA can only express straight-line arithmetic.
  • 256 constants per chunk. The OP_CONSTANT operand is one byte. addConstant does not refuse a 257th constant, so a caller has to check the index it returns (the web assembler does).

License

MIT. See LICENSE.

Author

Abir Deol · abirdeol.tech

About

Stack-based bytecode virtual machine in C with bounds-checked dispatch and a step-through WebAssembly debugger.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages