// VM: the interpreter for M1 bytecode. Step 8b2 scope — see the parent task's
// implement.md.
//
// Design references:
// - design.md §8 (Frame layout, main loop shape, unwind algorithm).
// - design.md §4.3 (opcode table).
// - `.trellis/spec/backend/quality-guidelines.md` (no functional `loop{}`,
// no `Ref[T]`, no `not(x)`, `pub(all) enum` for cross-package match).
//
// 8b2 delivers on top of 8b1: try / catch / finally (OP_ENTER_TRY /
// OP_LEAVE_TRY) with cross-frame unwind, full `instanceof` semantics with
// prototype-chain walking, and Function `.prototype` materialisation so
// user-defined constructors participate in `instanceof`.
//
// Cross-frame unwind: OP_THROW now consults the current frame's try_stack
// (LIFO). If there is a handler, execution resumes at the recorded
// catch_pc and the operand stack is truncated to the depth at try entry.
// If not, the frame returns `Err(exc)`; the CALL / CALL_METHOD / CONSTRUCT
// arm in the caller catches that and re-runs the same unwind logic on the
// caller. Repeated all the way up. This preserves the QuickJS-style "stack
// trace captured at throw site, propagated frame by frame" model.
//
// Still deferred to later milestones:
// - Full try/finally completion tracking. M1's compiler lowers finally as
// an inline block after catch (see compile_try in `src/compiler/stmt.mbt`)
// which does NOT run on catch-throw. That is a documented M1 limitation
// and does not need VM-side accommodation.
// ---------------------------------------------------------------------------
// Frame layout
// ---------------------------------------------------------------------------
///|
/// One activation record. Corresponds to design.md §8.1's Frame with the
/// M1 simplifications documented in this file's header:
///
/// - `pc` is the instruction word index into `chunk.code`.
/// - `locals` is preallocated to `chunk.local_count`. Every slot is a
/// `@value.Upvalue` heap cell (uniform for M1 — see design deviations in
/// `src/value/function.mbt`). `OP_GET_LOCAL` / `OP_SET_LOCAL` read/write
/// the cell contents; local-slot capture across a closure boundary
/// reuses the cell directly.
/// - `upvalues` holds shared cells captured from the enclosing scope. One
/// entry per `chunk.upvalue_slots` decl.
/// - `operand_stack` is a growable value stack.
/// - `try_stack` is reserved for 8b2 (handler installation). 8b1 never
/// writes to it — declared here so we don't reshape `Frame` for 8b2.
/// - `this_val` is stored per design.md §8.1 outside of the locals table.
/// - `caller` links to the calling frame for stack-trace capture.
pub struct Frame {
mut pc : Int
chunk : @bytecode.Chunk
locals : Array[@value.Upvalue]
upvalues : Array[@value.Upvalue]
operand_stack : Array[@value.JSValue]
try_stack : Array[TryHandler]
this_val : @value.JSValue
caller : Frame?
}
///|
/// Construct a fresh top-level frame. Parameters:
///
/// - `chunk`: the compiled unit to run. Its `local_count` determines how
/// many slots the frame preallocates.
/// - `this_val`: the initial `this` for the frame. Scripts get `Undefined`.
pub fn Frame::new(chunk : @bytecode.Chunk, this_val : @value.JSValue) -> Frame {
let locals : Array[@value.Upvalue] = []
for _ in 0..= 0`, the newly-created frame's
/// local at that slot is initialised to `Function(func)` so the function
/// body sees itself by name (e.g. named function expressions like
/// `var fact = function myfact(n){ ... myfact(n-1) ... }`).
fn Frame::new_for_call(
chunk : @bytecode.Chunk,
this_val : @value.JSValue,
args : Array[@value.JSValue],
func : @value.Function,
caller : Frame,
) -> Frame {
let locals : Array[@value.Upvalue] = []
for _ in 0..= 0 && chunk.self_binding_slot < locals.length() {
locals[chunk.self_binding_slot].set(@value.Function(func))
}
{
pc: 0,
chunk,
locals,
upvalues: func.upvalues(),
operand_stack: [],
try_stack: [],
this_val,
caller: Some(caller),
}
}
///|
/// Try-handler entry. Reserved for 8b2. `operand_depth` remembers the
/// operand-stack depth at the time the handler was installed so `throw`
/// can unwind the stack back to that point before landing in the catch
/// handler.
pub struct TryHandler {
catch_pc : Int
finally_pc : Int
operand_depth : Int
}
///|
/// 8b2 hook: construct a try handler. Not used by 8b1; declared so the type
/// checker doesn't warn about an unconstructed struct.
pub fn TryHandler::new(
catch_pc : Int,
finally_pc : Int,
operand_depth : Int,
) -> TryHandler {
{ catch_pc, finally_pc, operand_depth, }
}
// ---------------------------------------------------------------------------
// Engine
// ---------------------------------------------------------------------------
///|
/// Top-level engine. Holds:
///
/// - `globals`: the global object (Step 9 populates builtin constructors
/// and prototypes via the `Builtins` object; M1 also seeds `undefined` /
/// `NaN` / `Infinity` / `globalThis`).
/// - `chunk_registry`: maps chunk_id → Chunk. Every callable JS function
/// references its underlying chunk through this table; assignment happens
/// in `register_chunk`, called from `run_chunk` and from the
/// `OP_NEW_CLOSURE` arm when it walks into a nested chunk that has not
/// been registered yet. Chunks are physically-identified in the registry
/// (`physical_equal`), which means re-registering the same Chunk twice
/// returns the existing id.
/// - `builtins`: the Step 9 builtin registry. Kept as `Option[Builtins]`
/// so downstream milestones can construct an Engine with a stripped-down
/// builtin set (e.g. for isolated unit tests) if needed; M1 always sets
/// it to `Some(...)` inside `Engine::new`.
pub struct Engine {
globals : @value.ObjectRef
chunk_registry : Array[@bytecode.Chunk]
builtins : @builtins.Builtins?
}
///|
/// Construct a fresh engine with a minimal global object seeded via
/// `Builtins::install_into`. This wires up `Object` / `Error` / the four
/// Error subclasses / `undefined` / `NaN` / `Infinity` / `globalThis`.
pub fn Engine::new() -> Engine {
let globals = @value.Object::new(@value.Shape::new(), @value.Null)
let bi = @builtins.Builtins::new()
// Link the global object's own [[Prototype]] to Object.prototype so
// `globalThis.hasOwnProperty(...)` etc. work in later milestones.
globals.set_proto(@value.Object(bi.object_proto()))
bi.install_into(globals)
{ globals, chunk_registry: [], builtins: Some(bi), }
}
///|
/// Register a chunk (and its nested chunks, recursively) in the engine's
/// chunk registry. Idempotent: if the chunk is already present (identity),
/// returns the existing id.
fn Engine::register_chunk(self : Engine, chunk : @bytecode.Chunk) -> Int {
for i in 0.. Result[@value.JSValue, @value.JSException] {
let _ = self.register_chunk(chunk)
let frame = Frame::new(chunk, @value.Undefined)
self.execute_frame(frame)
}
///|
/// Top-level entry point: parse, compile, and run a JS source string. This
/// is the primary M1 public API — `Engine::new().eval_script(src, filename)`
/// takes JS source text and returns the top-level completion value (or an
/// unhandled `JSException`).
///
/// Error handling: parse errors and compile errors are surfaced as
/// `JSException` values wrapping a `SyntaxError`-shaped plain object (with
/// `name: "SyntaxError"`, a descriptive `message`, and a single-frame stack
/// pointing at the offending source location). Runtime exceptions come from
/// `execute_frame` unchanged.
///
/// The reason parse / compile errors are shaped as `SyntaxError` rather than
/// being returned via a separate error channel is that from the JS user's
/// perspective they are indistinguishable — both come out of `eval_script`
/// as thrown Error values. `eval()` in JS behaves the same way, and this
/// keeps the M1 API surface minimal.
pub fn Engine::eval_script(
self : Engine,
source : String,
filename : String,
) -> Result[@value.JSValue, @value.JSException] {
// Parse.
let parser = match @parser.Parser::new(source, filename) {
Ok(p) => p
Err(e) => {
let @parser.ParseError(msg~, loc~) = e
return Err(self.make_syntax_error(msg, loc, filename))
}
}
let program = match parser.parse_script() {
Ok(prog) => prog
Err(e) => {
let @parser.ParseError(msg~, loc~) = e
return Err(self.make_syntax_error(msg, loc, filename))
}
}
// Compile.
let compiler = @compiler.Compiler::new()
let chunk = match compiler.compile_script(program, filename) {
Ok(ch) => ch
Err(e) => {
let @compiler.CompileError(msg~, loc~) = e
return Err(self.make_syntax_error(msg, loc, filename))
}
}
// Run.
self.run_chunk(chunk)
}
///|
/// Build a `JSException` for a parse / compile-time syntax error. Fabricates
/// an Object with `name: "SyntaxError"`, `message: msg`, wires the proto to
/// `SyntaxError.prototype`, and packages a single-frame stack pointing at
/// the offending location.
fn Engine::make_syntax_error(
self : Engine,
msg : String,
loc : @util.SourceLoc,
filename : String,
) -> @value.JSException {
let obj = @value.Object::new(@value.Shape::new(), @value.Null)
obj.add_property("name", @value.Str("SyntaxError"), @value.ATTR_DEFAULT_DATA)
obj.add_property("message", @value.Str(msg), @value.ATTR_DEFAULT_DATA)
// Link to SyntaxError.prototype so `instanceof SyntaxError` succeeds.
match self.builtins {
Some(bi) => obj.set_proto(@value.Object(bi.syntax_error_proto()))
None => ()
}
let stack : Array[@value.StackFrameInfo] = []
stack.push(@value.StackFrameInfo::new("", filename, loc))
@value.JSException::new(@value.Object(obj), stack)
}
// ---------------------------------------------------------------------------
// Main loop
// ---------------------------------------------------------------------------
///|
/// Peek the "effective op" at `frame.pc`. When the current instruction is
/// `OP_WIDE`, the effective op is the one at `pc + 1` (the WIDE prefix
/// itself is transparent to dispatch); otherwise it is the op at `pc`.
///
/// Callers that need to read an operand should use
/// `chunk.read_operand_u24(pc)` / `read_operand_i24(pc)` which return the
/// merged operand and the `pc_advance` (1 for narrow, 2 for wide) — the arm
/// then sets `frame.pc = pc + advance`.
fn effective_op(frame : Frame) -> Byte {
let pc = frame.pc
let word = frame.chunk.code[pc]
let decoded = @bytecode.decode(word)
if decoded.op == @bytecode.OP_WIDE {
let next = @bytecode.decode(frame.chunk.code[pc + 1])
next.op
} else {
decoded.op
}
}
///|
/// Pop the top-of-stack value. Aborts on underflow — that is a compiler bug,
/// not a runtime error. Frame operand-stack invariants are the compiler's
/// contract; a failure here means we should regenerate the bytecode, not
/// convert to a JS-level exception.
fn pop_stack(frame : Frame) -> @value.JSValue {
match frame.operand_stack.pop() {
Some(v) => v
None =>
abort(
"VM: operand stack underflow at pc=" +
frame.pc.to_string() +
" in " +
frame.chunk.name,
)
}
}
///|
/// Peek at the top-of-stack value without removing it. Aborts on empty.
fn peek_stack(frame : Frame) -> @value.JSValue {
let n = frame.operand_stack.length()
if n == 0 {
abort(
"VM: operand stack empty on peek at pc=" +
frame.pc.to_string() +
" in " +
frame.chunk.name,
)
}
frame.operand_stack[n - 1]
}
///|
/// Build a `JSException` capturing the current frame chain. `cur_pc` is the
/// pc value at the time the throwing instruction was decoded, BEFORE the
/// arm advanced `frame.pc`. Walking `frame.caller` gives one
/// `StackFrameInfo` entry per active frame, with the innermost frame at
/// index 0 — matching V8/QuickJS convention for `Error.prototype.stack`
/// (top-of-trace first).
fn throw_from_frame(
frame : Frame,
cur_pc : Int,
value : @value.JSValue,
) -> @value.JSException {
let stack : Array[@value.StackFrameInfo] = []
// First entry is the throw site itself.
let loc = frame_loc_at(frame, cur_pc)
stack.push(
@value.StackFrameInfo::new(frame.chunk.name, frame.chunk.filename, loc),
)
// Walk caller chain; each caller's `pc` was incremented past the CALL,
// so we back off by 1 to point at the CALL instruction itself. (Wide
// CALL is 2 words; the loc table is parallel to `code`, and both entries
// for a wide instruction share the same loc, so a simple `-1` is safe.)
let mut cur = frame.caller
while true {
match cur {
Some(f) => {
let caller_pc = if f.pc > 0 { f.pc - 1 } else { 0 }
let caller_loc = frame_loc_at(f, caller_pc)
stack.push(
@value.StackFrameInfo::new(f.chunk.name, f.chunk.filename, caller_loc),
)
cur = f.caller
}
None => break
}
}
@value.JSException::new(value, stack)
}
///|
/// Fetch the source location associated with a `pc` in a frame's chunk,
/// with graceful degradation if the pc is out-of-range (empty chunk, or
/// an off-by-one).
fn frame_loc_at(frame : Frame, pc : Int) -> @util.SourceLoc {
if pc >= 0 && pc < frame.chunk.source_locs.length() {
frame.chunk.source_locs[pc]
} else if frame.chunk.source_locs.length() > 0 {
frame.chunk.source_locs[frame.chunk.source_locs.length() - 1]
} else {
@util.SourceLoc::new(1, 1)
}
}
///|
/// Manufacture a plain-object exception with a `message` string property.
/// Step 9 will replace this with real `TypeError` / `ReferenceError`
/// constructors that produce properly-linked Error objects. For 8b1 we
/// just need something callers can pattern-match on the message of.
fn make_error_object(name : String, message : String) -> @value.JSValue {
let obj = @value.Object::new(@value.Shape::new(), @value.Null)
obj.add_property("name", @value.Str(name), @value.ATTR_DEFAULT_DATA)
obj.add_property("message", @value.Str(message), @value.ATTR_DEFAULT_DATA)
@value.Object(obj)
}
///|
/// Convert a `NativeError` returned by a native function's implementation
/// into a JS `Error`-family object suitable for throwing. Uses the same
/// plain-object shape as `make_error_object`; Step 9's builtin machinery
/// takes care of populating the actual error prototypes so `err instanceof
/// Error` etc. behave correctly. Callers receive back a `JSValue` ready to
/// be wrapped by `throw_from_frame`.
fn Engine::make_error_from_native(
self : Engine,
err : @value.NativeError,
) -> @value.JSValue {
let obj = @value.Object::new(@value.Shape::new(), @value.Null)
obj.add_property("name", @value.Str(err.name()), @value.ATTR_DEFAULT_DATA)
obj.add_property(
"message",
@value.Str(err.message()),
@value.ATTR_DEFAULT_DATA,
)
// Wire the proto to the matching Error-family prototype installed by
// Builtins so `instanceof Error` / `instanceof TypeError` succeed.
let proto = self.error_proto_for(err.name())
obj.set_proto(proto)
@value.Object(obj)
}
///|
/// Look up the Error-family prototype object corresponding to a native
/// error's `name` field. Falls back to `Error.prototype` for unknown names
/// (defensive — every native error in M1 uses a name we know about).
fn Engine::error_proto_for(self : Engine, name : String) -> @value.JSValue {
match self.builtins {
Some(bi) =>
match name {
"TypeError" => @value.Object(bi.type_error_proto())
"RangeError" => @value.Object(bi.range_error_proto())
"SyntaxError" => @value.Object(bi.syntax_error_proto())
"ReferenceError" => @value.Object(bi.reference_error_proto())
_ => @value.Object(bi.error_proto())
}
None => @value.Null
}
}
///|
/// Return `Object(Object.prototype)` if the engine has builtins installed,
/// else `Null`. Used by `OP_NEW_OBJECT` and by `OP_CONSTRUCT` on a
/// NativeFn that lacks an explicit `.prototype`.
fn Engine::object_proto_or_null(self : Engine) -> @value.JSValue {
match self.builtins {
Some(bi) => @value.Object(bi.object_proto())
None => @value.Null
}
}
///|
/// Return `Object(Array.prototype)` if the engine has builtins installed,
/// else `Null`. Used by `OP_NEW_ARRAY`.
fn Engine::array_proto_or_null(self : Engine) -> @value.JSValue {
match self.builtins {
Some(bi) => @value.Object(bi.array_proto())
None => @value.Null
}
}
///|
/// Central execution loop. Each iteration decodes one instruction (peeling
/// the `OP_WIDE` prefix if present via `effective_op` + `read_operand_*`),
/// advances `pc` by the correct amount, and executes the arm.
///
/// Frame handling: `execute_frame` runs a single frame to its `RETURN_*`
/// or to fallthrough. Calls / returns swap the mutable `frame` binding —
/// no recursion, so a deeply recursive JS function does not blow up the
/// MoonBit stack.
///
/// Return semantics: `Ok(v)` when the top-level frame terminates via
/// `OP_RETURN_VAL` (with `v` the top-of-stack), `Ok(Undefined)` on
/// `OP_RETURN_UNDEF` or fallthrough at end of code, and `Err(exc)` for
/// uncaught exceptions.
fn Engine::execute_frame(
self : Engine,
initial : Frame,
) -> Result[@value.JSValue, @value.JSException] {
let mut frame = initial
while true {
if frame.pc >= frame.chunk.code.length() {
// Fell off the end without a RETURN_*. Treat as return undefined.
// If we're in a nested frame, propagate to caller; else terminate.
match frame.caller {
Some(caller) => {
caller.operand_stack.push(@value.Undefined)
frame = caller
continue
}
None => return Ok(@value.Undefined)
}
}
let op = effective_op(frame)
let cur_pc = frame.pc
// ---------------- 0x00-0x0A: push / stack ----------------
if op == @bytecode.OP_NOP {
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_PUSH_UNDEF {
frame.operand_stack.push(@value.Undefined)
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_PUSH_NULL {
frame.operand_stack.push(@value.Null)
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_PUSH_TRUE {
frame.operand_stack.push(@value.Bool(true))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_PUSH_FALSE {
frame.operand_stack.push(@value.Bool(false))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_PUSH_I32 {
let (i, adv) = frame.chunk.read_operand_i24(cur_pc)
frame.operand_stack.push(@value.Int32(i))
frame.pc = cur_pc + adv
} else if op == @bytecode.OP_PUSH_CONST {
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let i = idx.reinterpret_as_int()
frame.operand_stack.push(frame.chunk.const_pool[i])
frame.pc = cur_pc + adv
} else if op == @bytecode.OP_DUP {
let v = peek_stack(frame)
frame.operand_stack.push(v)
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_DROP || op == @bytecode.OP_POP {
let _ = pop_stack(frame)
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_SWAP {
let n = frame.operand_stack.length()
if n < 2 {
abort("VM: SWAP with fewer than 2 stack values")
}
let a = frame.operand_stack[n - 2]
let b = frame.operand_stack[n - 1]
frame.operand_stack[n - 2] = b
frame.operand_stack[n - 1] = a
frame.pc = cur_pc + 1
// ---------------- 0x10-0x19: variables ----------------
} else if op == @bytecode.OP_GET_LOCAL {
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let i = idx.reinterpret_as_int()
frame.operand_stack.push(frame.locals[i].get())
frame.pc = cur_pc + adv
} else if op == @bytecode.OP_SET_LOCAL {
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let i = idx.reinterpret_as_int()
// SET_LOCAL peeks (leaves TOS in place per compiler's convention that
// an assignment expression yields the assigned value). Callers that
// don't need the value emit an explicit DROP after.
let v = peek_stack(frame)
frame.locals[i].set(v)
frame.pc = cur_pc + adv
} else if op == @bytecode.OP_GET_UPVALUE {
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let i = idx.reinterpret_as_int()
frame.operand_stack.push(frame.upvalues[i].get())
frame.pc = cur_pc + adv
} else if op == @bytecode.OP_SET_UPVALUE {
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let i = idx.reinterpret_as_int()
let v = peek_stack(frame)
frame.upvalues[i].set(v)
frame.pc = cur_pc + adv
} else if op == @bytecode.OP_GET_GLOBAL {
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let name = string_const(frame, idx.reinterpret_as_int())
if self.globals.has_own(name) {
frame.operand_stack.push(self.globals.get_property(name))
frame.pc = cur_pc + adv
} else {
// Uncaught ReferenceError per JS semantics — but respect any active
// try_stack in the current frame first.
frame.pc = cur_pc + adv
let err = make_error_object("ReferenceError", name + " is not defined")
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
}
} else if op == @bytecode.OP_SET_GLOBAL {
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let name = string_const(frame, idx.reinterpret_as_int())
let v = peek_stack(frame)
// Fully-loose semantics: assigning to an undeclared global implicitly
// declares it. `set_own` handles both cases (existing + new).
let _ = self.globals.set_own(name, v)
frame.pc = cur_pc + adv
} else if op == @bytecode.OP_DECLARE_GLOBAL {
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let name = string_const(frame, idx.reinterpret_as_int())
if !self.globals.has_own(name) {
self.globals.add_property(
name,
@value.Undefined,
@value.ATTR_DEFAULT_DATA,
)
}
frame.pc = cur_pc + adv
} else if op == @bytecode.OP_GET_GLOBAL_OR_UNDEF {
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let name = string_const(frame, idx.reinterpret_as_int())
let v = if self.globals.has_own(name) {
self.globals.get_property(name)
} else {
@value.Undefined
}
frame.operand_stack.push(v)
frame.pc = cur_pc + adv
} else if op == @bytecode.OP_GET_THIS {
frame.operand_stack.push(frame.this_val)
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_TO_NUMBER {
let v = pop_stack(frame)
// If the source was already an Int32 that we didn't need to touch,
// preserve it; otherwise emit Number. This matches the Int32 fast path
// used elsewhere.
match v {
Int32(_) => frame.operand_stack.push(v)
_ => frame.operand_stack.push(@value.Number(to_number(v)))
}
frame.pc = cur_pc + 1
// ---------------- 0x20-0x2D: arithmetic / bitwise ----------------
} else if op == @bytecode.OP_ADD {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(js_add(a, b))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_SUB {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(js_sub(a, b))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_MUL {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(js_mul(a, b))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_DIV {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(js_div(a, b))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_MOD {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(js_mod(a, b))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_POW {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(js_pow(a, b))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_NEG {
let a = pop_stack(frame)
frame.operand_stack.push(js_neg(a))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_BNOT {
let a = pop_stack(frame)
frame.operand_stack.push(js_bnot(a))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_BAND {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(js_band(a, b))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_BOR {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(js_bor(a, b))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_BXOR {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(js_bxor(a, b))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_SHL {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(js_shl(a, b))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_SHR {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(js_shr(a, b))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_USHR {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(js_ushr(a, b))
frame.pc = cur_pc + 1
// ---------------- 0x2E-0x35: comparison ----------------
} else if op == @bytecode.OP_EQ {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(@value.Bool(js_loose_eq(a, b)))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_NE {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(@value.Bool(!js_loose_eq(a, b)))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_SEQ {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(@value.Bool(js_strict_eq(a, b)))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_SNE {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(@value.Bool(!js_strict_eq(a, b)))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_LT {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(@value.Bool(js_lt(a, b)))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_LE {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(@value.Bool(js_le(a, b)))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_GT {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(@value.Bool(js_gt(a, b)))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_GE {
let b = pop_stack(frame)
let a = pop_stack(frame)
frame.operand_stack.push(@value.Bool(js_ge(a, b)))
frame.pc = cur_pc + 1
// ---------------- 0x36-0x39: logical / type ----------------
} else if op == @bytecode.OP_NOT {
let a = pop_stack(frame)
frame.operand_stack.push(@value.Bool(!to_boolean(a)))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_TYPEOF {
let a = pop_stack(frame)
frame.operand_stack.push(@value.Str(typeof_string(a)))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_INSTANCEOF {
// JS `x instanceof C`: walk `x`'s prototype chain looking for a proto
// that is physically-equal to `C.prototype`. Primitives always return
// false (proto walk starts from an Object). If C is not callable or its
// `.prototype` is not an Object, throw TypeError.
let ctor_val = pop_stack(frame)
let obj_val = pop_stack(frame)
frame.pc = cur_pc + 1
match instanceof_check(obj_val, ctor_val) {
Ok(b) => frame.operand_stack.push(@value.Bool(b))
Err(msg) => {
let err = make_error_object("TypeError", msg)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
}
}
} else if op == @bytecode.OP_IN {
// `key in obj` needs the RHS to be an Object; walks the prototype
// chain via `Object::has_property`. 8b1 supports Object RHS; a
// non-Object RHS is a JS TypeError.
let obj_val = pop_stack(frame)
let key_val = pop_stack(frame)
frame.pc = cur_pc + 1
match obj_val {
Object(obj) => {
let key = to_string(key_val)
frame.operand_stack.push(@value.Bool(obj.has_property(key)))
}
_ => {
let err = make_error_object(
"TypeError", "Cannot use 'in' operator to search in non-object",
)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
}
}
// ---------------- 0x40-0x49: objects & arrays ----------------
} else if op == @bytecode.OP_NEW_OBJECT {
// Link fresh object literals to Object.prototype so `.hasOwnProperty`
// etc. (once M3 fleshes them out) are reachable.
let obj = @value.Object::new(
@value.Shape::new(),
self.object_proto_or_null(),
)
frame.operand_stack.push(@value.Object(obj))
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_DEFINE_PROP {
// stack: [obj, val] -> [obj]. `name` from constant pool.
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let name = string_const(frame, idx.reinterpret_as_int())
let val = pop_stack(frame)
let obj_v = peek_stack(frame)
match obj_v {
Object(obj) =>
if obj.has_own(name) {
// Overwrite an existing own property (object literal with
// duplicate key is legal in sloppy mode; last write wins).
let _ = obj.set_own(name, val)
} else {
obj.add_property(name, val, @value.ATTR_DEFAULT_DATA)
}
_ => abort("VM: DEFINE_PROP on non-Object at pc=" + cur_pc.to_string())
}
frame.pc = cur_pc + adv
} else if op == @bytecode.OP_GET_PROP {
// stack: [obj] -> [val]. `name` from constant pool.
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let name = string_const(frame, idx.reinterpret_as_int())
let obj_v = pop_stack(frame)
frame.pc = cur_pc + adv
match get_property_value(obj_v, name) {
Ok(v) => frame.operand_stack.push(v)
Err(msg) => {
let err = make_error_object("TypeError", msg)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
}
}
} else if op == @bytecode.OP_SET_PROP {
// stack: [obj, val] -> [val]. `name` from constant pool.
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let name = string_const(frame, idx.reinterpret_as_int())
let val = pop_stack(frame)
let obj_v = pop_stack(frame)
frame.pc = cur_pc + adv
match obj_v {
Object(obj) => {
let _ = obj.set_own(name, val)
}
Null | Undefined => {
let err = make_error_object(
"TypeError",
"Cannot set property '" +
name +
"' of " +
(match obj_v {
Null => "null"
_ => "undefined"
}),
)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => {
frame = next
continue
}
None => return Err(exc)
}
}
// Primitives silently ignore in sloppy mode.
_ => ()
}
frame.operand_stack.push(val)
} else if op == @bytecode.OP_GET_ELEM {
// stack: [obj, key] -> [val].
let key = pop_stack(frame)
let obj_v = pop_stack(frame)
frame.pc = cur_pc + 1
let key_str = to_string(key)
match get_element_value(obj_v, key, key_str) {
Ok(v) => frame.operand_stack.push(v)
Err(msg) => {
let err = make_error_object("TypeError", msg)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
}
}
} else if op == @bytecode.OP_SET_ELEM {
// stack: [obj, key, val] -> [val].
let val = pop_stack(frame)
let key = pop_stack(frame)
let obj_v = pop_stack(frame)
frame.pc = cur_pc + 1
match obj_v {
Object(obj) => {
let key_str = to_string(key)
// If this looks like an integer array index, extend length as
// JS spec §OrdinarySetLength does (we don't have full Array
// semantics in M1, but ARRAY_PUSH keeps `length` in sync so we
// just do a set_own here).
let _ = obj.set_own(key_str, val)
}
Null | Undefined => {
let err = make_error_object(
"TypeError", "Cannot set property of null / undefined",
)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => {
frame = next
continue
}
None => return Err(exc)
}
}
_ => ()
}
frame.operand_stack.push(val)
} else if op == @bytecode.OP_DELETE_PROP {
// stack: [obj] -> [Bool]. M1 all data properties are configurable
// so delete always succeeds (or is a no-op if the prop doesn't
// exist). Full JS delete semantics (strict-mode throw on
// non-configurable, prototype-chain walk) is M6.
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let name = string_const(frame, idx.reinterpret_as_int())
let obj_v = pop_stack(frame)
frame.pc = cur_pc + adv
match obj_v {
Object(obj) => {
delete_own(obj, name)
frame.operand_stack.push(@value.Bool(true))
}
_ => frame.operand_stack.push(@value.Bool(true))
}
} else if op == @bytecode.OP_DELETE_ELEM {
let key = pop_stack(frame)
let obj_v = pop_stack(frame)
frame.pc = cur_pc + 1
match obj_v {
Object(obj) => {
delete_own(obj, to_string(key))
frame.operand_stack.push(@value.Bool(true))
}
_ => frame.operand_stack.push(@value.Bool(true))
}
} else if op == @bytecode.OP_NEW_ARRAY {
// Operand: hint length. M1 arrays are regular objects with a
// `length` numeric property that ARRAY_PUSH keeps in sync.
let (_hint, adv) = frame.chunk.read_operand_u24(cur_pc)
let obj = @value.Object::new(
@value.Shape::new(),
self.array_proto_or_null(),
)
obj.add_property("length", @value.Int32(0), @value.ATTR_DEFAULT_DATA)
frame.operand_stack.push(@value.Object(obj))
frame.pc = cur_pc + adv
} else if op == @bytecode.OP_ARRAY_PUSH {
// stack: [arr, val] -> [arr]. Write to arr[length]; increment length.
let val = pop_stack(frame)
let arr_v = peek_stack(frame)
frame.pc = cur_pc + 1
match arr_v {
Object(arr) => {
let len_v = arr.get_property("length")
let len = match len_v {
Int32(i) => i
Number(d) => d.to_int()
_ => 0
}
let _ = arr.set_own(len.to_string(), val)
let _ = arr.set_own("length", @value.Int32(len + 1))
}
_ => abort("VM: ARRAY_PUSH on non-Object at pc=" + cur_pc.to_string())
}
// ---------------- 0x50-0x55: function / call / return ----------------
} else if op == @bytecode.OP_NEW_CLOSURE {
let (idx, adv) = frame.chunk.read_operand_u24(cur_pc)
let i = idx.reinterpret_as_int()
if i < 0 || i >= frame.chunk.nested_chunks.length() {
abort(
"VM: NEW_CLOSURE nested index " +
i.to_string() +
" out of range in " +
frame.chunk.name,
)
}
let nested = frame.chunk.nested_chunks[i]
// Ensure the nested chunk (and its sub-chunks) are registered so
// future OP_NEW_CLOSURE dispatches inside them find their chunks.
let child_id = self.register_chunk(nested)
// Resolve each upvalue slot to a shared `Upvalue` cell.
let upvalues : Array[@value.Upvalue] = []
for slot_i in 0.. upvalues.push(frame.locals[decl.from_idx])
ParentUpvalue => upvalues.push(frame.upvalues[decl.from_idx])
}
}
let fn_value = @value.Function::new(child_id, upvalues, nested.name, true)
// Every user function gets a fresh `.prototype` Object whose own
// `[[Prototype]]` is Object.prototype. This is required for
// `new F(...)` (OP_CONSTRUCT reads f.prototype for the new receiver's
// proto) and for `instance instanceof F` (OP_INSTANCEOF walks the
// receiver's proto chain looking for identity match with f.prototype).
let fn_proto = @value.Object::new(
@value.Shape::new(),
self.object_proto_or_null(),
)
// Wire `.constructor` back to the function value itself for JS spec
// parity (`f.prototype.constructor === f`). This is a data property so
// reading it does not incur additional runtime cost.
fn_proto.add_property(
"constructor",
@value.Function(fn_value),
@value.ATTR_DEFAULT_DATA,
)
fn_value.set_prototype(@value.Object(fn_proto))
frame.operand_stack.push(@value.Function(fn_value))
frame.pc = cur_pc + adv
} else if op == @bytecode.OP_CALL {
let (argc_u, adv) = frame.chunk.read_operand_u24(cur_pc)
let argc = argc_u.reinterpret_as_int()
// stack layout: [..., fn, arg0, arg1, ..., argN-1]
let n = frame.operand_stack.length()
if n < argc + 1 {
abort(
"VM: CALL insufficient operands at pc=" +
cur_pc.to_string() +
" (argc=" +
argc.to_string() +
", stack=" +
n.to_string() +
")",
)
}
let args : Array[@value.JSValue] = []
for ai in 0.. {
let chunk = self.chunk_registry[func.chunk_id()]
let new_frame = Frame::new_for_call(
chunk,
@value.Undefined,
args,
func,
frame,
)
frame = new_frame
}
NativeFn(nf) =>
// Native function: invoke synchronously and push the result back
// onto the caller's operand stack.
match nf.call(@value.Undefined, args) {
Ok(v) => frame.operand_stack.push(v)
Err(nerr) => {
let err = self.make_error_from_native(nerr)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
}
}
_ => {
let err = make_error_object(
"TypeError",
describe_call_target(callee) + " is not a function",
)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
}
}
} else if op == @bytecode.OP_CALL_METHOD {
let (argc_u, adv) = frame.chunk.read_operand_u24(cur_pc)
let argc = argc_u.reinterpret_as_int()
// stack: [..., this, fn, arg0, ..., argN-1]
let n = frame.operand_stack.length()
if n < argc + 2 {
abort(
"VM: CALL_METHOD insufficient operands at pc=" + cur_pc.to_string(),
)
}
let args : Array[@value.JSValue] = []
for ai in 0.. {
let chunk = self.chunk_registry[func.chunk_id()]
let new_frame = Frame::new_for_call(
chunk, this_val, args, func, frame,
)
frame = new_frame
}
NativeFn(nf) =>
// Method call on a native function: `this_val` is the receiver
// (obj in `obj.f()`); the native impl reads it directly.
match nf.call(this_val, args) {
Ok(v) => frame.operand_stack.push(v)
Err(nerr) => {
let err = self.make_error_from_native(nerr)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
}
}
_ => {
let err = make_error_object(
"TypeError",
describe_call_target(callee) + " is not a function",
)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
}
}
} else if op == @bytecode.OP_CONSTRUCT {
let (argc_u, adv) = frame.chunk.read_operand_u24(cur_pc)
let argc = argc_u.reinterpret_as_int()
let n = frame.operand_stack.length()
if n < argc + 1 {
abort("VM: CONSTRUCT insufficient operands at pc=" + cur_pc.to_string())
}
let args : Array[@value.JSValue] = []
for ai in 0.. {
if !func.is_constructor() {
let err = make_error_object(
"TypeError",
func.name() + " is not a constructor",
)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => {
frame = next
continue
}
None => return Err(exc)
}
}
// Materialise a fresh receiver whose `[[Prototype]]` is the
// function's `.prototype` object. OP_NEW_CLOSURE wires each user
// function's prototype to a fresh Object linked to Object.prototype,
// so this branch always has a real Object here. As a defensive
// fallback we still allow non-Object prototypes (which shouldn't
// happen in M1) to resolve to Object.prototype.
let proto : @value.JSValue = match func.prototype() {
Object(_) => func.prototype()
_ => self.object_proto_or_null()
}
let new_obj = @value.Object::new(@value.Shape::new(), proto)
let this_val : @value.JSValue = @value.Object(new_obj)
let chunk = self.chunk_registry[func.chunk_id()]
let new_frame = Frame::new_for_call(
chunk, this_val, args, func, frame,
)
frame = new_frame
}
NativeFn(nf) => {
if !nf.is_constructor() {
let err = make_error_object(
"TypeError",
nf.name() + " is not a constructor",
)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => {
frame = next
continue
}
None => return Err(exc)
}
}
// Materialise a fresh receiver whose proto is nf.prototype (set
// by builtin init) or Object.prototype as a fallback.
let proto = match nf.prototype() {
Object(_) => nf.prototype()
_ => self.object_proto_or_null()
}
let new_obj = @value.Object::new(@value.Shape::new(), proto)
let this_val : @value.JSValue = @value.Object(new_obj)
match nf.call(this_val, args) {
Ok(ret) => {
// JS spec: constructor returns `this` unless the impl returns
// an Object / Function / NativeFn.
let final_ret : @value.JSValue = match ret {
Object(_) | Function(_) | NativeFn(_) => ret
_ => this_val
}
frame.operand_stack.push(final_ret)
}
Err(nerr) => {
let err = self.make_error_from_native(nerr)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
}
}
}
_ => {
let err = make_error_object(
"TypeError",
describe_call_target(callee) + " is not a constructor",
)
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
}
}
} else if op == @bytecode.OP_RETURN_VAL {
let v = pop_stack(frame)
// For a constructor call, if the returned value isn't an Object,
// return `this` instead. Detect by peeking at the caller's pc — one
// instruction back is the CONSTRUCT / CALL / CALL_METHOD opcode.
match frame.caller {
Some(caller) => {
let return_val = adjust_ctor_return(caller, v, frame.this_val)
caller.operand_stack.push(return_val)
frame = caller
}
None => return Ok(v)
}
} else if op == @bytecode.OP_RETURN_UNDEF {
match frame.caller {
Some(caller) => {
let return_val = adjust_ctor_return(
caller,
@value.Undefined,
frame.this_val,
)
caller.operand_stack.push(return_val)
frame = caller
}
None => return Ok(@value.Undefined)
}
// ---------------- 0x60-0x62: control flow ----------------
} else if op == @bytecode.OP_JUMP {
let (offset, adv) = frame.chunk.read_operand_i24(cur_pc)
// Offset is measured from the instruction AFTER the jump (see
// Chunk::patch_jump); adv accounts for wide (2) vs narrow (1).
frame.pc = cur_pc + adv + offset
} else if op == @bytecode.OP_JUMP_IF_TRUE {
let (offset, adv) = frame.chunk.read_operand_i24(cur_pc)
let v = pop_stack(frame)
if to_boolean(v) {
frame.pc = cur_pc + adv + offset
} else {
frame.pc = cur_pc + adv
}
} else if op == @bytecode.OP_JUMP_IF_FALSE {
let (offset, adv) = frame.chunk.read_operand_i24(cur_pc)
let v = pop_stack(frame)
if !to_boolean(v) {
frame.pc = cur_pc + adv + offset
} else {
frame.pc = cur_pc + adv
}
// ---------------- 0x70-0x72: try / catch / throw ----------------
} else if op == @bytecode.OP_ENTER_TRY {
// Decode signed offset (or -1 sentinel for finally-only). Operand is
// encoded as a wide-slot 32-bit value by `compile_try`; a value of
// 0xFFFFFFFF (== -1 signed) means "no catch handler", any other value
// is a signed offset from the pc AFTER the ENTER_TRY (matching the
// convention used by OP_JUMP's patch_jump).
let (offset, adv) = frame.chunk.read_operand_i24(cur_pc)
frame.pc = cur_pc + adv
let catch_pc = if offset == -1 { -1 } else { cur_pc + adv + offset }
// Record the depth of the operand stack at try-entry so `unwind_in_frame`
// can restore it before pushing the exception value into the catch.
let handler = TryHandler::new(catch_pc, -1, frame.operand_stack.length())
frame.try_stack.push(handler)
} else if op == @bytecode.OP_LEAVE_TRY {
// Pop the top handler. If try_stack is empty this is a compiler bug —
// every ENTER_TRY should be paired with a LEAVE_TRY along every normal
// (non-throwing) exit path.
if frame.try_stack.length() == 0 {
abort("VM: LEAVE_TRY with empty try_stack at pc=" + cur_pc.to_string())
}
let _ = frame.try_stack.pop()
frame.pc = cur_pc + 1
} else if op == @bytecode.OP_THROW {
let v = pop_stack(frame)
// Build the exception with the current caller chain as the stack trace,
// then unwind across frames looking for a handler. If the throw
// escapes every frame (including the top-level), propagate via
// `Err(exc)` so `Engine::run_chunk`'s caller can inspect it.
frame.pc = cur_pc + 1
let exc = throw_from_frame(frame, cur_pc, v)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
} else {
// ---------------- Unimplemented opcode ----------------
let err = make_error_object(
"TypeError",
"opcode 0x" +
op.to_int().to_string(radix=16) +
" (" +
@bytecode.opcode_name(op) +
") not implemented",
)
frame.pc = cur_pc + 1
let exc = throw_from_frame(frame, cur_pc, err)
match propagate_exception(frame, exc) {
Some(next) => frame = next
None => return Err(exc)
}
}
}
// Unreachable: `while true { ... }` never falls through — the loop body
// either updates state and continues or returns. MoonBit's flow analysis
// still needs a final expression matching the return type.
Ok(@value.Undefined)
}
///|
/// Look up a `String` value from the constant pool. Aborts on a
/// non-String entry (compiler bug — every name operand should refer to a
/// `JSValue::Str`).
fn string_const(frame : Frame, idx : Int) -> String {
match frame.chunk.const_pool[idx] {
Str(s) => s
_ =>
abort(
"VM: expected String constant at pool index " +
idx.to_string() +
" in " +
frame.chunk.name,
)
}
}
///|
/// JS `typeof` operator's string result. Matches ECMA-262 12.5.5 — with the
/// caveat that Object-wrapped functions (Step 8b+) will return "function",
/// but M1's plain Objects always yield "object". Null special case is
/// respected (JS's ancient bug: `typeof null === "object"`).
fn typeof_string(v : @value.JSValue) -> String {
match v {
Undefined => "undefined"
Null => "object"
Bool(_) => "boolean"
Int32(_) | Number(_) => "number"
Str(_) => "string"
Object(_) => "object"
Function(_) => "function"
NativeFn(_) => "function"
}
}
///|
/// Fetch a named property from a JSValue. Handles:
///
/// - `null` / `undefined` → `TypeError` (returned as `Err`).
/// - `Object` → `Object::get_property` with prototype-chain walk.
/// - `Str` → the primitive fast path for `.length` (returns Int32 code-unit
/// count). Other properties currently return `Undefined` (Step 9 will add
/// String.prototype method dispatch).
/// - `Function` → treated like an object; own properties (empty in 8b1)
/// plus a `.length` and `.name` primitive fast path.
/// - Other primitives → Undefined for now. Real primitive wrapping is
/// Step 9's Object.prototype linking work.
///
/// Primitive method dispatch for `.toString()`: M1 M3-preview: for
/// `.toString`, return a NativeFn that when called returns
/// `ToString(receiver)`. The M1 AC uses this on Number, Boolean, and String
/// (test_cvt tail `.toString()`, test_op1 assert helper's error messages).
/// This is a stub — the full Number.prototype.toString(radix) with radix
/// support and String.prototype.toString identity semantics arrives in M3.
fn get_property_value(
v : @value.JSValue,
name : String,
) -> Result[@value.JSValue, String] {
match v {
Null => Err("Cannot read properties of null (reading '" + name + "')")
Undefined =>
Err("Cannot read properties of undefined (reading '" + name + "')")
Object(obj) => Ok(obj.get_property(name))
Str(s) => {
if name == "length" {
return Ok(@value.Int32(s.length()))
}
if name == "toString" || name == "valueOf" {
return Ok(primitive_to_string_fn(v))
}
// Try numeric index into the string.
match parse_array_index(name) {
Some(i) =>
if i >= 0 && i < s.length() {
Ok(@value.Str(s[i:i + 1].to_owned()))
} else {
Ok(@value.Undefined)
}
None => Ok(@value.Undefined)
}
}
Int32(_) | Number(_) | Bool(_) => {
if name == "toString" || name == "valueOf" {
return Ok(primitive_to_string_fn(v))
}
Ok(@value.Undefined)
}
Function(f) => {
if name == "name" {
return Ok(@value.Str(f.name()))
}
if name == "prototype" {
return Ok(f.prototype())
}
if name == "length" {
// Functions have a `.length` of their declared parameter count.
// We don't have direct access to param_count from Function — that
// lives in the Chunk. Step 9 will wire this properly; for 8b1
// return 0 as a placeholder.
return Ok(@value.Int32(0))
}
Ok(@value.Undefined)
}
NativeFn(nf) => {
if name == "name" {
return Ok(@value.Str(nf.name()))
}
if name == "prototype" {
return Ok(nf.prototype())
}
if name == "length" {
// Same placeholder rationale as `Function`.
return Ok(@value.Int32(0))
}
Ok(@value.Undefined)
}
}
}
///|
/// M1 stub for primitive `.toString` / `.valueOf`. Captures the receiver
/// value in a closure and returns a NativeFunction that, when called,
/// coerces it to a String / returns it unchanged. In JS the actual
/// `Number.prototype.toString` is a shared function on the Number prototype;
/// M1 doesn't have primitive-wrapping (`new Number(x)`) machinery so we
/// synthesise a fresh function per property lookup. This is enough to make
/// `(1968610...n).toString()` work as an M1 AC path.
fn primitive_to_string_fn(receiver : @value.JSValue) -> @value.JSValue {
let captured = receiver
let name = match receiver {
Str(_) => "toString"
_ => "toString"
}
let impl_ = fn(
_this : @value.JSValue,
_args : Array[@value.JSValue],
) -> Result[@value.JSValue, @value.NativeError] {
match captured {
Str(s) => Ok(@value.Str(s))
_ => Ok(@value.Str(to_string(captured)))
}
}
@value.NativeFn(@value.NativeFunction::new(name, false, impl_))
}
///|
/// Fetch an element by key (JS `obj[key]`). Delegates to the string-keyed
/// `get_property_value` after coercing `key` to a String. The `key_str`
/// argument is the pre-computed `to_string(key)` — passed in so the caller
/// can reuse it if it also needs the raw string form.
fn get_element_value(
v : @value.JSValue,
_key : @value.JSValue,
key_str : String,
) -> Result[@value.JSValue, String] {
get_property_value(v, key_str)
}
///|
/// Parse a JS canonical array-index string. Only non-negative integer
/// strings with no leading zeros (except "0" itself) are accepted, matching
/// JS's `IsArrayIndex(name)`.
fn parse_array_index(s : String) -> Int? {
if s.length() == 0 {
return None
}
if s == "0" {
return Some(0)
}
let zero_code = '0'.to_int()
let nine_code = '9'.to_int()
// First char must be 1-9 (no leading zero on multi-digit indices).
let first = s.at(0).to_uint().reinterpret_as_int()
if first < zero_code + 1 || first > nine_code {
return None
}
let mut acc = first - zero_code
for i in 1.. nine_code {
return None
}
acc = acc * 10 + (c - zero_code)
if acc < 0 {
// Integer overflow guard — bail out to non-index treatment.
return None
}
}
Some(acc)
}
///|
/// Best-effort description of a non-function value for error messages.
/// JS's actual formatting is more elaborate (e.g. `#