diff --git a/compare_aot.ce b/compare_aot.ce new file mode 100644 index 00000000..6e33640c --- /dev/null +++ b/compare_aot.ce @@ -0,0 +1,92 @@ +// compare_aot.ce — compile a .cm module via both paths and compare results +// +// Usage: +// cell --dev compare_aot.ce + +var build = use('build') +var fd_mod = use('fd') +var os = use('os') +var json = use('json') + +var show = function(v) { + return json.encode(v) +} + +if (length(args) < 1) { + print('usage: cell --dev compare_aot.ce ') + return +} + +var file = args[0] +if (!fd_mod.is_file(file)) { + if (!ends_with(file, '.cm') && fd_mod.is_file(file + '.cm')) + file = file + '.cm' + else { + print('file not found: ' + file) + return + } +} + +var abs = fd_mod.realpath(file) + +// Shared compilation front-end +var tokenize = use('tokenize') +var parse_mod = use('parse') +var fold = use('fold') +var mcode_mod = use('mcode') +var streamline_mod = use('streamline') + +var src = text(fd_mod.slurp(abs)) +var tok = tokenize(src, abs) +var ast = parse_mod(tok.tokens, src, abs, tokenize) +var folded = fold(ast) +var compiled = mcode_mod(folded) +var optimized = streamline_mod(compiled) + +// --- Interpreted (mach VM) --- +print('--- interpreted ---') +var mcode_json = json.encode(optimized) +var mach_blob = mach_compile_mcode_bin(abs, mcode_json) +var result_interp = mach_load(mach_blob, stone({})) +print('result: ' + show(result_interp)) + +// --- Native (AOT via QBE) --- +print('\n--- native ---') +var dylib_path = build.compile_native(abs, null, null, null) +print('dylib: ' + dylib_path) + +var handle = os.dylib_open(dylib_path) +if (!handle) { + print('failed to open dylib') + return +} + +// Build env with runtime functions. Must include starts_with etc. because +// the GC can lose global object properties after compaction. +var env = stone({ + logical: logical, + some: some, + every: every, + starts_with: starts_with, + ends_with: ends_with, + log: log, + fallback: fallback, + parallel: parallel, + race: race, + sequence: sequence +}) + +var result_native = os.native_module_load(handle, env) +print('result: ' + show(result_native)) + +// --- Comparison --- +print('\n--- comparison ---') +var s_interp = show(result_interp) +var s_native = show(result_native) +if (s_interp == s_native) { + print('MATCH') +} else { + print('MISMATCH') + print(' interp: ' + s_interp) + print(' native: ' + s_native) +} diff --git a/diff.ce b/diff.ce index dbb024d0..d5e75e8b 100644 --- a/diff.ce +++ b/diff.ce @@ -129,11 +129,11 @@ function diff_test_file(file_path) { // Build env for module loading var make_env = function() { - return { + return stone({ use: function(path) { return shop.use(path, use_pkg) } - } + }) } // Read and parse diff --git a/dump_ir.ce b/dump_ir.ce new file mode 100644 index 00000000..b138000e --- /dev/null +++ b/dump_ir.ce @@ -0,0 +1,22 @@ +var tokenize = use('tokenize') +var parse_mod = use('parse') +var fold = use('fold') +var mcode_mod = use('mcode') +var streamline_mod = use('streamline') +var json = use('json') +var fd = use('fd') + +var file = args[0] +var src = text(fd.slurp(file)) +var tok = tokenize(src, file) +var ast = parse_mod(tok.tokens, src, file, tokenize) +var folded = fold(ast) +var compiled = mcode_mod(folded) +var optimized = streamline_mod(compiled) + +var instrs = optimized.main.instructions +var i = 0 +while (i < length(instrs)) { + print(text(i) + ': ' + json.encode(instrs[i])) + i = i + 1 +} diff --git a/fuzz.ce b/fuzz.ce index bc32a0ff..fa131090 100644 --- a/fuzz.ce +++ b/fuzz.ce @@ -134,7 +134,7 @@ function run_fuzz(seed_val) { // Run optimized var _opt = function() { - mod_opt = run_ast_fn(name, ast, {use: function(p) { return use(p) }}) + mod_opt = run_ast_fn(name, ast, stone({use: function(p) { return use(p) }})) } disruption { opt_err = "disrupted" } @@ -142,7 +142,7 @@ function run_fuzz(seed_val) { // Run unoptimized var _noopt = function() { - mod_noopt = run_ast_noopt_fn(name + "_noopt", ast, {use: function(p) { return use(p) }}) + mod_noopt = run_ast_noopt_fn(name + "_noopt", ast, stone({use: function(p) { return use(p) }})) } disruption { noopt_err = "disrupted" } diff --git a/internal/bootstrap.cm b/internal/bootstrap.cm index 08dd172d..aaec4401 100644 --- a/internal/bootstrap.cm +++ b/internal/bootstrap.cm @@ -37,7 +37,7 @@ function boot_load(name) { } mcode_blob = fd.slurp(mcode_path) mach_blob = mach_compile_mcode_bin(name, text(mcode_blob)) - return mach_load(mach_blob, {use: use_embed}) + return mach_load(mach_blob, stone({use: use_embed})) } var tokenize_mod = boot_load("tokenize") diff --git a/internal/engine.cm b/internal/engine.cm index b338acf5..363a8814 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -22,6 +22,9 @@ function use_embed(name) { return load_internal("js_core_" + name + "_use") } +// These duplicate C builtins from runtime.c, but are needed because the GC can +// lose properties on the global object after compaction. The runtime_env provides +// a stable way for modules to access them via env_record linking. function logical(val1) { if (val1 == 0 || val1 == false || val1 == "false" || val1 == null) return false; @@ -101,7 +104,7 @@ function load_pipeline_module(name, env) { } // Load compilation pipeline -var pipeline_env = {use: use_embed} +var pipeline_env = stone({use: use_embed}) var tokenize_mod = load_pipeline_module('tokenize', pipeline_env) var parse_mod = load_pipeline_module('parse', pipeline_env) var fold_mod = load_pipeline_module('fold', pipeline_env) @@ -222,6 +225,7 @@ function use_core(path) { // Build env: merge core_extras env = {use: use_core} arrfor(array(core_extras), function(k) { env[k] = core_extras[k] }) + env = stone(env) var hash = null var cached_path = null @@ -398,7 +402,9 @@ var parallel = pronto.parallel var race = pronto.race var sequence = pronto.sequence -// Fill runtime_env (same object reference shop holds) +// Fill runtime_env — includes duplicates of C builtins because the GC can +// lose global object properties after compaction. Modules resolve these via +// env_record, not the global. runtime_env.logical = logical runtime_env.some = some runtime_env.every = every @@ -1045,6 +1051,7 @@ $_.clock(_ => { } env.args = _cell.args.arg env.log = log + env = stone(env) var source_blob = fd.slurp(prog_path) var hash = content_hash(source_blob) diff --git a/internal/os.c b/internal/os.c index 174e5192..053a1ab4 100644 --- a/internal/os.c +++ b/internal/os.c @@ -445,8 +445,8 @@ static JSValue js_os_dylib_close(JSContext *js, JSValue self, int argc, JSValue /* Load a native .cm module from a dylib handle. Uses cell_rt_native_module_load from qbe_helpers.c */ -extern JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle); -extern JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const char *sym_name); +extern JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle, JSValue env); +extern JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const char *sym_name, JSValue env); static JSValue js_os_native_module_load(JSContext *js, JSValue self, int argc, JSValue *argv) { @@ -457,7 +457,8 @@ static JSValue js_os_native_module_load(JSContext *js, JSValue self, int argc, J if (!handle) return JS_ThrowTypeError(js, "First argument must be a dylib object"); - return cell_rt_native_module_load(js, handle); + JSValue env = (argc >= 2) ? argv[1] : JS_NULL; + return cell_rt_native_module_load(js, handle, env); } static JSValue js_os_native_module_load_named(JSContext *js, JSValue self, int argc, JSValue *argv) @@ -473,7 +474,8 @@ static JSValue js_os_native_module_load_named(JSContext *js, JSValue self, int a if (!sym_name) return JS_EXCEPTION; - JSValue result = cell_rt_native_module_load_named(js, handle, sym_name); + JSValue env = (argc >= 3) ? argv[2] : JS_NULL; + JSValue result = cell_rt_native_module_load_named(js, handle, sym_name, env); JS_FreeCString(js, sym_name); return result; } @@ -653,8 +655,8 @@ static const JSCFunctionListEntry js_os_funcs[] = { MIST_FUNC_DEF(os, dylib_close, 1), MIST_FUNC_DEF(os, dylib_symbol, 2), MIST_FUNC_DEF(os, dylib_has_symbol, 2), - MIST_FUNC_DEF(os, native_module_load, 1), - MIST_FUNC_DEF(os, native_module_load_named, 2), + MIST_FUNC_DEF(os, native_module_load, 2), + MIST_FUNC_DEF(os, native_module_load_named, 3), MIST_FUNC_DEF(os, embedded_module, 1), MIST_FUNC_DEF(os, load_internal, 1), MIST_FUNC_DEF(os, internal_exists, 1), diff --git a/internal/shop.cm b/internal/shop.cm index 1c77f55c..ad43fe83 100644 --- a/internal/shop.cm +++ b/internal/shop.cm @@ -1009,6 +1009,7 @@ function execute_module(info) env = inject_env(inject) pkg = file_info.package env.use = make_use_fn(pkg) + env = stone(env) // Load compiled bytecode with env used = mach_load(mod_resolve.symbol, env) @@ -1044,6 +1045,7 @@ Shop.use = function use(path, package_context) { if (embedded) { embed_env = inject_env(SHOP_DEFAULT_INJECT) embed_env.use = make_use_fn(package_context) + embed_env = stone(embed_env) use_cache[embed_key] = mach_load(embedded, embed_env) return use_cache[embed_key] } @@ -1608,6 +1610,7 @@ Shop.load_as_mach = function(path, pkg) { inject = Shop.script_inject_for(file_info) env = inject_env(inject) env.use = make_use_fn(file_info.package) + env = stone(env) return mach_load(compiled, env) } @@ -1697,9 +1700,15 @@ Shop.use_native = function(path, package_context) { var handle = os.dylib_open(dylib_path) if (!handle) { print('Failed to open native dylib: ' + dylib_path); disrupt } + // Build env with runtime functions and capabilities + var inject = Shop.script_inject_for(file_info) + var env = inject_env(inject) + env.use = make_use_fn(pkg) + env = stone(env) + if (sym_name) - return os.native_module_load_named(handle, sym_name) - return os.native_module_load(handle) + return os.native_module_load_named(handle, sym_name, env) + return os.native_module_load(handle, env) } return Shop \ No newline at end of file diff --git a/qbe_emit.cm b/qbe_emit.cm index 747f154f..07c03cf9 100644 --- a/qbe_emit.cm +++ b/qbe_emit.cm @@ -23,10 +23,6 @@ var qbe_emit = function(ir, qbe, export_name) { return "u" + text(uid) } - var s = function(n) { - return "%s" + text(n) - } - var sanitize = function(lbl) { var r = replace(lbl, ".", "_") r = replace(r, "-", "_") @@ -49,6 +45,9 @@ var qbe_emit = function(ir, qbe, export_name) { str_id = str_id + 1 var escaped = replace(val, "\\", "\\\\") escaped = replace(escaped, "\"", "\\\"") + escaped = replace(escaped, "\n", "\\n") + escaped = replace(escaped, "\r", "\\r") + escaped = replace(escaped, "\t", "\\t") var line = "data " + label + ' = ' + '{ b "' + escaped + '", b 0 }' push(data_out, line) str_table[val] = label @@ -76,7 +75,8 @@ var qbe_emit = function(ir, qbe, export_name) { var instrs = fn.instructions var nr_slots = fn.nr_slots var nr_args = fn.nr_args - var captured = build_captured(fn) + var disruption_pc = fn.disruption_pc != null ? fn.disruption_pc : 0 + var has_handler = disruption_pc > 0 var name = is_main ? (export_name ? export_name : "cell_main") : "cell_fn_" + text(fn_idx) name = sanitize(name) var i = 0 @@ -94,44 +94,57 @@ var qbe_emit = function(ir, qbe, export_name) { var nr_elems = 0 var ei = 0 var elem_slot = 0 + var v = null + var lhs = null + var rhs = null + var obj = null + var chk = null + var pat_label = null + var flg_label = null // Function signature: (ctx, frame_ptr) → JSValue emit(`export function l $${name}(l %ctx, l %fp) {`) emit("@entry") - // Load all slots from frame into SSA variables - // Each slot is a JSValue (8 bytes) at fp + slot*8 - var off = 0 - i = 0 - while (i < nr_slots) { - off = i * 8 - emit(` %p${text(i)} =l add %fp, ${text(off)}`) - emit(` ${s(i)} =l loadl %p${text(i)}`) - i = i + 1 + // GC-safe slot access: every read/write goes through frame memory. + // %fp may become stale after GC-triggering calls — use refresh_fp(). + var s_read = function(slot) { + var t = fresh() + emit(` %${t} =l add %fp, ${text(slot * 8)}`) + emit(` %${t}v =l loadl %${t}`) + return `%${t}v` } - // Write-back: store SSA var to frame slot so closures see updates - var wb = function(slot) { - emit(` storel ${s(slot)}, %p${text(slot)}`) - } - - // Reload captured slots from frame (after invoke, closures may have modified them) - var reload_captured = function() { - var ri = 0 - while (ri < nr_slots) { - if (captured[text(ri)] == true) { - emit(` ${s(ri)} =l loadl %p${text(ri)}`) - } - ri = ri + 1 + var s_write = function(slot, val) { + var t = fresh() + var sv = val + if (!starts_with(val, "%")) { + sv = `%${t}c` + emit(` ${sv} =l copy ${val}`) } + emit(` %${t} =l add %fp, ${text(slot * 8)}`) + emit(` storel ${sv}, %${t}`) + } + + var refresh_fp = function() { + emit(` %fp =l call $cell_rt_refresh_fp(l %ctx)`) } // Walk instructions - // Slot loads above are not terminators var last_was_term = false i = 0 while (i < length(instrs)) { instr = instrs[i] + + // Emit @disruption_handler at the right flat index + // disruption_pc counts all entries (labels + instructions) + if (has_handler && i == disruption_pc) { + if (!last_was_term) { + emit(" jmp @disruption_handler") + } + emit("@disruption_handler") + last_was_term = false + } i = i + 1 // Labels are plain strings; skip _nop_ur_ pseudo-labels from streamline @@ -158,463 +171,534 @@ var qbe_emit = function(ir, qbe, export_name) { // --- Constants --- if (op == "int") { - emit(` ${s(a1)} =l copy ${text(a2 * 2)}`) - wb(a1) + s_write(a1, text(a2 * 2)) continue } if (op == "null") { - emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`) - wb(a1) + s_write(a1, text(qbe.js_null)) continue } if (op == "true") { - emit(` ${s(a1)} =l copy ${text(qbe.js_true)}`) - wb(a1) + s_write(a1, text(qbe.js_true)) continue } if (op == "false") { - emit(` ${s(a1)} =l copy ${text(qbe.js_false)}`) - wb(a1) + s_write(a1, text(qbe.js_false)) continue } if (op == "access") { + p = fresh() if (is_number(a2)) { if (is_integer(a2)) { - emit(` ${s(a1)} =l copy ${text(a2 * 2)}`) + s_write(a1, text(a2 * 2)) } else { - emit(` ${s(a1)} =l call $qbe_new_float64(l %ctx, d d_${text(a2)})`) + emit(` %${p} =l call $qbe_new_float64(l %ctx, d d_${text(a2)})`) + refresh_fp() + s_write(a1, `%${p}`) } } else if (is_text(a2)) { sl = intern_str(a2) - emit(` ${s(a1)} =l call $qbe_new_string(l %ctx, l ${sl})`) + emit(` %${p} =l call $qbe_new_string(l %ctx, l ${sl})`) + refresh_fp() + s_write(a1, `%${p}`) } else if (is_object(a2)) { if (a2.make == "intrinsic") { sl = intern_str(a2.name) - emit(` ${s(a1)} =l call $cell_rt_get_intrinsic(l %ctx, l ${sl})`) + emit(` %${p} =l call $cell_rt_get_intrinsic(l %ctx, l ${sl})`) + refresh_fp() + s_write(a1, `%${p}`) } else if (a2.kind == "number") { if (a2.number != null && is_integer(a2.number)) { - emit(` ${s(a1)} =l copy ${text(a2.number * 2)}`) + s_write(a1, text(a2.number * 2)) } else if (a2.number != null) { - emit(` ${s(a1)} =l call $qbe_new_float64(l %ctx, d d_${text(a2.number)})`) + emit(` %${p} =l call $qbe_new_float64(l %ctx, d d_${text(a2.number)})`) + refresh_fp() + s_write(a1, `%${p}`) } else { - emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`) + s_write(a1, text(qbe.js_null)) } } else if (a2.kind == "text") { sl = intern_str(a2.value) - emit(` ${s(a1)} =l call $qbe_new_string(l %ctx, l ${sl})`) + emit(` %${p} =l call $qbe_new_string(l %ctx, l ${sl})`) + refresh_fp() + s_write(a1, `%${p}`) } else if (a2.kind == "true") { - emit(` ${s(a1)} =l copy ${text(qbe.js_true)}`) + s_write(a1, text(qbe.js_true)) } else if (a2.kind == "false") { - emit(` ${s(a1)} =l copy ${text(qbe.js_false)}`) + s_write(a1, text(qbe.js_false)) } else if (a2.kind == "null") { - emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`) + s_write(a1, text(qbe.js_null)) } else { - emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`) + s_write(a1, text(qbe.js_null)) } } else { - emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`) + s_write(a1, text(qbe.js_null)) } - wb(a1) continue } // --- Movement --- if (op == "move") { - emit(` ${s(a1)} =l copy ${s(a2)}`) - wb(a1) + v = s_read(a2) + s_write(a1, v) continue } // --- Generic arithmetic (VM dispatches int/float) --- if (op == "add") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(` %${p} =l call $cell_rt_add(l %ctx, l ${s(a2)}, l ${s(a3)})`) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(` %${p} =l call $cell_rt_add(l %ctx, l ${lhs}, l ${rhs})`) + refresh_fp() + s_write(a1, `%${p}`) continue } if (op == "subtract") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.sub(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.sub(p, "%ctx", lhs, rhs)) + refresh_fp() + s_write(a1, `%${p}`) continue } if (op == "multiply") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.mul(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.mul(p, "%ctx", lhs, rhs)) + refresh_fp() + s_write(a1, `%${p}`) continue } if (op == "divide") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.div(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.div(p, "%ctx", lhs, rhs)) + refresh_fp() + s_write(a1, `%${p}`) continue } if (op == "modulo") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.mod(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.mod(p, "%ctx", lhs, rhs)) + refresh_fp() + s_write(a1, `%${p}`) continue } if (op == "negate") { + v = s_read(a2) p = fresh() - emit(qbe.neg(p, "%ctx", s(a2))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.neg(p, "%ctx", v)) + refresh_fp() + s_write(a1, `%${p}`) continue } if (op == "pow") { - emit(` ${s(a1)} =l call $qbe_float_pow(l %ctx, l ${s(a2)}, l ${s(a3)})`) - wb(a1) + lhs = s_read(a2) + rhs = s_read(a3) + p = fresh() + emit(` %${p} =l call $qbe_float_pow(l %ctx, l ${lhs}, l ${rhs})`) + refresh_fp() + s_write(a1, `%${p}`) continue } // --- String concat --- if (op == "concat") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.concat(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.concat(p, "%ctx", lhs, rhs)) + refresh_fp() + s_write(a1, `%${p}`) continue } - // --- Type checks — use qbe.cm macros --- + // --- Type checks — use qbe.cm macros (no GC, no refresh) --- if (op == "is_int") { + v = s_read(a2) p = fresh() - emit(qbe.is_int(p, s(a2))) + emit(qbe.is_int(p, v)) emit(qbe.new_bool(p + ".r", "%" + p)) - emit(` ${s(a1)} =l copy %${p}.r`) - wb(a1) + s_write(a1, `%${p}.r`) continue } if (op == "is_text") { + v = s_read(a2) p = fresh() - emit(qbe.is_imm_text(p, s(a2))) + emit(` %${p} =w call $JS_IsText(l ${v})`) emit(qbe.new_bool(p + ".r", "%" + p)) - emit(` ${s(a1)} =l copy %${p}.r`) - wb(a1) + s_write(a1, `%${p}.r`) continue } if (op == "is_num") { + v = s_read(a2) p = fresh() - emit(qbe.is_number(p, s(a2))) + emit(qbe.is_number(p, v)) emit(qbe.new_bool(p + ".r", "%" + p)) - emit(` ${s(a1)} =l copy %${p}.r`) - wb(a1) + s_write(a1, `%${p}.r`) continue } if (op == "is_bool") { + v = s_read(a2) p = fresh() - emit(qbe.is_bool(p, s(a2))) + emit(qbe.is_bool(p, v)) emit(qbe.new_bool(p + ".r", "%" + p)) - emit(` ${s(a1)} =l copy %${p}.r`) - wb(a1) + s_write(a1, `%${p}.r`) continue } if (op == "is_null") { + v = s_read(a2) p = fresh() - emit(qbe.is_null(p, s(a2))) + emit(qbe.is_null(p, v)) emit(qbe.new_bool(p + ".r", "%" + p)) - emit(` ${s(a1)} =l copy %${p}.r`) - wb(a1) + s_write(a1, `%${p}.r`) continue } if (op == "is_identical") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.is_identical(p, s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.is_identical(p, lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "is_array") { + v = s_read(a2) p = fresh() - emit(` %${p} =w call $JS_IsArray(l ${s(a2)})`) + emit(` %${p} =w call $JS_IsArray(l ${v})`) emit(qbe.new_bool(p + ".r", "%" + p)) - emit(` ${s(a1)} =l copy %${p}.r`) - wb(a1) + s_write(a1, `%${p}.r`) continue } if (op == "is_func") { + v = s_read(a2) p = fresh() - emit(` %${p} =w call $JS_IsFunction(l ${s(a2)})`) + emit(` %${p} =w call $JS_IsFunction(l ${v})`) emit(qbe.new_bool(p + ".r", "%" + p)) - emit(` ${s(a1)} =l copy %${p}.r`) - wb(a1) + s_write(a1, `%${p}.r`) continue } if (op == "is_record") { + v = s_read(a2) p = fresh() - emit(` %${p} =w call $JS_IsRecord(l ${s(a2)})`) + emit(` %${p} =w call $JS_IsRecord(l ${v})`) emit(qbe.new_bool(p + ".r", "%" + p)) - emit(` ${s(a1)} =l copy %${p}.r`) - wb(a1) + s_write(a1, `%${p}.r`) continue } if (op == "is_stone") { + v = s_read(a2) p = fresh() - emit(` %${p} =w call $JS_IsStone(l ${s(a2)})`) + emit(` %${p} =w call $JS_IsStone(l ${v})`) emit(qbe.new_bool(p + ".r", "%" + p)) - emit(` ${s(a1)} =l copy %${p}.r`) - wb(a1) + s_write(a1, `%${p}.r`) continue } if (op == "is_proxy") { + v = s_read(a2) p = fresh() - emit(` %${p} =w call $cell_rt_is_proxy(l %ctx, l ${s(a2)})`) + emit(` %${p} =w call $cell_rt_is_proxy(l %ctx, l ${v})`) emit(qbe.new_bool(p + ".r", "%" + p)) - emit(` ${s(a1)} =l copy %${p}.r`) - wb(a1) + s_write(a1, `%${p}.r`) continue } - // --- Comparisons (int path) --- + // --- Comparisons (int path, no GC) --- if (op == "eq_int") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.eq_int(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.eq_int(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "ne_int") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.ne_int(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.ne_int(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "lt_int") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.lt_int(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.lt_int(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "gt_int") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.gt_int(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.gt_int(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "le_int") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.le_int(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.le_int(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "ge_int") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.ge_int(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.ge_int(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } // --- Comparisons (float/text/bool) --- if (op == "eq_float") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.eq_float(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.eq_float(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "ne_float") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.ne_float(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.ne_float(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "lt_float") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.lt_float(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.lt_float(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "le_float") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.le_float(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.le_float(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "gt_float") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.gt_float(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.gt_float(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "ge_float") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.ge_float(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.ge_float(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "eq_text") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.eq_text(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.eq_text(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "ne_text") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.ne_text(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.ne_text(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "lt_text" || op == "gt_text" || op == "le_text" || op == "ge_text") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(` ${s(a1)} =l call $cell_rt_${op}(l %ctx, l ${s(a2)}, l ${s(a3)})`) - wb(a1) + emit(` %${p} =l call $cell_rt_${op}(l %ctx, l ${lhs}, l ${rhs})`) + s_write(a1, `%${p}`) continue } if (op == "eq_bool") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.eq_bool(p, s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.eq_bool(p, lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "ne_bool") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.ne_bool(p, s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.ne_bool(p, lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "eq_tol" || op == "ne_tol") { - emit(` ${s(a1)} =l call $cell_rt_${op}(l %ctx, l ${s(a2)}, l ${s(a3)})`) - wb(a1) + lhs = s_read(a2) + rhs = s_read(a3) + p = fresh() + emit(` %${p} =l call $cell_rt_${op}(l %ctx, l ${lhs}, l ${rhs})`) + s_write(a1, `%${p}`) continue } // --- Boolean ops --- if (op == "not") { + v = s_read(a2) p = fresh() - emit(qbe.lnot(p, "%ctx", s(a2))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.lnot(p, "%ctx", v)) + s_write(a1, `%${p}`) continue } if (op == "and") { - emit(` ${s(a1)} =l and ${s(a2)}, ${s(a3)}`) - wb(a1) + lhs = s_read(a2) + rhs = s_read(a3) + p = fresh() + emit(` %${p} =l call $cell_rt_and(l %ctx, l ${lhs}, l ${rhs})`) + s_write(a1, `%${p}`) continue } if (op == "or") { - emit(` ${s(a1)} =l or ${s(a2)}, ${s(a3)}`) - wb(a1) + lhs = s_read(a2) + rhs = s_read(a3) + p = fresh() + emit(` %${p} =l call $cell_rt_or(l %ctx, l ${lhs}, l ${rhs})`) + s_write(a1, `%${p}`) continue } - // --- Bitwise ops — use qbe.cm macros --- + // --- Bitwise ops — use qbe.cm macros (no GC) --- if (op == "bitnot") { + v = s_read(a2) p = fresh() - emit(qbe.bnot(p, "%ctx", s(a2))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.bnot(p, "%ctx", v)) + s_write(a1, `%${p}`) continue } if (op == "bitand") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.band(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.band(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "bitor") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.bor(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.bor(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "bitxor") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.bxor(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.bxor(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "shl") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.shl(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.shl(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "shr") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.shr(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.shr(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } if (op == "ushr") { + lhs = s_read(a2) + rhs = s_read(a3) p = fresh() - emit(qbe.ushr(p, "%ctx", s(a2), s(a3))) - emit(` ${s(a1)} =l copy %${p}`) - wb(a1) + emit(qbe.ushr(p, "%ctx", lhs, rhs)) + s_write(a1, `%${p}`) continue } - // --- Property access — runtime calls --- + // --- Property access — runtime calls [G] --- if (op == "load_field") { + v = s_read(a2) pn = null if (is_text(a3)) pn = a3 else if (is_object(a3) && a3.name != null) pn = a3.name else if (is_object(a3) && a3.value != null) pn = a3.value + p = fresh() if (pn != null) { sl = intern_str(pn) - emit(` ${s(a1)} =l call $cell_rt_load_field(l %ctx, l ${s(a2)}, l ${sl})`) + emit(` %${p} =l call $cell_rt_load_field(l %ctx, l ${v}, l ${sl})`) } else { - emit(` ${s(a1)} =l call $cell_rt_load_dynamic(l %ctx, l ${s(a2)}, l ${s(a3)})`) + lhs = s_read(a3) + emit(` %${p} =l call $cell_rt_load_dynamic(l %ctx, l ${v}, l ${lhs})`) } - wb(a1) + refresh_fp() + s_write(a1, `%${p}`) continue } if (op == "load_index") { - emit(` ${s(a1)} =l call $cell_rt_load_index(l %ctx, l ${s(a2)}, l ${s(a3)})`) - wb(a1) + lhs = s_read(a2) + rhs = s_read(a3) + p = fresh() + emit(` %${p} =l call $cell_rt_load_index(l %ctx, l ${lhs}, l ${rhs})`) + refresh_fp() + s_write(a1, `%${p}`) continue } if (op == "load_dynamic") { + v = s_read(a2) pn = null if (is_text(a3)) pn = a3 else if (is_object(a3) && a3.name != null) pn = a3.name else if (is_object(a3) && a3.value != null) pn = a3.value + p = fresh() if (pn != null) { sl = intern_str(pn) - emit(` ${s(a1)} =l call $cell_rt_load_field(l %ctx, l ${s(a2)}, l ${sl})`) + emit(` %${p} =l call $cell_rt_load_field(l %ctx, l ${v}, l ${sl})`) } else { - emit(` ${s(a1)} =l call $cell_rt_load_dynamic(l %ctx, l ${s(a2)}, l ${s(a3)})`) + lhs = s_read(a3) + emit(` %${p} =l call $cell_rt_load_dynamic(l %ctx, l ${v}, l ${lhs})`) } - wb(a1) + refresh_fp() + s_write(a1, `%${p}`) continue } if (op == "store_field") { // IR: ["store_field", obj, val, prop] → C: (ctx, val, obj, name) + obj = s_read(a1) + v = s_read(a2) pn = null if (is_text(a3)) { pn = a3 @@ -624,43 +708,55 @@ var qbe_emit = function(ir, qbe, export_name) { } if (pn != null) { sl = intern_str(pn) - emit(` call $cell_rt_store_field(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${sl})`) + emit(` call $cell_rt_store_field(l %ctx, l ${v}, l ${obj}, l ${sl})`) } else { - emit(` call $cell_rt_store_dynamic(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${s(a3)})`) + lhs = s_read(a3) + emit(` call $cell_rt_store_dynamic(l %ctx, l ${v}, l ${obj}, l ${lhs})`) } + refresh_fp() continue } if (op == "store_index") { // IR: ["store_index", obj, val, idx] → C: (ctx, val, obj, idx) - emit(` call $cell_rt_store_index(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${s(a3)})`) + obj = s_read(a1) + v = s_read(a2) + lhs = s_read(a3) + emit(` call $cell_rt_store_index(l %ctx, l ${v}, l ${obj}, l ${lhs})`) + refresh_fp() continue } if (op == "store_dynamic") { // IR: ["store_dynamic", obj, val, key] → C: (ctx, val, obj, key) + obj = s_read(a1) + v = s_read(a2) pn = null if (is_text(a3)) pn = a3 else if (is_object(a3) && a3.name != null) pn = a3.name else if (is_object(a3) && a3.value != null) pn = a3.value if (pn != null) { sl = intern_str(pn) - emit(` call $cell_rt_store_field(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${sl})`) + emit(` call $cell_rt_store_field(l %ctx, l ${v}, l ${obj}, l ${sl})`) } else { - emit(` call $cell_rt_store_dynamic(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${s(a3)})`) + lhs = s_read(a3) + emit(` call $cell_rt_store_dynamic(l %ctx, l ${v}, l ${obj}, l ${lhs})`) } + refresh_fp() continue } - // --- Closure access --- + // --- Closure access (no GC) --- if (op == "get") { // mcode: get(dest, slot, depth) — a2=slot, a3=depth - emit(` ${s(a1)} =l call $cell_rt_get_closure(l %ctx, l %fp, l ${text(a3)}, l ${text(a2)})`) - wb(a1) + p = fresh() + emit(` %${p} =l call $cell_rt_get_closure(l %ctx, l %fp, l ${text(a3)}, l ${text(a2)})`) + s_write(a1, `%${p}`) continue } if (op == "put") { // mcode: put(val, slot, depth) — a2=slot, a3=depth - emit(` call $cell_rt_put_closure(l %ctx, l %fp, l ${s(a1)}, l ${text(a3)}, l ${text(a2)})`) + v = s_read(a1) + emit(` call $cell_rt_put_closure(l %ctx, l %fp, l ${v}, l ${text(a3)}, l ${text(a2)})`) continue } @@ -672,161 +768,248 @@ var qbe_emit = function(ir, qbe, export_name) { continue } if (op == "jump_true") { + v = s_read(a1) p = fresh() - emit(` %${p} =w call $JS_ToBool(l %ctx, l ${s(a1)})`) + emit(` %${p} =w call $JS_ToBool(l %ctx, l ${v})`) emit(` jnz %${p}, @${sanitize(a2)}, @${p}_f`) emit(`@${p}_f`) continue } if (op == "jump_false") { + v = s_read(a1) p = fresh() - emit(` %${p} =w call $JS_ToBool(l %ctx, l ${s(a1)})`) + emit(` %${p} =w call $JS_ToBool(l %ctx, l ${v})`) emit(` jnz %${p}, @${p}_t, @${sanitize(a2)}`) emit(`@${p}_t`) continue } if (op == "jump_null") { + v = s_read(a1) p = fresh() - emit(` %${p} =w ceql ${s(a1)}, ${text(qbe.js_null)}`) + emit(` %${p} =w ceql ${v}, ${text(qbe.js_null)}`) emit(` jnz %${p}, @${sanitize(a2)}, @${p}_nn`) emit(`@${p}_nn`) continue } if (op == "jump_not_null") { + v = s_read(a1) p = fresh() - emit(` %${p} =w cnel ${s(a1)}, ${text(qbe.js_null)}`) + emit(` %${p} =w cnel ${v}, ${text(qbe.js_null)}`) emit(` jnz %${p}, @${sanitize(a2)}, @${p}_n`) emit(`@${p}_n`) continue } - if (op == "wary_true") { - p = fresh() - emit(` %${p} =w call $JS_ToBool(l %ctx, l ${s(a1)})`) - emit(` jnz %${p}, @${sanitize(a2)}, @${p}_f`) - emit(`@${p}_f`) - continue - } - if (op == "wary_false") { - p = fresh() - emit(` %${p} =w call $JS_ToBool(l %ctx, l ${s(a1)})`) - emit(` jnz %${p}, @${p}_t, @${sanitize(a2)}`) - emit(`@${p}_t`) - continue - } - // --- Function calls --- + // --- Function calls [G] --- if (op == "frame") { - emit(` ${s(a1)} =l call $cell_rt_frame(l %ctx, l ${s(a2)}, l ${text(a3)})`) - wb(a1) + v = s_read(a2) + p = fresh() + emit(` %${p} =l call $cell_rt_frame(l %ctx, l ${v}, l ${text(a3)})`) + refresh_fp() + s_write(a1, `%${p}`) continue } if (op == "setarg") { - emit(` call $cell_rt_setarg(l ${s(a1)}, l ${text(a2)}, l ${s(a3)})`) + v = s_read(a1) + lhs = s_read(a3) + emit(` call $cell_rt_setarg(l ${v}, l ${text(a2)}, l ${lhs})`) continue } if (op == "invoke") { - emit(` ${s(a2)} =l call $cell_rt_invoke(l %ctx, l ${s(a1)})`) - wb(a2) - reload_captured() + v = s_read(a1) + p = fresh() + emit(` %${p} =l call $cell_rt_invoke(l %ctx, l ${v})`) + chk = fresh() + emit(` %${chk} =w ceql %${p}, 15`) + if (has_handler) { + emit(` jnz %${chk}, @disruption_handler, @${chk}_ok`) + } else { + emit(` jnz %${chk}, @${chk}_exc, @${chk}_ok`) + emit(`@${chk}_exc`) + emit(` ret 15`) + } + emit(`@${chk}_ok`) + refresh_fp() + s_write(a2, `%${p}`) continue } if (op == "tail_invoke") { - emit(` ${s(a2)} =l call $cell_rt_invoke(l %ctx, l ${s(a1)})`) - wb(a2) - reload_captured() + v = s_read(a1) + p = fresh() + emit(` %${p} =l call $cell_rt_invoke(l %ctx, l ${v})`) + chk = fresh() + emit(` %${chk} =w ceql %${p}, 15`) + if (has_handler) { + emit(` jnz %${chk}, @disruption_handler, @${chk}_ok`) + } else { + emit(` jnz %${chk}, @${chk}_exc, @${chk}_ok`) + emit(`@${chk}_exc`) + emit(` ret 15`) + } + emit(`@${chk}_ok`) + refresh_fp() + s_write(a2, `%${p}`) continue } if (op == "goframe") { - emit(` ${s(a1)} =l call $cell_rt_goframe(l %ctx, l ${s(a2)}, l ${text(a3)})`) - wb(a1) + v = s_read(a2) + p = fresh() + emit(` %${p} =l call $cell_rt_goframe(l %ctx, l ${v}, l ${text(a3)})`) + refresh_fp() + s_write(a1, `%${p}`) continue } if (op == "goinvoke") { - emit(` %_goret =l call $cell_rt_goinvoke(l %ctx, l ${s(a1)})`) - emit(` ret %_goret`) + v = s_read(a1) + p = fresh() + emit(` %${p} =l call $cell_rt_goinvoke(l %ctx, l ${v})`) + chk = fresh() + emit(` %${chk} =w ceql %${p}, 15`) + if (has_handler) { + emit(` jnz %${chk}, @disruption_handler, @${chk}_ok`) + emit(`@${chk}_ok`) + refresh_fp() + emit(` ret %${p}`) + } else { + emit(` jnz %${chk}, @${chk}_exc, @${chk}_ok`) + emit(`@${chk}_exc`) + emit(` ret 15`) + emit(`@${chk}_ok`) + emit(` ret %${p}`) + } last_was_term = true continue } - // --- Function object creation --- + // --- Function object creation [G] --- if (op == "function") { - emit(` ${s(a1)} =l call $cell_rt_make_function(l %ctx, l ${text(a2)}, l %fp)`) - wb(a1) + p = fresh() + emit(` %${p} =l call $cell_rt_make_function(l %ctx, l ${text(a2)}, l %fp)`) + refresh_fp() + s_write(a1, `%${p}`) continue } - // --- Record/Array creation --- + // --- Record/Array creation [G] --- if (op == "record") { - emit(` ${s(a1)} =l call $JS_NewObject(l %ctx)`) - wb(a1) + p = fresh() + emit(` %${p} =l call $JS_NewObject(l %ctx)`) + refresh_fp() + s_write(a1, `%${p}`) continue } if (op == "array") { - nr_elems = a2 != null ? a2 : 0 - emit(` ${s(a1)} =l call $JS_NewArray(l %ctx)`) - ei = 0 - while (ei < nr_elems) { - elem_slot = instr[3 + ei] - emit(` call $JS_SetPropertyNumber(l %ctx, l ${s(a1)}, l ${text(ei)}, l ${s(elem_slot)})`) - ei = ei + 1 - } - wb(a1) + // a2 is a size hint; elements are pushed via separate push instructions + p = fresh() + emit(` %${p} =l call $JS_NewArray(l %ctx)`) + refresh_fp() + s_write(a1, `%${p}`) continue } - // --- Array push/pop --- + // --- Array push/pop [G] --- if (op == "push") { - emit(` call $cell_rt_push(l %ctx, l ${s(a1)}, l ${s(a2)})`) + lhs = s_read(a1) + rhs = s_read(a2) + emit(` call $cell_rt_push(l %ctx, l ${lhs}, l ${rhs})`) + refresh_fp() continue } if (op == "pop") { - emit(` ${s(a1)} =l call $cell_rt_pop(l %ctx, l ${s(a2)})`) - wb(a1) + v = s_read(a2) + p = fresh() + emit(` %${p} =l call $cell_rt_pop(l %ctx, l ${v})`) + refresh_fp() + s_write(a1, `%${p}`) continue } - // --- Length --- + // --- Length [G] --- if (op == "length") { - emit(` ${s(a1)} =l call $JS_CellLength(l %ctx, l ${s(a2)})`) - wb(a1) + v = s_read(a2) + p = fresh() + emit(` %${p} =l call $JS_CellLength(l %ctx, l ${v})`) + refresh_fp() + s_write(a1, `%${p}`) continue } // --- Misc --- if (op == "return") { - emit(` ret ${s(a1)}`) + v = s_read(a1) + emit(` ret ${v}`) last_was_term = true continue } if (op == "disrupt") { emit(` call $cell_rt_disrupt(l %ctx)`) - emit(` ret ${text(qbe.js_null)}`) + if (has_handler) { + emit(" jmp @disruption_handler") + } else { + emit(` ret 15`) + } last_was_term = true continue } if (op == "delete") { + v = s_read(a2) pn = null if (is_text(a3)) pn = a3 else if (is_object(a3) && a3.name != null) pn = a3.name else if (is_object(a3) && a3.value != null) pn = a3.value + p = fresh() if (pn != null) { sl = intern_str(pn) - emit(` ${s(a1)} =l call $cell_rt_delete(l %ctx, l ${s(a2)}, l ${sl})`) + emit(` %${p} =l call $cell_rt_delete(l %ctx, l ${v}, l ${sl})`) } else { - emit(` ${s(a1)} =l call $cell_rt_delete(l %ctx, l ${s(a2)}, l ${s(a3)})`) + lhs = s_read(a3) + emit(` %${p} =l call $cell_rt_delete(l %ctx, l ${v}, l ${lhs})`) } - wb(a1) + refresh_fp() + s_write(a1, `%${p}`) continue } - if (op == "typeof") { - emit(` ${s(a1)} =l call $cell_rt_typeof(l %ctx, l ${s(a2)})`) - wb(a1) + + // --- set_var [G] --- + + if (op == "set_var") { + // IR: ["set_var", name_string, value_slot] + v = s_read(a2) + sl = intern_str(a1) + emit(` call $cell_rt_set_var(l %ctx, l ${v}, l ${sl})`) + refresh_fp() + continue + } + + // --- in [G] --- + + if (op == "in") { + // IR: ["in", dest, key_slot, obj_slot] + lhs = s_read(a2) + rhs = s_read(a3) + p = fresh() + emit(` %${p} =l call $cell_rt_in(l %ctx, l ${lhs}, l ${rhs})`) + refresh_fp() + s_write(a1, `%${p}`) + continue + } + + // --- regexp [G] --- + + if (op == "regexp") { + // IR: ["regexp", dest_slot, pattern_string, flags_string] + pat_label = intern_str(a2) + flg_label = intern_str(a3) + p = fresh() + emit(` %${p} =l call $cell_rt_regexp(l %ctx, l ${pat_label}, l ${flg_label})`) + refresh_fp() + s_write(a1, `%${p}`) continue } @@ -840,7 +1023,11 @@ var qbe_emit = function(ir, qbe, export_name) { } emit("@disrupt") emit(` call $cell_rt_disrupt(l %ctx)`) - emit(` ret ${text(qbe.js_null)}`) + if (has_handler) { + emit(" jmp @disruption_handler") + } else { + emit(` ret 15`) + } emit("}") emit("") @@ -850,70 +1037,6 @@ var qbe_emit = function(ir, qbe, export_name) { // Main: compile all functions then main // ============================================================ - // ============================================================ - // Pre-scan: find which slots each function has that are modified - // by child closures (via "put" instructions at depth=1). - // Build a map: fn_idx → array of captured slot numbers. - // ============================================================ - - // For each function, find which fn_idxes it creates via "function" op - var find_children = function(fn_instrs) { - var children = [] - var ci = 0 - var cinstr = null - while (ci < length(fn_instrs)) { - cinstr = fn_instrs[ci] - ci = ci + 1 - if (!is_array(cinstr)) continue - if (cinstr[0] == "function") { - push(children, cinstr[2]) - } - } - return children - } - - // For a child function, find which parent slots it writes to via put(val, slot, depth=1) - var find_put_slots = function(fn_instrs) { - var slots = [] - var pi = 0 - var pinstr = null - while (pi < length(fn_instrs)) { - pinstr = fn_instrs[pi] - pi = pi + 1 - if (!is_array(pinstr)) continue - // put format: ["put", val, slot, depth] - if (pinstr[0] == "put" && pinstr[3] == 1) { - push(slots, pinstr[2]) - } - } - return slots - } - - // Build captured_slots for each function (and main) - var build_captured = function(fn) { - var children = find_children(fn.instructions) - var captured = {} - var bi = 0 - var child_idx = 0 - var child_fn = null - var pslots = null - var si = 0 - while (bi < length(children)) { - child_idx = children[bi] - bi = bi + 1 - if (child_idx >= 0 && child_idx < length(ir.functions)) { - child_fn = ir.functions[child_idx] - pslots = find_put_slots(child_fn.instructions) - si = 0 - while (si < length(pslots)) { - captured[text(pslots[si])] = true - si = si + 1 - } - } - } - return captured - } - var fi = 0 while (fi < length(ir.functions)) { compile_fn(ir.functions[fi], fi, false) diff --git a/source/mach.c b/source/mach.c index ec8daa77..8df30ac5 100644 --- a/source/mach.c +++ b/source/mach.c @@ -85,6 +85,11 @@ static JSValue *mach_materialize_cpool(JSContext *ctx, MachCPoolEntry *entries, /* ---- 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: WARNING env not stone (code=%s file=%s)\n", + code->name_cstr ? code->name_cstr : "", + code->filename_cstr ? code->filename_cstr : ""); + } JSGCRef env_ref; JS_PushGCRef(ctx, &env_ref); env_ref.val = env; @@ -809,7 +814,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, DT(MACH_ADD), DT(MACH_SUB), DT(MACH_MUL), DT(MACH_DIV), DT(MACH_MOD), DT(MACH_POW), - DT(MACH_NEG), DT(MACH_INC), DT(MACH_DEC), + DT(MACH_NEG), DT(MACH_EQ), DT(MACH_NEQ), DT(MACH_LT), DT(MACH_LE), DT(MACH_GT), DT(MACH_GE), @@ -1218,38 +1223,6 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, VM_BREAK(); } - VM_CASE(MACH_INC): { - JSValue v = frame->slots[b]; - if (JS_IsInt(v)) { - int32_t i = JS_VALUE_GET_INT(v); - if (i == INT32_MAX) - frame->slots[a] = JS_NewFloat64(ctx, (double)i + 1); - else - frame->slots[a] = JS_NewInt32(ctx, i + 1); - } else { - double d; - JS_ToFloat64(ctx, &d, v); - frame->slots[a] = JS_NewFloat64(ctx, d + 1); - } - VM_BREAK(); - } - - VM_CASE(MACH_DEC): { - 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 - 1); - else - frame->slots[a] = JS_NewInt32(ctx, i - 1); - } else { - double d; - JS_ToFloat64(ctx, &d, v); - frame->slots[a] = JS_NewFloat64(ctx, d - 1); - } - VM_BREAK(); - } - VM_CASE(MACH_LNOT): { int bval = JS_ToBool(ctx, frame->slots[b]); frame->slots[a] = JS_NewBool(ctx, !bval); @@ -3331,8 +3304,6 @@ static void dump_register_code(JSContext *ctx, JSCodeRegister *code, int indent) /* A, B: move, unary ops */ case MACH_MOVE: case MACH_NEG: - case MACH_INC: - case MACH_DEC: case MACH_LNOT: case MACH_BNOT: printf("r%d, r%d", a, b); diff --git a/source/qbe_helpers.c b/source/qbe_helpers.c index c996118b..75882fb0 100644 --- a/source/qbe_helpers.c +++ b/source/qbe_helpers.c @@ -260,23 +260,56 @@ void cell_rt_store_index(JSContext *ctx, JSValue val, JSValue arr, /* --- Intrinsic/global lookup --- */ +/* Native module environment — set before executing a native module's cell_main. + Contains runtime functions (starts_with, ends_with, etc.) and use(). */ +static JSGCRef g_native_env_ref; +static int g_has_native_env = 0; + +void cell_rt_set_native_env(JSContext *ctx, JSValue env) { + if (!JS_IsNull(env) && !JS_IsStone(env)) { + fprintf(stderr, "cell_rt_set_native_env: WARNING env not stone\n"); + } + if (g_has_native_env) + JS_DeleteGCRef(ctx, &g_native_env_ref); + if (!JS_IsNull(env)) { + JS_AddGCRef(ctx, &g_native_env_ref); + g_native_env_ref.val = env; + g_has_native_env = 1; + } else { + g_has_native_env = 0; + } +} + JSValue cell_rt_get_intrinsic(JSContext *ctx, const char *name) { + /* Check native env first (runtime-provided functions like starts_with) */ + if (g_has_native_env) { + JSValue v = JS_GetPropertyStr(ctx, g_native_env_ref.val, name); + if (!JS_IsNull(v)) return v; + } return JS_GetPropertyStr(ctx, ctx->global_obj, name); } /* --- Closure access --- - Slot 511 in each frame stores a pointer to the enclosing frame. - Walking depth levels up the chain gives the target frame. */ + Slot 511 in each frame stores the magic ID (registry index) of the + function that owns this frame. cell_rt_get/put_closure re-derive + the enclosing frame from the function's GC ref at call time, so + pointers stay valid even if GC moves frames. */ #define QBE_FRAME_OUTER_SLOT 511 +static JSValue *derive_outer_fp(int magic); + JSValue cell_rt_get_closure(JSContext *ctx, void *fp, int64_t depth, int64_t slot) { JSValue *frame = (JSValue *)fp; for (int64_t d = 0; d < depth; d++) { - void *outer = (void *)(uintptr_t)frame[QBE_FRAME_OUTER_SLOT]; - if (!outer) return JS_NULL; - frame = (JSValue *)outer; + /* fp[511] stores the magic ID (registry index) of the function + that owns this frame. derive_outer_fp re-derives the enclosing + frame from the function's GC ref, so it's always current even + if GC moved the frame. */ + int magic = (int)(int64_t)frame[QBE_FRAME_OUTER_SLOT]; + frame = derive_outer_fp(magic); + if (!frame) return JS_NULL; } return frame[slot]; } @@ -285,13 +318,46 @@ void cell_rt_put_closure(JSContext *ctx, void *fp, JSValue val, int64_t depth, int64_t slot) { JSValue *frame = (JSValue *)fp; for (int64_t d = 0; d < depth; d++) { - void *outer = (void *)(uintptr_t)frame[QBE_FRAME_OUTER_SLOT]; - if (!outer) return; - frame = (JSValue *)outer; + int magic = (int)(int64_t)frame[QBE_FRAME_OUTER_SLOT]; + frame = derive_outer_fp(magic); + if (!frame) return; } frame[slot] = val; } +/* --- GC-managed AOT frame stack --- + Each AOT function call pushes a GC ref so the GC can find and + update frame pointers when it moves objects. cell_rt_refresh_fp + re-derives the slot pointer after any GC-triggering call. */ + +#define MAX_AOT_DEPTH 256 +static JSGCRef g_aot_gc_refs[MAX_AOT_DEPTH]; +static int g_aot_depth = 0; + +JSValue *cell_rt_enter_frame(JSContext *ctx, int64_t nr_slots) { + if (g_aot_depth >= MAX_AOT_DEPTH) + return NULL; + JSFrameRegister *frame = alloc_frame_register(ctx, (int)nr_slots); + if (!frame) return NULL; + JSGCRef *ref = &g_aot_gc_refs[g_aot_depth]; + JS_AddGCRef(ctx, ref); + ref->val = JS_MKPTR(frame); + g_aot_depth++; + return (JSValue *)frame->slots; +} + +JSValue *cell_rt_refresh_fp(JSContext *ctx) { + (void)ctx; + JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR( + g_aot_gc_refs[g_aot_depth - 1].val); + return (JSValue *)frame->slots; +} + +void cell_rt_leave_frame(JSContext *ctx) { + g_aot_depth--; + JS_DeleteGCRef(ctx, &g_aot_gc_refs[g_aot_depth]); +} + /* --- Function creation and calling --- */ typedef JSValue (*cell_compiled_fn)(JSContext *ctx, void *fp); @@ -305,7 +371,8 @@ typedef JSValue (*cell_compiled_fn)(JSContext *ctx, void *fp); static struct { void *dl_handle; int fn_idx; - void *outer_fp; + JSGCRef frame_ref; /* independent GC ref for enclosing frame */ + int has_frame_ref; } g_native_fn_registry[MAX_NATIVE_FN]; static int g_native_fn_count = 0; @@ -313,6 +380,16 @@ static int g_native_fn_count = 0; /* Set before executing a native module's cell_main */ static void *g_current_dl_handle = NULL; +/* Derive the outer frame's slots pointer from the closure's own GC ref. + Each closure keeps an independent GC ref so the enclosing frame + survives even after cell_rt_leave_frame pops the stack ref. */ +static JSValue *derive_outer_fp(int magic) { + if (!g_native_fn_registry[magic].has_frame_ref) return NULL; + JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR( + g_native_fn_registry[magic].frame_ref.val); + return (JSValue *)frame->slots; +} + static JSValue cell_fn_trampoline(JSContext *ctx, JSValue this_val, int argc, JSValue *argv, int magic) { if (magic < 0 || magic >= g_native_fn_count) @@ -328,27 +405,44 @@ static JSValue cell_fn_trampoline(JSContext *ctx, JSValue this_val, if (!fn) return JS_ThrowTypeError(ctx, "native function %s not found in dylib", name); - /* Allocate frame: slot 0 = this, slots 1..argc = args */ - JSValue frame[512]; - memset(frame, 0, sizeof(frame)); - frame[0] = this_val; + /* Allocate GC-managed frame: slot 0 = this, slots 1..argc = args */ + JSValue *fp = cell_rt_enter_frame(ctx, 512); + if (!fp) return JS_EXCEPTION; + fp[0] = this_val; for (int i = 0; i < argc && i < 510; i++) - frame[1 + i] = argv[i]; + fp[1 + i] = argv[i]; - /* Link to outer frame for closure access */ - frame[QBE_FRAME_OUTER_SLOT] = (JSValue)(uintptr_t)g_native_fn_registry[magic].outer_fp; + /* Store the magic ID (registry index) so cell_rt_get/put_closure + can re-derive the enclosing frame from the GC ref at call time, + surviving GC moves */ + fp[QBE_FRAME_OUTER_SLOT] = (JSValue)(int64_t)magic; - return fn(ctx, frame); + JSValue result = fn(ctx, fp); + cell_rt_leave_frame(ctx); + if (result == JS_EXCEPTION) + return JS_EXCEPTION; + return result; } JSValue cell_rt_make_function(JSContext *ctx, int64_t fn_idx, void *outer_fp) { + (void)outer_fp; if (g_native_fn_count >= MAX_NATIVE_FN) return JS_ThrowTypeError(ctx, "too many native functions (max %d)", MAX_NATIVE_FN); int global_id = g_native_fn_count++; g_native_fn_registry[global_id].dl_handle = g_current_dl_handle; g_native_fn_registry[global_id].fn_idx = (int)fn_idx; - g_native_fn_registry[global_id].outer_fp = outer_fp; + + /* Create independent GC ref so the enclosing frame survives + even after cell_rt_leave_frame pops the stack ref */ + if (g_aot_depth > 0) { + JSGCRef *ref = &g_native_fn_registry[global_id].frame_ref; + JS_AddGCRef(ctx, ref); + ref->val = g_aot_gc_refs[g_aot_depth - 1].val; + g_native_fn_registry[global_id].has_frame_ref = 1; + } else { + g_native_fn_registry[global_id].has_frame_ref = 0; + } return JS_NewCFunction2(ctx, (JSCFunction *)cell_fn_trampoline, "native_fn", 255, JS_CFUNC_generic_magic, global_id); @@ -369,6 +463,7 @@ JSValue cell_rt_frame(JSContext *ctx, JSValue fn, int64_t nargs) { } void cell_rt_setarg(JSValue frame_val, int64_t idx, JSValue val) { + if (frame_val == JS_EXCEPTION) return; JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val); fr->slots[idx] = val; } @@ -474,18 +569,54 @@ int cell_rt_is_proxy(JSContext *ctx, JSValue v) { return fn->length == 2; } +/* --- Short-circuit and/or (non-allocating) --- */ + +JSValue cell_rt_and(JSContext *ctx, JSValue left, JSValue right) { + return JS_ToBool(ctx, left) ? right : left; +} + +JSValue cell_rt_or(JSContext *ctx, JSValue left, JSValue right) { + return JS_ToBool(ctx, left) ? left : right; +} + /* --- Disruption --- */ void cell_rt_disrupt(JSContext *ctx) { JS_ThrowTypeError(ctx, "type error in native code"); } +/* --- set_var: set a variable in env_record or global --- */ + +void cell_rt_set_var(JSContext *ctx, JSValue val, const char *name) { + JSValue key = JS_NewString(ctx, name); + JS_SetProperty(ctx, ctx->global_obj, key, val); +} + +/* --- in: key in obj --- */ + +JSValue cell_rt_in(JSContext *ctx, JSValue key, JSValue obj) { + int has = JS_HasProperty(ctx, obj, key); + return JS_NewBool(ctx, has > 0); +} + +/* --- regexp: create regex from pattern and flags --- */ + +JSValue cell_rt_regexp(JSContext *ctx, const char *pattern, const char *flags) { + JSValue argv[2]; + argv[0] = JS_NewString(ctx, pattern); + argv[1] = JS_NewString(ctx, flags); + JSValue re = js_regexp_constructor(ctx, JS_NULL, 2, argv); + if (JS_IsException(re)) + return JS_EXCEPTION; + return re; +} + /* --- Module entry point --- Loads a native .cm module from a dylib handle. Looks up cell_main, builds a heap-allocated frame, sets g_current_dl_handle so closures register in the right module. */ -JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle) { +JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle, JSValue env) { cell_compiled_fn fn = (cell_compiled_fn)dlsym(dl_handle, "cell_main"); if (!fn) return JS_ThrowTypeError(ctx, "cell_main not found in native module dylib"); @@ -495,22 +626,27 @@ JSValue cell_rt_native_module_load(JSContext *ctx, void *dl_handle) { void *prev_handle = g_current_dl_handle; g_current_dl_handle = dl_handle; - /* Heap-allocate so closures created in cell_main can reference - this frame after the module entry returns. */ - JSValue *frame = calloc(512, sizeof(JSValue)); - if (!frame) { + /* Make env available for cell_rt_get_intrinsic lookups */ + cell_rt_set_native_env(ctx, env); + + /* GC-managed frame for module execution */ + JSValue *fp = cell_rt_enter_frame(ctx, 512); + if (!fp) { g_current_dl_handle = prev_handle; return JS_ThrowTypeError(ctx, "frame allocation failed"); } - JSValue result = fn(ctx, frame); + JSValue result = fn(ctx, fp); + cell_rt_leave_frame(ctx); /* safe — closures have independent GC refs */ g_current_dl_handle = prev_handle; + if (result == JS_EXCEPTION) + return JS_EXCEPTION; return result; } /* Load a native module from a dylib handle, trying a named symbol first. Falls back to cell_main if the named symbol is not found. */ -JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const char *sym_name) { +JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const char *sym_name, JSValue env) { cell_compiled_fn fn = NULL; if (sym_name) fn = (cell_compiled_fn)dlsym(dl_handle, sym_name); @@ -522,19 +658,25 @@ JSValue cell_rt_native_module_load_named(JSContext *ctx, void *dl_handle, const void *prev_handle = g_current_dl_handle; g_current_dl_handle = dl_handle; - JSValue *frame = calloc(512, sizeof(JSValue)); - if (!frame) { + /* Make env available for cell_rt_get_intrinsic lookups */ + cell_rt_set_native_env(ctx, env); + + JSValue *fp = cell_rt_enter_frame(ctx, 512); + if (!fp) { g_current_dl_handle = prev_handle; return JS_ThrowTypeError(ctx, "frame allocation failed"); } - JSValue result = fn(ctx, frame); + JSValue result = fn(ctx, fp); + cell_rt_leave_frame(ctx); /* safe — closures have independent GC refs */ g_current_dl_handle = prev_handle; + if (result == JS_EXCEPTION) + return JS_EXCEPTION; return result; } /* Backward-compat: uses RTLD_DEFAULT (works when dylib opened with RTLD_GLOBAL) */ JSValue cell_rt_module_entry(JSContext *ctx) { void *handle = dlopen(NULL, RTLD_LAZY); - return cell_rt_native_module_load(ctx, handle); + return cell_rt_native_module_load(ctx, handle, JS_NULL); } diff --git a/source/quickjs-internal.h b/source/quickjs-internal.h index c2a2f5b9..e0ad8a0a 100644 --- a/source/quickjs-internal.h +++ b/source/quickjs-internal.h @@ -495,8 +495,8 @@ typedef enum MachOpcode { MACH_MOD, /* R(A) = R(B) % R(C) */ MACH_POW, /* R(A) = R(B) ** R(C) */ MACH_NEG, /* R(A) = -R(B) */ - MACH_INC, /* R(A) = R(B) + 1 */ - MACH_DEC, /* R(A) = R(B) - 1 */ + 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)) */ @@ -671,8 +671,8 @@ static const char *mach_opcode_names[MACH_OP_COUNT] = { [MACH_MOD] = "mod", [MACH_POW] = "pow", [MACH_NEG] = "neg", - [MACH_INC] = "inc", - [MACH_DEC] = "dec", + [MACH__DEAD_INC] = "dead_inc", + [MACH__DEAD_DEC] = "dead_dec", [MACH_EQ] = "eq", [MACH_NEQ] = "neq", [MACH_LT] = "lt", diff --git a/test.ce b/test.ce index d3bac1e2..c1a372c0 100644 --- a/test.ce +++ b/test.ce @@ -418,9 +418,9 @@ function run_tests(package_name, specific_test) { var src_path = prefix + '/' + f var src = text(fd.slurp(src_path)) var ast = analyze(src, src_path) - test_mod_noopt = run_ast_noopt_fn(mod_path + '_noopt', ast, { + test_mod_noopt = run_ast_noopt_fn(mod_path + '_noopt', ast, stone({ use: function(path) { return shop.use(path, use_pkg) } - }) + })) } disruption { log.console(` DIFF: failed to load noopt module for ${f}`) }