Status: Stage 13 — bytecode closure compilation + fn type checking. Target: A Nulang→Nulang compiler written in Nulang Core (RFC 0002) that targets the
.nbcformat (RFC 0001).
source.nula
→ compiler_core.nula (lexer + parser + type checker + evaluator in Core)
→ compile_hex.nula (Core → hex bytecode emitter)
→ fixup_hex.py (patch jump offsets + constant pool)
→ hex2nbc.py (hex → .nbc binary)
→ source.nbc (frozen bytecode artifact)
→ VM::run(nbc)
| File | Purpose |
|---|---|
host.nula |
Host shim |
compiler_core.nula |
Lexer + Pratt parser + type checker + evaluator in Nulang Core |
compile_arith.nula |
Bytecode compiler for arithmetic (prints VM instructions) |
compile_hex.nula |
Hex-output bytecode compiler (u32 words as 8-char hex) |
fixup_hex.py |
Patch Jmp/JmpF/JmpT offsets and ConstU indices |
hex2nbc.py |
Convert hex text to .nbc binary |
self_test.nula |
Core conformance target (fib(10) = 55) |
spill_bug_repro.nula |
Minimal repro for spill temp clobbering bug (fixed) |
# Interactive evaluator (stdin):
echo "1 + 2 * 3" | nulang bootstrap/compiler_core.nula
# → 7
# Bytecode compiler (stdin):
echo "1 + 2 * 3" | nulang bootstrap/compile_arith.nula
# ; 1 + 2 * 3
# Const1 r8
# Const2 r9
# ConstU r10 # 3
# IMul r9 r10 r11
# IAdd r8 r11 r10
# Halt
# ; result in r10
# Hex bytecode compiler (piped through fixup):
echo "1 + 2 * 3" | nulang bootstrap/compile_hex.nula | python3 bootstrap/fixup_hex.py
# Full .nbc pipeline:
echo "1 + 2 * 3" | nulang bootstrap/compile_hex.nula | python3 bootstrap/fixup_hex.py | python3 bootstrap/hex2nbc.py > out.nbc
nulang out.nbc
# → 7
# Hex compiler self-test (when stdin is empty, compiles "1 < 2 and 2 < 3"):
nulang bootstrap/compile_hex.nula < /dev/null
# Self-test (when stdin is empty):
nulang bootstrap/compiler_core.nula < /dev/null
# Expected: 42, 7, 9, 43- Lexer: character-at-a-time scanning via
perform String.charAt/String.length. - Parser: single-function Pratt parser with correct precedence and left-associativity.
- Let bindings:
let x = 42 in x + 1→ 43. 2-slot environment (e0, e1).
- Lambdas:
fn(x) => x + 1— parsed inline in the Pratt prefix handler. - Function application:
f(arg)— handled as a postfix operator with highest precedence. - Environment capture:
let a = 3 in (fn(x) => a + x)(5)→ 8. - Currying:
let add = fn(a) => fn(b) => a + b in add(3)(4)→ 7.
- Environment: expanded from 2 slots to 4 slots (e0..e3), supporting 4 nested
letbindings. - Lookup: recursive
env_lookupsearches most-recent slot first for correct shadowing.
- Conditional:
if <cond> then <then> else <else>— parsed in the Pratt prefix handler. - Non-zero condition values are truthy; zero is falsy.
- Comparisons:
==,!=,<,>,<=,>=— all return 1 (true) or 0 (false). - Boolean operators:
and(prec 1),or(prec 0),not(prefix). - Boolean literals:
true→ 1,false→ 0. - Stdin REPL: reads expression from stdin, evaluates, prints result.
- compile_arith.nula: single-pass Pratt compiler emits VM instructions as text.
- Supports integer literals,
+,-,*,/, and parenthesized expressions. - Register allocation: starts at r8, linear assignment per subexpression.
- let bindings: scoped variables via env (hash|reg), 4 slots. Variable refs emit Move.
- if/then/else: JmpF/Jmp with position-based labels (L0e/L0x).
- Outputs
Const0/1/2/M1/U,IAdd/ISub/IMul/IDiv,Move,ICmp*,JmpF(short-circuit),Jmp,JmpF,Jmp,Halt.
- compile_hex.nula: emits u32 instruction words as 8-char hex (one per line).
- Adds
hex_digithelper andemit_hexfor hex formatting. - Works around Nulang string-var concatenation bug using
""prefix trick. - fixup_hex.py: patches Jmp/JmpF/JmpT offsets and ConstU indices in a two-pass fixup.
- hex2nbc.py: converts corrected hex text to
.nbcbinary (NLBC magic, header, JSON metadata). - Bool/Int conversion: comparisons return Bool-tagged values (bit 39);
and/or/ifconvert Int→Bool viaICmpEq+Notwhen needed.!=lowered toICmpEq+Not. - Full pipeline:
compile_hex.nula | fixup_hex.py | hex2nbc.py > out.nbc - Outputs
Const0/1/2/M1/U,IAdd/ISub/IMul/IDiv,ICmp*,Not,Move,JmpF,JmpT,Jmp,Halt.
- Type checker: separate
tc_prattpass runs before evaluation. - Types:
Int(0),Bool(1),Error(2). Type environment mirrors value environment. - Integer literals →
Int,true/false→Bool. - Arithmetic (
+,-,*,/) requiresIntoperands, producesInt. - Comparisons (
==,!=,<,>,<=,>=) requireIntoperands, produceBool. - Boolean ops (
and,or,not) requireBooloperands, produceBool. if c then t else e:cmust beBool;tandemust have the same type.let x = v in body: propagates type ofvtoxinbody.- Error reporting: prints "Type error: expected X, got Y" and outputs 0 instead of evaluating.
- Fn type:
tc_prattreturns typeFn(3) for closures instead ofInt(0). - Function application:
f(arg)validates thatfhas typeFn, produces "expected function" errors. - Parameter typing: closure parameters added to type environment as
Int. - Error propagation: type errors inside closure bodies propagate outward.
- Currying:
Fntype preserved through application results for chained calls.
- compile_hex.nula: emits
Closure(0x60),ClosureCall(0x64), andRetVal(0x57) opcodes. - Named functions:
fn name(x) => bodysupport desugars to env binding + continuation. - Function table:
fixup_hex.pypatches placeholder function indices and Jmp offsets;hex2nbc.pybuilds.nbcfunction table fromFN_STARTmarkers. - Argument passing: caller moves arg to r10; VM copies all registers to the new frame on
ClosureCall. - Conditionals in closures:
(fn(x) => if x then 1 else 0)(1) = 1— fixedfixup_hex.pyJmp target priority (fn_endbeforeend). - End-to-end verified:
(fn(x) => x + 1)(5) = 6,fn add(x) => x + 1 add(5) = 6,(fn(x) => x + 1)((fn(y) => y * 2)(3)) = 7. - Limitations: multi-fn continuation parsing; nested closures without capture support return closure values.
types.lean:canonical_formsproved (wassorry).capabilities.lean:cap_sendableanddischarge_sendableproved;is_sendablefixed to includeIso.
- Module-level parsing (multiple
fndefinitions in one file) - Multi-binding closure capture via
CapStore/CapLoadopcodes - HM type inference
- Type ascription syntax (
x: Int) - Self-compilation (
compiler_core.nula→compiler_core.nbc)