feat(control_tree): add StepTreeContract and signature (dev-only)

This commit is contained in:
nyash-codex
2025-12-18 00:57:58 +09:00
parent 9bcda215f8
commit 14730c227f
4 changed files with 445 additions and 15 deletions

View File

@ -33,6 +33,34 @@ AST を「構造ード」に落とした木または木Block列
- `Loop`: `cond` と body の `Block` - `Loop`: `cond` と body の `Block`
- `Stmt`: 構造化していない文Local/Assign/Return/Break/Continue/Expr などを tag 化) - `Stmt`: 構造化していない文Local/Assign/Return/Break/Continue/Expr などを tag 化)
## StepTreeContract構造契約SSOT
StepTreeContract は「この構造が何を含み、何を要求するか」を最小の契約として宣言する。
lowering/PHI/CFG の判断にはまだ使わないdev-onlyだが、再解析の増殖を防ぐための SSOT になる。
最小フィールド案P1:
- `exits`: `return` / `break` / `continue` の存在(構造だけ)
- `writes`: 変数への書き込み(最小は `Assignment target=Variable(name)``Local` 宣言の集合)
- `required_caps`: capability 宣言(例: `NestedLoop`, `TryCatch`, `Throw`, `Lambda` など)
- `cond_sig`: if/loop 条件式の要約(下記)
### cond_sig の方針SSOT
- cond_sig は **ASTNode 参照を保持しない**巨大化・ライフタイム・clone コストの問題を避ける)。
- cond_sig は **summary要約SSOT** とし、`AstSummary` 相当の安定表現に固定する。
- 目的は「構造分類/契約の固定」であり、式同値(完全な等価判定)は非対象。
## StepTreeSignature構造署名
StepTreeSignature は StepTreeContract + node kinds の “安定な基底文字列” を hash した識別子。
用途:
- dev-only ログの検索キー
- “同型ループ/同型if” の増殖検知(再解析の増殖防止)
注意:
- `Span` 等の位置情報は signature に含めない(入力差でブレるため)。
## Capability段階投入のSSOT ## Capability段階投入のSSOT
StepTree は capability を“宣言”し、未対応は **Fail-Fastdev-only / strict** で止める。 StepTree は capability を“宣言”し、未対応は **Fail-Fastdev-only / strict** で止める。

View File

@ -70,6 +70,18 @@ pub(crate) fn detect_break_in_body(body: &[ASTNode]) -> bool {
false false
} }
/// Detect if a loop body contains return statements
///
/// This is used for dev-only parity checks with structure SSOT (StepTree).
pub(crate) fn detect_return_in_body(body: &[ASTNode]) -> bool {
for stmt in body {
if has_return_node(stmt) {
return true;
}
}
false
}
/// Extract full feature set from loop body AST /// Extract full feature set from loop body AST
/// ///
/// This is the main entry point for feature extraction. It analyzes the loop body /// This is the main entry point for feature extraction. It analyzes the loop body
@ -273,6 +285,26 @@ fn has_break_node(node: &ASTNode) -> bool {
} }
} }
/// Recursive helper to check if AST node contains return
fn has_return_node(node: &ASTNode) -> bool {
match node {
ASTNode::Return { .. } => true,
ASTNode::If {
then_body,
else_body,
..
} => {
then_body.iter().any(has_return_node)
|| else_body
.as_ref()
.map_or(false, |e| e.iter().any(has_return_node))
}
ASTNode::Loop { body, .. } => body.iter().any(has_return_node),
ASTNode::ScopeBox { body, .. } => body.iter().any(has_return_node),
_ => false,
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -41,6 +41,7 @@ pub(in crate::mir::builder) fn choose_pattern_kind(
// Phase 193: Use AST Feature Extractor Box for break/continue detection // Phase 193: Use AST Feature Extractor Box for break/continue detection
let has_continue = ast_features::detect_continue_in_body(body); let has_continue = ast_features::detect_continue_in_body(body);
let has_break = ast_features::detect_break_in_body(body); let has_break = ast_features::detect_break_in_body(body);
let has_return = ast_features::detect_return_in_body(body);
// Phase 110: StepTree parity check (structure-only SSOT). // Phase 110: StepTree parity check (structure-only SSOT).
// //
@ -56,10 +57,18 @@ pub(in crate::mir::builder) fn choose_pattern_kind(
}; };
let tree = StepTreeBuilderBox::build_from_ast(&loop_ast); let tree = StepTreeBuilderBox::build_from_ast(&loop_ast);
if tree.features.has_break != has_break || tree.features.has_continue != has_continue { if tree.features.has_break != has_break
|| tree.features.has_continue != has_continue
|| tree.features.has_return != has_return
{
let msg = format!( let msg = format!(
"[choose_pattern_kind/STEPTREE_PARITY] step_tree(break={}, cont={}) != extractor(break={}, cont={})", "[choose_pattern_kind/STEPTREE_PARITY] step_tree(break={}, cont={}, ret={}) != extractor(break={}, cont={}, ret={})",
tree.features.has_break, tree.features.has_continue, has_break, has_continue tree.features.has_break,
tree.features.has_continue,
tree.features.has_return,
has_break,
has_continue,
has_return
); );
if crate::config::env::joinir_dev::strict_enabled() { if crate::config::env::joinir_dev::strict_enabled() {

View File

@ -1,9 +1,13 @@
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span, UnaryOperator}; use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span, UnaryOperator};
use std::collections::BTreeSet;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct StepTree { pub struct StepTree {
pub root: StepNode, pub root: StepNode,
pub features: StepTreeFeatures, pub features: StepTreeFeatures,
pub contract: StepTreeContract,
pub signature: StepTreeSignature,
} }
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
@ -37,7 +41,7 @@ pub enum StepNode {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum StepStmtKind { pub enum StepStmtKind {
LocalDecl { vars: Vec<String> }, LocalDecl { vars: Vec<String> },
Assign, Assign { target: Option<String> },
Print, Print,
Return, Return,
Break, Break,
@ -67,6 +71,108 @@ impl StepTree {
self.root.write_compact(&mut out, 0); self.root.write_compact(&mut out, 0);
out out
} }
pub fn signature_basis_string(&self) -> String {
let mut kinds = Vec::new();
collect_node_kinds(&self.root, &mut kinds);
let kinds = kinds.join(",");
self.contract.signature_basis_string(&kinds)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ExitKind {
Return,
Break,
Continue,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum StepCapability {
If,
Loop,
NestedIf,
NestedLoop,
Return,
Break,
Continue,
TryCatch,
Throw,
Lambda,
While,
ForRange,
Match,
Arrow,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct StepTreeContract {
pub exits: BTreeSet<ExitKind>,
pub writes: BTreeSet<String>,
pub required_caps: BTreeSet<StepCapability>,
pub cond_sig: Vec<String>,
}
impl StepTreeContract {
pub fn signature_basis_string(&self, node_kinds: &str) -> String {
let exits = self
.exits
.iter()
.map(|e| match e {
ExitKind::Return => "return",
ExitKind::Break => "break",
ExitKind::Continue => "continue",
})
.collect::<Vec<_>>()
.join(",");
let writes = self.writes.iter().cloned().collect::<Vec<_>>().join(",");
let caps = self
.required_caps
.iter()
.map(|c| match c {
StepCapability::If => "If",
StepCapability::Loop => "Loop",
StepCapability::NestedIf => "NestedIf",
StepCapability::NestedLoop => "NestedLoop",
StepCapability::Return => "Return",
StepCapability::Break => "Break",
StepCapability::Continue => "Continue",
StepCapability::TryCatch => "TryCatch",
StepCapability::Throw => "Throw",
StepCapability::Lambda => "Lambda",
StepCapability::While => "While",
StepCapability::ForRange => "ForRange",
StepCapability::Match => "Match",
StepCapability::Arrow => "Arrow",
})
.collect::<Vec<_>>()
.join(",");
let cond_sig = self.cond_sig.join("|");
format!(
"kinds={};exits={};writes={};caps={};conds={}",
node_kinds, exits, writes, caps, cond_sig
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StepTreeSignature(pub u64);
impl StepTreeSignature {
pub fn from_basis_string(basis: &str) -> Self {
// FNV-1a 64-bit (stable, no external deps).
let mut hash: u64 = 0xcbf29ce484222325;
for b in basis.as_bytes() {
hash ^= *b as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
StepTreeSignature(hash)
}
pub fn to_hex(self) -> String {
format!("{:016x}", self.0)
}
} }
impl StepNode { impl StepNode {
@ -114,7 +220,10 @@ impl StepStmtKind {
fn to_compact_string(&self) -> String { fn to_compact_string(&self) -> String {
match self { match self {
StepStmtKind::LocalDecl { vars } => format!("local({})", vars.join(",")), StepStmtKind::LocalDecl { vars } => format!("local({})", vars.join(",")),
StepStmtKind::Assign => "assign".to_string(), StepStmtKind::Assign { target } => match target {
Some(name) => format!("assign({name})"),
None => "assign(?)".to_string(),
},
StepStmtKind::Print => "print".to_string(), StepStmtKind::Print => "print".to_string(),
StepStmtKind::Return => "return".to_string(), StepStmtKind::Return => "return".to_string(),
StepStmtKind::Break => "break".to_string(), StepStmtKind::Break => "break".to_string(),
@ -128,7 +237,7 @@ impl AstSummary {
fn to_compact_string(&self) -> String { fn to_compact_string(&self) -> String {
match self { match self {
AstSummary::Variable(name) => format!("var:{name}"), AstSummary::Variable(name) => format!("var:{name}"),
AstSummary::Literal(lit) => format!("lit:{lit:?}"), AstSummary::Literal(lit) => format!("lit:{}", lit_to_sig_string(lit)),
AstSummary::Unary { op, expr } => format!("({op:?} {})", expr.to_compact_string()), AstSummary::Unary { op, expr } => format!("({op:?} {})", expr.to_compact_string()),
AstSummary::Binary { op, lhs, rhs } => format!( AstSummary::Binary { op, lhs, rhs } => format!(
"({} {} {})", "({} {} {})",
@ -141,6 +250,22 @@ impl AstSummary {
} }
} }
fn lit_to_sig_string(lit: &LiteralValue) -> String {
match lit {
LiteralValue::String(s) => format!("str:{}", escape_sig_atom(s)),
LiteralValue::Integer(i) => format!("int:{i}"),
LiteralValue::Float(f) => format!("float:{:016x}", f.to_bits()),
LiteralValue::Bool(b) => format!("bool:{}", if *b { 1 } else { 0 }),
LiteralValue::Null => "null".to_string(),
LiteralValue::Void => "void".to_string(),
}
}
fn escape_sig_atom(s: &str) -> String {
// Minimal stable escaping for signature strings.
s.replace('\\', "\\\\").replace('|', "\\|").replace(',', "\\,")
}
pub struct StepTreeBuilderBox; pub struct StepTreeBuilderBox;
impl StepTreeBuilderBox { impl StepTreeBuilderBox {
@ -150,10 +275,7 @@ impl StepTreeBuilderBox {
ASTNode::ScopeBox { body, .. } => Self::build_from_block(body), ASTNode::ScopeBox { body, .. } => Self::build_from_block(body),
_ => { _ => {
let (node, features) = Self::build_node(ast, 0, 0); let (node, features) = Self::build_node(ast, 0, 0);
StepTree { build_step_tree(node, features)
root: node,
features,
}
} }
} }
} }
@ -166,10 +288,7 @@ impl StepTreeBuilderBox {
nodes.push(node); nodes.push(node);
features = merge_features(features, node_features); features = merge_features(features, node_features);
} }
StepTree { build_step_tree(StepNode::Block(nodes), features)
root: StepNode::Block(nodes),
features,
}
} }
fn build_node(ast: &ASTNode, if_depth: u32, loop_depth: u32) -> (StepNode, StepTreeFeatures) { fn build_node(ast: &ASTNode, if_depth: u32, loop_depth: u32) -> (StepNode, StepTreeFeatures) {
@ -277,7 +396,15 @@ impl StepTreeBuilderBox {
), ),
ASTNode::Assignment { span, .. } => ( ASTNode::Assignment { span, .. } => (
StepNode::Stmt { StepNode::Stmt {
kind: StepStmtKind::Assign, kind: StepStmtKind::Assign {
target: match ast {
ASTNode::Assignment { target, .. } => match target.as_ref() {
ASTNode::Variable { name, .. } => Some(name.clone()),
_ => None,
},
_ => None,
},
},
span: span.clone(), span: span.clone(),
}, },
StepTreeFeatures::default(), StepTreeFeatures::default(),
@ -315,6 +442,22 @@ impl StepTreeBuilderBox {
} }
} }
fn build_step_tree(root: StepNode, features: StepTreeFeatures) -> StepTree {
let contract = StepTreeContractBox::compute(&root, &features);
let mut kinds = Vec::new();
collect_node_kinds(&root, &mut kinds);
let kinds = kinds.join(",");
let basis = contract.signature_basis_string(&kinds);
let signature = StepTreeSignature::from_basis_string(&basis);
StepTree {
root,
features,
contract,
signature,
}
}
fn merge_features(mut a: StepTreeFeatures, b: StepTreeFeatures) -> StepTreeFeatures { fn merge_features(mut a: StepTreeFeatures, b: StepTreeFeatures) -> StepTreeFeatures {
a.has_if |= b.has_if; a.has_if |= b.has_if;
a.has_loop |= b.has_loop; a.has_loop |= b.has_loop;
@ -326,6 +469,143 @@ fn merge_features(mut a: StepTreeFeatures, b: StepTreeFeatures) -> StepTreeFeatu
a a
} }
struct StepTreeContractBox;
impl StepTreeContractBox {
fn compute(root: &StepNode, features: &StepTreeFeatures) -> StepTreeContract {
let mut contract = StepTreeContract::default();
// Required caps from features (structural only).
if features.has_if {
contract.required_caps.insert(StepCapability::If);
}
if features.max_if_depth > 1 {
contract.required_caps.insert(StepCapability::NestedIf);
}
if features.has_loop {
contract.required_caps.insert(StepCapability::Loop);
}
if features.max_loop_depth > 1 {
contract.required_caps.insert(StepCapability::NestedLoop);
}
if features.has_return {
contract.required_caps.insert(StepCapability::Return);
}
if features.has_break {
contract.required_caps.insert(StepCapability::Break);
}
if features.has_continue {
contract.required_caps.insert(StepCapability::Continue);
}
Self::walk(root, &mut contract);
contract
}
fn walk(node: &StepNode, contract: &mut StepTreeContract) {
match node {
StepNode::Block(nodes) => {
for n in nodes {
Self::walk(n, contract);
}
}
StepNode::If {
cond,
then_branch,
else_branch,
..
} => {
contract.cond_sig.push(cond.to_compact_string());
Self::walk(then_branch, contract);
if let Some(else_branch) = else_branch {
Self::walk(else_branch, contract);
}
}
StepNode::Loop { cond, body, .. } => {
contract.cond_sig.push(cond.to_compact_string());
Self::walk(body, contract);
}
StepNode::Stmt { kind, .. } => {
match kind {
StepStmtKind::LocalDecl { vars } => {
for v in vars {
contract.writes.insert(v.clone());
}
}
StepStmtKind::Assign { target } => {
if let Some(name) = target.as_ref() {
contract.writes.insert(name.clone());
}
}
StepStmtKind::Print => {}
StepStmtKind::Return => {
contract.exits.insert(ExitKind::Return);
}
StepStmtKind::Break => {
contract.exits.insert(ExitKind::Break);
}
StepStmtKind::Continue => {
contract.exits.insert(ExitKind::Continue);
}
StepStmtKind::Other(name) => match *name {
"TryCatch" => {
contract.required_caps.insert(StepCapability::TryCatch);
}
"Throw" => {
contract.required_caps.insert(StepCapability::Throw);
}
"Lambda" => {
contract.required_caps.insert(StepCapability::Lambda);
}
"While" => {
contract.required_caps.insert(StepCapability::While);
}
"ForRange" => {
contract.required_caps.insert(StepCapability::ForRange);
}
"MatchExpr" => {
contract.required_caps.insert(StepCapability::Match);
}
"Arrow" => {
contract.required_caps.insert(StepCapability::Arrow);
}
_ => {}
},
}
}
}
}
}
fn collect_node_kinds(node: &StepNode, out: &mut Vec<String>) {
match node {
StepNode::Block(nodes) => {
out.push("Block".to_string());
for n in nodes {
collect_node_kinds(n, out);
}
}
StepNode::If {
then_branch,
else_branch,
..
} => {
out.push("If".to_string());
collect_node_kinds(then_branch, out);
if let Some(else_branch) = else_branch {
collect_node_kinds(else_branch, out);
}
}
StepNode::Loop { body, .. } => {
out.push("Loop".to_string());
collect_node_kinds(body, out);
}
StepNode::Stmt { kind, .. } => {
out.push(format!("Stmt({})", kind.to_compact_string()));
}
}
}
fn summarize_ast(ast: &ASTNode) -> AstSummary { fn summarize_ast(ast: &ASTNode) -> AstSummary {
match ast { match ast {
ASTNode::Variable { name, .. } => AstSummary::Variable(name.clone()), ASTNode::Variable { name, .. } => AstSummary::Variable(name.clone()),
@ -495,6 +775,19 @@ mod tests {
assert!(tree.features.has_if); assert!(tree.features.has_if);
assert!(!tree.features.has_loop); assert!(!tree.features.has_loop);
assert_eq!(tree.features.max_if_depth, 2); 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;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 { match tree.root {
StepNode::Block(nodes) => { StepNode::Block(nodes) => {
@ -513,4 +806,72 @@ mod tests {
other => panic!("expected root Block, 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));
}
} }