ADR-0037: Go のコードを、Yaegi のインタプリタの WASM で動かす¶
Status¶
承認
Context¶
訪問者が書き換えた Go のコードを動かし、標準出力と go test 相当の結果を見せる方式を決める必要がある。
Go の HTTP サーバーを立てることは、対象にしない。
- 訪問者のコードは訪問者のブラウザの中だけで動かし、サイトのサーバーに送らない。
- ADR-0008: 読者の identity を前提とする機能を作らず、バックエンドを持たない
- 実行環境の選び方は、有効な決定が無い。
- ADR-0030: 種別を記事の 1 つにし、構成を記事の形式で、シリーズをディレクトリで表す
- 訪問者が書いたコードを動かすツールは、プレイグラウンドのサブドメインに置く。cross-origin isolation のヘッダは、Node の実行環境のパスにだけ返す。
- AI のエージェントが書いたコードは、利用者が自分の操作で動かしたコードとして扱う。
- プレイグラウンドでは、訪問者が動かしたコードが、同じ origin に保存した図とスライドにアクセスできることを受け入れる。条件は、他人が用意したコードを、訪問者の操作なしに動かさないことである。
- Go のコードは、WebContainers の中で動かさず、
play.fukuemon.devのページで動かす。ページの Web Worker の中で動くコードは、localStorageを読めず、IndexedDB を読める。 - TinyGo の公式の playground は、サーバーでコンパイルする。TinyGo のコンパイラは、LLVM に依存する。
- Yaegi の README は、Go 1.21 と 1.22 に対応すると書いている。
- 出典: traefik/yaegi
Decision¶
Go のコードは、Yaegi のインタプリタを WASM にして、ブラウザの中で動かす。
- ダウンロードのサイズが、Go のコンパイラを WASM にする形の約 4 分の 1 で済むためである。
- Yaegi の WASM は、cross-origin isolation を求めない。Go だけを動かす記事には、isolation のヘッダが掛からない。
go test相当の結果は、テストの関数をtesting.Mainに渡して出す。testing.Mainはos.Exitで終わるので、実行のたびに WASM のインスタンスを作り直す。- Yaegi の WASM は、利用者が実行のボタンを押したときのほかに、AI のエージェントが Go を実行する操作かテストを実行する操作を呼んだときにも読み込む。読み込んでから、操作を実行する。
- 操作が呼ばれるのは、利用者がスイッチをオンにし、自分のエージェントをつないだ後である。
- ADR-0045 の Decision 6 と同じく、エージェントの操作で読み込むことも、利用者の操作として扱う。
Yaegi で動かすコードが、ページの localStorage と IndexedDB を読み書きできないようにする。
AI のエージェントが呼ぶ、Go を実行する操作とテストを実行する操作に、ページが確認を出さないためである。
GoRunnerは、Yaegi を Web Worker の中で動かす。止めるときは、Web Worker を終了させる。GoRunnerが Yaegi に渡す symbol を、stdlib.Symbolsだけにする。syscall、unsafe、unrestrictedの symbol を渡さない。- Yaegi で動かすコードが
syscall/jsを呼べないことを、テストで確認する。
記事の Go のコードは、Yaegi が動かせる範囲で書く。
iter パッケージと、型の推論に失敗するジェネリックな関数は使わない。
Verification¶
Yaegi 0.16.3 と Go 1.26.4 を、Node の上で動かして比べた(Apple M2)。
ブラウザの中での起動の時間は、どちらの方式も確認していない。
| 項目 | Yaegi の WASM | Go のコンパイラとリンカの WASM |
|---|---|---|
| ダウンロードのサイズ | 40.0 MB。gzip で 8.41 MB、brotli で 5.78 MB | コンパイラ、リンカ、標準ライブラリの 187 個のパッケージの合計が、brotli で 20.99 MB |
| 起動 | 読み込みと初期化に 0.14 秒 | hello world のコンパイルに 0.51 秒、リンクに 0.31 秒 |
| 言語 | iter を import できない。関数を引数に取るジェネリックな関数の一部で、型の推論に失敗する |
Go 1.26.4 のコンパイラそのもの |
| 実行速度 | fib(27) に 1.1 秒 |
未確認 |
go test 相当 |
=== RUN、--- PASS、--- FAIL、サブテストの出力が出た |
テストの main を生成する処理が要る。未確認 |
| ブラウザでの実行 | 未確認 | 仮想のファイルシステムが要る。未確認 |
- Yaegi で、
fmt、encoding/json、sync、context、crypto/sha256、net/http/httptestは動いた。 - Yaegi のコンパイルのエラーは、行と列つきで返る。テストの失敗の行の場所は、テストのファイルの行にならない。
- WASM のインスタンスを作り直す時間は、0.06 秒だった。
34 の試作の Yaegi 0.16.3 の WASM(stdlib.Symbols だけを渡したもの)を、Node の上で動かした(Apple M2)。¶
| Yaegi で動かすコードが import したもの | 結果 |
|---|---|
syscall/js |
import がエラーになった |
syscall |
import がエラーになった |
unsafe |
import がエラーになった |
net/http |
import できた。呼び出しは、Node の上でエラーを返した |
Go を動かすツールの実装のタスクで、次の 2 つをブラウザの中で確認した。
1 つは、Yaegi に渡す symbol を stdlib.Symbols だけにした WASM の動きで、もう 1 つは、ディレクトリで複数の package に分けたファイルの import である。
確認のために、stdlib.Symbols だけを渡した Yaegi 0.16.3 と Go 1.26.4 の WASM を作った。
その WASM を、Playwright 1.64.1 の Chromium 156 と WebKit 27.2 の Web Worker の中で動かした。
ファイルは src/<モジュールの名前>/ に置き、GoPath を . にした。
| Yaegi で動かすコード | Chromium と WebKit の結果 |
|---|---|
syscall/js、syscall、unsafe、os/exec を import する |
どれも import "…" error: unable to find source related to: "…" で失敗した |
main.go が、別のディレクトリの play/greet を import する |
標準出力が出た |
main の package を 2 つのファイルに分け、play/internal/a が play/internal/b を import する |
標準出力が出た |
net/http で、同じ origin のパスを取得する |
応答を読めた |
net/http で、別の origin のパスを取得する |
応答は読めなかった。別の origin のサーバーにリクエストが届いた |
- import が失敗するのは、
stdlib.Symbolsに symbol が無く、Yaegi がソースを読むファイルシステムに、訪問者のファイルしか無いためである。 net/httpのリクエストの扱いは、ADR-0064 が決める。
Considered Options¶
| 選択肢 | 強み | 弱み |
|---|---|---|
| 採用: Yaegi のインタプリタの WASM | 読み込みが brotli で 5.78 MB。go test 相当の出力が、Node の上で動いた |
Go 1.22 までの言語で、ジェネリックの一部と iter が使えない。インタプリタなので、計算の多いコードが遅い |
| Go のコンパイラとリンカを WASM にする | 言語と標準ライブラリが、手元の Go と同じになる | brotli で約 21 MB を読み込む。ブラウザの仮想のファイルシステムと、テストの main の生成を作る。ブラウザで動くかは未確認である |
| TinyGo | 出力する WASM が小さい | 訪問者が書き換えたコードを、サーバーでコンパイルすることになる。訪問者のコードをサーバーに送らない条件と両立しない |
Yaegi を読み込む時期の選択肢は次のとおり。
| 選択肢 | 強み | 弱み |
|---|---|---|
| 採用: 実行のボタンを押したときのほかに、AI のエージェントが Go を実行する操作かテストを実行する操作を呼んだときにも読み込む | 利用者は、エージェントをつないだ後に、実行のボタンを押さなくても、エージェントに実行させられる | スイッチがオンの間は、利用者が実行のボタンを押していなくても、エージェントの操作で読み込む |
| Yaegi を読み込んでいないときは、AI のエージェントからの操作が失敗を返す | 読み込む時期が、実行のボタンを押したときの 1 つで済む | 利用者が、先に実行のボタンを押すことになる |
Consequences¶
Positive¶
- Go だけを動かす記事は、WebContainers のライセンスとブラウザの対応に依らずに公開できる。
- 訪問者は、コードを書き換えて、標準出力とテストの結果をすぐに見られる。
- 利用者は、エージェントをつないだ後に、実行のボタンを押さなくても、エージェントに Go を実行させられる。
Negative¶
- 訪問者が、Yaegi が対応しない書き方(
iter、一部のジェネリック)をすると、手元の Go では通るコードがエラーになる。 - テストの失敗の行が、テストのファイルの行を指さない。
- 性能を測る題材(ベンチマーク)は、インタプリタの実行速度になるので、扱えない。
- スイッチがオンの間は、利用者が実行のボタンを押していなくても、エージェントの操作で、Yaegi の WASM を読み込む。
GoRunnerは、Yaegi にstdlib.Symbolsのほかの symbol を渡さない。渡すときは、Go を実行する操作に確認を出すかを決め直す。GoRunnerに、複数の package の import を解決する処理が要る。- Yaegi で動かすコードは、
net/httpを import できる。ほかの origin へのリクエストの扱いは、ADR-0064 が決める。