rubyx/lib/parslet/atoms/base.rb

152 lines
4.8 KiB
Ruby
Raw Normal View History

# Base class for all parslets, handles orchestration of calls and implements
# a lot of the operator and chaining methods.
#
# Also see Parslet::Atoms::DSL chaining parslet atoms together.
#
class Parslet::Atoms::Base
include Parslet::Atoms::Precedence
include Parslet::Atoms::DSL
include Parslet::Atoms::CanFlatten
# Given a string or an IO object, this will attempt a parse of its contents
# and return a result. If the parse fails, a Parslet::ParseFailed exception
# will be thrown.
#
# @param io [String, Source] input for the parse process
# @option options [Parslet::ErrorReporter] :reporter error reporter to use,
# defaults to Parslet::ErrorReporter::Tree
# @option options [Boolean] :prefix Should a prefix match be accepted?
# (default: false)
# @return [Hash, Array, Parslet::Slice] PORO (Plain old Ruby object) result
# tree
#
def parse(io, options={})
source = io.respond_to?(:line_and_column) ?
io :
Parslet::Source.new(io)
# Try to cheat. Assuming that we'll be able to parse the input, don't
# run error reporting code.
success, value = setup_and_apply(source, nil, !options[:prefix])
# If we didn't succeed the parse, raise an exception for the user.
# Stack trace will be off, but the error tree should explain the reason
# it failed.
unless success
# Cheating has not paid off. Now pay the cost: Rerun the parse,
# gathering error information in the process.
reporter = options[:reporter] || Parslet::ErrorReporter::Tree.new
source.pos = 0
success, value = setup_and_apply(source, reporter, !options[:prefix])
fail "Assertion failed: success was true when parsing with reporter" \
if success
# Value is a Parslet::Cause, which can be turned into an exception:
value.raise
fail "NEVER REACHED"
end
# assert: success is true
# Extra input is now handled inline with the rest of the parsing. If
# really we have success == true, prefix: false and still some input
# is left dangling, that is a BUG.
if !options[:prefix] && source.chars_left > 0
fail "BUG: New error strategy should not reach this point."
end
return flatten(value)
end
# Creates a context for parsing and applies the current atom to the input.
# Returns the parse result.
#
# @return [<Boolean, Object>] Result of the parse. If the first member is
# true, the parse has succeeded.
def setup_and_apply(source, error_reporter, consume_all)
context = Parslet::Atoms::Context.new(error_reporter)
apply(source, context, consume_all)
end
# Calls the #try method of this parslet. Success consumes input, error will
# rewind the input.
#
# @param source [Parslet::Source] source to read input from
# @param context [Parslet::Atoms::Context] context to use for the parsing
# @param consume_all [Boolean] true if the current parse must consume
# all input by itself.
def apply(source, context, consume_all=false)
old_pos = source.pos
success, value = result = context.try_with_cache(self, source, consume_all)
if success
# If a consume_all parse was made and doesn't result in the consumption
# of all the input, that is considered an error.
if consume_all && source.chars_left>0
# Read 10 characters ahead. Why ten? I don't know.
offending_pos = source.pos
offending_input = source.consume(10)
# Rewind input (as happens always in error case)
source.pos = old_pos
return context.err_at(
self,
source,
"Don't know what to do with #{offending_input.to_s.inspect}",
offending_pos
)
end
# Looks like the parse was successful after all. Don't rewind the input.
return result
end
# We only reach this point if the parse has failed. Rewind the input.
source.pos = old_pos
return result
end
# Override this in your Atoms::Base subclasses to implement parsing
# behaviour.
#
def try(source, context, consume_all)
raise NotImplementedError, \
"Atoms::Base doesn't have behaviour, please implement #try(source, context)."
end
# Returns true if this atom can be cached in the packrat cache. Most parslet
# atoms are cached, so this always returns true, unless overridden.
#
def cached?
true
end
# Debug printing - in Treetop syntax.
#
def self.precedence(prec)
define_method(:precedence) { prec }
end
precedence BASE
def to_s(outer_prec=OUTER)
if outer_prec < precedence
"("+to_s_inner(precedence)+")"
else
to_s_inner(precedence)
end
end
def inspect
to_s(OUTER)
end
private
# Produces an instance of Success and returns it.
#
def succ(result)
[true, result]
end
end