ruby.wasmでrequire_relativeを使えるようにしたい

# 背景

[ruby.wasm](https://github.com/ruby/ruby.wasm)というRubyのランタイムをWebAsssemblyにコンパイルした実行環境があります。
これを使うとブラウザでRubyスクリプトを実行出来ます。
実際にいくつかのアプリケーションが作られ、次のページで公開されています。

https://github.com/ruby/ruby.wasm/wiki/Showcase

具体的にブラウザでRubyスクリプトを動かすには、たとえば、次のようにscriptタグにインラインでRubyスクリプトを記述します。

```html
<html>
  <script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.5.0/dist/browser.script.iife.js"></script>
  <script type="text/ruby">
    puts "Hello, world!"
  </script>
</html>
```

または次のようにscriptタグのsrc属性で指定したRubyスクリプトを読み込ませる事も出来ます。

```html
<html>
  <script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.5.0/dist/browser.script.iife.js"></script>
  <script type="text/ruby" src="hello.rb"></script>
</html>
```

# 現状の課題

アプリケーションが大きくなりRubyスクリプトを複数ファイルにわけたくなった場合はどうしたらいいでしょうか？
複数のscriptタグを使ってRubyスクリプトを順番に読み込むことができます。

```html
<html>
  <script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.5.0/dist/browser.script.iife.js"></script>
  <script type="text/ruby" src="lib.rb"></script>
  <script type="text/ruby" src="main.rb"></script>
</html>
```

ファイル数が増えてくると、どの順番で読み込むか考えるのが難しくなります。そのため実質的には2~3ファイルにしか分けられません。2010年代前半のJavaScriptアプリケーションを振り返ると、1ファイルが1000行を超えることは珍しくありませんでした。同様に1つのRubyスクリプトの行数が1000行を越えることが考えられます。

現代のJavaScriptでは、この問題をモジュールバンドラーで解決しています。モジュールバンドラーというのは、たとえばWebPackやViteやEsBuildやRollupです。JavaScriptのソースファイルに記述された`require`や`import`という命令によって、JavaScriptファイル間の依存関係を指定します。モジュールバンドラーはこれを手がかりに、JavaScriptのファイル群を上手い順番で並べ替え、1つのJavaScriptファイルに結合してくれます。

# 解決策

Rubyにも`require`や`requirue_relative`というファイル間の依存関係を指定する関数があります。
RubyでもJavaScriptのモジュールバンドラーのようにこの問題を解決できないでしょうか？

JavaScriptの`require`または`import`関数は、ファイルシステム上のパスとURL上のパスが一致しないことが知られています。
例えば`$ = require('jquery')`と書いた時、実際のファイルがどこにあるかは、`node_modules`配下のディレクトリを検索しないとわかりません。Rubyの`require`でも同様に`require 'csv'`と書いた時に、実際のファイルがどこにあるかはファイルシステムを検索する必要があります。Rubyにおいても、モジュールバンドラーのようなツールを作って、Rubyスクリプトを処理する必要があるのでしょうか？

運良くRubyでは`require`のほかに`require_relative`関数があります。`require_relative`関数は、相対パスで依存するRubyスクリプトを指定します。元々想定しているのはファイルシステム上の相対パスです。これを相対URLと読み替えられるはずです。ブラウザ上で実行する`requirer_relative`を「相対URLに配置されているRubyスクリプトの読み込み処理」に置き換えれぼ、Rubyスクリプトファイルを変換せずにブラウザ上でも実行可能になります。


# 実現までの課題

コンセプトが出来たので実装してみました。
課題をいくつか発見できました。解決できた課題もあります。解決途中の課題もあります。

## 解決済み課題

### 相対パスの解決

相対パスを解決するためには、`require_relative`が記述された実行中のRubyスクリプトのURLが必要です。
Rubyスクリプトを実行する際に、そのRubyスクリプトを取得したURLを保存する仕組みが必要です。

これは次のようなデータ構造を作り、実行するRubyスクリプトのURLを保存すると実現できます[^3]。

```ts
class EvaluatedScriptStack {
  private _vm;
  // Stores the URL of the running Ruby script to get the relative path from within the Ruby script.
  private _stack: Array<RubyScript>;

  constructor(vm) {
    this._vm = vm;
    this._stack = [];
  }

  eval(script: RubyScript): void {
    this._stack.push(script);
    this._vm.eval(script.ScriptBody);
    this._stack.pop();
  }
}
```

配列をスタックとして使いRubyスクリプトを実行するたびにURLを保存します。

[^3]: https://github.com/ledsun/ruby.wasm/commit/da37a0d0fdf1786537e7616f28692d0e082f2f06

### requrie_relativeを改変すると標準gemの読込ができなくなる

ruby.wasmには標準gemが含まれています。たとえば`require 'csv'`を実行すればcsv gemが使えます。csv gemの中で`requrie_relative`が使われています。単純に`requrie_relative`を置き換えると、csv gemは使えなくなります。

ruby.wasmに含まれない、scriptタグで読み込んだRubyスクリプトのみ`requrie_relative`を置き換える必要があります。
次のように、ダウンロードしたRubyスクリプトを実行前に書き換えることで実現出来ます[^2]。

```ts
const patchedScript = scriptBody.replace(
  /require_relative/g,
   "require_relative_url"
);
```

ここでは`require_relative`関数を`require_relative_url`に置き換えています。

[^2]: https://github.com/ledsun/ruby.wasm/commit/e72b63fa5527a3d73ea99dd22f377d2596fb250d

## 解決途中の課題

### fetchが非同期関数

WebAssemblyからはHTTPリクエストを送れません。Rubyスクリプトをダウンロードするには、WebAsssemblyの外側の関数を呼びます。ブラウザに組み込みのfetch関数を使います。fetch関数はPromiseを返す非同期関数です。JavaScriptのPromiseそのものはRubyにはありません。Promiseと同様の仕組みで、Promiseの完了を待ち合わせる必要があります。

待ち合わせが出来ない場合は、次のRubyスクリプトを実行すると

```ruby
require_relative 'lib.rb'

puts 'hello'
```

`lib.rb`を読み込む前に`puts 'hello'`が実行されます。依存しているライブラリを読み込み終わってから、スクリプト本体を実行したいです。requrie系の関数は同期的に実行する必要があります。

ruby.wasmには0.5.0から`evalAsync`という実行方法があります[^1]。これをつかって次のようにPromiseの完了を待てます。

```javascript
async functiton slowFunction() {
  return 1;
}

rubyVM.evalAsync('JS.global.slowFunction().await')
```

[^1]: https://github.com/ruby/ruby.wasm/tree/ruby-head-wasm-wasi-0.5.0/packages/npm-packages/ruby-head-wasm-wasi#evalasync

`evalAsync`を使って次のようにJavaScriptの非同期関数を待つ`require_relative_url`関数を定義します。

```ruby
module Kernel
  def require_relative_url(relative_feature)
    JS.global[:rubyWasm].requireRelativeURL(relative_feature).await
  end
end
```

そしてJavaScriptの`requireRelativeURL`関数でRubyスクリプトのfetchから実行までを行います。

```ts
async requireRelativeURL(relative_feature): Promise<boolean> {
  const filename = relative_feature.endsWith(".rb")
    ? relative_feature
    : `${relative_feature}.rb`;
  const url = new URL(filename, this._stack.currentURL);

  const response = await fetch(url);
  if (!response.ok) {
    return false;
  }

  const text = await response.text();
  await this._stack.eval(new RubyScript(url, text));

  return true;
}
```

これで上手く行くはずなのですが・・・実際に動かして見ると`require_relative`を1回呼ぶ分には上手く動きます。
2回呼ぶとPromiseの完了待ちが無限にブロックされます[^4]。この原因がわからず、調査中です。

[^4]: https://github.com/ledsun/ruby.wasm/commit/5e9d234b102c394729971703bc03ed84b629738f


# 結果

Advent Calendarの公開日までにはつくりたかったのですが、間に合いませんでした。年明けまでには頑張って動くようにするぞ！