parsanol 1.3.66-aarch64-linux → 1.3.67-aarch64-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8535329ca9b9c8836c7cbfd2644ca86f3bf79b2d42809ce1b4f949c27f71050
4
- data.tar.gz: be575af69b7a661f0b1168791af56ea55ba16e504b10436aea1cc72c456ded8a
3
+ metadata.gz: ccfa3bf06978a6ae539be84940ca4bbff440b324644bf539960f979ca0237f5b
4
+ data.tar.gz: 79ab5492c25ac5b12f69c36a0761cd5894ba641fd6cae5322044d3965ad161e5
5
5
  SHA512:
6
- metadata.gz: df07c688d90a26aece7c8f628350a7b8ac5bfdf86da38b43ea3141add51e918374c0dbbe20544deac0a95dab28e41d9bec65116455de47cdaced7a3011e53c08
7
- data.tar.gz: 6fac38097606659f867bab918c2972f181c0a765987a08537325455e7cf12030f2259af8272b1c3a4558d48c15773c9e05c13c51a12946a5f7a6f6c3a2ddc50f
6
+ metadata.gz: 33723ecbdfc86c933c24024e7b317c4d8d099a0759381f28587093847ad6bbe85249d7e8ec333f10914734735cc510bd77f0f1a2ad35bcbbae50b10d86737468
7
+ data.tar.gz: c17bf0042c56e63aeffb1a8efdcdc56f97888b00c0c4d22a8aeef9f18227315fa62cfc2b12c204e900f5d0e00ea88dd63711a550a6e78fe40599979eda0092a2
@@ -55,6 +55,17 @@ module Parsanol
55
55
  @warnings = []
56
56
  end
57
57
 
58
+ # QoL shorthands (ABNF core rules): materialized on reference when
59
+ # the grammar does not define its own rule of that name.
60
+ BUILTIN_CLASSES = {
61
+ "DIGIT" => [[0x30, 0x39]],
62
+ "ALPHA" => [[0x41, 0x5A], [0x61, 0x7A]],
63
+ "ALNUM" => [[0x30, 0x39], [0x41, 0x5A], [0x61, 0x7A]],
64
+ "WORD" => [[0x30, 0x39], [0x41, 0x5A], [0x5F, 0x5F], [0x61, 0x7A]],
65
+ "HEXDIG" => [[0x30, 0x39], [0x41, 0x46], [0x61, 0x66]],
66
+ "ANY" => [[0x00, 0x10FFFF]],
67
+ }.freeze
68
+
58
69
  def compile
59
70
  @document.rules.each_key { |name| atom_for(name) }
60
71
  errors = Lints.errors(@document, self)
@@ -79,7 +90,124 @@ module Parsanol
79
90
  end
80
91
 
81
92
  def atom_for(name)
82
- @atom_cache[name] ||= build_atom(fetch_rule(name))
93
+ @atom_cache[name] ||= begin
94
+ atom = build_atom(fetch_rule(name))
95
+ if @document.skip_declaration && !skip_exempt_names.include?(name)
96
+ atom = inject_skip_trivia(atom)
97
+ # Entry points take leading and trailing skips: tail trivia
98
+ # (the tail-comment case) has no following terminal to
99
+ # trigger an injection, and full-consumption would reject
100
+ # it; leading trivia has no preceding interleave either.
101
+ if entry_rule?(name)
102
+ atom = Atoms::Sequence.new(skip_maybe_atom, atom, skip_maybe_atom)
103
+ end
104
+ end
105
+ atom
106
+ end
107
+ end
108
+
109
+ def entry_rule?(name)
110
+ @document.entries.value?(name) || @document.own_entries&.include?(name)
111
+ end
112
+
113
+ # The skip declaration's rule references (and their transitive
114
+ # rule references) build without injection: the skip atom must
115
+ # not contain itself.
116
+ def skip_exempt_names
117
+ @skip_exempt_names ||= begin
118
+ names = Set.new
119
+ walk_refs = lambda do |node|
120
+ case node.kind
121
+ when :ref then names << node.a
122
+ when :alt, :seq then node.a.each { |b| walk_refs.call(b) }
123
+ when :cap then walk_refs.call(node.b)
124
+ end
125
+ end
126
+ walk_refs.call(@document.skip_declaration) if @document.skip_declaration
127
+ closed = Set.new
128
+ until names.subset?(closed)
129
+ (names - closed).each do |ref|
130
+ closed << ref
131
+ walk_refs.call(@document.rules[ref]) if @document.rules.key?(ref)
132
+ end
133
+ names += closed
134
+ end
135
+ names
136
+ end
137
+ end
138
+
139
+ def skip_wrapper?(atom)
140
+ @skip_wrapper_ids&.key?(atom)
141
+ end
142
+
143
+ def skip_maybe_atom
144
+ @skip_maybe_atom ||= begin
145
+ atom = build_atom(@document.skip_declaration)
146
+ while atom.is_a?(Atoms::Entity)
147
+ atom = atom.parslet
148
+ end
149
+ # the VM's internal compiler owns the ATOM-level nullability
150
+ # analysis (the node-level nullable? here works on rule nodes)
151
+ if Parsanol::VM::Compiler.new.nullable?(atom)
152
+ raise CompileError,
153
+ "skip rule must be non-nullable: a skip that can match " \
154
+ "empty loops forever at injection points"
155
+ end
156
+ # Ignored: consumed but contributes nothing to the enclosing
157
+ # sequence/repetition value — trivia never leaks into
158
+ # span-joined captures.
159
+ wrapper = Atoms::Ignored.new(Atoms::Repetition.new(atom, 0, 1))
160
+ (@skip_wrapper_ids ||= {}.compare_by_identity)[wrapper] = true
161
+ wrapper
162
+ end
163
+ end
164
+
165
+ # parsanol-ruby#134: inject an optional match of the declared skip
166
+ # rule before every terminal of every rule. Injected skips are
167
+ # capture-free (a bare Repetition contributes no Named results),
168
+ # optional (ordered-choice/backtracking semantics preserved), and
169
+ # leave first sets unchanged (an optional prefix is epsilon-able).
170
+ def inject_skip_trivia(atom)
171
+ return atom if @document.skip_declaration.nil?
172
+
173
+ case atom
174
+ # rubocop:disable Lint/DuplicateBranch -- pass-through is the
175
+ # deliberate default; identical bodies, different decisions
176
+ when Atoms::Sequence
177
+ kids = atom.parslets.map { |child| inject_skip_trivia(child) }
178
+ out = []
179
+ kids.each do |child|
180
+ out << skip_maybe_atom unless skip_wrapper?(child)
181
+ out << child
182
+ end
183
+ out.length == 1 ? out.first : Atoms::Sequence.new(*out)
184
+ when Atoms::Alternative
185
+ Atoms::Alternative.new(*atom.alternatives.map { |a| inject_skip_trivia(a) })
186
+ when Atoms::Repetition
187
+ if skip_wrapper?(atom)
188
+ atom
189
+ else
190
+ Atoms::Repetition.new(inject_skip_trivia(atom.parslet), atom.min, atom.max,
191
+ atom.result_tag)
192
+ end
193
+ when Atoms::Named
194
+ # Pass-through: trivia must stay OUTSIDE captures. Injecting
195
+ # inside a Named makes the captured span include the skipped
196
+ # bytes (span-join swallows them into the captured value).
197
+ # Inter-token trivia around a capture is injected by the
198
+ # enclosing sequence's interleave.
199
+ atom
200
+ when Atoms::Lookahead
201
+ Atoms::Lookahead.new(inject_skip_trivia(atom.bound_parslet), atom.positive)
202
+ when Atoms::Str, Atoms::Re
203
+ # bare terminal as a whole rule body: leading trivia position
204
+ Atoms::Sequence.new(skip_maybe_atom, atom)
205
+ # pass-through is the deliberate default; identical bodies,
206
+ # different decisions
207
+ else
208
+ atom
209
+ end
210
+ # rubocop:enable Lint/DuplicateBranch
83
211
  end
84
212
 
85
213
  def record_warning(message)
@@ -174,6 +302,10 @@ module Parsanol
174
302
  if !@document.rules.key?(name) && @document.customs.key?(name)
175
303
  return Node.new(:lit, "\u0001", false)
176
304
  end
305
+ if !@document.rules.key?(name) && BUILTIN_CLASSES.key?(name)
306
+ # QoL shorthand stand-in: the analysis passes see the class
307
+ return Node.new(:class, BUILTIN_CLASSES[name])
308
+ end
177
309
 
178
310
  @document.rules[name] || raise(CompileError, "unknown rule #{name.inspect}")
179
311
  end
@@ -193,6 +325,15 @@ module Parsanol
193
325
  Atoms::Str.new(node.a)
194
326
  end
195
327
  when :class then Atoms::Re.new(class_pattern(node.a))
328
+ when :until
329
+ # QoL: (*( !terminal any_char )) — consume everything up to
330
+ # (and excluding) the delimiter.
331
+ any_char = Atoms::Re.new(class_pattern([[0x00, 0x10FFFF]]))
332
+ Atoms::Repetition.new(
333
+ Atoms::Sequence.new(Atoms::Lookahead.new(Atoms::Str.new(node.a), false),
334
+ any_char),
335
+ 0, nil
336
+ )
196
337
  when :seq
197
338
  atoms = node.a.map { |child| build_atom(child) }
198
339
  atoms.length == 1 ? atoms.first : Atoms::Sequence.new(*atoms)
@@ -234,6 +375,10 @@ module Parsanol
234
375
  if @document.customs.key?(node.a)
235
376
  @dynamic = true
236
377
  resolve_custom(node.a)
378
+ elsif !@document.rules.key?(node.a) && BUILTIN_CLASSES.key?(node.a)
379
+ # QoL: character-class shorthands (ABNF core rules) — usable
380
+ # unless the grammar defines its own rule of the same name.
381
+ Atoms::Re.new(class_pattern(BUILTIN_CLASSES[node.a]))
237
382
  else
238
383
  Atoms::Entity.new(node.a) { atom_for(node.a) }
239
384
  end
@@ -472,6 +617,13 @@ module Parsanol
472
617
  "docs" => @document.docs,
473
618
  "source" => @document.source,
474
619
  }
620
+ if @document.skip_declaration
621
+ # parsanol-ruby#134: declared trivia rule. Injection is baked
622
+ # into the compiled trees (plain atom kinds), so consumers
623
+ # without skip support still parse these artifacts correctly;
624
+ # the field records the grammar-level declaration.
625
+ envelope["skip"] = @document.skip_source
626
+ end
475
627
  envelope["customs"] = @document.customs unless @document.customs.empty?
476
628
  envelope["states"] = @document.states unless @document.states.empty?
477
629
  envelope["dynamic"] = true if @dynamic
@@ -9,7 +9,7 @@ module Parsanol
9
9
 
10
10
  Test = Struct.new(:entry, :kind, :input, :expect)
11
11
 
12
- attr_accessor :grammar_name, :version, :source, :uses
12
+ attr_accessor :grammar_name, :version, :source, :uses, :skip_declaration, :skip_source
13
13
  attr_reader :rules, :bindings, :preprocess, :entries, :docs, :tests, :render, :derive,
14
14
  :customs, :states
15
15
  # Entries authored in this file (pre-import); nil-entry tests resolve
@@ -20,7 +20,8 @@ module Parsanol
20
20
 
21
21
  STATE_KEYWORDS = %w[custom state].freeze
22
22
  KEYWORDS = %w[grammar as alt from_table column bindings render derive
23
- preprocess entry table_lookup custom state set switch].freeze
23
+ preprocess entry table_lookup custom state set switch
24
+ skip until].freeze
24
25
 
25
26
  def initialize(text)
26
27
  @tokens = Lexer.new(text).tokens
@@ -161,6 +162,19 @@ module Parsanol
161
162
  skip_newlines
162
163
  next
163
164
  end
165
+ if peek&.type == :ident && peek.value == "skip"
166
+ advance
167
+ punct("=")
168
+ if document.skip_declaration
169
+ raise ParseError, "duplicate skip declaration"
170
+ end
171
+
172
+ node = parse_choice
173
+ document.skip_declaration = validate_skip_node(node)
174
+ document.skip_source = skip_source_string(node)
175
+ skip_newlines
176
+ next
177
+ end
164
178
  if peek&.type == :ident && STATE_KEYWORDS.include?(peek.value)
165
179
  keyword = advance.value
166
180
  slot = ident.value
@@ -188,6 +202,30 @@ module Parsanol
188
202
  punct("}")
189
203
  end
190
204
 
205
+ # The skip declaration may only name rules (a rule reference or an
206
+ # alternation of rule references) — arbitrary expressions would
207
+ # make the injected trivia unanalyzable.
208
+ def skip_source_string(node)
209
+ case node.kind
210
+ when :ref then node.a
211
+ when :alt then node.a.map { |b| skip_source_string(b) }.join(" / ")
212
+ end
213
+ end
214
+
215
+ def validate_skip_node(node)
216
+ case node.kind
217
+ when :ref
218
+ node
219
+ when :alt
220
+ node.a.each { |branch| validate_skip_node(branch) }
221
+ node
222
+ else
223
+ raise ParseError,
224
+ "skip must be a rule reference or an alternation of rule " \
225
+ "references, got #{node.kind.inspect}"
226
+ end
227
+ end
228
+
191
229
  def rule_name
192
230
  token = ident
193
231
  if KEYWORDS.include?(token.value)
@@ -325,6 +363,10 @@ module Parsanol
325
363
  end
326
364
  punct("}")
327
365
  Node.new(:switch, slot, arms)
366
+ when "until"
367
+ # QoL: any-until terminal — (*( !terminal any_char )), the
368
+ # idiomatic "consume everything up to the delimiter" run.
369
+ Node.new(:until, unquote(expect(:str).value))
328
370
  when "alt"
329
371
  ident("from_table")
330
372
  table = unquote(expect(:str).value)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.66"
4
+ VERSION = "1.3.67"
5
5
  end
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.66
4
+ version: 1.3.67
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.