Files
hakorune/docs/private/roadmap/phases/phase-20-macro-full-features/MIGRATION.md

11 KiB
Raw Blame History

Phase 20: Rust → Hakorune マクロ移行ガイド

Date: 2025-10-06 Status: REFERENCE - Phase 20実装時に参照

🎯 移行の目的

Phase 16で実装したRust版マクロシステムの主要機能を、 Hakoruneでセルフホスト実装に書き直す。

📋 移行対象機能一覧

優先度 HIGH: 即座に便利

機能 Rust実装場所 移行先(案) 難易度
@derive(Equals) src/macro/engine.rs:151-174 apps/macros/derive/derive_equals.hako
@derive(ToString) src/macro/engine.rs:176-194 apps/macros/derive/derive_tostring.hako
@test ランナー src/macro/mod.rs:61-401 apps/macros/test/test_runner.hako

優先度 MEDIUM: 複雑なマクロ用基盤

機能 Rust実装場所 移行先(案) 難易度
TemplatePattern src/macro/pattern.rs:116-238 apps/macros/pattern/template_pattern.hako
Quote/Unquote src/macro/pattern.rs:9-114 apps/macros/quote/quote_system.hako
MacroEngine src/macro/engine.rs:11-101 apps/macros/engine/macro_engine.hako

優先度 LOW: 構文サポートParser側

機能 Rust実装場所 移行判断
Match式構文 src/parser/expr/match_expr.rs 移行不要Rust Parserのまま維持
PatternAst src/ast.rs 移行不要AST定義はRust

🔄 移行戦略: 3つのアプローチ

Strategy 1: JSON AST直接操作推奨

使用場面: @derive, @test 難易度:

// apps/macros/derive/derive_equals.hako
static box DeriveEqualsMacro {
    expand(json_ast, ctx) {
        // 1. JSON文字列をパース
        local ast, box_decl, fields, new_method
        ast = JSON.parse(json_ast)

        // 2. BoxDeclaration検出
        if ast.kind != "Program" {
            return json_ast
        }

        // 3. public_fieldsを取得
        fields = box_decl.public_fields

        // 4. equals()メソッドJSON生成
        new_method = me.build_equals_method(fields)

        // 5. methodsに追加
        box_decl.methods.set("equals", new_method)

        // 6. JSON文字列化
        return JSON.stringify(ast)
    }

    build_equals_method(fields) {
        // JSON AST構築詳細は後述
        return method_json
    }
}

利点:

  • 現在の暫定実装JSON文字列操作の延長
  • Hakoruneの機能だけで完結
  • バグが少ない

欠点:

  • JSON操作が冗長
  • 複雑なマクロは困難

Strategy 2: AstBuilderBox実装中間層

使用場面: Pattern Matching, Quote/Unquote 難易度:

// apps/macros/ast/ast_builder.hako
box AstBuilderBox {
    // JSON ASTをHakoruneオブジェクトにラップ
    root: MapBox

    birth(json_str) {
        from MapBox.birth()
        me.root = JSON.parse(json_str)
    }

    // ヘルパーメソッド
    find_box_decl(name) {
        // AST走査してBoxDeclaration検出
    }

    add_method(box_name, method_name, body_ast) {
        // メソッド追加
    }

    to_json() {
        return JSON.stringify(me.root)
    }
}

利点:

  • API設計でRust版に近い書き方が可能
  • 複雑なマクロ実装が楽

欠点:

  • AstBuilderBox実装コストが大きい
  • JSON↔オブジェクト変換オーバーヘッド

Strategy 3: Rust Hybrid非推奨

Rust側にヘルパー関数を残し、Hakoruneから呼ぶ。

使用場面: なし(セルフホスティングの意義を損なう) 難易度:

推奨しない理由:

  • Phase 20の目的は「Rust依存を減らす」こと
  • ハイブリッドは複雑性が増すだけ

📝 移行実装例: @derive(Equals)

Rust版参考

// src/macro/engine.rs:151-174
fn build_equals_method(_box_name: &str, fields: &Vec<String>) -> ASTNode {
    let cond = if fields.is_empty() {
        ASTNode::Literal { value: LiteralValue::Bool(true), span: Span::unknown() }
    } else {
        let mut it = fields.iter();
        let first = it.next().unwrap();
        let mut expr = bin_eq(me_field(first), var_field("__ny_other", first));
        for f in it {
            expr = bin_and(expr, bin_eq(me_field(f), var_field("__ny_other", f)));
        }
        expr
    };
    ASTNode::FunctionDeclaration {
        name: "equals".to_string(),
        params: vec!["__ny_other".to_string()],
        body: vec![ASTNode::Return { value: Some(Box::new(cond)), span: Span::unknown() }],
        is_static: false,
        is_override: false,
        span: Span::unknown(),
    }
}

Hakorune版Strategy 1: JSON直接操作

// apps/macros/derive/derive_equals.hako
static box DeriveEqualsMacro {
    expand(json_str, ctx) {
        local ast, stmts, i, modified
        ast = JSON.parse(json_str)

        if ast.kind != "Program" {
            return json_str
        }

        stmts = ast.statements
        modified = false
        i = 0

        loop(i < stmts.length()) {
            local stmt
            stmt = stmts.get(i)

            if stmt.kind == "BoxDeclaration" {
                // public_fieldsチェック
                if stmt.public_fields && stmt.public_fields.length() > 0 {
                    // equals()メソッドが存在しないか確認
                    if !stmt.methods || !stmt.methods.has("equals") {
                        // equals()生成
                        local equals_method
                        equals_method = me.build_equals_json(stmt.name, stmt.public_fields)

                        // methodsに追加
                        if !stmt.methods {
                            stmt.methods = json({})
                        }
                        stmt.methods.set("equals", equals_method)
                        modified = true
                    }
                }
            }

            i = i + 1
        }

        if modified {
            return JSON.stringify(ast)
        } else {
            return json_str
        }
    }

    build_equals_json(box_name, fields) {
        // JSON AST for: equals(__ny_other) { return me.f1 == __ny_other.f1 && ... }
        local body, cond, i

        if fields.length() == 0 {
            cond = json({
                "kind": "Literal",
                "value": true,
                "span": json({})
            })
        } else {
            i = 0
            cond = null

            loop(i < fields.length()) {
                local field_name, eq_expr
                field_name = fields.get(i)

                // me.field == __ny_other.field
                eq_expr = json({
                    "kind": "BinaryOp",
                    "operator": "Equal",
                    "left": json({
                        "kind": "FieldAccess",
                        "object": json({ "kind": "Me", "span": json({}) }),
                        "field": field_name,
                        "span": json({})
                    }),
                    "right": json({
                        "kind": "FieldAccess",
                        "object": json({ "kind": "Variable", "name": "__ny_other", "span": json({}) }),
                        "field": field_name,
                        "span": json({})
                    }),
                    "span": json({})
                })

                if !cond {
                    cond = eq_expr
                } else {
                    // cond && eq_expr
                    cond = json({
                        "kind": "BinaryOp",
                        "operator": "And",
                        "left": cond,
                        "right": eq_expr,
                        "span": json({})
                    })
                }

                i = i + 1
            }
        }

        // FunctionDeclaration
        return json({
            "kind": "FunctionDeclaration",
            "name": "equals",
            "params": arr(["__ny_other"]),
            "body": arr([
                json({
                    "kind": "Return",
                    "value": cond,
                    "span": json({})
                })
            ]),
            "is_static": false,
            "is_override": false,
            "span": json({})
        })
    }
}

🧪 テスト戦略

1. ユニットテストHakorune版

// apps/macros/derive/test_derive_equals.hako
static box TestDeriveEquals {
    test_simple_box() {
        local input, expected, actual

        input = json({
            "kind": "Program",
            "statements": arr([
                json({
                    "kind": "BoxDeclaration",
                    "name": "Person",
                    "public_fields": arr(["name", "age"]),
                    "methods": json({})
                })
            ])
        })

        actual = DeriveEqualsMacro.expand(JSON.stringify(input), "{}")

        // equals()メソッドが追加されていることを確認
        local result
        result = JSON.parse(actual)
        assert(result.statements.get(0).methods.has("equals"))

        return true
    }
}

2. 統合テスト(スモークテスト)

# tools/smokes/v2/profiles/quick/macro/derive_equals_vm.sh
#!/usr/bin/env bash
# Smoke: @derive(Equals) basic functionality

NYASH_MACRO_ENABLE=1 \
NYASH_MACRO_PATHS=apps/macros/derive/derive_equals.hako \
./target/release/hako apps/tests/macro_derive_person.hako

3. パリティテストRust版 vs Hakorune版

# 同じ入力に対して同じ出力を生成するか確認
diff <(NYASH_MACRO_DERIVE_BACKEND=rust ./hako test.hako --dump-ast) \
     <(NYASH_MACRO_DERIVE_BACKEND=hako ./hako test.hako --dump-ast)

📊 進捗管理

Phase 20.1: @derive(Equals)実装

  • DeriveEqualsMacro.build_equals_json() 実装
  • ユニットテスト作成・通過
  • スモークテスト作成・通過
  • パリティテスト通過Rust版と同一出力
  • ドキュメント作成

Phase 20.2: @derive拡張

  • DeriveToStringMacro 実装
  • DeriveCloneMacro 実装
  • DeriveDebugMacro 実装

Phase 20.3: @test ランナー

  • TestCollectorBox 実装
  • TestRunnerBox 実装
  • JSON引数注入対応

🚨 リスク管理

Risk 1: JSON操作の複雑性

対策: AstBuilderBoxヘルパーを段階的に実装

Risk 2: パフォーマンス劣化

対策: ベンチマーク必須(目標: Rust版の2倍以内

Risk 3: バグ再発

対策: Rust版の既知バグを事前に洗い出し、テストケース化

🔗 参考資料

  • Phase 16 README - Rust版設計書
  • Phase 16 IMPLEMENTATION - Rust版実装ガイド
  • src/macro/engine.rs - derive実装参考
  • src/macro/pattern.rs - Pattern/Quote実装参考
  • src/macro/mod.rs - @test実装参考

Next: Phase 15.7完了後、Phase 20.1開始