コンパイラ作成(86) double型の単項マイナス演算子
今回の目標
今回は単項マイナス演算子だよ。
// double型 int main() { double x = 4.5, y = -x; printf("x = %f\ny = %f\n", x, y); }
コード生成部
codegen_unaryopの該当部分を修正。
# 単行演算子のコード生成 def codegen_unaryop(op, operand) type = "int" if op.str == "-" then # 単行マイナス演算子 type = codegen_el operand if type == "char" then codegen " neg al" elsif type == "int" then codegen " neg eax" elsif type == "double" then codegen " movq rax, xmm8" codegen " movabs r10, 8000000000000000H" codegen " xor rax, r10" codegen " movq xmm8, rax" else perror "unsupported type with unary '-'" end
いつものようにclang先輩のコードを参考にしたんだけど分かり辛いよねこれ。ようはサインビットを反転してるんだけど、なんでこんな方法になってるのかな。これが一番早いのかな。それとも何か別の理由があるんだろうか。単純に0から引くのじゃ拙いのかな。うーん。
動作テスト
~/myc$ myc p22.myc ~/myc$ ./p22 x = 4.500000 y = -4.500000 ~/myc$
できたよん。これでdouble型の演算は全部できるようになったかな。次回は引数がdouble型の関数定義かな。関数コールはもう終わってるけど定義の方はまだ未対応だよ。
コンパイラ作成(85) double型の比較演算子
今回の目標
前回の残りの比較演算をサポートするよ。
// double型の比較 int main() { double x; x = 1.5; if( x != 1.5 ) printf("true\n"); else printf("false\n"); x = 1.0; if( x != 1.5 ) printf("true\n"); else printf("false\n"); }
コード生成部
修正箇所は前回と同じ。
# ニーモニック def mnemonic(op, type) if type == "double" then if op.str == "+" then return "addsd " elsif op.str == "-" then return "subsd " elsif op.str == "*" then return "mulsd " elsif op.str == "/" then return "divsd " elsif op.str == "%" then return "divsd " elsif op.str == "==" then return "ucomisd" elsif op.str == "!=" then return "ucomisd" elsif op.str == "<" || op.str == "<" || op.str == ">" || op.str == "<=" || op.str == ">=" then return "ucomisd" else perror "unknown operator \"" + op.str + "\"" end else if op.str == "+" then return "add " elsif op.str == "-" then return "sub " elsif op.str == "*" then return "imul" elsif op.str == "/" then return "idiv" elsif op.str == "%" then return "idiv" elsif op.str == "==" then return "cmp " elsif op.str == "!=" then return "cmp " elsif op.str == "<" || op.str == "<" || op.str == ">" || op.str == "<=" || op.str == ">=" then return "cmp " else perror "unknown operator \"" + op.str + "\"" end end end
# 左被演算子と右被演算子とで計算 if op.str == "==" then codegen " " + ostr + " #{reg}, " + str if type_l == "double" then codegen " sete al" codegen " setnp r10b" codegen " and al, r10b" codegen " and eax, 1" type_l = "int" else codegen " sete al" codegen " and eax, 1" end elsif op.str == "!=" then codegen " " + ostr + " #{reg}, " + str if type_l == "double" then codegen " setne al" codegen " setp r10b" codegen " or al, r10b" codegen " and eax, 1" type_l = "int" else codegen " setne al" codegen " and eax, 1" end elsif op.str == "<" then codegen " " + ostr + " #{reg}, " + str if type_l == "double" then codegen " setb al" codegen " and eax, 1" type_l = "int" else codegen " setl al" codegen " and eax, 1" end elsif op.str == ">" then codegen " " + ostr + " #{reg}, " + str if type_l == "double" then codegen " seta al" codegen " and eax, 1" type_l = "int" else codegen " setg al" codegen " and eax, 1" end elsif op.str == "<=" then codegen " " + ostr + " #{reg}, " + str if type_l == "double" then codegen " setbe al" codegen " and eax, 1" type_l = "int" else codegen " setle al" codegen " and eax, 1" end elsif op.str == ">=" then codegen " " + ostr + " #{reg}, " + str if type_l == "double" then codegen " setae al" codegen " and eax, 1" type_l = "int" else codegen " setge al" codegen " and eax, 1" end
前回同様、clang先輩の出力結果を参考に。
動作テスト
~/myc$ myc p16.myc ~/myc$ ./p16 false true ~/myc$
大丈夫だね。残りの演算子についても同様にチェック。地味で面倒な作業だけどサボらずやったよ。これだけじゃ面白くないんでもういっちょやってみるよ。
// モンテカルロ法による円周率の計算 extern double sqrt(double x); extern size_t time(size_t *t); extern void srand48(size_t seed); extern double drand48(); int main() { size_t t; srand48(time(&t)); double count = 0.0; for(int i = 0; i < 1000000; i = i + 1) { double x = drand48(); double y = drand48(); if( x*x + y*y <= 1.0 ) count = count + 1.0; } printf("pi = %10.8f\n",count / 1000000.0 * 4.0); }
どうかな。
~/myc$ myc p21.myc ~/myc$ ./p21 pi = 3.14105600 ~/myc$
できたできた。countがdouble型になってるのは型変換がまだできないから。これも宿題だなあ。あとtime(0)とかtime(NULL)とか書けるようにしたいなあ。さて次は単項演算子かな。
コンパイラ作成(84) double型の等価演算子
今回の目標
等価演算子をサポートするよ。
// double型の乱数 int main() { double x; x = 1.5; if( x == 1.5 ) printf("true\n"); else printf("false\n"); x = 1.0; if( x == 1.5 ) printf("true\n"); else printf("false\n"); }
コード生成部
まずはここを修正。
# ニーモニック def mnemonic(op, type) if type == "double" then if op.str == "+" then return "addsd " elsif op.str == "-" then return "subsd " elsif op.str == "*" then return "mulsd " elsif op.str == "/" then return "divsd " elsif op.str == "%" then return "divsd " elsif op.str == "==" then return "ucomisd" elsif op.str == "!=" then return "cmp " elsif op.str == "<" || op.str == "<" || op.str == ">" || op.str == "<=" || op.str == ">=" then return "cmp " else perror "unknown operator \"" + op.str + "\"" end else if op.str == "+" then return "add " elsif op.str == "-" then return "sub " elsif op.str == "*" then return "imul" elsif op.str == "/" then return "idiv" elsif op.str == "%" then return "idiv" elsif op.str == "==" then return "cmp " elsif op.str == "!=" then return "cmp " elsif op.str == "<" || op.str == "<" || op.str == ">" || op.str == "<=" || op.str == ">=" then return "cmp " else perror "unknown operator \"" + op.str + "\"" end end end
ucomisdってので比較できるみたいだ。次はcodegen_elsの等価演算子の処理のところ。
# 左被演算子と右被演算子とで計算 if op.str == "==" then codegen " " + ostr + " #{reg}, " + str if type_l == "double" then codegen " sete al" codegen " setnp r10b" codegen " and al, r10b" codegen " and eax, 1" type_l = "int" else codegen " sete al" codegen " and eax, 1" end
clang先輩の出力を参考にしたんだけど、なぜこうなってるのか理解できないよ。この辺の知識が不足してるなあ。
コンパイラ作成(83) double型を返す関数の呼出
今回の目標
sqrtをコールしてみるよ。
// double型 extern double sqrt(double x); int main() { printf("%f\n", 2.0 * sqrt(2.0)); }
前回、関数コールに不安があったんだけど、やってみたらやっぱり駄目だったよ。見直してみたら未修正のところがあった。
関数コール
早速修正。
# 関数コールのコード生成 def codegen_func operand rettype = "int" xmm = 0 # xmmレジスタの使用数 f = @functions[operand[0].str] 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 if f != nil then if operand.size - 2 != f[1].size then perror "wrong number of parameters" end end (0...operand.size-2).each do |i| save = @numuseregs @numuseregs = i-xmm type = codegen_el operand[i+2] @numuseregs = save if f != nil then if f[1][i] == "size_t" then codegen " movsx rax, eax" type = f[1][i] elsif f[1][i] == "void*" && is_pointer_type?(type) then type = f[1][i] end if type != f[1][i] then perror "incompatible type parameter" end end if type == "int" || type == "char" then codegen " mov #{@regs32[i-xmm]}, eax" elsif type == "size_t" codegen " mov #{@regs64[i-xmm]}, rax" elsif is_pointer_type?(type) then codegen " mov #{@regs64[i-xmm]}, rax" elsif type == "double" then codegen " movsd xmm#{xmm}, xmm8" xmm += 1 else perror end end if f == nil then codegen " mov al, #{xmm}" end codegen " call " + operand[0].str if f != nil then rettype = f[0] end if rettype == "double" codegen " movsd xmm8, xmm0" end 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 return rettype end
三行追加。double型はxmm0に返ってくるんでそれをxmm8にmovsdしてる。
動作テスト
どうかな。
~/myc$ myc p11.myc /tmp/p11-6ed13e.o: 関数 `main' 内: (.text+0x38): `sqrt' に対する定義されていない参照です clang: error: linker command failed with exit code 1 (use -v to see invocation) ~/myc$
ありゃ。ああ、そっか。-lm付けなきゃいけないのか。
~/myc$ myc -c p11.myc ~/myc$ clang-5.0 p11.s -o p11 -lm ~/myc$ ./p11 2.828427
お、できた。もういっちょ行ってみるよ。
// double型の乱数 extern double sqrt(double x); extern size_t time(size_t *t); extern void srand48(size_t seed); extern double drand48(); int main() { size_t t; srand48(time(&t)); double x = drand48(); for(int i = 0; i < 10; i = i + 1) printf("%f\n", drand48()); }
timeのextern宣言おかしいのはまだlong型をサポートしてないから。それに型キャストもできないからね。無理やりだけどこれでいけるかな。
~/myc$ myc -c p14.myc ~/myc$ clang-5.0 p14.s -o p14 -lm ~/myc$ ./p14 0.517519 0.828659 0.209253 0.540878 0.017379 0.805752 0.770110 0.495048 0.851670 0.865793 ~/myc$
おお、上手く行ったよ。しかし、-lm指定するのにclangを呼ばないと駄目なのは面倒くさいなあ。。mycから-lmを渡してやればいいんだよなあ。簡単に修正できるかな。ちょっとやってみる。
メイン部
# コマンドラインオプションの解析 $opt_c = false $opt_d = false $opt_l = [] $opt_L = false $opt_O = 3 $opt_v = false $opt_cc = "clang-5.0" OptionParser.new do |opt| opt.on('-c', 'Compile only') {|v| $opt_c = v} opt.on('-d', 'Debug mode') {|v| $opt_d = v} opt.on('-l module','Link module') {|v| $opt_l << ("-l"+v)} opt.on('-L', 'Lexer module test') {|v| $opt_L = v} opt.on('-O val', 'Optimize level') {|v| $opt_O = v.to_i} opt.on('-v', 'Version') {|v| $opt_v = v} opt.on('--cc val', 'Assemble & Link') {|v| $opt_cc = v} begin opt.parse!(ARGV) rescue puts "Invalid option. \nsee #{opt}" exit end end # versionの表示 if $opt_v then puts "myc version #{$ver}" exit 0 end # 字句解析単体テスト if $opt_L then fname = ARGV[0] lex = Lexer.new(fname) lex.moduletest lex.close exit 0 end # コンパイル f = [] execfname = nil ARGV.each do |fname| if fname.match(/\.myc$/) then asmfname = fname.sub(/\.myc$/,'.s') # アセンブリコードのファイル名 if !execfname then execfname = fname.sub(/\.myc$/,'') end parser = Parser.new(fname) parser.program f << asmfname else f << fname end end if !$opt_c then system("#{$opt_cc} #{f.join(' ')} -o #{execfname} #{$opt_l.join(' ')}") end exit 0
Lexerクラスの単体テストのオプションとバッティングしちゃったんで、そっちは-Lに変更したよ。
動作テスト再び
~/myc$ myc p11.myc -lm ~/myc$ ./p11 2.828427
できたよ。一応-lは複数付けられるようにしたつもり。その内テストしとこう。(多分忘れる)
しかし、double型はやっぱり難しいなあ。char型と比べてやらなきゃならないことが多いよ。まあ大変な分面白いけどね。知らなかったこと知れるしね。次回はdouble型の比較演算かな。
コンパイラ作成(82) double型の複雑な四則演算
今回の目標
前回に続き四則演算。
// double型 int main() { printf("%f\n", 2.0 + 5.0 / 2.0); }
前回対応できなかった複雑な式。
コード生成部
codegen_elsの括弧の処理部と関数呼出の処理部を修正。
def codegen_els(op, operand, type_l) ostr = mnemonic op, type_l # 右被演算子を評価 type_r = "int" if operand.kind_of?(Array) then if operand[0].size == 2 && operand[0].kind == TK::ID && operand[1].str == "()" then if type_l == "double" then codegen " sub rsp, 16" codegen " movsd [rsp], xmm8" type_r = codegen_func operand codegen " movsd xmm9, xmm8" codegen " movsd xmm8, [rsp]" codegen " add rsp, 16" str = "xmm9" else codegen " sub rsp, 8" codegen " push rax" type_r = codegen_func operand codegen " mov r10d, eax" codegen " pop rax" codegen " add rsp, 8" str = "r10d" end else if type_l == "double" then codegen " sub rsp, 16" codegen " movsd [rsp], xmm8" type_r = codegen_el operand codegen " movsd xmm9, xmm8" codegen " movsd xmm8, [rsp]" codegen " add rsp, 16" str = "xmm9" else codegen " sub rsp, 8" codegen " push rax" type_r = codegen_el operand codegen " mov r10d, eax" codegen " pop rax" codegen " add rsp, 8" str = "r10d" end end
レジスタの退避処理だけど、xmmレジスタはpush/popできないんだね。仕方ないんでrsp弄った後にmovsdするようにしたよ。
動作テスト
~/myc$ myc p10.myc ~/myc$ ./p10 4.500000 ~/myc$
お、ちゃんと計算できるようになったよ。今回ので式の途中での関数呼出もできるようになったはずだよなあ。ああ、でももう一回処理を見直した方が良いかな。現状でどこまで実装できたのか分からなくなってきたよ。拙い状況だよ。うーむ。
Haskellのお勉強(4) 素数
素数の表示
Haskellだけど細々と勉強続けてるよ。でもなかなか進まない。解説サイト見たりしてるんだけどさ、書かれてる内容は理解できても、それを実際のプログラミングの中でどう活かせば良いのかが分からないんだよね。ということでまた簡単なとこから。
import System.Environment import Text.Printf prime p [] = p prime p (x:xs) = prime (p ++ [x]) [ i | i <- xs, i `mod` x /= 0 ] main = do args <- getArgs let n = if args == [] then 100 else read(args !! 0) print $prime [] [2..n]
エラトステネスの篩。再帰使ってやってみたよ。
~/haskell$ runghc prime.hs [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97] ~/haskell$
できたよ。相変わらずちょっとした記述間違いでドツボにハマっちゃうよ。prime p (x:xs) =のところをprime p x:xs =って間違えてたんだけど、それが原因だって気付くまでえらく時間掛かっちゃった。
計算途中でリストのお尻に要素を追加してるんだけど、こういうのHaskellは苦手なのかな。Lispと同じ感じで。ってことでちょっと変更してみる。
import System.Environment import Text.Printf prime p [] = reverse p prime p (x:xs) = prime (x:p) [ i | i <- xs, i `mod` x /= 0 ] main = do args <- getArgs let n = if args == [] then 100 else read(args !! 0) print $prime [] [2..n]
リストの先頭に要素を追加していくようにして、最後にreverseするようにしてみたよ。
~/haskell$ runghc prime2.hs [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97] ~/haskell$
同じ結果が得られたよ。おそらくprime2.hsの方が速いんだと思うけどどうだろうか。計測してみる。
~/haskell$ time runghc prime.hs 10000 [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97, 途中省略 ,9871,9883,9887,9901,9907,9923,9929,9931,9941,9949,9967,9973] real 0m3.346s user 0m2.567s sys 0m0.178s ~/haskell$ time runghc prime2.hs 10000 [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97, 途中省略 ,9871,9883,9887,9901,9907,9923,9929,9931,9941,9949,9967,9973] real 0m2.715s user 0m2.300s sys 0m0.161s ~/haskell$
おお、やっぱりprime2.hsの方がちょっと速いね。
vlangを試してみた
vlang
巷で話題になってるvlangを触ってみたよ。
vlang.io
奇麗なページでしっかりしたプログラミング言語なのかと思ったけど、一人だけで作ってるんだね。正直出来はまだまだって感じみたいだ。コンパイラの中身をちょこっと見てみたけど、C言語へのトランスレータだね。言語自体はGoに近いのかな。俺はGoは触ったことないんで良く分からないんだけど。変数がデフォルトでイミュータブルなのとかはRustっぽいのかな。
試してみる
#include <stdio.h> fn main() { for i := 1; i <= 9; i++ { for j := 1; j <= 9; j++ { C.printf('%3d', i*j) } println('') } }
~/vlang$ v run test2.v ============running test2============================== 1 2 3 4 5 6 7 8 9 2 4 6 8 10 12 14 16 18 3 6 9 12 15 18 21 24 27 4 8 12 16 20 24 28 32 36 5 10 15 20 25 30 35 40 45 6 12 18 24 30 36 42 48 54 7 14 21 28 35 42 49 56 63 8 16 24 32 40 48 56 64 72 9 18 27 36 45 54 63 72 81 ~/vlang$
書式付き出力の方法が良く分からなかったけど、C言語のprintfが呼べるのか。printfに限らず困ったらC言語のルーチン呼び出せば良いのかな。
雑感
今のところ将来性がある言語なのか分からないな。Github見てると結構な勢いでコミットされてるね。もしかしたらバケるのかもしれないけど、作者が飽きてほっぽり出しちゃう可能性もあるな。とりあえず注目を集めることだけは成功してる。これは間違いないんだけどね。さてさて。