🐇

自分が Moonbit 言語について知っていること

2024/04/29に公開

I will write an English version later to give back to the moonbit community.

Addition: https://gist.github.com/mizchi/aef3fa9977c8832148b00145a1d20f4b

この記事はリバースエンジニアリングを含んでいる。公式の Discord サーバーで質問して得られた内容を含むが、ここに書かれたものは自分の理解であって、公式の見解ではない。

前の紹介記事では煽り気味だったので、実際に調べながら書いてみてどう感じているかという実践的な話をする。

https://zenn.dev/mizchi/articles/introduce-moonbit

作者と開発組織

開発母体は深センの研究組織 IDEA

https://www.linkedin.com/company/idearesearch?trk=ppro_cprof

元 Meta で BuckleScript | ReScript を開発していた Hongbo Zhang 氏がチーフアーキテクト。

https://github.com/bobzhang

ReScript を知らない人のために説明すると、AltJS の一種で、OCaml から JS に変換する BuckleScript に、さらに JS 風の文法を被せたもの。JSX 記法を持ち、React コンポーネントを記述できる。

module Button = {
  @react.component
  let make = (~count) => {
    let times = switch count {
    | 1 => "once"
    | 2 => "twice"
    | n => n->Int.toString ++ " times"
    }
    let text = `Click me ${times}`

    <button> {text->React.string} </button>
  }
}

https://rescript-lang.org/

自分も過去にどこかで使えないか検討したことがあるが、 ReScript の世界に閉じて書く分にはJS/TSより遥かに表現力が高い優秀な言語だったが、JS 側の世界を参照するには全部に型を付けて import する必要があり TS の any のような柔軟さがなく断念した。(今は改善されているかも)

WebAssembly はそもそもエコシステムが存在してないところからスタートなので、このへんの問題はないと認識している。

情報ソースとコミュニティ

基本的に Discord で聞けば色々教えてくれる。

https://discord.com/invite/CVFRavvRav

組織と出自的に中国語話者が多い気がするが、英語でのディスカッションとドキュメンテーションを最優先としてくれているので、基本的に英語で問題ない(自分は全部Deeplを通して会話してる)

Discuss に多少のQAのログがある。

https://discuss.moonbitlang.com/

最新機能を紹介するブログ

https://www.moonbitlang.com/weekly-updates

内部実装の推測

コンパイラ本体のソースコードは現状非公開(たぶん後でOSS化されると楽観している)だが、推測はできる。

コンパイラ本体はおそらく OCaml。根拠は moon run main --target js で js_of_ocaml のエラーが出る。Hongbo Zhang 氏の BuckleScript, ReScript が OCaml であったのと、 Meta の言語開発部門はだいたい OCaml を共通言語にしていた。(例えば Flowtype も OCaml 実装)

# inline wat を含むコードを --target js でビルドする
$ moon run main --target js
# 中略
Fatal error: exception File "lib/xml/js_backend/js_of_mcore.ml", line 1254, characters 33-39: Assertion failed

おそらく moon moonc コマンドの CLI は Rust で書かれていて、根拠は moon コマンドのエラーで時折 Rust の unwrap 失敗のエラーが見える。

コアライブラリは moonbit 言語でセルフホストされており、そこはOSSになっている。

https://github.com/moonbitlang/core

プリミティブな型の実装はここに含まれていないが、ここ一週間ぐらいに入った変更で、 .mbt 拡張子の他に .mbti 拡張子が追加されて、ここにコンパイラから提供されるインターフェースの型シグネチャが宣言されるようになった。

https://github.com/moonbitlang/core/blob/main/builtin/builtin.mbti

コンパイラに LLVM は使っておらず、静的解析後に WAT(WebAssembly Text Format) を生成し、それを wasm-tools で .wasm に変換していると思われる。

(おそらく Moonbit 開発者の一人である) peter-jerry-ye 氏が書いた peter-jerry-ye/memory ライブラリのコードの一部で、WAT コードをハードコードしている例があった。

extern "wasm" fn load8_ffi(pos : Int) -> Int =
  #|(func (param $pos i32) (result i32) (i32.load8_u (local.get $pos)))

extern "wasm" fn load32_ffi(pos : Int) -> Int =
  #|(func (param $pos i32) (result i32) (i32.load (local.get $pos)))

https://github.com/peter-jerry-ye/memory/blob/main/memory.mbt

ビルドオプションの $ moon build --target wasm-gc --output-wat.wat ファイルを生成することができる。これを見る限り、この extern はそのまま wat コードに展開される。

生成されるのは非常に素直な .wat で、おそらく生成コードを小さくするように、コンパイラがゼロオーバーヘッドの抽象化を行っているのだろう。これは他の LLVM ベースの言語ではできない、しかし WebAssembly(-GC) ネイティブな言語では必要なアプローチだと思う。LLVM IR を経由すると、どうしても抽象レイヤーが一つ増えて生成コードが大きくなる傾向があった。

自分の知る限り、プリミティブ型はほぼ WebAssembly の仕様に対応するメソッドを備えている。例えば現状、Moonbit にビットシフトを行う>>, << のような構文はないが、 Int 型の Int::lsr => i32.lsr(logical shift right) や Int::lsl => i32.lsl(logical shift left) で代用できる。

開発環境の作り方

https://www.moonbitlang.com/download/

の通りにやるだけ。インストール後のアップグレードは moon upgrade でできるが、どうも experimental であると警告は出る。

インストールすると、~/.moon 以下にツール一式が入って、大事なのは ~/.moon/lib/core に moonbitlang/core が clone される。これが標準ライブラリで、標準ライブラリの更新はこのコードをアップデートして moon bundle --source-dir ~/.moon/lib/core を叩いている。 vscode の LSP もここを見ていそう。

自分が認識している、現状の使いづらい点も書いておく。自分が知る限り、vscode でサブディレクトリに moon.mod.json があるときのパス解決ができないように見える。

こういう時に foo/main/main.mbt が foo.mbt を解決できない。

foo/
  main/
    main.mbt
    moon.pkg.json
  foo.mbt
  moon.pkg.json
  moon.mod.json
moon.mod.json

これは foo ディレクトリを vscode のワークスペースのルートとして開くことで解決できる。

これが自分には結構なストレスなので、早めに治ってほしい。

パッケージマネージャとモジュールシステム

moon add username/lib で mooncakes.io からモジュールを落としてくる。ネームスペースはたぶん必須。

とりあえず moon publish したら、そのディレクトリをポンとアップロードして、moon add user/pkg を叩いたら .mooncakes/ の下に展開するっぽい挙動をしている。

moon コマンドは、.mooncakes 以下をコンパイル時にリンクする。moonbitlang/core だけは標準でビルトインされているが、--nostd で外せる。

正直完成度が高いとは言えない。パッケージ配布方式が独自開発されるのが嫌なので、最低限公式レジストリは用意してみました、という雰囲気。だめと言いたいわけではなく、このフェーズでは正しい選択だとは思う。

モジュールの sub dependencies は .mooncakes/* にフラットに展開される。現状は npm や cargo のようなバージョンソルバーや Sub Dependency の解決ルールがあるわけではないので、依存がぶつかった際はどちらかに上書きされていそうだった。正直ここの挙動はかなり怪しい。

一番の問題点として、 mooncakes.io から落としたコードの呼び方がどこにも説明されていない。(たぶん公式には一貫したモジュール解決ルールは別のドキュメントで説明しているので不要だと思っている節があるが、自分は足りてないと思う)

例えば、自分は最初に見つけた PerfectPan/base64 を最初に使ってみようとした。$ moon add PerfectPan/base64 するのはわかるとして、それをどう呼ぶといいのかが、どこにもドキュメントがなかった。

https://mooncakes.io/docs/#/PerfectPan/base64/members

(念の為に断っておくが、これはPerfectPan氏のドキュメントが良くないと言っているわけではない。 Moonbit のライブラリのドキュメンテーションのほとんどすべてにこの話は共通する)

正解は、利用側のディレクトリの moon.pkg.json に次のように記述すると、@base64 というネームスペースで参照できるようになる。

{
  "is_main": true,
  "import": ["PerfectPan/base64"]
}
fn main {
  let encoded = @base64.base64_encode("hello");
  let decoded = @base64.base64_decode(encoded);
  println("decoded \(decoded)");
}

わかってしまえばなんてことはないが、これにたどり着くまで実は自分はかなり時間がかかった。moonbitlang/core はコンパイラに特別扱いされていたので、参考にしていいかわからなかった。

これに付随する問題として、現状の VSCode extension では、外部モジュールの struct への補完が効かないことがある。 mooncakes のモジュール解決の不明瞭さと相まって、一体自分がどうやってライブラリを参照していいか、かなり混乱した。

例えば、このコードはコンパイル可能だが、@vec.V の時点で補完されず、コードを書ききるまでエラー表示のままで、結構混乱した。

fn main {
  let vec = @vec.Vec::[1, 2, 3]
}

(let vec: @vec.Vec[Int] = と書くときは補完される)

さらなる混乱ポイントとして、関数が Unit 型以外を返すとき、レシーバが存在しないと警告ではなくエラーとして報告される。

fn main {
  @vec.Vec::[1, 2, 3]
  // Expr Type Mismatch
  //         has type : @vec.Vec[_/0]
  //         wanted   : Unit
  // 
  ()
}

これが組み合わさって、外部モジュールに所属する struct の関数が非常に呼び出しづらい。関数呼び出し行はそれ単体だとほとんどがエラーに見えるからだ。レシーバがないときは、エラーではなく warning にしてほしい。

あと、パッケージ @username/foo から pub struct Foo {}; pub fn Foo::new() -> Foo {...} みたいな関数を公開した時、パッケージの外から呼び出すときは @foo.Foo::new() ではなく @foo.new() となってしまっている。これは自分の解釈がおかしいのか、バグなのかの判断ができない。

標準ライブラリを使う

moonbitlang/core が必ずインクルードされている。

fn main {
  // core に含まれてトップレベルの名前空間でアクセスできる例
  let list: List[Int] = List::[]
  let array: Array[Int] = Array::[]
  let tuple: (Int, Int) = (1, 2); // Tuple
  let r: Ref[Int] = { val: 1 }

  // core に含まれるが名前空間付きでアクセスするものの例
  let hm: @hashmap.HashMap[Int, Int] = @hashmap.HashMap::[]
  let vec: @vec.Vec[Int] = @vec.Vec::[]
  let set: @immutable_set.ImmutableSet[Int] = @immutable_set.ImmutableSet::[]
  let queue: @queue.Queue[Int] = @queue.Queue::[]
  let stack: @stack.Stack[Int] = @stack.Stack::[]
  let rand_int = @random.RandomInt::new(10);
}

ミュータブルな配列は @vec.Vec[T] で宣言して push しつつ、最後に vec.to_list().to_array() とすると配列にできる。直接 to_array する方法はない?

グローバル変数

トップレベルで Ref[T] 型を宣言する。

let count: Ref[Int] = { val: 0 }

fn increment() -> Int {
  count.val = count.val + 1
  count.val
}

イテレータ

...について書こうとしたが、絶賛開発中のようなので、後で追記する。

このサンプルコードは、原時点で自分の手元で動かなかった。moonbitlang/core をみてもこのインターフェースではないように見える。

https://twitter.com/moonbitlang/status/1783913471442297026

正直、今あるループの記法はちょっと使いづらい。list.iter(fn (x) {...}) の高階関数は、 TypeScript の経験だと async await 導入時に大変だったので、構文レベルでの JS の for of 相当のものがほしい。

ライブラリの作り方

普通の moon new hello で生成されるディレクトリはこういう形式になっている。

hello/
├── README.md
├── lib
│   ├── hello.mbt
│   ├── hello_test.mbt
│   └── moon.pkg.json
├── main
│   ├── main.mbt
│   └── moon.pkg.json
└── moon.mod.json

最初はこの構造のまま直接 lib 部分が公開されるのかなと思ったが、試した限りちょっと違った。

ライブラリを作るときは、 --lib を付けて生成されるコードみると理解しやすい。

$ moon new mylib --lib
$ tree mylib -I target
mylib
├── README.md
├── lib
│   ├── hello.mbt
│   ├── hello_test.mbt
│   └── moon.pkg.json
├── moon.mod.json
├── moon.pkg.json
└── top.mbt

top.mbtpub がモジュール外に export される関数になる。

ただ、 TypeScript で言う re-export (export {} from "...") 的な構文がないので、lib 関数の実装をそのまま公開しようとすると同じ関数シグネチャでラップするだけの関数を書く必要がある。

実際この top.mbt はこうなっている。

pub fn greeting() -> Unit {
  println(@lib.hello())
}

二度手間では?と思ってパッケージとして公開されているものを見ると、単にトップレベルで実装しているものが多かった。

$ tree ~/repo/peter-jerry-ye/memory/
├── LICENSE
├── README.md
├── memory.mbt
├── moon.mod.json
└── moon.pkg.json

シンプルなので、自分はこっちでいいと思う。

この方式の時、デバッグ用の example を置く方法で迷っていて、自分は main/ を掘ってから、main/moon.pkg.json で親のライブラリ名を指すようにしたらうまくいった。

├── main
│   ├── main.mbt
│   ├── moon.pkg.json
│   └── run_test.ts
├── mod.mbt
├── mod.ts
├── moon.mod.json
├── moon.pkg.json
└── tsconfig.json
{
  "link": {
    "wasm-gc": {
      "exports": ["write_bytes_test"]
    }
  },
  "import": [
    {
      "path": "mizchi/js_io", // 親の mod 名
      "alias": "js_io"
    }
  ]
}

この alias 本来なら不要だと思ってるんだけど、現状これがないとうまく名前解決ができなかった。うまく解決する方法があったら知りたい。

もう一つ困ってる点、現状は .npmignore 的なやつがないので、全部をアップロードしてしまう。この本来不要な main をパブリッシュしてしまうと、なんらかの理由でコンパイルエラーを誘発しそうで怖い。

希望を言うと、moon.pkg.json は省略可能にしてほしい。自分が試した限り、ほとんどのケースは {} のファイルを置くだけになるので...。

文法の TIPS

これもいけるんだ、と思った機能で、型引数に trait 境界を設定することができる。

fn eq[T: Eq](x: T, y: T) -> Bool {
  x == y
}

やっぱり enum が便利で、JSON パーサを実装したときはこういう風に定義した。

pub enum JSONValue {
  String(String)
  Boolean(Bool)
  IntNumber(Int)
  FloatNumber(Double)
  Object(Array[(String, JSONValue)])
  Array(Array[JSONValue])
  Null
} derive(Debug, Eq)

Rust より柔軟で便利だと思ったのがキーワード引数とデフォルト値

fn Point3d::new(~x: Int, ~y: Int, ~z: Int = 0) -> Point3d {
  Point3d::{ x, y, z }
}
fn main {
  let p1 = Point3d::new(x = 1, y = 2)
  let p2 = Point3d::new(x = 1, y = 2, z = 3)
}

例えば React のバインディングを書くとして、この機能で関数コンポーネントの props を表現できそう。

基本 Rust と似ているが、一番大きい差異がジェネリクスが T<X> ではなく T[X] なこと。GitHub Copilot は .mbt でも Rust だと思って補完してくるので、これを書き直すと動くことが多い。生成AIの仕組み上、学習量が多い Rust に引っ張られている。

今のところ残念なのが、 struct や trait に対して derive できるが、現状ビルトインの Eq, Debug, Show, Hash 等しか使えない。自分で定義した trait を derive できるのは今後とのこと。

ビルトインの type に対して自分で trait を定義したり、メソッドを定義することをすることはできない。

pub fn Int::to_xxx(self: Int) -> String {}
// Cannot define method to_xxx for foreign type Int

Serde 的なデシリアライザを作ろうとして、ここで頓挫している。

main 関数の実行

<dir>/moon.pkg.json"is_main": true が定義されているとき、そのモジュールの main 関数を moon run <dir> で呼べる。

fn main {
  println("hello")
}

moonbit では基本的に関数の return の型を明示する必要があるが、このときだけ免除される。

test の実行 + Result 型

test {} でテストケースを記述することができる。また、このとき暗黙に Result 型のブロックと認識され、? の文法が使える。

test {
  @assertion.assert_eq(1 , 1)?
}

// with test name
test "1=1" {
  @assertion.assert_eq(1 , 1)?
}

これは moon.mod.json を持つディレクトリで moon test を実行することで実行できる。ただ、現状は moon test --filter 的なものがないので、実行を除外したいときは一つずつコメントアウトする必要があった。

test ブロック以外でも、ビルトイン型の Result 型を返す関数なら、Rust のように使うことができる。

fn test_result(x: Int) -> Result[Int, String] {
  @assertion.assert_eq(1, 1)?
  if x > 5 {
    Err("x is too big".to_string())
  } else {
    Ok(x)
  }
}

Option[T] もあって、これも Rust と同じく ? で unwrap できる。

自分が困った点として、 .unwrap の実行時エラーはスタックトレースに未対応で、unwrap がエラーを吐いた以上の情報がなくなってしまう。あんまり使わないほうがいい。

fn test_result(x: Int) -> Result[Int, String] {
  let x: Result[Int, Int] = Err(1)
  let _ = x.unwrap() // ここでスタックトレースを失う
  Ok(1)
}

JS からのFFI

WebAssembly の instantiate で, import する実装を渡す。

const { instance: { exports } } = await WebAssembly.instantiateStreaming(
  fetch(new URL("../target/wasm-gc/release/build/main/main.wasm", import.meta.url)),
  {
    xxx: {
      foo: () => 1
    }
  }
);

const ret = exports.run() 

引数と返り値の型は Int (と後述する externref)を使うことができる。

fn xxx_foo() -> Int = "xxx" "foo"

pub fn run() -> Int {
  let v = xxx_foo()
  v + 1
}

この run 関数をコンパイルする時、 moon build --target wasm-gc でコンパイルするとして、次のように公開する関数を明記しておく必要がある。

{
  "link": {
    "wasm-gc": {
      "exports": ["run"]
    }
  }
}

externref

Moonbit というより WebAssembly の externref を前提とした知識になるのだが、 --target wasm-gcpub fn で数値以外の返り値を持つ関数を公開すると、 externref でラップしてくれる。

これはホスト(JS)側では中身に触れず、ただのポインタオブジェクトとして扱うしかないのだが、 Moonbit 側に返すと元の参照に戻る。これが結構便利。

struct Point {
  x: Int
  y: Int
}
pub fn point(x: Int, y: Int) -> Point {
  Point::{ x, y }
}
pub fn get_x(point: Point) -> Int {
  point.x
}

JS 側からの呼び出し

const p = exports.point(3,4);
// ホストからこの p の中身には触ることができないが、 wasm 側へ返すことはできる
exports.get_x(p) //=> 3

js_string

discord で聞いた感じだと Stable な API ではなさそうなのだが、--taget wasm-gc のとき、moonbit 側の println の出力を JS 側へ渡すことができる。その時のJS側の実装例がこれ。

  let memory;

  const [log, flush] = (() => {
    let buffer = [];
    function flush() {
      if (buffer.length > 0) {
        console.log(new TextDecoder("utf-16").decode(new Uint16Array(buffer).valueOf()));
        buffer = [];
      }
    }
    function log(ch) {
      if (ch == '\n'.charCodeAt(0)) { flush(); }
      else if (ch == '\r'.charCodeAt(0)) { /* noop */ }
      else { buffer.push(ch); }
    }
    return [log, flush]
  })();

  const importObject = {
    spectest: {
      print_char: log
    },
    js_string: {
      new: (offset, length) => {
        const bytes = new Uint16Array(memory.buffer, offset, length);
        const string = new TextDecoder("utf-16").decode(bytes);
        return string
      },
      empty: () => { return "" },
      log: (string) => { console.log(string) },
      append: (s1, s2) => { return (s1 + s2) },
    }
  };

  WebAssembly.instantiateStreaming(fetch("/target/wasm-gc/release/build/main/main.wasm"), importObject).then(
    (obj) => {
      memory = obj.instance.exports["moonbit.memory"];
      obj.instance.exports._start();
      flush();
    }
  )

https://github.com/moonbitlang/moonbit-docs/blob/main/examples/wasm-gc/index.html

--output-wat で js_string の振る舞いを読む限りだと、println されると moonbit.memory に文字列のデータを書き込み、 js_string.new に文字列のオフセットを長さを渡す。それをJS側でデコードして表示している。

(これは個人的な意見だが、 textEncoder.encode は Uint8Array を生成するので、JS は確かに内部表現が UTF-16 で合わせてるのはわかるけど、 Moonbit 側のバイナリ表現でも文字列は UTF-16 ではなくJSでデコードしやすい UTF-8 のバイナリ表現をしたほうがよいのではないか?)

WebAssembly.Memory でメモリを共有するパターン

js_string の例で、この moonbit.memory が共有バッファとして使われているのがわかった。

https://github.com/peter-jerry-ye/memory を参考に、任意のメモリの位置にデータを書き込むコードがこう書ける。

オフセットの最初の 4 byte にデータ長を書き込んで、残りはバイナリとする。

extern "wasm" fn load8_ffi(pos : Int) -> Int =
  #|(func (param $pos i32) (result i32) (i32.load8_u (local.get $pos)))

extern "wasm" fn store8_ffi(pos : Int, value : Int) =
  #|(func (param $pos i32) (param $value i32) (i32.store8 (local.get $pos) (local.get $value)))

// WebAssembly.Memory は 1ページ 64kb なので、
// js_string とぶつかりにくいようにその後半を使う
let buffer_offset : Ref[Int] = { val: 32768 }

pub fn write_buffer(bytes : Bytes) -> Unit {
  store_bytes(bytes, buffer_offset.val)
}

pub fn read_buffer() -> Bytes {
  let offset = buffer_offset.val
  let len = read_length(offset)
  let bytes = Bytes::make(len, 0)
  for i = 0; i < len; i = i + 1 {
    bytes[i] = load8_ffi(offset + 4 + i)
  }
  bytes
}


fn store_bytes(bytes : Bytes, offset : Int) -> Unit {
  let byte_length = bytes.length()
  let int_bytes = int_to_bytes(byte_length)
  for i = 0; i < 4; i = i + 1 {
    store8_ffi(offset + i, int_bytes[i])
  }
  for i = 0; i < byte_length; i = i + 1 {
    store8_ffi(offset + 4 + i, bytes[i])
  }
}

// read byte length
fn read_length(offset : Int) -> Int {
  load8_ffi(offset) + load8_ffi(offset + 1).lsl(8) + load8_ffi(offset + 2).lsl(
    16,
  ) + load8_ffi(offset + 3).lsl(24)
}

fn int_to_bytes(value : Int) -> Array[Int] {
  [
    value.land(0xFF),
    value.lsr(8).land(0xFF),
    value.lsr(16).land(0xFF),
    value.lsr(24).land(0xFF),
  ]
}

JS側

let _memory: WebAssembly.Memory;
let _offset = 32768;

export function setMemory(newMemory: WebAssembly.Memory) {
  _memory = newMemory;
}

export function writeBuffer(bytes: Uint8Array) {
  const buf = new Uint8Array(_memory.buffer);
  const intBytes = intToBytes(bytes.byteLength);
  buf.set(intBytes, _offset);
  buf.set(bytes, intBytes.byteLength + _offset);
}

export function readBuffer(): Uint8Array {
  const buf = new Uint8Array(_memory.buffer);
  const len = getLength(buf, _offset);
  return buf.slice(_offset + 4, _offset + 4 + len);
}

function getLength(buffer: Uint8Array, offset: number): number {
  return new DataView(buffer.buffer, offset, 4).getInt32(0, true);
}

function intToBytes(value: number): Uint8Array {
  const buffer = new ArrayBuffer(4);
  const dataView = new DataView(buffer);
  dataView.setInt32(0, value | 0, true);
  return new Uint8Array(buffer);
}

...というライブラリを https://mooncakes.io/docs/#/mizchi/js_io/ として公開している。

https://github.com/peter-jerry-ye/memory はもっとちゃんとしてて、可変長なメモリアロケータが実装されている。

Moonbit に足りない機能: 非同期

非同期。具体的に言うと async/await 構文。

これの実装が大変なのはわかる。とくに Rust では tokio で一悶着あったし、未だに std/async との間で混乱を生んでいる。

だが、Moonbit の掲げている Cloudflare Workers での実行のためには、Moonbit 自身が非同期をハンドルできる必要があって、具体的には fetch を叩く必要がある。

こういうイメージ

fn js_fetch(req: Request) -> Response = "js" "fetch"

pub fn handle(req: Request) -> Response {
  let res = await fetch(req); // or rust sytle fetch(req).await
  let json = await res.json()
  // ... something json decoder
  Response::json(returning_json)
}

JS の async/await は実体としては generator の糖衣構文なので、ホストでイベントループを握ると考えると、本当に必要なのは Generator かもしれない。

Moonbit に足りない機能: Component Model

Moonbit が複雑なアプリケーションで実用するとなると、数値と externref 以外のインターフェースが必要になる。つまり Rust の wit-bindgen 的な、何かしらの構造体を通信するためのスペックが必要になる。

というわけで、mizchi/js_io と組み合わせる前提で、簡単なJSONのバイナリエンコーダを書いた。

https://mooncakes.io/docs/#/mizchi/protocol/members

これはまだ実験中。内部構造をなんらかの仕様のABIに合わせたものにしたい。今 WebAssembly の Canonical ABI のスペックを読んでいる。

https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md

というわけで Proposal を書いておいた。

https://github.com/moonbitlang/moonbit-RFCs/pull/5

ただ、現状の component model は Wasm GC 未対応で、それ待ちという回答を得ている。

あと、この前にJSONエンコーダを書いたのだが、つい先週に moonbitlang/core に json が入ったので。これを元に拡張すれば一応非効率ながらJSONは喋れそう

https://mooncakes.io/docs/#/mizchi/json/members

https://github.com/moonbitlang/core/tree/main/json

Moonbit の現時点の実用性

サードパーティライブラリが整っていないのと、サブモジュールの解決方法に問題はあるが、外部モジュールに依存しない同期関数を書くだけなら Moonbit は十分有力な選択肢になる。

というか、他の選択肢である assemblyscript, Rust wasm-bindgen, zig がそこまで WebAssembly を書きやすくない。WebAssembly の水準で Moonbit ほど高水準で書きやすい言語が他にないと思っている。

あとはなんとか流行ってもらってライブラリが揃うのを待つ。自分はほしいやつを自力で書けるからもう使えると思ってるけど、非同期と Component Model は公式のサポートがほしい。

以上です。

Discussion