parsanol 1.3.107-arm-linux → 1.3.109-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/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/native/libparsanol.so +0 -0
- data/lib/parsanol/parg/cli.rb +97 -0
- data/lib/parsanol/parg/compiler.rb +102 -5
- data/lib/parsanol/parg/diff.rb +264 -0
- data/lib/parsanol/parg/document.rb +9 -1
- data/lib/parsanol/parg/export.rb +123 -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 +59 -2
- data/lib/parsanol/parg.rb +2 -0
- data/lib/parsanol/slice.rb +13 -4
- data/lib/parsanol/version.rb +1 -1
- metadata +5 -2
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module PARG
|
|
5
|
+
module Import
|
|
6
|
+
# ANTLR 4 importer — scoped v1 (ruby#309).
|
|
7
|
+
#
|
|
8
|
+
# Imported: parser rules, lexer rules (as PARG rules), fragments
|
|
9
|
+
# (inlined name rules), literals, character sets and ranges,
|
|
10
|
+
# `?`/`*`/`+` postfixes, groups, `|` alternatives, `//` and `/* */`
|
|
11
|
+
# comments.
|
|
12
|
+
#
|
|
13
|
+
# Reported per occurrence with position, never silently dropped:
|
|
14
|
+
# semantic predicates and actions `{...}`, non-greedy quantifiers
|
|
15
|
+
# (`*?`), alternative labels (`# name`), `options {}`, `imports`,
|
|
16
|
+
# `tokenVocab`, lexer commands (`-> mode/skip/channel`).
|
|
17
|
+
#
|
|
18
|
+
# The deep semantic difference, stated like the ABNF importer's:
|
|
19
|
+
# ANTLR's adaptive LL(*) tolerates ambiguity; PARG's ordered choice
|
|
20
|
+
# is decisive. The compile-time order-dependence lint surfaces
|
|
21
|
+
# every branch pair whose order matters, so a mechanical
|
|
22
|
+
# transliteration is reviewable instead of silently different.
|
|
23
|
+
class Antlr
|
|
24
|
+
class Error < StandardError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
TOKEN = /
|
|
28
|
+
(?<block_comment>\/\*(?:[^*]|\*+[^*\/])*\*+\/)
|
|
29
|
+
| (?<line_comment>\/\/[^\n]*)
|
|
30
|
+
| (?<literal>'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")
|
|
31
|
+
| (?<charset>\[(?:[^\]\\]|\\.)*\])
|
|
32
|
+
| (?<action>\{(?:(?>[^{}])|\{(?:(?>[^{}])|\{[^{}]*\}))*\}*)
|
|
33
|
+
| (?<punct>=\/|->|\.\.|[;:|()+*?.#\[\]=~^])
|
|
34
|
+
| (?<name>[A-Za-z_][A-Za-z0-9_]*)
|
|
35
|
+
| (?<nl>\n)
|
|
36
|
+
| (?<ws>[ \t\r]+)
|
|
37
|
+
/x
|
|
38
|
+
|
|
39
|
+
SKIP = %i[block_comment line_comment ws].freeze
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
def call(text)
|
|
43
|
+
new(text).call
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize(text)
|
|
48
|
+
@text = text
|
|
49
|
+
@tokens = []
|
|
50
|
+
@pos = 0
|
|
51
|
+
@rules = {}
|
|
52
|
+
@order = []
|
|
53
|
+
scan
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def call
|
|
57
|
+
parse_grammar
|
|
58
|
+
emit
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def scan
|
|
64
|
+
pos = 0
|
|
65
|
+
line = 1
|
|
66
|
+
until pos >= @text.length
|
|
67
|
+
if @text[pos] == "\n"
|
|
68
|
+
@tokens << [:nl, "\n", line]
|
|
69
|
+
line += 1
|
|
70
|
+
pos += 1
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
match = TOKEN.match(@text, pos)
|
|
74
|
+
if match.nil? || match.begin(0) != pos
|
|
75
|
+
raise Error, "ANTLR: unexpected #{@text[pos].inspect} at offset #{pos} (line #{line})"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
name = TOKEN.names.find { |n| match[n] }
|
|
79
|
+
@tokens << [name.to_sym, match[0], line]
|
|
80
|
+
pos = match.end(0)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def peek(offset = 0)
|
|
85
|
+
@tokens[@pos + offset]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def advance
|
|
89
|
+
token = @tokens[@pos]
|
|
90
|
+
@pos += 1
|
|
91
|
+
token
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def skip_fillers
|
|
95
|
+
advance while (peek && SKIP.include?(peek[0])) || peek&.[](0) == :nl
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def parse_grammar
|
|
99
|
+
loop do
|
|
100
|
+
skip_fillers
|
|
101
|
+
break if peek.nil?
|
|
102
|
+
|
|
103
|
+
# `grammar Name;` — the declaration carries no rules.
|
|
104
|
+
if peek&.[](0) == :name && peek[1] == "grammar"
|
|
105
|
+
advance
|
|
106
|
+
skip_fillers
|
|
107
|
+
advance while peek&.[](0) == :name
|
|
108
|
+
skip_fillers
|
|
109
|
+
advance if peek&.[](0) == :punct && peek[1] == ";"
|
|
110
|
+
next
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
parse_rule
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def parse_rule
|
|
118
|
+
skip_fillers
|
|
119
|
+
name_token = advance
|
|
120
|
+
raise Error, "ANTLR: expected rule name, got #{name_token&.[](1).inspect}" unless name_token&.[](0) == :name
|
|
121
|
+
|
|
122
|
+
skip_fillers
|
|
123
|
+
|
|
124
|
+
colon = advance
|
|
125
|
+
unless colon && colon[0] == :punct && colon[1] == ":"
|
|
126
|
+
what = colon&.[](1) == "=" ? "'=' (ANTLR has no rule assignment)" : colon&.[](1).inspect
|
|
127
|
+
raise Error,
|
|
128
|
+
"ANTLR: rule #{name_token[1].inspect} (line #{name_token[2]}): expected ':', got #{what} — " \
|
|
129
|
+
"options/imports/tokenVocab sections are out of scope for v1"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
name = normalize_name(name_token[1])
|
|
133
|
+
body = parse_alternation
|
|
134
|
+
finish_rule(name, body)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def finish_rule(name, body)
|
|
138
|
+
raise Error, "ANTLR: expected ';' after rule #{name.inspect}" unless semicolon?
|
|
139
|
+
|
|
140
|
+
if @rules.key?(name)
|
|
141
|
+
raise Error, "ANTLR: duplicate rule #{name.inspect}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
@rules[name] = body
|
|
145
|
+
@order << name
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def semicolon?
|
|
149
|
+
skip_fillers
|
|
150
|
+
return false unless peek&.[](0) == :punct && peek[1] == ";"
|
|
151
|
+
|
|
152
|
+
advance
|
|
153
|
+
true
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def parse_alternation
|
|
157
|
+
parts = [parse_element_sequence]
|
|
158
|
+
while peek&.[](0) == :punct && peek[1] == "|"
|
|
159
|
+
advance
|
|
160
|
+
parts << parse_element_sequence
|
|
161
|
+
end
|
|
162
|
+
reject_label!
|
|
163
|
+
parts.join(" / ")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def reject_label!
|
|
167
|
+
return unless peek&.[](0) == :punct && peek[1] == "#"
|
|
168
|
+
|
|
169
|
+
token = advance
|
|
170
|
+
label = advance
|
|
171
|
+
raise Error,
|
|
172
|
+
"ANTLR: alternative labels (##{label&.[](1)}) are out of scope for v1 " \
|
|
173
|
+
"(line #{token[2]}) — label the rule instead"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def element_sequence_terminator?(token)
|
|
177
|
+
token&.[](0) == :punct && %w[; | )].include?(token[1])
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def reject_lexer_command!
|
|
181
|
+
return unless peek&.[](0) == :punct && peek[1] == "->"
|
|
182
|
+
|
|
183
|
+
token = advance
|
|
184
|
+
skip_fillers
|
|
185
|
+
command = advance
|
|
186
|
+
guidance =
|
|
187
|
+
if command&.[](1) == "skip"
|
|
188
|
+
"PARG's equivalent is a `skip = <rule>` declaration at the grammar level, " \
|
|
189
|
+
"not a per-token command"
|
|
190
|
+
else
|
|
191
|
+
"lexer modes/channels have no PARG equivalent"
|
|
192
|
+
end
|
|
193
|
+
raise Error,
|
|
194
|
+
"ANTLR: lexer command -> #{command&.[](1).inspect} (line #{token[2]}) is out of " \
|
|
195
|
+
"scope for v1 — #{guidance}"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def parse_element_sequence
|
|
199
|
+
parts = []
|
|
200
|
+
loop do
|
|
201
|
+
skip_fillers
|
|
202
|
+
break if peek.nil?
|
|
203
|
+
break if element_sequence_terminator?(peek)
|
|
204
|
+
|
|
205
|
+
reject_lexer_command!
|
|
206
|
+
|
|
207
|
+
part = parse_postfixed
|
|
208
|
+
next if part == :eof # consume-all is implicit in PARG
|
|
209
|
+
|
|
210
|
+
parts << part
|
|
211
|
+
end
|
|
212
|
+
raise Error, "ANTLR: empty rule body" if parts.empty?
|
|
213
|
+
|
|
214
|
+
parts.length == 1 ? parts[0] : "(#{parts.join(' ')})"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def parse_postfixed
|
|
218
|
+
element = parse_element
|
|
219
|
+
loop do
|
|
220
|
+
skip_fillers
|
|
221
|
+
token = peek
|
|
222
|
+
break unless token&.[](0) == :punct
|
|
223
|
+
|
|
224
|
+
case token[1]
|
|
225
|
+
when "?" then advance and element = "[#{element}]"
|
|
226
|
+
when "*", "+"
|
|
227
|
+
advance
|
|
228
|
+
skip_fillers
|
|
229
|
+
if peek&.[](0) == :punct && peek[1] == "?"
|
|
230
|
+
raise Error,
|
|
231
|
+
"ANTLR: non-greedy quantifier #{token[1]}? (line #{token[2]}) is out of " \
|
|
232
|
+
"scope for v1 — ordered choice has no lazy repetition; " \
|
|
233
|
+
"restructure the rule"
|
|
234
|
+
end
|
|
235
|
+
element = token[1] == "*" ? "*#{element}" : "1*#{element}"
|
|
236
|
+
else break
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
element
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def parse_element
|
|
243
|
+
token = advance
|
|
244
|
+
raise Error, "ANTLR: unexpected end of rule body" if token.nil?
|
|
245
|
+
|
|
246
|
+
case token[0]
|
|
247
|
+
when :literal then emit_literal(token[1], token[2])
|
|
248
|
+
when :charset then emit_charset(token[1], token[2])
|
|
249
|
+
when :name
|
|
250
|
+
return :eof if token[1] == "EOF" # consume-all is implicit
|
|
251
|
+
|
|
252
|
+
normalize_name(token[1])
|
|
253
|
+
when :punct
|
|
254
|
+
case token[1]
|
|
255
|
+
when "#"
|
|
256
|
+
raise Error,
|
|
257
|
+
"ANTLR: alternative labels are out of scope for v1 " \
|
|
258
|
+
"(line #{token[2]}) — label the rule instead"
|
|
259
|
+
when "("
|
|
260
|
+
body = parse_alternation
|
|
261
|
+
skip_fillers
|
|
262
|
+
expect ")"
|
|
263
|
+
"(#{body})"
|
|
264
|
+
when "."
|
|
265
|
+
"ANY"
|
|
266
|
+
when "~"
|
|
267
|
+
# The negation idiom: ~[set] = any codepoint outside the
|
|
268
|
+
# set, as a lookahead-guarded ANY. Quantifiers land outside
|
|
269
|
+
# via parse_postfixed.
|
|
270
|
+
set_token = advance
|
|
271
|
+
raise Error, "ANTLR: ~ must apply to a character set (line #{token[2]})" unless set_token&.[](0) == :charset
|
|
272
|
+
|
|
273
|
+
ranges = charset_ranges(set_token[1][1..-2], token[2])
|
|
274
|
+
set_text = ranges.map { |first, last| range_text(first, last) }.join(" / ")
|
|
275
|
+
"( !( (#{set_text}) ) ANY )"
|
|
276
|
+
else
|
|
277
|
+
raise Error, "ANTLR: unexpected #{token[1].inspect} (line #{token[2]})"
|
|
278
|
+
end
|
|
279
|
+
when :action
|
|
280
|
+
raise Error,
|
|
281
|
+
"ANTLR: action/predicate #{token[1][0, 24].inspect} (line #{token[2]}) is out of " \
|
|
282
|
+
"scope for v1 — target-language code has no PARG equivalent"
|
|
283
|
+
else
|
|
284
|
+
raise Error, "ANTLR: unexpected token #{token[0]} (line #{token[2]})"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def expect(char)
|
|
289
|
+
token = advance
|
|
290
|
+
return if token&.[](0) == :punct && token[1] == char
|
|
291
|
+
|
|
292
|
+
raise Error, "ANTLR: expected #{char.inspect}, got #{token&.[](1).inspect} (line #{token&.[](2)})"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def emit_literal(token, line)
|
|
296
|
+
body = token[1..-2]
|
|
297
|
+
raise Error, "ANTLR: empty literal (line #{line})" if body.empty?
|
|
298
|
+
|
|
299
|
+
escaped = body.gsub("\\", "\\\\").gsub('"', '\\"')
|
|
300
|
+
"\"#{escaped}\""
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# [a-z], [abc], [^abc], [a-zA-Z0-9_] -- ANTLR char sets are
|
|
304
|
+
# case-SENSITIVE single characters. PARG expresses classes as
|
|
305
|
+
# %x-range alternations.
|
|
306
|
+
def emit_charset(token, line)
|
|
307
|
+
body = token[1..-2]
|
|
308
|
+
negated = body.start_with?("^")
|
|
309
|
+
body = body[1..] if negated
|
|
310
|
+
ranges = charset_ranges(body, line)
|
|
311
|
+
ranges = complement_ranges(ranges) if negated
|
|
312
|
+
"(#{ranges.map { |lo, hi| range_text(lo, hi) }.join(' / ')})"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def charset_ranges(body, line)
|
|
316
|
+
ranges = []
|
|
317
|
+
i = 0
|
|
318
|
+
while i < body.length
|
|
319
|
+
if body[i] == "\\"
|
|
320
|
+
byte = escape_byte(body[i + 1], line)
|
|
321
|
+
ranges << [byte, byte]
|
|
322
|
+
i += 2
|
|
323
|
+
elsif body[i + 1] == "-" && body[i + 2] && body[i + 2] != "]"
|
|
324
|
+
lo = literal_byte(body[i], line)
|
|
325
|
+
hi = literal_byte(body[i + 2], line)
|
|
326
|
+
raise Error, "ANTLR: reversed range (line #{line})" if lo > hi
|
|
327
|
+
|
|
328
|
+
ranges << [lo, hi]
|
|
329
|
+
i += 3
|
|
330
|
+
else
|
|
331
|
+
ranges << [literal_byte(body[i], line), literal_byte(body[i], line)]
|
|
332
|
+
i += 1
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
ranges
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# A plain (unescaped) charset character is its own codepoint --
|
|
339
|
+
# running it through the escape table mapped lowercase r/t/n to
|
|
340
|
+
# CR/TAB/LF (the [Rr][Ee][Cc][Tt]-style keyword idiom broke).
|
|
341
|
+
def literal_byte(char, line)
|
|
342
|
+
byte = char.ord
|
|
343
|
+
return byte if byte <= 0x7F
|
|
344
|
+
|
|
345
|
+
raise Error, "ANTLR: non-ASCII #{char.inspect} in a character set (line #{line}) -- " \
|
|
346
|
+
"write a %x range or a literal instead"
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def escape_byte(char, line)
|
|
350
|
+
case char
|
|
351
|
+
when "n" then 0x0A
|
|
352
|
+
when "r" then 0x0D
|
|
353
|
+
when "t" then 0x09
|
|
354
|
+
when "\\" then 0x5C
|
|
355
|
+
else
|
|
356
|
+
byte = char.ord
|
|
357
|
+
if byte > 0x7F
|
|
358
|
+
raise Error, "ANTLR: non-ASCII #{char.inspect} in a character set (line #{line}) -- " \
|
|
359
|
+
"write a %x range or a literal instead"
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
byte
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def complement_ranges(ranges)
|
|
367
|
+
result = []
|
|
368
|
+
cursor = 0x00
|
|
369
|
+
ranges.sort.each do |lo, hi|
|
|
370
|
+
result << [cursor, lo - 1] if lo > cursor
|
|
371
|
+
cursor = hi + 1
|
|
372
|
+
end
|
|
373
|
+
result << [cursor, 0x7F] if cursor <= 0x7F
|
|
374
|
+
raise Error, "ANTLR: negated set covers everything" if result.empty?
|
|
375
|
+
|
|
376
|
+
result
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def range_text(first, last)
|
|
380
|
+
first == last ? "%x#{format('%02X', first)}" : "%x#{format('%02X', first)}-#{format('%02X', last)}"
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def normalize_name(name)
|
|
384
|
+
# Lexer rules are UPPER_CASE; PARG rule names are lowercase.
|
|
385
|
+
name.downcase
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def emit
|
|
389
|
+
header = <<~PARG
|
|
390
|
+
# Imported from ANTLR 4 (scoped v1).
|
|
391
|
+
# Notes:
|
|
392
|
+
# - ANTLR's adaptive LL(*) tolerates ambiguity; PARG's ordered
|
|
393
|
+
# choice is decisive — the compiler's order-dependence lint
|
|
394
|
+
# surfaces every branch pair whose order matters.
|
|
395
|
+
# - Lexer rules imported as ordinary rules; case-sensitivity of
|
|
396
|
+
# ANTLR literals is preserved (plain "..." literals).
|
|
397
|
+
# - Actions, predicates, labels, options, lexer commands:
|
|
398
|
+
# rejected with position, never silently dropped.
|
|
399
|
+
PARG
|
|
400
|
+
body = @order.filter_map { |name| " #{"#{name} = #{@rules[name]}" if @rules[name]}" }
|
|
401
|
+
.join("\n")
|
|
402
|
+
"#{header}grammar Imported version \"0.0.0\" {\n#{body}\n}\n"
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
data/lib/parsanol/parg/lexer.rb
CHANGED
|
@@ -17,6 +17,7 @@ module Parsanol
|
|
|
17
17
|
| (?<doc>\#\#[^\n]*)
|
|
18
18
|
| (?<comment>\#[^\n]*)
|
|
19
19
|
| (?<hex>%x[0-9A-Fa-f]{2,6}(?:-[0-9A-Fa-f]{2,6}|(?:\.[0-9A-Fa-f]{2,6})+)?)
|
|
20
|
+
| (?<category>%p\{[A-Za-z]{1,2}\})
|
|
20
21
|
| (?<istr>%i"(?:[^"\\]|\\.)*")
|
|
21
22
|
| (?<sstr>%s"(?:[^"\\]|\\.)*")
|
|
22
23
|
| (?<str>"(?:[^"\\]|\\.)*")
|