parsanol 1.3.112 → 1.3.113

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: 683289b7d0453a88c2dfb691d54479ebacf6be93cc30f527c4199557bb99b48d
4
- data.tar.gz: '0182a60c9bca2c3c1fcf1f450fed4623aeb915fb3b3e80f3a14ee716c09f3ffd'
3
+ metadata.gz: d9a04b4f0add7d25933bf9ddd041f28cf067e9b2750abb6595915d63cdd4cd3a
4
+ data.tar.gz: 69767361cdcb52ddeb7e0dbd4b05498e72fffe4d64639c4c5d00aad723f7c659
5
5
  SHA512:
6
- metadata.gz: 15607095c23776f36bf34dcf4e53849a739cd373e9a31a766c484331fd998a01b4d66faebd528fb61077d648e58e7e35173e6ec7e3280640e1b2eaee17a884cc
7
- data.tar.gz: 8fc517dc729088dcac71596911ed5a57a8b72c580d246b315a1b8313cb0157ef7ee63b04b20d723583f03ac4699af4810ca1cb61e5c91d9aed1514fe68806a27
6
+ metadata.gz: 8407c77dbca38833e2e2ddf189d601be84ed14f07bbe5e972feb76ce016df3f88c7725951ed20ba74b5403ad83dae25fef8d3f1259c6312e6e4158e709c037ba
7
+ data.tar.gz: 9fe4fd4df2ba548a686226ee50c80c60945dfad0e3719d7b1593fe9678a5a2f464df833940cb87201e86b98243c025d94ab42664b914f29ea3ca2649773c6aff
data/Cargo.lock CHANGED
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
307
307
 
308
308
  [[package]]
309
309
  name = "parsanol"
310
- version = "0.19.6"
310
+ version = "0.19.7"
311
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
312
- checksum = "8ffe7e92d626c65e83d2c53c148a18db5897879858bcd37ebd0f406f5a251036"
312
+ checksum = "5233088cf0cde65b9a6e5e41d558b689becc722ccf156946c8f118049f2d3f5c"
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.19.6"
329
+ version = "0.19.7"
330
330
  source = "registry+https://github.com/rust-lang/crates.io-index"
331
- checksum = "3b94bb50df3fe1a9f66b6d8ee784672c27262e9e609568d5e9d14e1f7d6bec91"
331
+ checksum = "c84688541fbbd11509183b9c53debd924bcdbfae8cf8aca053414ea11a7aa7e9"
332
332
  dependencies = [
333
333
  "proc-macro2",
334
334
  "quote",
@@ -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
@@ -274,10 +274,10 @@ module Parsanol
274
274
  end
275
275
 
276
276
  # 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.
277
+ # literals strip to empty, character classes are whitespace-only;
278
+ # sequence/alternation/repetition compose the verdict; references
279
+ # resolve through the atom cache with a cycle set; anything else
280
+ # (customs, authored captures) disqualifies.
281
281
  def trivia_only?(name, seen)
282
282
  return false unless @document.rules.key?(name)
283
283
 
@@ -288,6 +288,8 @@ module Parsanol
288
288
  case atom
289
289
  when Atoms::Str
290
290
  atom.str.to_s.strip.empty?
291
+ when Atoms::Re
292
+ whitespace_class?(atom.match)
291
293
  when Atoms::Sequence, Atoms::Alternative
292
294
  kids = atom.respond_to?(:alternatives) ? atom.alternatives : atom.parslets
293
295
  kids.all? { |child| trivia_walk(child, seen) }
@@ -304,6 +306,91 @@ module Parsanol
304
306
  end
305
307
  end
306
308
 
309
+ # A class matches only whitespace when every codepoint it admits
310
+ # strips to empty — the same verdict Atoms::Str gets. Compiled
311
+ # classes are the dialect class_pattern emits: bracket bodies of
312
+ # literal chars, \t\n\v\f\r, \xHH, \uXXXX and \u{...}, plus ranges
313
+ # between them. Anything else disqualifies.
314
+ def whitespace_class?(pattern)
315
+ body = pattern.to_s[/\A\[(.*)\]\z/m, 1]
316
+ return false if body.nil? || body.empty? || body.start_with?("^")
317
+
318
+ ranges = class_ranges(body)
319
+ ranges&.all? { |lo, hi| whitespace_range?(lo, hi) }
320
+ end
321
+
322
+ WHITESPACE_CODEPOINTS = [0x00, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x20].freeze
323
+
324
+ def whitespace_range?(low, high)
325
+ return false if low > high
326
+
327
+ union = WHITESPACE_CODEPOINTS.count { |code| code.between?(low, high) }
328
+ union == high - low + 1
329
+ end
330
+
331
+ def class_ranges(body)
332
+ slots = []
333
+ pending_range = false
334
+ index = 0
335
+ while index < body.length
336
+ if body[index] == "-" && !pending_range && !slots.empty?
337
+ pending_range = true
338
+ index += 1
339
+ next
340
+ end
341
+
342
+ code, index = class_token(body, index)
343
+ return nil unless code
344
+
345
+ if pending_range
346
+ previous = slots.pop
347
+ slots << [previous[0], code].minmax
348
+ pending_range = false
349
+ else
350
+ slots << [code, code]
351
+ end
352
+ end
353
+ return nil if pending_range
354
+
355
+ slots
356
+ end
357
+
358
+ def class_token(body, index)
359
+ char = body[index]
360
+ unless char == "\\"
361
+ return [char.ord, index + 1]
362
+ end
363
+
364
+ escaped = body[index + 1]
365
+ return nil if escaped.nil?
366
+
367
+ case escaped
368
+ when "x" then hex_token(body, index + 2, 2)
369
+ when "u"
370
+ if body[index + 2] == "{"
371
+ close = body.index("}", index + 3)
372
+ return nil unless close
373
+
374
+ [body[(index + 3)...close].to_i(16), close + 1]
375
+ else
376
+ hex_token(body, index + 2, 4)
377
+ end
378
+ when "t" then [0x09, index + 2]
379
+ when "n" then [0x0A, index + 2]
380
+ when "v" then [0x0B, index + 2]
381
+ when "f" then [0x0C, index + 2]
382
+ when "r" then [0x0D, index + 2]
383
+ else [escaped.ord, index + 2]
384
+ end
385
+ end
386
+
387
+ def hex_token(body, start, length)
388
+ hex = body[start, length]
389
+ return nil unless hex&.match?(/\A\h{#{length}}\z/)
390
+
391
+ [hex.to_i(16), start + length]
392
+ end
393
+
307
394
  # Whether the atom's flattening surfaces any Named capture:
308
395
  # rule references resolve through (their own wrap pass already
309
396
  # ran — a wrapped ref produces a capture), trivia never
@@ -353,7 +440,10 @@ module Parsanol
353
440
  case atom
354
441
  when Atoms::Named then nil
355
442
  when Atoms::Entity
356
- unless produces_capture?(atom.parslet)
443
+ # Mirror wrap_uncaptured's verdicts: trivia glue and
444
+ # capture-producing refs both stay bare.
445
+ unless trivia_only?(atom.rule_name.to_s, { atom.rule_name.to_s => true }) ||
446
+ produces_capture?(atom.parslet)
357
447
  lines << "rule #{site}: reference to #{atom.rule_name} produces no capture -- " \
358
448
  "modern mode would name it #{atom.rule_name}"
359
449
  end
@@ -1233,13 +1323,11 @@ module Parsanol
1233
1323
  "unicode_tables" => "onigmo (Ruby #{RUBY_VERSION})",
1234
1324
  "compiler_version" => Parsanol::VERSION,
1235
1325
  }
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
1326
+ # ruby#304: the envelope always names the capture edition —
1327
+ # consumers read the tree-shape contract before parsing. The
1328
+ # numeric form is canonical: 1 = the legacy shapes, 2 = fully
1329
+ # named trees + explicit spans.
1330
+ envelope["edition"] = @document.edition == "v2" ? 2 : 1
1243
1331
  if @document.skip_declaration
1244
1332
  # parsanol-ruby#134: declared trivia rule. Injection is baked
1245
1333
  # 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,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
@@ -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.113"
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.112
4
+ version: 1.3.113
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.