optimize frames; remove trampoline

This commit is contained in:
2026-02-18 22:37:48 -06:00
parent 27ca008f18
commit e004b2c472
14 changed files with 318 additions and 91 deletions

View File

@@ -81,7 +81,7 @@ function content_hash(str) {
} }
// Bump when native codegen/runtime ABI changes so stale dylibs are not reused. // Bump when native codegen/runtime ABI changes so stale dylibs are not reused.
def NATIVE_CACHE_VERSION = "native-v22" def NATIVE_CACHE_VERSION = "native-v23"
// Enable AOT ASan by creating .cell/asan_aot in the package root. // Enable AOT ASan by creating .cell/asan_aot in the package root.
function native_sanitize_flags() { function native_sanitize_flags() {

View File

@@ -168,6 +168,9 @@ pit bench <suite> # run specific benchmark file
pit bench package <name> # benchmark a named package pit bench package <name> # benchmark a named package
pit bench package <name> <suite> # specific benchmark in a package pit bench package <name> <suite> # specific benchmark in a package
pit bench package all # benchmark all packages pit bench package all # benchmark all packages
pit bench --bytecode <suite> # force bytecode-only benchmark run
pit bench --native <suite> # force native-only benchmark run
pit bench --compare <suite> # run bytecode and native side-by-side
``` ```
Output includes median, mean, standard deviation, and percentiles for each benchmark. Output includes median, mean, standard deviation, and percentiles for each benchmark.

View File

@@ -37,7 +37,12 @@ On a cache miss, the C runtime loads `boot/bootstrap.cm.mcode` (a pre-compiled s
### Cache invalidation ### Cache invalidation
All caching is content-addressed by BLAKE2 hash of the source. When any source file changes, its hash changes and the old cache entry is simply never looked up again. No manual invalidation is needed. To force a full rebuild, delete `~/.pit/build/`. Caching is content-addressed by BLAKE2 over the relevant inputs for each artifact.
- Mach/script cache keys are source-content based.
- Native (`.dylib`) cache keys include source, host target, native mode marker, native cache version, and sanitize flags.
When inputs change, the old cache entry is simply never looked up again. To force a full rebuild, delete `~/.pit/build/` (or run `cell --dev clean shop --build` in a dev workspace).
## Module Resolution ## Module Resolution
@@ -73,7 +78,7 @@ use('gitea.pockle.world/john/renderer/sprite')
## Compilation and Caching ## Compilation and Caching
Every module goes through a content-addressed caching pipeline. The cache key is the BLAKE2 hash of the source content, so changing the source automatically invalidates the cache. Every module goes through a content-addressed caching pipeline. Cache keys are based on the inputs that affect the output artifact, so changing any relevant input automatically invalidates the cache.
### Cache Hierarchy ### Cache Hierarchy
@@ -104,7 +109,9 @@ The build cache at `~/.pit/build/` stores ephemeral artifacts named by the BLAKE
└── f3a4b5c6... # compiled dylib (checked before copying to lib/) └── f3a4b5c6... # compiled dylib (checked before copying to lib/)
``` ```
This scheme provides automatic cache invalidation: when source changes, its hash changes, and the old cache entry is simply never looked up again. When building a dylib, the build cache is checked first — if a matching hash exists, it is copied to `lib/` without recompiling. This scheme provides automatic cache invalidation: when an input changes, its hash changes, and the old cache entry is simply never looked up again. For native dylibs, inputs include target and native cache version in addition to source.
When native codegen/runtime ABI changes, bump `NATIVE_CACHE_VERSION` in both `build.cm` and `internal/shop.cm` so stale native artifacts are never reused.
### Core Module Caching ### Core Module Caching

View File

@@ -118,6 +118,45 @@ When a mismatch is found:
MISMATCH: test_foo: result mismatch opt=42 noopt=43 MISMATCH: test_foo: result mismatch opt=42 noopt=43
``` ```
## ASAN for Native AOT
When debugging native (`shop.use_native`) crashes, there are two useful sanitizer workflows.
### 1) AOT-only sanitizer (fastest loop)
Enable sanitizer flags for generated native modules by creating a marker file:
```bash
touch .cell/asan_aot
cell --dev bench --native fibonacci
```
This adds `-fsanitize=address -fno-omit-frame-pointer` to AOT module compilation.
Disable it with:
```bash
rm -f .cell/asan_aot
```
### 2) Full runtime sanitizer (CLI + runtime + AOT)
Build an ASAN-instrumented `cell` binary:
```bash
meson setup build-asan -Dbuildtype=debug -Db_sanitize=address
CCACHE_DISABLE=1 meson compile -C build-asan
ASAN_OPTIONS=abort_on_error=1:detect_leaks=0 ./build-asan/cell --dev bench --native fibonacci
```
This catches bugs crossing the boundary between generated dylibs and runtime helpers.
If stale native artifacts are suspected after compiler/runtime changes, clear build outputs first:
```bash
cell --dev clean shop --build
```
## Fuzz Testing ## Fuzz Testing
The fuzzer generates random self-checking programs, compiles them, and runs them through both optimized and unoptimized paths. Each generated program contains test functions that validate their own expected results, so failures catch both correctness bugs and optimizer mismatches. The fuzzer generates random self-checking programs, compiles them, and runs them through both optimized and unoptimized paths. Each generated program contains test functions that validate their own expected results, so failures catch both correctness bugs and optimizer mismatches.

View File

@@ -458,7 +458,7 @@ var fold = function(ast) {
else if (k == "-") result = lv - rv else if (k == "-") result = lv - rv
else if (k == "*") result = lv * rv else if (k == "*") result = lv * rv
else if (k == "/") result = lv / rv else if (k == "/") result = lv / rv
else if (k == "%") result = lv % rv else if (k == "%") result = lv - (trunc(lv / rv) * rv)
else if (k == "**") result = lv ** rv else if (k == "**") result = lv ** rv
if (result == null) return make_null(expr) if (result == null) return make_null(expr)
return make_number(result, expr) return make_number(result, expr)

View File

@@ -11,7 +11,9 @@ var json_mod = use_embed('json')
var crypto = use_embed('crypto') var crypto = use_embed('crypto')
function content_hash(content) { function content_hash(content) {
return text(crypto.blake2(content), 'h') var data = content
if (!is_blob(data)) data = stone(blob(text(data)))
return text(crypto.blake2(data), 'h')
} }
function cache_path(hash) { function cache_path(hash) {

View File

@@ -35,7 +35,9 @@ var packages_path = shop_path ? shop_path + '/packages' : null
var use_cache = {} var use_cache = {}
function content_hash(content) { function content_hash(content) {
return text(crypto.blake2(content), 'h') var data = content
if (!is_blob(data)) data = stone(blob(text(data)))
return text(crypto.blake2(data), 'h')
} }
function cache_path(hash) { function cache_path(hash) {

View File

@@ -434,6 +434,16 @@ function detect_host_target() {
var host_target = detect_host_target() var host_target = detect_host_target()
// Must match build.cm NATIVE_CACHE_VERSION to detect stale native artifacts.
def NATIVE_CACHE_VERSION = "native-v23"
function native_sanitize_flags() {
if (fd.is_file('.cell/asan_aot')) {
return ' -fsanitize=address -fno-omit-frame-pointer'
}
return ''
}
// Check for a native .cm dylib at the deterministic lib path // Check for a native .cm dylib at the deterministic lib path
// Returns a native descriptor {_native, _handle, _sym}, or null if no native dylib exists // Returns a native descriptor {_native, _handle, _sym}, or null if no native dylib exists
// Also checks staleness: if source has changed, the content-addressed build artifact // Also checks staleness: if source has changed, the content-addressed build artifact
@@ -444,6 +454,7 @@ function try_native_mod_dylib(pkg, stem) {
var src = null var src = null
var host = null var host = null
var hash = null var hash = null
var san_flags = null
var tc_ext = null var tc_ext = null
var build_path = null var build_path = null
var handle = null var handle = null
@@ -456,7 +467,8 @@ function try_native_mod_dylib(pkg, stem) {
if (fd.is_file(src_path)) { if (fd.is_file(src_path)) {
src = text(fd.slurp(src_path)) src = text(fd.slurp(src_path))
host = detect_host_target() host = detect_host_target()
hash = content_hash(src + '\n' + host + '\nnative') san_flags = native_sanitize_flags()
hash = content_hash(src + '\n' + host + '\nnative\n' + NATIVE_CACHE_VERSION + '\n' + san_flags)
tc_ext = dylib_ext tc_ext = dylib_ext
build_path = global_shop_path + '/build/' + hash + '.' + host + tc_ext build_path = global_shop_path + '/build/' + hash + '.' + host + tc_ext
if (!fd.is_file(build_path)) return null if (!fd.is_file(build_path)) return null
@@ -1918,4 +1930,4 @@ Shop.use_native = function(path, package_context) {
return os.native_module_load(handle, env) return os.native_module_load(handle, env)
} }
return Shop return Shop

View File

@@ -4,7 +4,7 @@ var mcode = function(ast) {
// Translation tables // Translation tables
var binop_map = { var binop_map = {
"+": "add", "-": "subtract", "*": "multiply", "/": "divide", "+": "add", "-": "subtract", "*": "multiply", "/": "divide",
"%": "modulo", "&": "bitand", "|": "bitor", "^": "bitxor", "%": "remainder", "&": "bitand", "|": "bitor", "^": "bitxor",
"<<": "shl", ">>": "shr", ">>>": "ushr", "<<": "shl", ">>": "shr", ">>>": "ushr",
"==": "eq", "===": "eq", "!=": "ne", "!==": "ne", "==": "eq", "===": "eq", "!=": "ne", "!==": "ne",
"<": "lt", "<=": "le", ">": "gt", ">=": "ge", "<": "lt", "<=": "le", ">": "gt", ">=": "ge",
@@ -24,13 +24,13 @@ var mcode = function(ast) {
var binop_sym = { var binop_sym = {
add: "+", subtract: "-", multiply: "*", divide: "/", add: "+", subtract: "-", multiply: "*", divide: "/",
modulo: "%", pow: "**", remainder: "%", pow: "**",
lt: "<", le: "<=", gt: ">", ge: ">=" lt: "<", le: "<=", gt: ">", ge: ">="
} }
var compound_map = { var compound_map = {
"+=": "add", "-=": "subtract", "*=": "multiply", "/=": "divide", "+=": "add", "-=": "subtract", "*=": "multiply", "/=": "divide",
"%=": "modulo", "&=": "bitand", "|=": "bitor", "^=": "bitxor", "%=": "remainder", "&=": "bitand", "|=": "bitor", "^=": "bitxor",
"<<=": "shl", ">>=": "shr", ">>>=": "ushr" "<<=": "shl", ">>=": "shr", ">>>=": "ushr"
} }
@@ -673,7 +673,8 @@ var mcode = function(ast) {
if (rel != null) { if (rel != null) {
emit_relational(rel[0], rel[1], rel[2]) emit_relational(rel[0], rel[1], rel[2])
} else if (op_str == "subtract" || op_str == "multiply" || } else if (op_str == "subtract" || op_str == "multiply" ||
op_str == "divide" || op_str == "modulo" || op_str == "pow") { op_str == "divide" || op_str == "modulo" || op_str == "remainder" ||
op_str == "pow") {
emit_numeric_binop(op_str) emit_numeric_binop(op_str)
} else { } else {
// Passthrough for bitwise, in, etc. // Passthrough for bitwise, in, etc.

View File

@@ -1113,6 +1113,9 @@ var qbe_emit = function(ir, qbe, export_name) {
var ri = 0 var ri = 0
var seg_num = 0 var seg_num = 0
var resume_val = 0 var resume_val = 0
// Native calls should mirror MACH semantics: function calls are mediated
// by the frame dispatcher, not recursive C calls.
var use_invoke_trampoline = true
var j_lbl = null var j_lbl = null
var j_idx = null var j_idx = null
var jt_lbl = null var jt_lbl = null
@@ -1139,6 +1142,10 @@ var qbe_emit = function(ir, qbe, export_name) {
var floor_this_slot = 0 var floor_this_slot = 0
var floor_arg_slot = 0 var floor_arg_slot = 0
var floor_dest_slot = 0 var floor_dest_slot = 0
var text_frame_slot = 0
var text_this_slot = 0
var text_arg_slot = 0
var text_dest_slot = 0
// Pre-scan: count invoke/tail_invoke points to assign segment numbers. // Pre-scan: count invoke/tail_invoke points to assign segment numbers.
// Must skip dead code (instructions after terminators) the same way // Must skip dead code (instructions after terminators) the same way
@@ -1187,15 +1194,41 @@ var qbe_emit = function(ir, qbe, export_name) {
} }
} }
if (scan_op == "invoke") { // Keep invoke segment counting consistent with main-loop peephole:
// inline text intrinsic call sequence does not emit an invoke.
if (scan_op == "access" && is_object(scan[2]) && scan[2].make == "intrinsic" && scan[2].name == "text") {
if (si + 4 < length(instrs)) {
peek1 = instrs[si]
peek2 = instrs[si + 1]
peek3 = instrs[si + 2]
peek4 = instrs[si + 3]
peek5 = instrs[si + 4]
if (is_array(peek1) && peek1[0] == "frame" && peek1[2] == scan[1] && peek1[3] == 1 &&
is_array(peek2) && peek2[0] == "null" &&
is_array(peek3) && peek3[0] == "setarg" &&
is_array(peek4) && peek4[0] == "setarg" &&
is_array(peek5) && peek5[0] == "invoke") {
text_frame_slot = peek1[1]
text_this_slot = peek2[1]
if (peek3[1] == text_frame_slot && peek3[2] == 0 && peek3[3] == text_this_slot &&
peek4[1] == text_frame_slot && peek4[2] == 1 &&
peek5[1] == text_frame_slot && peek5[2] == text_this_slot) {
si = si + 5
continue
}
}
}
}
if (use_invoke_trampoline && (scan_op == "invoke" || scan_op == "tail_invoke")) {
invoke_count = invoke_count + 1 invoke_count = invoke_count + 1
} }
// Track terminators — same set as in the main loop // Track terminators — same set as in the main loop
if (scan_op == "return" || scan_op == "jump" || scan_op == "goinvoke" || scan_op == "tail_invoke" || scan_op == "disrupt") { if (scan_op == "return" || scan_op == "jump" || scan_op == "goinvoke" || scan_op == "disrupt") {
scan_dead = true scan_dead = true
} }
} }
has_invokes = invoke_count > 0 has_invokes = use_invoke_trampoline && invoke_count > 0
// Function signature: (ctx, frame_ptr) → JSValue // Function signature: (ctx, frame_ptr) → JSValue
emit(`export function l $${name}(l %ctx, l %fp) {`) emit(`export function l $${name}(l %ctx, l %fp) {`)
@@ -1524,6 +1557,39 @@ var qbe_emit = function(ir, qbe, export_name) {
} }
} }
// Peephole: inline `text(x)` intrinsic call sequence
// access text; frame; null this; setarg 0 this; setarg 1 x; invoke
if (op == "access" && is_object(a2) && a2.make == "intrinsic" && a2.name == "text") {
if (instr_idx + 5 < length(instrs)) {
peek1 = instrs[instr_idx + 1]
peek2 = instrs[instr_idx + 2]
peek3 = instrs[instr_idx + 3]
peek4 = instrs[instr_idx + 4]
peek5 = instrs[instr_idx + 5]
if (is_array(peek1) && peek1[0] == "frame" && peek1[2] == a1 && peek1[3] == 1 &&
is_array(peek2) && peek2[0] == "null" &&
is_array(peek3) && peek3[0] == "setarg" &&
is_array(peek4) && peek4[0] == "setarg" &&
is_array(peek5) && peek5[0] == "invoke") {
text_frame_slot = peek1[1]
text_this_slot = peek2[1]
if (peek3[1] == text_frame_slot && peek3[2] == 0 && peek3[3] == text_this_slot &&
peek4[1] == text_frame_slot && peek4[2] == 1 &&
peek5[1] == text_frame_slot && peek5[2] == text_this_slot) {
text_arg_slot = peek4[3]
text_dest_slot = peek5[2]
v = s_read(text_arg_slot)
p = fresh()
emit(` %${p}_r =l call $JS_CellText(l %ctx, l ${v})`)
refresh_fp()
s_write(text_dest_slot, `%${p}_r`)
i = instr_idx + 6
continue
}
}
}
}
// --- Constants --- // --- Constants ---
if (op == "int") { if (op == "int") {
@@ -2416,42 +2482,33 @@ var qbe_emit = function(ir, qbe, export_name) {
emit(` storel ${lhs}, %${p}_slot`) emit(` storel ${lhs}, %${p}_slot`)
continue continue
} }
if (op == "invoke") { if (op == "invoke" || op == "tail_invoke") {
// Dispatch loop invoke: store resume info, signal, return 0 if (use_invoke_trampoline) {
seg_counter = seg_counter + 1 // Signal dispatcher to call frame in slot a1 and resume at @_segN.
seg_num = seg_counter seg_counter = seg_counter + 1
// Store (seg_num << 16 | result_slot) as tagged int in frame->address resume_val = seg_counter * 65536 + a2
resume_val = seg_num * 65536 + a2 p = fresh()
// frame->address is at fp - 8, store as tagged int (n << 1) emit(` %${p}_addrp =l sub %fp, 8`)
emit(` %_inv_addr${text(seg_num)} =l sub %fp, 8`) // frame->address holds JS_NewInt32((seg << 16) | ret_slot), tagged.
emit(` storel ${text(resume_val * 2)}, %_inv_addr${text(seg_num)}`) emit(` storel ${text(resume_val * 2)}, %${p}_addrp`)
emit(` call $cell_rt_signal_call(l %ctx, l %fp, l ${text(a1)})`) emit(` call $cell_rt_signal_call(l %ctx, l %fp, l ${text(a1)})`)
emit(" ret 0") emit(` ret ${text(qbe.js_null)}`)
emit(`@_seg${text(seg_num)}`) emit(`@_seg${text(seg_counter)}`)
// Check for exception marker in destination slot after resume. // Dispatcher writes JS_EXCEPTION into ret slot on error; branch here.
// Dispatch writes JS_EXCEPTION into ret_slot on exceptional return. v = s_read(a2)
rv = s_read(a2) emit(` %${p}_exc =w ceql ${v}, ${text(qbe.js_exception)}`)
p = fresh() if (has_handler && !in_handler) {
emit(` %${p} =w ceql ${rv}, ${text(qbe.js_exception)}`) emit(` jnz %${p}_exc, @disruption_handler, @${p}_ok`)
if (has_handler && !in_handler) { } else {
emit(` jnz %${p}, @disruption_handler, @${p}_ok`) needs_exc_ret = true
emit(` jnz %${p}_exc, @_exc_ret, @${p}_ok`)
}
emit(`@${p}_ok`)
} else { } else {
needs_exc_ret = true // Direct helper invoke path (disabled by default).
emit(` jnz %${p}, @_exc_ret, @${p}_ok`) emit(` %fp =l call $__invoke_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
emit_exc_check()
} }
emit(`@${p}_ok`)
last_was_term = false
continue
}
if (op == "tail_invoke") {
// Tail call: hand control to dispatch loop and do not resume this segment.
// Use 0xFFFF as ret_slot (no result writeback into current frame).
p = fresh()
emit(` %${p}_addr =l sub %fp, 8`)
emit(` storel ${text(65535 * 2)}, %${p}_addr`)
emit(` call $cell_rt_signal_tail_call(l %ctx, l %fp, l ${text(a1)})`)
emit(" ret 0")
last_was_term = true
continue continue
} }
if (op == "goframe") { if (op == "goframe") {
@@ -2460,13 +2517,30 @@ var qbe_emit = function(ir, qbe, export_name) {
continue continue
} }
if (op == "goinvoke") { if (op == "goinvoke") {
// Dispatch loop tail call: signal tail call and return 0 if (use_invoke_trampoline) {
// Use 0xFFFF as ret_slot (no result to store — it's a tail call) // Tail call via dispatcher: no resume in this frame.
p = fresh() emit(` call $cell_rt_signal_tail_call(l %ctx, l %fp, l ${text(a1)})`)
emit(` %${p}_addr =l sub %fp, 8`) emit(` ret ${text(qbe.js_null)}`)
emit(` storel ${text(65535 * 2)}, %${p}_addr`) } else {
emit(` call $cell_rt_signal_tail_call(l %ctx, l %fp, l ${text(a1)})`) // Direct helper goinvoke path (disabled by default).
emit(" ret 0") v = s_read(a1)
p = fresh()
emit(` %${p}_r =l call $cell_rt_goinvoke(l %ctx, l ${v})`)
emit(` %${p}_exc =w ceql %${p}_r, ${text(qbe.js_exception)}`)
if (has_handler && !in_handler) {
emit(` jnz %${p}_exc, @${p}_exc, @${p}_ok`)
emit(`@${p}_exc`)
emit(` %fp =l call $cell_rt_refresh_fp(l %ctx)`)
emit(` jmp @disruption_handler`)
emit(`@${p}_ok`)
emit(` ret %${p}_r`)
} else {
needs_exc_ret = true
emit(` jnz %${p}_exc, @_exc_ret, @${p}_ok`)
emit(`@${p}_ok`)
emit(` ret %${p}_r`)
}
}
last_was_term = true last_was_term = true
continue continue
} }

View File

@@ -37,6 +37,7 @@ static char *compute_blake2_hex(const char *data, size_t size) {
uint8_t hash[32]; uint8_t hash[32];
crypto_blake2b(hash, 32, (const uint8_t *)data, size); crypto_blake2b(hash, 32, (const uint8_t *)data, size);
char *hex = malloc(65); char *hex = malloc(65);
if (!hex) return NULL;
for (int i = 0; i < 32; i++) for (int i = 0; i < 32; i++)
snprintf(hex + i * 2, 3, "%02x", hash[i]); snprintf(hex + i * 2, 3, "%02x", hash[i]);
return hex; return hex;
@@ -64,6 +65,7 @@ static int write_cache_file(const char *path, const uint8_t *data, size_t size)
// Returns heap-allocated binary data and sets *out_size, or NULL on failure // Returns heap-allocated binary data and sets *out_size, or NULL on failure
static char *load_or_cache_bootstrap(const char *mcode_data, size_t mcode_size, size_t *out_size) { static char *load_or_cache_bootstrap(const char *mcode_data, size_t mcode_size, size_t *out_size) {
char *hex = compute_blake2_hex(mcode_data, mcode_size); char *hex = compute_blake2_hex(mcode_data, mcode_size);
if (!hex) return NULL;
char *cpath = build_cache_path(hex); char *cpath = build_cache_path(hex);
free(hex); free(hex);
@@ -222,6 +224,7 @@ static char *try_engine_cache(size_t *out_size) {
char *hex = compute_blake2_hex(src, src_size); char *hex = compute_blake2_hex(src, src_size);
free(src); free(src);
if (!hex) return NULL;
char *cpath = build_cache_path(hex); char *cpath = build_cache_path(hex);
if (!cpath) { free(hex); return NULL; } if (!cpath) { free(hex); return NULL; }
free(hex); free(hex);

View File

@@ -583,11 +583,13 @@ void cell_rt_put_closure(JSContext *ctx, void *fp, JSValue val, int64_t depth,
#define AOT_GC_REF_CHUNK_SIZE 1024 #define AOT_GC_REF_CHUNK_SIZE 1024
typedef struct AOTGCRefChunk { typedef struct AOTGCRefChunk {
JSGCRef refs[AOT_GC_REF_CHUNK_SIZE]; JSGCRef refs[AOT_GC_REF_CHUNK_SIZE];
uint8_t inited[AOT_GC_REF_CHUNK_SIZE];
} AOTGCRefChunk; } AOTGCRefChunk;
static CELL_THREAD_LOCAL AOTGCRefChunk **g_aot_gc_ref_chunks = NULL; static CELL_THREAD_LOCAL AOTGCRefChunk **g_aot_gc_ref_chunks = NULL;
static CELL_THREAD_LOCAL int g_aot_gc_ref_chunk_count = 0; static CELL_THREAD_LOCAL int g_aot_gc_ref_chunk_count = 0;
static CELL_THREAD_LOCAL int g_aot_depth = 0; static CELL_THREAD_LOCAL int g_aot_depth = 0;
static CELL_THREAD_LOCAL JSContext *g_aot_gc_ref_ctx = NULL;
int cell_rt_native_active(void) { int cell_rt_native_active(void) {
return g_aot_depth > 0; return g_aot_depth > 0;
@@ -624,14 +626,50 @@ static inline JSGCRef *aot_gc_ref_at(int depth_index) {
return &g_aot_gc_ref_chunks[chunk_index]->refs[slot_index]; return &g_aot_gc_ref_chunks[chunk_index]->refs[slot_index];
} }
static inline uint8_t *aot_gc_ref_inited_at(int depth_index) {
int chunk_index = depth_index / AOT_GC_REF_CHUNK_SIZE;
int slot_index = depth_index % AOT_GC_REF_CHUNK_SIZE;
return &g_aot_gc_ref_chunks[chunk_index]->inited[slot_index];
}
/* GC refs are owned by a specific JSContext. If context changes on this thread,
unregister previous refs and reset per-slot initialization state. */
static void aot_gc_ref_reset_ctx(JSContext *ctx) {
if (g_aot_gc_ref_ctx == ctx)
return;
if (g_aot_gc_ref_ctx) {
for (int ci = 0; ci < g_aot_gc_ref_chunk_count; ci++) {
AOTGCRefChunk *chunk = g_aot_gc_ref_chunks[ci];
for (int si = 0; si < AOT_GC_REF_CHUNK_SIZE; si++) {
if (chunk->inited[si]) {
JS_DeleteGCRef(g_aot_gc_ref_ctx, &chunk->refs[si]);
chunk->inited[si] = 0;
chunk->refs[si].val = JS_NULL;
}
}
}
}
g_aot_gc_ref_ctx = ctx;
}
static inline void aot_gc_ref_activate(JSContext *ctx, int depth_index) {
JSGCRef *ref = aot_gc_ref_at(depth_index);
uint8_t *inited = aot_gc_ref_inited_at(depth_index);
if (!*inited) {
JS_AddGCRef(ctx, ref);
*inited = 1;
}
}
JSValue *cell_rt_enter_frame(JSContext *ctx, int64_t nr_slots) { JSValue *cell_rt_enter_frame(JSContext *ctx, int64_t nr_slots) {
aot_gc_ref_reset_ctx(ctx);
if (!ensure_aot_gc_ref_slot(ctx, g_aot_depth)) { if (!ensure_aot_gc_ref_slot(ctx, g_aot_depth)) {
return NULL; return NULL;
} }
JSFrameRegister *frame = alloc_frame_register(ctx, (int)nr_slots); JSFrameRegister *frame = alloc_frame_register(ctx, (int)nr_slots);
if (!frame) return NULL; if (!frame) return NULL;
aot_gc_ref_activate(ctx, g_aot_depth);
JSGCRef *ref = aot_gc_ref_at(g_aot_depth); JSGCRef *ref = aot_gc_ref_at(g_aot_depth);
JS_AddGCRef(ctx, ref);
ref->val = JS_MKPTR(frame); ref->val = JS_MKPTR(frame);
g_aot_depth++; g_aot_depth++;
return (JSValue *)frame->slots; return (JSValue *)frame->slots;
@@ -639,10 +677,11 @@ JSValue *cell_rt_enter_frame(JSContext *ctx, int64_t nr_slots) {
/* Push an already-allocated frame onto the active AOT frame stack. */ /* Push an already-allocated frame onto the active AOT frame stack. */
static int cell_rt_push_existing_frame(JSContext *ctx, JSValue frame_val) { static int cell_rt_push_existing_frame(JSContext *ctx, JSValue frame_val) {
aot_gc_ref_reset_ctx(ctx);
if (!ensure_aot_gc_ref_slot(ctx, g_aot_depth)) if (!ensure_aot_gc_ref_slot(ctx, g_aot_depth))
return 0; return 0;
aot_gc_ref_activate(ctx, g_aot_depth);
JSGCRef *ref = aot_gc_ref_at(g_aot_depth); JSGCRef *ref = aot_gc_ref_at(g_aot_depth);
JS_AddGCRef(ctx, ref);
ref->val = frame_val; ref->val = frame_val;
g_aot_depth++; g_aot_depth++;
return 1; return 1;
@@ -682,12 +721,13 @@ JSValue *cell_rt_refresh_fp_checked(JSContext *ctx) {
} }
void cell_rt_leave_frame(JSContext *ctx) { void cell_rt_leave_frame(JSContext *ctx) {
(void)ctx;
if (g_aot_depth <= 0) { if (g_aot_depth <= 0) {
fprintf(stderr, "[BUG] cell_rt_leave_frame underflow\n"); fprintf(stderr, "[BUG] cell_rt_leave_frame underflow\n");
abort(); abort();
} }
g_aot_depth--; g_aot_depth--;
JS_DeleteGCRef(ctx, aot_gc_ref_at(g_aot_depth)); aot_gc_ref_at(g_aot_depth)->val = JS_NULL;
} }
/* --- Function creation and calling --- */ /* --- Function creation and calling --- */

View File

@@ -1426,8 +1426,6 @@ static JSValue js_cell_splat (JSContext *ctx, JSValue this_val, int argc, JSValu
static JSValue js_cell_meme (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); static JSValue js_cell_meme (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_fn_apply (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); static JSValue js_cell_fn_apply (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_call (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); static JSValue js_cell_call (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_modulo (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_neg (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_not (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); static JSValue js_cell_not (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
JSValue js_cell_text_lower (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); JSValue js_cell_text_lower (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
JSValue js_cell_text_upper (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); JSValue js_cell_text_upper (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
@@ -1438,17 +1436,6 @@ static JSValue js_cell_text_search (JSContext *ctx, JSValue this_val, int argc,
static JSValue js_cell_text_extract (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); static JSValue js_cell_text_extract (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
JSValue js_cell_character (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); JSValue js_cell_character (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_number (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); static JSValue js_cell_number (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_number_abs (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_number_sign (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_number_floor (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_number_ceiling (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_number_round (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_number_trunc (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_number_whole (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_number_fraction (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_number_min (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_number_max (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_number_remainder (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_object (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); static JSValue js_cell_object (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_text_format (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); static JSValue js_cell_text_format (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_print (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); static JSValue js_print (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);

View File

@@ -10514,15 +10514,46 @@ JSValue JS_CellCall (JSContext *ctx, JSValue fn, JSValue this_val, JSValue args)
return js_cell_call (ctx, JS_NULL, argc, argv); return js_cell_call (ctx, JS_NULL, argc, argv);
} }
static int js_cell_read_number_strict (JSValue val, double *out) {
uint32_t tag = JS_VALUE_GET_TAG (val);
if (tag == JS_TAG_INT) {
*out = (double)JS_VALUE_GET_INT (val);
return 0;
}
if (JS_TAG_IS_FLOAT64 (tag)) {
*out = JS_VALUE_GET_FLOAT64 (val);
return 0;
}
return -1;
}
static JSValue js_cell_number_from_double (JSContext *ctx, double d) {
if (d >= INT32_MIN && d <= INT32_MAX) {
int32_t i = (int32_t)d;
if ((double)i == d)
return JS_NewInt32 (ctx, i);
}
return JS_NewFloat64 (ctx, d);
}
/* C API: modulo(a, b) - modulo operation */ /* C API: modulo(a, b) - modulo operation */
JSValue JS_CellModulo (JSContext *ctx, JSValue a, JSValue b) { JSValue JS_CellModulo (JSContext *ctx, JSValue a, JSValue b) {
JSValue argv[2] = { a, b }; double dividend, divisor;
return js_cell_modulo (ctx, JS_NULL, 2, argv); if (js_cell_read_number_strict (a, &dividend) < 0) return JS_NULL;
if (js_cell_read_number_strict (b, &divisor) < 0) return JS_NULL;
if (isnan (dividend) || isnan (divisor)) return JS_NULL;
if (divisor == 0.0) return JS_NULL;
if (dividend == 0.0) return JS_NewFloat64 (ctx, 0.0);
return js_cell_number_from_double (ctx,
dividend - (divisor * floor (dividend / divisor)));
} }
/* C API: neg(val) - negate number */ /* C API: neg(val) - negate number */
JSValue JS_CellNeg (JSContext *ctx, JSValue val) { JSValue JS_CellNeg (JSContext *ctx, JSValue val) {
return js_cell_neg (ctx, JS_NULL, 1, &val); double d;
if (js_cell_read_number_strict (val, &d) < 0) return JS_NULL;
if (isnan (d)) return JS_NULL;
return js_cell_number_from_double (ctx, -d);
} }
/* C API: not(val) - logical not */ /* C API: not(val) - logical not */
@@ -10647,60 +10678,86 @@ JSValue JS_CellNumber (JSContext *ctx, JSValue val) {
/* C API: abs(num) - absolute value */ /* C API: abs(num) - absolute value */
JSValue JS_CellAbs (JSContext *ctx, JSValue num) { JSValue JS_CellAbs (JSContext *ctx, JSValue num) {
return js_cell_number_abs (ctx, JS_NULL, 1, &num); double d;
if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL;
return js_cell_number_from_double (ctx, fabs (d));
} }
/* C API: sign(num) - sign of number (-1, 0, 1) */ /* C API: sign(num) - sign of number (-1, 0, 1) */
JSValue JS_CellSign (JSContext *ctx, JSValue num) { JSValue JS_CellSign (JSContext *ctx, JSValue num) {
return js_cell_number_sign (ctx, JS_NULL, 1, &num); double d;
if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL;
if (d < 0) return JS_NewInt32 (ctx, -1);
if (d > 0) return JS_NewInt32 (ctx, 1);
return JS_NewInt32 (ctx, 0);
} }
/* C API: floor(num) - floor */ /* C API: floor(num) - floor */
JSValue JS_CellFloor (JSContext *ctx, JSValue num) { JSValue JS_CellFloor (JSContext *ctx, JSValue num) {
return js_cell_number_floor (ctx, JS_NULL, 1, &num); double d;
if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL;
return js_cell_number_from_double (ctx, floor (d));
} }
/* C API: ceiling(num) - ceiling */ /* C API: ceiling(num) - ceiling */
JSValue JS_CellCeiling (JSContext *ctx, JSValue num) { JSValue JS_CellCeiling (JSContext *ctx, JSValue num) {
return js_cell_number_ceiling (ctx, JS_NULL, 1, &num); double d;
if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL;
return js_cell_number_from_double (ctx, ceil (d));
} }
/* C API: round(num) - round to nearest integer */ /* C API: round(num) - round to nearest integer */
JSValue JS_CellRound (JSContext *ctx, JSValue num) { JSValue JS_CellRound (JSContext *ctx, JSValue num) {
return js_cell_number_round (ctx, JS_NULL, 1, &num); double d;
if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL;
return js_cell_number_from_double (ctx, round (d));
} }
/* C API: trunc(num) - truncate towards zero */ /* C API: trunc(num) - truncate towards zero */
JSValue JS_CellTrunc (JSContext *ctx, JSValue num) { JSValue JS_CellTrunc (JSContext *ctx, JSValue num) {
return js_cell_number_trunc (ctx, JS_NULL, 1, &num); double d;
if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL;
return js_cell_number_from_double (ctx, trunc (d));
} }
/* C API: whole(num) - integer part */ /* C API: whole(num) - integer part */
JSValue JS_CellWhole (JSContext *ctx, JSValue num) { JSValue JS_CellWhole (JSContext *ctx, JSValue num) {
return js_cell_number_whole (ctx, JS_NULL, 1, &num); double d;
if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL;
return js_cell_number_from_double (ctx, trunc (d));
} }
/* C API: fraction(num) - fractional part */ /* C API: fraction(num) - fractional part */
JSValue JS_CellFraction (JSContext *ctx, JSValue num) { JSValue JS_CellFraction (JSContext *ctx, JSValue num) {
return js_cell_number_fraction (ctx, JS_NULL, 1, &num); double d;
if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL;
return js_cell_number_from_double (ctx, d - trunc (d));
} }
/* C API: min(a, b) - minimum of two numbers */ /* C API: min(a, b) - minimum of two numbers */
JSValue JS_CellMin (JSContext *ctx, JSValue a, JSValue b) { JSValue JS_CellMin (JSContext *ctx, JSValue a, JSValue b) {
JSValue argv[2] = { a, b }; double da, db;
return js_cell_number_min (ctx, JS_NULL, 2, argv); if (js_cell_read_number_strict (a, &da) < 0) return JS_NULL;
if (js_cell_read_number_strict (b, &db) < 0) return JS_NULL;
return js_cell_number_from_double (ctx, da < db ? da : db);
} }
/* C API: max(a, b) - maximum of two numbers */ /* C API: max(a, b) - maximum of two numbers */
JSValue JS_CellMax (JSContext *ctx, JSValue a, JSValue b) { JSValue JS_CellMax (JSContext *ctx, JSValue a, JSValue b) {
JSValue argv[2] = { a, b }; double da, db;
return js_cell_number_max (ctx, JS_NULL, 2, argv); if (js_cell_read_number_strict (a, &da) < 0) return JS_NULL;
if (js_cell_read_number_strict (b, &db) < 0) return JS_NULL;
return js_cell_number_from_double (ctx, da > db ? da : db);
} }
/* C API: remainder(a, b) - remainder after division */ /* C API: remainder(a, b) - remainder after division */
JSValue JS_CellRemainder (JSContext *ctx, JSValue a, JSValue b) { JSValue JS_CellRemainder (JSContext *ctx, JSValue a, JSValue b) {
JSValue argv[2] = { a, b }; double dividend, divisor;
return js_cell_number_remainder (ctx, JS_NULL, 2, argv); if (js_cell_read_number_strict (a, &dividend) < 0) return JS_NULL;
if (js_cell_read_number_strict (b, &divisor) < 0) return JS_NULL;
if (divisor == 0.0) return JS_NULL;
return js_cell_number_from_double (ctx,
dividend - (trunc (dividend / divisor) * divisor));
} }
/* Object functions */ /* Object functions */
@@ -11374,7 +11431,7 @@ static void JS_AddIntrinsicBaseObjects (JSContext *ctx) {
js_set_global_cfunc(ctx, "filter", js_cell_array_filter, 2); js_set_global_cfunc(ctx, "filter", js_cell_array_filter, 2);
js_set_global_cfunc(ctx, "sort", js_cell_array_sort, 2); js_set_global_cfunc(ctx, "sort", js_cell_array_sort, 2);
/* Number utility functions */ /* Number intrinsics: direct calls lower to mcode; globals remain for first-class use. */
js_set_global_cfunc(ctx, "whole", js_cell_number_whole, 1); js_set_global_cfunc(ctx, "whole", js_cell_number_whole, 1);
js_set_global_cfunc(ctx, "fraction", js_cell_number_fraction, 1); js_set_global_cfunc(ctx, "fraction", js_cell_number_fraction, 1);
js_set_global_cfunc(ctx, "floor", js_cell_number_floor, 2); js_set_global_cfunc(ctx, "floor", js_cell_number_floor, 2);