Skip to content
Browse files

+ Implementation of a deepest error

This is a suggestion from jmettraux in ticket #64. It introduces a different way of aggregating errors that
seems to be better for reporting to the end user of a grammar.
  • Loading branch information...
1 parent fd879e5 commit 77a30117d8a3d81ce3d3481ae829281a521e26cf @kschiess committed May 2, 2012
View
131 example/deepest_errors.rb
@@ -0,0 +1,131 @@
+$:.unshift File.dirname(__FILE__) + "/../lib"
+
+# This example demonstrates how to do deepest error reporting, as invented
+# by John Mettraux (issue #64).
+
+require 'parslet'
+require 'parslet/convenience'
+
+def prettify(str)
+ puts " "*3 + " "*4 + "." + " "*4 + "10" + " "*3 + "." + " "*4 + "20"
+ str.lines.each_with_index do |line, index|
+ printf "%02d %s\n",
+ index+1,
+ line.chomp
+ end
+end
+
+class Parser < Parslet::Parser
+ # commons
+
+ rule(:space) { match('[ \t]').repeat(1) }
+ rule(:space?) { space.maybe }
+
+ rule(:newline) { match('[\r\n]') }
+
+ rule(:comment) { str('#') >> match('[^\r\n]').repeat }
+
+ rule(:line_separator) {
+ (space? >> ((comment.maybe >> newline) | str(';')) >> space?).repeat(1)
+ }
+
+ rule(:blank) { line_separator | space }
+ rule(:blank?) { blank.maybe }
+
+ rule(:identifier) { match('[a-zA-Z0-9_]').repeat(1) }
+
+ # res_statement
+
+ rule(:reference) {
+ (str('@').repeat(1,2) >> identifier).as(:reference)
+ }
+
+ rule(:res_action_or_link) {
+ str('.').as(:dot) >> (identifier >> str('?').maybe ).as(:name) >> str('()')
+ }
+
+ rule(:res_actions) {
+ (
+ reference
+ ).as(:resources) >>
+ (
+ res_action_or_link.as(:res_action)
+ ).repeat(0).as(:res_actions)
+ }
+
+ rule(:res_statement) {
+ res_actions >>
+ (str(':') >> identifier.as(:name)).maybe.as(:res_field)
+ }
+
+ # expression
+
+ rule(:expression) {
+ res_statement
+ }
+
+ # body
+
+ rule(:body) {
+ (line_separator >> (block | expression)).repeat(1).as(:body) >>
+ line_separator
+ }
+
+ # blocks
+
+ rule(:begin_block) {
+ (str('concurrent').as(:type) >> space).maybe.as(:pre) >>
+ str('begin').as(:begin) >>
+ body >>
+ str('end')
+ }
+
+ rule(:define_block) {
+ str('define').as(:define) >> space >>
+ identifier.as(:name) >> str('()') >>
+ body >>
+ str('end')
+ }
+
+ rule(:block) {
+ define_block | begin_block
+ }
+
+ # root
+
+ rule(:radix) {
+ line_separator.maybe >> block >> line_separator.maybe
+ }
+
+ root(:radix)
+end
+
+ds = [
+ %{
+ define f()
+ @res.name
+ end
+ },
+ %{
+ define f()
+ begin
+ @res.name
+ end
+ end
+ }
+]
+
+ds.each do |d|
+
+ puts '-' * 80
+ prettify(d)
+
+ parser = Parser.new
+
+ begin
+ parser.parse_with_debug(d,
+ :reporter => Parslet::ErrorReporter::Deepest.new)
+ end
+end
+
+puts '-' * 80
View
17 example/nested_errors.rb
@@ -3,6 +3,9 @@
require 'parslet'
require 'parslet/convenience'
+# This example demonstrates tree error reporting in a real life example.
+# The parser code has been contributed by John Mettraux.
+
def prettify(str)
puts " "*3 + " "*4 + "." + " "*4 + "10" + " "*3 + "." + " "*4 + "20"
str.lines.each_with_index do |line, index|
@@ -102,20 +105,6 @@ class Parser < Parslet::Parser
ds = [
%{
define f()
- begin
- @res:name
- end
- end
- },
- %{
- define f()
- begin
- @res.name()
- end
- end
- },
- %{
- define f()
@res.name
end
},
View
3 lib/parslet/atoms/base.rb
@@ -27,7 +27,8 @@ def parse(io, options={})
unless success
# Cheating has not paid off. Now pay the cost: Rerun the parse,
# gathering error information in the process.
- success, value = setup_and_apply(source, Parslet::ErrorReporter::Tree.new)
+ reporter = options[:reporter] || Parslet::ErrorReporter::Tree.new
+ success, value = setup_and_apply(source, reporter)
fail "Assertion failed: success was true when parsing with reporter" \
if success
View
4 lib/parslet/convenience.rb
@@ -23,8 +23,8 @@ class Parslet::Atoms::Base
#
# FooParser.new.parse_with_debug('bar')
#
- def parse_with_debug str
- parse str
+ def parse_with_debug str, opts={}
+ parse str, opts
rescue Parslet::UnconsumedInput => error
puts error
rescue Parslet::ParseFailed => error
View
3 lib/parslet/error_reporter.rb
@@ -3,4 +3,5 @@
module Parslet::ErrorReporter
end
-require 'parslet/error_reporter/tree'
+require 'parslet/error_reporter/tree'
+require 'parslet/error_reporter/deepest'
View
79 lib/parslet/error_reporter/deepest.rb
@@ -0,0 +1,79 @@
+module Parslet
+ module ErrorReporter
+ # TODO describe
+ class Deepest
+ def initialize
+ @deepest_cause = nil
+ end
+
+ # Produces an error cause that combines the message at the current level
+ # with the errors that happened at a level below (children).
+ #
+ # @param source [Source] Source that we're using for this parse. (line
+ # number information...)
+ # @param message [String, Array] Error message at this level.
+ # @param children [Array] A list of errors from a deeper level (or nil).
+ # @return [Cause] An error tree combining children with message.
+ #
+ def err(source, message, children=nil)
+ position = source.pos
+ cause = Cause.format(source, position, message, children)
+ return deepest(cause)
+ end
+
+ # Produces an error cause that combines the message at the current level
+ # with the errors that happened at a level below (children).
+ #
+ # @param source [Source] Source that we're using for this parse. (line
+ # number information...)
+ # @param message [String, Array] Error message at this level.
+ # @param pos [Fixnum] The real position of the error.
+ # @param children [Array] A list of errors from a deeper level (or nil).
+ # @return [Cause] An error tree combining children with message.
+ #
+ def err_at(source, message, pos, children=nil)
+ position = pos
+ cause = Cause.format(source, position, message, children)
+ return deepest(cause)
+ end
+
+ private
+ # Checks to see if the lineage of the cause given includes a cause with
+ # an error position deeper than the current deepest cause stored. If
+ # yes, it passes the cause through to the caller. If no, it returns the
+ # current deepest error that was saved as a reference.
+ #
+ def deepest(cause)
+ rank, leaf = deepest_child(cause)
+
+ if !@deepest_cause || leaf.pos >= @deepest_cause.pos
+ # This error reaches deeper into the input, save it as reference.
+ @deepest_cause = leaf
+ return cause
+ end
+
+ return @deepest_cause
+ end
+
+ # Returns the leaf from a given error tree with the biggest rank.
+ #
+ def deepest_child(cause, rank=0)
+ max_child = cause
+ max_rank = rank
+
+ if cause.children && !cause.children.empty?
+ cause.children.each do |child|
+ c_rank, c_cause = deepest_child(child, rank+1)
+
+ if c_rank > max_rank
+ max_rank = c_rank
+ max_child = c_cause
+ end
+ end
+ end
+
+ return max_rank, max_child
+ end
+ end
+ end
+end
View
27 spec/parslet/atoms/base_spec.rb
@@ -96,5 +96,30 @@ def unnamed(obj)
parslet.parse('foobarbaz', :prefix => true).should == 'foobar'
end
end
-
+
+ describe ':reporter option' do
+ let(:parslet) { Parslet.str('test') >> Parslet.str('ing') }
+ let(:reporter) { flexmock(:reporter) }
+
+ it "replaces the default reporter" do
+ cause = flexmock(:cause)
+
+ # Two levels of the parse, calling two different error reporting
+ # methods.
+ reporter.
+ should_receive(:err_at).once
+ reporter.
+ should_receive(:err => cause).once
+
+ # The final cause will be sent the #raise method.
+ cause.
+ should_receive(:raise).once.and_throw(:raise)
+
+ catch(:raise) {
+ parslet.parse('testung', :reporter => reporter)
+
+ fail "NEVER REACHED"
+ }
+ end
+ end
end
View
5 spec/parslet/error_reporter/deepest_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe Parslet::ErrorReporter::Deepest do
+
+end

0 comments on commit 77a3011

Please sign in to comment.
Something went wrong with that request. Please try again.