plurimath-parslet 3.0.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.
- checksums.yaml +7 -0
- data/HISTORY.txt +284 -0
- data/LICENSE +23 -0
- data/README.adoc +454 -0
- data/Rakefile +71 -0
- data/lib/parslet/accelerator/application.rb +62 -0
- data/lib/parslet/accelerator/engine.rb +112 -0
- data/lib/parslet/accelerator.rb +162 -0
- data/lib/parslet/atoms/alternative.rb +53 -0
- data/lib/parslet/atoms/base.rb +157 -0
- data/lib/parslet/atoms/can_flatten.rb +137 -0
- data/lib/parslet/atoms/capture.rb +38 -0
- data/lib/parslet/atoms/context.rb +103 -0
- data/lib/parslet/atoms/dsl.rb +112 -0
- data/lib/parslet/atoms/dynamic.rb +32 -0
- data/lib/parslet/atoms/entity.rb +45 -0
- data/lib/parslet/atoms/ignored.rb +26 -0
- data/lib/parslet/atoms/infix.rb +115 -0
- data/lib/parslet/atoms/lookahead.rb +52 -0
- data/lib/parslet/atoms/named.rb +32 -0
- data/lib/parslet/atoms/re.rb +41 -0
- data/lib/parslet/atoms/repetition.rb +87 -0
- data/lib/parslet/atoms/scope.rb +26 -0
- data/lib/parslet/atoms/sequence.rb +48 -0
- data/lib/parslet/atoms/str.rb +42 -0
- data/lib/parslet/atoms/visitor.rb +89 -0
- data/lib/parslet/atoms.rb +34 -0
- data/lib/parslet/cause.rb +101 -0
- data/lib/parslet/context.rb +21 -0
- data/lib/parslet/convenience.rb +33 -0
- data/lib/parslet/error_reporter/contextual.rb +120 -0
- data/lib/parslet/error_reporter/deepest.rb +100 -0
- data/lib/parslet/error_reporter/tree.rb +63 -0
- data/lib/parslet/error_reporter.rb +8 -0
- data/lib/parslet/export.rb +163 -0
- data/lib/parslet/expression/treetop.rb +92 -0
- data/lib/parslet/expression.rb +51 -0
- data/lib/parslet/graphviz.rb +97 -0
- data/lib/parslet/parser.rb +68 -0
- data/lib/parslet/pattern/binding.rb +49 -0
- data/lib/parslet/pattern.rb +113 -0
- data/lib/parslet/position.rb +21 -0
- data/lib/parslet/rig/rspec.rb +52 -0
- data/lib/parslet/scope.rb +42 -0
- data/lib/parslet/slice.rb +105 -0
- data/lib/parslet/source/line_cache.rb +99 -0
- data/lib/parslet/source.rb +96 -0
- data/lib/parslet/transform.rb +265 -0
- data/lib/parslet/version.rb +5 -0
- data/lib/parslet.rb +314 -0
- data/plurimath-parslet.gemspec +42 -0
- data/spec/acceptance/infix_parser_spec.rb +145 -0
- data/spec/acceptance/mixing_parsers_spec.rb +74 -0
- data/spec/acceptance/regression_spec.rb +329 -0
- data/spec/acceptance/repetition_and_maybe_spec.rb +44 -0
- data/spec/acceptance/unconsumed_input_spec.rb +21 -0
- data/spec/examples/boolean_algebra_spec.rb +257 -0
- data/spec/examples/calc_spec.rb +278 -0
- data/spec/examples/capture_spec.rb +137 -0
- data/spec/examples/comments_spec.rb +186 -0
- data/spec/examples/deepest_errors_spec.rb +420 -0
- data/spec/examples/documentation_spec.rb +205 -0
- data/spec/examples/email_parser_spec.rb +275 -0
- data/spec/examples/empty_spec.rb +37 -0
- data/spec/examples/erb_spec.rb +482 -0
- data/spec/examples/ip_address_spec.rb +153 -0
- data/spec/examples/json_spec.rb +413 -0
- data/spec/examples/local_spec.rb +302 -0
- data/spec/examples/mathn_spec.rb +151 -0
- data/spec/examples/minilisp_spec.rb +492 -0
- data/spec/examples/modularity_spec.rb +340 -0
- data/spec/examples/nested_errors_spec.rb +322 -0
- data/spec/examples/optimized_erb_spec.rb +299 -0
- data/spec/examples/parens_spec.rb +239 -0
- data/spec/examples/prec_calc_spec.rb +525 -0
- data/spec/examples/readme_spec.rb +228 -0
- data/spec/examples/scopes_spec.rb +187 -0
- data/spec/examples/seasons_spec.rb +196 -0
- data/spec/examples/sentence_spec.rb +119 -0
- data/spec/examples/simple_xml_spec.rb +250 -0
- data/spec/examples/string_parser_spec.rb +407 -0
- data/spec/fixtures/examples/boolean_algebra.rb +62 -0
- data/spec/fixtures/examples/calc.rb +86 -0
- data/spec/fixtures/examples/capture.rb +36 -0
- data/spec/fixtures/examples/comments.rb +22 -0
- data/spec/fixtures/examples/deepest_errors.rb +99 -0
- data/spec/fixtures/examples/documentation.rb +32 -0
- data/spec/fixtures/examples/email_parser.rb +42 -0
- data/spec/fixtures/examples/empty.rb +10 -0
- data/spec/fixtures/examples/erb.rb +39 -0
- data/spec/fixtures/examples/ip_address.rb +103 -0
- data/spec/fixtures/examples/json.rb +107 -0
- data/spec/fixtures/examples/local.rb +60 -0
- data/spec/fixtures/examples/mathn.rb +47 -0
- data/spec/fixtures/examples/minilisp.rb +75 -0
- data/spec/fixtures/examples/modularity.rb +60 -0
- data/spec/fixtures/examples/nested_errors.rb +95 -0
- data/spec/fixtures/examples/optimized_erb.rb +105 -0
- data/spec/fixtures/examples/parens.rb +25 -0
- data/spec/fixtures/examples/prec_calc.rb +71 -0
- data/spec/fixtures/examples/readme.rb +59 -0
- data/spec/fixtures/examples/scopes.rb +43 -0
- data/spec/fixtures/examples/seasons.rb +40 -0
- data/spec/fixtures/examples/sentence.rb +18 -0
- data/spec/fixtures/examples/simple_xml.rb +51 -0
- data/spec/fixtures/examples/string_parser.rb +77 -0
- data/spec/parslet/atom_results_spec.rb +39 -0
- data/spec/parslet/atoms/alternative_spec.rb +26 -0
- data/spec/parslet/atoms/base_spec.rb +127 -0
- data/spec/parslet/atoms/capture_spec.rb +21 -0
- data/spec/parslet/atoms/combinations_spec.rb +5 -0
- data/spec/parslet/atoms/dsl_spec.rb +7 -0
- data/spec/parslet/atoms/entity_spec.rb +77 -0
- data/spec/parslet/atoms/ignored_spec.rb +15 -0
- data/spec/parslet/atoms/infix_spec.rb +5 -0
- data/spec/parslet/atoms/lookahead_spec.rb +22 -0
- data/spec/parslet/atoms/named_spec.rb +4 -0
- data/spec/parslet/atoms/re_spec.rb +14 -0
- data/spec/parslet/atoms/repetition_spec.rb +24 -0
- data/spec/parslet/atoms/scope_spec.rb +26 -0
- data/spec/parslet/atoms/sequence_spec.rb +28 -0
- data/spec/parslet/atoms/str_spec.rb +15 -0
- data/spec/parslet/atoms/visitor_spec.rb +101 -0
- data/spec/parslet/atoms_spec.rb +488 -0
- data/spec/parslet/convenience_spec.rb +54 -0
- data/spec/parslet/error_reporter/contextual_spec.rb +118 -0
- data/spec/parslet/error_reporter/deepest_spec.rb +82 -0
- data/spec/parslet/error_reporter/tree_spec.rb +7 -0
- data/spec/parslet/export_spec.rb +40 -0
- data/spec/parslet/expression/treetop_spec.rb +74 -0
- data/spec/parslet/minilisp.citrus +29 -0
- data/spec/parslet/minilisp.tt +29 -0
- data/spec/parslet/parser_spec.rb +36 -0
- data/spec/parslet/parslet_spec.rb +38 -0
- data/spec/parslet/pattern_spec.rb +272 -0
- data/spec/parslet/position_spec.rb +14 -0
- data/spec/parslet/rig/rspec_spec.rb +54 -0
- data/spec/parslet/scope_spec.rb +45 -0
- data/spec/parslet/slice_spec.rb +186 -0
- data/spec/parslet/source/line_cache_spec.rb +74 -0
- data/spec/parslet/source_spec.rb +210 -0
- data/spec/parslet/transform/context_spec.rb +56 -0
- data/spec/parslet/transform_spec.rb +183 -0
- data/spec/spec_helper.rb +74 -0
- data/spec/support/opal.rb +8 -0
- data/spec/support/opal.rb.erb +14 -0
- data/spec/support/parslet_matchers.rb +96 -0
- metadata +240 -0
@@ -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
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# A slice is a small part from the parse input. A slice mainly behaves like
|
2
|
+
# any other string, except that it remembers where it came from (offset in
|
3
|
+
# original input).
|
4
|
+
#
|
5
|
+
# == Extracting line and column
|
6
|
+
#
|
7
|
+
# Using the #line_and_column method, you can extract the line and column in
|
8
|
+
# the original input where this slice starts.
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
# slice.line_and_column # => [1, 13]
|
12
|
+
# slice.offset # => 12
|
13
|
+
#
|
14
|
+
# == Likeness to strings
|
15
|
+
#
|
16
|
+
# Parslet::Slice behaves in many ways like a Ruby String. This likeness
|
17
|
+
# however is not complete - many of the myriad of operations String supports
|
18
|
+
# are not yet in Slice. You can always extract the internal string instance by
|
19
|
+
# calling #to_s.
|
20
|
+
#
|
21
|
+
# These omissions are somewhat intentional. Rather than maintaining a full
|
22
|
+
# delegation, we opt for a partial emulation that gets the job done.
|
23
|
+
#
|
24
|
+
class Parslet::Slice
|
25
|
+
attr_reader :str, :position, :line_cache
|
26
|
+
|
27
|
+
# Construct a slice using a string, an offset and an optional line cache.
|
28
|
+
# The line cache should be able to answer to the #line_and_column message.
|
29
|
+
#
|
30
|
+
def initialize(position, string, line_cache = nil)
|
31
|
+
@position = position
|
32
|
+
@str = string
|
33
|
+
@line_cache = line_cache
|
34
|
+
end
|
35
|
+
|
36
|
+
def offset
|
37
|
+
@position.charpos
|
38
|
+
end
|
39
|
+
|
40
|
+
# Compares slices to other slices or strings.
|
41
|
+
#
|
42
|
+
def ==(other)
|
43
|
+
str == other
|
44
|
+
end
|
45
|
+
|
46
|
+
# Match regular expressions.
|
47
|
+
#
|
48
|
+
def match(regexp)
|
49
|
+
str.match(regexp)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the slices size in characters.
|
53
|
+
#
|
54
|
+
def size
|
55
|
+
str.size
|
56
|
+
end
|
57
|
+
|
58
|
+
alias length size
|
59
|
+
|
60
|
+
# Concatenate two slices; it is assumed that the second slice begins
|
61
|
+
# where the first one ends. The offset of the resulting slice is the same
|
62
|
+
# as the one of this slice.
|
63
|
+
#
|
64
|
+
def +(other)
|
65
|
+
self.class.new(@position, str + other.to_s, line_cache)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns a <line, column> tuple referring to the original input.
|
69
|
+
#
|
70
|
+
def line_and_column
|
71
|
+
raise ArgumentError, 'No line cache was given, cannot infer line and column.' \
|
72
|
+
unless line_cache
|
73
|
+
|
74
|
+
line_cache.line_and_column(@position.bytepos)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Conversion operators -----------------------------------------------------
|
78
|
+
def to_str
|
79
|
+
str
|
80
|
+
end
|
81
|
+
alias to_s to_str
|
82
|
+
|
83
|
+
def to_slice
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_sym
|
88
|
+
str.to_sym
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_i
|
92
|
+
self.str.to_i
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_f
|
96
|
+
str.to_f
|
97
|
+
end
|
98
|
+
|
99
|
+
# Inspection & Debugging ---------------------------------------------------
|
100
|
+
|
101
|
+
# Prints the slice as <code>"string"@offset</code>.
|
102
|
+
def inspect
|
103
|
+
str.inspect + "@#{offset}"
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Parslet::Source
|
4
|
+
# A cache for line start positions.
|
5
|
+
#
|
6
|
+
class LineCache
|
7
|
+
def initialize
|
8
|
+
# Stores line endings as a simple position number. The first line always
|
9
|
+
# starts at 0; numbers beyond the biggest entry are on any line > size,
|
10
|
+
# but probably make a scan to that position neccessary.
|
11
|
+
@line_ends = []
|
12
|
+
@line_ends.extend RangeSearch
|
13
|
+
@last_line_end = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns a <line, column> tuple for the given input position. Input
|
17
|
+
# position must be given as byte offset into original string.
|
18
|
+
#
|
19
|
+
def line_and_column(pos)
|
20
|
+
pos = pos.bytepos if pos.respond_to? :bytepos
|
21
|
+
eol_idx = @line_ends.lbound(pos)
|
22
|
+
|
23
|
+
if eol_idx
|
24
|
+
# eol_idx points to the offset that ends the current line.
|
25
|
+
# Let's try to find the offset that starts it:
|
26
|
+
offset = eol_idx>0 && @line_ends[eol_idx-1] || 0
|
27
|
+
return [eol_idx+1, pos-offset+1]
|
28
|
+
else
|
29
|
+
# eol_idx is nil, that means that we're beyond the last line end that
|
30
|
+
# we know about. Pretend for now that we're just on the last line.
|
31
|
+
offset = @line_ends.last || 0
|
32
|
+
return [@line_ends.size+1, pos-offset+1]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def scan_for_line_endings(start_pos, buf)
|
37
|
+
return unless buf
|
38
|
+
|
39
|
+
buf = StringScanner.new(buf)
|
40
|
+
return unless buf.exist?(/\n/)
|
41
|
+
|
42
|
+
## If we have already read part or all of buf, we already know about
|
43
|
+
## line ends in that portion. remove it and correct cur (search index)
|
44
|
+
if @last_line_end && start_pos < @last_line_end
|
45
|
+
# Let's not search the range from start_pos to last_line_end again.
|
46
|
+
buf.pos = @last_line_end - start_pos
|
47
|
+
end
|
48
|
+
|
49
|
+
## Scan the string for line endings; store the positions of all endings
|
50
|
+
## in @line_ends.
|
51
|
+
while buf.skip_until(/\n/)
|
52
|
+
@last_line_end = start_pos + buf.pos
|
53
|
+
@line_ends << @last_line_end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Mixin for arrays that implicitly give a number of ranges, where one range
|
59
|
+
# begins where the other one ends.
|
60
|
+
#
|
61
|
+
# Example:
|
62
|
+
#
|
63
|
+
# [10, 20, 30]
|
64
|
+
# # would describe [0, 10], (10, 20], (20, 30]
|
65
|
+
#
|
66
|
+
module RangeSearch
|
67
|
+
def find_mid(left, right)
|
68
|
+
# NOTE: Jonathan Hinkle reported that when mathn is required, just
|
69
|
+
# dividing and relying on the integer truncation is not enough.
|
70
|
+
left + ((right - left) / 2).floor
|
71
|
+
end
|
72
|
+
|
73
|
+
# Scans the array for the first number that is > than bound. Returns the
|
74
|
+
# index of that number.
|
75
|
+
#
|
76
|
+
def lbound(bound)
|
77
|
+
return nil if empty?
|
78
|
+
return nil unless last > bound
|
79
|
+
|
80
|
+
left = 0
|
81
|
+
right = size - 1
|
82
|
+
|
83
|
+
loop do
|
84
|
+
mid = find_mid(left, right)
|
85
|
+
|
86
|
+
if self[mid] > bound
|
87
|
+
right = mid
|
88
|
+
else
|
89
|
+
# assert: self[mid] <= bound
|
90
|
+
left = mid+1
|
91
|
+
end
|
92
|
+
|
93
|
+
if right <= left
|
94
|
+
return right
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
|
2
|
+
require 'stringio'
|
3
|
+
require 'strscan'
|
4
|
+
|
5
|
+
require 'parslet/position'
|
6
|
+
require 'parslet/source/line_cache'
|
7
|
+
|
8
|
+
module Parslet
|
9
|
+
# Wraps the input string for parslet.
|
10
|
+
#
|
11
|
+
class Source
|
12
|
+
def initialize(str)
|
13
|
+
raise(
|
14
|
+
ArgumentError,
|
15
|
+
"Must construct Source with a string like object."
|
16
|
+
) unless str.respond_to?(:to_str)
|
17
|
+
|
18
|
+
@str = StringScanner.new(str)
|
19
|
+
|
20
|
+
# maps 1 => /./m, 2 => /../m, etc...
|
21
|
+
@re_cache = Hash.new { |h,k|
|
22
|
+
h[k] = /(.|$){#{k}}/m }
|
23
|
+
|
24
|
+
@line_cache = LineCache.new
|
25
|
+
@line_cache.scan_for_line_endings(0, str)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Checks if the given pattern matches at the current input position.
|
29
|
+
#
|
30
|
+
# @param pattern [Regexp] pattern to check for
|
31
|
+
# @return [Boolean] true if the pattern matches at #pos
|
32
|
+
#
|
33
|
+
def matches?(pattern)
|
34
|
+
@str.match?(pattern)
|
35
|
+
end
|
36
|
+
alias match matches?
|
37
|
+
|
38
|
+
# Consumes n characters from the input, returning them as a slice of the
|
39
|
+
# input.
|
40
|
+
#
|
41
|
+
def consume(n)
|
42
|
+
position = self.pos
|
43
|
+
slice_str = @str.scan(@re_cache[n])
|
44
|
+
slice = Parslet::Slice.new(
|
45
|
+
position,
|
46
|
+
slice_str,
|
47
|
+
@line_cache)
|
48
|
+
|
49
|
+
return slice
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns how many chars remain in the input.
|
53
|
+
#
|
54
|
+
def chars_left
|
55
|
+
@str.rest_size
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns how many chars there are between current position and the
|
59
|
+
# string given. If the string given doesn't occur in the source, then
|
60
|
+
# the remaining chars (#chars_left) are returned.
|
61
|
+
#
|
62
|
+
# @return [Fixnum] count of chars until str or #chars_left
|
63
|
+
#
|
64
|
+
def chars_until str
|
65
|
+
slice_str = @str.check_until(Regexp.new(Regexp.escape(str)))
|
66
|
+
return chars_left unless slice_str
|
67
|
+
return slice_str.size - str.size
|
68
|
+
end
|
69
|
+
|
70
|
+
# Position of the parse as a character offset into the original string.
|
71
|
+
#
|
72
|
+
# @note Please be aware of encodings at this point.
|
73
|
+
#
|
74
|
+
def pos
|
75
|
+
Position.new(@str.string, @str.pos)
|
76
|
+
end
|
77
|
+
def bytepos
|
78
|
+
@str.pos
|
79
|
+
end
|
80
|
+
|
81
|
+
# @note Please be aware of encodings at this point.
|
82
|
+
#
|
83
|
+
def bytepos=(n)
|
84
|
+
@str.pos = n
|
85
|
+
rescue RangeError
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns a <line, column> tuple for the given position. If no position is
|
89
|
+
# given, line/column information is returned for the current position
|
90
|
+
# given by #pos.
|
91
|
+
#
|
92
|
+
def line_and_column(position=nil)
|
93
|
+
@line_cache.line_and_column(position || self.bytepos)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,265 @@
|
|
1
|
+
|
2
|
+
require 'parslet/pattern'
|
3
|
+
|
4
|
+
# Transforms an expression tree into something else. The transformation
|
5
|
+
# performs a depth-first, post-order traversal of the expression tree. During
|
6
|
+
# that traversal, each time a rule matches a node, the node is replaced by the
|
7
|
+
# result of the block associated to the rule. Otherwise the node is accepted
|
8
|
+
# as is into the result tree.
|
9
|
+
#
|
10
|
+
# This is almost what you would generally do with a tree visitor, except that
|
11
|
+
# you can match several levels of the tree at once.
|
12
|
+
#
|
13
|
+
# As a consequence of this, the resulting tree will contain pieces of the
|
14
|
+
# original tree and new pieces. Most likely, you will want to transform the
|
15
|
+
# original tree wholly, so this isn't a problem.
|
16
|
+
#
|
17
|
+
# You will not be able to create a loop, given that each node will be replaced
|
18
|
+
# only once and then left alone. This means that the results of a replacement
|
19
|
+
# will not be acted upon.
|
20
|
+
#
|
21
|
+
# Example:
|
22
|
+
#
|
23
|
+
# class Example < Parslet::Transform
|
24
|
+
# rule(:string => simple(:x)) { # (1)
|
25
|
+
# StringLiteral.new(x)
|
26
|
+
# }
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# A tree transform (Parslet::Transform) is defined by a set of rules. Each
|
30
|
+
# rule can be defined by calling #rule with the pattern as argument. The block
|
31
|
+
# given will be called every time the rule matches somewhere in the tree given
|
32
|
+
# to #apply. It is passed a Hash containing all the variable bindings of this
|
33
|
+
# pattern match.
|
34
|
+
#
|
35
|
+
# In the above example, (1) illustrates a simple matching rule.
|
36
|
+
#
|
37
|
+
# Let's say you want to parse matching parentheses and distill a maximum nest
|
38
|
+
# depth. You would probably write a parser like the one in example/parens.rb;
|
39
|
+
# here's the relevant part:
|
40
|
+
#
|
41
|
+
# rule(:balanced) {
|
42
|
+
# str('(').as(:l) >> balanced.maybe.as(:m) >> str(')').as(:r)
|
43
|
+
# }
|
44
|
+
#
|
45
|
+
# If you now apply this to a string like '(())', you get a intermediate parse
|
46
|
+
# tree that looks like this:
|
47
|
+
#
|
48
|
+
# {
|
49
|
+
# l: '(',
|
50
|
+
# m: {
|
51
|
+
# l: '(',
|
52
|
+
# m: nil,
|
53
|
+
# r: ')'
|
54
|
+
# },
|
55
|
+
# r: ')'
|
56
|
+
# }
|
57
|
+
#
|
58
|
+
# This parse tree is good for debugging, but what we would really like to have
|
59
|
+
# is just the nesting depth. This transformation rule will produce that:
|
60
|
+
#
|
61
|
+
# rule(:l => '(', :m => simple(:x), :r => ')') {
|
62
|
+
# # innermost :m will contain nil
|
63
|
+
# x.nil? ? 1 : x+1
|
64
|
+
# }
|
65
|
+
#
|
66
|
+
# = Usage patterns
|
67
|
+
#
|
68
|
+
# There are four ways of using this class. The first one is very much
|
69
|
+
# recommended, followed by the second one for generality. The other ones are
|
70
|
+
# omitted here.
|
71
|
+
#
|
72
|
+
# Recommended usage is as follows:
|
73
|
+
#
|
74
|
+
# class MyTransformator < Parslet::Transform
|
75
|
+
# rule(...) { ... }
|
76
|
+
# rule(...) { ... }
|
77
|
+
# # ...
|
78
|
+
# end
|
79
|
+
# MyTransformator.new.apply(tree)
|
80
|
+
#
|
81
|
+
# Alternatively, you can use the Transform class as follows:
|
82
|
+
#
|
83
|
+
# transform = Parslet::Transform.new do
|
84
|
+
# rule(...) { ... }
|
85
|
+
# end
|
86
|
+
# transform.apply(tree)
|
87
|
+
#
|
88
|
+
# = Execution context
|
89
|
+
#
|
90
|
+
# The execution context of action blocks differs depending on the arity of
|
91
|
+
# said blocks. This can be confusing. It is however somewhat intentional. You
|
92
|
+
# should not create fat Transform descendants containing a lot of helper methods,
|
93
|
+
# instead keep your AST class construction in global scope or make it available
|
94
|
+
# through a factory. The following piece of code illustrates usage of global
|
95
|
+
# scope:
|
96
|
+
#
|
97
|
+
# transform = Parslet::Transform.new do
|
98
|
+
# rule(...) { AstNode.new(a_variable) }
|
99
|
+
# rule(...) { Ast.node(a_variable) } # modules are nice
|
100
|
+
# end
|
101
|
+
# transform.apply(tree)
|
102
|
+
#
|
103
|
+
# And here's how you would use a class builder (a factory):
|
104
|
+
#
|
105
|
+
# transform = Parslet::Transform.new do
|
106
|
+
# rule(...) { builder.add_node(a_variable) }
|
107
|
+
# rule(...) { |d| d[:builder].add_node(d[:a_variable]) }
|
108
|
+
# end
|
109
|
+
# transform.apply(tree, :builder => Builder.new)
|
110
|
+
#
|
111
|
+
# As you can see, Transform allows you to inject local context for your rule
|
112
|
+
# action blocks to use.
|
113
|
+
#
|
114
|
+
class Parslet::Transform
|
115
|
+
# FIXME: Maybe only part of it? Or maybe only include into constructor
|
116
|
+
# context?
|
117
|
+
include Parslet
|
118
|
+
|
119
|
+
class << self
|
120
|
+
# FIXME: Only do this for subclasses?
|
121
|
+
include Parslet
|
122
|
+
|
123
|
+
# Define a rule for the transform subclass.
|
124
|
+
#
|
125
|
+
def rule(expression, &block)
|
126
|
+
@__transform_rules ||= []
|
127
|
+
# Prepend new rules so they have higher precedence than older rules
|
128
|
+
@__transform_rules.unshift([Parslet::Pattern.new(expression), block])
|
129
|
+
end
|
130
|
+
|
131
|
+
# Allows accessing the class' rules
|
132
|
+
#
|
133
|
+
def rules
|
134
|
+
@__transform_rules ||= []
|
135
|
+
end
|
136
|
+
|
137
|
+
def inherited(subclass)
|
138
|
+
super
|
139
|
+
subclass.instance_variable_set(:@__transform_rules, rules.dup)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def initialize(raise_on_unmatch=false, &block)
|
144
|
+
@raise_on_unmatch = raise_on_unmatch
|
145
|
+
@rules = []
|
146
|
+
|
147
|
+
if block
|
148
|
+
instance_eval(&block)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Defines a rule to be applied whenever apply is called on a tree. A rule
|
153
|
+
# is composed of two parts:
|
154
|
+
#
|
155
|
+
# * an *expression pattern*
|
156
|
+
# * a *transformation block*
|
157
|
+
#
|
158
|
+
def rule(expression, &block)
|
159
|
+
# Prepend new rules so they have higher precedence than older rules
|
160
|
+
@rules.unshift([Parslet::Pattern.new(expression), block])
|
161
|
+
end
|
162
|
+
|
163
|
+
# Applies the transformation to a tree that is generated by Parslet::Parser
|
164
|
+
# or a simple parslet. Transformation will proceed down the tree, replacing
|
165
|
+
# parts/all of it with new objects. The resulting object will be returned.
|
166
|
+
#
|
167
|
+
# Using the context parameter, you can inject bindings for the transformation.
|
168
|
+
# This can be used to allow access to the outside world from transform blocks,
|
169
|
+
# like so:
|
170
|
+
#
|
171
|
+
# document = # some class that you act on
|
172
|
+
# transform.apply(tree, document: document)
|
173
|
+
#
|
174
|
+
# The above will make document available to all your action blocks:
|
175
|
+
#
|
176
|
+
# # Variant A
|
177
|
+
# rule(...) { document.foo(bar) }
|
178
|
+
# # Variant B
|
179
|
+
# rule(...) { |d| d[:document].foo(d[:bar]) }
|
180
|
+
#
|
181
|
+
# @param obj PORO ast to transform
|
182
|
+
# @param context start context to inject into the bindings.
|
183
|
+
#
|
184
|
+
def apply(obj, context=nil)
|
185
|
+
transform_elt(
|
186
|
+
case obj
|
187
|
+
when Hash
|
188
|
+
recurse_hash(obj, context)
|
189
|
+
when Array
|
190
|
+
recurse_array(obj, context)
|
191
|
+
else
|
192
|
+
obj
|
193
|
+
end,
|
194
|
+
context
|
195
|
+
)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Executes the block on the bindings obtained by Pattern#match, if such a match
|
199
|
+
# can be made. Depending on the arity of the given block, it is called in
|
200
|
+
# one of two environments: the current one or a clean toplevel environment.
|
201
|
+
#
|
202
|
+
# If you would like the current environment preserved, please use the
|
203
|
+
# arity 1 variant of the block. Alternatively, you can inject a context object
|
204
|
+
# and call methods on it (think :ctx => self).
|
205
|
+
#
|
206
|
+
# # the local variable a is simulated
|
207
|
+
# t.call_on_match(:a => :b) { a }
|
208
|
+
# # no change of environment here
|
209
|
+
# t.call_on_match(:a => :b) { |d| d[:a] }
|
210
|
+
#
|
211
|
+
def call_on_match(bindings, block)
|
212
|
+
if block
|
213
|
+
if block.arity == 1
|
214
|
+
return block.call(bindings)
|
215
|
+
else
|
216
|
+
context = Context.new(bindings)
|
217
|
+
return context.instance_eval(&block)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# Allow easy access to all rules, the ones defined in the instance and the
|
223
|
+
# ones predefined in a subclass definition.
|
224
|
+
#
|
225
|
+
def rules
|
226
|
+
self.class.rules + @rules
|
227
|
+
end
|
228
|
+
|
229
|
+
# @api private
|
230
|
+
#
|
231
|
+
def transform_elt(elt, context)
|
232
|
+
rules.each do |pattern, block|
|
233
|
+
if bindings=pattern.match(elt, context)
|
234
|
+
# Produces transformed value
|
235
|
+
return call_on_match(bindings, block)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# No rule matched - element is not transformed
|
240
|
+
if @raise_on_unmatch && elt.is_a?(Hash)
|
241
|
+
elt_types = elt.map do |key, value|
|
242
|
+
[ key, value.class ]
|
243
|
+
end.to_h
|
244
|
+
raise NotImplementedError, "Failed to match `#{elt_types.inspect}`"
|
245
|
+
else
|
246
|
+
return elt
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# @api private
|
251
|
+
#
|
252
|
+
def recurse_hash(hsh, ctx)
|
253
|
+
hsh.inject({}) do |new_hsh, (k,v)|
|
254
|
+
new_hsh[k] = apply(v, ctx)
|
255
|
+
new_hsh
|
256
|
+
end
|
257
|
+
end
|
258
|
+
# @api private
|
259
|
+
#
|
260
|
+
def recurse_array(ary, ctx)
|
261
|
+
ary.map { |elt| apply(elt, ctx) }
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
require 'parslet/context'
|