2021/02/24

Nimで加減算のできるコンパイラの作成

元ネタ

今回の元ネタになっているのは、Rui Ueyamaさんの「低レイヤを知りたい人のためのCコンパイラ作成入門」であります。

なぜNimなのか?

C言語でそのままやってしまうと、私の場合、丸写しになってしまい、何となく理解したような気分になってしまうからです。またNimの場合、C言語のヘッダーファイルや関数を直接使うことが可能なので、後々便利かな?なんて素人の考えでNimにしました。

ということで、現在ステップ2までやったので、そこまでを記事にしようと思います。

ステップ1

元ネタのページのステップ1は「整数1個をコンパイルする言語の作成」ということで、まずはnimbleにてプロジェクトを作成します。


$ mkdir nim9cc
$ cd nim9cc
$ nimble init

プロジェクトはbinaryで作成します。nimbleを使うのでMakefileは不要です。test.shも


$ touch test.sh
$ chmod a+x test.sh
と予め作っておきます。

メインとなるファイルはsrc/nim9cc.nimとなります。ディレクトリ構成は以下のようになります。


nim9cc/
  nim9cc.nimble
  test.sh
  src/
    nim9cc.nim

src/nim9cc.nimは以下のようにしました。


import os
import strformat

proc main() =
  let
    argc = paramCount()
    argv = commandLineParams()
  
  if argc != 1:
    echo "Incorrect number of arguments"
    quit(QuitFailure)
    
  echo fmt""".intel_syntax noprefix
.global main
main:
  mov rax, {argv[0].string}
  ret"""
  
  quit(QuitSuccess)
  
when isMainModule:
  main()

test.shは元ネタの./9cc "$input" > tmp.s./nim9cc "$input" > tmp.sにしただけのものを使います。

さて、Makefileのない状態で、どのようにテストしたり、クリーンしたりしよう?

ここで私はnimbleのtask機能を使うことにしました。nim9cc.nimbleに以下のコードを追記します。


task makeTest, "execute test.sh":
  exec("./test.sh")

task makeClean, "Remove tmp*":
  exec("rm -f tmp*")

こうすることで、nimble buildにて実行ファイルnim9ccを作成し、nimble makeTestとすることで、test.shを使ってテストすることができ、nimble makeCleanとすることで、tmpおよびtmp.sを削除することができます。と、誇らしげにしゃべってますが、実はワタクシ、今回ようやくこの便利機能を使えるようになりました……

これでテストには合格できるはずです(ワタクシの誤字脱字がなければ)

ステップ2

次にステップ2の「加減算のできるコンパイラの作成」は以下のようなものになりました。


import os
import strformat
import strutils
import sequtils


proc readNum(chars: openArray[char], idx: var int): string =
  result = ""
  while idx < chars.len:
    if chars[idx].isDigit:
      result.add(chars[idx])
      try:
        inc(idx)
      except:
        break
    else:
      break

proc main() =
  let
    argc = paramCount()
    argv = commandLineParams()
    p = toSeq(argv[0].string)
  var
    idx: int = 0

  if argc != 1:
    echo "Incorrect number of arguments"
    quit(QuitFailure)

  echo fmt""".intel_syntax noprefix
.global main
main:
  mov rax, {readNum(p, idx)}"""

  while idx < p.len:
    if p[idx] == '+':
      inc(idx)
      echo fmt"  add rax, {readNum(p, idx)}"
    elif p[idx] == '-':
      inc(idx)
      echo fmt"  sub rax, {readNum(p, idx)}"
    else:
      break

  echo "  ret"
  quit(QuitSuccess)

when isMainModule:
  main()

test.shにもassert 21 "5+20-4"を追記します。テストは一応合格しておりますが、ちょっと個人的には満足していないコードなので、う〜んという感じです。改良の余地はおおありだと思います。時間に余裕ができれば改良してまたご報告しようと思います。

まとめ

nimbleのtask便利!

追記(2021/2/26)

ちょっと修正して、空白文字ありもテストに合格できるようにしました。


import os
import strformat
import strutils
import sequtils


proc readNum(chars: openArray[char], idx: var int): string =
  result = ""
  while idx < chars.len:
    if chars[idx].isSpaceAscii:
      inc(idx)
      continue
    elif chars[idx].isDigit:
      result.add(chars[idx])
      inc(idx) 
    else:
      break

proc main() =
  let
    argc = paramCount()
    argv = commandLineParams()
    p = toSeq(argv[0].string)
  var
    idx: int = 0

  if argc != 1:
    stderr.writeLine("ERROR: Incorrect number of arguments")
    quit(QuitFailure)

  echo fmt""".intel_syntax noprefix
.global main
main:
  mov rax, {readNum(p, idx)}"""

  while idx < p.len:
    if p[idx].isSpaceAscii:
      inc(idx)
      continue
    elif p[idx] == '+':
      inc(idx)
      echo fmt"  add rax, {readNum(p, idx)}"
      continue
    elif p[idx] == '-':
      inc(idx)
      echo fmt"  sub rax, {readNum(p, idx)}"
      continue
    else:
      break

  echo "  ret"
  quit(QuitSuccess)

when isMainModule:
  main()

test.shassert 21 "5 + 20 - 4"と追記してテストしますと、無事合格できます。