パースに失敗したとき、失敗した位置をたとえば次のように表示したいですよね?
parse error: line 2, col 5
foo bar
^
短いのでとりあえず全部貼ります。
# parser.y
class Parser
rule
program: "a" "b" "c" "d" ";"
{
puts "program found"
result = val
}
end
---- header
TokenValue = Struct.new(:value, :pos)
---- inner
def initialize(src, tokens)
@src = src
@tokens = tokens
end
def next_token
@tokens.shift
end
def parse
do_parse
end
def get_lineno_and_range(pos)
lineno = 1
line_start = 0
range = nil
@src.each_line do |line|
next_line_start = line_start + line.size
range = line_start .. (next_line_start - 1)
break if range.include?(pos)
line_start = next_line_start
lineno += 1
end
[lineno, range]
end
def on_error(token_id, val, vstack)
lineno, range = get_lineno_and_range(val.pos)
colno = val.pos - range.begin + 1
line = @src[range]
puts "parse error: line #{lineno}, col #{colno}"
puts line
print " " * (colno - 1), "^\n"
end
---- footer
def tokenize(src)
tokens = []
pos = 0
while pos < src.size
c = src[pos]
case c
when /\s/
pos += 1
else
tokens << [c, TokenValue.new(c, pos)]
pos += 1
end
end
tokens
end
src = File.read(ARGV[0])
tokens = tokenize(src)
parser = Parser.new(src, tokens)
result = parser.parse()
puts "result: " + result.inspect
a
b
c
d
;
というトークン列だけを受理するパーサです。
入力となるソースコードではトークン同士をスペースや改行で区切ります。
パースに成功する例:
a
b
c d
;
たとえば d
を X
に置き換えた次のソースコードを与えるとパースに失敗します。
a
b
c X
;
失敗する場合の実行例:
$ racc parser.y -o parser.rb
$ ruby parser.rb sample_ng.txt
parse error: line 4, col 3
c X
^
result: nil
短いので上に貼ったコードを読んだ方が早いと思いますが、一応簡単にメモ。
- トークンの開始位置(元のソースコードで何文字目か)をトークン値に持たせておく
- 元のソースコードを
Parser#src
として保持しておく - パースに失敗したら
Parser#src
と失敗したトークンの開始位置を使って
元のソースコード上の行と桁を割り出す
メモ
終端/非終端にかかわらず、すべての記号はそれに対応する値(かっこよく言うと semantic value)を持っていて、これを使って情報をやりとりすることができる。
(略)
値としては好きなRubyのオブジェクト1個が使えるので、これはつまり なんでも送れる と同義だ。
『Rubyを256倍使うための本 無道編 (Amazon)』 p46
とのことなので、上記ではトークンの値として TokenValue
クラスのインスタンスを使っています。
関連
以下は Racc 関連ではありませんが、Ruby + パーサ関連で書いたものということで。
他に Ruby 関連で書いたもの