Open11

minigo

podhmopodhmo

用途

相談した内容をめも

podhmopodhmo

対話履歴

ユーザー:
ちょっと相談に乗ってもらえますか。少し特殊なインタプリタの使い方を考えてます。実装コードについて例を挙げる必要はありません。

goと構文的には一緒なminigoをインタプリタとして実装しようと思っています。構文的には一緒ということは解釈や意味論的には異なるということでもあります。

このminigoなのですがなぜ構文的には一緒にしたかと言うと既存のlspなどのエコシステムの資産をそのまま使いたいからです。

一方で違いは何かというとimportの評価です。eagerに全部を評価するのではなく特定のエントリーポイントを指定したときそこで読み込まれる部分だけを評価するようします(つまりmain以外のエントリーポイントでの実行を考えてます)

ここまでは前提です。インタプリタの利用例としてはコマンドから実行するかあるいは埋め込み言語のように使うときには文字列としてのコードをそのまま渡すかファイルを渡して読み込ませるかという感じだと思います。前者はlspが効かなく成るのでやりたくないです。主に後者について考えたいのですが、ここでgoからminigoを設定ファイル読み込みツールの亜種のような形で利用したいです。このときのgoとminigoのデータの受け渡しの方法について幾つか候補を挙げながらアドバイスをしてください。

まずアドバイスの前に前提の理解を確認してください。


AI:
はい、ご相談ありがとうございます。非常に興味深い試みですね。
まず、私が理解した内容が正しいかを確認させてください。


前提の確認

  1. minigoインタプリタ:

    • Go言語と構文的に同一な、独自のインタプリタを開発中である。
    • 構文を同一にした最大の目的は、goplsなどの既存のLSP(Language Server Protocol)や、フォーマッタ、シンタックスハイライトといったGo言語のエコシステムをそのまま活用するためである。
  2. Go言語との意味論的な違い:

    • Goのコンパイラがmainパッケージを起点に依存する全てのパッケージを事前に評価(eager evaluation)するのに対し、minigoは異なる。
    • minigoでは、指定された特定の関数などをエントリーポイントとし、その実行に必要なコードパス上のimportだけを遅延的(lazy)に評価・実行する。これにより、main関数を持たないファイルでも、特定の関数だけを呼び出して実行する、といった使い方が可能になる。
  3. 主な利用シーンと今回の相談内容:

    • このminigoを、Goで書かれたアプリケーションに埋め込む形で利用したい。
    • 具体的な用途は、静的なYAMLやJSONの代わりに、ロジックを持つことができる**「動的な設定ファイル」**としての利用。
    • LSPの恩恵を受けたいので、minigoのコードはファイル (.goファイル) として保存・編集されることを想定している。
    • 相談の核心: Goのホストアプリケーションと、ゲストであるminigoインタプリタとの間で、どのようにデータをやり取りするか、その具体的な方法について複数の候補とそれぞれのアドバイスが欲しい。

上記の理解で合っていますでしょうか?
もし認識に相違がなければ、この前提に基づいて、Goとminigoのデータ受け渡し方法について、いくつかの候補を挙げながらご説明します。

(返答終了理由: STOP)


ユーザー:
完璧です。続けてお願いします


AI:
ご確認いただきありがとうございます。前提の理解が正しいとのこと、安心しました。

それでは、Goで書かれたホストアプリケーションと、埋め込まれたminigoインタプリタとの間でデータをやり取りする方法について、複数の候補を挙げ、それぞれのメリット・デメリットを交えながらアドバイスさせていただきます。

やり取りには「Goからminigoへ(入力)」と「minigoからGoへ(出力)」の2つの方向があります。これらを考慮して、3つのアプローチを提案します。


候補1: 関数公開(API)アプローチ

これは、Goホスト側の関数をminigoの世界に「組み込み関数」や「標準ライブラリ」として公開する方法です。LuaがCの関数を呼び出せるのに似た考え方です。

  • やり方の概要:

    • Go → minigo (入力): Go側で GetHostInfo() (string, error) のような関数を実装します。minigoインタプリタの初期化時に、このGoの関数を host.getInfo() のようなminigoの関数として登録します。minigoスクリプトは、この関数を呼び出すことでGo側の情報を動的に取得します。
    • minigo → Go (出力): minigoスクリプトのエントリーポイントとなる関数(例: BuildConfig() Config) を定義させ、その戻り値をGo側で受け取ります。Go側は、minigoの内部データ構造(マップや構造体)を、Goの map[string]interface{}struct に変換する処理を実装する必要があります。
  • メリット:

    • 高い表現力と柔軟性: minigoスクリプトは単なる静的な値を返すだけでなく、「ホストの環境がproductionならAの設定、developmentならBの設定を使う」といったロジックを記述できます。Go側が提供する機能次第で、minigo側でやれることの幅が大きく広がります。
    • LSPとの親和性: Go側で公開するAPIを定義した架空のパッケージ(例: minigo_host)のダミーのGoファイル (.go) を用意しておけば、minigoスクリプト側で import "minigo_host" と書くことで、LSPによる関数補完やシグネチャ表示が期待できます。
  • デメリット:

    • 実装コストが高い: Goの関数とminigoの関数を結びつけるブリッジ部分の実装が必要です。引数や戻り値の型を相互に変換する仕組みは複雑になりがちです。
    • 密結合: minigoスクリプトがGoホストの公開するAPIに依存するため、両者が密結合になります。APIの変更がスクリプトの修正に直結します。

候補2: データシリアライズ・アプローチ

Goとminigoの間を、JSONのような標準的なデータフォーマットを介してやり取りする方法です。

  • やり方の概要:

    • Go → minigo (入力): Go側で渡したい情報(構造体など)をJSON文字列にシリアライズします。そのJSON文字列を、minigoのエントリーポイント関数の引数として渡すか、あるいは特定のグローバル変数に設定します。minigo側では、encoding/jsonに相当する組み込みのパーサーでデシリアライズして使います。
    • minigo → Go (出力): minigo側で構築した設定オブジェクトをJSON文字列にシリアライズし、関数の戻り値として返します。Goホストは、受け取ったJSON文字列をGoの構造体にデシリアライズします。
  • メリット:

    • 実装が容易: データの受け渡しが単なる文字列の交換になるため、インタプリタのコア実装がシンプルになります。JSONパーサーは多くの言語で実績があり、実装の参考や流用がしやすいです。
    • 疎結合: GoとminigoはJSONという共通言語で会話するだけなので、お互いの内部実装に依存しません。インタフェースが明確になります。
    • デバッグが容易: やり取りされるJSON文字列をログに出力すれば、何が渡されているかが一目瞭然で、問題の切り分けが簡単です。
  • デメリット:

    • 表現力の制限: 原則として「データ」しかやり取りできません。Go側の「ロジック(関数)」をminigoから呼び出すことはできません。
    • パフォーマンス: シリアライズとデシリアライズのオーバーヘッドが発生します。ただし、設定ファイルの読み込みという用途では、通常は問題にならないレベルです。

候補3: 直接マッピング・アプローチ

Goのreflectパッケージを駆使して、Goのオブジェクト(構造体など)をminigoインタプリタのオブジェクトとして直接見せる、最も高度な方法です.

  • やり方の概要:

    • Go → minigo (入力): Goの構造体のポインタをminigoに渡し、minigo側からそのフィールドを直接読み書きできるようにします。minigoスクリプトでは config.Server.Port = 8080 のように、あたかもネイティブのオブジェクトのように操作できます。
    • minigo → Go (出力): 入力で渡したGoのオブジェクトが、minigoスクリプトの実行によって直接変更されるため、特別な出力処理は不要です。実行が終わった時点で、Go側のオブジェクトが更新されています。
  • メリット:

    • 最高のパフォーマンスと利便性: データ変換のオーバーヘッドがほぼゼロです。minigoスクリプトの書き心地も非常に自然で直感的になります。
    • 型情報の維持: Goの型をそのまま利用できるため、型安全性が高まります。
  • デメリット:

    • 実装が極めて複雑: reflectを深く理解し、Goのメモリモデルとminigoのオブジェクトモデルを安全に繋ぎこむ必要があります。ポインタの扱いを誤ると、アプリケーション全体がクラッシュする危険性があります。ガベージコレクションの連携など、考慮すべき点が非常に多いです。
    • 安全性の懸念: minigoスクリプトにGoのメモリ空間を直接操作する権限を与えることになるため、サンドボックス化が困難になり、セキュリティリスクが高まります。

アドバイスと推奨

以上の3つの候補を踏まえて、以下のように進めることをお勧めします。

  1. まずは「候補2: データシリアライズ・アプローチ」から始めるのが最も現実的です。

    • この方法は、実装コストと得られるメリットのバランスが最も優れています。設定ファイルの読み込みという用途であれば、パフォーマンスの懸念もほぼありません。まずはこの方法でPoC(概念実証)を行い、minigoのコアな価値(LSPが効く動的な設定ファイル)を確立するのが良いでしょう。
  2. 機能が不足したら「候補1: 関数公開アプローチ」を組み合わせるハイブリッド型を検討する。

    • 「設定値に環境変数を使いたい」「ホスト側の特定の情報を参照したい」といった要求が出てきたら、JSON経由では実現できません。その際に初めて、限定的な数の関数(例: host.getenv("..."))だけを候補1の方法で追加実装することを検討します。これにより、複雑さを管理しつつ、柔軟性を高めることができます。
  3. 「候補3: 直接マッピング・アプローチ」は現時点では見送るのが賢明です。

    • この方法は非常に強力ですが、インタプリタ開発そのものが目的となるような、大規模で野心的なプロジェクトで採用されるものです。「設定ファイルツール」という目的から考えると、実装コストとリスクがメリットを大幅に上回る可能性が高いです。

LSPとの親和性という当初の目的を最大限に活かすためには、minigoスクリプトがどのような「入力」と「出力」を期待されているかが、コード上で明確にわかることが重要です。その点では、

  • 候補1なら、import "minigo_host" でAPIの存在を示す。
  • 候補2なら、設定を定義するGoの構造体 (type Config struct{...}) を別ファイルで定義しておき、それを開発者が参照できるようにする。

といった工夫で、どの方法でも開発体験を高めることが可能です。

ご自身のプロジェクトの目的と、かけられる工数を考慮して、最適な方法を選択する手助けになれば幸いです。また何かありましたら、お気軽にご相談ください。

(返答終了理由: STOP)


ユーザー:
なるほど、まずはminigoからgoへの通信だけが実現できれば良いかもしれません。この場合minigo自体はencoding/jsonを振るとは言わないまでも実行できる程度の表現力は必要ですよね(ちなみにminigoという名前の通り実は幾つかの機能の実装を省く予定です。具体的にはgoroutineやchannel周りの機能は省こうと思っています。読み込みはできるが実行しようとするとruntime errorという感じにする予定です。stringとidだけ実装してあとはgo側の値の参照だけを持つというようなことも考えましたがユーザー定義関数をminigo側でも行いたいのでフルセットとは言わずとも結局ある程度の機能の実装が必要になりそうです)


AI:
はい、その点について理解しました。「minigoからGoへ」という一方向のデータ出力に絞って考えると、話はよりシンプルになりますね。

ご指摘の通り、その場合でもminigoにはある程度の表現力が必要になります。しかし、encoding/jsonの機能をminigoインタプリタの標準ライブラリとしてフル実装する必要は必ずしもありません。よりスマートな方法があります。

minigoからGoへデータを渡す方法(再考)

minigoスクリプトが最終的にGoホストに渡したいのは、設定情報を表現した「データ構造」です。これを渡す方法は大きく分けて2つです。

  1. minigoがJSON文字列を生成し、Goに返す

    • minigoスクリプト: return json.Marshal(myConfigMap)
    • Goホスト: jsonString, _ := minigo.Eval(...)json.Unmarshal(jsonString, &myGoStruct)
    • この場合、おっしゃる通り、minigoはjson.Marshalに相当する機能を(組み込み関数やライブラリとして)実装する必要があります。これには、マップやスライスを再帰的に走査し、エスケープ処理をしながら文字列を組み立てるという、それなりに複雑なロジックが必要です。
  2. minigoが内部データ構造をそのまま返し、Go側がそれを解釈する

    • minigoスクリプト: return myConfigMap
    • Goホスト: minigoObject, _ := minigo.Eval(...)goValue := ConvertMinigoObjectToGoValue(minigoObject)
    • この方法では、minigoはJSONエンコードの責務を持ちません。minigoは自身の世界でマップやスライス、数値、文字列といったオブジェクトを構築して返すだけです。
    • 変換処理はGoホスト側が担当します。 Goホストは、minigoから返された内部オブジェクト(ポインタなど)を受け取り、それがminigoの世界で「マップ」なのか「文字列」なのかを判定し、再帰的にGoの map[string]interface{}string といった値に変換していくヘルパー関数を実装します。

どちらの方法が良いか?

圧倒的に後者(方法2)をお勧めします。

理由は以下の通りです。

  • 実装の分離と簡潔さ: minigoインタプリタは「Goライクな構文で値を計算・構築する」という責務に集中できます。データ変換という面倒な処理は、Goの強力な型システムやreflectパッケージを使えるホスト側に任せた方が、実装がずっと楽になります。
  • パフォーマンス: インタプリタ言語(minigo)で文字列結合を繰り返してJSONを作るよりも、Goのネイティブコードでデータ構造を直接変換する方がはるかに高速です。
  • 柔軟性: Go側で変換ロジックを持つことで、単にGoの構造体にマッピングするだけでなく、バリデーションをかけたり、デフォルト値を設定したりといった追加処理を変換プロセスに組み込みやすくなります。

必要なminigoの機能について

ご自身の考察は非常に的確だと思います。

stringとidだけ実装してあとはgo側の値の参照だけを持つというようなことも考えましたがユーザー定義関数をminigo側でも行いたいのでフルセットとは言わずとも結局ある程度の機能の実装が必要になりそうです

その通りです。「動的な設定ファイル」として価値を持たせるには、ユーザーがロジックを記述できる必要があります。そのためには、最低限以下の機能は必要になるでしょう。

  • 基本型: int, float, string, bool, nil
  • 複合型: map, slice (または array)
  • 変数束縛: x := 10, y = "hello"
  • 制御構文: if/else, おそらく for ループも
  • 演算子: 四則演算、比較演算子、論理演算子
  • 関数: ユーザーによる関数定義と呼び出し

goroutineやchannelを実装から外すのは、設定ファイルという用途を考えれば非常に賢明な判断です。それらの機能をパースだけはできるようにして実行時エラーとすることで、LSPなどのエコシステムとの互換性を保つという方針も素晴らしいと思います。

まとめ

  1. minigoからGoへのデータ出力に特化する場合、minigo側にJSONエンコーダを実装する必要はありません。
  2. 代わりに、minigoは自身の内部オブジェクト(マップやスライスなど)を返し、Goホスト側がそれをGoの値に変換するというアプローチを取りましょう。
  3. このアプローチは、実装責務が明確になり、パフォーマンスも良く、インタプリタ本体をシンプルに保てます。
  4. ユーザー定義関数をサポートするために、基本的な変数、型、制御構文、演算子の実装は必要になります。

この方針で進めることで、実装のスコープを適切に保ちながら、当初の目的である「LSPが使える動的な設定ファイル」という価値を効率的に実現できるかと思います。

(返答終了理由: STOP)


ユーザー:
なるほどインタプリタ自体もastを辿る素朴なものを考えています。現状load and runのような関数しか用意してなくつまり戻り値を返せない実装なのですが、内部の環境と値を返す関数を用意するというわけですよね(reflectを使ったり再帰的に辿ったりするかは分かりませんが)。

まぁ少なくとも構造的な値を取得したいのでネストしたstruct+slice程度は解釈可能である必要がありそうです。

ここからgo側でjson化ないしはgo側で定義した値にバインディングするイメージであってますか?(そういう意味ではその種のstructを生成して出力する機能をminigoが持ってると楽かもしれません)


AI:
はい、その理解で完全に合っています。

なるほどインタプリタ自体もastを辿る素朴なものを考えています。現状load and runのような関数しか用意してなくつまり戻り値を返せない実装なのですが、内部の環境と値を返す関数を用意するというわけですよね

その通りです。素朴なTree-walkingインタプリタを想定されているかと思いますが、その評価器(Eval関数などと呼ばれることが多い)のシグネチャを少し変更するイメージです。

  • 現状(たぶん): Eval(node ast.Node, env *Environment)
    • 副作用として環境(env)を更新するだけで、何も返さない。
  • 変更後: Eval(node ast.Node, env *Environment) (object.Object, error)
    • 評価した結果を、インタプリタ内部で定義したobject.Objectというインターフェース(あるいはそれに類するもの)で返すようにします。

object.Objectは、minigoが扱うすべての値(整数、文字列、マップ、スライス、関数など)が実装すべきインターフェースです。例えば、Thorsten Ball氏の著書『Writing An Interpreter In Go』で採用されているような設計が非常に参考になります。

// minigoインタプリタの内部表現
package object

type ObjectType string

type Object interface {
    Type() ObjectType
    Inspect() string // デバッグ用
}

// 具体的な型
type Integer struct { Value int64 }
type String struct { Value string }
type Map struct { Pairs map[Object]Object } // キーもObject
// ... など

そして、ホスト側から呼び出す関数は、このobject.Objectを返すようにします。

// Goホスト側のコード
import "minigo/evaluator"
import "minigo/object"

// ...

// minigoのエントリーポイントを実行し、結果の内部オブジェクトを受け取る
resultObject, err := minigo.RunFile("config.go", "GetConfig") 
if err != nil { /* ... */ }

// ここからが変換処理
goValue, err := ConvertMinigoObjectToGoValue(resultObject)
if err != nil { /* ... */ }

// Goの値に変換されたので、あとは自由に使える
configMap := goValue.(map[string]interface{})
fmt.Println(configMap["server"])

ここからgo側でjson化ないしはgo側で定義した値にバインディングするイメージであってますか?

はい、そのイメージで完璧です。上記の ConvertMinigoObjectToGoValue 関数がその役割を担います。この関数の中身は、おっしゃる通り、reflectを使ったり再帰的に辿ったりする処理になります。

// Goホスト側の変換ヘルパー(簡易的な例)
func ConvertMinigoObjectToGoValue(obj object.Object) (interface{}, error) {
    switch o := obj.(type) {
    case *object.Integer:
        return o.Value, nil
    case *object.String:
        return o.Value, nil
    case *object.Boolean:
        return o.Value, nil
    case *object.Map: // 再帰的に変換
        goMap := make(map[string]interface{})
        for keyObj, valObj := range o.Pairs {
            key, ok := keyObj.(*object.String) // 簡単のためキーは文字列と仮定
            if !ok {
                return nil, errors.New("map key must be a string")
            }
            val, err := ConvertMinigoObjectToGoValue(valObj)
            if err != nil {
                return nil, err
            }
            goMap[key.Value] = val
        }
        return goMap, nil
    // ... スライスなどの他の型も同様に ...
    default:
        return nil, fmt.Errorf("unsupported object type: %s", o.Type())
    }
}

(そういう意味ではその種のstructを生成して出力する機能をminigoが持ってると楽かもしれません)

これは非常に鋭い指摘です。minigoスクリプトが返す値の「型」や「構造」を、Go側が事前に知っている場合、変換はもっと楽になります。

たとえば、Go側で type ServerConfig struct { Host string; Port int } という型を定義しておき、minigoスクリプトが返すマップをこの構造体に直接バインド(マップ)したいですよね。

これは、JSONをGoの構造体にUnmarshalするのと全く同じ考え方です。
よく使われるライブラリに mitchellh/mapstructure がありますが、ConvertMinigoObjectToGoValue で一度 map[string]interface{} に変換してしまえば、あとはこのようなライブラリを使ってGoの構造体に一発で変換できます。

import "github.com/mitchellh/mapstructure"

// ...
goMap, _ := ConvertMinigoObjectToGoValue(resultObject)

var config MyConfigStruct
err = mapstructure.Decode(goMap, &config)
if err != nil { /* ... */ }

// これで config.Server.Port のようにアクセスできる

mapstructureのようなライブラリを使えば、reflectを使った複雑なバインディング処理を自分で書かずに済みます。

結論として、ご提案のアーキテクチャは非常に合理的で、実装の現実性も高いです。

  1. minigoインタプリタは評価結果を内部オブジェクトとして返す機能を持つ。
  2. minigoスクリプトは、Goの構文を使ってマップやスライスなどの複合データ構造を構築し、それを返す。
  3. Goホストは、返された内部オブジェクトを、再帰的なヘルパー関数を使って汎用的な map[string]interface{}[]interface{} に変換する。
  4. (オプションだが推奨)変換されたGoのマップを、mapstructureのようなライブラリを使って、Go側で定義済みの構造体にバインドする。

この流れで進めるのが、最もスムーズかと思います。

(返答終了理由: STOP)


ユーザー:
ちなみにこれに少し近しい関係の言語としてterraformの設定ファイル用の言語である github.com/hashicorp/hcl/v2 やその内部で使われてる github.com/zclconf/go-cty があると思います。これらは値のやりとりをどのように行っていて実装はどのようになってますか?


AI:
はい、非常に良い点に気づかれました。hashicorp/hcl/v2 とその心臓部である zclconf/go-cty は、まさにこの分野における最高の先行事例であり、ご自身の minigo を実装する上で非常に参考になります。

これらのライブラリがどのように値のやり取りを行っているかを理解することは、minigoの設計に大きな示唆を与えてくれるはずです。

HCLとgo-ctyの関係性

まず、2つのライブラリの役割分担を明確にすることが重要です。

  • hashicorp/hcl/v2: これは**構文(Syntax)と評価エンジン(Evaluation Engine)**です。

    • HCLのテキストファイル (.tf.hcl) をパースし、AST(抽象構文木)を生成します。
    • 式 ("hello, ${var.name}") やブロック (resource "local_file" "example" { ... }) といったHCLの言語機能を解釈します。
    • 評価のためのコンテキスト(hcl.EvalContext)を受け取り、ASTを評価します。
  • zclconf/go-cty: これは値(Value)と型(Type)のシステムです。

    • HCLが扱うすべてのデータ(文字列、数値、真偽値、リスト、マップなど)を表現するための、動的な型システムを提供します。
    • HCL言語エンジンは、cty.Value という統一された形式で値を操作します。これにより、HCLエンジンは「この値がGoのstringなのかint64なのか」を意識する必要がなくなり、実装がクリーンになります。

この「言語エンジン」と「値システム」の分離が、彼らの設計の最も優れた点です。 minigoで言えば、minigo/parserminigo/evaluatorがHCLに相当し、minigo/objectgo-ctyに相当します。

値のやり取りと実装

では、具体的にGoホスト(Terraformなど)とHCLの間でどうやって値をやり取りしているかを見ていきましょう。

1. Go → HCLへの値の入力

Goホストは、HCLの式を評価するための「環境」を提供します。これが hcl.EvalContext です。このコンテキストに、変数値や関数を詰め込みます。

  • 変数の提供: Go側で cty.Value を生成し、コンテキストの Variables マップに設定します。

    // Goホスト側のコード (概念)
    import "github.com/zclconf/go-cty/cty"
    
    vars := map[string]cty.Value{
        "name": cty.StringVal("World"),
        "port": cty.NumberIntVal(8080),
    }
    
    evalCtx := &hcl.EvalContext{
        Variables: map[string]cty.Value{
            "var": cty.ObjectVal(vars), // HCL側で var.name のようにアクセスできる
        },
    }
    

    HCL側は "${var.name}" という式を評価する際、このコンテキストから値を取得します。

  • 関数の提供: Goで書かれたネイティブ関数を cty.Function としてラップし、コンテキストの Functions マップに設定します。

    // Goホスト側のコード (概念)
    import "github.com/zclconf/go-cty/cty/function"
    
    upperFunc := function.New(&function.Spec{
        Params: []function.Parameter{
            {Name: "str", Type: cty.String},
        },
        Type: function.StaticReturnType(cty.String),
        Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
            input := args[0].AsString()
            return cty.StringVal(strings.ToUpper(input)), nil
        },
    })
    
    evalCtx.Functions["upper"] = upperFunc // HCL側で upper("hello") のように呼び出せる
    

2. HCL → Goへの値の出力(ここが重要です)

HCLの評価結果は、常に cty.Value としてGoホストに返されます。ここからGoのネイティブな構造体に変換するために、hclパッケージ群は gohcl という非常に便利なヘルパーパッケージを提供しています。

gohclは、cty.ValueをGoの構造体に**デコード(バインド)**する機能を持っています。これは reflect を多用して実現されています。

  • HCLファイル (config.hcl)

    server {
      host = "localhost"
      port = 8080
    }
    
    users = ["alice", "bob"]
    
  • Goホスト側のコード

    import (
        "github.com/hashicorp/hcl/v2"
        "github.com/hashicorp/hcl/v2/gohcl"
    )
    
    // 出力先のGoの構造体を定義
    type ServerConfig struct {
        Host string `hcl:"host"`
        Port int    `hcl:"port"`
    }
    
    type Config struct {
        Server ServerConfig `hcl:"server,block"`
        Users  []string     `hcl:"users"`
    }
    
    // ... HCLファイルをパースして body を取得 ...
    var hclFile *hcl.File
    // ...
    
    var cfg Config
    // hcl.Body を &cfg にデコードする
    diags := gohcl.DecodeBody(hclFile.Body, nil, &cfg)
    if diags.HasErrors() {
        // エラー処理
    }
    
    // これで cfg.Server.Host や cfg.Users[0] のようにアクセスできる
    fmt.Println(cfg.Server.Host) // "localhost"
    

minigoへの応用とアドバイス

HCL/ctyの事例から、minigoプロジェクトに以下のベストプラクティスを取り入れることができます。

  1. 値の表現に特化したパッケージを作る: minigo/object パッケージを go-cty のように、値と型の表現に集中させましょう。これはすでにご検討の方向性と一致しており、素晴らしいです。
  2. デコード用のヘルパーを作る: HCLの gohcl に相当する、minigo/object をGoの構造体に変換するためのヘルパーパッケージ(例: gominiminigodecode)を作ることを強くお勧めします。
    • このパッケージが reflect を使ったダーティな処理をすべて引き受けます。
    • インタプリタのコアロジックはクリーンなまま保たれ、Goホスト側の利便性は劇的に向上します。
    • 以前の会話で出た ConvertMinigoObjectToGoValue 関数と mapstructure の役割を、自前で統合したライブラリを作るイメージです。
  3. Go関数を公開する仕組みを参考にする: 将来的にGoの機能をminigoから呼び出せるようにしたくなった場合、cty.Function の実装(Goの関数をラップして引数と戻り値を cty.Value に変換する仕組み)は完璧な設計図になります。

結論として、あなたの「インタプリタは内部オブジェクトを返し、Goホスト側でそれをGoの構造体にバインドする」というアイデアは、Terraformのような大規模で成功しているプロダクトが採用しているアーキテクチャと完全に一致しています。自信を持ってその方針で進めて問題ないでしょう。

(返答終了理由: STOP)


ユーザー:
なるほどあなたと私の対話のログを列挙してまとめてください


AI:
かしこまりました。これまでの対話の要点を、思考の流れに沿って整理し、箇条書きでまとめます。


対話ログのまとめ

  1. 相談の開始と前提確認

    • 相談者: Goと構文的に同一のインタプリタ minigo を開発中。
    • 目的: 既存のGoエコシステム(特にLSP)を流用しつつ、動的な設定ファイルとしてGoアプリケーションに埋め込みたい。
    • Goとの違い: importの評価が遅延的で、main以外の特定関数をエントリーポイントにできる。
    • 相談内容: Goホストと minigo ゲスト間のデータ受け渡し方法についてアドバイスが欲しい。
  2. データ受け渡し方法の候補提示

    • 候補1: 関数公開アプローチ: Goの関数を minigo の組み込み関数として公開。柔軟性が高いが実装コストも高い。
    • 候補2: データシリアライズ・アプローチ: JSON等の文字列を介して疎結合にやり取り。実装は容易だが表現力に制限。
    • 候補3: 直接マッピング・アプローチ: reflectを使いGoのメモリを直接操作。高パフォーマンスだが実装が極めて複雑で危険。
    • 推奨: まずは「候補2」で始め、必要に応じて「候補1」の要素を追加するハイブリッド型が良い。「候補3」はリスクが高すぎる。
  3. 議論の焦点化(minigo → Goへの出力)

    • 相談者: まずは minigo からGoへの一方向のデータ出力が実現できれば良い。
    • 論点: その場合 minigoencoding/json のような機能は必要か? また、minigo に実装すべき最低限の機能は何か?
    • 提案: minigo がJSON文字列を生成するのではなく、minigo内部データ構造(マップ、スライス等)をそのまま返す方が良い。
      • 理由: 実装責務の分離、パフォーマンス向上、Goホスト側での柔軟な処理が可能になる。
    • minigo に必要な機能: ユーザーがロジックを記述できるよう、基本型、複合型(マップ/スライス)、変数、制御構文、関数定義は必要だろうと合意。goroutine/channelの省略は妥当。
  4. 具体的な実装アーキテクチャの確認

    • 相談者: インタプリタはASTを辿る素朴なものを想定。評価器が内部の値を返すように実装し、Goホスト側でその値を解釈(JSON化や構造体へのバインド)するイメージで良いか?
    • 回答と具体化: その理解で正しい。
      • インタプリタ側: 評価器(Eval)が、minigo 内部で定義した値のインターフェース(例: object.Object)を返すように変更する。
      • Goホスト側:
        1. minigo から object.Object を受け取る。
        2. それをGoの汎用的な map[string]interface{} などに変換するヘルパー関数 (ConvertMinigoObjectToGoValue) を用意する。
        3. (推奨)変換後のマップを mapstructure のようなライブラリで、Go側で定義した構造体にデコード(バインド)する。
  5. 先行事例(HCL/go-cty)との比較

    • 相談者: TerraformのHCL/go-ctyも同様の課題を解決しているはず。それらの実装はどうなっているか?
    • 回答: HCL/go-ctyは最高の先行事例。
      • 役割分担: hcl が構文と評価エンジン、go-cty が値と型のシステムを担当。この分離が秀逸。
      • 値のやり取り:
        • Go → HCL: cty.Value を生成し、評価コンテキスト (hcl.EvalContext) 経由で変数や関数として渡す。
        • HCL → Go: 評価結果は常に cty.Value。これを gohcl というヘルパーが reflect を使ってGoの構造体にデコードする。
    • minigoへの教訓:
      1. 値の表現に特化したパッケージ (minigo/object) を作る。
      2. gohcl に相当するデコード用ヘルパーを作ることで、インタプリタのコアをクリーンに保ちつつ、ホスト側の利便性を高める。
      3. 相談者が考案したアーキテクチャは、Terraformのような成功したプロダクトが採用しているものと本質的に同じであり、非常に良い設計方針である。

以上が、これまでの対話の要約です。当初の漠然とした相談から、具体的なアーキテクチャの設計、そしてその妥当性を先行事例で裏付けるところまで、一貫した流れで議論が進みました。

(返答終了理由: STOP)

メタデータ

{
  "runSettings": {
    "temperature": 1,
    "model": "models/gemini-2.5-pro",
    "topP": 0.95,
    "topK": 64,
    "maxOutputTokens": 65536,
    "safetySettings": [
      {
        "category": "HARM_CATEGORY_HARASSMENT",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_HATE_SPEECH",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
        "threshold": "OFF"
      }
    ],
    "responseMimeType": "text/plain",
    "enableCodeExecution": false,
    "enableSearchAsATool": false,
    "enableBrowseAsATool": false,
    "enableAutoFunctionResponse": false,
    "thinkingBudget": -1
  },
  "systemInstruction": {}
}
podhmopodhmo

さらにそれを使った例

対応関係を集めてきてその後何かの処理をしたい。例えばある型からある型への変換。

podhmopodhmo

これまでに記録した情報は以下の通りです。

  • メインの目的: go-scan ライブラリを利用して、特定の型から別の型への変換コードを生成したい。
  • 変換コードの具体的な要件:
    • src (変換元) と dst (変換先) の構造体が存在する。
    • embedded (埋め込み) フィールドや自由なフィールド名を扱える必要がある。
    • 変換関数は context を引数として受け取れるようにしたい。
    • 変換処理の途中で内部的な処理(例: 英語から日本語への翻訳)を組み込めるようにしたい。
    • 変換関数の公開範囲:
      • 指定された「トップレベルの型」に対する変換関数は exported (公開) にする。
      • それ以外の内部的な型への変換関数は unexported (非公開) にする。
      • 「トップレベルの型」の具体的な基準は未定。
  • 現在の課題(問題点):
    • 1. 変換関数の定義場所:
      • 選択肢: 別のパッケージとして定義するか、または変換元の型か変換先の型のメソッドとして定義するか。
      • 主な用途: DTO (Data Transfer Object) への変換など。
      • 変換が必要な理由: 微妙な形状の違い(例: ポインタで定義されたフィールドが値になるケース。nil の場合の挙動は後日詳細化予定)。
    • 2. srcdst の関係記述方法: (今回の本題)
      • go-scan を使って、この関係を手軽に記述できる記法を実現したい。
      • 検討中の代替案:
        • Go言語のサブセットである minigo のような言語で記述する。
        • コメントにマーカー (例: 特定のキーワード) を記述する。
      • 検討結果: コメントマーカーはIDEの補完が効かず、タイポによるエラーが実行時まで検出されないため、minigo のような記述方法の方が優れていると考えている。
podhmopodhmo

fullnameとかどうやって記述するんだろ?

package converter

import (
	"context"
	"reflect"
	"testing"
	"time"

	"example.com/convert/models"
)

func TestConvertUser(t *testing.T) {
	ctx := context.Background()
	now := time.Now()
	phone := "123-456-7890"
	updatedAt := now.Add(time.Hour)

	srcUser := models.SrcUser{
		ID:        101,
		FirstName: "John",
		LastName:  "Doe",
		SrcAddress: models.SrcAddress{
			Street: "123 Main St",
			City:   "Anytown",
		},
		ContactInfo: models.SrcContact{
			Email: "john.doe@example.com",
			Phone: &phone,
		},
		Details: []models.SrcInternalDetail{
			{Code: 1, Description: "Needs setup"},
			{Code: 2, Description: "Pending review"},
		},
		CreatedAt: now,
		UpdatedAt: &updatedAt,
	}

	expectedDstUser := models.DstUser{
		UserID:   "user-101",
		FullName: "John Doe",
		Address: models.DstAddress{
			FullStreet: "123 Main St",
			CityName:   "Anytown",
		},
		Contact: models.DstContact{
			EmailAddress: "john.doe@example.com",
			PhoneNumber:  "123-456-7890",
		},
		Details: []models.DstInternalDetail{
			{ItemCode: 1, LocalizedDesc: "翻訳済み (JP): Needs setup"},
			{ItemCode: 2, LocalizedDesc: "翻訳済み (JP): Pending review"},
		},
		CreatedAt: now.Format(time.RFC3339),
		UpdatedAt: updatedAt.Format(time.RFC3339),
	}

	dstUser := ConvertUser(ctx, srcUser)

	if !reflect.DeepEqual(dstUser, expectedDstUser) {
		t.Errorf("ConvertUser() got = %v, want %v", dstUser, expectedDstUser)
	}
}

func TestConvertUser_NilFields(t *testing.T) {
	ctx := context.Background()
	now := time.Now()

	srcUser := models.SrcUser{
		ID:        102,
		FirstName: "Jane",
		LastName:  "Doe",
		SrcAddress: models.SrcAddress{
			Street: "456 Oak St",
			City:   "Otherville",
		},
		ContactInfo: models.SrcContact{
			Email: "jane.doe@example.com",
			Phone: nil, // Nil phone
		},
		Details: []models.SrcInternalDetail{
			{Code: 3, Description: "Urgent"},
		},
		CreatedAt: now,
		UpdatedAt: nil, // Nil UpdatedAt
	}

	expectedDstUser := models.DstUser{
		UserID:   "user-102",
		FullName: "Jane Doe",
		Address: models.DstAddress{
			FullStreet: "456 Oak St",
			CityName:   "Otherville",
		},
		Contact: models.DstContact{
			EmailAddress: "jane.doe@example.com",
			PhoneNumber:  "N/A", // Default for nil phone
		},
		Details: []models.DstInternalDetail{
			{ItemCode: 3, LocalizedDesc: "翻訳済み (JP): Urgent"},
		},
		CreatedAt: now.Format(time.RFC3339),
		UpdatedAt: "", // Empty string for nil UpdatedAt
	}

	dstUser := ConvertUser(ctx, srcUser)

	if !reflect.DeepEqual(dstUser, expectedDstUser) {
		t.Errorf("ConvertUser() with nil fields got = %v, want %v", dstUser, expectedDstUser)
	}
}

func TestConvertOrder(t *testing.T) {
	ctx := context.Background()
	srcOrder := models.SrcOrder{
		OrderID: "ORD-001",
		Amount:  199.99,
		Items: []models.SrcItem{
			{SKU: "ITEM001", Quantity: 2},
			{SKU: "ITEM002", Quantity: 1},
		},
	}

	expectedDstOrder := models.DstOrder{
		ID:          "ORD-001",
		TotalAmount: 199.99,
		LineItems: []models.DstItem{
			{ProductCode: "ITEM001", Count: 2},
			{ProductCode: "ITEM002", Count: 1},
		},
	}

	dstOrder := ConvertOrder(ctx, srcOrder)

	if !reflect.DeepEqual(dstOrder, expectedDstOrder) {
		t.Errorf("ConvertOrder() got = %v, want %v", dstOrder, expectedDstOrder)
	}
}

func TestSrcAddressToDstAddress(t *testing.T) {
	ctx := context.Background()
	src := models.SrcAddress{Street: "123 Main St", City: "Anytown"}
	expected := models.DstAddress{FullStreet: "123 Main St", CityName: "Anytown"}
	got := srcAddressToDstAddress(ctx, src)
	if !reflect.DeepEqual(got, expected) {
		t.Errorf("srcAddressToDstAddress() = %v, want %v", got, expected)
	}
}

func TestSrcContactToDstContact(t *testing.T) {
	ctx := context.Background()
	phone := "555-0100"
	tests := []struct {
		name     string
		src      models.SrcContact
		expected models.DstContact
	}{
		{
			name: "with phone",
			src:      models.SrcContact{Email: "test@example.com", Phone: &phone},
			expected: models.DstContact{EmailAddress: "test@example.com", PhoneNumber: "555-0100"},
		},
		{
			name: "nil phone",
			src:      models.SrcContact{Email: "test2@example.com", Phone: nil},
			expected: models.DstContact{EmailAddress: "test2@example.com", PhoneNumber: "N/A"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := srcContactToDstContact(ctx, tt.src); !reflect.DeepEqual(got, tt.expected) {
				t.Errorf("srcContactToDstContact() = %v, want %v", got, tt.expected)
			}
		})
	}
}

func TestSrcInternalDetailToDstInternalDetail(t *testing.T) {
	ctx := context.Background()
	src := models.SrcInternalDetail{Code: 10, Description: "Test Desc"}
	expected := models.DstInternalDetail{ItemCode: 10, LocalizedDesc: "翻訳済み (JP): Test Desc"}
	got := srcInternalDetailToDstInternalDetail(ctx, src)
	if !reflect.DeepEqual(got, expected) {
		t.Errorf("srcInternalDetailToDstInternalDetail() = %v, want %v", got, expected)
	}
}

func TestSrcItemToDstItem(t *testing.T) {
	ctx := context.Background()
	src := models.SrcItem{SKU: "SKU007", Quantity: 3}
	expected := models.DstItem{ProductCode: "SKU007", Count: 3}
	got := srcItemToDstItem(ctx, src)
	if !reflect.DeepEqual(got, expected) {
		t.Errorf("srcItemToDstItem() = %v, want %v", got, expected)
	}
}
podhmopodhmo

goからgoのメタプログラミング的なものを行いたい

可能な範囲で実行時にやるというよりは静的解析のときのインターフェイスを整えるという話かも?

podhmopodhmo

minigoにおけるインポートされたGo関数の情報取得

1. 目的

minigoスクリプト内で、直接インポートした外部Goパッケージおよびその要素(関数、型、変数、定数など)の詳細情報を取得可能にすることを目的とします。これにより、minigoプログラムがより動的にGoのコードを理解し、利用するための基盤を提供します。

2. 背景

minigoはGoで実装されたインタプリタであり、Goのパッケージをインポートしてその中の定数や関数を利用する機能を持っています。現状では、インポートされた関数を呼び出すことはできますが、その関数がどのような引数を期待し、どのような値を返すのか、あるいはどのようなドキュメントコメントが付与されているのかをminigoスクリプト側から知る標準的な方法がありません。また、パッケージ自体がどのような要素(関数リスト、型リストなど)を公開しているかを網羅的に知ることもできません。

このような情報にアクセスできることで、以下のようなユースケースが考えられます。

  • 動的な関数呼び出しの際の事前チェックやシグネチャ検証。
  • パッケージ内の利用可能なシンボルの一覧表示や探索。
  • 関数情報や型情報を利用したコード生成やアダプタの自動生成。
  • 開発者ツール、ドキュメントジェネレータ、デバッガなどでのリッチな情報表示。

3. 提案アプローチ: minigo/inspect パッケージによる情報取得

Go言語の構文との整合性、LSP等外部ツールとの親和性、そしてminigoの言語仕様への影響を考慮し、専用パッケージ minigo/inspect 内のエクスポートされた関数によって、インポートされたGoの関数、型、そしてパッケージ全体の情報を取得するアプローチを提案します。

このパッケージは主に以下の3つの関数を提供します。

  1. inspect.GetFunctionInfo(fnSymbol interface{}) (FunctionInfo, error): 特定の関数の詳細情報を取得します。
  2. inspect.GetTypeInfo(typeName string) (TypeInfo, error): 特定の型名の詳細情報を取得します。
  3. inspect.GetPackageInfo(pkgPathOrSymbol interface{}) (PackageInfo, error): 特定のパッケージの詳細情報を取得します。

3.1. minigo/inspect.GetFunctionInfo 関数のシグネチャ(案)

// minigo/inspectパッケージの関数としてのイメージ (Go言語側)
package inspect

// FunctionInfo は関数の詳細情報を保持します。
type FunctionInfo struct {
	Name       string      // 関数名
	PkgPath    string      // パッケージのフルパス
	PkgName    string      // パッケージ名
	Doc        string      // godocコメント
	Params     []ParamInfo // 引数情報
	Returns    []ReturnInfo// 戻り値情報
	IsVariadic bool        // 可変長引数か
}

// ParamInfo は引数の情報を保持します。
type ParamInfo struct {
	Name string // 引数名
	Type string // 型名 (例: "string", "mypkg.MyStruct")
}

// ReturnInfo は戻り値の情報を保持します。
type ReturnInfo struct {
	Name string // 戻り値名
	Type string // 型名
}

// GetFunctionInfo は指定された関数の詳細情報を返します。
// fn_symbol はminigoの ImportedFunction オブジェクトから変換されたものを想定。
func GetFunctionInfo(fn_symbol interface{}) (FunctionInfo, error) {
	// ... 実装 ...
}
// minigoスクリプトからの呼び出しイメージ:
import "minigo/inspect"

info, err = inspect.GetFunctionInfo(mypkg.MyFunction)
if err == nil {
    fmt.Println(info.Name)
    // ...
}

3.2. minigo/inspect.GetTypeInfo 関数のシグネチャ(案)

// minigo/inspectパッケージの追加関数としてのイメージ (Go言語側)
package inspect

// TypeKind は型の種類を示します (例: Struct, Interface, Slice, Map, Basic)。
type TypeKind string

// TypeInfo は型の詳細情報を保持します。
type TypeInfo struct {
	Kind     TypeKind     // 型の種類
	Name     string       // 型名 (完全修飾名, 例: "mypkg.MyStruct", "string")
	PkgPath  string       // 型が定義されているパッケージパス (基本型の場合は空など特別な値)
	Doc      string       // 型定義のgodocコメント
	Fields   []FieldInfo  // KindがStructの場合のフィールド情報
	Methods  []MethodInfo // KindがInterfaceやStructの場合のメソッド情報 (公開メソッド)
	// ElemType *TypeInfo  // KindがSlice, Ptr, Array, Mapの場合の要素の型情報 (遅延評価または型名文字列)
	// ... その他、型に応じた情報 (例: underlying type for defined types)
}

// FieldInfo はstructのフィールド情報を保持します。
type FieldInfo struct {
	Name string // フィールド名
	Type string // 型名
	Doc  string // フィールドのgodocコメント
	Tag  string // structタグ
}

// MethodInfo はメソッドの情報を保持します (FunctionInfoと類似の構造)。
type MethodInfo FunctionInfo // FunctionInfoを再利用

// GetTypeInfo は指定された型名の詳細情報を返します。
func GetTypeInfo(typeName string) (TypeInfo, error) {
	// ... 実装 ...
}

3.3. minigo/inspect.GetPackageInfo 関数のシグネチャ(案)

// minigo/inspectパッケージの追加関数としてのイメージ (Go言語側)
package inspect

// PackageInfo はパッケージの詳細情報を保持します。
type PackageInfo struct {
	Name        string         // パッケージ名 (e.g., "os")
	Path        string         // パッケージのフルパス (e.g., "os", "github.com/user/mypkg")
	Doc         string         // パッケージのgodocコメント
	Imports     []string       // このパッケージがインポートしているパッケージパスのリスト
	Constants   []ValueInfo    // パッケージレベルの定数情報
	Variables   []ValueInfo    // パッケージレベルの変数情報
	Functions   []string       // パッケージレベルの関数名のリスト
	Types       []string       // パッケージで定義されている型名 (struct, interface, etc.)のリスト
}

// ValueInfo は定数や変数の情報を保持します。
type ValueInfo struct {
	Name string // 定数名/変数名
	Type string // 型名
	Doc  string // godocコメント
	// Value string // (可能であれば) 定数の文字列表現
}

// GetPackageInfo は指定されたパッケージの情報を返します。
// pkgPathOrSymbol はパッケージパス文字列またはそのパッケージに属するシンボル。
func GetPackageInfo(pkgPathOrSymbol interface{}) (PackageInfo, error) {
	// ... 実装 ...
}

3.4. なぜパッケージ関数か

  • LSP・外部ツールとの親和性: minigo/inspect パッケージとその関数は、GoのLSP等から認識されやすくなります。
  • 名前空間の明確化: 機能が minigo/inspect という明確な名前空間に属します。
  • Goの慣習との一致: Goの標準ライブラリの多くがこの形式で機能を提供しています。
  • モジュールとしての管理: 関連機能が増えた場合でも minigo/inspect パッケージ内で管理できます。

4. 代替アプローチとその検討

本提案である minigo/inspect パッケージによる情報取得アプローチに至るまでに、いくつかの代替アプローチを検討しました。以下にその概要と、今回は採用を見送った理由を記します。

4.1. グローバルな組み込み関数 (不採用)

  • 検討内容: minigoのグローバルスコープに get_function_info() のような組み込み関数を直接定義する案です。
  • 不採用理由: LSP(Language Server Protocol)などの外部ツールとの親和性が低く、エディタでの補完や型チェックの恩恵を受けにくい可能性があります。また、グローバル名前空間を汚染する可能性があり、Goの標準的なライブラリ提供方法であるパッケージ経由のアクセスとも異なります。最終的に、よりGoらしいアプローチとしてパッケージ関数を選択しました。

4.2. 特殊な構文の導入 (不採用)

  • 検討内容: info(pkg.Function) のような、情報取得専用の新しい構文キーワードをminigoに導入する案です。
  • 不採用理由: Goの標準的な構文から逸脱するため、minigoパーサーの複雑化を招き、ユーザーの学習コストも増加させます。「Goらしさ」を損なう可能性も考慮し、採用を見送りました。

4.3. オブジェクトのプロパティ/メソッドアクセス (現時点では不採用)

  • 検討内容: インポートされた関数オブジェクト自体が情報取得のためのプロパティやメソッド(例: pkg.Function.infopkg.Function.getInfo())を持つ形を検討しました。
  • 検討結果: Goの構造体フィールドアクセス (foo.Bar) に似せることは可能ですが、minigoの現在のオブジェクトシステムでは、これを汎用的に実現するには evalSelectorExpr の大幅な拡張が必要です。特に、FunctionInfo のような動的に取得・生成される情報を「フィールドのように」見せることは、Goの静的なフィールドアクセスとは意味合いが異なります。minigoに実質的なメソッド呼び出しやプロパティアクセスの概念を本格導入することになり、現時点では過剰な複雑化を招く可能性があるため、採用を見送りました。

4.4. インターフェースと型アサーションの導入 (不採用)

  • 検討内容: minigoにインターフェースと型アサーションの仕組みを導入し、それらを使って関数情報オブジェクトから詳細を引き出す方法を検討しました。
  • 不採用理由: Goにはこれらの強力な概念がありますが、現在のminigoにこれらを本格的に導入するのは非常に大規模な変更となり、minigoの設計のシンプルさとはかけ離れてしまいます。実装コストと複雑性が非常に高いため、採用を見送りました。

5. 実装が必要となる主な要素 (minigo/inspect アプローチの場合)

このアプローチを採用する場合、minigoのコア機能および新規パッケージに以下の追加・修正が必要となります。

5.1. ImportedFunction オブジェクト型

  • 場所: examples/minigo/object.go
  • 役割: インポートされたGo関数の情報を保持するための専用オブジェクト型。minigoインタープリタが inspect.GetFunctionInfo に渡す内部的な表現となります。
  • 内部データ: go-scan/scanner.FunctionInfo から得られる情報、またはそれを参照するためのキーを格納します。
  • インターフェース: Object インターフェースを実装 (Type(), Inspect())。
    • Type(): IMPORTED_FUNCTION_OBJ のような新しいオブジェクトタイプを返します。
    • Inspect(): <imported function mypkg.MyFunc> のような文字列を返します。
  • 特性: このオブジェクトはminigoスクリプト内で直接呼び出すことはできません。呼び出そうとした場合はエラーとなります。

5.2. evalSelectorExpr 関数の修正

  • 場所: examples/minigo/interpreter.go
  • 修正内容: go-scan を用いて外部パッケージの関数シンボルを解決する際、UserDefinedFunction の代わりに上記の ImportedFunction オブジェクトを生成し、minigoの実行環境に登録するように変更します。これにより、inspect.GetFunctionInfo にそのシンボルを渡せるようになります。

5.3. evalCallExpr 関数の修正

  • 場所: examples/minigo/interpreter.go
  • 修正内容: 呼び出そうとしている関数オブジェクトが ImportedFunction 型であった場合、呼び出しはエラーとして処理します(例: 「imported function mypkg.MyFunc cannot be called directly」)。

5.4. minigo/inspect パッケージ及び関数の実装

  • 新規パッケージ: minigo/inspect をGoで実装します。
    • 利用可能にする方法の検討:
      • 案1: 他のGoパッケージと同様に、minigoの import 文で解決できるようにする(GOPATHやモジュール依存関係で解決)。この場合、minigo/inspect は独立したGoモジュールとして提供されるか、minigo本体と同じモジュール内に配置される。
      • 案2: インタプリタに「組み込みパッケージ」として特別に登録する。この場合、import "minigo/inspect" はインタープリタによって内部的に処理される。LSP等との連携を考えると、案1の方が望ましい可能性があります。
  • GetFunctionInfo, GetTypeInfo, GetPackageInfo 関数:
    • Go実装: minigo/inspect/inspect.go (仮) にGoの関数として実装します。これらの関数は、minigoから渡される引数(ImportedFunctionオブジェクトや型名文字列、パッケージパス文字列など)を解釈し、go-scan を利用して情報を収集し、定義された FunctionInfo, TypeInfo, PackageInfo 構造体に詰めて返します。
    • minigoへの公開: minigoインタープリタがこれらのGo関数を呼び出し、結果をminigoのオブジェクト(専用のstruct様オブジェクトまたはマップ)に変換してスクリプトに返す仕組みが必要です。
  • 各種 xxxInfo struct (Go側):
    • FunctionInfo, ParamInfo, ReturnInfo, TypeInfo, FieldInfo, MethodInfo, PackageInfo, ValueInfo などの構造体をGoで定義します。これらがminigoに公開される情報のスキーマとなります。

6. minigo上で取得可能にすべき情報とその表現

minigo/inspect パッケージの各関数が返す情報構造について説明します。

6.1. inspect.FunctionInfo 構造体

inspect.GetFunctionInfo によって返される、個々の関数の詳細情報です。

  • Name string: 関数名。
  • PkgPath string: 関数が属するGoパッケージのフルパス。
  • PkgName string: 関数が属するGoパッケージ名。
  • Doc string: 関数のgodocコメント。
  • Params []ParamInfo: 関数の引数情報のスライス。
    • ParamInfo struct:
      • Name string: 引数名。
      • Type string: 引数の型名。この型名がユーザー定義型(例: mypkg.MyStruct)の場合、後述の inspect.GetTypeInfo を用いてさらに詳細な型情報を取得できる可能性があります。
  • Returns []ReturnInfo: 関数の戻り値情報のスライス。
    • ReturnInfo struct:
      • Name string: 戻り値名。
      • Type string: 戻り値の型名。同様に inspect.GetTypeInfo で詳細を取得できる可能性があります。
  • IsVariadic bool: 関数が可変長引数を取るかどうか。

6.2. inspect.TypeInfo 構造体

inspect.GetTypeInfo によって返される、特定の型の詳細情報です。

  • Kind TypeKind: 型の種類を示します (例: Struct, Interface, Slice, Map, Basic)。
  • Name string: 型名 (完全修飾名, 例: "mypkg.MyStruct", "string")。
  • PkgPath string: 型が定義されているパッケージパス (基本型の場合は空など特別な値)。
  • Doc string: 型定義のgodocコメント。
  • Fields []FieldInfo: KindStructの場合のフィールド情報。
    • FieldInfo struct: Name string, Type string, Doc string, Tag string
  • Methods []MethodInfo: KindInterfaceStructの場合のメソッド情報 (公開メソッド)。
    • MethodInfo struct: FunctionInfo と同様の構造を想定。
  • ElemType *TypeInfo: KindSlice, Ptr, Array, Mapの場合の要素の型情報。遅延評価されるか、型名文字列を保持し、必要に応じて再度 GetTypeInfo で解決します。
  • その他、型の種類に応じた情報(例: defined typeの underlying typeなど)。

6.3. inspect.PackageInfo 構造体

inspect.GetPackageInfo関数で返される、パッケージ全体の情報です。

  • Name string: パッケージ名。
  • Path string: パッケージのフルインポートパス。
  • Doc string: パッケージのgodocコメント。
  • Imports []string: このパッケージが直接インポートしているパッケージのパスのリスト。
  • Constants []ValueInfo: パッケージレベルでエクスポートされている定数の情報リスト。
    • ValueInfo struct: Name string, Type string, Doc string
  • Variables []ValueInfo: パッケージレベルでエクスポートされている変数の情報リスト。
  • Functions []string: パッケージレベルでエクスポートされている関数名のリスト。詳細情報は別途 inspect.GetFunctionInfo(packagePath + "." + functionName) で取得することを推奨します。
  • Types []string: パッケージレベルで定義・エクスポートされている型名のリスト。詳細情報は別途 inspect.GetTypeInfo(packagePath + "." + typeName) で取得することを推奨します。

Lazy Loadingのコンセプト: GetTypeInfoGetPackageInfo (特にその内部のシンボル詳細) は、呼び出された時点で初めて go-scan を利用して情報をスキャン・解析します。これにより、不要な情報まで先んじて大量にロードすることを防ぎます。一度取得した情報はキャッシュすることも考えられます。

7. 考慮事項・懸念事項

  • go-scan の機能への依存: 本機能の実現は、go-scanscanner.FunctionInfo や型情報、パッケージ情報をどれだけ詳細かつ正確に提供できるかに強く依存します。特に、型名、引数名、ドキュメントコメント、可変長引数フラグ、structのフィールド、メソッド、パッケージ内のシンボルリストなどの情報が正確に取得できることが前提となります。
  • 型情報の詳細度とパース: go-scan が提供する型名を基本的にそのままminigo文字列として提供することを想定します。minigo側でこれらの型文字列をさらにパースして構造的な型オブジェクトにするのは、現時点ではスコープ外とします(将来的な拡張可能性はあり)。mypkg.MyStruct のようにパッケージプレフィックスが付く型名の場合、そのプレフィックスの扱いも go-scan の出力に準じます。
  • 再帰的情報取得と循環参照: GetTypeInfo で型情報を再帰的に辿る際、型定義が互いに参照し合っている場合(例: type A struct { B *B }; type B struct { A *A })に無限ループに陥らないよう、go-scan および minigo/inspect の実装で検出・対処が必要です(例: 既に処理中の型であればプレースホルダを返す、深さ制限を設けるなど)。
  • Lazy Loadingの実装: TypeInfo 内の ElemType のような再帰的になる可能性のあるフィールドをどのように遅延評価させるか。関数型フィールドとして持つ、あるいは型名文字列だけを保持し都度 GetTypeInfo を呼び出すなどの方法が考えられます。キャッシュ戦略(一度取得した型情報をどの程度の期間・範囲でキャッシュするか)も重要です。
  • エラーハンドリング: シンボルが見つからない場合、シンボルが期待する型(例: ImportedFunction)でない場合、go-scan から期待した情報が得られなかった場合など、様々なエラーケースに対応し、inspect.GetFunctionInfo, inspect.GetTypeInfo, inspect.GetPackageInfo は適切なエラーオブジェクトを返す必要があります。
  • ドキュメントコメントの取得: go-scan が関数宣言、型定義、パッケージ宣言に直接関連付けられたgodocコメントを正確に抽出できることが前提です。
  • minigo/inspect パッケージの提供方法: minigoユーザーが特別な設定なしに import "minigo/inspect" を利用できるように、パッケージの配置場所やビルド方法を考慮する必要があります。minigo本体に同梱する形か、別途 go get 可能にするかなどが考えられます。
  • minigoオブジェクトへの変換: Goの FunctionInfo, TypeInfo, PackageInfo structを、minigoスクリプト側で扱いやすいオブジェクト(専用のstruct様オブジェクトまたはマップ)にどのように変換するか。特に TypeInfo のようにフィールドが可変になる構造(例: Fields, Methods スライス)や、ElemType のような再帰的構造を持つ場合、minigo側での表現方法が課題となります。
  • パッケージ全体の情報スキャンのパフォーマンス: GetPackageInfo はパッケージ内の多数の要素をスキャンする可能性があるため、特に大規模なパッケージに対して呼び出された場合のパフォーマンス影響を考慮する必要があります。go-scan の効率に大きく依存します。
  • シンボルからのパッケージ特定: GetPackageInfo にパッケージ内のシンボル (例: os.Getenv) を渡した場合、そのシンボルから所属パッケージ (例: "os") を特定する内部メカニズムが必要です。

8. 利用例 (minigoコード)

import "os"
import "strings"
import "minigo/inspect"

// GetFunctionInfo の利用例
funcInfo, err = inspect.GetFunctionInfo(os.Getenv)
if err == nil {
    fmt.Println("Function:", funcInfo.Name, ", Package:", funcInfo.PkgName)
    fmt.Println("  Doc:", funcInfo.Doc)
    fmt.Println("  Params:")
    for _, p = range funcInfo.Params {
        fmt.Println("    -", p.Name, p.Type)
    }
    // ... (Returnsなども同様にアクセス)
}

// GetTypeInfo の利用例
fileInfoType, err = inspect.GetTypeInfo("os.FileInfo") // os.FileInfoはインターフェース
if err == nil {
    fmt.Println("\nType:", fileInfoType.Name, ", Kind:", fileInfoType.Kind)
    fmt.Println("  Doc:", fileInfoType.Doc)
    if fileInfoType.Kind == "Interface" {
        fmt.Println("  Methods:")
        for _, m = range fileInfoType.Methods {
            fmt.Println("    -", m.Name) // MethodInfoはFunctionInfoと同じ構造
        }
    }
}

// GetPackageInfo の利用例
pkgInfo, err = inspect.GetPackageInfo("strings") // パッケージパスで指定
// または pkgInfo, err = inspect.GetPackageInfo(strings.Join) // パッケージ内のシンボルで指定
if err == nil {
    fmt.Println("\nPackage:", pkgInfo.Name, "(Path:", pkgInfo.Path, ")")
    fmt.Println("  Doc:", pkgInfo.Doc)
    fmt.Println("  Imports:", pkgInfo.Imports)

    fmt.Println("  Constants:")
    for _, c = range pkgInfo.Constants {
        fmt.Println("    -", c.Name, ":", c.Type)
    }
    fmt.Println("  Variables:")
    for _, v = range pkgInfo.Variables {
        fmt.Println("    -", v.Name, ":", v.Type)
    }
    fmt.Println("  Functions:")
    for _, fName = range pkgInfo.Functions {
        fmt.Println("    -", fName)
        // fInfo, _ := inspect.GetFunctionInfo(pkgInfo.Path + "." + fName) // 詳細取得
    }
    fmt.Println("  Types:")
    for _, tName = range pkgInfo.Types {
        fmt.Println("    -", tName)
        // tInfo, _ := inspect.GetTypeInfo(pkgInfo.Path + "." + tName) // 詳細取得
    }
}

9. 将来的な拡張可能性

この minigo/inspect パッケージは、将来的に以下のような方向へ拡張できる可能性があります。

  • minigoユーザー定義関数のイントロスペクション: 現在はインポートされたGo関数が主な対象ですが、minigoスクリプト内で定義された関数 (UserDefinedFunction) の情報も同様の仕組みで取得できるように拡張できます。
  • より詳細な型情報: 型名だけでなく、型の詳細情報(structのフィールドのタグ、埋め込みフィールド、インターフェースが埋め込んでいる他のインターフェースなど)をより深く、かつminigoの型システムと連携した形で取得・操作するための機能。これにはminigoの型システム自体の大きな拡張が必要になる可能性があります。
  • ジェネリクス対応: Go本体のジェネリクスサポートが安定し、go-scanがジェネリクスで定義された関数や型の情報を適切に提供できるようになった場合、minigo/inspect もこれに対応することが期待されます。
  • コード位置情報: 各情報(関数定義、型定義、フィールド定義など)がソースコード上のどこで定義されているか(ファイルパス、行番号)といった情報を追加することも有用です。
  • ASTノードへのアクセス: (上級者向け機能として)go-scan が提供するASTノードへの限定的なアクセスを提供し、より低レベルなコード解析をminigoから行えるようにする可能性も考えられます。

以上が、minigoでインポートされたGo関数の情報を取得するための機能提案です。
ご意見や懸念事項があれば、ぜひお寄せください。

podhmopodhmo

これもまぁパッケージをオブジェクトとして返すような感じにしてあげてあれこれやれば良いのか?