Refactor stack allocator to reuse freed slots

Introduce `free_slots` in `StackAllocator` to keep track of physical
slots that have been freed. When mapping a new virtual slot, the
allocator first attempts to reuse a slot from the `free_slots` vector
before allocating a new one.

This change also includes a new function `collect_scope_locals` that
identifies non-captured local variables defined within a block. After
these expressions are lowered, the allocator reclaims the physical slots
associated with these locals, making them available for reuse by
subsequent blocks.

A new test `test_slot_reuse_non_overlapping_scopes` is added to verify
that distinct scopes correctly reuse stack slots, ensuring optimal stack
usage.
This commit is contained in:
2026-03-26 14:01:46 +01:00
parent 78813e7197
commit 0088a644eb
2 changed files with 102 additions and 3 deletions
+36
View File
@@ -180,3 +180,39 @@ fn test_reproduce_inlining_slot_clash_crash() {
let res = env.run_script(source);
assert!(res.is_ok(), "Inlining slot clash triggered: {:?}", res.err());
}
// ── Slot reuse ────────────────────────────────────────────────────────────────
#[test]
fn test_slot_reuse_non_overlapping_scopes() {
let mut env = Environment::new();
env.optimization = true;
// Two sequential inner blocks each bind a mutable local.
// assign prevents the optimizer from inlining the variables away,
// so both slots survive into the lowering pass.
// x lives only in the first block; once that block ends its slot is free.
// y is defined in the second block and must reuse x's slot.
// Expected result: the function returns 3 (y = 2+1).
let source = r#"
(fn []
(do
(do (def x 1) (assign x (+ x 1)) x)
(do (def y 2) (assign y (+ y 1)) y)))
"#;
// Correctness: calling the function must still produce the right value.
let result = env
.run_script("((fn [] (do (do (def x 1) (assign x (+ x 1)) x) (do (def y 2) (assign y (+ y 1)) y))))")
.unwrap();
assert_eq!(format!("{}", result), "3");
// Stack efficiency: with slot reuse the Lambda's stack_size must be 1,
// not 2. The dump format is "stack_size: N" in the Lambda metadata line.
let dump = env.dump_ast(source).unwrap();
assert!(
dump.contains("stack_size: 1"),
"Non-overlapping scopes must reuse the same slot (expected stack_size: 1). Dump:\n{}",
dump
);
}