parsanol 1.3.56-arm-linux → 1.3.57-arm-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/exe/parsanol +7 -0
- data/lib/parsanol/3.2/parsanol_native.so +0 -0
- data/lib/parsanol/3.3/parsanol_native.so +0 -0
- data/lib/parsanol/3.4/parsanol_native.so +0 -0
- data/lib/parsanol/4.0/parsanol_native.so +0 -0
- data/lib/parsanol/native/libparsanol.so +0 -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 -4
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module PARG
|
|
5
|
+
# Recursive-descent parser for PARG source text.
|
|
6
|
+
#
|
|
7
|
+
# grammar PubidIso version "1.2.0" {
|
|
8
|
+
# digit = %x30-39
|
|
9
|
+
# year = 4digit
|
|
10
|
+
# root = %i"iso" [dash number as part]
|
|
11
|
+
# }
|
|
12
|
+
#
|
|
13
|
+
# Keywords: grammar version as alt from_table column bindings
|
|
14
|
+
# preprocess entry table_lookup
|
|
15
|
+
class Parser
|
|
16
|
+
PATH_CONTINUATION = [".", "["].freeze
|
|
17
|
+
CARD_CONTINUATION = [".", "*"].freeze
|
|
18
|
+
|
|
19
|
+
IN_BLOCK_SECTIONS = %w[entry bindings preprocess test].freeze
|
|
20
|
+
|
|
21
|
+
KEYWORDS = %w[grammar as alt from_table column bindings render derive
|
|
22
|
+
preprocess entry table_lookup].freeze
|
|
23
|
+
|
|
24
|
+
def initialize(text)
|
|
25
|
+
@tokens = Lexer.new(text).tokens
|
|
26
|
+
@pos = 0
|
|
27
|
+
@source_text = text
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse
|
|
31
|
+
document = Document.new
|
|
32
|
+
document.source = @source_text
|
|
33
|
+
skip_newlines
|
|
34
|
+
until eof?
|
|
35
|
+
take_doc_comments
|
|
36
|
+
break if eof?
|
|
37
|
+
|
|
38
|
+
section = expect(:ident)
|
|
39
|
+
case section.value
|
|
40
|
+
when "use" then document.uses << ident.value
|
|
41
|
+
when "grammar" then parse_grammar(document)
|
|
42
|
+
when "entry" then parse_entry(document)
|
|
43
|
+
when "bindings" then parse_bindings(document)
|
|
44
|
+
when "preprocess" then parse_preprocess(document)
|
|
45
|
+
when "render" then parse_render(document)
|
|
46
|
+
when "derive" then parse_derive(document)
|
|
47
|
+
when "test" then parse_test(document)
|
|
48
|
+
else
|
|
49
|
+
raise ParseError,
|
|
50
|
+
"expected a section (grammar/entry/bindings/preprocess/test), " \
|
|
51
|
+
"got #{section.value.inspect} at offset #{section.offset}"
|
|
52
|
+
end
|
|
53
|
+
skip_newlines
|
|
54
|
+
end
|
|
55
|
+
document.validate!
|
|
56
|
+
document.own_entries = document.entries.keys
|
|
57
|
+
document
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def skip_newlines
|
|
63
|
+
advance while peek&.type == :newline
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Doc comments (##) accumulate and attach to the next rule.
|
|
67
|
+
def take_doc_comments
|
|
68
|
+
docs = []
|
|
69
|
+
while peek&.type == :doc
|
|
70
|
+
docs << advance.value[2..].strip
|
|
71
|
+
skip_newlines
|
|
72
|
+
end
|
|
73
|
+
docs
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse_test(document, default_entry: nil)
|
|
77
|
+
entry = peek&.type == :ident && peek.value != "{" ? advance.value : default_entry
|
|
78
|
+
punct("{")
|
|
79
|
+
skip_newlines
|
|
80
|
+
until peek&.type == :punct && peek.value == "}"
|
|
81
|
+
kind = ident.value
|
|
82
|
+
input = unquote(expect(:str).value)
|
|
83
|
+
expect_pairs = {}
|
|
84
|
+
if kind == "example" && peek&.type == :punct && peek.value == "{"
|
|
85
|
+
advance
|
|
86
|
+
skip_newlines
|
|
87
|
+
until peek&.type == :punct && peek.value == "}"
|
|
88
|
+
key = ident.value
|
|
89
|
+
punct(":")
|
|
90
|
+
expect_pairs[key.to_sym] = parse_test_value
|
|
91
|
+
skip_newlines
|
|
92
|
+
end
|
|
93
|
+
punct("}")
|
|
94
|
+
end
|
|
95
|
+
document.tests << Document::Test.new(entry, kind.to_sym, input, expect_pairs)
|
|
96
|
+
skip_newlines
|
|
97
|
+
end
|
|
98
|
+
punct("}")
|
|
99
|
+
skip_newlines
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def parse_test_value
|
|
103
|
+
token = advance
|
|
104
|
+
case token&.type
|
|
105
|
+
when :str then unquote(token.value)
|
|
106
|
+
when :num then token.value.to_i
|
|
107
|
+
when :ident
|
|
108
|
+
raise ParseError, "expected true or false" unless %w[true false].include?(token.value)
|
|
109
|
+
|
|
110
|
+
token.value == "true"
|
|
111
|
+
else
|
|
112
|
+
raise ParseError, "expected a string, number, true or false"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def eof? = @pos >= @tokens.length
|
|
117
|
+
|
|
118
|
+
def peek = @tokens[@pos]
|
|
119
|
+
|
|
120
|
+
def advance
|
|
121
|
+
token = @tokens[@pos]
|
|
122
|
+
@pos += 1
|
|
123
|
+
token
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def expect(type, value = nil)
|
|
127
|
+
token = advance
|
|
128
|
+
if token.nil? || token.type != type || (value && token.value != value)
|
|
129
|
+
found = token ? "#{token.type}:#{token.value.inspect}" : "EOF"
|
|
130
|
+
raise ParseError,
|
|
131
|
+
"expected #{value ? value.inspect : type}, got #{found}"
|
|
132
|
+
end
|
|
133
|
+
token
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def ident(value = nil) = expect(:ident, value)
|
|
137
|
+
|
|
138
|
+
def punct(char) = expect(:punct, char)
|
|
139
|
+
|
|
140
|
+
def parse_grammar(document)
|
|
141
|
+
document.grammar_name = ident.value
|
|
142
|
+
ident("version")
|
|
143
|
+
document.version = unquote(expect(:str).value)
|
|
144
|
+
punct("{")
|
|
145
|
+
skip_newlines
|
|
146
|
+
until peek&.type == :punct && peek.value == "}"
|
|
147
|
+
docs = take_doc_comments
|
|
148
|
+
break if peek.nil? || (peek&.type == :punct && peek.value == "}")
|
|
149
|
+
|
|
150
|
+
# Sections may be authored inside the grammar block: the
|
|
151
|
+
# natural placement for entry/bindings/preprocess/test.
|
|
152
|
+
if peek&.type == :ident && IN_BLOCK_SECTIONS.include?(peek.value)
|
|
153
|
+
section = advance
|
|
154
|
+
case section.value
|
|
155
|
+
when "entry" then parse_entry(document)
|
|
156
|
+
when "bindings" then parse_bindings(document)
|
|
157
|
+
when "preprocess" then parse_preprocess(document)
|
|
158
|
+
when "test" then parse_test(document)
|
|
159
|
+
end
|
|
160
|
+
skip_newlines
|
|
161
|
+
next
|
|
162
|
+
end
|
|
163
|
+
name = rule_name
|
|
164
|
+
document.docs[name] = docs.join("\n") unless docs.empty?
|
|
165
|
+
punct("=")
|
|
166
|
+
node = parse_choice
|
|
167
|
+
if document.rules.key?(name)
|
|
168
|
+
raise ParseError, "duplicate rule #{name.inspect}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
document.rules[name] = node
|
|
172
|
+
skip_newlines
|
|
173
|
+
end
|
|
174
|
+
punct("}")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def rule_name
|
|
178
|
+
token = ident
|
|
179
|
+
if KEYWORDS.include?(token.value)
|
|
180
|
+
raise ParseError,
|
|
181
|
+
"#{token.value.inspect} is a keyword and cannot name a rule"
|
|
182
|
+
end
|
|
183
|
+
token.value
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def parse_choice
|
|
187
|
+
branches = [parse_seq]
|
|
188
|
+
while peek&.type == :punct && peek.value == "/"
|
|
189
|
+
advance
|
|
190
|
+
branches << parse_seq
|
|
191
|
+
end
|
|
192
|
+
branches.length == 1 ? branches.first : Node.new(:alt, branches)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def parse_seq
|
|
196
|
+
items = [parse_element]
|
|
197
|
+
until stop_element?
|
|
198
|
+
items << parse_element
|
|
199
|
+
end
|
|
200
|
+
items.length == 1 ? items.first : Node.new(:seq, items)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def stop_element?
|
|
204
|
+
peek.nil? || peek.type == :newline ||
|
|
205
|
+
(peek.type == :punct && ["/", ")", "]", "}"].include?(peek.value))
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def parse_element
|
|
209
|
+
token = peek
|
|
210
|
+
if token.nil?
|
|
211
|
+
raise ParseError, "unexpected end of input inside sequence"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
if token.type == :num
|
|
215
|
+
advance
|
|
216
|
+
min = token.value.to_i
|
|
217
|
+
if peek&.type == :punct && peek.value == "*"
|
|
218
|
+
advance
|
|
219
|
+
max = peek&.type == :num ? advance.value.to_i : nil
|
|
220
|
+
node = Node.new(:rep, parse_postfixed, min, max)
|
|
221
|
+
else
|
|
222
|
+
node = Node.new(:rep, parse_postfixed, min, min)
|
|
223
|
+
end
|
|
224
|
+
return node
|
|
225
|
+
elsif token.type == :punct && token.value == "*"
|
|
226
|
+
advance
|
|
227
|
+
max = peek&.type == :num ? advance.value.to_i : nil
|
|
228
|
+
return Node.new(:rep, parse_postfixed, 0, max)
|
|
229
|
+
end
|
|
230
|
+
parse_postfixed
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def parse_postfixed
|
|
234
|
+
sign = nil
|
|
235
|
+
if peek&.type == :punct && %w[! &].include?(peek.value)
|
|
236
|
+
sign = advance.value == "&"
|
|
237
|
+
end
|
|
238
|
+
node = parse_primary
|
|
239
|
+
if peek&.type == :ident && peek.value == "as"
|
|
240
|
+
advance
|
|
241
|
+
node = Node.new(:cap, ident.value, node)
|
|
242
|
+
end
|
|
243
|
+
sign.nil? ? node : Node.new(:pred, sign, node)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def parse_primary
|
|
247
|
+
token = advance
|
|
248
|
+
raise ParseError, "unexpected end of input" if token.nil?
|
|
249
|
+
|
|
250
|
+
case token.type
|
|
251
|
+
when :str, :sstr then Node.new(:lit, unquote(token.value), false)
|
|
252
|
+
when :istr then Node.new(:lit, unquote(token.value), true)
|
|
253
|
+
when :hex then parse_hex(token.value)
|
|
254
|
+
when :punct then parse_punctuated(token)
|
|
255
|
+
when :ident then parse_ident(token)
|
|
256
|
+
else raise ParseError, "unexpected token #{token.type}:#{token.value}"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def parse_punctuated(token)
|
|
261
|
+
case token.value
|
|
262
|
+
when "("
|
|
263
|
+
node = parse_choice
|
|
264
|
+
punct(")")
|
|
265
|
+
node
|
|
266
|
+
when "["
|
|
267
|
+
node = parse_choice
|
|
268
|
+
punct("]")
|
|
269
|
+
Node.new(:opt, node)
|
|
270
|
+
else
|
|
271
|
+
raise ParseError, "unexpected #{token.value.inspect} in expression"
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def parse_ident(token)
|
|
276
|
+
case token.value
|
|
277
|
+
when "alt"
|
|
278
|
+
ident("from_table")
|
|
279
|
+
table = unquote(expect(:str).value)
|
|
280
|
+
ident("column")
|
|
281
|
+
column = unquote(expect(:str).value)
|
|
282
|
+
Node.new(:table, table, column)
|
|
283
|
+
when *KEYWORDS
|
|
284
|
+
raise ParseError,
|
|
285
|
+
"#{token.value.inspect} is a keyword and cannot be referenced"
|
|
286
|
+
else
|
|
287
|
+
name = token.value
|
|
288
|
+
# Dotted cross-grammar references: `use cen_cenelec` makes
|
|
289
|
+
# cen_cenelec.identifier addressable.
|
|
290
|
+
while peek&.type == :punct && peek.value == "." &&
|
|
291
|
+
@tokens[@pos + 1]&.type == :ident
|
|
292
|
+
advance
|
|
293
|
+
name = "#{name}.#{advance.value}"
|
|
294
|
+
end
|
|
295
|
+
Node.new(:ref, name)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def parse_hex(spec)
|
|
300
|
+
if spec.include?("-")
|
|
301
|
+
lo, hi = spec.split("-")
|
|
302
|
+
Node.new(:class, [[lo.to_i(16), hi.to_i(16)]])
|
|
303
|
+
elsif spec.include?(".")
|
|
304
|
+
parts = spec.split(".").map { |h| Node.new(:class, [[h.to_i(16), h.to_i(16)]]) }
|
|
305
|
+
parts.length == 1 ? parts.first : Node.new(:seq, parts)
|
|
306
|
+
else
|
|
307
|
+
byte = spec.to_i(16)
|
|
308
|
+
Node.new(:class, [[byte, byte]])
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def parse_entry(document)
|
|
313
|
+
name = ident.value
|
|
314
|
+
punct(":")
|
|
315
|
+
document.entries[name] = rule_name
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def parse_bindings(document)
|
|
319
|
+
rule = ident.value
|
|
320
|
+
punct("{")
|
|
321
|
+
list = document.bindings[rule] ||= []
|
|
322
|
+
skip_newlines
|
|
323
|
+
until peek&.type == :punct && peek.value == "}"
|
|
324
|
+
capture = ident.value
|
|
325
|
+
expect(:arrow)
|
|
326
|
+
path = parse_path
|
|
327
|
+
type = "string"
|
|
328
|
+
card = nil
|
|
329
|
+
preprocess = nil
|
|
330
|
+
if peek&.type == :punct && peek.value == "("
|
|
331
|
+
advance
|
|
332
|
+
type = ident.value
|
|
333
|
+
if peek&.type == :punct && peek.value == ","
|
|
334
|
+
advance
|
|
335
|
+
card = parse_card
|
|
336
|
+
end
|
|
337
|
+
punct(")")
|
|
338
|
+
end
|
|
339
|
+
if peek&.type == :ident && peek.value == "preprocess"
|
|
340
|
+
advance
|
|
341
|
+
punct(":")
|
|
342
|
+
preprocess = ident.value
|
|
343
|
+
end
|
|
344
|
+
list << Document::Binding.new(capture, path, type, card, preprocess)
|
|
345
|
+
skip_newlines
|
|
346
|
+
end
|
|
347
|
+
punct("}")
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def parse_path
|
|
351
|
+
parts = [ident.value]
|
|
352
|
+
loop do
|
|
353
|
+
token = peek
|
|
354
|
+
break unless token&.type == :punct && PATH_CONTINUATION.include?(token.value)
|
|
355
|
+
|
|
356
|
+
advance
|
|
357
|
+
if token.value == "."
|
|
358
|
+
parts << ".#{ident.value}"
|
|
359
|
+
else
|
|
360
|
+
punct("]")
|
|
361
|
+
parts << "[]"
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
parts.join
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def parse_card
|
|
368
|
+
pieces = []
|
|
369
|
+
loop do
|
|
370
|
+
token = peek
|
|
371
|
+
break unless token&.type == :num ||
|
|
372
|
+
(token&.type == :punct && CARD_CONTINUATION.include?(token.value))
|
|
373
|
+
|
|
374
|
+
pieces << advance.value
|
|
375
|
+
end
|
|
376
|
+
pieces.join
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# F6 v1: render specs as data - named variants of ordered segments.
|
|
380
|
+
# Segments: field <path>, literal "<text>", cond <path> { segs }.
|
|
381
|
+
# Conditions are field-presence only; expressions disallowed, so the
|
|
382
|
+
# graduation to an output grammar stays mechanical.
|
|
383
|
+
# F6 v1: derive specs - named field-composition templates
|
|
384
|
+
# ("urn:{publisher}:{number}"); {field} interpolates from the bound
|
|
385
|
+
# map. Tier-1 data, language-neutral by construction.
|
|
386
|
+
def parse_derive(document)
|
|
387
|
+
name = ident.value
|
|
388
|
+
template = unquote(expect(:str).value)
|
|
389
|
+
document.derive[name] = template
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def parse_render(document)
|
|
393
|
+
variant = ident.value
|
|
394
|
+
punct("{")
|
|
395
|
+
segments = document.render[variant] ||= []
|
|
396
|
+
skip_newlines
|
|
397
|
+
until peek&.type == :punct && peek.value == "}"
|
|
398
|
+
segments << parse_render_segment(document)
|
|
399
|
+
skip_newlines
|
|
400
|
+
end
|
|
401
|
+
punct("}")
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def parse_render_segment(document)
|
|
405
|
+
case (word = ident.value)
|
|
406
|
+
when "field"
|
|
407
|
+
{ "type" => "field", "field" => ident.value }
|
|
408
|
+
when "literal"
|
|
409
|
+
{ "type" => "literal", "text" => unquote(expect(:str).value) }
|
|
410
|
+
when "cond"
|
|
411
|
+
field = ident.value
|
|
412
|
+
punct("{")
|
|
413
|
+
then_segments = []
|
|
414
|
+
skip_newlines
|
|
415
|
+
until peek&.type == :punct && peek.value == "}"
|
|
416
|
+
then_segments << parse_render_segment(document)
|
|
417
|
+
skip_newlines
|
|
418
|
+
end
|
|
419
|
+
punct("}")
|
|
420
|
+
{ "type" => "cond", "field" => field, "then" => then_segments }
|
|
421
|
+
else
|
|
422
|
+
raise ParseError,
|
|
423
|
+
"expected a render segment (field/literal/cond), got #{word.inspect}"
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def parse_preprocess(document)
|
|
428
|
+
name = ident.value
|
|
429
|
+
punct("{")
|
|
430
|
+
steps = document.preprocess[name] ||= []
|
|
431
|
+
skip_newlines
|
|
432
|
+
until peek&.type == :punct && peek.value == "}"
|
|
433
|
+
op = ident("table_lookup").value
|
|
434
|
+
table = ident.value
|
|
435
|
+
from = ident.value
|
|
436
|
+
expect(:arrow)
|
|
437
|
+
to = ident.value
|
|
438
|
+
steps << { "op" => op, "table" => table, "from" => from, "to" => to }
|
|
439
|
+
skip_newlines
|
|
440
|
+
end
|
|
441
|
+
punct("}")
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def unquote(raw)
|
|
445
|
+
body = raw.start_with?("%") ? raw[2..] : raw
|
|
446
|
+
body = body[1..-2]
|
|
447
|
+
body.gsub(/\\(.)/) do
|
|
448
|
+
case Regexp.last_match(1)
|
|
449
|
+
when "n" then "\n"
|
|
450
|
+
when "t" then "\t"
|
|
451
|
+
when "r" then "\r"
|
|
452
|
+
else Regexp.last_match(1)
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module PARG
|
|
5
|
+
# Registry of preprocessing operations (OCP): deterministic, data-only
|
|
6
|
+
# transformations applied between capture collection and type cast.
|
|
7
|
+
# New ops register here — neither Bindings nor the compiler changes.
|
|
8
|
+
# Non-deterministic rules are NOT registrable: they belong to the
|
|
9
|
+
# language-specific data-model ingestion end (PN 5).
|
|
10
|
+
module Preprocess
|
|
11
|
+
class << self
|
|
12
|
+
def register(name, &operation)
|
|
13
|
+
operations[name.to_sym] = operation
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def apply(step, value, source)
|
|
17
|
+
operation = operations.fetch(step["op"].to_sym) do
|
|
18
|
+
raise ArtifactError, "unknown preprocess op #{step['op'].inspect}"
|
|
19
|
+
end
|
|
20
|
+
operation.call(step, value, source)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def operations
|
|
24
|
+
@operations ||= {}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
register :table_lookup do |step, value, source|
|
|
29
|
+
map = source.table_rows(step["table"]).to_h do |row|
|
|
30
|
+
[row[step["from"]].to_s, row[step["to"]]]
|
|
31
|
+
end
|
|
32
|
+
map[value.to_s] || value
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module PARG
|
|
5
|
+
# F6 v1: generic renderer over the artifact's render spec — ordered
|
|
6
|
+
# segments (field / literal / cond-presence) evaluated against a bound
|
|
7
|
+
# attribute map. The spec lives under the artifact checksum, so every
|
|
8
|
+
# engine renders byte-identical strings by construction.
|
|
9
|
+
module Render
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def apply(render, variant, bound)
|
|
13
|
+
segments = render.fetch(variant) do
|
|
14
|
+
raise ArtifactError, "render variant #{variant.inspect} not declared"
|
|
15
|
+
end
|
|
16
|
+
render_segments(segments, bound)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render_segments(segments, bound)
|
|
20
|
+
segments.filter_map { |segment| render_segment(segment, bound) }.join
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Bindings produces symbol keys in-process; the JSON contract (and
|
|
24
|
+
# the other engines) use strings — accept both.
|
|
25
|
+
def lookup(bound, field)
|
|
26
|
+
bound[field.to_s] || bound[field.to_sym]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def render_segment(segment, bound)
|
|
30
|
+
case segment["type"]
|
|
31
|
+
when "field" then lookup(bound, segment["field"]).to_s
|
|
32
|
+
when "literal" then segment["text"].to_s
|
|
33
|
+
when "cond"
|
|
34
|
+
value = lookup(bound, segment["field"])
|
|
35
|
+
return "" if value.nil?
|
|
36
|
+
|
|
37
|
+
render_segments(segment["then"], bound)
|
|
38
|
+
else
|
|
39
|
+
raise ArtifactError, "unknown render segment #{segment['type'].inspect}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module PARG
|
|
5
|
+
# F10 phase 2 (gate slice): every .parg source must parse under the parg
|
|
6
|
+
# artifact — the language definition validates itself and every
|
|
7
|
+
# grammar written in it. The artifact is authoritative for document
|
|
8
|
+
# structure; the full shape->IR front end builds on this gate.
|
|
9
|
+
module SelfHost
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def artifact_path
|
|
13
|
+
ENV["PG_ARTIFACT"] || begin
|
|
14
|
+
dir = __dir__
|
|
15
|
+
ancestors = []
|
|
16
|
+
cursor = dir
|
|
17
|
+
6.times do
|
|
18
|
+
cursor = File.expand_path("..", cursor)
|
|
19
|
+
ancestors << File.join(cursor, "pubid", "pubid-grammar", "artifacts", "parg.json")
|
|
20
|
+
end
|
|
21
|
+
ancestors.find { |candidate| File.file?(candidate) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def available?
|
|
26
|
+
!artifact_path.nil?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Parse `source` under the parg artifact. Returns the shape tree, or
|
|
30
|
+
# raises ParseFailed with the native position when the source is not
|
|
31
|
+
# valid PARG as defined by the self-hosting grammar.
|
|
32
|
+
def validate(source)
|
|
33
|
+
raise ArtifactError, "parg artifact not found (set PG_ARTIFACT)" unless available?
|
|
34
|
+
|
|
35
|
+
envelope = JSON.parse(File.read(artifact_path))
|
|
36
|
+
artifact = Artifact.new(envelope, nil, nil)
|
|
37
|
+
artifact.parse("file", source)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# true when the source parses under the self-hosting artifact.
|
|
41
|
+
def valid?(source)
|
|
42
|
+
validate(source)
|
|
43
|
+
true
|
|
44
|
+
rescue Parsanol::ParseFailed, ArtifactError
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module PARG
|
|
5
|
+
# Visitor over the PARG IR (OCP): transformations and analyses walk the
|
|
6
|
+
# tree through #visit instead of hand-rolled case statements.
|
|
7
|
+
class Visitor
|
|
8
|
+
CHILDREN = {
|
|
9
|
+
lit: [], class: [], re: [], ref: [],
|
|
10
|
+
seq: :a, alt: :a, rep: %i[a], opt: %i[a], pred: %i[b], cap: %i[b],
|
|
11
|
+
table: []
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(node)
|
|
15
|
+
@node = node
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Yields every node (self first, depth-first).
|
|
19
|
+
def each(&block)
|
|
20
|
+
walk(@node, block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def map
|
|
24
|
+
rewrite(@node)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def walk(node, block)
|
|
30
|
+
return unless node.is_a?(Node)
|
|
31
|
+
|
|
32
|
+
block.call(node)
|
|
33
|
+
child_nodes(node).each { |child| walk(child, block) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def child_nodes(node)
|
|
37
|
+
case CHILDREN[node.kind]
|
|
38
|
+
when :a then node.a
|
|
39
|
+
when Array then node.a.map { |sym| node.public_send(sym) }
|
|
40
|
+
else []
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Rebuilds the tree: the block returns a replacement node or nil to
|
|
45
|
+
# keep the original; children are rewritten first (bottom-up).
|
|
46
|
+
def rewrite(node)
|
|
47
|
+
return node unless node.is_a?(Node)
|
|
48
|
+
|
|
49
|
+
case node.kind
|
|
50
|
+
when :seq, :alt
|
|
51
|
+
node.class.new(node.a.map { |child| rewrite(child) })
|
|
52
|
+
when :rep
|
|
53
|
+
Node.new(node.kind, rewrite(node.a), node.b, node.c)
|
|
54
|
+
when :opt
|
|
55
|
+
Node.new(node.kind, rewrite(node.a))
|
|
56
|
+
when :pred, :cap
|
|
57
|
+
Node.new(node.kind, node.a, rewrite(node.b))
|
|
58
|
+
else
|
|
59
|
+
yield(node) || node
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
# PARG — the parsanol grammar language.
|
|
5
|
+
#
|
|
6
|
+
# A text source format for parsanol grammars: human-readable, ABNF/RFC
|
|
7
|
+
# flavoured, PEG semantics (ordered choice). PARG sources compile to a
|
|
8
|
+
# checksummed artifact envelope whose grammar section is the same portable
|
|
9
|
+
# JSON the native engines register — the text is the contract, the JSON is
|
|
10
|
+
# its compiled form.
|
|
11
|
+
module PARG
|
|
12
|
+
autoload :Error, "parsanol/parg/error"
|
|
13
|
+
autoload :ParseError, "parsanol/parg/error"
|
|
14
|
+
autoload :CompileError, "parsanol/parg/error"
|
|
15
|
+
autoload :ArtifactError, "parsanol/parg/error"
|
|
16
|
+
autoload :Node, "parsanol/parg/node"
|
|
17
|
+
autoload :Lexer, "parsanol/parg/lexer"
|
|
18
|
+
autoload :Parser, "parsanol/parg/parser"
|
|
19
|
+
autoload :Document, "parsanol/parg/document"
|
|
20
|
+
autoload :Compiler, "parsanol/parg/compiler"
|
|
21
|
+
autoload :Artifact, "parsanol/parg/artifact"
|
|
22
|
+
autoload :Bindings, "parsanol/parg/bindings"
|
|
23
|
+
autoload :Render, "parsanol/parg/render"
|
|
24
|
+
autoload :Derive, "parsanol/parg/derive"
|
|
25
|
+
autoload :SelfHost, "parsanol/parg/selfhost"
|
|
26
|
+
autoload :Frontend, "parsanol/parg/frontend"
|
|
27
|
+
autoload :Lutaml, "parsanol/parg/lutaml"
|
|
28
|
+
autoload :Import, "parsanol/parg/import"
|
|
29
|
+
autoload :CLI, "parsanol/parg/cli"
|
|
30
|
+
autoload :Lsp, "parsanol/parg/lsp"
|
|
31
|
+
autoload :Lints, "parsanol/parg/lints"
|
|
32
|
+
autoload :Preprocess, "parsanol/parg/preprocess"
|
|
33
|
+
autoload :Visitor, "parsanol/parg/visitor"
|
|
34
|
+
autoload :Imports, "parsanol/parg/imports"
|
|
35
|
+
autoload :Suite, "parsanol/parg/authoring"
|
|
36
|
+
autoload :Schema, "parsanol/parg/authoring"
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/parsanol/version.rb
CHANGED