parsanol 1.3.56 → 1.3.57

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: 9fd20e8200bbd5f3eadcab0100d7854cd97cafdea7a2a04c4b6fda000c3a46c5
4
- data.tar.gz: d35a9d04dbb252eb567ddd4a267d1603a2e3ec538f32e866ad32bb6adcaf11a8
3
+ metadata.gz: dac1ca6df26172acdc4f7a676c018d1a9cf6107f5a2b6643b1274cbd56ba3b8f
4
+ data.tar.gz: bbeebb38922a236a170578538858e2f795e9da94679dd6c7a0cd81576d4542a7
5
5
  SHA512:
6
- metadata.gz: 7432ac5ea256ecc15bc615dd342c38bf0912fca1cda343b7dcbeafb05e636b9461f110e0d15830f81434e426eccd62c353fc786eee21f1b51e94372ed3adf051
7
- data.tar.gz: 609460b69aee699d6fd03d7ee90047f22f8d990e6abcd60e3bd8ebf48c99c853a94fd4d9290823069a517741fb3d8f7ff57c86d73a26746809eb35d1d223727b
6
+ metadata.gz: 9170d4b42f82853d41741475abb92fe26f126fde281b4c1b2f31b6b6004dab21fa7a9a9a23b580b83c86f89978dd021767d5464c79080ef19d9ba68771cd7d23
7
+ data.tar.gz: 633a3b087e6b6d6656e8ddd0db3c0548f6985ea72e43da51537a95d07c480957397b5fe6eda56b8651a2d302cb6b886b5661fc2446dfc4aceb2943a9c31ba2a5
data/Cargo.lock CHANGED
@@ -54,6 +54,15 @@ version = "2.13.2"
54
54
  source = "registry+https://github.com/rust-lang/crates.io-index"
55
55
  checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
56
56
 
57
+ [[package]]
58
+ name = "block-buffer"
59
+ version = "0.12.1"
60
+ source = "registry+https://github.com/rust-lang/crates.io-index"
61
+ checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
62
+ dependencies = [
63
+ "hybrid-array",
64
+ ]
65
+
57
66
  [[package]]
58
67
  name = "bumpalo"
59
68
  version = "3.20.3"
@@ -86,6 +95,41 @@ dependencies = [
86
95
  "libloading",
87
96
  ]
88
97
 
98
+ [[package]]
99
+ name = "const-oid"
100
+ version = "0.10.2"
101
+ source = "registry+https://github.com/rust-lang/crates.io-index"
102
+ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
103
+
104
+ [[package]]
105
+ name = "cpufeatures"
106
+ version = "0.3.1"
107
+ source = "registry+https://github.com/rust-lang/crates.io-index"
108
+ checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
109
+ dependencies = [
110
+ "libc",
111
+ ]
112
+
113
+ [[package]]
114
+ name = "crypto-common"
115
+ version = "0.2.2"
116
+ source = "registry+https://github.com/rust-lang/crates.io-index"
117
+ checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
118
+ dependencies = [
119
+ "hybrid-array",
120
+ ]
121
+
122
+ [[package]]
123
+ name = "digest"
124
+ version = "0.11.3"
125
+ source = "registry+https://github.com/rust-lang/crates.io-index"
126
+ checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
127
+ dependencies = [
128
+ "block-buffer",
129
+ "const-oid",
130
+ "crypto-common",
131
+ ]
132
+
89
133
  [[package]]
90
134
  name = "either"
91
135
  version = "1.18.0"
@@ -148,6 +192,15 @@ dependencies = [
148
192
  "foldhash",
149
193
  ]
150
194
 
195
+ [[package]]
196
+ name = "hybrid-array"
197
+ version = "0.4.15"
198
+ source = "registry+https://github.com/rust-lang/crates.io-index"
199
+ checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
200
+ dependencies = [
201
+ "typenum",
202
+ ]
203
+
151
204
  [[package]]
152
205
  name = "itertools"
153
206
  version = "0.13.0"
@@ -165,9 +218,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
165
218
 
166
219
  [[package]]
167
220
  name = "js-sys"
168
- version = "0.3.105"
221
+ version = "0.3.106"
169
222
  source = "registry+https://github.com/rust-lang/crates.io-index"
170
- checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
223
+ checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
171
224
  dependencies = [
172
225
  "cfg-if",
173
226
  "wasm-bindgen",
@@ -203,9 +256,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
203
256
 
204
257
  [[package]]
205
258
  name = "magnus"
206
- version = "0.9.0"
259
+ version = "0.9.1"
207
260
  source = "registry+https://github.com/rust-lang/crates.io-index"
208
- checksum = "379d50b6eea4ca84074ccd3e551eb9b07f7865194d5b3198325317e9ef78e5e9"
261
+ checksum = "4c01bef153b8d0f086991f0e166474a1d57b738221f9a9c11405d498ac95a44d"
209
262
  dependencies = [
210
263
  "magnus-macros",
211
264
  "rb-sys",
@@ -215,9 +268,9 @@ dependencies = [
215
268
 
216
269
  [[package]]
217
270
  name = "magnus-macros"
218
- version = "0.9.0"
271
+ version = "0.9.1"
219
272
  source = "registry+https://github.com/rust-lang/crates.io-index"
220
- checksum = "4ca70566af98184bb92d4d9853daaf533c1a3418b9570d0547ea44970035f3fe"
273
+ checksum = "9dafde2eeb66f090e0f326112323eb8324b12cb32b5d1210e541d3df6b0ce77e"
221
274
  dependencies = [
222
275
  "proc-macro2",
223
276
  "quote",
@@ -254,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
254
307
 
255
308
  [[package]]
256
309
  name = "parsanol"
257
- version = "0.8.11"
310
+ version = "0.10.1"
258
311
  source = "registry+https://github.com/rust-lang/crates.io-index"
259
- checksum = "c28a0cd3b3e2491ef72c99f3dc59ef25ea2e0f13b8cd6ea31a05eb085a33f801"
312
+ checksum = "3201f3b6340113ce57871a1405679ad7520a420cb8958777983371a2d4e6bf55"
260
313
  dependencies = [
261
314
  "ahash",
262
315
  "getrandom 0.3.4",
@@ -265,15 +318,17 @@ dependencies = [
265
318
  "magnus",
266
319
  "parsanol-derive",
267
320
  "regex",
321
+ "regex-syntax",
268
322
  "serde",
269
323
  "serde_json",
324
+ "sha2",
270
325
  ]
271
326
 
272
327
  [[package]]
273
328
  name = "parsanol-derive"
274
- version = "0.8.11"
329
+ version = "0.10.1"
275
330
  source = "registry+https://github.com/rust-lang/crates.io-index"
276
- checksum = "0ccfdf8325c4890930bbc1510bc9dfc929d485d1aa6c29cb563c5dc77d7a0080"
331
+ checksum = "07bea1e5fb6a78314596116ac62e08ab5c73c201c62cd83715e75309c44e0977"
277
332
  dependencies = [
278
333
  "proc-macro2",
279
334
  "quote",
@@ -445,6 +500,17 @@ dependencies = [
445
500
  "zmij",
446
501
  ]
447
502
 
503
+ [[package]]
504
+ name = "sha2"
505
+ version = "0.11.0"
506
+ source = "registry+https://github.com/rust-lang/crates.io-index"
507
+ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
508
+ dependencies = [
509
+ "cfg-if",
510
+ "cpufeatures",
511
+ "digest",
512
+ ]
513
+
448
514
  [[package]]
449
515
  name = "shell-words"
450
516
  version = "1.1.1"
@@ -479,6 +545,12 @@ dependencies = [
479
545
  "unicode-ident",
480
546
  ]
481
547
 
548
+ [[package]]
549
+ name = "typenum"
550
+ version = "1.20.1"
551
+ source = "registry+https://github.com/rust-lang/crates.io-index"
552
+ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
553
+
482
554
  [[package]]
483
555
  name = "unicode-ident"
484
556
  version = "1.0.26"
@@ -502,9 +574,9 @@ dependencies = [
502
574
 
503
575
  [[package]]
504
576
  name = "wasm-bindgen"
505
- version = "0.2.128"
577
+ version = "0.2.129"
506
578
  source = "registry+https://github.com/rust-lang/crates.io-index"
507
- checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
579
+ checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
508
580
  dependencies = [
509
581
  "cfg-if",
510
582
  "once_cell",
@@ -515,9 +587,9 @@ dependencies = [
515
587
 
516
588
  [[package]]
517
589
  name = "wasm-bindgen-macro"
518
- version = "0.2.128"
590
+ version = "0.2.129"
519
591
  source = "registry+https://github.com/rust-lang/crates.io-index"
520
- checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
592
+ checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
521
593
  dependencies = [
522
594
  "quote",
523
595
  "wasm-bindgen-macro-support",
@@ -525,9 +597,9 @@ dependencies = [
525
597
 
526
598
  [[package]]
527
599
  name = "wasm-bindgen-macro-support"
528
- version = "0.2.128"
600
+ version = "0.2.129"
529
601
  source = "registry+https://github.com/rust-lang/crates.io-index"
530
- checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
602
+ checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
531
603
  dependencies = [
532
604
  "bumpalo",
533
605
  "proc-macro2",
@@ -538,9 +610,9 @@ dependencies = [
538
610
 
539
611
  [[package]]
540
612
  name = "wasm-bindgen-shared"
541
- version = "0.2.128"
613
+ version = "0.2.129"
542
614
  source = "registry+https://github.com/rust-lang/crates.io-index"
543
- checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
615
+ checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
544
616
  dependencies = [
545
617
  "unicode-ident",
546
618
  ]
@@ -559,18 +631,18 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
559
631
 
560
632
  [[package]]
561
633
  name = "zerocopy"
562
- version = "0.8.57"
634
+ version = "0.8.59"
563
635
  source = "registry+https://github.com/rust-lang/crates.io-index"
564
- checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"
636
+ checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
565
637
  dependencies = [
566
638
  "zerocopy-derive",
567
639
  ]
568
640
 
569
641
  [[package]]
570
642
  name = "zerocopy-derive"
571
- version = "0.8.57"
643
+ version = "0.8.59"
572
644
  source = "registry+https://github.com/rust-lang/crates.io-index"
573
- checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"
645
+ checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
574
646
  dependencies = [
575
647
  "proc-macro2",
576
648
  "quote",
data/exe/parsanol ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "parsanol"
5
+ require "parsanol/parg/cli"
6
+ argv = ARGV[0] == "parg" ? ARGV[1..] : ARGV
7
+ exit Parsanol::PARG::CLI.run(argv)
@@ -12,4 +12,4 @@ crate-type = ["cdylib"]
12
12
  doc = false
13
13
 
14
14
  [dependencies]
15
- parsanol = { version = "0.8.8" }
15
+ parsanol = { version = "0.10" }
@@ -27,8 +27,8 @@ rb-sys = { version = "0.9.124", features = ["global-allocator"] }
27
27
  # magnus for Ruby bindings (from crates.io, patched locally for Ruby 4.0 support)
28
28
  magnus = { version = "0.9" }
29
29
 
30
- # parsanol parser library (from git for latest features)
31
- parsanol = { version = "0.8.9", features = ["ruby"] }
30
+ # parsanol parser library (crates.io; 0.10 carries the class fast-path fix)
31
+ parsanol = { version = "0.10", features = ["ruby"] }
32
32
 
33
33
  # Logging
34
34
  log = "0.4"
@@ -0,0 +1,9 @@
1
+ fn main() {
2
+ // MRI loadables on macOS resolve ruby symbols from the host process
3
+ // at load time; cargo's cdylib link defaults to failing on undefined
4
+ // symbols, so the flag must come from the top-level package.
5
+ if cfg!(target_os = "macos") {
6
+ println!("cargo:rustc-link-arg=-Wl,-undefined,dynamic_lookup");
7
+ }
8
+ println!("cargo:rerun-if-changed=build.rs");
9
+ }
@@ -30,6 +30,16 @@ create_rust_makefile("parsanol/parsanol_native") do |r|
30
30
  # Create debug builds in dev, release in production
31
31
  r.profile = ENV.fetch("RB_SYS_CARGO_PROFILE", :dev).to_sym
32
32
 
33
+ # cargo rustc builds dependency cdylibs verbatim, and parsanol ships
34
+ # a cdylib crate-type (the ffi tier). On macOS every dylib in the
35
+ # build resolves ruby symbols from the host process at load time, so
36
+ # every link must tolerate undefined symbols. (extconf runs as a
37
+ # subprocess, so this must be baked into the Makefile — env changes
38
+ # here would not survive it.)
39
+ if RbConfig::CONFIG["host_os"].include?("darwin")
40
+ r.extra_rustflags = %w[-C link-arg=-Wl,-undefined,dynamic_lookup]
41
+ end
42
+
33
43
  # Enable stable API compiled fallback for ruby-head and older Ruby versions
34
44
  r.use_stable_api_compiled_fallback = true
35
45
 
@@ -95,9 +95,12 @@ module Parsanol
95
95
 
96
96
  def serialize_re(atom)
97
97
  # Ruby's Regexp#to_s produces "(?-mix:pattern)" format
98
- # We need to extract just the pattern for the Rust parser
98
+ # The pattern must reach the Rust regex engine verbatim, including
99
+ # inline flag groups: stripping (?i:...) silently turned every
100
+ # case-insensitive regex into a case-sensitive one on the native
101
+ # path (the Ruby atom still compiled it with the flag, so the two
102
+ # engines disagreed). Rust regex understands the same inline groups.
99
103
  pattern = atom.match
100
- pattern = ::Regexp.last_match(1) if pattern =~ /^\(\?[-mix]*:(.+)\)$/
101
104
  {
102
105
  "Re" => {
103
106
  "pattern" => pattern,
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+
6
+ module Parsanol
7
+ module PARG
8
+ # A compiled artifact envelope: load, verify checksum, parse inputs.
9
+ #
10
+ # The envelope's grammar section is the portable JSON the native engines
11
+ # consume; the embedded PARG source lets a Ruby runtime recompile the atom
12
+ # tree locally without any other dependency.
13
+ class Artifact
14
+ attr_reader :envelope, :path, :tables_dir
15
+
16
+ def self.load(path, tables_dir: nil)
17
+ envelope = JSON.parse(File.read(path))
18
+ new(envelope, path, tables_dir || File.dirname(path))
19
+ end
20
+
21
+ def self.from_json(text, tables_dir: nil)
22
+ new(JSON.parse(text), nil, tables_dir)
23
+ end
24
+
25
+ SUPPORTED_SHAPE = "parsanol-tree/v2"
26
+
27
+ def initialize(envelope, path, tables_dir)
28
+ unless envelope["shape"] == SUPPORTED_SHAPE
29
+ raise ArtifactError,
30
+ "unsupported artifact shape #{envelope['shape'].inspect} " \
31
+ "(this runtime implements #{SUPPORTED_SHAPE.inspect})"
32
+ end
33
+ @envelope = envelope
34
+ @path = path
35
+ @tables_dir = tables_dir
36
+ verify_checksum!
37
+ @compiler = nil
38
+ end
39
+
40
+ def version = envelope["version"]
41
+
42
+ def entries = envelope["entries"].keys
43
+
44
+ def entry(name)
45
+ envelope["entries"].fetch(name) do
46
+ raise ArtifactError, "unknown entry #{name.inspect}"
47
+ end
48
+ end
49
+
50
+ # Parse via the native engine: the envelope's grammar section is the
51
+ # exact portable JSON the Rust side registers, so the artifact parse
52
+ # path is a straight register-and-run — no Ruby recompilation. The
53
+ # Ruby atom runtime (mode: :ruby) remains available explicitly, and
54
+ # serves as the fallback on platforms without the extension.
55
+ def parse(entry_name, input, mode: :native)
56
+ case mode
57
+ when :native
58
+ if Native.available?
59
+ return Native.parse(JSON.generate(entry(entry_name).fetch("grammar")), input)
60
+ end
61
+
62
+ root_atom(entry_name).parse(input)
63
+ when :ruby
64
+ root_atom(entry_name).parse(input)
65
+ else
66
+ raise ArgumentError, "unknown mode #{mode.inspect} (use :native or :ruby)"
67
+ end
68
+ end
69
+
70
+ def apply_bindings(entry_name, shape)
71
+ Bindings.apply(self, entry(entry_name), shape)
72
+ end
73
+
74
+ # Render the identifier string from a bound attribute map (F6).
75
+ def render(_entry_name, bound, variant: "default")
76
+ Render.apply(envelope["render"] || {}, variant, bound)
77
+ end
78
+
79
+ def render_string(entry_name, input, variant: "default")
80
+ render(entry_name, parse_and_bind(entry_name, input), variant: variant)
81
+ end
82
+
83
+ # Evaluate a named derive spec against a bound map (F6).
84
+ def derive(name, bound)
85
+ Derive.apply(envelope["derive"] || {}, name, bound)
86
+ end
87
+
88
+ def derive_string(entry_name, input, name)
89
+ derive(name, parse_and_bind(entry_name, input))
90
+ end
91
+
92
+ def parse_and_bind(entry_name, input, mode: :native)
93
+ apply_bindings(entry_name, parse(entry_name, input, mode: mode))
94
+ end
95
+
96
+ # Re-run the grammar's in-file tests against this artifact. Returns
97
+ # the failure descriptions; empty means every test passes.
98
+ def run_tests(mode: :native)
99
+ run_test_list(envelope.fetch("tests", []), mode: mode)
100
+ end
101
+
102
+ # Run an external test list (suite files yield Document::Test
103
+ # structs; embedded tests are artifact-shaped hashes).
104
+ def run_test_list(tests, mode: :native)
105
+ tests.filter_map do |test|
106
+ hash = test.is_a?(Document::Test) ? test_to_hash(test) : test
107
+ entry_name = hash["entry"] || entries.first
108
+ begin
109
+ bound = parse_and_bind(entry_name, hash["input"], mode: mode)
110
+ if hash["kind"] == "reject"
111
+ "test #{hash['input'].inspect}: expected the input to be rejected"
112
+ elsif hash["kind"] == "example"
113
+ expect = hash["expect"].to_h { |key, value| [key.to_sym, value] }
114
+ mismatched = expect.reject { |key, value| bound.key?(key) && bound[key] == value }
115
+ next if mismatched.empty?
116
+
117
+ "test #{hash['input'].inspect}: expected captures " \
118
+ "#{mismatched.transform_values(&:inspect).inspect}, got #{bound.inspect}"
119
+ end
120
+ rescue Parsanol::ParseFailed
121
+ unless hash["kind"] == "reject"
122
+ "test #{hash['input'].inspect}: expected the input to parse"
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ def test_to_hash(test)
129
+ {
130
+ "entry" => test.entry,
131
+ "kind" => test.kind.to_s,
132
+ "input" => test.input,
133
+ "expect" => test.expect,
134
+ }
135
+ end
136
+
137
+ # Rule documentation embedded from ## doc comments.
138
+ def rule_docs = envelope.fetch("docs", {})
139
+
140
+ # Structured parse diagnostics (PN 2): {offset, message} for the
141
+ # deepest failure of the most recent parse attempt on this entry.
142
+ def parse_with_diagnostics(entry_name, input, mode: :native)
143
+ shape = parse(entry_name, input, mode: mode)
144
+ { "ok" => true, "offset" => nil, "message" => nil, "shape" => shape }
145
+ rescue Parsanol::ParseFailed => e
146
+ cause = deepest_cause(e.parse_failure_cause)
147
+ { "ok" => false, "offset" => cause&.position,
148
+ "message" => cause&.message || e.message, "shape" => nil }
149
+ end
150
+
151
+ def deepest_cause(cause)
152
+ return cause if cause.nil? || cause.children.empty?
153
+
154
+ deepest = cause.children.filter_map { |child| deepest_cause(child) }
155
+ .max_by { |node| node.position.to_i }
156
+ (deepest&.position.to_i >= cause.position.to_i ? deepest : cause)
157
+ end
158
+
159
+ def table_rows(name)
160
+ @table_cache ||= {}
161
+ return @table_cache[name] if @table_cache.key?(name)
162
+
163
+ declared = envelope["tables"].fetch(name) do
164
+ raise ArtifactError, "artifact does not declare table #{name.inspect}"
165
+ end
166
+ if declared.is_a?(Hash) && declared.key?("rows")
167
+ return @table_cache[name] = declared["rows"]
168
+ end
169
+
170
+ file = declared
171
+ 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)
173
+ rows = case raw
174
+ when Hash
175
+ raw.map { |key, value| { "name" => key.to_s }.merge(value.to_h.transform_keys(&:to_s)) }
176
+ when Array then raw.map { |row| row.to_h.transform_keys(&:to_s) }
177
+ else raise ArtifactError, "table #{name.inspect} must be a map or array"
178
+ end
179
+ @table_cache[name] = rows
180
+ end
181
+
182
+ def compiler
183
+ @compiler ||= begin
184
+ document = Parser.new(envelope.fetch("source")).parse
185
+ # `use`-importing grammars carry dotted cross-grammar rule
186
+ # references that only resolve once the imported sources merge;
187
+ # without this the pure-Ruby path fails on artifacts whose
188
+ # native atom graphs inline everything. The imported sources
189
+ # sit beside the artifact (vendors ship them together).
190
+ imports_dir = path && File.directory?(File.dirname(path)) ? File.dirname(path) : nil
191
+ Imports.merge!(document, [imports_dir].compact) if imports_dir
192
+ Compiler.new(document, @tables_dir)
193
+ end
194
+ end
195
+
196
+ def root_atom(entry_name)
197
+ rule = entry(entry_name).fetch("root")
198
+ @compiled ||= {}
199
+ @compiled[rule] ||= compiler.atom_for(rule)
200
+ end
201
+
202
+ def verify_checksum!
203
+ stored = envelope["checksum"]
204
+ computed = Compiler.checksum(envelope)
205
+ return if stored == computed
206
+
207
+ raise ArtifactError,
208
+ "artifact checksum mismatch: stored #{stored.inspect}, " \
209
+ "computed #{computed.inspect}"
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Parsanol
6
+ module PARG
7
+ # External test suites: *.pgtest files carrying the same test syntax
8
+ # as the inline test section (accept / reject / example). Suites are
9
+ # development-time corpora — they iterate a grammar against its
10
+ # binding requirements without being embedded in the artifact.
11
+ #
12
+ # suite smoke for entry identifier {
13
+ # accept "ISO 8601-1:2019"
14
+ # example "ISO/CD 12345" { stage: "draft20" }
15
+ # }
16
+ module Suite
17
+ module_function
18
+
19
+ # Load every *.pgtest file in a directory.
20
+ # Returns { suite_name => [Document::Test, ...] }.
21
+ def load(dir)
22
+ Dir.glob(File.join(dir, "*.pgtest")).to_h do |file|
23
+ [File.basename(file, ".pgtest"), read_file(file)]
24
+ end
25
+ end
26
+
27
+ def read_file(file)
28
+ content = File.read(file).gsub(/^#[^\n]*\n/, "")
29
+ header = content.match(/suite\s+\w+(?:\s+for\s+entry\s+(?<entry>\w+))?\s*\{/)
30
+ unless header
31
+ raise ParseError,
32
+ "#{file}: expected 'suite <name> [for <entry>] { ... }'"
33
+ end
34
+
35
+ body = content[header.end(0)..].sub(/\}\s*\z/, "")
36
+ document = Parser.new("grammar Suite version \"0.0.0\" {\n}\ntest {\n#{body}\n}\n").parse
37
+ document.tests.map do |test|
38
+ Document::Test.new(header[:entry] || test.entry, test.kind, test.input, test.expect)
39
+ end
40
+ end
41
+ end
42
+
43
+ # The binding-requirements schema: what a language-specific data model
44
+ # must implement for an entry. Derived from the declared bindings plus
45
+ # the observed example values.
46
+ module Schema
47
+ module_function
48
+
49
+ def from_artifact(artifact)
50
+ artifact.entries.to_h do |entry|
51
+ entry_data = artifact.entry(entry)
52
+ fields = entry_data["bindings"].to_h do |binding|
53
+ requirements = {
54
+ "type" => binding["type"],
55
+ "card" => binding["card"] || "1",
56
+ }
57
+ requirements["preprocess"] = binding["preprocess"] if binding["preprocess"]
58
+ [binding["path"], requirements]
59
+ end
60
+ examples = artifact.envelope.fetch("tests", [])
61
+ .select { |test| (test["kind"] == "example" && test["entry"].nil?) || test["entry"] == entry }
62
+ .map { |test| { "input" => test["input"], "captures" => test["expect"] } }
63
+ [entry, { "root" => entry_data["root"], "fields" => fields, "examples" => examples }]
64
+ end
65
+ end
66
+
67
+ # TypeScript materialization types: one interface per entry, one
68
+ # property per bound field. Cards ending in * become arrays;
69
+ # 0..1 becomes nullable.
70
+ def to_typescript(schema)
71
+ lines = ["// Generated by parsanol parg schema --ts — binding-requirements types."]
72
+ schema.each do |entry, data|
73
+ lines << ""
74
+ lines << "export interface #{ts_name(entry)} {"
75
+ data["fields"].each do |path, requirements|
76
+ lines << " #{ts_prop(path)}: #{ts_type(requirements)};"
77
+ end
78
+ lines << "}"
79
+ end
80
+ "#{lines.join("\n")}\n"
81
+ end
82
+
83
+ def ts_name(entry)
84
+ entry.split(/[_.]/).map(&:capitalize).join
85
+ end
86
+
87
+ def ts_prop(path)
88
+ leaf = path.include?("[]") ? path.split("].").last : path
89
+ leaf.gsub(/([A-Za-z0-9])_([a-z])/) do
90
+ "#{Regexp.last_match(1)}#{Regexp.last_match(2).upcase}"
91
+ end
92
+ end
93
+
94
+ def ts_type(requirements)
95
+ base = case requirements["type"]
96
+ when "integer", "float" then "number"
97
+ when "boolean" then "boolean"
98
+ else "string"
99
+ end
100
+ if requirements["card"].to_s.end_with?("*")
101
+ "#{base}[]"
102
+ elsif requirements["card"] == "0..1"
103
+ "#{base} | null"
104
+ else
105
+ base
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end