Add dev normalized→MIR bridge for P1/P2 mini and JP _atoi

This commit is contained in:
nyash-codex
2025-12-11 22:12:46 +09:00
parent af6f95cd4b
commit a4756f3ce1
13 changed files with 539 additions and 94 deletions

View File

@ -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-TESTdev-only:
- P1/P2Phase 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-DEVdev-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-MINIdev-only:
- P1/P2 ミニ + JsonParser skip_ws/atoi ミニを Normalized→MIR 直ブリッジで実行できるようにし、normalized_dev ON 時は Structured→Normalized→MIR復元なし経路との比較テストで結果一致を固定。既定経路Structured→MIRは不変。
### 1. いまコード側で意識しておきたいフォーカス

View File

@ -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→MIRStructured に戻さない)で実行する dev 専用ブリッジを追加。
- `bridge_joinir_to_mir` が shape_guard 対応形状では Normalized→MIR 直経路を使い、従来の Structured→MIR と VM 実行結果が一致することを比較テストで固定env OFF 時は従来経路のまま)。

View File

@ -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"]

View File

@ -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, &param_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,
&param_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<JoinInst>,
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<ValueId> = 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<JoinFunction, LoweringError> {
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<String> = 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 &param_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<String> =
[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)
}

View File

@ -53,7 +53,16 @@ enum FunctionRoute {
fn resolve_function_route(func_name: &str) -> Result<FunctionRoute, String> {
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);

View File

@ -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<ValueId> = env_layout
let params: Vec<ValueId> = 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<ValueId> = 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<u32, usize> = 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 {

View File

@ -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
}

View File

@ -7,10 +7,14 @@ pub(crate) enum NormalizedDevShape {
Pattern1Mini,
Pattern2Mini,
JsonparserSkipWsMini,
JsonparserAtoiMini,
}
pub(crate) fn supported_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
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

View File

@ -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<NormalizedModule, JoinIrVmBridgeError> {
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<Option<MirModule>, 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"
);
}
}
}
}

View File

@ -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;

View File

@ -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<MirModule, JoinIrVmBridgeError> {
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<String> = 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)
}

View File

@ -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
);
}
}