diff --git a/docs/private b/docs/private index 330944a7..905e8521 160000 --- a/docs/private +++ b/docs/private @@ -1 +1 @@ -Subproject commit 330944a72d1a86de7b501a2b17ca19210e5f368b +Subproject commit 905e8521c9ebe41fce6bfb4705e787d2288706e9 diff --git a/src/mir/builder.rs b/src/mir/builder.rs index efa1702b..0286faca 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -31,6 +31,7 @@ mod exprs_qmark; // ?-propagate mod fields; // field access/assignment lowering split mod if_form; mod lifecycle; +mod loop_frontend_binding; // Phase 50: Loop Frontend Binding (JoinIR variable mapping) pub(crate) mod loops; mod ops; mod phi; diff --git a/src/mir/builder/control_flow.rs b/src/mir/builder/control_flow.rs index 19a96ba9..b5ccb124 100644 --- a/src/mir/builder/control_flow.rs +++ b/src/mir/builder/control_flow.rs @@ -132,17 +132,50 @@ impl super::MirBuilder { func_name: &str, debug: bool, ) -> Result, String> { + use super::loop_frontend_binding::LoopFrontendBinding; use crate::r#macro::ast_json::ast_to_json; use crate::mir::join_ir::frontend::{AstToJoinIrLowerer, JoinFuncMetaMap}; use crate::mir::join_ir_vm_bridge::convert_join_module_to_mir_with_meta; use crate::mir::types::ConstValue; + // Phase 50: Create appropriate binding based on function name + let binding = match func_name { + "JsonTokenizer.print_tokens/0" => LoopFrontendBinding::for_print_tokens(), + "ArrayExtBox.filter/2" => LoopFrontendBinding::for_array_filter(), + _ => { + if debug { + eprintln!( + "[cf_loop/joinir] No binding defined for {}, falling back", + func_name + ); + } + return Ok(None); + } + }; + + if debug { + eprintln!( + "[cf_loop/joinir] Using binding: counter={}, acc={:?}, pattern={:?}", + binding.counter_var, + binding.accumulator_var, + binding.pattern + ); + } + // Step 1: Convert condition and body to JSON let condition_json = ast_to_json(condition); - let body_json: Vec = body.iter().map(|s| ast_to_json(s)).collect(); + let mut body_json: Vec = + body.iter().map(|s| ast_to_json(s)).collect(); + + // Phase 50: Rename variables in body (e.g., "out" → "acc" for filter) + binding.rename_body_variables(&mut body_json); + + // Phase 50: Generate Local declarations from binding + let (i_local, acc_local, n_local) = binding.generate_local_declarations(); // Step 2: Construct JSON v0 format with "defs" array // The function is named "simple" to match JoinIR Frontend's pattern matching + // Phase 50: Include i/acc/n Local declarations to satisfy JoinIR Frontend expectations let program_json = serde_json::json!({ "defs": [ { @@ -151,16 +184,19 @@ impl super::MirBuilder { "body": { "type": "Block", "body": [ - // Placeholder locals (JoinIR Frontend will infer from loop) + // Phase 50: Inject i/acc/n Local declarations + i_local, + acc_local, + n_local, { "type": "Loop", "condition": condition_json, "body": body_json }, - // Placeholder return + // Return the accumulator (or null for side-effect loops) { "type": "Return", - "value": null + "value": { "kind": "Variable", "name": "acc" } } ] } diff --git a/src/mir/builder/loop_frontend_binding.rs b/src/mir/builder/loop_frontend_binding.rs new file mode 100644 index 00000000..af6ec1f0 --- /dev/null +++ b/src/mir/builder/loop_frontend_binding.rs @@ -0,0 +1,352 @@ +//! Phase 50: Loop Frontend Binding +//! +//! このモジュールは cf_loop から JoinIR Frontend への変数マッピングを提供する。 +//! +//! ## 問題背景 +//! +//! JoinIR Frontend (`loop_patterns.rs`) はハードコードされた変数名を期待: +//! - `i`: カウンタ変数 +//! - `acc`: アキュムレータ変数 +//! - `n`: ループ上限 +//! +//! しかし実際のループは異なる名前や構造を持つ: +//! - print_tokens: acc が存在しない(副作用のみ) +//! - filter: acc は `out` という名前 +//! +//! ## 解決策 +//! +//! LoopFrontendBinding が「実際の変数名」と「JoinIR 期待変数名」をマッピングし、 +//! JSON v0 生成時に適切な Local 宣言を注入する。 + +use crate::ast::ASTNode; + +/// ループ変数と JoinIR Frontend 期待変数のマッピング +#[derive(Debug, Clone)] +pub struct LoopFrontendBinding { + /// カウンタ変数名 (e.g., "i") + pub counter_var: String, + + /// カウンタの初期値 + pub counter_init: i64, + + /// アキュムレータ変数名 (None for side-effect loops like print_tokens) + pub accumulator_var: Option, + + /// ループ上限式 (e.g., "n" or "arr.size()") + pub bound_expr: BoundExpr, + + /// 外部参照 (e.g., ["me.tokens", "arr", "pred"]) + pub external_refs: Vec, + + /// ループパターン種別 + pub pattern: LoopPattern, +} + +/// ループ上限の表現 +#[derive(Debug, Clone)] +pub enum BoundExpr { + /// 変数名 (e.g., "n") + Variable(String), + /// メソッド呼び出し (e.g., "me.tokens", "length") + MethodCall { receiver: String, method: String }, + /// 不明(フォールバック用) + Unknown, +} + +/// ループパターン種別 +#[derive(Debug, Clone, PartialEq)] +pub enum LoopPattern { + /// 単純カウンタループ (print_tokens 相当) + /// has_accumulator: アキュムレータが存在するか + Simple { has_accumulator: bool }, + + /// フィルターパターン (if + push) + Filter, + + /// マップパターン (変換 + push) + Map, + + /// 不明(フォールバック用) + Unknown, +} + +impl LoopFrontendBinding { + /// print_tokens 専用のバインディングを生成 + /// + /// print_tokens の構造: + /// ```nyash + /// local i = 0 + /// loop(i < me.tokens.length()) { + /// local token = me.tokens.get(i) + /// print(token) + /// i = i + 1 + /// } + /// ``` + pub fn for_print_tokens() -> Self { + Self { + counter_var: "i".to_string(), + counter_init: 0, + accumulator_var: None, // print_tokens has no accumulator + bound_expr: BoundExpr::MethodCall { + receiver: "me.tokens".to_string(), + method: "length".to_string(), + }, + external_refs: vec!["me.tokens".to_string()], + pattern: LoopPattern::Simple { + has_accumulator: false, + }, + } + } + + /// array_ext.filter 専用のバインディングを生成 + /// + /// filter の構造: + /// ```nyash + /// 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 + /// ``` + pub fn for_array_filter() -> Self { + Self { + counter_var: "i".to_string(), + counter_init: 0, + accumulator_var: Some("out".to_string()), // filter has "out" as accumulator + bound_expr: BoundExpr::Variable("n".to_string()), + external_refs: vec!["arr".to_string(), "pred".to_string()], + pattern: LoopPattern::Filter, + } + } + + /// ループ条件と本体から変数パターンを分析して Binding を生成 + /// + /// Phase 50-3 で実装予定の汎用分析。現在は関数名ベースのハードコード。 + #[allow(dead_code)] + pub fn analyze(_condition: &ASTNode, _body: &[ASTNode]) -> Option { + // TODO: Phase 50-3 で AST 分析を実装 + // 1. カウンタ変数の検出 (i = i + 1 パターン) + // 2. アキュムレータの検出 (return で返される変数) + // 3. ループ上限の検出 (i < X の X) + None + } + + /// JoinIR Frontend 用の JSON v0 Local 宣言を生成 + /// + /// Returns: (i_local, acc_local, n_local) の JSON Value タプル + /// + /// Note: JoinIR Frontend expects specific type names: + /// - "Int" for integer literals (with "value" field) + /// - "Var" for variable references (with "name" field) + /// - "Method" for method calls (with "receiver", "method", "args" fields) + /// - "NewBox" for box instantiation + pub fn generate_local_declarations( + &self, + ) -> (serde_json::Value, serde_json::Value, serde_json::Value) { + use serde_json::json; + + // i の Local 宣言 + // JoinIR Frontend expects "Int" type with direct "value" field + let i_local = json!({ + "type": "Local", + "name": "i", + "expr": { + "type": "Int", + "value": self.counter_init + } + }); + + // acc の Local 宣言(合成または実際の変数から) + let acc_local = if self.accumulator_var.is_some() { + // 実際のアキュムレータがある場合(filter 等) + // "out" → "acc" にリネームして宣言 + json!({ + "type": "Local", + "name": "acc", + "expr": { + "type": "NewBox", + "box_name": "ArrayBox", + "args": [] + } + }) + } else { + // アキュムレータがない場合(print_tokens 等) + // 合成の unit 値を使う(Int 0 として表現) + json!({ + "type": "Local", + "name": "acc", + "expr": { + "type": "Int", + "value": 0 + } + }) + }; + + // n の Local 宣言 + let n_local = match &self.bound_expr { + BoundExpr::Variable(var) => { + // 既存変数を参照(filter の n 等) + // JoinIR Frontend expects "Var" type + json!({ + "type": "Local", + "name": "n", + "expr": { + "type": "Var", + "name": var + } + }) + } + BoundExpr::MethodCall { receiver, method } => { + // メソッド呼び出しを評価(print_tokens の me.tokens.length() 等) + // JoinIR Frontend expects "Method" type + json!({ + "type": "Local", + "name": "n", + "expr": { + "type": "Method", + "receiver": { "type": "Var", "name": receiver }, + "method": method, + "args": [] + } + }) + } + BoundExpr::Unknown => { + // フォールバック: 0 を使う + json!({ + "type": "Local", + "name": "n", + "expr": { + "type": "Int", + "value": 0 + } + }) + } + }; + + (i_local, acc_local, n_local) + } + + /// ループ本体の変数名をリネーム + /// + /// 実際の変数名 (e.g., "out") を JoinIR 期待名 (e.g., "acc") に置換 + pub fn rename_body_variables(&self, body_json: &mut Vec) { + if let Some(ref acc_name) = self.accumulator_var { + if acc_name != "acc" { + // "out" → "acc" にリネーム + Self::rename_variable_in_json(body_json, acc_name, "acc"); + } + } + } + + /// JSON 内の変数名を再帰的にリネーム + fn rename_variable_in_json(json_array: &mut Vec, from: &str, to: &str) { + for item in json_array.iter_mut() { + Self::rename_variable_in_value(item, from, to); + } + } + + fn rename_variable_in_value(value: &mut serde_json::Value, from: &str, to: &str) { + match value { + serde_json::Value::Object(map) => { + // Variable/Var ノードの名前をチェック + // Note: ast_to_json uses "kind", JoinIR Frontend expects "type" + let is_var = map.get("kind").and_then(|v| v.as_str()) == Some("Variable") + || map.get("type").and_then(|v| v.as_str()) == Some("Var"); + if is_var { + if let Some(name) = map.get_mut("name") { + if name.as_str() == Some(from) { + *name = serde_json::Value::String(to.to_string()); + } + } + } + // Local ノードの名前をチェック + if map.get("type").and_then(|v| v.as_str()) == Some("Local") + || map.get("kind").and_then(|v| v.as_str()) == Some("Local") + { + if let Some(name) = map.get_mut("name") { + if name.as_str() == Some(from) { + *name = serde_json::Value::String(to.to_string()); + } + } + } + // 再帰的に処理 + for (_, v) in map.iter_mut() { + Self::rename_variable_in_value(v, from, to); + } + } + serde_json::Value::Array(arr) => { + for item in arr.iter_mut() { + Self::rename_variable_in_value(item, from, to); + } + } + _ => {} + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_print_tokens_binding() { + let binding = LoopFrontendBinding::for_print_tokens(); + assert_eq!(binding.counter_var, "i"); + assert_eq!(binding.counter_init, 0); + assert!(binding.accumulator_var.is_none()); + assert_eq!( + binding.pattern, + LoopPattern::Simple { + has_accumulator: false + } + ); + } + + #[test] + fn test_array_filter_binding() { + let binding = LoopFrontendBinding::for_array_filter(); + assert_eq!(binding.counter_var, "i"); + assert_eq!(binding.accumulator_var, Some("out".to_string())); + assert_eq!(binding.pattern, LoopPattern::Filter); + } + + #[test] + fn test_local_declarations_no_acc() { + let binding = LoopFrontendBinding::for_print_tokens(); + let (i_local, acc_local, _n_local) = binding.generate_local_declarations(); + + // i should be initialized to 0 + assert_eq!(i_local["name"], "i"); + assert_eq!(i_local["expr"]["type"], "Int"); + assert_eq!(i_local["expr"]["value"], 0); + + // acc should be synthetic (Int 0 for side-effect loops) + assert_eq!(acc_local["name"], "acc"); + assert_eq!(acc_local["expr"]["type"], "Int"); + assert_eq!(acc_local["expr"]["value"], 0); + } + + #[test] + fn test_local_declarations_with_acc() { + let binding = LoopFrontendBinding::for_array_filter(); + let (i_local, acc_local, n_local) = binding.generate_local_declarations(); + + // i should be initialized to 0 + assert_eq!(i_local["name"], "i"); + assert_eq!(i_local["expr"]["type"], "Int"); + + // acc should be new ArrayBox() + assert_eq!(acc_local["name"], "acc"); + assert_eq!(acc_local["expr"]["type"], "NewBox"); + assert_eq!(acc_local["expr"]["box_name"], "ArrayBox"); + + // n should reference the "n" variable (Var type) + assert_eq!(n_local["name"], "n"); + assert_eq!(n_local["expr"]["type"], "Var"); + assert_eq!(n_local["expr"]["name"], "n"); + } +}