Browse files

Adds simple error handling

  • Loading branch information...
1 parent ea9f2bd commit 63f4dfdc273f99189a394e59dc4fb0b636b39a00 @madadam committed Dec 13, 2010
View
4 examples/json.rb
@@ -27,10 +27,10 @@
rule :number, ('0' / (:non_zero_digit >> zero_or_more(:digit))) >> :whitespace
rule :non_zero_digit, '1' .. '9'
- rule :digit, '0' / :non_zero_digit
+ rule :digit, '0' .. '9'
rule :string, :lquote >> zero_or_more(:char) >> :rquote
- rule :char, any_except('"', '\\') / :escape_sequence
+ rule :char, none_of('"', '\\') / :escape_sequence
rule :escape_sequence, transform('\\"', '"') /
transform('\\n', "\n") /
transform('\\t', "\t")
View
4 lib/braindead.rb
@@ -1,9 +1,10 @@
module Braindead
autoload :Range, 'braindead/range'
autoload :Choice, 'braindead/choice'
- autoload :Cursor, 'braindead/cursor'
autoload :Eof, 'braindead/eof'
+ autoload :Failure, 'braindead/output'
autoload :GraphWalker, 'braindead/graph_walker'
+ autoload :Input, 'braindead/input'
autoload :InvalidRange, 'braindead/errors'
autoload :None, 'braindead/none'
autoload :Parser, 'braindead/parser'
@@ -15,6 +16,7 @@ module Braindead
autoload :Sequence, 'braindead/sequence'
autoload :Skip, 'braindead/skip'
autoload :String, 'braindead/string'
+ autoload :Success, 'braindead/output'
autoload :SyntaxError, 'braindead/errors'
autoload :Transform, 'braindead/transform'
autoload :UnresolvedReference, 'braindead/errors'
View
16 lib/braindead/choice.rb
@@ -5,8 +5,16 @@ def initialize(first, second)
@second = second.to_parser_rule
end
- def parse(input, output)
- @first.parse(input, output) || @second.parse(input, output)
+ def parse(input)
+ @first.parse(input).if_failure do |first|
+ @second.parse(input).if_failure do |second|
+ case
+ when first.partial? then first
+ when second.partial? then second
+ else Failure.new(input.position, description)
+ end
+ end
+ end
end
def parts
@@ -17,5 +25,9 @@ def resolve_parts!(rules)
@first = @first.resolve(rules)
@second = @second.resolve(rules)
end
+
+ def description
+ "#{@first.description} or #{@second.description}"
+ end
end
end
View
29 lib/braindead/cursor.rb
@@ -1,29 +0,0 @@
-module Braindead
- class Cursor
- def initialize(input, position = 0)
- @input = input
- @position = position
- end
-
- attr_accessor :position
-
- def [](*args)
- case args.size
- when 2
- @input[position + args[0], args[1]]
- when 1
- @input[position + args[0]]
- else
- raise ArgumentError, "wrong number of arguments (#{args.size} for 1 or 2)"
- end
- end
-
- def end?
- position >= @input.length
- end
-
- def remaining
- @input[position .. -1]
- end
- end
-end
View
8 lib/braindead/eof.rb
@@ -1,7 +1,11 @@
module Braindead
class << Eof = Rule.new
- def parse(input, output)
- input.end?
+ def parse(input)
+ input.end? ? success : failure(input)
+ end
+
+ def description
+ 'end of file'
end
end
end
View
22 lib/braindead/errors.rb
@@ -2,6 +2,26 @@ module Braindead
Error = Class.new(RuntimeError)
RuleAlreadyDefined = Class.new(Error)
RuleNotDefined = Class.new(Error)
- SyntaxError = Class.new(Error)
UnresolvedReference = Class.new(Error)
+
+ class SyntaxError < Error
+ def initialize(position, expectation, input)
+ @position = position
+ @expectation = expectation
+ @input = input
+ end
+
+ attr_reader :position
+ attr_reader :expectation
+
+ def message
+ "unexpected #{excerpt.inspect} at position #{position}, expecting #{expectation}"
+ end
+
+ def excerpt(window = 20)
+ excerpt = @input[position, window]
+ excerpt += ' ...' if @input.length - position > window
+ excerpt
+ end
+ end
end
View
22 lib/braindead/input.rb
@@ -0,0 +1,22 @@
+module Braindead
+ class Input
+ def initialize(data, position = 0)
+ @data = data
+ @position = position
+ end
+
+ attr_accessor :position
+
+ def advance!(offset = 1)
+ self.position += offset
+ end
+
+ def peek(length = 1)
+ @data[position, length]
+ end
+
+ def end?
+ position >= @data.length
+ end
+ end
+end
View
4 lib/braindead/none.rb
@@ -1,7 +1,7 @@
module Braindead
class << None = Rule.new
- def parse(input, output)
- true
+ def parse(input)
+ success
end
end
end
View
66 lib/braindead/output.rb
@@ -0,0 +1,66 @@
+module Braindead
+ class Success
+ def initialize(*values)
+ @values = values
+ end
+
+ attr_reader :values
+
+ def value
+ @values.length > 1 ? @values : @values.first
+ end
+
+ def concat(other)
+ @values.concat(other.values)
+ self
+ end
+
+ def success?
+ true
+ end
+
+ def failure?
+ false
+ end
+
+ def if_success
+ yield(self)
+ end
+
+ def if_failure
+ self
+ end
+ end
+
+ class Failure
+ def initialize(position, description)
+ @position = position
+ @description = description
+ @partial = false
+ end
+
+ attr_reader :position
+ attr_reader :description
+ attr_writer :partial
+
+ def partial?
+ @partial
+ end
+
+ def success?
+ false
+ end
+
+ def failure?
+ true
+ end
+
+ def if_success
+ self
+ end
+
+ def if_failure
+ yield(self)
+ end
+ end
+end
View
31 lib/braindead/parser.rb
@@ -8,23 +8,22 @@ def initialize
@rules = {}
# Predefined rules
- @rules[:whitespace] = skip(zero_or_more(any_of(' ', "\t", "\n", "\v", "\f", "\r")))
+ @rules[:whitespace] = skip(zero_or_more(one_of(' ', "\t", "\n", "\v", "\f", "\r")))
end
attr_reader :rules
- def parse(input)
+ def parse(string)
resolve!
- root = rules[:root] or raise RuleNotDefined, "root rule not defined"
+ root = rules[:root] or raise RuleNotDefined, "root rule not defined"
+ input = Input.new(string)
+ result = root.parse(input)
- input = Cursor.new(input)
- output = []
-
- if root.parse(input, output)
- output.length > 1 ? output : output.first
+ if result.success?
+ result.value
else
- raise Braindead::SyntaxError
+ raise Braindead::SyntaxError.new(result.position, result.description, string)
end
end
@@ -64,20 +63,22 @@ def none
None
end
- def any_of(*tokens)
- satisfy { |token| tokens.include?(token) }
+ def one_of(*tokens)
+ description = "one of [#{tokens.map(&:inspect).join(', ')}]"
+ satisfy(description) { |token| tokens.include?(token) }
end
- def any_except(*tokens)
- satisfy { |token| !tokens.include?(token) }
+ def none_of(*tokens)
+ description = "none of [#{tokens.map(&:inspect).join(', ')}]"
+ satisfy(description) { |token| !tokens.include?(token) }
end
def eof
Eof
end
- def satisfy(&block)
- Satisfy.new(&block)
+ def satisfy(description = nil, &block)
+ Satisfy.new(description, &block)
end
def transform(rule, *args, &block)
View
14 lib/braindead/range.rb
@@ -5,15 +5,19 @@ def initialize(first, last)
@last = last
end
- def parse(input, output)
- token = input[0]
+ def parse(input)
+ token = input.peek
if token && token >= @first && token <= @last
- input.position += 1
- success(output, token)
+ input.advance!
+ success(token)
else
- failure
+ failure(input)
end
end
+
+ def description
+ "#{@first.inspect} .. #{@last.inspect}"
+ end
end
end
View
9 lib/braindead/rule.rb
@@ -25,13 +25,12 @@ def parts
private
- def success(output, *results)
- results.each { |result| output << result }
- true
+ def success(*values)
+ Success.new(*values)
end
- def failure
- false
+ def failure(input)
+ Failure.new(input.position, description)
end
end
end
View
17 lib/braindead/satisfy.rb
@@ -1,18 +1,21 @@
module Braindead
class Satisfy < Rule
- def initialize(&block)
- @block = block
+ def initialize(description = nil, &block)
+ @description = description
+ @block = block
end
- def parse(input, output)
- token = input[0]
+ def parse(input)
+ token = input.peek
if @block.call(token)
- input.position += 1
- success(output, token)
+ input.advance!
+ success(token)
else
- failure
+ failure(input)
end
end
+
+ attr_reader :description
end
end
View
26 lib/braindead/sequence.rb
@@ -5,16 +5,20 @@ def initialize(first, second)
@second = second.to_parser_rule
end
- def parse(input, output)
- input_position = input.position
- output_length = output.length
+ def parse(input)
+ position = input.position
- if @first.parse(input, output) && @second.parse(input, output)
- success(output)
- else
- input.position = input_position
- output.slice!(output_length .. -1)
- failure
+ @first.parse(input).if_success do |first|
+ second = @second.parse(input)
+
+ if second.success?
+ first.concat(second)
+ else
+ input.position = position
+
+ second.partial = true
+ second
+ end
end
end
@@ -26,5 +30,9 @@ def resolve_parts!(rules)
@first = @first.resolve(rules)
@second = @second.resolve(rules)
end
+
+ def description
+ @first.description
+ end
end
end
View
8 lib/braindead/skip.rb
@@ -10,8 +10,8 @@ def initialize(rule)
@rule = rule.to_parser_rule
end
- def parse(input, output)
- @rule.parse(input, [])
+ def parse(input)
+ @rule.parse(input).if_success { success }
end
def parts
@@ -21,5 +21,9 @@ def parts
def resolve_parts!(rules)
@rule = @rule.resolve(rules)
end
+
+ def description
+ @rule.description
+ end
end
end
View
16 lib/braindead/string.rb
@@ -4,14 +4,18 @@ def initialize(string)
@string = string
end
- def parse(input, output)
- if input[0, @string.length] == @string
- input.position += @string.length
- output << @string
- true
+ def parse(input)
+ if input.peek(@string.length) == @string
+ input.advance!(@string.length)
+
+ success(@string)
else
- false
+ failure(input)
end
end
+
+ def description
+ @string.inspect
+ end
end
end
View
14 lib/braindead/transform.rb
@@ -5,13 +5,9 @@ def initialize(rule, &block)
@block = block
end
- def parse(input, output)
- temp_output = []
-
- if @rule.parse(input, temp_output)
- success(output, @block.call(*temp_output))
- else
- failure
+ def parse(input)
+ @rule.parse(input).if_success do |result|
+ success(@block.call(*result.values))
end
end
@@ -22,5 +18,9 @@ def parts
def resolve_parts!(rules)
@rule = @rule.resolve(rules)
end
+
+ def description
+ @rule.description
+ end
end
end
View
16 lib/braindead/zero_or_more.rb
@@ -4,10 +4,20 @@ def initialize(rule)
@rule = rule.to_parser_rule
end
- def parse(input, output)
- loop { break unless @rule.parse(input, output) }
+ def parse(input)
+ results = success
- true
+ loop do
+ result = @rule.parse(input)
+
+ if result.success?
+ results.concat(result)
+ else
+ break
+ end
+ end
+
+ results
end
def parts
View
8 test/parser_test.rb
@@ -23,8 +23,8 @@ def test_range
assert_raise(SyntaxError) { parser.parse('g') }
end
- def test_any_except
- parser = Parser.define { root any_except('a', 'b', 'c') }
+ def test_none_of
+ parser = Parser.define { root none_of('a', 'b', 'c') }
assert_equal 'd', parser.parse('d')
@@ -33,8 +33,8 @@ def test_any_except
assert_raise(SyntaxError) { parser.parse('c') }
end
- def test_any_of
- parser = Parser.define { root any_of('a', 'c', 'e') }
+ def test_one_of
+ parser = Parser.define { root one_of('a', 'c', 'e') }
assert_equal 'a', parser.parse('a')
assert_equal 'c', parser.parse('c')
View
105 test/syntax_error_test.rb
@@ -0,0 +1,105 @@
+require 'helper'
+
+class SyntaxErrorTest < Test::Unit::TestCase
+ def test_string_error
+ parser = Parser.define { root 'foo' }
+
+ assert_syntax_error('"foo"') { parser.parse('bar') }
+ end
+
+ def test_range_error
+ parser = Parser.define { root 'a' .. 'f' }
+
+ assert_syntax_error('"a" .. "f"') { parser.parse('g') }
+ end
+
+ def test_none_of_error
+ parser = Parser.define { root none_of('a', 'b', 'c') }
+
+ assert_syntax_error('none of ["a", "b", "c"]') { parser.parse('b') }
+ end
+
+ def test_one_of_error
+ parser = Parser.define { root one_of('a', 'b', 'c') }
+
+ assert_syntax_error('one of ["a", "b", "c"]') { parser.parse('d') }
+ end
+
+ def test_sequence_error
+ parser = Parser.define { root 'foo' >> 'bar' >> 'baz' }
+
+ assert_syntax_error('"foo"', 0) { parser.parse('qux') }
+ assert_syntax_error('"bar"', 3) { parser.parse('fooqux') }
+ assert_syntax_error('"baz"', 6) { parser.parse('foobarqux') }
+ end
+
+ def test_choice_error
+ parser = Parser.define { root 'foo' / 'bar' / 'baz' }
+
+ assert_syntax_error('"foo" or "bar" or "baz"') { parser.parse('qux') }
+ end
+
+ def test_skip_error
+ parser = Parser.define { root skip('foo') }
+
+ assert_syntax_error('"foo"') { parser.parse('bar') }
+ end
+
+ def test_eof_error
+ parser = Parser.define { root 'foo' >> eof }
+
+ assert_syntax_error('end of file', 3) { parser.parse('foobar') }
+ end
+
+ def test_transform_error
+ parser = Parser.define { root 'foo', &:upcase }
+
+ assert_syntax_error('"foo"') { parser.parse('bar') }
+ end
+
+ def test_sequence_in_choice_error
+ parser = Parser.define do
+ root :array / :hash
+ rule :array, '[' >> ']'
+ rule :hash, '{' >> '}'
+ end
+
+ assert_syntax_error('"[" or "{"', 0) { parser.parse('<') }
+ assert_syntax_error('"]"', 1) { parser.parse('[>') }
+ assert_syntax_error('"}"', 1) { parser.parse('{>') }
+ end
+
+ def test_choice_in_sequence_error
+ parser = Parser.define do
+ root :name >> :operator
+ rule :name, 'foo' / 'bar'
+ rule :operator, '!' / '?'
+ end
+
+ assert_syntax_error('"foo" or "bar"', 0) { parser.parse('baz') }
+ assert_syntax_error('"!" or "?"', 3) { parser.parse('foo-') }
+ end
+
+ def test_recursion_error
+ parser = Parser.define do
+ root :list
+ rule :list, "(" >> ((:list >> ")") / ")")
+ end
+
+ assert_syntax_error('"(" or ")"', 1) { parser.parse('(]') }
+ assert_syntax_error('"(" or ")"', 2) { parser.parse('((])') }
+ end
+
+ private
+
+ def assert_syntax_error(expectation, position = nil, &block)
+ exception = assert_raise(SyntaxError, &block)
+
+ assert_equal expectation, exception.expectation
+
+ if position
+ message = "Expecting syntax error at possition #{position}, but got: #{exception.message}"
+ assert_equal position, exception.position, message
+ end
+ end
+end

0 comments on commit 63f4dfd

Please sign in to comment.