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-jitbackend that finalizes eachir::Functioninto executable memory, and - an
aotbackend that emits eachir::Functioninto 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
framepoints at the register frame;frame.handles(offset 16) is the*mut Valueregister array.frame.bytecode_pc(offset 8) records the active instruction and carries the resume token afterSuspend.outreceives the completion value; the returnedu32is abamts_native::CompletionTagdiscriminant (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 as0/1; it never writesoutand never throws. - [
Helper::IteratorNext] writes two registers — it receives thedoneandvalueregister indices and, onNormal, writes both slots in the frame directly (a single completion channel cannot carry two results);out.valueis used only to carry a thrown handle onThrow.
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 ConstantId → dst |
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 name → dst |
StoreGlobal |
[Helper::StoreGlobal] (string name, value) |
TypeOfGlobal |
[Helper::TypeOfGlobal] by string name → dst (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 specifier → dst |
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 |
undefined → out.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(0is a fresh call; the suspend at bytecode pcPuses tokenP + 1, so tokens never collide with a fresh entry or with each other), writesrcintoout.value, and returnSuspend. - Resume path — the dispatch prologue for token
P + 1calls [Helper::ResumeValue], which the runtime resolves to write the verified resumed value for this frame intoout.value(it may returnThrowforgenerator.throw, routed to a covering handler, orFatalTrap); the resumed value is then stored intodstand control continues atresume.
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.