Merge branch 'fix_gc' into pitweb

This commit is contained in:
2026-02-15 10:04:54 -06:00
52 changed files with 709697 additions and 135532 deletions

View File

@@ -76,6 +76,7 @@ var qbe_emit = function(ir, qbe, export_name) {
var instrs = fn.instructions
var nr_slots = fn.nr_slots
var nr_args = fn.nr_args
var captured = build_captured(fn)
var name = is_main ? (export_name ? export_name : "cell_main") : "cell_fn_" + text(fn_idx)
name = sanitize(name)
var i = 0
@@ -88,6 +89,7 @@ var qbe_emit = function(ir, qbe, export_name) {
var p = null
var pn = null
var sl = null
var lbl = null
var fop_id = 0
var nr_elems = 0
var ei = 0
@@ -113,22 +115,45 @@ var qbe_emit = function(ir, qbe, export_name) {
emit(` storel ${s(slot)}, %p${text(slot)}`)
}
// Reload captured slots from frame (after invoke, closures may have modified them)
var reload_captured = function() {
var ri = 0
while (ri < nr_slots) {
if (captured[text(ri)] == true) {
emit(` ${s(ri)} =l loadl %p${text(ri)}`)
}
ri = ri + 1
}
}
// Walk instructions
// Slot loads above are not terminators
var last_was_term = false
i = 0
while (i < length(instrs)) {
instr = instrs[i]
i = i + 1
// Labels are plain strings
// Labels are plain strings; skip _nop_ur_ pseudo-labels from streamline
if (is_text(instr)) {
emit("@" + sanitize(instr))
if (starts_with(instr, "_nop_ur_")) continue
lbl = sanitize(instr)
if (!last_was_term) {
emit(` jmp @${lbl}`)
}
emit("@" + lbl)
last_was_term = false
continue
}
// Skip dead code: non-label instructions after a terminator are unreachable
if (last_was_term) continue
op = instr[0]
a1 = instr[1]
a2 = instr[2]
a3 = instr[3]
last_was_term = false
// --- Constants ---
@@ -157,11 +182,11 @@ var qbe_emit = function(ir, qbe, export_name) {
if (is_integer(a2)) {
emit(` ${s(a1)} =l copy ${text(a2 * 2)}`)
} else {
emit(` ${s(a1)} =l call $__JS_NewFloat64(l %ctx, d d_${text(a2)})`)
emit(` ${s(a1)} =l call $qbe_new_float64(l %ctx, d d_${text(a2)})`)
}
} else if (is_text(a2)) {
sl = intern_str(a2)
emit(` ${s(a1)} =l call $JS_NewString(l %ctx, l ${sl})`)
emit(` ${s(a1)} =l call $qbe_new_string(l %ctx, l ${sl})`)
} else if (is_object(a2)) {
if (a2.make == "intrinsic") {
sl = intern_str(a2.name)
@@ -170,13 +195,13 @@ var qbe_emit = function(ir, qbe, export_name) {
if (a2.number != null && is_integer(a2.number)) {
emit(` ${s(a1)} =l copy ${text(a2.number * 2)}`)
} else if (a2.number != null) {
emit(` ${s(a1)} =l call $__JS_NewFloat64(l %ctx, d d_${text(a2.number)})`)
emit(` ${s(a1)} =l call $qbe_new_float64(l %ctx, d d_${text(a2.number)})`)
} else {
emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`)
}
} else if (a2.kind == "text") {
sl = intern_str(a2.value)
emit(` ${s(a1)} =l call $JS_NewString(l %ctx, l ${sl})`)
emit(` ${s(a1)} =l call $qbe_new_string(l %ctx, l ${sl})`)
} else if (a2.kind == "true") {
emit(` ${s(a1)} =l copy ${text(qbe.js_true)}`)
} else if (a2.kind == "false") {
@@ -205,7 +230,7 @@ var qbe_emit = function(ir, qbe, export_name) {
if (op == "add") {
p = fresh()
emit(qbe.add(p, "%ctx", s(a2), s(a3)))
emit(` %${p} =l call $cell_rt_add(l %ctx, l ${s(a2)}, l ${s(a3)})`)
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
@@ -246,6 +271,12 @@ var qbe_emit = function(ir, qbe, export_name) {
continue
}
if (op == "pow") {
emit(` ${s(a1)} =l call $qbe_float_pow(l %ctx, l ${s(a2)}, l ${s(a3)})`)
wb(a1)
continue
}
// --- String concat ---
if (op == "concat") {
@@ -305,6 +336,46 @@ var qbe_emit = function(ir, qbe, export_name) {
wb(a1)
continue
}
if (op == "is_array") {
p = fresh()
emit(` %${p} =w call $JS_IsArray(l ${s(a2)})`)
emit(qbe.new_bool(p + ".r", "%" + p))
emit(` ${s(a1)} =l copy %${p}.r`)
wb(a1)
continue
}
if (op == "is_func") {
p = fresh()
emit(` %${p} =w call $JS_IsFunction(l ${s(a2)})`)
emit(qbe.new_bool(p + ".r", "%" + p))
emit(` ${s(a1)} =l copy %${p}.r`)
wb(a1)
continue
}
if (op == "is_record") {
p = fresh()
emit(` %${p} =w call $JS_IsRecord(l ${s(a2)})`)
emit(qbe.new_bool(p + ".r", "%" + p))
emit(` ${s(a1)} =l copy %${p}.r`)
wb(a1)
continue
}
if (op == "is_stone") {
p = fresh()
emit(` %${p} =w call $JS_IsStone(l ${s(a2)})`)
emit(qbe.new_bool(p + ".r", "%" + p))
emit(` ${s(a1)} =l copy %${p}.r`)
wb(a1)
continue
}
if (op == "is_proxy") {
p = fresh()
emit(` %${p} =w call $cell_rt_is_proxy(l %ctx, l ${s(a2)})`)
emit(qbe.new_bool(p + ".r", "%" + p))
emit(` ${s(a1)} =l copy %${p}.r`)
wb(a1)
continue
}
// --- Comparisons (int path) ---
@@ -367,14 +438,30 @@ var qbe_emit = function(ir, qbe, export_name) {
wb(a1)
continue
}
if (op == "lt_float" || op == "gt_float" || op == "le_float" || op == "ge_float") {
if (op == "lt_float") {
p = fresh()
fop_id = 0
if (op == "lt_float") fop_id = 2
else if (op == "le_float") fop_id = 3
else if (op == "gt_float") fop_id = 4
else if (op == "ge_float") fop_id = 5
emit(qbe.cmp_float != null ? qbe.cmp_float(p, "%ctx", s(a2), s(a3), fop_id) : ` %${p} =l call $qbe_float_cmp(l %ctx, w ${text(fop_id)}, l ${s(a2)}, l ${s(a3)})`)
emit(qbe.lt_float(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "le_float") {
p = fresh()
emit(qbe.le_float(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "gt_float") {
p = fresh()
emit(qbe.gt_float(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "ge_float") {
p = fresh()
emit(qbe.ge_float(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
@@ -494,7 +581,10 @@ var qbe_emit = function(ir, qbe, export_name) {
// --- Property access — runtime calls ---
if (op == "load_field") {
pn = prop_name(a3)
pn = null
if (is_text(a3)) pn = a3
else if (is_object(a3) && a3.name != null) pn = a3.name
else if (is_object(a3) && a3.value != null) pn = a3.value
if (pn != null) {
sl = intern_str(pn)
emit(` ${s(a1)} =l call $cell_rt_load_field(l %ctx, l ${s(a2)}, l ${sl})`)
@@ -510,13 +600,28 @@ var qbe_emit = function(ir, qbe, export_name) {
continue
}
if (op == "load_dynamic") {
emit(` ${s(a1)} =l call $cell_rt_load_dynamic(l %ctx, l ${s(a2)}, l ${s(a3)})`)
pn = null
if (is_text(a3)) pn = a3
else if (is_object(a3) && a3.name != null) pn = a3.name
else if (is_object(a3) && a3.value != null) pn = a3.value
if (pn != null) {
sl = intern_str(pn)
emit(` ${s(a1)} =l call $cell_rt_load_field(l %ctx, l ${s(a2)}, l ${sl})`)
} else {
emit(` ${s(a1)} =l call $cell_rt_load_dynamic(l %ctx, l ${s(a2)}, l ${s(a3)})`)
}
wb(a1)
continue
}
if (op == "store_field") {
// IR: ["store_field", obj, val, prop] → C: (ctx, val, obj, name)
pn = prop_name(a3)
pn = null
if (is_text(a3)) {
pn = a3
} else if (is_object(a3)) {
if (a3.name != null) pn = a3.name
else if (a3.value != null) pn = a3.value
}
if (pn != null) {
sl = intern_str(pn)
emit(` call $cell_rt_store_field(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${sl})`)
@@ -532,19 +637,30 @@ var qbe_emit = function(ir, qbe, export_name) {
}
if (op == "store_dynamic") {
// IR: ["store_dynamic", obj, val, key] → C: (ctx, val, obj, key)
emit(` call $cell_rt_store_dynamic(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${s(a3)})`)
pn = null
if (is_text(a3)) pn = a3
else if (is_object(a3) && a3.name != null) pn = a3.name
else if (is_object(a3) && a3.value != null) pn = a3.value
if (pn != null) {
sl = intern_str(pn)
emit(` call $cell_rt_store_field(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${sl})`)
} else {
emit(` call $cell_rt_store_dynamic(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${s(a3)})`)
}
continue
}
// --- Closure access ---
if (op == "get") {
emit(` ${s(a1)} =l call $cell_rt_get_closure(l %ctx, l %fp, l ${text(a2)}, l ${text(a3)})`)
// mcode: get(dest, slot, depth) — a2=slot, a3=depth
emit(` ${s(a1)} =l call $cell_rt_get_closure(l %ctx, l %fp, l ${text(a3)}, l ${text(a2)})`)
wb(a1)
continue
}
if (op == "put") {
emit(` call $cell_rt_put_closure(l %ctx, l %fp, l ${s(a1)}, l ${text(a2)}, l ${text(a3)})`)
// mcode: put(val, slot, depth) — a2=slot, a3=depth
emit(` call $cell_rt_put_closure(l %ctx, l %fp, l ${s(a1)}, l ${text(a3)}, l ${text(a2)})`)
continue
}
@@ -552,6 +668,7 @@ var qbe_emit = function(ir, qbe, export_name) {
if (op == "jump") {
emit(` jmp @${sanitize(a1)}`)
last_was_term = true
continue
}
if (op == "jump_true") {
@@ -611,6 +728,13 @@ var qbe_emit = function(ir, qbe, export_name) {
if (op == "invoke") {
emit(` ${s(a2)} =l call $cell_rt_invoke(l %ctx, l ${s(a1)})`)
wb(a2)
reload_captured()
continue
}
if (op == "tail_invoke") {
emit(` ${s(a2)} =l call $cell_rt_invoke(l %ctx, l ${s(a1)})`)
wb(a2)
reload_captured()
continue
}
if (op == "goframe") {
@@ -621,6 +745,7 @@ var qbe_emit = function(ir, qbe, export_name) {
if (op == "goinvoke") {
emit(` %_goret =l call $cell_rt_goinvoke(l %ctx, l ${s(a1)})`)
emit(` ret %_goret`)
last_was_term = true
continue
}
@@ -664,19 +789,38 @@ var qbe_emit = function(ir, qbe, export_name) {
continue
}
// --- Length ---
if (op == "length") {
emit(` ${s(a1)} =l call $JS_CellLength(l %ctx, l ${s(a2)})`)
wb(a1)
continue
}
// --- Misc ---
if (op == "return") {
emit(` ret ${s(a1)}`)
last_was_term = true
continue
}
if (op == "disrupt") {
emit(` call $cell_rt_disrupt(l %ctx)`)
emit(` ret ${text(qbe.js_null)}`)
last_was_term = true
continue
}
if (op == "delete") {
emit(` ${s(a1)} =l call $cell_rt_delete(l %ctx, l ${s(a2)}, l ${s(a3)})`)
pn = null
if (is_text(a3)) pn = a3
else if (is_object(a3) && a3.name != null) pn = a3.name
else if (is_object(a3) && a3.value != null) pn = a3.value
if (pn != null) {
sl = intern_str(pn)
emit(` ${s(a1)} =l call $cell_rt_delete(l %ctx, l ${s(a2)}, l ${sl})`)
} else {
emit(` ${s(a1)} =l call $cell_rt_delete(l %ctx, l ${s(a2)}, l ${s(a3)})`)
}
wb(a1)
continue
}
@@ -690,6 +834,14 @@ var qbe_emit = function(ir, qbe, export_name) {
emit(` # unknown: ${op}`)
}
// Emit @disrupt landing pad for arithmetic type-error branches
if (!last_was_term) {
emit(" jmp @disrupt")
}
emit("@disrupt")
emit(` call $cell_rt_disrupt(l %ctx)`)
emit(` ret ${text(qbe.js_null)}`)
emit("}")
emit("")
}
@@ -698,6 +850,70 @@ var qbe_emit = function(ir, qbe, export_name) {
// Main: compile all functions then main
// ============================================================
// ============================================================
// Pre-scan: find which slots each function has that are modified
// by child closures (via "put" instructions at depth=1).
// Build a map: fn_idx → array of captured slot numbers.
// ============================================================
// For each function, find which fn_idxes it creates via "function" op
var find_children = function(fn_instrs) {
var children = []
var ci = 0
var cinstr = null
while (ci < length(fn_instrs)) {
cinstr = fn_instrs[ci]
ci = ci + 1
if (!is_array(cinstr)) continue
if (cinstr[0] == "function") {
push(children, cinstr[2])
}
}
return children
}
// For a child function, find which parent slots it writes to via put(val, slot, depth=1)
var find_put_slots = function(fn_instrs) {
var slots = []
var pi = 0
var pinstr = null
while (pi < length(fn_instrs)) {
pinstr = fn_instrs[pi]
pi = pi + 1
if (!is_array(pinstr)) continue
// put format: ["put", val, slot, depth]
if (pinstr[0] == "put" && pinstr[3] == 1) {
push(slots, pinstr[2])
}
}
return slots
}
// Build captured_slots for each function (and main)
var build_captured = function(fn) {
var children = find_children(fn.instructions)
var captured = {}
var bi = 0
var child_idx = 0
var child_fn = null
var pslots = null
var si = 0
while (bi < length(children)) {
child_idx = children[bi]
bi = bi + 1
if (child_idx >= 0 && child_idx < length(ir.functions)) {
child_fn = ir.functions[child_idx]
pslots = find_put_slots(child_fn.instructions)
si = 0
while (si < length(pslots)) {
captured[text(pslots[si])] = true
si = si + 1
}
}
}
return captured
}
var fi = 0
while (fi < length(ir.functions)) {
compile_fn(ir.functions[fi], fi, false)