diff --git a/streamline.cm b/streamline.cm index f7858c78..4a873d10 100644 --- a/streamline.cm +++ b/streamline.cm @@ -1598,6 +1598,8 @@ var streamline = function(ir, log) { var found = false var anc_remap = null var old_slot = 0 + var max_close = null + var needed = 0 var fi = 0 var i = 0 var j = 0 @@ -1703,6 +1705,8 @@ var streamline = function(ir, log) { } // Fix get/put parent_slot references using ancestor remap tables + // and track the max close slot per ancestor for nr_close_slots update + max_close = array(func_count + 1, -1) fi = 0 while (fi < func_count) { instrs = functions[fi].instructions @@ -1725,6 +1729,9 @@ var streamline = function(ir, log) { instr[2] = anc_remap[old_slot] } } + if (ancestor >= 0 && instr[2] > max_close[ancestor]) { + max_close[ancestor] = instr[2] + } } i = i + 1 } @@ -1732,6 +1739,26 @@ var streamline = function(ir, log) { fi = fi + 1 } + // Update nr_close_slots for functions whose close slots were remapped. + // Frame shortening keeps 1 + nr_args + nr_close_slots slots, so + // nr_close_slots must cover the highest-numbered close slot. + fi = 0 + while (fi < func_count) { + if (max_close[fi] >= 0) { + needed = max_close[fi] - (functions[fi].nr_args != null ? functions[fi].nr_args : 0) + if (needed > functions[fi].nr_close_slots) { + functions[fi].nr_close_slots = needed + } + } + fi = fi + 1 + } + if (max_close[func_count] >= 0 && ir.main != null) { + needed = max_close[func_count] - (ir.main.nr_args != null ? ir.main.nr_args : 0) + if (needed > ir.main.nr_close_slots) { + ir.main.nr_close_slots = needed + } + } + return null } diff --git a/vm_suite.ce b/vm_suite.ce index 0a391df1..79454a8c 100644 --- a/vm_suite.ce +++ b/vm_suite.ce @@ -5539,6 +5539,157 @@ run("gc blob forward pointer chase", function() { } }) +// ============================================================================ +// GC CLOSURE FRAME SHORTENING +// Verify that closure-captured variables survive GC collection, particularly +// when the streamline optimizer remaps close slots to different positions. +// ============================================================================ + +var force_gc = function() { + var _g = 0 + var _gx = null + for (_g = 0; _g < 200; _g = _g + 1) { + _gx = {a: _g, b: [1, 2, 3], c: "garbage"} + } +} + +run("gc closure basic - captured function survives gc", function() { + var make = function() { + function helper() { return 42 } + var obj = { call() { return helper() } } + return obj + } + var obj = make() + force_gc() + assert_eq(obj.call(), 42, "captured function should survive GC") +}) + +run("gc closure - captured variable survives gc", function() { + var make = function() { + var val = 99 + var obj = { get() { return val } } + return obj + } + var obj = make() + force_gc() + assert_eq(obj.get(), 99, "captured variable should survive GC") +}) + +run("gc closure - multiple captured variables survive gc", function() { + var make = function() { + var a = 10 + var b = 20 + var c = 30 + var obj = { + sum() { return a + b + c } + } + return obj + } + var obj = make() + force_gc() + assert_eq(obj.sum(), 60, "all captured vars should survive GC") +}) + +run("gc closure - captured function and var survive gc", function() { + var make = function() { + function double(x) { return x * 2 } + var base = 5 + var obj = { compute() { return double(base) } } + return obj + } + var obj = make() + force_gc() + assert_eq(obj.compute(), 10, "captured fn and var should survive GC") +}) + +run("gc closure - nested closure chain survives gc", function() { + var outer = function() { + var x = 7 + var mid = function() { + var y = 3 + var inner = function() { return x + y } + return inner + } + return mid() + } + var fn = outer() + force_gc() + assert_eq(fn(), 10, "nested closure chain should survive GC") +}) + +run("gc closure - multiple methods share captured frame", function() { + var make = function() { + var count = 0 + function inc() { count = count + 1 } + function get() { return count } + var obj = { + increment() { inc() }, + value() { return get() } + } + return obj + } + var obj = make() + obj.increment() + obj.increment() + force_gc() + obj.increment() + assert_eq(obj.value(), 3, "shared closure frame should survive GC") +}) + +run("gc closure - closure survives repeated gc cycles", function() { + var make = function() { + var val = 123 + var obj = { get() { return val } } + return obj + } + var obj = make() + force_gc() + force_gc() + force_gc() + assert_eq(obj.get(), 123, "closure should survive repeated GC cycles") +}) + +run("gc closure - object literal method with temp slot reuse", function() { + var make = function() { + function helper() { return "ok" } + var temp = [1, 2, 3] + var unused = {x: temp} + var obj = { call() { return helper() } } + return obj + } + var obj = make() + force_gc() + assert_eq(obj.call(), "ok", "closure should work after temp slots discarded") +}) + +run("gc closure - closure array survives gc", function() { + var make = function() { + var items = [10, 20, 30] + var obj = { + first() { return items[0] }, + last() { return items[2] } + } + return obj + } + var obj = make() + force_gc() + assert_eq(obj.first(), 10, "captured array first element") + assert_eq(obj.last(), 30, "captured array last element") +}) + +run("gc closure - factory pattern survives gc", function() { + var factory = function(name) { + function greet() { return "hello " + name } + var obj = { say() { return greet() } } + return obj + } + var a = factory("alice") + var b = factory("bob") + force_gc() + assert_eq(a.say(), "hello alice", "first factory closure") + assert_eq(b.say(), "hello bob", "second factory closure") +}) + // ============================================================================ // SUMMARY // ============================================================================