parsanol 1.3.80 → 1.3.82
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 +4 -4
- data/Cargo.lock +4 -4
- data/ext/parsanol_cdylib/Cargo.toml +1 -1
- data/ext/parsanol_native/Cargo.toml +1 -1
- data/lib/parsanol/atoms/sequence.rb +22 -11
- data/lib/parsanol/atoms/trivia.rb +13 -9
- data/lib/parsanol/native/parser.rb +13 -0
- data/lib/parsanol/native/serializer.rb +9 -5
- data/lib/parsanol/parg/artifact.rb +19 -17
- data/lib/parsanol/parser.rb +34 -2
- data/lib/parsanol/skip.rb +129 -44
- data/lib/parsanol/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c56435897ed6f65bc101a9df1cdb0c0510917824295fa003893cdf7e447a32a4
|
|
4
|
+
data.tar.gz: 8308c7b4bc8179fbf7867ab1769c53b1f6759ce6ad8cd7ca2dc9362e8c5a3c58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ad6958bee6a30a11ce020e95a1b084bd26155f7293c2c127c2ff264850da1df22bbde4eb1f321f8e442448492d5612adc25fc3ccd19fe8a65b3e9ea7a2f9731
|
|
7
|
+
data.tar.gz: 4e353b492c5113f197822c528bf37622b54bc6eae4d2a4bb73f171cbce7edadd00c96ed55928eda73a9d2746d68e14fb4fd212ec2c768bca30a733123a32c1c8
|
data/Cargo.lock
CHANGED
|
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
307
307
|
|
|
308
308
|
[[package]]
|
|
309
309
|
name = "parsanol"
|
|
310
|
-
version = "0.
|
|
310
|
+
version = "0.18.0"
|
|
311
311
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
312
|
-
checksum = "
|
|
312
|
+
checksum = "dc899494d6b94ea8df14279b7dcbac89d59ba4d1be52f7b8ffe840674c6c8e3c"
|
|
313
313
|
dependencies = [
|
|
314
314
|
"ahash",
|
|
315
315
|
"getrandom 0.3.4",
|
|
@@ -326,9 +326,9 @@ dependencies = [
|
|
|
326
326
|
|
|
327
327
|
[[package]]
|
|
328
328
|
name = "parsanol-derive"
|
|
329
|
-
version = "0.
|
|
329
|
+
version = "0.18.0"
|
|
330
330
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
331
|
-
checksum = "
|
|
331
|
+
checksum = "b437046956f356eee0eb9b407ff1f1f6286af1f70f1af1d305c54a64c65b64e2"
|
|
332
332
|
dependencies = [
|
|
333
333
|
"proc-macro2",
|
|
334
334
|
"quote",
|
|
@@ -32,7 +32,7 @@ magnus = { version = "0.9" }
|
|
|
32
32
|
# parsanol parser library (crates.io; 0.17.0 carries the LookSpec regex
|
|
33
33
|
# lookbehind (rs#163 gap 1) and the widened opaque writeback: the
|
|
34
34
|
# lazy-continuation fix, parsanol-ruby#160)
|
|
35
|
-
parsanol = { version = "0.
|
|
35
|
+
parsanol = { version = "0.18.0", features = ["ruby"] }
|
|
36
36
|
|
|
37
37
|
# Logging
|
|
38
38
|
log = "0.4"
|
|
@@ -18,14 +18,25 @@ module Parsanol
|
|
|
18
18
|
def initialize(*components)
|
|
19
19
|
super()
|
|
20
20
|
@parslets = components
|
|
21
|
-
|
|
22
|
-
#
|
|
23
|
-
|
|
21
|
+
# The failure message renders LAZILY (error_msgs): constructing
|
|
22
|
+
# sequences is hot-path work for the builder/injector, and
|
|
23
|
+
# stringifying the whole subtree eagerly is exponential on
|
|
24
|
+
# shared cross-referencing grammars (parsanol-ruby#180 side
|
|
25
|
+
# observation: a 25-rule Fibonacci-shaped grammar took 6s).
|
|
26
|
+
@fail_msg = nil
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
# Error messages hash (for compatibility)
|
|
27
30
|
def error_msgs
|
|
28
|
-
{ failed:
|
|
31
|
+
{ failed: fail_msg }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Rendered lazily (see initialize); construction is hot-path
|
|
35
|
+
# builder/injector work and stringifying the whole subtree
|
|
36
|
+
# eagerly is exponential on shared cross-referencing grammars
|
|
37
|
+
# (parsanol-ruby#180 side observation).
|
|
38
|
+
def fail_msg
|
|
39
|
+
@fail_msg ||= "Failed to match sequence (#{inspect})"
|
|
29
40
|
end
|
|
30
41
|
|
|
31
42
|
# Appends a parser to this sequence with flattening.
|
|
@@ -104,7 +115,7 @@ module Parsanol
|
|
|
104
115
|
# Single element sequence
|
|
105
116
|
def match_single(parser, source, context, consume_all)
|
|
106
117
|
success, value = parser.apply(source, context, consume_all)
|
|
107
|
-
return context.err(self, source,
|
|
118
|
+
return context.err(self, source, fail_msg, [value]) unless success
|
|
108
119
|
|
|
109
120
|
ok([:sequence, value])
|
|
110
121
|
end
|
|
@@ -112,10 +123,10 @@ module Parsanol
|
|
|
112
123
|
# Two-element sequence
|
|
113
124
|
def match_pair(p1, p2, source, context, consume_all)
|
|
114
125
|
success, v1 = p1.apply(source, context, false)
|
|
115
|
-
return context.err(self, source,
|
|
126
|
+
return context.err(self, source, fail_msg, [v1]) unless success
|
|
116
127
|
|
|
117
128
|
success, v2 = p2.apply(source, context, consume_all)
|
|
118
|
-
return context.err(self, source,
|
|
129
|
+
return context.err(self, source, fail_msg, [v2]) unless success
|
|
119
130
|
|
|
120
131
|
ok([:sequence, v1, v2])
|
|
121
132
|
end
|
|
@@ -123,13 +134,13 @@ module Parsanol
|
|
|
123
134
|
# Three-element sequence
|
|
124
135
|
def match_triple(p1, p2, p3, source, context, consume_all)
|
|
125
136
|
success, v1 = p1.apply(source, context, false)
|
|
126
|
-
return context.err(self, source,
|
|
137
|
+
return context.err(self, source, fail_msg, [v1]) unless success
|
|
127
138
|
|
|
128
139
|
success, v2 = p2.apply(source, context, false)
|
|
129
|
-
return context.err(self, source,
|
|
140
|
+
return context.err(self, source, fail_msg, [v2]) unless success
|
|
130
141
|
|
|
131
142
|
success, v3 = p3.apply(source, context, consume_all)
|
|
132
|
-
return context.err(self, source,
|
|
143
|
+
return context.err(self, source, fail_msg, [v3]) unless success
|
|
133
144
|
|
|
134
145
|
ok([:sequence, v1, v2, v3])
|
|
135
146
|
end
|
|
@@ -148,7 +159,7 @@ module Parsanol
|
|
|
148
159
|
success, value = components[idx].apply(source, context, must_consume)
|
|
149
160
|
|
|
150
161
|
unless success
|
|
151
|
-
return context.err(self, source,
|
|
162
|
+
return context.err(self, source, fail_msg, [value])
|
|
152
163
|
end
|
|
153
164
|
|
|
154
165
|
result[idx + 1] = value
|
|
@@ -28,12 +28,16 @@ module Parsanol
|
|
|
28
28
|
# +capturers+ maps leading-literal marker -> kind label (the
|
|
29
29
|
# compiler derives markers from each capturer rule's first Str
|
|
30
30
|
# atom, e.g. "//" => :line_comment); units not matching any
|
|
31
|
-
# marker are whitespace-shaped and stay unrecorded
|
|
32
|
-
|
|
31
|
+
# marker are whitespace-shaped and stay unrecorded — unless
|
|
32
|
+
# +whitespace_kind+ is declared (parsanol-ruby#180): every
|
|
33
|
+
# unit then records under that label, giving source-preserving
|
|
34
|
+
# grammars the full ordered trivia stream.
|
|
35
|
+
attr_reader :capturers, :whitespace_kind
|
|
33
36
|
|
|
34
|
-
def initialize(atom, capturers)
|
|
37
|
+
def initialize(atom, capturers, whitespace_kind: nil)
|
|
35
38
|
super(atom)
|
|
36
39
|
@capturers = capturers
|
|
40
|
+
@whitespace_kind = whitespace_kind
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
def apply(source, context, consume_all)
|
|
@@ -55,16 +59,16 @@ module Parsanol
|
|
|
55
59
|
private
|
|
56
60
|
|
|
57
61
|
def record(source, context, start_pos, end_pos)
|
|
58
|
-
return unless @capturers
|
|
59
|
-
|
|
60
62
|
text = source.input[start_pos...end_pos]
|
|
61
63
|
# A trivia unit may lead with whitespace before the comment
|
|
62
64
|
# shape; markers test the comment head.
|
|
63
65
|
stripped = text.lstrip
|
|
64
|
-
marker, label = @capturers
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
marker, label = @capturers&.find { |m, _label| stripped.start_with?(m) }
|
|
67
|
+
if marker
|
|
68
|
+
context.push_captured_trivia(label, stripped.strip)
|
|
69
|
+
elsif @whitespace_kind
|
|
70
|
+
context.push_captured_trivia(@whitespace_kind, text)
|
|
71
|
+
end
|
|
68
72
|
end
|
|
69
73
|
end
|
|
70
74
|
end
|
|
@@ -205,6 +205,19 @@ module Parsanol
|
|
|
205
205
|
[:constant, atom.value.inspect]
|
|
206
206
|
when ::Parsanol::Atoms::Lookbehind
|
|
207
207
|
[:lookbehind, atom.count, atom.pattern, atom.positive]
|
|
208
|
+
# Subclass first: TriviaCapture < Trivia < Ignored (the same
|
|
209
|
+
# dispatch discipline as the serializer's).
|
|
210
|
+
when ::Parsanol::Atoms::TriviaCapture
|
|
211
|
+
# The capturer table differentiates capture-carrying
|
|
212
|
+
# grammars from plain-trivia ones — without it the grammar
|
|
213
|
+
# cache serves one variant's wire to every other and the
|
|
214
|
+
# wrong marker table reaches the engine (parsanol-ruby#180).
|
|
215
|
+
[:trivia_capture, atom.capturers.to_a.sort,
|
|
216
|
+
atom_structure(atom.wrapped_atom, visited)]
|
|
217
|
+
when ::Parsanol::Atoms::Trivia
|
|
218
|
+
[:trivia, atom_structure(atom.wrapped_atom, visited)]
|
|
219
|
+
when ::Parsanol::Atoms::Ignored
|
|
220
|
+
[:ignored, atom_structure(atom.wrapped_atom, visited)]
|
|
208
221
|
else
|
|
209
222
|
[:unknown, atom.class.name]
|
|
210
223
|
end
|
|
@@ -332,12 +332,16 @@ module Parsanol
|
|
|
332
332
|
|
|
333
333
|
def serialize_trivia_capture(atom)
|
|
334
334
|
inner_id = serialize_atom(atom.wrapped_atom)
|
|
335
|
-
{
|
|
336
|
-
"
|
|
337
|
-
|
|
338
|
-
"rules" => atom.capturers.map { |marker, label| [marker, label.to_s] },
|
|
339
|
-
},
|
|
335
|
+
payload = {
|
|
336
|
+
"atom" => inner_id,
|
|
337
|
+
"rules" => atom.capturers.map { |marker, label| [marker, label.to_s] },
|
|
340
338
|
}
|
|
339
|
+
# parsanol-ruby#180: the optional whitespace kind — units that
|
|
340
|
+
# match no marker record under it (source-preserving grammars).
|
|
341
|
+
# Optional on the wire: engines predating it deserialize and
|
|
342
|
+
# simply don't record whitespace units (documented caveat).
|
|
343
|
+
payload["whitespace"] = atom.whitespace_kind.to_s if atom.whitespace_kind
|
|
344
|
+
{ "TriviaCapture" => payload }
|
|
341
345
|
end
|
|
342
346
|
|
|
343
347
|
def serialize_trivia(atom)
|
|
@@ -65,24 +65,26 @@ module Parsanol
|
|
|
65
65
|
def parse(entry_name, input, mode: :native)
|
|
66
66
|
case mode
|
|
67
67
|
when :native
|
|
68
|
-
if Native.available?
|
|
69
|
-
# Fast lane (parsanol-ruby#162):
|
|
70
|
-
#
|
|
68
|
+
if Native.available?
|
|
69
|
+
# Fast lane (parsanol-ruby#162): Native.parse with the
|
|
70
|
+
# per-instance root atom registers the grammar once
|
|
71
71
|
# (GRAMMAR_CACHE / HANDLE_CACHE keyed on the stable root
|
|
72
|
-
# atom)
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
72
|
+
# atom) and every parse after the first runs the native
|
|
73
|
+
# engine by handle — the same path a DSL parser's
|
|
74
|
+
# mode: :native takes. Atoms::Base#parse must NOT carry
|
|
75
|
+
# this lane: it ignores :mode, so it would silently run
|
|
76
|
+
# the pure-Ruby VM and its interpreter fallback (the
|
|
77
|
+
# 5-7x real-document gap in #162, and the root-position
|
|
78
|
+
# diagnostics in #171 before the wire fallback).
|
|
79
|
+
#
|
|
80
|
+
# Dynamic-flagged artifacts parse natively too (rs#204):
|
|
81
|
+
# the state atoms (StateSet/StateMatch/StateSwitch)
|
|
82
|
+
# evaluate in the engine, riding the capture store's
|
|
83
|
+
# rollback discipline. Reaching a CustomRef aborts the
|
|
84
|
+
# native pass outright — Native.parse's error path then
|
|
85
|
+
# re-runs the interpreter, so customs stay Ruby-tier with
|
|
86
|
+
# identical trees.
|
|
87
|
+
return Native.parse(root_atom(entry_name), input)
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
root_atom(entry_name).parse(input)
|
data/lib/parsanol/parser.rb
CHANGED
|
@@ -52,7 +52,13 @@ module Parsanol
|
|
|
52
52
|
define_method(:root) do
|
|
53
53
|
atom = __send__(rule_name)
|
|
54
54
|
skip_name = self.class.skip_rule_name
|
|
55
|
-
|
|
55
|
+
if skip_name
|
|
56
|
+
Parsanol::Skip.inject(self, atom, skip_name,
|
|
57
|
+
captures: self.class.skip_captures,
|
|
58
|
+
whitespace: self.class.skip_whitespace)[0]
|
|
59
|
+
else
|
|
60
|
+
atom
|
|
61
|
+
end
|
|
56
62
|
end
|
|
57
63
|
end
|
|
58
64
|
|
|
@@ -63,6 +69,13 @@ module Parsanol
|
|
|
63
69
|
# everything it references) builds without injection; it must be
|
|
64
70
|
# non-nullable. Order-independent with +root+.
|
|
65
71
|
#
|
|
72
|
+
# +capture:+ (parsanol-ruby#180) declares trivia capturers — a
|
|
73
|
+
# map of rule name to kind label. Units of the skip rule
|
|
74
|
+
# matching a capturer's leading literal attach to the next
|
|
75
|
+
# Named capture under +comments:+ (the PARG
|
|
76
|
+
# `skip = trivia capture: comments` shape) instead of being
|
|
77
|
+
# discarded; whitespace-shaped units never record.
|
|
78
|
+
#
|
|
66
79
|
# @example
|
|
67
80
|
# class MyParser < Parsanol::Parser
|
|
68
81
|
# rule(:spaces) { match(/[ \t]/).repeat(1) }
|
|
@@ -72,8 +85,15 @@ module Parsanol
|
|
|
72
85
|
# root :doc
|
|
73
86
|
# end
|
|
74
87
|
#
|
|
75
|
-
|
|
88
|
+
# @example capturing comments
|
|
89
|
+
# rule(:trivia) { spaces | line_comment }
|
|
90
|
+
# rule(:line_comment) { str("//") >> match(/[^\n]/).repeat }
|
|
91
|
+
# skip :trivia, capture: { line_comment: :line_comment }
|
|
92
|
+
#
|
|
93
|
+
def skip(rule_name, capture: nil, whitespace: nil)
|
|
76
94
|
@skip_rule_name = rule_name
|
|
95
|
+
@skip_captures = capture
|
|
96
|
+
@skip_whitespace = whitespace
|
|
77
97
|
nil
|
|
78
98
|
end
|
|
79
99
|
|
|
@@ -82,6 +102,18 @@ module Parsanol
|
|
|
82
102
|
def skip_rule_name
|
|
83
103
|
@skip_rule_name
|
|
84
104
|
end
|
|
105
|
+
|
|
106
|
+
# The declared capturer map ({rule => kind label}), or nil —
|
|
107
|
+
# see {.skip}.
|
|
108
|
+
def skip_captures
|
|
109
|
+
@skip_captures
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The declared whitespace kind (units matching no capturer
|
|
113
|
+
# record under it), or nil — see {.skip}.
|
|
114
|
+
def skip_whitespace
|
|
115
|
+
@skip_whitespace
|
|
116
|
+
end
|
|
85
117
|
end
|
|
86
118
|
|
|
87
119
|
# Delegates matching to the root rule.
|
data/lib/parsanol/skip.rb
CHANGED
|
@@ -13,7 +13,15 @@ module Parsanol
|
|
|
13
13
|
# methods. Returns [injected_root, skip_atom] with the wrapper
|
|
14
14
|
# built and the lint applied, or raises GrammarError for a
|
|
15
15
|
# nullable skip.
|
|
16
|
-
|
|
16
|
+
#
|
|
17
|
+
# +captures+ (parsanol-ruby#180) maps capturer rule names to kind
|
|
18
|
+
# labels (`skip :trivia, capture: { line_comment: :line }`); the
|
|
19
|
+
# injected wrapper becomes a TriviaCapture whose units matching a
|
|
20
|
+
# capturer's leading literal attach to the next Named capture
|
|
21
|
+
# under `comments:` — the PARG `skip = trivia capture: comments`
|
|
22
|
+
# shape. Whitespace-shaped units never record.
|
|
23
|
+
def self.inject(parser_instance, root_atom, skip_rule_name, captures: nil,
|
|
24
|
+
whitespace: nil)
|
|
17
25
|
skip_atom = parser_instance.__send__(skip_rule_name)
|
|
18
26
|
skip_atom = skip_atom.parslet while skip_atom.is_a?(Atoms::Entity)
|
|
19
27
|
|
|
@@ -23,24 +31,88 @@ module Parsanol
|
|
|
23
31
|
"empty loops forever at injection points"
|
|
24
32
|
end
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
markers = capturer_markers(parser_instance, captures)
|
|
35
|
+
whitespace_kind = whitespace&.to_sym
|
|
36
|
+
injector = Injector.new(skip_atom, markers, whitespace_kind)
|
|
27
37
|
[injector.inject(root_atom, entry: true), skip_atom]
|
|
28
38
|
end
|
|
29
39
|
|
|
40
|
+
# Derives each capturer's leading literal (the leftmost Str the
|
|
41
|
+
# rule can start with) and keys the marker table by it — the same
|
|
42
|
+
# derivation the PARG compiler applies to its skip declaration. A
|
|
43
|
+
# declared capturer without a derivable, non-blank literal is a
|
|
44
|
+
# declaration error: its units could never be told apart from
|
|
45
|
+
# whitespace-shaped trivia and it would silently record nothing.
|
|
46
|
+
def self.capturer_markers(parser_instance, captures)
|
|
47
|
+
return nil if captures.nil? || captures.empty?
|
|
48
|
+
|
|
49
|
+
captures.each_with_object({}) do |(rule_name, kind), markers|
|
|
50
|
+
atom = parser_instance.__send__(rule_name)
|
|
51
|
+
atom = atom.parslet while atom.is_a?(Atoms::Entity)
|
|
52
|
+
literal = leading_literal(atom)
|
|
53
|
+
if literal.nil? || literal.strip.empty?
|
|
54
|
+
raise GrammarError,
|
|
55
|
+
"skip capturer #{rule_name.inspect} has no leading " \
|
|
56
|
+
"literal: its units cannot be told apart from " \
|
|
57
|
+
"whitespace-shaped trivia"
|
|
58
|
+
end
|
|
59
|
+
markers[literal] = kind || rule_name
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The leftmost literal the atom can start with, or nil when the
|
|
64
|
+
# shape is not literal-led (alternatives with disagreeing
|
|
65
|
+
# branches, regexes, lookaheads).
|
|
66
|
+
def self.leading_literal(atom)
|
|
67
|
+
case atom
|
|
68
|
+
when Atoms::Str then atom.str
|
|
69
|
+
when Atoms::Sequence
|
|
70
|
+
atom.parslets.each do |child|
|
|
71
|
+
literal = leading_literal(child)
|
|
72
|
+
return literal if literal
|
|
73
|
+
end
|
|
74
|
+
nil
|
|
75
|
+
when Atoms::Alternative
|
|
76
|
+
first = nil
|
|
77
|
+
atom.alternatives.each do |branch|
|
|
78
|
+
literal = leading_literal(branch)
|
|
79
|
+
return nil if literal.nil? || (first && literal != first)
|
|
80
|
+
|
|
81
|
+
first ||= literal
|
|
82
|
+
end
|
|
83
|
+
first
|
|
84
|
+
when Atoms::Repetition
|
|
85
|
+
atom.min&.positive? ? leading_literal(atom.parslet) : nil
|
|
86
|
+
when Atoms::Named, Atoms::Capture, Atoms::Ignored, Atoms::Trivia
|
|
87
|
+
leading_literal(atom.parslet)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
30
91
|
# The injection walk. Mirrors the PARG compiler's semantics:
|
|
31
92
|
# sequences interleave the wrapper before every child, alternatives
|
|
32
93
|
# and repetitions descend, named captures descend (spans stay
|
|
33
94
|
# clean — the wrapper is Trivia: Ignored + diagnostics
|
|
34
95
|
# transparency), bare terminals gain a leading wrapper, and the
|
|
35
96
|
# entry root takes leading and trailing wrappers.
|
|
97
|
+
# The walk is a pure function of the atom subtree, so results are
|
|
98
|
+
# memoized: dense cross-referencing grammars (expressir's ~250
|
|
99
|
+
# rules) revisit shared subtrees once per referencing path, which
|
|
100
|
+
# made the path-scoped visited-set exponential — a 25-rule
|
|
101
|
+
# Fibonacci-shaped grammar took 6.2s to build (parsanol-ruby#180
|
|
102
|
+
# side observation). In-progress atoms mark a cycle re-entry and
|
|
103
|
+
# return raw, exactly as the path-scoped set did.
|
|
36
104
|
class Injector
|
|
37
|
-
def initialize(skip_atom)
|
|
105
|
+
def initialize(skip_atom, markers = nil, whitespace_kind = nil)
|
|
38
106
|
@skip_atom = skip_atom
|
|
107
|
+
@markers = markers
|
|
108
|
+
@whitespace_kind = whitespace_kind
|
|
39
109
|
@wrapper_ids = {}.compare_by_identity
|
|
110
|
+
@memo = {}.compare_by_identity
|
|
111
|
+
@in_progress = {}.compare_by_identity
|
|
40
112
|
end
|
|
41
113
|
|
|
42
114
|
def inject(atom, entry: false)
|
|
43
|
-
injected = walk(atom
|
|
115
|
+
injected = walk(atom)
|
|
44
116
|
return injected unless entry
|
|
45
117
|
|
|
46
118
|
Atoms::Sequence.new(maybe, injected, maybe)
|
|
@@ -50,7 +122,14 @@ module Parsanol
|
|
|
50
122
|
|
|
51
123
|
def maybe
|
|
52
124
|
@maybe ||= begin
|
|
53
|
-
|
|
125
|
+
inner = Atoms::Repetition.new(@skip_atom, 0, 1)
|
|
126
|
+
wrapper =
|
|
127
|
+
if @markers || @whitespace_kind
|
|
128
|
+
Atoms::TriviaCapture.new(inner, @markers,
|
|
129
|
+
whitespace_kind: @whitespace_kind)
|
|
130
|
+
else
|
|
131
|
+
Atoms::Trivia.new(inner)
|
|
132
|
+
end
|
|
54
133
|
@wrapper_ids[wrapper] = true
|
|
55
134
|
wrapper
|
|
56
135
|
end
|
|
@@ -60,50 +139,56 @@ module Parsanol
|
|
|
60
139
|
@wrapper_ids.key?(atom)
|
|
61
140
|
end
|
|
62
141
|
|
|
63
|
-
def walk(atom
|
|
64
|
-
|
|
142
|
+
def walk(atom)
|
|
143
|
+
cached = @memo[atom]
|
|
144
|
+
return cached if cached
|
|
145
|
+
return atom if @in_progress.key?(atom)
|
|
65
146
|
return atom if atom.equal?(@skip_atom)
|
|
66
147
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
Atoms::Alternative
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
atom
|
|
148
|
+
@in_progress[atom] = true
|
|
149
|
+
injected =
|
|
150
|
+
case atom
|
|
151
|
+
when Atoms::Sequence
|
|
152
|
+
out = []
|
|
153
|
+
atom.parslets.each do |child|
|
|
154
|
+
injected = walk(child)
|
|
155
|
+
out << maybe unless wrapper?(injected) || injected.is_a?(Atoms::Trivia)
|
|
156
|
+
out << injected
|
|
157
|
+
end
|
|
158
|
+
out.length == 1 ? out.first : Atoms::Sequence.new(*out)
|
|
159
|
+
when Atoms::Alternative
|
|
160
|
+
Atoms::Alternative.new(*atom.alternatives.map { |a| walk(a) })
|
|
161
|
+
when Atoms::Repetition
|
|
162
|
+
if wrapper?(atom)
|
|
163
|
+
atom
|
|
164
|
+
else
|
|
165
|
+
Atoms::Repetition.new(walk(atom.parslet), atom.min, atom.max,
|
|
166
|
+
atom.result_tag)
|
|
167
|
+
end
|
|
168
|
+
when Atoms::Named
|
|
169
|
+
Atoms::Named.new(walk(atom.parslet), atom.name)
|
|
170
|
+
when Atoms::Lookahead
|
|
171
|
+
Atoms::Lookahead.new(walk(atom.bound_parslet), atom.positive)
|
|
172
|
+
when Atoms::Ignored
|
|
173
|
+
Atoms::Ignored.new(walk(atom.wrapped_atom))
|
|
174
|
+
when Atoms::Entity
|
|
175
|
+
# Rule bodies own their injection when the grammar declares
|
|
176
|
+
# skip on every rule (PARG); in the DSL surface the root tree
|
|
177
|
+
# is walked once, so descend through resolved bodies with the
|
|
178
|
+
# identity visited-set guarding recursion.
|
|
179
|
+
begin
|
|
180
|
+
walk(atom.parslet)
|
|
181
|
+
rescue StandardError
|
|
182
|
+
atom
|
|
183
|
+
end
|
|
184
|
+
when Atoms::Str, Atoms::Re
|
|
185
|
+
Atoms::Sequence.new(maybe, atom)
|
|
82
186
|
else
|
|
83
|
-
Atoms::Repetition.new(walk(atom.parslet, visited), atom.min, atom.max,
|
|
84
|
-
atom.result_tag)
|
|
85
|
-
end
|
|
86
|
-
when Atoms::Named
|
|
87
|
-
Atoms::Named.new(walk(atom.parslet, visited), atom.name)
|
|
88
|
-
when Atoms::Lookahead
|
|
89
|
-
Atoms::Lookahead.new(walk(atom.bound_parslet, visited), atom.positive)
|
|
90
|
-
when Atoms::Ignored
|
|
91
|
-
Atoms::Ignored.new(walk(atom.wrapped_atom, visited))
|
|
92
|
-
when Atoms::Entity
|
|
93
|
-
# Rule bodies own their injection when the grammar declares
|
|
94
|
-
# skip on every rule (PARG); in the DSL surface the root tree
|
|
95
|
-
# is walked once, so descend through resolved bodies with the
|
|
96
|
-
# identity visited-set guarding recursion.
|
|
97
|
-
begin
|
|
98
|
-
walk(atom.parslet, visited)
|
|
99
|
-
rescue StandardError
|
|
100
187
|
atom
|
|
101
188
|
end
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
atom
|
|
106
|
-
end
|
|
189
|
+
@in_progress.delete(atom)
|
|
190
|
+
@memo[atom] = injected
|
|
191
|
+
injected
|
|
107
192
|
end
|
|
108
193
|
end
|
|
109
194
|
end
|
data/lib/parsanol/version.rb
CHANGED