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:
@ -218,9 +218,51 @@ pub fn joinir_llvm_experiment_enabled() -> bool {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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).
|
||||
|
||||
@ -11,7 +11,7 @@ use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub struct IfMergeLowerer {
|
||||
debug: bool,
|
||||
debug_level: u8,
|
||||
}
|
||||
|
||||
/// 検出された IfMerge パターン情報
|
||||
@ -30,8 +30,13 @@ struct IfBranch {
|
||||
}
|
||||
|
||||
impl IfMergeLowerer {
|
||||
pub fn new(debug: bool) -> Self {
|
||||
Self { debug }
|
||||
pub fn new(debug_level: u8) -> Self {
|
||||
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 できるかチェック
|
||||
@ -47,13 +52,25 @@ impl IfMergeLowerer {
|
||||
) -> Option<JoinInst> {
|
||||
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!(
|
||||
"[IfMergeLowerer] lowering to IfMerge with {} merge pairs",
|
||||
"[IfMergeLowerer] ✅ lowering to IfMerge with {} merge pairs",
|
||||
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 命令を生成
|
||||
Some(JoinInst::IfMerge {
|
||||
cond: pattern.cond,
|
||||
@ -99,9 +116,10 @@ impl IfMergeLowerer {
|
||||
);
|
||||
|
||||
if !is_then_return || !is_else_return {
|
||||
if self.debug {
|
||||
// Phase 33-8: Level 2 - Pattern matching details
|
||||
if self.debug_level >= 2 {
|
||||
eprintln!(
|
||||
"[IfMergeLowerer] not return pattern (then={}, else={})",
|
||||
"[IfMergeLowerer] ❌ not return pattern (then={}, else={})",
|
||||
is_then_return, is_else_return
|
||||
);
|
||||
}
|
||||
@ -112,7 +130,8 @@ impl IfMergeLowerer {
|
||||
let then_writes = self.extract_written_vars(&then_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!(
|
||||
"[IfMergeLowerer] 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();
|
||||
|
||||
if common_writes.is_empty() {
|
||||
if self.debug {
|
||||
eprintln!("[IfMergeLowerer] no common writes found");
|
||||
// Phase 33-8: Level 2 - Pattern matching details
|
||||
if self.debug_level >= 2 {
|
||||
eprintln!("[IfMergeLowerer] ❌ no common writes found");
|
||||
}
|
||||
return None;
|
||||
}
|
||||
@ -230,7 +250,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_if_merge_lowerer_creation() {
|
||||
let lowerer = IfMergeLowerer::new(false);
|
||||
assert!(!lowerer.debug);
|
||||
let lowerer = IfMergeLowerer::new(0);
|
||||
assert_eq!(lowerer.debug_level, 0);
|
||||
|
||||
let lowerer_compat = IfMergeLowerer::with_debug(true);
|
||||
assert_eq!(lowerer_compat.debug_level, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ use crate::mir::join_ir::JoinInst;
|
||||
use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
|
||||
|
||||
pub struct IfSelectLowerer {
|
||||
debug: bool,
|
||||
debug_level: u8,
|
||||
}
|
||||
|
||||
/// If/Else パターンの分類
|
||||
@ -37,8 +37,13 @@ struct IfBranch {
|
||||
}
|
||||
|
||||
impl IfSelectLowerer {
|
||||
pub fn new(debug: bool) -> Self {
|
||||
Self { debug }
|
||||
pub fn new(debug_level: u8) -> Self {
|
||||
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 できるかチェック
|
||||
@ -54,13 +59,22 @@ impl IfSelectLowerer {
|
||||
) -> Option<JoinInst> {
|
||||
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!(
|
||||
"[IfSelectLowerer] lowering {:?} pattern to Select",
|
||||
"[IfSelectLowerer] ✅ lowering {:?} pattern to Select",
|
||||
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 命令を生成
|
||||
let dst = pattern.dst.unwrap_or(pattern.then_val);
|
||||
|
||||
@ -99,8 +113,9 @@ impl IfSelectLowerer {
|
||||
|
||||
// 3. simple パターンのチェック
|
||||
if let Some(pattern) = self.try_match_simple_pattern(&branch, then_block, else_block) {
|
||||
if self.debug {
|
||||
eprintln!("[IfSelectLowerer] matched simple pattern");
|
||||
// Phase 33-8: Level 2 - Pattern matching details
|
||||
if self.debug_level >= 2 {
|
||||
eprintln!("[IfSelectLowerer] ✅ matched simple pattern");
|
||||
}
|
||||
return Some(pattern);
|
||||
}
|
||||
@ -108,14 +123,16 @@ impl IfSelectLowerer {
|
||||
// 4. local パターンのチェック
|
||||
if let Some(pattern) = self.try_match_local_pattern(func, &branch, then_block, else_block)
|
||||
{
|
||||
if self.debug {
|
||||
eprintln!("[IfSelectLowerer] matched local pattern");
|
||||
// Phase 33-8: Level 2 - Pattern matching details
|
||||
if self.debug_level >= 2 {
|
||||
eprintln!("[IfSelectLowerer] ✅ matched local pattern");
|
||||
}
|
||||
return Some(pattern);
|
||||
}
|
||||
|
||||
if self.debug {
|
||||
eprintln!("[IfSelectLowerer] no pattern matched");
|
||||
// Phase 33-8: Level 2 - Pattern matching details
|
||||
if self.debug_level >= 2 {
|
||||
eprintln!("[IfSelectLowerer] ❌ no pattern matched");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@ -73,15 +73,29 @@ pub fn try_lower_if_to_joinir(
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. Phase 33-7: 関数名ガード拡張(テスト + Stage-1/Stage-B 候補)
|
||||
let is_allowed = func.signature.name.starts_with("IfSelectTest.")
|
||||
|| func.signature.name.starts_with("IfMergeTest.") // Phase 33-7
|
||||
|| func.signature.name.starts_with("Stage1JsonScannerTestBox.") // Phase 33-5 test
|
||||
|| func.signature.name == "JsonShapeToMap._read_value_from_pair/1"
|
||||
|| func.signature.name == "Stage1JsonScannerBox.value_start_after_key_pos/2";
|
||||
// Phase 33-8: デバッグログレベル取得(0-3)
|
||||
let debug_level = crate::config::env::joinir_debug_level();
|
||||
let _debug = debug || debug_level >= 1;
|
||||
|
||||
// 2. Phase 33-8: 関数名ガード拡張(テスト + Stage-1 rollout + 明示承認)
|
||||
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 debug {
|
||||
if debug_level >= 2 {
|
||||
eprintln!(
|
||||
"[try_lower_if_to_joinir] skipping non-allowed function: {}",
|
||||
func.signature.name
|
||||
@ -90,15 +104,19 @@ pub fn try_lower_if_to_joinir(
|
||||
return None;
|
||||
}
|
||||
|
||||
if debug_level >= 1 {
|
||||
eprintln!("[try_lower_if_to_joinir] trying to lower {}", func.signature.name);
|
||||
}
|
||||
|
||||
// 3. Phase 33-7: IfMerge を優先的に試行(複数変数パターン)
|
||||
// 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 let Some(result) = if_merge_lowerer.lower_if_to_if_merge(func, block_id) {
|
||||
if debug {
|
||||
if debug_level >= 1 {
|
||||
eprintln!(
|
||||
"[try_lower_if_to_joinir] IfMerge lowering used for {}",
|
||||
"[try_lower_if_to_joinir] ✅ IfMerge lowering used for {}",
|
||||
func.signature.name
|
||||
);
|
||||
}
|
||||
@ -107,10 +125,10 @@ pub fn try_lower_if_to_joinir(
|
||||
}
|
||||
|
||||
// 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 debug {
|
||||
if debug_level >= 1 {
|
||||
eprintln!(
|
||||
"[try_lower_if_to_joinir] pattern not matched for {}",
|
||||
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);
|
||||
|
||||
if result.is_some() && debug {
|
||||
if result.is_some() && debug_level >= 1 {
|
||||
eprintln!(
|
||||
"[try_lower_if_to_joinir] Select lowering used for {}",
|
||||
"[try_lower_if_to_joinir] ✅ Select lowering used for {}",
|
||||
func.signature.name
|
||||
);
|
||||
}
|
||||
|
||||
72
tools/joinir_ab_test.sh
Normal file
72
tools/joinir_ab_test.sh
Normal 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"
|
||||
Reference in New Issue
Block a user