コンパイラ作成(54) 文字列定数が引数の関数
今回の目標
今回は引数がint型じゃない関数に対応するよ。
// 文字列の引数 int main() { puts_x("Hello, World!", 5); } int puts_x(char *s, int n) { int i; for(i = 0; i < n; i = i + 1) puts(s); }
いきなりchar型、それも*付き。C言語初心者を地獄に突き落とすポインタだよ。今回の目標は結構ハードルが高い。一回できれいに飛ぶのは難しんであちこち手抜きで進めるよ。
perror
今回の目標とは関係ないんだけどちょっと修正するよ。
# コンパイルエラーを表示 def perror(emsg = "syntax error" ) fname, lineno, pos = @lex.position print fname,":",lineno,":",pos," error: ", emsg, "\n" caller.each do |f| puts f end if $opt_d exit -1; # コンパイル処理を打ち切る end
デバッグモードの時にどっからコールされてるかを表示するようにしたよ。なくても何とかなるんだけど、これがあった方がデバッグ効率上がると思うんで入れてみた。
Lexer
トークンの種類を一つ増やした。
# トークンの種類 enumの代わり module TK EOF = 1 # End of file ID = 2 # Identifier NUMBER = 3 # Number SYMBOL = 4 # Symbol STRING = 5 # String RESERVE = 6 # Reserved word TYPE = 7 # Type word UNKNOWN = 8 # Unkown token end
RESERVEからTYPEを分離。
=begin @reservedword = [ "return","goto","if","else","for","while","until","do","break", "int","char", "print","puts","printf" ] =end @reservedword = [ "return","goto","if","else","for","while","until","do","break", "print","puts","printf" ] @typeword = [ "int","char" ]
int、charはTK::TYPE。
# identifireの切り出し if m = @line[@idx,@line.length].match(/^[\p{alpha}|_][\p{alnum}|_]*/) then str = m.to_s @idx += str.length if @reservedword.include?(str) then return TK::RESERVE, str elsif @typeword.include?(str) then return TK::TYPE, str else return TK::ID, str end
gettokenの該当箇所を修正。
# モジュールテスト def moduletest() i = 0 loop do i += 1 kind, str = gettoken # fname, lineno, pos = position # print fname,":",lineno,":",pos," " if kind == TK::EOF then break elsif kind == TK::ID then print "TK::ID ",str,"\n" elsif kind == TK::NUMBER then print "TK::NUMBER ",str,"\n" elsif kind == TK::STRING then print "TK::STRING ",str,"\n" elsif kind == TK::SYMBOL then print "TK::SYMBOL ",str,"\n" elsif kind == TK::RESERVE then print "TK::RESERVE ",str,"\n" elsif kind == TK::TYPE then print "TK::TYPE ",str,"\n" elsif kind == TK::UNKNOWN then print "TK::UNKNOWN ",str,"\n" break end end end
ここも忘れずに修正。
function
次は関数定義を処理するここを修正。
# 関数の構文解析 def function() @labelcnt = 0 @lvars = Hash.new @lvarsize = 0; rettype = nil kind, str = @lex.gettoken if kind == TK::TYPE && str == "int" then rettype = str kind, str = @lex.gettoken end if kind == TK::EOF then return false end if kind != TK::ID then perror "expected identifier" end @funcname = str parametersize = [] kind, str = @lex.gettoken if kind != TK::SYMBOL || str != "(" then perror end kind, str = @lex.gettoken loop do if kind == TK::SYMBOL && str == ")" then break end if kind == TK::TYPE then type = str kind, str = @lex.gettoken if kind == TK::SYMBOL && str == "*" then type += str kind, str = @lex.gettoken end if kind != TK::ID then perror end print "para "+str+"\n" if $opt_d size = sizeof type @lvarsize += size parametersize << size if @lvars[str] then perror "redefinition parameter \"" + str +"\"" end @lvars[str] = [type,@lvarsize] else perror end kind, str = @lex.gettoken if kind == TK::SYMBOL && str == "," then kind, str = @lex.gettoken end end codegen ".global "+@funcname codegen @funcname+":" codegen " push rbp" codegen " mov rbp, rsp" # 仮のコードを作成 idx = codegen " sub rsp, xx" n = @lvars.size offset = 0 (0...n).each do |i| offset += parametersize[i] if parametersize[i] == 4 then codegen " mov dword ptr [rbp - #{offset}], #{@regs32[i]}" else codegen " mov qword ptr [rbp - #{offset}], #{@regs64[i]}" end end kind, str = @lex.gettoken if kind != TK::SYMBOL || str != "{" then perror end kind, str = block codegen ".RET_" + @funcname + ":" # 16の倍数になるように揃える size = (@lvarsize+15) / 16 * 16 # 正しいサイズでコードを生成し置き換える if size != 0 then codechange idx," sub rsp, #{size}" codegen " add rsp, #{size}" else codechange idx,nil end codegen " pop rbp" codegen " ret" if @functions[@funcname] != nil then perror "redefinition of \"" + @funcname + "\"" end @functions[@funcname] = [rettype,[]] p @lvars if $opt_d # デバッグ用 optimize if $opt_O != 0 codeflush @funcname = nil return true end
intがTK::TYPEになったことに対応。引数の型によってバイト数が違うんでその関連の処理が増えてるよ。新しいメソッド、sizeofで指定されてる型に必要なバイト数を求めてる。引数をレジスタからスタック上へ移す処理も型によって分けるようにした。
このメソッド最初は単純だったのに大分ごちゃごちゃして分かり辛くなってきたなあ。
sizeof
でこれが追加したメソッド。
# 型のサイズ def sizeof(type) case type when "int" return 4 when "char*" return 8 else perror "unknown type" end end
手抜きでint型とchar*型にしか対応してないよ。
statement
次はstatementメソッドの修正。
elsif kind == TK::TYPE && str == "int" then # 変数宣言の処理 loop do kind, str = @lex.gettoken if kind != TK::ID then perror end print "var "+str+"\n" if $opt_d @lvarsize += 4 if @lvars[str] then perror "redefinition variable \"" + str +"\"" end @lvars[str] = ["int",@lvarsize] skind, sstr = @lex.gettoken if skind == TK::SYMBOL && sstr == "=" then kind, str = expr2 kind, str, skind, sstr; else kind, str = skind, sstr; end if kind != TK::SYMBOL || str != "," then break end end if kind != TK::SYMBOL || str != ";" then perror "expected ';' after variables" end
ここはTK::TYPE関連の修正だけ。char型の変数宣言とかには対応してないよ。
elsif kind == TK::RESERVE && str == "puts" then # 標準関数putsの処理 kind, str = @lex.gettoken if kind != TK::SYMBOL || str != "(" then perror end kind, str = @lex.gettoken if kind == TK::STRING then label = addliteral str codegen " lea rdi, "+label elsif kind == TK::ID then v = @lvars[str] if v == nil then perror "undeclared variable \"" + str + "\"" end codegen " mov rdi, qword ptr [rbp - " + v[1].to_s + "]" else perror end codegen " call puts" kind, str = @lex.gettoken if kind != TK::SYMBOL || str != ")" then perror end kind, str = @lex.gettoken if kind != TK::SYMBOL || str != ";" then perror "expected ';' after function" end
今までputs("string")の形式にしか対応してなかったけど、puts(s)にも対応したよ。えーと、そもそももうこの処理自体いらないのかな。引数付きの関数コールはexprの中で処理できるようになってるし、今回文字列定数にも対応するから、全部exprに任せて大丈夫なはずだよなあ。今回は変更箇所多いからテスト項目を増やしたくないんで、このまま残すことにするよ。
コード生成
ここが今回一番悩んだ場所。今まで全部int型だったけど、char*型にも対応しないとダメなんで、codegen_el以下のインターフェースを変更することにしたよ。
# 式のコード生成 def codegen_el(el) type = "int" if !el[0].kind_of?(Array) && el[0].kind == TK::SYMBOL codegen_unaryop el[0], [el[1]] elsif (el.size > 1) && el[1].str == "=" then codegen_assign el else type = codegen_elf el.shift if type != "int" && el != [] then perror end loop do if el == [] then break end op = el.shift operand = el.shift codegen_els op, operand end end return type end
戻り値で型情報を返すようにした。型をミックスした式はちゃんとチェックして無効な時はエラーにしないとダメなんだけどその辺未対応だよ。
# 式のコード生成(二項演算の左側被演算子) def codegen_elf(operand) type = "int" if operand.kind_of?(Array) then if !operand[0].kind_of?(Array) && operand[0].kind == TK::ID && operand[1].str == "()" then codegen_func operand else codegen_el operand end elsif operand.kind == TK::NUMBER then codegen " mov eax, " + operand.str elsif operand.kind == TK::ID then v = @lvars[operand.str] if v == nil then perror "undeclared variable \"" + operand.str + "\"" end codegen " mov eax, dword ptr [rbp - " + v[1].to_s + "]" elsif operand.kind == TK::STRING then type = "char*" label = addliteral operand.str codegen " lea rax, "+label else perror end return type end
ここも型情報を返すようにした。それと文字列定数の時の処理を追加。codegen_elsとかも変更す必要があるんだけど、今回は見なかったことにして先に進むよ。
# 関数コールのコード生成 def codegen_func operand if @numuseregs != 0 then if @numuseregs % 2 == 1 then codegen " sub rsp, 8" end (0...@numuseregs).each do |i| codegen " push #{@regs64[i]}" end end (0...operand.size-2).each do |i| save = @numuseregs @numuseregs = i type = codegen_el operand[i+2] @numuseregs = save if type == "int" codegen " mov #{@regs32[i]}, eax" elsif type == "char*" codegen " mov #{@regs64[i]}, rax" else perror end end codegen " call " + operand[0].str if @numuseregs != 0 then (0...@numuseregs).reverse_each do |i| codegen " pop #{@regs64[i]}" end if @numuseregs % 2 == 1 then codegen " add rsp, 8" end end end
codegen_elの戻り値を見てeaxかraxを判断してる。
動作テスト
ではテストしてみるよ。
~/myc$ myc -d err1.myc [42] [42] err1.myc:3:14 error: expected ';' after return statement /usr/local/bin/myc:767:in `statement' /usr/local/bin/myc:1022:in `block in block' /usr/local/bin/myc:1021:in `loop' /usr/local/bin/myc:1021:in `block' /usr/local/bin/myc:1091:in `function' /usr/local/bin/myc:1123:in `program' /usr/local/bin/myc:1179:in `block in <main>' /usr/local/bin/myc:1173:in `each' /usr/local/bin/myc:1173:in `<main>' ~/myc$
まずはperrorのコール元の表示をテスト。こんな感じで表示されるよ。
~/myc$ myc -l o9.myc TK::TYPE int TK::ID main TK::SYMBOL ( TK::SYMBOL ) TK::SYMBOL { TK::ID puts_x TK::SYMBOL ( TK::STRING Hello, World! TK::SYMBOL , TK::NUMBER 5 TK::SYMBOL ) TK::SYMBOL ; TK::SYMBOL } TK::TYPE int TK::ID puts_x TK::SYMBOL ( TK::TYPE char TK::SYMBOL * TK::ID s TK::SYMBOL , TK::TYPE int TK::ID n TK::SYMBOL ) TK::SYMBOL { TK::TYPE int TK::ID i TK::SYMBOL ; TK::RESERVE for TK::SYMBOL ( TK::ID i TK::SYMBOL = TK::NUMBER 0 TK::SYMBOL ; TK::ID i TK::SYMBOL < TK::ID n TK::SYMBOL ; TK::ID i TK::SYMBOL = TK::ID i TK::SYMBOL + TK::NUMBER 1 TK::SYMBOL ) TK::RESERVE puts TK::SYMBOL ( TK::ID s TK::SYMBOL ) TK::SYMBOL ; TK::SYMBOL } ~/myc$
Lexerのテスト。ちゃんとTK::TYPEになってるよ。
~/myc$ myc -d o9.myc [[puts_x, (), [Hello, World!], [5]]] [[puts_x, (), [Hello, World!], [5]]] {} para s para n var i [i, =, 0] [[i, =, 0]] [i, <, n] [[i, <, n]] [i, =, i, +, 1] [[i, =, [i, +, 1]]] {"s"=>["char*", 8], "n"=>["int", 12], "i"=>["int", 16]} {"main"=>["int", []], "puts_x"=>["int", []]} ~/myc$ ./o9 Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! ~/myc$
おお、ちゃんと動いた。今回の修正で型を意識したコード生成処理をいれたけど、あちこち手抜きになってるよ。その辺は次回以降ちょっとずつ頑張るよ。それと今回はコーディングに時間とられたんでテストが不十分だな。一応、testmycを走らせて過去のテストプログラムがコンパイル、実行できることは確かめてるけどね。
しかし新しい機能を追加するたびに積み残しがごっそり増えてくなあ。もう何が積み残されてるか把握できなくなってきたよ。まずいな、これ。