lib/treetop/runtime/compiled_parser.rb
module Treetop module Runtime class CompiledParser include Treetop::Runtime attr_reader :input, :index, :max_terminal_failure_index attr_writer :root attr_accessor :consume_all_input alias :consume_all_input? :consume_all_input def initialize self.consume_all_input = true end def parse(input, options = {}) prepare_to_parse(input) @index = options[:index] if options[:index] result = send("_nt_#{options[:root] || root}") should_consume_all = options.include?(:consume_all_input) ? options[:consume_all_input] : consume_all_input? if (should_consume_all && index != input.size) if index > max_terminal_failure_index # Otherwise the failure is already explained terminal_parse_failure('<END OF INPUT>', true) end return nil end return SyntaxNode.new(input, index...(index + 1)) if result == true return result end def failure_index max_terminal_failure_index end def failure_line @terminal_failures && input.line_of(failure_index) end def failure_column @terminal_failures && input.column_of(failure_index) end OtherThan = 'something other than ' def failure_reason return nil unless (tf = terminal_failures) && tf.size > 0 "Expected " + (tf.size == 1 ? (tf[0].unexpected ? OtherThan : '')+tf[0].expected_string : "one of #{tf.map{|f| (f.unexpected ? OtherThan : '')+f.expected_string}.uniq*', '}" ) + " at line #{failure_line}, column #{failure_column} (byte #{failure_index+1})" + (failure_index > 0 ? " after #{input[index...failure_index]}" : '') end def terminal_failures if @terminal_failures.empty? || @terminal_failures[-1].is_a?(TerminalParseFailure) @terminal_failures else @terminal_failures.map! {|tf_ary| tf_ary.is_a?(TerminalParseFailure) ? tf_ary : TerminalParseFailure.new(*tf_ary) } end end protected attr_reader :node_cache, :input_length attr_writer :index def prepare_to_parse(input) @input = input @input_length = input.length reset_index @node_cache = Hash.new {|hash, key| hash[key] = Hash.new} @regexps = {} @terminal_failures = [] @max_terminal_failure_index = 0 end def forget_failures_to_here @terminal_failures = [] @max_terminal_failure_index = -1 end def reset_index @index = 0 end def parse_anything(node_class = SyntaxNode, inline_module = nil) if index < input.length result = instantiate_node(node_class,input, index...(index + 1)) result.extend(inline_module) if inline_module @index += 1 result else terminal_parse_failure("any character") end end def instantiate_node(node_type,*args) if node_type.respond_to? :new node_type.new(*args) else SyntaxNode.new(*args).extend(node_type) end end def has_terminal?(terminal, mode, index) case mode when :regexp # A Regexp has been passed in, either a character class or a literel regex 'foo'r (terminal =~ input[index..-1]) == 0 && $&.length when false # The terminal is a string which must match exactly input[index, terminal.size] == terminal && terminal.size when :insens # The terminal is a downcased string which must match input downcased input[index, terminal.size].downcase == terminal && terminal.size when true # Only occurs with old compiled grammars, for character classes rx = @regexps[terminal] ||= Regexp.new(terminal) input.index(rx, index) == index && $&.length end end def terminal_parse_failure(expected_string, unexpected = false) if @max_terminal_failure_index == -1 @max_terminal_failure_index = 0 return nil end return nil if index < max_terminal_failure_index if index > max_terminal_failure_index @max_terminal_failure_index = index @terminal_failures = [] end @terminal_failures << [index, expected_string, unexpected] # It's very slow, but this shows the last 5 nested rules: # caller.reject{|l| l =~ /`loop'|`block in /}[0..5].reverse.map{|l| l.sub(/[^`]*`_nt_/,'').sub(/'/,'')} terminal_failures return nil end end end end