parsanol 1.3.91-arm-linux → 1.3.93-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: 35f9007af2a376a99643f989a6b302702da5b02f8f0b7afc6f85e03f3f0b5791
4
- data.tar.gz: 0ec6ceb1d8a6b2b67f81164caf3e54c724544e94b67a1ad2e6405822bfc459a5
3
+ metadata.gz: 38e6903eee4c564df8586705d284a07671cbf0935ed4c7641e8b44015c4c510e
4
+ data.tar.gz: c6f1c81c282193fd596386c717e756aa35d87ffd22d5c3c1554ba3b09d4e0e02
5
5
  SHA512:
6
- metadata.gz: dd6700a33f24fdb535e1949998bce420617548d8ff74f5d13d0479729ecef4cbf4287292f2f8861836cb9256b13c48d11302c85157fd0bbcfa3011e4b76547b7
7
- data.tar.gz: b7d1cb42710b38820fa27d4b8c56ea6e47819dea518707c33460e6a92ba167f18e876ff1bf738b97eca56ee11332dd599cb8fc05ee30908bc1cfea620743f231
6
+ metadata.gz: 94c5b1eb30a297566ca23877a53b1d63be7cff2fa04a92084fb1e9ff0102b238723fba755469daadb5d38b2113638cf0a02697f06a65671d04e631b2bd8647bf
7
+ data.tar.gz: ce29e977d953b08817a2f425f86090d4272cde2fc41e29d0d68b90571765e01951cdd3b90242671d0e9de018bc36f3aa8e52fc5359e5578a7b0ce8bd14554cab
@@ -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: 34005012 bytes
4
- sha256: a8d9bf7504fcf0ac72af4bac22ef380f91ffb941c38d27badd31a38aae2cdd0c
3
+ size: 34333284 bytes
4
+ sha256: 46c18a5ba0af8926c012eb98c6417dfca64931a837629b317fb7c3cc430b9085
@@ -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: 34190272 bytes
4
- sha256: 8ae6bfd6ecf22108657162245248e494a911493c8ab4ce1feccf520fc68254e0
3
+ size: 34518452 bytes
4
+ sha256: 0752c79e340c4c722b106443ff1faf6ff6d6d7c5800570906d1c548585e06a58
@@ -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: 34184432 bytes
4
- sha256: 2403ab2731c77acff92e87d5bfd55be82eecbeeb91349f0aef2a159508f53bf7
3
+ size: 34512620 bytes
4
+ sha256: 9b0744875d02d5c1957b82cdf9249129ce62214696606b84886609243342e23f
@@ -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: 34194168 bytes
4
- sha256: 897db94d6190f5fde15254a05fabe8dde3043951497c0b166ba168a7ca4a24ad
3
+ size: 34522276 bytes
4
+ sha256: 834bad07a21ef4ccaeb47fbfaf85c567866142fb141f6d93205d5d9958e0c2f2
Binary file
@@ -167,59 +167,58 @@ module Parsanol
167
167
  end
168
168
 
169
169
  def grammar_structure_hash(atom)
170
- Digest::MD5.hexdigest(atom_structure(atom).to_s)
170
+ Digest::MD5.hexdigest(append_atom_structure(atom))
171
171
  end
172
172
 
173
- def atom_structure(atom, visited = {})
174
- # Cycle detection - return a placeholder if we've seen this atom before
175
- obj_id = atom.object_id
176
- if visited[obj_id]
177
- return [:cycle, atom.class.name]
173
+ def append_atom_structure(atom, visited = {}.compare_by_identity, output = +"")
174
+ if visited[atom]
175
+ return output << "[:cycle, " << atom.class.name.inspect << "]"
178
176
  end
179
177
 
180
- visited[obj_id] = true
181
-
178
+ visited[atom] = true
182
179
  case atom
183
- when ::Parsanol::Atoms::Entity
184
- # Recursively resolve entity to get actual structure for hash
185
- atom_structure(atom.parslet, visited)
186
- when ::Parsanol::Atoms::Str
187
- [:str, atom.str]
188
- when ::Parsanol::Atoms::Re
189
- [:re, atom.match]
190
- when ::Parsanol::Atoms::Sequence
191
- [:seq, atom.parslets.map { |p| atom_structure(p, visited) }]
192
- when ::Parsanol::Atoms::Alternative
193
- [:alt, atom.alternatives.map { |p| atom_structure(p, visited) }]
194
- when ::Parsanol::Atoms::Repetition
195
- [:rep, atom.min, atom.max, atom_structure(atom.parslet, visited)]
196
- when ::Parsanol::Atoms::Named
197
- [:named, atom.name.to_s, atom_structure(atom.parslet, visited)]
198
- when ::Parsanol::Atoms::Lookahead
199
- [:lookahead, atom.positive,
200
- atom_structure(atom.bound_parslet, visited)]
201
- when ::Parsanol::Atoms::Constant
202
- # The value differentiates otherwise-identical constant
203
- # atoms — without it every Constant hashes alike and the
204
- # grammar cache serves the first wire forever.
205
- [:constant, atom.value.inspect]
206
- when ::Parsanol::Atoms::Lookbehind
207
- [:lookbehind, atom.count, atom.pattern, atom.positive]
208
- # Subclass first: TriviaCapture < Trivia < Ignored (the same
209
- # dispatch discipline as the serializer's).
210
- when ::Parsanol::Atoms::TriviaCapture
211
- # The capturer table differentiates capture-carrying
212
- # grammars from plain-trivia ones — without it the grammar
213
- # cache serves one variant's wire to every other and the
214
- # wrong marker table reaches the engine (parsanol-ruby#180).
215
- [:trivia_capture, atom.capturers.to_a.sort,
216
- atom_structure(atom.wrapped_atom, visited)]
217
- when ::Parsanol::Atoms::Trivia
218
- [:trivia, atom_structure(atom.wrapped_atom, visited)]
219
- when ::Parsanol::Atoms::Ignored
220
- [:ignored, atom_structure(atom.wrapped_atom, visited)]
180
+ when Parsanol::Atoms::Entity
181
+ append_atom_structure(atom.parslet, visited, output)
182
+ when Parsanol::Atoms::Str
183
+ output << "[:str, " << atom.str.inspect << "]"
184
+ when Parsanol::Atoms::Re
185
+ output << "[:re, " << atom.match.inspect << "]"
186
+ when Parsanol::Atoms::Sequence, Parsanol::Atoms::Alternative
187
+ children = atom.is_a?(Parsanol::Atoms::Sequence) ? atom.parslets : atom.alternatives
188
+ output << (atom.is_a?(Parsanol::Atoms::Sequence) ? "[:seq, [" : "[:alt, [")
189
+ index = 0
190
+ while index < children.length
191
+ output << ", " unless index.zero?
192
+ append_atom_structure(children[index], visited, output)
193
+ index += 1
194
+ end
195
+ output << "]]"
196
+ when Parsanol::Atoms::Repetition
197
+ output << "[:rep, " << atom.min.inspect << ", " << atom.max.inspect << ", "
198
+ append_atom_structure(atom.parslet, visited, output) << "]"
199
+ when Parsanol::Atoms::Named
200
+ output << "[:named, " << atom.name.to_s.inspect << ", "
201
+ append_atom_structure(atom.parslet, visited, output) << "]"
202
+ when Parsanol::Atoms::Lookahead
203
+ output << "[:lookahead, " << atom.positive.inspect << ", "
204
+ append_atom_structure(atom.bound_parslet, visited, output) << "]"
205
+ when Parsanol::Atoms::Constant
206
+ # The inspected constant remains a string inside the fingerprint.
207
+ output << "[:constant, " << atom.value.inspect.inspect << "]"
208
+ when Parsanol::Atoms::Lookbehind
209
+ output << "[:lookbehind, " << atom.count.inspect << ", " << atom.pattern.inspect << ", " << atom.positive.inspect << "]"
210
+ # Capturing trivia must precede its Trivia and Ignored parents.
211
+ when Parsanol::Atoms::TriviaCapture
212
+ output << "[:trivia_capture, " << atom.capturers.to_a.sort.inspect << ", "
213
+ append_atom_structure(atom.wrapped_atom, visited, output) << "]"
214
+ when Parsanol::Atoms::Trivia
215
+ output << "[:trivia, "
216
+ append_atom_structure(atom.wrapped_atom, visited, output) << "]"
217
+ when Parsanol::Atoms::Ignored
218
+ output << "[:ignored, "
219
+ append_atom_structure(atom.wrapped_atom, visited, output) << "]"
221
220
  else
222
- [:unknown, atom.class.name]
221
+ output << "[:unknown, " << atom.class.name.inspect << "]"
223
222
  end
224
223
  end
225
224
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
3
4
  require "json"
4
5
  require "yaml"
5
6
 
@@ -11,20 +12,21 @@ module Parsanol
11
12
  # consume; the embedded PARG source lets a Ruby runtime recompile the atom
12
13
  # tree locally without any other dependency.
13
14
  class Artifact
14
- attr_reader :envelope, :path, :tables_dir
15
+ attr_reader :envelope, :path, :tables_dir, :imports_dir
15
16
 
16
- def self.load(path, tables_dir: nil)
17
+ def self.load(path, tables_dir: nil, imports_dir: nil)
17
18
  envelope = JSON.parse(PARG.read_utf8(path))
18
- new(envelope, path, tables_dir || File.dirname(path))
19
+ new(envelope, path, tables_dir || File.dirname(path),
20
+ imports_dir: imports_dir)
19
21
  end
20
22
 
21
- def self.from_json(text, tables_dir: nil)
22
- new(JSON.parse(text), nil, tables_dir)
23
+ def self.from_json(text, tables_dir: nil, imports_dir: nil)
24
+ new(JSON.parse(text), nil, tables_dir, imports_dir: imports_dir)
23
25
  end
24
26
 
25
27
  SUPPORTED_SHAPE = "parsanol-tree/v2"
26
28
 
27
- def initialize(envelope, path, tables_dir)
29
+ def initialize(envelope, path, tables_dir, imports_dir: nil)
28
30
  unless envelope["shape"] == SUPPORTED_SHAPE
29
31
  raise ArtifactError,
30
32
  "unsupported artifact shape #{envelope['shape'].inspect} " \
@@ -38,12 +40,30 @@ module Parsanol
38
40
  @envelope = envelope
39
41
  @path = path
40
42
  @tables_dir = tables_dir
43
+ # parsanol-ruby#208: `use`-importing grammars need the imported
44
+ # sources when the pure-Ruby path recompiles the embedded source;
45
+ # in-memory artifacts (path nil) pass the directory explicitly.
46
+ @imports_dir = imports_dir
41
47
  verify_checksum!
42
48
  @compiler = nil
43
49
  end
44
50
 
45
51
  def version = envelope["version"]
46
52
 
53
+ # Freshness (parsanol-ruby#209): what source revision and compiler
54
+ # produced this artifact. `source_hash` is the SHA-256 of the .parg
55
+ # text at compile time; nil on artifacts baked before the field
56
+ # existed, so `fresh_against?` answers false rather than guessing.
57
+ def source_hash = envelope["source_hash"]
58
+
59
+ def compiler_version = envelope["compiler_version"]
60
+
61
+ def fresh_against?(source_text)
62
+ return false if source_hash.nil?
63
+
64
+ source_hash == Digest::SHA256.hexdigest(source_text.to_s)
65
+ end
66
+
47
67
  def entries = envelope["entries"].keys
48
68
 
49
69
  def entry(name)
@@ -137,16 +157,33 @@ module Parsanol
137
157
  hash = test.is_a?(Document::Test) ? test_to_hash(test) : test
138
158
  entry_name = hash["entry"] || entries.first
139
159
  begin
140
- bound = parse_and_bind(entry_name, hash["input"], mode: mode)
141
- if hash["kind"] == "reject"
160
+ shape = parse(entry_name, hash["input"], mode: mode)
161
+ case hash["kind"]
162
+ when "reject"
142
163
  "test #{hash['input'].inspect}: expected the input to be rejected"
143
- elsif hash["kind"] == "example"
164
+ when "example"
165
+ bound = apply_bindings(entry_name, shape)
144
166
  expect = hash["expect"].to_h { |key, value| [key.to_sym, value] }
145
- mismatched = expect.reject { |key, value| bound.key?(key) && bound[key] == value }
167
+ mismatched = expect.reject do |key, value|
168
+ bound.key?(key) && Bindings.canonical(bound[key]) == Bindings.canonical(value)
169
+ end
146
170
  next if mismatched.empty?
147
171
 
148
172
  "test #{hash['input'].inspect}: expected captures " \
149
173
  "#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
174
+ when "capture"
175
+ captures = Bindings.collect_captures(shape)
176
+ mismatched = hash["expect"].reject do |key, value|
177
+ actual = captures[key.to_sym]&.first
178
+ !actual.nil? && Bindings.canonical(actual) == Bindings.canonical(value)
179
+ end
180
+ next if mismatched.empty?
181
+
182
+ actuals = mismatched.keys.to_h do |key|
183
+ [key, Bindings.canonical(captures[key.to_sym]&.first)]
184
+ end
185
+ "test #{hash['input'].inspect}: expected captures " \
186
+ "#{mismatched.transform_values(&:inspect).inspect}, got #{actuals.inspect}"
150
187
  end
151
188
  rescue Parsanol::ParseFailed
152
189
  unless hash["kind"] == "reject"
@@ -263,8 +300,10 @@ module Parsanol
263
300
  # without this the pure-Ruby path fails on artifacts whose
264
301
  # native atom graphs inline everything. The imported sources
265
302
  # sit beside the artifact (vendors ship them together).
266
- imports_dir = path && File.directory?(File.dirname(path)) ? File.dirname(path) : nil
267
- Imports.merge!(document, [imports_dir].compact) if imports_dir
303
+ dirs = [path ? File.dirname(path) : nil, @imports_dir]
304
+ .compact
305
+ .select { |dir| File.directory?(dir) }
306
+ Imports.merge!(document, dirs) unless dirs.empty?
268
307
  Compiler.new(document, @tables_dir)
269
308
  end
270
309
  end
@@ -23,6 +23,19 @@ module Parsanol
23
23
  out
24
24
  end
25
25
 
26
+ # Canonical test-value form: one equality surface for capture and
27
+ # example expectations on both sides of the comparison (Slice vs
28
+ # source literal was comparing heterogeneous types). Terminals
29
+ # reduce to their text; structure recurses; keys stringify.
30
+ def canonical(value)
31
+ case value
32
+ when Hash then value.to_h { |key, item| [key.to_s, canonical(item)] }
33
+ when Array then value.map { |item| canonical(item) }
34
+ when nil, true, false, Integer then value
35
+ else String(value)
36
+ end
37
+ end
38
+
26
39
  def collect_captures(shape)
27
40
  found = Hash.new { |hash, key| hash[key] = [] }
28
41
  queue = [shape]
@@ -22,6 +22,10 @@ module Parsanol
22
22
  test <file.parg|artifact.json> run the grammar's test section
23
23
  parse <file> [--entry E] INPUT parse one input; prints the tree
24
24
  and the bound captures
25
+ check --source x.parg --artifact y.json
26
+ verify the artifact was compiled
27
+ from that source (source hash +
28
+ compiler version); exit 1 stale
25
29
  repl <file> [--entry E] interactive parse loop (blank
26
30
  line quits, :quit exits)
27
31
  doc <file> [rule] print rule documentation
@@ -53,6 +57,7 @@ module Parsanol
53
57
  when "test" then test
54
58
  when "schema" then schema
55
59
  when "parse" then parse
60
+ when "check" then check
56
61
  when "repl" then repl
57
62
  when "doc" then doc
58
63
  when "lsp" then Lsp.new.run && 0
@@ -88,6 +93,33 @@ module Parsanol
88
93
  0
89
94
  end
90
95
 
96
+ # Freshness (parsanol-ruby#209): verify an artifact was compiled
97
+ # from the given source. The stale-shipped-artifact incident class
98
+ # (an old artifact silently taking precedence over the corrected
99
+ # grammar) is undetectable without the recorded source hash.
100
+ # Exit 0 fresh, 1 stale or unrecorded.
101
+ def check
102
+ source = flag_value("--source")
103
+ artifact_path = flag_value("--artifact")
104
+ unless source && artifact_path && File.file?(source) && File.file?(artifact_path)
105
+ warn USAGE
106
+ return 1
107
+ end
108
+ artifact = Artifact.load(artifact_path)
109
+ if artifact.source_hash.nil?
110
+ puts "STALE? #{artifact_path}: no source hash recorded (pre-freshness artifact)"
111
+ return 1
112
+ end
113
+ if artifact.fresh_against?(PARG.read_utf8(source))
114
+ puts "FRESH #{artifact_path} == #{source} (compiler #{artifact.compiler_version})"
115
+ 0
116
+ else
117
+ puts "STALE #{artifact_path} does not match #{source} " \
118
+ "(built from #{artifact.source_hash[0, 12]}… by #{artifact.compiler_version})"
119
+ 1
120
+ end
121
+ end
122
+
91
123
  def test
92
124
  target = @argv.shift
93
125
  if File.directory?(target)
@@ -200,11 +232,13 @@ module Parsanol
200
232
 
201
233
  def artifact_for(file)
202
234
  if file.end_with?(".json")
203
- Parsanol::PARG::Artifact.load(file, tables_dir: @tables_dir || default_tables_dir(file))
235
+ Parsanol::PARG::Artifact.load(file, tables_dir: @tables_dir || default_tables_dir(file),
236
+ imports_dir: File.dirname(file))
204
237
  else
205
238
  artifact = compile_source(file)
206
239
  Parsanol::PARG::Artifact.from_json(JSON.generate(artifact.envelope),
207
- tables_dir: @tables_dir || default_tables_dir(file))
240
+ tables_dir: @tables_dir || default_tables_dir(file),
241
+ imports_dir: File.dirname(file))
208
242
  end
209
243
  end
210
244
 
@@ -314,22 +314,43 @@ module Parsanol
314
314
  def run_tests
315
315
  return [] if @document.tests.empty?
316
316
 
317
- default_entry = resolve_default_entry
318
317
  view = test_view
319
318
  @document.tests.filter_map do |test|
320
319
  entry_name = test.entry || default_entry
321
320
  rule = @document.entries.fetch(entry_name)
322
321
  begin
323
322
  shape = atom_for(rule).parse(test.input)
324
- if test.kind == :reject
323
+ case test.kind
324
+ when :reject
325
325
  "test #{test.input.inspect}: expected the input to be rejected"
326
- elsif test.kind == :example
327
- bound = Bindings.apply(view, { "bindings" => binding_list(rule) }, shape)
328
- mismatched = test.expect.reject { |key, value| bound.key?(key) && bound[key] == value }
326
+ when :example
327
+ bindings = binding_list(rule)
328
+ if bindings.empty?
329
+ raise CompileError,
330
+ "example tests need a bindings section (use `capture` " \
331
+ "for raw-capture assertions)"
332
+ end
333
+ bound = Bindings.apply(view, { "bindings" => bindings }, shape)
334
+ mismatched = test.expect.reject do |key, value|
335
+ bound.key?(key) && Bindings.canonical(bound[key]) == Bindings.canonical(value)
336
+ end
329
337
  next if mismatched.empty?
330
338
 
331
339
  "test #{test.input.inspect}: expected captures " \
332
340
  "#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
341
+ when :capture
342
+ captures = Bindings.collect_captures(shape)
343
+ mismatched = test.expect.reject do |key, value|
344
+ actual = captures[key.to_sym]&.first
345
+ !actual.nil? && Bindings.canonical(actual) == Bindings.canonical(value)
346
+ end
347
+ next if mismatched.empty?
348
+
349
+ actuals = mismatched.keys.to_h do |key|
350
+ [key, Bindings.canonical(captures[key.to_sym]&.first)]
351
+ end
352
+ "test #{test.input.inspect}: expected captures " \
353
+ "#{mismatched.transform_values(&:inspect).inspect}, got #{actuals.inspect}"
333
354
  end
334
355
  rescue Parsanol::ParseFailed
335
356
  unless test.kind == :reject
@@ -339,6 +360,13 @@ module Parsanol
339
360
  end
340
361
  end
341
362
 
363
+ # Only consulted when a test omits its entry: a multi-entry
364
+ # grammar whose tests are all explicit must compile (parsanol-
365
+ # ruby#216). Memoized so the raise happens once, on first need.
366
+ def default_entry
367
+ @default_entry ||= resolve_default_entry
368
+ end
369
+
342
370
  def resolve_default_entry
343
371
  own = @document.own_entries
344
372
  return own.first if own && own.size == 1
@@ -758,6 +786,12 @@ module Parsanol
758
786
  end,
759
787
  "docs" => @document.docs,
760
788
  "source" => @document.source,
789
+ # Freshness (parsanol-ruby#209): what the artifact was compiled
790
+ # from and with, so loaders can detect a stale artifact and fall
791
+ # back to a reference implementation instead of failing obscurely
792
+ # at parse time. Checksummed like the rest of the envelope.
793
+ "source_hash" => Digest::SHA256.hexdigest(@document.source.to_s),
794
+ "compiler_version" => Parsanol::VERSION,
761
795
  }
762
796
  if @document.skip_declaration
763
797
  # parsanol-ruby#134: declared trivia rule. Injection is baked
@@ -5,7 +5,15 @@ module Parsanol
5
5
  # Parsed PARG document: grammar rules plus the binding sections.
6
6
  class Document
7
7
  Binding = Struct.new(:capture, :path, :type, :card, :preprocess)
8
- TEST_KINDS = %i[accept reject example].freeze
8
+ # `capture` = phase 1 (grammar layer): asserts the raw capture tree,
9
+ # no bindings section needed. `example` = phase 2 (bindings layer):
10
+ # asserts the bound, preprocessed attribute hash.
11
+ # `capture` = phase 1 (grammar layer): asserts the raw capture tree,
12
+ # no bindings section needed. `example` = phase 2 (bindings layer):
13
+ # asserts the bound, preprocessed attribute hash.
14
+ TEST_KINDS = %i[accept reject example capture].freeze
15
+ # Test kinds whose expectations are `{ key: value }` pairs.
16
+ PAIR_KINDS = %w[example capture].freeze
9
17
 
10
18
  Test = Struct.new(:entry, :kind, :input, :expect)
11
19
 
@@ -83,7 +83,7 @@ module Parsanol
83
83
  kind = ident.value
84
84
  input = unquote(expect(:str).value)
85
85
  expect_pairs = {}
86
- if kind == "example" && peek&.type == :punct && peek.value == "{"
86
+ if Document::PAIR_KINDS.include?(kind) && peek&.type == :punct && peek.value == "{"
87
87
  advance
88
88
  skip_newlines
89
89
  until peek&.type == :punct && peek.value == "}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.91"
4
+ VERSION = "1.3.93"
5
5
  end
data/lib/parsanol/vm.rb CHANGED
@@ -146,7 +146,12 @@ module Parsanol
146
146
  # The shared program's compile-time analysis (heavy-memo
147
147
  # seeding) covered this structure; the fresh root inherits
148
148
  # it so its first parse memoizes like the original's.
149
- (@heavy ||= {}.compare_by_identity)[root] = true if @heavy_by_hash&.[](hash_key)
149
+ if @heavy_by_hash&.[](hash_key)
150
+ (@heavy ||= {}.compare_by_identity)[root] = true
151
+ trim_cache(@heavy)
152
+ end
153
+ cache[root] = shared
154
+ trim_cache(cache)
150
155
  return shared
151
156
  end
152
157
 
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.91
4
+ version: 1.3.93
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-11 00:00:00.000000000 Z
11
+ date: 2026-10-16 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