bamts-codegen 0.1.0

Code generation (JIT/AOT) backend for BamTS
Documentation

Shared, backend-neutral Cranelift lowering for verified BamTS bytecode.

This crate turns a canonical [bamts_bytecode::Program<Verified>] into Cranelift IR through [lower_program]. It retains one [LoweredModule] per program module, with module-local pools and module-qualified native symbols, for both feature-gated backends:

  • a host-jit backend that finalizes each ir::Function into executable memory, and
  • an aot backend that emits each ir::Function into an object file.

This slice performs no executable-memory allocation and no object linking; it only builds and verifies IR. Both later backends supply their own [isa::TargetFrontendConfig] (via isa.frontend_config()), so the ISA choice, calling convention, and pointer type stay outside this crate.

Entry ABI

Every lowered function has the native-entry signature from the canonical execution plan (N5), matching bamts_native::ShadowFrame and bamts_native::Completion:

extern "C" fn(frame: *mut ShadowFrame, out: *mut Completion) -> u32
  • frame points at the register frame; frame.handles (offset 16) is the *mut Value register array. frame.bytecode_pc (offset 8) records the active instruction and carries the resume token after Suspend.
  • out receives the completion value; the returned u32 is a bamts_native::CompletionTag discriminant (Normal/Throw/Suspend/ FatalTrap).

Register addressing convention

Register r[i] lives at frame.handles + i * 8 (one Value/u64 slot). Every access derives the byte offset as i64::from(register.get()) * 8; the validation pass ([validate_slots]) proves this offset fits the Offset32 used by loads and stores, so u32 register ids and CLIF addresses never mix widths inconsistently. This holds for register ids well past 127: a slot offset is a full 32-bit displacement, not a signed byte.

Dynamic operands: no fixed windows, no constant-keyed properties

The production ISA carries no fixed argument window and no constant-keyed property access. Calls and constructs take a single arguments array in a register (Call/Construct arguments), so spread and any arity flow through one Value handle with no pointer arithmetic. Property access takes its key from a register (GetProperty/SetProperty/DeleteProperty key), a Value the runtime coerces to a property key (string, symbol, or private name). Closures capture through an array register (CreateClosure captures), again a single Value handle.

Value semantics: the explicit helper ABI

The bytecode algebra ([bamts_bytecode::Instruction]) is structural: the verifier proves definite initialization and CFG validity but assigns no value meaning. The NaN-boxed runtime Value requires tag dispatch that this IR-only slice must not open-code, so every operation whose result depends on runtime value semantics is lowered to a call into a declared [Helper] (u1:<index> external names a backend resolves to a C symbol). This crate declares each helper's ABI and control-flow contract; it never defines the helper body.

Every value-producing helper follows one completion ABI: fn(frame, <operands…>, out: *mut Completion) -> u32(tag). On Normal (0) the result is in out.value; on Throw the thrown handle is in out.value and control routes to a covering handler; FatalTrap always propagates to the runtime. Two exceptions to the "result in out.value" rule:

  • [Helper::Truthy] performs the total ToBoolean coercion and returns the truth value directly as 0/1; it never writes out and never throws.
  • [Helper::IteratorNext] writes two registers — it receives the done and value register indices and, on Normal, writes both slots in the frame directly (a single completion channel cannot carry two results); out.value is used only to carry a thrown handle on Throw.

A subset of the completion helpers is total (Normal only, never Throw/FatalTrap): [Helper::TypeOfGlobal], [Helper::LoadThis], [Helper::LoadArguments], [Helper::LoadNewTarget], and [Helper::CreatePrivateName]. They still use the completion ABI (result in out.value) but their abnormal edge is unreachable, so they never mark a handler block reachable.

Opcode ledger (every variant has an explicit path)

Opcode Lowering
LoadConst [Helper::LoadConstant] by ConstantIddst
Move inline copy handles[src]handles[dst]
Unary [Helper::Unary] with the operator selector
Binary [Helper::Binary] with the operator selector
CreateObject [Helper::CreateObject] → dst
CreateArray [Helper::CreateArray] → dst
CreateCell [Helper::CreateCell] → dst
CreateClosure [Helper::CreateClosure] (function, captures array)→dst
GetProperty [Helper::GetProperty] (object, register key) → dst
SetProperty [Helper::SetProperty] (object, register key, value)
DeleteProperty [Helper::DeleteProperty] (object, register key) → dst
DefineAccessor [Helper::DefineAccessor] (object, key, accessor, kind)
Call [Helper::Call] (callee, this, arguments array) → dst
Construct [Helper::Construct] (callee, arguments array) → dst
LoadGlobal [Helper::LoadGlobal] by string namedst
StoreGlobal [Helper::StoreGlobal] (string name, value)
TypeOfGlobal [Helper::TypeOfGlobal] by string namedst (total)
LoadThis [Helper::LoadThis] → dst (total)
LoadArguments [Helper::LoadArguments] → dst (total)
LoadNewTarget [Helper::LoadNewTarget] → dst (total)
ArrayPush [Helper::ArrayPush] (array, value)
ArrayExtend [Helper::ArrayExtend] (array, iterable)
ObjectSpread [Helper::ObjectSpread] (target, source)
SetPrototype [Helper::SetPrototype] (object, prototype)
CreatePrivateName [Helper::CreatePrivateName] by description → dst (total)
CreateRegExp [Helper::CreateRegExp] (pattern, flags) → dst
GetIterator [Helper::GetIterator] (src, kind) → dst
IteratorNext [Helper::IteratorNext] (iterator) → done + value
Import [Helper::Import] by string specifierdst
Export [Helper::Export] (string name, src)
Jump unconditional branch
JumpIfTrue [Helper::Truthy] then conditional branch
JumpIfFalse [Helper::Truthy] then conditional branch
Return handles[value]out.value, return Normal
Throw route to covering handler (bind catch_register) or
out.value + return Throw
Suspend yield path + resume path via [Helper::ResumeValue]
Halt undefinedout.value, return Normal

No opcode is silently dropped and none is lowered to a placeholder no-op.

Exceptions

When a completion-helper call returns Throw and a bytecode handler covers the current pc, control branches to that handler's block after storing the thrown value (out.value) into the handler's catch_register slot; the explicit Throw opcode binds its operand into catch_register directly. FatalTrap bypasses handlers. When no handler covers the pc, the completion is returned to the caller.

Suspend and the resume helper

Suspend { dst, src, resume } yields src and, when resumed, delivers the resumed value into dst before continuing at resume. The native entry ABI carries no resume input (out.value is the yielded value, not an input), so the resumed value is obtained through an explicit runtime contract rather than invented:

  • Yield path — store this suspend's resume token into frame.bytecode_pc (0 is a fresh call; the suspend at bytecode pc P uses token P + 1, so tokens never collide with a fresh entry or with each other), write src into out.value, and return Suspend.
  • Resume path — the dispatch prologue for token P + 1 calls [Helper::ResumeValue], which the runtime resolves to write the verified resumed value for this frame into out.value (it may return Throw for generator.throw, routed to a covering handler, or FatalTrap); the resumed value is then stored into dst and control continues at resume.

bamts_bytecode currently exposes no ABI for the resume input, so [Helper::ResumeValue] is a new required contract the runtime must provide for any module that suspends.