238 lines
8.1 KiB
Rust
238 lines
8.1 KiB
Rust
use super::*;
|
|
use crate::ast::{Span, ASTNode, BinaryOperator, LiteralValue};
|
|
|
|
fn str_lit(s: &str) -> ASTNode {
|
|
ASTNode::Literal {
|
|
value: LiteralValue::String(s.to_string()),
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
|
|
fn eq(a: ASTNode, b: ASTNode) -> ASTNode {
|
|
ASTNode::BinaryOp {
|
|
operator: BinaryOperator::Equal,
|
|
left: Box::new(a),
|
|
right: Box::new(b),
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
|
|
fn assign_x(num: i64) -> ASTNode {
|
|
ASTNode::Assignment {
|
|
target: Box::new(ASTNode::Variable {
|
|
name: "x".to_string(),
|
|
span: Span::unknown(),
|
|
}),
|
|
value: Box::new(ASTNode::Literal {
|
|
value: LiteralValue::Integer(num),
|
|
span: Span::unknown(),
|
|
}),
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn build_step_tree_if_only_nested_if_is_structural() {
|
|
// Equivalent shape to Phase103 "if-only merge" fixture:
|
|
//
|
|
// local x = 0
|
|
// if "x" == "x" { if "y" == "z" { x=1 } else { x=2 } } else { x=3 }
|
|
// print(x)
|
|
let ast = vec![
|
|
ASTNode::Local {
|
|
variables: vec!["x".to_string()],
|
|
initial_values: vec![Some(Box::new(ASTNode::Literal {
|
|
value: LiteralValue::Integer(0),
|
|
span: Span::unknown(),
|
|
}))],
|
|
span: Span::unknown(),
|
|
},
|
|
ASTNode::If {
|
|
condition: Box::new(eq(str_lit("x"), str_lit("x"))),
|
|
then_body: vec![ASTNode::If {
|
|
condition: Box::new(eq(str_lit("y"), str_lit("z"))),
|
|
then_body: vec![assign_x(1)],
|
|
else_body: Some(vec![assign_x(2)]),
|
|
span: Span::unknown(),
|
|
}],
|
|
else_body: Some(vec![assign_x(3)]),
|
|
span: Span::unknown(),
|
|
},
|
|
ASTNode::Print {
|
|
expression: Box::new(ASTNode::Variable {
|
|
name: "x".to_string(),
|
|
span: Span::unknown(),
|
|
}),
|
|
span: Span::unknown(),
|
|
},
|
|
];
|
|
|
|
let tree = StepTreeBuilderBox::build_from_block(&ast);
|
|
assert!(tree.features.has_if);
|
|
assert!(!tree.features.has_loop);
|
|
assert_eq!(tree.features.max_if_depth, 2);
|
|
assert_eq!(tree.contract.exits.len(), 0);
|
|
assert!(tree.contract.writes.contains("x"));
|
|
assert!(tree.contract.required_caps.contains(&StepCapability::If));
|
|
assert!(tree.contract.required_caps.contains(&StepCapability::NestedIf));
|
|
|
|
let basis = tree.signature_basis_string();
|
|
assert_eq!(
|
|
basis,
|
|
"kinds=Block,Stmt(local(x)),If,Block,If,Block,Stmt(assign(x)),Block,Stmt(assign(x)),Block,Stmt(assign(x)),Stmt(print);exits=;writes=x;reads=;caps=If,NestedIf;conds=(lit:str:x == lit:str:x)|(lit:str:y == lit:str:z)"
|
|
);
|
|
|
|
let tree2 = StepTreeBuilderBox::build_from_block(&ast);
|
|
assert_eq!(tree.signature, tree2.signature);
|
|
|
|
match tree.root {
|
|
StepNode::Block(nodes) => {
|
|
assert_eq!(nodes.len(), 3);
|
|
match &nodes[1] {
|
|
StepNode::If { then_branch, cond_ast, .. } => {
|
|
// cond_ast should be populated
|
|
assert!(matches!(&cond_ast.0.as_ref(), ASTNode::BinaryOp { .. }));
|
|
|
|
match &**then_branch {
|
|
StepNode::Block(inner_nodes) => match &inner_nodes[0] {
|
|
StepNode::If { cond_ast: inner_cond_ast, .. } => {
|
|
// inner cond_ast should also be populated
|
|
assert!(matches!(&inner_cond_ast.0.as_ref(), ASTNode::BinaryOp { .. }));
|
|
}
|
|
other => panic!("expected nested If, got {other:?}"),
|
|
},
|
|
other => panic!("expected Block in then_branch, got {other:?}"),
|
|
}
|
|
}
|
|
other => panic!("expected If at index 1, got {other:?}"),
|
|
}
|
|
}
|
|
other => panic!("expected root Block, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn step_tree_cond_ast_is_populated() {
|
|
// Phase 119: cond_ast should hold AST reference.
|
|
let ast = vec![ASTNode::If {
|
|
condition: Box::new(eq(str_lit("a"), str_lit("b"))),
|
|
then_body: vec![assign_x(1)],
|
|
else_body: None,
|
|
span: Span::unknown(),
|
|
}];
|
|
|
|
let tree = StepTreeBuilderBox::build_from_block(&ast);
|
|
|
|
match &tree.root {
|
|
StepNode::Block(nodes) => match &nodes[0] {
|
|
StepNode::If { cond_ast, .. } => {
|
|
// cond_ast should be populated with BinaryOp
|
|
assert!(matches!(&cond_ast.0.as_ref(), ASTNode::BinaryOp { .. }));
|
|
}
|
|
other => panic!("expected If, got {other:?}"),
|
|
},
|
|
other => panic!("expected root Block, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn step_tree_signature_is_stable_with_cond_ast() {
|
|
// Phase 119: cond_ast should NOT affect signature stability.
|
|
// Signature is based on cond_sig (AstSummary), not cond_ast.
|
|
let ast = vec![
|
|
ASTNode::Loop {
|
|
condition: Box::new(ASTNode::Literal {
|
|
value: LiteralValue::Bool(true),
|
|
span: Span::unknown(),
|
|
}),
|
|
body: vec![assign_x(1)],
|
|
span: Span::unknown(),
|
|
},
|
|
];
|
|
|
|
let tree1 = StepTreeBuilderBox::build_from_block(&ast);
|
|
let tree2 = StepTreeBuilderBox::build_from_block(&ast);
|
|
|
|
// Signature should be identical (deterministic)
|
|
assert_eq!(tree1.signature, tree2.signature);
|
|
|
|
// cond_ast should be populated
|
|
match &tree1.root {
|
|
StepNode::Block(nodes) => match &nodes[0] {
|
|
StepNode::Loop { cond_ast, .. } => {
|
|
assert!(matches!(&cond_ast.0.as_ref(), ASTNode::Literal { .. }));
|
|
}
|
|
other => panic!("expected Loop, got {other:?}"),
|
|
},
|
|
other => panic!("expected root Block, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contract_extracts_loop_exits_and_writes_minimal() {
|
|
fn var(name: &str) -> ASTNode {
|
|
ASTNode::Variable {
|
|
name: name.to_string(),
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
fn int_lit(v: i64) -> ASTNode {
|
|
ASTNode::Literal {
|
|
value: LiteralValue::Integer(v),
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
fn bin(op: BinaryOperator, lhs: ASTNode, rhs: ASTNode) -> ASTNode {
|
|
ASTNode::BinaryOp {
|
|
operator: op,
|
|
left: Box::new(lhs),
|
|
right: Box::new(rhs),
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
fn assign(name: &str, value: ASTNode) -> ASTNode {
|
|
ASTNode::Assignment {
|
|
target: Box::new(var(name)),
|
|
value: Box::new(value),
|
|
span: Span::unknown(),
|
|
}
|
|
}
|
|
|
|
// local i=0; local x=0;
|
|
// loop(i < 3) { x = x + 1; if x == 2 { break } i = i + 1 }
|
|
let ast = vec![
|
|
ASTNode::Local {
|
|
variables: vec!["i".to_string()],
|
|
initial_values: vec![Some(Box::new(int_lit(0)))],
|
|
span: Span::unknown(),
|
|
},
|
|
ASTNode::Local {
|
|
variables: vec!["x".to_string()],
|
|
initial_values: vec![Some(Box::new(int_lit(0)))],
|
|
span: Span::unknown(),
|
|
},
|
|
ASTNode::Loop {
|
|
condition: Box::new(bin(BinaryOperator::Less, var("i"), int_lit(3))),
|
|
body: vec![
|
|
assign("x", bin(BinaryOperator::Add, var("x"), int_lit(1))),
|
|
ASTNode::If {
|
|
condition: Box::new(bin(BinaryOperator::Equal, var("x"), int_lit(2))),
|
|
then_body: vec![ASTNode::Break { span: Span::unknown() }],
|
|
else_body: None,
|
|
span: Span::unknown(),
|
|
},
|
|
assign("i", bin(BinaryOperator::Add, var("i"), int_lit(1))),
|
|
],
|
|
span: Span::unknown(),
|
|
},
|
|
];
|
|
|
|
let tree = StepTreeBuilderBox::build_from_block(&ast);
|
|
assert!(tree.features.has_loop);
|
|
assert!(tree.contract.exits.contains(&ExitKind::Break));
|
|
assert!(tree.contract.writes.contains("i"));
|
|
assert!(tree.contract.writes.contains("x"));
|
|
assert!(tree.contract.required_caps.contains(&StepCapability::Loop));
|
|
assert!(tree.contract.required_caps.contains(&StepCapability::If));
|
|
}
|