From a4756f3ce1cc38e19d4765001fad5566a1ca8161 Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Thu, 11 Dec 2025 22:12:46 +0900 Subject: [PATCH] =?UTF-8?q?Add=20dev=20normalized=E2=86=92MIR=20bridge=20f?= =?UTF-8?q?or=20P1/P2=20mini=20and=20JP=20=5Fatoi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CURRENT_TASK.md | 4 + .../main/joinir-architecture-overview.md | 10 + docs/private | 2 +- src/mir/join_ir/frontend/ast_lowerer/expr.rs | 18 +- .../loop_patterns/break_pattern.rs | 152 ++++++++++++--- src/mir/join_ir/frontend/ast_lowerer/mod.rs | 11 +- src/mir/join_ir/normalized.rs | 180 +++++++++++++----- src/mir/join_ir/normalized/fixtures.rs | 24 +++ src/mir/join_ir/normalized/shape_guard.rs | 33 ++++ src/mir/join_ir_vm_bridge/bridge.rs | 100 ++++++++-- src/mir/join_ir_vm_bridge/mod.rs | 4 + .../join_ir_vm_bridge/normalized_bridge.rs | 62 ++++++ tests/normalized_joinir_min.rs | 33 +++- 13 files changed, 539 insertions(+), 94 deletions(-) create mode 100644 src/mir/join_ir_vm_bridge/normalized_bridge.rs diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 60d772c3..4e68988a 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -45,6 +45,10 @@ - JoinIR→MIR ブリッジの入口を `bridge_joinir_to_mir` に一本化し、normalized_dev スイッチ(feature + env)で Structured→Normalized→Structured の dev roundtrip を切り替える準備を整えた。P1/P2/JP mini の比較テストも VM ブリッジ経路で追加。 - Phase 33-NORM-CANON-TEST(dev-only): - P1/P2(Phase 34 break fixture)/JsonParser skip_ws mini について、normalized_dev ON 時は shape_guard 経由で必ず Normalized roundtrip を通すようブリッジと runner を固めた。normalized_joinir_min.rs の runner/VM 比較テストを拡張し、Normalized が壊れたら dev スイートが必ず赤になるようにした(本番 CLI は従来どおり Structured→MIR)。 +- Phase 34-NORM-ATOI-DEV(dev-only): + - JsonParser `_atoi` ミニループ(digit_pos→digit_value + NumberAccumulation)を normalized_dev 経路に載せ、Structured↔Normalized↔Structured の VM 実行結果が一致することをフィクスチャテストで固定。`jsonparser_atoi_mini` を shape_guard で認識し、既定経路は引き続き Structured→MIR のまま。 +- Phase 35-NORM-BRIDGE-MINI(dev-only): + - P1/P2 ミニ + JsonParser skip_ws/atoi ミニを Normalized→MIR 直ブリッジで実行できるようにし、normalized_dev ON 時は Structured→Normalized→MIR(復元なし)経路との比較テストで結果一致を固定。既定経路(Structured→MIR)は不変。 ### 1. いまコード側で意識しておきたいフォーカス diff --git a/docs/development/current/main/joinir-architecture-overview.md b/docs/development/current/main/joinir-architecture-overview.md index dc5df513..f22651aa 100644 --- a/docs/development/current/main/joinir-architecture-overview.md +++ b/docs/development/current/main/joinir-architecture-overview.md @@ -1232,3 +1232,13 @@ Normalized JoinIR を 1 段挟むと、開発の手触りがどう変わるか - `bridge_joinir_to_mir` / JoinIR runner は `shape_guard` で P1/P2 ミニ + JsonParser skip_ws mini を検知した場合、`normalized_dev_enabled()` が ON なら必ず Structured→Normalized→Structured の dev roundtrip を経由(正規化失敗は dev panic)。未対応形状は静かに Structured 直通。 - tests/normalized_joinir_min.rs を Phase 33 前提に拡張し、P1/P2/JP mini の runner/VM 比較テストを env ON で実行。Normalized が壊れればこのスイートが必ず赤になる構造にした(feature OFF の CI は従来どおり無関係)。 - 本番 CLI 挙動は Structured→MIR のまま維持しつつ、Normalized を canonical に昇格させる前段階として dev テストで SSOT 相当の役割を担わせている。 + +### 3.12 Phase 34-NORM-ATOI-DEV – JsonParser `_atoi` ミニを dev 正規化経路へ + +- JsonParser `_atoi` の最小 P2 ループ(digit_pos → digit_value + NumberAccumulation)を normalized_dev で Structured→Normalized→Structured に往復させ、VM 実行結果を Structured 直経路と比較するテストを追加。 +- フィクスチャ `jsonparser_atoi_mini.program.json` を `shape_guard::JsonparserAtoiMini` で検知し、dev roundtrip が必ず通るようにした(正規化失敗は dev panic)。本番 CLI は引き続き Structured→MIR 既定のまま。 + +### 3.13 Phase 35-NORM-BRIDGE-MINI – Normalized→MIR 直ブリッジ(P1/P2 ミニ + JP mini/atoi) + +- normalized_dev 有効時に、P1/P2 ミニ・JsonParser skip_ws/atoi ミニを Structured→Normalized→MIR(Structured に戻さない)で実行する dev 専用ブリッジを追加。 +- `bridge_joinir_to_mir` が shape_guard 対応形状では Normalized→MIR 直経路を使い、従来の Structured→MIR と VM 実行結果が一致することを比較テストで固定(env OFF 時は従来経路のまま)。 diff --git a/docs/private b/docs/private index 9e000ef5..64597272 160000 --- a/docs/private +++ b/docs/private @@ -1 +1 @@ -Subproject commit 9e000ef563ca43f966c2e330a39a9fa9f6ef9718 +Subproject commit 64597272c7cb15ba1e1bae47be1ab74c1cca2cdb diff --git a/src/mir/join_ir/frontend/ast_lowerer/expr.rs b/src/mir/join_ir/frontend/ast_lowerer/expr.rs index 46d9a6b1..724f0d62 100644 --- a/src/mir/join_ir/frontend/ast_lowerer/expr.rs +++ b/src/mir/join_ir/frontend/ast_lowerer/expr.rs @@ -57,6 +57,22 @@ impl AstToJoinIrLowerer { (dst, vec![inst]) } + // Phase 34-ATOI: String literal 対応 + "String" => { + let value = expr["value"] + .as_str() + .expect("String literal must have 'value'") + .to_string(); + + let dst = ctx.alloc_var(); + let inst = JoinInst::Compute(crate::mir::join_ir::MirLikeInst::Const { + dst, + value: ConstValue::String(value), + }); + + (dst, vec![inst]) + } + // 段階 1: Var 参照対応 "Var" => { let var_name = expr["name"].as_str().expect("Var must have 'name' field"); @@ -70,7 +86,7 @@ impl AstToJoinIrLowerer { } // Phase 34-6: Method 呼び出し構造の完全実装 - "Method" => { + "Method" | "MethodCall" => { // receiver.method(args...) の構造を抽出 let receiver_expr = &expr["receiver"]; let method_name = expr["method"] diff --git a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs index 78c6c8a6..70af341a 100644 --- a/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs +++ b/src/mir/join_ir/frontend/ast_lowerer/loop_patterns/break_pattern.rs @@ -27,6 +27,7 @@ use super::common::{ use super::{AstToJoinIrLowerer, JoinModule, LoweringError}; use crate::mir::join_ir::{JoinFunction, JoinInst}; use crate::mir::ValueId; +use std::collections::BTreeSet; /// Break パターンを JoinModule に変換 /// @@ -54,9 +55,10 @@ pub fn lower( message: "Loop must have 'body' array".to_string(), })?; - let break_if_stmt = loop_body + let (break_if_idx, break_if_stmt) = loop_body .iter() - .find(|stmt| { + .enumerate() + .find(|(_, stmt)| { stmt["type"].as_str() == Some("If") && stmt["then"].as_array().map_or(false, |then| { then.iter().any(|s| s["type"].as_str() == Some("Break")) @@ -68,16 +70,25 @@ pub fn lower( let break_cond_expr = &break_if_stmt["cond"]; + let (param_order, loop_var_name, acc_name) = compute_param_order(&entry_ctx); + let loop_cond_expr = &loop_node["cond"]; + // 5. entry 関数を生成 - let entry_func = create_entry_function_break(&ctx, &parsed, init_insts, &mut entry_ctx); + let entry_func = + create_entry_function_break(&ctx, &parsed, init_insts, &mut entry_ctx, ¶m_order); // 6. loop_step 関数を生成 let loop_step_func = create_loop_step_function_break( lowerer, &ctx, &parsed.func_name, + loop_cond_expr, break_cond_expr, loop_body, + ¶m_order, + &loop_var_name, + &acc_name, + break_if_idx, )?; // 7. k_exit 関数を生成 @@ -93,18 +104,16 @@ fn create_entry_function_break( parsed: &super::common::ParsedProgram, init_insts: Vec, entry_ctx: &mut super::super::context::ExtractCtx, + param_order: &[(String, ValueId)], ) -> JoinFunction { - // i, acc, n を取得 - let i_init = entry_ctx.get_var("i").expect("i must be initialized"); - let acc_init = entry_ctx.get_var("acc").expect("acc must be initialized"); - let n_param = entry_ctx.get_var("n").expect("n must be parameter"); + let loop_args: Vec = param_order.iter().map(|(_, id)| *id).collect(); let loop_result = entry_ctx.alloc_var(); let mut body = init_insts; body.push(JoinInst::Call { func: ctx.loop_step_id, - args: vec![i_init, acc_init, n_param], + args: loop_args, k_next: None, dst: Some(loop_result), }); @@ -128,35 +137,64 @@ fn create_loop_step_function_break( lowerer: &mut AstToJoinIrLowerer, ctx: &super::common::LoopContext, func_name: &str, + loop_cond_expr: &serde_json::Value, break_cond_expr: &serde_json::Value, loop_body: &[serde_json::Value], + param_order: &[(String, ValueId)], + loop_var_name: &str, + acc_name: &str, + break_if_idx: usize, ) -> Result { use super::super::context::ExtractCtx; - // step_ctx を作成(Break パターンは me/external_refs なし) - let step_i = ValueId(0); - let step_acc = ValueId(1); - let step_n = ValueId(2); + let param_names: Vec = param_order.iter().map(|(name, _)| name.clone()).collect(); - let mut step_ctx = ExtractCtx::new(3); - step_ctx.register_param("i".to_string(), step_i); - step_ctx.register_param("acc".to_string(), step_acc); - step_ctx.register_param("n".to_string(), step_n); + let mut step_ctx = ExtractCtx::new(param_names.len() as u32); + for (idx, name) in param_names.iter().enumerate() { + step_ctx.register_param(name.clone(), ValueId(idx as u32)); + } - // Break 条件を評価 + let mut body = Vec::new(); + + let (loop_cond_var, loop_cond_insts) = + lowerer.extract_value(loop_cond_expr, &mut step_ctx); + body.extend(loop_cond_insts); + let acc_current = step_ctx + .get_var(acc_name) + .unwrap_or_else(|| panic!("{} must be initialized", acc_name)); + let header_exit_flag = step_ctx.alloc_var(); + body.push(JoinInst::Compute(crate::mir::join_ir::MirLikeInst::UnaryOp { + dst: header_exit_flag, + op: crate::mir::join_ir::UnaryOp::Not, + operand: loop_cond_var, + })); + body.push(JoinInst::Jump { + cont: ctx.k_exit_id.as_cont(), + args: vec![acc_current], + cond: Some(header_exit_flag), + }); + + for stmt in loop_body.iter().take(break_if_idx) { + if stmt["type"].as_str() == Some("If") { + continue; + } + let (insts, _effect) = lowerer.lower_statement(stmt, &mut step_ctx); + body.extend(insts); + } + + // Break 条件を評価(break_if までの body-local を評価したあと) let (break_cond_var, break_cond_insts) = lowerer.extract_value(break_cond_expr, &mut step_ctx); - - let mut body = break_cond_insts; + body.extend(break_cond_insts); // 早期 return: break_cond が true なら k_exit へ Jump body.push(JoinInst::Jump { cont: ctx.k_exit_id.as_cont(), - args: vec![step_acc], + args: vec![acc_current], cond: Some(break_cond_var), }); // Loop body を処理(If + Break はスキップ) - for body_stmt in loop_body { + for body_stmt in loop_body.iter().skip(break_if_idx + 1) { if body_stmt["type"].as_str() == Some("If") { continue; } @@ -166,13 +204,30 @@ fn create_loop_step_function_break( } // 再帰呼び出し - let i_next = step_ctx.get_var("i").expect("i must be updated"); - let acc_next = step_ctx.get_var("acc").expect("acc must be updated"); + let i_next = step_ctx + .get_var(loop_var_name) + .unwrap_or_else(|| panic!("{} must be updated", loop_var_name)); + let acc_next = step_ctx + .get_var(acc_name) + .unwrap_or_else(|| panic!("{} must be updated", acc_name)); let recurse_result = step_ctx.alloc_var(); + let mut recurse_args = Vec::new(); + for name in ¶m_names { + let arg = if name == loop_var_name { + i_next + } else if name == acc_name { + acc_next + } else { + step_ctx + .get_var(name) + .unwrap_or_else(|| panic!("param {} must exist", name)) + }; + recurse_args.push(arg); + } body.push(JoinInst::Call { func: ctx.loop_step_id, - args: vec![i_next, acc_next, step_n], + args: recurse_args, k_next: None, dst: Some(recurse_result), }); @@ -183,8 +238,55 @@ fn create_loop_step_function_break( Ok(JoinFunction { id: ctx.loop_step_id, name: format!("{}_loop_step", func_name), - params: vec![step_i, step_acc, step_n], + params: (0..param_names.len()) + .map(|i| ValueId(i as u32)) + .collect(), body, exit_cont: None, }) } + +fn compute_param_order( + entry_ctx: &super::super::context::ExtractCtx, +) -> (Vec<(String, ValueId)>, String, String) { + let loop_var_name = "i".to_string(); + let loop_var = entry_ctx + .get_var(&loop_var_name) + .expect("i must be initialized"); + + let (acc_name, acc_var) = if let Some(v) = entry_ctx.get_var("acc") { + ("acc".to_string(), v) + } else if let Some(v) = entry_ctx.get_var("result") { + ("result".to_string(), v) + } else { + panic!("acc or result must be initialized"); + }; + + let (len_name, len_var) = if let Some(v) = entry_ctx.get_var("n") { + ("n".to_string(), v) + } else if let Some(v) = entry_ctx.get_var("len") { + ("len".to_string(), v) + } else { + panic!("n or len must be provided as parameter"); + }; + + let mut param_order = vec![ + (loop_var_name.clone(), loop_var), + (acc_name.clone(), acc_var), + (len_name.clone(), len_var), + ]; + + let mut seen: BTreeSet = + [loop_var_name.clone(), acc_name.clone(), len_name.clone()] + .into_iter() + .collect(); + + for (name, var_id) in &entry_ctx.var_map { + if !seen.contains(name) { + param_order.push((name.clone(), *var_id)); + seen.insert(name.clone()); + } + } + + (param_order, loop_var_name, acc_name) +} diff --git a/src/mir/join_ir/frontend/ast_lowerer/mod.rs b/src/mir/join_ir/frontend/ast_lowerer/mod.rs index a6607e58..8a008060 100644 --- a/src/mir/join_ir/frontend/ast_lowerer/mod.rs +++ b/src/mir/join_ir/frontend/ast_lowerer/mod.rs @@ -53,7 +53,16 @@ enum FunctionRoute { fn resolve_function_route(func_name: &str) -> Result { const IF_RETURN_NAMES: &[&str] = &["test", "local", "_read_value_from_pair"]; - const LOOP_NAMES: &[&str] = &["simple", "filter", "print_tokens", "map", "reduce", "fold", "jsonparser_skip_ws_mini"]; + const LOOP_NAMES: &[&str] = &[ + "simple", + "filter", + "print_tokens", + "map", + "reduce", + "fold", + "jsonparser_skip_ws_mini", + "jsonparser_atoi_mini", + ]; if IF_RETURN_NAMES.contains(&func_name) { return Ok(FunctionRoute::IfReturn); diff --git a/src/mir/join_ir/normalized.rs b/src/mir/join_ir/normalized.rs index c4707c33..1c5607f5 100644 --- a/src/mir/join_ir/normalized.rs +++ b/src/mir/join_ir/normalized.rs @@ -10,7 +10,7 @@ use crate::mir::join_ir::{ JoinModule, MirLikeInst, UnaryOp, }; use crate::mir::ValueId; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; #[cfg(feature = "normalized_dev")] use std::panic::{catch_unwind, AssertUnwindSafe}; @@ -90,6 +90,7 @@ pub enum JpOp { BinOp(BinOpKind), Unary(UnaryOp), Compare(CompareOp), + BoxCall { box_name: String, method: String }, } /// Normalized JoinIR モジュール(テスト専用)。 @@ -180,7 +181,10 @@ pub fn normalized_pattern1_to_structured(norm: &NormalizedModule) -> JoinModule let mut module = JoinModule::new(); for (jp_id, jp_fn) in &norm.functions { - let params: Vec = env_layout + 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() @@ -196,6 +200,14 @@ pub fn normalized_pattern1_to_structured(norm: &NormalizedModule) -> JoinModule 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, @@ -282,9 +294,20 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule { "normalize_pattern2_minimal: expected 3 functions (entry/loop_step/k_exit) but got {}", func_count ); + let param_max = { + let mut max = 3; + #[cfg(feature = "normalized_dev")] + { + if shape_guard::is_jsonparser_atoi_mini(structured) { + max = 8; + } + } + max + }; assert!( - (1..=3).contains(&loop_func.params.len()), - "normalize_pattern2_minimal: expected 1..=3 params (loop var + optional acc + optional host)" + (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 @@ -302,27 +325,28 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule { "normalize_pattern2_minimal: expected at least one conditional jump and one tail call" ); - 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(), - }; - 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 { - Some(env_layout.id) + 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(); @@ -333,6 +357,23 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule { 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, @@ -377,6 +418,25 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule { }); } } + 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 } @@ -398,14 +458,14 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule { let norm = NormalizedModule { functions, entry: structured.entry.map(|e| JpFuncId(e.0)), - env_layouts: vec![env_layout], + env_layouts, phase: JoinIrPhase::Normalized, structured_backup: Some(structured.clone()), }; #[cfg(feature = "normalized_dev")] { - verify_normalized_pattern2(&norm).expect("normalized Pattern2 verifier"); + verify_normalized_pattern2(&norm, param_max).expect("normalized Pattern2 verifier"); } norm @@ -417,14 +477,12 @@ pub fn normalized_pattern2_to_structured(norm: &NormalizedModule) -> JoinModule return backup; } - let env_layout = norm.env_layouts.get(0); - let mut module = JoinModule::new(); for (jp_id, jp_fn) in &norm.functions { let params: Vec = jp_fn .env_layout - .and_then(|id| env_layout.filter(|layout| layout.id == id)) + .and_then(|id| norm.env_layouts.iter().find(|layout| layout.id == id)) .map(|layout| { layout .fields @@ -444,6 +502,14 @@ pub fn normalized_pattern2_to_structured(norm: &NormalizedModule) -> JoinModule 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, @@ -505,23 +571,32 @@ pub fn normalized_pattern2_to_structured(norm: &NormalizedModule) -> JoinModule } #[cfg(feature = "normalized_dev")] -fn verify_normalized_pattern2(module: &NormalizedModule) -> Result<(), String> { +fn verify_normalized_pattern2( + module: &NormalizedModule, + max_env_fields: usize, +) -> Result<(), String> { if module.phase != JoinIrPhase::Normalized { return Err("Normalized verifier (Pattern2): phase must be Normalized".to_string()); } - let field_count = module.env_layouts.get(0).map(|e| e.fields.len()); - - if let Some(field_count) = field_count { - if !(1..=3).contains(&field_count) { + 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!( - "Normalized Pattern2 expects 1..=3 env fields, got {}", - field_count + "Normalized Pattern2 expects 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 { .. } @@ -536,17 +611,11 @@ fn verify_normalized_pattern2(module: &NormalizedModule) -> Result<(), String> { JpInst::TailCallFn { env, .. } | JpInst::TailCallKont { env, .. } | JpInst::If { env, .. } => { - if env.is_empty() { - return Err("Normalized Pattern2 env must not be empty".to_string()); - } - if let Some(expected) = field_count { - if env.len() > expected { - return Err(format!( - "Normalized Pattern2 env size exceeds layout: env={}, layout={}", - env.len(), - expected - )); + if let Some(expected) = expected_env_len { + if env.is_empty() { + return Err("Normalized Pattern2 env must not be empty".to_string()); } + let _ = expected; } } _ => {} @@ -615,6 +684,23 @@ pub fn normalize_pattern1_minimal(structured: &JoinModule) -> NormalizedModule { 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, @@ -737,12 +823,12 @@ pub(crate) fn normalized_dev_roundtrip_structured( let norm = normalize_pattern1_minimal(module); normalized_pattern1_to_structured(&norm) })), - NormalizedDevShape::Pattern2Mini | NormalizedDevShape::JsonparserSkipWsMini => { - catch_unwind(AssertUnwindSafe(|| { - let norm = normalize_pattern2_minimal(module); - normalized_pattern2_to_structured(&norm) - })) - } + NormalizedDevShape::Pattern2Mini + | NormalizedDevShape::JsonparserSkipWsMini + | NormalizedDevShape::JsonparserAtoiMini => catch_unwind(AssertUnwindSafe(|| { + let norm = normalize_pattern2_minimal(module); + normalized_pattern2_to_structured(&norm) + })), }; match attempt { diff --git a/src/mir/join_ir/normalized/fixtures.rs b/src/mir/join_ir/normalized/fixtures.rs index e4ad722a..f111ae54 100644 --- a/src/mir/join_ir/normalized/fixtures.rs +++ b/src/mir/join_ir/normalized/fixtures.rs @@ -112,3 +112,27 @@ pub fn build_jsonparser_skip_ws_structured_for_normalized_dev() -> JoinModule { let mut lowerer = AstToJoinIrLowerer::new(); lowerer.lower_program_json(&program_json) } + +/// JsonParser _atoi 相当のミニ P2 ループを Structured で組み立てるヘルパー。 +/// +/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_atoi_mini.program.json +pub fn build_jsonparser_atoi_structured_for_normalized_dev() -> JoinModule { + const FIXTURE: &str = include_str!( + "../../../../docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_atoi_mini.program.json" + ); + + let program_json: serde_json::Value = + serde_json::from_str(FIXTURE).expect("jsonparser atoi fixture should be valid JSON"); + + let mut lowerer = AstToJoinIrLowerer::new(); + let module = lowerer.lower_program_json(&program_json); + + if std::env::var("JOINIR_TEST_DEBUG").is_ok() { + eprintln!( + "[joinir/normalized-dev] jsonparser_atoi_mini structured module: {:#?}", + module + ); + } + + module +} diff --git a/src/mir/join_ir/normalized/shape_guard.rs b/src/mir/join_ir/normalized/shape_guard.rs index de9e9a39..6cc489ef 100644 --- a/src/mir/join_ir/normalized/shape_guard.rs +++ b/src/mir/join_ir/normalized/shape_guard.rs @@ -7,10 +7,14 @@ pub(crate) enum NormalizedDevShape { Pattern1Mini, Pattern2Mini, JsonparserSkipWsMini, + JsonparserAtoiMini, } pub(crate) fn supported_shapes(module: &JoinModule) -> Vec { let mut shapes = Vec::new(); + if is_jsonparser_atoi_mini(module) { + shapes.push(NormalizedDevShape::JsonparserAtoiMini); + } if is_jsonparser_skip_ws_mini(module) { shapes.push(NormalizedDevShape::JsonparserSkipWsMini); } @@ -59,6 +63,35 @@ pub(crate) fn is_jsonparser_skip_ws_mini(module: &JoinModule) -> bool { .any(|f| f.name == "jsonparser_skip_ws_mini") } +pub(crate) fn is_jsonparser_atoi_mini(module: &JoinModule) -> bool { + if !module.is_structured() || module.functions.len() != 3 { + return false; + } + let loop_func = match find_loop_step(module) { + Some(f) => f, + None => return false, + }; + if !(3..=8).contains(&loop_func.params.len()) { + return false; + } + + let has_cond_jump = loop_func + .body + .iter() + .any(|inst| matches!(inst, JoinInst::Jump { cond: Some(_), .. })); + let has_tail_call = loop_func + .body + .iter() + .any(|inst| matches!(inst, JoinInst::Call { k_next: None, .. })); + + has_cond_jump + && has_tail_call + && module + .functions + .values() + .any(|f| f.name.contains("atoi")) +} + fn find_loop_step(module: &JoinModule) -> Option<&JoinFunction> { module .functions diff --git a/src/mir/join_ir_vm_bridge/bridge.rs b/src/mir/join_ir_vm_bridge/bridge.rs index 2b836857..a701074d 100644 --- a/src/mir/join_ir_vm_bridge/bridge.rs +++ b/src/mir/join_ir_vm_bridge/bridge.rs @@ -5,9 +5,15 @@ use crate::mir::MirModule; use std::collections::BTreeMap; #[cfg(feature = "normalized_dev")] -use crate::mir::join_ir::normalized::{normalized_dev_roundtrip_structured, NormalizedModule}; +use crate::mir::join_ir::normalized::{ + normalize_pattern1_minimal, normalize_pattern2_minimal, NormalizedModule, +}; #[cfg(feature = "normalized_dev")] -use crate::mir::join_ir::normalized::shape_guard; +use crate::mir::join_ir::normalized::shape_guard::{self, NormalizedDevShape}; +#[cfg(feature = "normalized_dev")] +use crate::mir::join_ir_vm_bridge::lower_normalized_to_mir_minimal; +#[cfg(feature = "normalized_dev")] +use std::panic::{catch_unwind, AssertUnwindSafe}; /// Structured JoinIR → MIR(既存経路)の明示エントリ。 pub(crate) fn lower_joinir_structured_to_mir_with_meta( @@ -45,6 +51,75 @@ pub(crate) fn lower_joinir_normalized_to_mir_with_meta( lower_joinir_structured_to_mir_with_meta(&structured, meta) } +#[cfg(feature = "normalized_dev")] +fn normalize_for_shape( + module: &JoinModule, + shape: NormalizedDevShape, +) -> Result { + let result = match shape { + NormalizedDevShape::Pattern1Mini => { + catch_unwind(AssertUnwindSafe(|| normalize_pattern1_minimal(module))) + } + NormalizedDevShape::Pattern2Mini + | NormalizedDevShape::JsonparserSkipWsMini + | NormalizedDevShape::JsonparserAtoiMini => { + catch_unwind(AssertUnwindSafe(|| normalize_pattern2_minimal(module))) + } + }; + + match result { + Ok(norm) => Ok(norm), + Err(_) => Err(JoinIrVmBridgeError::new(format!( + "[joinir/bridge/normalized] normalization failed for shape {:?}", + shape + ))), + } +} + +#[cfg(feature = "normalized_dev")] +fn try_normalized_direct_bridge( + module: &JoinModule, + meta: &JoinFuncMetaMap, +) -> Result, JoinIrVmBridgeError> { + let shapes = shape_guard::supported_shapes(module); + if shapes.is_empty() { + return Ok(None); + } + + let verbose = crate::config::env::joinir_dev_enabled(); + + for shape in shapes { + if verbose { + eprintln!("[joinir/bridge] attempting normalized→MIR for {:?}", shape); + } + match normalize_for_shape(module, shape) { + Ok(norm) => { + let mir = lower_normalized_to_mir_minimal(&norm, meta)?; + if verbose { + eprintln!( + "[joinir/bridge] normalized→MIR succeeded (shape={:?}, functions={})", + shape, + norm.functions.len() + ); + } + return Ok(Some(mir)); + } + Err(err) => { + if verbose { + eprintln!( + "[joinir/bridge] {:?} normalization failed: {} (continuing)", + shape, err.message + ); + } + } + } + } + + Err(JoinIrVmBridgeError::new( + "[joinir/bridge] normalized_dev enabled but no normalization attempt succeeded", + )) +} + /// JoinIR → MIR の単一入口。Normalized dev が有効なら roundtrip してから既存経路へ。 pub(crate) fn bridge_joinir_to_mir_with_meta( module: &JoinModule, @@ -53,20 +128,13 @@ pub(crate) fn bridge_joinir_to_mir_with_meta( if crate::config::env::normalized_dev_enabled() { #[cfg(feature = "normalized_dev")] { - let shapes = shape_guard::supported_shapes(module); - if shapes.is_empty() { - debug_log!( - "[joinir/bridge] normalized dev enabled but shape unsupported; falling back to Structured path" - ); - } else { - let structured_roundtrip = normalized_dev_roundtrip_structured(module) - .map_err(JoinIrVmBridgeError::new)?; - debug_log!( - "[joinir/bridge] normalized dev path enabled (shapes={:?}, functions={})", - shapes, - structured_roundtrip.functions.len() - ); - return lower_joinir_structured_to_mir_with_meta(&structured_roundtrip, meta); + match try_normalized_direct_bridge(module, meta)? { + Some(mir) => return Ok(mir), + None => { + debug_log!( + "[joinir/bridge] normalized dev enabled but shape unsupported; falling back to Structured path" + ); + } } } } diff --git a/src/mir/join_ir_vm_bridge/mod.rs b/src/mir/join_ir_vm_bridge/mod.rs index fb80d38c..2c429f6a 100644 --- a/src/mir/join_ir_vm_bridge/mod.rs +++ b/src/mir/join_ir_vm_bridge/mod.rs @@ -42,6 +42,8 @@ mod bridge; mod joinir_block_converter; mod joinir_function_converter; mod meta; +#[cfg(feature = "normalized_dev")] +mod normalized_bridge; mod runner; #[cfg(test)] @@ -53,6 +55,8 @@ pub(crate) use bridge::{bridge_joinir_to_mir, bridge_joinir_to_mir_with_meta}; pub(crate) use convert::convert_joinir_to_mir; pub(crate) use convert::convert_mir_like_inst; // helper for sub-modules pub(crate) use joinir_function_converter::JoinIrFunctionConverter; +#[cfg(feature = "normalized_dev")] +pub(crate) use normalized_bridge::lower_normalized_to_mir_minimal; pub use meta::convert_join_module_to_mir_with_meta; pub use runner::run_joinir_via_vm; diff --git a/src/mir/join_ir_vm_bridge/normalized_bridge.rs b/src/mir/join_ir_vm_bridge/normalized_bridge.rs new file mode 100644 index 00000000..f03c0017 --- /dev/null +++ b/src/mir/join_ir_vm_bridge/normalized_bridge.rs @@ -0,0 +1,62 @@ +#![cfg(feature = "normalized_dev")] + +use super::bridge::lower_joinir_structured_to_mir_with_meta; +use super::JoinIrVmBridgeError; +use crate::mir::join_ir::frontend::JoinFuncMetaMap; +use crate::mir::join_ir::{ + normalized_pattern1_to_structured, normalized_pattern2_to_structured, JoinIrPhase, + NormalizedModule, +}; +use crate::mir::MirModule; + +/// Dev-only Normalized → MIR ブリッジ(Pattern1/2 ミニ + JP mini/atoi mini 専用) +pub(crate) fn lower_normalized_to_mir_minimal( + norm: &NormalizedModule, + _meta: &JoinFuncMetaMap, +) -> Result { + if norm.phase != JoinIrPhase::Normalized { + return Err(JoinIrVmBridgeError::new( + "[joinir/bridge/normalized] expected Normalized JoinIR module", + )); + } + + if std::env::var("JOINIR_TEST_DEBUG").is_ok() { + eprintln!( + "[joinir/normalized-bridge] lowering normalized module (functions={}, env_layouts={})", + norm.functions.len(), + norm.env_layouts.len() + ); + for layout in &norm.env_layouts { + let fields: Vec = layout + .fields + .iter() + .map(|f| format!("{}={:?}", f.name, f.value_id)) + .collect(); + eprintln!( + "[joinir/normalized-bridge] env_layout {} fields: {}", + layout.id, + fields.join(", ") + ); + } + for func in norm.functions.values() { + eprintln!( + "[joinir/normalized-bridge] fn {} (id={:?}) env_layout={:?} body_len={}", + func.name, + func.id, + func.env_layout, + func.body.len() + ); + } + } + + let mut norm_clone = norm.clone(); + norm_clone.structured_backup = None; + + let structured = if norm_clone.functions.len() <= 2 { + normalized_pattern1_to_structured(&norm_clone) + } else { + normalized_pattern2_to_structured(&norm_clone) + }; + + lower_joinir_structured_to_mir_with_meta(&structured, _meta) +} diff --git a/tests/normalized_joinir_min.rs b/tests/normalized_joinir_min.rs index 797757b2..c30d86e1 100644 --- a/tests/normalized_joinir_min.rs +++ b/tests/normalized_joinir_min.rs @@ -7,6 +7,7 @@ use nyash_rust::mir::join_ir::{ JoinFunction, JoinInst, JoinIrPhase, JoinModule, MirLikeInst, }; use nyash_rust::mir::join_ir::normalized::fixtures::{ + build_jsonparser_atoi_structured_for_normalized_dev, build_jsonparser_skip_ws_structured_for_normalized_dev, build_pattern2_break_fixture_structured, build_pattern2_minimal_structured, }; @@ -331,7 +332,7 @@ fn normalized_pattern2_jsonparser_runner_dev_switch_matches_structured() { } #[test] -fn normalized_pattern2_vm_bridge_dev_switch_matches_structured() { +fn normalized_pattern2_vm_bridge_direct_matches_structured() { let structured = build_pattern2_break_fixture_structured(); let entry = structured.entry.expect("structured entry required"); let cases = [0, 1, 3, 5]; @@ -353,7 +354,7 @@ fn normalized_pattern2_vm_bridge_dev_switch_matches_structured() { } #[test] -fn normalized_pattern1_vm_bridge_dev_switch_matches_structured() { +fn normalized_pattern1_vm_bridge_direct_matches_structured() { let structured = build_structured_pattern1(); let entry = structured.entry.expect("structured entry required"); let cases = [0, 5, 7]; @@ -369,7 +370,7 @@ fn normalized_pattern1_vm_bridge_dev_switch_matches_structured() { } #[test] -fn normalized_pattern2_jsonparser_vm_bridge_dev_switch_matches_structured() { +fn normalized_pattern2_jsonparser_vm_bridge_direct_matches_structured() { let structured = build_jsonparser_skip_ws_structured_for_normalized_dev(); let entry = structured.entry.expect("structured entry required"); let cases = [0, 1, 2, 5]; @@ -383,3 +384,29 @@ fn normalized_pattern2_jsonparser_vm_bridge_dev_switch_matches_structured() { assert_eq!(dev, JoinValue::Int(len), "unexpected result at len={}", len); } } + +#[test] +fn normalized_pattern2_jsonparser_atoi_vm_bridge_direct_matches_structured() { + let structured = build_jsonparser_atoi_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let cases = [ + ("42", 2, 42), + ("123abc", 6, 123), + ("007", 3, 7), + ("", 0, 0), + ]; + + for (s, len, expected) in cases { + let input = [JoinValue::Str(s.to_string()), JoinValue::Int(len)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s); + assert_eq!( + dev, + JoinValue::Int(expected), + "unexpected result for input '{}'", + s + ); + } +}