以前、WebブラウザでCASL2を動かすCASL2 Playgoundを実装しました。
その時の日記に書いていたのですが、作り変えたい部分があったので作り変えました、という日記です。
変更前
アセンブラとして機械語を生成する処理と、Commet2上で動きをシミュレートするための関数と、両方をごちゃっと実装してしまっている感じがあって分かりづらい。
これは以前のブログ記事にも貼った画像。入力となるソースコードをアセンブルして機械語に変換、同時にCommet2上での動きをシミュレートする関数を生成する。
画面の「Step over」ボタンをクリックする度に1行分シミュレート関数を実行する。
アセンブル時に機械語だけでなく、シミュレート関数も生成してしまっているのがとても分かりづらい。
変更後
Commet2で動かす部分については「言語実装パターン」に書かれている パターン28 レジスタ方式バイトコードインタプリタ を参考しながら機械語を入力とするインタプリタとして作り直すと良さそう。
変更後はこうしました。入力となるソースコードをアセンブルして機械語に変換しメモリに保存するassemberと、メモリから機械語を読み取り処理を実行するinterpreterに分離。interpreterは以前のブログ記事にも書いた通り「言語実装パターン」という書籍のパターン28 レジスタ方式バイトコードインタプリタを参考に実装した。
メモリから2byte (CASL2では1語が2byte)ずつ読み取り、読み取った機械語で分岐する。
// 機械語の命令 const LD = 0x14 const ADDA = 0x24 ... export class Interpreter { PR: GeneralRegister memory: Memory ... constructor( PR: GeneralRegister, memory: Memory, ... ) { this.PR = PR this.memory = memory // アセンブルした結果が格納されてる ... } step(): boolean { const [opcode, operands] = this.readWord() advancePR(this.PR, 1) // 参照するメモリ1語分だけ進める switch (opcode) { case LD: this.ld(operands) break case ADDA: this.adda(operands) break ... } private readWord(): [number, number] { const word = this.memory.lookup(this.PR.lookup()) const upperByte = word >> 8 const lowerByte = word & 0b11111111 return [upperByte, lowerByte] } }
JavaScriptでバイト列を扱いたい時は、ArrayBufferで領域を確保、読み書きはTypedArrayかDataViewでArrayBufferをwrapしてからやる。CASL2では2byteのうち、先頭1byteが命令、それ以降がoperandという仕様であり、ビッグエンディアンで読み書きしたい。エンディアンを明示的に指定する場合はDataViewを使う。
元の実装と比べて実際の挙動に近くなったと思うし、コードも読みやすくなった!
ちなみに「言語実装パターン」というのはこの書籍です。
おしまい
書き換えたいなぁとモヤモヤが残っていたのですっきり!!