moonbit の js backend について調べる
この記事にある Moonbit JS Backend で遊んでみる。
json5 のデコードで25倍速い、というのは自分の見た感じだと、文字列をコンパイル時に char code に落として扱っているので、文字列比較を行わないから速い、という理解をしている。これはJSレベルでも頑張れば可能な範囲で、実際やったことがあるがかなり面倒臭い。
こういうのは言語が云々よりアルゴリズムに依存するので、moonbit が効率的なデータ構造を採用していて、それがベンチマーク対象に効く場合 JSより速いことがある、程度の理解でいい。
ベンチマークは置いといて、どういうコードを生成するか見てみる。
こういう構造。
├── jsbackend
│ ├── lib.mbt
│ └── moon.pkg.json
└── moon.mod.json
簡単なコードを書く
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 }
素朴
さすがに単純すぎるのでフィボナッチ
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 }
構造体を返してみる
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 に対応するコードが出た。
試しに terser に突っ込んだらこれぐらいは短くなった。
TypeScript の型ジェネレーターがほしいな...
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 }
TypeScript の型生成できないかな、と思って構文定義をちょっと調べた。
これは ocaml の ocmalyacc というパーサージェネレーターの構文らしい。
これを自分でセマンティクス込みでTSに落とすにはしんどい。 Feature Request するぐらいか。
ReScript にはTypeScriptの型を生成できたのか調べてみる。一応あった。要望出したら対応してくれるだろうか。
// 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)
}
そもそも 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 は引数を持てないので、綺麗には対応できない。
引数で 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 }
どこまでプレーンな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 そのままなのだろう。
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 }
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 に渡す型を生成する必要がありそう。
これは自分なら簡単なパターンを書けるので、あとで試作する。
結論
- moonbit は js コードを出力できる
- オブジェクトの対応関係は明示されてないけど、たぶん BuckleScript かどこかの対応に従ってそう
- JS と対応する構造体は以下
-
struct
=> JS Object -
Array[T]
=> JS Array - あとは String, Int, Bool, 等の各プリミティブ
- enum はなんか惜しい
- 例えば Rust の wasm bindgen は引数なしの enum だけ双方向で渡せる。こういう対応があるといいのだろうか
- https://rustwasm.github.io/wasm-bindgen/reference/types/imported-js-types.html
- 仮に複雑なインターフェースを持つとしても、型定義が生成されるなら実運用は問題ない気がする
-
- moonbit のReact バインディングを書くなら、wasm ビルドより JS ビルドのほうが楽そう
Moonbit への要望
- export された関数の TypeScript の
.d.ts
を出力してほしい- 自分はとくに素朴なJSの範囲に絞って使いたい
- 素朴といっても、ジェネリクスの対応もほしい
-
--target js
に限らず、 target 以外に*.js
や*.wasm
を吐き出すオプションがあると嬉しい-
moon build --target-dir xxx
はxxx/js/release/build/jsbackend/*
にファイルを出力するが、このパスが深くないのがほしい。 - rust の wasm-pack が
pkg/*
に出力するみたいなやつ。
-
おまけ: 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);
});