Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9299

Node.js でつくる Node.js-WASM コンパイラー - 02:四則演算を実装する

$
0
0

はじめに

Node.jsで小さなプログラミング言語を作ってみるシリーズを、「ミニインタープリター」「ミニコンパイラー」とやってきました。そして三部作(?)の最後として、 ミニNode.jsからWASMを生成する小さなコンパイラーに取り組んでいます。今回は四則演算と余り(+, -, *, /, %)を実装してみましょう。

※以前の記事「Node.js でつくる WASM トランスパイラー - 02:四則演算を実装する」のアップデート版になります

これまでの取り組み

足し算の実現

対象ソース

Node.jsの対象となるコードはこちらです。単純な整数の足し算です。

add.js
1+2;

これをミニインタープリター、ミニコンパイラーで使ったモジュールの拡張版(module_parser_15.js)でパース/単純化したASTを作ります。結果はこちらのようになります。

[ '+', [ 'lit', 1 ], [ 'lit', 2 ] ]

足し算を表現するWASMは次のようになります。32ビット整数の足し算は i32.add を使います。

(module
  (export "exported_main" (func $main))
  (func $main (result i32)
    i32.const 1
    i32.const 2
    i32.add
    return
  )
)

WATの生成

前回の整数リテラルに加えて、足し算('+')も2項演算子として生成できるように拡張します。

  • generateLiteral() ... 整数リテラル生成
  • generateAddOperator() ... 足し算生成
    • 内部で、演算子の左の項、右の項を生成するために、generate()を再帰的に呼び出す
  • TABs() ... WATのインデントを整形するためのユーティリティ関数
generate()を拡張
// ---- genereate WAT block ---functiongenerate(tree,indent){if(tree===null){return'';}if(tree[0]==='lit'){returngenerateLiteral(tree,indent);}// --- add operator ---if(tree[0]==='+'){returngenerateAddOperator(tree,indent);}println('-- ERROR: unknown node in generate() ---');printObj(tree);abort();}// -- 整数リテラル --functiongenerateLiteral(tree,indent){constv=tree[1];constblock=TABs(indent)+'i32.const '+v;returnblock;}// --- 足し算 ---functiongenerateAddOperator(tree,indent){constleftBlock=generate(tree[1],indent);constrightBlock=generate(tree[2],indent);letblock='';block=block+leftBlock+LF();block=block+rightBlock+LF();block=block+TABs(indent)+'i32.add'+LF();;returnblock;}// --- インデント整形用 ---functionTABs(n){lettabs='';leti=0;while(i<n){tabs=tabs+TAB();i=i+1;}returntabs;}

これで、足し算も生成できるようになりました。

足し算の実行

足し算までサポートしたコンパイラーのコードを mininode_wasm_02_add.js とします。これでWASMを生成して実行してみましょう。

  • WAT→WASM変換 ... wat2wasm を利用
  • WASMの実行 ... 前回用意した run_wasm_simple.js を利用
$ node mininode_wasm_02_add.js add.js
$ cat generated.wat
(module
  (export "exported_main" (func $main))
  (func $main (result i32)
    i32.const 1
    i32.const 2
    i32.add

    return
  )
)
$ wat2wasm generated.wat
$ node run_wasm_simple.js generated.wasm
Loading wasm file: generated.wasm
ret code=3

終了コードが「3」となり、1+2が計算できました。

複数の足し算

今回の足し算の生成では、再帰的な処理を行っています。これにより、複数の足し算があっても生成できるようになっています。

対象となるソースと、それを表す単純化ASTはこちらです。

add_many.js
1+2+(3+(4+5));
単純化AST
[ '+',
  [ '+', [ 'lit', 1 ], [ 'lit', 2 ] ],
  [ '+', [ 'lit', 3 ], [ '+', [ 'lit', 4 ], [ 'lit', 5 ] ] ]
]

WAT/WASM生成、実行結果がこちら。

複数足し算の実行結果
$ node mininode_wasm_02_add.js sample/add_many.js 
$ cat generated.wat
(module
  (export "exported_main" (func $main))
  (func $main (result i32)
    i32.const 1
    i32.const 2
    i32.add

    i32.const 3
    i32.const 4
    i32.const 5
    i32.add

    i32.add

    i32.add

    return
  )
)
$ wat2wasm generated.wat
$ node run_wasm_simple.js generated.wasm
Loading wasm file: generated.wasm
ret code=15

入れ子の足し算が生成され、実行結果も正しく「15」が得られました。

残りの四則演算のサポート

WASMの演算命令

符号付32ビット整数の演算には、次の命令を使います。

  • i32.add ... 足し算(+)
  • i32.sub ... 引き算(-)
  • i32.mul ... 掛け算(*)
  • i32.div_s ... 割り算(/)
  • i32.rem_s ... 余り(%)

これをサポートするように、generate()関数を拡張します。

二項演算子の生成

先ほどは足し算専用でしたが、四則演算の二項演算子に対応するようにかくちょうします。

generate()の拡張
// ---- genereate WAT block ---functiongenerate(tree,indent){if(tree===null){return'';}if(tree[0]==='lit'){returngenerateLiteral(tree,indent);}// --- binary operator ---if(tree[0]==='+'){returngenerateBinaryOperator(tree,indent,'add');}if(tree[0]==='-'){returngenerateBinaryOperator(tree,indent,'sub');}if(tree[0]==='*'){returngenerateBinaryOperator(tree,indent,'mul');}if(tree[0]==='/'){returngenerateBinaryOperator(tree,indent,'div_s');}if(tree[0]==='%'){returngenerateBinaryOperator(tree,indent,'rem_s');}println('-- ERROR: unknown node in generate() ---');printObj(tree);abort();}// --- 二項演算子 ---functiongenerateBinaryOperator(tree,indent,operator){constleftBlock=generate(tree[1],indent);constrightBlock=generate(tree[2],indent);constop='i32.'+operator;letblock='';block=block+leftBlock+LF();block=block+rightBlock+LF();block=block+TABs(indent)+op+LF();returnblock;}

演算子の左の項、右の項を再帰的に生成するのは、足し算の時と同様です。

四則演算の実行

対象ソース

四則演算と余りを用いた、次のソースを用意しました。

sample/binoperator.js
// expect 2(1+3*5-4/2)%3;

WASM生成と実行

コンパイラーのソースコードを、mininode_wasm_02.js とします。

  • コンパイラーで、sample/binoperator.js → generated.wat
  • wat2wasm で、generated.wat → generated.wasm
  • run_wasm_simple.js で実行
$ node mininode_wasm_02.js sample/binoperator.js
$ cat generated.wat
(module
  (export "exported_main" (func $main))
  (func $main (result i32)
    i32.const 1
    i32.const 3
    i32.const 5
    i32.mul

    i32.add

    i32.const 4
    i32.const 2
    i32.div_s

    i32.sub

    i32.const 3
    i32.rem_s

    return
  )
)
$
$ wat2wasm generated.wat
$ node run_wasm_simple.js generated.wasm
Loading wasm file: generated.wasm
ret code=2

実行結果の終了コードは「2」です。(1 + 3*5 - 4/2) % 3 = (1 + 15 - 2) % 3 = 14 % 3 = 2 なので、想定通りになりました。

テストの改良

前回は、単一のソースを対象としたテストとして、test_exitcode.shというシェルスクリプトを用意しました。
今回はこれを複数回実行して、トータルの成功/失敗件数を出すシェルスクリプトを用意します。

メインの処理はこちらです。

# --- test ---
TestSingleExitCode() {
  # --- exec 1 test case --
  testfile=$1

  # usage:
  #  sh test_exitcode.sh compilername interpname filename 
  #
  sh test_exitcode.sh $compiler $interpreter $testfile
  last_case_exit=$?

  # --- check test result--
  case_count=$(($case_count+1))
  if [ "$last_case_exit" -eq 0 ]
  then
    # -- test OK --
    ok_count=$(($ok_count+1))
    echo "$testfile ... OK" >> $summary_file 
  else
    # -- test NG --
    err_count=$(($err_count+1))
    echo "$testfile ... NG" >> $summary_file 
  fi
}
  • 引数 ... 実行対象のソース
  • $compiler ... コンパイラー(テストの対象)
  • $interpreter ... 比較に使うミニNode.jsインタープリター
  • $case_count ... 実行したテストの数
  • $ok_count ... 成功したテストの数
  • $err_count ... 失敗したテストのkazu

複数回実行し、結果をレポートします。

# ---- exec test case -----
# step_01
TestSingleExitCode one.js
TestSingleExitCode two.js
TestSingleExitCode eight.js

# step_02
TestSingleExitCode add.js
TestSingleExitCode add_many.js
TestSingleExitCode binoperator.js

# --- report --
Report

# --- exit ----
exit $err_count

実行すると、次のように結果を出します。

$ sh test_exitcode_multi.sh
... 途中経過は省略 ... 
===== test finish ======
 total=6
 OK=6
 NG=0
======================

これで今後の回帰テストも楽になりました。

次回は

次回は、複数行のとローカル変数を実装する予定です。

これまでのソース

GitHubにソースを上げておきます。


Viewing all articles
Browse latest Browse all 9299

Trending Articles