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:
nyash-codex
2025-12-18 06:30:55 +09:00
parent 7eeeb588e4
commit 4c98313b58

View File

@ -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-callPHI 禁止)
/// - 対応ノード: 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)");
}
}