parsanol 1.3.113-arm-linux → 1.3.115-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: 59bf45f5736835d38547fff0ff9d13b7ad23825119796444a80dce968400f4fd
4
- data.tar.gz: 8db9c700e580e573cc7756ee0c3e1db29569485786b2f09f5690bdafdbadfa16
3
+ metadata.gz: 87e094abf1d96459b9558d746f9a436b32e0b3ac58d6529b565159a771e1a9e9
4
+ data.tar.gz: a4331601780b6d3681b45ed55dfbdbe94c227d59cff2c743d3804b5ad2a263bf
5
5
  SHA512:
6
- metadata.gz: 4561a05b21b1632884400ee4ac534d7b1a530de59d504fe15bc8522b481083a63a37c8c26f1c7eec8ee6dce32eb0c7ff8525e5eaf600a135e21cf6ddd0a830de
7
- data.tar.gz: f7b545fc61370ddbafeca737ed5152e98d6f0cc79249a8235a7f550814c896c588dfc01b10bc106f02b4065da6e597199061012d9c9e58b6b75396612ee7a669
6
+ metadata.gz: 491dd96953046d5f729a5bbb403cdb4ce0ce7d1db7365b14fbd395145067e26e1a43968dae4d31176f6e77dc19978039a6297363e27ea63dfbd225370b42430d
7
+ data.tar.gz: 01e15a6809c13dde391770ba64eac2edca3c794df440278e8099832696d212f6c8446d7eded2c03d443879c11b7877314a1178cf1b328c5c6f0da576aaf3edec
data/README.adoc CHANGED
@@ -31,6 +31,17 @@ While maintaining full API compatibility with Parslet, Parsanol features a compl
31
31
  * <<debug-tools,Debug Tools>> - Tracing and grammar visualization
32
32
  * <<benchmarking,Benchmarking>> - Built-in performance testing tools
33
33
 
34
+ == Releases
35
+
36
+ parsanol is one product on **four runtimes**: **rs** (crates.io +
37
+ `@parsanol/wasm` + release binaries), **ruby** (rubygems), **python**
38
+ (PyPI, `parsanol`), and **typescript** (npm, `parsanol` — the
39
+ wasm-backed runtime). **Every engine update releases all four
40
+ together**; a release that ships some runtimes and not the rest is
41
+ incomplete. If the runtime you use lags the latest grammar-language
42
+ features, check the other registries' changelogs and the release train
43
+ on the https://github.com/parsanol/parsanol-ruby/actions[Release workflow].
44
+
34
45
  == Installation
35
46
 
36
47
  Add this line to your application's Gemfile:
@@ -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: 38879708 bytes
4
- sha256: cfda9148339117f0a402c721cc6fe0c8b6eb9a9f1e6379349535800678d12984
3
+ size: 39170404 bytes
4
+ sha256: bbab9454dfa91df09fdcc00143cf18ff89d277657f975d13959c01255f7c5707
@@ -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: 39064908 bytes
4
- sha256: 676a7d260f73301517018e397e50405a74af0a13eceeac0edc47c04c3d4759ba
3
+ size: 39355636 bytes
4
+ sha256: 808eb780f0317adb4f98e7323b90e7d99e55d729a8d961b1b63fcf4660ab4562
@@ -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: 39059120 bytes
4
- sha256: 5deb0995f7b0602d389163621434f6631b4a65d284fa9e78876b19a3842ed94d
3
+ size: 39349796 bytes
4
+ sha256: bbc22c4d658fc4a0a9a72344e5765a44ec9985e0ef1373704cc31014565e711d
@@ -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: 39068812 bytes
4
- sha256: 991d4aa597d6457f3571a791b3cc53a733d9db6caebfab09c4618efc7f78f4da
3
+ size: 39359512 bytes
4
+ sha256: 017f6f3c4909e39c878a6f28f562d577a8b02442de38a0ac9760b48873871426
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/parsanol/native/libparsanol.so
3
- size: 35239956 bytes
4
- sha256: 70ffc11f9a321d8162cb81609cd9b43529c45b7f22095d1ddfa86bc94329b1b6
3
+ size: 35531016 bytes
4
+ sha256: 968ae4d54093a740fd7ce2e59fe10ab73dd853e23a852e5d22e292168a3f9c5a
@@ -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
@@ -440,9 +453,10 @@ module Parsanol
440
453
  case atom
441
454
  when Atoms::Named then nil
442
455
  when Atoms::Entity
443
- # Mirror wrap_uncaptured's verdicts: trivia glue and
444
- # capture-producing refs both stay bare.
456
+ # Mirror wrap_uncaptured's verdicts: trivia glue, declared
457
+ # glue, and capture-producing refs all stay bare.
445
458
  unless trivia_only?(atom.rule_name.to_s, { atom.rule_name.to_s => true }) ||
459
+ glue_rule?(atom.rule_name.to_s) ||
446
460
  produces_capture?(atom.parslet)
447
461
  lines << "rule #{site}: reference to #{atom.rule_name} produces no capture -- " \
448
462
  "modern mode would name it #{atom.rule_name}"
@@ -482,7 +496,7 @@ module Parsanol
482
496
  # atom_for; a mutual-reference grammar must hit the cache,
483
497
  # not rebuild), then the cache takes the wrapped tree.
484
498
  @atom_cache[name] = atom
485
- atom = apply_modern_captures(atom)
499
+ atom = apply_modern_captures(atom, name)
486
500
  @atom_cache[name] = atom
487
501
  atom
488
502
  end
@@ -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
@@ -20,9 +20,24 @@ module Parsanol
20
20
  ENTRY_LINE = /\Aentry\s+(\w+)\s*:\s*(\w+)/
21
21
  USE_LINE = /\Ause\s+(\w+)/
22
22
  GRAMMAR_LINE = /\Agrammar\s+(\w+)\s+version\s+"([^"]+)"/
23
+ # The capture-edition tail: the canonical numeric spelling or the
24
+ # quoted aliases (ruby#361); `mode` stays the deprecated keyword.
25
+ EDITION_TAIL = /
26
+ \s+(?:edition\s+(?:"(?<quoted>[^"]+)"|(?<number>\d+))|mode\s+"(?<mode>[^"]+)")
27
+ /x
28
+ GLUE_LINE = /\Aglue\s+([a-z_][a-z0-9_]*(?:\s+[a-z_][a-z0-9_]*)*)\s*\z/
23
29
 
24
30
  module_function
25
31
 
32
+ # Brace depth for the line-oriented collection: braces inside
33
+ # string literals are grammar data — test pins quote whole
34
+ # language forms, including deliberately malformed ones whose
35
+ # unbalanced braces would push the section depth off (ruby#369).
36
+ def brace_delta(line)
37
+ bare = line.gsub(/"(?:[^"\\]|\\.)*"/, "")
38
+ bare.scan("{").count - bare.scan("}").count
39
+ end
40
+
26
41
  def parse(source)
27
42
  SelfHost.validate(source) unless SelfHost.available? && !SelfHost.valid?(source)
28
43
 
@@ -43,7 +58,17 @@ module Parsanol
43
58
  end
44
59
  next if stripped.empty? || stripped.start_with?("#")
45
60
 
46
- if state[:section]
61
+ if state[:rule_depth]&.positive?
62
+ # A brace-form rule body spans lines (ruby#311): keep
63
+ # appending until its braces balance.
64
+ state[:rule_lines][state[:rule_depth_name]] << line
65
+ state[:rule_depth] += brace_delta(line)
66
+ if state[:rule_depth].zero?
67
+ merge_rule(document, state[:rule_lines][state[:rule_depth_name]])
68
+ state[:rule_depth] = nil
69
+ state[:rule_depth_name] = nil
70
+ end
71
+ elsif state[:section]
47
72
  advance_section(document, state, line, deferred, preprocess_texts)
48
73
  else
49
74
  route_top_level(document, stripped, line, state,
@@ -59,7 +84,7 @@ module Parsanol
59
84
  # Consumes one line inside a deferred/closed-by-brace section.
60
85
  def advance_section(document, state, line, deferred, preprocess_texts)
61
86
  section = state[:section]
62
- state[:depth] += line.scan("{").count - line.scan("}").count
87
+ state[:depth] += brace_delta(line)
63
88
  if state[:depth].positive?
64
89
  preprocess_texts.last << line if section == "preprocess"
65
90
  state[:section_lines] << line
@@ -91,9 +116,19 @@ module Parsanol
91
116
  when USE_LINE
92
117
  document.uses << Regexp.last_match(1)
93
118
  doc_comments = []
119
+ when GLUE_LINE
120
+ document.glue_rules.concat(Regexp.last_match(1).split)
121
+ doc_comments = []
94
122
  when GRAMMAR_LINE
95
123
  document.grammar_name = Regexp.last_match(1)
96
124
  document.version = Regexp.last_match(2)
125
+ tail = stripped.match(EDITION_TAIL)
126
+ document.edition = if tail.nil?
127
+ "v1"
128
+ else
129
+ level = tail[:number] || tail[:quoted] || tail[:mode]
130
+ ["2", "v2"].include?(level) ? "v2" : "v1"
131
+ end
97
132
  doc_comments = []
98
133
  when NAMED_SECTION
99
134
  state[:section] = Regexp.last_match(1)
@@ -128,7 +163,15 @@ module Parsanol
128
163
  document.docs[rule_name] = doc_comments.join("\n") unless doc_comments.empty?
129
164
  doc_comments = []
130
165
  state[:rule_lines][rule_name] = line
131
- merge_rule(document, line)
166
+ depth = brace_delta(line)
167
+ if depth.positive?
168
+ # Brace-form body opens (ruby#311): the rule completes on a
169
+ # later line; the continuation loop appends and merges then.
170
+ state[:rule_depth] = depth
171
+ state[:rule_depth_name] = rule_name
172
+ else
173
+ merge_rule(document, line)
174
+ end
132
175
  else
133
176
  doc_comments = []
134
177
  end
@@ -338,6 +338,24 @@ module Parsanol
338
338
  skip_newlines
339
339
  next
340
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
341
359
  # `atomic <rule> = ...` suppresses skip-trivia injection inside
342
360
  # the rule body: token-shaped rules match their input
343
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.113"
4
+ VERSION = "1.3.115"
5
5
  end
data/lib/parsanol/vm.rb CHANGED
@@ -452,6 +452,11 @@ module Parsanol
452
452
  @subs = {} # [obj_id, flag] => pc or :pending
453
453
  @pending = {} # [obj_id, flag] => [[call_idx, body], ...]
454
454
  @backtracking_prone = false
455
+ # Grammar rules form a shared graph. Re-analyzing every reference
456
+ # repeats whole alternative trees during inlining. These caches
457
+ # live only for this compiler and retain the depth limit's meaning.
458
+ @first_byte_tables = {}.compare_by_identity
459
+ @nullability = {}.compare_by_identity
455
460
  end
456
461
 
457
462
  attr_reader :ops, :backtracking_prone
@@ -484,6 +489,13 @@ module Parsanol
484
489
  def first_byte_table(atom, depth = 0)
485
490
  return nil if depth > 10
486
491
 
492
+ cached = (@first_byte_tables[atom] ||= {})
493
+ return cached[depth] if cached.key?(depth)
494
+
495
+ cached[depth] = compute_first_byte_table(atom, depth)&.freeze
496
+ end
497
+
498
+ def compute_first_byte_table(atom, depth)
487
499
  case atom
488
500
  when Parsanol::Atoms::Str
489
501
  b = atom.str.getbyte(0)
@@ -508,7 +520,7 @@ module Parsanol
508
520
  i += 1
509
521
  end
510
522
  else
511
- t = ct
523
+ t = ct.dup
512
524
  end
513
525
  unless nullable?(child, depth + 1)
514
526
  nullable_all = false
@@ -529,7 +541,7 @@ module Parsanol
529
541
  i += 1
530
542
  end
531
543
  else
532
- t = bt
544
+ t = bt.dup
533
545
  end
534
546
  end
535
547
  t
@@ -572,6 +584,13 @@ module Parsanol
572
584
  def nullable?(atom, depth = 0)
573
585
  return false if depth > 10
574
586
 
587
+ cached = (@nullability[atom] ||= {})
588
+ return cached[depth] if cached.key?(depth)
589
+
590
+ cached[depth] = compute_nullable(atom, depth)
591
+ end
592
+
593
+ def compute_nullable(atom, depth)
575
594
  case atom
576
595
  when Parsanol::Atoms::Str then atom.str.bytesize.zero?
577
596
  when Parsanol::Atoms::Re then false
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.113
4
+ version: 1.3.115
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