feat(joinir): Phase 33-8 Stage-1 rollout infrastructure

Add environment variable controls and debug logging for JoinIR lowering rollout.

Changes:
- Add HAKO_JOINIR_STAGE1 env var for Stage-1 function rollout control
- Add HAKO_JOINIR_DEBUG (0-3) for granular debug logging
  - Level 0: Silent (default)
  - Level 1: Basic lowering info
  - Level 2: Pattern matching details
  - Level 3: Full variable/instruction dump
- Implement 3-tier whitelist system:
  - Tier 1: Test functions (always enabled)
  - Tier 2: Stage-1 rollout (env-controlled)
  - Tier 3: Explicit approvals (validated in Phase 33-4)
- Add A/B test automation script (tools/joinir_ab_test.sh)
- Update if_merge.rs and if_select.rs with debug_level support

Environment variables (with NYASH_* fallback for compatibility):
- HAKO_JOINIR_IF_SELECT: Enable JoinIR lowering
- HAKO_JOINIR_STAGE1: Enable Stage-1 function rollout
- HAKO_JOINIR_DEBUG: Debug log level (0-3)

A/B test verification: PASSED on joinir_if_merge_{simple,multiple}.hako

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-27 09:30:54 +09:00
parent 5b7818f5c9
commit 517b20fe88
5 changed files with 211 additions and 39 deletions

View File

@ -218,9 +218,51 @@ pub fn joinir_llvm_experiment_enabled() -> bool {
} }
/// Phase 33: JoinIR If Select 実験の有効化 /// Phase 33: JoinIR If Select 実験の有効化
/// Set NYASH_JOINIR_IF_SELECT=1 to enable experimental If/Else → Select lowering. /// Primary: HAKO_JOINIR_IF_SELECT (Phase 33-8+). Fallback: NYASH_JOINIR_IF_SELECT (deprecated).
pub fn joinir_if_select_enabled() -> bool { pub fn joinir_if_select_enabled() -> bool {
env_bool("NYASH_JOINIR_IF_SELECT") // Primary: HAKO_JOINIR_IF_SELECT
if let Some(v) = env_flag("HAKO_JOINIR_IF_SELECT") {
return v;
}
// Fallback: NYASH_JOINIR_IF_SELECT (deprecated)
if env_bool("NYASH_JOINIR_IF_SELECT") {
warn_alias_once("NYASH_JOINIR_IF_SELECT", "HAKO_JOINIR_IF_SELECT");
return true;
}
false
}
/// Phase 33-8: JoinIR Stage-1 rollout toggle
/// Set HAKO_JOINIR_STAGE1=1 to enable JoinIR lowering for Stage-1 functions.
pub fn joinir_stage1_enabled() -> bool {
// Primary: HAKO_JOINIR_STAGE1
if let Some(v) = env_flag("HAKO_JOINIR_STAGE1") {
return v;
}
// Fallback: NYASH_JOINIR_STAGE1 (deprecated)
if env_bool("NYASH_JOINIR_STAGE1") {
warn_alias_once("NYASH_JOINIR_STAGE1", "HAKO_JOINIR_STAGE1");
return true;
}
false
}
/// Phase 33-8: JoinIR debug log level (0-3)
/// - 0: No logs (default)
/// - 1: Basic logs (which functions were lowered)
/// - 2: Pattern matching details (CFG analysis)
/// - 3: Full dump (all variables, all instructions)
pub fn joinir_debug_level() -> u8 {
// Primary: HAKO_JOINIR_DEBUG
if let Ok(v) = std::env::var("HAKO_JOINIR_DEBUG") {
return v.parse().unwrap_or(0);
}
// Fallback: NYASH_JOINIR_DEBUG (deprecated)
if let Ok(v) = std::env::var("NYASH_JOINIR_DEBUG") {
warn_alias_once("NYASH_JOINIR_DEBUG", "HAKO_JOINIR_DEBUG");
return v.parse().unwrap_or(0);
}
0
} }
// VM legacy by-name call fallback was removed (Phase 2 complete). // VM legacy by-name call fallback was removed (Phase 2 complete).

View File

@ -11,7 +11,7 @@ use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
use std::collections::HashSet; use std::collections::HashSet;
pub struct IfMergeLowerer { pub struct IfMergeLowerer {
debug: bool, debug_level: u8,
} }
/// 検出された IfMerge パターン情報 /// 検出された IfMerge パターン情報
@ -30,8 +30,13 @@ struct IfBranch {
} }
impl IfMergeLowerer { impl IfMergeLowerer {
pub fn new(debug: bool) -> Self { pub fn new(debug_level: u8) -> Self {
Self { debug } Self { debug_level }
}
/// Phase 33-8: debug-level backward compat wrapper
pub fn with_debug(debug: bool) -> Self {
Self { debug_level: if debug { 1 } else { 0 } }
} }
/// if/else が IfMerge に lowering できるかチェック /// if/else が IfMerge に lowering できるかチェック
@ -47,13 +52,25 @@ impl IfMergeLowerer {
) -> Option<JoinInst> { ) -> Option<JoinInst> {
let pattern = self.find_if_merge_pattern(func, if_block_id)?; let pattern = self.find_if_merge_pattern(func, if_block_id)?;
if self.debug { // Phase 33-8: Level 1 - Basic lowering info
if self.debug_level >= 1 {
eprintln!( eprintln!(
"[IfMergeLowerer] lowering to IfMerge with {} merge pairs", "[IfMergeLowerer] lowering to IfMerge with {} merge pairs",
pattern.merge_pairs.len() pattern.merge_pairs.len()
); );
} }
// Phase 33-8: Level 3 - Full merge details
if self.debug_level >= 3 {
eprintln!("[IfMergeLowerer] cond: {:?}", pattern.cond);
for (i, pair) in pattern.merge_pairs.iter().enumerate() {
eprintln!(
"[IfMergeLowerer] pair[{}]: dst={:?}, then={:?}, else={:?}",
i, pair.dst, pair.then_val, pair.else_val
);
}
}
// IfMerge 命令を生成 // IfMerge 命令を生成
Some(JoinInst::IfMerge { Some(JoinInst::IfMerge {
cond: pattern.cond, cond: pattern.cond,
@ -99,9 +116,10 @@ impl IfMergeLowerer {
); );
if !is_then_return || !is_else_return { if !is_then_return || !is_else_return {
if self.debug { // Phase 33-8: Level 2 - Pattern matching details
if self.debug_level >= 2 {
eprintln!( eprintln!(
"[IfMergeLowerer] not return pattern (then={}, else={})", "[IfMergeLowerer] not return pattern (then={}, else={})",
is_then_return, is_else_return is_then_return, is_else_return
); );
} }
@ -112,7 +130,8 @@ impl IfMergeLowerer {
let then_writes = self.extract_written_vars(&then_block.instructions); let then_writes = self.extract_written_vars(&then_block.instructions);
let else_writes = self.extract_written_vars(&else_block.instructions); let else_writes = self.extract_written_vars(&else_block.instructions);
if self.debug { // Phase 33-8: Level 3 - Full variable dump
if self.debug_level >= 3 {
eprintln!( eprintln!(
"[IfMergeLowerer] then writes: {:?}, else writes: {:?}", "[IfMergeLowerer] then writes: {:?}, else writes: {:?}",
then_writes, else_writes then_writes, else_writes
@ -123,8 +142,9 @@ impl IfMergeLowerer {
let common_writes: HashSet<_> = then_writes.intersection(&else_writes).copied().collect(); let common_writes: HashSet<_> = then_writes.intersection(&else_writes).copied().collect();
if common_writes.is_empty() { if common_writes.is_empty() {
if self.debug { // Phase 33-8: Level 2 - Pattern matching details
eprintln!("[IfMergeLowerer] no common writes found"); if self.debug_level >= 2 {
eprintln!("[IfMergeLowerer] ❌ no common writes found");
} }
return None; return None;
} }
@ -230,7 +250,10 @@ mod tests {
#[test] #[test]
fn test_if_merge_lowerer_creation() { fn test_if_merge_lowerer_creation() {
let lowerer = IfMergeLowerer::new(false); let lowerer = IfMergeLowerer::new(0);
assert!(!lowerer.debug); assert_eq!(lowerer.debug_level, 0);
let lowerer_compat = IfMergeLowerer::with_debug(true);
assert_eq!(lowerer_compat.debug_level, 1);
} }
} }

View File

@ -6,7 +6,7 @@ use crate::mir::join_ir::JoinInst;
use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId}; use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
pub struct IfSelectLowerer { pub struct IfSelectLowerer {
debug: bool, debug_level: u8,
} }
/// If/Else パターンの分類 /// If/Else パターンの分類
@ -37,8 +37,13 @@ struct IfBranch {
} }
impl IfSelectLowerer { impl IfSelectLowerer {
pub fn new(debug: bool) -> Self { pub fn new(debug_level: u8) -> Self {
Self { debug } Self { debug_level }
}
/// Phase 33-8: debug-level backward compat wrapper
pub fn with_debug(debug: bool) -> Self {
Self { debug_level: if debug { 1 } else { 0 } }
} }
/// if/else が Select に lowering できるかチェック /// if/else が Select に lowering できるかチェック
@ -54,13 +59,22 @@ impl IfSelectLowerer {
) -> Option<JoinInst> { ) -> Option<JoinInst> {
let pattern = self.find_if_pattern(func, if_block_id)?; let pattern = self.find_if_pattern(func, if_block_id)?;
if self.debug { // Phase 33-8: Level 1 - Basic lowering info
if self.debug_level >= 1 {
eprintln!( eprintln!(
"[IfSelectLowerer] lowering {:?} pattern to Select", "[IfSelectLowerer] lowering {:?} pattern to Select",
pattern.pattern_type pattern.pattern_type
); );
} }
// Phase 33-8: Level 3 - Full pattern details
if self.debug_level >= 3 {
eprintln!("[IfSelectLowerer] cond: {:?}", pattern.cond);
eprintln!("[IfSelectLowerer] then_val: {:?}", pattern.then_val);
eprintln!("[IfSelectLowerer] else_val: {:?}", pattern.else_val);
eprintln!("[IfSelectLowerer] dst: {:?}", pattern.dst);
}
// Select 命令を生成 // Select 命令を生成
let dst = pattern.dst.unwrap_or(pattern.then_val); let dst = pattern.dst.unwrap_or(pattern.then_val);
@ -99,8 +113,9 @@ impl IfSelectLowerer {
// 3. simple パターンのチェック // 3. simple パターンのチェック
if let Some(pattern) = self.try_match_simple_pattern(&branch, then_block, else_block) { if let Some(pattern) = self.try_match_simple_pattern(&branch, then_block, else_block) {
if self.debug { // Phase 33-8: Level 2 - Pattern matching details
eprintln!("[IfSelectLowerer] matched simple pattern"); if self.debug_level >= 2 {
eprintln!("[IfSelectLowerer] ✅ matched simple pattern");
} }
return Some(pattern); return Some(pattern);
} }
@ -108,14 +123,16 @@ impl IfSelectLowerer {
// 4. local パターンのチェック // 4. local パターンのチェック
if let Some(pattern) = self.try_match_local_pattern(func, &branch, then_block, else_block) if let Some(pattern) = self.try_match_local_pattern(func, &branch, then_block, else_block)
{ {
if self.debug { // Phase 33-8: Level 2 - Pattern matching details
eprintln!("[IfSelectLowerer] matched local pattern"); if self.debug_level >= 2 {
eprintln!("[IfSelectLowerer] ✅ matched local pattern");
} }
return Some(pattern); return Some(pattern);
} }
if self.debug { // Phase 33-8: Level 2 - Pattern matching details
eprintln!("[IfSelectLowerer] no pattern matched"); if self.debug_level >= 2 {
eprintln!("[IfSelectLowerer] ❌ no pattern matched");
} }
None None
} }

View File

@ -73,15 +73,29 @@ pub fn try_lower_if_to_joinir(
return None; return None;
} }
// 2. Phase 33-7: 関数名ガード拡張(テスト + Stage-1/Stage-B 候補 // Phase 33-8: デバッグログレベル取得0-3
let is_allowed = func.signature.name.starts_with("IfSelectTest.") let debug_level = crate::config::env::joinir_debug_level();
|| func.signature.name.starts_with("IfMergeTest.") // Phase 33-7 let _debug = debug || debug_level >= 1;
|| func.signature.name.starts_with("Stage1JsonScannerTestBox.") // Phase 33-5 test
|| func.signature.name == "JsonShapeToMap._read_value_from_pair/1" // 2. Phase 33-8: 関数名ガード拡張(テスト + Stage-1 rollout + 明示承認)
|| func.signature.name == "Stage1JsonScannerBox.value_start_after_key_pos/2"; let is_allowed =
// Test functions (always enabled)
func.signature.name.starts_with("IfSelectTest.") ||
func.signature.name.starts_with("IfMergeTest.") ||
func.signature.name.starts_with("Stage1JsonScannerTestBox.") || // Phase 33-5 test
// Stage-1 rollout (env-controlled)
(crate::config::env::joinir_stage1_enabled() &&
func.signature.name.starts_with("Stage1")) ||
// Explicit approvals (Phase 33-4で検証済み, always on)
matches!(func.signature.name.as_str(),
"JsonShapeToMap._read_value_from_pair/1" |
"Stage1JsonScannerBox.value_start_after_key_pos/2"
);
if !is_allowed { if !is_allowed {
if debug { if debug_level >= 2 {
eprintln!( eprintln!(
"[try_lower_if_to_joinir] skipping non-allowed function: {}", "[try_lower_if_to_joinir] skipping non-allowed function: {}",
func.signature.name func.signature.name
@ -90,15 +104,19 @@ pub fn try_lower_if_to_joinir(
return None; return None;
} }
if debug_level >= 1 {
eprintln!("[try_lower_if_to_joinir] trying to lower {}", func.signature.name);
}
// 3. Phase 33-7: IfMerge を優先的に試行(複数変数パターン) // 3. Phase 33-7: IfMerge を優先的に試行(複数変数パターン)
// IfMerge が成功すればそれを返す、失敗したら Select を試行 // IfMerge が成功すればそれを返す、失敗したら Select を試行
let if_merge_lowerer = if_merge::IfMergeLowerer::new(debug); let if_merge_lowerer = if_merge::IfMergeLowerer::new(debug_level);
if if_merge_lowerer.can_lower_to_if_merge(func, block_id) { if if_merge_lowerer.can_lower_to_if_merge(func, block_id) {
if let Some(result) = if_merge_lowerer.lower_if_to_if_merge(func, block_id) { if let Some(result) = if_merge_lowerer.lower_if_to_if_merge(func, block_id) {
if debug { if debug_level >= 1 {
eprintln!( eprintln!(
"[try_lower_if_to_joinir] IfMerge lowering used for {}", "[try_lower_if_to_joinir] IfMerge lowering used for {}",
func.signature.name func.signature.name
); );
} }
@ -107,10 +125,10 @@ pub fn try_lower_if_to_joinir(
} }
// 4. IfMerge が失敗したら Select を試行(単一変数パターン) // 4. IfMerge が失敗したら Select を試行(単一変数パターン)
let if_select_lowerer = if_select::IfSelectLowerer::new(debug); let if_select_lowerer = if_select::IfSelectLowerer::new(debug_level);
if !if_select_lowerer.can_lower_to_select(func, block_id) { if !if_select_lowerer.can_lower_to_select(func, block_id) {
if debug { if debug_level >= 1 {
eprintln!( eprintln!(
"[try_lower_if_to_joinir] pattern not matched for {}", "[try_lower_if_to_joinir] pattern not matched for {}",
func.signature.name func.signature.name
@ -121,9 +139,9 @@ pub fn try_lower_if_to_joinir(
let result = if_select_lowerer.lower_if_to_select(func, block_id); let result = if_select_lowerer.lower_if_to_select(func, block_id);
if result.is_some() && debug { if result.is_some() && debug_level >= 1 {
eprintln!( eprintln!(
"[try_lower_if_to_joinir] Select lowering used for {}", "[try_lower_if_to_joinir] Select lowering used for {}",
func.signature.name func.signature.name
); );
} }

72
tools/joinir_ab_test.sh Normal file
View File

@ -0,0 +1,72 @@
#!/bin/bash
# Phase 33-8: A/B test automation for JoinIR lowering
#
# Usage:
# tools/joinir_ab_test.sh <test_file.hako>
#
# Example:
# tools/joinir_ab_test.sh apps/tests/joinir_if_merge_simple.hako
set -euo pipefail
test_case=$1 # e.g., "apps/tests/joinir_if_merge_simple.hako"
if [ ! -f "$test_case" ]; then
echo "❌ Test file not found: $test_case"
exit 1
fi
echo "🧪 Testing: $test_case"
echo ""
# Route A: Traditional if_phi
echo "=== Route A (if_phi) ==="
HAKO_JOINIR_IF_SELECT=0 \
NYASH_PARSER_STAGE3=1 \
HAKO_PARSER_STAGE3=1 \
./target/release/hakorune "$test_case" \
> /tmp/route_a.out 2>&1
route_a_rc=$?
echo "Route A RC: $route_a_rc"
echo ""
# Route B: JoinIR Select/IfMerge
echo "=== Route B (JoinIR) ==="
HAKO_JOINIR_IF_SELECT=1 \
HAKO_JOINIR_STAGE1=1 \
HAKO_JOINIR_DEBUG=1 \
NYASH_PARSER_STAGE3=1 \
HAKO_PARSER_STAGE3=1 \
./target/release/hakorune "$test_case" \
> /tmp/route_b.out 2>&1
route_b_rc=$?
echo "Route B RC: $route_b_rc"
echo ""
# Comparison
echo "=== 📊 Comparison ==="
# RC check
if [ $route_a_rc -eq $route_b_rc ]; then
echo "✅ RC matched: $route_a_rc"
else
echo "❌ RC mismatch: A=$route_a_rc, B=$route_b_rc"
exit 1
fi
# Output check (ignore debug logs starting with '[')
if diff <(grep -v '^\[' /tmp/route_a.out) <(grep -v '^\[' /tmp/route_b.out); then
echo "✅ Output matched"
else
echo "❌ Output differs:"
diff <(grep -v '^\[' /tmp/route_a.out) <(grep -v '^\[' /tmp/route_b.out) || true
exit 1
fi
# Extract lowering info from Route B
echo ""
echo "=== 🔍 Lowering Info ==="
grep -E "IfMerge|IfSelect|if_phi" /tmp/route_b.out || echo "⚠️ No lowering info found"
echo ""
echo "🎉 A/B test PASSED for $test_case"