//! Minimal Normalized JoinIR model (Phase 26-H.B). //! //! テスト専用の極小サブセット。Pattern1 の while だけを Structured → Normalized に //! 変換して遊ぶための足場だよ。本線の Structured→MIR 経路には影響しない。 use std::collections::{BTreeMap, HashSet}; use crate::mir::join_ir::{ BinOpKind, CompareOp, ConstValue, JoinContId, JoinFuncId, JoinFunction, JoinInst, JoinIrPhase, JoinModule, MirLikeInst, UnaryOp, }; use crate::mir::ValueId; #[cfg(feature = "normalized_dev")] use std::collections::HashMap; #[cfg(feature = "normalized_dev")] use std::panic::{catch_unwind, AssertUnwindSafe}; #[cfg(feature = "normalized_dev")] pub mod fixtures; #[cfg(feature = "normalized_dev")] pub mod dev_env; #[cfg(feature = "normalized_dev")] pub mod dev_fixtures; #[cfg(feature = "normalized_dev")] pub mod shape_guard; #[cfg(feature = "normalized_dev")] use crate::mir::join_ir::normalized::shape_guard::NormalizedDevShape; /// 環境レイアウト(最小)。 #[derive(Debug, Clone)] pub struct EnvLayout { pub id: u32, pub fields: Vec, } #[derive(Debug, Clone)] pub struct EnvField { pub name: String, pub ty: Option, pub value_id: Option, } /// 正規化済み関数 ID #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct JpFuncId(pub u32); /// 正規化済み関数(Kont 兼用、is_kont で区別)。 #[derive(Debug, Clone)] pub struct JpFunction { pub id: JpFuncId, pub name: String, pub env_layout: Option, pub body: Vec, pub is_kont: bool, } /// 正規化済み命令(最小セット)。 #[derive(Debug, Clone)] pub enum JpInst { Let { dst: ValueId, op: JpOp, args: Vec, }, EnvLoad { dst: ValueId, env: ValueId, field: usize, }, EnvStore { env: ValueId, field: usize, src: ValueId, }, TailCallFn { target: JpFuncId, env: Vec, }, TailCallKont { target: JpFuncId, env: Vec, }, If { cond: ValueId, then_target: JpFuncId, else_target: JpFuncId, env: Vec, }, } /// 演算(Let 用の最小サブセット)。 #[derive(Debug, Clone)] pub enum JpOp { Const(ConstValue), BinOp(BinOpKind), Unary(UnaryOp), Compare(CompareOp), BoxCall { box_name: String, method: String }, /// 三項演算子(cond ? then : else) Select, } /// Normalized JoinIR モジュール(テスト専用)。 #[derive(Debug, Clone)] pub struct NormalizedModule { pub functions: BTreeMap, pub entry: Option, pub env_layouts: Vec, pub phase: JoinIrPhase, /// Structured に戻すためのスナップショット(テスト専用)。 pub structured_backup: Option, } impl NormalizedModule { pub fn to_structured(&self) -> Option { self.structured_backup.clone() } } #[cfg(feature = "normalized_dev")] fn verify_normalized_pattern1(module: &NormalizedModule) -> Result<(), String> { if module.phase != JoinIrPhase::Normalized { return Err("[joinir/normalized-dev] pattern1: phase must be Normalized".to_string()); } // Env field bounds check if let Some(env) = module.env_layouts.get(0) { let field_count = env.fields.len(); for func in module.functions.values() { for inst in &func.body { match inst { JpInst::EnvLoad { field, .. } | JpInst::EnvStore { field, .. } => { if *field >= field_count { return Err(format!( "[joinir/normalized-dev] pattern1: env field out of range: {} (fields={})", field, field_count )); } } _ => {} } } } } for func in module.functions.values() { for inst in &func.body { match inst { JpInst::Let { .. } | JpInst::EnvLoad { .. } | JpInst::EnvStore { .. } | JpInst::TailCallFn { .. } | JpInst::TailCallKont { .. } | JpInst::If { .. } => {} } } // Tail: allow TailCall or If only if let Some(last) = func.body.last() { match last { JpInst::TailCallFn { .. } | JpInst::TailCallKont { .. } | JpInst::If { .. } => {} _ => { return Err(format!( "[joinir/normalized-dev] pattern1: function '{}' does not end with tail call/if", func.name )) } } } } Ok(()) } /// Pattern1 専用: Normalized → Structured への簡易逆変換。 pub fn normalized_pattern1_to_structured(norm: &NormalizedModule) -> JoinModule { assert_eq!( norm.phase, JoinIrPhase::Normalized, "normalized_pattern1_to_structured expects Normalized phase" ); let env_layout = norm .env_layouts .get(0) .expect("normalized_pattern1_to_structured: missing env layout"); let mut module = JoinModule::new(); for (jp_id, jp_fn) in &norm.functions { let params: Vec = jp_fn .env_layout .and_then(|id| norm.env_layouts.iter().find(|layout| layout.id == id)) .unwrap_or(env_layout) .fields .iter() .enumerate() .map(|(idx, f)| f.value_id.unwrap_or(ValueId(idx as u32))) .collect(); let mut func = JoinFunction::new(JoinFuncId(jp_id.0), jp_fn.name.clone(), params); for inst in &jp_fn.body { match inst { JpInst::Let { dst, op, args } => match op { JpOp::Const(v) => func.body.push(JoinInst::Compute(MirLikeInst::Const { dst: *dst, value: v.clone(), })), JpOp::BoxCall { box_name, method } => { func.body.push(JoinInst::Compute(MirLikeInst::BoxCall { dst: Some(*dst), box_name: box_name.clone(), method: method.clone(), args: args.clone(), })) } JpOp::BinOp(op) => func.body.push(JoinInst::Compute(MirLikeInst::BinOp { dst: *dst, op: *op, lhs: args.get(0).copied().unwrap_or(ValueId(0)), rhs: args.get(1).copied().unwrap_or(ValueId(0)), })), JpOp::Unary(op) => func.body.push(JoinInst::Compute(MirLikeInst::UnaryOp { dst: *dst, op: *op, operand: args.get(0).copied().unwrap_or(ValueId(0)), })), JpOp::Select => func.body.push(JoinInst::Compute(MirLikeInst::Select { dst: *dst, cond: args.get(0).copied().unwrap_or(ValueId(0)), then_val: args.get(1).copied().unwrap_or(ValueId(0)), else_val: args.get(2).copied().unwrap_or(ValueId(0)), })), JpOp::Compare(op) => func.body.push(JoinInst::Compute(MirLikeInst::Compare { dst: *dst, op: *op, lhs: args.get(0).copied().unwrap_or(ValueId(0)), rhs: args.get(1).copied().unwrap_or(ValueId(0)), })), }, JpInst::TailCallFn { target, env } => func.body.push(JoinInst::Call { func: JoinFuncId(target.0), args: env.clone(), k_next: None, dst: None, }), JpInst::TailCallKont { target, env } => func.body.push(JoinInst::Jump { cont: JoinContId(target.0), args: env.clone(), cond: None, }), JpInst::If { cond, then_target, else_target, env, } => { // Jump to then_target on cond, else jump to else_target (Pattern1 minimal) func.body.push(JoinInst::Jump { cont: JoinContId(then_target.0), args: env.clone(), cond: Some(*cond), }); func.body.push(JoinInst::Jump { cont: JoinContId(else_target.0), args: env.clone(), cond: None, }); } JpInst::EnvLoad { .. } | JpInst::EnvStore { .. } => { // Not used in Pattern1 minimal; ignore for now } } } module.add_function(func); } module.entry = norm.entry.map(|e| JoinFuncId(e.0)); module.phase = JoinIrPhase::Structured; module } /// Pattern2 専用のミニ変換(最小サブセット: ループ変数1つ + break、acc などの LoopState を 1 個まで+ホスト 1 個まで)。 /// /// 制約: /// - structured.phase は Structured であること /// - main/loop_step/k_exit の 3 関数構成(joinir_min_loop 相当) pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule { assert!( structured.is_structured(), "normalize_pattern2_minimal: expected Structured JoinIR" ); // Minimal guardrail: Pattern2 mini should have main/loop_step/k_exit only, with 1 loop param. let func_count = structured.functions.len(); let loop_func = structured .functions .values() .find(|f| f.name == "loop_step") .or_else(|| structured.functions.get(&JoinFuncId::new(1))) .expect("normalize_pattern2_minimal: loop_step not found"); assert!( func_count == 3, "normalize_pattern2_minimal: expected 3 functions (entry/loop_step/k_exit) but got {}", func_count ); let param_max = { #[allow(unused_mut)] let mut max = 3; #[cfg(feature = "normalized_dev")] { let shapes = shape_guard::supported_shapes(structured); if shapes .iter() .any(|s| matches!(s, NormalizedDevShape::JsonparserAtoiMini)) { max = max.max(8); } if shapes .iter() .any(|s| matches!(s, NormalizedDevShape::JsonparserAtoiReal)) { max = max.max(10); } if shapes .iter() .any(|s| matches!(s, NormalizedDevShape::JsonparserParseNumberReal)) { max = max.max(12); } if shapes .iter() .any(|s| matches!(s, NormalizedDevShape::JsonparserSkipWsReal)) { max = max.max(6); } if shapes.iter().any(|s| { matches!( s, NormalizedDevShape::Pattern4ContinueMinimal | NormalizedDevShape::JsonparserParseArrayContinueSkipWs | NormalizedDevShape::JsonparserParseObjectContinueSkipWs ) }) { max = max.max(6); } if shapes.iter().any(|s| { matches!( s, NormalizedDevShape::Pattern3IfSumMinimal | NormalizedDevShape::Pattern3IfSumMulti | NormalizedDevShape::Pattern3IfSumJson | NormalizedDevShape::SelfhostIfSumP3 | NormalizedDevShape::SelfhostIfSumP3Ext | NormalizedDevShape::SelfhostStmtCountP3 | NormalizedDevShape::SelfhostDetectFormatP3 ) }) { max = max.max(6); } } max }; assert!( (1..=param_max).contains(&loop_func.params.len()), "normalize_pattern2_minimal: expected 1..={} params (loop var + carriers + optional host)", param_max ); let jump_conds = loop_func .body .iter() .filter(|inst| matches!(inst, JoinInst::Jump { cond: Some(_), .. })) .count(); let tail_calls = loop_func .body .iter() .filter(|inst| matches!(inst, JoinInst::Call { k_next: None, .. })) .count(); assert!( jump_conds >= 1 && tail_calls >= 1, "normalize_pattern2_minimal: expected at least one conditional jump and one tail call" ); let mut functions = BTreeMap::new(); let mut env_layouts = Vec::new(); for (fid, func) in &structured.functions { let env_layout_id = if func.params.is_empty() { None } else { let id = env_layouts.len() as u32; env_layouts.push(EnvLayout { id, fields: func .params .iter() .enumerate() .map(|(idx, vid)| EnvField { name: format!("field{}", idx), ty: None, value_id: Some(*vid), }) .collect(), }); Some(id) }; let mut body = Vec::new(); for inst in &func.body { match inst { JoinInst::Compute(MirLikeInst::Const { dst, value }) => body.push(JpInst::Let { dst: *dst, op: JpOp::Const(value.clone()), args: vec![], }), JoinInst::Compute(MirLikeInst::BoxCall { dst, box_name, method, args, }) => { if let Some(dst) = dst { body.push(JpInst::Let { dst: *dst, op: JpOp::BoxCall { box_name: box_name.clone(), method: method.clone(), }, args: args.clone(), }) } } JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs }) => { body.push(JpInst::Let { dst: *dst, op: JpOp::BinOp(*op), args: vec![*lhs, *rhs], }) } JoinInst::Compute(MirLikeInst::UnaryOp { dst, op, operand }) => { body.push(JpInst::Let { dst: *dst, op: JpOp::Unary(*op), args: vec![*operand], }) } JoinInst::Compute(MirLikeInst::Compare { dst, op, lhs, rhs }) => { body.push(JpInst::Let { dst: *dst, op: JpOp::Compare(*op), args: vec![*lhs, *rhs], }) } JoinInst::Compute(MirLikeInst::Select { dst, cond, then_val, else_val, }) => { body.push(JpInst::Let { dst: *dst, op: JpOp::Select, args: vec![*cond, *then_val, *else_val], }) } JoinInst::Jump { cont, args, cond } => { if let Some(cond_val) = cond { body.push(JpInst::If { cond: *cond_val, then_target: JpFuncId(cont.0), else_target: JpFuncId(loop_func.id.0), env: args.clone(), }); } else { body.push(JpInst::TailCallKont { target: JpFuncId(cont.0), env: args.clone(), }); } } JoinInst::Select { dst, cond, then_val, else_val, .. } => { body.push(JpInst::Let { dst: *dst, op: JpOp::Select, args: vec![*cond, *then_val, *else_val], }); } JoinInst::Call { func, args, k_next, .. } => { if k_next.is_none() { body.push(JpInst::TailCallFn { target: JpFuncId(func.0), env: args.clone(), }); } } JoinInst::MethodCall { dst, receiver, method, args, .. } => { let mut call_args = Vec::with_capacity(args.len() + 1); call_args.push(*receiver); call_args.extend(args.iter().copied()); body.push(JpInst::Let { dst: *dst, op: JpOp::BoxCall { box_name: "unknown".to_string(), method: method.clone(), }, args: call_args, }); } _ => { // Ret / other instructions are ignored in this minimal prototype } } } functions.insert( JpFuncId(fid.0), JpFunction { id: JpFuncId(fid.0), name: func.name.clone(), env_layout: env_layout_id, body, is_kont: func.name.starts_with("k_"), }, ); } let norm = NormalizedModule { functions, entry: structured.entry.map(|e| JpFuncId(e.0)), env_layouts, phase: JoinIrPhase::Normalized, structured_backup: Some(structured.clone()), }; #[cfg(feature = "normalized_dev")] { verify_normalized_pattern2(&norm, param_max).expect("normalized Pattern2 verifier"); } norm } #[cfg(feature = "normalized_dev")] fn normalize_pattern2_shape( structured: &JoinModule, target_shape: NormalizedDevShape, ) -> Result { if !structured.is_structured() { return Err("[normalize_p2] Not structured JoinIR".to_string()); } let shapes = shape_guard::supported_shapes(structured); if !shapes.contains(&target_shape) { return Err(format!( "[normalize_p2] shape mismatch: expected {:?}, got {:?}", target_shape, shapes )); } Ok(normalize_pattern2_minimal(structured)) } /// Phase 50: selfhost token-scan P2 を Normalized に載せる(dev-only)。 #[cfg(feature = "normalized_dev")] pub fn normalize_selfhost_token_scan_p2( structured: &JoinModule, ) -> Result { normalize_pattern2_shape(structured, NormalizedDevShape::SelfhostTokenScanP2) } /// Phase 51: selfhost token-scan P2(accum 拡張)を Normalized に載せる(dev-only)。 #[cfg(feature = "normalized_dev")] pub fn normalize_selfhost_token_scan_p2_accum( structured: &JoinModule, ) -> Result { normalize_pattern2_shape(structured, NormalizedDevShape::SelfhostTokenScanP2Accum) } /// Phase 47-A/B: Normalize Pattern3 if-sum shapes to Normalized JoinIR #[cfg(feature = "normalized_dev")] pub fn normalize_pattern3_if_sum_minimal( structured: &JoinModule, ) -> Result { normalize_pattern3_if_sum_shape(structured, NormalizedDevShape::Pattern3IfSumMinimal) } /// Phase 47-B: Normalize Pattern3 if-sum multi-carrier (sum+count) shape. #[cfg(feature = "normalized_dev")] pub fn normalize_pattern3_if_sum_multi_minimal( structured: &JoinModule, ) -> Result { normalize_pattern3_if_sum_shape(structured, NormalizedDevShape::Pattern3IfSumMulti) } /// Phase 47-B: Normalize JsonParser if-sum (mini) shape. #[cfg(feature = "normalized_dev")] pub fn normalize_pattern3_if_sum_json_minimal( structured: &JoinModule, ) -> Result { normalize_pattern3_if_sum_shape(structured, NormalizedDevShape::Pattern3IfSumJson) } /// Phase 50: selfhost if-sum P3 を Normalized に載せる(dev-only)。 #[cfg(feature = "normalized_dev")] pub fn normalize_selfhost_if_sum_p3( structured: &JoinModule, ) -> Result { normalize_pattern3_if_sum_shape(structured, NormalizedDevShape::SelfhostIfSumP3) } /// Phase 51: selfhost if-sum P3(ext 拡張)を Normalized に載せる(dev-only)。 #[cfg(feature = "normalized_dev")] pub fn normalize_selfhost_if_sum_p3_ext( structured: &JoinModule, ) -> Result { normalize_pattern3_if_sum_shape(structured, NormalizedDevShape::SelfhostIfSumP3Ext) } #[cfg(feature = "normalized_dev")] fn normalize_pattern3_if_sum_shape( structured: &JoinModule, target_shape: NormalizedDevShape, ) -> Result { if !structured.is_structured() { return Err("[normalize_p3] Not structured JoinIR".to_string()); } let shapes = shape_guard::supported_shapes(structured); if !shapes.contains(&target_shape) { return Err(format!( "[normalize_p3] shape mismatch: expected {:?}, got {:?}", target_shape, shapes )); } // Phase 47-B: P3 if-sum は既存の P2 ミニ正規化器で十分に表現できる // (Select/If/Compare/BinOp をそのまま JpInst に写す)。 Ok(normalize_pattern2_minimal(structured)) } /// Phase 48-A: Pattern4 (continue) minimal ループの正規化。 /// /// ガード: /// - structured.phase は Structured であること /// - 対象は Pattern4ContinueMinimal のシンプル continue パターン /// /// 実装方針: /// - Phase 48-A minimal: P2 正規化に委譲(P4 は P2 の逆制御フローなので同じインフラ利用可) /// - TODO (Phase 48-B): P4 固有の正規化実装 /// - EnvLayout for i, count carriers /// - HeaderCond → ContinueCheck → Updates → Tail step sequence /// - continue = early TailCallFn (skip Updates) #[cfg(feature = "normalized_dev")] pub fn normalize_pattern4_continue_minimal( structured: &JoinModule, ) -> Result { normalize_pattern4_continue_shape(structured, NormalizedDevShape::Pattern4ContinueMinimal) } /// Phase 48-B: JsonParser _parse_array continue skip_ws を Normalized に載せる(dev-only)。 #[cfg(feature = "normalized_dev")] pub fn normalize_jsonparser_parse_array_continue_skip_ws( structured: &JoinModule, ) -> Result { normalize_pattern4_continue_shape( structured, NormalizedDevShape::JsonparserParseArrayContinueSkipWs, ) } /// Phase 48-B: JsonParser _parse_object continue skip_ws を Normalized に載せる(dev-only)。 #[cfg(feature = "normalized_dev")] pub fn normalize_jsonparser_parse_object_continue_skip_ws( structured: &JoinModule, ) -> Result { normalize_pattern4_continue_shape( structured, NormalizedDevShape::JsonparserParseObjectContinueSkipWs, ) } #[cfg(feature = "normalized_dev")] fn normalize_pattern4_continue_shape( structured: &JoinModule, target_shape: NormalizedDevShape, ) -> Result { if !structured.is_structured() { return Err("[normalize_p4] Not structured JoinIR".to_string()); } // Use shape detection to verify P4 shape let shapes = shape_guard::supported_shapes(structured); if !shapes.contains(&target_shape) { return Err(format!( "[normalize_p4] shape mismatch: expected {:?}, got {:?}", target_shape, shapes )); } // Phase 48-B: reuse Pattern2 minimal normalizer (continue is early tail-call). Ok(normalize_pattern2_minimal(structured)) } /// Pattern2 専用: Normalized → Structured への簡易逆変換。 pub fn normalized_pattern2_to_structured(norm: &NormalizedModule) -> JoinModule { if let Some(backup) = norm.to_structured() { return backup; } let mut module = JoinModule::new(); for (jp_id, jp_fn) in &norm.functions { let params: Vec = jp_fn .env_layout .and_then(|id| norm.env_layouts.iter().find(|layout| layout.id == id)) .map(|layout| { layout .fields .iter() .enumerate() .map(|(idx, f)| f.value_id.unwrap_or(ValueId(idx as u32))) .collect() }) .unwrap_or_default(); let mut func = JoinFunction::new(JoinFuncId(jp_id.0), jp_fn.name.clone(), params); for inst in &jp_fn.body { match inst { JpInst::Let { dst, op, args } => match op { JpOp::Const(v) => func.body.push(JoinInst::Compute(MirLikeInst::Const { dst: *dst, value: v.clone(), })), JpOp::BoxCall { box_name, method } => { func.body.push(JoinInst::Compute(MirLikeInst::BoxCall { dst: Some(*dst), box_name: box_name.clone(), method: method.clone(), args: args.clone(), })) } JpOp::BinOp(op) => func.body.push(JoinInst::Compute(MirLikeInst::BinOp { dst: *dst, op: *op, lhs: args.get(0).copied().unwrap_or(ValueId(0)), rhs: args.get(1).copied().unwrap_or(ValueId(0)), })), JpOp::Unary(op) => func.body.push(JoinInst::Compute(MirLikeInst::UnaryOp { dst: *dst, op: *op, operand: args.get(0).copied().unwrap_or(ValueId(0)), })), JpOp::Select => func .body .push(JoinInst::Compute(MirLikeInst::Select { dst: *dst, cond: args.get(0).copied().unwrap_or(ValueId(0)), then_val: args.get(1).copied().unwrap_or(ValueId(0)), else_val: args.get(2).copied().unwrap_or(ValueId(0)), })), JpOp::Compare(op) => func.body.push(JoinInst::Compute(MirLikeInst::Compare { dst: *dst, op: *op, lhs: args.get(0).copied().unwrap_or(ValueId(0)), rhs: args.get(1).copied().unwrap_or(ValueId(0)), })), }, JpInst::TailCallFn { target, env } => func.body.push(JoinInst::Call { func: JoinFuncId(target.0), args: env.clone(), k_next: None, dst: None, }), JpInst::TailCallKont { target, env } => func.body.push(JoinInst::Jump { cont: JoinContId(target.0), args: env.clone(), cond: None, }), JpInst::If { cond, then_target, else_target, env, } => { func.body.push(JoinInst::Jump { cont: JoinContId(then_target.0), args: env.clone(), cond: Some(*cond), }); func.body.push(JoinInst::Jump { cont: JoinContId(else_target.0), args: env.clone(), cond: None, }); } JpInst::EnvLoad { .. } | JpInst::EnvStore { .. } => { // Not used in minimal pattern2 bridge } } } module.add_function(func); } module.entry = norm.entry.map(|e| JoinFuncId(e.0)); module.phase = JoinIrPhase::Structured; module } #[cfg(feature = "normalized_dev")] fn verify_normalized_pattern2( module: &NormalizedModule, max_env_fields: usize, ) -> Result<(), String> { if module.phase != JoinIrPhase::Normalized { return Err( "[joinir/normalized-dev] pattern2: phase must be Normalized".to_string(), ); } let mut layout_sizes: HashMap = HashMap::new(); for layout in &module.env_layouts { let size = layout.fields.len(); if !(1..=max_env_fields).contains(&size) { return Err(format!( "[joinir/normalized-dev] pattern2: expected 1..={} env fields, got {}", max_env_fields, size )); } layout_sizes.insert(layout.id, size); } for func in module.functions.values() { let expected_env_len = func .env_layout .and_then(|id| layout_sizes.get(&id)) .copied(); for inst in &func.body { match inst { JpInst::Let { .. } | JpInst::EnvLoad { .. } | JpInst::EnvStore { .. } | JpInst::TailCallFn { .. } | JpInst::TailCallKont { .. } | JpInst::If { .. } => {} } match inst { JpInst::TailCallFn { env, .. } | JpInst::TailCallKont { env, .. } | JpInst::If { env, .. } => { if let Some(expected) = expected_env_len { if env.is_empty() { return Err( "[joinir/normalized-dev] pattern2: env must not be empty" .to_string(), ); } let _ = expected; } } _ => {} } } if let Some(last) = func.body.last() { match last { JpInst::TailCallFn { .. } | JpInst::TailCallKont { .. } | JpInst::If { .. } => {} _ => { return Err(format!( "[joinir/normalized-dev] pattern2: function '{}' does not end with tail call/if", func.name )); } } } } Ok(()) } /// Pattern1 専用のミニ変換。 /// /// 制約: /// - structured.phase は Structured であること /// - 対象は Pattern1 のシンプル while(break/continue なし) pub fn normalize_pattern1_minimal(structured: &JoinModule) -> NormalizedModule { assert!( structured.is_structured(), "normalize_pattern1_minimal: expected Structured JoinIR" ); // entry/loop_step/k_exit を前提に、loop_step を拾う let loop_func = structured .functions .values() .find(|f| f.name == "loop_step") .or_else(|| structured.functions.get(&JoinFuncId::new(1))) .expect("normalize_pattern1_minimal: loop_step not found"); // EnvLayout をざっくり作る(フィールド名は field0, field1,... で代用) let env_layout = EnvLayout { id: 0, fields: loop_func .params .iter() .enumerate() .map(|(idx, vid)| EnvField { name: format!("field{}", idx), ty: None, value_id: Some(*vid), }) .collect(), }; // loop_step の Compute を Let に写経(Pattern1 では Compute/Call/Ret のみ想定) let mut extra_konts: HashSet = HashSet::new(); let mut jp_body = Vec::new(); for inst in &loop_func.body { match inst { JoinInst::Compute(MirLikeInst::Const { dst, value }) => jp_body.push(JpInst::Let { dst: *dst, op: JpOp::Const(value.clone()), args: vec![], }), JoinInst::Compute(MirLikeInst::BoxCall { dst, box_name, method, args, }) => { if let Some(dst) = dst { jp_body.push(JpInst::Let { dst: *dst, op: JpOp::BoxCall { box_name: box_name.clone(), method: method.clone(), }, args: args.clone(), }) } } JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs }) => { jp_body.push(JpInst::Let { dst: *dst, op: JpOp::BinOp(*op), args: vec![*lhs, *rhs], }) } JoinInst::Compute(MirLikeInst::UnaryOp { dst, op, operand }) => { jp_body.push(JpInst::Let { dst: *dst, op: JpOp::Unary(*op), args: vec![*operand], }) } JoinInst::Compute(MirLikeInst::Compare { dst, op, lhs, rhs }) => { jp_body.push(JpInst::Let { dst: *dst, op: JpOp::Compare(*op), args: vec![*lhs, *rhs], }) } // Tail recursion / exit は TailCall と If でざっくり表現 JoinInst::Jump { cont, args, cond } => { if let Some(cond_val) = cond { extra_konts.insert(JpFuncId(cont.0)); jp_body.push(JpInst::If { cond: *cond_val, then_target: JpFuncId(cont.0), else_target: JpFuncId(loop_func.id.0), env: args.clone(), }); } else { extra_konts.insert(JpFuncId(cont.0)); jp_body.push(JpInst::TailCallKont { target: JpFuncId(cont.0), env: args.clone(), }); } } JoinInst::Call { func, args, .. } => jp_body.push(JpInst::TailCallFn { target: JpFuncId(func.0), env: args.clone(), }), JoinInst::Ret { value } => { if let Some(v) = value { let kont_id = JpFuncId(loop_func.id.0 + 1); extra_konts.insert(kont_id); jp_body.push(JpInst::TailCallKont { target: kont_id, env: vec![*v], }); } } _ => { // Pattern1 の最小変換なので他は無視(将来拡張) } } } let loop_fn = JpFunction { id: JpFuncId(loop_func.id.0), name: loop_func.name.clone(), env_layout: Some(env_layout.id), body: jp_body, is_kont: false, }; let mut functions = BTreeMap::new(); functions.insert(loop_fn.id, loop_fn); for kont_id in extra_konts { functions.entry(kont_id).or_insert_with(|| JpFunction { id: kont_id, name: format!("kont_{}", kont_id.0), env_layout: Some(env_layout.id), body: Vec::new(), is_kont: true, }); } let norm = NormalizedModule { functions, entry: Some(JpFuncId(loop_func.id.0)), env_layouts: vec![env_layout], phase: JoinIrPhase::Normalized, structured_backup: Some(structured.clone()), }; #[cfg(feature = "normalized_dev")] { verify_normalized_pattern1(&norm).expect("normalized verifier"); } norm } /// Dev helper: Structured → Normalized → Structured roundtrip (Pattern1/2 minis only). #[cfg(feature = "normalized_dev")] pub(crate) fn normalized_dev_roundtrip_structured( module: &JoinModule, ) -> Result { if !module.is_structured() { return Err("[joinir/normalized-dev] expected Structured JoinModule".to_string()); } let shapes = shape_guard::supported_shapes(module); if shapes.is_empty() { return Err("[joinir/normalized-dev] module shape is not supported by normalized_dev".into()); } let debug = dev_env::normalized_dev_logs_enabled() && crate::config::env::joinir_dev_enabled(); for shape in shapes { if debug { eprintln!( "[joinir/normalized-dev/roundtrip] attempting {:?} normalization", shape ); } let attempt = match shape { NormalizedDevShape::Pattern1Mini => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_pattern1_minimal(module); normalized_pattern1_to_structured(&norm) })), NormalizedDevShape::Pattern2Mini | NormalizedDevShape::JsonparserSkipWsMini | NormalizedDevShape::JsonparserSkipWsReal | NormalizedDevShape::JsonparserAtoiMini | NormalizedDevShape::JsonparserAtoiReal | NormalizedDevShape::JsonparserParseNumberReal => catch_unwind(AssertUnwindSafe( || { let norm = normalize_pattern2_minimal(module); normalized_pattern2_to_structured(&norm) }, )), NormalizedDevShape::SelfhostTokenScanP2 => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_selfhost_token_scan_p2(module).expect("selfhost P2 normalization failed"); normalized_pattern2_to_structured(&norm) })), NormalizedDevShape::SelfhostTokenScanP2Accum => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_selfhost_token_scan_p2_accum(module) .expect("selfhost P2 accum normalization failed"); normalized_pattern2_to_structured(&norm) })), // Phase 47-A: P3 minimal (delegates to P2 for now, but uses proper guard) NormalizedDevShape::Pattern3IfSumMinimal => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_pattern3_if_sum_minimal(module) .expect("P3 normalization failed"); normalized_pattern2_to_structured(&norm) })), NormalizedDevShape::Pattern3IfSumMulti => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_pattern3_if_sum_multi_minimal(module) .expect("P3 multi normalization failed"); normalized_pattern2_to_structured(&norm) })), NormalizedDevShape::Pattern3IfSumJson => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_pattern3_if_sum_json_minimal(module) .expect("P3 json normalization failed"); normalized_pattern2_to_structured(&norm) })), NormalizedDevShape::SelfhostIfSumP3 => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_selfhost_if_sum_p3(module) .expect("selfhost P3 normalization failed"); normalized_pattern2_to_structured(&norm) })), NormalizedDevShape::SelfhostIfSumP3Ext => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_selfhost_if_sum_p3_ext(module) .expect("selfhost P3 ext normalization failed"); normalized_pattern2_to_structured(&norm) })), // Phase 53: selfhost P2/P3 practical variations (delegate to existing normalizers) NormalizedDevShape::SelfhostArgsParseP2 => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_pattern2_minimal(module); normalized_pattern2_to_structured(&norm) })), NormalizedDevShape::SelfhostStmtCountP3 => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_selfhost_if_sum_p3_ext(module) .expect("selfhost stmt_count P3 normalization failed"); normalized_pattern2_to_structured(&norm) })), // Phase 54: selfhost P2/P3 shape growth (delegate to existing normalizers) NormalizedDevShape::SelfhostVerifySchemaP2 => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_pattern2_minimal(module); normalized_pattern2_to_structured(&norm) })), NormalizedDevShape::SelfhostDetectFormatP3 => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_selfhost_if_sum_p3_ext(module) .expect("selfhost detect_format P3 normalization failed"); normalized_pattern2_to_structured(&norm) })), // Phase 48-A: P4 minimal (delegates to P2 for now, but uses proper guard) NormalizedDevShape::Pattern4ContinueMinimal => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_pattern4_continue_minimal(module) .expect("P4 normalization failed"); normalized_pattern2_to_structured(&norm) })), NormalizedDevShape::JsonparserParseArrayContinueSkipWs => { catch_unwind(AssertUnwindSafe(|| { let norm = normalize_jsonparser_parse_array_continue_skip_ws(module) .expect("P4 array normalization failed"); normalized_pattern2_to_structured(&norm) })) } NormalizedDevShape::JsonparserParseObjectContinueSkipWs => { catch_unwind(AssertUnwindSafe(|| { let norm = normalize_jsonparser_parse_object_continue_skip_ws(module) .expect("P4 object normalization failed"); normalized_pattern2_to_structured(&norm) })) } // Phase 89: Continue + Early Return pattern (dev-only, delegates to P2 for now) NormalizedDevShape::PatternContinueReturnMinimal => catch_unwind(AssertUnwindSafe(|| { let norm = normalize_pattern2_minimal(module); normalized_pattern2_to_structured(&norm) })), }; match attempt { Ok(structured) => { if debug { eprintln!( "[joinir/normalized-dev/roundtrip] {:?} normalization succeeded (functions={})", shape, structured.functions.len() ); } return Ok(structured); } Err(_) => { if debug { eprintln!( "[joinir/normalized-dev/roundtrip] {:?} normalization failed (unsupported)", shape ); } } } } Err("[joinir/normalized-dev] all normalization attempts failed".into()) } #[cfg(test)] mod tests { use super::*; fn build_structured_pattern1() -> JoinModule { let mut module = JoinModule::new(); let mut loop_fn = JoinFunction::new( JoinFuncId::new(1), "loop_step".to_string(), vec![ValueId(10)], ); loop_fn.body.push(JoinInst::Compute(MirLikeInst::Const { dst: ValueId(11), value: ConstValue::Integer(0), })); loop_fn.body.push(JoinInst::Compute(MirLikeInst::BinOp { dst: ValueId(12), op: BinOpKind::Add, lhs: ValueId(10), rhs: ValueId(11), })); loop_fn.body.push(JoinInst::Jump { cont: JoinContId(2), args: vec![ValueId(12)], cond: Some(ValueId(12)), // dummy }); let mut k_exit = JoinFunction::new(JoinFuncId::new(2), "k_exit".to_string(), vec![ValueId(12)]); k_exit.body.push(JoinInst::Ret { value: Some(ValueId(12)), }); module.entry = Some(loop_fn.id); module.add_function(loop_fn); module.add_function(k_exit); module } #[test] fn normalized_pattern1_minimal_smoke() { let structured = build_structured_pattern1(); let normalized = normalize_pattern1_minimal(&structured); assert_eq!(normalized.phase, JoinIrPhase::Normalized); assert!(!normalized.env_layouts.is_empty()); assert!(!normalized.functions.is_empty()); #[cfg(feature = "normalized_dev")] { verify_normalized_pattern1(&normalized).expect("verifier should pass"); } let restored = normalized.to_structured().expect("backup"); assert!(restored.is_structured()); assert_eq!(restored.functions.len(), structured.functions.len()); } #[test] fn normalized_pattern1_roundtrip_structured_equivalent() { let structured = build_structured_pattern1(); let normalized = normalize_pattern1_minimal(&structured); let reconstructed = normalized_pattern1_to_structured(&normalized); assert!(reconstructed.is_structured()); assert_eq!(reconstructed.functions.len(), structured.functions.len()); } }