parsanol 1.3.107-arm-linux → 1.3.109-arm-linux
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/lib/parsanol/3.2/parsanol_native.so +2 -2
- data/lib/parsanol/3.3/parsanol_native.so +2 -2
- data/lib/parsanol/3.4/parsanol_native.so +2 -2
- data/lib/parsanol/4.0/parsanol_native.so +2 -2
- data/lib/parsanol/native/libparsanol.so +0 -0
- data/lib/parsanol/parg/cli.rb +97 -0
- data/lib/parsanol/parg/compiler.rb +102 -5
- data/lib/parsanol/parg/diff.rb +264 -0
- data/lib/parsanol/parg/document.rb +9 -1
- data/lib/parsanol/parg/export.rb +123 -0
- data/lib/parsanol/parg/import.rb +3 -1
- data/lib/parsanol/parg/importers/abnf.rb +148 -28
- data/lib/parsanol/parg/importers/antlr.rb +407 -0
- data/lib/parsanol/parg/lexer.rb +1 -0
- data/lib/parsanol/parg/lints.rb +448 -1
- data/lib/parsanol/parg/parser.rb +59 -2
- data/lib/parsanol/parg.rb +2 -0
- data/lib/parsanol/slice.rb +13 -4
- data/lib/parsanol/version.rb +1 -1
- metadata +5 -2
data/lib/parsanol/parg/lints.rb
CHANGED
|
@@ -9,6 +9,15 @@ module Parsanol
|
|
|
9
9
|
# Lints use the compiler only as an analysis facade (find_left_cycle,
|
|
10
10
|
# first_set, nullable?).
|
|
11
11
|
module Lints
|
|
12
|
+
# Advisory warnings are author guidance — they never ride the
|
|
13
|
+
# envelope. The envelope's lint.order_warnings is a wire contract:
|
|
14
|
+
# recompiling an unchanged grammar must reproduce it byte-for-byte,
|
|
15
|
+
# so style/authoring findings (which fire on perfectly valid
|
|
16
|
+
# grammars) stay out of it.
|
|
17
|
+
Advisory = Struct.new(:message) do
|
|
18
|
+
def to_s = message
|
|
19
|
+
end
|
|
20
|
+
|
|
12
21
|
class << self
|
|
13
22
|
def register(name, strategy)
|
|
14
23
|
strategies[name.to_sym] = strategy
|
|
@@ -22,6 +31,14 @@ module Parsanol
|
|
|
22
31
|
strategies.values.flat_map { |strategy| strategy.warnings(document, compiler) }
|
|
23
32
|
end
|
|
24
33
|
|
|
34
|
+
# The envelope-carried subset: strategies that are not advisory.
|
|
35
|
+
def envelope_warnings(document, compiler)
|
|
36
|
+
strategies.values
|
|
37
|
+
.reject { |strategy| strategy.respond_to?(:advisory?) && strategy.advisory? }
|
|
38
|
+
.flat_map { |strategy| strategy.warnings(document, compiler) }
|
|
39
|
+
.grep_v(Advisory)
|
|
40
|
+
end
|
|
41
|
+
|
|
25
42
|
def strategies
|
|
26
43
|
@strategies ||= {}
|
|
27
44
|
end
|
|
@@ -71,8 +88,19 @@ module Parsanol
|
|
|
71
88
|
branches = node.a
|
|
72
89
|
branches.each_with_index do |branch, index|
|
|
73
90
|
if compiler.nullable?(branch) && index < branches.length - 1
|
|
74
|
-
|
|
91
|
+
later = branches[(index + 1)..]
|
|
92
|
+
results << if later.all? { |candidate| compiler.nullable?(candidate) }
|
|
93
|
+
# Both sides always succeed: the later branches are dead
|
|
94
|
+
# code, but acceptance is identical. Imported grammars
|
|
95
|
+
# legitimately produce this (unordered ABNF choice), so
|
|
96
|
+
# it is a warning, not the shadowing hazard.
|
|
97
|
+
{ errors: [],
|
|
98
|
+
warnings: [Advisory.new(dead_nullable_warning(name, index + 1))] }
|
|
99
|
+
else
|
|
100
|
+
{ errors: [empty_shadow_error(name, index + 1)], warnings: [] }
|
|
101
|
+
end
|
|
75
102
|
end
|
|
103
|
+
|
|
76
104
|
branches[(index + 1)..].each_with_index do |later, j|
|
|
77
105
|
results << compare_branches(name, branch, index + 1, later, index + j + 2, compiler)
|
|
78
106
|
end
|
|
@@ -85,14 +113,25 @@ module Parsanol
|
|
|
85
113
|
"shadows all later branches"
|
|
86
114
|
end
|
|
87
115
|
|
|
116
|
+
def dead_nullable_warning(name, branch_number)
|
|
117
|
+
"rule #{name}: branches after #{branch_number} are unreachable " \
|
|
118
|
+
"(that branch always succeeds) — dead code, acceptance unchanged"
|
|
119
|
+
end
|
|
120
|
+
|
|
88
121
|
def compare_branches(name, earlier, earlier_n, later, later_n, compiler)
|
|
89
122
|
earlier_lit = literal_string(earlier)
|
|
90
123
|
later_lit = literal_string(later)
|
|
124
|
+
earlier_fold = literal_fold(earlier)
|
|
91
125
|
if earlier_lit && later_lit
|
|
92
126
|
if earlier_lit == later_lit
|
|
93
127
|
return { errors: ["rule #{name}: branches #{earlier_n} and #{later_n} are identical"], warnings: [] }
|
|
94
128
|
elsif later_lit.start_with?(earlier_lit)
|
|
95
129
|
return { errors: [shadow_error(name, earlier_n, earlier_lit, later_n, later_lit)], warnings: [] }
|
|
130
|
+
elsif earlier_fold && earlier_lit.casecmp(later_lit).zero?
|
|
131
|
+
# A case-insensitive earlier literal matches every casing
|
|
132
|
+
# the later literal could: the later branch is dead (same
|
|
133
|
+
# class as the prefix shadow above).
|
|
134
|
+
return { errors: [case_shadow_error(name, earlier_n, earlier_lit, later_n, later_lit)], warnings: [] }
|
|
96
135
|
end
|
|
97
136
|
end
|
|
98
137
|
first_earlier = compiler.first_set(earlier) - [Compiler::EPS]
|
|
@@ -116,6 +155,16 @@ module Parsanol
|
|
|
116
155
|
"rule #{name}: branches #{earlier_n} and #{later_n} are order-dependent (#{reason})"
|
|
117
156
|
end
|
|
118
157
|
|
|
158
|
+
def literal_fold(node)
|
|
159
|
+
node.kind == :lit ? node.b : nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def case_shadow_error(name, earlier_n, earlier_lit, later_n, later_lit)
|
|
163
|
+
"rule #{name}: branch #{later_n} (#{later_lit.inspect}) can never match -- " \
|
|
164
|
+
"branch #{earlier_n} is case-insensitive and already matches " \
|
|
165
|
+
"#{earlier_lit.inspect} in every casing"
|
|
166
|
+
end
|
|
167
|
+
|
|
119
168
|
def literal_string(node)
|
|
120
169
|
case node.kind
|
|
121
170
|
when :lit then node.a
|
|
@@ -125,8 +174,276 @@ module Parsanol
|
|
|
125
174
|
end
|
|
126
175
|
end
|
|
127
176
|
|
|
177
|
+
# TODO.sota/9 T1/T2 lints (ruby#307): the core-rule prelude and
|
|
178
|
+
# the brace form both carry a migration path — the lint carries
|
|
179
|
+
# the suggestion. Core-rule shadows are deliberate-compatibility
|
|
180
|
+
# (a local rule wins), so they are warnings, never errors.
|
|
181
|
+
class CoreRuleStyle
|
|
182
|
+
def advisory? = true
|
|
183
|
+
|
|
184
|
+
CORE_NAMES = (
|
|
185
|
+
Compiler::BUILTIN_CLASSES.keys + Compiler::STRUCTURED_CORE_RULE_NAMES
|
|
186
|
+
).freeze
|
|
187
|
+
|
|
188
|
+
def warnings(document, _compiler)
|
|
189
|
+
authored = document.authored_rules || document.rules.keys
|
|
190
|
+
shadows = authored.select { |name| CORE_NAMES.include?(name) }
|
|
191
|
+
shadow_warnings = shadows.map do |name|
|
|
192
|
+
"rule #{name}: shadows the RFC 5234 core rule — drop the local " \
|
|
193
|
+
"definition unless the narrowing is deliberate"
|
|
194
|
+
end
|
|
195
|
+
shadow_warnings + style_mixing_warning(document)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def errors(_document, _compiler) = []
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
def style_mixing_warning(document)
|
|
203
|
+
brace = document.brace_rules.size
|
|
204
|
+
line = document.rules.size - brace
|
|
205
|
+
return [] if brace.zero? || line.zero?
|
|
206
|
+
return [] if document.rules.size < 4
|
|
207
|
+
|
|
208
|
+
["grammar mixes brace (#{brace}) and line (#{line}) rule bodies — " \
|
|
209
|
+
"pick one style per grammar"]
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
128
213
|
register :left_recursion, LeftRecursion.new
|
|
214
|
+
register :core_rule_style, CoreRuleStyle.new
|
|
215
|
+
|
|
216
|
+
# ruby#307: authoring-contract lints. Each flags a grammar whose
|
|
217
|
+
# artifact ships without something its consumers will want: pins,
|
|
218
|
+
# docs, or a canonical entry placement. All warnings — the
|
|
219
|
+
# constructs are legal; the omissions are the hazard.
|
|
220
|
+
class AuthoringContracts
|
|
221
|
+
def advisory? = true
|
|
222
|
+
|
|
223
|
+
def warnings(document, compiler)
|
|
224
|
+
unpinned(document) + undocumented(document, compiler)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def errors(_document, _compiler) = []
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
def unpinned(document)
|
|
232
|
+
return [] unless document.tests.empty?
|
|
233
|
+
|
|
234
|
+
["grammar ships without a test section — an unpinned grammar is " \
|
|
235
|
+
"unpinned behavior (accept/reject/capture tests ride the artifact " \
|
|
236
|
+
"to every engine)"]
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def undocumented(document, _compiler)
|
|
240
|
+
# Injected core rules and left-recursion tier rules are
|
|
241
|
+
# compiler-generated: they are reachable and necessarily
|
|
242
|
+
# undocumented, and flagging them would be noise.
|
|
243
|
+
injected = Compiler::BUILTIN_CLASSES.keys +
|
|
244
|
+
Compiler::STRUCTURED_CORE_RULE_NAMES
|
|
245
|
+
authored = document.authored_rules
|
|
246
|
+
prefix = Compiler::LEFT_RECURSION_PREFIX
|
|
247
|
+
entry_reachable(document).filter_map do |name|
|
|
248
|
+
next if authored && !authored.include?(name)
|
|
249
|
+
next if document.docs.key?(name)
|
|
250
|
+
next if injected.include?(name)
|
|
251
|
+
next if name.to_s.start_with?(prefix)
|
|
252
|
+
|
|
253
|
+
"rule #{name}: entry-reachable without a ## doc comment — the " \
|
|
254
|
+
"artifact ships docs for LSP hovers; a public rule without one " \
|
|
255
|
+
"is a missing contract"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Rules reachable from any entry root through rule references.
|
|
260
|
+
def entry_reachable(document)
|
|
261
|
+
reachable = []
|
|
262
|
+
seen = {}
|
|
263
|
+
walk = lambda do |node|
|
|
264
|
+
case node.kind
|
|
265
|
+
when :ref
|
|
266
|
+
name = node.a
|
|
267
|
+
next if seen[name]
|
|
268
|
+
|
|
269
|
+
seen[name] = true
|
|
270
|
+
reachable << name
|
|
271
|
+
walk.call(document.rules[name]) if document.rules.key?(name)
|
|
272
|
+
when :alt, :seq then node.a.each { |child| walk.call(child) }
|
|
273
|
+
when :cap, :pred then walk.call(node.b)
|
|
274
|
+
when :rep, :opt then walk.call(node.a)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
document.entries.each_value do |root|
|
|
278
|
+
walk.call(document.rules[root]) if document.rules.key?(root)
|
|
279
|
+
end
|
|
280
|
+
reachable
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
register :authoring_contracts, AuthoringContracts.new
|
|
285
|
+
# ruby#308 first slice: the two halves of binding coverage.
|
|
286
|
+
# A binding that names a capture the grammar never produces binds
|
|
287
|
+
# nothing; a capture no binding consumes is dead weight. Both are
|
|
288
|
+
# advisory: the grammar compiles either way.
|
|
289
|
+
class BindingCoverage
|
|
290
|
+
def advisory? = true
|
|
291
|
+
|
|
292
|
+
def warnings(document, _compiler)
|
|
293
|
+
all_captures = produced_captures(document)
|
|
294
|
+
bound = []
|
|
295
|
+
document.bindings.each_value do |list|
|
|
296
|
+
Array(list).each { |binding| bound << binding.capture }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# No bindings section = the author opted out of binding (a
|
|
300
|
+
# parse-only grammar): uncovered captures are not oversights.
|
|
301
|
+
# The orphan half moved to BindingPaths, whose resolution
|
|
302
|
+
# failures fail the build (ruby#308 v2) -- a warning could
|
|
303
|
+
# never fire past that error.
|
|
304
|
+
dead = document.bindings.empty? ? [] : all_captures - bound.uniq
|
|
305
|
+
dead.map do |capture|
|
|
306
|
+
"capture #{capture.inspect} is bound by no bindings section -- " \
|
|
307
|
+
"ship the binding beside the capture, or drop the capture"
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def errors(_document, _compiler) = []
|
|
312
|
+
|
|
313
|
+
private
|
|
314
|
+
|
|
315
|
+
def produced_captures(document)
|
|
316
|
+
captures = []
|
|
317
|
+
seen = {}
|
|
318
|
+
document.rules.each_value do |node|
|
|
319
|
+
collect_captures(node, document, captures, seen)
|
|
320
|
+
end
|
|
321
|
+
captures.uniq
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def collect_captures(node, document, captures, seen)
|
|
325
|
+
case node.kind
|
|
326
|
+
when :cap
|
|
327
|
+
captures << node.a
|
|
328
|
+
collect_captures(node.b, document, captures, seen)
|
|
329
|
+
when :ref
|
|
330
|
+
rule = node.a
|
|
331
|
+
return if seen[rule]
|
|
332
|
+
|
|
333
|
+
seen[rule] = true
|
|
334
|
+
target = document.rules[rule]
|
|
335
|
+
collect_captures(target, document, captures, seen) if target
|
|
336
|
+
when :alt, :seq
|
|
337
|
+
node.a.each { |child| collect_captures(child, document, captures, seen) }
|
|
338
|
+
when :pred
|
|
339
|
+
collect_captures(node.b, document, captures, seen)
|
|
340
|
+
when :rep, :opt
|
|
341
|
+
collect_captures(node.a, document, captures, seen)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
129
346
|
register :alternatives, Alternatives.new
|
|
347
|
+
register :binding_coverage, BindingCoverage.new
|
|
348
|
+
|
|
349
|
+
# ruby#308 v2: compile-time binding resolution. The CAPTURE a
|
|
350
|
+
# binding reads must exist in its rule's capture subtree (the
|
|
351
|
+
# orphan check, now with teeth); a `x[]...` path needs that
|
|
352
|
+
# capture to sit under a repetition (the flattening presumes
|
|
353
|
+
# iteration). The path's names are output naming -- free -- but
|
|
354
|
+
# its SHAPE is a contract: one `[]` level, one dotted-free leaf,
|
|
355
|
+
# and a nested path always carries the marker. Anything the
|
|
356
|
+
# runtime would silently bind as nothing fails the build instead,
|
|
357
|
+
# naming the rule, the path, and the offending segment.
|
|
358
|
+
class BindingPaths
|
|
359
|
+
def errors(document, _compiler)
|
|
360
|
+
document.bindings.flat_map do |rule, bindings|
|
|
361
|
+
tree = CaptureTree.new(document, rule)
|
|
362
|
+
Array(bindings).flat_map { |binding| resolve(rule, binding, tree) }
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def warnings(_document, _compiler) = []
|
|
367
|
+
def advisory? = false
|
|
368
|
+
|
|
369
|
+
private
|
|
370
|
+
|
|
371
|
+
# The validated language is what the runtime honors today:
|
|
372
|
+
# `capture` and `capture[].leaf` -- one repetition level, one
|
|
373
|
+
# leaf. Anything else fails the build instead of silently
|
|
374
|
+
# binding nothing (the runtime's leaf lookup would miss).
|
|
375
|
+
def resolve(rule, binding, tree)
|
|
376
|
+
capture = binding.capture.to_s
|
|
377
|
+
path = binding.path.to_s
|
|
378
|
+
return [unresolvable(rule, path, path)] if path.include?(".") && !path.include?("[]")
|
|
379
|
+
|
|
380
|
+
scope = tree.scope
|
|
381
|
+
return [unresolvable(rule, capture, capture)] unless scope.key?(capture)
|
|
382
|
+
return [] unless path.include?("[]")
|
|
383
|
+
|
|
384
|
+
levels = path.split("[]", -1)
|
|
385
|
+
return [unresolvable(rule, path, "[]")] if levels.size > 2
|
|
386
|
+
|
|
387
|
+
leaf = levels[1].to_s.delete_prefix(".")
|
|
388
|
+
return [unresolvable(rule, path, "[]")] unless scope[capture][0]
|
|
389
|
+
return [unresolvable(rule, path, leaf)] if leaf.empty? || leaf.include?(".")
|
|
390
|
+
|
|
391
|
+
[]
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def unresolvable(rule, path, segment)
|
|
395
|
+
"binding on #{rule.inspect}: path #{path.inspect} does not resolve -- " \
|
|
396
|
+
"segment #{segment.inspect} names no capture at that position " \
|
|
397
|
+
"(supported: capture, capture[].leaf)"
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# The capture TREE a binding path resolves against: per capture,
|
|
402
|
+
# whether it sits under a repetition and which captures its
|
|
403
|
+
# subtree contains (transitively -- the runtime's capture lookup
|
|
404
|
+
# is subtree-wide too).
|
|
405
|
+
class CaptureTree
|
|
406
|
+
def initialize(document, rule)
|
|
407
|
+
@document = document
|
|
408
|
+
@scope = Hash.new { |hash, name| hash[name] = [false, []] }
|
|
409
|
+
walk(document.rules[rule], { rule => true }) if document.rules.key?(rule)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def scope
|
|
413
|
+
@scope
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def walk(node, seen, under_rep: false, inside: [])
|
|
417
|
+
case node.kind
|
|
418
|
+
when :cap
|
|
419
|
+
entry = @scope[node.a]
|
|
420
|
+
# `1*x as n` compiles to cap(n, rep): the capture WRAPS its
|
|
421
|
+
# repetition, so a repetition in either position makes the
|
|
422
|
+
# capture repeated.
|
|
423
|
+
repeated = under_rep || node.b&.kind == :rep
|
|
424
|
+
entry[0] ||= repeated
|
|
425
|
+
inside.each { |parent| entry[1] |= [parent] unless entry[1].include?(parent) }
|
|
426
|
+
walk(node.b, seen, under_rep: repeated, inside: inside + [node.a])
|
|
427
|
+
when :ref
|
|
428
|
+
rule = node.a
|
|
429
|
+
return if seen[rule]
|
|
430
|
+
|
|
431
|
+
seen[rule] = true
|
|
432
|
+
target = @document.rules[rule]
|
|
433
|
+
walk(target, seen, under_rep: under_rep, inside: inside) if target
|
|
434
|
+
when :alt, :seq
|
|
435
|
+
node.a.each { |child| walk(child, seen, under_rep: under_rep, inside: inside) }
|
|
436
|
+
when :pred
|
|
437
|
+
walk(node.b, seen, under_rep: under_rep, inside: inside)
|
|
438
|
+
when :rep
|
|
439
|
+
walk(node.a, seen, under_rep: true, inside: inside)
|
|
440
|
+
when :opt
|
|
441
|
+
walk(node.a, seen, under_rep: under_rep, inside: inside)
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
register :binding_paths, BindingPaths.new
|
|
130
447
|
|
|
131
448
|
# An unbounded repetition whose body can match empty input is an
|
|
132
449
|
# INVALID GRAMMAR: the parse can never terminate, and any VM that
|
|
@@ -166,6 +483,136 @@ module Parsanol
|
|
|
166
483
|
end
|
|
167
484
|
end
|
|
168
485
|
register :repetitions, Repetitions.new
|
|
486
|
+
|
|
487
|
+
# ruby#307 lint completion: a rule no other rule references is
|
|
488
|
+
# dead weight (unless it carries an entry); a rule referenced
|
|
489
|
+
# exactly once is an inline candidate. Both advisory -- the
|
|
490
|
+
# grammar is correct either way. Entry roots, the injected core
|
|
491
|
+
# rules, and the left-recursion tier rules are exempt.
|
|
492
|
+
class RuleUsage
|
|
493
|
+
def advisory? = true
|
|
494
|
+
|
|
495
|
+
def warnings(document, _compiler)
|
|
496
|
+
counts = Hash.new(0)
|
|
497
|
+
document.rules.each do |name, node|
|
|
498
|
+
collect_refs(node, document, { name => true }, counts)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
injected = Compiler::BUILTIN_CLASSES.keys +
|
|
502
|
+
Compiler::STRUCTURED_CORE_RULE_NAMES
|
|
503
|
+
resync_prefix = Compiler::LEFT_RECURSION_PREFIX
|
|
504
|
+
document.rules.filter_map do |name, _node|
|
|
505
|
+
next if injected.include?(name)
|
|
506
|
+
next if name.to_s.start_with?(resync_prefix)
|
|
507
|
+
next if document.entries.value?(name)
|
|
508
|
+
|
|
509
|
+
case counts[name]
|
|
510
|
+
when 0
|
|
511
|
+
"rule #{name.inspect} is referenced by no rule -- dead " \
|
|
512
|
+
"unless it is a future entry; drop it or wire it in"
|
|
513
|
+
when 1
|
|
514
|
+
"rule #{name.inspect} has exactly one reference -- inline " \
|
|
515
|
+
"candidate (keep it if the name carries the domain)"
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def errors(_document, _compiler) = []
|
|
521
|
+
|
|
522
|
+
private
|
|
523
|
+
|
|
524
|
+
def collect_refs(node, document, seen, counts)
|
|
525
|
+
case node.kind
|
|
526
|
+
when :ref
|
|
527
|
+
rule = node.a
|
|
528
|
+
counts[rule] += 1
|
|
529
|
+
return if seen[rule]
|
|
530
|
+
|
|
531
|
+
seen[rule] = true
|
|
532
|
+
target = document.rules[rule]
|
|
533
|
+
collect_refs(target, document, seen, counts) if target
|
|
534
|
+
when :alt, :seq
|
|
535
|
+
node.a.each { |child| collect_refs(child, document, seen, counts) }
|
|
536
|
+
when :cap, :pred
|
|
537
|
+
collect_refs(node.b, document, seen, counts)
|
|
538
|
+
when :rep, :opt
|
|
539
|
+
collect_refs(node.a, document, seen, counts)
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
register :rule_usage, RuleUsage.new
|
|
544
|
+
|
|
545
|
+
# ruby#307: canonical entry placement. Every corpus grammar and
|
|
546
|
+
# the guide's canonical demo declare entries at the FOOT of the
|
|
547
|
+
# grammar block, after the rules they root; an entry above its
|
|
548
|
+
# rule reads as a forward table-of-contents and breaks the
|
|
549
|
+
# reading order. Advisory — placement is style, resolution is
|
|
550
|
+
# order-independent.
|
|
551
|
+
class EntryPlacement
|
|
552
|
+
def advisory? = true
|
|
553
|
+
|
|
554
|
+
def warnings(document, _compiler)
|
|
555
|
+
index = {}
|
|
556
|
+
document.declaration_order.each_with_index do |(kind, name), i|
|
|
557
|
+
index[[kind, name]] = i
|
|
558
|
+
end
|
|
559
|
+
document.entries.filter_map do |name, rule|
|
|
560
|
+
entry_at = index[[:entry, name]]
|
|
561
|
+
rule_at = index[[:rule, rule]]
|
|
562
|
+
next if entry_at.nil? || rule_at.nil? || entry_at > rule_at
|
|
563
|
+
|
|
564
|
+
"entry #{name.inspect} is declared before the rule #{rule.inspect} it " \
|
|
565
|
+
"roots -- canonical placement: entries at the foot of the grammar block"
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def errors(_document, _compiler) = []
|
|
570
|
+
end
|
|
571
|
+
register :entry_placement, EntryPlacement.new
|
|
572
|
+
|
|
573
|
+
# ruby#307 B5 --suggest-upgrades: machine-applicable authoring
|
|
574
|
+
# upgrades. NOT registered -- its output is a suggestion surface
|
|
575
|
+
# (parg lint --suggest-upgrades), never a compile diagnostic and
|
|
576
|
+
# never part of the envelope.
|
|
577
|
+
class CoreRuleDuplicates
|
|
578
|
+
def warnings(document, _compiler)
|
|
579
|
+
document.rules.filter_map do |name, node|
|
|
580
|
+
next if Compiler::BUILTIN_CLASSES.key?(name)
|
|
581
|
+
|
|
582
|
+
core = core_rule_for(node)
|
|
583
|
+
next if core.nil?
|
|
584
|
+
|
|
585
|
+
"rule #{name.inspect} duplicates the core rule #{core} -- " \
|
|
586
|
+
"reference #{core} instead (or keep the local spelling deliberate)"
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def errors(_document, _compiler) = []
|
|
591
|
+
def advisory? = true
|
|
592
|
+
|
|
593
|
+
private
|
|
594
|
+
|
|
595
|
+
def core_rule_for(node)
|
|
596
|
+
case node.kind
|
|
597
|
+
when :class
|
|
598
|
+
Compiler::BUILTIN_CLASSES.find { |_, ranges| normalized(ranges) == normalized(node.a) }&.first
|
|
599
|
+
when :seq
|
|
600
|
+
crlf?(node) ? "CRLF" : nil
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def crlf?(node)
|
|
605
|
+
parts = node.a
|
|
606
|
+
parts.length == 2 &&
|
|
607
|
+
parts.all? { |part| part.kind == :class } &&
|
|
608
|
+
normalized(parts[0].a) == [[0x0D, 0x0D]] &&
|
|
609
|
+
normalized(parts[1].a) == [[0x0A, 0x0A]]
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def normalized(ranges)
|
|
613
|
+
ranges.map(&:sort).sort
|
|
614
|
+
end
|
|
615
|
+
end
|
|
169
616
|
end
|
|
170
617
|
end
|
|
171
618
|
end
|
data/lib/parsanol/parg/parser.rb
CHANGED
|
@@ -27,6 +27,7 @@ module Parsanol
|
|
|
27
27
|
@tokens = Lexer.new(text).tokens
|
|
28
28
|
@pos = 0
|
|
29
29
|
@source_text = text
|
|
30
|
+
@brace_depth = 0
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def parse
|
|
@@ -65,6 +66,20 @@ module Parsanol
|
|
|
65
66
|
advance while peek&.type == :newline
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
# Whitespace inside a brace body: newlines and comments (the `##`
|
|
70
|
+
# doc token included — inside a body it is a comment, not a doc
|
|
71
|
+
# attachment). No-op outside braces, so line-mode parsing is
|
|
72
|
+
# unchanged.
|
|
73
|
+
def skip_body_ws
|
|
74
|
+
return unless @brace_depth.positive?
|
|
75
|
+
|
|
76
|
+
skip_newlines
|
|
77
|
+
while peek&.type == :doc
|
|
78
|
+
advance
|
|
79
|
+
skip_newlines
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
68
83
|
# Doc comments (##) accumulate and attach to the next rule.
|
|
69
84
|
def take_doc_comments
|
|
70
85
|
docs = []
|
|
@@ -294,12 +309,41 @@ module Parsanol
|
|
|
294
309
|
name = rule_name
|
|
295
310
|
document.docs[name] = docs.join("\n") unless docs.empty?
|
|
296
311
|
punct("=")
|
|
312
|
+
# Brace bodies (TODO.sota/9 T2, ruby#302): `name = { ... }`
|
|
313
|
+
# is self-delimiting — newlines and comments are whitespace
|
|
314
|
+
# inside, the body extends to the matching `}`. Additive: `{`
|
|
315
|
+
# never appeared at the rule head before.
|
|
316
|
+
brace = peek&.type == :punct && peek.value == "{"
|
|
317
|
+
if !brace
|
|
318
|
+
# The brace may also sit on the next line: `name =` NL `{`.
|
|
319
|
+
# Peek past the newline; restore when it is not a brace so
|
|
320
|
+
# line-mode error behavior is untouched.
|
|
321
|
+
saved = @pos
|
|
322
|
+
skip_newlines
|
|
323
|
+
if peek&.type == :punct && peek.value == "{"
|
|
324
|
+
brace = true
|
|
325
|
+
else
|
|
326
|
+
@pos = saved
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
if brace
|
|
330
|
+
advance
|
|
331
|
+
@brace_depth += 1
|
|
332
|
+
skip_body_ws
|
|
333
|
+
end
|
|
297
334
|
node = parse_choice
|
|
335
|
+
if brace
|
|
336
|
+
skip_body_ws
|
|
337
|
+
punct("}")
|
|
338
|
+
@brace_depth -= 1
|
|
339
|
+
end
|
|
298
340
|
if document.rules.key?(name)
|
|
299
341
|
raise ParseError, "duplicate rule #{name.inspect}"
|
|
300
342
|
end
|
|
301
343
|
|
|
302
344
|
document.rules[name] = node
|
|
345
|
+
document.declaration_order << [:rule, name]
|
|
346
|
+
document.brace_rules << name if brace
|
|
303
347
|
document.atomic_rules << name if atomic
|
|
304
348
|
skip_newlines
|
|
305
349
|
end
|
|
@@ -365,7 +409,10 @@ module Parsanol
|
|
|
365
409
|
|
|
366
410
|
def parse_choice
|
|
367
411
|
branches = [parse_seq]
|
|
368
|
-
|
|
412
|
+
loop do
|
|
413
|
+
skip_body_ws
|
|
414
|
+
break unless peek&.type == :punct && peek.value == "/"
|
|
415
|
+
|
|
369
416
|
advance
|
|
370
417
|
branches << parse_seq
|
|
371
418
|
end
|
|
@@ -373,15 +420,22 @@ module Parsanol
|
|
|
373
420
|
end
|
|
374
421
|
|
|
375
422
|
def parse_seq
|
|
423
|
+
skip_body_ws
|
|
376
424
|
items = [parse_element]
|
|
377
425
|
until stop_element?
|
|
426
|
+
skip_body_ws
|
|
427
|
+
# After skipped body whitespace the terminator (`}`, `)`, `/`)
|
|
428
|
+
# may only now be visible: re-check before parsing an element.
|
|
429
|
+
break if stop_element?
|
|
430
|
+
break if @brace_depth.positive? && peek&.type == :punct && peek.value == "/"
|
|
431
|
+
|
|
378
432
|
items << parse_element
|
|
379
433
|
end
|
|
380
434
|
items.length == 1 ? items.first : Node.new(:seq, items)
|
|
381
435
|
end
|
|
382
436
|
|
|
383
437
|
def stop_element?
|
|
384
|
-
peek.nil? || peek.type == :newline ||
|
|
438
|
+
peek.nil? || (peek.type == :newline && @brace_depth.zero?) ||
|
|
385
439
|
(peek.type == :punct && ["/", ")", "]", "}"].include?(peek.value)) ||
|
|
386
440
|
# `capture:` suffix terminates the skip declaration's RHS
|
|
387
441
|
# (parsanol-ruby#152); everywhere else parse_element's guard
|
|
@@ -452,6 +506,7 @@ module Parsanol
|
|
|
452
506
|
end
|
|
453
507
|
|
|
454
508
|
def capture_postfix(node)
|
|
509
|
+
skip_body_ws
|
|
455
510
|
if peek&.type == :ident && peek.value == "as"
|
|
456
511
|
advance
|
|
457
512
|
node = Node.new(:cap, ident.value, node)
|
|
@@ -477,6 +532,7 @@ module Parsanol
|
|
|
477
532
|
when :str, :sstr then Node.new(:lit, unquote(token.value), false)
|
|
478
533
|
when :istr then Node.new(:lit, unquote(token.value), true)
|
|
479
534
|
when :hex then parse_hex(token.value)
|
|
535
|
+
when :category then Node.new(:category, token.value[3..-2])
|
|
480
536
|
when :punct then parse_punctuated(token)
|
|
481
537
|
when :ident then parse_ident(token)
|
|
482
538
|
else raise ParseError, "unexpected token #{token.type}:#{token.value}"
|
|
@@ -580,6 +636,7 @@ module Parsanol
|
|
|
580
636
|
name = ident.value
|
|
581
637
|
punct(":")
|
|
582
638
|
document.entries[name] = rule_name
|
|
639
|
+
document.declaration_order << [:entry, name]
|
|
583
640
|
end
|
|
584
641
|
|
|
585
642
|
def parse_bindings(document)
|
data/lib/parsanol/parg.rb
CHANGED
|
@@ -18,6 +18,7 @@ module Parsanol
|
|
|
18
18
|
autoload :CompileError, "parsanol/parg/error"
|
|
19
19
|
autoload :ArtifactError, "parsanol/parg/error"
|
|
20
20
|
autoload :Node, "parsanol/parg/node"
|
|
21
|
+
autoload :Export, "parsanol/parg/export"
|
|
21
22
|
autoload :Lexer, "parsanol/parg/lexer"
|
|
22
23
|
autoload :Parser, "parsanol/parg/parser"
|
|
23
24
|
autoload :Document, "parsanol/parg/document"
|
|
@@ -33,6 +34,7 @@ module Parsanol
|
|
|
33
34
|
autoload :CLI, "parsanol/parg/cli"
|
|
34
35
|
autoload :Lsp, "parsanol/parg/lsp"
|
|
35
36
|
autoload :Lints, "parsanol/parg/lints"
|
|
37
|
+
autoload :Diff, "parsanol/parg/diff"
|
|
36
38
|
autoload :Preprocess, "parsanol/parg/preprocess"
|
|
37
39
|
autoload :Visitor, "parsanol/parg/visitor"
|
|
38
40
|
autoload :Imports, "parsanol/parg/imports"
|
data/lib/parsanol/slice.rb
CHANGED
|
@@ -143,12 +143,21 @@ module Parsanol
|
|
|
143
143
|
# Unified line/column computation:
|
|
144
144
|
# - String input: compute from input string
|
|
145
145
|
# - Source / LineCache: delegate
|
|
146
|
+
def prefix_bytes(pos)
|
|
147
|
+
(@input.byteslice(0, pos) || "").b
|
|
148
|
+
end
|
|
149
|
+
|
|
146
150
|
def line_and_column_at(pos)
|
|
147
151
|
if @input.is_a?(String)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
# All index math in BINARY (rindex returns a character index on
|
|
153
|
+
# a text string — mixing it with byteslice was the off-by-one);
|
|
154
|
+
# the column is a CHARACTER count (the guide's wire contract;
|
|
155
|
+
# rs walks chars()). Offsets stay bytes.
|
|
156
|
+
raw = prefix_bytes(pos)
|
|
157
|
+
line = 1 + raw.count("\n".b)
|
|
158
|
+
last_nl = raw.rindex("\n".b)
|
|
159
|
+
tail = last_nl ? raw.byteslice(last_nl + 1, pos - last_nl - 1) : raw
|
|
160
|
+
column = tail.force_encoding(Encoding::UTF_8).length + 1
|
|
152
161
|
[line, column]
|
|
153
162
|
else
|
|
154
163
|
@input.line_and_column(pos)
|
data/lib/parsanol/version.rb
CHANGED