parslet 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: