最近WASMに興味がでてきて、ゴールデンウィークにWASMランタイムをScala 3で実装している。もう結構な命令を実行できるようになって、バイナリをパースして足し算を実行するくらいならできるようになった。
WASMは機械語なので、オペコードと引数が命令として並んでいる形をとる。自分のコードではオペコードはenum
を利用して実装している:
enum OpCode(val code: Byte): case End extends OpCode(0x0b) case Call extends OpCode(0x10) case LocalGet extends OpCode(0x20) case LocalSet extends OpCode(0x21) ...
こうすると簡単にオペコードからそのバイトが得られる:
OpCode.LocalGet.code // => 0x20
そうそう、Scalaは0x20
みたいなhexadecimal literalが書けるので便利ですね。
他方、バイトからオペコードを得るのは面倒だ。何も考えずに素朴に実装すると、全部のオペコードに対してマッピングを考えなくてはならない:
object OpCode: def fromByte(byte: Byte): OpCode = byte match case End.code => End case Call.code => Call case LocalGet.code => LocalGet case LocalSet.code => LocalSet ...
オエー
Discordで質問したところ、便利な方法を教えてもらった。
enum OpCode(val code: Byte): case End extends OpCode(0x0b) case Call extends OpCode(0x10) case LocalGet extends OpCode(0x20) case LocalSet extends OpCode(0x21) end OpCode object OpCode: private val opMap: Map[Byte, OpCode] = OpCode.values.view.map(op => op.code -> op).toMap def ofCode(code: Byte): Option[OpCode] = opMap.get(key = code) end OpCode OpCode.ofCode(0x20) // => Some(LocalGet)
コツは、enum
のコンパニオンオブジェクトとして内部的にマッピングを生成しておくことだ。opMap
はofCode
が呼ばれた瞬間に初期化されて計算が行なわれる(オブジェクトは必要になったときにインスタンスになるため)。たぶんマクロを頑張ればコンパイル時に確定させることもできると思う。
加えて、便利そうなライブラリも教えてもらった。