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 +4 -4
- data/Cargo.lock +94 -22
- data/exe/parsanol +7 -0
- data/ext/parsanol_cdylib/Cargo.toml +1 -1
- data/ext/parsanol_native/Cargo.toml +2 -2
- data/ext/parsanol_native/build.rs +9 -0
- data/ext/parsanol_native/extconf.rb +10 -0
- data/lib/parsanol/native/serializer.rb +5 -2
- data/lib/parsanol/parg/artifact.rb +213 -0
- data/lib/parsanol/parg/authoring.rb +110 -0
- data/lib/parsanol/parg/bindings.rb +129 -0
- data/lib/parsanol/parg/cli.rb +251 -0
- data/lib/parsanol/parg/compiler.rb +465 -0
- data/lib/parsanol/parg/derive.rb +28 -0
- data/lib/parsanol/parg/document.rb +69 -0
- data/lib/parsanol/parg/error.rb +18 -0
- data/lib/parsanol/parg/frontend.rb +173 -0
- data/lib/parsanol/parg/import.rb +34 -0
- data/lib/parsanol/parg/importers/abnf.rb +299 -0
- data/lib/parsanol/parg/importers/ebnf.rb +201 -0
- data/lib/parsanol/parg/importers/pest.rb +314 -0
- data/lib/parsanol/parg/imports.rb +87 -0
- data/lib/parsanol/parg/lexer.rb +72 -0
- data/lib/parsanol/parg/lints.rb +171 -0
- data/lib/parsanol/parg/lsp.rb +208 -0
- data/lib/parsanol/parg/lutaml.rb +67 -0
- data/lib/parsanol/parg/node.rb +20 -0
- data/lib/parsanol/parg/parser.rb +458 -0
- data/lib/parsanol/parg/preprocess.rb +36 -0
- data/lib/parsanol/parg/render.rb +44 -0
- data/lib/parsanol/parg/selfhost.rb +49 -0
- data/lib/parsanol/parg/visitor.rb +64 -0
- data/lib/parsanol/parg.rb +38 -0
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +7 -5
- data/lib/parsanol.rb +3 -0
- data/parsanol.gemspec +3 -1
- metadata +31 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dac1ca6df26172acdc4f7a676c018d1a9cf6107f5a2b6643b1274cbd56ba3b8f
|
|
4
|
+
data.tar.gz: bbeebb38922a236a170578538858e2f795e9da94679dd6c7a0cd81576d4542a7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
221
|
+
version = "0.3.106"
|
|
169
222
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
170
|
-
checksum = "
|
|
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.
|
|
259
|
+
version = "0.9.1"
|
|
207
260
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
208
|
-
checksum = "
|
|
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.
|
|
271
|
+
version = "0.9.1"
|
|
219
272
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
220
|
-
checksum = "
|
|
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.
|
|
310
|
+
version = "0.10.1"
|
|
258
311
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
259
|
-
checksum = "
|
|
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.
|
|
329
|
+
version = "0.10.1"
|
|
275
330
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
276
|
-
checksum = "
|
|
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.
|
|
577
|
+
version = "0.2.129"
|
|
506
578
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
507
|
-
checksum = "
|
|
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.
|
|
590
|
+
version = "0.2.129"
|
|
519
591
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
520
|
-
checksum = "
|
|
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.
|
|
600
|
+
version = "0.2.129"
|
|
529
601
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
530
|
-
checksum = "
|
|
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.
|
|
613
|
+
version = "0.2.129"
|
|
542
614
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
543
|
-
checksum = "
|
|
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.
|
|
634
|
+
version = "0.8.59"
|
|
563
635
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
564
|
-
checksum = "
|
|
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.
|
|
643
|
+
version = "0.8.59"
|
|
572
644
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
573
|
-
checksum = "
|
|
645
|
+
checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
|
|
574
646
|
dependencies = [
|
|
575
647
|
"proc-macro2",
|
|
576
648
|
"quote",
|
data/exe/parsanol
ADDED
|
@@ -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 (
|
|
31
|
-
parsanol = { version = "0.
|
|
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
|
-
#
|
|
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
|