feat(control_tree): Phase 130 assign var/add in if-only normalized

This commit is contained in:
nyash-codex
2025-12-18 09:13:08 +09:00
parent 05fccce09a
commit 15c2eda1cf
3 changed files with 155 additions and 7 deletions

View File

@ -27,12 +27,15 @@ impl EnvLayout {
) -> Self { ) -> Self {
// Phase 125 P2: writes from contract // Phase 125 P2: writes from contract
let writes: Vec<String> = contract.writes.iter().cloned().collect(); let writes: Vec<String> = contract.writes.iter().cloned().collect();
let writes_set: std::collections::BTreeSet<&String> = contract.writes.iter().collect();
// Phase 125 P2: inputs = reads ∩ available_inputs (deterministic order) // Phase 125 P2: inputs = (reads ∩ available_inputs) \ writes
// inputs are read-only by definition; if a variable is written, it must not be treated as an input.
let inputs: Vec<String> = contract let inputs: Vec<String> = contract
.reads .reads
.iter() .iter()
.filter(|name| available_inputs.contains_key(*name)) .filter(|name| available_inputs.contains_key(*name))
.filter(|name| !writes_set.contains(name))
.cloned() .cloned()
.collect(); .collect();

View File

@ -161,7 +161,7 @@ impl LegacyLowerer {
} }
} }
/// Phase 128: Lower assign statement (int literal only) /// Phase 128-130: Lower assign statement (int literal, variable, add)
pub fn lower_assign_stmt( pub fn lower_assign_stmt(
target: &Option<String>, target: &Option<String>,
value_ast: &Option<crate::mir::control_tree::step_tree::AstNodeHandle>, value_ast: &Option<crate::mir::control_tree::step_tree::AstNodeHandle>,
@ -169,8 +169,9 @@ impl LegacyLowerer {
next_value_id: &mut u32, next_value_id: &mut u32,
env: &mut BTreeMap<String, ValueId>, env: &mut BTreeMap<String, ValueId>,
) -> Result<(), String> { ) -> Result<(), String> {
use crate::ast::{ASTNode, LiteralValue}; use crate::ast::{ASTNode, BinaryOperator, LiteralValue};
use crate::mir::join_ir::{ConstValue, JoinInst, MirLikeInst}; use crate::mir::join_ir::{BinOpKind, ConstValue, JoinInst, MirLikeInst};
use crate::mir::join_ir::lowering::error_tags;
// Check target // Check target
let target_name = target let target_name = target
@ -182,8 +183,9 @@ impl LegacyLowerer {
"[phase128/assign/value] Assign value AST is missing".to_string() "[phase128/assign/value] Assign value AST is missing".to_string()
})?; })?;
// Parse value - Phase 128: int literal only // Parse value - Phase 128: int literal, Phase 130 P1: variable, Phase 130 P2: add
match value_ast.0.as_ref() { match value_ast.0.as_ref() {
// Phase 128: Integer literal
ASTNode::Literal { ASTNode::Literal {
value: LiteralValue::Integer(i), value: LiteralValue::Integer(i),
.. ..
@ -202,10 +204,116 @@ impl LegacyLowerer {
Ok(()) Ok(())
} }
// Phase 130 P1: Variable (x = y)
ASTNode::Variable { name, .. } => {
// RHS must resolve from env (writes or inputs)
let src_vid = env.get(name).copied().ok_or_else(|| {
error_tags::freeze_with_hint(
"phase130/assign/var/rhs_missing",
&format!("RHS variable '{name}' not found in env"),
"ensure the variable is defined before assignment (in writes or inputs)",
)
})?;
// Phase 130 P1: Variable assignment in Normalized IR
// In the continuation-passing style, we just update the env map.
// The actual SSA phi/merge happens at function boundaries via env args.
// No instruction emission needed here.
env.insert(target_name.clone(), src_vid);
Ok(())
}
// Phase 130 P2: BinaryOp (x = x + <int literal> only)
ASTNode::BinaryOp {
operator,
left,
right,
..
} => {
// Phase 130 P2 contract: only Add with specific shape
if !matches!(operator, BinaryOperator::Add) {
return Err(error_tags::freeze_with_hint(
"phase130/assign/add/unsupported_op",
&format!("Phase 130 only supports Add operator, got {:?}", operator),
"use x = x + <literal> pattern or wait for future phases",
));
}
// Left must be variable
let lhs_var = match &**left {
ASTNode::Variable { name, .. } => name.clone(),
_ => {
return Err(error_tags::freeze_with_hint(
"phase130/assign/add/lhs_not_var",
"Phase 130 Add: LHS must be a variable",
"use pattern x = x + <literal>",
));
}
};
// Right must be integer literal
let rhs_int = match &**right {
ASTNode::Literal {
value: LiteralValue::Integer(i),
..
} => *i,
_ => {
return Err(error_tags::freeze_with_hint(
"phase130/assign/add/rhs_not_int_literal",
"Phase 130 Add: RHS must be integer literal",
"use pattern x = x + <literal>",
));
}
};
// Phase 130 P2 contract: dst must equal lhs (x = x + 3, not y = x + 3)
if target_name != &lhs_var {
return Err(error_tags::freeze_with_hint(
"phase130/assign/add/dst_neq_lhs",
&format!(
"Phase 130 Add: dst '{}' must equal lhs '{}' (x = x + 3 pattern)",
target_name, lhs_var
),
"use pattern x = x + <literal> where dst == lhs",
));
}
// Load lhs from env
let lhs_vid = env.get(&lhs_var).copied().ok_or_else(|| {
error_tags::freeze_with_hint(
"phase130/assign/add/lhs_missing",
&format!("Add LHS variable '{}' not found in env", lhs_var),
"ensure the variable is defined before the add operation",
)
})?;
// Create constant for RHS
let rhs_vid = ValueId(*next_value_id);
*next_value_id += 1;
body.push(JoinInst::Compute(MirLikeInst::Const {
dst: rhs_vid,
value: ConstValue::Integer(rhs_int),
}));
// Generate BinOp Add
let result_vid = ValueId(*next_value_id);
*next_value_id += 1;
body.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: result_vid,
op: BinOpKind::Add,
lhs: lhs_vid,
rhs: rhs_vid,
}));
// Update env
env.insert(target_name.clone(), result_vid);
Ok(())
}
_ => { _ => {
// Phase 128 limitation: only int literal supported // Out of scope
Err(format!( Err(format!(
"[phase128/assign/unsupported] Phase128 supports int literal only Hint: Assign RHS must be an integer literal (e.g., x = 42)" "[phase130/assign/unsupported] Phase 130 supports: int literal, variable, or x = x + <int literal> Hint: Use supported pattern or wait for future phases"
)) ))
} }
} }

View File

@ -5,6 +5,8 @@
use crate::mir::join_ir::{JoinFuncId, JoinInst, JoinModule}; use crate::mir::join_ir::{JoinFuncId, JoinInst, JoinModule};
use crate::mir::join_ir::lowering::error_tags; use crate::mir::join_ir::lowering::error_tags;
use super::env_layout::EnvLayout;
use std::collections::BTreeMap;
/// Verify Normalized JoinModule structure emitted by the shadow lowerer. /// Verify Normalized JoinModule structure emitted by the shadow lowerer.
/// ///
@ -224,3 +226,38 @@ pub fn verify_normalized_structure(
)), )),
} }
} }
/// Phase 130 P3: Verify env map keyset stays within env layout
///
/// ## Contract
/// - The env map must not introduce variables outside the env layout (`writes + inputs`)
///
/// ## Implementation Note
/// This is a structural check only. It does not (yet) prove that `inputs` are never
/// reassigned; it only ensures the env map does not grow beyond the declared layout.
pub fn verify_env_writes_discipline(
env_map: &BTreeMap<String, crate::mir::ValueId>,
env_layout: &EnvLayout,
context: &str,
) -> Result<(), String> {
let expected_fields: std::collections::BTreeSet<&String> = env_layout
.writes
.iter()
.chain(env_layout.inputs.iter())
.collect();
for var_name in env_map.keys() {
if !expected_fields.contains(var_name) {
return Err(error_tags::freeze_with_hint(
"phase130/verifier/env_unexpected_var",
&format!(
"{}: unexpected variable '{}' in env map (not in writes or inputs)",
context, var_name
),
"ensure env updates only reference variables from the env layout (writes + inputs)",
));
}
}
Ok(())
}