Skip to content
MokiMeowPublic

About

A small language with a JIT compiler that emits real x86-64 machine code into executable memory

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

nutjit

A small language with a JIT compiler that emits real x86-64 machine code into executable memory and jumps to it: no LLVM and no interpreter in the execution path.

x86_64 C++17 hand-rolled codegen v1.1.0 MIT

nutjit REPL demo showing persistent variables and functions

What this is

Source passes through a hand-written lexer, a recursive-descent parser with semantic validation, and nutjit's own x86-64 code generator. The emitted bytes are copied into writable pages, those pages become read/execute-only, and the host calls the generated function. The result really was computed by machine code written at runtime.

source → lexer → parser + validation → AST → optimise → x86-64 bytes → W^X memory → CALL

Architecture

The front end produces a validated AST shared by two backends. The interpreter is the semantic oracle; the JIT lowers the same tree into System V AMD64 instructions, resolves branches and calls, copies the bytes into writable pages, changes those pages to read/execute, and invokes the generated entry point. Differential tests compare both paths while byte-level tests pin the important encodings.

Measured result

One representative WSL2 run of make bench:

nutjit benchmark: fib(30), single-threaded medians
  3 interpreter runs, 21 compile samples, 21 JIT runs

  fib(30) = 832040  (all three back ends agree)

  implementation           run (ms) compile (ms)    speedup
  tree-walking interp       3507.69            -       1.0x
  nutjit (naive)               5.08        0.001     690.9x
  nutjit (optimised)           4.72        0.002     742.5x

  code size: naive 208 bytes, optimised 151 bytes

Timing varies by host, so the harness reports medians rather than presenting a single sample as a constant. The optimized backend combines constant folding, short immediate encodings, and a small R10/R11 scratch-register allocator. It spills when an expression exhausts the pool or must preserve a value across a call.

$ ./build/nutjit --dump "2 + 3 * 4 - 1;"
b8 0d 00 00 00        # mov eax, 13: one instruction

The language

fn fib(n) {
  if (n < 2) { return n; }
  return fib(n - 1) + fib(n - 2);
}

let total = 0;
let i = 0;
while (i < 10) {
  total = total + fib(i);
  i = i + 1;
}
total;

The language has signed 64-bit integers, let, assignment, arithmetic, comparisons, if/else, while, and functions with up to six parameters and recursion. The validator rejects unknown names, duplicate functions or parameters, wrong call arity, top-level return, and variables that are not definitely declared on every control-flow path.

Why it is interesting

  • The complete compiler pipeline is small enough to read.
  • The backend hand-encodes REX prefixes, ModR/M bytes, signed division, comparisons, relative calls, and backpatched jumps.
  • Generated functions follow the System V AMD64 ABI. Temporary expression spills are tracked so every generated call is correctly 16-byte aligned.
  • JIT pages obey W^X: writable while filled, read/execute while called.
  • A tree-walking interpreter provides an independent oracle. Every behavioral test must agree in both backends.
  • Byte-exact golden tests also lock down important instruction encodings and call-alignment sequences.

See the architecture guide, the code generator, and the calling-convention notes.

Quick start

sudo apt-get install -y g++ make

make run
make test                  # 63 differential cases + encoding goldens
make bench

./build/nutjit "let x = 5; x * 2;"
./build/nutjit --dump "1 + 2;"
./build/nutjit --interp "1 + 2;"
./build/nutjit --file program.nut
./build/nutjit --repl      # successful variables/functions persist

Status

# Milestone State
0 Arithmetic JIT ✅ done
1 Variables and stack frames ✅ done
2 Comparisons and conditionals ✅ done
3 Loops ✅ done
4 Functions and recursion ✅ done
5 Optimisation and interpreter oracle ✅ done
6 REPL, benchmark, tests, CI, and releases ✅ done

The release build is warning-free. make test currently proves 63 behavioral cases through both backends and six byte-level encoding properties.

Inspect the output

./build/nutjit --dump "1 + 2;" 2>&1 |
  grep -E '^[0-9a-f ]+$' | xxd -r -p > /tmp/nutjit-code.bin
objdump -D -b binary -m i386:x86-64 /tmp/nutjit-code.bin

Requirements

Linux or WSL2 on x86-64 with g++ (C++17) and make. The JIT deliberately targets the System V x86-64 ABI and uses mmap/mprotect.

Limitations

The language uses signed 64-bit integers and targets only the System V x86-64 ABI. It has no floating-point values, aggregates, garbage collector, native object-file output, debugger integration, or cross-platform backend. Benchmark claims cover the documented recursive integer workload, not general compiler performance.

Documentation

Read the architecture, then the lexer and parser, x86-64 code generator, JIT memory policy, calling convention, and optimization notes. Testing and disassembly workflows are in docs/09.

License

MIT: see LICENSE.

About

A small language with a JIT compiler that emits real x86-64 machine code into executable memory

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages