heyq02/moonjs/src/bytecode does not have a README file

    Chunk

    #alias(ChunkRef)
    pub struct Chunk {
    code : Array[UInt]
    const_pool : Array[
    JSValue
    ]
    source_locs : Array[
    SourceLoc
    ]
    name : String
    filename : String
    param_count : Int
    local_count : Int
    upvalue_slots : Array[UpvalueSlotDecl]
    nested_chunks : Array[Chunk]
    is_strict : Bool
    self_binding_slot : Int
    } derive(
    Debug
    )

    A compiled bytecode chunk plus its metadata. One per function template (and one for the top-level script). Mutable — the compiler builds a Chunk incrementally via the emit_* helpers, then hands it to the VM.

    • code: 32-bit instructions, encoded via encode from instr.mbt.
    • const_pool: run-time constant values referenced by push_const and friends. Deduplication (add_const) uses JSValue's ==, so equal primitives share one slot and distinct object references never dedup.
    • source_locs: parallel to code; source_locs[pc] is the source location that produced code[pc]. Kept as a separate array (rather than an interleaved encoding) so a release build can drop it with strip_debug() (M6).
    • name: the function's display name (or "<top>" for scripts). Used by Error.prototype.stack and by the disassembler.
    • filename: whatever Engine::eval_script received. Same consumers.
    • param_count: number of declared parameters. VM Step 8 uses this to decide arity fill / drop when a call passes too few or too many args.
    • local_count: total number of local slots (params + var / let / const bindings). Determines the size of Frame.locals.
    • upvalue_slots: capture plan for this template. See upvalue.mbt.
    • nested_chunks: function-template children. new_closure references them by index; keeping them here rather than in const_pool matches design.md §4.4 and avoids a special-case in the pool.
    • is_strict: reserved for M2 strict-mode semantics; always false in M1.
    • self_binding_slot: for a named function expression, the local slot that the VM should populate with the newly-created Function value at call time so function f() { ... f() ... } sees f inside the body. -1 when the function has no self-binding (top-level scripts, anonymous function expressions, function foo declarations — those bind the name through their enclosing scope's mechanism, not a slot).

    Chunk::add_const

    fn Chunk::add_const(self : Chunk, v :
    JSValue
    ) -> Int

    Deduplicating constant-pool insert. Two intern-equal JSValues always share one slot; two distinct Object(_) references never dedup because JSValue's Eq uses physical equality for objects (see value/value.mbt).

    Returns the pool index. The dedup is O(n) in the pool size — acceptable for M1 chunks (dozens to hundreds of entries) and easy to swap for a hash index if a future compiler pushes it into the thousands.

    Chunk::add_nested

    fn Chunk::add_nested(self : Chunk, chunk : Chunk) -> Int

    Add a nested function template. Returns the index used by OP_NEW_CLOSURE.

    Chunk::add_upvalue

    fn Chunk::add_upvalue(self : Chunk, slot : UpvalueSlotDecl) -> Int

    Add an upvalue slot to the capture plan. Returns the newly-allocated slot index (used by the compiler to emit get_upvalue / set_upvalue).

    Chunk::alloc_local

    fn Chunk::alloc_local(self : Chunk) -> Int

    Bump local_count when the compiler introduces a new local slot beyond the parameter list. Returns the newly-allocated slot index.

    Chunk::disassemble

    fn Chunk::disassemble(self : Chunk) -> String

    Human-readable disassembly of the chunk. One line per logical instruction (a wide-encoded operand still renders as a single line). Trailing newline is included so appending disassembler output to a growing log is clean.

    This is a debug helper, not a stability contract — the exact spacing and column widths may change to accommodate longer opcode names in later milestones. Snapshot tests should be updated when the format shifts.

    Chunk::emit

    fn Chunk::emit(self : Chunk, op : Byte, a : Byte, b : Byte, c : Byte, loc :
    SourceLoc
    ) -> Unit

    Emit a raw four-byte instruction. The caller is responsible for choosing wide-vs-narrow encoding; most callers use emit_wide_u32 / emit_wide_i32 instead. loc is pushed to source_locs so the array stays parallel with code.

    Chunk::emit_wide_i32

    fn Chunk::emit_wide_i32(self : Chunk, op : Byte, operand : Int, loc :
    SourceLoc
    ) -> Unit

    Emit an opcode carrying a signed 32-bit operand. Chooses the narrowest form that fits — 24-bit signed range is -8_388_608 ..= 8_388_607; outside that, falls back to the wide two-instruction form (high 24 bits + low 8 bits).

    The reader side sign-extends the 24-bit narrow form; the 32-bit wide form carries a full signed integer so no extension is needed. See read_operand_i24.

    Chunk::emit_wide_u32

    fn Chunk::emit_wide_u32(self : Chunk, op : Byte, operand : UInt, loc :
    SourceLoc
    ) -> Unit

    Emit an opcode carrying an unsigned integer operand up to 32 bits. Picks the narrowest form:

    • If operand <= 0xFFFFFF: single instruction [op | HI | MID | LO].
    • Otherwise: [WIDE | W_HI | W_MID | W_LO] [op | LOW8 | 0 | 0], where the wide slot supplies the high 24 bits.

    The wide encoding is symmetric with read_operand_u24. Both entries in the pair share loc — the disassembler treats a wide pair as one logical line.

    Chunk::new

    fn Chunk::new(name : String, filename : String, param_count : Int) -> Chunk

    Fresh, empty chunk. local_count starts at param_count because every parameter occupies one local slot before the compiler even walks the function body. Additional locals are recorded by the compiler by growing local_count as it processes var / let / const declarators.

    Chunk::patch_jump

    fn Chunk::patch_jump(self : Chunk, at_pc : Int, target_pc : Int) -> Unit

    Rewrite the operand of the jump instruction at at_pc so that it targets target_pc (both are word indices into code). The offset stored is signed, relative to the instruction following at_pc — i.e. target_pc - (at_pc + narrow_advance).

    Constraint: this M1 helper only supports patching within whatever encoding form the emit call originally chose. Because the compiler cannot know the eventual distance to a forward target when it emits the jump, it should always emit forward jumps via emit_wide_u32 / emit_wide_i32 with a placeholder that already reserves the wide two-instruction form (i.e. pass a placeholder operand >= 0x1000000 so the emit picks wide). Backward jumps have a known distance and can be emitted narrow directly.

    If the wide slot is present but the new offset happens to fit in 24 bits, this function still writes the wide encoding (keeping the code array shape unchanged); if the wide slot is absent but the offset needs it, this function aborts with a clear message.

    Chunk::read_operand_i24

    fn Chunk::read_operand_i24(self : Chunk, pc : Int) -> (Int, Int)

    Decode the operand of an opcode expecting a signed integer. Narrow form: sign-extends a 24-bit value; wide form: reinterprets the 32-bit UInt as a signed Int (which is what MoonBit's to_int does bit-for-bit).

    Chunk::read_operand_u24

    fn Chunk::read_operand_u24(self : Chunk, pc : Int) -> (UInt, Int)

    Decode the operand of an opcode expecting an unsigned integer. Handles both the narrow (24-bit) and wide (32-bit) forms transparently.

    Returns (operand, pc_advance) where pc_advance is 1 for the narrow form and 2 for the wide form. The VM's pc should be advanced by that amount to skip past both instructions in the wide case.

    Precondition: pc must point at an instruction whose opcode carries an unsigned operand (or at an OP_WIDE followed by such an instruction). Reading past the end of code aborts.

    Chunk::set_self_binding_slot

    fn Chunk::set_self_binding_slot(self : Chunk, slot : Int) -> Unit

    Record which local slot the VM should populate with the newly-created Function at call time. Emitted by the compiler for named function expressions: the body's reference to its own name resolves to this local slot, and the VM writes the function reference into the slot when a frame for the chunk is created (see design.md §8.1 handoff notes).

    Pass -1 (the default from Chunk::new) to indicate no self-binding.

    Chunk::set_strict

    fn Chunk::set_strict(self : Chunk, is_strict : Bool) -> Unit

    Mark the chunk as strict-mode. M1 never calls this (the parser is not yet strict-aware), but the setter is exposed so M2's strict-directive pass has a place to write.

    DecodedInstr

    pub struct DecodedInstr {
    op : Byte
    a : Byte
    b : Byte
    c : Byte
    } derive(Eq,
    Debug
    )

    Decoded view of a single instruction word. Field names match the encoding documented at the top of this file.

    UpvalueFromKind

    pub(all) enum UpvalueFromKind {
    Local
    ParentUpvalue
    } derive(Eq,
    Debug
    )

    Where does an upvalue slot come from? Either a local slot in the immediately-enclosing function (Local) or another upvalue slot on the enclosing function (ParentUpvalue, which lets the compiler chain captures through several nesting levels without every level having to be a direct consumer).

    UpvalueSlotDecl

    pub struct UpvalueSlotDecl {
    from_kind : UpvalueFromKind
    from_idx : Int
    } derive(Eq,
    Debug
    )

    One capture-plan entry. from_idx indexes into the enclosing function's locals array (when from_kind = Local) or its upvalues array (when from_kind = ParentUpvalue).

    UpvalueSlotDecl::new

    fn UpvalueSlotDecl::new(from_kind : UpvalueFromKind, from_idx : Int) -> UpvalueSlotDecl

    Explicit constructor so callers do not need to know the field order — the enclosing Chunk uses Array[UpvalueSlotDecl], and later milestones may grow this struct (e.g. an is_mutable bit for const capture).

    OP_ADD

    let OP_ADD : Byte

    OP_ARRAY_PUSH

    let OP_ARRAY_PUSH : Byte

    OP_BAND

    let OP_BAND : Byte

    OP_BNOT

    let OP_BNOT : Byte

    OP_BOR

    let OP_BOR : Byte

    OP_BXOR

    let OP_BXOR : Byte

    OP_CALL

    let OP_CALL : Byte

    OP_CALL_METHOD

    let OP_CALL_METHOD : Byte

    OP_CONSTRUCT

    let OP_CONSTRUCT : Byte

    OP_DECLARE_GLOBAL

    let OP_DECLARE_GLOBAL : Byte

    OP_DEFINE_PROP

    let OP_DEFINE_PROP : Byte

    OP_DELETE_ELEM

    let OP_DELETE_ELEM : Byte

    OP_DELETE_PROP

    let OP_DELETE_PROP : Byte

    OP_DIV

    let OP_DIV : Byte

    OP_DROP

    let OP_DROP : Byte

    OP_DUP

    let OP_DUP : Byte

    OP_ENTER_TRY

    let OP_ENTER_TRY : Byte

    OP_EQ

    let OP_EQ : Byte

    OP_GE

    let OP_GE : Byte

    OP_GET_ELEM

    let OP_GET_ELEM : Byte

    OP_GET_GLOBAL

    let OP_GET_GLOBAL : Byte

    OP_GET_GLOBAL_OR_UNDEF

    let OP_GET_GLOBAL_OR_UNDEF : Byte

    Push the global's value if it exists, or Undefined if it does not.

    Distinct from OP_GET_GLOBAL, which throws ReferenceError on a miss. The compiler emits OP_GET_GLOBAL_OR_UNDEF for typeof foo and related side-effect-free reads that must succeed with undefined for never-declared identifiers per ES semantics (typeof undeclared === "undefined" must not throw). Introduced in M1 Step 7; see design.md §4.3 (0x17 allocation) and Step 7 notes on typeof emission.

    OP_GET_LOCAL

    let OP_GET_LOCAL : Byte

    OP_GET_PROP

    let OP_GET_PROP : Byte

    OP_GET_THIS

    let OP_GET_THIS : Byte

    Push the current frame's this value. Introduced alongside OP_GET_GLOBAL_OR_UNDEF (M1 Step 7) so the compiler doesn't have to encode this as a local slot — VM Step 8 stores this in a dedicated Frame.this_val field per design.md §8.1.

    OP_GET_UPVALUE

    let OP_GET_UPVALUE : Byte

    OP_GT

    let OP_GT : Byte

    OP_HALT

    let OP_HALT : Byte

    Reserved terminator opcode. Never emitted by the M1 compiler; useful as a sentinel in tests to check "we ran past the end of the code array".

    OP_IN

    let OP_IN : Byte

    OP_INSTANCEOF

    let OP_INSTANCEOF : Byte

    OP_JUMP

    let OP_JUMP : Byte

    OP_JUMP_IF_FALSE

    let OP_JUMP_IF_FALSE : Byte

    OP_JUMP_IF_TRUE

    let OP_JUMP_IF_TRUE : Byte

    OP_LE

    let OP_LE : Byte

    OP_LEAVE_TRY

    let OP_LEAVE_TRY : Byte

    OP_LT

    let OP_LT : Byte

    OP_MOD

    let OP_MOD : Byte

    OP_MUL

    let OP_MUL : Byte

    OP_NE

    let OP_NE : Byte

    OP_NEG

    let OP_NEG : Byte

    OP_NEW_ARRAY

    let OP_NEW_ARRAY : Byte

    OP_NEW_CLOSURE

    let OP_NEW_CLOSURE : Byte

    OP_NEW_OBJECT

    let OP_NEW_OBJECT : Byte

    OP_NOP

    let OP_NOP : Byte

    No-op. Handy as a padding instruction for jump patching that later needs to be widened; the compiler occasionally emits nops to reserve wide slots.

    OP_NOT

    let OP_NOT : Byte

    OP_POP

    let OP_POP : Byte

    Alias of drop, kept for QuickJS-name compatibility in the disassembler.

    OP_POW

    let OP_POW : Byte

    OP_PUSH_CONST

    let OP_PUSH_CONST : Byte

    Push a value from the chunk's constant pool. Operand = const pool index.

    OP_PUSH_FALSE

    let OP_PUSH_FALSE : Byte

    OP_PUSH_I32

    let OP_PUSH_I32 : Byte

    Push a 32-bit signed integer. Operand encoding: push_i32 hi mid lo — a 24-bit signed value (fits in one instruction). wide W_HI W_MID W_LO; push_i32 LOW8 0 0 — 32-bit signed (two-instr). See Chunk::emit_wide_i32 / Chunk::read_operand_i24.

    OP_PUSH_NULL

    let OP_PUSH_NULL : Byte

    OP_PUSH_TRUE

    let OP_PUSH_TRUE : Byte

    OP_PUSH_UNDEF

    let OP_PUSH_UNDEF : Byte

    OP_RETURN_UNDEF

    let OP_RETURN_UNDEF : Byte

    OP_RETURN_VAL

    let OP_RETURN_VAL : Byte

    OP_SEQ

    let OP_SEQ : Byte

    OP_SET_ELEM

    let OP_SET_ELEM : Byte

    OP_SET_GLOBAL

    let OP_SET_GLOBAL : Byte

    OP_SET_LOCAL

    let OP_SET_LOCAL : Byte

    OP_SET_PROP

    let OP_SET_PROP : Byte

    OP_SET_UPVALUE

    let OP_SET_UPVALUE : Byte

    OP_SHL

    let OP_SHL : Byte

    OP_SHR

    let OP_SHR : Byte

    OP_SNE

    let OP_SNE : Byte

    OP_SUB

    let OP_SUB : Byte

    OP_SWAP

    let OP_SWAP : Byte

    OP_THROW

    let OP_THROW : Byte

    OP_TO_NUMBER

    let OP_TO_NUMBER : Byte

    Coerce the top-of-stack value to a JS Number (per ToNumber in ES spec). Emitted by the compiler for unary +x — where x might be any JS value including a String (in which case JS + on strings concatenates, so we cannot lower +x to 0 + x; that would produce "0" + "5" = "05" instead of 5). Introduced in M1 Step 7 fix; see design.md §4.3.

    OP_TYPEOF

    let OP_TYPEOF : Byte

    OP_USHR

    let OP_USHR : Byte

    OP_WIDE

    let OP_WIDE : Byte

    Prefix opcode used to widen the operand of the following instruction to a full 32-bit value. See chunk.mbt for the exact convention.

    decode

    fn decode(word : UInt) -> DecodedInstr

    Unpack an instruction word into (op, a, b, c).

    encode

    fn encode(op : Byte, a : Byte, b : Byte, c : Byte) -> UInt

    Pack four bytes into an instruction word. Round-trips with decode.

    opcode_name

    fn opcode_name(op : Byte) -> String

    Human-readable name of an opcode, used by the disassembler. Unknown / reserved opcodes render as <op:0xNN>.

    New opcodes must be added here manually — an if-chain has no compile-time exhaustiveness check, so the disassembler tests in bytecode_test.mbt double as a coverage tripwire when adding a new constant.