parsanol 1.3.112-arm-linux → 1.3.114-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f86341729e0c13dfe000814acffe0a7806dd23c2112c75f0f46720e839d1437e
4
- data.tar.gz: 9880883b997abf73225f2e33aa8ffba669b838b3962bc2554e0177e9d021d666
3
+ metadata.gz: 9ccfe53145a4fbf2af4f7c2cc47071231be0016d8047e9bb0f75386738ff0480
4
+ data.tar.gz: 395d7bccac544d4842b29e5764d1198816ab823e1f9defdc3874009395caaa24
5
5
  SHA512:
6
- metadata.gz: 725b8abcd3649a8e59d773075469c1a6774618510d16bea92961b58aadbb104597fa293aeb599d0b62f04f0f70efd75cfacc57992012b854e27d68f9046075ae
7
- data.tar.gz: daef10d533ff00cc4b7a9206949b48a13f104897c56a10d463ce71e1d7d040bab389f2b6d50a6ea3b1ea06eb5a6c0b278b62d7b9157b1c99f3737cf34d48be83
6
+ metadata.gz: b1436b7af7ea025b302e14fc14970c42252c75f5aa6a2252f1701862ebca604ccc05349a39fdbd77b09fef78c7e633b9a6ff15c3f22e3a36845f49133a84eb7a
7
+ data.tar.gz: 188fe5c41d516ab6f3372ca339d0c313b0460e912e122a9e32ed5f8b948deb743c3a38f406997dddbb97cc9e3a5a7a25587640273eb44c97dde75d518d24ad6b
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/3.2/parsanol_native.so
3
- size: 38874288 bytes
4
- sha256: 754877c02d5009e0d9b1ce188976c6434e47db3b5830bb521a2d7db5d9176110
3
+ size: 39082700 bytes
4
+ sha256: d252dd306fb0dcb4726ed8a1deb826cecfc1ca8bf5528603f50ac20058e0aea9
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/3.3/parsanol_native.so
3
- size: 39059492 bytes
4
- sha256: a46e280499f470142aabcb4d87986248d39e869193ee0121418515cf55a77bf1
3
+ size: 39267920 bytes
4
+ sha256: 4f500faf69c8b85dff0055043e37d08bdb897cabd0bc814ce1a5a7f31db0fad4
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/3.4/parsanol_native.so
3
- size: 39053696 bytes
4
- sha256: 95bd7e4ca67253501ba472ef1f5da717a9cf1bd64fd3eac4e83713ce802f85c8
3
+ size: 39262108 bytes
4
+ sha256: b24c02b2f5d0848c59dba33cf4751107b1799ec8f96c961c3952114e96eb10f7
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/4.0/parsanol_native.so
3
- size: 39063392 bytes
4
- sha256: 5824569712de4619ca2c8122c521cbca91f0e284e3f6b85e54ec2e0a4310f08e
3
+ size: 39271824 bytes
4
+ sha256: 3bc6176aec3d4bb697ce024b174ddeca6a4b50a297bdd84492a277205e46efed
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/native/libparsanol.so
3
- size: 35234412 bytes
4
- sha256: 25dc6c51f9f09389dbf57c462ffec5d2aea36e04b6a9597c58f7eb6520b32e76
3
+ size: 35442920 bytes
4
+ sha256: 0a129844ae8acce2330a6b009be2fc9d918a6f46d53f744429ae02b9bf039aea
@@ -27,8 +27,16 @@ module Parsanol
27
27
  SUPPORTED_SHAPE = "parsanol-tree/v2"
28
28
 
29
29
  # The editions this runtime parses (ruby#357's consolidation):
30
- # v1 = the legacy shapes; v2 = named trees + explicit spans.
31
- SUPPORTED_EDITIONS = %w[v1 v2].freeze
30
+ # 1 = the legacy shapes; 2 = named trees + explicit spans.
31
+ SUPPORTED_EDITIONS = [1, 2].freeze
32
+
33
+ # The header and envelope spellings that have shipped: the quoted
34
+ # v-forms stay loadable so artifacts baked before the numeric
35
+ # surface remain readable.
36
+ EDITION_ALIASES = {
37
+ 1 => 1, "1" => 1, "v1" => 1, "classic" => 1,
38
+ 2 => 2, "2" => 2, "v2" => 2
39
+ }.freeze
32
40
 
33
41
  def initialize(envelope, path, tables_dir, imports_dir: nil)
34
42
  unless envelope["shape"] == SUPPORTED_SHAPE
@@ -42,16 +50,13 @@ module Parsanol
42
50
  bindings = envelope["bindings"]
43
51
  validate_bindings!(bindings) unless bindings.nil?
44
52
  @envelope = envelope
45
- # ruby#353: absent field = the pre-1.3.109 semantics. A level
46
- # above this runtime's is a hard error (the trees would carry
47
- # nodes this engine never emits); a lower one warns — parsing
48
- # works, but frozen tree comparisons will differ.
49
53
  # The declared edition must be one this runtime understands
50
- # (ruby#357's consolidation: one axis, `edition`, values v1/v2).
51
- edition = envelope["edition"] || "v1"
54
+ # (ruby#357's consolidation: one axis, `edition`). Absent field
55
+ # = the pre-1.3.109 artifacts, whose trees are edition 1.
56
+ edition = EDITION_ALIASES[envelope.fetch("edition", 1)]
52
57
  unless SUPPORTED_EDITIONS.include?(edition)
53
58
  raise ArtifactError,
54
- "artifact declares edition #{edition.inspect}; this " \
59
+ "artifact declares edition #{envelope['edition'].inspect}; this " \
55
60
  "runtime supports #{SUPPORTED_EDITIONS.map(&:inspect).join(', ')}"
56
61
  end
57
62
  @path = path
@@ -66,6 +71,13 @@ module Parsanol
66
71
 
67
72
  def version = envelope["version"]
68
73
 
74
+ # The edition the artifact's trees carry: 1 = the legacy shapes,
75
+ # 2 = fully named trees. Legacy artifacts that spell the field
76
+ # "v2" (or omit it) normalize here.
77
+ def edition
78
+ EDITION_ALIASES.fetch(envelope.fetch("edition", 1))
79
+ end
80
+
69
81
  # Freshness (parsanol-ruby#209): what source revision and compiler
70
82
  # produced this artifact. `source_hash` is the SHA-256 of the .parg
71
83
  # text at compile time; nil on artifacts baked before the field
@@ -233,12 +233,24 @@ module Parsanol
233
233
  # compile time — the tree shape consumers read, opt-in via
234
234
  # `mode` in the grammar header. Engines see ordinary
235
235
  # Named atoms; identity rides the differential corpus.
236
- def apply_modern_captures(atom)
236
+ def apply_modern_captures(atom, rule_name)
237
237
  return atom if @document.edition == "v1"
238
+ # A glue rule's own body compiles unwrapped (parsanol-ruby#364):
239
+ # the declaration means the tree never sees this subtree named,
240
+ # inside or out.
241
+ return atom if glue_rule?(rule_name)
238
242
 
239
243
  wrap_uncaptured(atom)
240
244
  end
241
245
 
246
+ # The `glue` declaration's verdict (parsanol-ruby#364): references
247
+ # to declared glue rules never wrap — the trivia exemption's
248
+ # "glue never becomes a capture", author-declared for rules that
249
+ # eat more than whitespace.
250
+ def glue_rule?(name)
251
+ @document.glue_rules.include?(name.to_s)
252
+ end
253
+
242
254
  def wrap_uncaptured(atom)
243
255
  case atom
244
256
  when Atoms::Named
@@ -254,10 +266,11 @@ module Parsanol
254
266
  # capture-producing refs both stay unwrapped; the bodies
255
267
  # agree by design.
256
268
  if trivia_only?(atom.rule_name.to_s, { atom.rule_name.to_s => true }) ||
269
+ glue_rule?(atom.rule_name.to_s) ||
257
270
  produces_capture?(atom.parslet)
258
271
  atom
259
272
  else
260
- Atoms::Named.new(atom, atom.rule_name.to_s)
273
+ Atoms::Named.new(atom, atom.rule_name.to_sym)
261
274
  end
262
275
  # rubocop:enable Lint/DuplicateBranch
263
276
  when Atoms::Sequence, Atoms::Alternative
@@ -274,10 +287,10 @@ module Parsanol
274
287
  end
275
288
 
276
289
  # Whether the named rule's body is provably whitespace-only:
277
- # literals strip to empty; sequence/alternation/repetition
278
- # compose the verdict; references resolve through the atom cache
279
- # with a cycle set; anything else (classes, customs, authored
280
- # captures) disqualifies.
290
+ # literals strip to empty, character classes are whitespace-only;
291
+ # sequence/alternation/repetition compose the verdict; references
292
+ # resolve through the atom cache with a cycle set; anything else
293
+ # (customs, authored captures) disqualifies.
281
294
  def trivia_only?(name, seen)
282
295
  return false unless @document.rules.key?(name)
283
296
 
@@ -288,6 +301,8 @@ module Parsanol
288
301
  case atom
289
302
  when Atoms::Str
290
303
  atom.str.to_s.strip.empty?
304
+ when Atoms::Re
305
+ whitespace_class?(atom.match)
291
306
  when Atoms::Sequence, Atoms::Alternative
292
307
  kids = atom.respond_to?(:alternatives) ? atom.alternatives : atom.parslets
293
308
  kids.all? { |child| trivia_walk(child, seen) }
@@ -304,6 +319,91 @@ module Parsanol
304
319
  end
305
320
  end
306
321
 
322
+ # A class matches only whitespace when every codepoint it admits
323
+ # strips to empty — the same verdict Atoms::Str gets. Compiled
324
+ # classes are the dialect class_pattern emits: bracket bodies of
325
+ # literal chars, \t\n\v\f\r, \xHH, \uXXXX and \u{...}, plus ranges
326
+ # between them. Anything else disqualifies.
327
+ def whitespace_class?(pattern)
328
+ body = pattern.to_s[/\A\[(.*)\]\z/m, 1]
329
+ return false if body.nil? || body.empty? || body.start_with?("^")
330
+
331
+ ranges = class_ranges(body)
332
+ ranges&.all? { |lo, hi| whitespace_range?(lo, hi) }
333
+ end
334
+
335
+ WHITESPACE_CODEPOINTS = [0x00, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x20].freeze
336
+
337
+ def whitespace_range?(low, high)
338
+ return false if low > high
339
+
340
+ union = WHITESPACE_CODEPOINTS.count { |code| code.between?(low, high) }
341
+ union == high - low + 1
342
+ end
343
+
344
+ def class_ranges(body)
345
+ slots = []
346
+ pending_range = false
347
+ index = 0
348
+ while index < body.length
349
+ if body[index] == "-" && !pending_range && !slots.empty?
350
+ pending_range = true
351
+ index += 1
352
+ next
353
+ end
354
+
355
+ code, index = class_token(body, index)
356
+ return nil unless code
357
+
358
+ if pending_range
359
+ previous = slots.pop
360
+ slots << [previous[0], code].minmax
361
+ pending_range = false
362
+ else
363
+ slots << [code, code]
364
+ end
365
+ end
366
+ return nil if pending_range
367
+
368
+ slots
369
+ end
370
+
371
+ def class_token(body, index)
372
+ char = body[index]
373
+ unless char == "\\"
374
+ return [char.ord, index + 1]
375
+ end
376
+
377
+ escaped = body[index + 1]
378
+ return nil if escaped.nil?
379
+
380
+ case escaped
381
+ when "x" then hex_token(body, index + 2, 2)
382
+ when "u"
383
+ if body[index + 2] == "{"
384
+ close = body.index("}", index + 3)
385
+ return nil unless close
386
+
387
+ [body[(index + 3)...close].to_i(16), close + 1]
388
+ else
389
+ hex_token(body, index + 2, 4)
390
+ end
391
+ when "t" then [0x09, index + 2]
392
+ when "n" then [0x0A, index + 2]
393
+ when "v" then [0x0B, index + 2]
394
+ when "f" then [0x0C, index + 2]
395
+ when "r" then [0x0D, index + 2]
396
+ else [escaped.ord, index + 2]
397
+ end
398
+ end
399
+
400
+ def hex_token(body, start, length)
401
+ hex = body[start, length]
402
+ return nil unless hex&.match?(/\A\h{#{length}}\z/)
403
+
404
+ [hex.to_i(16), start + length]
405
+ end
406
+
307
407
  # Whether the atom's flattening surfaces any Named capture:
308
408
  # rule references resolve through (their own wrap pass already
309
409
  # ran — a wrapped ref produces a capture), trivia never
@@ -353,7 +453,11 @@ module Parsanol
353
453
  case atom
354
454
  when Atoms::Named then nil
355
455
  when Atoms::Entity
356
- unless produces_capture?(atom.parslet)
456
+ # Mirror wrap_uncaptured's verdicts: trivia glue, declared
457
+ # glue, and capture-producing refs all stay bare.
458
+ unless trivia_only?(atom.rule_name.to_s, { atom.rule_name.to_s => true }) ||
459
+ glue_rule?(atom.rule_name.to_s) ||
460
+ produces_capture?(atom.parslet)
357
461
  lines << "rule #{site}: reference to #{atom.rule_name} produces no capture -- " \
358
462
  "modern mode would name it #{atom.rule_name}"
359
463
  end
@@ -392,7 +496,7 @@ module Parsanol
392
496
  # atom_for; a mutual-reference grammar must hit the cache,
393
497
  # not rebuild), then the cache takes the wrapped tree.
394
498
  @atom_cache[name] = atom
395
- atom = apply_modern_captures(atom)
499
+ atom = apply_modern_captures(atom, name)
396
500
  @atom_cache[name] = atom
397
501
  atom
398
502
  end
@@ -1233,13 +1337,11 @@ module Parsanol
1233
1337
  "unicode_tables" => "onigmo (Ruby #{RUBY_VERSION})",
1234
1338
  "compiler_version" => Parsanol::VERSION,
1235
1339
  }
1236
- # ruby#304: the envelope always names the capture dialect —
1237
- # consumers read the tree-shape contract before parsing.
1238
- envelope["edition"] = @document.edition
1239
- # ruby#353: the language-semantics level the trees were frozen
1240
- # against (1.3.108's implicit-capture shapes = 1; the
1241
- # explicit end_guard/marker/hyphen nodes = 2). Consumers freeze
1242
- # tree shapes and mappers; the loader asserts the level at
1340
+ # ruby#304: the envelope always names the capture edition —
1341
+ # consumers read the tree-shape contract before parsing. The
1342
+ # numeric form is canonical: 1 = the legacy shapes, 2 = fully
1343
+ # named trees + explicit spans.
1344
+ envelope["edition"] = @document.edition == "v2" ? 2 : 1
1243
1345
  if @document.skip_declaration
1244
1346
  # parsanol-ruby#134: declared trivia rule. Injection is baked
1245
1347
  # into the compiled trees (plain atom kinds), so consumers
@@ -21,7 +21,7 @@ module Parsanol
21
21
 
22
22
  def diff(old_envelope, new_envelope)
23
23
  findings = []
24
- findings.concat(diff_mode(old_envelope, new_envelope))
24
+ findings.concat(diff_edition(old_envelope, new_envelope))
25
25
  findings.concat(diff_entries(old_envelope, new_envelope))
26
26
  findings.concat(diff_bindings(old_envelope, new_envelope))
27
27
  findings.concat(diff_tests(old_envelope, new_envelope))
@@ -168,18 +168,25 @@ module Parsanol
168
168
  branches.uniq.sort
169
169
  end
170
170
 
171
- # -- mode ---------------------------------------------------------
171
+ # -- edition ------------------------------------------------------
172
172
 
173
- def diff_mode(old_env, new_env)
174
- old_mode = old_env["mode"]
175
- new_mode = new_env["mode"]
176
- return [] if old_mode == new_mode
173
+ def diff_edition(old_env, new_env)
174
+ old_edition = edition_level(old_env)
175
+ new_edition = edition_level(new_env)
176
+ return [] if old_edition == new_edition
177
177
 
178
- [Finding.new(:major, :mode_changed,
179
- "mode changed #{old_mode.inspect} -> #{new_mode.inspect} " \
178
+ [Finding.new(:major, :edition_changed,
179
+ "edition changed #{old_edition.inspect} -> #{new_edition.inspect} " \
180
180
  "(every uncaptured node's tree shape changes)")]
181
181
  end
182
182
 
183
+ # Artifacts baked before the numeric surface spell the edition
184
+ # "v1"/"v2"; they mean the same levels, not a change.
185
+ def edition_level(envelope)
186
+ declared = envelope.fetch("edition", 1)
187
+ Artifact::EDITION_ALIASES.fetch(declared, declared)
188
+ end
189
+
183
190
  # -- bindings -----------------------------------------------------
184
191
 
185
192
  def diff_bindings(old_env, new_env)
@@ -18,7 +18,7 @@ module Parsanol
18
18
  Test = Struct.new(:entry, :kind, :input, :expect)
19
19
 
20
20
  attr_accessor :grammar_name, :version, :edition, :source, :uses, :skip_declaration, :skip_source,
21
- :atomic_rules, :skip_capture_key, :brace_rules
21
+ :atomic_rules, :skip_capture_key, :brace_rules, :glue_rules
22
22
  # R2 declared resync: rule name -> skip pattern (the span matched
23
23
  # at a recovery point is consumed as annotated error text).
24
24
  attr_reader :resyncs
@@ -60,6 +60,10 @@ module Parsanol
60
60
  # contiguously).
61
61
  @atomic_rules = []
62
62
  @brace_rules = []
63
+ # Rules declared `glue <name> ...` (parsanol-ruby#364): the
64
+ # edition-2 wrap never names them — references stay bare and
65
+ # the rule's own body compiles unwrapped.
66
+ @glue_rules = []
63
67
  @authored_rules = nil
64
68
  # R2 declared resync: rule -> skip pattern. Recovery consumes
65
69
  # each pattern match at a failure point as annotated error text
@@ -18,6 +18,14 @@ module Parsanol
18
18
 
19
19
  IN_BLOCK_SECTIONS = %w[entry bindings preprocess test].freeze
20
20
 
21
+ # The header's edition spellings: the bare numbers are canonical
22
+ # (Rust's Cargo.toml model); the quoted forms stay accepted as
23
+ # aliases for grammars written before the numeric surface.
24
+ EDITIONS = {
25
+ "1" => "v1", "v1" => "v1", "classic" => "v1",
26
+ "2" => "v2", "v2" => "v2"
27
+ }.freeze
28
+
21
29
  STATE_KEYWORDS = %w[custom state].freeze
22
30
  KEYWORDS = %w[grammar as alt from_table column bindings render derive
23
31
  preprocess entry table_lookup custom state set switch
@@ -227,6 +235,23 @@ module Parsanol
227
235
 
228
236
  def punct(char) = expect(:punct, char)
229
237
 
238
+ def edition_value
239
+ token = advance
240
+ raw = case token&.type
241
+ when :num then token.value
242
+ when :str then unquote(token.value)
243
+ else
244
+ raise ParseError,
245
+ "edition must be a number (1 or 2) or a quoted alias " \
246
+ "(\"v1\", \"v2\", \"classic\")"
247
+ end
248
+ EDITIONS.fetch(raw) do
249
+ raise ParseError,
250
+ "unknown edition #{raw.inspect} -- supported editions are " \
251
+ "1 (default) and 2 (named trees)"
252
+ end
253
+ end
254
+
230
255
  def parse_grammar(document)
231
256
  document.grammar_name = ident.value
232
257
  ident("version")
@@ -234,19 +259,14 @@ module Parsanol
234
259
  # `edition` — the language edition (ruby#304, ruby#357's
235
260
  # consolidation; Rust's Cargo.toml `edition` is the model):
236
261
  # the author declares, per grammar, which tree semantics the
237
- # grammar means. THE DEFAULT IS "v1" (the legacy shapes — the
238
- # owner's 2026-10-28 reversal). "v2" = fully named trees +
262
+ # grammar means. THE DEFAULT IS 1 (the legacy shapes — the
263
+ # owner's 2026-10-28 reversal). 2 = fully named trees +
239
264
  # explicit spans. `mode` remains accepted as a deprecated
240
265
  # alias for the keyword. The edition is canonicalized here —
241
266
  # everything downstream only ever sees "v1"/"v2".
242
267
  if peek&.type == :ident && %w[edition mode].include?(peek.value)
243
- keyword = advance.value
244
- edition = unquote(expect(:str).value)
245
- unless %w[v2 v1 classic].include?(edition)
246
- raise ParseError,
247
- "unknown edition #{edition.inspect} -- supported editions are \"v1\" (default) and \"v2\" (named trees)"
248
- end
249
- document.edition = edition == "v2" ? "v2" : "v1"
268
+ advance
269
+ document.edition = edition_value
250
270
  end
251
271
  punct("{")
252
272
  skip_newlines
@@ -318,6 +338,24 @@ module Parsanol
318
338
  skip_newlines
319
339
  next
320
340
  end
341
+ # `glue <rule> [<rule>...]` (parsanol-ruby#364): names rules
342
+ # whose references edition 2 never wraps — glue the tree must
343
+ # not see. Same-line names accumulate; the declaration may
344
+ # appear any number of times.
345
+ if peek&.type == :ident && peek.value == "glue"
346
+ advance
347
+ names = []
348
+ while peek&.type == :ident
349
+ names << ident.value
350
+ end
351
+ if names.empty?
352
+ raise ParseError, "glue must name at least one rule"
353
+ end
354
+
355
+ document.glue_rules.concat(names)
356
+ skip_newlines
357
+ next
358
+ end
321
359
  # `atomic <rule> = ...` suppresses skip-trivia injection inside
322
360
  # the rule body: token-shaped rules match their input
323
361
  # contiguously instead of spanning trivia between iterations.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.112"
4
+ VERSION = "1.3.114"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parsanol
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.112
4
+ version: 1.3.114
5
5
  platform: arm-linux
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-28 00:00:00.000000000 Z
11
+ date: 2026-10-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A small Ruby library for constructing parsers in the PEG (Parsing Expression
14
14
  Grammar) fashion. Parsanol provides Parslet-compatible API with additional features