parsanol 1.3.61-arm-linux → 1.3.63-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: b9cb26b86a6945fa3e32d5a273d1202c32dc1b29207ceb26ec254bcbd89c99b5
4
- data.tar.gz: 264d5cc2e7f99688a67f8ce095955491d98b77aa607fed90ed8f628d5949b159
3
+ metadata.gz: 686fbbce80e4f2fabc5b050d06eb209ccf919c347fc9bc4a6a755e5b7aadc69e
4
+ data.tar.gz: 342c41ab713eaade585308f97cd5d94016612e66a06069a93c531aec32395a25
5
5
  SHA512:
6
- metadata.gz: 1ea23aac23e879ac4b599aba68778485b2336a7576466b271794309dee6cfc462123e552574123d4c572a2b5ee1a0d71badce00aed5d570e425c78515aee9691
7
- data.tar.gz: 9cf3429cb1f051a6514b1dd1c68673af18eaea577780413374c3441e89f1ccfed7c78ce268c8c090af1e4717dd2bd9541478ca86f1ed7786a5a1aca9a9614410
6
+ metadata.gz: 9eb70ff88a8d02ac69f7ddcdcce3dba5e4015ff0e60331259b165d2f470474b09cf2435b4a0f43c6965c05e6ea7be798fd72d494b4b87ad064681052b99d55ab
7
+ data.tar.gz: 7d0f829a1c4ab6b068420873d90aee866b5e2cb5f1c200857750a6ac54d5eedccc0abfdc221a693be9126fc17db6ef8c1ab206903bc474801650b1e41199622a
@@ -220,15 +220,21 @@ module Parsanol
220
220
  # to recover inputs from grammars native cannot express.
221
221
  parsanol_grammar =
222
222
  grammar.is_a?(Parsanol::Atoms::Base) || grammar.is_a?(Parsanol::Parser)
223
- if parsanol_grammar && (m = error.message.match(NATIVE_POS_MARKER))
223
+ # The position capture is extracted as a plain String: a MatchData
224
+ # would pin its subject — the full multi-KB failure message — for
225
+ # as long as anything downstream retains it, and callers that
226
+ # rescue-and-collect (pubid's flavor dispatcher) retain millions
227
+ # across long parses (#123).
228
+ message = error.message
229
+ if parsanol_grammar && (pos_str = message[NATIVE_POS_MARKER, 1])
224
230
  source = Parsanol::Source.new(input)
225
231
  success, value = grammar.run_with_context(source, nil, true)
226
232
  return grammar.finalize_result(value) if success
227
233
 
228
- pos = m[1].to_i
229
- msg = error.message.sub(NATIVE_POS_MARKER, "")
234
+ pos = pos_str.to_i
235
+ msg = message.sub(NATIVE_POS_MARKER, "")
230
236
  cause = Parsanol::Cause.new(msg, source, pos)
231
- raise Parsanol::ParseFailed.new(cause.to_s, cause)
237
+ raise Parsanol::ParseFailed.new(cause.to_s, cause), cause: nil
232
238
  end
233
239
 
234
240
  if parsanol_grammar
@@ -243,17 +249,20 @@ module Parsanol
243
249
  value.raise
244
250
  end
245
251
 
246
- if (marker = error.message.match(NATIVE_POS_MARKER))
252
+ # Same capture-string extraction as above; `cause: nil` keeps the
253
+ # retained ParseFailed from pinning the original RuntimeError and
254
+ # its message through the exception-cause chain (#123).
255
+ if (pos_str = message[NATIVE_POS_MARKER, 1])
247
256
  source = Parsanol::Source.new(input)
248
257
  cause = Parsanol::Cause.new(
249
- error.message.sub(NATIVE_POS_MARKER, ""), source, marker[1].to_i
258
+ message.sub(NATIVE_POS_MARKER, ""), source, pos_str.to_i
250
259
  )
251
- raise Parsanol::ParseFailed.new(cause.to_s, cause)
260
+ raise Parsanol::ParseFailed.new(cause.to_s, cause), cause: nil
252
261
  end
253
262
 
254
263
  source = Parsanol::Source.new(input)
255
- cause = Parsanol::Cause.new(error.message, source, source.bytepos)
256
- raise Parsanol::ParseFailed.new(cause.to_s, cause)
264
+ cause = Parsanol::Cause.new(message, source, source.bytepos)
265
+ raise Parsanol::ParseFailed.new(cause.to_s, cause), cause: nil
257
266
  end
258
267
 
259
268
  # Pre-serialized JSON grammar path (library authors with cached JSON).
@@ -14,7 +14,7 @@ module Parsanol
14
14
  attr_reader :envelope, :path, :tables_dir
15
15
 
16
16
  def self.load(path, tables_dir: nil)
17
- envelope = JSON.parse(File.read(path))
17
+ envelope = JSON.parse(PARG.read_utf8(path))
18
18
  new(envelope, path, tables_dir || File.dirname(path))
19
19
  end
20
20
 
@@ -28,8 +28,13 @@ module Parsanol
28
28
  unless envelope["shape"] == SUPPORTED_SHAPE
29
29
  raise ArtifactError,
30
30
  "unsupported artifact shape #{envelope['shape'].inspect} " \
31
- "(this runtime implements #{SUPPORTED_SHAPE.inspect})"
31
+ "(this runtime implements #{SUPPORTED_SHAPE.inspect}; the " \
32
+ "contract is docs/PARSANOL-SHAPE-v2.md in parsanol-rs — " \
33
+ "a shape bump requires a new engine family, artifacts " \
34
+ "declaring #{SUPPORTED_SHAPE.inspect} parse identically forever)"
32
35
  end
36
+ bindings = envelope["bindings"]
37
+ validate_bindings!(bindings) unless bindings.nil?
33
38
  @envelope = envelope
34
39
  @path = path
35
40
  @tables_dir = tables_dir
@@ -139,15 +144,27 @@ module Parsanol
139
144
 
140
145
  # Structured parse diagnostics (PN 2): {offset, message} for the
141
146
  # deepest failure of the most recent parse attempt on this entry.
147
+ # Structured parse diagnostics — the flat v1 wire format every
148
+ # backend agrees on (parsanol-rs#145): offset, the expected-symbol
149
+ # list, and a message, fetched out-of-band so success paths pay
150
+ # nothing. Full cause trees stay a Ruby-side diagnostic.
142
151
  def parse_with_diagnostics(entry_name, input, mode: :native)
143
152
  shape = parse(entry_name, input, mode: mode)
144
- { "ok" => true, "offset" => nil, "message" => nil, "shape" => shape }
153
+ { "ok" => true, "offset" => nil, "expected" => [],
154
+ "message" => nil, "shape" => shape }
145
155
  rescue Parsanol::ParseFailed => e
146
156
  cause = deepest_cause(e.parse_failure_cause)
147
157
  { "ok" => false, "offset" => cause&.position,
158
+ "expected" => expected_labels_for(cause),
148
159
  "message" => cause&.message || e.message, "shape" => nil }
149
160
  end
150
161
 
162
+ def expected_labels_for(cause)
163
+ return [] unless cause.respond_to?(:expected)
164
+
165
+ Array(cause.expected)
166
+ end
167
+
151
168
  def deepest_cause(cause)
152
169
  return cause if cause.nil? || cause.children.empty?
153
170
 
@@ -169,7 +186,7 @@ module Parsanol
169
186
 
170
187
  file = declared
171
188
  path = File.join(@tables_dir.to_s, file)
172
- raw = path.end_with?(".json") ? JSON.parse(File.read(path)) : YAML.safe_load_file(path, aliases: true)
189
+ raw = path.end_with?(".json") ? JSON.parse(PARG.read_utf8(path)) : YAML.safe_load(PARG.read_utf8(path), aliases: true)
173
190
  rows = case raw
174
191
  when Hash
175
192
  raw.map { |key, value| { "name" => key.to_s }.merge(value.to_h.transform_keys(&:to_s)) }
@@ -179,6 +196,39 @@ module Parsanol
179
196
  @table_cache[name] = rows
180
197
  end
181
198
 
199
+ # The envelope's optional grammar-to-model bindings section
200
+ # (envelope v2, parsanol-rs#143): capture name -> model attribute
201
+ # paths, type casts, cardinality, and preprocessing step refs,
202
+ # declared once and consumed identically by every tier's binder.
203
+ # Phase 1 validates the section's shape and exposes it as data.
204
+ def bindings
205
+ envelope["bindings"]
206
+ end
207
+
208
+ BINDINGS_ALLOWED_KEYS = %w[captures entries version].freeze
209
+
210
+ def validate_bindings!(bindings)
211
+ unless bindings.is_a?(Hash)
212
+ raise ArtifactError, "bindings section must be a map (got #{bindings.class})"
213
+ end
214
+
215
+ unknown = bindings.keys - BINDINGS_ALLOWED_KEYS
216
+ unless unknown.empty?
217
+ raise ArtifactError, "bindings section has unknown keys: #{unknown.inspect}"
218
+ end
219
+
220
+ (bindings["captures"] || {}).each do |name, spec|
221
+ unless spec.is_a?(Hash) && spec["path"].is_a?(String)
222
+ raise ArtifactError,
223
+ "bindings capture #{name.inspect} must declare a string :path"
224
+ end
225
+ end
226
+ rescue ArtifactError
227
+ raise
228
+ rescue StandardError => e
229
+ raise ArtifactError, "malformed bindings section: #{e.class}: #{e.message}"
230
+ end
231
+
182
232
  def compiler
183
233
  @compiler ||= begin
184
234
  document = Parser.new(envelope.fetch("source")).parse
@@ -25,7 +25,7 @@ module Parsanol
25
25
  end
26
26
 
27
27
  def read_file(file)
28
- content = File.read(file).gsub(/^#[^\n]*\n/, "")
28
+ content = PARG.read_utf8(file).gsub(/^#[^\n]*\n/, "")
29
29
  header = content.match(/suite\s+\w+(?:\s+for\s+entry\s+(?<entry>\w+))?\s*\{/)
30
30
  unless header
31
31
  raise ParseError,
@@ -36,7 +36,8 @@ module Parsanol
36
36
  end
37
37
 
38
38
  def initialize(argv)
39
- @argv = argv.dup
39
+ # Under a non-UTF-8 locale ARGV arrives as BINARY; inputs are UTF-8.
40
+ @argv = argv.map { |arg| arg.dup.force_encoding(Encoding::UTF_8) }
40
41
  @tables_dir = nil
41
42
  end
42
43
 
@@ -82,7 +83,7 @@ module Parsanol
82
83
  source = @argv.shift
83
84
  out = flag_value("-o") || default_artifact_name(source)
84
85
  envelope = compile_source(source).envelope
85
- File.write(out, "#{JSON.generate(envelope)}\n")
86
+ File.write(out, "#{JSON.generate(envelope)}\n", encoding: Encoding::UTF_8)
86
87
  puts "#{out} #{envelope['checksum']} warnings=#{envelope['lint']['order_warnings'].size}"
87
88
  0
88
89
  end
@@ -161,7 +162,7 @@ module Parsanol
161
162
  puts "parsanol parg repl — #{file} entry #{entry_name.inspect}; blank line quits"
162
163
  loop do
163
164
  print "> "
164
- input = $stdin.gets
165
+ input = $stdin.gets&.force_encoding(Encoding::UTF_8)
165
166
  break if input.nil? || input.strip.empty?
166
167
 
167
168
  begin
@@ -209,7 +210,7 @@ module Parsanol
209
210
 
210
211
  def compile_source(file)
211
212
  tables = @tables_dir || default_tables_dir(file)
212
- document = Parsanol::PARG::Parser.new(File.read(file)).parse
213
+ document = Parsanol::PARG::Parser.new(PARG.read_utf8(file)).parse
213
214
  Parsanol::PARG::Imports.merge!(document, [File.dirname(file)])
214
215
  Parsanol::PARG::Compiler.compile(document, tables_dir: tables)
215
216
  end
@@ -40,9 +40,13 @@ module Parsanol
40
40
  end
41
41
  end
42
42
 
43
- def initialize(document, tables_dir)
43
+ def initialize(document, tables_dir, bindings: nil)
44
44
  @document = document
45
45
  @tables_dir = tables_dir
46
+ # Envelope v2 (parsanol-rs#143): grammar-to-model bindings bake
47
+ # into the checksummed envelope so every tier consumes one
48
+ # declaration. Data pass-through in phase 1.
49
+ @bindings = bindings
46
50
  @atom_cache = {}
47
51
  @first_cache = {}.compare_by_identity
48
52
  @ref_stack = []
@@ -254,7 +258,7 @@ module Parsanol
254
258
  end
255
259
 
256
260
  raw = if path.end_with?(".json")
257
- JSON.parse(File.read(path))
261
+ JSON.parse(PARG.read_utf8(path))
258
262
  else
259
263
  YAML.safe_load_file(path, aliases: true)
260
264
  end
@@ -396,6 +400,7 @@ module Parsanol
396
400
  [name, entry_envelope(rule)]
397
401
  end
398
402
  envelope = {
403
+ "bindings" => @bindings,
399
404
  "version" => @document.version,
400
405
  "grammar" => @document.grammar_name,
401
406
  "shape" => "parsanol-tree/v2",
@@ -19,7 +19,7 @@ module Parsanol
19
19
  raise CompileError, "import cycle: #{(merging + [name]).join(' -> ')}" if merging.include?(name)
20
20
 
21
21
  source = find_file(name, import_dirs)
22
- used = Parser.new(File.read(source)).parse
22
+ used = Parser.new(PARG.read_utf8(source)).parse
23
23
  merge!(used, import_dirs, merging + [name])
24
24
  merge_used(document, used, "#{name}.")
25
25
  end
@@ -32,7 +32,7 @@ module Parsanol
32
32
  def validate(source)
33
33
  raise ArtifactError, "parg artifact not found (set PG_ARTIFACT)" unless available?
34
34
 
35
- envelope = JSON.parse(File.read(artifact_path))
35
+ envelope = JSON.parse(PARG.read_utf8(artifact_path))
36
36
  artifact = Artifact.new(envelope, nil, nil)
37
37
  artifact.parse("file", source)
38
38
  end
data/lib/parsanol/parg.rb CHANGED
@@ -38,5 +38,12 @@ module Parsanol
38
38
  autoload :Imports, "parsanol/parg/imports"
39
39
  autoload :Suite, "parsanol/parg/authoring"
40
40
  autoload :Schema, "parsanol/parg/authoring"
41
+
42
+ # PARG sources and JSON artifacts are UTF-8 by definition: read them as
43
+ # UTF-8 whatever the locale's default external encoding, and skip a
44
+ # byte-order mark the way YAML.safe_load_file does (GH-121).
45
+ def self.read_utf8(path)
46
+ File.read(path, mode: "r:bom|utf-8")
47
+ end
41
48
  end
42
49
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parsanol
4
- VERSION = "1.3.61"
4
+ VERSION = "1.3.63"
5
5
  end
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.61
4
+ version: 1.3.63
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-01 00:00:00.000000000 Z
11
+ date: 2026-10-02 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