birman 0.2.2 → 0.3.0
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/CHANGELOG.md +204 -0
- data/README.md +381 -14
- data/lib/birman/ast.rb +10 -1
- data/lib/birman/combinators.rb +25 -14
- data/lib/birman/compiler/actions.rb +309 -0
- data/lib/birman/compiler/emitter.rb +940 -0
- data/lib/birman/compiler/literals.rb +29 -0
- data/lib/birman/compiler/nodes.rb +158 -0
- data/lib/birman/compiler/regexps.rb +182 -0
- data/lib/birman/compiler/runtime.rb +758 -0
- data/lib/birman/compiler/types.rb +128 -0
- data/lib/birman/compiler.rb +155 -0
- data/lib/birman/context.rb +124 -15
- data/lib/birman/grammar.rb +93 -16
- data/lib/birman/left_recursion.rb +409 -0
- data/lib/birman/matchers.rb +224 -38
- data/lib/birman/result.rb +10 -1
- data/lib/birman/rule_set.rb +19 -6
- data/lib/birman/rules.rb +10 -5
- data/lib/birman/standalone.rb +111 -0
- data/lib/birman/version.rb +1 -1
- data/lib/birman.rb +22 -0
- metadata +11 -1
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
# The runtime every parser made by Grammar.to_ruby ships with: the
|
|
2
|
+
# generator copies everything after the require below into the parser's
|
|
3
|
+
# namespace, so two generated parsers never share (or clash over) one.
|
|
4
|
+
# It's written in a deliberately small Ruby subset so Spinel can compile
|
|
5
|
+
# it: plain classes (no Data, no Struct), integer byte positions read
|
|
6
|
+
# with getbyte, no keyword arguments, no blocks, no metaprogramming. Inline RBS type
|
|
7
|
+
# comments give Spinel types it can't infer from empty literals; CRuby
|
|
8
|
+
# ignores them.
|
|
9
|
+
#
|
|
10
|
+
# Generated code calls this as follows:
|
|
11
|
+
#
|
|
12
|
+
# - Every match method takes a byte position and returns the byte
|
|
13
|
+
# position where its match ended, or FAIL (-1). Values go in instance
|
|
14
|
+
# variables, one per rule, so each one keeps a single type.
|
|
15
|
+
# - fail_at, merge and the save/restore of @furthest/@expected around a
|
|
16
|
+
# rule call do what Birman::Context's do, so the parser reports the
|
|
17
|
+
# same errors as the interpreter.
|
|
18
|
+
|
|
19
|
+
require "strscan"
|
|
20
|
+
|
|
21
|
+
module BirmanRT
|
|
22
|
+
FAIL = -1
|
|
23
|
+
IN_PROGRESS = -2
|
|
24
|
+
# A memo entry abandoned by a commit failure unwinding through it (a
|
|
25
|
+
# lookahead caught the commit). Written instead of deleting the key:
|
|
26
|
+
# Hash#delete on Spinel's Integer-keyed Hash is O(size).
|
|
27
|
+
ABSENT = -3
|
|
28
|
+
|
|
29
|
+
# Bignum literals: see BirmanRT.to_i and based_to_i.
|
|
30
|
+
TEN19 = 10_000_000_000_000_000_000
|
|
31
|
+
TWO64 = 18_446_744_073_709_551_616
|
|
32
|
+
|
|
33
|
+
# A Failure's expectations before anything has failed. It is never
|
|
34
|
+
# appended to: fail_at replaces an empty list rather than adding to it.
|
|
35
|
+
# Typed through Array.new: a bare [].freeze trips a Spinel pin bug.
|
|
36
|
+
NONE = Array.new(0, "").freeze
|
|
37
|
+
|
|
38
|
+
class Committed < StandardError
|
|
39
|
+
attr_reader :pos, :expected
|
|
40
|
+
|
|
41
|
+
#: (Integer, Array[String]) -> void
|
|
42
|
+
def initialize(pos, expected)
|
|
43
|
+
@pos = pos
|
|
44
|
+
@expected = expected
|
|
45
|
+
super("committed failure")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Lookaheads nested past the parser's LOOKAHEAD_DEPTH: Parser#parse
|
|
50
|
+
# turns it into a Failure, as it does SystemStackError.
|
|
51
|
+
class TooDeep < StandardError
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Base class of every generated node, so a rule whose value is one of
|
|
55
|
+
# several node classes still has a single static type.
|
|
56
|
+
class Node
|
|
57
|
+
attr_reader :span
|
|
58
|
+
|
|
59
|
+
# Overridden by every node class: inspect, plus each node's span as
|
|
60
|
+
# [byte_from,byte_to], for the diff tests.
|
|
61
|
+
def dump
|
|
62
|
+
""
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class Span
|
|
67
|
+
attr_reader :input, :byte_from, :byte_to
|
|
68
|
+
|
|
69
|
+
def initialize(input, byte_from, byte_to)
|
|
70
|
+
@input = input
|
|
71
|
+
@byte_from = byte_from
|
|
72
|
+
@byte_to = byte_to
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def text
|
|
76
|
+
@input.byteslice(@byte_from, @byte_to - @byte_from)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Character offsets, as Birman::Span's.
|
|
80
|
+
def from
|
|
81
|
+
BirmanRT.char_count(@input, 0, @byte_from)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def to
|
|
85
|
+
BirmanRT.char_count(@input, 0, @byte_to)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def line
|
|
89
|
+
n = 1
|
|
90
|
+
i = 0
|
|
91
|
+
while i < @byte_from
|
|
92
|
+
n += 1 if @input.getbyte(i) == 10
|
|
93
|
+
i += 1
|
|
94
|
+
end
|
|
95
|
+
n
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def column
|
|
99
|
+
i = @byte_from
|
|
100
|
+
i -= 1 while i.positive? && @input.getbyte(i - 1) != 10
|
|
101
|
+
BirmanRT.char_count(@input, i, @byte_from) + 1
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def to_s
|
|
105
|
+
"line #{line}, column #{column}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def inspect
|
|
109
|
+
"#<Birman::Span #{line}:#{column} #{text.inspect}>"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class Success
|
|
114
|
+
attr_reader :value
|
|
115
|
+
|
|
116
|
+
def initialize(value)
|
|
117
|
+
@value = value
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def success?
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# pos counts characters, as Birman::Failure's does; byte_pos is kept
|
|
126
|
+
# so line, column and found don't have to convert back.
|
|
127
|
+
class Failure
|
|
128
|
+
attr_reader :input, :pos, :byte_pos, :expected, :committed, :reason
|
|
129
|
+
|
|
130
|
+
#: (String, Integer, Array[String], bool, String?) -> void
|
|
131
|
+
def initialize(input, byte_pos, expected, committed, reason)
|
|
132
|
+
@input = input
|
|
133
|
+
@byte_pos = byte_pos
|
|
134
|
+
@pos = BirmanRT.char_count(input, 0, byte_pos)
|
|
135
|
+
@expected = expected
|
|
136
|
+
@committed = committed
|
|
137
|
+
@reason = reason
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def success?
|
|
141
|
+
false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def line
|
|
145
|
+
n = 1
|
|
146
|
+
i = 0
|
|
147
|
+
while i < @byte_pos
|
|
148
|
+
n += 1 if @input.getbyte(i) == 10
|
|
149
|
+
i += 1
|
|
150
|
+
end
|
|
151
|
+
n
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def column
|
|
155
|
+
BirmanRT.char_count(@input, line_start, @byte_pos) + 1
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# The character at the failure, inspected, as Birman::Failure#found.
|
|
159
|
+
def found
|
|
160
|
+
return "end of input" if @byte_pos >= @input.bytesize
|
|
161
|
+
|
|
162
|
+
width = BirmanRT.single_byte?(@input) ? 1 : BirmanRT.utf8_width(@input, @byte_pos)
|
|
163
|
+
width = 1 if width.zero?
|
|
164
|
+
@input.byteslice(@byte_pos, width).inspect
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Birman::ErrorFormatter's wording.
|
|
168
|
+
def message
|
|
169
|
+
problem = @reason || "expected #{expected_list}, found #{found}"
|
|
170
|
+
"line #{line}, column #{column}: #{problem}\n #{source_line}\n #{caret_indent}^"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def line_start
|
|
176
|
+
i = @byte_pos
|
|
177
|
+
i -= 1 while i.positive? && @input.getbyte(i - 1) != 10
|
|
178
|
+
i
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def source_line
|
|
182
|
+
start = line_start
|
|
183
|
+
stop = start
|
|
184
|
+
stop += 1 while stop < @input.bytesize && @input.getbyte(stop) != 10
|
|
185
|
+
stop -= 1 if stop > start && @input.getbyte(stop - 1) == 13
|
|
186
|
+
@input.byteslice(start, stop - start)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Spaces up to the caret, except tabs stay tabs.
|
|
190
|
+
def caret_indent
|
|
191
|
+
out = +""
|
|
192
|
+
i = line_start
|
|
193
|
+
while i < @byte_pos
|
|
194
|
+
b = @input.getbyte(i)
|
|
195
|
+
out << (b == 9 ? "\t" : " ") unless BirmanRT.continuation?(b) && !BirmanRT.single_byte?(@input)
|
|
196
|
+
i += 1
|
|
197
|
+
end
|
|
198
|
+
out
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def expected_list
|
|
202
|
+
return "nothing" if @expected.empty?
|
|
203
|
+
return @expected[0] if @expected.size == 1
|
|
204
|
+
|
|
205
|
+
"#{@expected[0, @expected.size - 1].join(", ")} or #{@expected[@expected.size - 1]}"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Binary (and US-ASCII) input has a character per byte; otherwise
|
|
210
|
+
# it's UTF-8 (Parser#initialize refuses anything else).
|
|
211
|
+
def self.single_byte?(input)
|
|
212
|
+
encoding = input.encoding
|
|
213
|
+
encoding == Encoding::BINARY || encoding == Encoding::US_ASCII
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def self.continuation?(b)
|
|
217
|
+
b >= 0x80 && b < 0xC0
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def self.char_width(b)
|
|
221
|
+
return 1 if b < 0x80
|
|
222
|
+
return 2 if b < 0xE0
|
|
223
|
+
return 3 if b < 0xF0
|
|
224
|
+
|
|
225
|
+
4
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# The first byte of input that doesn't start a valid character, or
|
|
229
|
+
# FAIL: Birman.invalid_byte, by hand. Binary input is always valid,
|
|
230
|
+
# and US-ASCII is only ASCII.
|
|
231
|
+
def self.invalid_byte(input)
|
|
232
|
+
return FAIL if input.encoding == Encoding::BINARY
|
|
233
|
+
|
|
234
|
+
ascii = single_byte?(input)
|
|
235
|
+
i = 0
|
|
236
|
+
while i < input.bytesize
|
|
237
|
+
w = utf8_width(input, i)
|
|
238
|
+
# US-ASCII has no multibyte characters.
|
|
239
|
+
return i if w.zero? || (ascii && w > 1)
|
|
240
|
+
|
|
241
|
+
i += w
|
|
242
|
+
end
|
|
243
|
+
FAIL
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# The width of the UTF-8 character at byte i, or 0 if the bytes there
|
|
247
|
+
# aren't one, as String#valid_encoding? sees it: the second byte's
|
|
248
|
+
# range rules out overlong forms, surrogates and code points past
|
|
249
|
+
# U+10FFFF, and a truncated character is invalid at its first byte.
|
|
250
|
+
def self.utf8_width(input, i)
|
|
251
|
+
b = input.getbyte(i)
|
|
252
|
+
return 1 if b < 0x80
|
|
253
|
+
return 0 if b < 0xC2 || b > 0xF4
|
|
254
|
+
|
|
255
|
+
w = char_width(b)
|
|
256
|
+
return 0 if i + w > input.bytesize
|
|
257
|
+
|
|
258
|
+
lo = 0x80
|
|
259
|
+
hi = 0xBF
|
|
260
|
+
if b == 0xE0
|
|
261
|
+
lo = 0xA0
|
|
262
|
+
elsif b == 0xF0
|
|
263
|
+
lo = 0x90
|
|
264
|
+
elsif b == 0xED
|
|
265
|
+
hi = 0x9F
|
|
266
|
+
elsif b == 0xF4
|
|
267
|
+
hi = 0x8F
|
|
268
|
+
end
|
|
269
|
+
c = input.getbyte(i + 1)
|
|
270
|
+
return 0 if c < lo || c > hi
|
|
271
|
+
|
|
272
|
+
k = 2
|
|
273
|
+
while k < w
|
|
274
|
+
return 0 unless continuation?(input.getbyte(i + k))
|
|
275
|
+
|
|
276
|
+
k += 1
|
|
277
|
+
end
|
|
278
|
+
w
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Characters in input's bytes from...to: in UTF-8, every byte that
|
|
282
|
+
# isn't a continuation byte starts one.
|
|
283
|
+
def self.char_count(input, from, to)
|
|
284
|
+
return to - from if single_byte?(input)
|
|
285
|
+
|
|
286
|
+
n = 0
|
|
287
|
+
i = from
|
|
288
|
+
while i < to
|
|
289
|
+
n += 1 unless continuation?(input.getbyte(i))
|
|
290
|
+
i += 1
|
|
291
|
+
end
|
|
292
|
+
n
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# A value as the diff tests compare it: like inspect, but each node
|
|
296
|
+
# also shows its span as [byte_from,byte_to], and a string that isn't
|
|
297
|
+
# valid UTF-8 (a lone \ud83d escape in JSON, say) shows its bytes,
|
|
298
|
+
# since Spinel's String#inspect writes those differently from CRuby's.
|
|
299
|
+
def self.dump(value)
|
|
300
|
+
case value
|
|
301
|
+
when Node then value.dump
|
|
302
|
+
when Array then dump_array(value)
|
|
303
|
+
when Hash then dump_hash(value)
|
|
304
|
+
when String then value.valid_encoding? ? value.inspect : value.b.inspect
|
|
305
|
+
else value.inspect
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def self.dump_array(values)
|
|
310
|
+
out = +"["
|
|
311
|
+
i = 0
|
|
312
|
+
while i < values.size
|
|
313
|
+
out << ", " if i.positive?
|
|
314
|
+
out << dump(values[i])
|
|
315
|
+
i += 1
|
|
316
|
+
end
|
|
317
|
+
out << "]"
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def self.dump_hash(hash)
|
|
321
|
+
out = +"{"
|
|
322
|
+
keys = hash.keys
|
|
323
|
+
i = 0
|
|
324
|
+
while i < keys.size
|
|
325
|
+
out << ", " if i.positive?
|
|
326
|
+
out << dump(keys[i]) << " => " << dump(hash[keys[i]])
|
|
327
|
+
i += 1
|
|
328
|
+
end
|
|
329
|
+
out << "}"
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# String#to_i, Bignums included. Spinel has no Bignum arithmetic by
|
|
333
|
+
# default (Integer overflow raises), and --int-overflow=promote would
|
|
334
|
+
# box every position in the parser. Arithmetic with a Bignum literal
|
|
335
|
+
# operand does promote, so longer numerals are built 18 digits at a
|
|
336
|
+
# time, scaled through one.
|
|
337
|
+
# A String only: the generator routes nothing else here, and anything
|
|
338
|
+
# else would be read wrongly rather than fail.
|
|
339
|
+
def self.to_i(text)
|
|
340
|
+
raise TypeError, "BirmanRT.to_i reads a String, not #{text.class}" unless text.is_a?(String)
|
|
341
|
+
return text.to_i if text.bytesize <= 18
|
|
342
|
+
|
|
343
|
+
i = 0
|
|
344
|
+
i += 1 while i < text.bytesize && space?(text.getbyte(i))
|
|
345
|
+
negative = i < text.bytesize && text.getbyte(i) == 45
|
|
346
|
+
i += 1 if i < text.bytesize && (text.getbyte(i) == 45 || text.getbyte(i) == 43)
|
|
347
|
+
n = digits_to_i(integer_digits(text, i))
|
|
348
|
+
negative ? -n : n
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# The digits of the numeral starting at i, as to_i reads it: single
|
|
352
|
+
# underscores between digits are skipped, and anything else ends it.
|
|
353
|
+
def self.integer_digits(text, i)
|
|
354
|
+
out = +""
|
|
355
|
+
while i < text.bytesize
|
|
356
|
+
b = text.getbyte(i)
|
|
357
|
+
if digit?(b)
|
|
358
|
+
out << text.byteslice(i, 1)
|
|
359
|
+
elsif !(b == 95 && !out.empty? && i + 1 < text.bytesize && digit?(text.getbyte(i + 1)))
|
|
360
|
+
break
|
|
361
|
+
end
|
|
362
|
+
i += 1
|
|
363
|
+
end
|
|
364
|
+
out
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def self.digits_to_i(digits)
|
|
368
|
+
n = 0
|
|
369
|
+
i = 0
|
|
370
|
+
while i < digits.bytesize
|
|
371
|
+
k = digits.bytesize - i
|
|
372
|
+
k = 18 if k > 18
|
|
373
|
+
n = (n * (TEN19 / (10**(19 - k)))) + digits.byteslice(i, k).to_i
|
|
374
|
+
i += k
|
|
375
|
+
end
|
|
376
|
+
n
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Kernel#Integer(value, base), for Grammar#large_integer in copied
|
|
380
|
+
# actions: Spinel's Integer raises RangeError past 63 bits, so long
|
|
381
|
+
# strings and big Floats are read here. Bases are Integers, or Floats
|
|
382
|
+
# truncated; Rational and NaN bases differ from Kernel's (TypeError,
|
|
383
|
+
# FloatDomainError).
|
|
384
|
+
#
|
|
385
|
+
# Each branch compiles whatever the callers pass: Spinel's is_a? doesn't
|
|
386
|
+
# narrow value's type, so a branch for another type would call a method
|
|
387
|
+
# value doesn't have. to_s and to_f are no-ops where they run, and keep
|
|
388
|
+
# the typed helpers' arguments typed.
|
|
389
|
+
def self.to_integer(value, base)
|
|
390
|
+
base = integer_base(base)
|
|
391
|
+
if value.is_a?(String)
|
|
392
|
+
string_to_integer(value.to_s, base)
|
|
393
|
+
elsif value.is_a?(Float) && base == 0
|
|
394
|
+
float_to_i(value.to_f)
|
|
395
|
+
else
|
|
396
|
+
base == 0 ? Integer(value) : Integer(value, base)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Integer's syntax: whitespace around, a sign, a prefix (with base 0:
|
|
401
|
+
# 0x 0b 0o 0d or 0; with base 2, 8, 10 or 16, its own), digits with
|
|
402
|
+
# single underscores. 12 characters in any base are under 36**12,
|
|
403
|
+
# within 63 bits, so those go to Integer itself.
|
|
404
|
+
#: (String, Integer) -> untyped
|
|
405
|
+
def self.string_to_integer(text, base)
|
|
406
|
+
unless text.bytesize > 12 && (base == 0 || (base >= 2 && base <= 36))
|
|
407
|
+
return base == 0 ? Integer(text) : Integer(text, base)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
base, digits, negative = integer_syntax(text, base)
|
|
411
|
+
raise ArgumentError, "invalid value for Integer(): #{text.inspect}" if digits.nil?
|
|
412
|
+
|
|
413
|
+
n = base == 10 ? digits_to_i(digits) : based_to_i(digits, base)
|
|
414
|
+
negative ? -n : n
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Past 2**62, Floats are whole numbers; Spinel's Float#to_i raises
|
|
418
|
+
# RangeError past 63 bits ("Bignum promotion pending").
|
|
419
|
+
FLOAT_CHUNK = 4_611_686_018_427_387_904.0
|
|
420
|
+
|
|
421
|
+
# Integer(f), exactly. A Float past 2**62 is halved below 2**61 (exact:
|
|
422
|
+
# only its exponent changes), converted natively, then doubled back
|
|
423
|
+
# through Bignum literals, up to 62 bits at a time. Each step is at
|
|
424
|
+
# least 2 bits (so at least twice halved), since 2**(64 - 1) overflows.
|
|
425
|
+
#: (Float) -> untyped
|
|
426
|
+
def self.float_to_i(f)
|
|
427
|
+
raise FloatDomainError, "NaN" if f.nan?
|
|
428
|
+
raise FloatDomainError, f > 0 ? "Infinity" : "-Infinity" if f.infinite?
|
|
429
|
+
return f.to_i if f.abs < FLOAT_CHUNK
|
|
430
|
+
|
|
431
|
+
e = 0
|
|
432
|
+
while f.abs >= FLOAT_CHUNK / 2
|
|
433
|
+
f /= 2
|
|
434
|
+
e += 1
|
|
435
|
+
end
|
|
436
|
+
n = f.to_i
|
|
437
|
+
while e > 0
|
|
438
|
+
k = e > 62 ? 62 : e
|
|
439
|
+
k -= 1 if e - k == 1
|
|
440
|
+
n = n * (TWO64 / (2**(64 - k)))
|
|
441
|
+
e -= k
|
|
442
|
+
end
|
|
443
|
+
n
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Kernel#Integer's base argument: an Integer, or a Float truncated (a
|
|
447
|
+
# Rational, which Kernel takes, raises TypeError here).
|
|
448
|
+
def self.integer_base(base)
|
|
449
|
+
return base if base.is_a?(Integer)
|
|
450
|
+
return base.to_i if base.is_a?(Float)
|
|
451
|
+
raise TypeError, "no implicit conversion from nil to integer" if base.nil?
|
|
452
|
+
|
|
453
|
+
raise TypeError, "no implicit conversion of #{base == true || base == false ? base : base.class} into Integer"
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# [base, digits without underscores, negative], or nil if text isn't
|
|
457
|
+
# an integer in Integer's syntax for base (0: from its prefix).
|
|
458
|
+
def self.integer_syntax(text, base)
|
|
459
|
+
i = 0
|
|
460
|
+
i += 1 while i < text.bytesize && space?(text.getbyte(i))
|
|
461
|
+
negative = i < text.bytesize && text.getbyte(i) == 45
|
|
462
|
+
i += 1 if i < text.bytesize && (text.getbyte(i) == 45 || text.getbyte(i) == 43)
|
|
463
|
+
if text.getbyte(i) == 48 && i + 1 < text.bytesize
|
|
464
|
+
b = text.getbyte(i + 1)
|
|
465
|
+
if base == 0
|
|
466
|
+
base = prefix_base(b)
|
|
467
|
+
i += 2 unless base == 8 && !letter_prefix?(b) # a bare 0 is an octal digit
|
|
468
|
+
elsif base == prefix_base(b) && (base != 8 || letter_prefix?(b))
|
|
469
|
+
i += 2
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
base = 10 if base == 0
|
|
473
|
+
digits = based_digits(text, i, base)
|
|
474
|
+
return nil if digits.nil?
|
|
475
|
+
|
|
476
|
+
[base, digits, negative]
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# The base a "0" prefix followed by b gives: 0x 0b 0o 0d, else octal.
|
|
480
|
+
def self.prefix_base(b)
|
|
481
|
+
return 16 if b == 120 || b == 88
|
|
482
|
+
return 2 if b == 98 || b == 66
|
|
483
|
+
return 10 if b == 100 || b == 68
|
|
484
|
+
|
|
485
|
+
8
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def self.letter_prefix?(b)
|
|
489
|
+
b == 111 || b == 79
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# The digits from i to the end (bar trailing whitespace), in base, with
|
|
493
|
+
# single underscores between digits dropped; nil if anything else.
|
|
494
|
+
def self.based_digits(text, i, base)
|
|
495
|
+
out = +""
|
|
496
|
+
while i < text.bytesize
|
|
497
|
+
b = text.getbyte(i)
|
|
498
|
+
if digit_in?(b, base)
|
|
499
|
+
out << text.byteslice(i, 1)
|
|
500
|
+
elsif !(b == 95 && !out.empty? && i + 1 < text.bytesize && digit_in?(text.getbyte(i + 1), base))
|
|
501
|
+
break
|
|
502
|
+
end
|
|
503
|
+
i += 1
|
|
504
|
+
end
|
|
505
|
+
i += 1 while i < text.bytesize && space?(text.getbyte(i))
|
|
506
|
+
out.empty? || i < text.bytesize ? nil : out
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Digits are 0-9 then a-z (either case) for 10 to 35.
|
|
510
|
+
def self.digit_in?(b, base)
|
|
511
|
+
return b >= 48 && b < 48 + base if base <= 10
|
|
512
|
+
|
|
513
|
+
digit?(b) || (b >= 97 && b < 87 + base) || (b >= 65 && b < 55 + base)
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Under 2**62, so a chunk of digits is an Integer in Spinel.
|
|
517
|
+
CHUNK_LIMIT = 4_611_686_018_427_387_904
|
|
518
|
+
|
|
519
|
+
# digits in base, as many at a time as fit under CHUNK_LIMIT, scaled
|
|
520
|
+
# through a Bignum literal as digits_to_i does. Typed, since its
|
|
521
|
+
# arguments come out of integer_syntax's Array: boxed, Spinel's
|
|
522
|
+
# arithmetic would raise on overflow rather than promote.
|
|
523
|
+
#: (String, Integer) -> untyped
|
|
524
|
+
def self.based_to_i(digits, base)
|
|
525
|
+
size = 1
|
|
526
|
+
size += 1 while base**size <= CHUNK_LIMIT / base
|
|
527
|
+
n = 0
|
|
528
|
+
i = 0
|
|
529
|
+
while i < digits.bytesize
|
|
530
|
+
k = digits.bytesize - i
|
|
531
|
+
k = size if k > size
|
|
532
|
+
n = (n * ((TWO64 * (base**k)) / TWO64)) + digits.byteslice(i, k).to_i(base)
|
|
533
|
+
i += k
|
|
534
|
+
end
|
|
535
|
+
n
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Regexp classes, ASCII-only as Ruby's are: \s \d \w.
|
|
539
|
+
def self.space?(b)
|
|
540
|
+
b == 32 || (b >= 9 && b <= 13)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def self.digit?(b)
|
|
544
|
+
b >= 48 && b <= 57
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def self.word?(b)
|
|
548
|
+
digit?(b) || alpha?(b) || b == 95
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def self.alpha?(b)
|
|
552
|
+
(b >= 65 && b <= 90) || (b >= 97 && b <= 122)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# The parser state shared by every generated parser, the counterpart
|
|
556
|
+
# of Birman::Context.
|
|
557
|
+
class Parser
|
|
558
|
+
# @rbs @expected: Array[String]
|
|
559
|
+
# @rbs @in_progress_at: Array[Integer]
|
|
560
|
+
|
|
561
|
+
attr_reader :input
|
|
562
|
+
|
|
563
|
+
def initialize(input)
|
|
564
|
+
unless input.encoding == Encoding::UTF_8 || BirmanRT.single_byte?(input)
|
|
565
|
+
raise ArgumentError, "the generated parser reads UTF-8 or binary input, not #{input.encoding}"
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
@input = input
|
|
569
|
+
@len = input.bytesize
|
|
570
|
+
reset
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
private
|
|
574
|
+
|
|
575
|
+
# Per-parse state, fresh for every parse (as Birman's Context is), so
|
|
576
|
+
# a parse that ended in a commit failure, with rules still marked in
|
|
577
|
+
# progress, leaves nothing behind. Generated parsers also clear their
|
|
578
|
+
# memos (init_memos).
|
|
579
|
+
def reset
|
|
580
|
+
@furthest = 0
|
|
581
|
+
@expected = NONE
|
|
582
|
+
@token_ends = {}
|
|
583
|
+
@position = 0
|
|
584
|
+
@in_progress = []
|
|
585
|
+
@in_progress_at = []
|
|
586
|
+
@round = 0 # growth rounds of left-recursive cycles so far
|
|
587
|
+
@lookaheads = 0
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
#: (Integer, String) -> Integer
|
|
591
|
+
def fail_at(pos, what)
|
|
592
|
+
if pos > @furthest
|
|
593
|
+
@furthest = pos
|
|
594
|
+
@expected = [what]
|
|
595
|
+
elsif pos == @furthest
|
|
596
|
+
if @expected.empty?
|
|
597
|
+
@expected = [what]
|
|
598
|
+
else
|
|
599
|
+
@expected << what
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
FAIL
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# Skips expectations already recorded at pos, as Context#merge does.
|
|
606
|
+
#: (Integer, Array[String]) -> void
|
|
607
|
+
def merge(pos, expected)
|
|
608
|
+
i = 0
|
|
609
|
+
while i < expected.size
|
|
610
|
+
what = expected[i]
|
|
611
|
+
fail_at(pos, what) unless pos == @furthest && @expected.include?(what)
|
|
612
|
+
i += 1
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
#: (Integer, String) -> void
|
|
617
|
+
def commit_failure(pos, described)
|
|
618
|
+
fail_at(pos, described) if @furthest < pos || @expected.empty?
|
|
619
|
+
raise Committed.new(@furthest, @expected.dup)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# Context#ignored_after / #token_end.
|
|
623
|
+
#: (Integer, Integer) -> void
|
|
624
|
+
def ignored_after(token_end, pos)
|
|
625
|
+
@token_ends[pos] = token_end if pos > token_end
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
#: (Integer) -> Integer
|
|
629
|
+
def token_end(pos)
|
|
630
|
+
e = @token_ends[pos]
|
|
631
|
+
e.nil? ? pos : e
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
# Where a node's span ends: the last token's end, with trailing
|
|
635
|
+
# whitespace and NULs trimmed (String#rstrip).
|
|
636
|
+
#: (Integer, Integer) -> Integer
|
|
637
|
+
def span_end(from, to)
|
|
638
|
+
to = token_end(to)
|
|
639
|
+
while to > from
|
|
640
|
+
b = @input.getbyte(to - 1)
|
|
641
|
+
break unless b.zero? || BirmanRT.space?(b)
|
|
642
|
+
|
|
643
|
+
to -= 1
|
|
644
|
+
end
|
|
645
|
+
to
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
#: (Integer, Integer) -> Span
|
|
649
|
+
def span(from, to)
|
|
650
|
+
Span.new(@input, from, span_end(from, to))
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
#: (Integer, Array[String], bool) -> Failure
|
|
654
|
+
def failure(byte_pos, expected, committed)
|
|
655
|
+
Failure.new(@input, byte_pos, expected.uniq, committed, nil)
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
#: (Integer, String) -> Failure
|
|
659
|
+
def failure_because(byte_pos, reason)
|
|
660
|
+
Failure.new(@input, byte_pos, [], false, reason)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Grammar#parse's check, made before a regexp can raise on the bad
|
|
664
|
+
# byte: invalid input fails at the first one.
|
|
665
|
+
#: (Integer) -> Failure
|
|
666
|
+
def invalid_input(byte_pos)
|
|
667
|
+
failure_because(byte_pos, BirmanRT.single_byte?(@input) ? "invalid US-ASCII" : "invalid UTF-8")
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
# The end of a Label, after its isolated inner match: keep the
|
|
671
|
+
# deeper error, or say "expected <name>" if it got nowhere.
|
|
672
|
+
#: (Integer, Integer, Integer, Array[String], String) -> Integer
|
|
673
|
+
def label_end(p, q, of, oe, name)
|
|
674
|
+
f = @furthest
|
|
675
|
+
ex = @expected
|
|
676
|
+
@furthest = of
|
|
677
|
+
@expected = oe
|
|
678
|
+
if f == p && (q == FAIL || (q == p && !ex.empty?))
|
|
679
|
+
fail_at(p, name)
|
|
680
|
+
else
|
|
681
|
+
merge(f, ex)
|
|
682
|
+
end
|
|
683
|
+
q
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# The end of a Check, after its isolated inner match: ok says the
|
|
687
|
+
# inner match failed or the predicate accepted it. A rejection drops
|
|
688
|
+
# what the inner match recorded and says "expected <name>" at p.
|
|
689
|
+
#: (Integer, Integer, bool, Integer, Array[String], String) -> Integer
|
|
690
|
+
def check_end(p, q, ok, of, oe, name)
|
|
691
|
+
f = @furthest
|
|
692
|
+
ex = @expected
|
|
693
|
+
@furthest = of
|
|
694
|
+
@expected = oe
|
|
695
|
+
return fail_at(p, name) unless ok
|
|
696
|
+
|
|
697
|
+
merge(f, ex)
|
|
698
|
+
q
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
# The end of a positive lookahead (Context#peek?), after its isolated
|
|
702
|
+
# inner match g: whether it matched. If not, the failure is recorded
|
|
703
|
+
# at p, as what it expected there or as the lookahead itself.
|
|
704
|
+
#: (Integer, Integer, Integer, Array[String], String) -> bool
|
|
705
|
+
def peek_end(p, g, of, oe, described)
|
|
706
|
+
f = @furthest
|
|
707
|
+
ex = @expected
|
|
708
|
+
@furthest = of
|
|
709
|
+
@expected = oe
|
|
710
|
+
return true if g != FAIL
|
|
711
|
+
|
|
712
|
+
if f == p && !ex.empty?
|
|
713
|
+
merge(p, ex)
|
|
714
|
+
else
|
|
715
|
+
fail_at(p, described)
|
|
716
|
+
end
|
|
717
|
+
false
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# Grammar#large_integer, for copied actions: Kernel#Integer, Bignums
|
|
721
|
+
# included.
|
|
722
|
+
def large_integer(value, base = 0)
|
|
723
|
+
BirmanRT.to_integer(value, base)
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
# Exactly text's bytes at p?
|
|
727
|
+
#: (Integer, String) -> bool
|
|
728
|
+
def bytes_at?(p, text)
|
|
729
|
+
n = text.bytesize
|
|
730
|
+
return false if p + n > @len
|
|
731
|
+
|
|
732
|
+
i = 0
|
|
733
|
+
while i < n
|
|
734
|
+
return false if @input.getbyte(p + i) != text.getbyte(i)
|
|
735
|
+
|
|
736
|
+
i += 1
|
|
737
|
+
end
|
|
738
|
+
true
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
# A lookahead caught a commit failure: every rule entered since it
|
|
742
|
+
# began (depth entries ago) was unwound mid-match. Their memo entries
|
|
743
|
+
# still say IN_PROGRESS, or hold a left-recursive rule's seed; mark
|
|
744
|
+
# them ABSENT so they can be tried again.
|
|
745
|
+
#: (Integer) -> void
|
|
746
|
+
def abandon(depth)
|
|
747
|
+
while @in_progress_at.size > depth
|
|
748
|
+
memo = @in_progress.pop
|
|
749
|
+
p = @in_progress_at.pop
|
|
750
|
+
memo[p] = ABSENT
|
|
751
|
+
end
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
def left_recursion(name)
|
|
755
|
+
raise ArgumentError, "left recursion in rule :#{name}: it can reach itself without consuming input"
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
end
|