0088a644eb
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.
219 lines
6.8 KiB
Rust
219 lines
6.8 KiB
Rust
use myc::ast::environment::Environment;
|
|
|
|
// ── Block simplification ─────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn test_empty_block_eliminated() {
|
|
// (do) is semantically Void/Nop — the optimizer must collapse it,
|
|
// even though nothing inside the block changes (block_changed stays false).
|
|
let env = Environment::new();
|
|
let dump = env.dump_ast("(do)").unwrap();
|
|
assert!(
|
|
!dump.contains("Block"),
|
|
"Empty block must be collapsed to Nop. Dump:\n{}",
|
|
dump
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_single_expr_block_unwrapped() {
|
|
// (do 42) carries exactly one expression — the Block wrapper is redundant.
|
|
// The optimizer must unwrap it to a bare Constant node.
|
|
let env = Environment::new();
|
|
let result = env.run_script("(do 42)").unwrap();
|
|
assert_eq!(format!("{}", result), "42");
|
|
let dump = env.dump_ast("(do 42)").unwrap();
|
|
assert!(
|
|
!dump.contains("Block"),
|
|
"Single-expression block must be unwrapped. Dump:\n{}",
|
|
dump
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inner_empty_block_eliminated() {
|
|
// The inner (do) must be collapsed to Nop during recursive optimization.
|
|
// Nop-pruning in the outer block then removes it, leaving just Constant(42).
|
|
let env = Environment::new();
|
|
let result = env.run_script("(do (do) 42)").unwrap();
|
|
assert_eq!(format!("{}", result), "42");
|
|
let dump = env.dump_ast("(do (do) 42)").unwrap();
|
|
assert!(
|
|
!dump.contains("Block"),
|
|
"Inner empty block must be eliminated entirely. Dump:\n{}",
|
|
dump
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_optimizer_destructuring_inlining_and_mutation() {
|
|
let env = Environment::new();
|
|
|
|
// 1. Destructuring definition should allow inlining if not mutated
|
|
let source_inline = "(do (def [x y] [10 20]) (+ x y))";
|
|
let res_inline = env.run_script(source_inline).unwrap();
|
|
assert_eq!(format!("{}", res_inline), "30");
|
|
|
|
// 2. Destructuring definition should NOT be inlined if mutated (regression test)
|
|
let source_mutation = r#"
|
|
(do
|
|
(def [a b] [1 2])
|
|
(def f (fn [] (assign a (+ a b))))
|
|
(f)
|
|
a)
|
|
"#;
|
|
let res_mutation = env.run_script(source_mutation).unwrap();
|
|
assert_eq!(format!("{}", res_mutation), "3");
|
|
}
|
|
|
|
#[test]
|
|
fn test_inline_two_lambdas_same_slots() {
|
|
let env = Environment::new();
|
|
// Two lambdas each binding a local at slot 0 get inlined into the same scope.
|
|
// Slot remapping must prevent collision between them.
|
|
let source = r#"
|
|
(do
|
|
(def add1 (fn [x] (+ x 1)))
|
|
(def add2 (fn [x] (+ x 2)))
|
|
(+ (add1 10) (add2 20)))
|
|
"#;
|
|
assert_eq!(format!("{}", env.run_script(source).unwrap()), "33");
|
|
let dump = env.dump_ast(source).unwrap();
|
|
assert!(
|
|
dump.contains("Constant: 33"),
|
|
"Should be fully folded to Constant 33. Dump:\n{}",
|
|
dump
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_multipass_const_propagation() {
|
|
let env = Environment::new();
|
|
// A constant chain where each step is only resolvable after the previous has been folded.
|
|
// Requires multiple optimizer passes.
|
|
let source = r#"
|
|
(do
|
|
(def a 1)
|
|
(def b (+ a 1))
|
|
(def c (+ b 1))
|
|
(+ c 1))
|
|
"#;
|
|
assert_eq!(format!("{}", env.run_script(source).unwrap()), "4");
|
|
let dump = env.dump_ast(source).unwrap();
|
|
assert!(
|
|
dump.contains("Constant: 4"),
|
|
"Multi-pass chain should fold to Constant 4. Dump:\n{}",
|
|
dump
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dead_def_destructuring_correctness() {
|
|
// A dead destructuring def must not change the result — with or without optimization.
|
|
let source = "(do (def [x y] [1 2]) 42)";
|
|
let mut env_opt = Environment::new();
|
|
env_opt.optimization = true;
|
|
let mut env_no = Environment::new();
|
|
env_no.optimization = false;
|
|
assert_eq!(
|
|
format!("{}", env_opt.run_script(source).unwrap()),
|
|
format!("{}", env_no.run_script(source).unwrap())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dead_destructuring_def_eliminated() {
|
|
let env = Environment::new();
|
|
// A dead destructuring def with a pure RHS should be eliminated by the optimizer.
|
|
let dump = env.dump_ast("(do (def [x y] [1 2]) 42)").unwrap();
|
|
assert!(
|
|
!dump.contains("Def"),
|
|
"Dead destructuring def should be eliminated from AST. Dump:\n{}",
|
|
dump
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_optimizer_inlining_slot_clash_repro() {
|
|
let mut env = Environment::new();
|
|
env.optimization = true;
|
|
|
|
let source = r#"
|
|
(do
|
|
(def outer (fn [length]
|
|
(do
|
|
(def history (series 100 :float))
|
|
(fn [val] length)
|
|
)
|
|
))
|
|
((outer 20) 5)
|
|
)
|
|
"#;
|
|
|
|
let res = env.run_script(source).unwrap();
|
|
assert_eq!(format!("{}", res), "20");
|
|
}
|
|
|
|
#[test]
|
|
fn test_reproduce_inlining_slot_clash_crash() {
|
|
let mut env = Environment::new();
|
|
env.optimization = true;
|
|
|
|
let source = r#"
|
|
(do
|
|
(def MY_SMA
|
|
(fn [length]
|
|
(do
|
|
(def history (series 100 :float))
|
|
(fn [val]
|
|
(do
|
|
(push history val)
|
|
(len history))))))
|
|
|
|
(def src (create-random-ohlc 42 100))
|
|
(def s (.close src))
|
|
|
|
[(pipe [s] (MY_SMA 5))]
|
|
[(pipe [s] (MY_SMA 20))])
|
|
"#;
|
|
|
|
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
|
|
);
|
|
}
|