Open15

moonbit の js backend について調べる

mizchimizchi

この記事にある Moonbit JS Backend で遊んでみる。

https://www.moonbitlang.com/blog/js-support

json5 のデコードで25倍速い、というのは自分の見た感じだと、文字列をコンパイル時に char code に落として扱っているので、文字列比較を行わないから速い、という理解をしている。これはJSレベルでも頑張れば可能な範囲で、実際やったことがあるがかなり面倒臭い。

こういうのは言語が云々よりアルゴリズムに依存するので、moonbit が効率的なデータ構造を採用していて、それがベンチマーク対象に効く場合 JSより速いことがある、程度の理解でいい。

mizchimizchi

ベンチマークは置いといて、どういうコードを生成するか見てみる。

こういう構造。

├── jsbackend
│   ├── lib.mbt
│   └── moon.pkg.json
└── moon.mod.json

簡単なコードを書く

lib.mbt
pub fn run() -> Int {
  1
}

これを JS にコンパイルする moon.pkg.json

{
  "link": {
    "js": {
      "exports": ["run"]
    }
  }
}

実行

$ moon build --target js
$ tree target/js/release/build/jsbackend 
target/js/release/build/jsbackend
├── jsbackend.core
├── jsbackend.js
└── jsbackend.mi

jsbackend の中身

function mizchi$playground$jsbackend$$run() {
  return 1;
}
export { mizchi$playground$jsbackend$$run as run }

素朴

mizchimizchi

さすがに単純すぎるのでフィボナッチ

pub fn fib(n : Int) -> Int {
  if n <= 1 {
    return n
  }
  fib(n - 1) + fib(n - 2)
}
function mizchi$playground$jsbackend$$fib(n) {
  if (n <= 1) {
    return n;
  }
  return mizchi$playground$jsbackend$$fib(n - 1 | 0) + mizchi$playground$jsbackend$$fib(n - 2 | 0) | 0;
}
export { mizchi$playground$jsbackend$$fib as fib }
mizchimizchi

構造体を返してみる

pub struct Point {
  x : Int
  y : Int
}
pub fn run() -> Point {
  { x: 1, y: 2 }
}

出力

function mizchi$playground$jsbackend$$run() {
  return { x: 1, y: 2 };
}
export { mizchi$playground$jsbackend$$run as run }

おっ、JS構造体がそのまま出た。
試した感じ Array みたいなのはそのまま JS 側に出力された。

hashmap

少しいじめてみる。JS にない moonbitlang/core ライブラリを使う。

pub fn run() -> @hashmap.HashMap[Int, Int] {
  let map : @hashmap.HashMap[Int, Int] = @hashmap.HashMap::[]
  map.set(1, 2)
  map.set(3, 4)
  return map;
}
const $64$moonbitlang$47$core$47$hashmap$46$Entry$Empty$1$ = { $tag: 0 };
function $64$moonbitlang$47$core$47$hashmap$46$Entry$Valid$1$(param0, param1, param2, param3) {
  this._0 = param0;
  this._1 = param1;
  this._2 = param2;
  this._3 = param3;
}
$64$moonbitlang$47$core$47$hashmap$46$Entry$Valid$1$.prototype.$tag = 1;
const Option$None$3$ = { $tag: 0 };
function Option$Some$3$(param0) {
  this._0 = param0;
}
Option$Some$3$.prototype.$tag = 1;
function $raise(a) {
  throw new Error(a);
}
function $make_array_len_and_init(a, b) {
  const arr = new Array(a);
  for (let i = 0; i < a; i++) {
    arr[i] = b;
  }
  return arr;
}
const moonbitlang$core$hashmap$$default_init_capacity = 8;
function moonbitlang$core$hashmap$$calc_grow_threshold(capacity) {
  return (Math.imul(capacity, 13) | 0) / 16 | 0;
}
function moonbitlang$core$hashmap$$HashMap$new$0$(hasher) {
  return { entries: moonbitlang$core$array$$new$2$(moonbitlang$core$hashmap$$default_init_capacity, function () {
    return $64$moonbitlang$47$core$47$hashmap$46$Entry$Empty$1$;
  }), size: 0, capacity: moonbitlang$core$hashmap$$default_init_capacity, growAt: moonbitlang$core$hashmap$$calc_grow_threshold(moonbitlang$core$hashmap$$default_init_capacity), hasher: hasher };
}
function moonbitlang$core$hashmap$$HashMap$new$46$hasher$46$default$0$() {
  return Option$None$3$;
}
function moonbitlang$core$hashmap$$HashMap$index$0$(self, hash) {
  return moonbitlang$core$int$$Int$abs(hash) & (self.capacity - 1 | 0);
}
function moonbitlang$core$hashmap$$HashMap$next_index$0$(self, index) {
  return (index + 1 | 0) & (self.capacity - 1 | 0);
}
function moonbitlang$core$hashmap$$HashMap$make_hash$0$(self, key) {
  const _bind = self.hasher;
  let hash;
  if (_bind.$tag === 1) {
    const _Some = _bind;
    const _x = _Some._0;
    hash = _x(key);
  } else {
    hash = moonbitlang$core$int$$Int$hash(key);
  }
  return hash ^ hash >>> 16;
}
function moonbitlang$core$hashmap$$HashMap$set$0$(self, key, value) {
  if (self.capacity === 0 || self.size >= self.growAt) {
    moonbitlang$core$hashmap$$HashMap$grow$0$(self);
  }
  const hash = moonbitlang$core$hashmap$$HashMap$make_hash$0$(self, key);
  let _tmp$0 = 0;
  let _tmp$1 = moonbitlang$core$hashmap$$HashMap$index$0$(self, hash);
  let _tmp$2 = 0;
  let _tmp$3 = hash;
  let _tmp$4 = key;
  let _tmp$5 = value;
  while (true) {
    const _param = _tmp$0;
    const _param$2 = _tmp$1;
    const _param$3 = _tmp$2;
    const _param$4 = _tmp$3;
    const _param$5 = _tmp$4;
    const _param$6 = _tmp$5;
    if (_param === self.capacity) {
      $raise("HashMap is full");
    }
    const _bind = self.entries[_param$2];
    if (_bind.$tag === 0) {
      self.entries[_param$2] = new $64$moonbitlang$47$core$47$hashmap$46$Entry$Valid$1$(_param$3, _param$4, _param$5, _param$6);
      self.size = self.size + 1 | 0;
      break;
    } else {
      const _Valid = _bind;
      const _x = _Valid._0;
      const _x$2 = _Valid._1;
      const _x$3 = _Valid._2;
      const _x$4 = _Valid._3;
      if (_x$2 === _param$4 && _x$3 === _param$5) {
        self.entries[_param$2] = new $64$moonbitlang$47$core$47$hashmap$46$Entry$Valid$1$(_x, _x$2, _x$3, _param$6);
        break;
      }
      if (_param$3 > _x) {
        self.entries[_param$2] = new $64$moonbitlang$47$core$47$hashmap$46$Entry$Valid$1$(_param$3, _param$4, _param$5, _param$6);
        const _tmp$6 = _param + 1 | 0;
        const _tmp$7 = moonbitlang$core$hashmap$$HashMap$next_index$0$(self, _param$2);
        const _tmp$8 = _x + 1 | 0;
        _tmp$0 = _tmp$6;
        _tmp$1 = _tmp$7;
        _tmp$2 = _tmp$8;
        _tmp$3 = _x$2;
        _tmp$4 = _x$3;
        _tmp$5 = _x$4;
        continue;
      }
      const _tmp$9 = _param + 1 | 0;
      const _tmp$10 = moonbitlang$core$hashmap$$HashMap$next_index$0$(self, _param$2);
      const _tmp$11 = _param$3 + 1 | 0;
      _tmp$0 = _tmp$9;
      _tmp$1 = _tmp$10;
      _tmp$2 = _tmp$11;
      continue;
    }
  }
}
function moonbitlang$core$hashmap$$HashMap$grow$0$(self) {
  if (self.capacity === 0) {
    self.capacity = moonbitlang$core$hashmap$$default_init_capacity;
    self.growAt = moonbitlang$core$hashmap$$calc_grow_threshold(self.capacity);
    self.size = 0;
    self.entries = moonbitlang$core$array$$new$2$(self.capacity, function () {
      return $64$moonbitlang$47$core$47$hashmap$46$Entry$Empty$1$;
    });
    return;
  }
  const old_entries = self.entries;
  self.entries = moonbitlang$core$array$$new$2$(Math.imul(self.capacity, 2) | 0, function () {
    return $64$moonbitlang$47$core$47$hashmap$46$Entry$Empty$1$;
  });
  self.capacity = Math.imul(self.capacity, 2) | 0;
  self.growAt = moonbitlang$core$hashmap$$calc_grow_threshold(self.capacity);
  self.size = 0;
  let _tmp$12 = 0;
  while (true) {
    const i = _tmp$12;
    if (i < old_entries.length) {
      const _bind = old_entries[i];
      if (_bind.$tag === 1) {
        const _Valid = _bind;
        const _x = _Valid._2;
        const _x$2 = _Valid._3;
        moonbitlang$core$hashmap$$HashMap$set$0$(self, _x, _x$2);
      }
      _tmp$12 = i + 1 | 0;
      continue;
    } else {
      return;
    }
  }
}
function moonbitlang$core$hashmap$$HashMap$from_array$0$(arr) {
  const m = moonbitlang$core$hashmap$$HashMap$new$0$(moonbitlang$core$hashmap$$HashMap$new$46$hasher$46$default$0$());
  const _len = arr.length;
  let _tmp$13 = 0;
  while (true) {
    const _i = _tmp$13;
    if (_i < _len) {
      const _elem = arr[_i];
      moonbitlang$core$hashmap$$HashMap$set$0$(m, _elem._0, _elem._1);
      _tmp$13 = _i + 1 | 0;
      continue;
    } else {
      break;
    }
  }
  return m;
}
function moonbitlang$core$array$$new$2$(length, value) {
  if (length <= 0) {
    return [];
  } else {
    const array = $make_array_len_and_init(length, value());
    let _tmp$14 = 1;
    while (true) {
      const i = _tmp$14;
      if (i < length) {
        array[i] = value();
        _tmp$14 = i + 1 | 0;
        continue;
      } else {
        break;
      }
    }
    return array;
  }
}
function moonbitlang$core$int$$Int$abs(self) {
  return self < 0 ? -self : self;
}
function moonbitlang$core$int$$Int$hash(self) {
  return self;
}
function mizchi$playground$jsbackend$$run() {
  const map = moonbitlang$core$hashmap$$HashMap$from_array$0$([]);
  moonbitlang$core$hashmap$$HashMap$set$0$(map, 1, 2);
  moonbitlang$core$hashmap$$HashMap$set$0$(map, 3, 4);
  return map;
}
export { mizchi$playground$jsbackend$$run as run }

moonbit と 1:1 に対応するコードが出た。

mizchimizchi

JSON 型があわよくばそのまま JSON にならないかなと思って実行してみたけど、さすがに moonbit 側の json object 構造体が出るだけだった。

pub fn run() -> @json.JsonValue {
  let json = @json.JsonValue::Object(
    @map.Map::[
      (
        "key",
        @json.JsonValue::Array(
          @vec.Vec::[
            @json.JsonValue::Number(1.0),
            @json.JsonValue::Boolean(true),
            @json.JsonValue::Null,
            @json.JsonValue::Array(@vec.Vec::[]),
            @json.JsonValue::Object(
              @map.Map::[
                ("key", @json.JsonValue::String("value")),
                ("value", @json.JsonValue::Number(100.0)),
              ],
            ),
          ],
        ),
      ),
      ("null", @json.JsonValue::Null),
      ("bool", @json.JsonValue::Boolean(false)),
      ("obj", @json.JsonValue::Object(@map.Map::[])),
    ],
  )
  json
}
const $64$moonbitlang$47$core$47$map$46$Map$Empty$1$ = { $tag: 0 };
function $64$moonbitlang$47$core$47$map$46$Map$Tree$1$(param0, param1, param2, param3, param4) {
  this._0 = param0;
  this._1 = param1;
  this._2 = param2;
  this._3 = param3;
  this._4 = param4;
}
$64$moonbitlang$47$core$47$map$46$Map$Tree$1$.prototype.$tag = 1;
function $raise(a) {
  throw new Error(a);
}
function $compare_int(a, b) {
  if (a < b) return -1;
  if (a > b) return 1;
  return 0;
}
function $compare_char(a, b) {
  if (a < b) return -1;
  if (a > b) return 1;
  return 0;
}
const $64$moonbitlang$47$core$47$json$46$JsonValue$Null = { $tag: 0 };
function $64$moonbitlang$47$core$47$json$46$JsonValue$Boolean(param0) {
  this._0 = param0;
}
$64$moonbitlang$47$core$47$json$46$JsonValue$Boolean.prototype.$tag = 1;
function $64$moonbitlang$47$core$47$json$46$JsonValue$Number(param0) {
  this._0 = param0;
}
$64$moonbitlang$47$core$47$json$46$JsonValue$Number.prototype.$tag = 2;
function $64$moonbitlang$47$core$47$json$46$JsonValue$String(param0) {
  this._0 = param0;
}
$64$moonbitlang$47$core$47$json$46$JsonValue$String.prototype.$tag = 3;
function $64$moonbitlang$47$core$47$json$46$JsonValue$Array(param0) {
  this._0 = param0;
}
$64$moonbitlang$47$core$47$json$46$JsonValue$Array.prototype.$tag = 4;
function $64$moonbitlang$47$core$47$json$46$JsonValue$Object(param0) {
  this._0 = param0;
}
$64$moonbitlang$47$core$47$json$46$JsonValue$Object.prototype.$tag = 5;
function moonbitlang$core$map$$singleton$0$(key, value) {
  return new $64$moonbitlang$47$core$47$map$46$Map$Tree$1$(key, value, 1, $64$moonbitlang$47$core$47$map$46$Map$Empty$1$, $64$moonbitlang$47$core$47$map$46$Map$Empty$1$);
}
function moonbitlang$core$map$$Map$size$0$(self) {
  if (self.$tag === 0) {
    return 0;
  } else {
    const _Tree = self;
    const _x = _Tree._2;
    return _x;
  }
}
function moonbitlang$core$map$$new$0$(key, value, l, r) {
  const size = (moonbitlang$core$map$$Map$size$0$(l) + moonbitlang$core$map$$Map$size$0$(r) | 0) + 1 | 0;
  return new $64$moonbitlang$47$core$47$map$46$Map$Tree$1$(key, value, size, l, r);
}
const moonbitlang$core$map$$ratio = 5;
function moonbitlang$core$map$$balance$0$(key, value, l, r) {
  const ln = moonbitlang$core$map$$Map$size$0$(l);
  const rn = moonbitlang$core$map$$Map$size$0$(r);
  if ((ln + rn | 0) < 2) {
    return moonbitlang$core$map$$new$0$(key, value, l, r);
  } else {
    if (rn > (Math.imul(moonbitlang$core$map$$ratio, ln) | 0)) {
      let _bind$2;
      if (r.$tag === 1) {
        const _Tree = r;
        const _x = _Tree._3;
        const _x$2 = _Tree._4;
        _bind$2 = { _0: _x, _1: _x$2 };
      } else {
        _bind$2 = $raise("unreachable");
      }
      const _x$3 = _bind$2._0;
      const _x$4 = _bind$2._1;
      const rln = moonbitlang$core$map$$Map$size$0$(_x$3);
      const rrn = moonbitlang$core$map$$Map$size$0$(_x$4);
      if (rln < rrn) {
        if (r.$tag === 1) {
          const _Tree$2 = r;
          const _x$5 = _Tree$2._0;
          const _x$6 = _Tree$2._1;
          const _x$7 = _Tree$2._3;
          const _x$8 = _Tree$2._4;
          return moonbitlang$core$map$$new$0$(_x$5, _x$6, moonbitlang$core$map$$new$0$(key, value, l, _x$7), _x$8);
        } else {
          return $raise("single_l error");
        }
      } else {
        _J$_arm: {
          if (r.$tag === 1) {
            const _Tree$3 = r;
            const _x$9 = _Tree$3._0;
            const _x$10 = _Tree$3._1;
            const _x$11 = _Tree$3._3;
            if (_x$11.$tag === 1) {
              const _Tree$4 = _x$11;
              const _x$12 = _Tree$4._0;
              const _x$13 = _Tree$4._1;
              const _x$14 = _Tree$4._3;
              const _x$15 = _Tree$4._4;
              const _x$16 = _Tree$3._4;
              return moonbitlang$core$map$$new$0$(_x$12, _x$13, moonbitlang$core$map$$new$0$(key, value, l, _x$14), moonbitlang$core$map$$new$0$(_x$9, _x$10, _x$15, _x$16));
            } else {
              break _J$_arm;
            }
          } else {
            break _J$_arm;
          }
        }
        return $raise("double_l error");
      }
    } else {
      if (ln > (Math.imul(moonbitlang$core$map$$ratio, rn) | 0)) {
        let _bind;
        if (l.$tag === 1) {
          const _Tree$5 = l;
          const _x$17 = _Tree$5._3;
          const _x$18 = _Tree$5._4;
          _bind = { _0: _x$17, _1: _x$18 };
        } else {
          _bind = $raise("unreachable");
        }
        const _x$19 = _bind._0;
        const _x$20 = _bind._1;
        const lln = moonbitlang$core$map$$Map$size$0$(_x$19);
        const lrn = moonbitlang$core$map$$Map$size$0$(_x$20);
        if (lrn < lln) {
          if (l.$tag === 1) {
            const _Tree$6 = l;
            const _x$21 = _Tree$6._0;
            const _x$22 = _Tree$6._1;
            const _x$23 = _Tree$6._3;
            const _x$24 = _Tree$6._4;
            return moonbitlang$core$map$$new$0$(_x$21, _x$22, _x$23, moonbitlang$core$map$$new$0$(key, value, _x$24, r));
          } else {
            return $raise("single_r error");
          }
        } else {
          _J$_arm$2: {
            if (l.$tag === 1) {
              const _Tree$7 = l;
              const _x$25 = _Tree$7._0;
              const _x$26 = _Tree$7._1;
              const _x$27 = _Tree$7._3;
              const _x$28 = _Tree$7._4;
              if (_x$28.$tag === 1) {
                const _Tree$8 = _x$28;
                const _x$29 = _Tree$8._0;
                const _x$30 = _Tree$8._1;
                const _x$31 = _Tree$8._3;
                const _x$32 = _Tree$8._4;
                return moonbitlang$core$map$$new$0$(_x$29, _x$30, moonbitlang$core$map$$new$0$(_x$25, _x$26, _x$27, _x$31), moonbitlang$core$map$$new$0$(key, value, _x$32, r));
              } else {
                break _J$_arm$2;
              }
            } else {
              break _J$_arm$2;
            }
          }
          return $raise("double_r error");
        }
      } else {
        return moonbitlang$core$map$$new$0$(key, value, l, r);
      }
    }
  }
}
function moonbitlang$core$map$$Map$insert$0$(self, key, value) {
  if (self.$tag === 0) {
    return moonbitlang$core$map$$singleton$0$(key, value);
  } else {
    const _Tree = self;
    const _x = _Tree._0;
    const _x$2 = _Tree._1;
    const _x$3 = _Tree._3;
    const _x$4 = _Tree._4;
    const _bind = moonbitlang$core$string$$String$compare(key, _x);
    switch (_bind) {
      case -1: {
        return moonbitlang$core$map$$balance$0$(_x, _x$2, moonbitlang$core$map$$Map$insert$0$(_x$3, key, value), _x$4);
      }
      case 1: {
        return moonbitlang$core$map$$balance$0$(_x, _x$2, _x$3, moonbitlang$core$map$$Map$insert$0$(_x$4, key, value));
      }
      default: {
        return moonbitlang$core$map$$new$0$(_x, value, _x$3, _x$4);
      }
    }
  }
}
function moonbitlang$core$map$$Map$from_array$0$(array) {
  let _tmp$0 = 0;
  let _tmp$1 = $64$moonbitlang$47$core$47$map$46$Map$Empty$1$;
  while (true) {
    const i = _tmp$0;
    const mp = _tmp$1;
    if (i < array.length) {
      const _bind = array[i];
      const _x = _bind._0;
      const _x$2 = _bind._1;
      const _tmp$2 = i + 1 | 0;
      const _tmp$3 = moonbitlang$core$map$$Map$insert$0$(mp, _x, _x$2);
      _tmp$0 = _tmp$2;
      _tmp$1 = _tmp$3;
      continue;
    } else {
      return mp;
    }
  }
}
function moonbitlang$core$vec$$Vec$from_array$2$(arr) {
  const len = arr.length;
  const buf = new Array(len);
  let _tmp$4 = 0;
  while (true) {
    const i = _tmp$4;
    if (i < len) {
      buf[i] = arr[i];
      _tmp$4 = i + 1 | 0;
      continue;
    } else {
      break;
    }
  }
  return { buf: buf, len: len };
}
function moonbitlang$core$string$$String$compare(self, other) {
  const len = self.length;
  const _bind = $compare_int(len, other.length);
  if (_bind === 0) {
    let _tmp$5 = 0;
    while (true) {
      const i = _tmp$5;
      if (i < len) {
        const order = $compare_char(self.charCodeAt(i), other.charCodeAt(i));
        if (order !== 0) {
          return order;
        }
        _tmp$5 = i + 1 | 0;
        continue;
      } else {
        break;
      }
    }
    return 0;
  } else {
    return _bind;
  }
}
function mizchi$playground$jsbackend$$run() {
  const json = new $64$moonbitlang$47$core$47$json$46$JsonValue$Object(moonbitlang$core$map$$Map$from_array$0$([{ _0: "key", _1: new $64$moonbitlang$47$core$47$json$46$JsonValue$Array(moonbitlang$core$vec$$Vec$from_array$2$([new $64$moonbitlang$47$core$47$json$46$JsonValue$Number(1), new $64$moonbitlang$47$core$47$json$46$JsonValue$Boolean(true), $64$moonbitlang$47$core$47$json$46$JsonValue$Null, new $64$moonbitlang$47$core$47$json$46$JsonValue$Array(moonbitlang$core$vec$$Vec$from_array$2$([])), new $64$moonbitlang$47$core$47$json$46$JsonValue$Object(moonbitlang$core$map$$Map$from_array$0$([{ _0: "key", _1: new $64$moonbitlang$47$core$47$json$46$JsonValue$String("value") }, { _0: "value", _1: new $64$moonbitlang$47$core$47$json$46$JsonValue$Number(100) }]))])) }, { _0: "null", _1: $64$moonbitlang$47$core$47$json$46$JsonValue$Null }, { _0: "bool", _1: new $64$moonbitlang$47$core$47$json$46$JsonValue$Boolean(false) }, { _0: "obj", _1: new $64$moonbitlang$47$core$47$json$46$JsonValue$Object(moonbitlang$core$map$$Map$from_array$0$([])) }]));
  return json;
}
export { mizchi$playground$jsbackend$$run as run }
mizchimizchi

TypeScript の型生成できないかな、と思って構文定義をちょっと調べた。

https://github.com/moonbitlang/moonbit-docs/blob/main/parser.mly

これは ocaml の ocmalyacc というパーサージェネレーターの構文らしい。

https://ocaml.jp/archive/ocaml-manual-3.06-ja/manual026.html

これを自分でセマンティクス込みでTSに落とすにはしんどい。 Feature Request するぐらいか。

ReScript にはTypeScriptの型を生成できたのか調べてみる。一応あった。要望出したら対応してくれるだろうか。

https://rescript-lang.org/docs/manual/latest/typescript-integration

// src/Color.res

@genType
type color =
  | Red
  | Blue

@genType
let printColorMessage = (~color, ~message) => {
  let prefix = switch color {
  | Red => "\x1b[91m"
  | Blue => "\x1b[94m"
  }
  let reset = "\x1b[0m"

  Console.log(prefix ++ message ++ reset)
}
mizchimizchi

そもそも TypeScript と Moonbit のセマンティクスが対応可能なのか enum の視点で見てみる。(struct, array は問題なさそう)

pub enum Value {
  Int(Int)
  Bool(Bool)
}

pub fn run() -> Value {
  Value::Int(42)
}
function Value$Int(param0) {
  this._0 = param0;
}
Value$Int.prototype.$tag = 0;
function Value$Bool(param0) {
  this._0 = param0;
}
Value$Bool.prototype.$tag = 1;
function mizchi$playground$jsbackend$$run() {
  return new Value$Int(42);
}
export { mizchi$playground$jsbackend$$run as run }

TypeScript 的に解釈するならこうか

type Value = {
  $tag: 0,
  _0: number,
} | {
  $tag: 1,
  _0: boolean
}

export declare function run(): Value;

moonbit enum の出力をそのままJS側で使うには、もう一つラップする必要がありそう。そもそも TypeScript の enum は引数を持てないので、綺麗には対応できない。

mizchimizchi

引数で struct を取る場合の扱いを確認してみる。

pub struct Input {
  a : Int
  b : Int
}

pub struct Output {
  x : Int
  y : Int
}

pub fn run(input : Input) -> Output {
  { x: input.a + input.b, y: input.a - input.b }
}

特に問題なさそう。

function mizchi$playground$jsbackend$$run(input) {
  return { x: input.a + input.b | 0, y: input.a - input.b | 0 };
}
export { mizchi$playground$jsbackend$$run as run }
mizchimizchi

どこまでプレーンなJSオブジェクトに対応するかを確認

pub struct Sub {
  x : Int
  y : Int
}

pub struct Data {
  a : Int
  b : Bool
  xs : Array[Int]
  vec : @vec.Vec[Int]
  sub : Sub
}

pub fn run(input : Data) -> Data {
  input.vec.push(42)
  {
    a: input.a,
    b: input.b,
    xs: input.xs.map(fn(x) { x + 1 }),
    sub: Sub::{ x: input.a, y: 2 },
    vec: input.vec,
  }
}
function $make_array_len_and_init(a, b) {
  const arr = new Array(a);
  for (let i = 0; i < a; i++) {
    arr[i] = b;
  }
  return arr;
}
function moonbitlang$core$array$$Array$map$0$(self, f) {
  if (self.length === 0) {
    return [];
  }
  const res = $make_array_len_and_init(self.length, f(self[0]));
  let _tmp$0 = 1;
  while (true) {
    const i = _tmp$0;
    if (i < self.length) {
      res[i] = f(self[i]);
      _tmp$0 = i + 1 | 0;
      continue;
    } else {
      break;
    }
  }
  return res;
}
function moonbitlang$core$vec$$Vec$realloc$1$(self) {
  const old_cap = self.len;
  const new_cap = old_cap === 0 ? 8 : Math.imul(old_cap, 2) | 0;
  const new_buf = new Array(new_cap);
  let _tmp$1 = 0;
  while (true) {
    const i = _tmp$1;
    if (i < old_cap) {
      new_buf[i] = self.buf[i];
      _tmp$1 = i + 1 | 0;
      continue;
    } else {
      break;
    }
  }
  self.buf = new_buf;
}
function moonbitlang$core$vec$$Vec$push$1$(self, value) {
  if (self.len === self.buf.length) {
    moonbitlang$core$vec$$Vec$realloc$1$(self);
  }
  self.buf[self.len] = value;
  self.len = self.len + 1 | 0;
}
function mizchi$playground$jsbackend$$run(input) {
  moonbitlang$core$vec$$Vec$push$1$(input.vec, 42);
  return { a: input.a, b: input.b, xs: moonbitlang$core$array$$Array$map$0$(input.xs, function (x) {
    return x + 1 | 0;
  }), vec: input.vec, sub: { x: input.a, y: 2 } };
}
export { mizchi$playground$jsbackend$$run as run }

素朴に対応しているのは struct と、array と各プリミティブ。

vec は moonbit 用の構造体だが、 Arary[Int] は JS の Array にそのまま対応してそうだ。

ここまで moonbit の出力を確かめたが、たぶんこのへんは js_of_ocaml や buclescript の binding そのままなのだろう。

mizchimizchi

test {} がどうなるかの確認

pub struct Sub {
  x : Int
  y : Int
} derive(Debug, Show)

pub struct Data {
  a : Int
  b : Bool
  sub : Sub
} derive(Debug, Show)

pub fn run(input : Data) -> Data {
  { a: input.a, b: input.b, sub: Sub::{ x: input.a, y: 2 } }
}

test {
  inspect(
    run(Data::{ a: 1, b: true, sub: Sub::{ x: 0, y: 0 } }),
    content="{a: 1, b: true, sub: {x: 1, y: 2}}",
  )?
}

これ自体は $ moon test が通る。

出力からは単に無視される。

function mizchi$playground$jsbackend$$run(input) {
  return { a: input.a, b: input.b, sub: { x: input.a, y: 2 } };
}
export { mizchi$playground$jsbackend$$run as run }
mizchimizchi

FFI

--target wasm は初期化時にimportObject で外側の関数を import できた。同じような記述をするとどうなるか。

fn outer() -> Int = "js" "func"
pub fn run() -> Int {
  outer()
}

出力

function mizchi$playground$jsbackend$$outer() {
  return js.func();
}
function mizchi$playground$jsbackend$$run() {
  return mizchi$playground$jsbackend$$outer();
}
export { mizchi$playground$jsbackend$$run as run }

(予想通りだが)グローバル参照になる。

仮に、 TypeScript の関数を受け渡そうとすると、 TypeScript の型定義から Moonbit に渡す型を生成する必要がありそう。

これは自分なら簡単なパターンを書けるので、あとで試作する。

mizchimizchi

結論

  • moonbit は js コードを出力できる
  • オブジェクトの対応関係は明示されてないけど、たぶん BuckleScript かどこかの対応に従ってそう
  • JS と対応する構造体は以下
    • struct => JS Object
    • Array[T] => JS Array
    • あとは String, Int, Bool, 等の各プリミティブ
    • enum はなんか惜しい
  • moonbit のReact バインディングを書くなら、wasm ビルドより JS ビルドのほうが楽そう

Moonbit への要望

  • export された関数の TypeScript の .d.ts を出力してほしい
    • 自分はとくに素朴なJSの範囲に絞って使いたい
    • 素朴といっても、ジェネリクスの対応もほしい
  • --target js に限らず、 target 以外に *.js*.wasm を吐き出すオプションがあると嬉しい
    • moon build --target-dir xxxxxx/js/release/build/jsbackend/* にファイルを出力するが、このパスが深くないのがほしい。
    • rust の wasm-pack が pkg/* に出力するみたいなやつ。
mizchimizchi

おまけ: TypeScript の dts から Moonbit の型を生成してみた

真面目にやったわけではない。メジャーなパターンに絞って、こうなるよなーという程度。

import ts from "npm:typescript@5.4.2";
import { expect } from "jsr:@std/expect@0.224.0";

export function dtsToMoonbitTypes(code: string) {
  const source = ts.createSourceFile("types.d.ts", code, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TSX)
  let moonbitTypes = '';
  const innerTypes: ts.Node[] = []
  ts.forEachChild(source, (node) => {
    if (ts.isTypeAliasDeclaration(node)) {
      const typeName = node.name.getText();
      const typeArgs = node.typeParameters?.map(t => t.getText());
      const nameWithArgs = typeArgs ? `${typeName}[${typeArgs.join(', ')}]` : typeName;
      moonbitTypes += `pub struct ${nameWithArgs} {\n`;
      const prefix = '  '.repeat(1);
      ts.forEachChild(node.type, (child) => {
        if (ts.isPropertySignature(child)) {
          moonbitTypes += `${prefix}${child.name.getText()}: ${tsToMoonbitType(child.type!, innerTypes)}\n`
        }
      });
      moonbitTypes += `${'  '.repeat((0))}}\n\n`;
    } else if (ts.isInterfaceDeclaration(node)) {
      const typeName = node.name.getText();
      const typeArgs = node.typeParameters?.map(t => t.getText());
      const nameWithArgs = typeArgs ? `${typeName}[${typeArgs.join(', ')}]` : typeName;

      moonbitTypes += `pub struct ${nameWithArgs} {\n`;
      const prefix = '  '.repeat(1);
      ts.forEachChild(node, (child) => {
        if (ts.isPropertySignature(child)) {
          moonbitTypes += `${prefix}${child.name.getText()}: ${tsToMoonbitType(child.type!, innerTypes)}\n`
        }
      });
      moonbitTypes += `${'  '.repeat((0))}}\n\n`;
    } else if (ts.isFunctionDeclaration(node)) {
      const params = node.parameters.map(p => `${p.name.getText()}: ${tsToMoonbitType(p.type!, innerTypes)}`);
      const typeArgs = node.typeParameters?.map(t => t.getText());
      const nameWithArgs = typeArgs ? `${node.name!.getText()}[${typeArgs.join(', ')}]` : node.name!.getText();
      const returnType = tsToMoonbitType(node.type!, innerTypes);
      const boundName = tsToMoonbitType(node.name!, innerTypes);
      moonbitTypes += `pub fn ${nameWithArgs}(${params.join(', ')}) -> ${returnType} = "${boundName}"\n`
    } else {
      if (node.kind === ts.SyntaxKind.EndOfFileToken) {
        return;
      }
      throw new Error(`Unsupported node type: ${ts.SyntaxKind[node.kind]}`)
    }
  });

  for (const innerType of innerTypes) {
    let innerCode = `pub struct _${innerTypes.indexOf(innerType)} {\n`;
    const prefix = '  '.repeat(1);
    ts.forEachChild(innerType, (child) => {
      if (ts.isPropertySignature(child)) {
        innerCode += `${prefix}${child.name.getText()}: ${tsToMoonbitType(child.type!, innerTypes)}\n`
      }
    });
    innerCode += `}\n\n`;
    moonbitTypes += innerCode;
  }
  return moonbitTypes;
}


function tsToMoonbitType(node: ts.Node, innerTypes: Array<ts.Node>): string {
  if (node.kind === ts.SyntaxKind.NumberKeyword) {
    return 'Int'
  }
  if (ts.isIdentifier(node)) {
    return node.getText();
  }

  if (ts.isFunctionTypeNode(node)) {
    const args = node.parameters.map(p => tsToMoonbitType(p.type!, innerTypes));
    const returnType = tsToMoonbitType(node.type!, innerTypes);
    return `(${args.join(', ')}) -> ${returnType}`;
  }
  if (ts.isArrayTypeNode(node)) {
    return `Array[${tsToMoonbitType(node.elementType, innerTypes)}]`;
  }
  if (ts.isTypeReferenceNode(node)) {
    const typeName = node.typeName.getText()
    const args = node.typeArguments?.map(t => tsToMoonbitType(t, innerTypes))
    if (args) {
      return `${typeName}[${args.join(', ')}]`;
    }
    return typeName;
  } else if (ts.isTypeLiteralNode(node)) {
    const innerId = innerTypes.length;
    innerTypes.push(node);
    return `_${innerId}`
  } else {

    const typeName = node.getText();
    if (typeName === 'void') {
      return 'Unit';
    }
    if (typeName === 'null') {
      return 'Unit';
    }
    if (typeName === 'undefined') {
      return 'Unit';
    }
    if (typeName === 'number') {
      return 'Int';
    }
    if (typeName === 'boolean') {
      return 'Bool';
    }
    if (typeName === 'string') {
      return 'String';
    }
    return typeName;
  }
}


Deno.test("type to struct", () => {
  const code = `
  export type X = {
    a: number;
    b: string;
    c: boolean;
    d: Array<number>
    e: string[]
  }
  `;
  const expected = `
pub struct X {
  a: Int
  b: String
  c: Bool
  d: Array[Int]
  e: Array[String]
}`;
  expect(dtsToMoonbitTypes(code).trim()).toBe(expected.trim());
});

Deno.test("type to struct with generics", () => {
  const code = `
  export type X<T> = {
    val: T
  }
  `;
  const expected = `
pub struct X[T] {
  val: T
}`;
  expect(dtsToMoonbitTypes(code).trim()).toBe(expected.trim());
});

Deno.test("type with function type", () => {
  const code = `
  export type X = {
    fun: (a: number) => string
  }
  `;
  const expected = `
pub struct X {
  fun: (Int) -> String
}`;
  expect(dtsToMoonbitTypes(code).trim()).toBe(expected.trim());
});


Deno.test("interface", () => {
  const code = `
  export interface X {
    a: number;
    b: string;
    c: boolean;
    d: Array<number>;
    e: null;
    f: undefined;
    g: void;
  }
  `;
  const expected = `
pub struct X {
  a: Int
  b: String
  c: Bool
  d: Array[Int]
  e: Unit
  f: Unit
  g: Unit
}`;
  expect(dtsToMoonbitTypes(code).trim()).toBe(expected.trim());
});

Deno.test("with type literal", () => {
  const code = `
  export type X = {
    nested: {
      a: number;
      b: string;
    }
  }
  `;
  const expected = `pub struct X {
  nested: _0
}

pub struct _0 {
  a: Int
  b: String
}
`;
  expect(dtsToMoonbitTypes(code).trim()).toBe(expected.trim());
})

Deno.test("func", () => {
  const code = `
export declare function foo(code: string): number;
export declare function none(): void;
`;
  const expected = `
pub fn foo(code: String) -> Int = "foo"
pub fn none() -> Unit = "none"
`.trim();
  expect(dtsToMoonbitTypes(code).trim()).toBe(expected);
});

Deno.test("func with type literal", () => {
  const code = `
export declare function foo(opts: {x: number}): number;
export declare function bar<T>(v: T): void;
`;
  const expected = `
pub fn foo(opts: _0) -> Int = "foo"
pub fn bar[T](v: T) -> Unit = "bar"
pub struct _0 {
  x: Int
}
`.trim();
  expect(dtsToMoonbitTypes(code).trim()).toBe(expected);
});