6c8e2df45c
Refactor the `updated_metrics` function to `refreshed_metrics`. This function now clones the `NodeMetrics` if the new type is identical to the original, avoiding unnecessary allocations. Also, adjust the `Block` and `Lambda` node kinds to correctly propagate concrete types after inlining, especially when dealing with polymorphic functions. This ensures that the return types of blocks and lambdas reflect the actual computed types rather than stale type variables. This change improves type inference accuracy and prepares for more advanced optimizations by ensuring type information is correctly propagated through the AST.
280 lines
9.0 KiB
Rust
280 lines
9.0 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))
|
|
(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))
|
|
(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
|
|
);
|
|
}
|
|
|
|
// ── Tuple type recomputation after inlining ─────────────────────────────────
|
|
|
|
#[test]
|
|
fn test_inlined_tuple_has_concrete_type_homogeneous() {
|
|
// A polymorphic function returning a tuple, called with all-Int args.
|
|
// After inlining, the tuple type must be concrete (no unresolved TypeVars).
|
|
let env = Environment::new();
|
|
let source = r#"(do
|
|
(def wrap (fn [x y z] [x y z]))
|
|
(wrap 1 2 3)
|
|
)"#;
|
|
let dump = env.dump_ast_compact(source).unwrap();
|
|
assert!(
|
|
!dump.contains('?'),
|
|
"Inlined tuple must not contain unresolved TypeVars. Dump:\n{dump}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_inlined_tuple_has_concrete_type_heterogeneous() {
|
|
// Heterogeneous case: Int + String should produce Tuple([int, str]), not ?0/?1.
|
|
let env = Environment::new();
|
|
let source = r#"(do
|
|
(def wrap (fn [x y] [x y]))
|
|
(wrap 1 "hello")
|
|
)"#;
|
|
let dump = env.dump_ast_compact(source).unwrap();
|
|
assert!(
|
|
!dump.contains('?'),
|
|
"Inlined tuple must not contain unresolved TypeVars. Dump:\n{dump}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_block_and_lambda_propagate_concrete_type_after_inlining() {
|
|
// When a polymorphic function is inlined and the result tuple gets a concrete type,
|
|
// the enclosing Block and outer Lambda return type must propagate that concrete type
|
|
// upward — not retain stale TypeVars from before inlining.
|
|
let env = Environment::new();
|
|
let source = r#"(do
|
|
(def last (fn [s] (s 0)))
|
|
(def f (fn [n] n))
|
|
(def r (series 5))
|
|
(push r 4)
|
|
[(last f) (last r)]
|
|
)"#;
|
|
let dump = env.dump_ast_compact(source).unwrap();
|
|
// The outer Lambda and Block must carry concrete types, not stale TypeVars.
|
|
// Inner polymorphic lambdas (like `last`) legitimately retain TypeVars.
|
|
let first_line = dump.lines().next().unwrap();
|
|
assert!(
|
|
!first_line.contains('?'),
|
|
"Outer Lambda return type must be concrete. First line:\n{first_line}"
|
|
);
|
|
let block_line = dump.lines().find(|l| l.contains("Block")).unwrap();
|
|
assert!(
|
|
!block_line.contains('?'),
|
|
"Block type must be concrete. Line:\n{block_line}"
|
|
);
|
|
}
|