Merge branch 'gen_dylib'

This commit is contained in:
2026-02-17 08:59:05 -06:00
23 changed files with 96093 additions and 87168 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -582,6 +582,93 @@ Build.compile_native = function(src_path, target, buildtype, pkg) {
return dylib_path
}
// Compile pre-compiled mcode IR to a native .dylib via QBE.
// Use this when the caller already has the optimized IR (avoids calling mcode
// twice and hitting module-level state pollution).
Build.compile_native_ir = function(optimized, src_path, opts) {
var _target = (opts && opts.target) || Build.detect_host_target()
var _buildtype = (opts && opts.buildtype) || 'release'
var pkg = opts && opts.pkg
var qbe_rt_path = null
var native_stem = null
var native_install_dir = null
var native_install_path = null
var tc = toolchains[_target]
var dylib_ext = tc.system == 'windows' ? '.dll' : (tc.system == 'darwin' ? '.dylib' : '.so')
var cc = tc.c
var qbe_macros = use('qbe')
var qbe_emit = use('qbe_emit')
var sym_name = null
if (pkg) {
sym_name = shop.c_symbol_for_file(pkg, fd.basename(src_path))
}
var il = qbe_emit(optimized, qbe_macros, sym_name)
il = qbe_insert_dead_labels(il)
var src = text(fd.slurp(src_path))
var hash = content_hash(src + '\n' + _target + '\nnative')
var build_dir = get_build_dir()
ensure_dir(build_dir)
var dylib_path = build_dir + '/' + hash + '.' + _target + dylib_ext
if (fd.is_file(dylib_path))
return dylib_path
var tmp = '/tmp/cell_native_' + hash
var ssa_path = tmp + '.ssa'
var s_path = tmp + '.s'
var o_path = tmp + '.o'
var rt_o_path = '/tmp/cell_qbe_rt.o'
fd.slurpwrite(ssa_path, stone(blob(il)))
var rc = os.system('qbe -o ' + s_path + ' ' + ssa_path)
if (rc != 0) {
print('QBE compilation failed for: ' + src_path); disrupt
}
rc = os.system(cc + ' -c ' + s_path + ' -o ' + o_path)
if (rc != 0) {
print('Assembly failed for: ' + src_path); disrupt
}
if (!fd.is_file(rt_o_path)) {
qbe_rt_path = shop.get_package_dir('core') + '/qbe_rt.c'
rc = os.system(cc + ' -c ' + qbe_rt_path + ' -o ' + rt_o_path + ' -fPIC')
if (rc != 0) {
print('QBE runtime stubs compilation failed'); disrupt
}
}
var link_cmd = cc + ' -shared -fPIC'
if (tc.system == 'darwin') {
link_cmd = link_cmd + ' -undefined dynamic_lookup'
} else if (tc.system == 'linux') {
link_cmd = link_cmd + ' -Wl,--allow-shlib-undefined'
}
link_cmd = link_cmd + ' ' + o_path + ' ' + rt_o_path + ' -o ' + dylib_path
rc = os.system(link_cmd)
if (rc != 0) {
print('Linking native dylib failed for: ' + src_path); disrupt
}
log.console('Built native: ' + fd.basename(dylib_path))
if (pkg) {
native_stem = fd.basename(src_path)
native_install_dir = shop.get_lib_dir() + '/' + shop.lib_name_for_package(pkg)
ensure_dir(native_install_dir)
native_install_path = native_install_dir + '/' + native_stem + dylib_ext
fd.slurpwrite(native_install_path, fd.slurp(dylib_path))
}
return dylib_path
}
// ============================================================================
// Module table generation (for static builds)
// ============================================================================

113
compare_aot.ce Normal file
View File

@@ -0,0 +1,113 @@
// compare_aot.ce — compile a .ce/.cm file via both paths and compare results
//
// Usage:
// cell --dev compare_aot.ce <file.ce>
var build = use('build')
var fd_mod = use('fd')
var os = use('os')
var json = use('json')
var show = function(v) {
if (v == null) return "null"
return json.encode(v)
}
if (length(args) < 1) {
print('usage: cell --dev compare_aot.ce <file>')
return
}
var file = args[0]
if (!fd_mod.is_file(file)) {
if (!ends_with(file, '.ce') && fd_mod.is_file(file + '.ce'))
file = file + '.ce'
else 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)
// Shared env for both paths — only non-intrinsic runtime functions.
// Intrinsics (starts_with, ends_with, logical, some, every, etc.) live on
// the stoned global and are found via GETINTRINSIC/cell_rt_get_intrinsic.
var env = stone({
log: log,
fallback: fallback,
parallel: parallel,
race: race,
sequence: sequence,
use
})
// --- Interpreted (mach VM) ---
var result_interp = null
var interp_ok = false
var run_interp = function() {
print('--- interpreted ---')
var mcode_json = json.encode(optimized)
var mach_blob = mach_compile_mcode_bin(abs, mcode_json)
result_interp = mach_load(mach_blob, env)
interp_ok = true
print('result: ' + show(result_interp))
} disruption {
interp_ok = true
print('(disruption escaped from interpreted run)')
}
run_interp()
// --- Native (AOT via QBE) ---
var result_native = null
var native_ok = false
var run_native = function() {
print('\n--- native ---')
var dylib_path = build.compile_native_ir(optimized, abs, null)
print('dylib: ' + dylib_path)
var handle = os.dylib_open(dylib_path)
if (!handle) {
print('failed to open dylib')
return
}
result_native = os.native_module_load(handle, env)
native_ok = true
print('result: ' + show(result_native))
} disruption {
native_ok = true
print('(disruption escaped from native run)')
}
run_native()
// --- Comparison ---
print('\n--- comparison ---')
var s_interp = show(result_interp)
var s_native = show(result_native)
if (interp_ok && native_ok) {
if (s_interp == s_native) {
print('MATCH')
} else {
print('MISMATCH')
print(' interp: ' + s_interp)
print(' native: ' + s_native)
}
} else {
if (!interp_ok) print('interpreted run failed')
if (!native_ok) print('native run failed')
}

View File

@@ -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

View File

@@ -229,7 +229,6 @@ Function calls are decomposed into three instructions:
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `access` | `dest, name` | Load variable (intrinsic or module environment) |
| `set_var` | `name, src` | Set top-level variable by name |
| `get` | `dest, level, slot` | Get closure variable from parent scope |
| `put` | `level, slot, src` | Set closure variable in parent scope |

22
dump_ir.ce Normal file
View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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")

View File

@@ -22,30 +22,6 @@ function use_embed(name) {
return load_internal("js_core_" + name + "_use")
}
function logical(val1) {
if (val1 == 0 || val1 == false || val1 == "false" || val1 == null)
return false;
if (val1 == 1 || val1 == true || val1 == "true")
return true;
return null;
}
function some(arr, pred) {
return find(arr, pred) != null
}
function every(arr, pred) {
return find(arr, x => not(pred(x))) == null
}
function starts_with(str, prefix) {
return search(str, prefix) == 0
}
function ends_with(str, suffix) {
return search(str, suffix, -length(suffix)) != null
}
var fd = use_embed('internal_fd')
var js = use_embed('js')
var crypto = use_embed('crypto')
@@ -84,7 +60,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}))
}
// Load a pipeline module from cache; on miss compile from source via boot chain
@@ -149,7 +125,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)
@@ -270,6 +246,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
@@ -447,12 +424,6 @@ var parallel = pronto.parallel
var race = pronto.race
var sequence = pronto.sequence
// Fill runtime_env (same object reference shop holds)
runtime_env.logical = logical
runtime_env.some = some
runtime_env.every = every
runtime_env.starts_with = starts_with
runtime_env.ends_with = ends_with
runtime_env.actor = actor
runtime_env.is_actor = is_actor
runtime_env.log = log
@@ -1109,6 +1080,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)

View File

@@ -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),

View File

@@ -959,6 +959,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)
@@ -994,6 +995,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]
}
@@ -1587,6 +1589,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)
}
@@ -1676,9 +1679,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

View File

@@ -1384,8 +1384,6 @@ var mcode = function(ast) {
pstate = parent_states[length(parent_states) - 1 - _lv]
pslot = find_var_in_saved(pstate, name)
emit_3("put", dest, pslot, level)
} else {
add_instr(["set_var", name, dest])
}
return dest
} else if (left_kind == ".") {
@@ -1486,9 +1484,6 @@ var mcode = function(ast) {
return val_slot
}
val_slot = gen_expr(right, -1)
if (level == -1) {
add_instr(["set_var", name, val_slot])
}
} else {
val_slot = gen_expr(right, -1)
if (level > 0) {

File diff suppressed because it is too large Load Diff

View File

@@ -85,6 +85,12 @@ 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: ERROR env not stone (code=%s file=%s)\n",
code->name_cstr ? code->name_cstr : "<unknown>",
code->filename_cstr ? code->filename_cstr : "<unknown>");
abort();
}
JSGCRef env_ref;
JS_PushGCRef(ctx, &env_ref);
env_ref.val = env;
@@ -809,7 +815,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),
@@ -859,7 +865,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
DT(MACH_GOINVOKE),
DT(MACH_JMPNOTNULL),
DT(MACH_DISRUPT),
DT(MACH_SET_VAR), DT(MACH_IN),
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),
@@ -1218,38 +1224,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);
@@ -2075,21 +2049,6 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
VM_CASE(MACH_DISRUPT):
goto disrupt;
/* Variable storage: env/global[K(Bx)] = R(A) */
VM_CASE(MACH_SET_VAR): {
int bx = MACH_GET_Bx(instr);
JSValue key = code->cpool[bx];
JSValue val = frame->slots[a];
JSValue cur_env = JS_VALUE_GET_FUNCTION(frame->function)->u.reg.env_record;
if (!JS_IsNull(cur_env)) {
JS_SetProperty(ctx, cur_env, key, val);
} else {
JS_SetProperty(ctx, ctx->global_obj, key, val);
}
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
VM_BREAK();
}
/* Has-property check (mcode name) */
VM_CASE(MACH_IN): {
JSValue key = frame->slots[b];
@@ -2128,6 +2087,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
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_IsNull(frame->caller)) {
@@ -2334,9 +2294,6 @@ static int mcode_reg_items(cJSON *it, cJSON **out) {
/* goinvoke: [1]=frame only (no result) */
if (!strcmp(op, "goinvoke")) { ADD(1); return c; }
/* set_var: [1]=name(string), [2]=val */
if (!strcmp(op, "set_var")) { ADD(2); return c; }
/* setarg: [1]=call, [2]=arg_idx(const), [3]=val */
if (!strcmp(op, "setarg")) { ADD(1); ADD(3); return c; }
@@ -2906,12 +2863,6 @@ static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) {
else if (strcmp(op, "disrupt") == 0) {
EM(MACH_ABC(MACH_DISRUPT, 0, 0, 0));
}
/* Variable storage */
else if (strcmp(op, "set_var") == 0) {
const char *vname = cJSON_GetArrayItem(it, 1)->valuestring;
int val_reg = A2;
EM(MACH_ABx(MACH_SET_VAR, val_reg, ml_cpool_str(&s, vname)));
}
/* Misc */
else if (strcmp(op, "in") == 0) {
EM(MACH_ABC(MACH_IN, A1, A2, A3));
@@ -3341,8 +3292,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);

View File

@@ -260,23 +260,70 @@ 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: ERROR env not stone\n");
abort();
}
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) {
return JS_GetPropertyStr(ctx, ctx->global_obj, name);
/* Check native env first (runtime-provided functions like log) */
if (g_has_native_env) {
JSValue v = JS_GetPropertyStr(ctx, g_native_env_ref.val, name);
if (!JS_IsNull(v))
return v;
}
/* Linear scan of global object — avoids hash mismatch issues with
stoned records whose keys may be in cold storage */
JSValue gobj = ctx->global_obj;
if (JS_IsRecord(gobj)) {
JSRecord *rec = (JSRecord *)chase(gobj);
uint64_t mask = objhdr_cap56(rec->mist_hdr);
for (uint64_t i = 1; i <= mask; i++) {
if (js_key_equal_str(rec->slots[i].key, name))
return rec->slots[i].val;
}
}
JS_ThrowReferenceError(ctx, "'%s' is not defined", name);
return JS_EXCEPTION;
}
/* --- 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 +332,74 @@ 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;
if (g_aot_depth <= 0) {
fprintf(stderr, "[BUG] cell_rt_refresh_fp: g_aot_depth=%d\n", g_aot_depth);
abort();
}
JSValue val = g_aot_gc_refs[g_aot_depth - 1].val;
JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(val);
if (!frame) {
fprintf(stderr, "[BUG] cell_rt_refresh_fp: frame is NULL at depth=%d val=%lld\n",
g_aot_depth, (long long)val);
abort();
}
return (JSValue *)frame->slots;
}
/* Combined refresh + exception check in a single call.
Returns the refreshed fp, or NULL if there is a pending exception.
This avoids QBE register-allocation issues from two consecutive calls. */
JSValue *cell_rt_refresh_fp_checked(JSContext *ctx) {
if (JS_HasException(ctx))
return NULL;
if (g_aot_depth <= 0) {
fprintf(stderr, "[BUG] cell_rt_refresh_fp_checked: g_aot_depth=%d\n", g_aot_depth);
abort();
}
JSValue val = g_aot_gc_refs[g_aot_depth - 1].val;
JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(val);
if (!frame) {
fprintf(stderr, "[BUG] cell_rt_refresh_fp_checked: frame is NULL\n");
abort();
}
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 +413,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 +422,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 +447,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,11 +505,13 @@ 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 || frame_val == JS_NULL) return;
JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
fr->slots[idx] = val;
}
JSValue cell_rt_invoke(JSContext *ctx, JSValue frame_val) {
if (frame_val == JS_EXCEPTION) return JS_EXCEPTION;
JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
int nr_slots = (int)objhdr_cap56(fr->header);
int c_argc = (nr_slots >= 2) ? nr_slots - 2 : 0;
@@ -474,18 +612,56 @@ 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;
}
/* --- Exception checking ---
After potentially-throwing runtime calls, QBE-generated code needs to
check for pending exceptions and branch to the disruption handler. */
void cell_rt_clear_exception(JSContext *ctx) {
if (JS_HasException(ctx))
JS_GetException(ctx);
}
/* --- Disruption --- */
void cell_rt_disrupt(JSContext *ctx) {
JS_ThrowTypeError(ctx, "type error in native code");
}
/* --- 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 +671,31 @@ 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);
/* Clear any stale exception left by a previous interpreted run */
if (JS_HasException(ctx))
JS_GetException(ctx);
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 +707,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);
}

View File

@@ -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)) */
@@ -639,9 +639,6 @@ typedef enum MachOpcode {
/* Error handling */
MACH_DISRUPT, /* trigger disruption (A only) */
/* Variable storage */
MACH_SET_VAR, /* env/global[K(Bx)] = R(A) — store to var (ABx) */
/* Misc */
MACH_IN, /* R(A) = (R(B) in R(C)) — has property (ABC) */
@@ -671,8 +668,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",
@@ -764,7 +761,6 @@ static const char *mach_opcode_names[MACH_OP_COUNT] = {
[MACH_GOINVOKE] = "goinvoke",
[MACH_JMPNOTNULL] = "jmpnotnull",
[MACH_DISRUPT] = "disrupt",
[MACH_SET_VAR] = "set_var",
[MACH_IN] = "in",
/* Extended type checks */
[MACH_IS_ARRAY] = "is_array",

View File

@@ -1688,6 +1688,34 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
scan += obj_size;
}
#ifdef VALIDATE_GC
{
JSRecord *grec = JS_VALUE_GET_RECORD(ctx->global_obj);
uint32_t mask = (uint32_t)objhdr_cap56(grec->mist_hdr);
for (uint32_t i = 1; i <= mask; i++) {
JSValue k = grec->slots[i].key;
if (!rec_key_is_empty(k) && !rec_key_is_tomb(k)) {
if (!JS_IsPtr(k)) {
fprintf(stderr, "VALIDATE_GC: global slot[%u] key is not a pointer (tag=0x%llx)\n",
i, (unsigned long long)k);
} else {
void *kp = JS_VALUE_GET_PTR(k);
if (!ptr_in_range(kp, to_base, to_free) && !is_ct_ptr(ctx, kp)) {
fprintf(stderr, "VALIDATE_GC: global slot[%u] key=%p outside valid ranges\n", i, kp);
}
}
JSValue v = grec->slots[i].val;
if (JS_IsPtr(v)) {
void *vp = JS_VALUE_GET_PTR(v);
if (!ptr_in_range(vp, to_base, to_free) && !is_ct_ptr(ctx, vp)) {
fprintf(stderr, "VALIDATE_GC: global slot[%u] val=%p outside valid ranges\n", i, vp);
}
}
}
}
}
#endif
/* Return old block (in poison mode, just poison it and leak) */
heap_block_free (rt, from_base, old_heap_size);
@@ -1960,6 +1988,7 @@ JSContext *JS_NewContext (JSRuntime *rt) {
if (!ctx) return NULL;
JS_AddIntrinsicBaseObjects (ctx);
JS_AddIntrinsicRegExp (ctx);
obj_set_stone (JS_VALUE_GET_RECORD (ctx->global_obj));
return ctx;
}
@@ -1967,7 +1996,8 @@ JSContext *JS_NewContextWithHeapSize (JSRuntime *rt, size_t heap_size) {
JSContext *ctx = JS_NewContextRawWithHeapSize (rt, heap_size);
if (!ctx) return NULL;
JS_AddIntrinsicBaseObjects (ctx);
JS_AddIntrinsicRegExp (ctx);
JS_AddIntrinsicRegExp (ctx);
obj_set_stone (JS_VALUE_GET_RECORD (ctx->global_obj));
return ctx;
}
@@ -3046,7 +3076,7 @@ JSValue JS_ThrowError2 (JSContext *ctx, JSErrorEnum error_num, const char *fmt,
if (add_backtrace) {
print_backtrace (ctx, NULL, 0, 0);
}
return JS_Throw (ctx, JS_NULL);
return JS_Throw (ctx, JS_TRUE);
}
static JSValue JS_ThrowError (JSContext *ctx, JSErrorEnum error_num, const char *fmt, va_list ap) {

View File

@@ -1020,6 +1020,461 @@ var streamline = function(ir, log) {
return null
}
// =========================================================
// Pass: compress_slots — linear-scan register allocation
// Reuses slots with non-overlapping live ranges to reduce
// nr_slots. Mirrors mcode_compress_regs from mach.c.
// Works across all functions for captured-slot tracking.
// =========================================================
// Which instruction positions hold slot references (special cases)
var slot_idx_special = {
get: [1], put: [1],
access: [1], int: [1], function: [1], regexp: [1],
true: [1], false: [1], null: [1],
record: [1], array: [1],
invoke: [1, 2], tail_invoke: [1, 2],
goinvoke: [1],
setarg: [1, 3],
frame: [1, 2], goframe: [1, 2],
jump: [], disrupt: [],
jump_true: [1], jump_false: [1], jump_not_null: [1],
return: [1]
}
var get_slot_refs = function(instr) {
var special = slot_idx_special[instr[0]]
var result = null
var j = 0
var limit = 0
if (special != null) return special
result = []
limit = length(instr) - 2
j = 1
while (j < limit) {
if (is_number(instr[j])) result[] = j
j = j + 1
}
return result
}
var compress_one_fn = function(func, captured_slots) {
var instructions = func.instructions
var nr_slots = func.nr_slots
var nr_args = func.nr_args != null ? func.nr_args : 0
var n = 0
var pinned = 0
var first_ref = null
var last_ref = null
var i = 0
var j = 0
var k = 0
var s = 0
var instr = null
var refs = null
var op = null
var target = null
var tpos = 0
var changed = false
var label_map = null
var live_slots = null
var live_first = null
var live_last = null
var cnt = 0
var key_s = 0
var key_f = 0
var key_l = 0
var remap = null
var pool = null
var next_phys = 0
var active_phys = null
var active_last = null
var phys = 0
var mi = 0
var new_max = 0
var old_val = 0
var new_active_phys = null
var new_active_last = null
var new_pool = null
if (instructions == null || !is_number(nr_slots) || nr_slots <= 1) return null
n = length(instructions)
pinned = 1 + nr_args
// Step 1: build live ranges
first_ref = array(nr_slots, -1)
last_ref = array(nr_slots, -1)
// Pin this + args
k = 0
while (k < pinned) {
first_ref[k] = 0
last_ref[k] = n
k = k + 1
}
// Scan instructions for slot references
i = 0
while (i < n) {
instr = instructions[i]
if (is_array(instr)) {
refs = get_slot_refs(instr)
j = 0
while (j < length(refs)) {
s = instr[refs[j]]
if (is_number(s) && s >= 0 && s < nr_slots) {
if (first_ref[s] < 0) first_ref[s] = i
last_ref[s] = i
}
j = j + 1
}
}
i = i + 1
}
// Pin captured slots (AFTER scan so last_ref isn't overwritten)
if (captured_slots != null) {
k = 0
while (k < length(captured_slots)) {
s = captured_slots[k]
if (s >= 0 && s < nr_slots) {
if (first_ref[s] < 0) first_ref[s] = 0
last_ref[s] = n
}
k = k + 1
}
}
// Step 1b: extend for backward jumps (loops)
label_map = {}
i = 0
while (i < n) {
instr = instructions[i]
if (is_text(instr) && !starts_with(instr, "_nop_")) {
label_map[instr] = i
}
i = i + 1
}
changed = true
while (changed) {
changed = false
i = 0
while (i < n) {
instr = instructions[i]
if (!is_array(instr)) {
i = i + 1
continue
}
op = instr[0]
target = null
if (op == "jump") {
target = instr[1]
} else if (op == "jump_true" || op == "jump_false" || op == "jump_not_null") {
target = instr[2]
}
if (target == null || !is_text(target)) {
i = i + 1
continue
}
tpos = label_map[target]
if (tpos == null || tpos >= i) {
i = i + 1
continue
}
// Backward jump: extend slots live into loop
s = pinned
while (s < nr_slots) {
if (first_ref[s] >= 0 && first_ref[s] < tpos && last_ref[s] >= tpos && last_ref[s] < i) {
last_ref[s] = i
changed = true
}
s = s + 1
}
i = i + 1
}
}
// Step 2: sort live intervals by first_ref
live_slots = []
live_first = []
live_last = []
s = pinned
while (s < nr_slots) {
if (first_ref[s] >= 0) {
live_slots[] = s
live_first[] = first_ref[s]
live_last[] = last_ref[s]
}
s = s + 1
}
cnt = length(live_slots)
i = 1
while (i < cnt) {
key_s = live_slots[i]
key_f = live_first[i]
key_l = live_last[i]
j = i - 1
while (j >= 0 && (live_first[j] > key_f || (live_first[j] == key_f && live_slots[j] > key_s))) {
live_slots[j + 1] = live_slots[j]
live_first[j + 1] = live_first[j]
live_last[j + 1] = live_last[j]
j = j - 1
}
live_slots[j + 1] = key_s
live_first[j + 1] = key_f
live_last[j + 1] = key_l
i = i + 1
}
// Linear-scan allocation
remap = array(nr_slots)
s = 0
while (s < nr_slots) {
remap[s] = s
s = s + 1
}
pool = []
next_phys = pinned
active_phys = []
active_last = []
i = 0
while (i < cnt) {
// Expire intervals whose last < live_first[i]
new_active_phys = []
new_active_last = []
j = 0
while (j < length(active_phys)) {
if (active_last[j] < live_first[i]) {
pool[] = active_phys[j]
} else {
new_active_phys[] = active_phys[j]
new_active_last[] = active_last[j]
}
j = j + 1
}
active_phys = new_active_phys
active_last = new_active_last
// Pick lowest available physical register
if (length(pool) > 0) {
mi = 0
j = 1
while (j < length(pool)) {
if (pool[j] < pool[mi]) mi = j
j = j + 1
}
phys = pool[mi]
new_pool = []
j = 0
while (j < length(pool)) {
if (j != mi) new_pool[] = pool[j]
j = j + 1
}
pool = new_pool
} else {
phys = next_phys
next_phys = next_phys + 1
}
remap[live_slots[i]] = phys
active_phys[] = phys
active_last[] = live_last[i]
i = i + 1
}
// Compute new nr_slots
new_max = pinned
s = 0
while (s < nr_slots) {
if (first_ref[s] >= 0 && remap[s] >= new_max) {
new_max = remap[s] + 1
}
s = s + 1
}
if (new_max >= nr_slots) return null
// Step 3: apply remap to instructions
i = 0
while (i < n) {
instr = instructions[i]
if (is_array(instr)) {
refs = get_slot_refs(instr)
j = 0
while (j < length(refs)) {
old_val = instr[refs[j]]
if (is_number(old_val) && old_val >= 0 && old_val < nr_slots) {
instr[refs[j]] = remap[old_val]
}
j = j + 1
}
}
i = i + 1
}
func.nr_slots = new_max
return remap
}
var compress_slots = function(ir) {
if (ir == null || ir.main == null) return null
var functions = ir.functions != null ? ir.functions : []
var func_count = length(functions)
var parent_of = null
var captured = null
var remaps = null
var remap_sizes = null
var instrs = null
var instr = null
var child_idx = 0
var parent_slot = 0
var level = 0
var ancestor = 0
var caps = null
var found = false
var anc_remap = null
var old_slot = 0
var fi = 0
var i = 0
var j = 0
var k = 0
// Build parent_of: parent_of[i] = parent index, func_count = main
parent_of = array(func_count, -1)
// Scan main for function instructions
if (ir.main != null && ir.main.instructions != null) {
instrs = ir.main.instructions
i = 0
while (i < length(instrs)) {
instr = instrs[i]
if (is_array(instr) && instr[0] == "function") {
child_idx = instr[2]
if (child_idx >= 0 && child_idx < func_count) {
parent_of[child_idx] = func_count
}
}
i = i + 1
}
}
// Scan each function for function instructions
fi = 0
while (fi < func_count) {
instrs = functions[fi].instructions
if (instrs != null) {
i = 0
while (i < length(instrs)) {
instr = instrs[i]
if (is_array(instr) && instr[0] == "function") {
child_idx = instr[2]
if (child_idx >= 0 && child_idx < func_count) {
parent_of[child_idx] = fi
}
}
i = i + 1
}
}
fi = fi + 1
}
// Build captured slots per function
captured = array(func_count + 1)
i = 0
while (i < func_count + 1) {
captured[i] = []
i = i + 1
}
fi = 0
while (fi < func_count) {
instrs = functions[fi].instructions
if (instrs != null) {
i = 0
while (i < length(instrs)) {
instr = instrs[i]
if (is_array(instr) && (instr[0] == "get" || instr[0] == "put")) {
parent_slot = instr[2]
level = instr[3]
ancestor = fi
j = 0
while (j < level && ancestor >= 0) {
ancestor = parent_of[ancestor]
j = j + 1
}
if (ancestor >= 0) {
caps = captured[ancestor]
found = false
k = 0
while (k < length(caps)) {
if (caps[k] == parent_slot) {
found = true
k = length(caps)
}
k = k + 1
}
if (!found) caps[] = parent_slot
}
}
i = i + 1
}
}
fi = fi + 1
}
// Compress each function and save remap tables
remaps = array(func_count + 1)
remap_sizes = array(func_count + 1, 0)
fi = 0
while (fi < func_count) {
remap_sizes[fi] = functions[fi].nr_slots
remaps[fi] = compress_one_fn(functions[fi], captured[fi])
fi = fi + 1
}
if (ir.main != null) {
remap_sizes[func_count] = ir.main.nr_slots
remaps[func_count] = compress_one_fn(ir.main, captured[func_count])
}
// Fix get/put parent_slot references using ancestor remap tables
fi = 0
while (fi < func_count) {
instrs = functions[fi].instructions
if (instrs != null) {
i = 0
while (i < length(instrs)) {
instr = instrs[i]
if (is_array(instr) && (instr[0] == "get" || instr[0] == "put")) {
level = instr[3]
ancestor = fi
j = 0
while (j < level && ancestor >= 0) {
ancestor = parent_of[ancestor]
j = j + 1
}
if (ancestor >= 0 && remaps[ancestor] != null) {
anc_remap = remaps[ancestor]
old_slot = instr[2]
if (old_slot >= 0 && old_slot < remap_sizes[ancestor]) {
instr[2] = anc_remap[old_slot]
}
}
}
i = i + 1
}
}
fi = fi + 1
}
return null
}
// =========================================================
// Compose all passes
// =========================================================
@@ -1090,6 +1545,9 @@ var streamline = function(ir, log) {
}
}
// Compress slots across all functions (must run after per-function passes)
compress_slots(ir)
return ir
}

View File

@@ -443,9 +443,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}`)
}