Files
hakorune/src/mir/join_ir/normalized.rs
nyash-codex 99bdf93dfe feat(joinir): Phase 47-A-LOWERING - P3 Normalized→MIR(direct)
Implement full Pattern3 (if-sum) Normalized→MIR(direct) support, completing
the P3 minimal implementation.

Key changes:

1. P3 Shape Detection (shape_guard.rs):
   - Implemented is_pattern3_if_sum_minimal() with structure-based detection
   - Detects P3 characteristics:
     - Has Compare instruction (loop condition)
     - Has Select instruction (conditional carrier update)
     - Has tail call (Call with k_next: None)
     - Reasonable param count (2-4 for i, sum carriers)
   - Handles both JoinInst::Select and Compute(MirLikeInst::Select)
   - Added unit test: test_detect_pattern3_if_sum_minimal_shape 

2. P3 Normalization Function (normalized.rs):
   - Implemented normalize_pattern3_if_sum_minimal()
   - Guards: Validates Structured JoinIR + P3 shape detection
   - Phase 47-A minimal: Delegates to P2 normalization (works for simple cases)
   - Updated normalized_dev_roundtrip_structured() integration
   - Returns Result<NormalizedModule, String> for proper error handling

3. Bridge Integration (bridge.rs):
   - Updated normalize_for_shape() to call P3 normalization
   - No changes needed to direct.rs (already handles P3 instructions)

4. Integration Tests (normalized_joinir_min.rs):
   - Added test_phase47a_pattern3_if_sum_minimal_normalization
     - Tests Structured→Normalized transformation
     - Verifies shape detection + normalization succeeds
     - Validates function count and phase correctness
   - Added test_phase47a_pattern3_if_sum_minimal_runner
     - Basic smoke test for P3 fixture validity
     - Verifies 3-function structure and entry point

Benefits:
- P3 now uses Normalized→MIR(direct) pipeline (same as P1/P2)
- Structure-based detection (no name-based heuristics)
- Minimal implementation (delegates to P2 for simplicity)
- Pure additive (no P1/P2 behavioral changes)

Tests: 938/938 PASS (lib), shape_guard P3 test PASS
- test_detect_pattern3_if_sum_minimal_shape 
- test_phase47a_pattern3_if_sum_minimal_normalization 
- test_phase47a_pattern3_if_sum_minimal_runner 

Next phase: Phase 47-B (proper P3-specific normalization, array_filter, multi-carrier)

Design note: This minimal implementation reuses P2 normalization for simplicity.
Proper P3-specific normalization (IfCond, ThenUpdates, ElseUpdates step sequence)
will be implemented in Phase 47-B when needed for more complex P3 patterns.
2025-12-12 05:53:23 +09:00

1035 lines
36 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
}
/// Phase 47-A: Normalize Pattern3 if-sum minimal to Normalized JoinIR
#[cfg(feature = "normalized_dev")]
pub fn normalize_pattern3_if_sum_minimal(
structured: &JoinModule,
) -> Result<NormalizedModule, String> {
// Guard: Must be Structured and match Pattern3IfSumMinimal shape
if !structured.is_structured() {
return Err("[normalize_p3] Not structured JoinIR".to_string());
}
// Use shape detection to verify P3 shape
let shapes = shape_guard::supported_shapes(&structured);
if !shapes.contains(&NormalizedDevShape::Pattern3IfSumMinimal) {
return Err("[normalize_p3] Not Pattern3IfSumMinimal shape".to_string());
}
// Phase 47-A minimal: Reuse P2 normalization temporarily
// TODO: Implement proper P3-specific normalization with:
// - EnvLayout for i, sum carriers
// - IfCond → ThenUpdates / ElseUpdates step sequence
// - Proper JpInst::If for conditional carrier updates
// For now, delegate to P2 normalization (works for simple cases)
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<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)
})),
// 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)
})),
};
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());
}
}