Devlog · July 2026

From spec to running VM: the initial build

NL started life fully specified: nlvm-specs defines the grammar, the semantics, 49 compile-time checks, the bytecode format and a milestone roadmap — before this implementation existed. This post retraces how the toolchain was built against it.

Two decisions before any code

Rust, as a multi-crate Cargo workspace. And a vertical-slice strategy: rather than finishing the lexer, then the parser, then the type checker, the first milestone was to get one minimal program through the entire pipeline — lexer → parser → bytecode → VM → exit code — with the YAML test runner in place from day one. Everything after that widens the slice.

The workspace layout has survived unchanged:

crates/
├── nl-syntax/       # lexer + parser + AST
├── nl-sema/         # name resolution, typing, the 49 checks
├── nl-bytecode/     # .nlm format: encoding AND decoding, shared
├── nl-codegen/      # validated AST → bytecode
├── nl-vm/           # interpreter, object model, exceptions, natives
├── nlc/  nlvm/      # the two CLI binaries
└── nl-test-runner/  # `nltest`, runs the YAML suites

The load-bearing choice is nl-bytecode: one crate owns the module format, the opcodes and the type descriptors, and both the compiler and the VM depend on it. There is no way for the two halves to disagree about what the bytecode means.

Tests as the steering wheel

The specification ships its own conformance tests — YAML files with NL sources, an expected stdout and an expected exit code. The test runner was built in phase 1, before most of the language existed, so from the first week every feature landed the same way: pick the spec tests it should unlock, write local tests for the corners, make them green, move on. The local suite now counts 232 files, one per feature.

Phase by phase

Decisions worth recording

Memory management is reference counting. The Arc refcount is the GC. Assumed limitation: object cycles are never collected, so their destructors don't run — a cycle collector is a possible later addition. Writing the limitation down mattered more than the choice itself.

Runtime safety is part of the spec. Some conformance tests check behavior like use-after-close on files — the specs demand runtime guarantees, not just compile-time ones, and the VM is built accordingly.

The toolchain releases itself. Since 0.3.0, tagging a release is done by tools/Release.nl — an NL script using the stdlib's file, regex and process APIs. Dogfooding found real bugs, which is the point.


The full phase-by-phase working notes (in French) are in journal/journal_01_initial_build.md.