parsanol 1.3.109-aarch64-linux → 1.3.112-aarch64-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 +4 -4
- data/lib/parsanol/3.2/parsanol_native.so +2 -2
- data/lib/parsanol/3.3/parsanol_native.so +2 -2
- data/lib/parsanol/3.4/parsanol_native.so +2 -2
- data/lib/parsanol/4.0/parsanol_native.so +2 -2
- data/lib/parsanol/atoms/repetition.rb +22 -0
- data/lib/parsanol/native/libparsanol.so +2 -2
- data/lib/parsanol/parg/artifact.rb +37 -8
- data/lib/parsanol/parg/authoring.rb +196 -5
- data/lib/parsanol/parg/cli.rb +11 -0
- data/lib/parsanol/parg/compiler.rb +161 -0
- data/lib/parsanol/parg/document.rb +6 -1
- data/lib/parsanol/parg/export.rb +73 -5
- data/lib/parsanol/parg/parser.rb +17 -0
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +50 -24
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e622ed0c55dbbcc4295a1b3740c36e0dc725a89251eb36c7799d6b60dbb026ff
|
|
4
|
+
data.tar.gz: d6d4c119f0e08a8b167faae5f1bb8203cf1f18d77d9cebd132a8bed6c2817882
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6db2994d50d7d9e77a5a9b6614d3b0b461a623d06372bace9d30e9c45e09f6f55aa4efffbaa411ff00462e93e1f8b4681edc9229a15b391034293875a319846b
|
|
7
|
+
data.tar.gz: 4a36caf756fb3e162bdbe24235f1005a1a56e322677e86e2606900441994dbe6107ca8ce4822d02314bdda6bc38fb246dbd9767f1e816566daea4482e0ea558f
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/3.2/parsanol_native.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 45892256 bytes
|
|
4
|
+
sha256: baf93c649b2dc899fc6b6a9e42d2f83ba44c08738db11fbb3ce74c1788507e55
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/3.3/parsanol_native.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 46127304 bytes
|
|
4
|
+
sha256: 542480a12619696edd8e73bbb30d995b2ed2bae78f660050ea5f3cbcbc39f94c
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/3.4/parsanol_native.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 46122944 bytes
|
|
4
|
+
sha256: d01871833b692c8d59f1d88b7c69792d1c44ed14fa9d62180534cabf508eb440
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/4.0/parsanol_native.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 46132984 bytes
|
|
4
|
+
sha256: a3d424e1bdd7e31770de8e830d3531df4010f7cb1ef5d9e10a9da04d1ac935b2
|
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: data/lib/parsanol/native/libparsanol.so
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 41651720 bytes
|
|
4
|
+
sha256: cff3505da15196c090a20db6db228680d94b0467a1daa0d28ebc1e22f2066145
|
|
@@ -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
|
@@ -116,6 +116,7 @@ module Parsanol
|
|
|
116
116
|
# upgrade suggestions on top.
|
|
117
117
|
def lint
|
|
118
118
|
source = @argv.shift
|
|
119
|
+
@captures_radar = @argv.delete("--captures") ? true : false
|
|
119
120
|
tables = @tables_dir || default_tables_dir(source)
|
|
120
121
|
document = Parsanol::PARG::Parser.new(PARG.read_utf8(source)).parse
|
|
121
122
|
Parsanol::PARG::Imports.merge!(document, [File.dirname(source)])
|
|
@@ -131,6 +132,10 @@ module Parsanol
|
|
|
131
132
|
Lints::CoreRuleDuplicates.new.warnings(document, compiler)
|
|
132
133
|
.each { |upgrade| puts "upgrade: #{upgrade}" }
|
|
133
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
|
|
134
139
|
0
|
|
135
140
|
rescue Parsanol::PARG::ParseError, Parsanol::PARG::Error => e
|
|
136
141
|
warn "#{e.class}: #{e.message}"
|
|
@@ -205,10 +210,16 @@ module Parsanol
|
|
|
205
210
|
|
|
206
211
|
def schema
|
|
207
212
|
ts = @argv.delete("--ts")
|
|
213
|
+
ruby = @argv.delete("--ruby")
|
|
214
|
+
rust = @argv.delete("--rust")
|
|
208
215
|
artifact = artifact_for(@argv.shift)
|
|
209
216
|
schema = Parsanol::PARG::Schema.from_artifact(artifact)
|
|
210
217
|
if ts
|
|
211
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)
|
|
212
223
|
else
|
|
213
224
|
puts JSON.pretty_generate(schema)
|
|
214
225
|
end
|
|
@@ -228,6 +228,144 @@ module Parsanol
|
|
|
228
228
|
@resync_cache[rule.to_sym] ||= build_atom(pattern)
|
|
229
229
|
end
|
|
230
230
|
|
|
231
|
+
# Modern mode (ruby#304, the #315 spec): every rule reference
|
|
232
|
+
# that produces no capture is wrapped in Named(rule_name) at
|
|
233
|
+
# compile time — the tree shape consumers read, opt-in via
|
|
234
|
+
# `mode` in the grammar header. Engines see ordinary
|
|
235
|
+
# Named atoms; identity rides the differential corpus.
|
|
236
|
+
def apply_modern_captures(atom)
|
|
237
|
+
return atom if @document.edition == "v1"
|
|
238
|
+
|
|
239
|
+
wrap_uncaptured(atom)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def wrap_uncaptured(atom)
|
|
243
|
+
case atom
|
|
244
|
+
when Atoms::Named
|
|
245
|
+
atom
|
|
246
|
+
when Atoms::Entity
|
|
247
|
+
# A rule that can only match whitespace is glue, not a
|
|
248
|
+
# semantic capture (ruby#352): wrapping it named every gap
|
|
249
|
+
# in the tree and broke consumers that fold captures into
|
|
250
|
+
# parameters. Conservatively: the rule's body is literals
|
|
251
|
+
# (through sequence/alternation/repetition composition) and
|
|
252
|
+
# every literal strips to empty.
|
|
253
|
+
# rubocop:disable Lint/DuplicateBranch -- trivia-glue and
|
|
254
|
+
# capture-producing refs both stay unwrapped; the bodies
|
|
255
|
+
# agree by design.
|
|
256
|
+
if trivia_only?(atom.rule_name.to_s, { atom.rule_name.to_s => true }) ||
|
|
257
|
+
produces_capture?(atom.parslet)
|
|
258
|
+
atom
|
|
259
|
+
else
|
|
260
|
+
Atoms::Named.new(atom, atom.rule_name.to_s)
|
|
261
|
+
end
|
|
262
|
+
# rubocop:enable Lint/DuplicateBranch
|
|
263
|
+
when Atoms::Sequence, Atoms::Alternative
|
|
264
|
+
# rubocop:disable Lint/DuplicateBranch -- the two composites
|
|
265
|
+
# rebuild with their own constructors; identical bodies,
|
|
266
|
+
# different decisions
|
|
267
|
+
kids = atom.respond_to?(:alternatives) ? atom.alternatives : atom.parslets
|
|
268
|
+
atom.class.new(*kids.map { |child| wrap_uncaptured(child) })
|
|
269
|
+
when Atoms::Repetition
|
|
270
|
+
Atoms::Repetition.new(wrap_uncaptured(atom.parslet), atom.min, atom.max, atom.result_tag)
|
|
271
|
+
else
|
|
272
|
+
atom
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Whether the named rule's body is provably whitespace-only:
|
|
277
|
+
# literals strip to empty; sequence/alternation/repetition
|
|
278
|
+
# compose the verdict; references resolve through the atom cache
|
|
279
|
+
# with a cycle set; anything else (classes, customs, authored
|
|
280
|
+
# captures) disqualifies.
|
|
281
|
+
def trivia_only?(name, seen)
|
|
282
|
+
return false unless @document.rules.key?(name)
|
|
283
|
+
|
|
284
|
+
trivia_walk(atom_for(name), seen)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def trivia_walk(atom, seen)
|
|
288
|
+
case atom
|
|
289
|
+
when Atoms::Str
|
|
290
|
+
atom.str.to_s.strip.empty?
|
|
291
|
+
when Atoms::Sequence, Atoms::Alternative
|
|
292
|
+
kids = atom.respond_to?(:alternatives) ? atom.alternatives : atom.parslets
|
|
293
|
+
kids.all? { |child| trivia_walk(child, seen) }
|
|
294
|
+
when Atoms::Repetition
|
|
295
|
+
trivia_walk(atom.parslet, seen)
|
|
296
|
+
when Atoms::Entity
|
|
297
|
+
name = atom.rule_name.to_s
|
|
298
|
+
return false if seen[name]
|
|
299
|
+
|
|
300
|
+
seen[name] = true
|
|
301
|
+
trivia_only?(name, seen)
|
|
302
|
+
else
|
|
303
|
+
false
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Whether the atom's flattening surfaces any Named capture:
|
|
308
|
+
# rule references resolve through (their own wrap pass already
|
|
309
|
+
# ran — a wrapped ref produces a capture), trivia never
|
|
310
|
+
# surfaces, everything else recurses structurally.
|
|
311
|
+
def produces_capture?(atom, seen = [])
|
|
312
|
+
return false if seen.include?(atom.object_id)
|
|
313
|
+
|
|
314
|
+
seen << atom.object_id
|
|
315
|
+
case atom
|
|
316
|
+
when Atoms::Named then true
|
|
317
|
+
when Atoms::Entity
|
|
318
|
+
wrapped = atom.parslet.is_a?(Atoms::Named)
|
|
319
|
+
wrapped || produces_capture?(atom.parslet, seen)
|
|
320
|
+
when Atoms::Sequence, Atoms::Alternative
|
|
321
|
+
kids = atom.respond_to?(:alternatives) ? atom.alternatives : atom.parslets
|
|
322
|
+
kids.any? { |child| produces_capture?(child, seen) }
|
|
323
|
+
when Atoms::Repetition
|
|
324
|
+
produces_capture?(atom.parslet, seen)
|
|
325
|
+
else
|
|
326
|
+
false
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# The migration radar (ruby#304 `parg lint --captures`): for a
|
|
331
|
+
# LEGACY grammar, every rule reference modern mode would wrap —
|
|
332
|
+
# reference site, target rule. The compiler's state is warm
|
|
333
|
+
# (all rules built) when the lint calls this.
|
|
334
|
+
def modern_radar
|
|
335
|
+
# The radar is the classic dialect's migration tool: on a v2
|
|
336
|
+
# grammar every reference is already named.
|
|
337
|
+
return [] if @document.edition != "v1"
|
|
338
|
+
|
|
339
|
+
lines = []
|
|
340
|
+
@document.rules.each_key do |name|
|
|
341
|
+
atom = @atom_cache[name]
|
|
342
|
+
next unless atom
|
|
343
|
+
|
|
344
|
+
radar_walk(atom, name, lines, [])
|
|
345
|
+
end
|
|
346
|
+
lines
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def radar_walk(atom, site, lines, seen)
|
|
350
|
+
return if seen.include?(atom.object_id)
|
|
351
|
+
|
|
352
|
+
seen << atom.object_id
|
|
353
|
+
case atom
|
|
354
|
+
when Atoms::Named then nil
|
|
355
|
+
when Atoms::Entity
|
|
356
|
+
unless produces_capture?(atom.parslet)
|
|
357
|
+
lines << "rule #{site}: reference to #{atom.rule_name} produces no capture -- " \
|
|
358
|
+
"modern mode would name it #{atom.rule_name}"
|
|
359
|
+
end
|
|
360
|
+
radar_walk(atom.parslet, site, lines, seen)
|
|
361
|
+
when Atoms::Sequence, Atoms::Alternative
|
|
362
|
+
kids = atom.respond_to?(:alternatives) ? atom.alternatives : atom.parslets
|
|
363
|
+
kids.each { |child| radar_walk(child, site, lines, seen) }
|
|
364
|
+
when Atoms::Repetition
|
|
365
|
+
radar_walk(atom.parslet, site, lines, seen)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
231
369
|
def atom_for(name)
|
|
232
370
|
inject_core_rules!
|
|
233
371
|
expand_left_recursion! unless @expanded
|
|
@@ -247,6 +385,15 @@ module Parsanol
|
|
|
247
385
|
atom = Atoms::Sequence.new(skip_maybe_atom, atom, skip_maybe_atom)
|
|
248
386
|
end
|
|
249
387
|
end
|
|
388
|
+
# The modern wrap runs LAST — the guide's spec table: trivia
|
|
389
|
+
# atoms are in place first, so the pass sees the final tree.
|
|
390
|
+
# The unwrapped tree lands in the cache BEFORE the wrap (the
|
|
391
|
+
# wrap's capture probes resolve rule references through
|
|
392
|
+
# atom_for; a mutual-reference grammar must hit the cache,
|
|
393
|
+
# not rebuild), then the cache takes the wrapped tree.
|
|
394
|
+
@atom_cache[name] = atom
|
|
395
|
+
atom = apply_modern_captures(atom)
|
|
396
|
+
@atom_cache[name] = atom
|
|
250
397
|
atom
|
|
251
398
|
end
|
|
252
399
|
end
|
|
@@ -1077,8 +1224,22 @@ module Parsanol
|
|
|
1077
1224
|
# back to a reference implementation instead of failing obscurely
|
|
1078
1225
|
# at parse time. Checksummed like the rest of the envelope.
|
|
1079
1226
|
"source_hash" => Digest::SHA256.hexdigest(@document.source.to_s),
|
|
1227
|
+
# The U3 provenance stamp (rs#297): which Unicode table source
|
|
1228
|
+
# the compiling engine's categories compiled against. An
|
|
1229
|
+
# engine-specific stamp (checksummed here, excluded from the
|
|
1230
|
+
# cross-engine parity set like compiler_version); the
|
|
1231
|
+
# observable behavior stays pinned by the unicode corpus
|
|
1232
|
+
# fixtures.
|
|
1233
|
+
"unicode_tables" => "onigmo (Ruby #{RUBY_VERSION})",
|
|
1080
1234
|
"compiler_version" => Parsanol::VERSION,
|
|
1081
1235
|
}
|
|
1236
|
+
# ruby#304: the envelope always names the capture dialect —
|
|
1237
|
+
# consumers read the tree-shape contract before parsing.
|
|
1238
|
+
envelope["edition"] = @document.edition
|
|
1239
|
+
# ruby#353: the language-semantics level the trees were frozen
|
|
1240
|
+
# against (1.3.108's implicit-capture shapes = 1; the
|
|
1241
|
+
# explicit end_guard/marker/hyphen nodes = 2). Consumers freeze
|
|
1242
|
+
# tree shapes and mappers; the loader asserts the level at
|
|
1082
1243
|
if @document.skip_declaration
|
|
1083
1244
|
# parsanol-ruby#134: declared trivia rule. Injection is baked
|
|
1084
1245
|
# into the compiled trees (plain atom kinds), so consumers
|
|
@@ -17,7 +17,7 @@ module Parsanol
|
|
|
17
17
|
|
|
18
18
|
Test = Struct.new(:entry, :kind, :input, :expect)
|
|
19
19
|
|
|
20
|
-
attr_accessor :grammar_name, :version, :source, :uses, :skip_declaration, :skip_source,
|
|
20
|
+
attr_accessor :grammar_name, :version, :edition, :source, :uses, :skip_declaration, :skip_source,
|
|
21
21
|
:atomic_rules, :skip_capture_key, :brace_rules
|
|
22
22
|
# R2 declared resync: rule name -> skip pattern (the span matched
|
|
23
23
|
# at a recovery point is consumed as annotated error text).
|
|
@@ -35,6 +35,11 @@ module Parsanol
|
|
|
35
35
|
|
|
36
36
|
def initialize
|
|
37
37
|
@grammar_name = nil
|
|
38
|
+
# The default capture dialect is v1 (the owner's reversal,
|
|
39
|
+
# 2026-10-28): runtime compiles of unpinned sources keep the
|
|
40
|
+
# legacy shapes — the pubid fleet froze against them. mode "v2"
|
|
41
|
+
# stays the named-trees opt-in via the grammar header.
|
|
42
|
+
@edition = "v1"
|
|
38
43
|
@version = "0.0.0"
|
|
39
44
|
@source = nil
|
|
40
45
|
@rules = {}
|
data/lib/parsanol/parg/export.rb
CHANGED
|
@@ -19,9 +19,15 @@ module Parsanol
|
|
|
19
19
|
TARGETS = %i[xgrammar].freeze
|
|
20
20
|
|
|
21
21
|
class << self
|
|
22
|
-
|
|
22
|
+
# `recovery: true` lowers each declared `resync <rule> = <pattern>`
|
|
23
|
+
# as an escape alternative at the repetition spine: the target's
|
|
24
|
+
# matcher then accepts exactly the strings the engines' recovery
|
|
25
|
+
# path accepts — clean parses, and violate-then-resync parses
|
|
26
|
+
# where the junk is emitted deliberately and the grammar closes
|
|
27
|
+
# back over the continuation (ruby#321's partial-mask story).
|
|
28
|
+
def call(artifact_json, target: :xgrammar, recovery: false)
|
|
23
29
|
case target
|
|
24
|
-
when :xgrammar then Xgrammar.new(artifact_json).call
|
|
30
|
+
when :xgrammar then Xgrammar.new(artifact_json, recovery: recovery).call
|
|
25
31
|
else raise Error, "unknown export target #{target.inspect} (supported: #{TARGETS.join(', ')})"
|
|
26
32
|
end
|
|
27
33
|
end
|
|
@@ -34,20 +40,64 @@ module Parsanol
|
|
|
34
40
|
# equality. The artifact wire has no rule boundaries (all atoms
|
|
35
41
|
# inline), so each entry lowers as one expression.
|
|
36
42
|
class Xgrammar
|
|
37
|
-
def initialize(artifact_json)
|
|
43
|
+
def initialize(artifact_json, recovery: false)
|
|
38
44
|
@envelope = JSON.parse(artifact_json)
|
|
45
|
+
@recovery = recovery
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
def call
|
|
42
49
|
rules = @envelope["entries"].map do |entry, meta|
|
|
43
50
|
grammar = meta.fetch("grammar")
|
|
44
|
-
"#{entry}_entry ::= #{
|
|
51
|
+
"#{entry}_entry ::= #{lower_entry(entry, grammar)}"
|
|
45
52
|
end
|
|
46
53
|
"#{rules.join("\n")}\n"
|
|
47
54
|
end
|
|
48
55
|
|
|
49
56
|
private
|
|
50
57
|
|
|
58
|
+
# The recovery wrap mirrors the engines' resync contract: the
|
|
59
|
+
# root must be a repetition spine (the same shape resync
|
|
60
|
+
# recovery itself requires), and each declared resync pattern
|
|
61
|
+
# becomes an alternative of the spine body — junk between
|
|
62
|
+
# complete iterations is exactly what the recovery path
|
|
63
|
+
# consumes and resumes. Inferred (undeclared) policies are the
|
|
64
|
+
# trial scan and have no closed form here: the plain grammar
|
|
65
|
+
# ships, and the differential reports the gap honestly.
|
|
66
|
+
def lower_entry(entry, grammar)
|
|
67
|
+
atoms = grammar.fetch("atoms")
|
|
68
|
+
body = lower(atoms, grammar.fetch("root"))
|
|
69
|
+
return body unless @recovery
|
|
70
|
+
|
|
71
|
+
pattern = @envelope.dig("entries", entry, "resync")
|
|
72
|
+
return body unless pattern
|
|
73
|
+
|
|
74
|
+
spine = repetition_spine(atoms, grammar.fetch("root"))
|
|
75
|
+
unless spine
|
|
76
|
+
unsupported("recovery export: the entry root is not a repetition spine")
|
|
77
|
+
end
|
|
78
|
+
junk = lower(pattern.fetch("atoms"), pattern.fetch("root"))
|
|
79
|
+
# A spine item is a body, optionally preceded by one junk
|
|
80
|
+
# span: the engines scan forward to the first pattern match,
|
|
81
|
+
# consume it as one annotated span, and resume with the next
|
|
82
|
+
# iteration — which may be absent (trailing junk ends the
|
|
83
|
+
# parse, recovered).
|
|
84
|
+
"( #{body} | ( #{junk} ) ( #{body} )? )*"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The lowerable body of the spine's repetition: Named/Entity
|
|
88
|
+
# unwrapping down to the Repetition atom, unbounded (max nil).
|
|
89
|
+
# Nil when the root is not that shape.
|
|
90
|
+
def repetition_spine(atoms, index)
|
|
91
|
+
tag, fields = atoms.fetch(index).first
|
|
92
|
+
case tag
|
|
93
|
+
when "Named", "Entity" then repetition_spine(atoms, fields.fetch("atom"))
|
|
94
|
+
when "Repetition"
|
|
95
|
+
return nil unless fields["max"].nil?
|
|
96
|
+
|
|
97
|
+
fields.fetch("atom")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
51
101
|
def lower(atoms, index)
|
|
52
102
|
tag, fields = atoms.fetch(index).first
|
|
53
103
|
case tag
|
|
@@ -91,7 +141,15 @@ module Parsanol
|
|
|
91
141
|
# which IS the simple-case-fold semantics for ASCII. A non-ASCII
|
|
92
142
|
# fold group needs a fold table the target does not have.
|
|
93
143
|
def re_class(pattern)
|
|
94
|
-
|
|
144
|
+
# Categories have no EBNF-class spelling: they must be
|
|
145
|
+
# rejected naming the construct, never emitted raw (the
|
|
146
|
+
# target's lexer rejects them, which fails the round trip
|
|
147
|
+
# obscurely instead of at export time).
|
|
148
|
+
if pattern.include?("\\p{")
|
|
149
|
+
unsupported("unicode category #{pattern.inspect}")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
return de_escape_class(pattern) if pattern.start_with?("[") && pattern.end_with?("]")
|
|
95
153
|
|
|
96
154
|
if (folded = pattern[/\A\(\?i:(.*)\)\z/m, 1])
|
|
97
155
|
return expand_fold(folded, pattern)
|
|
@@ -100,6 +158,16 @@ module Parsanol
|
|
|
100
158
|
unsupported("regex pattern #{pattern.inspect}")
|
|
101
159
|
end
|
|
102
160
|
|
|
161
|
+
# Class endpoints \u00ff / \u{10ffff} spell as the literal
|
|
162
|
+
# characters — XGrammar's EBNF classes are UTF-8 text and
|
|
163
|
+
# accept them; emitting the escapes raw fails the target's
|
|
164
|
+
# lexer instead.
|
|
165
|
+
def de_escape_class(pattern)
|
|
166
|
+
pattern
|
|
167
|
+
.gsub(/\\u\{([0-9a-fA-F]+)\}/) { [$1.to_i(16)].pack("U") }
|
|
168
|
+
.gsub(/\\u([0-9a-fA-F]{4})/) { [$1.to_i(16)].pack("U") }
|
|
169
|
+
end
|
|
170
|
+
|
|
103
171
|
def expand_fold(text, pattern)
|
|
104
172
|
unsupported("non-ASCII fold group #{pattern.inspect} -- the target has no fold table") unless text.ascii_only?
|
|
105
173
|
|
data/lib/parsanol/parg/parser.rb
CHANGED
|
@@ -231,6 +231,23 @@ module Parsanol
|
|
|
231
231
|
document.grammar_name = ident.value
|
|
232
232
|
ident("version")
|
|
233
233
|
document.version = unquote(expect(:str).value)
|
|
234
|
+
# `edition` — the language edition (ruby#304, ruby#357's
|
|
235
|
+
# consolidation; Rust's Cargo.toml `edition` is the model):
|
|
236
|
+
# the author declares, per grammar, which tree semantics the
|
|
237
|
+
# grammar means. THE DEFAULT IS "v1" (the legacy shapes — the
|
|
238
|
+
# owner's 2026-10-28 reversal). "v2" = fully named trees +
|
|
239
|
+
# explicit spans. `mode` remains accepted as a deprecated
|
|
240
|
+
# alias for the keyword. The edition is canonicalized here —
|
|
241
|
+
# everything downstream only ever sees "v1"/"v2".
|
|
242
|
+
if peek&.type == :ident && %w[edition mode].include?(peek.value)
|
|
243
|
+
keyword = advance.value
|
|
244
|
+
edition = unquote(expect(:str).value)
|
|
245
|
+
unless %w[v2 v1 classic].include?(edition)
|
|
246
|
+
raise ParseError,
|
|
247
|
+
"unknown edition #{edition.inspect} -- supported editions are \"v1\" (default) and \"v2\" (named trees)"
|
|
248
|
+
end
|
|
249
|
+
document.edition = edition == "v2" ? "v2" : "v1"
|
|
250
|
+
end
|
|
234
251
|
punct("{")
|
|
235
252
|
skip_newlines
|
|
236
253
|
until peek&.type == :punct && peek.value == "}"
|
data/lib/parsanol/version.rb
CHANGED
data/lib/parsanol/vm.rb
CHANGED
|
@@ -971,6 +971,50 @@ module Parsanol
|
|
|
971
971
|
@memoize = memoize
|
|
972
972
|
end
|
|
973
973
|
|
|
974
|
+
# The RE match's [char width, byte advance]: single-class
|
|
975
|
+
# patterns match one character; multi-character patterns (%i
|
|
976
|
+
# literals reach RE with no byte table) consume their full
|
|
977
|
+
# length (ruby#357: advancing one character skipped the
|
|
978
|
+
# pattern's remainder and let the parse re-match inside it).
|
|
979
|
+
def re_match_width(scanner, input, pos, table, pattern)
|
|
980
|
+
if table
|
|
981
|
+
w = CHAR_WIDTH[input.byteslice(pos, 1).bytes[0]]
|
|
982
|
+
w = char_width_slow(input, pos) if w.zero?
|
|
983
|
+
[w, w]
|
|
984
|
+
else
|
|
985
|
+
len = scanner.skip(pattern)
|
|
986
|
+
[input.byteslice(pos, len).length, len]
|
|
987
|
+
end
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
# An RE instruction's match at pos: [matched?, char width, byte
|
|
991
|
+
# advance]. Single-class patterns match one character;
|
|
992
|
+
# multi-character patterns (%i literals reach RE with no byte
|
|
993
|
+
# table) consume their FULL length (ruby#357: advancing one
|
|
994
|
+
# character skipped the pattern's remainder and let the parse
|
|
995
|
+
# re-match inside it).
|
|
996
|
+
def re_scan(scanner, input, pos, table, pattern)
|
|
997
|
+
b = table && input.getbyte(pos)
|
|
998
|
+
matched =
|
|
999
|
+
if b && b < 128
|
|
1000
|
+
table[b]
|
|
1001
|
+
elsif pos < input.length
|
|
1002
|
+
(scanner.pos = pos) && scanner.match?(pattern)
|
|
1003
|
+
else
|
|
1004
|
+
false
|
|
1005
|
+
end
|
|
1006
|
+
return [false, 0, 0] unless matched
|
|
1007
|
+
|
|
1008
|
+
if b && b < 128
|
|
1009
|
+
w = CHAR_WIDTH[b]
|
|
1010
|
+
w = char_width_slow(input, pos) if w.zero?
|
|
1011
|
+
[true, w, w]
|
|
1012
|
+
else
|
|
1013
|
+
len = scanner.skip(pattern)
|
|
1014
|
+
[true, input.byteslice(pos, len).length, len]
|
|
1015
|
+
end
|
|
1016
|
+
end
|
|
1017
|
+
|
|
974
1018
|
def execute # rubocop:disable Metrics/MethodLength, Metrics/BlockLength, Metrics/BlockNesting -- single dispatch loop; hot path
|
|
975
1019
|
ops = @ops
|
|
976
1020
|
input = @input
|
|
@@ -1054,20 +1098,11 @@ module Parsanol
|
|
|
1054
1098
|
pc = fail_pc
|
|
1055
1099
|
end
|
|
1056
1100
|
when RE
|
|
1057
|
-
|
|
1058
|
-
matched =
|
|
1059
|
-
if (table = ops[pc + 2]) && b && b < 128
|
|
1060
|
-
table[b]
|
|
1061
|
-
elsif pos < n
|
|
1062
|
-
(scanner.pos = pos) && scanner.match?(ops[pc + 1])
|
|
1063
|
-
else
|
|
1064
|
-
false
|
|
1065
|
-
end
|
|
1101
|
+
table = ops[pc + 2]
|
|
1102
|
+
matched, w, advance = re_scan(scanner, input, pos, table, ops[pc + 1])
|
|
1066
1103
|
if matched
|
|
1067
|
-
w = CHAR_WIDTH[b]
|
|
1068
|
-
w = char_width_slow(input, pos) if w.zero?
|
|
1069
1104
|
rstack << ((pos << 20) | w)
|
|
1070
|
-
pos +=
|
|
1105
|
+
pos += advance
|
|
1071
1106
|
pc += 4
|
|
1072
1107
|
else
|
|
1073
1108
|
pc = fail_pc
|
|
@@ -1257,19 +1292,10 @@ module Parsanol
|
|
|
1257
1292
|
kc = kids[ki + 3]
|
|
1258
1293
|
case kop
|
|
1259
1294
|
when RE
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
kb[b]
|
|
1263
|
-
elsif pos < n
|
|
1264
|
-
(scanner.pos = pos) && scanner.match?(ka)
|
|
1265
|
-
else
|
|
1266
|
-
false
|
|
1267
|
-
end
|
|
1268
|
-
if m
|
|
1269
|
-
w = CHAR_WIDTH[b]
|
|
1270
|
-
w = char_width_slow(input, pos) if w.zero?
|
|
1295
|
+
matched, w, advance = re_scan(scanner, input, pos, kb, ka)
|
|
1296
|
+
if matched
|
|
1271
1297
|
values << ((pos << 20) | w)
|
|
1272
|
-
pos +=
|
|
1298
|
+
pos += advance
|
|
1273
1299
|
else
|
|
1274
1300
|
failed = true
|
|
1275
1301
|
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.
|
|
4
|
+
version: 1.3.112
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-28 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
|