気にはなっていたけど後回しにしていたアレです。
今 v3 を作っているのですが、たしか
関数呼び出しの引数や return文で任意の式を書きたくなったとかで
いじっていて、コード生成器の冗長すぎる箇所をうまいこと整理できると分かりました。
良さそうだったので v2 にもフィードバックしておきます。
rename: codegen_expr() => _codegen_expr_binary()
codegen_expr() で行っているのは現状では
二項演算のみなので、名前と処理内容が乖離しています。
まずはこの乖離を解消しました。
diff は省略して変更後の _codegen_expr_binary() を貼っておきます。
def _codegen_expr_binary(fn_arg_names, lvar_names, expr)
operator, *args = expr
arg_l = args[0]
arg_r = args[1]
_codegen_expr_push(fn_arg_names, lvar_names, arg_l)
_codegen_expr_push(fn_arg_names, lvar_names, arg_r)
case operator
when "+"
_codegen_expr_add()
when "*"
_codegen_expr_mult()
when "eq"
_codegen_expr_eq()
when "neq"
_codegen_expr_neq()
else
raise not_yet_impl("operator", operator)
end
end
reg_a への転送とスタックへの push を分離(_codegen_expr_push)
_codegen_expr_push()
というメソッドがあります。
さっきリネームした _codegen_expr_binary()
から呼び出されていて、
二項演算を行う前に引数を評価して結果をスタックに push する、ということをやっています。
呼び出し元はこう:
def _codegen_expr_binary(...)
_codegen_expr_push(fn_arg_names, lvar_names, arg_l)
_codegen_expr_push(fn_arg_names, lvar_names, arg_r)
_codegen_expr_push()
では結果を reg_a
に転送するところまでで止めて、
スタックへ push するのは呼び出し側で行うようにします。
呼び出し側がこうなるように:
def _codegen_expr_binary(...)
_codegen_expr_push(fn_arg_names, lvar_names, arg_l)
puts " push reg_a"
_codegen_expr_push(fn_arg_names, lvar_names, arg_r)
puts " push reg_a"
--- a/vgcg.rb
+++ b/vgcg.rb
@@ -117,27 +117,25 @@ def codegen_while(fn_arg_names, lvar_names, rest)
end
def _codegen_expr_push(fn_arg_names, lvar_names, val)
- push_arg =
case val
when Integer
- val
+ puts " cp #{val} reg_a"
when String
case
when fn_arg_names.include?(val)
- to_fn_arg_addr(fn_arg_names, val)
+ cp_src = to_fn_arg_addr(fn_arg_names, val)
+ puts " cp #{cp_src} reg_a"
when lvar_names.include?(val)
- to_lvar_addr(lvar_names, val)
+ cp_src = to_lvar_addr(lvar_names, val)
+ puts " cp #{cp_src} reg_a"
else
raise not_yet_impl("val", val)
end
when Array
_codegen_expr_binary(fn_arg_names, lvar_names, val)
#=> 結果が reg_a に入る
- "reg_a"
else
raise not_yet_impl("val", val)
end
-
- puts " push #{push_arg}"
end
def _codegen_expr_add
出力されるアセンブリコードが次のように変化します。
1命令だったところが2命令に増えて非効率になりますが、命令実行後の結果は変わりませんし、
コード生成処理がより良くなる方が嬉しいので気にせずやってしまいます。
- push [bp+3]
+ cp [bp+3] reg_a
+ push reg_a
あらためて codegen_expr() を作成
def codegen_expr(fn_arg_names, lvar_names, expr)
case expr
when Integer
puts " cp #{expr} reg_a"
when String
case
when fn_arg_names.include?(expr)
cp_src = to_fn_arg_addr(fn_arg_names, expr)
puts " cp #{cp_src} reg_a"
when lvar_names.include?(expr)
cp_src = to_lvar_addr(lvar_names, expr)
puts " cp #{cp_src} reg_a"
else
raise not_yet_impl("expr", expr)
end
when Array
_codegen_expr_binary(fn_arg_names, lvar_names, expr)
else
raise not_yet_impl("expr", expr)
end
end
修正前は codegen_expr() が二項演算しか受け付けていなかったのが、
それに加えて、整数、関数の引数、ローカル変数も式として扱うようになりました。
たとえば
while ({式}) {
}
という文があったとして、これまでは {式}
の部分に x == 1
のような二項演算しか書けないようになっていたのが、次のような文が書けるようになるイメージです。
while (1) { ... }
while (is_foo) { ... }
では codegen_expr() を使って置き換えていきます。
まずは _codegen_expr_push() が置き換えられます。
あらためて見ても分かるように、この段階では codegen_expr() とまったく同じ内容になっているので、置き換えて OK。
--- a/vgcg.rb
+++ b/vgcg.rb
@@ -116,28 +116,6 @@ def codegen_while(fn_arg_names, lvar_names, rest)
puts ""
end
-def _codegen_expr_push(fn_arg_names, lvar_names, val)
- case val
- when Integer
- puts " cp #{val} reg_a"
- when String
- case
- when fn_arg_names.include?(val)
- cp_src = to_fn_arg_addr(fn_arg_names, val)
- puts " cp #{cp_src} reg_a"
- when lvar_names.include?(val)
- cp_src = to_lvar_addr(lvar_names, val)
- puts " cp #{cp_src} reg_a"
- else
- raise not_yet_impl("val", val)
- end
- when Array
- _codegen_expr_binary(fn_arg_names, lvar_names, val)
- else
- raise not_yet_impl("val", val)
- end
-end
-
def _codegen_expr_add
puts " pop reg_b"
puts " pop reg_a"
@@ -206,9 +184,9 @@ def _codegen_expr_binary(fn_arg_names, lvar_names, expr)
arg_l = args[0]
arg_r = args[1]
- _codegen_expr_push(fn_arg_names, lvar_names, arg_l)
+ codegen_expr(fn_arg_names, lvar_names, arg_l)
puts " push reg_a"
- _codegen_expr_push(fn_arg_names, lvar_names, arg_r)
+ codegen_expr(fn_arg_names, lvar_names, arg_r)
puts " push reg_a"
case operator
_codegen_expr_push() はこの2箇所でしか使われていないため、用済みになりました。
_codegen_call_push_fn_arg() を置き換え
_codegen_expr_push() と同じ要領で。
--- a/vgcg.rb
+++ b/vgcg.rb
@@ -225,34 +225,14 @@ def codegen_expr(fn_arg_names, lvar_names, expr)
end
end
-def _codegen_call_push_fn_arg(fn_arg_names, lvar_names, fn_arg)
- push_arg =
- case fn_arg
- when Integer
- fn_arg
- when String
- case
- when fn_arg_names.include?(fn_arg)
- to_fn_arg_addr(fn_arg_names, fn_arg)
- when lvar_names.include?(fn_arg)
- to_lvar_addr(lvar_names, fn_arg)
- else
- raise not_yet_impl(fn_arg)
- end
- else
- raise not_yet_impl(fn_arg)
- end
-
- puts " push #{push_arg}"
-end
-
def codegen_call(fn_arg_names, lvar_names, stmt_rest)
fn_name, *fn_args = stmt_rest
fn_args.reverse.each do |fn_arg|
- _codegen_call_push_fn_arg(
+ codegen_expr(
fn_arg_names, lvar_names, fn_arg
)
+ puts " push reg_a"
end
codegen_vm_comment("call #{fn_name}")
@@ -265,9 +245,10 @@ def codegen_call_set(fn_arg_names, lvar_names, stmt_rest)
fn_name, *fn_args = fn_temp
fn_args.reverse.each do |fn_arg|
- _codegen_call_push_fn_arg(
+ codegen_expr(
fn_arg_names, lvar_names, fn_arg
)
+ puts " push reg_a"
end
codegen_vm_comment("call_set #{fn_name}")
_codegen_call_push_fn_arg() は codegen_expr() と完全に同じではないため、
この修正によって、関数呼び出しの引数に二項演算を置けるようになります。
修正前:
call foo_func(1 + 2);
//=> コンパイルエラー
修正後:
call foo_func(1 + 2);
//=> コンパイルエラーにならない
挙動が変わってしまいますが、この変化は特に問題ないですし、記述力が高まるのでよしとします。
codegen_set() のセットする値のコード生成処理の部分
他にも置き換えられる箇所がないかと見ていきます。
codegen_set() でセットする値のコード生成を行っている部分も置き換えられそう。
var x = vram[{ローカル変数}];
のような文を書けるようにするために
codegen_set() には vram[{ローカル変数}]
の部分のコード生成処理があるのですが、
今の codegn_expr() ではこれはできません。
というわけで codegen_expr() で vram[{ローカル変数}]
も扱えるようにします。
一応問題ないか具体例で考えてみましょうか。
while (vram[vi] == x) { ... }
call foo_func(vram[vi] + 1);
たぶん大丈夫。
まずは codegen_expr() で
vram[{ローカル変数}]
も扱えるようにします。
codegen_set() からコピペして codegen_expr() を修正し、
codegen_set() からは codegen_expr() を呼び出すように修正。
--- a/vgcg.rb
+++ b/vgcg.rb
@@ -215,6 +215,15 @@ def codegen_expr(fn_arg_names, lvar_names, expr)
when lvar_names.include?(expr)
cp_src = to_lvar_addr(lvar_names, expr)
puts " cp #{cp_src} reg_a"
+ when _match_vram_ref(expr)
+ var_name = _match_vram_ref(expr)
+ case
+ when lvar_names.include?(var_name)
+ vram_addr = to_lvar_addr(lvar_names, var_name)
+ puts " get_vram #{vram_addr} reg_a"
+ else
+ raise not_yet_impl("rest", rest)
+ end
else
raise not_yet_impl("expr", expr)
end
@@ -277,39 +286,8 @@ def codegen_set(fn_arg_names, lvar_names, rest)
dest = rest[0]
expr = rest[1]
- src_val =
- case expr
- when Integer
- expr
- when Array
- _codegen_expr_binary(fn_arg_names, lvar_names, expr)
- "reg_a"
- when String
- case
- when fn_arg_names.include?(expr)
- to_fn_arg_addr(fn_arg_names, expr)
- when lvar_names.include?(expr)
- to_lvar_addr(lvar_names, expr)
- when _match_vram_addr(expr)
- vram_addr = _match_vram_addr(expr)
- puts " get_vram #{vram_addr} reg_a"
- "reg_a"
- when _match_vram_ref(expr)
- var_name = _match_vram_ref(expr)
- case
- when lvar_names.include?(var_name)
- lvar_addr = to_lvar_addr(lvar_names, var_name)
- puts " get_vram #{ lvar_addr } reg_a"
- else
- raise not_yet_impl("rest", rest)
- end
- "reg_a"
- else
- raise not_yet_impl("rest", rest)
- end
- else
- raise not_yet_impl("set src_val", rest)
- end
+ codegen_expr(fn_arg_names, lvar_names, expr)
+ src_val = "reg_a"
case
when _match_vram_addr(dest)
codegen_set() の鬱陶しかった部分がすっきり!
reg_a
への値のセットと push reg_a
が分かれる都合で VM の修正も必要でした。
set_vram {VRAMのアドレス} reg_a
のパターンです。
--- a/vgvm.rb
+++ b/vgvm.rb
@@ -448,6 +448,8 @@ class Vm
case arg2
when Integer
arg2
+ when "reg_a"
+ @reg_a
when /^ind:/
stack_addr = calc_indirect_addr(arg2)
@mem.stack[stack_addr]
あとは codegen_return() も置き換えられます。
なんかパズルみたいで楽しい。
--- a/vgcg.rb
+++ b/vgcg.rb
@@ -312,30 +312,7 @@ end
def codegen_return(lvar_names, stmt_rest)
retval = stmt_rest[0]
-
- case retval
- when Integer
- puts " cp #{retval} reg_a"
- when String
- case
- when _match_vram_ref(retval)
- var_name = _match_vram_ref(retval)
- case
- when lvar_names.include?(var_name)
- lvar_addr = to_lvar_addr(lvar_names, var_name)
- puts " get_vram #{lvar_addr} reg_a"
- else
- raise not_yet_impl("retval", retval)
- end
- when lvar_names.include?(retval)
- lvar_addr = to_lvar_addr(lvar_names, retval)
- puts " cp #{lvar_addr} reg_a"
- else
- raise not_yet_impl("retval", retval)
- end
- else
- raise not_yet_impl("retval", retval)
- end
+ codegen_expr([], lvar_names, retval);
end
def codegen_vm_comment(comment)
これもすっきり。うーむ、もっと早くやってればよかった……。
while, case の条件式
while (ここ) { ... }
case {
(ここ) { ... }
}
これらは現状では二項演算のみ受け付けるようになっていますが、
ここもついでに codegen_expr() に置き換えてしまいます。
while (do_break) { ... }
みたいなことができると嬉しいと思いますし、
二項演算だけに限定する理由もあんまりないと思いますし。
--- a/vgcg.rb
+++ b/vgcg.rb
@@ -48,7 +48,7 @@ def codegen_case(fn_arg_names, lvar_names, when_blocks)
when "eq"
# 式の結果が reg_a に入る
puts " # -->> expr"
- _codegen_expr_binary(fn_arg_names, lvar_names, cond)
+ codegen_expr(fn_arg_names, lvar_names, cond)
puts " # <<-- expr"
# 式の結果と比較するための値を reg_b に入れる
@@ -94,7 +94,7 @@ def codegen_while(fn_arg_names, lvar_names, rest)
puts "label #{label_begin}"
# 条件の評価 ... 結果が reg_a に入る
- _codegen_expr_binary(fn_arg_names, lvar_names, cond_expr)
+ codegen_expr(fn_arg_names, lvar_names, cond_expr)
# 比較対象の値(真)をセット
puts " set_reg_b 1"
puts " compare"
ここまでの修正により、 _codegen_expr_binary()
の呼び出し元が
codegen_expr()
だけになりました。
codegen_case() の不要な分岐を削除
codegen_expr()
でさまざまな式を統一的に扱えるようになったため、演算子を見て振り分ける処理が不要になりました。
eq
以外をあえてエラーにする理由も特にないですし、行数も減ってすっきりさせられるので削除します。
--- a/vgcg.rb
+++ b/vgcg.rb
@@ -40,12 +40,9 @@ def codegen_case(fn_arg_names, lvar_names, when_blocks)
when_blocks.each do |when_block|
when_idx += 1
cond, *rest = when_block
- cond_head, *cond_rest = cond
puts " # when_#{label_id}_#{when_idx}: #{cond.inspect}"
- case cond_head
- when "eq"
# 式の結果が reg_a に入る
puts " # -->> expr"
codegen_expr(fn_arg_names, lvar_names, cond)
@@ -67,10 +64,6 @@ def codegen_case(fn_arg_names, lvar_names, when_blocks)
# 偽の場合ここにジャンプ
puts "label #{label_end_when_head}_#{when_idx}"
-
- else
- raise not_yet_impl("cond_head", cond_head)
- end
end
puts "label #{label_end}"
コードの量はどのくらい減ったか
[before]
41 common.rb
65 vgasm.rb
478 vgcg.rb
58 vglexer.rb
454 vgparser.rb
512 vgvm.rb
1608 合計
[after]
64 vgasm.rb
408 vgcg.rb
58 vglexer.rb
454 vgparser.rb
514 vgvm.rb
41 common.rb
1539 合計
70行近く減らせました! 🙌
(まあ、元が冗長すぎたんですけどね)