parsanol 1.3.108 → 1.3.112
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 +4 -4
- data/ext/parsanol_cdylib/Cargo.toml +1 -1
- data/ext/parsanol_native/Cargo.toml +1 -1
- data/lib/parsanol/atoms/repetition.rb +22 -0
- data/lib/parsanol/parg/artifact.rb +37 -8
- data/lib/parsanol/parg/authoring.rb +196 -5
- data/lib/parsanol/parg/cli.rb +108 -0
- data/lib/parsanol/parg/compiler.rb +263 -5
- data/lib/parsanol/parg/diff.rb +264 -0
- data/lib/parsanol/parg/document.rb +15 -2
- data/lib/parsanol/parg/export.rb +191 -0
- data/lib/parsanol/parg/import.rb +3 -1
- data/lib/parsanol/parg/importers/abnf.rb +148 -28
- data/lib/parsanol/parg/importers/antlr.rb +407 -0
- data/lib/parsanol/parg/lexer.rb +1 -0
- data/lib/parsanol/parg/lints.rb +448 -1
- data/lib/parsanol/parg/parser.rb +76 -2
- data/lib/parsanol/parg.rb +2 -0
- data/lib/parsanol/slice.rb +13 -4
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +50 -24
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 683289b7d0453a88c2dfb691d54479ebacf6be93cc30f527c4199557bb99b48d
|
|
4
|
+
data.tar.gz: '0182a60c9bca2c3c1fcf1f450fed4623aeb915fb3b3e80f3a14ee716c09f3ffd'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15607095c23776f36bf34dcf4e53849a739cd373e9a31a766c484331fd998a01b4d66faebd528fb61077d648e58e7e35173e6ec7e3280640e1b2eaee17a884cc
|
|
7
|
+
data.tar.gz: 8fc517dc729088dcac71596911ed5a57a8b72c580d246b315a1b8313cb0157ef7ee63b04b20d723583f03ac4699af4810ca1cb61e5c91d9aed1514fe68806a27
|
data/Cargo.lock
CHANGED
|
@@ -307,9 +307,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
307
307
|
|
|
308
308
|
[[package]]
|
|
309
309
|
name = "parsanol"
|
|
310
|
-
version = "0.
|
|
310
|
+
version = "0.19.6"
|
|
311
311
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
312
|
-
checksum = "
|
|
312
|
+
checksum = "8ffe7e92d626c65e83d2c53c148a18db5897879858bcd37ebd0f406f5a251036"
|
|
313
313
|
dependencies = [
|
|
314
314
|
"ahash",
|
|
315
315
|
"getrandom 0.3.4",
|
|
@@ -326,9 +326,9 @@ dependencies = [
|
|
|
326
326
|
|
|
327
327
|
[[package]]
|
|
328
328
|
name = "parsanol-derive"
|
|
329
|
-
version = "0.
|
|
329
|
+
version = "0.19.6"
|
|
330
330
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
331
|
-
checksum = "
|
|
331
|
+
checksum = "3b94bb50df3fe1a9f66b6d8ee784672c27262e9e609568d5e9d14e1f7d6bec91"
|
|
332
332
|
dependencies = [
|
|
333
333
|
"proc-macro2",
|
|
334
334
|
"quote",
|
|
@@ -32,7 +32,7 @@ magnus = { version = "0.9" }
|
|
|
32
32
|
# parsanol parser library (crates.io; 0.17.0 carries the LookSpec regex
|
|
33
33
|
# lookbehind (rs#163 gap 1) and the widened opaque writeback: the
|
|
34
34
|
# lazy-continuation fix, parsanol-ruby#160)
|
|
35
|
-
parsanol = { version = "0.
|
|
35
|
+
parsanol = { version = "0.19.1", features = ["ruby"] }
|
|
36
36
|
|
|
37
37
|
# Logging
|
|
38
38
|
log = "0.4"
|
|
@@ -195,8 +195,19 @@ module Parsanol
|
|
|
195
195
|
|
|
196
196
|
unless success
|
|
197
197
|
last_failure = value
|
|
198
|
+
if ENV["PARSANOL_REP_TRACE"]
|
|
199
|
+
warn "REP body FAIL at iter=#{occurrence} start=#{iteration_start} pos_after=#{source.bytepos} restoring"
|
|
200
|
+
end
|
|
201
|
+
# A failed iteration may have consumed input before its own
|
|
202
|
+
# inner failure (an ordered choice's partial alternative);
|
|
203
|
+
# the repetition owns the rollback — the rep ends at the
|
|
204
|
+
# last SUCCESSFUL iteration's end (ruby#357).
|
|
205
|
+
source.bytepos = iteration_start
|
|
198
206
|
break
|
|
199
207
|
end
|
|
208
|
+
if ENV["PARSANOL_REP_TRACE"]
|
|
209
|
+
warn "REP body OK at iter=#{occurrence} start=#{iteration_start} end=#{source.bytepos}"
|
|
210
|
+
end
|
|
200
211
|
|
|
201
212
|
occurrence += 1
|
|
202
213
|
result[occurrence] = value
|
|
@@ -276,8 +287,19 @@ module Parsanol
|
|
|
276
287
|
|
|
277
288
|
unless success
|
|
278
289
|
last_failure = value
|
|
290
|
+
if ENV["PARSANOL_REP_TRACE"]
|
|
291
|
+
warn "REP body FAIL at iter=#{occurrence} start=#{iteration_start} pos_after=#{source.bytepos} restoring"
|
|
292
|
+
end
|
|
293
|
+
# A failed iteration may have consumed input before its own
|
|
294
|
+
# inner failure (an ordered choice's partial alternative);
|
|
295
|
+
# the repetition owns the rollback — the rep ends at the
|
|
296
|
+
# last SUCCESSFUL iteration's end (ruby#357).
|
|
297
|
+
source.bytepos = iteration_start
|
|
279
298
|
break
|
|
280
299
|
end
|
|
300
|
+
if ENV["PARSANOL_REP_TRACE"]
|
|
301
|
+
warn "REP body OK at iter=#{occurrence} start=#{iteration_start} end=#{source.bytepos}"
|
|
302
|
+
end
|
|
281
303
|
|
|
282
304
|
occurrence += 1
|
|
283
305
|
result[occurrence] = value
|
|
@@ -26,6 +26,10 @@ module Parsanol
|
|
|
26
26
|
|
|
27
27
|
SUPPORTED_SHAPE = "parsanol-tree/v2"
|
|
28
28
|
|
|
29
|
+
# The editions this runtime parses (ruby#357's consolidation):
|
|
30
|
+
# v1 = the legacy shapes; v2 = named trees + explicit spans.
|
|
31
|
+
SUPPORTED_EDITIONS = %w[v1 v2].freeze
|
|
32
|
+
|
|
29
33
|
def initialize(envelope, path, tables_dir, imports_dir: nil)
|
|
30
34
|
unless envelope["shape"] == SUPPORTED_SHAPE
|
|
31
35
|
raise ArtifactError,
|
|
@@ -38,6 +42,18 @@ module Parsanol
|
|
|
38
42
|
bindings = envelope["bindings"]
|
|
39
43
|
validate_bindings!(bindings) unless bindings.nil?
|
|
40
44
|
@envelope = envelope
|
|
45
|
+
# ruby#353: absent field = the pre-1.3.109 semantics. A level
|
|
46
|
+
# above this runtime's is a hard error (the trees would carry
|
|
47
|
+
# nodes this engine never emits); a lower one warns — parsing
|
|
48
|
+
# works, but frozen tree comparisons will differ.
|
|
49
|
+
# The declared edition must be one this runtime understands
|
|
50
|
+
# (ruby#357's consolidation: one axis, `edition`, values v1/v2).
|
|
51
|
+
edition = envelope["edition"] || "v1"
|
|
52
|
+
unless SUPPORTED_EDITIONS.include?(edition)
|
|
53
|
+
raise ArtifactError,
|
|
54
|
+
"artifact declares edition #{edition.inspect}; this " \
|
|
55
|
+
"runtime supports #{SUPPORTED_EDITIONS.map(&:inspect).join(', ')}"
|
|
56
|
+
end
|
|
41
57
|
@path = path
|
|
42
58
|
@tables_dir = tables_dir
|
|
43
59
|
# parsanol-ruby#208: `use`-importing grammars need the imported
|
|
@@ -233,13 +249,27 @@ module Parsanol
|
|
|
233
249
|
# initial parse's (the native FFI's coarse failure position
|
|
234
250
|
# loses the deepest offset).
|
|
235
251
|
captures, reparse_error = recovery_journal(entry_name, input, mode)
|
|
236
|
-
|
|
252
|
+
# The spine boundary, not the failure offset, is where the
|
|
253
|
+
# resync scan starts (rs#304 convergence): junk frequently lies
|
|
254
|
+
# BEFORE the deepest failure (the failed iteration consumed its
|
|
255
|
+
# own prefix), and the scan from the failure offset can never
|
|
256
|
+
# match it. When no iteration matched, the boundary is 0 —
|
|
257
|
+
# recovery still runs (rs#304 Gap B: a declared pattern is
|
|
258
|
+
# usable from the start).
|
|
259
|
+
stash = Parsanol::Recovery.take_partial
|
|
260
|
+
partial = recovery_partial(entry_name, stash)
|
|
261
|
+
boundary = stash&.first || 0
|
|
237
262
|
resyncs = []
|
|
238
|
-
if
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
items
|
|
263
|
+
if reparse_error
|
|
264
|
+
root_name = entry(entry_name)["root"].to_sym
|
|
265
|
+
items = partial&.values&.first
|
|
266
|
+
created_partial = items.nil?
|
|
267
|
+
items ||= []
|
|
268
|
+
extended, resyncs = resync_extension(entry_name, input, boundary)
|
|
269
|
+
items.concat(extended)
|
|
270
|
+
# Recovered iterations with no prior partial form the partial
|
|
271
|
+
# (the rs engine's stash exists from offset 0).
|
|
272
|
+
partial = { root_name => items } if created_partial && !items.empty?
|
|
243
273
|
end
|
|
244
274
|
{ "ok" => false,
|
|
245
275
|
"error" => reparse_error || recovery_error(e),
|
|
@@ -367,8 +397,7 @@ module Parsanol
|
|
|
367
397
|
# R1.5 (TODO.sota/1): the deepest repetition prefix the failed
|
|
368
398
|
# journaling pass matched, shaped like the root (the item array
|
|
369
399
|
# under the root rule's name). Nil when nothing matched.
|
|
370
|
-
def recovery_partial(entry_name)
|
|
371
|
-
stash = Parsanol::Recovery.take_partial
|
|
400
|
+
def recovery_partial(entry_name, stash = Parsanol::Recovery.take_partial)
|
|
372
401
|
return nil if stash.nil?
|
|
373
402
|
|
|
374
403
|
_end_pos, items, _tag = stash
|
|
@@ -64,22 +64,213 @@ module Parsanol
|
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# The bound model's shape: a nested type tree derived from the
|
|
68
|
+
# binding paths (ruby#308 v2). Dot segments nest objects, a
|
|
69
|
+
# `[]` segment is an array of objects (or of leaf values when
|
|
70
|
+
# the array is itself the leaf), and the leaf carries the
|
|
71
|
+
# binding's type and card (`0..1` = nullable, `*` cards arrive
|
|
72
|
+
# through the array node itself).
|
|
73
|
+
#
|
|
74
|
+
# shape({ "items[].n" => { "type" => "integer", "card" => "1" } })
|
|
75
|
+
# # => { "items" => { "kind" => "array",
|
|
76
|
+
# # "of" => { "n" => { "kind" => "leaf", "type" => "integer", "card" => "1" } } } }
|
|
77
|
+
def shape(fields)
|
|
78
|
+
root = {}
|
|
79
|
+
fields.each do |path, requirements|
|
|
80
|
+
segments = path.split(".")
|
|
81
|
+
node = root
|
|
82
|
+
segments.each_with_index do |segment, index|
|
|
83
|
+
array = segment.end_with?("[]")
|
|
84
|
+
name = array ? segment.delete_suffix("[]") : segment
|
|
85
|
+
leaf = index == segments.length - 1
|
|
86
|
+
if array && leaf
|
|
87
|
+
node[name] = {
|
|
88
|
+
"kind" => "array",
|
|
89
|
+
"of" => { "kind" => "leaf", "type" => requirements["type"], "card" => requirements["card"] },
|
|
90
|
+
}
|
|
91
|
+
break
|
|
92
|
+
end
|
|
93
|
+
slot = (node[name] ||= array ? { "kind" => "array", "of" => {} } : {})
|
|
94
|
+
node = array ? slot["of"] : slot
|
|
95
|
+
next unless leaf
|
|
96
|
+
|
|
97
|
+
slot.update("kind" => "leaf", "type" => requirements["type"], "card" => requirements["card"])
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
root
|
|
101
|
+
end
|
|
102
|
+
|
|
67
103
|
# TypeScript materialization types: one interface per entry, one
|
|
68
|
-
#
|
|
104
|
+
# interface per nested object node (v2: the interfaces mirror
|
|
105
|
+
# the bound model's shape). Cards ending in * become arrays;
|
|
69
106
|
# 0..1 becomes nullable.
|
|
70
107
|
def to_typescript(schema)
|
|
71
108
|
lines = ["// Generated by parsanol parg schema --ts — binding-requirements types."]
|
|
72
109
|
schema.each do |entry, data|
|
|
73
110
|
lines << ""
|
|
74
|
-
lines
|
|
75
|
-
|
|
76
|
-
|
|
111
|
+
emit_ts_interface(lines, ts_name(entry), shape(data["fields"]))
|
|
112
|
+
end
|
|
113
|
+
"#{lines.join("\n")}\n"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def emit_ts_interface(lines, name, node)
|
|
117
|
+
nested = []
|
|
118
|
+
lines << "export interface #{name} {"
|
|
119
|
+
node.each do |prop, child|
|
|
120
|
+
case child["kind"]
|
|
121
|
+
when "array"
|
|
122
|
+
element = child["of"]
|
|
123
|
+
inner = element["kind"] == "leaf" ? ts_leaf(element) : ts_ref_name(name, prop)
|
|
124
|
+
lines << " #{ts_prop(prop)}: #{inner}[];"
|
|
125
|
+
nested << [ts_ref_name(name, prop), element] unless element["kind"] == "leaf"
|
|
126
|
+
when "leaf"
|
|
127
|
+
lines << " #{ts_prop(prop)}: #{ts_leaf(child)};"
|
|
77
128
|
end
|
|
78
|
-
|
|
129
|
+
end
|
|
130
|
+
lines << "}"
|
|
131
|
+
lines << ""
|
|
132
|
+
nested.each { |inner, element| emit_ts_interface(lines, inner, element) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def ts_leaf(leaf)
|
|
136
|
+
base = ts_base(leaf["type"])
|
|
137
|
+
if leaf["card"].to_s.end_with?("*")
|
|
138
|
+
"#{base}[]"
|
|
139
|
+
elsif leaf["card"] == "0..1"
|
|
140
|
+
"#{base} | null"
|
|
141
|
+
else
|
|
142
|
+
base
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def ts_base(type)
|
|
147
|
+
case type
|
|
148
|
+
when "integer", "float" then "number"
|
|
149
|
+
when "boolean" then "boolean"
|
|
150
|
+
else "string"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def ts_ref_name(parent, prop)
|
|
155
|
+
"#{parent}#{ts_prop(prop).split('.').map(&:capitalize).join}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Ruby materialization models: lutaml-model classes (the
|
|
159
|
+
# established serialization framework — the generated file
|
|
160
|
+
# requires it; the consumer's bundle carries the gem).
|
|
161
|
+
def to_ruby(schema)
|
|
162
|
+
lines = [
|
|
163
|
+
"# frozen_string_literal: true",
|
|
164
|
+
"",
|
|
165
|
+
"# Generated by parsanol parg schema --ruby — lutaml-model binding types.",
|
|
166
|
+
"require \"lutaml/model\"",
|
|
167
|
+
"",
|
|
168
|
+
]
|
|
169
|
+
schema.each do |entry, data|
|
|
170
|
+
emit_ruby_class(lines, ts_name(entry), shape(data["fields"]))
|
|
79
171
|
end
|
|
80
172
|
"#{lines.join("\n")}\n"
|
|
81
173
|
end
|
|
82
174
|
|
|
175
|
+
def emit_ruby_class(lines, name, node)
|
|
176
|
+
nested = []
|
|
177
|
+
node.each do |prop, child|
|
|
178
|
+
next unless child["kind"] == "array" && child["of"]["kind"] != "leaf"
|
|
179
|
+
|
|
180
|
+
inner = lutaml_ref_name(name, prop)
|
|
181
|
+
nested << [inner, child["of"]]
|
|
182
|
+
end
|
|
183
|
+
# Ruby resolves the constant at class-body time: the nested
|
|
184
|
+
# models precede their parents.
|
|
185
|
+
nested.each { |inner, element| emit_ruby_class(lines, inner, element) }
|
|
186
|
+
lines << "class #{name} < Lutaml::Model::Serializable"
|
|
187
|
+
lines <<
|
|
188
|
+
node.each do |prop, child|
|
|
189
|
+
attr_name = ts_prop(prop)
|
|
190
|
+
case child["kind"]
|
|
191
|
+
when "array"
|
|
192
|
+
element = child["of"]
|
|
193
|
+
if element["kind"] == "leaf"
|
|
194
|
+
lines << " attribute :#{attr_name}, #{lutaml_type(element)}, collection: true"
|
|
195
|
+
else
|
|
196
|
+
inner = lutaml_ref_name(name, prop)
|
|
197
|
+
lines << " attribute :#{attr_name}, #{inner}, collection: true"
|
|
198
|
+
nested << [inner, element]
|
|
199
|
+
end
|
|
200
|
+
when "leaf"
|
|
201
|
+
# lutaml-model attributes are nilable unless validated:
|
|
202
|
+
# a 0..1 card is the plain attribute; presence-validated
|
|
203
|
+
# cards are the consumer's policy, not the schema's.
|
|
204
|
+
lines << " attribute :#{attr_name}, #{lutaml_type(child)}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
lines << "end"
|
|
208
|
+
lines << ""
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def lutaml_type(leaf)
|
|
212
|
+
case leaf["type"]
|
|
213
|
+
when "integer" then ":integer"
|
|
214
|
+
when "float" then ":float"
|
|
215
|
+
when "boolean" then ":boolean"
|
|
216
|
+
else ":string"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def lutaml_ref_name(parent, prop)
|
|
221
|
+
"#{parent}#{ts_prop(prop).split('.').map(&:capitalize).join}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Rust materialization structs (serde): one struct per nested
|
|
225
|
+
# object node, i64/f64/bool/String leaves, Vec for arrays,
|
|
226
|
+
# Option for 0..1.
|
|
227
|
+
def to_rust(schema)
|
|
228
|
+
lines = [
|
|
229
|
+
"// Generated by parsanol parg schema --rust — serde binding types.",
|
|
230
|
+
"",
|
|
231
|
+
]
|
|
232
|
+
schema.each do |entry, data|
|
|
233
|
+
emit_rust_struct(lines, ts_name(entry), shape(data["fields"]))
|
|
234
|
+
end
|
|
235
|
+
"#{lines.join("\n")}\n"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def emit_rust_struct(lines, name, node)
|
|
239
|
+
nested = []
|
|
240
|
+
lines << "#[derive(Debug, serde::Serialize, serde::Deserialize)]"
|
|
241
|
+
lines << "pub struct #{name} {"
|
|
242
|
+
node.each do |prop, child|
|
|
243
|
+
field = ts_prop(prop)
|
|
244
|
+
snake = field.gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }
|
|
245
|
+
case child["kind"]
|
|
246
|
+
when "array"
|
|
247
|
+
element = child["of"]
|
|
248
|
+
inner = element["kind"] == "leaf" ? rust_leaf(element) : rust_ref_name(name, prop)
|
|
249
|
+
lines << " pub #{snake}: Vec<#{inner}>,"
|
|
250
|
+
nested << [rust_ref_name(name, prop), element] unless element["kind"] == "leaf"
|
|
251
|
+
when "leaf"
|
|
252
|
+
base = rust_leaf(child)
|
|
253
|
+
lines << " pub #{snake}: #{child['card'] == '0..1' ? "Option<#{base}>" : base},"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
lines << "}"
|
|
257
|
+
lines << ""
|
|
258
|
+
nested.each { |inner, element| emit_rust_struct(lines, inner, element) }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def rust_leaf(leaf)
|
|
262
|
+
case leaf["type"]
|
|
263
|
+
when "integer" then "i64"
|
|
264
|
+
when "float" then "f64"
|
|
265
|
+
when "boolean" then "bool"
|
|
266
|
+
else "String"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def rust_ref_name(parent, prop)
|
|
271
|
+
ts_ref_name(parent, prop)
|
|
272
|
+
end
|
|
273
|
+
|
|
83
274
|
def ts_name(entry)
|
|
84
275
|
entry.split(/[_.]/).map(&:capitalize).join
|
|
85
276
|
end
|
data/lib/parsanol/parg/cli.rb
CHANGED
|
@@ -29,6 +29,13 @@ module Parsanol
|
|
|
29
29
|
repl <file> [--entry E] interactive parse loop (blank
|
|
30
30
|
line quits, :quit exits)
|
|
31
31
|
doc <file> [rule] print rule documentation
|
|
32
|
+
export <file|artifact.json> --target xgrammar
|
|
33
|
+
lower the compiled artifact to a
|
|
34
|
+
decoder-facing grammar (ruby#321)
|
|
35
|
+
lint <file> [--suggest-upgrades]
|
|
36
|
+
print lint errors (exit 1) and
|
|
37
|
+
warnings; the flag adds
|
|
38
|
+
machine-applicable upgrades
|
|
32
39
|
|
|
33
40
|
options:
|
|
34
41
|
--tables DIR directory for alt-from-table data (default:
|
|
@@ -51,6 +58,7 @@ module Parsanol
|
|
|
51
58
|
@argv.delete_at(index)
|
|
52
59
|
end
|
|
53
60
|
@json = !!@argv.delete("--json")
|
|
61
|
+
@suggest_upgrades = !!@argv.delete("--suggest-upgrades")
|
|
54
62
|
command = @argv.shift
|
|
55
63
|
case command
|
|
56
64
|
when "compile" then compile
|
|
@@ -60,6 +68,9 @@ module Parsanol
|
|
|
60
68
|
when "check" then check
|
|
61
69
|
when "repl" then repl
|
|
62
70
|
when "doc" then doc
|
|
71
|
+
when "lint" then lint
|
|
72
|
+
when "export" then export_grammar
|
|
73
|
+
when "diff" then diff_artifacts
|
|
63
74
|
when "lsp" then Lsp.new.run && 0
|
|
64
75
|
else
|
|
65
76
|
warn USAGE
|
|
@@ -84,6 +95,53 @@ module Parsanol
|
|
|
84
95
|
end
|
|
85
96
|
end
|
|
86
97
|
|
|
98
|
+
# `parg export FILE --target xgrammar`: the decoder-facing
|
|
99
|
+
# lowering (ruby#321). Reads a .parg (compiling it) or an
|
|
100
|
+
# artifact json, prints the target grammar on stdout.
|
|
101
|
+
def export_grammar
|
|
102
|
+
source = @argv.shift
|
|
103
|
+
target = (flag_value("--target") || "xgrammar").to_sym
|
|
104
|
+
json = if source.end_with?(".json")
|
|
105
|
+
File.read(source)
|
|
106
|
+
else
|
|
107
|
+
JSON.generate(compile_source(source).envelope)
|
|
108
|
+
end
|
|
109
|
+
print Export.call(json, target: target)
|
|
110
|
+
0
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# `parg lint FILE`: the lint suite on one grammar — errors
|
|
114
|
+
# first (exit 1), then every warning including the advisory
|
|
115
|
+
# strategies. --suggest-upgrades adds the machine-applicable
|
|
116
|
+
# upgrade suggestions on top.
|
|
117
|
+
def lint
|
|
118
|
+
source = @argv.shift
|
|
119
|
+
@captures_radar = @argv.delete("--captures") ? true : false
|
|
120
|
+
tables = @tables_dir || default_tables_dir(source)
|
|
121
|
+
document = Parsanol::PARG::Parser.new(PARG.read_utf8(source)).parse
|
|
122
|
+
Parsanol::PARG::Imports.merge!(document, [File.dirname(source)])
|
|
123
|
+
compiler = Parsanol::PARG::Compiler.new(document, tables)
|
|
124
|
+
document.rules.each_key { |rule| compiler.atom_for(rule) }
|
|
125
|
+
|
|
126
|
+
errors = Lints.errors(document, compiler)
|
|
127
|
+
errors.each { |error| puts "error: #{error}" }
|
|
128
|
+
return 1 unless errors.empty?
|
|
129
|
+
|
|
130
|
+
Lints.warnings(document, compiler).each { |warning| puts "warning: #{warning}" }
|
|
131
|
+
if @suggest_upgrades
|
|
132
|
+
Lints::CoreRuleDuplicates.new.warnings(document, compiler)
|
|
133
|
+
.each { |upgrade| puts "upgrade: #{upgrade}" }
|
|
134
|
+
end
|
|
135
|
+
if @captures_radar
|
|
136
|
+
# The migration radar (ruby#304): what modern mode would name.
|
|
137
|
+
compiler.modern_radar.each { |line| puts "captures: #{line}" }
|
|
138
|
+
end
|
|
139
|
+
0
|
|
140
|
+
rescue Parsanol::PARG::ParseError, Parsanol::PARG::Error => e
|
|
141
|
+
warn "#{e.class}: #{e.message}"
|
|
142
|
+
1
|
|
143
|
+
end
|
|
144
|
+
|
|
87
145
|
def compile
|
|
88
146
|
source = @argv.shift
|
|
89
147
|
out = flag_value("-o") || default_artifact_name(source)
|
|
@@ -152,16 +210,66 @@ module Parsanol
|
|
|
152
210
|
|
|
153
211
|
def schema
|
|
154
212
|
ts = @argv.delete("--ts")
|
|
213
|
+
ruby = @argv.delete("--ruby")
|
|
214
|
+
rust = @argv.delete("--rust")
|
|
155
215
|
artifact = artifact_for(@argv.shift)
|
|
156
216
|
schema = Parsanol::PARG::Schema.from_artifact(artifact)
|
|
157
217
|
if ts
|
|
158
218
|
puts Parsanol::PARG::Schema.to_typescript(schema)
|
|
219
|
+
elsif ruby
|
|
220
|
+
puts Parsanol::PARG::Schema.to_ruby(schema)
|
|
221
|
+
elsif rust
|
|
222
|
+
puts Parsanol::PARG::Schema.to_rust(schema)
|
|
159
223
|
else
|
|
160
224
|
puts JSON.pretty_generate(schema)
|
|
161
225
|
end
|
|
162
226
|
0
|
|
163
227
|
end
|
|
164
228
|
|
|
229
|
+
# parg diff OLD.artifact.json NEW.artifact.json [--json] [--suggest-bump] [--check LEVEL]
|
|
230
|
+
# Semantic classification of a grammar change (ruby#306): majors
|
|
231
|
+
# break consumers, minors extend them, patches preserve behavior.
|
|
232
|
+
def diff_artifacts
|
|
233
|
+
old_path = @argv.shift
|
|
234
|
+
new_path = @argv.shift
|
|
235
|
+
unless old_path && new_path
|
|
236
|
+
warn "usage: parsanol parg diff OLD.artifact.json NEW.artifact.json [--json] [--suggest-bump] [--check minor]"
|
|
237
|
+
return 2
|
|
238
|
+
end
|
|
239
|
+
suggest = !!@argv.delete("--suggest-bump")
|
|
240
|
+
check = @argv.delete("--check")
|
|
241
|
+
check_level = check && check.split("=", 2).last&.to_sym
|
|
242
|
+
|
|
243
|
+
old_env = JSON.parse(File.read(old_path))
|
|
244
|
+
new_env = JSON.parse(File.read(new_path))
|
|
245
|
+
findings = Diff.diff(old_env, new_env)
|
|
246
|
+
level = Diff.level(findings)
|
|
247
|
+
|
|
248
|
+
if @json
|
|
249
|
+
payload = {
|
|
250
|
+
level: level.to_s,
|
|
251
|
+
findings: findings.map { |f| { level: f.level.to_s, kind: f.kind.to_s, detail: f.detail } },
|
|
252
|
+
}
|
|
253
|
+
payload[:suggested] = Diff.suggest_bump(findings) if suggest
|
|
254
|
+
puts JSON.generate(payload)
|
|
255
|
+
else
|
|
256
|
+
if findings.empty?
|
|
257
|
+
puts "no semantic changes (#{level})"
|
|
258
|
+
else
|
|
259
|
+
findings.each { |f| puts "#{f.level.upcase} #{f.kind}: #{f.detail}" }
|
|
260
|
+
puts "level: #{level}"
|
|
261
|
+
end
|
|
262
|
+
puts Diff.suggest_bump(findings) if suggest
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
if check_level
|
|
266
|
+
ok = Diff::SEVERITY[level] >= Diff::SEVERITY[check_level]
|
|
267
|
+
warn "check: #{level} is #{ok ? 'at least' : 'BELOW'} #{check_level}" unless @json
|
|
268
|
+
return ok ? 0 : 3
|
|
269
|
+
end
|
|
270
|
+
0
|
|
271
|
+
end
|
|
272
|
+
|
|
165
273
|
def parse
|
|
166
274
|
file = @argv.shift
|
|
167
275
|
entry = flag_value("--entry")
|