jit
the jit is a minimal x86-64 code generator that walks
skalman's REPL
ASTthe same ast nodes repl_eval_node interprets in c0.
and emits raw machine code into an identity-mapped page. the generated code is
invoked through c0's indirect-call mechanismint fn = jit_page; return fn();, c0's function-reference feature.
and the return value in rax becomes the c0 return value. no coff, no page-table
changes. the kernel identity-maps the first 16 mb,
jenna's pmm_alloc_page
returns addresses in that range, and every page is executable by default.
architecture
instruction encoder
eighteen functions that emit raw x86-64 bytes. jit_emit(pos, byte)
does a single store8 into the jit page. everything else is built on
top of it, mov eax, imm32, add/sub/imul rax, rbx,
cqo + idiv, cmp + setcc + movzx for comparisons,
push/pop for register save and restore, call rax for
function calls into kernel helpers, test + je/jmp with rel32 offsets
for control flow. all immediates are byte-extracted via arithmetic (c0 has no
bitwise operators).
a helper jit_byte(v, n) extracts byte n from value
v using repeated division by 256. byte 7 overflows
(256^7 × 256 = 2^64 wraps to 0 → divide-by-zero), so the function
short-circuits to 0 for all bytes 4 and above, since all kernel addresses are
identity-mapped and fit in 32 bits.
ast compiler
jit_compile_node(nd, pos) recursively walks the repl ast and emits
code for every supported node kind.
- expressions: literals, negations, arithmetic (add, sub, mul, div), six comparisons (eq, ne, lt, le, gt, ge), variables (compile-time lookup from the repl symbol table), string literals (embedded in the jit page's data area)
- statements: blocks, expression-statements, if/else, while, for, break, continue, giving full control flow with label-based jumps and deferred patching
- builtins:
print()(integer and string, via kernel helpers), assignment (x = expr, also via a helper),load8andload64(single-instruction inline),store8andstore64(two-argument inline)
right-first evaluation (compile right → push rax → compile left → pop
rbx) gives rax=left, rbx=right for all binary operators
with just two bytes of push/pop overhead per operation.
lifecycle
jit_init allocates one 4096-byte page. jit_compile_and_run
sets the page writable, compiles the ast, emits a final ret, removes
the writable bit, and calls the code. after execution, the writable bit is restored
for the next compilation. the page is never simultaneously writable and executable
(w^x via the writable bit).
shell integration
eval -c <expr> in skalman routes through
repl_eval_jit, which does tokenize → parse → compile → execute. output is
prefixed [jit] (vs [repl] for the interpreter), both
for testability and to make the distinction visible. a full program with multiple
statements compiles natively. loops, variables, conditionals, and function calls
all work in a single pass.
compiled code can call back into c0 functions at runtime. kernel helpers live in
the elf at known addresses (auto-patched by sync_addrs.sh after every
rebuild). this is the current helper table.
| slot | function | purpose |
|---|---|---|
JIT_PRINT_ADDR | jit_print_helper(v) | print an integer + newline |
JIT_PRINT_STR_ADDR | jit_print_str_helper(str) | print a string + newline (serial + screen) |
JIT_ASSIGN_ADDR | jit_assign_helper(name, len, value) | create or update a repl variable |
w^x protection
efer.nxe is enabled at boot (bit 11 in the efer msr), so bit 63 in a page-table entry controls executability. the jit page toggles between two modes.
| mode | pte | w | nx | effect |
|---|---|---|---|---|
| compile | phys + 3 + NX | yes | yes | can write, cannot execute |
| execute | phys + 1 | no | no | can execute, cannot write |
jit_set_writable(w) walks the four-level identity-mapped page table,
clears the nx bit arithmetically (c0's truncate-toward-zero division gives wrong
results on negative pte values), and stores the new pte. no tlb flush, a write
to a pte invalidates any cached tlb entries for that virtual address on x86.