353 lines
12 KiB
Rust
353 lines
12 KiB
Rust
|
|
//! 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<String>,
|
|||
|
|
|
|||
|
|
/// ループ上限式 (e.g., "n" or "arr.size()")
|
|||
|
|
pub bound_expr: BoundExpr,
|
|||
|
|
|
|||
|
|
/// 外部参照 (e.g., ["me.tokens", "arr", "pred"])
|
|||
|
|
pub external_refs: Vec<String>,
|
|||
|
|
|
|||
|
|
/// ループパターン種別
|
|||
|
|
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<Self> {
|
|||
|
|
// 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<serde_json::Value>) {
|
|||
|
|
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<serde_json::Value>, 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");
|
|||
|
|
}
|
|||
|
|
}
|