feat(joinir): Phase 49-4 multi-target routing with graceful fallback

- Add ArrayExtBox.filter/2 as second JoinIR mainline target
- Fix function name arity: print_tokens is /0 (no implicit me in arity)
- Construct proper JSON v0 format with defs array for JoinIR Frontend
- Add catch_unwind for graceful fallback on unsupported patterns
- Add 3 array_filter tests (smoke, fallback, A/B comparison)
- All 6 Phase 49 tests passing

Dev flags:
- HAKO_JOINIR_PRINT_TOKENS_MAIN=1: JsonTokenizer.print_tokens/0
- HAKO_JOINIR_ARRAY_FILTER_MAIN=1: ArrayExtBox.filter/2

Note: Currently all loops fall back to legacy LoopBuilder due to
JoinIR Frontend expecting hardcoded variable names (i, acc, n).
Full JoinIR integration pending variable scope support in Phase 50+.

🤖 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-28 20:12:39 +09:00
parent 5819423e25
commit 1e1b2183b2
2 changed files with 310 additions and 31 deletions

View File

@ -24,10 +24,14 @@ impl super::MirBuilder {
/// ///
/// # Phase 49: JoinIR Frontend Mainline Integration /// # Phase 49: JoinIR Frontend Mainline Integration
/// ///
/// This is the unified entry point for all loop lowering. When enabled via /// This is the unified entry point for all loop lowering. Specific functions
/// `HAKO_JOINIR_PRINT_TOKENS_MAIN=1`, specific functions (starting with /// are routed through JoinIR Frontend instead of the traditional LoopBuilder path
/// `JsonTokenizer.print_tokens/1`) are routed through JoinIR Frontend instead /// when enabled via dev flags:
/// of the traditional LoopBuilder path. ///
/// - `HAKO_JOINIR_PRINT_TOKENS_MAIN=1`: JsonTokenizer.print_tokens/0
/// - `HAKO_JOINIR_ARRAY_FILTER_MAIN=1`: ArrayExtBox.filter/2
///
/// Note: Arity does NOT include implicit `me` receiver.
pub(super) fn cf_loop( pub(super) fn cf_loop(
&mut self, &mut self,
condition: ASTNode, condition: ASTNode,
@ -51,16 +55,21 @@ impl super::MirBuilder {
/// ///
/// Returns `Ok(Some(value))` if the current function should use JoinIR Frontend, /// Returns `Ok(Some(value))` if the current function should use JoinIR Frontend,
/// `Ok(None)` to fall through to the legacy LoopBuilder path. /// `Ok(None)` to fall through to the legacy LoopBuilder path.
///
/// # Phase 49-4: Multi-target support
///
/// Targets are enabled via separate dev flags:
/// - `HAKO_JOINIR_PRINT_TOKENS_MAIN=1`: JsonTokenizer.print_tokens/0
/// - `HAKO_JOINIR_ARRAY_FILTER_MAIN=1`: ArrayExtBox.filter/2
///
/// Note: Arity in function names does NOT include implicit `me` receiver.
/// - Instance method `print_tokens()` → `/0` (no explicit params)
/// - Static method `filter(arr, pred)` → `/2` (two params)
fn try_cf_loop_joinir( fn try_cf_loop_joinir(
&mut self, &mut self,
condition: &ASTNode, condition: &ASTNode,
body: &[ASTNode], body: &[ASTNode],
) -> Result<Option<ValueId>, String> { ) -> Result<Option<ValueId>, String> {
// Phase 49-2: Check if feature is enabled
if std::env::var("HAKO_JOINIR_PRINT_TOKENS_MAIN").ok().as_deref() != Some("1") {
return Ok(None);
}
// Get current function name // Get current function name
let func_name = self let func_name = self
.current_function .current_function
@ -68,8 +77,19 @@ impl super::MirBuilder {
.map(|f| f.signature.name.clone()) .map(|f| f.signature.name.clone())
.unwrap_or_default(); .unwrap_or_default();
// Phase 49-2: Only handle print_tokens for now // Phase 49-4: Multi-target routing with separate dev flags
if func_name != "JsonTokenizer.print_tokens/1" { // Note: Arity does NOT include implicit `me` receiver
let is_target = match func_name.as_str() {
"JsonTokenizer.print_tokens/0" => {
std::env::var("HAKO_JOINIR_PRINT_TOKENS_MAIN").ok().as_deref() == Some("1")
}
"ArrayExtBox.filter/2" => {
std::env::var("HAKO_JOINIR_ARRAY_FILTER_MAIN").ok().as_deref() == Some("1")
}
_ => false,
};
if !is_target {
return Ok(None); return Ok(None);
} }
@ -90,10 +110,21 @@ impl super::MirBuilder {
/// Phase 49-3: JoinIR Frontend integration implementation /// Phase 49-3: JoinIR Frontend integration implementation
/// ///
/// # Pipeline /// # Pipeline
/// 1. Build Loop AST → Program JSON /// 1. Build Loop AST → JSON v0 format (with "defs" array)
/// 2. AstToJoinIrLowerer::lower_program_json() → JoinModule /// 2. AstToJoinIrLowerer::lower_program_json() → JoinModule
/// 3. convert_join_module_to_mir_with_meta() → MirModule /// 3. convert_join_module_to_mir_with_meta() → MirModule
/// 4. Merge MIR blocks into current_function /// 4. Merge MIR blocks into current_function
///
/// # Phase 49-4 Note
///
/// JoinIR Frontend expects a complete function definition with:
/// - local variable initializations
/// - loop body
/// - return statement
///
/// Since cf_loop only has access to the loop condition and body,
/// we construct a minimal JSON v0 wrapper with function name "simple"
/// to match the JoinIR Frontend's expected pattern.
fn cf_loop_joinir_impl( fn cf_loop_joinir_impl(
&mut self, &mut self,
condition: &ASTNode, condition: &ASTNode,
@ -106,34 +137,79 @@ impl super::MirBuilder {
use crate::mir::join_ir_vm_bridge::convert_join_module_to_mir_with_meta; use crate::mir::join_ir_vm_bridge::convert_join_module_to_mir_with_meta;
use crate::mir::types::ConstValue; use crate::mir::types::ConstValue;
// Step 1: Build Loop AST wrapped in a minimal function // Step 1: Convert condition and body to JSON
let loop_ast = ASTNode::Loop { let condition_json = ast_to_json(condition);
condition: Box::new(condition.clone()), let body_json: Vec<serde_json::Value> = body.iter().map(|s| ast_to_json(s)).collect();
body: body.to_vec(),
span: Span::unknown(),
};
// Wrap in a minimal function for JoinIR lowering // Step 2: Construct JSON v0 format with "defs" array
// JoinIR Frontend expects a function body, not just a loop // The function is named "simple" to match JoinIR Frontend's pattern matching
let wrapper_func = ASTNode::Program { let program_json = serde_json::json!({
statements: vec![loop_ast], "defs": [
span: Span::unknown(), {
}; "name": "simple",
"params": [],
// Step 2: Convert to JSON "body": {
let program_json = ast_to_json(&wrapper_func); "type": "Block",
"body": [
// Placeholder locals (JoinIR Frontend will infer from loop)
{
"type": "Loop",
"condition": condition_json,
"body": body_json
},
// Placeholder return
{
"type": "Return",
"value": null
}
]
}
}
]
});
if debug { if debug {
eprintln!( eprintln!(
"[cf_loop/joinir] Generated JSON for {}: {}", "[cf_loop/joinir] Generated JSON v0 for {}: {}",
func_name, func_name,
serde_json::to_string_pretty(&program_json).unwrap_or_default() serde_json::to_string_pretty(&program_json).unwrap_or_default()
); );
} }
// Step 3: Lower to JoinIR // Step 3: Lower to JoinIR
let mut lowerer = AstToJoinIrLowerer::new(); // Phase 49-4: Use catch_unwind for graceful fallback on unsupported patterns
let join_module = lowerer.lower_program_json(&program_json); // The JoinIR Frontend may panic if the loop doesn't match expected patterns
// (e.g., missing variable initializations like "i must be initialized")
let join_module = {
let json_clone = program_json.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut lowerer = AstToJoinIrLowerer::new();
lowerer.lower_program_json(&json_clone)
}));
match result {
Ok(module) => module,
Err(e) => {
// Extract panic message for debugging
let panic_msg = if let Some(s) = e.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = e.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
if debug {
eprintln!(
"[cf_loop/joinir] JoinIR lowering failed for {}: {}, falling back to legacy",
func_name, panic_msg
);
}
// Return None to fall back to legacy LoopBuilder
return Ok(None);
}
}
};
// Phase 49-3 MVP: Use empty meta map (full if-analysis is Phase 40+ territory) // Phase 49-3 MVP: Use empty meta map (full if-analysis is Phase 40+ territory)
let join_meta = JoinFuncMetaMap::new(); let join_meta = JoinFuncMetaMap::new();

View File

@ -1,4 +1,4 @@
// Phase 49-3.2: JoinIR Frontend Mainline Integration Test // Phase 49: JoinIR Frontend Mainline Integration Test
// //
// このテストは cf_loop の JoinIR Frontend mainline route が // このテストは cf_loop の JoinIR Frontend mainline route が
// 正常に動作することを確認する。 // 正常に動作することを確認する。
@ -7,8 +7,13 @@
// - merge_joinir_mir_blocks() によるブロックマージ // - merge_joinir_mir_blocks() によるブロックマージ
// - A/B 比較テストRoute A vs Route B // - A/B 比較テストRoute A vs Route B
// //
// Phase 49-4 実装済み:
// - ArrayExtBox.filter/2 対応
// - HAKO_JOINIR_ARRAY_FILTER_MAIN=1 dev フラグ追加
//
// テスト方法: // テスト方法:
// HAKO_JOINIR_PRINT_TOKENS_MAIN=1 cargo test --release joinir_mainline_phase49 // HAKO_JOINIR_PRINT_TOKENS_MAIN=1 cargo test --release joinir_mainline_phase49
// HAKO_JOINIR_ARRAY_FILTER_MAIN=1 cargo test --release phase49_joinir_array_filter
use crate::ast::ASTNode; use crate::ast::ASTNode;
use crate::mir::MirCompiler; use crate::mir::MirCompiler;
@ -222,3 +227,201 @@ static box Main {
std::env::remove_var("HAKO_PARSER_STAGE3"); std::env::remove_var("HAKO_PARSER_STAGE3");
std::env::remove_var("NYASH_DISABLE_PLUGINS"); std::env::remove_var("NYASH_DISABLE_PLUGINS");
} }
// =============================================================================
// Phase 49-4: ArrayExtBox.filter/2 Tests
// =============================================================================
/// Phase 49-4: JoinIR Frontend mainline パイプラインが
/// ArrayExtBox.filter 関数のコンパイル時にクラッシュしないことを確認
#[test]
fn phase49_joinir_array_filter_smoke() {
// Phase 49-4 mainline route は dev フラグで制御
std::env::set_var("HAKO_JOINIR_ARRAY_FILTER_MAIN", "1");
std::env::set_var("NYASH_JOINIR_MAINLINE_DEBUG", "1");
std::env::set_var("NYASH_PARSER_STAGE3", "1");
std::env::set_var("HAKO_PARSER_STAGE3", "1");
std::env::set_var("NYASH_DISABLE_PLUGINS", "1");
// ArrayExtBox.filter 簡易実装if-in-loop パターン)
// fn パラメータは pred に変更(予約語競合回避)
let src = r#"
static box ArrayExtBox {
filter(arr, pred) {
local out = new ArrayBox()
local i = 0
local n = arr.size()
loop(i < n) {
local v = arr.get(i)
if pred(v) {
out.push(v)
}
i = i + 1
}
return out
}
}
static box Main {
main() {
return 0
}
}
"#;
let ast: ASTNode = NyashParser::parse_from_string(src)
.expect("phase49-4: parse failed");
let mut mc = MirCompiler::with_options(false);
let result = mc.compile(ast);
// パイプラインがクラッシュしないことを確認
assert!(
result.is_ok(),
"phase49-4 array_filter mainline compile should not crash: {:?}",
result.err()
);
// クリーンアップ
std::env::remove_var("HAKO_JOINIR_ARRAY_FILTER_MAIN");
std::env::remove_var("NYASH_JOINIR_MAINLINE_DEBUG");
std::env::remove_var("NYASH_PARSER_STAGE3");
std::env::remove_var("HAKO_PARSER_STAGE3");
std::env::remove_var("NYASH_DISABLE_PLUGINS");
}
/// Phase 49-4: dev フラグ OFF 時は従来経路を使用することを確認
#[test]
fn phase49_joinir_array_filter_fallback() {
// dev フラグ OFF
std::env::remove_var("HAKO_JOINIR_ARRAY_FILTER_MAIN");
std::env::set_var("NYASH_PARSER_STAGE3", "1");
std::env::set_var("HAKO_PARSER_STAGE3", "1");
std::env::set_var("NYASH_DISABLE_PLUGINS", "1");
let src = r#"
static box ArrayExtBox {
filter(arr, pred) {
local out = new ArrayBox()
local i = 0
local n = arr.size()
loop(i < n) {
local v = arr.get(i)
if pred(v) {
out.push(v)
}
i = i + 1
}
return out
}
}
static box Main {
main() {
return 0
}
}
"#;
let ast: ASTNode = NyashParser::parse_from_string(src)
.expect("phase49-4 fallback: parse failed");
let mut mc = MirCompiler::with_options(false);
let result = mc.compile(ast);
assert!(
result.is_ok(),
"phase49-4 fallback compile should succeed: {:?}",
result.err()
);
// クリーンアップ
std::env::remove_var("NYASH_PARSER_STAGE3");
std::env::remove_var("HAKO_PARSER_STAGE3");
std::env::remove_var("NYASH_DISABLE_PLUGINS");
}
/// Phase 49-4: A/B 比較テスト - Route A (legacy) vs Route B (JoinIR)
/// ArrayExtBox.filter版
#[test]
fn phase49_joinir_array_filter_ab_comparison() {
let src = r#"
static box ArrayExtBox {
filter(arr, pred) {
local out = new ArrayBox()
local i = 0
local n = arr.size()
loop(i < n) {
local v = arr.get(i)
if pred(v) {
out.push(v)
}
i = i + 1
}
return out
}
}
static box Main {
main() {
return 0
}
}
"#;
// Route A: Legacy path (flag OFF)
std::env::remove_var("HAKO_JOINIR_ARRAY_FILTER_MAIN");
std::env::set_var("NYASH_PARSER_STAGE3", "1");
std::env::set_var("HAKO_PARSER_STAGE3", "1");
std::env::set_var("NYASH_DISABLE_PLUGINS", "1");
let ast_a: ASTNode = NyashParser::parse_from_string(src)
.expect("phase49-4 A/B: parse failed (Route A)");
let mut mc_a = MirCompiler::with_options(false);
let result_a = mc_a.compile(ast_a);
assert!(
result_a.is_ok(),
"Route A compile should succeed: {:?}",
result_a.err()
);
let module_a = result_a.unwrap().module;
let blocks_a: usize = module_a
.functions
.values()
.map(|f| f.blocks.len())
.sum();
// Route B: JoinIR Frontend path (flag ON)
std::env::set_var("NYASH_PARSER_STAGE3", "1");
std::env::set_var("HAKO_PARSER_STAGE3", "1");
std::env::set_var("NYASH_DISABLE_PLUGINS", "1");
std::env::set_var("HAKO_JOINIR_ARRAY_FILTER_MAIN", "1");
let ast_b: ASTNode = NyashParser::parse_from_string(src)
.expect("phase49-4 A/B: parse failed (Route B)");
let mut mc_b = MirCompiler::with_options(false);
let result_b = mc_b.compile(ast_b);
assert!(
result_b.is_ok(),
"Route B compile should succeed: {:?}",
result_b.err()
);
let module_b = result_b.unwrap().module;
let blocks_b: usize = module_b
.functions
.values()
.map(|f| f.blocks.len())
.sum();
// Log block counts for debugging
eprintln!(
"[phase49-4 A/B filter] Route A: {} total blocks, Route B: {} total blocks",
blocks_a, blocks_b
);
// クリーンアップ
std::env::remove_var("HAKO_JOINIR_ARRAY_FILTER_MAIN");
std::env::remove_var("NYASH_PARSER_STAGE3");
std::env::remove_var("HAKO_PARSER_STAGE3");
std::env::remove_var("NYASH_DISABLE_PLUGINS");
}