78813e7197
The optimizer now correctly handles empty blocks by collapsing them into Nop nodes. It also unwraps blocks containing a single expression, making the AST more concise. This change improves AST simplification by ensuring that redundant block structures are removed.
183 lines
5.3 KiB
Rust
183 lines
5.3 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());
|
|
}
|