Files
hakorune/src/mir/join_ir/normalized.rs

929 lines
31 KiB
Rust
Raw Normal View History

//! 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(crate) 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 },
}
/// 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("Normalized verifier: 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!(
"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!(
"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::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")]
{
if shape_guard::is_jsonparser_atoi_mini(structured) {
max = 8;
}
}
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::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::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::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("Normalized verifier (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!(
"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 { .. }
| 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("Normalized 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!(
"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 verbose = crate::config::env::joinir_dev_enabled();
for shape in shapes {
if verbose {
eprintln!("[joinir/normalized-dev] 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::JsonparserAtoiMini => catch_unwind(AssertUnwindSafe(|| {
let norm = normalize_pattern2_minimal(module);
normalized_pattern2_to_structured(&norm)
})),
};
match attempt {
Ok(structured) => {
if verbose {
eprintln!(
"[joinir/normalized-dev] {:?} normalization succeeded (functions={})",
shape,
structured.functions.len()
);
}
return Ok(structured);
}
Err(_) => {
if verbose {
eprintln!(
"[joinir/normalized-dev] {:?} 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());
}
}