Merge branch 'fix_heap_closure'

This commit is contained in:
2026-02-18 12:46:18 -06:00
13 changed files with 124048 additions and 121950 deletions

View File

@@ -179,6 +179,26 @@ When running locally with `./cell --dev`, these commands manage packages:
Local paths are symlinked into `.cell/packages/`. The build step compiles C files to `.cell/lib/<pkg>/<stem>.dylib`. C files in `src/` are support files linked into module dylibs, not standalone modules.
## Debugging Compiler Issues
When investigating bugs in compiled output (wrong values, missing operations, incorrect comparisons), **start from the optimizer down, not the VM up**. The compiler inspection tools will usually identify the problem faster than adding C-level tracing:
```
./cell --dev streamline --types <file> # show inferred slot types — look for wrong types
./cell --dev ir_report --events <file> # show every optimization applied and why
./cell --dev ir_report --types <file> # show type inference results per function
./cell --dev mcode --pretty <file> # show raw IR before optimization
./cell --dev streamline --ir <file> # show human-readable optimized IR
```
**Triage order:**
1. `streamline --types` — are slot types correct? Wrong type inference causes wrong optimizations.
2. `ir_report --events` — are type checks being incorrectly eliminated? Look for `known_type_eliminates_guard` on slots that shouldn't have known types.
3. `mcode --pretty` — is the raw IR correct before optimization? If so, the bug is in streamline.
4. Only dig into `source/mach.c` if the IR looks correct at all levels.
See `docs/compiler-tools.md` for the full tool reference and `docs/spec/streamline.md` for pass details.
## Testing
After any C runtime changes, run all three test suites before considering the work done:

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

File diff suppressed because it is too large Load Diff

View File

@@ -62,12 +62,13 @@ See [Mcode IR](mcode.md) for the instruction format and complete instruction ref
Optimizes the Mcode IR through a series of independent passes. Operates per-function:
1. **Backward type inference**: Infers parameter types from how they are used in typed operators (`add_int`, `store_index`, `load_field`, `push`, `pop`, etc.). Immutable `def` parameters keep their inferred type across label join points.
2. **Type-check elimination**: When a slot's type is known, eliminates `is_<type>` + conditional jump pairs. Narrows `load_dynamic`/`store_dynamic` to typed variants.
3. **Algebraic simplification**: Rewrites identity operations (add 0, multiply 1, divide 1) and folds same-slot comparisons.
4. **Boolean simplification**: Fuses `not` + conditional jump into a single jump with inverted condition.
5. **Move elimination**: Removes self-moves (`move a, a`).
6. **Unreachable elimination**: Nops dead code after `return` until the next label.
7. **Dead jump elimination**: Removes jumps to the immediately following label.
2. **Write-type invariance**: Determines which local slots have a consistent write type across all instructions. Slots written by child closures (via `put`) are excluded (forced to unknown).
3. **Type-check elimination**: When a slot's type is known, eliminates `is_<type>` + conditional jump pairs. Narrows `load_dynamic`/`store_dynamic` to typed variants.
4. **Algebraic simplification**: Rewrites identity operations (add 0, multiply 1, divide 1) and folds same-slot comparisons.
5. **Boolean simplification**: Fuses `not` + conditional jump into a single jump with inverted condition.
6. **Move elimination**: Removes self-moves (`move a, a`).
7. **Unreachable elimination**: Nops dead code after `return` until the next label.
8. **Dead jump elimination**: Removes jumps to the immediately following label.
See [Streamline Optimizer](streamline.md) for detailed pass descriptions.

View File

@@ -95,7 +95,9 @@ Write type mapping:
| `move`, `load_field`, `load_index`, `load_dynamic`, `pop`, `get` | T_UNKNOWN |
| `invoke`, `tail_invoke` | T_UNKNOWN |
The result is a map of slot→type for slots where all writes agree on a single known type. Parameter slots (1..nr_args) and slot 0 are excluded.
Before filtering, a pre-pass (`mark_closure_writes`) scans all inner functions for `put` instructions (closure variable writes). For each `put`, the pass traverses the parent chain to find the target ancestor function and marks the written slot as `closure_written`. Slots marked as closure-written are forced to T_UNKNOWN regardless of what the local write analysis infers, because the actual runtime write happens in a child function and can produce any type.
The result is a map of slot→type for slots where all writes agree on a single known type. Parameter slots (1..nr_args) and slot 0 are excluded. Closure-written slots are excluded (forced to unknown).
Common patterns this enables:

View File

@@ -78,6 +78,7 @@ function load_pipeline_module(name, env) {
var boot_par = null
var boot_fld = null
var boot_mc = null
var boot_sl = null
var tok_result = null
var ast = null
var compiled = null
@@ -105,6 +106,8 @@ function load_pipeline_module(name, env) {
}
ast = boot_fld(ast)
compiled = boot_mc(ast)
boot_sl = boot_load("streamline")
compiled = boot_sl(compiled)
mcode_json = json.encode(compiled)
mach_blob = mach_compile_mcode_bin(name, mcode_json)
if (cached) {

View File

@@ -54,6 +54,7 @@ var compiled = null
var optimized = null
var mcode_json = null
var out_path = null
var build_dir = null
// Regenerate pipeline module seeds
for (i = 0; i < length(pipeline_modules); i++) {
@@ -103,7 +104,7 @@ if (fd.is_file(bootstrap_path)) {
print('\nRegenerated ' + text(generated) + ' seed(s)')
if (clean) {
var build_dir = shop.get_build_dir()
build_dir = shop.get_build_dir()
if (fd.is_dir(build_dir)) {
print('Clearing build cache: ' + build_dir)
os.system('rm -rf "' + build_dir + '"')

View File

@@ -336,6 +336,7 @@ var streamline = function(ir, log) {
var slot = 0
var typ = null
var rule = null
var cw_keys = null
if (instructions == null) {
return array(func.nr_slots)
@@ -362,6 +363,19 @@ var streamline = function(ir, log) {
i = i + 1
}
// Closure-written slots can have any type at runtime — mark unknown
if (func.closure_written != null) {
cw_keys = array(func.closure_written)
k = 0
while (k < length(cw_keys)) {
slot = number(cw_keys[k])
if (slot >= 0 && slot < length(write_types)) {
write_types[slot] = T_UNKNOWN
}
k = k + 1
}
}
// Filter to only slots with known (non-unknown) types
k = 0
while (k < length(write_types)) {
@@ -1475,6 +1489,100 @@ var streamline = function(ir, log) {
return null
}
// =========================================================
// Pre-pass: mark closure-written slots
// Scans child functions for 'put' instructions and annotates
// the target ancestor func with closure_written[slot] = true.
// =========================================================
var mark_closure_writes = function(ir) {
var functions = ir.functions != null ? ir.functions : []
var fc = length(functions)
var parent_of = array(fc, -1)
var instrs = null
var instr = null
var fi = 0
var i = 0
var j = 0
var level = 0
var anc = 0
var slot = 0
var target = null
if (fc == 0) {
return null
}
// Build parent_of map
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") {
if (instr[2] >= 0 && instr[2] < fc) {
parent_of[instr[2]] = fc
}
}
i = i + 1
}
}
fi = 0
while (fi < fc) {
instrs = functions[fi].instructions
if (instrs != null) {
i = 0
while (i < length(instrs)) {
instr = instrs[i]
if (is_array(instr) && instr[0] == "function") {
if (instr[2] >= 0 && instr[2] < fc) {
parent_of[instr[2]] = fi
}
}
i = i + 1
}
}
fi = fi + 1
}
// Scan for 'put' instructions and mark ancestor slots
fi = 0
while (fi < fc) {
instrs = functions[fi].instructions
if (instrs != null) {
i = 0
while (i < length(instrs)) {
instr = instrs[i]
if (is_array(instr) && instr[0] == "put") {
slot = instr[2]
level = instr[3]
anc = fi
j = 0
while (j < level && anc >= 0) {
anc = parent_of[anc]
j = j + 1
}
if (anc >= 0) {
if (anc == fc) {
target = ir.main
} else {
target = functions[anc]
}
if (target != null) {
if (target.closure_written == null) {
target.closure_written = {}
}
target.closure_written[text(slot)] = true
}
}
}
i = i + 1
}
}
fi = fi + 1
}
return null
}
// =========================================================
// Compose all passes
// =========================================================
@@ -1530,6 +1638,12 @@ var streamline = function(ir, log) {
return null
}
// Pre-pass: mark slots written by child closures via 'put' instructions.
// Without this, infer_slot_write_types would assume those slots keep their
// initial type (e.g. T_NULL from 'var x = null'), causing the type-check
// eliminator to mis-optimize comparisons on closure-written variables.
mark_closure_writes(ir)
// Process main function
if (ir.main != null) {
optimize_function(ir.main, log)

View File

@@ -4284,6 +4284,32 @@ run("closure set and get", function() {
assert_eq(o.get(), 42, "overwrite")
})
run("closure write heap values visible to outer scope", function() {
var a = null
var b = null
var c = null
var d = null
var e = null
var f1 = function() { a = 42 }
var f2 = function() { b = true }
var f3 = function() { c = "hello" }
var f4 = function() { d = {x: 1} }
var f5 = function() { e = [1, 2] }
f1()
f2()
f3()
f4()
f5()
assert_eq(a, 42, "closure write number")
assert_eq(b, true, "closure write boolean")
assert_eq(c != null, true, "closure write text not null")
assert_eq(c, "hello", "closure write text value")
assert_eq(d != null, true, "closure write object not null")
assert_eq(d.x, 1, "closure write object property")
assert_eq(e != null, true, "closure write array not null")
assert_eq(e[0], 1, "closure write array element")
})
// ============================================================================
// STRING COMPARISON OPERATORS
// ============================================================================