元ネタ
今回の元ネタになっているのは、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.sh
にassert 21 "5 + 20 - 4"
と追記してテストしますと、無事合格できます。