refactor(control_tree): Phase 125 P2-P4 introduce EnvLayout (writes+inputs) + Return(Variable) from inputs
Phase 125 P2: EnvLayout introduction - Add EnvLayout struct (writes + inputs) - from_contract() creates layout from StepTreeContract + available_inputs - SSOT: inputs = reads ∩ available_inputs (deterministic order) - No AST inference (don't capture from AST, use provided available_inputs) Phase 125 P2: Builder env mapping - lower_if_only_to_normalized: Use EnvLayout to create env map - writes: Generate ValueId (as before) - inputs: Reference ValueId from available_inputs (placeholder for P3) - Function params: writes only (inputs come from outer scope) Phase 125 P4: Return(Variable) resolution extended - lower_return_value: Check env (writes + inputs) - If found: return it (Phase 124 for writes, Phase 125 for inputs) - If not found: Fail-Fast with structured error + hint - Hint: "Pass as param, add to pinned capture, or define before if" - Phase 125 errors return Ok(None) (out of scope, graceful degradation) Unit tests: - test_return_variable_from_env: PASS (Phase 124 regression) - test_return_variable_out_of_scope: PASS (updated for Phase 125) - test_return_variable_from_inputs_stub: PASS (P3 not wired yet) - All 1160 lib tests PASS (no regression) Note: - P3 (available_inputs wiring) not implemented yet - available_inputs is empty BTreeMap (stub) - EnvLayout.inputs will be empty until P3 is wired - Structured error tags: [phase125/return/var_not_in_env] Ref: docs/development/current/main/phases/phase-125/README.md
This commit is contained in:
@ -19,6 +19,58 @@ use crate::mir::join_ir::JoinModule;
|
||||
|
||||
use super::contracts::{check_if_only, CapabilityCheckResult};
|
||||
|
||||
/// Phase 125: Normalized env layout (writes + inputs)
|
||||
///
|
||||
/// ## Design
|
||||
///
|
||||
/// - writes: Variables written in the function (generate ValueId)
|
||||
/// - inputs: Variables read from outer scope (reference ValueId, don't generate)
|
||||
///
|
||||
/// ## SSOT
|
||||
///
|
||||
/// - writes: From StepTreeContract.writes
|
||||
/// - inputs: From (StepTreeContract.reads ∩ available_inputs)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnvLayout {
|
||||
/// Variables written (generate ValueId for these)
|
||||
pub writes: Vec<String>,
|
||||
/// Variables read from outer scope (reference ValueId from available_inputs)
|
||||
pub inputs: Vec<String>,
|
||||
}
|
||||
|
||||
impl EnvLayout {
|
||||
/// Create env layout from contract and available_inputs (Phase 125)
|
||||
///
|
||||
/// ## Contract
|
||||
///
|
||||
/// - writes: All variables from contract.writes (deterministic order)
|
||||
/// - inputs: Variables in contract.reads that are available from outer scope
|
||||
///
|
||||
/// ## SSOT
|
||||
///
|
||||
/// - available_inputs source: function params + CapturedEnv (pinned/captured)
|
||||
/// - No AST inference (don't capture from AST)
|
||||
pub fn from_contract(
|
||||
contract: &crate::mir::control_tree::step_tree_contract_box::StepTreeContract,
|
||||
available_inputs: &std::collections::BTreeMap<String, crate::mir::ValueId>,
|
||||
) -> Self {
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
// Phase 125 P2: writes from contract
|
||||
let writes: Vec<String> = contract.writes.iter().cloned().collect();
|
||||
|
||||
// Phase 125 P2: inputs = reads ∩ available_inputs (deterministic order)
|
||||
let inputs: Vec<String> = contract
|
||||
.reads
|
||||
.iter()
|
||||
.filter(|name| available_inputs.contains_key(*name))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
EnvLayout { writes, inputs }
|
||||
}
|
||||
}
|
||||
|
||||
/// Box-First: StepTree → Normalized shadow lowering
|
||||
pub struct StepTreeNormalizedShadowLowererBox;
|
||||
|
||||
@ -60,25 +112,26 @@ impl StepTreeNormalizedShadowLowererBox {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lower if-only StepTree to Normalized JoinModule (Phase 122-123)
|
||||
/// Lower if-only StepTree to Normalized JoinModule (Phase 122-125)
|
||||
///
|
||||
/// ## Design
|
||||
///
|
||||
/// - env レイアウト: `writes` に含まれる変数だけ(決定的順序)
|
||||
/// - env レイアウト: writes + inputs(決定的順序)
|
||||
/// - merge 形式: `join_k(env)` への tail-call(PHI 禁止)
|
||||
/// - 対応ノード: If/Return/Assign(最小セット)
|
||||
///
|
||||
/// ## Phase 123 Node Support
|
||||
/// ## Phase 123-125 Node Support
|
||||
///
|
||||
/// - Return(Integer literal): `Const + Ret(Some(vid))`
|
||||
/// - Return(Variable): Out of scope (Phase 124)
|
||||
/// - Return(Variable from writes): Phase 124
|
||||
/// - Return(Variable from inputs): Phase 125 (reads-only)
|
||||
/// - Return(void): `Ret(None)`
|
||||
/// - If(minimal compare): Compare with Integer literal only
|
||||
///
|
||||
/// ## Returns
|
||||
///
|
||||
/// - `Ok(Some((module, meta)))`: Normalized JoinModule生成成功
|
||||
/// - `Ok(None)`: Out of scope for Phase 123 (unsupported patterns)
|
||||
/// - `Ok(None)`: Out of scope for Phase 123-125 (unsupported patterns)
|
||||
/// - `Err(msg)`: 生成できるはずなのに失敗(内部エラー)
|
||||
fn lower_if_only_to_normalized(
|
||||
step_tree: &StepTree,
|
||||
@ -87,13 +140,18 @@ impl StepTreeNormalizedShadowLowererBox {
|
||||
use crate::mir::ValueId;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// Phase 122: env レイアウト
|
||||
let env_fields: Vec<String> = step_tree.contract.writes.iter().cloned().collect();
|
||||
// Phase 125 P2: available_inputs (P3 で配線、今は空)
|
||||
let available_inputs: BTreeMap<String, ValueId> = BTreeMap::new();
|
||||
|
||||
// Phase 125 P2: EnvLayout 生成
|
||||
let env_layout = EnvLayout::from_contract(&step_tree.contract, &available_inputs);
|
||||
|
||||
let main_func_id = JoinFuncId::new(0);
|
||||
|
||||
// env フィールドに対応する引数ValueIdを生成
|
||||
// Phase 125 P2: writes 用の ValueId 生成
|
||||
let mut next_value_id = 1;
|
||||
let env_params: Vec<ValueId> = env_fields
|
||||
let writes_params: Vec<ValueId> = env_layout
|
||||
.writes
|
||||
.iter()
|
||||
.map(|_| {
|
||||
let vid = ValueId(next_value_id);
|
||||
@ -102,13 +160,36 @@ impl StepTreeNormalizedShadowLowererBox {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Phase 124: env マッピング(変数名 → ValueId)
|
||||
let env: BTreeMap<String, ValueId> = env_fields
|
||||
// Phase 125 P2: inputs 用の ValueId 生成(今は空だが、P3 で available_inputs から参照)
|
||||
let inputs_params: Vec<ValueId> = env_layout
|
||||
.inputs
|
||||
.iter()
|
||||
.cloned()
|
||||
.zip(env_params.iter().cloned())
|
||||
.map(|name| {
|
||||
// Phase 125 P3: available_inputs から取得
|
||||
// 今は空なので到達しないはずだが、念のため placeholder
|
||||
available_inputs
|
||||
.get(name)
|
||||
.copied()
|
||||
.unwrap_or_else(|| {
|
||||
// Should not reach here (inputs は available_inputs にある前提)
|
||||
ValueId(0) // placeholder
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Phase 125 P2: env マッピング(writes + inputs)
|
||||
let mut env: BTreeMap<String, ValueId> = BTreeMap::new();
|
||||
for (name, vid) in env_layout.writes.iter().zip(writes_params.iter()) {
|
||||
env.insert(name.clone(), *vid);
|
||||
}
|
||||
for (name, vid) in env_layout.inputs.iter().zip(inputs_params.iter()) {
|
||||
env.insert(name.clone(), *vid);
|
||||
}
|
||||
|
||||
// Phase 125 P2: 関数パラメータは writes のみ(inputs は外側から来る前提)
|
||||
// TODO P3: inputs も params に含める必要があるか検討
|
||||
let env_params = writes_params;
|
||||
|
||||
// main 関数生成
|
||||
let mut main_func = JoinFunction::new(
|
||||
main_func_id,
|
||||
@ -116,8 +197,8 @@ impl StepTreeNormalizedShadowLowererBox {
|
||||
env_params.clone(),
|
||||
);
|
||||
|
||||
// Phase 123-124: Return node lowering
|
||||
// If Phase 123-124 patterns are not supported, return Ok(None)
|
||||
// Phase 123-125: Return node lowering
|
||||
// If Phase 123-125 patterns are not supported, return Ok(None)
|
||||
match Self::lower_return_from_tree(
|
||||
&step_tree.root,
|
||||
&mut main_func.body,
|
||||
@ -128,8 +209,12 @@ impl StepTreeNormalizedShadowLowererBox {
|
||||
Ok(()) => {
|
||||
// Success - continue
|
||||
}
|
||||
Err(msg) if msg.starts_with("[phase123/") || msg.starts_with("[phase124/") => {
|
||||
// Phase 123-124 limitation - out of scope
|
||||
Err(msg)
|
||||
if msg.starts_with("[phase123/")
|
||||
|| msg.starts_with("[phase124/")
|
||||
|| msg.starts_with("[phase125/") =>
|
||||
{
|
||||
// Phase 123-125 limitation - out of scope
|
||||
return Ok(None);
|
||||
}
|
||||
Err(msg) => {
|
||||
@ -404,13 +489,16 @@ impl StepTreeNormalizedShadowLowererBox {
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 123-124 P1-P2-P3: Lower return value
|
||||
/// Phase 123-125 P1-P2-P3-P4: Lower return value
|
||||
///
|
||||
/// ## Support
|
||||
///
|
||||
/// - Integer literal: Generate Const + Ret(Some(vid))
|
||||
/// - None: Ret(None)
|
||||
/// - Variable: Phase 124 - lookup from env (dev-only, Fail-Fast if not in env)
|
||||
/// - Variable (Phase 124-125): lookup from env (writes + inputs)
|
||||
/// - Phase 124: writes (written variables)
|
||||
/// - Phase 125: inputs (reads-only from outer scope)
|
||||
/// - Fail-Fast if not in env (structured error with hint)
|
||||
/// - Other: Fail-Fast (out of scope)
|
||||
fn lower_return_value(
|
||||
value_ast: &Option<crate::mir::control_tree::step_tree::AstNodeHandle>,
|
||||
@ -458,20 +546,41 @@ impl StepTreeNormalizedShadowLowererBox {
|
||||
}
|
||||
},
|
||||
ASTNode::Variable { name, .. } => {
|
||||
// Phase 124 P3: Variable return support (dev-only)
|
||||
// Check if variable is in env (writes-derived)
|
||||
// Phase 124-125 P3-P4: Variable return support (dev-only)
|
||||
// Check if variable is in env (writes + inputs)
|
||||
if let Some(&vid) = env.get(name) {
|
||||
// Variable found in env - return it
|
||||
// Phase 125 P4: Variable found in env (writes or inputs) - return it
|
||||
body.push(JoinInst::Ret { value: Some(vid) });
|
||||
Ok(())
|
||||
} else {
|
||||
// Variable not in env - out of scope for Phase 124
|
||||
// Phase 124 only supports Return(Variable) when variable is in env (writes)
|
||||
// Variables in reads-only or undefined are not supported
|
||||
Err(format!(
|
||||
"[phase124/return/var_not_in_env] Variable '{}' not in env (writes). Phase 124 only supports return of written variables. Hint: Assign variable before return",
|
||||
name
|
||||
))
|
||||
// Phase 125 P4: Variable not in env - Fail-Fast with hint
|
||||
// Check if variable is in reads (potential input)
|
||||
let in_reads = contract.reads.contains(name);
|
||||
let in_writes = contract.writes.contains(name);
|
||||
|
||||
let hint = if in_reads && !in_writes {
|
||||
// Variable is read but not available as input
|
||||
format!(
|
||||
"Variable '{}' is read but not available from outer scope. \
|
||||
Hint: Pass as function parameter, add to pinned capture, or define before if",
|
||||
name
|
||||
)
|
||||
} else if !in_reads && !in_writes {
|
||||
// Variable is neither read nor written (undefined)
|
||||
format!(
|
||||
"Variable '{}' is undefined. \
|
||||
Hint: Define variable before return or check spelling",
|
||||
name
|
||||
)
|
||||
} else {
|
||||
// In writes but not in env (internal error)
|
||||
format!(
|
||||
"Variable '{}' in writes but not in env (internal error)",
|
||||
name
|
||||
)
|
||||
};
|
||||
|
||||
Err(format!("[phase125/return/var_not_in_env] {}", hint))
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@ -684,16 +793,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_return_variable_out_of_scope() {
|
||||
// Phase 125 P4: Test Return(Variable) when variable is not in env (neither writes nor inputs)
|
||||
// Expected: Ok(None) because variable is not available
|
||||
use crate::ast::{ASTNode, Span};
|
||||
use crate::mir::control_tree::step_tree::AstNodeHandle;
|
||||
|
||||
// Create StepTree with "return x" (variable)
|
||||
// Create StepTree with "return x" (variable not in env)
|
||||
let return_ast = Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: Span::unknown(),
|
||||
});
|
||||
|
||||
let mut tree = make_if_only_tree();
|
||||
|
||||
// Phase 125: Add x to reads (to simulate variable reference)
|
||||
tree.contract.reads.insert("x".to_string());
|
||||
|
||||
tree.root = StepNode::Stmt {
|
||||
kind: StepStmtKind::Return {
|
||||
value_ast: Some(AstNodeHandle(return_ast)),
|
||||
@ -701,10 +816,12 @@ mod tests {
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
// Lower to JoinModule - should return Ok(None) (out of scope for Phase 123)
|
||||
// Phase 125 P4: Lower to JoinModule - should return Ok(None)
|
||||
// because x is in reads but not in available_inputs (not in env)
|
||||
let result = StepTreeNormalizedShadowLowererBox::try_lower_if_only(&tree);
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().is_none(), "Should return None for Phase 123 unsupported patterns");
|
||||
let option = result.unwrap();
|
||||
assert!(option.is_none(), "Should return None when variable is in reads but not available as input");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -834,4 +951,41 @@ mod tests {
|
||||
panic!("Expected Ret instruction");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_return_variable_from_inputs_stub() {
|
||||
// Phase 125 P2-P4: Test Return(Variable) when variable is in inputs (reads-only)
|
||||
// Note: P3 (available_inputs wiring) is not implemented yet, so this test
|
||||
// demonstrates the structure but won't actually provide inputs
|
||||
use crate::ast::{ASTNode, Span};
|
||||
use crate::mir::control_tree::step_tree::AstNodeHandle;
|
||||
|
||||
// Create StepTree with "return x" where x is read-only input
|
||||
let mut tree = make_if_only_tree();
|
||||
|
||||
// Phase 125 P2: Add x to reads (simulating outer scope variable read)
|
||||
tree.contract.reads.insert("x".to_string());
|
||||
|
||||
// Create return x node
|
||||
tree.root = StepNode::Stmt {
|
||||
kind: StepStmtKind::Return {
|
||||
value_ast: Some(AstNodeHandle(Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: Span::unknown(),
|
||||
}))),
|
||||
},
|
||||
span: Span::unknown(),
|
||||
};
|
||||
|
||||
// Phase 125 P2-P4: Lower to JoinModule
|
||||
// Because available_inputs is empty (P3 not wired), x won't be in inputs
|
||||
// So this should return Ok(None) (out of scope)
|
||||
let result = StepTreeNormalizedShadowLowererBox::try_lower_if_only(&tree);
|
||||
assert!(result.is_ok());
|
||||
let option = result.unwrap();
|
||||
|
||||
// Phase 125 P2: EnvLayout.inputs will be empty (no available_inputs)
|
||||
// So x is not in env, and we get Ok(None)
|
||||
assert!(option.is_none(), "Should return None when x is in reads but not in available_inputs (P3 not wired yet)");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user