stage3: unify to cleanup; MIR return-defer; docs+smokes updated; LLVM(harness): finalize_phis ownership, ret.py simplified, uses-predeclare; cleanup return override green; method-postfix cleanup return WIP (PHI head)
This commit is contained in:
112
src/tests/if_form_no_phi_tests.rs
Normal file
112
src/tests/if_form_no_phi_tests.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
|
||||
use crate::mir::{MirCompiler, MirInstruction};
|
||||
|
||||
fn lit_i(i: i64) -> ASTNode {
|
||||
ASTNode::Literal { value: LiteralValue::Integer(i), span: Span::unknown() }
|
||||
}
|
||||
|
||||
fn bool_lt(lhs: ASTNode, rhs: ASTNode) -> ASTNode {
|
||||
ASTNode::BinaryOp { operator: BinaryOperator::LessThan, left: Box::new(lhs), right: Box::new(rhs), span: Span::unknown() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ifform_no_phi_one_sided_merge_uses_edge_copies_only() {
|
||||
// Force PHI-off mode
|
||||
std::env::set_var("NYASH_MIR_NO_PHI", "1");
|
||||
|
||||
// Program:
|
||||
// local x = 1;
|
||||
// if (1 < 2) { x = 10 } else { }
|
||||
// return x
|
||||
let ast = ASTNode::Program {
|
||||
statements: vec![
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "x".into(), span: Span::unknown() }), value: Box::new(lit_i(1)), span: Span::unknown() },
|
||||
ASTNode::If { condition: Box::new(bool_lt(lit_i(1), lit_i(2))), then_body: vec![ ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "x".into(), span: Span::unknown() }), value: Box::new(lit_i(10)), span: Span::unknown() } ], else_body: Some(vec![]), span: Span::unknown() },
|
||||
ASTNode::Return { value: Some(Box::new(ASTNode::Variable { name: "x".into(), span: Span::unknown() })), span: Span::unknown() }
|
||||
],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let mut mc = MirCompiler::with_options(false);
|
||||
let cr = mc.compile(ast).expect("compile");
|
||||
let f = cr.module.functions.get("main").expect("function main");
|
||||
|
||||
// Find return block and value id
|
||||
let mut ret_block_id = None;
|
||||
let mut ret_val = None;
|
||||
for (bid, bb) in &f.blocks {
|
||||
if let Some(MirInstruction::Return { value: Some(v) }) = bb.terminator.clone() {
|
||||
ret_block_id = Some(*bid);
|
||||
ret_val = Some(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let ret_block = ret_block_id.expect("ret block");
|
||||
let out_v = ret_val.expect("ret value");
|
||||
|
||||
// Preds should have Copy to out_v; merge/ret should not have Copy to out_v
|
||||
let preds: Vec<_> = f.blocks.get(&ret_block).unwrap().predecessors.iter().copied().collect();
|
||||
assert!(preds.len() >= 2, "expected at least two predecessors");
|
||||
for p in preds {
|
||||
let bb = f.blocks.get(&p).unwrap();
|
||||
let has_copy = bb.instructions.iter().any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(has_copy, "missing Copy to merged value in predecessor {:?}", p);
|
||||
}
|
||||
let merge_bb = f.blocks.get(&ret_block).unwrap();
|
||||
let merge_has_copy = merge_bb.instructions.iter().any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(!merge_has_copy, "merge/ret block must not contain Copy to merged value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ifform_nested_no_merge_block_copies() {
|
||||
std::env::set_var("NYASH_MIR_NO_PHI", "1");
|
||||
// if (1<2) { if (1<2) { y = 3 } else { y = 4 } } else { y = 5 }; return y
|
||||
let inner_if = ASTNode::If {
|
||||
condition: Box::new(bool_lt(lit_i(1), lit_i(2))),
|
||||
then_body: vec![ ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "y".into(), span: Span::unknown() }), value: Box::new(lit_i(3)), span: Span::unknown() } ],
|
||||
else_body: Some(vec![ ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "y".into(), span: Span::unknown() }), value: Box::new(lit_i(4)), span: Span::unknown() } ]),
|
||||
span: Span::unknown(),
|
||||
};
|
||||
let ast = ASTNode::Program {
|
||||
statements: vec![
|
||||
ASTNode::If {
|
||||
condition: Box::new(bool_lt(lit_i(1), lit_i(2))),
|
||||
then_body: vec![ inner_if ],
|
||||
else_body: Some(vec![ ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "y".into(), span: Span::unknown() }), value: Box::new(lit_i(5)), span: Span::unknown() } ]),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Return { value: Some(Box::new(ASTNode::Variable { name: "y".into(), span: Span::unknown() })), span: Span::unknown() }
|
||||
],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let mut mc = MirCompiler::with_options(false);
|
||||
let cr = mc.compile(ast).expect("compile");
|
||||
let f = cr.module.functions.get("main").expect("function main");
|
||||
|
||||
// Find return block/value
|
||||
let (ret_block, out_v) = f
|
||||
.blocks
|
||||
.iter()
|
||||
.find_map(|(bid, bb)| match &bb.terminator {
|
||||
Some(MirInstruction::Return { value: Some(v) }) => Some((*bid, *v)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("ret block");
|
||||
|
||||
// Preds must have Copy to merged value; merge block must not
|
||||
for p in f.blocks.get(&ret_block).unwrap().predecessors.iter().copied() {
|
||||
let bb = f.blocks.get(&p).unwrap();
|
||||
let has_copy = bb.instructions.iter().any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(has_copy, "missing Copy in pred {:?}", p);
|
||||
}
|
||||
let merge_has_copy = f
|
||||
.blocks
|
||||
.get(&ret_block)
|
||||
.unwrap()
|
||||
.instructions
|
||||
.iter()
|
||||
.any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(!merge_has_copy, "merge/ret block must not contain Copy to merged value");
|
||||
}
|
||||
|
||||
75
src/tests/loop_continue_break_no_phi_tests.rs
Normal file
75
src/tests/loop_continue_break_no_phi_tests.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use crate::ast::{ASTNode, LiteralValue, Span, BinaryOperator};
|
||||
use crate::mir::{MirCompiler, MirInstruction};
|
||||
|
||||
fn lit_i(i: i64) -> ASTNode {
|
||||
ASTNode::Literal { value: LiteralValue::Integer(i), span: Span::unknown() }
|
||||
}
|
||||
|
||||
fn bin(op: BinaryOperator, l: ASTNode, r: ASTNode) -> ASTNode {
|
||||
ASTNode::BinaryOp { operator: op, left: Box::new(l), right: Box::new(r), span: Span::unknown() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loop_with_continue_and_break_edge_copy_merge() {
|
||||
// PHI-off
|
||||
std::env::set_var("NYASH_MIR_NO_PHI", "1");
|
||||
|
||||
// i=0; sum=0; loop(i < 5) {
|
||||
// i = i + 1;
|
||||
// if (i == 3) { break }
|
||||
// if (i % 2 == 0) { continue }
|
||||
// sum = sum + i;
|
||||
// }
|
||||
// return sum
|
||||
let ast = ASTNode::Program {
|
||||
statements: vec![
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "i".into(), span: Span::unknown() }), value: Box::new(lit_i(0)), span: Span::unknown() },
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "sum".into(), span: Span::unknown() }), value: Box::new(lit_i(0)), span: Span::unknown() },
|
||||
ASTNode::Loop {
|
||||
condition: Box::new(bin(BinaryOperator::LessThan, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(5))),
|
||||
body: vec![
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "i".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(1))), span: Span::unknown() },
|
||||
ASTNode::If { condition: Box::new(bin(BinaryOperator::Equal, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(3))), then_body: vec![ ASTNode::Break { span: Span::unknown() } ], else_body: Some(vec![]), span: Span::unknown() },
|
||||
ASTNode::If { condition: Box::new(bin(BinaryOperator::Equal, bin(BinaryOperator::Mod, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(2)), lit_i(0))), then_body: vec![ ASTNode::Continue { span: Span::unknown() } ], else_body: Some(vec![]), span: Span::unknown() },
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "sum".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "sum".into(), span: Span::unknown() }, ASTNode::Variable { name: "i".into(), span: Span::unknown() })), span: Span::unknown() },
|
||||
],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Return { value: Some(Box::new(ASTNode::Variable { name: "sum".into(), span: Span::unknown() })), span: Span::unknown() }
|
||||
],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let mut mc = MirCompiler::with_options(false);
|
||||
let cr = mc.compile(ast).expect("compile");
|
||||
let f = cr.module.functions.get("main").expect("function main");
|
||||
|
||||
// Locate return block/value
|
||||
let (ret_block, out_v) = f
|
||||
.blocks
|
||||
.iter()
|
||||
.find_map(|(bid, bb)| match &bb.terminator {
|
||||
Some(MirInstruction::Return { value: Some(v) }) => Some((*bid, *v)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("ret block");
|
||||
|
||||
// In PHI-off, the after_loop/ret block should have predecessors with edge copies to the merged 'sum'
|
||||
let preds: Vec<_> = f.blocks.get(&ret_block).unwrap().predecessors.iter().copied().collect();
|
||||
assert!(preds.len() >= 1, "ret must have at least one predecessor");
|
||||
for p in preds {
|
||||
let bb = f.blocks.get(&p).unwrap();
|
||||
let has_copy = bb.instructions.iter().any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(has_copy, "expected edge Copy to merged sum at predecessor {:?}", p);
|
||||
}
|
||||
// And the ret block itself must not contain an extra Copy to out_v
|
||||
let merge_has_copy = f
|
||||
.blocks
|
||||
.get(&ret_block)
|
||||
.unwrap()
|
||||
.instructions
|
||||
.iter()
|
||||
.any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(!merge_has_copy, "ret/merge must not contain Copy to merged sum");
|
||||
}
|
||||
|
||||
183
src/tests/loop_nested_no_phi_tests.rs
Normal file
183
src/tests/loop_nested_no_phi_tests.rs
Normal file
@ -0,0 +1,183 @@
|
||||
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
|
||||
use crate::mir::{MirCompiler, MirInstruction};
|
||||
|
||||
fn lit_i(i: i64) -> ASTNode {
|
||||
ASTNode::Literal { value: LiteralValue::Integer(i), span: Span::unknown() }
|
||||
}
|
||||
|
||||
fn bin(op: BinaryOperator, l: ASTNode, r: ASTNode) -> ASTNode {
|
||||
ASTNode::BinaryOp { operator: op, left: Box::new(l), right: Box::new(r), span: Span::unknown() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_loop_with_multi_continue_break_edge_copy_merge() {
|
||||
// PHI-off mode
|
||||
std::env::set_var("NYASH_MIR_NO_PHI", "1");
|
||||
|
||||
// i=0; sum=0; loop(i < 10) {
|
||||
// i = i + 1;
|
||||
// if (i == 2 || i == 4) { continue }
|
||||
// if (i == 7) { if (1 == 1) { break } }
|
||||
// if ((i % 3) == 0) { continue }
|
||||
// sum = sum + i;
|
||||
// }
|
||||
// return sum
|
||||
let ast = ASTNode::Program {
|
||||
statements: vec![
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "i".into(), span: Span::unknown() }), value: Box::new(lit_i(0)), span: Span::unknown() },
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "sum".into(), span: Span::unknown() }), value: Box::new(lit_i(0)), span: Span::unknown() },
|
||||
ASTNode::Loop {
|
||||
condition: Box::new(bin(BinaryOperator::Less, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(10))),
|
||||
body: vec![
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "i".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(1))), span: Span::unknown() },
|
||||
ASTNode::If { condition: Box::new(bin(BinaryOperator::Or, bin(BinaryOperator::Equal, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(2)), bin(BinaryOperator::Equal, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(4)))), then_body: vec![ ASTNode::Continue { span: Span::unknown() } ], else_body: Some(vec![]), span: Span::unknown() },
|
||||
ASTNode::If { condition: Box::new(bin(BinaryOperator::Equal, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(7))), then_body: vec![ ASTNode::If { condition: Box::new(bin(BinaryOperator::Equal, lit_i(1), lit_i(1))), then_body: vec![ ASTNode::Break { span: Span::unknown() } ], else_body: Some(vec![]), span: Span::unknown() } ], else_body: Some(vec![]), span: Span::unknown() },
|
||||
ASTNode::If { condition: Box::new(bin(BinaryOperator::Equal, bin(BinaryOperator::Modulo, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(3)), lit_i(0))), then_body: vec![ ASTNode::Continue { span: Span::unknown() } ], else_body: Some(vec![]), span: Span::unknown() },
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "sum".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "sum".into(), span: Span::unknown() }, ASTNode::Variable { name: "i".into(), span: Span::unknown() })), span: Span::unknown() },
|
||||
],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Return { value: Some(Box::new(ASTNode::Variable { name: "sum".into(), span: Span::unknown() })), span: Span::unknown() }
|
||||
],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let mut mc = MirCompiler::with_options(false);
|
||||
let cr = mc.compile(ast).expect("compile");
|
||||
let f = cr.module.functions.get("main").expect("function main");
|
||||
|
||||
// Locate return block/value
|
||||
let (ret_block, out_v) = f
|
||||
.blocks
|
||||
.iter()
|
||||
.find_map(|(bid, bb)| match &bb.terminator {
|
||||
Some(MirInstruction::Return { value: Some(v) }) => Some((*bid, *v)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("ret block");
|
||||
|
||||
// In PHI-off, every predecessor of the ret block should write the merged value via edge Copy
|
||||
let preds: Vec<_> = f.blocks.get(&ret_block).unwrap().predecessors.iter().copied().collect();
|
||||
assert!(preds.len() >= 1, "ret must have at least one predecessor");
|
||||
for p in preds {
|
||||
let bb = f.blocks.get(&p).unwrap();
|
||||
let has_copy = bb.instructions.iter().any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(has_copy, "expected edge Copy to merged value at predecessor {:?}", p);
|
||||
}
|
||||
// ret block itself must not contain Copy to out_v
|
||||
let merge_has_copy = f
|
||||
.blocks
|
||||
.get(&ret_block)
|
||||
.unwrap()
|
||||
.instructions
|
||||
.iter()
|
||||
.any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(!merge_has_copy, "ret/merge must not contain Copy to merged value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loop_inner_if_multilevel_edge_copy() {
|
||||
// PHI-off
|
||||
std::env::set_var("NYASH_MIR_NO_PHI", "1");
|
||||
|
||||
// j=0; acc=0; loop(j<6){ j=j+1; if(j<3){ if(j%2==0){continue} acc=acc+10 } else { if(j==5){break} acc=acc+1 } } return acc
|
||||
let ast = ASTNode::Program {
|
||||
statements: vec![
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "j".into(), span: Span::unknown() }), value: Box::new(lit_i(0)), span: Span::unknown() },
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "acc".into(), span: Span::unknown() }), value: Box::new(lit_i(0)), span: Span::unknown() },
|
||||
ASTNode::Loop {
|
||||
condition: Box::new(bin(BinaryOperator::Less, ASTNode::Variable { name: "j".into(), span: Span::unknown() }, lit_i(6))),
|
||||
body: vec![
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "j".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "j".into(), span: Span::unknown() }, lit_i(1))), span: Span::unknown() },
|
||||
ASTNode::If {
|
||||
condition: Box::new(bin(BinaryOperator::Less, ASTNode::Variable { name: "j".into(), span: Span::unknown() }, lit_i(3))),
|
||||
then_body: vec![
|
||||
ASTNode::If {
|
||||
condition: Box::new(bin(BinaryOperator::Equal, bin(BinaryOperator::Modulo, ASTNode::Variable { name: "j".into(), span: Span::unknown() }, lit_i(2)), lit_i(0))),
|
||||
then_body: vec![ASTNode::Continue { span: Span::unknown() }],
|
||||
else_body: Some(vec![]),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "acc".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "acc".into(), span: Span::unknown() }, lit_i(10))), span: Span::unknown() },
|
||||
],
|
||||
else_body: Some(vec![
|
||||
ASTNode::If { condition: Box::new(bin(BinaryOperator::Equal, ASTNode::Variable { name: "j".into(), span: Span::unknown() }, lit_i(5))), then_body: vec![ ASTNode::Break { span: Span::unknown() } ], else_body: Some(vec![]), span: Span::unknown() },
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "acc".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "acc".into(), span: Span::unknown() }, lit_i(1))), span: Span::unknown() },
|
||||
]),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Return { value: Some(Box::new(ASTNode::Variable { name: "acc".into(), span: Span::unknown() })), span: Span::unknown() }
|
||||
],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let mut mc = MirCompiler::with_options(false);
|
||||
let cr = mc.compile(ast).expect("compile");
|
||||
let f = cr.module.functions.get("main").expect("function main");
|
||||
|
||||
// Find ret block/value
|
||||
let (ret_block, out_v) = f
|
||||
.blocks
|
||||
.iter()
|
||||
.find_map(|(bid, bb)| match &bb.terminator {
|
||||
Some(MirInstruction::Return { value: Some(v) }) => Some((*bid, *v)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("ret block");
|
||||
|
||||
let preds: Vec<_> = f.blocks.get(&ret_block).unwrap().predecessors.iter().copied().collect();
|
||||
assert!(preds.len() >= 1, "ret must have predecessors");
|
||||
for p in preds {
|
||||
let bb = f.blocks.get(&p).unwrap();
|
||||
assert!(bb.instructions.iter().any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "phi-legacy")]
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn phi_on_loop_has_phi_in_header() {
|
||||
// PHI-on (explicit)
|
||||
std::env::set_var("NYASH_MIR_NO_PHI", "0");
|
||||
|
||||
// x=0; loop(x<3){ x=x+1 } return x
|
||||
let ast = ASTNode::Program {
|
||||
statements: vec![
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "x".into(), span: Span::unknown() }), value: Box::new(lit_i(0)), span: Span::unknown() },
|
||||
ASTNode::Loop {
|
||||
condition: Box::new(bin(BinaryOperator::Less, ASTNode::Variable { name: "x".into(), span: Span::unknown() }, lit_i(3))),
|
||||
body: vec![ ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "x".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "x".into(), span: Span::unknown() }, lit_i(1))), span: Span::unknown() } ],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Return { value: Some(Box::new(ASTNode::Variable { name: "x".into(), span: Span::unknown() })), span: Span::unknown() }
|
||||
],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let mut mc = MirCompiler::with_options(false);
|
||||
let cr = mc.compile(ast).expect("compile");
|
||||
let f = cr.module.functions.get("main").expect("function main");
|
||||
|
||||
// Find any block with Phi(s) — loop header should have them in PHI-on.
|
||||
let phi_blocks: Vec<_> = f
|
||||
.blocks
|
||||
.iter()
|
||||
.filter(|(_bid, bb)| bb.phi_instructions().count() > 0)
|
||||
.map(|(bid, _)| *bid)
|
||||
.collect();
|
||||
|
||||
assert!(!phi_blocks.is_empty(), "expected at least one Phi block in PHI-on");
|
||||
|
||||
// Spot-check: each Phi should have at least 2 inputs (preheader + latch) in a loop
|
||||
for bid in phi_blocks.into_iter() {
|
||||
let bb = f.blocks.get(&bid).unwrap();
|
||||
for inst in bb.phi_instructions() {
|
||||
if let MirInstruction::Phi { inputs, .. } = inst {
|
||||
assert!(inputs.len() >= 2, "Phi should have at least 2 inputs");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
155
src/tests/loop_return_no_phi_tests.rs
Normal file
155
src/tests/loop_return_no_phi_tests.rs
Normal file
@ -0,0 +1,155 @@
|
||||
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
|
||||
use crate::mir::{MirCompiler, MirInstruction};
|
||||
|
||||
fn lit_i(i: i64) -> ASTNode {
|
||||
ASTNode::Literal { value: LiteralValue::Integer(i), span: Span::unknown() }
|
||||
}
|
||||
|
||||
fn bin(op: BinaryOperator, l: ASTNode, r: ASTNode) -> ASTNode {
|
||||
ASTNode::BinaryOp { operator: op, left: Box::new(l), right: Box::new(r), span: Span::unknown() }
|
||||
}
|
||||
|
||||
// PHI-off: mixed break + early return in loop; verify ret merge uses edge copies only
|
||||
#[test]
|
||||
fn loop_break_and_early_return_edge_copy() {
|
||||
std::env::set_var("NYASH_MIR_NO_PHI", "1");
|
||||
|
||||
// i=0; acc=0;
|
||||
// loop (i < 6) {
|
||||
// i = i + 1;
|
||||
// if (i == 5) { break }
|
||||
// if (i == 3) { return acc }
|
||||
// acc = acc + i;
|
||||
// }
|
||||
// return acc
|
||||
let ast = ASTNode::Program {
|
||||
statements: vec![
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "i".into(), span: Span::unknown() }), value: Box::new(lit_i(0)), span: Span::unknown() },
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "acc".into(), span: Span::unknown() }), value: Box::new(lit_i(0)), span: Span::unknown() },
|
||||
ASTNode::Loop {
|
||||
condition: Box::new(bin(BinaryOperator::LessThan, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(6))),
|
||||
body: vec![
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "i".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(1))), span: Span::unknown() },
|
||||
ASTNode::If { condition: Box::new(bin(BinaryOperator::Equal, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(5))), then_body: vec![ ASTNode::Break { span: Span::unknown() } ], else_body: Some(vec![]), span: Span::unknown() },
|
||||
ASTNode::If { condition: Box::new(bin(BinaryOperator::Equal, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(3))), then_body: vec![ ASTNode::Return { value: Some(Box::new(ASTNode::Variable { name: "acc".into(), span: Span::unknown() })), span: Span::unknown() } ], else_body: Some(vec![]), span: Span::unknown() },
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "acc".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "acc".into(), span: Span::unknown() }, ASTNode::Variable { name: "i".into(), span: Span::unknown() })), span: Span::unknown() },
|
||||
],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Return { value: Some(Box::new(ASTNode::Variable { name: "acc".into(), span: Span::unknown() })), span: Span::unknown() }
|
||||
],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let mut mc = MirCompiler::with_options(false);
|
||||
let cr = mc.compile(ast).expect("compile");
|
||||
let f = cr.module.functions.get("main").expect("function main");
|
||||
|
||||
// Locate the final return block/value (after-loop ret)
|
||||
let (ret_block, out_v) = f
|
||||
.blocks
|
||||
.iter()
|
||||
.find_map(|(bid, bb)| match &bb.terminator {
|
||||
Some(MirInstruction::Return { value: Some(v) }) => Some((*bid, *v)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("ret block");
|
||||
|
||||
// ret block's predecessors must write the merged destination via edge Copy (PHI-off)
|
||||
let preds: Vec<_> = f.blocks.get(&ret_block).unwrap().predecessors.iter().copied().collect();
|
||||
assert!(preds.len() >= 1, "ret must have at least one predecessor");
|
||||
for p in preds {
|
||||
let bb = f.blocks.get(&p).unwrap();
|
||||
let has_copy = bb.instructions.iter().any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(has_copy, "expected edge Copy to merged value at predecessor {:?}", p);
|
||||
}
|
||||
// Merge/ret block must not contain self-copy to out_v
|
||||
let merge_has_copy = f
|
||||
.blocks
|
||||
.get(&ret_block)
|
||||
.unwrap()
|
||||
.instructions
|
||||
.iter()
|
||||
.any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(!merge_has_copy, "ret/merge must not contain Copy to merged value");
|
||||
}
|
||||
|
||||
// PHI-off: deeper nested if chain in loop; verify edge copies on ret
|
||||
#[test]
|
||||
fn loop_if_three_level_merge_edge_copy() {
|
||||
std::env::set_var("NYASH_MIR_NO_PHI", "1");
|
||||
|
||||
// x=0; i=0;
|
||||
// loop(i<7){
|
||||
// i=i+1;
|
||||
// if (i%2==0) {
|
||||
// if (i==4) { continue }
|
||||
// x = x + 2
|
||||
// } else {
|
||||
// if (i==5) { break }
|
||||
// x = x + 1
|
||||
// }
|
||||
// }
|
||||
// return x
|
||||
let ast = ASTNode::Program {
|
||||
statements: vec![
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "x".into(), span: Span::unknown() }), value: Box::new(lit_i(0)), span: Span::unknown() },
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "i".into(), span: Span::unknown() }), value: Box::new(lit_i(0)), span: Span::unknown() },
|
||||
ASTNode::Loop {
|
||||
condition: Box::new(bin(BinaryOperator::LessThan, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(7))),
|
||||
body: vec![
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "i".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(1))), span: Span::unknown() },
|
||||
ASTNode::If {
|
||||
condition: Box::new(bin(BinaryOperator::Equal, bin(BinaryOperator::Mod, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(2)), lit_i(0))),
|
||||
then_body: vec![
|
||||
ASTNode::If {
|
||||
condition: Box::new(bin(BinaryOperator::Equal, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(4))),
|
||||
then_body: vec![ ASTNode::Continue { span: Span::unknown() } ],
|
||||
else_body: Some(vec![]),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "x".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "x".into(), span: Span::unknown() }, lit_i(2))), span: Span::unknown() },
|
||||
],
|
||||
else_body: Some(vec![
|
||||
ASTNode::If { condition: Box::new(bin(BinaryOperator::Equal, ASTNode::Variable { name: "i".into(), span: Span::unknown() }, lit_i(5))), then_body: vec![ ASTNode::Break { span: Span::unknown() } ], else_body: Some(vec![]), span: Span::unknown() },
|
||||
ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "x".into(), span: Span::unknown() }), value: Box::new(bin(BinaryOperator::Add, ASTNode::Variable { name: "x".into(), span: Span::unknown() }, lit_i(1))), span: Span::unknown() },
|
||||
]),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Return { value: Some(Box::new(ASTNode::Variable { name: "x".into(), span: Span::unknown() })), span: Span::unknown() }
|
||||
],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
let mut mc = MirCompiler::with_options(false);
|
||||
let cr = mc.compile(ast).expect("compile");
|
||||
let f = cr.module.functions.get("main").expect("function main");
|
||||
|
||||
let (ret_block, out_v) = f
|
||||
.blocks
|
||||
.iter()
|
||||
.find_map(|(bid, bb)| match &bb.terminator {
|
||||
Some(MirInstruction::Return { value: Some(v) }) => Some((*bid, *v)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("ret block");
|
||||
|
||||
let preds: Vec<_> = f.blocks.get(&ret_block).unwrap().predecessors.iter().copied().collect();
|
||||
assert!(preds.len() >= 1, "ret must have predecessors");
|
||||
for p in preds {
|
||||
let bb = f.blocks.get(&p).unwrap();
|
||||
assert!(bb.instructions.iter().any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v)));
|
||||
}
|
||||
let merge_has_copy = f
|
||||
.blocks
|
||||
.get(&ret_block)
|
||||
.unwrap()
|
||||
.instructions
|
||||
.iter()
|
||||
.any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(!merge_has_copy, "ret/merge must not contain Copy to merged value");
|
||||
}
|
||||
|
||||
@ -58,5 +58,16 @@ fn mir13_no_phi_if_merge_inserts_edge_copies_for_return() {
|
||||
.any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(has_copy, "expected Copy to out_v in predecessor {:?}", p);
|
||||
}
|
||||
}
|
||||
|
||||
// And we expect that the merge/ret block itself does not contain
|
||||
// an extra Copy to the merged value (edge-copy only policy)
|
||||
let merge_bb = f.blocks.get(&ret_block).expect("ret block present");
|
||||
let merge_has_copy = merge_bb
|
||||
.instructions
|
||||
.iter()
|
||||
.any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == out_v));
|
||||
assert!(
|
||||
!merge_has_copy,
|
||||
"ret/merge block should not contain Copy to merged value (edge-copy only)"
|
||||
);
|
||||
}
|
||||
|
||||
83
src/tests/parser_block_postfix_catch.rs
Normal file
83
src/tests/parser_block_postfix_catch.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use crate::parser::NyashParser;
|
||||
|
||||
fn enable_stage3() {
|
||||
std::env::set_var("NYASH_PARSER_STAGE3", "1");
|
||||
// Accept block‑postfix under Stage‑3 gate
|
||||
std::env::set_var("NYASH_BLOCK_CATCH", "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_postfix_catch_basic() {
|
||||
enable_stage3();
|
||||
let src = r#"
|
||||
{
|
||||
print("x")
|
||||
throw "IO"
|
||||
} catch (e) {
|
||||
print(e)
|
||||
}
|
||||
"#;
|
||||
let ast = NyashParser::parse_from_string(src).expect("parse ok");
|
||||
fn has_try(ast: &crate::ast::ASTNode) -> bool {
|
||||
match ast {
|
||||
crate::ast::ASTNode::TryCatch { .. } => true,
|
||||
crate::ast::ASTNode::Program { statements, .. } => statements.iter().any(has_try),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
assert!(has_try(&ast), "expected TryCatch from block‑postfix catch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_postfix_cleanup_only() {
|
||||
enable_stage3();
|
||||
let src = r#"
|
||||
{
|
||||
print("ok")
|
||||
} cleanup {
|
||||
print("done")
|
||||
}
|
||||
"#;
|
||||
let ast = NyashParser::parse_from_string(src).expect("parse ok");
|
||||
// Ensure TryCatch with empty catches and Some(cleanup)
|
||||
fn check(ast: &crate::ast::ASTNode) -> bool {
|
||||
match ast {
|
||||
crate::ast::ASTNode::TryCatch { catch_clauses, finally_body, .. } => {
|
||||
catch_clauses.is_empty() && finally_body.is_some()
|
||||
}
|
||||
crate::ast::ASTNode::Program { statements, .. } => statements.iter().any(check),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
assert!(check(&ast), "expected TryCatch with cleanup only");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_without_catch_with_direct_throw_should_error() {
|
||||
enable_stage3();
|
||||
let src = r#"
|
||||
{ throw "Oops" }
|
||||
"#;
|
||||
assert!(NyashParser::parse_from_string(src).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_catch_after_block_should_error() {
|
||||
enable_stage3();
|
||||
let src = r#"
|
||||
{ print("x") }
|
||||
catch (e) { print(e) }
|
||||
catch (e2) { print(e2) }
|
||||
"#;
|
||||
assert!(NyashParser::parse_from_string(src).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_attach_catch_to_if_block() {
|
||||
enable_stage3();
|
||||
let src = r#"
|
||||
if true { print("x") }
|
||||
catch (e) { print(e) }
|
||||
"#;
|
||||
assert!(NyashParser::parse_from_string(src).is_err());
|
||||
}
|
||||
40
src/tests/parser_block_postfix_errors.rs
Normal file
40
src/tests/parser_block_postfix_errors.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use crate::parser::NyashParser;
|
||||
|
||||
fn enable_stage3() {
|
||||
std::env::set_var("NYASH_PARSER_STAGE3", "1");
|
||||
std::env::set_var("NYASH_BLOCK_CATCH", "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_catch_is_error() {
|
||||
enable_stage3();
|
||||
let src = r#"catch (e) { print(e) }"#;
|
||||
assert!(NyashParser::parse_from_string(src).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_cleanup_is_error() {
|
||||
enable_stage3();
|
||||
let src = r#"cleanup { print("x") }"#;
|
||||
assert!(NyashParser::parse_from_string(src).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_then_catch_after_block_is_error() {
|
||||
enable_stage3();
|
||||
let src = r#"
|
||||
{ print("x") } cleanup { print("a") }
|
||||
catch (e) { print(e) }
|
||||
"#;
|
||||
assert!(NyashParser::parse_from_string(src).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_attach_catch_to_loop_block() {
|
||||
enable_stage3();
|
||||
let src = r#"
|
||||
loop { print("x") }
|
||||
catch (e) { print(e) }
|
||||
"#;
|
||||
assert!(NyashParser::parse_from_string(src).is_err());
|
||||
}
|
||||
42
src/tests/parser_method_postfix.rs
Normal file
42
src/tests/parser_method_postfix.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use crate::parser::NyashParser;
|
||||
|
||||
fn enable_stage3() {
|
||||
std::env::set_var("NYASH_PARSER_STAGE3", "1");
|
||||
std::env::set_var("NYASH_METHOD_CATCH", "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn method_postfix_cleanup_only_wraps_trycatch() {
|
||||
enable_stage3();
|
||||
let src = r#"
|
||||
box SafeBox {
|
||||
value: IntegerBox
|
||||
|
||||
update() {
|
||||
value = 41
|
||||
return value
|
||||
} cleanup {
|
||||
value = value + 1
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let ast = NyashParser::parse_from_string(src).expect("parse ok");
|
||||
// Find FunctionDeclaration 'update' and ensure its body contains a TryCatch
|
||||
fn has_method_trycatch(ast: &crate::ast::ASTNode) -> bool {
|
||||
match ast {
|
||||
crate::ast::ASTNode::BoxDeclaration { methods, .. } => {
|
||||
for (_name, m) in methods {
|
||||
if let crate::ast::ASTNode::FunctionDeclaration { name, body, .. } = m {
|
||||
if name == "update" {
|
||||
return body.iter().any(|n| matches!(n, crate::ast::ASTNode::TryCatch { .. }));
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
crate::ast::ASTNode::Program { statements, .. } => statements.iter().any(has_method_trycatch),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
assert!(has_method_trycatch(&ast), "expected TryCatch inside method body");
|
||||
}
|
||||
Reference in New Issue
Block a user