parsanol 1.3.132-arm-linux → 1.3.134-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: 8a9a959842c1ec95a2e7d65f2e4571468d74006d3a6e0934c652c49d34c2d123
4
- data.tar.gz: e341531eaff526a3ec332f5acff1dbe26b520c141e343a280ae3aa340f121698
3
+ metadata.gz: eba59af9b2628f8313df5bd663534638265d7342ea7f3c316aa3450008117a22
4
+ data.tar.gz: 9cf48c617f20c87ec5523c00e49f7720315845f401a6818246697ccb185b67d6
5
5
  SHA512:
6
- metadata.gz: 8c5c65ac083e83de4891d1635ab046e09c34d783ef7863d01113c7ef41bbf15ac103df6c768525928366dec353daeb9789fdbbcb84d8040f6abf55dfc915dd04
7
- data.tar.gz: c8832db22368e9b0715369bee7ddf9a46e19aeddb51217486f509300f84e533ae4e996cfe890eae9fad6a6782fa019ebe57e368daa10436dcefd17548ba41749
6
+ metadata.gz: ac6711d8aa414e6a19d9e69f949dfddafbcb5a858c0d31cf1f8d73fc7e49e25535b94cc2217b4bb4f9f5c618bd26101bb02bafc73d00755ed286d080c8217305
7
+ data.tar.gz: 1c40a9cf0320bfb7dd9600607993e02b2f26d62ac65562b0bc7c0c80171fc6381251760ee5e40856b71c4ff6c76f2f6ed07ebd0e8b86ee518780026e1b14e67b
@@ -99,7 +99,7 @@ module Parsanol
99
99
  return handle
100
100
  end
101
101
 
102
- json = legacy ? GrammarSerializer.serialize(root_atom) : grammar_json(root_atom)
102
+ json = legacy ? GrammarSerializer.serialize(root_atom, preserve_identity: true) : grammar_json(root_atom)
103
103
  handle = Native._register_grammar(json)
104
104
  HANDLE_CACHE[cache_key] = handle
105
105
  trim_caches
@@ -8,14 +8,22 @@ module Parsanol
8
8
  # Serialize a Parslet grammar (root atom) to JSON string
9
9
  #
10
10
  # @param root [Parsanol::Atoms::Base] The root atom of the grammar
11
+ # @param preserve_identity [Boolean] emit exactly one wire atom per
12
+ # Ruby atom, even when an atom is reached again through a recursive
13
+ # rule while its own children are still being serialized. Parslet
14
+ # compatibility needs this: its memo is keyed by atom identity, so a
15
+ # duplicated wire atom splits one memo entry in two and the native
16
+ # engine can pick a different alternative than the Ruby engine. The
17
+ # default keeps the established wire layout of strict grammars.
11
18
  # @return [String] JSON representation of the grammar
12
- def self.serialize(root)
19
+ def self.serialize(root, preserve_identity: false)
13
20
  # Create fresh instance for each serialization
14
21
  # (state is specific to each grammar)
15
- new.serialize(root)
22
+ new(preserve_identity: preserve_identity).serialize(root)
16
23
  end
17
24
 
18
- def initialize
25
+ def initialize(preserve_identity: false)
26
+ @preserve_identity = preserve_identity
19
27
  @atoms = []
20
28
  @atom_cache = {} # object_id => atom_id for deduplication
21
29
  # Strong retention: cache keys are object_ids; without holding the
@@ -52,74 +60,19 @@ module Parsanol
52
60
  # Don't create a new atom, just resolve and return the referenced atom_id
53
61
  return serialize_entity(atom) if atom.is_a?(Parsanol::Atoms::Entity)
54
62
 
55
- # Serialize based on atom type first (recursively)
56
- serialized = case atom
57
- when Parsanol::Atoms::Str
58
- serialize_str(atom)
59
- when Parsanol::Atoms::Re
60
- serialize_re(atom)
61
- when Parsanol::Atoms::Sequence
62
- serialize_sequence(atom)
63
- when Parsanol::Atoms::Alternative
64
- serialize_alternative(atom)
65
- when Parsanol::Atoms::Repetition
66
- serialize_repetition(atom)
67
- when Parsanol::Atoms::Named
68
- serialize_named(atom)
69
- when Parsanol::Atoms::Lookahead
70
- serialize_lookahead(atom)
71
- when Parsanol::Atoms::Capture
72
- serialize_capture(atom)
73
- when Parsanol::Atoms::Scope
74
- serialize_scope(atom)
75
- when Parsanol::Atoms::StateSet
76
- serialize_state_set(atom)
77
- when Parsanol::Atoms::StateMatch
78
- serialize_state_match(atom)
79
- when Parsanol::Atoms::StateSwitch
80
- serialize_state_switch(atom)
81
- when Parsanol::Atoms::CustomRef
82
- serialize_custom_ref(atom)
83
- when Parsanol::Atoms::Dynamic
84
- serialize_dynamic(atom)
85
- when Parsanol::Atoms::TriviaCapture
86
- # parsanol-ruby#152: the capture wrapper carries
87
- # its marker table so the native engine records
88
- # comment-shaped units into the pending channel
89
- # and attaches them to the next Named capture
90
- # under `comments:`. A distinct variant keeps the
91
- # declared gate: engines predating it reject
92
- # capture-carrying artifacts loudly.
93
- serialize_trivia_capture(atom)
94
- when Parsanol::Atoms::Trivia
95
- # Wire-distinct from Ignore (rs#172): the Rust
96
- # engine suppresses trivia-position diagnostics by
97
- # atom kind. Engines predating the Trivia tag
98
- # reject skip-carrying artifacts loudly (unknown
99
- # variant), which is the declared gate.
100
- serialize_trivia(atom)
101
- when Parsanol::Atoms::Constant
102
- # rs#137 follow-up: coradoc-markdown's Output
103
- # atoms — empty match yielding a wire constant.
104
- { "Constant" => { "value" => serialize_constant_value(atom.value) } }
105
- when Parsanol::Atoms::Lookbehind
106
- # rs#137/#163: the behind-window test — a fixed
107
- # literal window or an end-anchored regex (the
108
- # CommonMark flanking form).
109
- look = if atom.regex?
110
- { "Regex" => { "source" => atom.regex_source } }
111
- else
112
- { "Literal" => { "count" => atom.count,
113
- "pattern" => atom.pattern } }
114
- end
115
- { "Lookbehind" => { "look" => look,
116
- "positive" => atom.positive } }
117
- when Parsanol::Atoms::Ignored
118
- serialize_ignored(atom)
119
- else
120
- # Fallback for unknown atom types
121
- serialize_unknown(atom)
122
- end
63
+ # Reserve the id before the children, as serialize_entity does, so a
64
+ # recursive path back to this atom reuses it instead of emitting a
65
+ # second copy.
66
+ if @preserve_identity
67
+ atom_id = @atoms.size
68
+ @atom_cache[cache_key] = atom_id
69
+ @retained_atoms << atom
70
+ @atoms << nil
71
+ @atoms[atom_id] = serialize_body(atom)
72
+ return atom_id
73
+ end
74
+
75
+ serialized = serialize_body(atom)
123
76
 
124
77
  # Now reserve an atom_id and cache
125
78
  atom_id = @atoms.size
@@ -130,6 +83,77 @@ module Parsanol
130
83
  atom_id
131
84
  end
132
85
 
86
+ def serialize_body(atom)
87
+ # Serialize based on atom type first (recursively)
88
+ case atom
89
+ when Parsanol::Atoms::Str
90
+ serialize_str(atom)
91
+ when Parsanol::Atoms::Re
92
+ serialize_re(atom)
93
+ when Parsanol::Atoms::Sequence
94
+ serialize_sequence(atom)
95
+ when Parsanol::Atoms::Alternative
96
+ serialize_alternative(atom)
97
+ when Parsanol::Atoms::Repetition
98
+ serialize_repetition(atom)
99
+ when Parsanol::Atoms::Named
100
+ serialize_named(atom)
101
+ when Parsanol::Atoms::Lookahead
102
+ serialize_lookahead(atom)
103
+ when Parsanol::Atoms::Capture
104
+ serialize_capture(atom)
105
+ when Parsanol::Atoms::Scope
106
+ serialize_scope(atom)
107
+ when Parsanol::Atoms::StateSet
108
+ serialize_state_set(atom)
109
+ when Parsanol::Atoms::StateMatch
110
+ serialize_state_match(atom)
111
+ when Parsanol::Atoms::StateSwitch
112
+ serialize_state_switch(atom)
113
+ when Parsanol::Atoms::CustomRef
114
+ serialize_custom_ref(atom)
115
+ when Parsanol::Atoms::Dynamic
116
+ serialize_dynamic(atom)
117
+ when Parsanol::Atoms::TriviaCapture
118
+ # parsanol-ruby#152: the capture wrapper carries
119
+ # its marker table so the native engine records
120
+ # comment-shaped units into the pending channel
121
+ # and attaches them to the next Named capture
122
+ # under `comments:`. A distinct variant keeps the
123
+ # declared gate: engines predating it reject
124
+ # capture-carrying artifacts loudly.
125
+ serialize_trivia_capture(atom)
126
+ when Parsanol::Atoms::Trivia
127
+ # Wire-distinct from Ignore (rs#172): the Rust
128
+ # engine suppresses trivia-position diagnostics by
129
+ # atom kind. Engines predating the Trivia tag
130
+ # reject skip-carrying artifacts loudly (unknown
131
+ # variant), which is the declared gate.
132
+ serialize_trivia(atom)
133
+ when Parsanol::Atoms::Constant
134
+ # rs#137 follow-up: coradoc-markdown's Output
135
+ # atoms — empty match yielding a wire constant.
136
+ { "Constant" => { "value" => serialize_constant_value(atom.value) } }
137
+ when Parsanol::Atoms::Lookbehind
138
+ # rs#137/#163: the behind-window test — a fixed
139
+ # literal window or an end-anchored regex (the
140
+ # CommonMark flanking form).
141
+ look = if atom.regex?
142
+ { "Regex" => { "source" => atom.regex_source } }
143
+ else
144
+ { "Literal" => { "count" => atom.count,
145
+ "pattern" => atom.pattern } }
146
+ end
147
+ { "Lookbehind" => { "look" => look,
148
+ "positive" => atom.positive } }
149
+ when Parsanol::Atoms::Ignored
150
+ serialize_ignored(atom)
151
+ else
152
+ # Fallback for unknown atom types
153
+ serialize_unknown(atom)
154
+ end
155
+ end
156
+
133
157
  def serialize_str(atom)
134
158
  {
135
159
  "Str" => {
@@ -402,12 +402,22 @@ module Parsanol
402
402
  BatchDecoder.decode_and_flatten(raw, input, Parsanol::Slice)
403
403
  end
404
404
 
405
+ # The native engine's recursion-limit abort (ParseError::
406
+ # RecursionLimitExceeded in parsanol-rs). It carries no position
407
+ # marker: it is a resource limit, not a refusal of the input.
408
+ NATIVE_RECURSION_LIMIT = /\A(?:Recursion limit exceeded\b|Maximum recursion depth \(\d+\) exceeded)/
409
+
405
410
  def raise_compatible_parse_error(error, input)
406
411
  message = error.message
407
412
  position = message[NATIVE_POS_MARKER, 1].to_i
408
413
  cause = Parsanol::Cause.new(message.sub(NATIVE_POS_MARKER, ""),
409
414
  Parsanol::Source.new(input), position)
410
- raise Parsanol::ParseFailed.new(cause.to_s, cause), cause: nil
415
+ failure = if !message.match?(NATIVE_POS_MARKER) && message.match?(NATIVE_RECURSION_LIMIT)
416
+ Parsanol::RecursionLimitExceeded
417
+ else
418
+ Parsanol::ParseFailed
419
+ end
420
+ raise failure.new(cause.to_s, cause), cause: nil
411
421
  end
412
422
 
413
423
  # Reference-walker path (TODO.sota/8 B1): the plain recursive
@@ -22,7 +22,12 @@ module Parsanol
22
22
  compile to an artifact envelope;
23
23
  --bindings also writes one
24
24
  checksummed per-target binding
25
- artifact beside it (#432)
25
+ artifact beside it (#432);
26
+ --model-bindings FILE embeds a
27
+ rule -> model sidecar (JSON/YAML)
28
+ in the artifact
29
+ bindings <artifact.json> dump the embedded rule -> model
30
+ binding table as JSON (coradoc#307)
26
31
  test <file.parg|artifact.json> run the grammar's test section
27
32
  parse <file> [--entry E] INPUT parse one input; prints the tree
28
33
  and the bound captures
@@ -75,6 +80,7 @@ module Parsanol
75
80
  when "lint" then lint
76
81
  when "export" then export_grammar
77
82
  when "diff" then diff_artifacts
83
+ when "bindings" then dump_bindings
78
84
  when "lsp" then Lsp.new.run && 0
79
85
  else
80
86
  warn USAGE
@@ -145,6 +151,9 @@ module Parsanol
145
151
  source = @argv.shift
146
152
  out = flag_value("-o") || default_artifact_name(source)
147
153
  envelope = compile_source(source).envelope
154
+ if (sidecar = flag_value("--model-bindings"))
155
+ envelope = ModelBindings.embed(envelope, ModelBindings.load(sidecar))
156
+ end
148
157
  # Seal: the exact text written is the exact text the byte-level
149
158
  # checksum covers (Compiler.seal_json includes the trailing
150
159
  # newline).
@@ -162,6 +171,18 @@ module Parsanol
162
171
  0
163
172
  end
164
173
 
174
+ # `parg bindings ARTIFACT`: the embedded rule -> model table as JSON.
175
+ def dump_bindings
176
+ path = @argv.shift
177
+ unless path
178
+ warn "usage: parsanol parg bindings ARTIFACT.json"
179
+ return 2
180
+ end
181
+ envelope = ModelBindings.read_envelope(path)
182
+ puts ModelBindings.dump(envelope)
183
+ 0
184
+ end
185
+
165
186
  # Every --bindings flag value, `all` expanded to the target set.
166
187
  # Empty when the flag is absent (compile emits no binding artifacts).
167
188
  def bindings_targets
@@ -61,7 +61,8 @@ module Parsanol
61
61
  # unicode_tables — is byte-stable when the grammar's behavior is
62
62
  # unchanged. Mirrors the cross-engine parity view.
63
63
  SEMANTICS_EXCLUDED = %w[checksum byte_checksum compiler_version
64
- unicode_tables semantics_hash validated].freeze
64
+ unicode_tables semantics_hash validated
65
+ model_bindings].freeze
65
66
 
66
67
  def self.semantics_hash(envelope)
67
68
  payload = envelope.except(*SEMANTICS_EXCLUDED)
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+
6
+ module Parsanol
7
+ module PARG
8
+ # The rule -> model binding table embedded in an artifact (coradoc#307).
9
+ #
10
+ # A sidecar (JSON or YAML) declares, per rule, which model a raw-tree
11
+ # node inflates to:
12
+ #
13
+ # version: 1
14
+ # rules:
15
+ # thematic_break:
16
+ # keys: [hr] # exact capture-key set of the node
17
+ # model: HorizontalRule
18
+ # captures: {} # attribute => scalar capture key
19
+ # children: {} # attribute => nested-node capture key
20
+ # defaults: { style: "---" } # attributes fixed by the rule
21
+ #
22
+ # `parg compile --model-bindings FILE` embeds it under the envelope's
23
+ # additive "model_bindings" key; artifacts compiled without a sidecar
24
+ # carry no such key and are byte-identical to before. The table is not
25
+ # parse semantics, so semantics_hash ignores it; the checksum covers it.
26
+ module ModelBindings
27
+ KEY = "model_bindings"
28
+ VERSION = 1
29
+ RULE_FIELDS = %w[keys model captures children defaults].freeze
30
+
31
+ module_function
32
+
33
+ # @param path [String] a .json / .yml / .yaml sidecar
34
+ # @return [Hash] the validated, string-keyed table
35
+ def load(path)
36
+ text = File.read(path, encoding: Encoding::UTF_8)
37
+ raw = File.extname(path).match?(/\.ya?ml\z/i) ? YAML.safe_load(text) : JSON.parse(text)
38
+ normalize(raw)
39
+ rescue JSON::ParserError, Psych::SyntaxError => e
40
+ raise ArtifactError, "model bindings #{path}: #{e.message}"
41
+ end
42
+
43
+ # @return [Hash] the table with every rule filled to the full shape
44
+ def normalize(raw)
45
+ raise ArtifactError, "model bindings: expected a mapping" unless raw.is_a?(Hash)
46
+
47
+ table = stringify(raw)
48
+ unless table["version"] == VERSION
49
+ raise ArtifactError, "model bindings: unsupported version #{table['version'].inspect} (want #{VERSION})"
50
+ end
51
+
52
+ rules = table["rules"]
53
+ raise ArtifactError, "model bindings: `rules` must be a mapping" unless rules.is_a?(Hash)
54
+
55
+ { "version" => VERSION, "rules" => rules.to_h { |name, rule| [name, normalize_rule(name, rule)] } }
56
+ end
57
+
58
+ def normalize_rule(name, rule)
59
+ raise ArtifactError, "model bindings: rule #{name.inspect} must be a mapping" unless rule.is_a?(Hash)
60
+
61
+ unknown = rule.keys - RULE_FIELDS
62
+ raise ArtifactError, "model bindings: rule #{name.inspect} has unknown field #{unknown.first.inspect}" unless unknown.empty?
63
+ unless rule["model"].is_a?(String) && !rule["model"].empty?
64
+ raise ArtifactError, "model bindings: rule #{name.inspect} needs a `model`"
65
+ end
66
+ unless rule["keys"].is_a?(Array) && !rule["keys"].empty?
67
+ raise ArtifactError, "model bindings: rule #{name.inspect} needs non-empty `keys`"
68
+ end
69
+
70
+ {
71
+ "keys" => rule["keys"].map(&:to_s),
72
+ "model" => rule["model"],
73
+ "captures" => hash_field(name, rule, "captures"),
74
+ "children" => hash_field(name, rule, "children"),
75
+ "defaults" => hash_field(name, rule, "defaults"),
76
+ }
77
+ end
78
+
79
+ def hash_field(name, rule, field)
80
+ value = rule.fetch(field, {})
81
+ raise ArtifactError, "model bindings: rule #{name.inspect} `#{field}` must be a mapping" unless value.is_a?(Hash)
82
+
83
+ value
84
+ end
85
+
86
+ def stringify(value)
87
+ case value
88
+ when Hash then value.to_h { |key, item| [key.to_s, stringify(item)] }
89
+ when Array then value.map { |item| stringify(item) }
90
+ else value
91
+ end
92
+ end
93
+
94
+ # Embed a table in an envelope and re-seal the canonical checksum.
95
+ def embed(envelope, table)
96
+ sealed = envelope.merge(KEY => table)
97
+ sealed["checksum"] = Compiler.checksum(sealed)
98
+ sealed
99
+ end
100
+
101
+ # Read an artifact file for the dump: JSON plus checksum verification
102
+ # only, so no grammar source and no custom-atom class is needed.
103
+ def read_envelope(path)
104
+ envelope = JSON.parse(File.read(path, encoding: Encoding::UTF_8))
105
+ unless envelope.is_a?(Hash) && envelope["checksum"] == Compiler.checksum(envelope)
106
+ raise ArtifactError, "#{path}: artifact checksum mismatch"
107
+ end
108
+
109
+ envelope
110
+ rescue JSON::ParserError => e
111
+ raise ArtifactError, "#{path}: #{e.message}"
112
+ end
113
+
114
+ # @return [Hash, nil] the embedded table, nil for an artifact without one
115
+ def from(envelope)
116
+ envelope[KEY]
117
+ end
118
+
119
+ # The machine-readable dump: rule -> model/keys/captures/children/defaults.
120
+ def dump(envelope)
121
+ table = from(envelope) || { "version" => VERSION, "rules" => {} }
122
+ JSON.pretty_generate(table)
123
+ end
124
+ end
125
+ end
126
+ end
data/lib/parsanol/parg.rb CHANGED
@@ -26,6 +26,7 @@ module Parsanol
26
26
  autoload :Compiler, "parsanol/parg/compiler"
27
27
  autoload :Artifact, "parsanol/parg/artifact"
28
28
  autoload :Bindings, "parsanol/parg/bindings"
29
+ autoload :ModelBindings, "parsanol/parg/model_bindings"
29
30
  autoload :Render, "parsanol/parg/render"
30
31
  autoload :Derive, "parsanol/parg/derive"
31
32
  autoload :SelfHost, "parsanol/parg/selfhost"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.132"
4
+ VERSION = "1.3.134"
5
5
  end
data/lib/parsanol.rb CHANGED
@@ -66,6 +66,13 @@ module Parsanol
66
66
  attr_reader :parse_failure_cause
67
67
  end
68
68
 
69
+ # Raised when the native engine stops because the input nests deeper
70
+ # than its recursion limit. A subclass of ParseFailed with the same
71
+ # message and cause, so existing rescues behave as before; callers that
72
+ # want to retry on another engine can rescue this class instead of
73
+ # matching the message.
74
+ class RecursionLimitExceeded < ParseFailed; end
75
+
69
76
  # Class methods added to classes that include Parsanol.
70
77
  module ClassMethods
71
78
  # Enable automatic rule optimization for all rules in this parser.
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.132
4
+ version: 1.3.134
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-11-03 00:00:00.000000000 Z
11
+ date: 2026-11-05 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
@@ -120,6 +120,7 @@ files:
120
120
  - lib/parsanol/parg/lints.rb
121
121
  - lib/parsanol/parg/lsp.rb
122
122
  - lib/parsanol/parg/lutaml.rb
123
+ - lib/parsanol/parg/model_bindings.rb
123
124
  - lib/parsanol/parg/node.rb
124
125
  - lib/parsanol/parg/parser.rb
125
126
  - lib/parsanol/parg/preprocess.rb