parsanol 1.3.70 → 1.3.72
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 +7 -36
- data/ext/parsanol_cdylib/Cargo.toml +2 -1
- data/ext/parsanol_native/Cargo.toml +3 -1
- data/lib/parsanol/atoms/context.rb +33 -0
- data/lib/parsanol/atoms/named.rb +24 -4
- data/lib/parsanol/atoms/trivia.rb +48 -0
- data/lib/parsanol/native/serializer.rb +16 -0
- data/lib/parsanol/parg/artifact.rb +4 -1
- data/lib/parsanol/parg/compiler.rb +52 -1
- data/lib/parsanol/parg/document.rb +1 -1
- data/lib/parsanol/parg/parser.rb +23 -2
- data/lib/parsanol/parser.rb +32 -1
- data/lib/parsanol/skip.rb +110 -0
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +6 -0
- data/lib/parsanol.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd9773776674bd865171d5b4b378628a063b0dee92e2a4442fcf3fe0182262da
|
|
4
|
+
data.tar.gz: 6e8554e163d07070a54b9255d6bed7ca348fd72fd6a8968a17308814323d0d4c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa0c3526c5ab79f7d9b6ed08e7523738cf6b46e2b8a6c30e2a021845d6bdeeabd5bb3e7ddd4d31f36725c1682ad94e650975289889c1bc991364d48bd9e2c8d7
|
|
7
|
+
data.tar.gz: ea7fd569d5a432d56583d76ec896c5a0c03ab8b769d9af7863041f02e64343ce1280ee226f450efde730bcb98bb4a0391fb7fcd03d3f490c7e35b133f6f427e1
|
data/Cargo.lock
CHANGED
|
@@ -307,34 +307,16 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
307
307
|
|
|
308
308
|
[[package]]
|
|
309
309
|
name = "parsanol"
|
|
310
|
-
version = "0.
|
|
311
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
312
|
-
checksum = "dd43c104276def3f79a54ce6e8d26642ea330c13f97d66380bd94fc057f0a849"
|
|
313
|
-
dependencies = [
|
|
314
|
-
"ahash",
|
|
315
|
-
"getrandom 0.3.4",
|
|
316
|
-
"getrandom 0.4.3",
|
|
317
|
-
"hashbrown",
|
|
318
|
-
"parsanol-derive 0.10.2",
|
|
319
|
-
"regex",
|
|
320
|
-
"regex-syntax",
|
|
321
|
-
"serde",
|
|
322
|
-
"serde_json",
|
|
323
|
-
"sha2",
|
|
324
|
-
]
|
|
325
|
-
|
|
326
|
-
[[package]]
|
|
327
|
-
name = "parsanol"
|
|
328
|
-
version = "0.12.0"
|
|
310
|
+
version = "0.13.0"
|
|
329
311
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
330
|
-
checksum = "
|
|
312
|
+
checksum = "126b715ffe53b3bc75ed8ff753333e350d51aaea64a0a51a6eb8588f0786d925"
|
|
331
313
|
dependencies = [
|
|
332
314
|
"ahash",
|
|
333
315
|
"getrandom 0.3.4",
|
|
334
316
|
"getrandom 0.4.3",
|
|
335
317
|
"hashbrown",
|
|
336
318
|
"magnus",
|
|
337
|
-
"parsanol-derive
|
|
319
|
+
"parsanol-derive",
|
|
338
320
|
"regex",
|
|
339
321
|
"regex-syntax",
|
|
340
322
|
"serde",
|
|
@@ -344,20 +326,9 @@ dependencies = [
|
|
|
344
326
|
|
|
345
327
|
[[package]]
|
|
346
328
|
name = "parsanol-derive"
|
|
347
|
-
version = "0.
|
|
348
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
349
|
-
checksum = "eca430ba21d7cc2d674080d40dd37a5567e1a0acdc37faef9a9b3738b8651dd6"
|
|
350
|
-
dependencies = [
|
|
351
|
-
"proc-macro2",
|
|
352
|
-
"quote",
|
|
353
|
-
"syn 3.0.6",
|
|
354
|
-
]
|
|
355
|
-
|
|
356
|
-
[[package]]
|
|
357
|
-
name = "parsanol-derive"
|
|
358
|
-
version = "0.12.0"
|
|
329
|
+
version = "0.13.0"
|
|
359
330
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
360
|
-
checksum = "
|
|
331
|
+
checksum = "6f1413076b79ef60c0af56973f8bbfddcc6034bd3cb47fbc5205fcd188bc8d79"
|
|
361
332
|
dependencies = [
|
|
362
333
|
"proc-macro2",
|
|
363
334
|
"quote",
|
|
@@ -368,7 +339,7 @@ dependencies = [
|
|
|
368
339
|
name = "parsanol_cdylib"
|
|
369
340
|
version = "1.3.52"
|
|
370
341
|
dependencies = [
|
|
371
|
-
"parsanol
|
|
342
|
+
"parsanol",
|
|
372
343
|
]
|
|
373
344
|
|
|
374
345
|
[[package]]
|
|
@@ -377,7 +348,7 @@ version = "1.0.2"
|
|
|
377
348
|
dependencies = [
|
|
378
349
|
"log",
|
|
379
350
|
"magnus",
|
|
380
|
-
"parsanol
|
|
351
|
+
"parsanol",
|
|
381
352
|
"rb-sys",
|
|
382
353
|
]
|
|
383
354
|
|
|
@@ -29,7 +29,9 @@ magnus = { version = "0.9" }
|
|
|
29
29
|
|
|
30
30
|
# parsanol parser library (crates.io; 0.12 carries the rs#174 native
|
|
31
31
|
# backtracking fix: dynamic-dependent memoization + outcome cache)
|
|
32
|
-
parsanol
|
|
32
|
+
# parsanol parser library (crates.io; 0.13 carries the rs#172 Trivia
|
|
33
|
+
# atom: native diagnostics skip trivia positions, matching ruby #145)
|
|
34
|
+
parsanol = { version = "0.13.0", features = ["ruby"] }
|
|
33
35
|
|
|
34
36
|
# Logging
|
|
35
37
|
log = "0.4"
|
|
@@ -67,6 +67,11 @@ module Parsanol
|
|
|
67
67
|
# Injected-trivia depth: while positive, reported causes are
|
|
68
68
|
# marked trivia and never surface in diagnostics.
|
|
69
69
|
@trivia_depth = 0
|
|
70
|
+
# Captured trivia channel (parsanol-ruby#152): when the grammar
|
|
71
|
+
# declares `skip ... capture:`, matched trivia units land here
|
|
72
|
+
# and the NEXT successful Named capture attaches (and drains)
|
|
73
|
+
# them under `comments:`.
|
|
74
|
+
@pending_trivia = []
|
|
70
75
|
@evict_threshold = 200
|
|
71
76
|
@evict_counter = 0
|
|
72
77
|
@evict_interval = 100
|
|
@@ -258,6 +263,34 @@ module Parsanol
|
|
|
258
263
|
[false, @reporter.err(*)]
|
|
259
264
|
end
|
|
260
265
|
|
|
266
|
+
# Records one captured trivia unit (kind, text) for attachment to
|
|
267
|
+
# the next successful Named capture.
|
|
268
|
+
def push_captured_trivia(kind, text)
|
|
269
|
+
@pending_trivia << { kind: kind, text: text }
|
|
270
|
+
nil
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Snapshot for backtracking: a failed branch must not leak its
|
|
274
|
+
# half-recorded trivia into the successful alternative.
|
|
275
|
+
def take_pending_trivia_snapshot
|
|
276
|
+
@pending_trivia.dup
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def restore_pending_trivia(snapshot)
|
|
280
|
+
@pending_trivia = snapshot
|
|
281
|
+
nil
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Drains pending captured trivia (attached by the next
|
|
285
|
+
# successful Named capture). Backtracking discards whatever has
|
|
286
|
+
# not been attached yet — the caller snapshots position and
|
|
287
|
+
# restores this list alongside.
|
|
288
|
+
def take_pending_trivia
|
|
289
|
+
list = @pending_trivia
|
|
290
|
+
@pending_trivia = []
|
|
291
|
+
list
|
|
292
|
+
end
|
|
293
|
+
|
|
261
294
|
# Brackets a parse inside injected trivia: causes reported while
|
|
262
295
|
# the depth is positive carry the trivia flag and never surface
|
|
263
296
|
# in rendered trees or deepest-failure positions.
|
data/lib/parsanol/atoms/named.rb
CHANGED
|
@@ -32,10 +32,32 @@ module Parsanol
|
|
|
32
32
|
# @param consume_all [Boolean] require full consumption
|
|
33
33
|
# @return [Array(Boolean, Object)] result
|
|
34
34
|
def apply(source, context, consume_all)
|
|
35
|
+
trivia_before = context.take_pending_trivia_snapshot
|
|
35
36
|
success, value = @parslet.apply(source, context, consume_all)
|
|
36
|
-
|
|
37
|
+
unless success
|
|
38
|
+
context.restore_pending_trivia(trivia_before)
|
|
39
|
+
return [false, value]
|
|
40
|
+
end
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
comments = context.take_pending_trivia
|
|
43
|
+
result = wrap_result(value)
|
|
44
|
+
result = attach_comments(result, comments) unless comments.empty?
|
|
45
|
+
ok(result)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# parsanol-ruby#152: trivia captured ahead of this capture rides
|
|
51
|
+
# with it under `comments:`. The attachment only fires when the
|
|
52
|
+
# grammar declared capture AND trivia actually preceded — key
|
|
53
|
+
# absence everywhere else keeps v1 trees byte-identical.
|
|
54
|
+
def attach_comments(result, comments)
|
|
55
|
+
list = comments.map { |unit| { unit[:kind] => unit[:text] } }
|
|
56
|
+
if result.is_a?(Hash)
|
|
57
|
+
result.merge(comments: list)
|
|
58
|
+
else
|
|
59
|
+
{ @name => result, comments: list }
|
|
60
|
+
end
|
|
39
61
|
end
|
|
40
62
|
|
|
41
63
|
# Named wrappers skip caching (inner parser handles it).
|
|
@@ -60,8 +82,6 @@ module Parsanol
|
|
|
60
82
|
@parslet.first_set
|
|
61
83
|
end
|
|
62
84
|
|
|
63
|
-
private
|
|
64
|
-
|
|
65
85
|
# Wraps matched value in labeled hash.
|
|
66
86
|
#
|
|
67
87
|
# @param matched [Object] matched value
|
|
@@ -17,5 +17,53 @@ module Parsanol
|
|
|
17
17
|
"trivia(#{@wrapped_atom.to_s(prec)})"
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
# parsanol-ruby#152: the skip-declared capture wrapper. Whitespace
|
|
22
|
+
# trivia stays Ignored; comment-shaped units (the grammar names the
|
|
23
|
+
# capture rules in the declaration) are recorded into the context
|
|
24
|
+
# channel for attachment to the next successful Named capture.
|
|
25
|
+
# Span discipline is unchanged: the matched bytes still contribute
|
|
26
|
+
# nothing to values or capture spans.
|
|
27
|
+
class TriviaCapture < Trivia
|
|
28
|
+
# +capturers+ maps leading-literal marker -> kind label (the
|
|
29
|
+
# compiler derives markers from each capturer rule's first Str
|
|
30
|
+
# atom, e.g. "//" => :line_comment); units not matching any
|
|
31
|
+
# marker are whitespace-shaped and stay unrecorded.
|
|
32
|
+
def initialize(atom, capturers)
|
|
33
|
+
super(atom)
|
|
34
|
+
@capturers = capturers
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def apply(source, context, consume_all)
|
|
38
|
+
context.with_trivia do
|
|
39
|
+
start = source.bytepos
|
|
40
|
+
ok, _value = @wrapped_atom.apply(source, context, consume_all)
|
|
41
|
+
if ok && source.bytepos > start
|
|
42
|
+
record(source, context, start, source.bytepos)
|
|
43
|
+
return ok(nil)
|
|
44
|
+
end
|
|
45
|
+
[ok, _value]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s_inner(_prec)
|
|
50
|
+
"trivia!(@wrapped_atom.to_s(prec))"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def record(source, context, start_pos, end_pos)
|
|
56
|
+
return unless @capturers
|
|
57
|
+
|
|
58
|
+
text = source.input[start_pos...end_pos]
|
|
59
|
+
# A trivia unit may lead with whitespace before the comment
|
|
60
|
+
# shape; markers test the comment head.
|
|
61
|
+
stripped = text.lstrip
|
|
62
|
+
marker, label = @capturers.find { |m, _label| stripped.start_with?(m) }
|
|
63
|
+
return unless marker
|
|
64
|
+
|
|
65
|
+
context.push_captured_trivia(label, stripped.strip)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
20
68
|
end
|
|
21
69
|
end
|
|
@@ -78,6 +78,13 @@ module Parsanol
|
|
|
78
78
|
serialize_custom_ref(atom)
|
|
79
79
|
when Parsanol::Atoms::Dynamic
|
|
80
80
|
serialize_dynamic(atom)
|
|
81
|
+
when Parsanol::Atoms::Trivia
|
|
82
|
+
# Wire-distinct from Ignore (rs#172): the Rust
|
|
83
|
+
# engine suppresses trivia-position diagnostics by
|
|
84
|
+
# atom kind. Engines predating the Trivia tag
|
|
85
|
+
# reject skip-carrying artifacts loudly (unknown
|
|
86
|
+
# variant), which is the declared gate.
|
|
87
|
+
serialize_trivia(atom)
|
|
81
88
|
when Parsanol::Atoms::Ignored
|
|
82
89
|
serialize_ignored(atom)
|
|
83
90
|
else
|
|
@@ -276,6 +283,15 @@ module Parsanol
|
|
|
276
283
|
"parse this grammar with mode: :ruby"
|
|
277
284
|
end
|
|
278
285
|
|
|
286
|
+
def serialize_trivia(atom)
|
|
287
|
+
inner_id = serialize_atom(atom.wrapped_atom)
|
|
288
|
+
{
|
|
289
|
+
"Trivia" => {
|
|
290
|
+
"atom" => inner_id,
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
end
|
|
294
|
+
|
|
279
295
|
def serialize_ignored(atom)
|
|
280
296
|
inner_id = serialize_atom(atom.wrapped_atom)
|
|
281
297
|
{
|
|
@@ -71,7 +71,10 @@ module Parsanol
|
|
|
71
71
|
|
|
72
72
|
root_atom(entry_name).parse(input)
|
|
73
73
|
when :ruby
|
|
74
|
-
|
|
74
|
+
# Deepest-failure reporting parity with the native engine: the
|
|
75
|
+
# default Tree reporter keeps the most recent (root) failure,
|
|
76
|
+
# which reports position 1:1 for grammars with entry wrappers.
|
|
77
|
+
root_atom(entry_name).parse(input, reporter: ErrorReporter::Deepest.new)
|
|
75
78
|
else
|
|
76
79
|
raise ArgumentError, "unknown mode #{mode.inspect} (use :native or :ruby)"
|
|
77
80
|
end
|
|
@@ -141,6 +141,51 @@ module Parsanol
|
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
+
# parsanol-ruby#152: capturer rules are the rules transitively
|
|
145
|
+
# referenced by the skip declaration whose leading literal is a
|
|
146
|
+
# non-whitespace marker — the comment shapes ("//", "/*", ...).
|
|
147
|
+
# Whitespace rules stay unrecorded trivia.
|
|
148
|
+
def skip_capturer_markers
|
|
149
|
+
@skip_capturer_markers ||= begin
|
|
150
|
+
markers = {}
|
|
151
|
+
seen = {}
|
|
152
|
+
collect = lambda do |node|
|
|
153
|
+
case node.kind
|
|
154
|
+
when :ref
|
|
155
|
+
next if seen[node.a]
|
|
156
|
+
|
|
157
|
+
seen[node.a] = true
|
|
158
|
+
literal = leading_literal(node.a)
|
|
159
|
+
markers[literal] = node.a.to_sym if literal && !literal.strip.empty?
|
|
160
|
+
collect.call(@document.rules[node.a]) if @document.rules.key?(node.a)
|
|
161
|
+
when :alt, :seq then node.a.each { |b| collect.call(b) }
|
|
162
|
+
when :rep, :opt, :cap then collect.call(node.a)
|
|
163
|
+
when :pred then collect.call(node.b)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
collect.call(@document.skip_declaration)
|
|
167
|
+
markers
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def leading_literal(rule_name)
|
|
172
|
+
node = @document.rules[rule_name]
|
|
173
|
+
return nil unless node
|
|
174
|
+
|
|
175
|
+
literal = nil
|
|
176
|
+
walk = lambda do |n|
|
|
177
|
+
break if literal
|
|
178
|
+
|
|
179
|
+
case n.kind
|
|
180
|
+
when :lit then literal = n.a
|
|
181
|
+
when :seq, :alt then n.a.each { |c| walk.call(c) unless literal }
|
|
182
|
+
when :rep, :opt, :cap, :pred then walk.call(n.a || n.b) unless literal
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
walk.call(node)
|
|
186
|
+
literal
|
|
187
|
+
end
|
|
188
|
+
|
|
144
189
|
def skip_wrapper?(atom)
|
|
145
190
|
@skip_wrapper_ids&.key?(atom)
|
|
146
191
|
end
|
|
@@ -163,7 +208,13 @@ module Parsanol
|
|
|
163
208
|
# span-joined captures — and failures inside it never surface
|
|
164
209
|
# in diagnostics (rendered trees and deepest-failure
|
|
165
210
|
# positions skip trivia causes).
|
|
166
|
-
|
|
211
|
+
inner = Atoms::Repetition.new(atom, 0, 1)
|
|
212
|
+
wrapper =
|
|
213
|
+
if @document.skip_capture_key
|
|
214
|
+
Atoms::TriviaCapture.new(inner, skip_capturer_markers)
|
|
215
|
+
else
|
|
216
|
+
Atoms::Trivia.new(inner)
|
|
217
|
+
end
|
|
167
218
|
(@skip_wrapper_ids ||= {}.compare_by_identity)[wrapper] = true
|
|
168
219
|
wrapper
|
|
169
220
|
end
|
|
@@ -10,7 +10,7 @@ module Parsanol
|
|
|
10
10
|
Test = Struct.new(:entry, :kind, :input, :expect)
|
|
11
11
|
|
|
12
12
|
attr_accessor :grammar_name, :version, :source, :uses, :skip_declaration, :skip_source,
|
|
13
|
-
:atomic_rules
|
|
13
|
+
:atomic_rules, :skip_capture_key
|
|
14
14
|
attr_reader :rules, :bindings, :preprocess, :entries, :docs, :tests, :render, :derive,
|
|
15
15
|
:customs, :states
|
|
16
16
|
# Entries authored in this file (pre-import); nil-entry tests resolve
|
data/lib/parsanol/parg/parser.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Parsanol
|
|
|
21
21
|
STATE_KEYWORDS = %w[custom state].freeze
|
|
22
22
|
KEYWORDS = %w[grammar as alt from_table column bindings render derive
|
|
23
23
|
preprocess entry table_lookup custom state set switch
|
|
24
|
-
skip until atomic].freeze
|
|
24
|
+
skip until atomic capture].freeze
|
|
25
25
|
|
|
26
26
|
def initialize(text)
|
|
27
27
|
@tokens = Lexer.new(text).tokens
|
|
@@ -170,6 +170,15 @@ module Parsanol
|
|
|
170
170
|
end
|
|
171
171
|
|
|
172
172
|
node = parse_choice
|
|
173
|
+
# parsanol-ruby#152 phase 2: `skip = trivia capture:
|
|
174
|
+
# comments` attaches comment-shaped trivia (the rules the
|
|
175
|
+
# declaration references, identified by leading literal)
|
|
176
|
+
# to the next successful capture under `comments:`.
|
|
177
|
+
if peek&.type == :ident && peek.value == "capture"
|
|
178
|
+
advance
|
|
179
|
+
punct(":")
|
|
180
|
+
document.skip_capture_key = ident.value.to_sym
|
|
181
|
+
end
|
|
173
182
|
document.skip_declaration = validate_skip_node(node)
|
|
174
183
|
document.skip_source = skip_source_string(node)
|
|
175
184
|
skip_newlines
|
|
@@ -260,7 +269,12 @@ module Parsanol
|
|
|
260
269
|
|
|
261
270
|
def stop_element?
|
|
262
271
|
peek.nil? || peek.type == :newline ||
|
|
263
|
-
(peek.type == :punct && ["/", ")", "]", "}"].include?(peek.value))
|
|
272
|
+
(peek.type == :punct && ["/", ")", "]", "}"].include?(peek.value)) ||
|
|
273
|
+
# `capture:` suffix terminates the skip declaration's RHS
|
|
274
|
+
# (parsanol-ruby#152); everywhere else parse_element's guard
|
|
275
|
+
# rejects it.
|
|
276
|
+
(peek&.type == :ident && peek.value == "capture" &&
|
|
277
|
+
@tokens[@pos + 1]&.type == :punct && @tokens[@pos + 1]&.value == ":")
|
|
264
278
|
end
|
|
265
279
|
|
|
266
280
|
def parse_element
|
|
@@ -269,6 +283,13 @@ module Parsanol
|
|
|
269
283
|
raise ParseError, "unexpected end of input inside sequence"
|
|
270
284
|
end
|
|
271
285
|
|
|
286
|
+
# `capture:` is a skip-declaration suffix, never an expression
|
|
287
|
+
# element (reserved keyword; parsanol-ruby#152).
|
|
288
|
+
if token.type == :ident && token.value == "capture" &&
|
|
289
|
+
@tokens[@pos + 1]&.type == :punct && @tokens[@pos + 1]&.value == ":"
|
|
290
|
+
raise ParseError, "capture: only modifies the skip declaration"
|
|
291
|
+
end
|
|
292
|
+
|
|
272
293
|
if token.type == :num
|
|
273
294
|
advance
|
|
274
295
|
min = token.value.to_i
|
data/lib/parsanol/parser.rb
CHANGED
|
@@ -49,7 +49,38 @@ module Parsanol
|
|
|
49
49
|
def root(rule_name)
|
|
50
50
|
# Remove any existing root method before redefining
|
|
51
51
|
undef_method :root if method_defined?(:root)
|
|
52
|
-
define_method(:root)
|
|
52
|
+
define_method(:root) do
|
|
53
|
+
atom = __send__(rule_name)
|
|
54
|
+
skip_name = self.class.skip_rule_name
|
|
55
|
+
skip_name ? Parsanol::Skip.inject(self, atom, skip_name)[0] : atom
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Declares the grammar's skip rule (parsanol-ruby#134 DSL
|
|
60
|
+
# surface): an optional match of the named rule is injected
|
|
61
|
+
# before every terminal of the root's rule tree, and the entry
|
|
62
|
+
# takes leading and trailing skips. The referenced rule (and
|
|
63
|
+
# everything it references) builds without injection; it must be
|
|
64
|
+
# non-nullable. Order-independent with +root+.
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# class MyParser < Parsanol::Parser
|
|
68
|
+
# rule(:spaces) { match(/[ \t]/).repeat(1) }
|
|
69
|
+
# rule(:word) { match(/[a-z]/).repeat(1).as(:w) }
|
|
70
|
+
# rule(:doc) { word >> str(",") >> word }
|
|
71
|
+
# skip :spaces
|
|
72
|
+
# root :doc
|
|
73
|
+
# end
|
|
74
|
+
#
|
|
75
|
+
def skip(rule_name)
|
|
76
|
+
@skip_rule_name = rule_name
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The declared skip rule name, or nil (class-level; subclasses
|
|
81
|
+
# inherit unless they declare their own).
|
|
82
|
+
def skip_rule_name
|
|
83
|
+
@skip_rule_name
|
|
53
84
|
end
|
|
54
85
|
end
|
|
55
86
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
# Skip-rule trivia injection for Ruby-DSL grammars (the PARG surface
|
|
5
|
+
# compiles the same semantics into artifacts; see
|
|
6
|
+
# Parsanol::PARG::Compiler#inject_skip_trivia). A parser declares
|
|
7
|
+
# `skip :trivia`; the resolved rule's atom becomes the injected
|
|
8
|
+
# optional trivia wrapper before every terminal of every rule body,
|
|
9
|
+
# with the declaration's transitive closure exempt (the skip atom
|
|
10
|
+
# must not contain itself).
|
|
11
|
+
module Skip
|
|
12
|
+
# +skip_rule_name+ resolves against the parser instance's rule
|
|
13
|
+
# methods. Returns [injected_root, skip_atom] with the wrapper
|
|
14
|
+
# built and the lint applied, or raises GrammarError for a
|
|
15
|
+
# nullable skip.
|
|
16
|
+
def self.inject(parser_instance, root_atom, skip_rule_name)
|
|
17
|
+
skip_atom = parser_instance.__send__(skip_rule_name)
|
|
18
|
+
skip_atom = skip_atom.parslet while skip_atom.is_a?(Atoms::Entity)
|
|
19
|
+
|
|
20
|
+
if VM::Compiler.new.nullable?(skip_atom)
|
|
21
|
+
raise GrammarError,
|
|
22
|
+
"skip rule must be non-nullable: a skip that can match " \
|
|
23
|
+
"empty loops forever at injection points"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
injector = Injector.new(skip_atom)
|
|
27
|
+
[injector.inject(root_atom, entry: true), skip_atom]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The injection walk. Mirrors the PARG compiler's semantics:
|
|
31
|
+
# sequences interleave the wrapper before every child, alternatives
|
|
32
|
+
# and repetitions descend, named captures descend (spans stay
|
|
33
|
+
# clean — the wrapper is Trivia: Ignored + diagnostics
|
|
34
|
+
# transparency), bare terminals gain a leading wrapper, and the
|
|
35
|
+
# entry root takes leading and trailing wrappers.
|
|
36
|
+
class Injector
|
|
37
|
+
def initialize(skip_atom)
|
|
38
|
+
@skip_atom = skip_atom
|
|
39
|
+
@wrapper_ids = {}.compare_by_identity
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def inject(atom, entry: false)
|
|
43
|
+
injected = walk(atom, {})
|
|
44
|
+
return injected unless entry
|
|
45
|
+
|
|
46
|
+
Atoms::Sequence.new(maybe, injected, maybe)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def maybe
|
|
52
|
+
@maybe ||= begin
|
|
53
|
+
wrapper = Atoms::Trivia.new(Atoms::Repetition.new(@skip_atom, 0, 1))
|
|
54
|
+
@wrapper_ids[wrapper] = true
|
|
55
|
+
wrapper
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def wrapper?(atom)
|
|
60
|
+
@wrapper_ids.key?(atom)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def walk(atom, visited)
|
|
64
|
+
return atom if visited.key?(atom)
|
|
65
|
+
return atom if atom.equal?(@skip_atom)
|
|
66
|
+
|
|
67
|
+
visited = visited.merge(atom => true)
|
|
68
|
+
case atom
|
|
69
|
+
when Atoms::Sequence
|
|
70
|
+
out = []
|
|
71
|
+
atom.parslets.each do |child|
|
|
72
|
+
injected = walk(child, visited)
|
|
73
|
+
out << maybe unless wrapper?(injected) || injected.is_a?(Atoms::Trivia)
|
|
74
|
+
out << injected
|
|
75
|
+
end
|
|
76
|
+
out.length == 1 ? out.first : Atoms::Sequence.new(*out)
|
|
77
|
+
when Atoms::Alternative
|
|
78
|
+
Atoms::Alternative.new(*atom.alternatives.map { |a| walk(a, visited) })
|
|
79
|
+
when Atoms::Repetition
|
|
80
|
+
if wrapper?(atom)
|
|
81
|
+
atom
|
|
82
|
+
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
|
+
atom
|
|
101
|
+
end
|
|
102
|
+
when Atoms::Str, Atoms::Re
|
|
103
|
+
Atoms::Sequence.new(maybe, atom)
|
|
104
|
+
else
|
|
105
|
+
atom
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/parsanol/version.rb
CHANGED
data/lib/parsanol/vm.rb
CHANGED
|
@@ -649,6 +649,12 @@ module Parsanol
|
|
|
649
649
|
compile_entity(atom, consume_all)
|
|
650
650
|
when Parsanol::Atoms::Lookahead
|
|
651
651
|
compile_lookahead(atom, consume_all)
|
|
652
|
+
when Parsanol::Atoms::TriviaCapture
|
|
653
|
+
# Captured trivia records into the context channel at match
|
|
654
|
+
# time (parsanol-ruby#152); the bytecode program has no
|
|
655
|
+
# channel, so the interpreter (which does) must own the
|
|
656
|
+
# grammar. Declining here routes program_for to :fallback.
|
|
657
|
+
nil
|
|
652
658
|
when Parsanol::Atoms::Ignored
|
|
653
659
|
inner = atom.wrapped_atom
|
|
654
660
|
return nil if inner.nil?
|
data/lib/parsanol.rb
CHANGED
|
@@ -290,6 +290,7 @@ module Parsanol
|
|
|
290
290
|
autoload :Pattern, "parsanol/pattern"
|
|
291
291
|
autoload :Transform, "parsanol/transform"
|
|
292
292
|
autoload :VM, "parsanol/vm"
|
|
293
|
+
autoload :Skip, "parsanol/skip"
|
|
293
294
|
autoload :Parser, "parsanol/parser"
|
|
294
295
|
autoload :ErrorReporter, "parsanol/error_reporter"
|
|
295
296
|
autoload :Scope, "parsanol/scope"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parsanol
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.72
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -155,6 +155,7 @@ files:
|
|
|
155
155
|
- lib/parsanol/rig/rspec.rb
|
|
156
156
|
- lib/parsanol/rope.rb
|
|
157
157
|
- lib/parsanol/scope.rb
|
|
158
|
+
- lib/parsanol/skip.rb
|
|
158
159
|
- lib/parsanol/slice.rb
|
|
159
160
|
- lib/parsanol/source.rb
|
|
160
161
|
- lib/parsanol/source/line_cache.rb
|