Expand description
Translating one QCode block into a native function.
§What this compiles, and what it declines
A QCode block is already SSA, so the translation to Cranelift’s SSA is direct: an instruction’s result becomes a Cranelift value, and a value used only inside the block never reaches memory at all. That is the point of compiling. The interpreter has to materialise every intermediate into its value table because it cannot see past one operation at a time; compiled code keeps them in registers.
The compiler is deliberately partial. It handles integer arithmetic and
accesses to flat spaces — registers, uniques, per-function temporaries —
whose addresses are constants known at compile time, so a register access
becomes a load at a fixed offset from a base pointer. Anything else
(Unsupported) is declined, and the caller runs that block on the
interpreter instead. Declining is a normal outcome, not a failure: it is what
lets this be an alternative strategy rather than a replacement.
Guest RAM is handled here, but not by addressing it: an access to it owes
a translation, a permission check and a fault report, and those belong to
the MMU. What is inlined is the MMU’s answer — a software TLB entry giving
the host address of a resident guest page, and the permission bytes sitting
beside that page’s data. An access that the inlined form cannot settle (a
page not yet cached, one straddling a boundary, a permission it refuses)
calls back into the VM, which is the only implementation of what an access
means. See qcode_vm::jit_abi.
A faulting access stops the block where it happened and hands the fault back through the function’s status result. The stores that ran before it stay applied, which is what the interpreter would have left behind too — so guest RAM and the flat spaces are deliberately compiled without alias regions, keeping Cranelift from reordering one store past another and making that prefix something other than a prefix.
§Terminators
Control flow itself stays with the interpreter: it runs the terminator after
compiled code has run the body, so branch resolution, block parameters and
call semantics live in exactly one implementation. What the compiler has to
supply is the terminator’s operands — a cbranch condition, a branch’s
block arguments — because those are values the body computed and compiled
code keeps in registers, where the interpreter cannot see them.
So a block’s compiled function takes a second argument: an export buffer.
Every terminator operand defined in this block is written there as a u64,
and the runtime copies it into the interpreter’s value table before handing
the terminator back. A terminator whose operands are all literals, addresses
or values from earlier blocks exports nothing and costs nothing — which is
the argument-less unconditional branch that straight-line guest code lifts
to.
§Escaping values
The same reasoning applies to any value that outlives the block, not just the ones the terminator reads. Values crossing a block boundary as bare SSA references do not arise in SLEIGH-lifted code — guest state travels through registers and uniques, which are memory — but nothing in QCode forbids them, and dropping one would be a silent miscompile rather than a decline. So a block with a result used from outside it is declined outright.
Structs§
- Export
- One value compiled code hands back for the interpreter to read.
- Helpers
- The runtime entry points compiled code calls when an access cannot be settled inline, as declared in the module.
- Space
Table - The flat spaces a compiled block touches, and how many bytes of each it must be able to address.
Enums§
- Unsupported
- Why a block could not be compiled.
Constants§
- BLOCK_
FAULT - Status a compiled block returns: an access faulted and the block stopped there. The fault itself is on the VM’s memory.
- BLOCK_
OK - Status a compiled block returns: it ran to the end of its body.