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

@ -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)))
}