Open13

moonbit 言語仕様の確認

mizchimizchi

Module を見てみる

https://www.moonbitlang.com/docs/build-system-tutorial

プロジェクトルートに moon.mod.json があり、ディレクトリ毎に moon.pkg.json がある。

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

moon.mod.json を見てみる。

moon.mod.json
{
  "name": "username/hello",
  "version": "0.1.0",
  "readme": "README.md",
  "repository": "",
  "license": "Apache-2.0",
  "keywords": [],
  "description": ""
}

呼び出しは、@lib を付ける。

fn main {
  println(@lib.hello())
}

これはどういう命名則でアクセスできてるんだ?

調査班はジャングルの奥地に向かった。

パッケージを書き換えてみる

username となってる部分を書き換える

moon.mod.json
{
  "name": "mizchi/hello",
  "version": "0.1.0",
  "readme": "README.md",
  "repository": "",
  "license": "Apache-2.0",
  "keywords": [],
  "description": ""
}
main/moon.pkg.json
{
  "is_main": true,
  "import": ["mizchi/hello/lib"]
}

動いた。

ちなみに mizchi/hello はスラッシュ抜きの hello にしてみても動いた。ネームスペースの衝突を避けてるだけか、それともパブリッシュするときに変わるか...?

run できる main モジュールを追加する

$ cp -r main main2
$ moon run main2  
lib:init
Hello, world!

こういう状態

$ tree . -I target
.
├── README.md
├── lib
│   ├── hello.mbt
│   ├── hello_test.mbt
│   └── moon.pkg.json
├── main
│   ├── main.mbt
│   └── moon.pkg.json
├── main2
│   ├── main.mbt
│   └── moon.pkg.json

$ moon run ./main2 でもいいらしい。要は moon.pkg.json"is_main": true が指定されていれば、pkg/main.mbt が発火する。

lib package を追加する

lib/fib 以下に、次のファイルを追加する。

lib/fib/a.mbt
pub fn fib(n : Int) -> Int {
  match n {
    0 => 0
    1 => 1
    _ => fib(n - 1) + fib(n - 2)
  }
}
lib/fib/b.mbt
pub fn fib2(num : Int) -> Int {
  fn aux(n, acc1, acc2) {
    match n {
      0 => acc1
      1 => acc2
      _ => aux(n - 1, acc2, acc1 + acc2)
    }
  }

  aux(num, 0, 1)
}
lib/fib/moon.pkg.json
{}

これを main から呼び出す。

main/moon.pkg.json
{
  "is_main": true,
  "import": [
    "username/hello/lib",
    {
      "path": "mizchi/hello/lib/fib",
      "alias": "my_awesome_fibonacci"
    }
  ]
}
main/main.mbt
fn main {
  let a = @my_awesome_fibonacci.fib(10)
  let b = @my_awesome_fibonacci.fib2(11)
  println("fib(10) = \(a), fib(11) = \(b)")
  
  println(@lib.hello())
}

呼ぶ

$ moon run ./main
fib(10) = 55, fib(11) = 89
Hello, world!

username/hello の部分は、moon.mod.json の name で、それ以下はルートからのパスになるっぽい。
外部モジュールには alias で名前を付けられる。

テストの実行

lib/fib/a.mbt
pub fn fib(n : Int) -> Int {
  match n {
    0 => 0
    1 => 1
    _ => fib(n - 1) + fib(n - 2)
  }
}

fn assert_eq[T: Show + Eq](lhs: T, rhs: T) -> Unit {
  if lhs != rhs {
    abort("assert_eq failed.\n    lhs: \(lhs)\n    rhs: \(rhs)")
  }
}

test {
  assert_eq(fib(1), 1)
  assert_eq(fib(2), 1)
  assert_eq(fib(3), 2)
  assert_eq(fib(4), 3)
  assert_eq(fib(5), 5)
}
$ moon test
Total tests: 2, passed: 2, failed: 0.

lib 以外の場所に module を置く

TBD

core module を呼ぶ

外部モジュールを呼ぶ...その前に、最初から読み込まれている core モジュールを呼んでみる。

https://mooncakes.io/docs/#/moonbitlang/core/

main/main.mbt
fn main {
  println("hello")
  let map1 = @map.Map::[(3, "three"), (8, "eight"), (1, "one")]
  let map2 = map1.insert(2, "two").remove(3)
  let map3 = map2.insert(2, "updated")
  println(map3.lookup(2)) // output: Some("updated")
}

外部モジュールを呼ぶ

$ moon add PerfectPan/base64

moon.mod.json の deps に追加される

使いたいモジュールの import に追加。今回は main/main.pkg.json

main/moon.pkg.json
{
  "is_main": true,
  "import": ["PerfectPan/base64"]
}

使う。

fn main {
  let x = @base64.base64_encode("hello")
  let y = @base64.base64_decode(x)
  println(x)

  match y {
    Ok(v) => {
      println(v)
    }
    Err(e) => {
      println("parse error")
      // debug(e)
    }
  }
  // or just unwrap
  println(y.unwrap())
}

lib 以外のモジュール

$ tree ./lib2     
./lib2
├── hello.mbt
└── moon.pkg.json

1 directory, 2 files

これも lib と同じ用に呼べる。lib は特別扱いされているわけではなく、慣習的なものっぽい。

mizchimizchi

FFI

moonbit から JS を呼び出す

  // wasm 初期化時の importObject にわたす
  const importObject = {
    myns: {
      log: (num) => {
        console.log('[myns:log]', num);
      },
    },

Moonbit 側から呼び出す

fn mylog(color : Int) = "myns" "log"

fn main {
  mylog(1)
}

TODO: string の渡し方

mizchimizchi

この myns はブラウザ実行時しか呼べず、 moon run main では呼べない。
ビルド時に分岐するのはどうする?

mizchimizchi

先の wasm ビルドは main モードとして実行したが、次はライブラリとして使う

lib

update 関数は引数で Ctx オブジェクトを受け取る。ctx は外で定義される。

lib/lib.mbt
type Ctx
fn set(self: Ctx, cid: Int) = "ctx" "set"

pub fn update(ctx: Ctx) -> Unit {
  ctx.set(1)
}
lib/moon.pkg.json
{
  "name": "lib",
  "link": {
    "wasm-gc": {
      "exports": ["update"]
    }
  }
}

moon build --target wasm-gc で実行

呼び出し側

index.html

<html lang="en">

<body>
  <canvas id="canvas" width="150" height="150"></canvas>
</body>
<script>
  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 = {
    ctx: {
      set(cid) {
        console.log('[ctx:set]', cid)
      }
    }
  };

  WebAssembly.instantiateStreaming(fetch("/target/wasm-gc/release/build/lib/lib.wasm"), importObject).then(
    (obj) => {
      memory = obj.instance.exports["moonbit.memory"];
      obj.instance.exports._start();
      const api = obj.instance.exports;
      api.update(1);
      flush();
    }
  )
</script>

</html>

flush 部分はおまじない部分として、ctx set を実行しておく

mizchimizchi

from_array と from

let map = @map.Map::[]
let map: @map.Map[Int, Int] = @map.Map::from_arary([])

これらは一緒

mizchimizchi

wasm 内部ポインタ

snake のコードを読んでいると、面白い挙動を見つけた。wasm から return したオブジェクトはJSからは見えないが、内部的にはコンテキストをちゃんと持っている。内部的には extern_ref になっていそう。

lib/lib.mbt
pub struct App {
  id: Int
} derive(Debug)

pub fn startApp() -> App {
  App::{
    id: 1
  }
}

pub fn printApp(app: App) -> Unit {
  debug(app)
}

app を返し、appを受け取って debug するだけの関数

js から使う

  WebAssembly.instantiateStreaming(fetch("/target/wasm-gc/release/build/lib/lib.wasm"), importObject).then(
    (obj) => {
      memory = obj.instance.exports["moonbit.memory"];
      obj.instance.exports._start();
      const api = obj.instance.exports;
      api.update(1);
      const app = api.startApp();
      console.log(app); //=> {}
      api.printApp(app); //=> {id: 1}
    }
  )
mizchimizchi

文字列を取り出す

moonbit は標準機能として文字列を受け渡す方法を持たない。(wasmの仕様上ない)
wasm の入出力は ref or int なので、無理矢理文字コードを数値として取り出してみた。

struct MyCtx {
  result: String
  mut cur: Int
}

pub fn app() -> MyCtx {
  MyCtx::{
    result: "hello", // extracting target
    cur: 0
  }
}

fn MyCtx::next(self: MyCtx) -> Int {
  if (self.cur >= self.result.length()) {
    return -1
  }
  let r = self.result[self.cur].to_int()
  self.cur += 1
  r
}

pub fn reset(buf: MyCtx) -> Unit {
  buf.cur = 0
}

pub fn next_char(buf: MyCtx) -> Int {
  buf.next()
}

pub fn get_offset(buf: MyCtx) -> Int {
  let x = buf.result.to_js_string()
  x.log()
  1
}
moon.pkg.json
{
  "name": "lib",
  "link": {
    "wasm-gc": {
      "exports": [
        "app",
        "reset",
        "next_char"
      ]
    }
  }
}

js

  obj.instance.exports._start();
  const api = obj.instance.exports;
  const decoder = new TextDecoder("utf-16");

  function getString(b) {
    api.reset(b);
    let next;
    let buf = [];
    while ((next = api.next_char(b)) !== -1) {
      buf.push(next);
    }
    return decoder.decode(new Uint16Array(buf).valueOf());
  }
  const b = api.app();
  const str = getString(b);
  console.log(str);

これで "hello" が取り出せる

mizchimizchi

同様にJS => Wasm へ文字列を渡す

struct MyCtx {
  result: String
  mut cur: Int
  input: @vec.Vec[Int]
}

pub fn app() -> MyCtx {
  MyCtx::{
    result: "hello", // extracting target
    cur: 0,
    input: @vec.Vec::new()
  }
}

fn MyCtx::next(self: MyCtx) -> Int {
  if (self.cur >= self.result.length()) {
    return -1
  }
  let r = self.result[self.cur].to_int()
  self.cur += 1
  r
}

pub fn reset(buf: MyCtx) -> Unit {
  buf.cur = 0
}

pub fn next_char(buf: MyCtx) -> Int {
  buf.next()
}

pub fn reset_input(buf: MyCtx) -> Unit {
  buf.input.clear()
}

pub fn input(buf: MyCtx, code: Int) -> Unit {
  buf.input.push(code)
}

pub fn read_input(buf: MyCtx) -> Unit {
  let mut str: String = ""
  buf.input.iter(fn (c) {
    let char = Char::from_int(c)
    str += char.to_string()
  });
  println("input: \(str)")
}

moon.pkg.json は省略

  const api = obj.instance.exports;
  const decoder = new TextDecoder("utf-16");
  function write(app, text) {
    api.reset_input(app);
    const buf = new Uint16Array(new TextEncoder().encode(text));
    for (let i = 0; i < buf.length; i++) {
      api.input(app, buf[i]);
    }
  }
  const app = api.app();
  write(app, "Hello, World!");
  api.read_input(app);

result

input: Hello, World!
mizchimizchi

メモリを直接触るために https://github.com/peter-jerry-ye/memory を参考にする

fn main {
  let bytes = "hello world".to_bytes()
  let m = @memory.allocate(1024).unwrap()
  m.store_bytes(bytes)
  println(m.load_bytes().to_string())
}

インラインでwasmが書かれているため、moon run main だと動かない。一旦ビルドして deno から実行する。

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

export const spectest = {
  print_char: log
}

export const 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) },
};


const { instance } = await WebAssembly.instantiateStreaming(
  fetch(new URL("./target/wasm-gc/release/build/main/main.wasm", import.meta.url)),
  { js_string, spectest }
);

setMemory(instance.exports["moonbit.memory"]);
const exports = instance.exports as any;
exports._start();
flush()

実行

$ moon build --target wasm-gc 
$ deno run --allow-read run.ts

中をみるとこんな感じ

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)))

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

extern "wasm" fn loadf64_ffi(pos : Int) -> Double =
  #|(func (param $pos i32) (result f64) (f64.load (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)))

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

extern "wasm" fn store64_ffi(pos : Int, value : Int64) =
  #|(func (param $pos i32) (param $value i64) (i64.store (local.get $pos) (local.get $value)))

extern "wasm" fn storef64_ffi(pos : Int, value : Double) =
  #|(func (param $pos i32) (param $value f64) (f64.store (local.get $pos) (local.get $value)))

extern "wasm" fn memory_size_ffi() -> Int =
  #|(func (result i32) (memory.size))

extern "wasm" fn memory_grow_ffi(delta : Int) -> Int =
  #|(func (param $size i32) (result i32) (memory.grow (local.get $size)))

extern "wasm" fn memory_copy_ffi(origin : Int, target : Int, len : Int) =
  #|(func (param $origin i32) (param $target i32) (param $len i32) (memory.copy (local.get $origin) (local.get $target) (local.get $len)))
mizchimizchi

js_string の実装を確認する。

fn main {
  let s = "hello world"
  s.to_js_string().log()
}

--output-wat で wat ファイルを出力する。

$ moon build --target wasm-gc --output-wat

出力された wat

(data $moonbit.string_data "h\00e\00l\00l\00o\00 \00w\00o\00r\00l\00d\00 ")
(func $js_string.log (import "js_string" "log") (param externref)
 (result i32))
(import "js_string" "new"
 (func $js_string.new (param i32) (param i32) (result externref)))
(import "js_string" "empty" (func $js_string.empty (result externref)))
(memory $moonbit.memory (export "moonbit.memory") 1)
(type $moonbit.string (array (mut i16)))
(type $moonbit.string_pool_type (array (mut (ref null $moonbit.string))))
(global $moonbit.empty_js_string (mut externref) (ref.null extern))
(func $moonbit.string_literal (param $index i32) (param $offset i32)
 (param $length i32) (result (ref $moonbit.string))
 (local $cached (ref null $moonbit.string))
 (local $new_string (ref $moonbit.string)) global.get $moonbit.string_pool
 local.get $index array.get $moonbit.string_pool_type local.tee $cached
 ref.is_null i32.eqz if local.get $cached ref.as_non_null return else end
 local.get $offset local.get $length array.new_data $moonbit.string
 $moonbit.string_data local.set $new_string global.get $moonbit.string_pool
 local.get $index local.get $new_string array.set $moonbit.string_pool_type
 local.get $new_string return)
(func $moonbit.string_to_js_string (param $x (ref $moonbit.string))
 (result externref) (local $s (ref $moonbit.string)) local.get $x
 ref.as_non_null local.tee $s array.len i32.const 0 i32.eq if call
 $moonbit.get_empty_js_string return else end local.get $s i32.const 0 call
 $moonbit.copy_string_to_memory i32.const 0 local.get $s array.len call
 $js_string.new)
(func $moonbit.copy_string_to_memory (param $src (ref $moonbit.string))
 (param $dst_addr i32) (local $cur_addr i32) (local $str_len i32)
 (local $src_index i32) local.get $dst_addr local.set $cur_addr local.get
 $src array.len local.set $str_len i32.const 0 local.set $src_index loop
 $label2 block $label0 local.get $src_index local.get $str_len i32.lt_s
 i32.eqz br_if $label0 local.get $cur_addr local.get $src local.get
 $src_index array.get_u $moonbit.string i32.store16 local.get $cur_addr
 i32.const 2 i32.add local.set $cur_addr local.get $src_index i32.const 1
 i32.add local.set $src_index br $label2 end $label0 end $label2)
(func $moonbit.get_empty_js_string (result externref)
 (local $value externref) global.get $moonbit.empty_js_string local.tee
 $value ref.is_null if call $js_string.empty local.set $value local.get
 $value global.set $moonbit.empty_js_string else end local.get $value return)
(global $moonbit.string_pool (ref $moonbit.string_pool_type)
 (array.new_default $moonbit.string_pool_type (i32.const 1)))
(func $Js_string::log.fn/1 (param $*param/2 externref) (result i32)
 (ref.as_non_null (local.get $*param/2)) (call $js_string.log))
(func $$mizchi/mem/main.init_js_memory.fn/2 (result i32)
 (call " (import \22js\22 \22mem\22 (memory $mem 1))") (i32.const 0))
(func $*init*/3 (local $s/1 (ref $moonbit.string))
 (call $$mizchi/mem/main.init_js_memory.fn/2) (drop)
 (call $moonbit.string_literal (i32.const 0) (i32.const 0) (i32.const 11))
 (local.tee $s/1) (call $moonbit.string_to_js_string)
 (call $Js_string::log.fn/1) (drop))
(export "_start" (func $*init*/3))
mizchimizchi

claulde-3-opus に突っ込んで解説させた。

;; 文字列データセクション
;; "hello world" という文字列が UTF-16 エンコーディングで格納されています
(data $moonbit.string_data "h\\00e\\00l\\00l\\00o\\00 \\00w\\00o\\00r\\00l\\00d\\00 ")

;; js_string.log 関数のインポート宣言
;; js_string モジュールの log 関数を externref 型の引数を1つ取り、i32 型の値を返すものとしてインポートしています
(func $js_string.log (import "js_string" "log") (param externref) (result i32))

;; js_string.new 関数のインポート宣言
;; js_string モジュールの new 関数を i32 型の引数を2つ取り、externref 型の値を返すものとしてインポートしています
(import "js_string" "new" (func $js_string.new (param i32) (param i32) (result externref)))

;; js_string.empty 関数のインポート宣言
;; js_string モジュールの empty 関数を引数なしで、externref 型の値を返すものとしてインポートしています
(import "js_string" "empty" (func $js_string.empty (result externref)))

;; メモリ宣言
;; 1ページ分のメモリを moonbit.memory という名前でエクスポートしています
(memory $moonbit.memory (export "moonbit.memory") 1)

;; moonbit.string 型の定義
;; i16 型の可変長配列として定義されています
(type $moonbit.string (array (mut i16)))

;; moonbit.string_pool_type 型の定義
;; moonbit.string 型への参照の可変長配列として定義されています
(type $moonbit.string_pool_type (array (mut (ref null $moonbit.string))))

;; moonbit.empty_js_string グローバル変数の定義
;; externref 型の可変グローバル変数で、初期値は null になっています
(global $moonbit.empty_js_string (mut externref) (ref.null extern))

;; moonbit.string_literal 関数の定義
;; 文字列リテラルを moonbit.string 型に変換する関数です
;; index, offset, length の3つの i32 型引数を取り、moonbit.string 型への参照を返します
(func $moonbit.string_literal (param $index i32) (param $offset i32) (param $length i32) (result (ref $moonbit.string))
  (local $cached (ref null $moonbit.string))
  (local $new_string (ref $moonbit.string))
  
  ;; moonbit.string_pool から index 番目の要素を取得し、$cached に格納
  global.get $moonbit.string_pool
  local.get $index
  array.get $moonbit.string_pool_type
  local.tee $cached
  
  ;; $cached が null でない場合は、それを返す
  ref.is_null
  i32.eqz
  if
    local.get $cached
    ref.as_non_null
    return
  else
  end
  
  ;; 新しい moonbit.string を作成し、$new_string に格納
  local.get $offset
  local.get $length
  array.new_data $moonbit.string $moonbit.string_data
  local.set $new_string
  
  ;; moonbit.string_pool の index 番目の要素に $new_string を格納
  global.get $moonbit.string_pool
  local.get $index
  local.get $new_string
  array.set $moonbit.string_pool_type
  
  ;; $new_string を返す
  local.get $new_string
  return
)

;; moonbit.string_to_js_string 関数の定義
;; moonbit.string 型の文字列を js_string に変換する関数です
;; moonbit.string 型への参照を引数に取り、externref 型の値を返します
(func $moonbit.string_to_js_string (param $x (ref $moonbit.string)) (result externref)
  (local $s (ref $moonbit.string))
  
  ;; $x が null でないことを確認し、$s に格納
  local.get $x
  ref.as_non_null
  local.tee $s
  
  ;; $s の長さが0の場合は、空の js_string を返す
  array.len
  i32.const 0
  i32.eq
  if
    call $moonbit.get_empty_js_string
    return
  else
  end
  
  ;; $s の内容をメモリにコピーし、js_string.new を呼び出して js_string を作成
  local.get $s
  i32.const 0
  call $moonbit.copy_string_to_memory
  i32.const 0
  local.get $s
  array.len
  call $js_string.new
)

;; moonbit.copy_string_to_memory 関数の定義
;; moonbit.string 型の文字列をメモリにコピーする関数です
;; src に moonbit.string 型への参照、dst_addr に i32 型の宛先アドレスを取ります
(func $moonbit.copy_string_to_memory (param $src (ref $moonbit.string)) (param $dst_addr i32)
  (local $cur_addr i32)
  (local $str_len i32)
  (local $src_index i32)
  
  ;; $dst_addr を $cur_addr に格納
  local.get $dst_addr
  local.set $cur_addr
  
  ;; $src の長さを $str_len に格納
  local.get $src
  array.len
  local.set $str_len
  
  ;; $src_index を0に初期化
  i32.const 0
  local.set $src_index
  
  ;; $src_index が $str_len 未満の間、ループ
  loop $label2
    block $label0
      local.get $src_index
      local.get $str_len
      i32.lt_s
      i32.eqz
      br_if $label0
      
      ;; $cur_addr に $src の $src_index 番目の要素を格納
      local.get $cur_addr
      local.get $src
      local.get $src_index
      array.get_u $moonbit.string
      i32.store16
      
      ;; $cur_addr を2バイト進める
      local.get $cur_addr
      i32.const 2
      i32.add
      local.set $cur_addr
      
      ;; $src_index を1増やす
      local.get $src_index
      i32.const 1
      i32.add
      local.set $src_index
      
      br $label2
    end $label0
  end $label2
)

;; moonbit.get_empty_js_string 関数の定義
;; 空の js_string を取得する関数です
;; 引数はなく、externref 型の値を返します
(func $moonbit.get_empty_js_string (result externref)
  (local $value externref)
  
  ;; moonbit.empty_js_string が null の場合は、js_string.empty を呼び出して空の js_string を作成し、moonbit.empty_js_string に格納
  global.get $moonbit.empty_js_string
  local.tee $value
  ref.is_null
  if
    call $js_string.empty
    local.set $value
    local.get $value
    global.set $moonbit.empty_js_string
  else
  end
  
  ;; moonbit.empty_js_string を返す
  local.get $value
  return
)

;; moonbit.string_pool グローバル変数の定義
;; moonbit.string への参照の配列として定義されています
;; 初期値は長さ1の配列で、要素はすべて null になっています
(global $moonbit.string_pool (ref $moonbit.string_pool_type) (array.new_default $moonbit.string_pool_type (i32.const 1)))

;; Js_string::log.fn/1 関数の定義
;; js_string.log を呼び出すためのラッパー関数です
;; externref 型の引数を1つ取り、i32 型の値を返します
(func $Js_string::log.fn/1 (param $*param/2 externref) (result i32)
  (ref.as_non_null (local.get $*param/2))
  (call $js_string.log)
)

;; $mizchi/mem/main.init_js_memory.fn/2 関数の定義
;; js のメモリを初期化するための関数です
;; 引数はなく、i32 型の値を返します
(func $$mizchi/mem/main.init_js_memory.fn/2 (result i32)
  (call " (import \\22js\\22 \\22mem\\22 (memory $mem 1))")
  (i32.const 0)
)

;; $*init*/3 関数の定義
;; モジュールの初期化を行う関数です
;; 引数はなく、返り値もありません
(func $*init*/3
  (local $s/1 (ref $moonbit.string))
  
  ;; js のメモリを初期化
  (call $$mizchi/mem/main.init_js_memory.fn/2)
  (drop)
  
  ;; "hello world" という文字列を moonbit.string 型に変換し、$s/1 に格納
  (call $moonbit.string_literal (i32.const 0) (i32.const 0) (i32.const 11))
  (local.tee $s/1)
  
  ;; $s/1 を js_string に変換し、js_string.log を呼び出して出力
  (call $moonbit.string_to_js_string)
  (call $Js_string::log.fn/1)
  (drop)
)

;; $*init*/3 関数を _start という名前でエクスポート
(export "_start" (func $*init*/3))
mizchimizchi

このコードの出力を確認してメモリ状態を見る

fn main {
  "hi".to_js_string().log()
}

moon build --target wasm-gc --output-wat

(data $moonbit.string_data "h\00i\00 ")
(func $js_string.log (import "js_string" "log") (param externref)
 (result i32))
(import "js_string" "new"
 (func $js_string.new (param i32) (param i32) (result externref)))
(memory $moonbit.memory (export "moonbit.memory") 1)
(func $Js_string::log.fn/1 (param $*param/1 externref) (result i32)
 (ref.as_non_null (local.get $*param/1)) (call $js_string.log))
(func $*init*/2 (i32.const 0) (i32.const 0) (i32.const 4)
 (memory.init $moonbit.string_data) (i32.const 0) (i32.const 2)
 (call $js_string.new) (call $Js_string::log.fn/1) (drop))
(export "_start" (func $*init*/2))
mizchimizchi

setTimeout binding

moonbit 側から js の setTimeout/setInterval のバインディングを作ってみる
Moonbit側から関数にInt で IDを振って、そのIDをJS側から呼ばせるようにした。

import { expect } from "jsr:@std/expect@0.223.0";
import { flush, js_string, setMemory, spectest } from "../.mooncakes/mizchi/js_io/mod.ts";

type Instance = {
  fire: (fid: number) => void;
};

const initJs = () => {
  let instance: Instance;
  return {
    set<I extends Instance>(i: I) {
      instance = i;
    },
    setInterval: (fid: number, ms: number) => setInterval(() => instance.fire(fid), ms),
    clearInterval,
    setTimeout: (fid: number, ms: number) => setTimeout(() => instance.fire(fid), ms),
    clearTimeout,
  }
}

const js = initJs();

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

const {
  run,
  fire,
  _start,
  ["moonbit.memory"]: memory,
} = exports as any;

_start();

setMemory(memory);
js.set({ fire });

run();
expect(1).toBe(1);
flush();

moonbit

fn js_set_timeout(fid : Int, timeout : Int) -> Int = "js" "setTimeout"

fn js_clear_timeout(tid : Int) -> Unit = "js" "clearTimeout"

fn js_set_interval(fid : Int, timeout : Int) -> Int = "js" "setInterval"

fn js_clear_interval(tid : Int) -> Unit = "js" "clearInterval"

// fn js_fetch(tid : Int) -> Unit = "js" "fetch"

type FnId Int derive(Eq)

pub fn FnId::hash(self : FnId) -> Int {
  self.0
}

type TimeoutId Int derive(Eq)

pub fn TimeoutId::new(i : Int) -> TimeoutId {
  TimeoutId(i)
}

pub fn TimeoutId::hash(self : TimeoutId) -> Int {
  self.0
}

type IntervalId Int derive(Eq)

pub fn IntervalId::new(i : Int) -> IntervalId {
  IntervalId(i)
}

pub fn IntervalId::hash(self : IntervalId) -> Int {
  self.0
}

let functions : @hashmap.HashMap[FnId, () -> Unit] = @hashmap.HashMap::[]

let timeout_ids : @hashmap.HashMap[TimeoutId, FnId] = @hashmap.HashMap::[]

let interval_ids : @hashmap.HashMap[IntervalId, FnId] = @hashmap.HashMap::[]

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

fn new_fid() -> FnId {
  let id = fid.val
  fid.val = fid.val + 1
  FnId(id)
}

pub fn fire(id : Int) -> Unit {
  // println("fire \(id)")
  match functions.get(FnId(id)) {
    Some(callback) => callback()
    None => println("function not found")
  }
}

pub fn set_timeout(cb : () -> Unit, ms : Int) -> TimeoutId {
  let fid = new_fid()
  println("set_timeout " + fid.0.to_string())
  functions.set(
    fid,
    fn() {
      cb()
      functions.remove(fid)
    },
  )
  let timeout_id = js_set_timeout(fid.0, ms)
  let tid = TimeoutId::new(timeout_id)
  timeout_ids.set(tid, fid)
  tid
}

pub fn clear_timeout(tid : TimeoutId) -> Unit {
  match timeout_ids.get(tid) {
    Some(fid) => {
      js_clear_timeout(tid.0)
      timeout_ids.remove(tid)
      functions.remove(fid)
      println("[mbt] timeout removed " + fid.0.to_string())
    }
    None => println("[mbt] timeout not found")
  }
}

pub fn set_interval(cb : () -> Unit, ms : Int) -> IntervalId {
  let fid = new_fid()
  // println("set_interval " + fid.0.to_string())
  functions.set(fid, cb)
  let id = js_set_interval(fid.0, ms)
  let iid = IntervalId::new(id)
  interval_ids.set(iid, fid)
  iid
}

pub fn clear_interval(id : IntervalId) -> Unit {
  match interval_ids.get(id) {
    Some(fid) => {
      js_clear_interval(id.0)
      interval_ids.remove(id)
      functions.remove(fid)
      println("[mbt] interval cleared" + fid.0.to_string())
    }
    None => println("[mbt] interval not found")
  }
}

pub fn run() -> Unit {
  let _t1 = set_timeout(
    fn() {
      println("[mbt] callback called")
      // xxx
    },
    100,
  )
  let t0 = set_timeout(
    fn() {
      println("[mbt] never called")
      // xxx
    },
    100,
  )
  clear_timeout(t0)
  let interval_id = set_interval(fn() { println("[mbt] interval called") }, 16)
  let _ = set_timeout(
    fn() {
      println("[mbt] clear interval")
      clear_interval(interval_id)
    },
    16 * 5,
  )

  // timout loop
  let mut cnt = 0
  let mut f : Option[() -> Unit] = None
  f = Some(
    fn() {
      let _ = set_timeout(
        fn() {
          cnt += 1
          // loop
          println("loop " + cnt.to_string())
          if cnt > 5 {
            f = None
            println("loop end")
            return ()
          }
          if f.is_empty().not() {
            let _ = set_timeout(f.unwrap(), 100)

          }
        },
        100,
      )

    },
  )
  f.unwrap()()
}

setTimeoutループも実装できた。評価順の関係で、一旦 Some に突っ込んでから自己参照するループになっていて、これは Rust で書いたときと同じ

これの Promise 版も作れないか?