Files
hakorune/src/mir/join_ir/normalized.rs
nyash-codex d4b9ae3ba5 feat(joinir): Phase 46 - P2-Mid canonical Normalized promotion
Promote P2-Mid patterns (_atoi real, _parse_number real) to canonical
Normalized→MIR(direct) route, completing P2 line transition.

Canonical set expansion (Phase 41 → Phase 46):
- P2-Core: Pattern2Mini, skip_ws mini/real, atoi mini
- P2-Mid: atoi real, parse_number real (NEW)

All JsonParser P2 loops (_skip_whitespace, _atoi, _parse_number) now
canonical Normalized - Structured→MIR is legacy/comparison-only.

Key changes:
- shape_guard.rs: Expanded is_canonical_shape() (+2 patterns)
  - JsonparserAtoiReal
  - JsonparserParseNumberReal
  - Made NormalizedDevShape enum public
- bridge.rs: Updated canonical routing comments (Phase 41 → 46)
- normalized.rs: Made shape_guard module public
- normalized_joinir_min.rs: Added Phase 46 canonical verification test
- phase46-norm-canon-p2-mid.md: Complete design documentation

Out of scope (deferred):
- P3/P4 Normalized support → NORM-P3/NORM-P4 phases
- Selfhost complex loops → separate phases

Benefits:
- Clear P2 boundary: All JsonParser P2 = Normalized canonical
- Infrastructure validation: Phase 43/245B proven production-ready
- Simplified mental model: P2 = Normalized-first, P3/P4 = future

Tests: 937/937 PASS (lib), 20/20 PASS (normalized_dev feature)
Phase 46 test: test_phase46_canonical_set_includes_p2_mid 
2025-12-12 04:40:46 +09:00

1003 lines
34 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Minimal Normalized JoinIR model (Phase 26-H.B).
//!
//! テスト専用の極小サブセット。Pattern1 の while だけを Structured → Normalized に
//! 変換して遊ぶための足場だよ。本線の Structured→MIR 経路には影響しない。
use std::collections::BTreeMap;
use crate::mir::join_ir::{
BinOpKind, CompareOp, ConstValue, JoinContId, JoinFuncId, JoinFunction, JoinInst, JoinIrPhase,
JoinModule, MirLikeInst, UnaryOp,
};
use crate::mir::ValueId;
use std::collections::{HashMap, HashSet};
#[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 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<EnvField>,
}
#[derive(Debug, Clone)]
pub struct EnvField {
pub name: String,
pub ty: Option<crate::mir::MirType>,
pub value_id: Option<ValueId>,
}
/// 正規化済み関数 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<u32>,
pub body: Vec<JpInst>,
pub is_kont: bool,
}
/// 正規化済み命令(最小セット)。
#[derive(Debug, Clone)]
pub enum JpInst {
Let {
dst: ValueId,
op: JpOp,
args: Vec<ValueId>,
},
EnvLoad {
dst: ValueId,
env: ValueId,
field: usize,
},
EnvStore {
env: ValueId,
field: usize,
src: ValueId,
},
TailCallFn {
target: JpFuncId,
env: Vec<ValueId>,
},
TailCallKont {
target: JpFuncId,
env: Vec<ValueId>,
},
If {
cond: ValueId,
then_target: JpFuncId,
else_target: JpFuncId,
env: Vec<ValueId>,
},
}
/// 演算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<JpFuncId, JpFunction>,
pub entry: Option<JpFuncId>,
pub env_layouts: Vec<EnvLayout>,
pub phase: JoinIrPhase,
/// Structured に戻すためのスナップショット(テスト専用)。
pub structured_backup: Option<JoinModule>,
}
impl NormalizedModule {
pub fn to_structured(&self) -> Option<JoinModule> {
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<ValueId> = 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 = {
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);
}
}
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
}
/// 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<ValueId> = 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<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!(
"[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 のシンプル whilebreak/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<JpFuncId> = 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<JoinModule, String> {
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)
})),
};
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());
}
}