1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Copilotが生成した日本語トランスコンパイラ言語 Re:MindのASTパーサが動作するまで(ステップ10)

1
Posted at

はじめに

生成AIに日本語トランスコンパイラ言語 Re:Mindの実装言語仕様を読み込ませて、いくつかの抽象度レベルのプロンプトからトランスコンパイラ的な動きをさせてみたところ、Re:Mindっぽい仕様のASTパーサのテンプレと3言語(Java,C#,VB.NET)のコードジェネレータのテンプレを生成してもらえた1ので、本記事はその検証の第10ステップです。

Microsoft Copilot

生成AIはWindows11アプリケーションのMicrosoft Copilotとします。モードは「Smart(GPT5.1)」。サインインした状態で使用します。第6ステップの後にはサインアウトがあります。

第10ステップ

第6ステップ以降はプロンプト10240文字以内の制限内での生成済ソースコードの分割提示しており、今回もその続きです。今回で再提示はいったん完了します。

プロンプト

残りの関数を10240文字の制限内で入るだけ、下記に示します。

        private LocalVariableDeclaration? ParseLocalVariableDeclaration(JavadocComment javadoc)
        {
            if (_index >= _lines.Length) return null;

            var line = _lines[_index].Trim();
            // 例: ・int[] 配列 = 15,13,9,6,4,1
            //     ・int 外側 = 0
            var m = Regex.Match(line, @"・(?<type>[a-zA-Z\[\]]+)\s+(?<nameJa>\S+)\s*=\s*(?<rhs>.+)");
            if (!m.Success)
            {
                _index++;
                return null;
            }

            var type = m.Groups["type"].Value;
            var nameJa = m.Groups["nameJa"].Value;
            var rhs = m.Groups["rhs"].Value.Trim();

            Expression initializer;

            if (rhs.Contains(","))
            {
                // 配列のようなリテラル(今回: int[] 配列)
                var arr = new ArrayLiteralExpression
                {
                    ElementType = "int"
                };
                var nums = rhs.Split(',').Select(s => s.Trim());
                foreach (var n in nums)
                {
                    if (int.TryParse(n, out int v))
                    {
                        arr.Elements.Add(new LiteralExpression { Value = v });
                    }
                }
                initializer = arr;
            }
            else if (int.TryParse(rhs, out int v))
            {
                initializer = new LiteralExpression { Value = v };
            }
            else
            {
                // 簡易対応
                initializer = new IdentifierExpression { NameJa = rhs, NameEn = MapIdentifierName(rhs) };
            }

            var decl = new LocalVariableDeclaration
            {
                Javadoc = javadoc,
                Type = type,
                NameJa = nameJa,
                NameEn = MapIdentifierName(nameJa),
                Initializer = initializer
            };

            _index++;
            return decl;
        }

        private WhileStatement ParseWhileStatement(string line)
        {
            // 〇外側 < 配列.Length の間は繰り返す
            var condPart = line.TrimStart('〇').Replace("の間は繰り返す", "").Trim();
            var cond = ParseBinaryCondition(condPart);

            var ws = new WhileStatement
            {
                Condition = cond
            };

            _index++;

            // 本体を読み取る(〇ここまでまで)
            while (_index < _lines.Length)
            {
                var cur = _lines[_index].Trim();
                if (cur.StartsWith("〇ここまで"))
                {
                    _index++;
                    break;
                }

                if (cur.StartsWith("/**"))
                {
                    var j = ParseJavadoc();
                    var decl = ParseLocalVariableDeclaration(j);
                    if (decl != null)
                    {
                        ws.Body.Add(decl);
                        continue;
                    }
                }
                else if (cur.StartsWith("〇") && cur.Contains("の間は繰り返す"))
                {
                    // 内側 while
                    var innerWhile = ParseWhileStatement(cur);
                    ws.Body.Add(innerWhile);
                    continue;
                }
                else if (cur.StartsWith("◇配列[内側] > 配列[内側 + 1] の場合"))
                {
                    var ifStmt = ParseIfSwapBlock();
                    ws.Body.Add(ifStmt);
                    continue;
                }
                else if (cur.StartsWith("□外側++"))
                {
                    var expr = new UnaryExpression
                    {
                        Operator = "++",
                        Operand = new IdentifierExpression
                        {
                            NameJa = "外側",
                            NameEn = "outer"
                        }
                    };
                    ws.Body.Add(new ExpressionStatement { Expression = expr });
                    _index++;
                    continue;
                }
                else if (cur.StartsWith("□内側++"))
                {
                    var expr = new UnaryExpression
                    {
                        Operator = "++",
                        Operand = new IdentifierExpression
                        {
                            NameJa = "内側",
                            NameEn = "inner"
                        }
                    };
                    ws.Body.Add(new ExpressionStatement { Expression = expr });
                    _index++;
                    continue;
                }
                else
                {
                    _index++;
                }
            }

            return ws;
        }

        private ForStatement ParseForStatement(string line)
        {
            // 〇int i=0,i<配列.Length,i++ 繰り返す
            var body = line.TrimStart('〇').Replace("繰り返す", "").Trim();
            // int i=0,i<配列.Length,i++
            var parts = body.Split(',');

            // 初期化: int i=0
            var initPart = parts[0].Trim();
            var mInit = Regex.Match(initPart, @"int\s+(?<name>\w+)\s*=\s*(?<value>\d+)");
            var initializer = new LocalVariableDeclaration
            {
                Type = "int",
                NameJa = mInit.Groups["name"].Value,
                NameEn = mInit.Groups["name"].Value,
                Initializer = new LiteralExpression { Value = int.Parse(mInit.Groups["value"].Value) }
            };

            // 条件: i<配列.Length
            var condPart = parts[1].Trim();
            var condition = ParseBinaryCondition(condPart);

            // 反復: i++
            var iterPart = parts[2].Trim();
            var iterExpr = new UnaryExpression
            {
                Operator = "++",
                Operand = new IdentifierExpression
                {
                    NameJa = "i",
                    NameEn = "i"
                }
            };

            return new ForStatement
            {
                Initializer = initializer,
                Condition = condition,
                Iterator = iterExpr
            };
        }

        private Expression ParseBinaryCondition(string condPart)
        {
            // かなり固定フォーマットに依存: 例 「外側 < 配列.Length」/「内側 < 配列.Length - 外側 - 1」/「i<配列.Length」
            condPart = condPart.Replace(" ", "");

            // 不等号で切る簡易版
            if (condPart.Contains("<="))
            {
                var parts = condPart.Split(new[] { "<=" }, StringSplitOptions.None);
                return new BinaryExpression
                {
                    Operator = "<=",
                    Left = ParseSimpleExpression(parts[0]),
                    Right = ParseSimpleExpression(parts[1])
                };
            }
            if (condPart.Contains("<"))
            {
                var parts = condPart.Split('<');
                return new BinaryExpression
                {
                    Operator = "<",
                    Left = ParseSimpleExpression(parts[0]),
                    Right = ParseSimpleExpression(parts[1])
                };
            }

            throw new NotSupportedException("この条件は簡易パーサでは未対応です: " + condPart);
        }

        private Expression ParseSimpleExpression(string text)
        {
            // 配列.Length - 外側 - 1 のようなものも来るので、ここも簡易。
            if (int.TryParse(text, out var n))
            {
                return new LiteralExpression { Value = n };
            }

            if (text.Contains("配列.Length"))
            {
                // 配列.Length - 外側 - 1 など
                // ここでは BinaryExpression ネストを手書き
                // 今回用途に特化
                if (text == "配列.Length")
                {
                    return new MemberAccessExpression
                    {
                        Expression = new IdentifierExpression
                        {
                            NameJa = "配列",
                            NameEn = "array"
                        },
                        MemberName = "Length"
                    };
                }

                // 配列.Length-外側-1
                var replaced = text.Replace("配列.Length", "L");
                var tokens = replaced.Split('-'); // L,外側,1
                Expression expr = new MemberAccessExpression
                {
                    Expression = new IdentifierExpression
                    {
                        NameJa = "配列",
                        NameEn = "array"
                    },
                    MemberName = "Length"
                };
                for (int i = 1; i < tokens.Length; i++)
                {
                    Expression right;
                    if (int.TryParse(tokens[i], out var vi))
                    {
                        right = new LiteralExpression { Value = vi };
                    }
                    else
                    {
                        right = new IdentifierExpression
                        {
                            NameJa = tokens[i],
                            NameEn = MapIdentifierName(tokens[i])
                        };
                    }
                    expr = new BinaryExpression
                    {
                        Operator = "-",
                        Left = expr,
                        Right = right
                    };
                }

                return expr;
            }

            // 単純識別子
            return new IdentifierExpression
            {
                NameJa = text,
                NameEn = MapIdentifierName(text)
            };
        }

生成レスポンス

結論から言うと:

  • デシリアライズがノードを「消している」のではなく
  • そもそもシリアライズ前の AST の時点で Body が空 ([])
  • つまり原因は パーサー側(ParseMethodBody 周辺)にあり、JSON のプロパティ名対応やデシリアライズの問題ではない
    です。

もしよければ、次の一歩として、

  • ParseJavadoc の実装
  • 1 回だけログ入り ParseMethodBody を走らせた結果
    あたりを一緒に見て、「どの行でマッチがすり抜けているか」をピンポイントで特定していきませんか。
    そこが分かれば、Body に Statement が入り始めて、一気に Re:Mind → C# が「フル AST モード」で動き出します。

プロンプト2

ParseJavadoc()の全文です。

       private JavadocComment ParseJavadoc()
        {
            var j = new JavadocComment();
            _index++; // /** 行を飛ばす

            while (_index < _lines.Length)
            {
                var line = _lines[_index].Trim();

                if (line.StartsWith("*/"))
                {
                    _index++;
                    break;
                }

                // * Program
                if (line.StartsWith("*"))
                {
                    var content = line.TrimStart('*').Trim();

                    if (content.StartsWith("@param"))
                    {
                        // @param 引数 args
                        var parts = content.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                        if (parts.Length >= 3)
                        {
                            j.Params.Add(new JavadocParam
                            {
                                NameJa = parts[1],
                                NameEn = parts[2]
                            });
                        }
                    }
                    else if (!string.IsNullOrEmpty(content))
                    {
                        j.Summary = content;
                    }
                }

                _index++;
            }

            return j;
        }

生成レスポンス2

ParseJavadoc() を見せていただきありがとうございます。
この関数自体は 正常に動いています。
そして、ここまでの全コードを総合すると、Body が空になる原因は ParseJavadoc() ではなく、ParseMethodBody() の行判定ロジックにあります。
ただし、ParseJavadoc() にも 副作用として Body が空になる原因を引き起こす可能性があるポイント が 1 つだけあります。

🎯 結論
ParseJavadoc() 自体は正しく動いているが、
ParseJavadoc() が _index を 1 行多く進めてしまうと、
直後のローカル変数行(・)をスキップしてしまう可能性がある。

つまり:

/** array */
int[] 配列 = 15,13,9,6,4,1

この 2 行をパースするときに、

  • ParseJavadoc() が */ の次の行まで進む
  • その後 ParseLocalVariableDeclaration() が呼ばれる
  • しかし _index がすでに「・」行を通り過ぎていると、ローカル変数がパースされない
  • 結果として Body に Statement が追加されない
    という現象が起きます。

第10ステップの評価

この生成レスポンスの後も、詳しいソースコードの評価から正規表現の実装の調整などが繰り返されたのですが、かなり長大とりますのでそれは割愛します。

おわりに

第10ステップは以上です。次回はログ出力機能を実装したところから開示いたします。

生成レスポンスセクションの内容は生成AIが生成したものです。引用されているソースコードは完全には動作しない状態が含まれます。

  1. Microsoft Copilotで日本語トランスコンパイラ言語 Re:Mindをいじりたおす - Qiita
    「Microsoft Copilotで日本語トランスコンパイラ言語 Re:Mindをいじりたおす」の記事リストです。(ステップ降順)
    ステップ26 ターゲット言語JavaとVB.NETへの展開
    ステップ25 ターゲット言語C#へのASTからの生成処理
    ステップ24 ASTを生成するサンプル実装をC#で生成
    ステップ23 マッピングルール生成
    ステップ22 AST(抽象構文木)風の構造化表示
    ステップ21 バブルソート 実行可能形式 ターゲットJava C# VB.NET
    ステップ20 二分探索法 実行可能形式 ターゲットJava C# VB.NET
    ステップ19 線形探索法 実行可能形式 ターゲットJava C# VB.NET
    ステップ18 バブルソート 1案件づつやりなおし
    ステップ17 二分探索法 1案件づつやりなおし
    ステップ16 線形探索法 1案件づつやりなおし
    ステップ15 3つの定番アルゴリズムソースの忘却原因調査
    ステップ14 バブルソートソースの忘却発生再確認
    ステップ13 二分探索法ソースの忘却発生
    ステップ12 線形探索法の6言語変換
    ステップ11 バージョン情報付きの3定番アルゴリズムソースのフィードバック修正
    ステップ10 バージョン情報付きの3定番アルゴリズムソースのフィードバック修正
    ステップ9 バージョン情報付きで3定番アルゴリズムソースの生成
    ステップ8 バージョニングが可能か確認
    ステップ7 Re:Mindの詳しい言語仕様の学習結果をバブルソートで確認
    ステップ6 Re:Mindの詳しい言語仕様の学習結果を二分探索法で確認
    ステップ5 Re:Mindの詳しい言語仕様の学習結果を線形探索法で確認
    ステップ4 Re:Mindの詳しい言語仕様の学習成果を保持するスコープを確認
    ステップ3 Re:Mindの詳しい言語仕様を学習できる方法を確認
    ステップ2 Re:Mindの言語仕様の認知度を確認
    ステップ1 Re:Mindの認知度を確認

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?