Skip to content

Commit

Permalink
Introduced more lightweight parsing model
Browse files Browse the repository at this point in the history
This commit introduces LOTS of changes, most notably the following:

- A parse now instantiates only one Match object. This object recieves
  an array of events in its initializer which contains information about
  how to generate the parse tree beneath it as needed. This change
  provides a significant speed boost and uses a lot less memory.
- Added a case-insensitive primitive type that can be used as a quick
  substitute for regular expressions with the i option. This type uses
  backticks instead of quotes and follows the same conventions as
  double-quoted strings.
- Rule#match was changed to Rule#parse to match GrammarMethods#parse. It
  also provides a much simpler interface when dealing with Rule objects
  because a plain string may be given instead of an Input.
- Rule#test (and similarly Input#test) allow checking for a match
  without actually instantiating and returning one. This is faster than
  using Rule#parse when all that is needed is a quick check.
- Rule[] can now be used to retrieve any Rule by its id. This is needed
  by Match objects to retrieve rule objects as needed to lazily
  instantiate submatches.
  • Loading branch information
mjackson committed Nov 12, 2010
1 parent 5b1f776 commit 456adbe
Show file tree
Hide file tree
Showing 25 changed files with 1,210 additions and 944 deletions.
595 changes: 380 additions & 215 deletions lib/citrus.rb

Large diffs are not rendered by default.

69 changes: 0 additions & 69 deletions lib/citrus/debug.rb

This file was deleted.

23 changes: 16 additions & 7 deletions lib/citrus/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module Citrus
# Some helper methods for rules that alias +module_name+ and don't want to
# use +Kernel#eval+ to retrieve Module objects.
module ModuleHelpers #:nodoc:
module ModuleNameHelpers #:nodoc:
def module_segments
@module_segments ||= module_name.value.split('::')
end
Expand Down Expand Up @@ -35,7 +35,7 @@ def module_basename

rule :grammar do
all(:grammar_keyword, :module_name, :grammar_body, :end_keyword) {
include ModuleHelpers
include ModuleNameHelpers

def value
module_namespace.const_set(module_basename, grammar_body.value)
Expand Down Expand Up @@ -78,7 +78,11 @@ def value
rule :choice do
all(:sequence, zero_or_more([ :bar, :sequence ])) {
def rules
@rules ||= [ sequence.value ] + matches[1].matches.map {|m| m.matches[1].value }
@rules ||= begin
[ sequence.value ] + matches[1].matches.map do |m|
m.matches[1].value
end
end
end

def value
Expand Down Expand Up @@ -142,7 +146,7 @@ def value

rule :include do
all(:include_keyword, :module_name) {
include ModuleHelpers
include ModuleNameHelpers

def value
module_namespace.const_get(module_basename)
Expand Down Expand Up @@ -236,7 +240,7 @@ def flags

rule :tag do
all(:lt, :module_name, :gt) {
include ModuleHelpers
include ModuleNameHelpers

def value
module_namespace.const_get(module_basename)
Expand Down Expand Up @@ -300,8 +304,13 @@ def max; Infinity end

rule :star do
all(/[0-9]*/, '*', /[0-9]*/, :space) {
def min; matches[0] == '' ? 0 : matches[0].to_i end
def max; matches[2] == '' ? Infinity : matches[2].to_i end
def min
matches[0] == '' ? 0 : matches[0].to_i
end

def max
matches[2] == '' ? Infinity : matches[2].to_i
end
}
end

Expand Down
53 changes: 18 additions & 35 deletions test/alias_test.rb
Original file line number Diff line number Diff line change
@@ -1,65 +1,48 @@
require File.expand_path('../helper', __FILE__)

Citrus.load(File.expand_path('../_files/alias', __FILE__))

class AliasTest < Test::Unit::TestCase

def test_terminal?
rule = Alias.new
assert_equal(false, rule.terminal?)
end

def test_match
def test_exec
grammar = Grammar.new {
rule :a, :b
rule :b, 'b'
rule :b, 'abc'
}

match = grammar.parse('b')
assert(match)
assert_equal('b', match)
assert_equal(1, match.length)
rule = grammar.rule(:a)
rule_b = grammar.rule(:b)
events = rule.exec(Input.new('abc'))
assert_equal([rule_b.id, CLOSE, 3], events)
end

def test_match_renamed
def test_exec_miss
grammar = Grammar.new {
rule :a, ext(:b) {
'a' + to_s
}
rule :b, 'b'
rule :a, :b
rule :b, 'abc'
}

match = grammar.parse('b')
assert(match)
assert('ab', match.value)

assert_raise NoMatchError do
match.b
end
rule = grammar.rule(:a)
events = rule.exec(Input.new('def'))
assert_equal([], events)
end

def test_peg
match = AliasOne.parse('a')
assert(match)
end

def test_included
def test_exec_included
grammar1 = Grammar.new {
rule :a, 'a'
rule :a, 'abc'
}

grammar2 = Grammar.new {
include grammar1
rule :b, :a
}

match = grammar2.parse('a')
assert(match)
rule = grammar2.rule(:b)
rule_a = grammar1.rule(:a)
events = rule.exec(Input.new('abc'))
assert_equal([rule_a.id, CLOSE, 3], events)
end

def test_to_s
rule = Alias.new(:alpha)
assert_equal('alpha', rule.to_s)
end

end
25 changes: 15 additions & 10 deletions test/and_predicate_test.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
require File.expand_path('../helper', __FILE__)

class AndPredicateTest < Test::Unit::TestCase

def test_terminal?
rule = AndPredicate.new
assert_equal(false, rule.terminal?)
end

def test_match
rule = AndPredicate.new('a')
def test_exec
rule = AndPredicate.new('abc')
events = rule.exec(Input.new('abc'))
assert_equal([rule.id, CLOSE, 0], events)
end

match = rule.match(input('b'))
assert_equal(nil, match)
def test_exec_miss
rule = AndPredicate.new('def')
events = rule.exec(Input.new('abc'))
assert_equal([], events)
end

match = rule.match(input('a'))
assert(match)
assert_equal('', match)
assert_equal(0, match.length)
def test_consumption
rule = AndPredicate.new('abc')
input = Input.new('abc')
events = rule.exec(input)
assert_equal(0, input.pos)
end

def test_to_s
rule = AndPredicate.new('a')
assert_equal('&"a"', rule.to_s)
end

end
39 changes: 22 additions & 17 deletions test/but_predicate_test.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
require File.expand_path('../helper', __FILE__)

class ButPredicateTest < Test::Unit::TestCase

def test_terminal?
rule = ButPredicate.new
assert_equal(false, rule.terminal?)
end

def test_match
rule = ButPredicate.new('a')
def test_exec
rule = ButPredicate.new('abc')

match = rule.match(input('b'))
assert(match)
assert_equal('b', match)
assert_equal(1, match.length)
events = rule.exec(Input.new('def'))
assert_equal([rule.id, CLOSE, 3], events)

match = rule.match(input('bbba'))
assert(match)
assert_equal('bbb', match)
assert_equal(3, match.length)
events = rule.exec(Input.new('defabc'))
assert_equal([rule.id, CLOSE, 3], events)
end

match = rule.match(input('a'))
assert_equal(nil, match)
def test_exec_miss
rule = ButPredicate.new('abc')
events = rule.exec(Input.new('abc'))
assert_equal([], events)
end

# ButPredicate must match at least one character.
match = rule.match(input(''))
assert_equal(nil, match)
def test_consumption
rule = ButPredicate.new('abc')

input = Input.new('def')
events = rule.exec(input)
assert_equal(3, input.pos)

input = Input.new('defabc')
events = rule.exec(input)
assert_equal(3, input.pos)
end

def test_to_s
rule = ButPredicate.new('a')
assert_equal('~"a"', rule.to_s)
end

end
45 changes: 0 additions & 45 deletions test/cache_test.rb

This file was deleted.

2 changes: 1 addition & 1 deletion test/calc_file_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Object.__send__(:remove_const, :Calc)
end

Citrus.load(File.dirname(__FILE__) + '/../examples/calc')
Citrus.load File.expand_path('../../examples/calc', __FILE__)

class CalcFileTest < Test::Unit::TestCase
include CalcTestMethods
Expand Down
Loading

0 comments on commit 456adbe

Please sign in to comment.