Merge branch 'audit_gc' into fix_slots
This commit is contained in:
File diff suppressed because it is too large
Load Diff
9246
boot/engine.cm.mcode
9246
boot/engine.cm.mcode
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
27049
boot/qbe_emit.cm.mcode
27049
boot/qbe_emit.cm.mcode
File diff suppressed because it is too large
Load Diff
2139
boot/time.cm.mcode
2139
boot/time.cm.mcode
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ Every heap-allocated object begins with a 64-bit header word (`objhdr_t`):
|
||||
|
||||
### Flags (bits 3-7)
|
||||
|
||||
- **Bit 3 (S)** — Stone flag. If set, the object is immutable and excluded from GC.
|
||||
- **Bit 3 (S)** — Stone flag. If set, the object is immutable. Stone text in the constant table (ct) is not copied by GC since it lives outside the heap; stone objects on the GC heap are copied normally.
|
||||
- **Bit 4 (P)** — Properties flag.
|
||||
- **Bit 5 (A)** — Array flag.
|
||||
- **Bit 7 (R)** — Reserved.
|
||||
@@ -69,7 +69,9 @@ struct JSText {
|
||||
};
|
||||
```
|
||||
|
||||
Text is stored as UTF-32, with two 32-bit codepoints packed per 64-bit word. When a text object is stoned, the length field is repurposed to cache the hash value (computed via `fash64`), since stoned text is immutable and the hash never changes.
|
||||
Text is stored as UTF-32, with two 32-bit codepoints packed per 64-bit word.
|
||||
|
||||
A mutable text (pretext) uses capacity for the allocated slot count and length for the current codepoint count. When a pretext is stoned, the capacity field is set to the actual length (codepoint count), and the length field is zeroed for use as a lazy hash cache (computed via `fash64` on first use as a key). Since stoned text is immutable, the hash never changes. Stoning is done in-place — no new allocation is needed.
|
||||
|
||||
## Record
|
||||
|
||||
@@ -111,7 +113,7 @@ struct JSFrame {
|
||||
objhdr_t header; // type=6, capacity=slot count
|
||||
JSValue function; // owning function
|
||||
JSValue caller; // parent frame
|
||||
uint32_t return_pc; // return address
|
||||
JSValue address; // return address
|
||||
JSValue slots[]; // [this][args][captured][locals][temps]
|
||||
};
|
||||
```
|
||||
@@ -138,4 +140,4 @@ All objects are aligned to 8 bytes. The total size in bytes for each type:
|
||||
| Record | `8 + 8 + 8 + (capacity + 1) * 16` |
|
||||
| Function | `sizeof(JSFunction)` (fixed) |
|
||||
| Code | `sizeof(JSFunctionBytecode)` (fixed) |
|
||||
| Frame | `8 + 8 + 8 + 4 + capacity * 8` |
|
||||
| Frame | `8 + 8 + 8 + 8 + capacity * 8` |
|
||||
|
||||
@@ -82,6 +82,7 @@ use_cache['fold'] = fold_mod
|
||||
// Always load mcode compiler module
|
||||
var mcode_mod = boot_load("mcode", boot_env)
|
||||
use_cache['mcode'] = mcode_mod
|
||||
use_cache['core/mcode'] = mcode_mod
|
||||
var streamline_mod = null
|
||||
|
||||
// Warn if any .cm source is newer than its compiled bytecode
|
||||
@@ -163,6 +164,7 @@ function analyze(src, filename) {
|
||||
// Load optimization pipeline modules (needs analyze to be defined)
|
||||
streamline_mod = boot_load("streamline", boot_env)
|
||||
use_cache['streamline'] = streamline_mod
|
||||
use_cache['core/streamline'] = streamline_mod
|
||||
|
||||
// Lazy-loaded verify_ir module (loaded on first use)
|
||||
var _verify_ir_mod = null
|
||||
|
||||
@@ -242,6 +242,9 @@ core_extras.shop_path = shop_path
|
||||
core_extras.analyze = analyze
|
||||
core_extras.run_ast_fn = run_ast_fn
|
||||
core_extras.run_ast_noopt_fn = run_ast_noopt_fn
|
||||
os.analyze = analyze
|
||||
os.run_ast_fn = run_ast_fn
|
||||
os.run_ast_noopt_fn = run_ast_noopt_fn
|
||||
core_extras.core_json = json
|
||||
core_extras.actor_api = $_
|
||||
core_extras.runtime_env = runtime_env
|
||||
|
||||
@@ -537,8 +537,14 @@ function resolve_mod_fn(path, pkg) {
|
||||
}
|
||||
|
||||
// Compile via full pipeline: analyze → mcode → streamline → serialize
|
||||
if (!_mcode_mod) _mcode_mod = Shop.use("mcode", null)
|
||||
if (!_streamline_mod) _streamline_mod = Shop.use("streamline", null)
|
||||
// Load compiler modules from use_cache directly (NOT via Shop.use, which
|
||||
// would re-enter resolve_locator → resolve_mod_fn → infinite recursion)
|
||||
if (!_mcode_mod) _mcode_mod = use_cache['core/mcode'] || use_cache['mcode']
|
||||
if (!_streamline_mod) _streamline_mod = use_cache['core/streamline'] || use_cache['streamline']
|
||||
if (!_mcode_mod || !_streamline_mod) {
|
||||
print(`error: compiler modules not loaded (mcode=${_mcode_mod != null}, streamline=${_streamline_mod != null})`)
|
||||
disrupt
|
||||
}
|
||||
ast = analyze(content, path)
|
||||
ir = _mcode_mod(ast)
|
||||
optimized = _streamline_mod(ir)
|
||||
|
||||
@@ -850,9 +850,14 @@ static inline objhdr_t *chase(JSValue v) {
|
||||
updated to follow the chain to the live copy. */
|
||||
static inline void mach_resolve_forward(JSValue *slot) {
|
||||
if (JS_IsPtr(*slot)) {
|
||||
objhdr_t h = *(objhdr_t *)JS_VALUE_GET_PTR(*slot);
|
||||
if (objhdr_type(h) == OBJ_FORWARD) {
|
||||
*slot = JS_MKPTR(objhdr_fwd_ptr(h));
|
||||
objhdr_t *oh = (objhdr_t *)JS_VALUE_GET_PTR(*slot);
|
||||
if (objhdr_type(*oh) == OBJ_FORWARD) {
|
||||
do {
|
||||
objhdr_t *next = (objhdr_t *)objhdr_fwd_ptr(*oh);
|
||||
if (!next) break;
|
||||
oh = next;
|
||||
} while (objhdr_type(*oh) == OBJ_FORWARD);
|
||||
*slot = JS_MKPTR(oh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
104
source/runtime.c
104
source/runtime.c
@@ -595,6 +595,10 @@ int rec_resize (JSContext *ctx, JSValue *pobj, uint64_t new_mask) {
|
||||
/* Allocate new record with larger capacity - may trigger GC! */
|
||||
size_t slots_size = sizeof (slot) * (new_mask + 1);
|
||||
size_t total_size = sizeof (JSRecord) + slots_size;
|
||||
if (total_size >= 100000) {
|
||||
fprintf(stderr, "LARGE_REC_RESIZE: new_mask=%llu total=%zu old_mask=%llu\n",
|
||||
(unsigned long long)new_mask, total_size, (unsigned long long)old_mask);
|
||||
}
|
||||
|
||||
JSRecord *new_rec = js_malloc (ctx, total_size);
|
||||
if (!new_rec) {
|
||||
@@ -1327,9 +1331,6 @@ static inline int ptr_in_range (void *p, uint8_t *b, uint8_t *e) {
|
||||
return q >= b && q < e;
|
||||
}
|
||||
|
||||
static const char *gc_dbg_phase = "?";
|
||||
static void *gc_dbg_parent = NULL;
|
||||
|
||||
JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *from_end, uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) {
|
||||
if (!JS_IsPtr (v)) return v;
|
||||
|
||||
@@ -1353,8 +1354,8 @@ JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *f
|
||||
}
|
||||
|
||||
if (type != OBJ_ARRAY && type != OBJ_TEXT && type != OBJ_RECORD && type != OBJ_FUNCTION && type != OBJ_FRAME) {
|
||||
fprintf (stderr, "gc_copy_value: invalid type %d at %p (hdr=0x%llx) phase=%s parent=%p\n",
|
||||
type, ptr, (unsigned long long)hdr, gc_dbg_phase, gc_dbg_parent);
|
||||
fprintf (stderr, "gc_copy_value: invalid type %d at %p (hdr=0x%llx)\n",
|
||||
type, ptr, (unsigned long long)hdr);
|
||||
fflush (stderr);
|
||||
abort ();
|
||||
}
|
||||
@@ -1375,6 +1376,18 @@ JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *f
|
||||
}
|
||||
}
|
||||
|
||||
/* Recursively scan a code tree's cpools to arbitrary nesting depth */
|
||||
static void gc_scan_code_tree (JSContext *ctx, JSCodeRegister *code,
|
||||
uint8_t *from_base, uint8_t *from_end,
|
||||
uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) {
|
||||
for (uint32_t i = 0; i < code->cpool_count; i++)
|
||||
code->cpool[i] = gc_copy_value (ctx, code->cpool[i], from_base, from_end, to_base, to_free, to_end);
|
||||
code->name = gc_copy_value (ctx, code->name, from_base, from_end, to_base, to_free, to_end);
|
||||
for (uint32_t i = 0; i < code->func_count; i++)
|
||||
if (code->functions[i])
|
||||
gc_scan_code_tree (ctx, code->functions[i], from_base, from_end, to_base, to_free, to_end);
|
||||
}
|
||||
|
||||
/* Scan a copied object and update its internal references */
|
||||
void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *from_end,
|
||||
uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) {
|
||||
@@ -1398,8 +1411,9 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro
|
||||
#endif
|
||||
/* Copy prototype */
|
||||
rec->proto = gc_copy_value (ctx, rec->proto, from_base, from_end, to_base, to_free, to_end);
|
||||
/* Copy table entries */
|
||||
for (uint32_t i = 0; i <= mask; i++) {
|
||||
/* Copy table entries — skip slot[0] which stores packed metadata
|
||||
(class_id | rec_id << 32), not JSValues */
|
||||
for (uint32_t i = 1; i <= mask; i++) {
|
||||
JSValue k = rec->slots[i].key;
|
||||
if (!rec_key_is_empty (k) && !rec_key_is_tomb (k)) {
|
||||
rec->slots[i].key = gc_copy_value (ctx, k, from_base, from_end, to_base, to_free, to_end);
|
||||
@@ -1413,26 +1427,11 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro
|
||||
/* Scan the function name */
|
||||
fn->name = gc_copy_value (ctx, fn->name, from_base, from_end, to_base, to_free, to_end);
|
||||
if (fn->kind == JS_FUNC_KIND_REGISTER && fn->u.reg.code) {
|
||||
/* Register VM function - scan cpool (off-heap but contains JSValues) */
|
||||
JSCodeRegister *code = fn->u.reg.code;
|
||||
for (uint32_t i = 0; i < code->cpool_count; i++) {
|
||||
code->cpool[i] = gc_copy_value (ctx, code->cpool[i], from_base, from_end, to_base, to_free, to_end);
|
||||
}
|
||||
/* Scan function name */
|
||||
code->name = gc_copy_value (ctx, code->name, from_base, from_end, to_base, to_free, to_end);
|
||||
/* Scan code tree to arbitrary nesting depth */
|
||||
gc_scan_code_tree (ctx, fn->u.reg.code, from_base, from_end, to_base, to_free, to_end);
|
||||
/* Scan outer_frame and env_record */
|
||||
fn->u.reg.outer_frame = gc_copy_value (ctx, fn->u.reg.outer_frame, from_base, from_end, to_base, to_free, to_end);
|
||||
fn->u.reg.env_record = gc_copy_value (ctx, fn->u.reg.env_record, from_base, from_end, to_base, to_free, to_end);
|
||||
/* Recursively scan nested function cpools */
|
||||
for (uint32_t i = 0; i < code->func_count; i++) {
|
||||
if (code->functions[i]) {
|
||||
JSCodeRegister *nested = code->functions[i];
|
||||
for (uint32_t j = 0; j < nested->cpool_count; j++) {
|
||||
nested->cpool[j] = gc_copy_value (ctx, nested->cpool[j], from_base, from_end, to_base, to_free, to_end);
|
||||
}
|
||||
nested->name = gc_copy_value (ctx, nested->name, from_base, from_end, to_base, to_free, to_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1532,9 +1531,6 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
|
||||
uint8_t *to_free = new_block;
|
||||
uint8_t *to_end = new_block + new_size;
|
||||
|
||||
gc_dbg_phase = "roots";
|
||||
gc_dbg_parent = NULL;
|
||||
|
||||
#ifdef VALIDATE_GC
|
||||
/* Pre-GC: walk live frame chain and check for bad slot values */
|
||||
if (JS_IsPtr (ctx->reg_current_frame)) {
|
||||
@@ -1665,7 +1661,6 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
|
||||
}
|
||||
|
||||
/* Cheney scan: scan copied objects to find more references */
|
||||
gc_dbg_phase = "scan";
|
||||
uint8_t *scan = to_base;
|
||||
#ifdef DUMP_GC_DETAIL
|
||||
printf(" scan: to_base=%p to_free=%p to_end=%p\n", (void*)to_base, (void*)to_free, (void*)to_end);
|
||||
@@ -1682,7 +1677,6 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
|
||||
printf(" size=%zu\n", obj_size);
|
||||
fflush(stdout);
|
||||
#endif
|
||||
gc_dbg_parent = scan;
|
||||
gc_scan_object (ctx, scan, from_base, from_end, to_base, &to_free, to_end);
|
||||
scan += obj_size;
|
||||
}
|
||||
@@ -1698,6 +1692,7 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
|
||||
ctx->gc_bytes_copied += new_used;
|
||||
size_t recovered = old_used > new_used ? old_used - new_used : 0;
|
||||
|
||||
|
||||
ctx->heap_base = to_base;
|
||||
ctx->heap_free = to_free;
|
||||
ctx->heap_end = to_end;
|
||||
@@ -2322,7 +2317,17 @@ JSText *pretext_concat_value (JSContext *ctx, JSText *s, JSValue v) {
|
||||
JSText *p = JS_VALUE_GET_STRING (v);
|
||||
return pretext_concat (ctx, s, p, 0, (uint32_t)JSText_len (p));
|
||||
}
|
||||
/* Slow path: v needs conversion — root s across JS_ToString which can
|
||||
allocate and trigger GC */
|
||||
JSGCRef s_ref;
|
||||
JS_PushGCRef (ctx, &s_ref);
|
||||
s_ref.val = JS_MKPTR (s);
|
||||
|
||||
JSValue v1 = JS_ToString (ctx, v);
|
||||
|
||||
s = (JSText *)chase (s_ref.val); /* re-fetch after possible GC */
|
||||
JS_PopGCRef (ctx, &s_ref);
|
||||
|
||||
if (JS_IsException (v1)) return NULL;
|
||||
|
||||
if (MIST_IsImmediateASCII (v1)) {
|
||||
@@ -3467,7 +3472,7 @@ int JS_SetPropertyKey (JSContext *ctx, JSValue this_obj, JSValue key, JSValue va
|
||||
JS_ThrowTypeError (ctx, "cannot modify frozen object");
|
||||
return -1;
|
||||
}
|
||||
return rec_set_own (ctx, rec, key, val);
|
||||
return rec_set_own (ctx, &this_obj, key, val);
|
||||
}
|
||||
|
||||
/* For string keys, use text directly as key */
|
||||
@@ -5211,16 +5216,31 @@ JSValue js_regexp_toString (JSContext *ctx, JSValue this_val, int argc, JSValue
|
||||
JSText *b = pretext_init (ctx, 0);
|
||||
if (!b) return JS_EXCEPTION;
|
||||
|
||||
/* Root b across allocating calls (JS_GetProperty can trigger GC) */
|
||||
JSGCRef b_ref;
|
||||
JS_PushGCRef (ctx, &b_ref);
|
||||
b_ref.val = JS_MKPTR (b);
|
||||
|
||||
b = pretext_putc (ctx, b, '/');
|
||||
if (!b) return JS_EXCEPTION;
|
||||
if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; }
|
||||
b_ref.val = JS_MKPTR (b);
|
||||
|
||||
pattern = JS_GetProperty (ctx, this_val, JS_KEY_source);
|
||||
b = (JSText *)chase (b_ref.val);
|
||||
b = pretext_concat_value (ctx, b, pattern);
|
||||
if (!b) return JS_EXCEPTION;
|
||||
if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; }
|
||||
b_ref.val = JS_MKPTR (b);
|
||||
|
||||
b = pretext_putc (ctx, b, '/');
|
||||
if (!b) return JS_EXCEPTION;
|
||||
if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; }
|
||||
b_ref.val = JS_MKPTR (b);
|
||||
|
||||
flags = JS_GetProperty (ctx, this_val, JS_KEY_flags);
|
||||
b = (JSText *)chase (b_ref.val);
|
||||
b = pretext_concat_value (ctx, b, flags);
|
||||
if (!b) return JS_EXCEPTION;
|
||||
if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; }
|
||||
|
||||
JS_PopGCRef (ctx, &b_ref);
|
||||
return pretext_end (ctx, b);
|
||||
}
|
||||
|
||||
@@ -7098,14 +7118,26 @@ JSValue js_cell_text_codepoint (JSContext *ctx, JSValue this_val, int argc, JSVa
|
||||
* file. */
|
||||
|
||||
static JSText *pt_concat_value_to_string_free (JSContext *ctx, JSText *b, JSValue v) {
|
||||
JSGCRef s_ref;
|
||||
/* Root b across JS_ToString which can allocate and trigger GC */
|
||||
JSGCRef b_ref, s_ref;
|
||||
JS_PushGCRef (ctx, &b_ref);
|
||||
b_ref.val = JS_MKPTR (b);
|
||||
|
||||
JSValue s = JS_ToString (ctx, v);
|
||||
if (JS_IsException (s)) return NULL;
|
||||
if (JS_IsException (s)) {
|
||||
JS_PopGCRef (ctx, &b_ref);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Root s — pretext_concat_value can trigger GC and move the heap string */
|
||||
JS_PushGCRef (ctx, &s_ref);
|
||||
s_ref.val = s;
|
||||
|
||||
b = (JSText *)chase (b_ref.val); /* re-fetch after possible GC */
|
||||
b = pretext_concat_value (ctx, b, s_ref.val);
|
||||
|
||||
JS_PopGCRef (ctx, &s_ref);
|
||||
JS_PopGCRef (ctx, &b_ref);
|
||||
return b;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user