Skip to main content

Module compile

Module compile 

Source
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.
SpaceTable
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.