Refine normalized bridge direct path and env guard

This commit is contained in:
nyash-codex
2025-12-11 22:50:23 +09:00
parent a4756f3ce1
commit 12e2f87c6f
7 changed files with 484 additions and 60 deletions

View File

@ -17,6 +17,8 @@ use std::panic::{catch_unwind, AssertUnwindSafe};
#[cfg(feature = "normalized_dev")] #[cfg(feature = "normalized_dev")]
pub mod fixtures; pub mod fixtures;
#[cfg(feature = "normalized_dev")] #[cfg(feature = "normalized_dev")]
pub mod dev_env;
#[cfg(feature = "normalized_dev")]
pub(crate) mod shape_guard; pub(crate) mod shape_guard;
#[cfg(feature = "normalized_dev")] #[cfg(feature = "normalized_dev")]
use crate::mir::join_ir::normalized::shape_guard::NormalizedDevShape; use crate::mir::join_ir::normalized::shape_guard::NormalizedDevShape;

View File

@ -0,0 +1,38 @@
#![cfg(feature = "normalized_dev")]
use once_cell::sync::Lazy;
use std::sync::Mutex;
/// RAII guard for normalized_dev env toggling (NYASH_JOINIR_NORMALIZED_DEV_RUN).
/// 汚染防止のため tests/runner の両方で再利用できるようにここに置く。
pub struct NormalizedDevEnvGuard {
_lock: std::sync::MutexGuard<'static, ()>,
prev: Option<String>,
}
static NORMALIZED_ENV_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
impl NormalizedDevEnvGuard {
pub fn new(enabled: bool) -> Self {
let lock = NORMALIZED_ENV_LOCK
.lock()
.expect("normalized env mutex poisoned");
let prev = std::env::var("NYASH_JOINIR_NORMALIZED_DEV_RUN").ok();
if enabled {
std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", "1");
} else {
std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN");
}
Self { _lock: lock, prev }
}
}
impl Drop for NormalizedDevEnvGuard {
fn drop(&mut self) {
if let Some(prev) = &self.prev {
std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", prev);
} else {
std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN");
}
}
}

View File

@ -10,6 +10,11 @@ pub(crate) enum NormalizedDevShape {
JsonparserAtoiMini, JsonparserAtoiMini,
} }
/// 直接 Normalized→MIR ブリッジで扱う shape を返すdev 限定)。
pub(crate) fn direct_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
supported_shapes(module)
}
pub(crate) fn supported_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> { pub(crate) fn supported_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
let mut shapes = Vec::new(); let mut shapes = Vec::new();
if is_jsonparser_atoi_mini(module) { if is_jsonparser_atoi_mini(module) {
@ -27,6 +32,11 @@ pub(crate) fn supported_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
shapes shapes
} }
#[allow(dead_code)]
pub(crate) fn is_direct_supported(module: &JoinModule) -> bool {
!direct_shapes(module).is_empty()
}
pub(crate) fn is_pattern1_mini(module: &JoinModule) -> bool { pub(crate) fn is_pattern1_mini(module: &JoinModule) -> bool {
module.is_structured() && find_loop_step(module).is_some() module.is_structured() && find_loop_step(module).is_some()
} }

View File

@ -81,7 +81,7 @@ fn try_normalized_direct_bridge(
module: &JoinModule, module: &JoinModule,
meta: &JoinFuncMetaMap, meta: &JoinFuncMetaMap,
) -> Result<Option<MirModule>, JoinIrVmBridgeError> { ) -> Result<Option<MirModule>, JoinIrVmBridgeError> {
let shapes = shape_guard::supported_shapes(module); let shapes = shape_guard::direct_shapes(module);
if shapes.is_empty() { if shapes.is_empty() {
return Ok(None); return Ok(None);
} }

View File

@ -9,10 +9,32 @@ use crate::mir::join_ir::{
}; };
use crate::mir::MirModule; use crate::mir::MirModule;
mod direct;
/// Direct Normalized → MIR 変換が未対応のときに使うフォールバック。
fn lower_normalized_via_structured(
norm: &NormalizedModule,
meta: &JoinFuncMetaMap,
) -> Result<MirModule, JoinIrVmBridgeError> {
let structured = if let Some(snapshot) = norm.to_structured() {
snapshot
} else if norm.functions.len() <= 2 {
normalized_pattern1_to_structured(norm)
} else {
normalized_pattern2_to_structured(norm)
};
if crate::config::env::joinir_dev_enabled() {
eprintln!("[joinir/normalized-bridge/fallback] using structured path (functions={})", structured.functions.len());
}
lower_joinir_structured_to_mir_with_meta(&structured, meta)
}
/// Dev-only Normalized → MIR ブリッジPattern1/2 ミニ + JP mini/atoi mini 専用) /// Dev-only Normalized → MIR ブリッジPattern1/2 ミニ + JP mini/atoi mini 専用)
pub(crate) fn lower_normalized_to_mir_minimal( pub(crate) fn lower_normalized_to_mir_minimal(
norm: &NormalizedModule, norm: &NormalizedModule,
_meta: &JoinFuncMetaMap, meta: &JoinFuncMetaMap,
) -> Result<MirModule, JoinIrVmBridgeError> { ) -> Result<MirModule, JoinIrVmBridgeError> {
if norm.phase != JoinIrPhase::Normalized { if norm.phase != JoinIrPhase::Normalized {
return Err(JoinIrVmBridgeError::new( return Err(JoinIrVmBridgeError::new(
@ -49,14 +71,14 @@ pub(crate) fn lower_normalized_to_mir_minimal(
} }
} }
let mut norm_clone = norm.clone(); // direct 対象は Normalized → MIR をそのまま吐く。未対応 shape は Structured 経由にフォールバック。
norm_clone.structured_backup = None; match direct::lower_normalized_direct_minimal(norm) {
Ok(mir) => Ok(mir),
let structured = if norm_clone.functions.len() <= 2 { Err(err) => {
normalized_pattern1_to_structured(&norm_clone) if crate::config::env::joinir_dev_enabled() {
} else { eprintln!("[joinir/normalized-bridge/fallback] direct path failed: {}; falling back to Structured path", err.message);
normalized_pattern2_to_structured(&norm_clone) }
}; lower_normalized_via_structured(norm, meta)
}
lower_joinir_structured_to_mir_with_meta(&structured, _meta) }
} }

View File

@ -0,0 +1,388 @@
#![cfg(feature = "normalized_dev")]
use super::super::join_func_name;
use super::super::JoinIrVmBridgeError;
use super::super::convert_mir_like_inst;
use crate::ast::Span;
use crate::config::env::joinir_dev_enabled;
use crate::mir::join_ir::normalized::{JpFuncId, JpFunction, JpInst, JpOp, NormalizedModule};
use crate::mir::join_ir::{JoinFuncId, JoinIrPhase, MirLikeInst};
use crate::mir::{
BasicBlock, BasicBlockId, ConstValue as MirConstValue, EffectMask, FunctionSignature,
MirFunction, MirInstruction, MirModule, MirType, ValueId,
};
use std::collections::BTreeMap;
use std::mem;
/// Normalized → MIR の最小 direct 変換Pattern1/2 ミニJP mini/atoi mini
pub(crate) fn lower_normalized_direct_minimal(
norm: &NormalizedModule,
) -> Result<MirModule, JoinIrVmBridgeError> {
if norm.phase != JoinIrPhase::Normalized {
return Err(JoinIrVmBridgeError::new(
"[joinir/normalized-bridge] expected Normalized JoinIR module",
));
}
let debug_dump = std::env::var("JOINIR_TEST_DEBUG").is_ok();
let verbose = joinir_dev_enabled();
if verbose {
eprintln!(
"[joinir/normalized-bridge/direct] lowering normalized module (functions={}, env_layouts={})",
norm.functions.len(),
norm.env_layouts.len()
);
}
let mut mir_module = MirModule::new("joinir_normalized_direct".to_string());
for func in norm.functions.values() {
let mir_func = lower_normalized_function_direct(func, norm)?;
mir_module.add_function(mir_func);
}
if debug_dump {
eprintln!(
"[joinir/normalized-bridge/direct] produced MIR (debug dump): {:#?}",
mir_module
);
}
Ok(mir_module)
}
fn lower_normalized_function_direct(
func: &JpFunction,
norm: &NormalizedModule,
) -> Result<MirFunction, JoinIrVmBridgeError> {
let verbose = joinir_dev_enabled();
let env_fields = func
.env_layout
.and_then(|id| norm.env_layouts.iter().find(|layout| layout.id == id));
let params: Vec<ValueId> = env_fields
.map(|layout| {
layout
.fields
.iter()
.enumerate()
.map(|(idx, f)| f.value_id.unwrap_or(ValueId(idx as u32)))
.collect()
})
.unwrap_or_default();
// Build a dense ValueId mapping to ensure VM registersはVoidにならない。
let mut value_map: BTreeMap<ValueId, ValueId> = BTreeMap::new();
for id in &params {
remap_value(*id, &mut value_map);
}
for inst in &func.body {
for vid in value_ids_in_inst(inst) {
remap_value(vid, &mut value_map);
}
}
let remap = |id: ValueId, map: &mut BTreeMap<ValueId, ValueId>| remap_value(id, map);
let remap_vec = |ids: &[ValueId], map: &mut BTreeMap<ValueId, ValueId>| {
ids.iter().map(|id| remap_value(*id, map)).collect::<Vec<_>>()
};
let remapped_params = remap_vec(&params, &mut value_map);
let param_types = vec![MirType::Unknown; remapped_params.len()];
let signature = FunctionSignature {
name: join_func_name(JoinFuncId(func.id.0)),
params: param_types,
return_type: MirType::Unknown,
effects: EffectMask::PURE,
};
let mut mir_func = MirFunction::new(signature, BasicBlockId(0));
if !remapped_params.is_empty() {
mir_func.params = remapped_params.clone();
}
mir_func.next_value_id = value_map.len() as u32;
let mut current_block_id = BasicBlockId(0);
let mut next_block_id = 1;
let mut current_insts: Vec<MirInstruction> = Vec::new();
let mut terminated = false;
if verbose {
eprintln!(
"[joinir/normalized-bridge/direct] lowering fn={} params={:?} remapped_params={:?} body_len={}",
func.name, params, remapped_params, func.body.len()
);
}
for inst in &func.body {
if terminated {
break;
}
match inst {
JpInst::Let { dst, op, args } => {
let remapped_dst = remap(*dst, &mut value_map);
let remapped_args = remap_vec(args, &mut value_map);
let mir_like = jp_op_to_mir_like(remapped_dst, op, &remapped_args)?;
let mir_inst = convert_mir_like_inst(&mir_like)?;
current_insts.push(mir_inst);
}
JpInst::EnvLoad { .. } | JpInst::EnvStore { .. } => {
return Err(JoinIrVmBridgeError::new(
"[joinir/normalized-bridge/direct] EnvLoad/EnvStore not supported in minimal direct bridge",
));
}
JpInst::TailCallFn { target, env } => {
let env_remapped = remap_vec(env, &mut value_map);
let (instructions, terminator) = build_tail_call(&mut mir_func, target, &env_remapped);
finalize_block(
&mut mir_func,
current_block_id,
{
let mut insts = mem::take(&mut current_insts);
insts.extend(instructions);
insts
},
terminator,
None,
);
terminated = true;
}
JpInst::TailCallKont { env, .. } => {
let env_remapped = remap_vec(env, &mut value_map);
let return_val = env_remapped.first().copied();
finalize_block(
&mut mir_func,
current_block_id,
mem::take(&mut current_insts),
MirInstruction::Return { value: return_val },
Some(env_remapped),
);
terminated = true;
}
JpInst::If {
cond,
then_target,
else_target,
env,
} => {
let then_bb = BasicBlockId(next_block_id);
next_block_id += 1;
let else_bb = BasicBlockId(next_block_id);
next_block_id += 1;
let cond_remapped = remap(*cond, &mut value_map);
let env_remapped = remap_vec(env, &mut value_map);
// Branch from current block
finalize_block(
&mut mir_func,
current_block_id,
mem::take(&mut current_insts),
MirInstruction::Branch {
condition: cond_remapped,
then_bb,
else_bb,
},
None,
);
// Decide which branch continues the loop (self) and which exits.
let (exit_bb, exit_target, cont_bb) = if then_target.0 == func.id.0 {
(else_bb, else_target, then_bb)
} else if else_target.0 == func.id.0 {
(then_bb, then_target, else_bb)
} else {
// Both branches exit; build both and stop.
build_exit_or_tail_branch(
&mut mir_func,
then_bb,
then_target,
&env_remapped,
func.id,
)?;
build_exit_or_tail_branch(
&mut mir_func,
else_bb,
else_target,
&env_remapped,
func.id,
)?;
terminated = true;
continue;
};
build_exit_or_tail_branch(
&mut mir_func,
exit_bb,
exit_target,
&env_remapped,
func.id,
)?;
mir_func
.blocks
.entry(cont_bb)
.or_insert_with(|| BasicBlock::new(cont_bb));
current_block_id = cont_bb;
current_insts = Vec::new();
}
}
}
if !terminated {
// Flush remaining instructions into the current block and end with Return None
let block = mir_func
.blocks
.entry(current_block_id)
.or_insert_with(|| BasicBlock::new(current_block_id));
if !current_insts.is_empty() {
block.instructions = current_insts;
block.instruction_spans = vec![Span::unknown(); block.instructions.len()];
}
if block.terminator.is_none() {
block.terminator = Some(MirInstruction::Return { value: None });
}
}
Ok(mir_func)
}
fn jp_op_to_mir_like(
dst: ValueId,
op: &JpOp,
args: &[ValueId],
) -> Result<MirLikeInst, JoinIrVmBridgeError> {
match op {
JpOp::Const(v) => Ok(MirLikeInst::Const {
dst,
value: v.clone(),
}),
JpOp::BinOp(op) => Ok(MirLikeInst::BinOp {
dst,
op: *op,
lhs: args.get(0).copied().unwrap_or(ValueId(0)),
rhs: args.get(1).copied().unwrap_or(ValueId(0)),
}),
JpOp::Unary(op) => Ok(MirLikeInst::UnaryOp {
dst,
op: *op,
operand: args.get(0).copied().unwrap_or(ValueId(0)),
}),
JpOp::Compare(op) => Ok(MirLikeInst::Compare {
dst,
op: *op,
lhs: args.get(0).copied().unwrap_or(ValueId(0)),
rhs: args.get(1).copied().unwrap_or(ValueId(0)),
}),
JpOp::BoxCall { box_name, method } => Ok(MirLikeInst::BoxCall {
dst: Some(dst),
box_name: box_name.clone(),
method: method.clone(),
args: args.to_vec(),
}),
}
}
fn build_tail_call(
mir_func: &mut MirFunction,
target: &JpFuncId,
env: &[ValueId],
) -> (Vec<MirInstruction>, MirInstruction) {
let func_name_id = mir_func.next_value_id();
let result_id = mir_func.next_value_id();
let func_name = join_func_name(JoinFuncId(target.0));
let mut instructions = Vec::new();
instructions.push(MirInstruction::Const {
dst: func_name_id,
value: MirConstValue::String(func_name),
});
instructions.push(MirInstruction::Call {
dst: Some(result_id),
func: func_name_id,
callee: None,
args: env.to_vec(),
effects: EffectMask::PURE,
});
(instructions, MirInstruction::Return { value: Some(result_id) })
}
fn build_exit_or_tail_branch(
mir_func: &mut MirFunction,
block_id: BasicBlockId,
target: &JpFuncId,
env: &[ValueId],
self_id: JpFuncId,
) -> Result<(), JoinIrVmBridgeError> {
if target.0 == self_id.0 {
// Continue the loop: tail call self with env, then return its result
let (insts, term) = build_tail_call(mir_func, target, env);
let block = mir_func
.blocks
.entry(block_id)
.or_insert_with(|| BasicBlock::new(block_id));
block.instructions = insts;
block.instruction_spans = vec![Span::unknown(); block.instructions.len()];
block.terminator = Some(term);
return Ok(());
}
// Exit: return first env arg (Pattern2 minis pass loop state in env[0])
let ret_val = env.first().copied();
let block = mir_func
.blocks
.entry(block_id)
.or_insert_with(|| BasicBlock::new(block_id));
block.instructions.clear();
block.instruction_spans.clear();
block.terminator = Some(MirInstruction::Return { value: ret_val });
block.jump_args = Some(env.to_vec());
Ok(())
}
fn value_ids_in_inst(inst: &JpInst) -> Vec<ValueId> {
match inst {
JpInst::Let { dst, args, .. } => {
let mut ids = vec![*dst];
ids.extend(args.iter().copied());
ids
}
JpInst::EnvLoad { dst, env, .. } => vec![*dst, *env],
JpInst::EnvStore { env, src, .. } => vec![*env, *src],
JpInst::TailCallFn { env, .. } | JpInst::TailCallKont { env, .. } => env.clone(),
JpInst::If { cond, env, .. } => {
let mut ids = vec![*cond];
ids.extend(env.iter().copied());
ids
}
}
}
fn remap_value(id: ValueId, map: &mut BTreeMap<ValueId, ValueId>) -> ValueId {
if let Some(mapped) = map.get(&id) {
return *mapped;
}
let next = ValueId(map.len() as u32);
map.insert(id, next);
next
}
fn finalize_block(
mir_func: &mut MirFunction,
block_id: BasicBlockId,
instructions: Vec<MirInstruction>,
terminator: MirInstruction,
jump_args: Option<Vec<ValueId>>,
) {
let block = mir_func
.blocks
.entry(block_id)
.or_insert_with(|| BasicBlock::new(block_id));
block.instructions = instructions;
block.instruction_spans = vec![Span::unknown(); block.instructions.len()];
block.terminator = Some(terminator);
block.jump_args = jump_args;
}

View File

@ -6,6 +6,7 @@ use nyash_rust::mir::join_ir::{
normalized_pattern2_to_structured, BinOpKind, ConstValue, JoinContId, JoinFuncId, normalized_pattern2_to_structured, BinOpKind, ConstValue, JoinContId, JoinFuncId,
JoinFunction, JoinInst, JoinIrPhase, JoinModule, MirLikeInst, JoinFunction, JoinInst, JoinIrPhase, JoinModule, MirLikeInst,
}; };
use nyash_rust::mir::join_ir::normalized::dev_env::NormalizedDevEnvGuard;
use nyash_rust::mir::join_ir::normalized::fixtures::{ use nyash_rust::mir::join_ir::normalized::fixtures::{
build_jsonparser_atoi_structured_for_normalized_dev, build_jsonparser_atoi_structured_for_normalized_dev,
build_jsonparser_skip_ws_structured_for_normalized_dev, build_jsonparser_skip_ws_structured_for_normalized_dev,
@ -15,39 +16,6 @@ use nyash_rust::mir::join_ir_runner::run_joinir_function;
use nyash_rust::mir::join_ir_ops::JoinValue; use nyash_rust::mir::join_ir_ops::JoinValue;
use nyash_rust::mir::join_ir_vm_bridge::run_joinir_via_vm; use nyash_rust::mir::join_ir_vm_bridge::run_joinir_via_vm;
use nyash_rust::mir::ValueId; use nyash_rust::mir::ValueId;
use once_cell::sync::Lazy;
use std::sync::Mutex;
static NORMALIZED_ENV_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
struct NormalizedRunGuard {
_lock: std::sync::MutexGuard<'static, ()>,
prev: Option<String>,
}
impl NormalizedRunGuard {
fn new(enabled: bool) -> Self {
let lock = NORMALIZED_ENV_LOCK.lock().expect("env mutex poisoned");
let prev = std::env::var("NYASH_JOINIR_NORMALIZED_DEV_RUN").ok();
if enabled {
std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", "1");
} else {
std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN");
}
Self { _lock: lock, prev }
}
}
impl Drop for NormalizedRunGuard {
fn drop(&mut self) {
if let Some(prev) = &self.prev {
std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", prev);
} else {
std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN");
}
}
}
fn assert_normalized_dev_ready() { fn assert_normalized_dev_ready() {
assert!( assert!(
nyash_rust::config::env::normalized_dev_enabled(), nyash_rust::config::env::normalized_dev_enabled(),
@ -61,7 +29,7 @@ fn run_joinir_runner(
args: &[JoinValue], args: &[JoinValue],
normalized: bool, normalized: bool,
) -> JoinValue { ) -> JoinValue {
let _guard = NormalizedRunGuard::new(normalized); let _guard = NormalizedDevEnvGuard::new(normalized);
if normalized { if normalized {
assert_normalized_dev_ready(); assert_normalized_dev_ready();
} }
@ -75,7 +43,7 @@ fn run_joinir_vm_bridge(
args: &[JoinValue], args: &[JoinValue],
normalized: bool, normalized: bool,
) -> JoinValue { ) -> JoinValue {
let _guard = NormalizedRunGuard::new(normalized); let _guard = NormalizedDevEnvGuard::new(normalized);
if normalized { if normalized {
assert_normalized_dev_ready(); assert_normalized_dev_ready();
} }
@ -160,10 +128,8 @@ fn normalized_pattern1_exec_matches_structured() {
let entry = structured.entry.unwrap_or(JoinFuncId::new(1)); let entry = structured.entry.unwrap_or(JoinFuncId::new(1));
let input = [JoinValue::Int(0)]; let input = [JoinValue::Int(0)];
let result_structured = let result_structured = run_joinir_vm_bridge(&structured, entry, &input, false);
run_joinir_via_vm(&structured, entry, &input).expect("structured run should succeed"); let result_norm = run_joinir_vm_bridge(&reconstructed, entry, &input, false);
let result_norm = run_joinir_via_vm(&reconstructed, entry, &input)
.expect("normalized roundtrip run should succeed");
assert_eq!(result_structured, result_norm); assert_eq!(result_structured, result_norm);
} }
@ -180,9 +146,9 @@ fn normalized_pattern1_exec_matches_structured_roundtrip_backup() {
let entry = structured.entry.unwrap_or(JoinFuncId::new(1)); let entry = structured.entry.unwrap_or(JoinFuncId::new(1));
let input = [JoinValue::Int(0)]; let input = [JoinValue::Int(0)];
let base = run_joinir_via_vm(&structured, entry, &input).expect("structured run"); let base = run_joinir_vm_bridge(&structured, entry, &input, false);
let recon = run_joinir_via_vm(&reconstructed, entry, &input).expect("reconstructed run"); let recon = run_joinir_vm_bridge(&reconstructed, entry, &input, false);
let restored = run_joinir_via_vm(&restored_backup, entry, &input).expect("backup run"); let restored = run_joinir_vm_bridge(&restored_backup, entry, &input, false);
assert_eq!(base, recon); assert_eq!(base, recon);
assert_eq!(base, restored); assert_eq!(base, restored);
@ -218,8 +184,8 @@ fn normalized_pattern2_exec_matches_structured() {
let entry = structured.entry.unwrap_or(JoinFuncId::new(0)); let entry = structured.entry.unwrap_or(JoinFuncId::new(0));
let input = [JoinValue::Int(0)]; let input = [JoinValue::Int(0)];
let base = run_joinir_via_vm(&structured, entry, &input).expect("structured run"); let base = run_joinir_vm_bridge(&structured, entry, &input, false);
let recon = run_joinir_via_vm(&reconstructed, entry, &input).expect("normalized roundtrip run"); let recon = run_joinir_vm_bridge(&reconstructed, entry, &input, false);
assert_eq!(base, recon); assert_eq!(base, recon);
} }
@ -264,10 +230,8 @@ fn normalized_pattern2_real_loop_exec_matches_structured() {
for n in cases { for n in cases {
let input = [JoinValue::Int(n)]; let input = [JoinValue::Int(n)];
let base = let base = run_joinir_vm_bridge(&structured, entry, &input, false);
run_joinir_via_vm(&structured, entry, &input).expect("structured execution should pass"); let recon = run_joinir_vm_bridge(&reconstructed, entry, &input, false);
let recon = run_joinir_via_vm(&reconstructed, entry, &input)
.expect("normalized roundtrip execution should pass");
assert_eq!(base, recon, "mismatch at n={}", n); assert_eq!(base, recon, "mismatch at n={}", n);
let expected_sum = n * (n.saturating_sub(1)) / 2; let expected_sum = n * (n.saturating_sub(1)) / 2;