feat(joinir): Phase 248 - Normalized JoinIR infrastructure

Major refactoring of JoinIR normalization pipeline:

Key changes:
- Structured→Normalized→MIR(direct) pipeline established
- ShapeGuard enhanced with Pattern2 loop validation
- dev_env.rs: New development fixtures and env control
- fixtures.rs: jsonparser_parse_number_real fixture
- normalized_bridge/direct.rs: Direct MIR generation from Normalized
- pattern2_step_schedule.rs: Extracted step scheduling logic

Files changed:
- normalized.rs: Enhanced NormalizedJoinModule with DevEnv support
- shape_guard.rs: Pattern2-specific validation (+300 lines)
- normalized_bridge.rs: Unified bridge with direct path
- loop_with_break_minimal.rs: Integrated step scheduling
- Deleted: step_schedule.rs (moved to pattern2_step_schedule.rs)

New files:
- param_guess.rs: Loop parameter inference
- pattern2_step_schedule.rs: Step scheduling for Pattern2
- phase43-norm-canon-p2-mid.md: Design doc

Tests: 937/937 PASS (+6 from baseline 931)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-12 03:15:45 +09:00
parent 59caf5864c
commit ed8e2d3142
32 changed files with 1559 additions and 421 deletions

View File

@ -117,6 +117,11 @@ pub fn normalized_dev_enabled() -> bool {
}
}
/// JOINIR_TEST_DEBUG=1 (or NYASH_JOINIR_TEST_DEBUG=1) - Verbose logging for normalized dev tests
pub fn joinir_test_debug_enabled() -> bool {
env_bool("JOINIR_TEST_DEBUG") || env_bool("NYASH_JOINIR_TEST_DEBUG")
}
/// Phase 82: NYASH_PHI_FALLBACK_DISABLED=1 - Disable if_phi fallback (dev mode)
///
/// lifecycle.rs の infer_type_from_phi* callsite を封じて、

View File

@ -8,9 +8,11 @@ use crate::mir::join_ir::lowering::condition_env::{ConditionBinding, ConditionEn
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr;
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
use crate::mir::loop_pattern_detection::error_messages;
use crate::mir::ValueId;
use std::collections::BTreeMap;
fn log_pattern2(verbose: bool, tag: &str, message: impl AsRef<str>) {
if verbose {
@ -701,25 +703,19 @@ impl MirBuilder {
verbose,
"updates",
format!(
"Phase 176-3: Analyzed {} carrier updates",
carrier_updates.len()
),
);
"Phase 176-3: Analyzed {} carrier updates",
carrier_updates.len()
),
);
let original_carrier_count = inputs.carrier_info.carriers.len();
inputs.carrier_info.carriers.retain(|carrier| {
use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole};
carrier_updates.contains_key(&carrier.name)
|| carrier.role == CarrierRole::ConditionOnly
|| carrier.init == CarrierInit::FromHost
|| carrier.init == CarrierInit::LoopLocalZero
});
filter_carriers_for_updates(&mut inputs.carrier_info, &carrier_updates);
log_pattern2(
verbose,
"updates",
format!(
"Phase 176-4: Filtered carriers: {}{} (kept only carriers with updates)",
"Phase 176-4: Filtered carriers: {}{} (kept only carriers with updates/condition-only/loop-local-zero)",
original_carrier_count,
inputs.carrier_info.carriers.len()
),
@ -858,6 +854,19 @@ impl MirBuilder {
}
}
/// 更新を持たない FromHost キャリアを落とすヘルパー。
fn filter_carriers_for_updates(
carrier_info: &mut CarrierInfo,
carrier_updates: &BTreeMap<String, UpdateExpr>,
) {
use crate::mir::join_ir::lowering::carrier_info::{CarrierInit, CarrierRole};
carrier_info.carriers.retain(|carrier| {
carrier_updates.contains_key(&carrier.name)
|| carrier.role == CarrierRole::ConditionOnly
|| carrier.init == CarrierInit::LoopLocalZero
});
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -138,6 +138,8 @@ impl AstToJoinIrLowerer {
"-" => BinOpKind::Sub,
"*" => BinOpKind::Mul,
"/" => BinOpKind::Div,
"&&" => BinOpKind::And,
"||" => BinOpKind::Or,
_ => panic!("Unsupported binary op: {}", op_str),
};

View File

@ -24,10 +24,10 @@ use super::common::{
build_join_module, create_k_exit_function, create_loop_context, parse_program_json,
process_local_inits,
};
use super::param_guess::{build_param_order, compute_param_guess};
use super::{AstToJoinIrLowerer, JoinModule, LoweringError};
use crate::mir::join_ir::{JoinFunction, JoinInst};
use crate::mir::ValueId;
use std::collections::BTreeSet;
/// Break パターンを JoinModule に変換
///
@ -70,7 +70,10 @@ 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 param_guess = compute_param_guess(&entry_ctx);
let param_order = build_param_order(&param_guess, &entry_ctx);
let loop_var_name = param_guess.loop_var.0.clone();
let acc_name = param_guess.acc.0.clone();
let loop_cond_expr = &loop_node["cond"];
// 5. entry 関数を生成
@ -245,48 +248,3 @@ fn create_loop_step_function_break(
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

@ -13,6 +13,7 @@
//! - `create_k_exit_function()`: k_exit 関数生成
use super::{AstToJoinIrLowerer, JoinModule};
use super::super::stmt_handlers::StatementEffect;
use crate::mir::join_ir::JoinIrPhase;
use crate::mir::join_ir::{JoinFuncId, JoinFunction, JoinInst};
use crate::mir::ValueId;
@ -119,19 +120,15 @@ pub fn process_local_inits(
let stmt_type = stmt["type"].as_str().expect("Statement must have type");
match stmt_type {
"Local" => {
let var_name = stmt["name"]
.as_str()
.expect("Local must have 'name'")
.to_string();
let expr = &stmt["expr"];
// extract_value で式を評価
let (var_id, insts) = lowerer.extract_value(expr, ctx);
"Local" | "Assignment" | "If" => {
let (insts, effect) = lowerer.lower_statement(stmt, ctx);
init_insts.extend(insts);
// 同名再宣言 = var_map を更新(再代入の意味論)
ctx.register_param(var_name, var_id);
if let StatementEffect::VarUpdate { name, value_id } = effect {
ctx.register_param(name, value_id);
} else if matches!(effect, StatementEffect::SideEffect) {
panic!("Unexpected side-effecting statement before Loop: {}", stmt_type);
}
}
_ => panic!("Unexpected statement type before Loop: {}", stmt_type),
}

View File

@ -19,6 +19,7 @@ pub mod break_pattern;
pub mod common;
pub mod continue_pattern;
pub mod filter;
pub mod param_guess;
pub mod print_tokens;
pub mod simple;

View File

@ -0,0 +1,71 @@
use std::collections::BTreeSet;
use crate::mir::ValueId;
/// 推定したループ引数の並び。
#[derive(Debug, Clone)]
pub(crate) struct ParamGuess {
pub loop_var: (String, ValueId),
pub acc: (String, ValueId),
pub len: Option<(String, ValueId)>,
}
/// Break パターン向けのパラメータ推定テーブル。
/// - ループ変数優先順: p → i → 先頭の var
/// - アキュムレータ優先順: num_str → acc → result → ループ変数
pub(crate) fn compute_param_guess(ctx: &super::super::context::ExtractCtx) -> ParamGuess {
let loop_var = ctx
.get_var("p")
.map(|v| ("p".to_string(), v))
.or_else(|| ctx.get_var("i").map(|v| ("i".to_string(), v)))
.or_else(|| {
ctx.var_map
.iter()
.find(|(name, _)| name.as_str() != "me")
.map(|(name, v)| (name.clone(), *v))
})
.unwrap_or_else(|| panic!("[joinir/frontend] break_pattern: loop variable missing"));
let acc = if let Some(v) = ctx.get_var("num_str") {
("num_str".to_string(), v)
} else if let Some(v) = ctx.get_var("acc") {
("acc".to_string(), v)
} else if let Some(v) = ctx.get_var("result") {
("result".to_string(), v)
} else {
loop_var.clone()
};
let len = ctx
.get_var("n")
.map(|v| ("n".to_string(), v))
.or_else(|| ctx.get_var("len").map(|v| ("len".to_string(), v)));
ParamGuess { loop_var, acc, len }
}
/// 推定結果と ExtractCtx からパラメータの並びを構成する。
pub(crate) fn build_param_order(
guess: &ParamGuess,
entry_ctx: &super::super::context::ExtractCtx,
) -> Vec<(String, ValueId)> {
let mut order = Vec::new();
order.push(guess.loop_var.clone());
if guess.acc.0 != guess.loop_var.0 {
order.push(guess.acc.clone());
}
if let Some(len) = &guess.len {
if len.0 != guess.loop_var.0 && len.0 != guess.acc.0 {
order.push(len.clone());
}
}
let mut seen: BTreeSet<String> = order.iter().map(|(n, _)| n.clone()).collect();
for (name, var_id) in &entry_ctx.var_map {
if !seen.contains(name) {
order.push((name.clone(), *var_id));
seen.insert(name.clone());
}
}
order
}

View File

@ -54,13 +54,21 @@ impl AstToJoinIrLowerer {
pub(crate) fn has_break_in_loop_body(loop_body: &[serde_json::Value]) -> bool {
loop_body.iter().any(|stmt| {
if stmt["type"].as_str() == Some("If") {
if let Some(then_body) = stmt["then"].as_array() {
then_body
.iter()
.any(|s| s["type"].as_str() == Some("Break"))
} else {
false
}
let then_has = stmt["then"]
.as_array()
.map(|body| {
body.iter()
.any(|s| s["type"].as_str() == Some("Break"))
})
.unwrap_or(false);
let else_has = stmt["else"]
.as_array()
.map(|body| {
body.iter()
.any(|s| s["type"].as_str() == Some("Break"))
})
.unwrap_or(false);
then_has || else_has
} else {
false
}
@ -71,13 +79,21 @@ impl AstToJoinIrLowerer {
pub(crate) fn has_continue_in_loop_body(loop_body: &[serde_json::Value]) -> bool {
loop_body.iter().any(|stmt| {
if stmt["type"].as_str() == Some("If") {
if let Some(then_body) = stmt["then"].as_array() {
then_body
.iter()
.any(|s| s["type"].as_str() == Some("Continue"))
} else {
false
}
let then_has = stmt["then"]
.as_array()
.map(|body| {
body.iter()
.any(|s| s["type"].as_str() == Some("Continue"))
})
.unwrap_or(false);
let else_has = stmt["else"]
.as_array()
.map(|body| {
body.iter()
.any(|s| s["type"].as_str() == Some("Continue"))
})
.unwrap_or(false);
then_has || else_has
} else {
false
}

View File

@ -52,24 +52,25 @@ 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",
"jsonparser_atoi_mini",
const TABLE: &[(&str, FunctionRoute)] = &[
("test", FunctionRoute::IfReturn),
("local", FunctionRoute::IfReturn),
("_read_value_from_pair", FunctionRoute::IfReturn),
("simple", FunctionRoute::LoopFrontend),
("filter", FunctionRoute::LoopFrontend),
("print_tokens", FunctionRoute::LoopFrontend),
("map", FunctionRoute::LoopFrontend),
("reduce", FunctionRoute::LoopFrontend),
("fold", FunctionRoute::LoopFrontend),
("jsonparser_skip_ws_mini", FunctionRoute::LoopFrontend),
("jsonparser_skip_ws_real", FunctionRoute::LoopFrontend),
("jsonparser_atoi_mini", FunctionRoute::LoopFrontend),
("jsonparser_atoi_real", FunctionRoute::LoopFrontend),
("jsonparser_parse_number_real", FunctionRoute::LoopFrontend),
];
if IF_RETURN_NAMES.contains(&func_name) {
return Ok(FunctionRoute::IfReturn);
}
if LOOP_NAMES.contains(&func_name) {
return Ok(FunctionRoute::LoopFrontend);
if let Some((_, route)) = TABLE.iter().find(|(name, _)| *name == func_name) {
return Ok(*route);
}
if func_name == "parse_loop" {

View File

@ -58,7 +58,6 @@
use crate::ast::ASTNode;
mod boundary_builder;
mod header_break_lowering;
mod step_schedule;
#[cfg(test)]
mod tests;
@ -71,6 +70,9 @@ use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr;
use crate::mir::join_ir::lowering::pattern2_step_schedule::{
build_pattern2_schedule, Pattern2ScheduleContext, Pattern2StepKind,
};
use crate::mir::join_ir::lowering::update_env::UpdateEnv;
use crate::mir::join_ir::{
BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst,
@ -84,7 +86,6 @@ use crate::mir::ValueId;
use boundary_builder::build_fragment_meta;
use header_break_lowering::{lower_break_condition, lower_header_condition};
use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism
use step_schedule::{Pattern2Step, Pattern2StepSchedule};
/// Lower Pattern 2 (Loop with Conditional Break) to JoinIR
///
@ -260,6 +261,20 @@ pub(crate) fn lower_loop_with_break_minimal(
"[joinir/pattern2] Phase 201: loop_step params - i_param={:?}, carrier_params={:?}",
i_param, carrier_param_ids
);
if crate::config::env::joinir_dev_enabled()
|| crate::config::env::joinir_test_debug_enabled()
{
eprintln!(
"[joinir/pattern2/debug] loop_var='{}' env.get(loop_var)={:?}, carriers={:?}",
loop_var_name,
env.get(loop_var_name),
carrier_info
.carriers
.iter()
.map(|c| (c.name.clone(), c.join_id))
.collect::<Vec<_>>()
);
}
// Phase 169 / Phase 171-fix / Phase 240-EX / Phase 244: Lower condition
let (cond_value, mut cond_instructions) = lower_header_condition(
@ -327,23 +342,9 @@ pub(crate) fn lower_loop_with_break_minimal(
let mut loop_step_func = JoinFunction::new(loop_step_id, "loop_step".to_string(), loop_params);
// Decide evaluation order (header/body-init/break/updates/tail) up-front.
let schedule =
Pattern2StepSchedule::for_pattern2(body_local_env.as_ref().map(|env| &**env), carrier_info);
let schedule_desc: Vec<&str> = schedule
.iter()
.map(|step| match step {
Pattern2Step::HeaderAndNaturalExit => "header+exit",
Pattern2Step::BodyLocalInit => "body-init",
Pattern2Step::BreakCondition => "break",
Pattern2Step::CarrierUpdates => "updates",
Pattern2Step::TailCall => "tail",
})
.collect();
eprintln!(
"[pattern2/schedule] Selected Pattern2 step schedule: {:?} ({})",
schedule_desc,
schedule.reason()
);
let schedule_ctx =
Pattern2ScheduleContext::from_env(body_local_env.as_ref().map(|env| &**env), carrier_info);
let schedule = build_pattern2_schedule(&schedule_ctx);
// Collect fragments per step; append them according to the schedule below.
let mut header_block: Vec<JoinInst> = Vec::new();
@ -661,11 +662,11 @@ pub(crate) fn lower_loop_with_break_minimal(
// Apply scheduled order to assemble the loop_step body.
for step in schedule.iter() {
match step {
Pattern2Step::HeaderAndNaturalExit => loop_step_func.body.append(&mut header_block),
Pattern2Step::BodyLocalInit => loop_step_func.body.append(&mut body_init_block),
Pattern2Step::BreakCondition => loop_step_func.body.append(&mut break_block),
Pattern2Step::CarrierUpdates => loop_step_func.body.append(&mut carrier_update_block),
Pattern2Step::TailCall => loop_step_func.body.append(&mut tail_block),
Pattern2StepKind::HeaderCond => loop_step_func.body.append(&mut header_block),
Pattern2StepKind::BodyInit => loop_step_func.body.append(&mut body_init_block),
Pattern2StepKind::BreakCheck => loop_step_func.body.append(&mut break_block),
Pattern2StepKind::Updates => loop_step_func.body.append(&mut carrier_update_block),
Pattern2StepKind::Tail => loop_step_func.body.append(&mut tail_block),
}
}

View File

@ -1,76 +0,0 @@
//! Pattern 2 step scheduler.
//!
//! Decides the evaluation order for Pattern 2 lowering without hardcoding it
//! in the lowerer. This keeps the lowerer focused on emitting fragments while
//! the scheduler decides how to interleave them (e.g., body-local init before
//! break checks when the break depends on body-local values).
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierInit};
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
/// Steps that can be reordered by the scheduler.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Pattern2Step {
HeaderAndNaturalExit,
BodyLocalInit,
BreakCondition,
CarrierUpdates,
TailCall,
}
/// Data-driven schedule for Pattern 2 lowering.
#[derive(Debug, Clone)]
pub(crate) struct Pattern2StepSchedule {
steps: Vec<Pattern2Step>,
reason: &'static str,
}
impl Pattern2StepSchedule {
/// Choose a schedule based on whether the break condition relies on fresh
/// body-local values (DigitPos-style).
pub(crate) fn for_pattern2(
body_local_env: Option<&LoopBodyLocalEnv>,
carrier_info: &CarrierInfo,
) -> Self {
let has_body_locals = body_local_env.map(|env| !env.is_empty()).unwrap_or(false);
let has_loop_local_carrier = carrier_info
.carriers
.iter()
.any(|c| matches!(c.init, CarrierInit::LoopLocalZero));
// If there are body-local dependencies, evaluate them before the break
// condition so the break uses fresh values.
if has_body_locals || has_loop_local_carrier {
Self {
steps: vec![
Pattern2Step::HeaderAndNaturalExit,
Pattern2Step::BodyLocalInit,
Pattern2Step::BreakCondition,
Pattern2Step::CarrierUpdates,
Pattern2Step::TailCall,
],
reason: "body-local break dependency",
}
} else {
// Default order: header → break → updates → tail.
Self {
steps: vec![
Pattern2Step::HeaderAndNaturalExit,
Pattern2Step::BreakCondition,
Pattern2Step::BodyLocalInit,
Pattern2Step::CarrierUpdates,
Pattern2Step::TailCall,
],
reason: "default",
}
}
}
pub(crate) fn iter(&self) -> impl Iterator<Item = Pattern2Step> + '_ {
self.steps.iter().copied()
}
pub(crate) fn reason(&self) -> &'static str {
self.reason
}
}

View File

@ -61,6 +61,7 @@ pub(crate) mod loop_view_builder; // Phase 33-23: Loop lowering dispatch
pub mod loop_with_break_minimal; // Phase 188-Impl-2: Pattern 2 minimal lowerer
pub mod loop_with_continue_minimal;
pub mod method_call_lowerer; // Phase 224-B: MethodCall lowering (metadata-driven)
pub(crate) mod pattern2_step_schedule; // Phase 39: Pattern2 evaluation order scheduler
pub mod method_return_hint; // Phase 83: P3-D 既知メソッド戻り値型推論箱
pub mod scope_manager; // Phase 231: Unified variable scope management // Phase 195: Pattern 4 minimal lowerer
// Phase 242-EX-A: loop_with_if_phi_minimal removed - replaced by loop_with_if_phi_if_sum

View File

@ -0,0 +1,224 @@
//! Pattern 2 step scheduler (StepScheduleBox).
//!
//! Decides the evaluation order for Pattern 2 lowering without hardcoding it
//! inside the lowerer. This keeps the lowerer focused on emitting fragments,
//! while this box decides how to interleave them (e.g., body-local init before
//! break checks when the break depends on body-local values).
use crate::config::env;
use crate::config::env::joinir_dev::joinir_test_debug_enabled;
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierInit};
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
/// Steps that can be reordered by the scheduler.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Pattern2StepKind {
HeaderCond,
BodyInit,
BreakCheck,
Updates,
Tail,
}
impl Pattern2StepKind {
fn as_str(&self) -> &'static str {
match self {
Pattern2StepKind::HeaderCond => "header-cond",
Pattern2StepKind::BodyInit => "body-init",
Pattern2StepKind::BreakCheck => "break",
Pattern2StepKind::Updates => "updates",
Pattern2StepKind::Tail => "tail",
}
}
}
/// Data-driven schedule for Pattern 2 lowering.
#[derive(Debug, Clone)]
pub(crate) struct Pattern2StepSchedule {
steps: Vec<Pattern2StepKind>,
reason: &'static str,
}
impl Pattern2StepSchedule {
pub(crate) fn iter(&self) -> impl Iterator<Item = Pattern2StepKind> + '_ {
self.steps.iter().copied()
}
pub(crate) fn reason(&self) -> &'static str {
self.reason
}
pub(crate) fn steps(&self) -> &[Pattern2StepKind] {
&self.steps
}
}
/// Minimal context for deciding the step order.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Pattern2ScheduleContext {
pub(crate) has_body_local_init: bool,
pub(crate) has_loop_local_carrier: bool,
}
impl Pattern2ScheduleContext {
pub(crate) fn from_env(
body_local_env: Option<&LoopBodyLocalEnv>,
carrier_info: &CarrierInfo,
) -> Self {
let has_body_local_init = body_local_env.map(|env| !env.is_empty()).unwrap_or(false);
let has_loop_local_carrier = carrier_info
.carriers
.iter()
.any(|c| matches!(c.init, CarrierInit::LoopLocalZero));
Self {
has_body_local_init,
has_loop_local_carrier,
}
}
fn requires_body_init_before_break(&self) -> bool {
self.has_body_local_init || self.has_loop_local_carrier
}
}
/// Build a schedule for Pattern 2 lowering.
///
/// - Default P2: header → break → body-init → updates → tail
/// - Body-local break dependency (DigitPos/_atoi style):
/// header → body-init → break → updates → tail
pub(crate) fn build_pattern2_schedule(ctx: &Pattern2ScheduleContext) -> Pattern2StepSchedule {
let schedule = if ctx.requires_body_init_before_break() {
Pattern2StepSchedule {
steps: vec![
Pattern2StepKind::HeaderCond,
Pattern2StepKind::BodyInit,
Pattern2StepKind::BreakCheck,
Pattern2StepKind::Updates,
Pattern2StepKind::Tail,
],
reason: "body-local break dependency",
}
} else {
Pattern2StepSchedule {
steps: vec![
Pattern2StepKind::HeaderCond,
Pattern2StepKind::BreakCheck,
Pattern2StepKind::BodyInit,
Pattern2StepKind::Updates,
Pattern2StepKind::Tail,
],
reason: "default",
}
};
log_schedule(ctx, &schedule);
schedule
}
fn log_schedule(ctx: &Pattern2ScheduleContext, schedule: &Pattern2StepSchedule) {
if !(env::joinir_dev_enabled() || joinir_test_debug_enabled()) {
return;
}
let steps_desc = schedule
.steps()
.iter()
.map(Pattern2StepKind::as_str)
.collect::<Vec<_>>()
.join(" -> ");
eprintln!(
"[joinir/p2-sched] steps={steps_desc} reason={} ctx={{body_local_init={}, loop_local_carrier={}}}",
schedule.reason(),
ctx.has_body_local_init,
ctx.has_loop_local_carrier
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mir::join_ir::lowering::carrier_info::{CarrierRole, CarrierVar};
use crate::mir::ValueId;
fn carrier(loop_local: bool) -> CarrierVar {
let init = if loop_local {
CarrierInit::LoopLocalZero
} else {
CarrierInit::FromHost
};
CarrierVar::with_role_and_init(
"c".to_string(),
ValueId(1),
CarrierRole::LoopState,
init,
)
}
fn carrier_info(carriers: Vec<CarrierVar>) -> CarrierInfo {
CarrierInfo {
loop_var_name: "i".to_string(),
loop_var_id: ValueId(0),
carriers,
trim_helper: None,
promoted_loopbodylocals: vec![],
}
}
#[test]
fn default_schedule_break_before_body_init() {
let ctx = Pattern2ScheduleContext::from_env(None, &carrier_info(vec![]));
let schedule = build_pattern2_schedule(&ctx);
assert_eq!(
schedule.steps(),
&[
Pattern2StepKind::HeaderCond,
Pattern2StepKind::BreakCheck,
Pattern2StepKind::BodyInit,
Pattern2StepKind::Updates,
Pattern2StepKind::Tail
]
);
assert_eq!(schedule.reason(), "default");
}
#[test]
fn body_local_moves_init_before_break() {
let mut body_env = LoopBodyLocalEnv::new();
body_env.insert("tmp".to_string(), ValueId(5));
let ctx =
Pattern2ScheduleContext::from_env(Some(&body_env), &carrier_info(vec![carrier(false)]));
let schedule = build_pattern2_schedule(&ctx);
assert_eq!(
schedule.steps(),
&[
Pattern2StepKind::HeaderCond,
Pattern2StepKind::BodyInit,
Pattern2StepKind::BreakCheck,
Pattern2StepKind::Updates,
Pattern2StepKind::Tail
]
);
assert_eq!(schedule.reason(), "body-local break dependency");
}
#[test]
fn loop_local_carrier_triggers_body_first() {
let ctx =
Pattern2ScheduleContext::from_env(None, &carrier_info(vec![carrier(true)]));
let schedule = build_pattern2_schedule(&ctx);
assert_eq!(
schedule.steps(),
&[
Pattern2StepKind::HeaderCond,
Pattern2StepKind::BodyInit,
Pattern2StepKind::BreakCheck,
Pattern2StepKind::Updates,
Pattern2StepKind::Tail
]
);
assert_eq!(schedule.reason(), "body-local break dependency");
}
}

View File

@ -51,8 +51,6 @@ pub use normalized::{
normalize_pattern1_minimal, normalize_pattern2_minimal, normalized_pattern1_to_structured,
normalized_pattern2_to_structured, NormalizedModule,
};
#[cfg(feature = "normalized_dev")]
pub use normalized::fixtures;
pub use verify::verify_progress_for_skip_ws;
// Phase 200-3: Contract verification functions are in merge/mod.rs (private module access)

View File

@ -93,6 +93,8 @@ pub enum JpOp {
Unary(UnaryOp),
Compare(CompareOp),
BoxCall { box_name: String, method: String },
/// 三項演算子cond ? then : else
Select,
}
/// Normalized JoinIR モジュール(テスト専用)。
@ -115,7 +117,7 @@ impl NormalizedModule {
#[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());
return Err("[joinir/normalized-dev] pattern1: phase must be Normalized".to_string());
}
// Env field bounds check
@ -127,7 +129,7 @@ fn verify_normalized_pattern1(module: &NormalizedModule) -> Result<(), String> {
JpInst::EnvLoad { field, .. } | JpInst::EnvStore { field, .. } => {
if *field >= field_count {
return Err(format!(
"Env field out of range: {} (fields={})",
"[joinir/normalized-dev] pattern1: env field out of range: {} (fields={})",
field, field_count
));
}
@ -156,7 +158,7 @@ fn verify_normalized_pattern1(module: &NormalizedModule) -> Result<(), String> {
JpInst::TailCallFn { .. } | JpInst::TailCallKont { .. } | JpInst::If { .. } => {}
_ => {
return Err(format!(
"Function '{}' does not end with tail call/if",
"[joinir/normalized-dev] pattern1: function '{}' does not end with tail call/if",
func.name
))
}
@ -221,6 +223,12 @@ pub fn normalized_pattern1_to_structured(norm: &NormalizedModule) -> JoinModule
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,
@ -300,8 +308,30 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule {
let mut max = 3;
#[cfg(feature = "normalized_dev")]
{
if shape_guard::is_jsonparser_atoi_mini(structured) {
max = 8;
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
@ -397,6 +427,18 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule {
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 {
@ -412,6 +454,19 @@ pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule {
});
}
}
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 {
@ -523,6 +578,14 @@ pub fn normalized_pattern2_to_structured(norm: &NormalizedModule) -> JoinModule
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,
@ -578,7 +641,9 @@ fn verify_normalized_pattern2(
max_env_fields: usize,
) -> Result<(), String> {
if module.phase != JoinIrPhase::Normalized {
return Err("Normalized verifier (Pattern2): phase must be Normalized".to_string());
return Err(
"[joinir/normalized-dev] pattern2: phase must be Normalized".to_string(),
);
}
let mut layout_sizes: HashMap<u32, usize> = HashMap::new();
@ -586,7 +651,7 @@ fn verify_normalized_pattern2(
let size = layout.fields.len();
if !(1..=max_env_fields).contains(&size) {
return Err(format!(
"Normalized Pattern2 expects 1..={} env fields, got {}",
"[joinir/normalized-dev] pattern2: expected 1..={} env fields, got {}",
max_env_fields, size
));
}
@ -615,7 +680,10 @@ fn verify_normalized_pattern2(
| 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());
return Err(
"[joinir/normalized-dev] pattern2: env must not be empty"
.to_string(),
);
}
let _ = expected;
}
@ -631,7 +699,7 @@ fn verify_normalized_pattern2(
| JpInst::If { .. } => {}
_ => {
return Err(format!(
"Function '{}' does not end with tail call/if",
"[joinir/normalized-dev] pattern2: function '{}' does not end with tail call/if",
func.name
));
}
@ -813,11 +881,14 @@ pub(crate) fn normalized_dev_roundtrip_structured(
return Err("[joinir/normalized-dev] module shape is not supported by normalized_dev".into());
}
let verbose = crate::config::env::joinir_dev_enabled();
let debug = dev_env::normalized_dev_logs_enabled() && crate::config::env::joinir_dev_enabled();
for shape in shapes {
if verbose {
eprintln!("[joinir/normalized-dev] attempting {:?} normalization", shape);
if debug {
eprintln!(
"[joinir/normalized-dev/roundtrip] attempting {:?} normalization",
shape
);
}
let attempt = match shape {
@ -827,7 +898,10 @@ pub(crate) fn normalized_dev_roundtrip_structured(
})),
NormalizedDevShape::Pattern2Mini
| NormalizedDevShape::JsonparserSkipWsMini
| NormalizedDevShape::JsonparserAtoiMini => catch_unwind(AssertUnwindSafe(|| {
| NormalizedDevShape::JsonparserSkipWsReal
| NormalizedDevShape::JsonparserAtoiMini
| NormalizedDevShape::JsonparserAtoiReal
| NormalizedDevShape::JsonparserParseNumberReal => catch_unwind(AssertUnwindSafe(|| {
let norm = normalize_pattern2_minimal(module);
normalized_pattern2_to_structured(&norm)
})),
@ -835,9 +909,9 @@ pub(crate) fn normalized_dev_roundtrip_structured(
match attempt {
Ok(structured) => {
if verbose {
if debug {
eprintln!(
"[joinir/normalized-dev] {:?} normalization succeeded (functions={})",
"[joinir/normalized-dev/roundtrip] {:?} normalization succeeded (functions={})",
shape,
structured.functions.len()
);
@ -845,9 +919,9 @@ pub(crate) fn normalized_dev_roundtrip_structured(
return Ok(structured);
}
Err(_) => {
if verbose {
if debug {
eprintln!(
"[joinir/normalized-dev] {:?} normalization failed (unsupported)",
"[joinir/normalized-dev/roundtrip] {:?} normalization failed (unsupported)",
shape
);
}

View File

@ -1,38 +1,111 @@
#![cfg(feature = "normalized_dev")]
use once_cell::sync::Lazy;
use std::sync::Mutex;
use std::sync::{Mutex, MutexGuard};
/// 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>,
active: bool,
}
static NORMALIZED_ENV_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
#[derive(Default)]
struct EnvState {
stack: Vec<Option<String>>,
}
static NORMALIZED_ENV_STATE: Lazy<Mutex<EnvState>> = Lazy::new(|| Mutex::new(EnvState::default()));
static NORMALIZED_TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
impl NormalizedDevEnvGuard {
pub fn new(enabled: bool) -> Self {
let lock = NORMALIZED_ENV_LOCK
let mut state = NORMALIZED_ENV_STATE
.lock()
.expect("normalized env mutex poisoned");
// Save current value before overriding.
let prev = std::env::var("NYASH_JOINIR_NORMALIZED_DEV_RUN").ok();
state.stack.push(prev);
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 }
Self { active: true }
}
}
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");
if !self.active {
return;
}
let mut state = NORMALIZED_ENV_STATE
.lock()
.expect("normalized env mutex poisoned");
if let Some(prev) = state.stack.pop() {
if let Some(prev) = prev {
std::env::set_var("NYASH_JOINIR_NORMALIZED_DEV_RUN", prev);
} else {
std::env::remove_var("NYASH_JOINIR_NORMALIZED_DEV_RUN");
}
}
}
}
/// normalized_dev feature + env の ON/OFF をまとめた判定。
pub fn normalized_dev_enabled() -> bool {
crate::config::env::normalized_dev_enabled()
}
/// normalized_dev かつ test/debug ログが有効なときだけ true。
pub fn normalized_dev_logs_enabled() -> bool {
crate::config::env::normalized_dev_enabled() && crate::config::env::joinir_test_debug_enabled()
}
/// テスト用コンテキストenv を ON にしつつロックで並列汚染を防ぐ。
pub struct NormalizedTestContext<'a> {
_lock: MutexGuard<'a, ()>,
_env_guard: NormalizedDevEnvGuard,
}
impl<'a> NormalizedTestContext<'a> {
fn new(lock: MutexGuard<'a, ()>) -> Self {
let env_guard = NormalizedDevEnvGuard::new(true);
NormalizedTestContext {
_lock: lock,
_env_guard: env_guard,
}
}
}
/// テストで使う共通ガード。
pub fn test_ctx() -> NormalizedTestContext<'static> {
let lock = NORMALIZED_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
NormalizedTestContext::new(lock)
}
/// 簡易ラッパー:クロージャを normalized_dev ON で実行。
pub fn with_dev_env<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
let _ctx = test_ctx();
f()
}
/// env が既に ON のときはそのまま、OFF のときだけ with_dev_env を噛ませる。
pub fn with_dev_env_if_unset<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
if normalized_dev_enabled() {
f()
} else {
with_dev_env(f)
}
}

View File

@ -9,6 +9,7 @@ use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr;
use crate::mir::join_ir::lowering::loop_with_break_minimal::lower_loop_with_break_minimal;
use crate::mir::join_ir::JoinModule;
use crate::mir::{BasicBlockId, ValueId};
use crate::{config::env::joinir_dev_enabled, config::env::joinir_test_debug_enabled};
use std::collections::{BTreeMap, BTreeSet};
/// Structured Pattern2 (joinir_min_loop 相当) をテスト用に生成するヘルパー。
@ -113,6 +114,54 @@ pub fn build_jsonparser_skip_ws_structured_for_normalized_dev() -> JoinModule {
lowerer.lower_program_json(&program_json)
}
/// JsonParser _skip_whitespace 本体相当の P2 ループを Structured で組み立てるヘルパー。
///
/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_skip_ws_real.program.json
pub fn build_jsonparser_skip_ws_real_structured_for_normalized_dev() -> JoinModule {
const FIXTURE: &str = include_str!(
"../../../../docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_skip_ws_real.program.json"
);
let program_json: serde_json::Value =
serde_json::from_str(FIXTURE).expect("jsonparser skip_ws real fixture should be valid JSON");
let mut lowerer = AstToJoinIrLowerer::new();
let module = lowerer.lower_program_json(&program_json);
if joinir_dev_enabled() && joinir_test_debug_enabled() {
eprintln!(
"[joinir/normalized-dev] jsonparser_skip_ws_real structured module: {:#?}",
module
);
}
module
}
/// JsonParser _parse_number 本体相当の P2 ループを Structured で組み立てるヘルパー。
///
/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_parse_number_real.program.json
pub fn build_jsonparser_parse_number_real_structured_for_normalized_dev() -> JoinModule {
const FIXTURE: &str = include_str!(
"../../../../docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_parse_number_real.program.json"
);
let program_json: serde_json::Value =
serde_json::from_str(FIXTURE).expect("jsonparser parse_number real fixture should be valid JSON");
let mut lowerer = AstToJoinIrLowerer::new();
let module = lowerer.lower_program_json(&program_json);
if joinir_dev_enabled() && joinir_test_debug_enabled() {
eprintln!(
"[joinir/normalized-dev] jsonparser_parse_number_real structured module: {:#?}",
module
);
}
module
}
/// JsonParser _atoi 相当のミニ P2 ループを Structured で組み立てるヘルパー。
///
/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_atoi_mini.program.json
@ -127,12 +176,48 @@ pub fn build_jsonparser_atoi_structured_for_normalized_dev() -> JoinModule {
let mut lowerer = AstToJoinIrLowerer::new();
let module = lowerer.lower_program_json(&program_json);
if std::env::var("JOINIR_TEST_DEBUG").is_ok() {
if joinir_dev_enabled() && joinir_test_debug_enabled() {
eprintln!(
"[joinir/normalized-dev] jsonparser_atoi_mini structured module: {:#?}",
"[joinir/normalized-dev] jsonparser_atoi_mini structured module: {:#?}",
module
);
}
module
}
/// JsonParser _atoi 本体相当の P2 ループを Structured で組み立てるヘルパー。
///
/// Fixture: docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_atoi_real.program.json
pub fn build_jsonparser_atoi_real_structured_for_normalized_dev() -> JoinModule {
const FIXTURE: &str = include_str!(
"../../../../docs/private/roadmap2/phases/normalized_dev/fixtures/jsonparser_atoi_real.program.json"
);
let program_json: serde_json::Value =
serde_json::from_str(FIXTURE).expect("jsonparser atoi real fixture should be valid JSON");
let mut lowerer = AstToJoinIrLowerer::new();
let module = lowerer.lower_program_json(&program_json);
if joinir_dev_enabled() && joinir_test_debug_enabled() {
eprintln!(
"[joinir/normalized-dev] jsonparser_atoi_real structured module: {:#?}",
module
);
}
module
}
/// まとめて import したいとき用のプレリュード。
pub mod prelude {
pub use super::{
build_jsonparser_atoi_real_structured_for_normalized_dev,
build_jsonparser_atoi_structured_for_normalized_dev,
build_jsonparser_parse_number_real_structured_for_normalized_dev,
build_jsonparser_skip_ws_real_structured_for_normalized_dev,
build_jsonparser_skip_ws_structured_for_normalized_dev,
build_pattern2_break_fixture_structured, build_pattern2_minimal_structured,
};
}

View File

@ -1,5 +1,7 @@
#![cfg(feature = "normalized_dev")]
use crate::config::env::joinir_dev_enabled;
use crate::mir::join_ir::normalized::dev_env;
use crate::mir::join_ir::{JoinFuncId, JoinFunction, JoinInst, JoinModule};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -7,105 +9,261 @@ pub(crate) enum NormalizedDevShape {
Pattern1Mini,
Pattern2Mini,
JsonparserSkipWsMini,
JsonparserSkipWsReal,
JsonparserAtoiMini,
JsonparserAtoiReal,
JsonparserParseNumberReal,
}
/// 直接 Normalized→MIR ブリッジで扱う shape を返すdev 限定)。
type Detector = fn(&JoinModule) -> bool;
const SHAPE_DETECTORS: &[(NormalizedDevShape, Detector)] = &[
(NormalizedDevShape::Pattern1Mini, detectors::is_pattern1_mini),
(NormalizedDevShape::Pattern2Mini, detectors::is_pattern2_mini),
(
NormalizedDevShape::JsonparserSkipWsMini,
detectors::is_jsonparser_skip_ws_mini,
),
(
NormalizedDevShape::JsonparserSkipWsReal,
detectors::is_jsonparser_skip_ws_real,
),
(
NormalizedDevShape::JsonparserAtoiMini,
detectors::is_jsonparser_atoi_mini,
),
(
NormalizedDevShape::JsonparserAtoiReal,
detectors::is_jsonparser_atoi_real,
),
(
NormalizedDevShape::JsonparserParseNumberReal,
detectors::is_jsonparser_parse_number_real,
),
];
/// direct ブリッジで扱う shapedev 限定)。
pub(crate) fn direct_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
supported_shapes(module)
let shapes = detect_shapes(module);
log_shapes("direct", &shapes);
shapes
}
/// Structured→Normalized の対象 shapedev 限定)。
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);
}
if is_pattern2_mini(module) {
shapes.push(NormalizedDevShape::Pattern2Mini);
}
if is_pattern1_mini(module) {
shapes.push(NormalizedDevShape::Pattern1Mini);
}
let shapes = detect_shapes(module);
log_shapes("roundtrip", &shapes);
shapes
}
/// canonical常時 Normalized 経路を通す)対象。
/// Phase 41: P2 コアセットP2 mini + JP skip_ws mini/real + JP atoi mini
pub(crate) fn canonical_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
let shapes: Vec<_> = detect_shapes(module)
.into_iter()
.filter(|s| {
matches!(
s,
NormalizedDevShape::Pattern2Mini
| NormalizedDevShape::JsonparserSkipWsMini
| NormalizedDevShape::JsonparserSkipWsReal
| NormalizedDevShape::JsonparserAtoiMini
)
})
.collect();
log_shapes("canonical", &shapes);
shapes
}
#[allow(dead_code)]
pub(crate) fn is_direct_supported(module: &JoinModule) -> bool {
!direct_shapes(module).is_empty()
!detect_shapes(module).is_empty()
}
pub(crate) fn is_pattern1_mini(module: &JoinModule) -> bool {
module.is_structured() && find_loop_step(module).is_some()
}
fn detect_shapes(module: &JoinModule) -> Vec<NormalizedDevShape> {
let mut shapes: Vec<_> = SHAPE_DETECTORS
.iter()
.filter_map(|(shape, detector)| if detector(module) { Some(*shape) } else { None })
.collect();
pub(crate) fn is_pattern2_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 !(1..=3).contains(&loop_func.params.len()) {
return false;
// Pattern1 は「最小の後方互換」なので、より具体的な shape が見つかった場合は外しておく。
if shapes.len() > 1 {
shapes.retain(|s| *s != NormalizedDevShape::Pattern1Mini);
}
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
shapes
}
pub(crate) fn is_jsonparser_skip_ws_mini(module: &JoinModule) -> bool {
is_pattern2_mini(module)
&& module
// --- 判定ロジック(共通) ---
mod detectors {
use super::*;
pub(super) fn is_pattern1_mini(module: &JoinModule) -> bool {
module.is_structured() && find_loop_step(module).is_some()
}
pub(super) fn is_pattern2_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 !(1..=3).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
}
pub(super) fn is_jsonparser_skip_ws_mini(module: &JoinModule) -> bool {
is_pattern2_mini(module)
&& module
.functions
.values()
.any(|f| f.name == "jsonparser_skip_ws_mini")
}
pub(crate) fn is_jsonparser_skip_ws_real(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 !(2..=6).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 == "jsonparser_skip_ws_real")
}
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 == "jsonparser_atoi_mini")
}
pub(crate) fn is_jsonparser_atoi_real(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..=10).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 == "jsonparser_atoi_real")
}
pub(crate) fn is_jsonparser_parse_number_real(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..=12).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 == "jsonparser_parse_number_real")
}
pub(super) fn find_loop_step(module: &JoinModule) -> Option<&JoinFunction> {
module
.functions
.values()
.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;
.find(|f| f.name == "loop_step")
.or_else(|| module.functions.get(&JoinFuncId::new(1)))
}
let loop_func = match find_loop_step(module) {
Some(f) => f,
None => return false,
};
if !(3..=8).contains(&loop_func.params.len()) {
return false;
}
fn log_shapes(tag: &str, shapes: &[NormalizedDevShape]) {
if shapes.is_empty() {
return;
}
if dev_env::normalized_dev_logs_enabled() && joinir_dev_enabled() {
eprintln!("[joinir/normalized-dev/shape] {}: {:?}", tag, shapes);
}
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
.values()
.find(|f| f.name == "loop_step")
.or_else(|| module.functions.get(&JoinFuncId::new(1)))
}

View File

@ -34,7 +34,9 @@ use std::collections::HashMap;
use crate::config::env::normalized_dev_enabled;
use crate::mir::join_ir::{ConstValue, JoinFuncId, JoinInst, JoinModule, MirLikeInst, VarId};
#[cfg(feature = "normalized_dev")]
use crate::mir::join_ir::normalized::{normalized_dev_roundtrip_structured, shape_guard};
use crate::mir::join_ir::normalized::{
dev_env, normalized_dev_roundtrip_structured, shape_guard,
};
// Phase 27.8: ops box からの再エクスポート
pub use crate::mir::join_ir_ops::{JoinIrOpError, JoinValue};
@ -64,31 +66,32 @@ fn run_joinir_function_normalized_dev(
args: &[JoinValue],
) -> Result<JoinValue, JoinRuntimeError> {
// Keep dev path opt-in and fail-fast: only Structured P1/P2 minis are supported.
let verbose = crate::config::env::joinir_dev_enabled();
let args_vec = args.to_vec();
dev_env::with_dev_env_if_unset(|| {
let debug = dev_env::normalized_dev_logs_enabled();
let args_vec = args.to_vec();
let shapes = shape_guard::supported_shapes(module);
if shapes.is_empty() {
if verbose {
let shapes = shape_guard::supported_shapes(module);
if shapes.is_empty() {
if debug {
eprintln!("[joinir/normalized-dev/runner] shape unsupported; staying on Structured path");
}
return execute_function(vm, module, entry, args_vec);
}
let structured_roundtrip = normalized_dev_roundtrip_structured(module).map_err(|msg| {
JoinRuntimeError::new(format!("[joinir/normalized-dev/runner] {}", msg))
})?;
if debug {
eprintln!(
"[joinir/runner/normalized-dev] shape unsupported; staying on Structured path"
"[joinir/normalized-dev/runner] normalized roundtrip succeeded (shapes={:?}, functions={})",
shapes,
structured_roundtrip.functions.len()
);
}
return execute_function(vm, module, entry, args_vec);
}
let structured_roundtrip = normalized_dev_roundtrip_structured(module)
.map_err(|msg| JoinRuntimeError::new(format!("[joinir/runner/normalized-dev] {}", msg)))?;
if verbose {
eprintln!(
"[joinir/runner/normalized-dev] normalized roundtrip succeeded (shapes={:?}, functions={})",
shapes,
structured_roundtrip.functions.len()
);
}
execute_function(vm, &structured_roundtrip, entry, args_vec)
execute_function(vm, &structured_roundtrip, entry, args_vec)
})
}
fn execute_function(

View File

@ -62,7 +62,10 @@ fn normalize_for_shape(
}
NormalizedDevShape::Pattern2Mini
| NormalizedDevShape::JsonparserSkipWsMini
| NormalizedDevShape::JsonparserAtoiMini => {
| NormalizedDevShape::JsonparserSkipWsReal
| NormalizedDevShape::JsonparserAtoiMini
| NormalizedDevShape::JsonparserAtoiReal
| NormalizedDevShape::JsonparserParseNumberReal => {
catch_unwind(AssertUnwindSafe(|| normalize_pattern2_minimal(module)))
}
};
@ -80,44 +83,79 @@ fn normalize_for_shape(
fn try_normalized_direct_bridge(
module: &JoinModule,
meta: &JoinFuncMetaMap,
shapes: &[NormalizedDevShape],
allow_structured_fallback: bool,
use_env_guard: bool,
) -> Result<Option<MirModule>, JoinIrVmBridgeError> {
let shapes = shape_guard::direct_shapes(module);
if shapes.is_empty() {
return Ok(None);
crate::mir::join_ir_vm_bridge::normalized_bridge::log_dev(
"fallback",
"normalized dev enabled but shape unsupported; using Structured path",
true,
);
return if allow_structured_fallback {
Ok(None)
} else {
Err(JoinIrVmBridgeError::new(
"[joinir/bridge] canonical normalized route requested but shape unsupported",
))
};
}
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));
let exec = || {
let debug = crate::mir::join_ir::normalized::dev_env::normalized_dev_logs_enabled();
for &shape in shapes {
if debug {
crate::mir::join_ir_vm_bridge::normalized_bridge::log_dev(
"direct",
format!("attempting normalized→MIR for {:?}", shape),
false,
);
}
Err(err) => {
if verbose {
eprintln!(
"[joinir/bridge] {:?} normalization failed: {} (continuing)",
shape, err.message
match normalize_for_shape(module, shape) {
Ok(norm) => {
let mir =
lower_normalized_to_mir_minimal(&norm, meta, allow_structured_fallback)?;
crate::mir::join_ir_vm_bridge::normalized_bridge::log_dev(
"direct",
format!(
"normalized→MIR succeeded (shape={:?}, functions={})",
shape,
norm.functions.len()
),
false,
);
return Ok(Some(mir));
}
Err(err) => {
if debug {
crate::mir::join_ir_vm_bridge::normalized_bridge::log_dev(
"direct",
format!(
"{:?} normalization failed: {} (continuing)",
shape, err.message
),
false,
);
}
}
}
}
}
Err(JoinIrVmBridgeError::new(
"[joinir/bridge] normalized_dev enabled but no normalization attempt succeeded",
))
if allow_structured_fallback {
Ok(None)
} else {
Err(JoinIrVmBridgeError::new(
"[joinir/bridge] canonical normalized route failed for all shapes",
))
}
};
if use_env_guard {
crate::mir::join_ir::normalized::dev_env::with_dev_env_if_unset(exec)
} else {
exec()
}
}
/// JoinIR → MIR の単一入口。Normalized dev が有効なら roundtrip してから既存経路へ。
@ -125,18 +163,28 @@ pub(crate) fn bridge_joinir_to_mir_with_meta(
module: &JoinModule,
meta: &JoinFuncMetaMap,
) -> Result<MirModule, JoinIrVmBridgeError> {
if crate::config::env::normalized_dev_enabled() {
#[cfg(feature = "normalized_dev")]
{
match try_normalized_direct_bridge(module, meta)? {
#[cfg(feature = "normalized_dev")]
{
// Phase 41: canonical shapes は env が OFF でも常に Normalized → MIR を通す。
let canonical_shapes = shape_guard::canonical_shapes(module);
if !canonical_shapes.is_empty() {
match try_normalized_direct_bridge(module, meta, &canonical_shapes, false, false)? {
Some(mir) => return Ok(mir),
None => {
debug_log!(
"[joinir/bridge] normalized dev enabled but shape unsupported; falling back to Structured path"
);
return Err(JoinIrVmBridgeError::new(
"[joinir/bridge] canonical normalized route returned None unexpectedly",
))
}
}
}
if crate::config::env::normalized_dev_enabled() {
let shapes = shape_guard::direct_shapes(module);
match try_normalized_direct_bridge(module, meta, &shapes, true, true)? {
Some(mir) => return Ok(mir),
None => {}
}
}
}
lower_joinir_structured_to_mir_with_meta(module, meta)

View File

@ -11,6 +11,22 @@ use crate::mir::MirModule;
mod direct;
fn dev_debug_enabled() -> bool {
crate::mir::join_ir::normalized::dev_env::normalized_dev_logs_enabled()
}
/// Dev logging helper with unified category prefix.
pub(super) fn log_dev(category: &str, message: impl AsRef<str>, important: bool) {
let debug = dev_debug_enabled();
if debug || important {
eprintln!("[joinir/normalized-dev/bridge/{}] {}", category, message.as_ref());
}
}
pub(super) fn log_debug(category: &str, message: impl AsRef<str>) {
log_dev(category, message, false);
}
/// Direct Normalized → MIR 変換が未対応のときに使うフォールバック。
fn lower_normalized_via_structured(
norm: &NormalizedModule,
@ -24,9 +40,14 @@ fn lower_normalized_via_structured(
normalized_pattern2_to_structured(norm)
};
if crate::config::env::joinir_dev_enabled() {
eprintln!("[joinir/normalized-bridge/fallback] using structured path (functions={})", structured.functions.len());
}
log_dev(
"fallback",
format!(
"using structured path (functions={})",
structured.functions.len()
),
true,
);
lower_joinir_structured_to_mir_with_meta(&structured, meta)
}
@ -35,6 +56,7 @@ fn lower_normalized_via_structured(
pub(crate) fn lower_normalized_to_mir_minimal(
norm: &NormalizedModule,
meta: &JoinFuncMetaMap,
allow_structured_fallback: bool,
) -> Result<MirModule, JoinIrVmBridgeError> {
if norm.phase != JoinIrPhase::Normalized {
return Err(JoinIrVmBridgeError::new(
@ -42,11 +64,14 @@ pub(crate) fn lower_normalized_to_mir_minimal(
));
}
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()
if dev_debug_enabled() {
log_debug(
"debug",
format!(
"lowering normalized module (functions={}, env_layouts={})",
norm.functions.len(),
norm.env_layouts.len()
),
);
for layout in &norm.env_layouts {
let fields: Vec<String> = layout
@ -54,19 +79,21 @@ pub(crate) fn lower_normalized_to_mir_minimal(
.iter()
.map(|f| format!("{}={:?}", f.name, f.value_id))
.collect();
eprintln!(
"[joinir/normalized-bridge] env_layout {} fields: {}",
layout.id,
fields.join(", ")
log_debug(
"debug",
format!("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()
log_debug(
"debug",
format!(
"fn {} (id={:?}) env_layout={:?} body_len={}",
func.name,
func.id,
func.env_layout,
func.body.len()
),
);
}
}
@ -74,11 +101,20 @@ pub(crate) fn lower_normalized_to_mir_minimal(
// direct 対象は Normalized → MIR をそのまま吐く。未対応 shape は Structured 経由にフォールバック。
match direct::lower_normalized_direct_minimal(norm) {
Ok(mir) => Ok(mir),
Err(err) => {
if crate::config::env::joinir_dev_enabled() {
eprintln!("[joinir/normalized-bridge/fallback] direct path failed: {}; falling back to Structured path", err.message);
}
Err(err) if allow_structured_fallback => {
log_dev(
"fallback",
format!(
"direct path failed: {}; falling back to Structured path",
err.message
),
true,
);
lower_normalized_via_structured(norm, meta)
}
Err(err) => Err(JoinIrVmBridgeError::new(format!(
"[joinir/normalized-bridge] direct path failed and fallback disabled: {}",
err.message
))),
}
}

View File

@ -4,7 +4,6 @@ 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::{
@ -20,19 +19,20 @@ pub(crate) fn lower_normalized_direct_minimal(
) -> Result<MirModule, JoinIrVmBridgeError> {
if norm.phase != JoinIrPhase::Normalized {
return Err(JoinIrVmBridgeError::new(
"[joinir/normalized-bridge] expected Normalized JoinIR module",
"[joinir/normalized-bridge/direct] 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={})",
let debug_dump = crate::mir::join_ir::normalized::dev_env::normalized_dev_logs_enabled();
super::log_dev(
"direct",
format!(
"using direct normalized bridge (functions={}, env_layouts={})",
norm.functions.len(),
norm.env_layouts.len()
);
}
),
false,
);
let mut mir_module = MirModule::new("joinir_normalized_direct".to_string());
@ -42,9 +42,9 @@ pub(crate) fn lower_normalized_direct_minimal(
}
if debug_dump {
eprintln!(
"[joinir/normalized-bridge/direct] produced MIR (debug dump): {:#?}",
mir_module
super::log_debug(
"direct",
format!("produced MIR (debug dump): {:#?}", mir_module),
);
}
@ -55,7 +55,6 @@ 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));
@ -108,12 +107,16 @@ fn lower_normalized_function_direct(
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()
);
}
super::log_debug(
"direct",
format!(
"lowering fn={} params={:?} remapped_params={:?} body_len={}",
func.name,
params,
remapped_params,
func.body.len()
),
);
for inst in &func.body {
if terminated {
@ -122,6 +125,56 @@ fn lower_normalized_function_direct(
match inst {
JpInst::Let { dst, op, args } => {
if matches!(op, JpOp::Select) {
let cond = remap(*args.get(0).unwrap_or(&ValueId(0)), &mut value_map);
let then_val = remap(*args.get(1).unwrap_or(&ValueId(0)), &mut value_map);
let else_val = remap(*args.get(2).unwrap_or(&ValueId(0)), &mut value_map);
let remapped_dst = remap(*dst, &mut value_map);
let then_bb = BasicBlockId(next_block_id);
next_block_id += 1;
let else_bb = BasicBlockId(next_block_id);
next_block_id += 1;
let merge_bb = BasicBlockId(next_block_id);
next_block_id += 1;
finalize_block(
&mut mir_func,
current_block_id,
mem::take(&mut current_insts),
MirInstruction::Branch {
condition: cond,
then_bb,
else_bb,
},
None,
);
finalize_block(
&mut mir_func,
then_bb,
Vec::new(),
MirInstruction::Jump { target: merge_bb },
None,
);
finalize_block(
&mut mir_func,
else_bb,
Vec::new(),
MirInstruction::Jump { target: merge_bb },
None,
);
current_block_id = merge_bb;
current_insts = vec![MirInstruction::Phi {
dst: remapped_dst,
inputs: vec![(then_bb, then_val), (else_bb, else_val)],
type_hint: None,
}];
mir_func.next_value_id = value_map.len() as u32;
continue;
}
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)?;
@ -276,6 +329,17 @@ fn jp_op_to_mir_like(
lhs: args.get(0).copied().unwrap_or(ValueId(0)),
rhs: args.get(1).copied().unwrap_or(ValueId(0)),
}),
JpOp::Select => {
let cond = args.get(0).copied().unwrap_or(ValueId(0));
let then_val = args.get(1).copied().unwrap_or(ValueId(0));
let else_val = args.get(2).copied().unwrap_or(ValueId(0));
Ok(MirLikeInst::Select {
dst,
cond,
then_val,
else_val,
})
}
JpOp::BoxCall { box_name, method } => Ok(MirLikeInst::BoxCall {
dst: Some(dst),
box_name: box_name.clone(),

View File

@ -2,6 +2,7 @@
//!
//! 目的: フィクスチャベースの AST→JoinIR テストを簡潔に書けるようにする
use crate::config::env::joinir_test_debug_enabled;
use crate::mir::join_ir::frontend::AstToJoinIrLowerer;
use crate::mir::join_ir::JoinModule;
use crate::mir::join_ir_ops::JoinValue;
@ -22,7 +23,7 @@ impl JoinIrFrontendTestRunner {
Self {
fixture_path: fixture_path.to_string(),
join_module: None,
debug_enabled: std::env::var("JOINIR_TEST_DEBUG").is_ok(),
debug_enabled: joinir_test_debug_enabled(),
}
}