SVX日記

2004|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|04|05|

2023-12-01(Fri) WebAssemblyのstackで開発がstuck

 WebAssemblyのひとつだけの使い道」ということで、ボチボチと開発を進めていたのだが、どうにも不可解な動きがあって、理解するまでにだいぶかかってしまった。

  まずは、以下のサンプルコードだ。Wasmの関数に引数として0を渡すと20、非0を渡すと10がログに出力される。

main = ->
    importObjects = {
        console:    { log: (arg) => console.log(arg) },
    }
    obj = await WebAssembly.instantiateStreaming(fetch('test.wasm'), importObjects)
    console.log('call test(0)')
    obj.instance.exports.test(0)
    console.log('call test(1)')
    obj.instance.exports.test(1)
main()
(module
    (import "console" "log" (func $log (param i32)))
    (func (export "test") (param $val i32)
        push        val
        if
            i32.push    10
            call        log
        else
            i32.push    20
            call        log
        end
    )
)

  したがって、結果はこうなる。

call test(0)
20
call test(1)
10

  ところが、以下のようにlog出力をifブロックの外に出すとアセンブルエラーが起きてしまう。

        push        val
        if
            i32.push    10
        else
            i32.push    20
        end
        call        log
test.wat:10:4: error: type mismatch in if true branch, expected [] but got [i32]
            i32.const       10
            ^^^^^^^^^
test.wat:13:3: error: type mismatch in if false branch, expected [] but got [i32]
        end
        ^^^
test.wat:14:3: error: type mismatch in call, expected [i32] but got []
        call        $log
        ^^^^

  これは、以下のようにすると解決する。「ifブロックを出る時にi32を返しますよ」という宣言を加えるわけだ。

        push        val
        if  (result i32)
            i32.push    10
        else
            i32.push    20
        end
        call        log

  そんなら「ifブロックを出る時にi32を『ふたつ』返しますよ」という宣言をしたらどうなるかというと、アセンブルエラーが起きてしまう。

        push        val
        if  (result i32) (result i32)
            i32.push    10
            i32.push    10
        else
            i32.push    20
            i32.push    20
        end
        call        log
        call        log
test.wat:9:3: error: multiple if results not currently supported.
        if  (result i32) (result i32)
        ^^

  「not currently supported.」なので、将来的には可能になるのかもしれないが、現状では認められない、というように読める。

  じゃ、こういうコードはどうか?

        i32.push    100
        push        val
        if  (result i32)
            i32.push    10
            i32.add
        else
            i32.push    20
            i32.add
        end
        call        log

  あらかじめ、スタックに100を積んでおいて、引数として0を渡すと20、非0を渡すと10を加算した値をログに出力させたい……のだが、アセンブルエラーが起きてしまう。

test.wat:12:4: error: type mismatch in i32.add, expected [i32, i32] but got [i32]
            i32.add
            ^^^^^^^
test.wat:15:4: error: type mismatch in i32.add, expected [i32, i32] but got [i32]
            i32.add
            ^^^^^^^
test.wat:17:3: error: type mismatch in function, expected [] but got [i32]
        call        $log
        ^^^^

  こうなることがどうにも理解できなくて、長らくグダグダしていた。MDNのifの項を読んでも、特段なにも触れられていない。が、これは「ifは関数コール」のようなものだ、と理解するべきだという結論にたどり着いた。

  ifブロックに入ったら「スタックの内容は持ち込めない」し「スタックの内容は持ち出せない(ただし返値としてひとつだけは許容される)」ということで、これは関数コールの特性そのものである。これまでのアセンブラ知識が邪魔になって必要以上に理解するのに時間がかかってしまった。

  上記のプログラムは、中で加算せず、加算対象を返値として、外で加算する形とすれば意図した結果が得られる。

        i32.push    100
        push        val
        if  (result i32)
            i32.push    10
        else
            i32.push    20
        end
        i32.add
        call        log

  「スタックの内容は持ち込めない」のはblockも同じだ。上記を書き直すと以下のようになる。

        i32.push    100
blk1:   block   (result i32)
            i32.push    10
            push        val
            br_if       blk1
            drop
            i32.push    20
        end
        i32.add
        call        log

  以下は、引数が0だった場合に、100をログに出力させるプログラムだが、非0だった場合はスタックに100が残ることになる。が、それは許容されるらしい。関数コールと考えれば、それが捨てられるだろうことは、まぁ理解できなくもないのだが。

blk1:   block
            i32.push    100         ;; [ 100
            push        val         ;; [ 100 val
            br_if       blk1        ;; [ 100
            call        log         ;; [
        end
;;                                  ;; [ ???

  では、返値を設定したらどうなるか。様々に試したところ、以下のコードから挙動を掴むことができた。

        i32.push    99              ;; [ 99
blk1:   block   (result i32)
            i32.push    100         ;; [ 99 100
            i32.push    12          ;; [ 99 100 12
            i32.push    11          ;; [ 99 100 12 11
            i32.push    10          ;; [ 99 100 12 11 10
            push        val         ;; [ 99 100 12 11 10 val
            br_if       blk1        ;; [ 99 100 12 11 10
            call        log         ;; [ 99 100 12 11
            drop                    ;; [ 99 100 12
            drop                    ;; [ 99 100
        end
        call        log             ;; [ ???
        call        log             ;; [ ???

  結果はこうなる。

call test(0)
10
100
99
call test(1)
10
99

  引数が0だった場合、brをスルーし、中のlogで10を出力した後、11, 12をdropし、外のlogで100と99を出力して終了する。一方で、引数が非0だった場合、スタックが「99 100 12 11 10」の状態でbrでblockを抜けるが、その際、頭の10が返値となり、blockに入る時の状態である「99」の上に10を積んだ「99 10」という状態でblockを抜けるらしい。

  というわけで、挙動については掴むことができたが、むしろ面倒くさい仕様に思えてならない。さて、だいぶ寄り道してしまったが、本来の目的に戻ろう。