mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
562 lines
15 KiB
Ruby
562 lines
15 KiB
Ruby
#--
|
|
#
|
|
#
|
|
#
|
|
# Copyright (c) 1999-2006 Minero Aoki
|
|
#
|
|
# This program is free software.
|
|
# You can distribute/modify this program under the terms of
|
|
# the GNU LGPL, Lesser General Public License version 2.1.
|
|
# For details of the GNU LGPL, see the file "COPYING".
|
|
#
|
|
#++
|
|
|
|
require 'racc'
|
|
require 'racc/compat'
|
|
require 'racc/grammar'
|
|
require 'racc/parserfilegenerator'
|
|
require 'racc/sourcetext'
|
|
require 'stringio'
|
|
|
|
module Racc
|
|
|
|
grammar = Grammar.define {
|
|
g = self
|
|
|
|
g.class = seq(:CLASS, :cname, many(:param), :RULE, :rules, option(:END))
|
|
|
|
g.cname = seq(:rubyconst) {|name|
|
|
@result.params.classname = name
|
|
}\
|
|
| seq(:rubyconst, "<", :rubyconst) {|c, _, s|
|
|
@result.params.classname = c
|
|
@result.params.superclass = s
|
|
}
|
|
|
|
g.rubyconst = separated_by1(:colon2, :SYMBOL) {|syms|
|
|
syms.map {|s| s.to_s }.join('::')
|
|
}
|
|
|
|
g.colon2 = seq(':', ':')
|
|
|
|
g.param = seq(:CONV, many1(:convdef), :END) {|*|
|
|
#@grammar.end_convert_block # FIXME
|
|
}\
|
|
| seq(:PRECHIGH, many1(:precdef), :PRECLOW) {|*|
|
|
@grammar.end_precedence_declaration true
|
|
}\
|
|
| seq(:PRECLOW, many1(:precdef), :PRECHIGH) {|*|
|
|
@grammar.end_precedence_declaration false
|
|
}\
|
|
| seq(:START, :symbol) {|_, sym|
|
|
@grammar.start_symbol = sym
|
|
}\
|
|
| seq(:TOKEN, :symbols) {|_, syms|
|
|
syms.each do |s|
|
|
s.should_terminal
|
|
end
|
|
}\
|
|
| seq(:OPTION, :options) {|_, syms|
|
|
syms.each do |opt|
|
|
case opt
|
|
when 'result_var'
|
|
@result.params.result_var = true
|
|
when 'no_result_var'
|
|
@result.params.result_var = false
|
|
when 'omit_action_call'
|
|
@result.params.omit_action_call = true
|
|
when 'no_omit_action_call'
|
|
@result.params.omit_action_call = false
|
|
else
|
|
raise CompileError, "unknown option: #{opt}"
|
|
end
|
|
end
|
|
}\
|
|
| seq(:EXPECT, :DIGIT) {|_, num|
|
|
if @grammar.n_expected_srconflicts
|
|
raise CompileError, "`expect' seen twice"
|
|
end
|
|
@grammar.n_expected_srconflicts = num
|
|
}
|
|
|
|
g.convdef = seq(:symbol, :STRING) {|sym, code|
|
|
sym.serialized = code
|
|
}
|
|
|
|
g.precdef = seq(:LEFT, :symbols) {|_, syms|
|
|
@grammar.declare_precedence :Left, syms
|
|
}\
|
|
| seq(:RIGHT, :symbols) {|_, syms|
|
|
@grammar.declare_precedence :Right, syms
|
|
}\
|
|
| seq(:NONASSOC, :symbols) {|_, syms|
|
|
@grammar.declare_precedence :Nonassoc, syms
|
|
}
|
|
|
|
g.symbols = seq(:symbol) {|sym|
|
|
[sym]
|
|
}\
|
|
| seq(:symbols, :symbol) {|list, sym|
|
|
list.push sym
|
|
list
|
|
}\
|
|
| seq(:symbols, "|")
|
|
|
|
g.symbol = seq(:SYMBOL) {|sym| @grammar.intern(sym) }\
|
|
| seq(:STRING) {|str| @grammar.intern(str) }
|
|
|
|
g.options = many(:SYMBOL) {|syms| syms.map {|s| s.to_s } }
|
|
|
|
g.rules = option(:rules_core) {|list|
|
|
add_rule_block list unless list.empty?
|
|
nil
|
|
}
|
|
|
|
g.rules_core = seq(:symbol) {|sym|
|
|
[sym]
|
|
}\
|
|
| seq(:rules_core, :rule_item) {|list, i|
|
|
list.push i
|
|
list
|
|
}\
|
|
| seq(:rules_core, ';') {|list, *|
|
|
add_rule_block list unless list.empty?
|
|
list.clear
|
|
list
|
|
}\
|
|
| seq(:rules_core, ':') {|list, *|
|
|
next_target = list.pop
|
|
add_rule_block list unless list.empty?
|
|
[next_target]
|
|
}
|
|
|
|
g.rule_item = seq(:symbol)\
|
|
| seq("|") {|*|
|
|
OrMark.new(@scanner.lineno)
|
|
}\
|
|
| seq("=", :symbol) {|_, sym|
|
|
Prec.new(sym, @scanner.lineno)
|
|
}\
|
|
| seq(:ACTION) {|src|
|
|
UserAction.source_text(src)
|
|
}
|
|
}
|
|
|
|
GrammarFileParser = grammar.parser_class
|
|
|
|
if grammar.states.srconflict_exist?
|
|
raise 'Racc boot script fatal: S/R conflict in build'
|
|
end
|
|
if grammar.states.rrconflict_exist?
|
|
raise 'Racc boot script fatal: R/R conflict in build'
|
|
end
|
|
|
|
class GrammarFileParser # reopen
|
|
|
|
class Result
|
|
def initialize(grammar)
|
|
@grammar = grammar
|
|
@params = ParserFileGenerator::Params.new
|
|
end
|
|
|
|
attr_reader :grammar
|
|
attr_reader :params
|
|
end
|
|
|
|
def GrammarFileParser.parse_file(filename)
|
|
parse(File.read(filename), filename, 1)
|
|
end
|
|
|
|
def GrammarFileParser.parse(src, filename = '-', lineno = 1)
|
|
new().parse(src, filename, lineno)
|
|
end
|
|
|
|
def initialize(debug_flags = DebugFlags.new)
|
|
@yydebug = debug_flags.parse
|
|
end
|
|
|
|
def parse(src, filename = '-', lineno = 1)
|
|
@filename = filename
|
|
@lineno = lineno
|
|
@scanner = GrammarFileScanner.new(src, @filename)
|
|
@scanner.debug = @yydebug
|
|
@grammar = Grammar.new
|
|
@result = Result.new(@grammar)
|
|
@embedded_action_seq = 0
|
|
yyparse @scanner, :yylex
|
|
parse_user_code
|
|
@result.grammar.init
|
|
@result
|
|
end
|
|
|
|
private
|
|
|
|
def next_token
|
|
@scanner.scan
|
|
end
|
|
|
|
def on_error(tok, val, _values)
|
|
if val.respond_to?(:id2name)
|
|
v = val.id2name
|
|
elsif val.kind_of?(String)
|
|
v = val
|
|
else
|
|
v = val.inspect
|
|
end
|
|
raise CompileError, "#{location()}: unexpected token '#{v}'"
|
|
end
|
|
|
|
def location
|
|
"#{@filename}:#{@lineno - 1 + @scanner.lineno}"
|
|
end
|
|
|
|
def add_rule_block(list)
|
|
sprec = nil
|
|
target = list.shift
|
|
case target
|
|
when OrMark, UserAction, Prec
|
|
raise CompileError, "#{target.lineno}: unexpected symbol #{target.name}"
|
|
end
|
|
curr = []
|
|
list.each do |i|
|
|
case i
|
|
when OrMark
|
|
add_rule target, curr, sprec
|
|
curr = []
|
|
sprec = nil
|
|
when Prec
|
|
raise CompileError, "'=<prec>' used twice in one rule" if sprec
|
|
sprec = i.symbol
|
|
else
|
|
curr.push i
|
|
end
|
|
end
|
|
add_rule target, curr, sprec
|
|
end
|
|
|
|
def add_rule(target, list, sprec)
|
|
if list.last.kind_of?(UserAction)
|
|
act = list.pop
|
|
else
|
|
act = UserAction.empty
|
|
end
|
|
list.map! {|s| s.kind_of?(UserAction) ? embedded_action(s) : s }
|
|
rule = Rule.new(target, list, act)
|
|
rule.specified_prec = sprec
|
|
@grammar.add rule
|
|
end
|
|
|
|
def embedded_action(act)
|
|
sym = @grammar.intern("@#{@embedded_action_seq += 1}".intern, true)
|
|
@grammar.add Rule.new(sym, [], act)
|
|
sym
|
|
end
|
|
|
|
#
|
|
# User Code Block
|
|
#
|
|
|
|
def parse_user_code
|
|
line = @scanner.lineno
|
|
_, *blocks = *@scanner.epilogue.split(/^----/)
|
|
blocks.each do |block|
|
|
header, *body = block.lines.to_a
|
|
label0, pathes = *header.sub(/\A-+/, '').split('=', 2)
|
|
label = canonical_label(label0)
|
|
(pathes ? pathes.strip.split(' ') : []).each do |path|
|
|
add_user_code label, SourceText.new(File.read(path), path, 1)
|
|
end
|
|
add_user_code label, SourceText.new(body.join(''), @filename, line + 1)
|
|
line += (1 + body.size)
|
|
end
|
|
end
|
|
|
|
USER_CODE_LABELS = {
|
|
'header' => :header,
|
|
'prepare' => :header, # obsolete
|
|
'inner' => :inner,
|
|
'footer' => :footer,
|
|
'driver' => :footer # obsolete
|
|
}
|
|
|
|
def canonical_label(src)
|
|
label = src.to_s.strip.downcase.slice(/\w+/)
|
|
unless USER_CODE_LABELS.key?(label)
|
|
raise CompileError, "unknown user code type: #{label.inspect}"
|
|
end
|
|
label
|
|
end
|
|
|
|
def add_user_code(label, src)
|
|
@result.params.public_send(USER_CODE_LABELS[label]).push src
|
|
end
|
|
|
|
end
|
|
|
|
|
|
class GrammarFileScanner
|
|
|
|
def initialize(str, filename = '-')
|
|
@lines = str.b.split(/\n|\r\n|\r/)
|
|
@filename = filename
|
|
@lineno = -1
|
|
@line_head = true
|
|
@in_rule_blk = false
|
|
@in_conv_blk = false
|
|
@in_block = nil
|
|
@epilogue = ''
|
|
@debug = false
|
|
next_line
|
|
end
|
|
|
|
attr_reader :epilogue
|
|
|
|
def lineno
|
|
@lineno + 1
|
|
end
|
|
|
|
attr_accessor :debug
|
|
|
|
def yylex(&block)
|
|
unless @debug
|
|
yylex0(&block)
|
|
else
|
|
yylex0 do |sym, tok|
|
|
$stderr.printf "%7d %-10s %s\n", lineno(), sym.inspect, tok.inspect
|
|
yield [sym, tok]
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def yylex0
|
|
begin
|
|
until @line.empty?
|
|
@line.sub!(/\A\s+/, '')
|
|
if /\A\#/ =~ @line
|
|
break
|
|
elsif /\A\/\*/ =~ @line
|
|
skip_comment
|
|
elsif s = reads(/\A[a-zA-Z_]\w*/)
|
|
yield [atom_symbol(s), s.intern]
|
|
elsif s = reads(/\A\d+/)
|
|
yield [:DIGIT, s.to_i]
|
|
elsif ch = reads(/\A./)
|
|
case ch
|
|
when '"', "'"
|
|
yield [:STRING, eval(scan_quoted(ch))]
|
|
when '{'
|
|
lineno = lineno()
|
|
yield [:ACTION, SourceText.new(scan_action(), @filename, lineno)]
|
|
else
|
|
if ch == '|'
|
|
@line_head = false
|
|
end
|
|
yield [ch, ch]
|
|
end
|
|
else
|
|
end
|
|
end
|
|
end while next_line()
|
|
yield nil
|
|
end
|
|
|
|
def next_line
|
|
@lineno += 1
|
|
@line = @lines[@lineno]
|
|
if not @line or /\A----/ =~ @line
|
|
@epilogue = @lines.join("\n")
|
|
@lines.clear
|
|
@line = nil
|
|
if @in_block
|
|
@lineno -= 1
|
|
scan_error! sprintf('unterminated %s', @in_block)
|
|
end
|
|
false
|
|
else
|
|
@line.sub!(/(?:\n|\r\n|\r)\z/, '')
|
|
@line_head = true
|
|
true
|
|
end
|
|
end
|
|
|
|
ReservedWord = {
|
|
'right' => :RIGHT,
|
|
'left' => :LEFT,
|
|
'nonassoc' => :NONASSOC,
|
|
'preclow' => :PRECLOW,
|
|
'prechigh' => :PRECHIGH,
|
|
'token' => :TOKEN,
|
|
'convert' => :CONV,
|
|
'options' => :OPTION,
|
|
'start' => :START,
|
|
'expect' => :EXPECT,
|
|
'class' => :CLASS,
|
|
'rule' => :RULE,
|
|
'end' => :END
|
|
}
|
|
|
|
def atom_symbol(token)
|
|
if token == 'end'
|
|
symbol = :END
|
|
@in_conv_blk = false
|
|
@in_rule_blk = false
|
|
else
|
|
if @line_head and not @in_conv_blk and not @in_rule_blk
|
|
symbol = ReservedWord[token] || :SYMBOL
|
|
else
|
|
symbol = :SYMBOL
|
|
end
|
|
case symbol
|
|
when :RULE then @in_rule_blk = true
|
|
when :CONV then @in_conv_blk = true
|
|
end
|
|
end
|
|
@line_head = false
|
|
symbol
|
|
end
|
|
|
|
def skip_comment
|
|
@in_block = 'comment'
|
|
until m = /\*\//.match(@line)
|
|
next_line
|
|
end
|
|
@line = m.post_match
|
|
@in_block = nil
|
|
end
|
|
|
|
$raccs_print_type = false
|
|
|
|
def scan_action
|
|
buf = String.new
|
|
nest = 1
|
|
pre = nil
|
|
@in_block = 'action'
|
|
begin
|
|
pre = nil
|
|
if s = reads(/\A\s+/)
|
|
# does not set 'pre'
|
|
buf << s
|
|
end
|
|
until @line.empty?
|
|
if s = reads(/\A[^'"`{}%#\/\$]+/)
|
|
buf << (pre = s)
|
|
next
|
|
end
|
|
case ch = read(1)
|
|
when '{'
|
|
nest += 1
|
|
buf << (pre = ch)
|
|
when '}'
|
|
nest -= 1
|
|
if nest == 0
|
|
@in_block = nil
|
|
buf.sub!(/[ \t\f]+\z/, '')
|
|
return buf
|
|
end
|
|
buf << (pre = ch)
|
|
when '#' # comment
|
|
buf << ch << @line
|
|
break
|
|
when "'", '"', '`'
|
|
buf << (pre = scan_quoted(ch))
|
|
when '%'
|
|
if literal_head? pre, @line
|
|
# % string, regexp, array
|
|
buf << ch
|
|
case ch = read(1)
|
|
when /[qQx]/n
|
|
buf << ch << (pre = scan_quoted(read(1), '%string'))
|
|
when /wW/n
|
|
buf << ch << (pre = scan_quoted(read(1), '%array'))
|
|
when /s/n
|
|
buf << ch << (pre = scan_quoted(read(1), '%symbol'))
|
|
when /r/n
|
|
buf << ch << (pre = scan_quoted(read(1), '%regexp'))
|
|
when /[a-zA-Z0-9= ]/n # does not include "_"
|
|
scan_error! "unknown type of % literal '%#{ch}'"
|
|
else
|
|
buf << (pre = scan_quoted(ch, '%string'))
|
|
end
|
|
else
|
|
# operator
|
|
buf << '||op->' if $raccs_print_type
|
|
buf << (pre = ch)
|
|
end
|
|
when '/'
|
|
if literal_head? pre, @line
|
|
# regexp
|
|
buf << (pre = scan_quoted(ch, 'regexp'))
|
|
else
|
|
# operator
|
|
buf << '||op->' if $raccs_print_type
|
|
buf << (pre = ch)
|
|
end
|
|
when '$' # gvar
|
|
buf << ch << (pre = read(1))
|
|
else
|
|
raise 'racc: fatal: must not happen'
|
|
end
|
|
end
|
|
buf << "\n"
|
|
end while next_line()
|
|
raise 'racc: fatal: scan finished before parser finished'
|
|
end
|
|
|
|
def literal_head?(pre, post)
|
|
(!pre || /[a-zA-Z_0-9]/n !~ pre[-1,1]) &&
|
|
!post.empty? && /\A[\s\=]/n !~ post
|
|
end
|
|
|
|
def read(len)
|
|
s = @line[0, len]
|
|
@line = @line[len .. -1]
|
|
s
|
|
end
|
|
|
|
def reads(re)
|
|
m = re.match(@line) or return nil
|
|
@line = m.post_match
|
|
m[0]
|
|
end
|
|
|
|
def scan_quoted(left, tag = 'string')
|
|
buf = left.dup
|
|
buf = "||#{tag}->" + buf if $raccs_print_type
|
|
re = get_quoted_re(left)
|
|
sv, @in_block = @in_block, tag
|
|
begin
|
|
if s = reads(re)
|
|
buf << s
|
|
break
|
|
else
|
|
buf << @line
|
|
end
|
|
end while next_line()
|
|
@in_block = sv
|
|
buf << "<-#{tag}||" if $raccs_print_type
|
|
buf
|
|
end
|
|
|
|
LEFT_TO_RIGHT = {
|
|
'(' => ')',
|
|
'{' => '}',
|
|
'[' => ']',
|
|
'<' => '>'
|
|
}
|
|
|
|
CACHE = {}
|
|
|
|
def get_quoted_re(left)
|
|
term = Regexp.quote(LEFT_TO_RIGHT[left] || left)
|
|
CACHE[left] ||= /\A[^#{term}\\]*(?:\\.[^\\#{term}]*)*#{term}/
|
|
end
|
|
|
|
def scan_error!(msg)
|
|
raise CompileError, "#{lineno()}: #{msg}"
|
|
end
|
|
|
|
end
|
|
|
|
end # module Racc
|