From 2b5c141e226516fa8e8b25cd41a6368d2c85a6d3 Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Thu, 18 Dec 2025 00:22:21 +0900 Subject: [PATCH] feat(control_tree): add StepTree builder (dev-only) --- src/mir/builder/calls/lowering.rs | 6 + .../builder/control_flow/joinir/routing.rs | 28 + src/mir/control_tree/mod.rs | 12 + src/mir/control_tree/step_tree.rs | 565 ++++++++++++++++++ src/mir/mod.rs | 1 + 5 files changed, 612 insertions(+) create mode 100644 src/mir/control_tree/mod.rs create mode 100644 src/mir/control_tree/step_tree.rs diff --git a/src/mir/builder/calls/lowering.rs b/src/mir/builder/calls/lowering.rs index 8c6f4135..30326a86 100644 --- a/src/mir/builder/calls/lowering.rs +++ b/src/mir/builder/calls/lowering.rs @@ -166,6 +166,12 @@ impl MirBuilder { /// 🎯 箱理論: Step 4 - 本体lowering fn lower_function_body(&mut self, body: Vec) -> Result<(), String> { + if crate::config::env::joinir_dev_enabled() { + let tree = crate::mir::control_tree::StepTreeBuilderBox::build_from_block(&body); + crate::mir::builder::control_flow::joinir::trace::trace() + .dev("control_tree/step_tree", &tree.to_compact_string()); + } + eprintln!("[DEBUG/lower_function_body] body.len() = {}", body.len()); let program_ast = function_lowering::wrap_in_program(body); eprintln!("[DEBUG/lower_function_body] About to call build_expression"); diff --git a/src/mir/builder/control_flow/joinir/routing.rs b/src/mir/builder/control_flow/joinir/routing.rs index cb1136f2..b588b355 100644 --- a/src/mir/builder/control_flow/joinir/routing.rs +++ b/src/mir/builder/control_flow/joinir/routing.rs @@ -42,6 +42,34 @@ pub(in crate::mir::builder) fn choose_pattern_kind( let has_continue = ast_features::detect_continue_in_body(body); let has_break = ast_features::detect_break_in_body(body); + // Phase 110: StepTree parity check (structure-only SSOT). + // + // This is dev-only; strict mode turns mismatch into a fail-fast. + if crate::config::env::joinir_dev_enabled() { + use crate::ast::Span; + use crate::mir::control_tree::StepTreeBuilderBox; + + let loop_ast = ASTNode::Loop { + condition: Box::new(condition.clone()), + body: body.to_vec(), + span: Span::unknown(), + }; + + let tree = StepTreeBuilderBox::build_from_ast(&loop_ast); + if tree.features.has_break != has_break || tree.features.has_continue != has_continue { + let msg = format!( + "[choose_pattern_kind/STEPTREE_PARITY] step_tree(break={}, cont={}) != extractor(break={}, cont={})", + tree.features.has_break, tree.features.has_continue, has_break, has_continue + ); + + if crate::config::env::joinir_dev::strict_enabled() { + panic!("{}", msg); + } else { + trace::trace().dev("choose_pattern_kind/step_tree_parity", &msg); + } + } + } + // Phase 193: Extract features using modularized extractor let features = ast_features::extract_features(condition, body, has_continue, has_break); diff --git a/src/mir/control_tree/mod.rs b/src/mir/control_tree/mod.rs new file mode 100644 index 00000000..fa5eed40 --- /dev/null +++ b/src/mir/control_tree/mod.rs @@ -0,0 +1,12 @@ +//! ControlTree / StepTree (structure-only SSOT) +//! +//! Policy: +//! - This module must NOT reference MIR/JoinIR values (`ValueId`, `BlockId`, `PHI`, ...). +//! - It only describes AST control structure and derived features/capabilities. + +mod step_tree; + +pub use step_tree::{ + AstSummary, StepNode, StepStmtKind, StepTree, StepTreeBuilderBox, StepTreeFeatures, +}; + diff --git a/src/mir/control_tree/step_tree.rs b/src/mir/control_tree/step_tree.rs new file mode 100644 index 00000000..0bccfd43 --- /dev/null +++ b/src/mir/control_tree/step_tree.rs @@ -0,0 +1,565 @@ +use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span, UnaryOperator}; + +#[derive(Debug, Clone, PartialEq)] +pub struct StepTree { + pub root: StepNode, + pub features: StepTreeFeatures, +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct StepTreeFeatures { + pub has_if: bool, + pub has_loop: bool, + pub has_break: bool, + pub has_continue: bool, + pub has_return: bool, + pub max_if_depth: u32, + pub max_loop_depth: u32, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum StepNode { + Block(Vec), + If { + cond: AstSummary, + then_branch: Box, + else_branch: Option>, + span: Span, + }, + Loop { + cond: AstSummary, + body: Box, + span: Span, + }, + Stmt { kind: StepStmtKind, span: Span }, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum StepStmtKind { + LocalDecl { vars: Vec }, + Assign, + Print, + Return, + Break, + Continue, + Other(&'static str), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum AstSummary { + Variable(String), + Literal(LiteralValue), + Unary { + op: UnaryOperator, + expr: Box, + }, + Binary { + op: BinaryOperator, + lhs: Box, + rhs: Box, + }, + Other(&'static str), +} + +impl StepTree { + pub fn to_compact_string(&self) -> String { + let mut out = String::new(); + self.root.write_compact(&mut out, 0); + out + } +} + +impl StepNode { + fn write_compact(&self, out: &mut String, indent: usize) { + let pad = " ".repeat(indent); + match self { + StepNode::Block(nodes) => { + out.push_str(&format!("{pad}Block(len={})\n", nodes.len())); + for n in nodes { + n.write_compact(out, indent + 1); + } + } + StepNode::If { + cond, + then_branch, + else_branch, + .. + } => { + out.push_str(&format!( + "{pad}If(cond={})\n", + cond.to_compact_string() + )); + out.push_str(&format!("{pad} then:\n")); + then_branch.write_compact(out, indent + 2); + if let Some(else_branch) = else_branch { + out.push_str(&format!("{pad} else:\n")); + else_branch.write_compact(out, indent + 2); + } + } + StepNode::Loop { cond, body, .. } => { + out.push_str(&format!( + "{pad}Loop(cond={})\n", + cond.to_compact_string() + )); + body.write_compact(out, indent + 1); + } + StepNode::Stmt { kind, .. } => { + out.push_str(&format!("{pad}Stmt({})\n", kind.to_compact_string())); + } + } + } +} + +impl StepStmtKind { + fn to_compact_string(&self) -> String { + match self { + StepStmtKind::LocalDecl { vars } => format!("local({})", vars.join(",")), + StepStmtKind::Assign => "assign".to_string(), + StepStmtKind::Print => "print".to_string(), + StepStmtKind::Return => "return".to_string(), + StepStmtKind::Break => "break".to_string(), + StepStmtKind::Continue => "continue".to_string(), + StepStmtKind::Other(name) => format!("other:{name}"), + } + } +} + +impl AstSummary { + fn to_compact_string(&self) -> String { + match self { + AstSummary::Variable(name) => format!("var:{name}"), + AstSummary::Literal(lit) => format!("lit:{lit:?}"), + AstSummary::Unary { op, expr } => format!("({op:?} {})", expr.to_compact_string()), + AstSummary::Binary { op, lhs, rhs } => format!( + "({} {} {})", + lhs.to_compact_string(), + op, + rhs.to_compact_string() + ), + AstSummary::Other(k) => format!("other:{k}"), + } + } +} + +pub struct StepTreeBuilderBox; + +impl StepTreeBuilderBox { + pub fn build_from_ast(ast: &ASTNode) -> StepTree { + match ast { + ASTNode::Program { statements, .. } => Self::build_from_block(statements), + ASTNode::ScopeBox { body, .. } => Self::build_from_block(body), + _ => { + let (node, features) = Self::build_node(ast, 0, 0); + StepTree { + root: node, + features, + } + } + } + } + + pub fn build_from_block(stmts: &[ASTNode]) -> StepTree { + let mut nodes = Vec::with_capacity(stmts.len()); + let mut features = StepTreeFeatures::default(); + for stmt in stmts { + let (node, node_features) = Self::build_node(stmt, 0, 0); + nodes.push(node); + features = merge_features(features, node_features); + } + StepTree { + root: StepNode::Block(nodes), + features, + } + } + + fn build_node(ast: &ASTNode, if_depth: u32, loop_depth: u32) -> (StepNode, StepTreeFeatures) { + match ast { + ASTNode::If { + condition, + then_body, + else_body, + span, + } => { + let cond = summarize_ast(condition); + let (then_node, then_features) = + Self::build_block_node(then_body, if_depth + 1, loop_depth); + let (else_node, else_features) = match else_body { + Some(else_body) => { + let (node, f) = + Self::build_block_node(else_body, if_depth + 1, loop_depth); + (Some(Box::new(node)), f) + } + None => (None, StepTreeFeatures::default()), + }; + let mut features = StepTreeFeatures { + has_if: true, + max_if_depth: (if_depth + 1).max(then_features.max_if_depth), + ..StepTreeFeatures::default() + }; + features = merge_features(features, then_features); + features = merge_features(features, else_features); + + ( + StepNode::If { + cond, + then_branch: Box::new(then_node), + else_branch: else_node, + span: span.clone(), + }, + features, + ) + } + ASTNode::Loop { + condition, body, span, .. + } => { + let cond = summarize_ast(condition); + let (body_node, body_features) = + Self::build_block_node(body, if_depth, loop_depth + 1); + let mut features = StepTreeFeatures { + has_loop: true, + max_loop_depth: (loop_depth + 1).max(body_features.max_loop_depth), + ..StepTreeFeatures::default() + }; + features = merge_features(features, body_features); + ( + StepNode::Loop { + cond, + body: Box::new(body_node), + span: span.clone(), + }, + features, + ) + } + ASTNode::ScopeBox { body, span } => { + let (node, features) = Self::build_block_node(body, if_depth, loop_depth); + (node.with_span(span.clone()), features) + } + ASTNode::Return { span, .. } => ( + StepNode::Stmt { + kind: StepStmtKind::Return, + span: span.clone(), + }, + StepTreeFeatures { + has_return: true, + ..StepTreeFeatures::default() + }, + ), + ASTNode::Break { span } => ( + StepNode::Stmt { + kind: StepStmtKind::Break, + span: span.clone(), + }, + StepTreeFeatures { + has_break: true, + ..StepTreeFeatures::default() + }, + ), + ASTNode::Continue { span } => ( + StepNode::Stmt { + kind: StepStmtKind::Continue, + span: span.clone(), + }, + StepTreeFeatures { + has_continue: true, + ..StepTreeFeatures::default() + }, + ), + ASTNode::Local { + variables, span, .. + } => ( + StepNode::Stmt { + kind: StepStmtKind::LocalDecl { + vars: variables.clone(), + }, + span: span.clone(), + }, + StepTreeFeatures::default(), + ), + ASTNode::Assignment { span, .. } => ( + StepNode::Stmt { + kind: StepStmtKind::Assign, + span: span.clone(), + }, + StepTreeFeatures::default(), + ), + ASTNode::Print { span, .. } => ( + StepNode::Stmt { + kind: StepStmtKind::Print, + span: span.clone(), + }, + StepTreeFeatures::default(), + ), + other => ( + StepNode::Stmt { + kind: StepStmtKind::Other(ast_kind_name(other)), + span: span_of(other), + }, + StepTreeFeatures::default(), + ), + } + } + + fn build_block_node( + stmts: &[ASTNode], + if_depth: u32, + loop_depth: u32, + ) -> (StepNode, StepTreeFeatures) { + let mut nodes = Vec::with_capacity(stmts.len()); + let mut features = StepTreeFeatures::default(); + for stmt in stmts { + let (node, node_features) = Self::build_node(stmt, if_depth, loop_depth); + nodes.push(node); + features = merge_features(features, node_features); + } + (StepNode::Block(nodes), features) + } +} + +fn merge_features(mut a: StepTreeFeatures, b: StepTreeFeatures) -> StepTreeFeatures { + a.has_if |= b.has_if; + a.has_loop |= b.has_loop; + a.has_break |= b.has_break; + a.has_continue |= b.has_continue; + a.has_return |= b.has_return; + a.max_if_depth = a.max_if_depth.max(b.max_if_depth); + a.max_loop_depth = a.max_loop_depth.max(b.max_loop_depth); + a +} + +fn summarize_ast(ast: &ASTNode) -> AstSummary { + match ast { + ASTNode::Variable { name, .. } => AstSummary::Variable(name.clone()), + ASTNode::Literal { value, .. } => AstSummary::Literal(value.clone()), + ASTNode::UnaryOp { + operator, operand, .. + } => AstSummary::Unary { + op: operator.clone(), + expr: Box::new(summarize_ast(operand)), + }, + ASTNode::BinaryOp { + operator, + left, + right, + .. + } => AstSummary::Binary { + op: operator.clone(), + lhs: Box::new(summarize_ast(left)), + rhs: Box::new(summarize_ast(right)), + }, + other => AstSummary::Other(ast_kind_name(other)), + } +} + +fn ast_kind_name(ast: &ASTNode) -> &'static str { + match ast { + ASTNode::Program { .. } => "Program", + ASTNode::Assignment { .. } => "Assignment", + ASTNode::Print { .. } => "Print", + ASTNode::If { .. } => "If", + ASTNode::Loop { .. } => "Loop", + ASTNode::While { .. } => "While", + ASTNode::ForRange { .. } => "ForRange", + ASTNode::Return { .. } => "Return", + ASTNode::Break { .. } => "Break", + ASTNode::Continue { .. } => "Continue", + ASTNode::UsingStatement { .. } => "UsingStatement", + ASTNode::ImportStatement { .. } => "ImportStatement", + ASTNode::Nowait { .. } => "Nowait", + ASTNode::AwaitExpression { .. } => "AwaitExpression", + ASTNode::QMarkPropagate { .. } => "QMarkPropagate", + ASTNode::MatchExpr { .. } => "MatchExpr", + ASTNode::ArrayLiteral { .. } => "ArrayLiteral", + ASTNode::MapLiteral { .. } => "MapLiteral", + ASTNode::Lambda { .. } => "Lambda", + ASTNode::Arrow { .. } => "Arrow", + ASTNode::TryCatch { .. } => "TryCatch", + ASTNode::Throw { .. } => "Throw", + ASTNode::BoxDeclaration { .. } => "BoxDeclaration", + ASTNode::FunctionDeclaration { .. } => "FunctionDeclaration", + ASTNode::GlobalVar { .. } => "GlobalVar", + ASTNode::Literal { .. } => "Literal", + ASTNode::Variable { .. } => "Variable", + ASTNode::UnaryOp { .. } => "UnaryOp", + ASTNode::BinaryOp { .. } => "BinaryOp", + ASTNode::GroupedAssignmentExpr { .. } => "GroupedAssignmentExpr", + ASTNode::MethodCall { .. } => "MethodCall", + ASTNode::Call { .. } => "Call", + ASTNode::FunctionCall { .. } => "FunctionCall", + ASTNode::FieldAccess { .. } => "FieldAccess", + ASTNode::Index { .. } => "Index", + ASTNode::New { .. } => "New", + ASTNode::This { .. } => "This", + ASTNode::Me { .. } => "Me", + ASTNode::FromCall { .. } => "FromCall", + ASTNode::ThisField { .. } => "ThisField", + ASTNode::MeField { .. } => "MeField", + ASTNode::Local { .. } => "Local", + ASTNode::ScopeBox { .. } => "ScopeBox", + ASTNode::Outbox { .. } => "Outbox", + } +} + +fn span_of(ast: &ASTNode) -> Span { + match ast { + ASTNode::Program { span, .. } => span.clone(), + ASTNode::Assignment { span, .. } => span.clone(), + ASTNode::Print { span, .. } => span.clone(), + ASTNode::If { span, .. } => span.clone(), + ASTNode::Loop { span, .. } => span.clone(), + ASTNode::While { span, .. } => span.clone(), + ASTNode::ForRange { span, .. } => span.clone(), + ASTNode::Return { span, .. } => span.clone(), + ASTNode::Break { span } => span.clone(), + ASTNode::Continue { span } => span.clone(), + ASTNode::UsingStatement { span, .. } => span.clone(), + ASTNode::ImportStatement { span, .. } => span.clone(), + ASTNode::Nowait { span, .. } => span.clone(), + ASTNode::AwaitExpression { span, .. } => span.clone(), + ASTNode::QMarkPropagate { span, .. } => span.clone(), + ASTNode::MatchExpr { span, .. } => span.clone(), + ASTNode::ArrayLiteral { span, .. } => span.clone(), + ASTNode::MapLiteral { span, .. } => span.clone(), + ASTNode::Lambda { span, .. } => span.clone(), + ASTNode::Arrow { span, .. } => span.clone(), + ASTNode::TryCatch { span, .. } => span.clone(), + ASTNode::Throw { span, .. } => span.clone(), + ASTNode::BoxDeclaration { span, .. } => span.clone(), + ASTNode::FunctionDeclaration { span, .. } => span.clone(), + ASTNode::GlobalVar { span, .. } => span.clone(), + ASTNode::Literal { span, .. } => span.clone(), + ASTNode::Variable { span, .. } => span.clone(), + ASTNode::UnaryOp { span, .. } => span.clone(), + ASTNode::BinaryOp { span, .. } => span.clone(), + ASTNode::GroupedAssignmentExpr { span, .. } => span.clone(), + ASTNode::MethodCall { span, .. } => span.clone(), + ASTNode::Call { span, .. } => span.clone(), + ASTNode::FunctionCall { span, .. } => span.clone(), + ASTNode::FieldAccess { span, .. } => span.clone(), + ASTNode::Index { span, .. } => span.clone(), + ASTNode::New { span, .. } => span.clone(), + ASTNode::This { span } => span.clone(), + ASTNode::Me { span } => span.clone(), + ASTNode::FromCall { span, .. } => span.clone(), + ASTNode::ThisField { span, .. } => span.clone(), + ASTNode::MeField { span, .. } => span.clone(), + ASTNode::Local { span, .. } => span.clone(), + ASTNode::ScopeBox { span, .. } => span.clone(), + ASTNode::Outbox { span, .. } => span.clone(), + } +} + +impl StepNode { + fn with_span(self, span: Span) -> StepNode { + match self { + StepNode::Block(nodes) => StepNode::Block(nodes), + StepNode::If { + cond, + then_branch, + else_branch, + .. + } => StepNode::If { + cond, + then_branch, + else_branch, + span, + }, + StepNode::Loop { cond, body, .. } => StepNode::Loop { cond, body, span }, + StepNode::Stmt { kind, .. } => StepNode::Stmt { kind, span }, + } + } +} + +#[cfg(test)] +mod tests { + 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); + + match tree.root { + StepNode::Block(nodes) => { + assert_eq!(nodes.len(), 3); + match &nodes[1] { + StepNode::If { then_branch, .. } => match &**then_branch { + StepNode::Block(inner_nodes) => match &inner_nodes[0] { + StepNode::If { .. } => {} + 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:?}"), + } + } +} diff --git a/src/mir/mod.rs b/src/mir/mod.rs index a7e29b89..e17a0134 100644 --- a/src/mir/mod.rs +++ b/src/mir/mod.rs @@ -27,6 +27,7 @@ pub mod utils; // Phase 15 control flow utilities for root treatment // pub mod lowerers; // reserved: Stage-3 loop lowering (while/for-range) pub mod cfg_extractor; // Phase 154: CFG extraction for hako_check pub mod control_form; +pub mod control_tree; // Phase 110: Structure-only SSOT (StepTree) pub mod function_emission; // FunctionEmissionBox(MirFunction直編集の発行ヘルパ) pub mod hints; // scaffold: zero-cost guidance (no-op) pub mod join_ir; // Phase 26-H: 関数正規化IR(JoinIR)