スタック上の非終端記号を一致規則の rhs に置き換える一般的な方法では、予測された時点で文法構造を事実上忘れてしまうため、問題が発生します。ただし、AST を生成するには、後でルール解析が完了したときにその構造が必要になります。
非終端記号を一致ルールの rhs 記号に置き換えるのではなく、そのままにして、一致した記号をリスト オブジェクトとしてプッシュします。このようにして、スタックは文法の階層構造を保持します。
解析は、一番上のリストのシンボルを消費します。リストの枯渇は、ルールの完了に対応します。非終端記号は、予測されたときではなく、ルールが完了したときにスタックから削除されます。
スタックが消費されると、関連するルールを記憶し、解析されたトークンを格納する必然的な AST 構造を構築します。したがって、スタックは、解析された AST に流れ込む予測された AST のように機能します。
これは、呼び出しフレームのスタックとしてシンボル リストのスタックを使用して、再帰降下パーサーの呼び出し階層をエミュレートしていると考えることができます。
ルビーで:
# g is the grammar; a list of rules
# s is a terminal sequence to parse
# note, this code does not tokenize EOF
def parse(g, s)
tab = gen_table(g)
stack = [[g.start_sym]]
# intermediate ast node format: [rule-action, symbols...]
ast = [[->(rhs){[:_S, rhs[0]]}]]
loop do
puts "PARSE\n #{s}\n #{stack}\n #{ast}"
if stack.first.empty?
raise "extraneous input" if not s.empty?
break
end
if stack.last.empty? # rule complete
stack.pop
node = ast.pop
# transform the node (eg to a class) using the associated rule-action
node = node.first.(node.drop(1))
ast.last.push(node)
stack.last.shift # rm sym from stack after completing it
next
end
raise "incomplete input" if s.empty?
tok = s.first
topsym = stack.last.first
if topsym.is_a? String # terminal
raise "mismatch #{tok} != #{topsym}" if tok != topsym
stack.last.shift
ast.last.push(s.shift)
elsif topsym.is_a? Symbol # nonterminal
ri = tab[topsym][tok]
raise "no rule for #{topsym}, #{tok}" if ri.nil?
stack.push(g[ri].rhs.clone)
ast.push([g[ri].action])
end
end
node = ast.first
node.first.(node.drop(1))
end