parslet 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,22 @@
3
3
  - prsnt? and absnt? are now finally banned into oblivion. Wasting vocals for
4
4
  the win.
5
5
 
6
+ = 1.5 / ??
7
+
8
+ + Handles unconsumed input at end of parse completely differently. Instead
9
+ of generating a toplevel error, it now raises an error in every branch
10
+ of the parse. More information in the resulting exception ensues! Thanks
11
+ again to John Mettraux for inspiration & acceptance specs.
12
+
13
+ NOTE that this means that the UnconsumedInput exception is gone, since the
14
+ unconsumed input case is nothing special anymore.
15
+
16
+ * This history now finally reads like the Changelog of the linux kernel.
17
+ Meaning that probably no one ever reads this.
18
+
19
+ + Captures and parsing subsequent input based on captured values. This has
20
+ been long overdue - finally you can parse HEREdocs with parslet!
21
+
6
22
  = 1.4.0 / 25May2012
7
23
 
8
24
  + Revised documentation. A few new API features have finally made it into
data/README CHANGED
@@ -65,6 +65,6 @@ ruby-1.8.7-p334 for better results.
65
65
 
66
66
  STATUS
67
67
 
68
- At version 1.4.0 - See HISTORY.txt for changes.
68
+ Production worthy.
69
69
 
70
70
  (c) 2010, 2011, 2012 Kaspar Schiess
data/Rakefile CHANGED
@@ -16,29 +16,10 @@ end
16
16
 
17
17
  task :default => :spec
18
18
 
19
- # Generate documentation
20
- RDoc::Task.new do |rdoc|
21
- rdoc.title = "parslet - construction of parsers made easy"
22
- rdoc.options << '--line-numbers'
23
- rdoc.options << '--fmt' << 'shtml' # explictly set shtml generator
24
- rdoc.template = 'direct' # lighter template used on railsapi.com
25
- rdoc.main = "README"
26
- rdoc.rdoc_files.include("README", "lib/**/*.rb")
27
- rdoc.rdoc_dir = "rdoc"
28
- end
29
-
30
- desc 'Clear out RDoc'
31
- task :clean => [:clobber_rdoc, :clobber_package]
32
-
33
19
  # This task actually builds the gem.
34
20
  task :gem => :spec
35
21
  spec = eval(File.read('parslet.gemspec'))
36
22
 
37
- desc "Generate the gem package."
38
- Gem::PackageTask.new(spec) do |pkg|
39
- pkg.gem_spec = spec
40
- end
41
-
42
23
  desc "Prints LOC stats"
43
24
  task :stat do
44
25
  %w(lib spec example).each do |dir|
@@ -0,0 +1,153 @@
1
+ # A simple integer calculator to answer the question about how to do
2
+ # left and right associativity in parslet (PEG) once and for all.
3
+
4
+ $:.unshift File.dirname(__FILE__) + "/../lib"
5
+
6
+ require 'rspec'
7
+ require 'parslet'
8
+ require 'parslet/rig/rspec'
9
+
10
+ # This is the parsing stage. It expresses left associativity by compiling
11
+ # list of things that have the same associativity.
12
+ class CalcParser < Parslet::Parser
13
+ root :addition
14
+
15
+ rule(:addition) {
16
+ multiplication.as(:l) >> (add_op >> multiplication.as(:r)).repeat(1) |
17
+ multiplication
18
+ }
19
+
20
+ rule(:multiplication) {
21
+ integer.as(:l) >> (mult_op >> integer.as(:r)).repeat(1) |
22
+ integer }
23
+
24
+ rule(:integer) { digit.repeat(1).as(:i) >> space? }
25
+
26
+ rule(:mult_op) { match['*/'].as(:o) >> space? }
27
+ rule(:add_op) { match['+-'].as(:o) >> space? }
28
+
29
+ rule(:digit) { match['0-9'] }
30
+ rule(:space?) { match['\s'].repeat }
31
+ end
32
+
33
+ # Classes for the abstract syntax tree.
34
+ Int = Struct.new(:int) {
35
+ def eval; self end
36
+ def op(operation, other)
37
+ left = int
38
+ right = other.int
39
+
40
+ Int.new(
41
+ case operation
42
+ when '+'
43
+ left + right
44
+ when '-'
45
+ left - right
46
+ when '*'
47
+ left * right
48
+ when '/'
49
+ left / right
50
+ end)
51
+ end
52
+ def to_i
53
+ int
54
+ end
55
+ }
56
+ Seq = Struct.new(:sequence) {
57
+ def eval
58
+ sequence.reduce { |accum, operation|
59
+ operation.call(accum) }
60
+ end
61
+ }
62
+ LeftOp = Struct.new(:operation, :right) {
63
+ def call(left)
64
+ left = left.eval
65
+ right = self.right.eval
66
+
67
+ left.op(operation, right)
68
+ end
69
+ }
70
+
71
+ # Transforming intermediary syntax tree into a real AST.
72
+ class CalcTransform < Parslet::Transform
73
+ rule(i: simple(:i)) { Int.new(Integer(i)) }
74
+ rule(o: simple(:o), r: simple(:i)) { LeftOp.new(o, i) }
75
+ rule(l: simple(:i)) { i }
76
+ rule(sequence(:seq)) { Seq.new(seq) }
77
+ end
78
+
79
+ # And this calls everything in the right order.
80
+ def calculate(str)
81
+ intermediary_tree = CalcParser.new.parse(str)
82
+ abstract_tree = CalcTransform.new.apply(intermediary_tree)
83
+ result = abstract_tree.eval
84
+
85
+ result.to_i
86
+ end
87
+
88
+ # A test suite for the above parser
89
+ describe CalcParser do
90
+ let(:p) { described_class.new }
91
+ describe '#integer' do
92
+ let(:i) { p.integer }
93
+ it "parses integers" do
94
+ i.should parse('1')
95
+ i.should parse('123')
96
+ end
97
+ it "consumes trailing white space" do
98
+ i.should parse('123 ')
99
+ end
100
+ it "doesn't parse floats" do
101
+ i.should_not parse('1.3')
102
+ end
103
+ end
104
+ describe '#multiplication' do
105
+ let(:m) { p.multiplication }
106
+ it "parses simple multiplication" do
107
+ m.should parse('1*2')
108
+ end
109
+ it "parses division" do
110
+ m.should parse('1/2')
111
+ end
112
+ end
113
+ describe '#addition' do
114
+ let(:a) { p.addition }
115
+
116
+ it "parses simple addition" do
117
+ a.should parse('1+2')
118
+ a.should parse('1+2+3-4')
119
+ end
120
+ end
121
+ end
122
+ describe CalcTransform do
123
+ def t(obj)
124
+ described_class.new.apply(obj)
125
+ end
126
+
127
+ it "transforms integers" do
128
+ t(i: '1').should == Int.new(1)
129
+ end
130
+ it "unwraps left operand" do
131
+ t(l: :obj).should == :obj
132
+ end
133
+ end
134
+ describe 'whole computation specs' do
135
+ def self.result_of(str, int)
136
+ it(str) { calculate(str).should == int }
137
+ end
138
+
139
+ result_of '1+1', 2
140
+ result_of '1-1-1', -1
141
+ result_of '1+1+3*5/2', 9
142
+ result_of '123*2', 246
143
+ end
144
+
145
+
146
+ # Enable these if you want to change the code.
147
+ # RSpec::Core::Runner.run([], $stderr, $stdout)
148
+
149
+ str = ARGV.join
150
+ str = '123*2' if str.match(/^\s*$/)
151
+
152
+ print "#{str} (command line): -> "
153
+ puts calculate(str)
@@ -0,0 +1,49 @@
1
+
2
+ # This example demonstrates how pieces of input can be captured and matched
3
+ # against later on. Without this, you cannot match here-documents and other
4
+ # self-dependent grammars.
5
+
6
+ $:.unshift File.dirname(__FILE__) + "/../lib"
7
+ require 'parslet'
8
+ require 'parslet/convenience'
9
+ require 'pp'
10
+
11
+ class CapturingParser < Parslet::Parser
12
+ root :document
13
+
14
+ # Introduce a scope for each document. This ensures that documents can be
15
+ # nested.
16
+ rule(:document) { scope { doc_start >> text >> doc_end } }
17
+
18
+ # Start of a document is a heredoc marker. This is captured in :marker
19
+ rule(:doc_start) { str('<') >> marker >> newline }
20
+ rule(:marker) { match['A-Z'].repeat(1).capture(:marker) }
21
+
22
+ # The content of a document can be either lines of text or another
23
+ # document, introduced by <HERE, where HERE is the doc marker.
24
+ rule(:text) { (document.as(:doc) | text_line.as(:line)).repeat(1) }
25
+ rule(:text_line) { captured_marker.absent? >> any >>
26
+ (newline.absent? >> any).repeat >> newline }
27
+
28
+ # The end of the document is marked by the marker that was at the beginning
29
+ # of the document, by itself on a line.
30
+ rule(:doc_end) { captured_marker }
31
+ rule(:captured_marker) {
32
+ dynamic { |source, context|
33
+ str(context.captures[:marker])
34
+ }
35
+ }
36
+
37
+ rule(:newline) { match["\n"] }
38
+ end
39
+
40
+ parser = CapturingParser.new
41
+ pp parser.parse_with_debug %Q(<CAPTURE
42
+ Text1
43
+ <FOOBAR
44
+ Text3
45
+ Text4
46
+ FOOBAR
47
+ Text2
48
+ CAPTURE)
49
+
@@ -10,8 +10,8 @@ class IgnoreParslet < Parslet::Atoms::Base
10
10
  def to_s_inner(prec)
11
11
  @parslet.to_s(prec)
12
12
  end
13
- def try(source, context)
14
- success, value = result = @parslet.try(source, context)
13
+ def try(source, context, consume_all)
14
+ success, value = result = @parslet.try(source, context, consume_all)
15
15
 
16
16
  return succ(nil) if success
17
17
  return result
@@ -0,0 +1 @@
1
+ 123*2 (command line): -> 246
@@ -0,0 +1,3 @@
1
+ [{:line=>"Text1\n"@9},
2
+ {:doc=>[{:line=>"Text3\n"@23}, {:line=>"Text4\n"@29}]},
3
+ {:line=>"\nText2\n"@41}]
@@ -0,0 +1 @@
1
+ parses 'aba'
@@ -0,0 +1,15 @@
1
+
2
+ $:.unshift File.dirname(__FILE__) + "/../lib"
3
+ require 'parslet'
4
+
5
+ include Parslet
6
+
7
+ parser = str('a').capture(:a) >> scope { str('b').capture(:a) } >>
8
+ dynamic { |s,c| str(c.captures[:a]) }
9
+
10
+ begin
11
+ parser.parse('aba')
12
+ puts "parses 'aba'"
13
+ rescue
14
+ puts "exception!"
15
+ end
@@ -83,22 +83,6 @@ module Parslet
83
83
  attr_reader :cause
84
84
  end
85
85
 
86
- # Raised when the parse operation didn't consume all of its input. In this
87
- # case, it makes only limited sense to look at the error tree. Maybe the
88
- # parser worked just fine, but didn't account for the characters at the tail
89
- # of the input?
90
- #
91
- # str('foo').parse('foobar')
92
- # # raises Parslet::UnconsumedInput:
93
- # # Don't know what to do with "bar" at line 1 char 4.
94
- #
95
- # Note that you can have parslet ignore this error:
96
- #
97
- # str('foo').parse('foobar', prefix: true) # => "foo"@0
98
- #
99
- class UnconsumedInput < ParseFailed
100
- end
101
-
102
86
  module ClassMethods
103
87
  # Define an entity for the parser. This generates a method of the same
104
88
  # name that can be used as part of other patterns. Those methods can be
@@ -185,6 +169,37 @@ module Parslet
185
169
  end
186
170
  module_function :any
187
171
 
172
+ # Introduces a new capture scope. This means that all old captures stay
173
+ # accessible, but new values stored will only be available during the block
174
+ # given and the old values will be restored after the block.
175
+ #
176
+ # Example:
177
+ # # :a will be available until the end of the block. Afterwards,
178
+ # # :a from the outer scope will be available again, if such a thing
179
+ # # exists.
180
+ # scope { str('a').capture(:a) }
181
+ #
182
+ def scope(&block)
183
+ Parslet::Atoms::Scope.new(block)
184
+ end
185
+ module_function :scope
186
+
187
+ # Designates a piece of the parser as being dynamic. Dynamic parsers can
188
+ # either return a parser at runtime, which will be applied on the input, or
189
+ # return a result from a parse.
190
+ #
191
+ # Dynamic parse pieces are never cached and can introduce performance
192
+ # abnormalitites - use sparingly where other constructs fail.
193
+ #
194
+ # Example:
195
+ # # Parses either 'a' or 'b', depending on the weather
196
+ # dynamic { rand() < 0.5 ? str('a') : str('b') }
197
+ #
198
+ def dynamic(&block)
199
+ Parslet::Atoms::Dynamic.new(block)
200
+ end
201
+ module_function :dynamic
202
+
188
203
  # A special kind of atom that allows embedding whole treetop expressions
189
204
  # into parslet construction.
190
205
  #
@@ -251,4 +266,5 @@ require 'parslet/pattern'
251
266
  require 'parslet/pattern/binding'
252
267
  require 'parslet/transform'
253
268
  require 'parslet/parser'
254
- require 'parslet/error_reporter'
269
+ require 'parslet/error_reporter'
270
+ require 'parslet/scope'
@@ -27,5 +27,8 @@ module Parslet::Atoms
27
27
  require 'parslet/atoms/re'
28
28
  require 'parslet/atoms/str'
29
29
  require 'parslet/atoms/entity'
30
+ require 'parslet/atoms/capture'
31
+ require 'parslet/atoms/dynamic'
32
+ require 'parslet/atoms/scope'
30
33
  end
31
34
 
@@ -30,9 +30,9 @@ class Parslet::Atoms::Alternative < Parslet::Atoms::Base
30
30
  self.class.new(*@alternatives + [parslet])
31
31
  end
32
32
 
33
- def try(source, context)
33
+ def try(source, context, consume_all)
34
34
  errors = alternatives.map { |a|
35
- success, value = result = a.apply(source, context)
35
+ success, value = result = a.apply(source, context, consume_all)
36
36
  return result if success
37
37
 
38
38
  # Aggregate all errors
@@ -27,7 +27,7 @@ class Parslet::Atoms::Base
27
27
 
28
28
  # Try to cheat. Assuming that we'll be able to parse the input, don't
29
29
  # run error reporting code.
30
- success, value = setup_and_apply(source, nil)
30
+ success, value = setup_and_apply(source, nil, !options[:prefix])
31
31
 
32
32
  # If we didn't succeed the parse, raise an exception for the user.
33
33
  # Stack trace will be off, but the error tree should explain the reason
@@ -36,7 +36,8 @@ class Parslet::Atoms::Base
36
36
  # Cheating has not paid off. Now pay the cost: Rerun the parse,
37
37
  # gathering error information in the process.
38
38
  reporter = options[:reporter] || Parslet::ErrorReporter::Tree.new
39
- success, value = setup_and_apply(source, reporter)
39
+ source.pos = 0
40
+ success, value = setup_and_apply(source, reporter, !options[:prefix])
40
41
 
41
42
  fail "Assertion failed: success was true when parsing with reporter" \
42
43
  if success
@@ -48,15 +49,12 @@ class Parslet::Atoms::Base
48
49
  end
49
50
 
50
51
  # assert: success is true
51
-
52
- # If we haven't consumed the input, then the pattern doesn't match. Try
53
- # to provide a good error message (even asking down below)
54
- if !options[:prefix] && !source.eof?
55
- old_pos = source.pos
56
- Parslet::Cause.format(
57
- source, old_pos,
58
- "Don't know what to do with #{source.consume(10).to_s.inspect}").
59
- raise(Parslet::UnconsumedInput)
52
+
53
+ # Extra input is now handled inline with the rest of the parsing. If
54
+ # really we have success == true, prefix: false and still some input
55
+ # is left dangling, that is a BUG.
56
+ if !options[:prefix] && source.chars_left > 0
57
+ fail "BUG: New error strategy should not reach this point."
60
58
  end
61
59
 
62
60
  return flatten(value)
@@ -67,21 +65,45 @@ class Parslet::Atoms::Base
67
65
  #
68
66
  # @return [<Boolean, Object>] Result of the parse. If the first member is
69
67
  # true, the parse has succeeded.
70
- def setup_and_apply(source, error_reporter)
68
+ def setup_and_apply(source, error_reporter, consume_all)
71
69
  context = Parslet::Atoms::Context.new(error_reporter)
72
- apply(source, context)
70
+ apply(source, context, consume_all)
73
71
  end
74
72
 
75
- #---
76
- # Calls the #try method of this parslet. In case of a parse error, apply
77
- # leaves the source in the state it was before the attempt.
78
- #+++
79
- def apply(source, context)
73
+ # Calls the #try method of this parslet. Success consumes input, error will
74
+ # rewind the input.
75
+ #
76
+ # @param source [Parslet::Source] source to read input from
77
+ # @param context [Parslet::Atoms::Context] context to use for the parsing
78
+ # @param consume_all [Boolean] true if the current parse must consume
79
+ # all input by itself.
80
+ def apply(source, context, consume_all=false)
80
81
  old_pos = source.pos
81
82
 
82
- success, value = result = context.try_with_cache(self, source)
83
+ success, value = result = context.try_with_cache(self, source, consume_all)
83
84
 
84
- return result if success
85
+ if success
86
+ # If a consume_all parse was made and doesn't result in the consumption
87
+ # of all the input, that is considered an error.
88
+ if consume_all && source.chars_left>0
89
+ # Read 10 characters ahead. Why ten? I don't know.
90
+ offending_pos = source.pos
91
+ offending_input = source.consume(10)
92
+
93
+ # Rewind input (as happens always in error case)
94
+ source.pos = old_pos
95
+
96
+ return context.err_at(
97
+ self,
98
+ source,
99
+ "Don't know what to do with #{offending_input.to_s.inspect}",
100
+ offending_pos
101
+ )
102
+ end
103
+
104
+ # Looks like the parse was successful after all. Don't rewind the input.
105
+ return result
106
+ end
85
107
 
86
108
  # We only reach this point if the parse has failed. Rewind the input.
87
109
  source.pos = old_pos
@@ -91,11 +113,18 @@ class Parslet::Atoms::Base
91
113
  # Override this in your Atoms::Base subclasses to implement parsing
92
114
  # behaviour.
93
115
  #
94
- def try(source, context)
116
+ def try(source, context, consume_all)
95
117
  raise NotImplementedError, \
96
118
  "Atoms::Base doesn't have behaviour, please implement #try(source, context)."
97
119
  end
98
120
 
121
+ # Returns true if this atom can be cached in the packrat cache. Most parslet
122
+ # atoms are cached, so this always returns true, unless overridden.
123
+ #
124
+ def cached?
125
+ true
126
+ end
127
+
99
128
  # Debug printing - in Treetop syntax.
100
129
  #
101
130
  def self.precedence(prec)
@@ -0,0 +1,38 @@
1
+
2
+ # Stores the result of matching an atom against input in the #captures in
3
+ # parse context. Doing so will allow you to pull parts of the ongoing parse
4
+ # out later and use them to match other pieces of input.
5
+ #
6
+ # Example:
7
+ # # After this, context.captures[:an_a] returns 'a'
8
+ # str('a').capture(:an_a)
9
+ #
10
+ # # Capture and use of the capture: (matches either 'aa' or 'bb')
11
+ # match['ab'].capture(:first) >>
12
+ # dynamic { |src, ctx| str(ctx.captures[:first]) }
13
+ #
14
+ class Parslet::Atoms::Capture < Parslet::Atoms::Base
15
+ attr_reader :parslet, :name
16
+
17
+ def initialize(parslet, name)
18
+ super()
19
+
20
+ @parslet, @name = parslet, name
21
+ end
22
+
23
+ def apply(source, context, consume_all)
24
+ success, value = result = parslet.apply(source, context, consume_all)
25
+
26
+ if success
27
+ context.captures[name.to_sym] =
28
+ flatten(value)
29
+ end
30
+
31
+ return result
32
+ end
33
+
34
+ def to_s_inner(prec)
35
+ "(#{name.inspect} = #{parslet.to_s(prec)})"
36
+ end
37
+ end
38
+
@@ -12,6 +12,7 @@ module Parslet::Atoms
12
12
  def initialize(reporter=Parslet::ErrorReporter::Tree.new)
13
13
  @cache = Hash.new { |h, k| h[k] = {} }
14
14
  @reporter = reporter
15
+ @captures = Parslet::Scope.new
15
16
  end
16
17
 
17
18
  # Caches a parse answer for obj at source.pos. Applying the same parslet
@@ -22,14 +23,17 @@ module Parslet::Atoms
22
23
  # were consumed by a successful parse. Imitation of such a parse must
23
24
  # advance the input pos by the same amount of bytes.
24
25
  #
25
- def try_with_cache(obj, source)
26
+ def try_with_cache(obj, source, consume_all)
26
27
  beg = source.pos
27
28
 
28
29
  # Not in cache yet? Return early.
29
30
  unless entry = lookup(obj, beg)
30
- result = obj.try(source, self)
31
+ result = obj.try(source, self, consume_all)
31
32
 
32
- set obj, beg, [result, source.pos-beg]
33
+ if obj.cached?
34
+ set obj, beg, [result, source.pos-beg]
35
+ end
36
+
33
37
  return result
34
38
  end
35
39
 
@@ -59,6 +63,23 @@ module Parslet::Atoms
59
63
  return [false, nil]
60
64
  end
61
65
 
66
+ # Returns the current captures made on the input (see
67
+ # Parslet::Atoms::Base#capture). Use as follows:
68
+ #
69
+ # context.captures[:foobar] # => returns capture :foobar
70
+ #
71
+ attr_reader :captures
72
+
73
+ # Starts a new scope. Use the #scope method of Parslet::Atoms::DSL
74
+ # to call this.
75
+ #
76
+ def scope
77
+ captures.push
78
+ yield
79
+ ensure
80
+ captures.pop
81
+ end
82
+
62
83
  private
63
84
  def lookup(obj, pos)
64
85
  @cache[pos][obj]
@@ -95,4 +95,15 @@ module Parslet::Atoms::DSL
95
95
  def as(name)
96
96
  Parslet::Atoms::Named.new(self, name)
97
97
  end
98
+
99
+ # Captures a part of the input and stores it under the name given. This
100
+ # is very useful to create self-referential parses. A capture stores
101
+ # the result of its parse (may be complex) on a successful parse action.
102
+ #
103
+ # Example:
104
+ # str('a').capture(:b) # will store captures[:b] == 'a'
105
+ #
106
+ def capture(name)
107
+ Parslet::Atoms::Capture.new(self, name)
108
+ end
98
109
  end
@@ -0,0 +1,32 @@
1
+ # Evaluates a block at parse time. The result from the block must be a parser
2
+ # (something which implements #apply). In the first case, the parser will then
3
+ # be applied to the input, creating the result.
4
+ #
5
+ # Dynamic parses are never cached.
6
+ #
7
+ # Example:
8
+ # dynamic { rand < 0.5 ? str('a') : str('b') }
9
+ #
10
+ class Parslet::Atoms::Dynamic < Parslet::Atoms::Base
11
+ attr_reader :block
12
+
13
+ def initialize(block)
14
+ @block = block
15
+ end
16
+
17
+ def cached?
18
+ false
19
+ end
20
+
21
+ def try(source, context, consume_all)
22
+ result = block.call(source, context)
23
+
24
+ # Result is a parslet atom.
25
+ return result.apply(source, context, consume_all)
26
+ end
27
+
28
+ def to_s_inner(prec)
29
+ "dynamic { ... }"
30
+ end
31
+ end
32
+
@@ -17,8 +17,8 @@ class Parslet::Atoms::Entity < Parslet::Atoms::Base
17
17
  @block = block
18
18
  end
19
19
 
20
- def try(source, context)
21
- parslet.apply(source, context)
20
+ def try(source, context, consume_all)
21
+ parslet.apply(source, context, consume_all)
22
22
  end
23
23
 
24
24
  def parslet
@@ -21,10 +21,10 @@ class Parslet::Atoms::Lookahead < Parslet::Atoms::Base
21
21
  }
22
22
  end
23
23
 
24
- def try(source, context)
24
+ def try(source, context, consume_all)
25
25
  pos = source.pos
26
26
 
27
- success, value = bound_parslet.apply(source, context)
27
+ success, value = bound_parslet.apply(source, context, consume_all)
28
28
 
29
29
  if positive
30
30
  return succ(nil) if success
@@ -13,8 +13,8 @@ class Parslet::Atoms::Named < Parslet::Atoms::Base
13
13
  @parslet, @name = parslet, name
14
14
  end
15
15
 
16
- def apply(source, context)
17
- success, value = result = parslet.apply(source, context)
16
+ def apply(source, context, consume_all)
17
+ success, value = result = parslet.apply(source, context, consume_all)
18
18
 
19
19
  return result unless success
20
20
  succ(
@@ -20,12 +20,12 @@ class Parslet::Atoms::Re < Parslet::Atoms::Base
20
20
  }
21
21
  end
22
22
 
23
- def try(source, context)
23
+ def try(source, context, consume_all)
24
24
  return succ(source.consume(1)) if source.matches?(re)
25
25
 
26
26
  # No string could be read
27
27
  return context.err(self, source, @error_msgs[:premature]) \
28
- if source.eof?
28
+ if source.chars_left < 1
29
29
 
30
30
  # No match
31
31
  return context.err(self, source, @error_msgs[:failed])
@@ -15,18 +15,19 @@ class Parslet::Atoms::Repetition < Parslet::Atoms::Base
15
15
  @min, @max = min, max
16
16
  @tag = tag
17
17
  @error_msgs = {
18
- :minrep => "Expected at least #{min} of #{parslet.inspect}"
18
+ :minrep => "Expected at least #{min} of #{parslet.inspect}",
19
+ :unconsumed => "Extra input after last repetition"
19
20
  }
20
21
  end
21
22
 
22
- def try(source, context)
23
+ def try(source, context, consume_all)
23
24
  occ = 0
24
25
  accum = [@tag] # initialize the result array with the tag (for flattening)
25
26
  start_pos = source.pos
26
27
 
27
28
  break_on = nil
28
29
  loop do
29
- success, value = parslet.apply(source, context)
30
+ success, value = parslet.apply(source, context, false)
30
31
 
31
32
  break_on = value
32
33
  break unless success
@@ -49,6 +50,20 @@ class Parslet::Atoms::Repetition < Parslet::Atoms::Base
49
50
  start_pos,
50
51
  [break_on]) if occ < min
51
52
 
53
+ # consume_all is true, that means that we're inside the part of the parser
54
+ # that should consume the input completely. Repetition failing here means
55
+ # probably that we didn't.
56
+ #
57
+ # We have a special clause to create an error here because otherwise
58
+ # break_on would get thrown away. It turns out, that contains very
59
+ # interesting information in a lot of cases.
60
+ #
61
+ return context.err(
62
+ self,
63
+ source,
64
+ @error_msgs[:unconsumed],
65
+ [break_on]) if consume_all && source.chars_left>0
66
+
52
67
  return succ(accum)
53
68
  end
54
69
 
@@ -0,0 +1,26 @@
1
+ # Starts a new scope in the parsing process. Please also see the #captures
2
+ # method.
3
+ #
4
+ class Parslet::Atoms::Scope < Parslet::Atoms::Base
5
+ attr_reader :block
6
+ def initialize(block)
7
+ super()
8
+
9
+ @block = block
10
+ end
11
+
12
+ def cached?
13
+ false
14
+ end
15
+
16
+ def apply(source, context, consume_all)
17
+ context.scope do
18
+ parslet = block.call
19
+ return parslet.apply(source, context, consume_all)
20
+ end
21
+ end
22
+
23
+ def to_s_inner(prec)
24
+ "scope { #{block.call.to_s(prec)} }"
25
+ end
26
+ end
@@ -19,16 +19,23 @@ class Parslet::Atoms::Sequence < Parslet::Atoms::Base
19
19
  self.class.new(* @parslets+[parslet])
20
20
  end
21
21
 
22
- def try(source, context)
23
- succ([:sequence]+parslets.map { |p|
24
- success, value = p.apply(source, context)
22
+ def try(source, context, consume_all)
23
+ # Presize an array
24
+ result = Array.new(parslets.size + 1)
25
+ result[0] = :sequence
26
+
27
+ parslets.each_with_index do |p, idx|
28
+ child_consume_all = consume_all && (idx == parslets.size-1)
29
+ success, value = p.apply(source, context, child_consume_all)
25
30
 
26
31
  unless success
27
32
  return context.err(self, source, @error_msgs[:failed], [value])
28
33
  end
29
34
 
30
- value
31
- })
35
+ result[idx+1] = value
36
+ end
37
+
38
+ return succ(result)
32
39
  end
33
40
 
34
41
  precedence SEQUENCE
@@ -17,13 +17,14 @@ class Parslet::Atoms::Str < Parslet::Atoms::Base
17
17
  }
18
18
  end
19
19
 
20
- def try(source, context)
20
+ def try(source, context, consume_all)
21
21
  return succ(source.consume(@len)) if source.matches?(str)
22
22
 
23
- # Failures:
23
+ # Input ending early:
24
24
  return context.err(self, source, @error_msgs[:premature]) \
25
25
  if source.chars_left<@len
26
-
26
+
27
+ # Expected something, but got something else instead:
27
28
  error_pos = source.pos
28
29
  return context.err_at(
29
30
  self, source,
@@ -26,8 +26,6 @@ class Parslet::Atoms::Base
26
26
  #
27
27
  def parse_with_debug str, opts={}
28
28
  parse str, opts
29
- rescue Parslet::UnconsumedInput => error
30
- puts error
31
29
  rescue Parslet::ParseFailed => error
32
30
  puts error.cause.ascii_tree
33
31
  end
@@ -57,8 +57,8 @@ class Parslet::Parser < Parslet::Atoms::Base
57
57
  end
58
58
  end
59
59
 
60
- def try(source, context)
61
- root.try(source, context)
60
+ def try(source, context, consume_all)
61
+ root.try(source, context, consume_all)
62
62
  end
63
63
 
64
64
  def to_s_inner(prec)
@@ -0,0 +1,42 @@
1
+ class Parslet::Scope
2
+ # Raised when the accessed slot has never been assigned a value.
3
+ #
4
+ class NotFound < StandardError
5
+ end
6
+
7
+ class Binding
8
+ attr_reader :parent
9
+
10
+ def initialize(parent=nil)
11
+ @parent = parent
12
+ @hash = Hash.new
13
+ end
14
+
15
+ def [](k)
16
+ @hash.has_key?(k) && @hash[k] ||
17
+ parent && parent[k] or
18
+ raise NotFound
19
+ end
20
+ def []=(k,v)
21
+ @hash.store(k,v)
22
+ end
23
+ end
24
+
25
+ def [](k)
26
+ @current[k]
27
+ end
28
+ def []=(k,v)
29
+ @current[k] = v
30
+ end
31
+
32
+ def initialize
33
+ @current = Binding.new
34
+ end
35
+
36
+ def push
37
+ @current = Binding.new(@current)
38
+ end
39
+ def pop
40
+ @current = @current.parent
41
+ end
42
+ end
@@ -47,10 +47,6 @@ module Parslet
47
47
  @str.size - @pos
48
48
  end
49
49
 
50
- def eof?
51
- @pos >= @str.size
52
- end
53
-
54
50
  # Position of the parse as a character offset into the original string.
55
51
  # @note: Encodings...
56
52
  attr_accessor :pos
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parslet
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,136 +9,152 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-05-25 00:00:00.000000000 Z
12
+ date: 2012-12-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: blankslate
16
+ prerelease: false
16
17
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
21
  version: '2.0'
22
+ none: false
22
23
  type: :runtime
23
- prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
25
  requirements:
27
26
  - - ~>
28
27
  - !ruby/object:Gem::Version
29
28
  version: '2.0'
29
+ none: false
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: rspec
32
+ prerelease: false
32
33
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
34
  requirements:
35
35
  - - ! '>='
36
36
  - !ruby/object:Gem::Version
37
37
  version: '0'
38
+ none: false
38
39
  type: :development
39
- prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
41
  requirements:
43
42
  - - ! '>='
44
43
  - !ruby/object:Gem::Version
45
44
  version: '0'
45
+ none: false
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: flexmock
48
+ prerelease: false
48
49
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
50
  requirements:
51
51
  - - ! '>='
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
+ none: false
54
55
  type: :development
55
- prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
57
  requirements:
59
58
  - - ! '>='
60
59
  - !ruby/object:Gem::Version
61
60
  version: '0'
61
+ none: false
62
62
  - !ruby/object:Gem::Dependency
63
63
  name: rdoc
64
+ prerelease: false
64
65
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
66
  requirements:
67
67
  - - ! '>='
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
+ none: false
70
71
  type: :development
71
- prerelease: false
72
72
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
73
  requirements:
75
74
  - - ! '>='
76
75
  - !ruby/object:Gem::Version
77
76
  version: '0'
77
+ none: false
78
78
  - !ruby/object:Gem::Dependency
79
79
  name: sdoc
80
+ prerelease: false
80
81
  requirement: !ruby/object:Gem::Requirement
81
- none: false
82
82
  requirements:
83
83
  - - ! '>='
84
84
  - !ruby/object:Gem::Version
85
85
  version: '0'
86
+ none: false
86
87
  type: :development
87
- prerelease: false
88
88
  version_requirements: !ruby/object:Gem::Requirement
89
- none: false
90
89
  requirements:
91
90
  - - ! '>='
92
91
  - !ruby/object:Gem::Version
93
92
  version: '0'
93
+ none: false
94
94
  - !ruby/object:Gem::Dependency
95
95
  name: guard
96
+ prerelease: false
96
97
  requirement: !ruby/object:Gem::Requirement
97
- none: false
98
98
  requirements:
99
99
  - - ! '>='
100
100
  - !ruby/object:Gem::Version
101
101
  version: '0'
102
+ none: false
102
103
  type: :development
103
- prerelease: false
104
104
  version_requirements: !ruby/object:Gem::Requirement
105
- none: false
106
105
  requirements:
107
106
  - - ! '>='
108
107
  - !ruby/object:Gem::Version
109
108
  version: '0'
109
+ none: false
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: guard-rspec
112
+ prerelease: false
112
113
  requirement: !ruby/object:Gem::Requirement
113
- none: false
114
114
  requirements:
115
115
  - - ! '>='
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0'
118
+ none: false
118
119
  type: :development
119
- prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ none: false
126
+ - !ruby/object:Gem::Dependency
127
+ name: rb-fsevent
128
+ prerelease: false
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
121
134
  none: false
135
+ type: :development
136
+ version_requirements: !ruby/object:Gem::Requirement
122
137
  requirements:
123
138
  - - ! '>='
124
139
  - !ruby/object:Gem::Version
125
140
  version: '0'
141
+ none: false
126
142
  - !ruby/object:Gem::Dependency
127
143
  name: growl
144
+ prerelease: false
128
145
  requirement: !ruby/object:Gem::Requirement
129
- none: false
130
146
  requirements:
131
147
  - - ! '>='
132
148
  - !ruby/object:Gem::Version
133
149
  version: '0'
150
+ none: false
134
151
  type: :development
135
- prerelease: false
136
152
  version_requirements: !ruby/object:Gem::Requirement
137
- none: false
138
153
  requirements:
139
154
  - - ! '>='
140
155
  - !ruby/object:Gem::Version
141
156
  version: '0'
157
+ none: false
142
158
  description:
143
159
  email: kaspar.schiess@absurd.li
144
160
  executables: []
@@ -153,13 +169,16 @@ files:
153
169
  - lib/parslet/atoms/alternative.rb
154
170
  - lib/parslet/atoms/base.rb
155
171
  - lib/parslet/atoms/can_flatten.rb
172
+ - lib/parslet/atoms/capture.rb
156
173
  - lib/parslet/atoms/context.rb
157
174
  - lib/parslet/atoms/dsl.rb
175
+ - lib/parslet/atoms/dynamic.rb
158
176
  - lib/parslet/atoms/entity.rb
159
177
  - lib/parslet/atoms/lookahead.rb
160
178
  - lib/parslet/atoms/named.rb
161
179
  - lib/parslet/atoms/re.rb
162
180
  - lib/parslet/atoms/repetition.rb
181
+ - lib/parslet/atoms/scope.rb
163
182
  - lib/parslet/atoms/sequence.rb
164
183
  - lib/parslet/atoms/str.rb
165
184
  - lib/parslet/atoms/visitor.rb
@@ -176,6 +195,7 @@ files:
176
195
  - lib/parslet/pattern/binding.rb
177
196
  - lib/parslet/pattern.rb
178
197
  - lib/parslet/rig/rspec.rb
198
+ - lib/parslet/scope.rb
179
199
  - lib/parslet/slice.rb
180
200
  - lib/parslet/source/line_cache.rb
181
201
  - lib/parslet/source.rb
@@ -183,6 +203,8 @@ files:
183
203
  - lib/parslet/transform.rb
184
204
  - lib/parslet.rb
185
205
  - example/boolean_algebra.rb
206
+ - example/calc.rb
207
+ - example/capture.rb
186
208
  - example/comments.rb
187
209
  - example/deepest_errors.rb
188
210
  - example/documentation.rb
@@ -198,6 +220,8 @@ files:
198
220
  - example/modularity.rb
199
221
  - example/nested_errors.rb
200
222
  - example/output/boolean_algebra.out
223
+ - example/output/calc.out
224
+ - example/output/capture.out
201
225
  - example/output/comments.out
202
226
  - example/output/deepest_errors.out
203
227
  - example/output/documentation.err
@@ -216,12 +240,14 @@ files:
216
240
  - example/output/nested_errors.out
217
241
  - example/output/parens.out
218
242
  - example/output/readme.out
243
+ - example/output/scopes.out
219
244
  - example/output/seasons.out
220
245
  - example/output/sentence.out
221
246
  - example/output/simple_xml.out
222
247
  - example/output/string_parser.out
223
248
  - example/parens.rb
224
249
  - example/readme.rb
250
+ - example/scopes.rb
225
251
  - example/seasons.rb
226
252
  - example/sentence.rb
227
253
  - example/simple.lit
@@ -237,20 +263,17 @@ rdoc_options:
237
263
  require_paths:
238
264
  - lib
239
265
  required_ruby_version: !ruby/object:Gem::Requirement
240
- none: false
241
266
  requirements:
242
267
  - - ! '>='
243
268
  - !ruby/object:Gem::Version
244
269
  version: '0'
245
- segments:
246
- - 0
247
- hash: 1524575203779308108
248
- required_rubygems_version: !ruby/object:Gem::Requirement
249
270
  none: false
271
+ required_rubygems_version: !ruby/object:Gem::Requirement
250
272
  requirements:
251
273
  - - ! '>='
252
274
  - !ruby/object:Gem::Version
253
275
  version: '0'
276
+ none: false
254
277
  requirements: []
255
278
  rubyforge_project:
256
279
  rubygems_version: 1.8.24
@@ -258,3 +281,4 @@ signing_key:
258
281
  specification_version: 3
259
282
  summary: Parser construction library with great error reporting in Ruby.
260
283
  test_files: []
284
+ has_rdoc: