03862d50c6
Introduce helper functions to recompute the `StaticType` of a tuple node based on its children's types. This ensures that after inlining, tuples within the AST have concrete types, rather than unresolved type variables, even when derived from polymorphic functions. Also adds tests to verify this behavior for both homogeneous and heterogeneous tuples.
252 lines
7.9 KiB
Rust
252 lines
7.9 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}"
|
|
);
|
|
}
|