parsanol 1.3.61 → 1.3.63
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 +4 -4
- data/lib/parsanol/native.rb +18 -9
- data/lib/parsanol/parg/artifact.rb +54 -4
- data/lib/parsanol/parg/authoring.rb +1 -1
- data/lib/parsanol/parg/cli.rb +5 -4
- data/lib/parsanol/parg/compiler.rb +7 -2
- data/lib/parsanol/parg/imports.rb +1 -1
- data/lib/parsanol/parg/selfhost.rb +1 -1
- data/lib/parsanol/parg.rb +7 -0
- data/lib/parsanol/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f7ad59a2f852639eec49d8aaf0cfce4be63d8e13a20941b7db8f325244445e47
|
|
4
|
+
data.tar.gz: b4f7021d4c4dd9f30df4eddf64b2109da9005291b4d39edadd5075e54c6600c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5fe08eb23fa452e06432cb8b72cb101b257b854fb33ae71455fd8c2a0d81356704f66fd11af330392783aa6c6a5b4730df1884718459c5d4acfe3abc74b8b48
|
|
7
|
+
data.tar.gz: b2d8251fc66c8d47085bf6338457975b6f29e707a173c8e107d5755137bd850ead86c6d130b6410b29e4155e87ae4c67cac3739278f0ddd4a7826fc94ffba53e
|
data/lib/parsanol/native.rb
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
229
|
-
msg =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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, "
|
|
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(
|
|
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 =
|
|
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,
|
data/lib/parsanol/parg/cli.rb
CHANGED
|
@@ -36,7 +36,8 @@ module Parsanol
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def initialize(argv)
|
|
39
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
data/lib/parsanol/version.rb
CHANGED