parsanol 1.3.54-arm-linux → 1.3.57-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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/exe/parsanol +7 -0
  3. data/lib/parsanol/3.2/parsanol_native.so +0 -0
  4. data/lib/parsanol/3.3/parsanol_native.so +0 -0
  5. data/lib/parsanol/3.4/parsanol_native.so +0 -0
  6. data/lib/parsanol/4.0/parsanol_native.so +0 -0
  7. data/lib/parsanol/atoms/base.rb +1 -1
  8. data/lib/parsanol/atoms/cut.rb +1 -1
  9. data/lib/parsanol/atoms/repetition.rb +13 -2
  10. data/lib/parsanol/cause.rb +1 -1
  11. data/lib/parsanol/error_reporter/contextual.rb +1 -1
  12. data/lib/parsanol/lazy_result.rb +12 -3
  13. data/lib/parsanol/native/libparsanol.so +0 -0
  14. data/lib/parsanol/native/serializer.rb +5 -2
  15. data/lib/parsanol/native/transformer.rb +12 -8
  16. data/lib/parsanol/native.rb +4 -2
  17. data/lib/parsanol/parg/artifact.rb +213 -0
  18. data/lib/parsanol/parg/authoring.rb +110 -0
  19. data/lib/parsanol/parg/bindings.rb +129 -0
  20. data/lib/parsanol/parg/cli.rb +251 -0
  21. data/lib/parsanol/parg/compiler.rb +465 -0
  22. data/lib/parsanol/parg/derive.rb +28 -0
  23. data/lib/parsanol/parg/document.rb +69 -0
  24. data/lib/parsanol/parg/error.rb +18 -0
  25. data/lib/parsanol/parg/frontend.rb +173 -0
  26. data/lib/parsanol/parg/import.rb +34 -0
  27. data/lib/parsanol/parg/importers/abnf.rb +299 -0
  28. data/lib/parsanol/parg/importers/ebnf.rb +201 -0
  29. data/lib/parsanol/parg/importers/pest.rb +314 -0
  30. data/lib/parsanol/parg/imports.rb +87 -0
  31. data/lib/parsanol/parg/lexer.rb +72 -0
  32. data/lib/parsanol/parg/lints.rb +171 -0
  33. data/lib/parsanol/parg/lsp.rb +208 -0
  34. data/lib/parsanol/parg/lutaml.rb +67 -0
  35. data/lib/parsanol/parg/node.rb +20 -0
  36. data/lib/parsanol/parg/parser.rb +458 -0
  37. data/lib/parsanol/parg/preprocess.rb +36 -0
  38. data/lib/parsanol/parg/render.rb +44 -0
  39. data/lib/parsanol/parg/selfhost.rb +49 -0
  40. data/lib/parsanol/parg/visitor.rb +64 -0
  41. data/lib/parsanol/parg.rb +38 -0
  42. data/lib/parsanol/parser.rb +1 -1
  43. data/lib/parsanol/pattern.rb +1 -1
  44. data/lib/parsanol/pool.rb +1 -1
  45. data/lib/parsanol/rope.rb +1 -1
  46. data/lib/parsanol/slice.rb +4 -6
  47. data/lib/parsanol/source/line_cache.rb +1 -1
  48. data/lib/parsanol/source.rb +2 -2
  49. data/lib/parsanol/version.rb +1 -1
  50. data/lib/parsanol/vm.rb +95 -5
  51. data/lib/parsanol.rb +9 -0
  52. data/parsanol.gemspec +3 -1
  53. metadata +31 -4
@@ -90,7 +90,7 @@ module Parsanol
90
90
  return true if pattern_val === target
91
91
 
92
92
  # Check if pattern is a binding expression (like simple(:x))
93
- if pattern_val.respond_to?(:can_bind?) && pattern_val.can_bind?(target)
93
+ if pattern_val.is_a?(Parsanol::Pattern::SubtreeBind) && pattern_val.can_bind?(target)
94
94
  return capture_binding(target, pattern_val, captured)
95
95
  end
96
96
 
data/lib/parsanol/pool.rb CHANGED
@@ -119,7 +119,7 @@ module Parsanol
119
119
  end
120
120
 
121
121
  # Reset object state if it supports the protocol
122
- obj.reset! if obj.respond_to?(:reset!)
122
+ obj.reset! if obj.is_a?(Parsanol::Resettable)
123
123
 
124
124
  @stats[:released] += 1
125
125
  @available.push(obj)
data/lib/parsanol/rope.rb CHANGED
@@ -67,7 +67,7 @@ module Parsanol
67
67
  #
68
68
  # @return [Integer] The sum of all segment sizes
69
69
  def size
70
- @segments.sum { |s| s.respond_to?(:size) ? s.size : s.to_s.size }
70
+ @segments.sum(&:size)
71
71
  end
72
72
 
73
73
  # Creates a rope from an existing string.
@@ -142,18 +142,16 @@ module Parsanol
142
142
 
143
143
  # Unified line/column computation:
144
144
  # - String input: compute from input string
145
- # - LineCache: delegate to cache
145
+ # - Source / LineCache: delegate
146
146
  def line_and_column_at(pos)
147
- if @input.respond_to?(:line_and_column)
148
- # LineCache or duck-typed object
149
- @input.line_and_column(pos)
150
- else
151
- # String input
147
+ if @input.is_a?(String)
152
148
  prefix = @input.byteslice(0, pos) || ""
153
149
  line = 1 + prefix.count("\n")
154
150
  last_nl = prefix.rindex("\n")
155
151
  column = last_nl ? pos - last_nl : pos + 1
156
152
  [line, column]
153
+ else
154
+ @input.line_and_column(pos)
157
155
  end
158
156
  end
159
157
  end
@@ -34,7 +34,7 @@ module Parsanol
34
34
  def line_and_column(position)
35
35
  scan_buffer_once
36
36
 
37
- position = position.bytepos if position.respond_to?(:bytepos)
37
+ position = position.bytepos if position.is_a?(Parsanol::Source)
38
38
 
39
39
  line_idx = @breaks.lower_bound_index(position)
40
40
 
@@ -33,9 +33,9 @@ module Parsanol
33
33
  # @raise [ArgumentError] if input doesn't respond to to_str
34
34
  #
35
35
  def initialize(input)
36
- unless input.respond_to?(:to_str)
36
+ unless input.is_a?(String)
37
37
  raise ArgumentError,
38
- "Source requires a string-like object (responds to to_str)"
38
+ "Source requires a String (got #{input.class})"
39
39
  end
40
40
 
41
41
  @raw_string = input.to_str
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.54"
4
+ VERSION = "1.3.57"
5
5
  end
data/lib/parsanol/vm.rb CHANGED
@@ -149,6 +149,7 @@ module Parsanol
149
149
  def compile_with_inline(atom, inline)
150
150
  root = atom.is_a?(Parsanol::Parser) ? atom.root : atom
151
151
  compiler = Compiler.new(inline: inline)
152
+ validate_alternatives(root)
152
153
  return nil unless compiler.compile_atom(root, true)
153
154
 
154
155
  # HALT terminates the MAIN program; subroutines follow it so the
@@ -167,13 +168,88 @@ module Parsanol
167
168
  # included — the seed is recomputed at every compile), so the
168
169
  # cold-start never pays the doomed unmemoized exploration.
169
170
  if compiler.backtracking_prone
170
- # rubocop:disable-next Lint/HashCompareByIdentity -- object_id keys match run_for's heavy flag; would otherwise pin every grammar alive
171
+ # rubocop:disable-next Lint/HashCompareByIdentity -- object_id keys mirror run_for's heavy flag
171
172
  (@heavy ||= {})[root.object_id] = true
172
173
  end
173
174
 
174
175
  compiler.to_program
175
176
  end
176
177
 
178
+ def validate_choice_branches(atom)
179
+ atom.alternatives.each_with_index do |branch, idx|
180
+ walk_branch = branch
181
+ next if idx == atom.alternatives.size - 1
182
+
183
+ # Entities resolve lazily and may be recursive; resolve
184
+ # through the wrapper for the nullability check only.
185
+ while walk_branch.is_a?(Parsanol::Atoms::Entity) ||
186
+ walk_branch.is_a?(Parsanol::Atoms::Named)
187
+ walk_branch = begin
188
+ walk_branch.parslet
189
+ rescue StandardError
190
+ nil
191
+ end
192
+ break if walk_branch.nil?
193
+ end
194
+ next if walk_branch.nil?
195
+ next unless Compiler.new.nullable?(walk_branch)
196
+
197
+ raise Parsanol::GrammarError,
198
+ "wrong grammar: alternative branch #{idx} " \
199
+ "(#{branch.inspect}) can match empty input and is not " \
200
+ "the last branch — ordered choice commits to its empty " \
201
+ "match, so #{atom.alternatives.size - idx - 1} later " \
202
+ "branch(es) can never match at positions where it " \
203
+ "matches empty. Reorder branches so empty-matchable " \
204
+ "ones come last, or make the branch non-empty."
205
+ end
206
+ end
207
+
208
+ # Grammar validation, run once per compile: an ordered-choice
209
+ # branch that can match empty input while not being last commits
210
+ # to its empty match and permanently shadows every later branch —
211
+ # the grammar is wrong, and inputs it would mis-parse fail with a
212
+ # baffling cause. Raise before any input is seen.
213
+ def validate_alternatives(root)
214
+ visited = {}.compare_by_identity
215
+ walk = lambda do |atom, depth|
216
+ next if atom.nil? || depth > 20
217
+ next if visited[atom]
218
+
219
+ visited[atom] = true
220
+ case atom
221
+ when Parsanol::Atoms::Alternative
222
+ validate_choice_branches(atom)
223
+ atom.alternatives.each { |c| walk.call(c, depth + 1) }
224
+ when Parsanol::Atoms::Sequence
225
+ atom.parslets.each { |c| walk.call(c, depth + 1) }
226
+ when Parsanol::Atoms::Repetition, Parsanol::Atoms::Named
227
+ walk.call(atom.parslet, depth + 1)
228
+ when Parsanol::Atoms::Lookahead
229
+ walk.call(atom.bound_parslet, depth + 1)
230
+ when Parsanol::Atoms::Ignored
231
+ walk.call(atom.wrapped_atom, depth + 1)
232
+ when Parsanol::Atoms::Entity
233
+ begin
234
+ walk.call(atom.parslet, depth + 1)
235
+ rescue Parsanol::GrammarError
236
+ raise
237
+ rescue StandardError
238
+ # Lazily resolved / recursive entity that cannot resolve
239
+ # outside a parse: skip this branch of the walk.
240
+ nil
241
+ end
242
+ when Parsanol::Atoms::Scope
243
+ begin
244
+ walk.call(atom.block.call, depth + 1)
245
+ rescue StandardError
246
+ nil
247
+ end
248
+ end
249
+ end
250
+ walk.call(root, 0)
251
+ end
252
+
177
253
  # Executes a compiled program. Returns [true, value] on success;
178
254
  # [false, nil] on failure or budget exhaustion — callers fall back
179
255
  # to the interpreter, which reproduces exact diagnostics.
@@ -380,8 +456,7 @@ module Parsanol
380
456
  end
381
457
  end
382
458
 
383
- # Wrapper branches (Entity/Scope) share body shapes by design.
384
- # rubocop:disable-next Lint/DuplicateBranch
459
+ # rubocop:disable Lint/DuplicateBranch -- Wrapper branches (Entity/Scope) share body shapes by design
385
460
  def nullable?(atom, depth = 0)
386
461
  return false if depth > 10
387
462
 
@@ -397,12 +472,14 @@ module Parsanol
397
472
  when Parsanol::Atoms::Ignored then nullable?(atom.wrapped_atom, depth + 1)
398
473
  when Parsanol::Atoms::Lookahead then true
399
474
  when Parsanol::Atoms::Entity, Parsanol::Atoms::Scope
400
- nullable_wrapped?( # rubocop:disable Lint/DuplicateBranch -- Entity/Scope differ only in accessor
475
+ # rubocop:disable Lint/DuplicateBranch -- Entity/Scope differ only in accessor
476
+ nullable_wrapped?(
401
477
  -> { atom.is_a?(Parsanol::Atoms::Entity) ? atom.parslet : atom.block.call }, depth
402
478
  )
403
479
  else false
404
480
  end
405
481
  end
482
+ # rubocop:enable Lint/DuplicateBranch
406
483
 
407
484
  # Entity and Scope wrap lazily-resolvable inners through different
408
485
  # accessors; the nullability logic is shared.
@@ -790,7 +867,7 @@ module Parsanol
790
867
  memo_stack = []
791
868
  trace = ENV["VM_TRACE"] ? [] : nil
792
869
 
793
- # rubocop:disable-next Metrics/BlockLength -- the dispatch loop IS execute
870
+ # rubocop:disable Metrics/BlockLength -- the dispatch loop IS execute
794
871
  loop do
795
872
  steps += 1
796
873
  if steps > budget
@@ -977,12 +1054,24 @@ module Parsanol
977
1054
  count = frames[cbase]
978
1055
  return BAIL if count.nil?
979
1056
 
1057
+ old_end = frames[cbase + 1]
980
1058
  count += 1
981
1059
  frames[cbase] = count
982
1060
  frames[cbase + 1] = pos
983
1061
  max = ops[pc + 3]
984
1062
  pc = if max && count >= max
985
1063
  ops[pc + 2] # exit
1064
+ elsif pos == old_end
1065
+ # Zero-width body match: count it, stop iterating.
1066
+ # Mirrors Repetition#try_general: success with the
1067
+ # prefix when min is met, failure of the repetition
1068
+ # otherwise.
1069
+ if count >= frames[cbase + 3]
1070
+ ops[pc + 2] # exit
1071
+ else
1072
+ frames.slice!(cbase..)
1073
+ fail_pc
1074
+ end
986
1075
  else
987
1076
  ops[pc + 1] # test
988
1077
  end
@@ -1252,6 +1341,7 @@ module Parsanol
1252
1341
  else
1253
1342
  return BAIL
1254
1343
  end
1344
+ # rubocop:enable Metrics/BlockLength
1255
1345
  end
1256
1346
  end
1257
1347
 
data/lib/parsanol.rb CHANGED
@@ -39,11 +39,20 @@
39
39
  # Inspired by Parslet (MIT License).
40
40
 
41
41
  module Parsanol
42
+ # The PARG grammar language (text source -> checksummed artifact).
43
+ autoload :PARG, "parsanol/parg"
44
+
42
45
  # Hook to extend including classes with ClassMethods.
43
46
  def self.included(base)
44
47
  base.extend(ClassMethods)
45
48
  end
46
49
 
50
+ # Raised when the GRAMMAR itself is malformed — detected before any
51
+ # input is parsed. The canonical case: an ordered-choice branch that
52
+ # can match empty input while not being last, which commits to its
53
+ # empty match and permanently shadows every later branch.
54
+ class GrammarError < StandardError; end
55
+
47
56
  # Exception raised when parsing fails. Contains detailed error information
48
57
  # in the #parse_failure_cause attribute.
49
58
  class ParseFailed < StandardError
data/parsanol.gemspec CHANGED
@@ -29,7 +29,9 @@ Gem::Specification.new do |spec|
29
29
  # Rust extension
30
30
  spec.extensions = ["ext/parsanol_native/extconf.rb"]
31
31
 
32
- spec.files = Dir.glob("{lib,ext}/**/*") + %w[
32
+ spec.executables = %w[parsanol]
33
+ spec.bindir = "exe"
34
+ spec.files = Dir.glob("{lib,ext,exe}/**/*") + %w[
33
35
  HISTORY.txt
34
36
  LICENSE
35
37
  Rakefile
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.54
4
+ version: 1.3.57
5
5
  platform: arm-linux
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-24 00:00:00.000000000 Z
11
+ date: 2026-09-30 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
@@ -16,7 +16,8 @@ description: A small Ruby library for constructing parsers in the PEG (Parsing E
16
16
  for improved performance.
17
17
  email:
18
18
  - open.source@ribose.com
19
- executables: []
19
+ executables:
20
+ - parsanol
20
21
  extensions: []
21
22
  extra_rdoc_files: []
22
23
  files:
@@ -24,6 +25,7 @@ files:
24
25
  - LICENSE
25
26
  - README.adoc
26
27
  - Rakefile
28
+ - exe/parsanol
27
29
  - lib/parsanol.rb
28
30
  - lib/parsanol/3.2/parsanol_native.so
29
31
  - lib/parsanol/3.3/parsanol_native.so
@@ -91,6 +93,31 @@ files:
91
93
  - lib/parsanol/options.rb
92
94
  - lib/parsanol/options/zero_copy.rb
93
95
  - lib/parsanol/parallel.rb
96
+ - lib/parsanol/parg.rb
97
+ - lib/parsanol/parg/artifact.rb
98
+ - lib/parsanol/parg/authoring.rb
99
+ - lib/parsanol/parg/bindings.rb
100
+ - lib/parsanol/parg/cli.rb
101
+ - lib/parsanol/parg/compiler.rb
102
+ - lib/parsanol/parg/derive.rb
103
+ - lib/parsanol/parg/document.rb
104
+ - lib/parsanol/parg/error.rb
105
+ - lib/parsanol/parg/frontend.rb
106
+ - lib/parsanol/parg/import.rb
107
+ - lib/parsanol/parg/importers/abnf.rb
108
+ - lib/parsanol/parg/importers/ebnf.rb
109
+ - lib/parsanol/parg/importers/pest.rb
110
+ - lib/parsanol/parg/imports.rb
111
+ - lib/parsanol/parg/lexer.rb
112
+ - lib/parsanol/parg/lints.rb
113
+ - lib/parsanol/parg/lsp.rb
114
+ - lib/parsanol/parg/lutaml.rb
115
+ - lib/parsanol/parg/node.rb
116
+ - lib/parsanol/parg/parser.rb
117
+ - lib/parsanol/parg/preprocess.rb
118
+ - lib/parsanol/parg/render.rb
119
+ - lib/parsanol/parg/selfhost.rb
120
+ - lib/parsanol/parg/visitor.rb
94
121
  - lib/parsanol/parser.rb
95
122
  - lib/parsanol/parslet.rb
96
123
  - lib/parsanol/pattern.rb