/* * QuickJS Javascript Engine * * Copyright (c) 2017-2025 Fabrice Bellard * Copyright (c) 2017-2025 Charlie Gordon * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ #include "quickjs-internal.h" #include /* ============================================================ Mach VM instruction definitions (private to mach.c) ============================================================ */ /* Encoding macros */ #define MACH_ABC(op, a, b, c) ((uint32_t)(op) | ((uint32_t)(a)<<8) | ((uint32_t)(b)<<16) | ((uint32_t)(c)<<24)) #define MACH_ABx(op, a, bx) ((uint32_t)(op) | ((uint32_t)(a)<<8) | ((uint32_t)(bx)<<16)) #define MACH_AsBx(op, a, sbx) ((uint32_t)(op) | ((uint32_t)(a)<<8) | ((uint32_t)(uint16_t)(sbx)<<16)) #define MACH_sJ(op, sj) ((uint32_t)(op) | (((uint32_t)(sj) & 0xFFFFFF) << 8)) /* Decoding macros */ #define MACH_GET_OP(i) ((i) & 0xFF) #define MACH_GET_A(i) (((i) >> 8) & 0xFF) #define MACH_GET_B(i) (((i) >> 16) & 0xFF) #define MACH_GET_C(i) (((i) >> 24) & 0xFF) #define MACH_GET_Bx(i) ((i) >> 16) #define MACH_GET_sBx(i) ((int16_t)((i) >> 16)) #define MACH_GET_sJ(i) ((int32_t)((i) & 0xFFFFFF00) >> 8) /* ============================================================ GC Safepoint Classification for the MACH VM Dispatch Loop ============================================================ Every opcode falls into one of three categories: [P] Pure inline — never calls C, never allocates. No GC possible. No frame re-derivation needed after execution. [N] Non-allocating C call — calls a C function that is guaranteed to never allocate (e.g. JS_ToBool, js_string_compare_value). No frame re-derivation needed. [G] GC safepoint — calls C that may allocate, triggering GC. After the call, all heap pointers (including `frame`) MUST be re-derived via: frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); The 18 C entry points that can allocate (GC safepoints): 1. JS_GetProperty — key interning for string keys >7 chars 2. JS_SetProperty — rec_resize when record grows 3. JS_GetPropertyNumber — text substring extraction 4. JS_SetPropertyNumber — array grow 5. JS_NewObject — allocates record 6. JS_NewArray / JS_NewArrayLen — allocates array 7. js_new_register_function — allocates function object (closure) 8. alloc_frame_register — allocates frame via js_mallocz 9. js_call_c_function — arbitrary C code 10. JS_CallInternal — arbitrary bytecode 11. JS_Call — arbitrary call 12. JS_ConcatString — allocates new string 13. JS_ArrayPush — array grow 14. JS_ArrayPop — reads, but frame refresh needed 15. JS_DeleteProperty — mutates record 16. JS_HasProperty — complex traversal 17. js_regexp_constructor — allocates regex 18. reg_vm_binop — polymorphic dispatch (legacy opcodes) Opcode-level classification: [P] LOADK, LOADI, LOADNULL, LOADTRUE, LOADFALSE, MOVE, NOP [P] ADD_INT..MOD_INT, NEG_INT, ADD_FLOAT..MOD_FLOAT, NEG_FLOAT [P] EQ_INT..GE_INT, EQ_FLOAT..GE_FLOAT, EQ_BOOL, NE_BOOL [P] IS_IDENTICAL, IS_INT, IS_NUM, IS_TEXT, IS_BOOL, IS_NULL [P] IS_ARRAY, IS_FUNC, IS_RECORD, IS_STONE, IS_PROXY [P] NOT, AND, OR, BITNOT, BITAND, BITOR, BITXOR [P] JMP, JMPTRUE, JMPFALSE, JMPNULL, JMPNOTNULL, WARYTRUE, WARYFALSE, JMPEMPTY [P] RETURN, RETNIL, SETARG, GETUP, SETUP, DISRUPT, THROW [P] LENGTH (array + imm-ASCII fast path only; text/blob fallback is [G]) [N] EQ_TEXT..GE_TEXT (js_string_compare_value — no allocation) [N] LNOT (JS_ToBool — no allocation) [G] ADD..USHR, NEG, INC, DEC, EQ..GE (legacy: reg_vm_binop) [G] EQ_TOL, NEQ_TOL (tolerance comparison, may fall back) [G] CONCAT (JS_ConcatString) [G] GETFIELD, SETFIELD, GETINDEX, SETINDEX (property access) [G] LOAD_FIELD, STORE_FIELD, LOAD_INDEX, STORE_INDEX [G] LOAD_DYNAMIC, STORE_DYNAMIC [G] GETNAME, GETINTRINSIC, GETENV, SET_VAR [G] NEWOBJECT, NEWRECORD, NEWARRAY (object/array creation) [G] CLOSURE (js_new_register_function) [G] FRAME, GOFRAME (alloc_frame_register) [G] INVOKE, GOINVOKE (function calls) [G] PUSH (JS_ArrayPush), POP (JS_ArrayPop) [G] DELETE, DELETEINDEX (JS_DeleteProperty) [G] HASPROP, IN (JS_HasProperty) [G] REGEXP (js_regexp_constructor) ============================================================ */ typedef enum MachOpcode { /* === Legacy opcodes (used by existing .mach files) === */ /* Constants & Loading */ MACH_LOADK, /* R(A) = K(Bx) — load from constant pool (ABx) */ MACH_LOADI, /* R(A) = (int16_t)sBx — load small integer (AsBx) */ MACH_LOADNULL, /* R(A) = null (A only) */ MACH_LOADTRUE, /* R(A) = true (A only) */ MACH_LOADFALSE, /* R(A) = false (A only) */ /* Movement */ MACH_MOVE, /* R(A) = R(B) */ /* Generic arithmetic (ABC) — used by legacy .mach */ MACH_ADD, /* R(A) = R(B) + R(C) */ MACH_SUB, /* R(A) = R(B) - R(C) */ MACH_MUL, /* R(A) = R(B) * R(C) */ MACH_DIV, /* R(A) = R(B) / R(C) */ MACH_MOD, /* R(A) = R(B) % R(C) */ MACH_POW, /* R(A) = R(B) ** R(C) */ MACH_NEG, /* R(A) = -R(B) */ MACH_REMAINDER, /* R(A) = remainder(R(B), R(C)) */ MACH_MAX, /* R(A) = max(R(B), R(C)) */ MACH_MIN, /* R(A) = min(R(B), R(C)) */ MACH_ABS, /* R(A) = abs(R(B)) */ MACH_SIGN, /* R(A) = sign(R(B)) */ MACH_FRACTION, /* R(A) = fraction(R(B)) */ MACH_INTEGER, /* R(A) = integer(R(B)) */ MACH_FLOOR, /* R(A) = floor(R(B), R(C)) */ MACH_CEILING, /* R(A) = ceiling(R(B), R(C)) */ MACH_ROUND, /* R(A) = round(R(B), R(C)) */ MACH_TRUNC, /* R(A) = trunc(R(B), R(C)) */ MACH__DEAD_INC, /* reserved — was MACH_INC, never emitted */ MACH__DEAD_DEC, /* reserved — was MACH_DEC, never emitted */ /* Generic comparison (ABC) — used by legacy .mach */ MACH_EQ, /* R(A) = (R(B) == R(C)) */ MACH_NEQ, /* R(A) = (R(B) != R(C)) */ MACH_LT, /* R(A) = (R(B) < R(C)) */ MACH_LE, /* R(A) = (R(B) <= R(C)) */ MACH_GT, /* R(A) = (R(B) > R(C)) */ MACH_GE, /* R(A) = (R(B) >= R(C)) */ /* Logical/Bitwise — used by legacy .mach */ MACH_LNOT, /* R(A) = !R(B) */ MACH_BNOT, /* R(A) = ~R(B) */ MACH_BAND, /* R(A) = R(B) & R(C) */ MACH_BOR, /* R(A) = R(B) | R(C) */ MACH_BXOR, /* R(A) = R(B) ^ R(C) */ MACH_SHL, /* R(A) = R(B) << R(C) */ MACH_SHR, /* R(A) = R(B) >> R(C) */ MACH_USHR, /* R(A) = R(B) >>> R(C) */ /* Property access — used by legacy .mach */ MACH_GETFIELD, /* R(A) = R(B)[K(C)] — named property */ MACH_SETFIELD, /* R(A)[K(B)] = R(C) — named property */ MACH_GETINDEX, /* R(A) = R(B)[R(C)] — computed property */ MACH_SETINDEX, /* R(A)[R(B)] = R(C) — computed property */ /* Unbound variable access (ABx) */ MACH_GETNAME, /* R(A) = resolve(K(Bx)) — compiler placeholder, patched by link */ MACH_GETINTRINSIC, /* R(A) = global[K(Bx)] — post-link, intrinsic/built-in */ MACH_GETENV, /* R(A) = env[K(Bx)] — post-link, module environment */ /* Closure access (ABC) */ MACH_GETUP, /* R(A) = outer_frame[B].slots[C] */ MACH_SETUP, /* outer_frame[B].slots[C] = R(A) */ /* Control flow */ MACH_JMP, /* pc += sJ — unconditional (isJ format) */ MACH_JMPTRUE, /* if R(A): pc += sBx — (iAsBx format) */ MACH_JMPFALSE, /* if !R(A): pc += sBx — (iAsBx format) */ MACH_JMPNULL, /* if R(A)==null: pc += sBx */ /* Function calls — Lua-style consecutive registers (legacy .mach) */ MACH_RETURN, /* Return R(A) */ MACH_RETNIL, /* Return null */ /* Object/array creation — legacy .mach */ MACH_NEWOBJECT, /* R(A) = {} */ MACH_NEWARRAY, /* R(A) = new array, B = element count in R(A+1)..R(A+B) */ MACH_CLOSURE, /* R(A) = closure(functions[Bx]) (ABx) */ MACH_THROW, /* disrupt — trigger disruption */ MACH_PUSH, /* push R(B) onto array R(A) */ MACH_POP, /* R(A) = pop last element from array R(B) */ MACH_DELETE, /* R(A) = delete R(B)[K(C)] — named property delete */ MACH_DELETEINDEX, /* R(A) = delete R(B)[R(C)] — computed property delete */ MACH_HASPROP, /* R(A) = R(C) in R(B) — has property check */ MACH_REGEXP, /* R(A) = regexp(K(B), K(C)) — regex literal */ MACH_EQ_TOL, /* R(A) = eq_tol(R(B), R(B+1), R(B+2)), C=3 */ MACH_NEQ_TOL, /* R(A) = ne_tol(R(B), R(B+1), R(B+2)), C=3 */ MACH_NOP, /* === New mcode-derived opcodes (1:1 mapping to mcode IR) === */ /* Text */ MACH_CONCAT, /* R(A) = R(B) ++ R(C) — string concatenation */ MACH_STONE_TEXT, /* stone(R(A)) — freeze mutable text before escape */ /* Special comparisons */ MACH_IS_IDENTICAL, /* R(A) = (R(B) === R(C)) — identity check (ABC) */ /* Type checks (AB) */ MACH_IS_INT, /* R(A) = is_int(R(B)) */ MACH_IS_NUM, /* R(A) = is_num(R(B)) */ MACH_IS_TEXT, /* R(A) = is_text(R(B)) */ MACH_IS_BOOL, /* R(A) = is_bool(R(B)) */ MACH_IS_NULL, /* R(A) = is_null(R(B)) */ /* Logical (mcode-style) */ MACH_NOT, /* R(A) = !R(B) — boolean not (AB) */ MACH_AND, /* R(A) = R(B) && R(C) (ABC) */ MACH_OR, /* R(A) = R(B) || R(C) (ABC) */ /* Bitwise (mcode names) */ MACH_BITNOT, /* R(A) = ~R(B) (AB) */ MACH_BITAND, /* R(A) = R(B) & R(C) (ABC) */ MACH_BITOR, /* R(A) = R(B) | R(C) (ABC) */ MACH_BITXOR, /* R(A) = R(B) ^ R(C) (ABC) */ /* Property access (mcode names) */ MACH_LOAD_FIELD, /* R(A) = R(B).K(C) — named property (ABC) */ MACH_STORE_FIELD, /* R(A).K(B) = R(C) — named property (ABC) */ MACH_LOAD_INDEX, /* R(A) = R(B)[R(C)] — integer index (ABC) */ MACH_STORE_INDEX, /* R(A)[R(B)] = R(C) — integer index (ABC) */ MACH_LOAD_DYNAMIC, /* R(A) = R(B)[R(C)] — dynamic key (ABC) */ MACH_STORE_DYNAMIC, /* R(A)[R(B)] = R(C) — dynamic key (ABC) */ /* Object/Array creation (mcode names) */ MACH_NEWRECORD, /* R(A) = {} — new empty record (A only) */ /* Decomposed function calls (mcode-style) */ MACH_FRAME, /* R(A) = frame(R(B), C) — alloc call frame (ABC) */ MACH_SETARG, /* frame R(A)[B] = R(C) — set arg in frame (ABC) */ MACH_INVOKE, /* R(B) = invoke(R(A)) — call frame, result in R(B) (AB) */ MACH_GOFRAME, /* R(A) = goframe(R(B), C) — async frame (ABC) */ MACH_GOINVOKE, /* goinvoke(R(A)) — async invoke, no result (A only) */ /* Control flow */ MACH_JMPNOTNULL, /* if R(A)!=null: pc += sBx (iAsBx) */ /* Error handling */ MACH_DISRUPT, /* trigger disruption (A only) */ /* Misc */ MACH_IN, /* R(A) = (R(B) in R(C)) — has property (ABC) */ /* Extended type checks (AB) */ MACH_IS_ARRAY, /* R(A) = is_array(R(B)) */ MACH_IS_FUNC, /* R(A) = is_function(R(B)) */ MACH_IS_RECORD, /* R(A) = is_object(R(B)) */ MACH_IS_STONE, /* R(A) = is_stone(R(B)) */ MACH_LENGTH, /* R(A) = length(R(B)) — array/text/blob length */ MACH_IS_PROXY, /* R(A) = is_function(R(B)) && R(B).length == 2 */ MACH_IS_BLOB, /* R(A) = is_blob(R(B)) */ MACH_IS_DATA, /* R(A) = is_data(R(B)) — plain record, not array/func/blob */ MACH_IS_TRUE, /* R(A) = (R(B) === true) */ MACH_IS_FALSE, /* R(A) = (R(B) === false) */ MACH_IS_FIT, /* R(A) = is_fit(R(B)) — safe integer */ MACH_IS_CHAR, /* R(A) = is_character(R(B)) — single char text */ MACH_IS_DIGIT, /* R(A) = is_digit(R(B)) */ MACH_IS_LETTER, /* R(A) = is_letter(R(B)) */ MACH_IS_LOWER, /* R(A) = is_lower(R(B)) */ MACH_IS_UPPER, /* R(A) = is_upper(R(B)) */ MACH_IS_WS, /* R(A) = is_whitespace(R(B)) */ MACH_IS_ACTOR, /* R(A) = is_actor(R(B)) — has actor_sym property */ MACH_APPLY, /* R(A) = apply(R(B), R(C)) — call fn with args from array (ABC) */ MACH_WARYTRUE, /* if toBool(R(A)): pc += sBx — coercing (iAsBx) */ MACH_WARYFALSE, /* if !toBool(R(A)): pc += sBx — coercing (iAsBx) */ MACH_JMPEMPTY, /* if R(A)==empty_text: pc += sBx (iAsBx) */ MACH_OP_COUNT } MachOpcode; static const char *mach_opcode_names[MACH_OP_COUNT] = { /* Legacy */ [MACH_LOADK] = "loadk", [MACH_LOADI] = "loadi", [MACH_LOADNULL] = "loadnull", [MACH_LOADTRUE] = "loadtrue", [MACH_LOADFALSE] = "loadfalse", [MACH_MOVE] = "move", [MACH_ADD] = "add", [MACH_SUB] = "sub", [MACH_MUL] = "mul", [MACH_DIV] = "div", [MACH_MOD] = "mod", [MACH_POW] = "pow", [MACH_NEG] = "neg", [MACH_REMAINDER] = "remainder", [MACH_MAX] = "max", [MACH_MIN] = "min", [MACH_ABS] = "abs", [MACH_SIGN] = "sign", [MACH_FRACTION] = "fraction", [MACH_INTEGER] = "integer", [MACH_FLOOR] = "floor", [MACH_CEILING] = "ceiling", [MACH_ROUND] = "round", [MACH_TRUNC] = "trunc", [MACH__DEAD_INC] = "dead_inc", [MACH__DEAD_DEC] = "dead_dec", [MACH_EQ] = "eq", [MACH_NEQ] = "neq", [MACH_LT] = "lt", [MACH_LE] = "le", [MACH_GT] = "gt", [MACH_GE] = "ge", [MACH_LNOT] = "lnot", [MACH_BNOT] = "bnot", [MACH_BAND] = "band", [MACH_BOR] = "bor", [MACH_BXOR] = "bxor", [MACH_SHL] = "shl", [MACH_SHR] = "shr", [MACH_USHR] = "ushr", [MACH_GETFIELD] = "getfield", [MACH_SETFIELD] = "setfield", [MACH_GETINDEX] = "getindex", [MACH_SETINDEX] = "setindex", [MACH_GETNAME] = "getname", [MACH_GETINTRINSIC] = "getintrinsic", [MACH_GETENV] = "getenv", [MACH_GETUP] = "getup", [MACH_SETUP] = "setup", [MACH_JMP] = "jmp", [MACH_JMPTRUE] = "jmptrue", [MACH_JMPFALSE] = "jmpfalse", [MACH_JMPNULL] = "jmpnull", [MACH_RETURN] = "return", [MACH_RETNIL] = "retnil", [MACH_NEWOBJECT] = "newobject", [MACH_NEWARRAY] = "newarray", [MACH_CLOSURE] = "closure", [MACH_THROW] = "throw", [MACH_PUSH] = "push", [MACH_POP] = "pop", [MACH_DELETE] = "delete", [MACH_DELETEINDEX] = "deleteindex", [MACH_HASPROP] = "hasprop", [MACH_REGEXP] = "regexp", [MACH_EQ_TOL] = "eq_tol", [MACH_NEQ_TOL] = "neq_tol", [MACH_NOP] = "nop", /* Mcode-derived */ [MACH_CONCAT] = "concat", [MACH_STONE_TEXT] = "stone_text", [MACH_IS_IDENTICAL] = "is_identical", [MACH_IS_INT] = "is_int", [MACH_IS_NUM] = "is_num", [MACH_IS_TEXT] = "is_text", [MACH_IS_BOOL] = "is_bool", [MACH_IS_NULL] = "is_null", [MACH_NOT] = "not", [MACH_AND] = "and", [MACH_OR] = "or", [MACH_BITNOT] = "bitnot", [MACH_BITAND] = "bitand", [MACH_BITOR] = "bitor", [MACH_BITXOR] = "bitxor", [MACH_LOAD_FIELD] = "load_field", [MACH_STORE_FIELD] = "store_field", [MACH_LOAD_INDEX] = "load_index", [MACH_STORE_INDEX] = "store_index", [MACH_LOAD_DYNAMIC] = "load_dynamic", [MACH_STORE_DYNAMIC] = "store_dynamic", [MACH_NEWRECORD] = "newrecord", [MACH_FRAME] = "frame", [MACH_SETARG] = "setarg", [MACH_INVOKE] = "invoke", [MACH_GOFRAME] = "goframe", [MACH_GOINVOKE] = "goinvoke", [MACH_JMPNOTNULL] = "jmpnotnull", [MACH_DISRUPT] = "disrupt", [MACH_IN] = "in", /* Extended type checks */ [MACH_IS_ARRAY] = "is_array", [MACH_IS_FUNC] = "is_func", [MACH_IS_RECORD] = "is_record", [MACH_IS_STONE] = "is_stone", [MACH_LENGTH] = "length", [MACH_IS_PROXY] = "is_proxy", [MACH_IS_BLOB] = "is_blob", [MACH_IS_DATA] = "is_data", [MACH_IS_TRUE] = "is_true", [MACH_IS_FALSE] = "is_false", [MACH_IS_FIT] = "is_fit", [MACH_IS_CHAR] = "is_char", [MACH_IS_DIGIT] = "is_digit", [MACH_IS_LETTER] = "is_letter", [MACH_IS_LOWER] = "is_lower", [MACH_IS_UPPER] = "is_upper", [MACH_IS_WS] = "is_ws", [MACH_IS_ACTOR] = "is_actor", [MACH_APPLY] = "apply", [MACH_WARYTRUE] = "wary_true", [MACH_WARYFALSE] = "wary_false", [MACH_JMPEMPTY] = "jump_empty", }; /* ---- Compile-time constant pool entry ---- */ /* Stores raw data during compilation; converted to JSValues when loading into context */ typedef enum { MACH_CP_INT, MACH_CP_FLOAT, MACH_CP_STR } MachCPType; typedef struct { MachCPType type; union { int32_t ival; /* integer constant */ double fval; /* float constant */ char *str; /* owned C string */ }; } MachCPoolEntry; /* ---- Compiled output (context-free) ---- */ typedef struct MachCode { uint16_t arity; uint16_t nr_close_slots; uint16_t nr_slots; uint16_t entry_point; uint32_t cpool_count; MachCPoolEntry *cpool; uint32_t instr_count; MachInstr32 *instructions; uint32_t func_count; struct MachCode **functions; char *name; /* owned C string, or NULL */ MachLineEntry *line_table; /* [instr_count], parallel to instructions[] */ char *filename; /* source filename (sys_malloc'd) */ uint16_t disruption_pc; /* start of disruption handler (0 = none) */ } MachCode; /* ---- Helpers ---- */ static JSValue *mach_materialize_cpool(JSContext *ctx, MachCPoolEntry *entries, int count) { if (count == 0) { sys_free(entries); return NULL; } JSValue *cpool = js_malloc_rt(count * sizeof(JSValue)); for (int i = 0; i < count; i++) { switch (entries[i].type) { case MACH_CP_INT: cpool[i] = JS_NewInt32(ctx, entries[i].ival); break; case MACH_CP_FLOAT: cpool[i] = JS_NewFloat64(ctx, entries[i].fval); break; case MACH_CP_STR: cpool[i] = js_key_new(ctx, entries[i].str); break; } } return cpool; } static int mach_check_call_arity(JSContext *ctx, JSFunction *fn, int argc) { if (unlikely(fn->length >= 0 && argc > fn->length)) { char buf[KEY_GET_STR_BUF_SIZE]; JS_RaiseDisrupt(ctx, "too many arguments for %s: expected %d, got %d", JS_KeyGetStr(ctx, buf, KEY_GET_STR_BUF_SIZE, fn->name), fn->length, argc); return 0; } return 1; } /* Scan backwards from pc to find what loaded the callee register. Returns the name in buf, or NULL if unknown. */ static const char *mach_callee_name(JSContext *ctx, JSCodeRegister *code, uint32_t pc, int reg, char *buf, size_t buf_size) { int hops = 4; /* limit move-chain depth */ for (int i = (int)pc - 2; i >= 0 && hops > 0; i--) { MachInstr32 prev = code->instructions[i]; int prev_op = MACH_GET_OP(prev); int prev_a = MACH_GET_A(prev); if (prev_a != reg) continue; if (prev_op == MACH_GETENV || prev_op == MACH_GETINTRINSIC) { int bx = MACH_GET_Bx(prev); if ((uint32_t)bx < code->cpool_count && JS_IsText(code->cpool[bx])) return JS_KeyGetStr(ctx, buf, buf_size, code->cpool[bx]); } if (prev_op == MACH_LOAD_FIELD) { int ci = MACH_GET_C(prev); if ((uint32_t)ci < code->cpool_count && JS_IsText(code->cpool[ci])) return JS_KeyGetStr(ctx, buf, buf_size, code->cpool[ci]); } if (prev_op == MACH_MOVE) { reg = MACH_GET_B(prev); hops--; continue; } break; /* some other op wrote to this reg — give up */ } return NULL; } /* ---- Link pass: resolve GETNAME to GETINTRINSIC or GETENV ---- */ static void mach_link_code(JSContext *ctx, JSCodeRegister *code, JSValue env) { if (!JS_IsNull(env) && !JS_IsStone(env)) { fprintf(stderr, "mach_link_code: ERROR env not stone (code=%s file=%s)\n", code->name_cstr ? code->name_cstr : "", code->filename_cstr ? code->filename_cstr : ""); abort(); } JSGCRef env_ref; JS_PushGCRef(ctx, &env_ref); env_ref.val = env; for (uint32_t i = 0; i < code->instr_count; i++) { MachInstr32 instr = code->instructions[i]; if (MACH_GET_OP(instr) != MACH_GETNAME) continue; int a = MACH_GET_A(instr); int bx = MACH_GET_Bx(instr); int in_env = 0; if (!JS_IsNull(env_ref.val) && (uint32_t)bx < code->cpool_count) { in_env = JS_HasProperty(ctx, env_ref.val, code->cpool[bx]); } code->instructions[i] = MACH_ABx(in_env ? MACH_GETENV : MACH_GETINTRINSIC, a, bx); } for (uint32_t i = 0; i < code->func_count; i++) if (code->functions[i]) mach_link_code(ctx, code->functions[i], env_ref.val); JS_PopGCRef(ctx, &env_ref); } /* Free a MachCode tree (compiled but not yet loaded) */ void JS_FreeMachCode(MachCode *mc) { if (!mc) return; sys_free(mc->instructions); for (uint32_t i = 0; i < mc->cpool_count; i++) { if (mc->cpool[i].type == MACH_CP_STR) sys_free(mc->cpool[i].str); } sys_free(mc->cpool); for (uint32_t i = 0; i < mc->func_count; i++) JS_FreeMachCode(mc->functions[i]); sys_free(mc->functions); sys_free(mc->name); sys_free(mc->line_table); sys_free(mc->filename); sys_free(mc); } /* ---- MachCode binary serialization ---- */ static size_t mach_serialized_size(MachCode *mc) { /* Header: 6 x uint16_t + 1 x uint16_t padding = 14 bytes (but let's be explicit) */ size_t sz = 6 * sizeof(uint16_t); /* arity, nr_close_slots, nr_slots, entry_point, disruption_pc, padding */ /* name */ sz += sizeof(uint32_t); if (mc->name) sz += strlen(mc->name); /* filename */ sz += sizeof(uint32_t); if (mc->filename) sz += strlen(mc->filename); /* cpool */ sz += sizeof(uint32_t); /* cpool_count */ for (uint32_t i = 0; i < mc->cpool_count; i++) { sz += 1; /* type tag */ switch (mc->cpool[i].type) { case MACH_CP_INT: sz += sizeof(int32_t); break; case MACH_CP_FLOAT: sz += sizeof(double); break; case MACH_CP_STR: sz += sizeof(uint32_t); sz += strlen(mc->cpool[i].str); break; } } /* instructions + line_table */ sz += sizeof(uint32_t); /* instr_count */ sz += mc->instr_count * sizeof(MachInstr32); sz += mc->instr_count * sizeof(MachLineEntry); /* nested functions */ sz += sizeof(uint32_t); /* func_count */ for (uint32_t i = 0; i < mc->func_count; i++) sz += mach_serialized_size(mc->functions[i]); return sz; } static void mach_serialize_write(MachCode *mc, uint8_t **p) { uint8_t *w = *p; /* Header fields */ memcpy(w, &mc->arity, 2); w += 2; memcpy(w, &mc->nr_close_slots, 2); w += 2; memcpy(w, &mc->nr_slots, 2); w += 2; memcpy(w, &mc->entry_point, 2); w += 2; memcpy(w, &mc->disruption_pc, 2); w += 2; uint16_t pad = 0; memcpy(w, &pad, 2); w += 2; /* name */ uint32_t name_len = mc->name ? (uint32_t)strlen(mc->name) : 0; memcpy(w, &name_len, 4); w += 4; if (name_len) { memcpy(w, mc->name, name_len); w += name_len; } /* filename */ uint32_t fn_len = mc->filename ? (uint32_t)strlen(mc->filename) : 0; memcpy(w, &fn_len, 4); w += 4; if (fn_len) { memcpy(w, mc->filename, fn_len); w += fn_len; } /* cpool */ memcpy(w, &mc->cpool_count, 4); w += 4; for (uint32_t i = 0; i < mc->cpool_count; i++) { uint8_t tag = (uint8_t)mc->cpool[i].type; *w++ = tag; switch (mc->cpool[i].type) { case MACH_CP_INT: memcpy(w, &mc->cpool[i].ival, 4); w += 4; break; case MACH_CP_FLOAT: memcpy(w, &mc->cpool[i].fval, 8); w += 8; break; case MACH_CP_STR: { uint32_t slen = (uint32_t)strlen(mc->cpool[i].str); memcpy(w, &slen, 4); w += 4; memcpy(w, mc->cpool[i].str, slen); w += slen; break; } } } /* instructions */ memcpy(w, &mc->instr_count, 4); w += 4; memcpy(w, mc->instructions, mc->instr_count * sizeof(MachInstr32)); w += mc->instr_count * sizeof(MachInstr32); /* line_table (write zeros if NULL) */ if (mc->line_table) { memcpy(w, mc->line_table, mc->instr_count * sizeof(MachLineEntry)); } else { memset(w, 0, mc->instr_count * sizeof(MachLineEntry)); } w += mc->instr_count * sizeof(MachLineEntry); /* nested functions */ memcpy(w, &mc->func_count, 4); w += 4; *p = w; for (uint32_t i = 0; i < mc->func_count; i++) mach_serialize_write(mc->functions[i], p); } uint8_t *JS_SerializeMachCode(MachCode *mc, size_t *out_size) { if (!mc) return NULL; size_t sz = mach_serialized_size(mc); uint8_t *buf = sys_malloc(sz); if (!buf) return NULL; uint8_t *p = buf; mach_serialize_write(mc, &p); *out_size = sz; return buf; } #define DESER_CHECK(cond) do { if (!(cond)) goto fail; } while(0) static MachCode *mach_deserialize_read(const uint8_t **p, const uint8_t *end) { const uint8_t *r = *p; /* Need at least the fixed header: 6 * uint16_t = 12 bytes */ if (r + 12 > end) return NULL; MachCode *mc = sys_malloc(sizeof(MachCode)); if (!mc) return NULL; memset(mc, 0, sizeof(MachCode)); memcpy(&mc->arity, r, 2); r += 2; memcpy(&mc->nr_close_slots, r, 2); r += 2; memcpy(&mc->nr_slots, r, 2); r += 2; memcpy(&mc->entry_point, r, 2); r += 2; memcpy(&mc->disruption_pc, r, 2); r += 2; r += 2; /* skip padding */ /* name */ DESER_CHECK(r + 4 <= end); uint32_t name_len; memcpy(&name_len, r, 4); r += 4; DESER_CHECK(r + name_len <= end); if (name_len) { mc->name = sys_malloc(name_len + 1); memcpy(mc->name, r, name_len); mc->name[name_len] = '\0'; r += name_len; } /* filename */ DESER_CHECK(r + 4 <= end); uint32_t fn_len; memcpy(&fn_len, r, 4); r += 4; DESER_CHECK(r + fn_len <= end); if (fn_len) { mc->filename = sys_malloc(fn_len + 1); memcpy(mc->filename, r, fn_len); mc->filename[fn_len] = '\0'; r += fn_len; } /* cpool */ DESER_CHECK(r + 4 <= end); memcpy(&mc->cpool_count, r, 4); r += 4; if (mc->cpool_count) { mc->cpool = sys_malloc(mc->cpool_count * sizeof(MachCPoolEntry)); for (uint32_t i = 0; i < mc->cpool_count; i++) { DESER_CHECK(r + 1 <= end); uint8_t tag = *r++; mc->cpool[i].type = (MachCPType)tag; switch (tag) { case MACH_CP_INT: DESER_CHECK(r + 4 <= end); memcpy(&mc->cpool[i].ival, r, 4); r += 4; break; case MACH_CP_FLOAT: DESER_CHECK(r + 8 <= end); memcpy(&mc->cpool[i].fval, r, 8); r += 8; break; case MACH_CP_STR: { DESER_CHECK(r + 4 <= end); uint32_t slen; memcpy(&slen, r, 4); r += 4; DESER_CHECK(r + slen <= end); mc->cpool[i].str = sys_malloc(slen + 1); memcpy(mc->cpool[i].str, r, slen); mc->cpool[i].str[slen] = '\0'; r += slen; break; } default: goto fail; } } } /* instructions */ DESER_CHECK(r + 4 <= end); memcpy(&mc->instr_count, r, 4); r += 4; DESER_CHECK(r + mc->instr_count * sizeof(MachInstr32) <= end); mc->instructions = sys_malloc(mc->instr_count * sizeof(MachInstr32)); memcpy(mc->instructions, r, mc->instr_count * sizeof(MachInstr32)); r += mc->instr_count * sizeof(MachInstr32); /* line_table */ DESER_CHECK(r + mc->instr_count * sizeof(MachLineEntry) <= end); mc->line_table = sys_malloc(mc->instr_count * sizeof(MachLineEntry)); memcpy(mc->line_table, r, mc->instr_count * sizeof(MachLineEntry)); r += mc->instr_count * sizeof(MachLineEntry); /* nested functions */ DESER_CHECK(r + 4 <= end); memcpy(&mc->func_count, r, 4); r += 4; if (mc->func_count) { mc->functions = sys_malloc(mc->func_count * sizeof(MachCode *)); for (uint32_t i = 0; i < mc->func_count; i++) { mc->functions[i] = mach_deserialize_read(&r, end); if (!mc->functions[i]) { mc->func_count = i; /* only free what we allocated */ goto fail; } } } *p = r; return mc; fail: JS_FreeMachCode(mc); return NULL; } #undef DESER_CHECK MachCode *JS_DeserializeMachCode(const uint8_t *data, size_t size) { if (!data || size == 0) return NULL; const uint8_t *p = data; const uint8_t *end = data + size; MachCode *mc = mach_deserialize_read(&p, end); if (mc && p != end) { /* Trailing data — treat as error */ JS_FreeMachCode(mc); return NULL; } return mc; } /* Load a MachCode into a JSCodeRegister (materializes JSValues, needs ctx) */ JSCodeRegister *JS_LoadMachCode(JSContext *ctx, MachCode *mc, JSValue env) { /* Protect env from GC — materialize/link calls can trigger collection */ JSGCRef env_ref; JS_PushGCRef(ctx, &env_ref); env_ref.val = env; JSCodeRegister *code = js_mallocz_rt(sizeof(JSCodeRegister)); code->arity = mc->arity; code->nr_close_slots = mc->nr_close_slots; code->nr_slots = mc->nr_slots; code->entry_point = mc->entry_point; code->instr_count = mc->instr_count; code->instructions = mc->instructions; /* transfer ownership */ mc->instructions = NULL; /* Materialize cpool: raw -> JSValue */ code->cpool_count = mc->cpool_count; code->cpool = mach_materialize_cpool(ctx, mc->cpool, mc->cpool_count); /* Recursively load nested functions */ code->func_count = mc->func_count; if (mc->func_count > 0) { code->functions = js_malloc_rt(mc->func_count * sizeof(JSCodeRegister *)); for (uint32_t i = 0; i < mc->func_count; i++) code->functions[i] = JS_LoadMachCode(ctx, mc->functions[i], env_ref.val); } else { code->functions = NULL; } /* Intern function name */ code->name = mc->name ? js_key_new(ctx, mc->name) : JS_NULL; /* Transfer debug info */ code->line_table = mc->line_table; mc->line_table = NULL; code->filename_cstr = mc->filename ? js_strdup_rt(mc->filename) : NULL; code->name_cstr = mc->name ? js_strdup_rt(mc->name) : NULL; code->disruption_pc = mc->disruption_pc; /* Link: resolve GETNAME to GETENV/GETINTRINSIC */ mach_link_code(ctx, code, env_ref.val); JS_PopGCRef(ctx, &env_ref); return code; } /* Free a JSCodeRegister and all nested functions */ static void js_free_code_register(JSCodeRegister *code) { if (!code) return; js_free_rt(code->instructions); js_free_rt(code->cpool); for (uint32_t i = 0; i < code->func_count; i++) { js_free_code_register(code->functions[i]); } js_free_rt(code->functions); js_free_rt(code->line_table); js_free_rt(code->filename_cstr); js_free_rt(code->name_cstr); js_free_rt(code); } /* ============================================================ MACH VM — register-based bytecode interpreter ============================================================ */ /* Allocate a JSFrameRegister on the GC heap */ JSFrameRegister *alloc_frame_register(JSContext *ctx, int slot_count) { size_t size = sizeof(JSFrameRegister) + slot_count * sizeof(JSValue); JSFrameRegister *frame = js_malloc(ctx, size); if (!frame) return NULL; /* cap56 = slot count (used by gc_object_size) */ frame->header = objhdr_make(slot_count, OBJ_FRAME, 0, 0, 0, 0); frame->function = JS_NULL; frame->caller = JS_NULL; frame->address = JS_NewInt32(ctx, 0); /* Initialize slots to null */ for (int i = 0; i < slot_count; i++) { frame->slots[i] = JS_NULL; } return frame; } static JSValue js_new_register_code(JSContext *ctx, JSCodeRegister *code) { JSCode *jc; if (!code) return JS_EXCEPTION; jc = ct_alloc(ctx, sizeof(JSCode), 8); if (!jc) return JS_EXCEPTION; memset(jc, 0, sizeof(JSCode)); jc->header = objhdr_make(0, OBJ_CODE, 0, 0, 0, 0); jc->kind = JS_CODE_KIND_REGISTER; jc->arity = (int16_t)code->arity; jc->u.reg.code = code; return JS_MKPTR(jc); } static JSValue js_new_native_code(JSContext *ctx, void *fn_ptr, void *dl_handle, uint16_t nr_slots, int arity) { JSCode *jc = ct_alloc(ctx, sizeof(JSCode), 8); if (!jc) return JS_EXCEPTION; memset(jc, 0, sizeof(JSCode)); jc->header = objhdr_make(0, OBJ_CODE, 0, 0, 0, 0); jc->kind = JS_CODE_KIND_NATIVE; jc->arity = (int16_t)arity; jc->u.native.fn_ptr = fn_ptr; jc->u.native.dl_handle = dl_handle; jc->u.native.nr_slots = nr_slots; return JS_MKPTR(jc); } /* Create a register-based function from JSCodeRegister */ JSValue js_new_register_function(JSContext *ctx, JSCodeRegister *code, JSValue env, JSValue outer_frame) { /* Protect env and outer_frame from GC — js_mallocz can trigger collection which moves heap objects, invalidating stack-local copies */ JSGCRef env_ref, frame_ref; JS_PushGCRef(ctx, &env_ref); env_ref.val = env; JS_PushGCRef(ctx, &frame_ref); frame_ref.val = outer_frame; JSGCRef fn_ref; JSFunction *fn; JSValue code_obj; JS_AddGCRef(ctx, &fn_ref); fn_ref.val = JS_NULL; fn = js_mallocz(ctx, sizeof(JSFunction)); if (!fn) { JS_DeleteGCRef(ctx, &fn_ref); JS_PopGCRef(ctx, &frame_ref); JS_PopGCRef(ctx, &env_ref); return JS_EXCEPTION; } fn_ref.val = JS_MKPTR(fn); fn->header = objhdr_make(0, OBJ_FUNCTION, 0, 0, 0, 0); fn->kind = JS_FUNC_KIND_REGISTER; fn->length = code->arity; fn->name = code->name; code_obj = js_new_register_code(ctx, code); if (JS_IsException(code_obj)) { JS_DeleteGCRef(ctx, &fn_ref); JS_PopGCRef(ctx, &frame_ref); JS_PopGCRef(ctx, &env_ref); return JS_EXCEPTION; } fn = JS_VALUE_GET_FUNCTION(fn_ref.val); fn->u.cell.code = code_obj; fn->u.cell.env_record = env_ref.val; fn->u.cell.outer_frame = frame_ref.val; JSValue out = fn_ref.val; JS_DeleteGCRef(ctx, &fn_ref); JS_PopGCRef(ctx, &frame_ref); JS_PopGCRef(ctx, &env_ref); return out; } JSValue js_new_native_function_with_code(JSContext *ctx, JSValue code_obj, int arity, JSValue outer_frame, JSValue env_record) { JSGCRef frame_ref; JSGCRef fn_ref; JSFunction *fn; JS_PushGCRef(ctx, &frame_ref); frame_ref.val = outer_frame; JS_AddGCRef(ctx, &fn_ref); fn_ref.val = JS_NULL; fn = js_mallocz(ctx, sizeof(JSFunction)); if (!fn) { JS_DeleteGCRef(ctx, &fn_ref); JS_PopGCRef(ctx, &frame_ref); return JS_EXCEPTION; } fn_ref.val = JS_MKPTR(fn); fn->header = objhdr_make(0, OBJ_FUNCTION, 0, 0, 0, 0); fn->kind = JS_FUNC_KIND_NATIVE; fn->length = arity; fn->name = JS_NULL; fn = JS_VALUE_GET_FUNCTION(fn_ref.val); fn->u.cell.code = code_obj; fn->u.cell.env_record = env_record; fn->u.cell.outer_frame = frame_ref.val; JSValue out = fn_ref.val; JS_DeleteGCRef(ctx, &fn_ref); JS_PopGCRef(ctx, &frame_ref); return out; } /* Create a native (QBE-compiled) function */ JSValue js_new_native_function(JSContext *ctx, void *fn_ptr, void *dl_handle, uint16_t nr_slots, int arity, JSValue outer_frame, JSValue env) { JSValue code_obj = js_new_native_code(ctx, fn_ptr, dl_handle, nr_slots, arity); if (JS_IsException(code_obj)) return JS_EXCEPTION; return js_new_native_function_with_code(ctx, code_obj, arity, outer_frame, env); } /* Binary operations helper */ static JSValue reg_vm_binop(JSContext *ctx, int op, JSValue a, JSValue b) { /* Fast path for integers */ if (JS_VALUE_IS_BOTH_INT(a, b)) { int32_t ia = JS_VALUE_GET_INT(a); int32_t ib = JS_VALUE_GET_INT(b); switch (op) { case MACH_ADD: { int64_t r = (int64_t)ia + (int64_t)ib; if (r >= INT32_MIN && r <= INT32_MAX) return JS_NewInt32(ctx, (int32_t)r); return JS_NewFloat64(ctx, (double)r); } case MACH_SUB: { int64_t r = (int64_t)ia - (int64_t)ib; if (r >= INT32_MIN && r <= INT32_MAX) return JS_NewInt32(ctx, (int32_t)r); return JS_NewFloat64(ctx, (double)r); } case MACH_MUL: { int64_t r = (int64_t)ia * (int64_t)ib; if (r >= INT32_MIN && r <= INT32_MAX) return JS_NewInt32(ctx, (int32_t)r); return JS_NewFloat64(ctx, (double)r); } case MACH_DIV: if (ib == 0) return JS_NULL; if (ia % ib == 0) return JS_NewInt32(ctx, ia / ib); return JS_NewFloat64(ctx, (double)ia / (double)ib); case MACH_MOD: if (ib == 0) return JS_NULL; return JS_NewFloat64(ctx, (double)ia - ((double)ib * floor((double)ia / (double)ib))); case MACH_EQ: return JS_NewBool(ctx, ia == ib); case MACH_NEQ: return JS_NewBool(ctx, ia != ib); case MACH_LT: return JS_NewBool(ctx, ia < ib); case MACH_LE: return JS_NewBool(ctx, ia <= ib); case MACH_GT: return JS_NewBool(ctx, ia > ib); case MACH_GE: return JS_NewBool(ctx, ia >= ib); case MACH_BAND: return JS_NewInt32(ctx, ia & ib); case MACH_BOR: return JS_NewInt32(ctx, ia | ib); case MACH_BXOR: return JS_NewInt32(ctx, ia ^ ib); case MACH_SHL: return JS_NewInt32(ctx, ia << (ib & 31)); case MACH_SHR: return JS_NewInt32(ctx, ia >> (ib & 31)); case MACH_USHR: return JS_NewInt32(ctx, (uint32_t)ia >> (ib & 31)); default: break; } } /* Comparison ops allow mixed types — return false for mismatches */ if (op >= MACH_EQ && op <= MACH_GE) { /* Fast path: identical values (chase pointers for forwarded objects) */ { JSValue ca = JS_IsPtr(a) ? JS_MKPTR(chase(a)) : a; JSValue cb = JS_IsPtr(b) ? JS_MKPTR(chase(b)) : b; if (ca == cb) { if (op == MACH_EQ || op == MACH_LE || op == MACH_GE) return JS_TRUE; if (op == MACH_NEQ) return JS_FALSE; } } if (JS_IsNumber(a) && JS_IsNumber(b)) { double da, db; JS_ToFloat64(ctx, &da, a); JS_ToFloat64(ctx, &db, b); switch (op) { case MACH_EQ: return JS_NewBool(ctx, da == db); case MACH_NEQ: return JS_NewBool(ctx, da != db); case MACH_LT: return JS_NewBool(ctx, da < db); case MACH_LE: return JS_NewBool(ctx, da <= db); case MACH_GT: return JS_NewBool(ctx, da > db); case MACH_GE: return JS_NewBool(ctx, da >= db); default: break; } } /* String comparisons */ if (mist_is_text(a) && mist_is_text(b)) { int cmp = js_string_compare_value(ctx, a, b, FALSE); switch (op) { case MACH_EQ: return JS_NewBool(ctx, cmp == 0); case MACH_NEQ: return JS_NewBool(ctx, cmp != 0); case MACH_LT: return JS_NewBool(ctx, cmp < 0); case MACH_LE: return JS_NewBool(ctx, cmp <= 0); case MACH_GT: return JS_NewBool(ctx, cmp > 0); case MACH_GE: return JS_NewBool(ctx, cmp >= 0); default: break; } } /* Null comparisons */ if (JS_IsNull(a) && JS_IsNull(b)) { if (op == MACH_EQ || op == MACH_LE || op == MACH_GE) return JS_TRUE; return JS_FALSE; } /* Boolean comparisons */ if (JS_IsBool(a) && JS_IsBool(b)) { int ba = JS_VALUE_GET_BOOL(a); int bb = JS_VALUE_GET_BOOL(b); switch (op) { case MACH_EQ: return JS_NewBool(ctx, ba == bb); case MACH_NEQ: return JS_NewBool(ctx, ba != bb); case MACH_LT: return JS_NewBool(ctx, ba < bb); case MACH_LE: return JS_NewBool(ctx, ba <= bb); case MACH_GT: return JS_NewBool(ctx, ba > bb); case MACH_GE: return JS_NewBool(ctx, ba >= bb); default: break; } } /* Different types for ordering comparisons: disrupt */ if (op >= MACH_LT && op <= MACH_GE) return JS_RaiseDisrupt(ctx, "cannot compare: operands must be same type"); /* EQ/NEQ with different types: false/true */ if (op == MACH_NEQ) return JS_NewBool(ctx, 1); return JS_NewBool(ctx, 0); } /* Numeric operations — both must be numeric */ if (JS_IsNumber(a) && JS_IsNumber(b)) { double da, db; JS_ToFloat64(ctx, &da, a); JS_ToFloat64(ctx, &db, b); switch (op) { case MACH_ADD: { double r = da + db; if (!isfinite(r)) return JS_NULL; return JS_NewFloat64(ctx, r); } case MACH_SUB: { double r = da - db; if (!isfinite(r)) return JS_NULL; return JS_NewFloat64(ctx, r); } case MACH_MUL: { double r = da * db; if (!isfinite(r)) return JS_NULL; return JS_NewFloat64(ctx, r); } case MACH_DIV: { if (db == 0.0) return JS_NULL; double r = da / db; if (!isfinite(r)) return JS_NULL; return JS_NewFloat64(ctx, r); } case MACH_MOD: { if (db == 0.0) return JS_NULL; if (isnan(da) || isnan(db)) return JS_NULL; if (da == 0.0) return JS_NewFloat64(ctx, 0.0); double r = da - (db * floor(da / db)); return JS_NewFloat64(ctx, r); } case MACH_POW: { double r = pow(da, db); if (!isfinite(r) && isfinite(da) && isfinite(db)) return JS_NULL; return JS_NewFloat64(ctx, r); } case MACH_BAND: case MACH_BOR: case MACH_BXOR: case MACH_SHL: case MACH_SHR: case MACH_USHR: { int32_t ia = (int32_t)da; int32_t ib = (int32_t)db; switch (op) { case MACH_BAND: return JS_NewInt32(ctx, ia & ib); case MACH_BOR: return JS_NewInt32(ctx, ia | ib); case MACH_BXOR: return JS_NewInt32(ctx, ia ^ ib); case MACH_SHL: return JS_NewInt32(ctx, ia << (ib & 31)); case MACH_SHR: return JS_NewInt32(ctx, ia >> (ib & 31)); case MACH_USHR: return JS_NewInt32(ctx, (uint32_t)ia >> (ib & 31)); default: break; } } default: break; } } /* Type mismatch — disrupt */ return JS_RaiseDisrupt(ctx, "type mismatch in binary operation"); } static inline int mach_get_number(JSValue v, double *out) { uint32_t tag = JS_VALUE_GET_TAG(v); if (tag == JS_TAG_INT) { *out = (double)JS_VALUE_GET_INT(v); return 0; } if (JS_TAG_IS_FLOAT64(tag)) { *out = JS_VALUE_GET_FLOAT64(v); return 0; } return -1; } static inline int mach_get_place(JSContext *ctx, JSValue v, int32_t *out) { uint32_t tag = JS_VALUE_GET_NORM_TAG(v); if (tag == JS_TAG_INT || tag == JS_TAG_BOOL || tag == JS_TAG_NULL || tag == JS_TAG_FLOAT64) { return JS_ToInt32(ctx, out, v); } return -1; } static inline double mach_apply_place(double d, int32_t place, double (*f)(double)) { if (place == 0) return f(d); double mult = pow(10.0, -(double)place); return f(d * mult) / mult; } #ifdef HAVE_ASAN void __asan_on_error(void) { JSContext *ctx = __asan_js_ctx; if (!ctx) return; if (JS_IsNull(ctx->reg_current_frame)) return; JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(ctx->reg_current_frame); uint32_t cur_pc = ctx->current_register_pc; fprintf(stderr, "\n=== ASAN error: VM stack trace ===\n"); int is_first = 1; while (frame) { if (!mist_is_function(frame->function)) break; JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function); const char *func_name = NULL; const char *file = NULL; uint16_t line = 0; uint32_t pc = is_first ? cur_pc : 0; if (fn->kind == JS_FUNC_KIND_REGISTER && JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code) { JSCodeRegister *code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code; file = code->filename_cstr; func_name = code->name_cstr; if (!is_first) pc = (uint32_t)(JS_VALUE_GET_INT(frame->address) >> 16); if (code->line_table && pc < code->instr_count) line = code->line_table[pc].line; } fprintf(stderr, " %s (%s:%u)\n", func_name ? func_name : "", file ? file : "", line); if (!JS_IsPtr(frame->caller)) break; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller); is_first = 0; } fprintf(stderr, "=================================\n"); } #endif /* Main register VM execution loop */ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, JSValue this_obj, int argc, JSValue *argv, JSValue env, JSValue outer_frame) { JSGCRef frame_ref; JSFrameRegister *frame; uint32_t pc; JSValue result; /* Resume path: if VM was suspended, restore state and jump into dispatch */ if (ctx->suspended) { ctx->suspended = 0; JS_AddGCRef(ctx, &frame_ref); frame_ref.val = ctx->suspended_frame_ref.val; ctx->suspended_frame_ref.val = JS_NULL; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function); code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code; env = fn->u.cell.env_record; pc = ctx->suspended_pc; result = JS_NULL; #ifdef HAVE_ASAN __asan_js_ctx = ctx; #endif goto vm_dispatch; } { /* Normal path: set up a new call */ /* Protect env and outer_frame from GC — alloc_frame_register can trigger collection which moves heap objects, invalidating stack-local copies */ JSGCRef env_gc, of_gc; JS_PushGCRef(ctx, &env_gc); env_gc.val = env; JS_PushGCRef(ctx, &of_gc); of_gc.val = outer_frame; /* Protect argv and this_obj from GC using JSGCRef. alloc_frame_register and js_new_register_function can trigger GC. */ int nargs_copy = (argc < code->arity) ? argc : code->arity; JSGCRef this_gc; JS_PushGCRef(ctx, &this_gc); this_gc.val = this_obj; JSGCRef arg_gcs[nargs_copy > 0 ? nargs_copy : 1]; for (int i = 0; i < nargs_copy; i++) { JS_PushGCRef(ctx, &arg_gcs[i]); arg_gcs[i].val = argv[i]; } /* Allocate initial frame */ frame = alloc_frame_register(ctx, code->nr_slots); if (!frame) { for (int i = nargs_copy - 1; i >= 0; i--) JS_PopGCRef(ctx, &arg_gcs[i]); JS_PopGCRef(ctx, &this_gc); JS_PopGCRef(ctx, &of_gc); JS_PopGCRef(ctx, &env_gc); return JS_EXCEPTION; } /* Protect frame from GC */ JS_AddGCRef(ctx, &frame_ref); frame_ref.val = JS_MKPTR(frame); #ifdef HAVE_ASAN __asan_js_ctx = ctx; #endif /* Setup initial frame — wrap top-level code in a function object so that returning from a called register function can read code/env from frame */ JSValue top_fn = js_new_register_function(ctx, code, env_gc.val, of_gc.val); env = env_gc.val; /* refresh — GC may have moved env during allocation */ frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); frame->function = top_fn; frame->caller = JS_NewInt32(ctx, 0); /* sentinel: active top-level, not eligible for GC shortening */ frame->slots[0] = this_gc.val; /* slot 0 = this */ /* Copy arguments from GC-safe refs */ for (int i = 0; i < nargs_copy; i++) { frame->slots[1 + i] = arg_gcs[i].val; } for (int i = nargs_copy - 1; i >= 0; i--) JS_PopGCRef(ctx, &arg_gcs[i]); JS_PopGCRef(ctx, &this_gc); JS_PopGCRef(ctx, &of_gc); JS_PopGCRef(ctx, &env_gc); pc = code->entry_point; result = JS_NULL; /* Fire trace hook for top-level register function entry */ if (unlikely(ctx->trace_hook) && (ctx->trace_type & JS_HOOK_CALL)) { js_debug dbg = {0}; if (code->name_cstr) snprintf(dbg.name, sizeof(dbg.name), "%s", code->name_cstr); if (code->filename_cstr) snprintf(dbg.filename, sizeof(dbg.filename), "%s", code->filename_cstr); if (code->line_table) dbg.line = code->line_table[code->entry_point].line; dbg.param_n = code->arity; dbg.unique = (int)(uintptr_t)code; ctx->trace_hook(ctx, JS_HOOK_CALL, &dbg, ctx->trace_data); } } /* end normal path block */ vm_dispatch: /* Execution loop — 32-bit instruction dispatch */ for (;;) { #ifndef NDEBUG if (pc >= code->instr_count) { fprintf(stderr, "mach VM: pc %u overran code->instr_count %u (missing RETURN/RETNIL?)\n", pc, code->instr_count); result = JS_RaiseDisrupt(ctx, "pc overrun"); goto done; } #endif MachInstr32 instr; int op, a, b, c; #ifdef __GNUC__ /* Computed goto dispatch — each opcode gets its own branch predictor entry */ /* Use a macro to generate consistent dispatch table entries and labels */ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Woverride-init" #ifdef __clang__ #pragma clang diagnostic ignored "-Winitializer-overrides" #endif #define DT(x) [x] = &&op_##x static const void *dispatch_table[256] = { [0 ... 255] = &&op_DEFAULT, DT(MACH_LOADK), DT(MACH_LOADI), DT(MACH_LOADNULL), DT(MACH_LOADTRUE), DT(MACH_LOADFALSE), DT(MACH_MOVE), DT(MACH_ADD), DT(MACH_SUB), DT(MACH_MUL), DT(MACH_DIV), DT(MACH_MOD), DT(MACH_POW), DT(MACH_NEG), DT(MACH_REMAINDER), DT(MACH_MAX), DT(MACH_MIN), DT(MACH_ABS), DT(MACH_SIGN), DT(MACH_FRACTION), DT(MACH_INTEGER), DT(MACH_FLOOR), DT(MACH_CEILING), DT(MACH_ROUND), DT(MACH_TRUNC), DT(MACH_EQ), DT(MACH_NEQ), DT(MACH_LT), DT(MACH_LE), DT(MACH_GT), DT(MACH_GE), DT(MACH_LNOT), DT(MACH_BNOT), DT(MACH_BAND), DT(MACH_BOR), DT(MACH_BXOR), DT(MACH_SHL), DT(MACH_SHR), DT(MACH_USHR), DT(MACH_GETFIELD), DT(MACH_SETFIELD), DT(MACH_GETINDEX), DT(MACH_SETINDEX), DT(MACH_GETNAME), DT(MACH_GETINTRINSIC), DT(MACH_GETENV), DT(MACH_GETUP), DT(MACH_SETUP), DT(MACH_JMP), DT(MACH_JMPTRUE), DT(MACH_JMPFALSE), DT(MACH_JMPNULL), DT(MACH_RETURN), DT(MACH_RETNIL), DT(MACH_NEWOBJECT), DT(MACH_NEWARRAY), DT(MACH_CLOSURE), DT(MACH_THROW), DT(MACH_PUSH), DT(MACH_POP), DT(MACH_DELETE), DT(MACH_DELETEINDEX), DT(MACH_HASPROP), DT(MACH_REGEXP), DT(MACH_EQ_TOL), DT(MACH_NEQ_TOL), DT(MACH_NOP), DT(MACH_CONCAT), DT(MACH_STONE_TEXT), DT(MACH_IS_IDENTICAL), DT(MACH_IS_INT), DT(MACH_IS_NUM), DT(MACH_IS_TEXT), DT(MACH_IS_BOOL), DT(MACH_IS_NULL), DT(MACH_NOT), DT(MACH_AND), DT(MACH_OR), DT(MACH_BITNOT), DT(MACH_BITAND), DT(MACH_BITOR), DT(MACH_BITXOR), DT(MACH_LOAD_FIELD), DT(MACH_STORE_FIELD), DT(MACH_LOAD_INDEX), DT(MACH_STORE_INDEX), DT(MACH_LOAD_DYNAMIC), DT(MACH_STORE_DYNAMIC), DT(MACH_NEWRECORD), DT(MACH_FRAME), DT(MACH_SETARG), DT(MACH_INVOKE), DT(MACH_GOFRAME), DT(MACH_GOINVOKE), DT(MACH_JMPNOTNULL), DT(MACH_DISRUPT), DT(MACH_IN), DT(MACH_IS_ARRAY), DT(MACH_IS_FUNC), DT(MACH_IS_RECORD), DT(MACH_IS_STONE), DT(MACH_LENGTH), DT(MACH_IS_PROXY), DT(MACH_IS_BLOB), DT(MACH_IS_DATA), DT(MACH_IS_TRUE), DT(MACH_IS_FALSE), DT(MACH_IS_FIT), DT(MACH_IS_CHAR), DT(MACH_IS_DIGIT), DT(MACH_IS_LETTER), DT(MACH_IS_LOWER), DT(MACH_IS_UPPER), DT(MACH_IS_WS), DT(MACH_IS_ACTOR), DT(MACH_APPLY), DT(MACH_WARYTRUE), DT(MACH_WARYFALSE), DT(MACH_JMPEMPTY), }; #pragma GCC diagnostic pop #undef DT #define VM_DECODE() do { \ ctx->reg_current_frame = frame_ref.val; \ ctx->current_register_pc = pc; \ instr = code->instructions[pc++]; \ op = MACH_GET_OP(instr); \ a = MACH_GET_A(instr); \ b = MACH_GET_B(instr); \ c = MACH_GET_C(instr); \ } while(0) #define VM_DISPATCH() do { VM_DECODE(); goto *dispatch_table[op]; } while(0) /* VM_CASE: dual-label — goto target for computed goto + case for switch fallback */ #define VM_CASE(x) op_##x: case x #define VM_DEFAULT op_DEFAULT: default #define VM_BREAK() VM_DISPATCH() #else #define VM_DECODE() do { \ ctx->reg_current_frame = frame_ref.val; \ ctx->current_register_pc = pc; \ instr = code->instructions[pc++]; \ op = MACH_GET_OP(instr); \ a = MACH_GET_A(instr); \ b = MACH_GET_B(instr); \ c = MACH_GET_C(instr); \ } while(0) #define VM_DISPATCH() do { VM_DECODE(); } while(0) #define VM_CASE(x) case x #define VM_DEFAULT default #define VM_BREAK() break #endif VM_DECODE(); #ifdef __GNUC__ goto *dispatch_table[op]; #endif switch (op) { VM_CASE(MACH_NOP): VM_BREAK(); VM_CASE(MACH_LOADK): { int bx = MACH_GET_Bx(instr); if (bx < (int)code->cpool_count) frame->slots[a] = code->cpool[bx]; VM_BREAK(); } VM_CASE(MACH_LOADI): frame->slots[a] = JS_NewInt32(ctx, MACH_GET_sBx(instr)); VM_BREAK(); VM_CASE(MACH_LOADNULL): frame->slots[a] = JS_NULL; VM_BREAK(); VM_CASE(MACH_LOADTRUE): frame->slots[a] = JS_TRUE; VM_BREAK(); VM_CASE(MACH_LOADFALSE): frame->slots[a] = JS_FALSE; VM_BREAK(); VM_CASE(MACH_MOVE): frame->slots[a] = frame->slots[b]; VM_BREAK(); /* Arithmetic — mcode guarantees both operands are numbers */ VM_CASE(MACH_ADD): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { int64_t r = (int64_t)JS_VALUE_GET_INT(left) + (int64_t)JS_VALUE_GET_INT(right); frame->slots[a] = (r >= INT32_MIN && r <= INT32_MAX) ? JS_NewInt32(ctx, (int32_t)r) : JS_NewFloat64(ctx, (double)r); } else { double da, db, r; JS_ToFloat64(ctx, &da, left); JS_ToFloat64(ctx, &db, right); r = da + db; frame->slots[a] = !isfinite(r) ? JS_NULL : JS_NewFloat64(ctx, r); } VM_BREAK(); } VM_CASE(MACH_SUB): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { int64_t r = (int64_t)JS_VALUE_GET_INT(left) - (int64_t)JS_VALUE_GET_INT(right); frame->slots[a] = (r >= INT32_MIN && r <= INT32_MAX) ? JS_NewInt32(ctx, (int32_t)r) : JS_NewFloat64(ctx, (double)r); } else { double da, db, r; JS_ToFloat64(ctx, &da, left); JS_ToFloat64(ctx, &db, right); r = da - db; frame->slots[a] = !isfinite(r) ? JS_NULL : JS_NewFloat64(ctx, r); } VM_BREAK(); } VM_CASE(MACH_MUL): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { int64_t r = (int64_t)JS_VALUE_GET_INT(left) * (int64_t)JS_VALUE_GET_INT(right); frame->slots[a] = (r >= INT32_MIN && r <= INT32_MAX) ? JS_NewInt32(ctx, (int32_t)r) : JS_NewFloat64(ctx, (double)r); } else { double da, db, r; JS_ToFloat64(ctx, &da, left); JS_ToFloat64(ctx, &db, right); r = da * db; frame->slots[a] = !isfinite(r) ? JS_NULL : JS_NewFloat64(ctx, r); } VM_BREAK(); } VM_CASE(MACH_DIV): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { int32_t ia = JS_VALUE_GET_INT(left), ib = JS_VALUE_GET_INT(right); if (ib != 0 && ia % ib == 0) frame->slots[a] = JS_NewInt32(ctx, ia / ib); else if (ib != 0) frame->slots[a] = JS_NewFloat64(ctx, (double)ia / (double)ib); else frame->slots[a] = JS_NULL; } else { double da, db, r; JS_ToFloat64(ctx, &da, left); JS_ToFloat64(ctx, &db, right); if (db == 0.0) { frame->slots[a] = JS_NULL; } else { r = da / db; frame->slots[a] = !isfinite(r) ? JS_NULL : JS_NewFloat64(ctx, r); } } VM_BREAK(); } VM_CASE(MACH_MOD): { JSValue left = frame->slots[b], right = frame->slots[c]; double da, db; if (mach_get_number(left, &da) != 0 || mach_get_number(right, &db) != 0 || db == 0.0 || isnan(da) || isnan(db)) { frame->slots[a] = JS_NULL; } else { if (da == 0.0) { frame->slots[a] = JS_NewFloat64(ctx, 0.0); } else { frame->slots[a] = JS_NewFloat64(ctx, da - (db * floor(da / db))); } } VM_BREAK(); } VM_CASE(MACH_POW): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { double r = pow((double)JS_VALUE_GET_INT(left), (double)JS_VALUE_GET_INT(right)); if (!isfinite(r)) frame->slots[a] = JS_NULL; else if (r >= INT32_MIN && r <= INT32_MAX && r == (int32_t)r) frame->slots[a] = JS_NewInt32(ctx, (int32_t)r); else frame->slots[a] = JS_NewFloat64(ctx, r); } else { double da, db, r; JS_ToFloat64(ctx, &da, left); JS_ToFloat64(ctx, &db, right); r = pow(da, db); frame->slots[a] = (!isfinite(r) && isfinite(da) && isfinite(db)) ? JS_NULL : JS_NewFloat64(ctx, r); } VM_BREAK(); } VM_CASE(MACH_REMAINDER): { JSValue left = frame->slots[b], right = frame->slots[c]; double da, db; if (mach_get_number(left, &da) != 0 || mach_get_number(right, &db) != 0 || db == 0.0) { frame->slots[a] = JS_NULL; } else { frame->slots[a] = JS_NewFloat64(ctx, da - (trunc(da / db) * db)); } VM_BREAK(); } VM_CASE(MACH_MAX): { JSValue left = frame->slots[b], right = frame->slots[c]; double da, db; if (mach_get_number(left, &da) != 0 || mach_get_number(right, &db) != 0) { frame->slots[a] = JS_NULL; } else { frame->slots[a] = JS_NewFloat64(ctx, da > db ? da : db); } VM_BREAK(); } VM_CASE(MACH_MIN): { JSValue left = frame->slots[b], right = frame->slots[c]; double da, db; if (mach_get_number(left, &da) != 0 || mach_get_number(right, &db) != 0) { frame->slots[a] = JS_NULL; } else { frame->slots[a] = JS_NewFloat64(ctx, da < db ? da : db); } VM_BREAK(); } VM_CASE(MACH_ABS): { JSValue v = frame->slots[b]; double d; if (mach_get_number(v, &d) != 0) { frame->slots[a] = JS_NULL; } else { frame->slots[a] = JS_NewFloat64(ctx, fabs(d)); } VM_BREAK(); } VM_CASE(MACH_SIGN): { JSValue v = frame->slots[b]; double d; if (mach_get_number(v, &d) != 0) { frame->slots[a] = JS_NULL; } else if (d < 0) { frame->slots[a] = JS_NewInt32(ctx, -1); } else if (d > 0) { frame->slots[a] = JS_NewInt32(ctx, 1); } else { frame->slots[a] = JS_NewInt32(ctx, 0); } VM_BREAK(); } VM_CASE(MACH_FRACTION): { JSValue v = frame->slots[b]; double d; if (mach_get_number(v, &d) != 0) { frame->slots[a] = JS_NULL; } else { frame->slots[a] = JS_NewFloat64(ctx, d - trunc(d)); } VM_BREAK(); } VM_CASE(MACH_INTEGER): { JSValue v = frame->slots[b]; double d; if (mach_get_number(v, &d) != 0) { frame->slots[a] = JS_NULL; } else { frame->slots[a] = JS_NewFloat64(ctx, trunc(d)); } VM_BREAK(); } VM_CASE(MACH_FLOOR): VM_CASE(MACH_CEILING): VM_CASE(MACH_ROUND): VM_CASE(MACH_TRUNC): { JSValue v = frame->slots[b]; JSValue pval = frame->slots[c]; double d, r; int32_t place = 0; if (mach_get_number(v, &d) != 0) { frame->slots[a] = JS_NULL; VM_BREAK(); } if (!JS_IsNull(pval) && mach_get_place(ctx, pval, &place) != 0) { frame->slots[a] = JS_NULL; VM_BREAK(); } if (op == MACH_FLOOR) { r = mach_apply_place(d, place, floor); } else if (op == MACH_CEILING) { r = mach_apply_place(d, place, ceil); } else if (op == MACH_ROUND) { r = mach_apply_place(d, place, round); } else { r = mach_apply_place(d, place, trunc); } frame->slots[a] = JS_NewFloat64(ctx, r); VM_BREAK(); } /* Comparison — inline integer fast paths */ VM_CASE(MACH_EQ): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(left) == JS_VALUE_GET_INT(right)); } else { JSValue res = reg_vm_binop(ctx, MACH_EQ, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_NEQ): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(left) != JS_VALUE_GET_INT(right)); } else { JSValue res = reg_vm_binop(ctx, MACH_NEQ, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_LT): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(left) < JS_VALUE_GET_INT(right)); } else { JSValue res = reg_vm_binop(ctx, MACH_LT, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_LE): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(left) <= JS_VALUE_GET_INT(right)); } else { JSValue res = reg_vm_binop(ctx, MACH_LE, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_GT): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(left) > JS_VALUE_GET_INT(right)); } else { JSValue res = reg_vm_binop(ctx, MACH_GT, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_GE): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(left) >= JS_VALUE_GET_INT(right)); } else { JSValue res = reg_vm_binop(ctx, MACH_GE, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } /* Bitwise — inline integer fast paths */ VM_CASE(MACH_BAND): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { frame->slots[a] = JS_NewInt32(ctx, JS_VALUE_GET_INT(left) & JS_VALUE_GET_INT(right)); } else { JSValue res = reg_vm_binop(ctx, MACH_BAND, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_BOR): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { frame->slots[a] = JS_NewInt32(ctx, JS_VALUE_GET_INT(left) | JS_VALUE_GET_INT(right)); } else { JSValue res = reg_vm_binop(ctx, MACH_BOR, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_BXOR): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { frame->slots[a] = JS_NewInt32(ctx, JS_VALUE_GET_INT(left) ^ JS_VALUE_GET_INT(right)); } else { JSValue res = reg_vm_binop(ctx, MACH_BXOR, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_SHL): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { frame->slots[a] = JS_NewInt32(ctx, JS_VALUE_GET_INT(left) << (JS_VALUE_GET_INT(right) & 31)); } else { JSValue res = reg_vm_binop(ctx, MACH_SHL, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_SHR): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { frame->slots[a] = JS_NewInt32(ctx, JS_VALUE_GET_INT(left) >> (JS_VALUE_GET_INT(right) & 31)); } else { JSValue res = reg_vm_binop(ctx, MACH_SHR, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_USHR): { JSValue left = frame->slots[b], right = frame->slots[c]; if (JS_VALUE_IS_BOTH_INT(left, right)) { frame->slots[a] = JS_NewInt32(ctx, (uint32_t)JS_VALUE_GET_INT(left) >> (JS_VALUE_GET_INT(right) & 31)); } else { JSValue res = reg_vm_binop(ctx, MACH_USHR, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_EQ_TOL): VM_CASE(MACH_NEQ_TOL): { /* A=dest, B=base, C=3; args in R(B), R(B+1), R(B+2) */ JSValue left = frame->slots[b]; JSValue right = frame->slots[b + 1]; JSValue tol = frame->slots[b + 2]; BOOL is_eq_op = (op == MACH_EQ_TOL); if (JS_IsNumber(left) && JS_IsNumber(right) && JS_IsNumber(tol)) { double da, db, dt; JS_ToFloat64(ctx, &da, left); JS_ToFloat64(ctx, &db, right); JS_ToFloat64(ctx, &dt, tol); BOOL eq = fabs(da - db) <= dt; frame->slots[a] = JS_NewBool(ctx, is_eq_op ? eq : !eq); } else if (mist_is_text(left) && mist_is_text(right) && JS_VALUE_GET_TAG(tol) == JS_TAG_BOOL && JS_VALUE_GET_BOOL(tol)) { BOOL eq = js_string_compare_value_nocase(ctx, left, right) == 0; frame->slots[a] = JS_NewBool(ctx, is_eq_op ? eq : !eq); } else { /* Fall through to standard eq/neq */ JSValue res = reg_vm_binop(ctx, is_eq_op ? MACH_EQ : MACH_NEQ, left, right); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) { goto disrupt; } frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_NEG): { JSValue v = frame->slots[b]; if (JS_IsInt(v)) { int32_t i = JS_VALUE_GET_INT(v); if (i == INT32_MIN) frame->slots[a] = JS_NewFloat64(ctx, -(double)i); else frame->slots[a] = JS_NewInt32(ctx, -i); } else { double d; JS_ToFloat64(ctx, &d, v); frame->slots[a] = JS_NewFloat64(ctx, -d); } VM_BREAK(); } VM_CASE(MACH_LNOT): { int bval = JS_ToBool(ctx, frame->slots[b]); frame->slots[a] = JS_NewBool(ctx, !bval); VM_BREAK(); } VM_CASE(MACH_BNOT): { int32_t i; JS_ToInt32(ctx, &i, frame->slots[b]); frame->slots[a] = JS_NewInt32(ctx, ~i); VM_BREAK(); } VM_CASE(MACH_GETFIELD): { JSValue obj = frame->slots[b]; JSValue key = code->cpool[c]; /* Non-proxy functions (arity != 2) can't have properties read */ if (mist_is_function(obj)) { JSFunction *fn_chk = JS_VALUE_GET_FUNCTION(obj); if (fn_chk->length != 2) { JS_RaiseDisrupt(ctx, "cannot read property of non-proxy function"); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); goto disrupt; } } JSValue val = JS_GetProperty(ctx, obj, key); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(val)) goto disrupt; frame->slots[a] = val; VM_BREAK(); } VM_CASE(MACH_SETFIELD): { /* R(A)[K(B)] = R(C) */ JSValue obj = frame->slots[a]; JSValue key = code->cpool[b]; JSValue val = frame->slots[c]; int ret = JS_SetProperty(ctx, obj, key, val); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (ret < 0) goto disrupt; mach_resolve_forward(&frame->slots[a]); VM_BREAK(); } VM_CASE(MACH_GETINDEX): { /* R(A) = R(B)[R(C)] — mcode guarantees R(C) is int */ JSValue obj = frame->slots[b]; JSValue val = JS_GetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(frame->slots[c])); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(val)) goto disrupt; frame->slots[a] = val; VM_BREAK(); } VM_CASE(MACH_SETINDEX): { /* R(A)[R(B)] = R(C) — mcode guarantees R(B) is int */ JSValue obj = frame->slots[a]; JSValue val = frame->slots[c]; JSValue r = JS_SetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(frame->slots[b]), val); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(r)) goto disrupt; mach_resolve_forward(&frame->slots[a]); VM_BREAK(); } VM_CASE(MACH_GETINTRINSIC): { int bx = MACH_GET_Bx(instr); JSValue key = code->cpool[bx]; JSValue val = JS_GetProperty(ctx, ctx->global_obj, key); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsNull(val)) { int has = JS_HasProperty(ctx, ctx->global_obj, key); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (has <= 0) { char buf[128]; JS_KeyGetStr(ctx, buf, sizeof(buf), key); JS_RaiseDisrupt(ctx, "'%s' is not defined", buf); goto disrupt; } } frame->slots[a] = val; VM_BREAK(); } VM_CASE(MACH_GETENV): { /* Read env fresh from frame->function — C local env can go stale after GC */ int bx = MACH_GET_Bx(instr); JSValue key = code->cpool[bx]; JSValue cur_env = JS_VALUE_GET_FUNCTION(frame->function)->u.cell.env_record; JSValue val = JS_GetProperty(ctx, cur_env, key); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); frame->slots[a] = val; VM_BREAK(); } VM_CASE(MACH_GETNAME): { /* Runtime fallback: try env then global (should not appear in linked code) */ int bx = MACH_GET_Bx(instr); JSValue key = code->cpool[bx]; JSValue val = JS_NULL; JSValue cur_env = JS_VALUE_GET_FUNCTION(frame->function)->u.cell.env_record; if (!JS_IsNull(cur_env)) { val = JS_GetProperty(ctx, cur_env, key); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); } if (JS_IsNull(val) || JS_IsException(val)) { val = JS_GetProperty(ctx, ctx->global_obj, key); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); } frame->slots[a] = val; VM_BREAK(); } VM_CASE(MACH_GETUP): { /* R(A) = outer_frame[B].slots[C] — walk lexical scope chain */ int depth = b; JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function); JSFrameRegister *target = (JSFrameRegister *)JS_VALUE_GET_PTR(fn->u.cell.outer_frame); assert(depth > 0); assert(target != NULL); for (int d = 1; d < depth; d++) { fn = JS_VALUE_GET_FUNCTION(target->function); JSFrameRegister *next = (JSFrameRegister *)JS_VALUE_GET_PTR(fn->u.cell.outer_frame); assert(next != NULL); target = next; } stone_mutable_text(target->slots[c]); frame->slots[a] = target->slots[c]; VM_BREAK(); } VM_CASE(MACH_SETUP): { /* outer_frame[B].slots[C] = R(A) — walk lexical scope chain */ int depth = b; JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function); JSFrameRegister *target = (JSFrameRegister *)JS_VALUE_GET_PTR(fn->u.cell.outer_frame); assert(depth > 0); assert(target != NULL); for (int d = 1; d < depth; d++) { fn = JS_VALUE_GET_FUNCTION(target->function); target = (JSFrameRegister *)JS_VALUE_GET_PTR(fn->u.cell.outer_frame); assert(target != NULL); } assert((unsigned)c < objhdr_cap56(target->header)); target->slots[c] = frame->slots[a]; VM_BREAK(); } VM_CASE(MACH_JMP): { int offset = MACH_GET_sJ(instr); pc = (uint32_t)((int32_t)pc + offset); if (offset < 0) { int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed); if (pf == 2) { result = JS_RaiseDisrupt(ctx, "interrupted"); goto done; } if (pf == 1) { if (ctx->vm_call_depth > 0) atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed); else goto suspend; } } VM_BREAK(); } VM_CASE(MACH_JMPTRUE): { if (frame->slots[a] == JS_TRUE) { int offset = MACH_GET_sBx(instr); pc = (uint32_t)((int32_t)pc + offset); if (offset < 0) { int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed); if (pf == 2) { result = JS_RaiseDisrupt(ctx, "interrupted"); goto done; } if (pf == 1) { if (ctx->vm_call_depth > 0) atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed); else goto suspend; } } } VM_BREAK(); } VM_CASE(MACH_JMPFALSE): { if (frame->slots[a] == JS_FALSE) { int offset = MACH_GET_sBx(instr); pc = (uint32_t)((int32_t)pc + offset); if (offset < 0) { int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed); if (pf == 2) { result = JS_RaiseDisrupt(ctx, "interrupted"); goto done; } if (pf == 1) { if (ctx->vm_call_depth > 0) atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed); else goto suspend; } } } VM_BREAK(); } VM_CASE(MACH_JMPNULL): { if (JS_IsNull(frame->slots[a])) { int offset = MACH_GET_sBx(instr); pc = (uint32_t)((int32_t)pc + offset); } VM_BREAK(); } VM_CASE(MACH_RETURN): stone_mutable_text(frame->slots[a]); result = frame->slots[a]; if (!JS_IsPtr(frame->caller)) goto done; if (unlikely(ctx->trace_hook) && (ctx->trace_type & JS_HOOK_RET)) ctx->trace_hook(ctx, JS_HOOK_RET, NULL, ctx->trace_data); { #ifdef VALIDATE_GC const char *callee_name = "?"; const char *callee_file = "?"; { JSFunction *callee_fn = JS_VALUE_GET_FUNCTION(frame->function); if (callee_fn->kind == JS_FUNC_KIND_REGISTER && JS_VALUE_GET_CODE(FN_READ_CODE(callee_fn))->u.reg.code) { if (JS_VALUE_GET_CODE(FN_READ_CODE(callee_fn))->u.reg.code->name_cstr) callee_name = JS_VALUE_GET_CODE(FN_READ_CODE(callee_fn))->u.reg.code->name_cstr; if (JS_VALUE_GET_CODE(FN_READ_CODE(callee_fn))->u.reg.code->filename_cstr) callee_file = JS_VALUE_GET_CODE(FN_READ_CODE(callee_fn))->u.reg.code->filename_cstr; } } #endif JSFrameRegister *caller = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller); frame->caller = JS_NULL; frame = caller; frame_ref.val = JS_MKPTR(frame); int ret_info = JS_VALUE_GET_INT(frame->address); JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function); code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code; env = fn->u.cell.env_record; pc = ret_info >> 16; int ret_slot = ret_info & 0xFFFF; if (ret_slot != 0xFFFF) { #ifdef VALIDATE_GC if (JS_IsPtr(result)) { void *rp = JS_VALUE_GET_PTR(result); if ((uint8_t *)rp < ctx->heap_base || (uint8_t *)rp >= ctx->heap_free) { if (!is_ct_ptr(ctx, rp)) fprintf(stderr, "VALIDATE_GC: stale RETURN into slot %d, ptr=%p heap=[%p,%p) fn_slots=%d pc=%u callee=%s (%s) caller=%s (%s)\n", ret_slot, rp, (void*)ctx->heap_base, (void*)ctx->heap_free, code->nr_slots, pc, callee_name, callee_file, code->name_cstr ? code->name_cstr : "?", code->filename_cstr ? code->filename_cstr : "?"); } } #endif frame->slots[ret_slot] = result; } } VM_BREAK(); VM_CASE(MACH_RETNIL): result = JS_NULL; if (!JS_IsPtr(frame->caller)) goto done; if (unlikely(ctx->trace_hook) && (ctx->trace_type & JS_HOOK_RET)) ctx->trace_hook(ctx, JS_HOOK_RET, NULL, ctx->trace_data); { JSFrameRegister *caller = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller); frame->caller = JS_NULL; frame = caller; frame_ref.val = JS_MKPTR(frame); int ret_info = JS_VALUE_GET_INT(frame->address); JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function); code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code; env = fn->u.cell.env_record; pc = ret_info >> 16; int ret_slot = ret_info & 0xFFFF; if (ret_slot != 0xFFFF) { frame->slots[ret_slot] = result; } } VM_BREAK(); VM_CASE(MACH_NEWOBJECT): { JSValue obj = JS_NewObject(ctx); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(obj)) { goto disrupt; } frame->slots[a] = obj; VM_BREAK(); } VM_CASE(MACH_NEWARRAY): { JSValue arr = JS_NewArrayCap(ctx, b); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(arr)) { goto disrupt; } frame->slots[a] = arr; VM_BREAK(); } VM_CASE(MACH_CLOSURE): { int bx = MACH_GET_Bx(instr); if ((uint32_t)bx < code->func_count) { JSCodeRegister *fn_code = code->functions[bx]; /* Read env fresh from frame->function — C local can be stale */ JSValue cur_env = JS_VALUE_GET_FUNCTION(frame->function)->u.cell.env_record; JSValue fn_val = js_new_register_function(ctx, fn_code, cur_env, frame_ref.val); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); frame->slots[a] = fn_val; } else { frame->slots[a] = JS_NULL; } VM_BREAK(); } VM_CASE(MACH_PUSH): { /* push R(B) onto array R(A) — mcode guarantees R(A) is array */ JSValue arr = frame->slots[a]; JSValue val = frame->slots[b]; JSGCRef arr_gc; JS_PushGCRef(ctx, &arr_gc); arr_gc.val = arr; int rc = JS_ArrayPush(ctx, &arr_gc.val, val); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); JS_PopGCRef(ctx, &arr_gc); if (rc < 0) goto disrupt; if (arr_gc.val != arr) frame->slots[a] = arr_gc.val; VM_BREAK(); } VM_CASE(MACH_POP): { /* R(A) = pop last element from array R(B) — mcode guarantees R(B) is array */ JSValue arr = frame->slots[b]; JSValue val = JS_ArrayPop(ctx, arr); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(val)) goto disrupt; frame->slots[a] = val; VM_BREAK(); } VM_CASE(MACH_DELETE): { JSValue obj = frame->slots[b]; JSValue key = code->cpool[c]; int ret = JS_DeleteProperty(ctx, obj, key); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (ret < 0) goto disrupt; frame->slots[a] = JS_NewBool(ctx, ret >= 0); VM_BREAK(); } VM_CASE(MACH_DELETEINDEX): { JSValue obj = frame->slots[b]; JSValue key = frame->slots[c]; int ret = JS_DeleteProperty(ctx, obj, key); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (ret < 0) goto disrupt; frame->slots[a] = JS_NewBool(ctx, ret >= 0); VM_BREAK(); } VM_CASE(MACH_HASPROP): { JSValue obj = frame->slots[b]; JSValue key = frame->slots[c]; int has = JS_HasProperty(ctx, obj, key); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); frame->slots[a] = JS_NewBool(ctx, has > 0); VM_BREAK(); } VM_CASE(MACH_REGEXP): { JSValue argv[2]; argv[0] = code->cpool[b]; /* pattern */ argv[1] = code->cpool[c]; /* flags */ JSValue re = js_regexp_constructor(ctx, JS_NULL, 2, argv); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(re)) goto disrupt; frame->slots[a] = re; VM_BREAK(); } VM_CASE(MACH_THROW): goto disrupt; /* === New mcode-derived opcodes === */ /* Text concatenation — with in-place append fast path for s = s + x */ VM_CASE(MACH_CONCAT): { if (a == b) { /* Self-assign pattern: slot[a] = slot[a] + slot[c] */ JSValue left = frame->slots[a]; JSValue right = frame->slots[c]; /* Inline fast path: mutable heap text with enough capacity */ if (JS_IsPtr(left)) { JSText *s = (JSText *)chase(left); int slen = (int)s->length; int rlen = js_string_value_len(right); int cap = (int)objhdr_cap56(s->hdr); if (objhdr_type(s->hdr) == OBJ_TEXT && !(s->hdr & OBJHDR_S_MASK) && slen + rlen <= cap) { /* Append in-place — zero allocation, no GC possible */ for (int i = 0; i < rlen; i++) string_put(s, slen + i, js_string_value_get(right, i)); s->length = slen + rlen; VM_BREAK(); } } /* Slow path: allocate with growth factor, leave unstoned */ JSValue res = JS_ConcatStringGrow(ctx, frame->slots[b], frame->slots[c]); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } else { /* Different target: use existing exact-fit stoned path */ JSValue res = JS_ConcatString(ctx, frame->slots[b], frame->slots[c]); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(res)) goto disrupt; frame->slots[a] = res; } VM_BREAK(); } /* Stone mutable text — compiler-emitted at escape points */ VM_CASE(MACH_STONE_TEXT): stone_mutable_text(frame->slots[a]); VM_BREAK(); /* Identity check */ VM_CASE(MACH_IS_IDENTICAL): { JSValue va = JS_IsPtr(frame->slots[b]) ? JS_MKPTR(chase(frame->slots[b])) : frame->slots[b]; JSValue vb = JS_IsPtr(frame->slots[c]) ? JS_MKPTR(chase(frame->slots[c])) : frame->slots[c]; frame->slots[a] = JS_NewBool(ctx, va == vb); VM_BREAK(); } /* Type checks */ VM_CASE(MACH_IS_INT): frame->slots[a] = JS_NewBool(ctx, JS_IsInt(frame->slots[b])); VM_BREAK(); VM_CASE(MACH_IS_NUM): frame->slots[a] = JS_NewBool(ctx, JS_IsNumber(frame->slots[b])); VM_BREAK(); VM_CASE(MACH_IS_TEXT): frame->slots[a] = JS_NewBool(ctx, mist_is_text(frame->slots[b])); VM_BREAK(); VM_CASE(MACH_IS_BOOL): frame->slots[a] = JS_NewBool(ctx, JS_IsBool(frame->slots[b])); VM_BREAK(); VM_CASE(MACH_IS_NULL): frame->slots[a] = JS_NewBool(ctx, JS_IsNull(frame->slots[b])); VM_BREAK(); VM_CASE(MACH_IS_ARRAY): frame->slots[a] = JS_NewBool(ctx, mist_is_array(frame->slots[b])); VM_BREAK(); VM_CASE(MACH_IS_FUNC): frame->slots[a] = JS_NewBool(ctx, mist_is_function(frame->slots[b])); VM_BREAK(); VM_CASE(MACH_IS_RECORD): frame->slots[a] = JS_NewBool(ctx, mist_is_record(frame->slots[b])); VM_BREAK(); VM_CASE(MACH_IS_STONE): frame->slots[a] = JS_NewBool(ctx, mist_is_stone(frame->slots[b])); VM_BREAK(); VM_CASE(MACH_LENGTH): { JSValue v = frame->slots[b]; if (mist_is_array(v)) { JSArray *arr = JS_VALUE_GET_ARRAY(v); frame->slots[a] = JS_NewInt32(ctx, (int32_t)arr->len); } else if (MIST_IsImmediateASCII(v)) { frame->slots[a] = JS_NewInt32(ctx, MIST_GetImmediateASCIILen(v)); } else { /* fallback to C for text/blob/function (still a GC safepoint) */ JSValue res = JS_CellLength(ctx, v); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); frame->slots[a] = res; } VM_BREAK(); } VM_CASE(MACH_IS_PROXY): { JSValue v = frame->slots[b]; int is_proxy = 0; if (mist_is_function(v)) { JSFunction *fn = JS_VALUE_GET_FUNCTION(v); is_proxy = (fn->length == 2); } frame->slots[a] = JS_NewBool(ctx, is_proxy); VM_BREAK(); } VM_CASE(MACH_IS_BLOB): frame->slots[a] = JS_NewBool(ctx, mist_is_blob(frame->slots[b])); VM_BREAK(); VM_CASE(MACH_IS_DATA): { JSValue v = frame->slots[b]; int result = 0; if (mist_is_gc_object(v) && !mist_is_array(v) && !mist_is_function(v) && !mist_is_blob(v)) result = 1; frame->slots[a] = JS_NewBool(ctx, result); VM_BREAK(); } VM_CASE(MACH_IS_TRUE): frame->slots[a] = JS_NewBool(ctx, frame->slots[b] == JS_TRUE); VM_BREAK(); VM_CASE(MACH_IS_FALSE): frame->slots[a] = JS_NewBool(ctx, frame->slots[b] == JS_FALSE); VM_BREAK(); VM_CASE(MACH_IS_FIT): { JSValue v = frame->slots[b]; int result = 0; if (JS_IsInt(v)) { result = 1; } else if (JS_IsShortFloat(v)) { double d = JS_VALUE_GET_FLOAT64(v); result = (isfinite(d) && trunc(d) == d && fabs(d) <= 9007199254740992.0); } frame->slots[a] = JS_NewBool(ctx, result); VM_BREAK(); } VM_CASE(MACH_IS_CHAR): { JSValue v = frame->slots[b]; int result = 0; if (MIST_IsImmediateASCII(v)) result = (MIST_GetImmediateASCIILen(v) == 1); else if (mist_is_text(v)) result = (js_string_value_len(v) == 1); frame->slots[a] = JS_NewBool(ctx, result); VM_BREAK(); } VM_CASE(MACH_IS_DIGIT): { JSValue v = frame->slots[b]; int result = 0; if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) { int ch = MIST_GetImmediateASCIIChar(v, 0); result = (ch >= '0' && ch <= '9'); } else if (mist_is_text(v) && js_string_value_len(v) == 1) { uint32_t ch = js_string_value_get(v, 0); result = (ch >= '0' && ch <= '9'); } frame->slots[a] = JS_NewBool(ctx, result); VM_BREAK(); } VM_CASE(MACH_IS_LETTER): { JSValue v = frame->slots[b]; int result = 0; if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) { int ch = MIST_GetImmediateASCIIChar(v, 0); result = ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')); } else if (mist_is_text(v) && js_string_value_len(v) == 1) { uint32_t ch = js_string_value_get(v, 0); result = ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')); } frame->slots[a] = JS_NewBool(ctx, result); VM_BREAK(); } VM_CASE(MACH_IS_LOWER): { JSValue v = frame->slots[b]; int result = 0; if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) { int ch = MIST_GetImmediateASCIIChar(v, 0); result = (ch >= 'a' && ch <= 'z'); } else if (mist_is_text(v) && js_string_value_len(v) == 1) { uint32_t ch = js_string_value_get(v, 0); result = (ch >= 'a' && ch <= 'z'); } frame->slots[a] = JS_NewBool(ctx, result); VM_BREAK(); } VM_CASE(MACH_IS_UPPER): { JSValue v = frame->slots[b]; int result = 0; if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) { int ch = MIST_GetImmediateASCIIChar(v, 0); result = (ch >= 'A' && ch <= 'Z'); } else if (mist_is_text(v) && js_string_value_len(v) == 1) { uint32_t ch = js_string_value_get(v, 0); result = (ch >= 'A' && ch <= 'Z'); } frame->slots[a] = JS_NewBool(ctx, result); VM_BREAK(); } VM_CASE(MACH_IS_WS): { JSValue v = frame->slots[b]; int result = 0; if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) { int ch = MIST_GetImmediateASCIIChar(v, 0); result = (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\f' || ch == '\v'); } else if (mist_is_text(v) && js_string_value_len(v) == 1) { uint32_t ch = js_string_value_get(v, 0); result = (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\f' || ch == '\v'); } frame->slots[a] = JS_NewBool(ctx, result); VM_BREAK(); } VM_CASE(MACH_IS_ACTOR): { JSValue v = frame->slots[b]; int result = 0; if (mist_is_record(v) && !JS_IsNull(ctx->actor_sym)) { result = JS_HasPropertyKey(ctx, v, ctx->actor_sym) > 0; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); } frame->slots[a] = JS_NewBool(ctx, result); VM_BREAK(); } VM_CASE(MACH_APPLY): { /* A=dest, B=fn, C=arr_or_val */ JSValue fn_val = frame->slots[b]; JSValue arg_val = frame->slots[c]; if (!mist_is_function(fn_val)) { frame->slots[a] = fn_val; VM_BREAK(); } JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val); JSValue ret; ctx->reg_current_frame = frame_ref.val; ctx->current_register_pc = pc > 0 ? pc - 1 : 0; ctx->vm_call_depth++; if (!mist_is_array(arg_val)) { /* Non-array: use as single argument */ if (!mach_check_call_arity(ctx, fn, 1)) { ctx->vm_call_depth--; goto disrupt; } ret = JS_CallInternal(ctx, fn_val, JS_NULL, 1, &arg_val, 0); } else { JSArray *arr = JS_VALUE_GET_ARRAY(arg_val); int len = arr->len; if (!mach_check_call_arity(ctx, fn, len)) { ctx->vm_call_depth--; goto disrupt; } if (len == 0) { ret = JS_CallInternal(ctx, fn_val, JS_NULL, 0, NULL, 0); } else { JSValue *args = alloca(sizeof(JSValue) * len); for (int i = 0; i < len; i++) args[i] = arr->values[i]; ret = JS_CallInternal(ctx, fn_val, JS_NULL, len, args, 0); } } ctx->vm_call_depth--; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); ctx->reg_current_frame = JS_NULL; if (JS_IsException(ret)) goto disrupt; frame->slots[a] = ret; VM_BREAK(); } /* Logical */ VM_CASE(MACH_NOT): { int bval = JS_ToBool(ctx, frame->slots[b]); frame->slots[a] = JS_NewBool(ctx, !bval); VM_BREAK(); } VM_CASE(MACH_AND): { JSValue left = frame->slots[b]; if (!JS_ToBool(ctx, left)) frame->slots[a] = left; else frame->slots[a] = frame->slots[c]; VM_BREAK(); } VM_CASE(MACH_OR): { JSValue left = frame->slots[b]; if (JS_ToBool(ctx, left)) frame->slots[a] = left; else frame->slots[a] = frame->slots[c]; VM_BREAK(); } /* Bitwise (mcode names — delegate to same code as legacy) */ VM_CASE(MACH_BITNOT): { int32_t i; JS_ToInt32(ctx, &i, frame->slots[b]); frame->slots[a] = JS_NewInt32(ctx, ~i); VM_BREAK(); } VM_CASE(MACH_BITAND): VM_CASE(MACH_BITOR): VM_CASE(MACH_BITXOR): { int32_t ia, ib; JS_ToInt32(ctx, &ia, frame->slots[b]); JS_ToInt32(ctx, &ib, frame->slots[c]); int32_t r; if (op == MACH_BITAND) r = ia & ib; else if (op == MACH_BITOR) r = ia | ib; else r = ia ^ ib; frame->slots[a] = JS_NewInt32(ctx, r); VM_BREAK(); } /* Property access (mcode names) */ VM_CASE(MACH_LOAD_FIELD): { JSValue obj = frame->slots[b]; JSValue key = code->cpool[c]; if (mist_is_function(obj)) { JS_RaiseDisrupt(ctx, "cannot read property of function"); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); goto disrupt; } JSValue val = JS_GetProperty(ctx, obj, key); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(val)) goto disrupt; frame->slots[a] = val; VM_BREAK(); } VM_CASE(MACH_STORE_FIELD): { JSValue obj = frame->slots[a]; JSValue key = code->cpool[b]; JSValue val = frame->slots[c]; int ret = JS_SetProperty(ctx, obj, key, val); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (ret < 0) goto disrupt; mach_resolve_forward(&frame->slots[a]); VM_BREAK(); } VM_CASE(MACH_LOAD_INDEX): { /* R(A) = R(B)[R(C)] — mcode guarantees R(C) is int */ JSValue obj = frame->slots[b]; JSValue val = JS_GetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(frame->slots[c])); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(val)) goto disrupt; frame->slots[a] = val; VM_BREAK(); } VM_CASE(MACH_STORE_INDEX): { /* R(A)[R(B)] = R(C) — mcode guarantees R(B) is int */ JSValue obj = frame->slots[a]; JSValue val = frame->slots[c]; JSValue r = JS_SetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(frame->slots[b]), val); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(r)) goto disrupt; mach_resolve_forward(&frame->slots[a]); VM_BREAK(); } VM_CASE(MACH_LOAD_DYNAMIC): { JSValue obj = frame->slots[b]; JSValue key = frame->slots[c]; JSValue val; if (JS_IsInt(key)) val = JS_GetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(key)); else val = JS_GetProperty(ctx, obj, key); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(val)) goto disrupt; frame->slots[a] = val; VM_BREAK(); } VM_CASE(MACH_STORE_DYNAMIC): { JSValue obj = frame->slots[a]; JSValue key = frame->slots[b]; JSValue val = frame->slots[c]; int ret; if (JS_IsInt(key)) { JSValue r = JS_SetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(key), val); ret = JS_IsException(r) ? -1 : 0; } else if (mist_is_array(obj)) { JS_RaiseDisrupt(ctx, "array index must be a number"); ret = -1; } else if (JS_IsBool(key) || JS_IsNull(key) || mist_is_array(key) || mist_is_function(key)) { JS_RaiseDisrupt(ctx, "object key must be text"); ret = -1; } else { ret = JS_SetProperty(ctx, obj, key, val); } frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (ret < 0) goto disrupt; mach_resolve_forward(&frame->slots[a]); VM_BREAK(); } /* New record */ VM_CASE(MACH_NEWRECORD): { JSValue obj = b > 0 ? JS_NewObjectCap(ctx, b) : JS_NewObject(ctx); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); if (JS_IsException(obj)) goto disrupt; frame->slots[a] = obj; VM_BREAK(); } /* Decomposed function calls (inlined from qbe_helpers) */ VM_CASE(MACH_FRAME): VM_CASE(MACH_GOFRAME): { /* A=frame_slot, B=func_reg, C=argc */ JSValue func_val = frame->slots[b]; if (!mist_is_function(func_val)) { char nbuf[KEY_GET_STR_BUF_SIZE]; const char *name = mach_callee_name(ctx, code, pc, b, nbuf, sizeof(nbuf)); if (name) JS_RaiseDisrupt(ctx, "%s is not a function", name); else JS_RaiseDisrupt(ctx, "not a function"); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); goto disrupt; } JSFunction *fn = JS_VALUE_GET_FUNCTION(func_val); int nr; if (fn->kind == JS_FUNC_KIND_REGISTER) { JSCodeRegister *fn_code = JS_VALUE_GET_CODE(fn->u.cell.code)->u.reg.code; nr = fn_code->nr_slots; if (nr < c + 2) nr = c + 2; /* safety: never smaller than argc+2 */ } else { nr = c + 2; } JSFrameRegister *call_frame = alloc_frame_register(ctx, nr); if (!call_frame) { frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); goto disrupt; } frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); func_val = frame->slots[b]; /* re-read after GC */ call_frame->function = func_val; call_frame->address = JS_NewInt32(ctx, c); /* store actual argc */ frame->slots[a] = JS_MKPTR(call_frame); VM_BREAK(); } VM_CASE(MACH_SETARG): { /* A=frame_slot, B=arg_idx, C=val_reg */ JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]); fr->slots[b] = frame->slots[c]; VM_BREAK(); } VM_CASE(MACH_INVOKE): { /* A=frame_slot, B=result_slot */ JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]); int c_argc = JS_VALUE_GET_INT(fr->address); /* actual argc stored by FRAME */ JSValue fn_val = fr->function; JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val); if (!mach_check_call_arity(ctx, fn, c_argc)) goto disrupt; if (fn->kind == JS_FUNC_KIND_REGISTER) { /* Register function: FRAME already allocated nr_slots — just switch */ JSCodeRegister *fn_code = JS_VALUE_GET_CODE(fn->u.cell.code)->u.reg.code; /* Fire trace hook for register-to-register call */ if (unlikely(ctx->trace_hook) && (ctx->trace_type & JS_HOOK_CALL)) { js_debug dbg = {0}; if (fn_code->name_cstr) snprintf(dbg.name, sizeof(dbg.name), "%s", fn_code->name_cstr); if (fn_code->filename_cstr) snprintf(dbg.filename, sizeof(dbg.filename), "%s", fn_code->filename_cstr); if (fn_code->line_table) dbg.line = fn_code->line_table[fn_code->entry_point].line; dbg.param_n = fn_code->arity; dbg.unique = (int)(uintptr_t)fn_code; ctx->trace_hook(ctx, JS_HOOK_CALL, &dbg, ctx->trace_data); } /* Save return info */ frame->address = JS_NewInt32(ctx, (pc << 16) | b); fr->caller = JS_MKPTR(frame); frame = fr; frame_ref.val = JS_MKPTR(frame); code = fn_code; env = fn->u.cell.env_record; pc = code->entry_point; } else { /* C, native, or bytecode function */ ctx->reg_current_frame = frame_ref.val; ctx->current_register_pc = pc > 0 ? pc - 1 : 0; ctx->vm_call_depth++; JSValue ret; if (fn->kind == JS_FUNC_KIND_C) ret = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); else if (fn->kind == JS_FUNC_KIND_NATIVE) ret = cell_native_dispatch(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); else ret = JS_CallInternal(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1], 0); ctx->vm_call_depth--; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); ctx->reg_current_frame = JS_NULL; if (JS_IsException(ret)) goto disrupt; #ifdef VALIDATE_GC if (JS_IsPtr(ret)) { void *rp = JS_VALUE_GET_PTR(ret); if ((uint8_t *)rp < ctx->heap_base || (uint8_t *)rp >= ctx->heap_free) { if (!is_ct_ptr(ctx, rp)) { int magic = (fn->kind == JS_FUNC_KIND_C) ? fn->u.cfunc.magic : -1; void *cfp = (fn->kind == JS_FUNC_KIND_C) ? (void *)fn->u.cfunc.c_function.generic : NULL; fprintf(stderr, "VALIDATE_GC: stale INVOKE result into slot %d, ptr=%p heap=[%p,%p) fn_slots=%d pc=%u kind=%d magic=%d cfunc=%p caller=%s (%s)\n", b, rp, (void*)ctx->heap_base, (void*)ctx->heap_free, code->nr_slots, pc - 1, fn->kind, magic, cfp, code->name_cstr ? code->name_cstr : "?", code->filename_cstr ? code->filename_cstr : "?"); } } } #endif frame->slots[b] = ret; } VM_BREAK(); } VM_CASE(MACH_GOINVOKE): { /* Tail call: replace current frame with callee */ JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]); int c_argc = JS_VALUE_GET_INT(fr->address); /* actual argc stored by FRAME */ JSValue fn_val = fr->function; JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val); if (!mach_check_call_arity(ctx, fn, c_argc)) goto disrupt; if (fn->kind == JS_FUNC_KIND_REGISTER) { JSCodeRegister *fn_code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code; /* Tail call: fire RET for current, CALL for new */ if (unlikely(ctx->trace_hook)) { if (ctx->trace_type & JS_HOOK_RET) ctx->trace_hook(ctx, JS_HOOK_RET, NULL, ctx->trace_data); if (ctx->trace_type & JS_HOOK_CALL) { js_debug dbg = {0}; if (fn_code->name_cstr) snprintf(dbg.name, sizeof(dbg.name), "%s", fn_code->name_cstr); if (fn_code->filename_cstr) snprintf(dbg.filename, sizeof(dbg.filename), "%s", fn_code->filename_cstr); if (fn_code->line_table) dbg.line = fn_code->line_table[fn_code->entry_point].line; dbg.param_n = fn_code->arity; dbg.unique = (int)(uintptr_t)fn_code; ctx->trace_hook(ctx, JS_HOOK_CALL, &dbg, ctx->trace_data); } } int current_slots = (int)objhdr_cap56(frame->header); if (fn_code->nr_slots <= current_slots) { /* FAST PATH: reuse current frame — no allocation */ int copy_count = (c_argc < fn_code->arity) ? c_argc : fn_code->arity; frame->slots[0] = fr->slots[0]; /* this */ for (int i = 0; i < copy_count; i++) frame->slots[1 + i] = fr->slots[1 + i]; /* Null out remaining slots (locals/temps) */ for (int i = 1 + copy_count; i < current_slots; i++) frame->slots[i] = JS_NULL; frame->function = fn_val; /* caller stays the same — we're reusing this frame */ code = fn_code; env = fn->u.cell.env_record; pc = code->entry_point; } else { /* SLOW PATH: GOFRAME already allocated nr_slots — use fr directly */ fr->caller = frame->caller; frame->caller = JS_NULL; frame = fr; frame_ref.val = JS_MKPTR(frame); code = fn_code; env = fn->u.cell.env_record; pc = code->entry_point; } } else { /* C, native, or bytecode function: call it, then return result to our caller */ ctx->reg_current_frame = frame_ref.val; ctx->current_register_pc = pc > 0 ? pc - 1 : 0; ctx->vm_call_depth++; JSValue ret; if (fn->kind == JS_FUNC_KIND_C) ret = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); else if (fn->kind == JS_FUNC_KIND_NATIVE) ret = cell_native_dispatch(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); else ret = JS_CallInternal(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1], 0); ctx->vm_call_depth--; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); ctx->reg_current_frame = JS_NULL; if (JS_IsException(ret)) goto disrupt; /* Tail-return: act like MACH_RETURN with the result */ result = ret; if (!JS_IsPtr(frame->caller)) goto done; JSFrameRegister *caller = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller); frame->caller = JS_NULL; frame = caller; frame_ref.val = JS_MKPTR(frame); int ret_info = JS_VALUE_GET_INT(frame->address); JSFunction *ret_fn = JS_VALUE_GET_FUNCTION(frame->function); code = JS_VALUE_GET_CODE(FN_READ_CODE(ret_fn))->u.reg.code; env = ret_fn->u.cell.env_record; pc = ret_info >> 16; int ret_slot = ret_info & 0xFFFF; if (ret_slot != 0xFFFF) frame->slots[ret_slot] = ret; } VM_BREAK(); } /* Jump if not null */ VM_CASE(MACH_JMPNOTNULL): { if (!JS_IsNull(frame->slots[a])) { int offset = MACH_GET_sBx(instr); pc = (uint32_t)((int32_t)pc + offset); } VM_BREAK(); } /* Wary jumps — coerce via JS_ToBool (old JMPTRUE/JMPFALSE behavior) */ VM_CASE(MACH_WARYTRUE): { JSValue v = frame->slots[a]; int cond; if (v == JS_TRUE) cond = 1; else if (v == JS_FALSE || v == JS_NULL) cond = 0; else cond = JS_ToBool(ctx, v); if (cond) { int offset = MACH_GET_sBx(instr); pc = (uint32_t)((int32_t)pc + offset); if (offset < 0) { int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed); if (pf == 2) { result = JS_RaiseDisrupt(ctx, "interrupted"); goto done; } if (pf == 1) { if (ctx->vm_call_depth > 0) atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed); else goto suspend; } } } VM_BREAK(); } VM_CASE(MACH_WARYFALSE): { JSValue v = frame->slots[a]; int cond; if (v == JS_TRUE) cond = 1; else if (v == JS_FALSE || v == JS_NULL) cond = 0; else cond = JS_ToBool(ctx, v); if (!cond) { int offset = MACH_GET_sBx(instr); pc = (uint32_t)((int32_t)pc + offset); if (offset < 0) { int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed); if (pf == 2) { result = JS_RaiseDisrupt(ctx, "interrupted"); goto done; } if (pf == 1) { if (ctx->vm_call_depth > 0) atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed); else goto suspend; } } } VM_BREAK(); } VM_CASE(MACH_JMPEMPTY): { if (frame->slots[a] == JS_EMPTY_TEXT) { int offset = MACH_GET_sBx(instr); pc = (uint32_t)((int32_t)pc + offset); } VM_BREAK(); } /* Disrupt (mcode alias) */ VM_CASE(MACH_DISRUPT): goto disrupt; /* Has-property check (mcode name) */ VM_CASE(MACH_IN): { JSValue key = frame->slots[b]; JSValue obj = frame->slots[c]; int has = JS_HasProperty(ctx, obj, key); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); frame->slots[a] = JS_NewBool(ctx, has > 0); VM_BREAK(); } VM_DEFAULT: result = JS_RaiseDisrupt(ctx, "unknown register VM opcode %d: %s", op, mach_opcode_names[op]); goto done; } continue; disrupt: /* Save debug info for stack traces */ ctx->reg_current_frame = frame_ref.val; ctx->current_register_pc = pc > 0 ? pc - 1 : 0; /* Search frame chain for a disruption handler. Use frame_pc to track each frame's execution point: - For the faulting frame, it's the current pc. - For unwound caller frames, read from frame->address. Keep frame_ref.val at the faulting frame and preserve caller links so the trace walk can show the full call chain. */ frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); /* re-chase after GC */ { uint32_t frame_pc = pc; for (;;) { JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function); code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code; /* Only enter handler if we're not already inside it */ if (code->disruption_pc > 0 && frame_pc < code->disruption_pc) { env = fn->u.cell.env_record; pc = code->disruption_pc; ctx->disruption_reported = FALSE; frame_ref.val = JS_MKPTR(frame); /* root handler frame for GC */ ctx->current_exception = JS_NULL; break; } if (!JS_IsPtr(frame->caller)) { /* Stack trace was already included in the JS_RaiseDisrupt log via the callback. */ ctx->disruption_reported = TRUE; frame_ref.val = JS_MKPTR(frame); /* update root for GC / done */ ctx->current_exception = JS_NULL; result = JS_EXCEPTION; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); goto done; } /* Advance to caller — keep chain intact (no nulling caller links) */ frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller); frame_pc = (uint32_t)(JS_VALUE_GET_INT(frame->address) >> 16); } } } suspend: ctx->suspended = 1; ctx->suspended_pc = pc; ctx->suspended_frame_ref.val = frame_ref.val; result = JS_SUSPENDED; JS_DeleteGCRef(ctx, &frame_ref); #ifdef HAVE_ASAN __asan_js_ctx = NULL; #endif return result; done: /* Fire trace hook for top-level register function return */ if (unlikely(ctx->trace_hook) && (ctx->trace_type & JS_HOOK_RET)) ctx->trace_hook(ctx, JS_HOOK_RET, NULL, ctx->trace_data); #ifdef HAVE_ASAN __asan_js_ctx = NULL; #endif ctx->reg_current_frame = JS_NULL; if (JS_IsException(result)) { ctx->reg_current_frame = frame_ref.val; ctx->current_register_pc = pc > 0 ? pc - 1 : 0; } if (JS_IsPtr(frame_ref.val)) { JSFrameRegister *f = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); f->caller = JS_NULL; /* mark as returned so GC can shorten */ } JS_DeleteGCRef(ctx, &frame_ref); return result; } JSValue JS_ResumeRegisterVM(JSContext *ctx) { if (!ctx->suspended) return JS_RaiseDisrupt(ctx, "no suspended VM to resume"); /* ctx->suspended is set; JS_CallRegisterVM will take the resume path */ return JS_CallRegisterVM(ctx, NULL, JS_NULL, 0, NULL, JS_NULL, JS_NULL); } /* ============================================================ MCODE Lowering — mcode JSON IR → MachInstr32 ============================================================ */ typedef struct { MachInstr32 *code; int code_count, code_cap; MachCPoolEntry *cpool; int cpool_count, cpool_cap; MachLineEntry *lines; int nr_slots; struct { const char *name; int pc; } *labels; int label_count, label_cap; struct { int pc; const char *label; int is_sJ; int reg_a; } *patches; int patch_count, patch_cap; int *flat_to_pc; int flat_count; } McodeLowerState; static void ml_emit(McodeLowerState *s, MachInstr32 instr, int line, int col) { if (s->code_count >= s->code_cap) { int nc = s->code_cap ? s->code_cap * 2 : 64; s->code = sys_realloc(s->code, nc * sizeof(MachInstr32)); s->lines = sys_realloc(s->lines, nc * sizeof(MachLineEntry)); s->code_cap = nc; } s->lines[s->code_count] = (MachLineEntry){(uint16_t)line, (uint16_t)col}; s->code[s->code_count++] = instr; } static int ml_cpool_int(McodeLowerState *s, int32_t val) { for (int i = 0; i < s->cpool_count; i++) if (s->cpool[i].type == MACH_CP_INT && s->cpool[i].ival == val) return i; if (s->cpool_count >= s->cpool_cap) { int nc = s->cpool_cap ? s->cpool_cap * 2 : 16; s->cpool = sys_realloc(s->cpool, nc * sizeof(MachCPoolEntry)); s->cpool_cap = nc; } s->cpool[s->cpool_count] = (MachCPoolEntry){.type = MACH_CP_INT, .ival = val}; return s->cpool_count++; } static int ml_cpool_float(McodeLowerState *s, double val) { for (int i = 0; i < s->cpool_count; i++) if (s->cpool[i].type == MACH_CP_FLOAT && s->cpool[i].fval == val) return i; if (s->cpool_count >= s->cpool_cap) { int nc = s->cpool_cap ? s->cpool_cap * 2 : 16; s->cpool = sys_realloc(s->cpool, nc * sizeof(MachCPoolEntry)); s->cpool_cap = nc; } s->cpool[s->cpool_count] = (MachCPoolEntry){.type = MACH_CP_FLOAT, .fval = val}; return s->cpool_count++; } static int ml_cpool_str(McodeLowerState *s, const char *str) { if (!str) { fprintf(stderr, "ml_cpool_str: NULL string\n"); str = ""; } for (int i = 0; i < s->cpool_count; i++) if (s->cpool[i].type == MACH_CP_STR && strcmp(s->cpool[i].str, str) == 0) return i; if (s->cpool_count >= s->cpool_cap) { int nc = s->cpool_cap ? s->cpool_cap * 2 : 16; s->cpool = sys_realloc(s->cpool, nc * sizeof(MachCPoolEntry)); s->cpool_cap = nc; } char *dup = sys_malloc(strlen(str) + 1); memcpy(dup, str, strlen(str) + 1); s->cpool[s->cpool_count] = (MachCPoolEntry){.type = MACH_CP_STR, .str = dup}; return s->cpool_count++; } static void ml_label(McodeLowerState *s, const char *name) { if (s->label_count >= s->label_cap) { int nc = s->label_cap ? s->label_cap * 2 : 32; s->labels = sys_realloc(s->labels, nc * sizeof(s->labels[0])); s->label_cap = nc; } s->labels[s->label_count].name = name; s->labels[s->label_count].pc = s->code_count; s->label_count++; } static void ml_patch(McodeLowerState *s, int pc, const char *label, int is_sJ, int reg_a) { if (s->patch_count >= s->patch_cap) { int nc = s->patch_cap ? s->patch_cap * 2 : 32; s->patches = sys_realloc(s->patches, nc * sizeof(s->patches[0])); s->patch_cap = nc; } s->patches[s->patch_count].pc = pc; s->patches[s->patch_count].label = label; s->patches[s->patch_count].is_sJ = is_sJ; s->patches[s->patch_count].reg_a = reg_a; s->patch_count++; } static int ml_int(cJSON *arr, int idx) { return (int)cJSON_GetArrayItem(arr, idx)->valuedouble; } /* Lower one function's mcode instructions to MachInstr32 */ static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) { McodeLowerState s = {0}; int nr_args = (int)cJSON_GetNumberValue( cJSON_GetObjectItemCaseSensitive(fobj, "nr_args")); int nr_close = (int)cJSON_GetNumberValue( cJSON_GetObjectItemCaseSensitive(fobj, "nr_close_slots")); s.nr_slots = (int)cJSON_GetNumberValue( cJSON_GetObjectItemCaseSensitive(fobj, "nr_slots")); if (s.nr_slots > 255) { cJSON *nm_chk = cJSON_GetObjectItemCaseSensitive(fobj, "name"); const char *fn_name = nm_chk ? cJSON_GetStringValue(nm_chk) : ""; fprintf(stderr, "FATAL: function '%s' has %d slots (max 255). " "Ensure the streamline optimizer ran before mach compilation.\n", fn_name, s.nr_slots); abort(); } int dis_raw = (int)cJSON_GetNumberValue( cJSON_GetObjectItemCaseSensitive(fobj, "disruption_pc")); cJSON *nm = cJSON_GetObjectItemCaseSensitive(fobj, "name"); const char *name = nm ? cJSON_GetStringValue(nm) : NULL; cJSON *fn_j = cJSON_GetObjectItemCaseSensitive(fobj, "filename"); const char *fname = fn_j ? cJSON_GetStringValue(fn_j) : filename; cJSON *instrs = cJSON_GetObjectItemCaseSensitive(fobj, "instructions"); int n = instrs ? cJSON_GetArraySize(instrs) : 0; s.flat_to_pc = sys_malloc((n + 1) * sizeof(int)); s.flat_count = n; { cJSON *it = instrs ? instrs->child : NULL; for (int i = 0; it; i++, it = it->next) { s.flat_to_pc[i] = s.code_count; if (cJSON_IsString(it)) { ml_label(&s, it->valuestring); continue; } int sz = cJSON_GetArraySize(it); const char *op = cJSON_GetArrayItem(it, 0)->valuestring; int line = ml_int(it, sz - 2); int col = ml_int(it, sz - 1); #define A1 ml_int(it,1) #define A2 ml_int(it,2) #define A3 ml_int(it,3) #define A4 ml_int(it,4) #define EM(instr) ml_emit(&s, (instr), line, col) #define ABC3(opc) EM(MACH_ABC(opc, A1, A2, A3)) #define AB2(opc) EM(MACH_ABC(opc, A1, A2, 0)) if (strcmp(op, "access") == 0) { int dest = A1; cJSON *val = cJSON_GetArrayItem(it, 2); if (cJSON_IsNumber(val)) { double dv = val->valuedouble; int32_t iv = (int32_t)dv; if ((double)iv == dv && iv >= -32768 && iv <= 32767) EM(MACH_AsBx(MACH_LOADI, dest, iv)); else if ((double)iv == dv) EM(MACH_ABx(MACH_LOADK, dest, ml_cpool_int(&s, iv))); else EM(MACH_ABx(MACH_LOADK, dest, ml_cpool_float(&s, dv))); } else if (cJSON_IsString(val)) { EM(MACH_ABx(MACH_LOADK, dest, ml_cpool_str(&s, val->valuestring))); } else if (cJSON_IsObject(val)) { const char *vn = cJSON_GetStringValue( cJSON_GetObjectItemCaseSensitive(val, "name")); EM(MACH_ABx(MACH_GETNAME, dest, ml_cpool_str(&s, vn))); } } else if (strcmp(op, "int") == 0) { int dest = A1, v = A2; if (v >= -32768 && v <= 32767) EM(MACH_AsBx(MACH_LOADI, dest, v)); else EM(MACH_ABx(MACH_LOADK, dest, ml_cpool_int(&s, v))); } else if (strcmp(op, "true") == 0) { EM(MACH_ABC(MACH_LOADTRUE, A1, 0, 0)); } else if (strcmp(op, "false") == 0) { EM(MACH_ABC(MACH_LOADFALSE, A1, 0, 0)); } else if (strcmp(op, "null") == 0) { EM(MACH_ABC(MACH_LOADNULL, A1, 0, 0)); } else if (strcmp(op, "move") == 0) { AB2(MACH_MOVE); } /* Text */ else if (strcmp(op, "concat") == 0) { ABC3(MACH_CONCAT); } else if (strcmp(op, "stone_text") == 0) { EM(MACH_ABC(MACH_STONE_TEXT, A1, 0, 0)); } /* Generic arithmetic */ else if (strcmp(op, "add") == 0) { ABC3(MACH_ADD); } else if (strcmp(op, "subtract") == 0) { ABC3(MACH_SUB); } else if (strcmp(op, "multiply") == 0) { ABC3(MACH_MUL); } else if (strcmp(op, "divide") == 0) { ABC3(MACH_DIV); } else if (strcmp(op, "modulo") == 0) { ABC3(MACH_MOD); } else if (strcmp(op, "pow") == 0) { ABC3(MACH_POW); } else if (strcmp(op, "negate") == 0) { AB2(MACH_NEG); } else if (strcmp(op, "remainder") == 0) { ABC3(MACH_REMAINDER); } else if (strcmp(op, "max") == 0) { ABC3(MACH_MAX); } else if (strcmp(op, "min") == 0) { ABC3(MACH_MIN); } else if (strcmp(op, "abs") == 0) { AB2(MACH_ABS); } else if (strcmp(op, "sign") == 0) { AB2(MACH_SIGN); } else if (strcmp(op, "fraction") == 0) { AB2(MACH_FRACTION); } else if (strcmp(op, "integer") == 0) { AB2(MACH_INTEGER); } else if (strcmp(op, "floor") == 0) { ABC3(MACH_FLOOR); } else if (strcmp(op, "ceiling") == 0) { ABC3(MACH_CEILING); } else if (strcmp(op, "round") == 0) { ABC3(MACH_ROUND); } else if (strcmp(op, "trunc") == 0) { ABC3(MACH_TRUNC); } /* Generic comparisons */ else if (strcmp(op, "eq") == 0) { ABC3(MACH_EQ); } else if (strcmp(op, "ne") == 0) { ABC3(MACH_NEQ); } else if (strcmp(op, "lt") == 0) { ABC3(MACH_LT); } else if (strcmp(op, "le") == 0) { ABC3(MACH_LE); } else if (strcmp(op, "gt") == 0) { ABC3(MACH_GT); } else if (strcmp(op, "ge") == 0) { ABC3(MACH_GE); } /* Special comparisons */ else if (strcmp(op, "is_identical") == 0) { ABC3(MACH_IS_IDENTICAL); } else if (strcmp(op, "eq_tol") == 0) { /* A=dest, B=a, C=b; tolerance in operand 4 */ int dest = A1, ra = A2, rb = A3, rt = A4; /* Move to consecutive scratch slots at end of frame */ int base = s.nr_slots; s.nr_slots += 3; EM(MACH_ABC(MACH_MOVE, base, ra, 0)); EM(MACH_ABC(MACH_MOVE, base + 1, rb, 0)); EM(MACH_ABC(MACH_MOVE, base + 2, rt, 0)); EM(MACH_ABC(MACH_EQ_TOL, dest, base, 3)); } else if (strcmp(op, "ne_tol") == 0) { int dest = A1, ra = A2, rb = A3, rt = A4; int base = s.nr_slots; s.nr_slots += 3; EM(MACH_ABC(MACH_MOVE, base, ra, 0)); EM(MACH_ABC(MACH_MOVE, base + 1, rb, 0)); EM(MACH_ABC(MACH_MOVE, base + 2, rt, 0)); EM(MACH_ABC(MACH_NEQ_TOL, dest, base, 3)); } /* Type checks */ else if (strcmp(op, "is_int") == 0) { AB2(MACH_IS_INT); } else if (strcmp(op, "is_num") == 0) { AB2(MACH_IS_NUM); } else if (strcmp(op, "is_text") == 0) { AB2(MACH_IS_TEXT); } else if (strcmp(op, "is_bool") == 0) { AB2(MACH_IS_BOOL); } else if (strcmp(op, "is_null") == 0) { AB2(MACH_IS_NULL); } else if (strcmp(op, "is_array") == 0) { AB2(MACH_IS_ARRAY); } else if (strcmp(op, "is_func") == 0) { AB2(MACH_IS_FUNC); } else if (strcmp(op, "is_record") == 0) { AB2(MACH_IS_RECORD); } else if (strcmp(op, "is_stone") == 0) { AB2(MACH_IS_STONE); } else if (strcmp(op, "length") == 0) { AB2(MACH_LENGTH); } else if (strcmp(op, "is_proxy") == 0) { AB2(MACH_IS_PROXY); } else if (strcmp(op, "is_blob") == 0) { AB2(MACH_IS_BLOB); } else if (strcmp(op, "is_data") == 0) { AB2(MACH_IS_DATA); } else if (strcmp(op, "is_true") == 0) { AB2(MACH_IS_TRUE); } else if (strcmp(op, "is_false") == 0) { AB2(MACH_IS_FALSE); } else if (strcmp(op, "is_fit") == 0) { AB2(MACH_IS_FIT); } else if (strcmp(op, "is_char") == 0) { AB2(MACH_IS_CHAR); } else if (strcmp(op, "is_digit") == 0) { AB2(MACH_IS_DIGIT); } else if (strcmp(op, "is_letter") == 0) { AB2(MACH_IS_LETTER); } else if (strcmp(op, "is_lower") == 0) { AB2(MACH_IS_LOWER); } else if (strcmp(op, "is_upper") == 0) { AB2(MACH_IS_UPPER); } else if (strcmp(op, "is_ws") == 0) { AB2(MACH_IS_WS); } else if (strcmp(op, "is_actor") == 0) { AB2(MACH_IS_ACTOR); } else if (strcmp(op, "apply") == 0) { ABC3(MACH_APPLY); } /* Logical */ else if (strcmp(op, "not") == 0) { AB2(MACH_NOT); } else if (strcmp(op, "and") == 0) { ABC3(MACH_AND); } else if (strcmp(op, "or") == 0) { ABC3(MACH_OR); } /* Bitwise */ else if (strcmp(op, "bitnot") == 0) { AB2(MACH_BITNOT); } else if (strcmp(op, "bitand") == 0) { ABC3(MACH_BITAND); } else if (strcmp(op, "bitor") == 0) { ABC3(MACH_BITOR); } else if (strcmp(op, "bitxor") == 0) { ABC3(MACH_BITXOR); } else if (strcmp(op, "shl") == 0) { ABC3(MACH_SHL); } else if (strcmp(op, "shr") == 0) { ABC3(MACH_SHR); } else if (strcmp(op, "ushr") == 0) { ABC3(MACH_USHR); } /* Property access */ else if (strcmp(op, "load_field") == 0) { int dest = A1, obj = A2; cJSON *key_item = cJSON_GetArrayItem(it, 3); if (cJSON_IsString(key_item)) { int ki = ml_cpool_str(&s, key_item->valuestring); if (ki <= 255) { EM(MACH_ABC(MACH_LOAD_FIELD, dest, obj, ki)); } else { /* cpool index > 255: load key via LOADK, then use dynamic access */ int tmp = s.nr_slots++; EM(MACH_ABx(MACH_LOADK, tmp, ki)); EM(MACH_ABC(MACH_LOAD_DYNAMIC, dest, obj, tmp)); } } else { /* key is a register — fall back to dynamic access */ int key_reg = (int)key_item->valuedouble; EM(MACH_ABC(MACH_LOAD_DYNAMIC, dest, obj, key_reg)); } } else if (strcmp(op, "store_field") == 0) { int obj = A1, val = A2; cJSON *key_item = cJSON_GetArrayItem(it, 3); if (cJSON_IsString(key_item)) { int ki = ml_cpool_str(&s, key_item->valuestring); if (ki <= 255) { EM(MACH_ABC(MACH_STORE_FIELD, obj, ki, val)); } else { /* cpool index > 255: load key via LOADK, then use dynamic access */ int tmp = s.nr_slots++; EM(MACH_ABx(MACH_LOADK, tmp, ki)); EM(MACH_ABC(MACH_STORE_DYNAMIC, obj, tmp, val)); } } else { /* key is a register — fall back to dynamic access */ int key_reg = (int)key_item->valuedouble; EM(MACH_ABC(MACH_STORE_DYNAMIC, obj, key_reg, val)); } } else if (strcmp(op, "load_index") == 0) { ABC3(MACH_LOAD_INDEX); } else if (strcmp(op, "store_index") == 0) { /* mcode: store_index obj val idx → VM: R(A)[R(B)] = R(C) */ EM(MACH_ABC(MACH_STORE_INDEX, A1, A3, A2)); } else if (strcmp(op, "load_dynamic") == 0) { ABC3(MACH_LOAD_DYNAMIC); } else if (strcmp(op, "store_dynamic") == 0) { /* mcode: store_dynamic obj val key → VM: R(A)[R(B)] = R(C) */ EM(MACH_ABC(MACH_STORE_DYNAMIC, A1, A3, A2)); } /* Delete */ else if (strcmp(op, "delete") == 0) { int dest = A1, obj = A2; cJSON *key_item = cJSON_GetArrayItem(it, 3); if (cJSON_IsString(key_item)) { int ki = ml_cpool_str(&s, key_item->valuestring); if (ki <= 255) { EM(MACH_ABC(MACH_DELETE, dest, obj, ki)); } else { int tmp = s.nr_slots++; EM(MACH_ABx(MACH_LOADK, tmp, ki)); EM(MACH_ABC(MACH_DELETEINDEX, dest, obj, tmp)); } } else { int key_reg = (int)key_item->valuedouble; EM(MACH_ABC(MACH_DELETEINDEX, dest, obj, key_reg)); } } /* Array/Object creation */ else if (strcmp(op, "array") == 0) { EM(MACH_ABC(MACH_NEWARRAY, A1, A2, 0)); } else if (strcmp(op, "record") == 0) { EM(MACH_ABC(MACH_NEWRECORD, A1, A2, 0)); } /* Push/Pop */ else if (strcmp(op, "push") == 0) { EM(MACH_ABC(MACH_PUSH, A1, A2, 0)); } else if (strcmp(op, "pop") == 0) { EM(MACH_ABC(MACH_POP, A1, A2, 0)); } /* Closure access */ else if (strcmp(op, "get") == 0) { /* mcode: get dest slot level → GETUP A=dest B=level C=slot */ EM(MACH_ABC(MACH_GETUP, A1, A3, A2)); } else if (strcmp(op, "put") == 0) { /* mcode: put src slot level → SETUP A=src B=level C=slot */ EM(MACH_ABC(MACH_SETUP, A1, A3, A2)); } /* Function creation */ else if (strcmp(op, "function") == 0) { EM(MACH_ABx(MACH_CLOSURE, A1, A2)); } /* Decomposed function calls */ else if (strcmp(op, "frame") == 0) { EM(MACH_ABC(MACH_FRAME, A1, A2, A3)); } else if (strcmp(op, "setarg") == 0) { EM(MACH_ABC(MACH_SETARG, A1, A2, A3)); } else if (strcmp(op, "invoke") == 0 || strcmp(op, "tail_invoke") == 0) { EM(MACH_ABC(MACH_INVOKE, A1, A2, 0)); } else if (strcmp(op, "goframe") == 0) { EM(MACH_ABC(MACH_GOFRAME, A1, A2, A3)); } else if (strcmp(op, "goinvoke") == 0) { EM(MACH_ABC(MACH_GOINVOKE, A1, 0, 0)); } /* Control flow */ else if (strcmp(op, "jump") == 0) { const char *lbl = cJSON_GetArrayItem(it, 1)->valuestring; int pc_now = s.code_count; EM(MACH_sJ(MACH_JMP, 0)); ml_patch(&s, pc_now, lbl, 1, 0); } else if (strcmp(op, "jump_true") == 0) { int reg = A1; const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring; int pc_now = s.code_count; EM(MACH_AsBx(MACH_JMPTRUE, reg, 0)); ml_patch(&s, pc_now, lbl, 0, reg); } else if (strcmp(op, "jump_false") == 0) { int reg = A1; const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring; int pc_now = s.code_count; EM(MACH_AsBx(MACH_JMPFALSE, reg, 0)); ml_patch(&s, pc_now, lbl, 0, reg); } else if (strcmp(op, "jump_not_null") == 0) { int reg = A1; const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring; int pc_now = s.code_count; EM(MACH_AsBx(MACH_JMPNOTNULL, reg, 0)); ml_patch(&s, pc_now, lbl, 0, reg); } else if (strcmp(op, "jump_null") == 0) { int reg = A1; const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring; int pc_now = s.code_count; EM(MACH_AsBx(MACH_JMPNULL, reg, 0)); ml_patch(&s, pc_now, lbl, 0, reg); } else if (strcmp(op, "wary_true") == 0) { int reg = A1; const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring; int pc_now = s.code_count; EM(MACH_AsBx(MACH_WARYTRUE, reg, 0)); ml_patch(&s, pc_now, lbl, 0, reg); } else if (strcmp(op, "wary_false") == 0) { int reg = A1; const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring; int pc_now = s.code_count; EM(MACH_AsBx(MACH_WARYFALSE, reg, 0)); ml_patch(&s, pc_now, lbl, 0, reg); } else if (strcmp(op, "jump_empty") == 0) { int reg = A1; const char *lbl = cJSON_GetArrayItem(it, 2)->valuestring; int pc_now = s.code_count; EM(MACH_AsBx(MACH_JMPEMPTY, reg, 0)); ml_patch(&s, pc_now, lbl, 0, reg); } /* Return / error */ else if (strcmp(op, "return") == 0) { EM(MACH_ABC(MACH_RETURN, A1, 0, 0)); } else if (strcmp(op, "disrupt") == 0) { EM(MACH_ABC(MACH_DISRUPT, 0, 0, 0)); } /* Misc */ else if (strcmp(op, "in") == 0) { EM(MACH_ABC(MACH_IN, A1, A2, A3)); } else if (strcmp(op, "regexp") == 0) { int dest = A1; const char *pat = cJSON_GetArrayItem(it, 2)->valuestring; const char *flg = cJSON_GetArrayItem(it, 3)->valuestring; EM(MACH_ABC(MACH_REGEXP, dest, ml_cpool_str(&s, pat), ml_cpool_str(&s, flg))); } else { /* Unknown opcode — emit NOP */ EM(MACH_ABC(MACH_NOP, 0, 0, 0)); } } } /* Sentinel for flat_to_pc */ s.flat_to_pc[n] = s.code_count; #undef A1 #undef A2 #undef A3 #undef A4 #undef EM #undef ABC3 #undef AB2 /* Resolve pending jump patches */ for (int i = 0; i < s.patch_count; i++) { int target = -1; for (int j = 0; j < s.label_count; j++) { if (strcmp(s.labels[j].name, s.patches[i].label) == 0) { target = s.labels[j].pc; break; } } if (target < 0) { fprintf(stderr, "mcode_lower: unknown label '%s'\n", s.patches[i].label); continue; } int offset = target - (s.patches[i].pc + 1); if (s.patches[i].is_sJ) { int old_op = MACH_GET_OP(s.code[s.patches[i].pc]); s.code[s.patches[i].pc] = MACH_sJ(old_op, offset); } else { int old_op = MACH_GET_OP(s.code[s.patches[i].pc]); s.code[s.patches[i].pc] = MACH_AsBx(old_op, s.patches[i].reg_a, offset); } } MachCode *mc = sys_malloc(sizeof(MachCode)); memset(mc, 0, sizeof(MachCode)); mc->arity = nr_args; mc->nr_close_slots = nr_close; mc->nr_slots = s.nr_slots; mc->entry_point = 0; mc->instr_count = s.code_count; mc->instructions = s.code; mc->cpool_count = s.cpool_count; mc->cpool = s.cpool; mc->line_table = s.lines; if (name && name[0]) { mc->name = sys_malloc(strlen(name) + 1); memcpy(mc->name, name, strlen(name) + 1); } if (fname) { mc->filename = sys_malloc(strlen(fname) + 1); memcpy(mc->filename, fname, strlen(fname) + 1); } mc->disruption_pc = 0; if (dis_raw > 0 && dis_raw < s.flat_count) mc->disruption_pc = s.flat_to_pc[dis_raw]; sys_free(s.flat_to_pc); sys_free(s.labels); sys_free(s.patches); return mc; } /* Assign nested functions to each MachCode based on CLOSURE instructions. all_funcs is the flat array from the mcode JSON; compiled[i] is the compiled MachCode for all_funcs[i]. mc is a compiled MachCode (main or one of the flat functions). Scans its instructions for MACH_CLOSURE Bx and builds mc->functions with remapped local indices. */ static void mcode_assign_children(MachCode *mc, MachCode **compiled, int total) { /* Count unique CLOSURE references */ int *refs = NULL; int ref_count = 0, ref_cap = 0; for (uint32_t i = 0; i < mc->instr_count; i++) { if (MACH_GET_OP(mc->instructions[i]) == MACH_CLOSURE) { int bx = MACH_GET_Bx(mc->instructions[i]); /* Check if already in refs */ int found = 0; for (int j = 0; j < ref_count; j++) { if (refs[j] == bx) { found = 1; break; } } if (!found) { if (ref_count >= ref_cap) { ref_cap = ref_cap ? ref_cap * 2 : 8; refs = sys_realloc(refs, ref_cap * sizeof(int)); } refs[ref_count++] = bx; } } } if (ref_count == 0) { sys_free(refs); return; } /* Build local functions array (preserve original order by flat index) */ /* Sort refs by value to maintain consistent ordering */ for (int i = 0; i < ref_count - 1; i++) for (int j = i + 1; j < ref_count; j++) if (refs[i] > refs[j]) { int t = refs[i]; refs[i] = refs[j]; refs[j] = t; } mc->func_count = ref_count; mc->functions = sys_malloc(ref_count * sizeof(MachCode *)); for (int i = 0; i < ref_count; i++) mc->functions[i] = (refs[i] < total) ? compiled[refs[i]] : NULL; /* Remap CLOSURE Bx: flat index → local index */ for (uint32_t i = 0; i < mc->instr_count; i++) { if (MACH_GET_OP(mc->instructions[i]) == MACH_CLOSURE) { int a = MACH_GET_A(mc->instructions[i]); int bx = MACH_GET_Bx(mc->instructions[i]); for (int j = 0; j < ref_count; j++) { if (refs[j] == bx) { mc->instructions[i] = MACH_ABx(MACH_CLOSURE, a, j); break; } } } } sys_free(refs); } /* Compile mcode JSON IR to MachCode binary. mcode_json has: functions[], main{}, filename, name */ MachCode *mach_compile_mcode(cJSON *mcode_json) { const char *filename = cJSON_GetStringValue( cJSON_GetObjectItemCaseSensitive(mcode_json, "filename")); cJSON *funcs_arr = cJSON_GetObjectItemCaseSensitive(mcode_json, "functions"); int func_count = funcs_arr ? cJSON_GetArraySize(funcs_arr) : 0; cJSON *main_obj = cJSON_GetObjectItemCaseSensitive(mcode_json, "main"); /* Slot compression is handled by the streamline optimizer before mach compilation. mcode_lower_func() asserts nr_slots <= 255. */ /* Compile all flat functions */ MachCode **compiled = NULL; if (func_count > 0) { compiled = sys_malloc(func_count * sizeof(MachCode *)); memset(compiled, 0, func_count * sizeof(MachCode *)); { cJSON *fobj = funcs_arr->child; for (int i = 0; fobj; i++, fobj = fobj->next) compiled[i] = mcode_lower_func(fobj, filename); } } /* Compile main */ MachCode *main_code = mcode_lower_func(main_obj, filename); /* Assign nested functions to each compiled unit */ for (int i = 0; i < func_count; i++) mcode_assign_children(compiled[i], compiled, func_count); mcode_assign_children(main_code, compiled, func_count); sys_free(compiled); return main_code; } /* ============================================================ MACH Public API ============================================================ */ /* Print a single constant pool value for dump output */ static void dump_cpool_value(JSContext *ctx, JSValue val) { uint32_t tag = JS_VALUE_GET_TAG(val); if (JS_IsPtr(val)) { void *ptr = JS_VALUE_GET_PTR(val); objhdr_t hdr = *(objhdr_t *)ptr; uint8_t mist_type = objhdr_type(hdr); if (mist_type == OBJ_TEXT) { const char *str = JS_ToCString(ctx, val); if (str) { printf("\"%s\"", str); JS_FreeCString(ctx, str); } else { printf(""); } return; } printf("", mist_type); return; } switch (tag) { case JS_TAG_INT: printf("%d", JS_VALUE_GET_INT(val)); break; case JS_TAG_BOOL: printf("%s", JS_VALUE_GET_BOOL(val) ? "true" : "false"); break; case JS_TAG_NULL: printf("null"); break; case JS_TAG_SHORT_FLOAT: printf("%g", JS_VALUE_GET_FLOAT64(val)); break; case JS_TAG_STRING_IMM: { const char *str = JS_ToCString(ctx, val); if (str) { printf("\"%s\"", str); JS_FreeCString(ctx, str); } else { printf(""); } break; } default: printf("", tag); break; } } /* (labels removed in new format) */ /* Internal helper to dump JSCodeRegister (32-bit instruction format) */ static void dump_register_code(JSContext *ctx, JSCodeRegister *code, int indent) { char pad[64]; int pad_len = indent * 2; if (pad_len > 60) pad_len = 60; memset(pad, ' ', pad_len); pad[pad_len] = '\0'; /* Function header */ const char *name = ""; if (!JS_IsNull(code->name)) { const char *n = JS_ToCString(ctx, code->name); if (n) name = n; } printf("%sFunction: %s\n", pad, name); printf("%s Arity: %d, Slots: %d, Close: %d\n", pad, code->arity, code->nr_slots, code->nr_close_slots); if (!JS_IsNull(code->name)) { JS_FreeCString(ctx, name); } if (code->disruption_pc > 0) printf("%s Disruption handler at: %d\n", pad, code->disruption_pc); /* Constant pool */ if (code->cpool_count > 0) { printf("%s\n%sConstant Pool (%d entries):\n", pad, pad, code->cpool_count); for (uint32_t i = 0; i < code->cpool_count; i++) { printf("%s [%d]: ", pad, i); dump_cpool_value(ctx, code->cpool[i]); printf("\n"); } } /* Instructions */ printf("%s\n%sInstructions (%d):\n", pad, pad, code->instr_count); for (uint32_t i = 0; i < code->instr_count; i++) { MachInstr32 instr = code->instructions[i]; int op = MACH_GET_OP(instr); int a = MACH_GET_A(instr); int b = MACH_GET_B(instr); int c = MACH_GET_C(instr); const char *op_name = (op < MACH_OP_COUNT) ? mach_opcode_names[op] : "???"; if (!op_name) op_name = "???"; printf("%s %3d: %-14s ", pad, i, op_name); switch (op) { /* No operands */ case MACH_NOP: case MACH_RETNIL: break; /* A only */ case MACH_LOADNULL: case MACH_LOADTRUE: case MACH_LOADFALSE: printf("r%d", a); break; /* ABx: load constant */ case MACH_LOADK: { int bx = MACH_GET_Bx(instr); printf("r%d, #%d", a, bx); if (bx >= 0 && (uint32_t)bx < code->cpool_count) { printf(" ; "); dump_cpool_value(ctx, code->cpool[bx]); } break; } /* AsBx: load small int */ case MACH_LOADI: printf("r%d, %d", a, MACH_GET_sBx(instr)); break; /* A, B: move, unary ops */ case MACH_MOVE: case MACH_NEG: case MACH_LNOT: case MACH_BNOT: printf("r%d, r%d", a, b); break; /* A, B, C: arithmetic, comparison, bitwise */ case MACH_ADD: case MACH_SUB: case MACH_MUL: case MACH_DIV: case MACH_MOD: case MACH_POW: case MACH_EQ: case MACH_NEQ: case MACH_LT: case MACH_LE: case MACH_GT: case MACH_GE: case MACH_BAND: case MACH_BOR: case MACH_BXOR: case MACH_SHL: case MACH_SHR: case MACH_USHR: printf("r%d, r%d, r%d", a, b, c); break; case MACH_EQ_TOL: case MACH_NEQ_TOL: printf("r%d, r%d, %d", a, b, c); break; /* Property access */ case MACH_GETFIELD: printf("r%d, r%d, #%d", a, b, c); if ((uint32_t)c < code->cpool_count) { printf(" ; "); dump_cpool_value(ctx, code->cpool[c]); } break; case MACH_SETFIELD: printf("r%d, #%d, r%d", a, b, c); if ((uint32_t)b < code->cpool_count) { printf(" ; "); dump_cpool_value(ctx, code->cpool[b]); } break; case MACH_GETINDEX: case MACH_SETINDEX: printf("r%d, r%d, r%d", a, b, c); break; /* ABx: name/intrinsic/env access */ case MACH_GETNAME: case MACH_GETINTRINSIC: case MACH_GETENV: { int bx = MACH_GET_Bx(instr); printf("r%d, #%d", a, bx); if ((uint32_t)bx < code->cpool_count) { printf(" ; "); dump_cpool_value(ctx, code->cpool[bx]); } break; } /* Closure access */ case MACH_GETUP: case MACH_SETUP: printf("r%d, depth=%d, slot=%d", a, b, c); break; /* isJ: unconditional jump */ case MACH_JMP: { int offset = MACH_GET_sJ(instr); printf("%+d", offset); printf(" ; -> %d", (int)i + 1 + offset); break; } /* iAsBx: conditional jumps */ case MACH_JMPTRUE: case MACH_JMPFALSE: case MACH_JMPNULL: { int offset = MACH_GET_sBx(instr); printf("r%d, %+d", a, offset); printf(" ; -> %d", (int)i + 1 + offset); break; } /* Return / throw */ case MACH_RETURN: case MACH_THROW: printf("r%d", a); break; /* Object/array creation */ case MACH_NEWOBJECT: printf("r%d", a); break; case MACH_NEWARRAY: printf("r%d, %d", a, b); break; /* Push/Pop */ case MACH_PUSH: printf("r%d, r%d", a, b); break; case MACH_POP: printf("r%d, r%d", a, b); break; /* Closure */ case MACH_CLOSURE: { int bx = MACH_GET_Bx(instr); printf("r%d, func#%d", a, bx); break; } default: printf("0x%08x", instr); break; } printf("\n"); } /* Nested functions */ if (code->func_count > 0) { printf("%s\n%sNested Functions (%d):\n", pad, pad, code->func_count); for (uint32_t i = 0; i < code->func_count; i++) { printf("%s [%d]:\n", pad, i); if (code->functions[i]) { dump_register_code(ctx, code->functions[i], indent + 2); } else { printf("%s \n", pad); } } } } JSValue JS_RunMachBin(JSContext *ctx, const uint8_t *data, size_t size, JSValue env) { MachCode *mc = JS_DeserializeMachCode(data, size); if (!mc) return JS_RaiseDisrupt(ctx, "failed to deserialize MACH bytecode"); JSGCRef env_ref; JS_PushGCRef(ctx, &env_ref); env_ref.val = env; JSCodeRegister *code = JS_LoadMachCode(ctx, mc, env_ref.val); JS_FreeMachCode(mc); JSValue result = JS_CallRegisterVM(ctx, code, ctx->global_obj, 0, NULL, env_ref.val, JS_NULL); JS_PopGCRef(ctx, &env_ref); return result; } JSValue JS_RunMachMcode(JSContext *ctx, const char *json_str, size_t len, JSValue env) { (void)len; cJSON *mcode = cJSON_Parse(json_str); if (!mcode) return JS_RaiseDisrupt(ctx, "failed to parse mcode JSON"); MachCode *mc = mach_compile_mcode(mcode); cJSON_Delete(mcode); if (!mc) return JS_RaiseDisrupt(ctx, "mcode compilation failed"); JSGCRef env_ref; JS_PushGCRef(ctx, &env_ref); env_ref.val = env; JSCodeRegister *code = JS_LoadMachCode(ctx, mc, env_ref.val); JS_FreeMachCode(mc); JSValue result = JS_CallRegisterVM(ctx, code, ctx->global_obj, 0, NULL, env_ref.val, JS_NULL); JS_PopGCRef(ctx, &env_ref); return result; } void JS_DumpMachBin(JSContext *ctx, const uint8_t *data, size_t size, JSValue env) { MachCode *mc = JS_DeserializeMachCode(data, size); if (!mc) { printf("Failed to deserialize MACH bytecode\n"); return; } JSGCRef env_ref; JS_PushGCRef(ctx, &env_ref); env_ref.val = env; JSCodeRegister *code = JS_LoadMachCode(ctx, mc, env_ref.val); JS_FreeMachCode(mc); dump_register_code(ctx, code, 0); JS_PopGCRef(ctx, &env_ref); }