moonbit 言語仕様の確認
Module を見てみる
プロジェクトルートに 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 を見てみる。
{
"name": "username/hello",
"version": "0.1.0",
"readme": "README.md",
"repository": "",
"license": "Apache-2.0",
"keywords": [],
"description": ""
}
呼び出しは、@lib を付ける。
fn main {
println(@lib.hello())
}
これはどういう命名則でアクセスできてるんだ?
調査班はジャングルの奥地に向かった。
パッケージを書き換えてみる
username となってる部分を書き換える
{
"name": "mizchi/hello",
"version": "0.1.0",
"readme": "README.md",
"repository": "",
"license": "Apache-2.0",
"keywords": [],
"description": ""
}
{
"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 以下に、次のファイルを追加する。
pub fn fib(n : Int) -> Int {
match n {
0 => 0
1 => 1
_ => fib(n - 1) + fib(n - 2)
}
}
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)
}
{}
これを main から呼び出す。
{
"is_main": true,
"import": [
"username/hello/lib",
{
"path": "mizchi/hello/lib/fib",
"alias": "my_awesome_fibonacci"
}
]
}
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 で名前を付けられる。
テストの実行
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 モジュールを呼んでみる。
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
{
"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 は特別扱いされているわけではなく、慣習的なものっぽい。
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 の渡し方
この myns はブラウザ実行時しか呼べず、 moon run main
では呼べない。
ビルド時に分岐するのはどうする?
先の wasm ビルドは main モードとして実行したが、次はライブラリとして使う
lib
update 関数は引数で Ctx オブジェクトを受け取る。ctx は外で定義される。
type Ctx
fn set(self: Ctx, cid: Int) = "ctx" "set"
pub fn update(ctx: Ctx) -> Unit {
ctx.set(1)
}
{
"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 を実行しておく
from_array と from
let map = @map.Map::[]
let map: @map.Map[Int, Int] = @map.Map::from_arary([])
これらは一緒
wasm 内部ポインタ
snake のコードを読んでいると、面白い挙動を見つけた。wasm から return したオブジェクトはJSからは見えないが、内部的にはコンテキストをちゃんと持っている。内部的には extern_ref になっていそう。
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}
}
)
文字列を取り出す
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
}
{
"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" が取り出せる
同様に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!
メモリを直接触るために 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)))
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))
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))
このコードの出力を確認してメモリ状態を見る
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))
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 版も作れないか?