vm2gol v2 (59) while, case の真偽判定の変更 / 予約語の判定の改良



真偽判定の変更(コード生成)

while 文と case 文では 条件式の評価結果を 1 と比較して真偽を判定していました。 疑似コードで書くとこう。

# 修正前

if 条件式の評価結果 == 1
  真と判定 => while_true ラベルにジャンプ
else
  偽と判定 => end_while ラベルにジャンプ(ループを抜ける)
end

label while_true
ループの処理本体

label end_while

この場合、たとえば条件式の評価結果が 2 だった場合も偽になってしまいます。 ちょっと違和感がありますね。

というわけで、次のように 0 と比較する形に変更しました。

# 修正後

if 条件式の評価結果 == 0
  偽と判定 => end_while ラベルにジャンプ(ループを抜ける)
else
  真と判定 => while_true ラベルにジャンプ
end

label while_true
ループの処理本体

label end_while

この方が整数から真偽値へのマッピングのルールとして筋が良い気がします。少なくとも C や JavaScript などに近い挙動にはなります。

さらに、この修正により、真だった場合の while_true ラベルへのジャンプが不要(すぐ次のラベルにジャンプしている)になります。これも消してしまえるので、最終的に次のようになりました。

if 条件式の評価結果 == 0
  偽 => end_while ラベルにジャンプ(ループを抜ける)
end

ループの処理本体

label end_while

コンパイラのコードもコンパイラが生成するコードもちょっとだけコンパクトに。

予約語の判定の改善(レキサ)

予約語と識別子を切り出している箇所(修正前)です。

  while pos < src.size
    rest = src[pos .. -1]

    case rest
    # ...

    # 予約語の判定
    when /\A(func|set|var|call_set|call|return|case|while|_cmt|_debug)[^a-z_]/
      str = $1
      tokens << Token.new(:kw, str)
      pos += str.size
      # ...

    # 識別子の判定
    when /\A([a-z_][a-z0-9_]*)/
      str = $1
      tokens << Token.new(:ident, str)
      pos += str.size
    else
      # ...
    end
  end

この、予約語用の正規表現[^a-z_] の部分がなんか微妙で、なんとかできないかなという気持ちが前からありました。

そこで、次のように修正しました。

--- a/vglexer.rb
+++ b/vglexer.rb
@@ -1,5 +1,10 @@
 require_relative "./common"
 
+KEYWORDS = [
+  "func", "set", "var", "call_set", "call", "return", "case", "while",
+  "_cmt", "_debug"
+]
+
 def tokenize(src)
   tokens = []
 
@@ -19,10 +24,6 @@ def tokenize(src)
       str = $1
       tokens << Token.new(:str, str)
       pos += str.size + 2
-    when /\A(func|set|var|call_set|call|return|case|while|_cmt|_debug)[^a-z_]/
-      str = $1
-      tokens << Token.new(:kw, str)
-      pos += str.size
     when /\A(-?[0-9]+)/
       str = $1
       tokens << Token.new(:int, str.to_i)
@@ -33,7 +34,8 @@ def tokenize(src)
       pos += str.size
     when /\A([a-z_][a-z0-9_]*)/
       str = $1
-      tokens << Token.new(:ident, str)
+      type = KEYWORDS.include?(str) ? :kw : :ident
+      tokens << Token.new(type, str)
       pos += str.size
     else
       p_e rest[0...100]

曖昧さが減り、安定感のあるコードになったと思います。パターンの考慮漏れについての不安が小さいコードになったというか。

これは正規表現が使えない(標準ライブラリ等に含まれていない)言語に移植していたときに思いついた方法で、それを Ruby 版にフィードバックしたものです。

ちなみに、このように書けるのは 予約語のパターンが識別子のパターンに含まれるためですね。 その前提が崩れるとダメですが、まあ、問題ないでしょう。

その他

  • パーサ: loop を while で書き換え
  • トークンの値を取り出す処理を Token#get_value に抽出