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:
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user