parsanol 1.3.14-arm-linux → 1.3.16-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/atoms/base.rb +26 -6
- data/lib/parsanol/native/transformer.rb +8 -7
- data/lib/parsanol/native.rb +34 -14
- data/lib/parsanol/parser.rb +27 -23
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +1140 -0
- data/lib/parsanol.rb +1 -0
- metadata +3 -2
data/lib/parsanol/vm.rb
ADDED
|
@@ -0,0 +1,1140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
require "parsanol/atoms/can_flatten"
|
|
5
|
+
|
|
6
|
+
module Parsanol
|
|
7
|
+
# Bytecode VM for the pure-Ruby parse path.
|
|
8
|
+
#
|
|
9
|
+
# Compiles an atom tree once into a flat integer program and executes it
|
|
10
|
+
# with a single dispatch loop. Terminals record packed span Integers
|
|
11
|
+
# ((pos << 20) | len) instead of allocating Slices; composite values
|
|
12
|
+
# mirror the interpreter's tagged arrays exactly. A bottom-up
|
|
13
|
+
# materialization pass turns spans into Slices and Named markers into
|
|
14
|
+
# their eagerly-flattened hashes, so output is byte-identical to the
|
|
15
|
+
# tree interpreter (mode: :ruby).
|
|
16
|
+
#
|
|
17
|
+
# Design notes: TODO.perf/6-ruby-vm.md
|
|
18
|
+
module VM
|
|
19
|
+
HALT = 0
|
|
20
|
+
STR = 1
|
|
21
|
+
RE = 2
|
|
22
|
+
ANY = 3
|
|
23
|
+
SEQ_BEGIN = 4
|
|
24
|
+
SEQ_END = 5
|
|
25
|
+
CHOICE = 6
|
|
26
|
+
POPBT = 7
|
|
27
|
+
JMP = 8
|
|
28
|
+
NAME_END = 9
|
|
29
|
+
CALL = 10
|
|
30
|
+
RET = 11
|
|
31
|
+
LOOK_POS = 12
|
|
32
|
+
LOOK_POS_END = 13
|
|
33
|
+
LOOK_NEG = 14
|
|
34
|
+
LOOK_NEG_END = 15
|
|
35
|
+
DROP = 16
|
|
36
|
+
FAIL = 17
|
|
37
|
+
REP_INIT = 18
|
|
38
|
+
REP_TEST = 19
|
|
39
|
+
REP_STEP = 20
|
|
40
|
+
REP_EXIT = 21
|
|
41
|
+
MAYBE_RE = 22
|
|
42
|
+
MAYBE_STR = 23
|
|
43
|
+
RUN_RE = 24
|
|
44
|
+
RUN_STR = 25
|
|
45
|
+
SEQ_SIMPLE = 26
|
|
46
|
+
|
|
47
|
+
K_ALT = 0
|
|
48
|
+
K_REP = 1
|
|
49
|
+
K_LOOK_FAIL = 2
|
|
50
|
+
K_NEG = 3
|
|
51
|
+
|
|
52
|
+
SEQ_MARK = Object.new.freeze
|
|
53
|
+
|
|
54
|
+
# Deferred Named wrapper; materialization produces
|
|
55
|
+
# { name => flatten(inner, named: true) } like Named#apply.
|
|
56
|
+
NamedValue = Struct.new(:name, :value)
|
|
57
|
+
|
|
58
|
+
SINGLE_CLASS_RE = /\A(?:\^)?(?:\[[^\[\]]*\]|\\[dDwWsShH]|\\x[0-9a-fA-F]{2}|\\[0-7]{1,3}|[^\[\\\]|()?*+{}\^])\z/
|
|
59
|
+
|
|
60
|
+
CHAR_WIDTH = Array.new(256, 0)
|
|
61
|
+
(0...128).each { |b| CHAR_WIDTH[b] = 1 }
|
|
62
|
+
(0xC2..0xDF).each { |b| CHAR_WIDTH[b] = 2 }
|
|
63
|
+
(0xE0..0xEF).each { |b| CHAR_WIDTH[b] = 3 }
|
|
64
|
+
(0xF0..0xF4).each { |b| CHAR_WIDTH[b] = 4 }
|
|
65
|
+
|
|
66
|
+
MAX_PACK_LEN = (1 << 20) - 1
|
|
67
|
+
FRAME_STRIDE = 4 # count, end_pos, rbase, min
|
|
68
|
+
BT_STRIDE = 6 # pc, pos, rlen, cbase, clen, kind
|
|
69
|
+
# Returned (as the sole value) when the VM cannot complete a parse
|
|
70
|
+
# for internal reasons — callers should fall back to the interpreter
|
|
71
|
+
# and may skip the VM for this grammar afterwards. A clean
|
|
72
|
+
# [false, nil] means the input genuinely does not parse.
|
|
73
|
+
BAIL = Object.new.freeze
|
|
74
|
+
# Step budget exhausted on the naive pass — internal to #run, which
|
|
75
|
+
# retries once with packrat memoization before giving up.
|
|
76
|
+
BUDGET = Object.new.freeze
|
|
77
|
+
# Memo marker for a subroutine currently being evaluated at a given
|
|
78
|
+
# position. Re-entry (left recursion) fails that path instead of
|
|
79
|
+
# looping forever.
|
|
80
|
+
PENDING = Object.new.freeze
|
|
81
|
+
|
|
82
|
+
STEP_BUDGET_FACTOR = 200
|
|
83
|
+
STEP_BUDGET_FIXED = 10_000
|
|
84
|
+
MEMO_BUDGET_FACTOR = 4_096
|
|
85
|
+
MEMO_BUDGET_CELLS_FACTOR = 16
|
|
86
|
+
|
|
87
|
+
class << self
|
|
88
|
+
@programs = {}
|
|
89
|
+
|
|
90
|
+
# Cached compile keyed by root-atom object identity. Atoms are
|
|
91
|
+
# effectively immutable once constructed; false caches grammars the
|
|
92
|
+
# VM cannot handle so we do not re-walk them on every parse.
|
|
93
|
+
#
|
|
94
|
+
# A grammar whose VM run failed once (unsupported backtracking shape
|
|
95
|
+
# or step budget) is marked :fallback so later parses skip the VM
|
|
96
|
+
# attempt entirely — the interpreter result is identical.
|
|
97
|
+
def program_for(atom)
|
|
98
|
+
root = atom.is_a?(Parsanol::Parser) ? atom.root : atom
|
|
99
|
+
key = root.object_id
|
|
100
|
+
cache = (@programs ||= {})
|
|
101
|
+
cached = cache[key]
|
|
102
|
+
return nil if cached == :fallback
|
|
103
|
+
return cached if key?(cache, key)
|
|
104
|
+
|
|
105
|
+
cache[key] = compile(atom)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Marks a grammar as VM-incompatible after a runtime failure.
|
|
109
|
+
def disable_for!(atom)
|
|
110
|
+
root = atom.is_a?(Parsanol::Parser) ? atom.root : atom
|
|
111
|
+
(@programs ||= {})[root.object_id] = :fallback # rubocop:disable Lint/HashCompareByIdentity -- object_id keys avoid holding strong references to grammar atoms
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def key?(cache, key) # rubocop:disable Naming/PredicateMethod -- mirrors Hash#key?
|
|
115
|
+
cache.key?(key)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def clear_program_cache
|
|
119
|
+
@programs&.clear
|
|
120
|
+
@heavy&.clear
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Compiles the grammar rooted at +atom+. Returns the flat program
|
|
124
|
+
# Array, or nil when the grammar uses unsupported atoms.
|
|
125
|
+
def compile(atom)
|
|
126
|
+
program = compile_with_inline(atom, true)
|
|
127
|
+
# Inlining duplicates every non-recursive rule body at each
|
|
128
|
+
# reference site; grammars with many cross-referencing rules
|
|
129
|
+
# explode past the program cap even though the atom tree is
|
|
130
|
+
# modest. Recompiling with inlining off (each rule a CALL/RET
|
|
131
|
+
# subroutine) keeps the program O(atoms) at a small per-rule
|
|
132
|
+
# dispatch cost — far better than refusing the grammar outright.
|
|
133
|
+
return program unless program == :oversize
|
|
134
|
+
|
|
135
|
+
compile_with_inline(atom, false)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def compile_with_inline(atom, inline)
|
|
139
|
+
root = atom.is_a?(Parsanol::Parser) ? atom.root : atom
|
|
140
|
+
compiler = Compiler.new(inline: inline)
|
|
141
|
+
return nil unless compiler.compile_atom(root, true)
|
|
142
|
+
|
|
143
|
+
# HALT terminates the MAIN program; subroutines follow it so the
|
|
144
|
+
# root CALL's return address can never collide with a subroutine.
|
|
145
|
+
compiler.ops << [HALT, nil, nil, nil]
|
|
146
|
+
return nil unless compiler.append_subroutines
|
|
147
|
+
|
|
148
|
+
compiler.to_program
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Executes a compiled program. Returns [true, value] on success;
|
|
152
|
+
# [false, nil] on failure or budget exhaustion — callers fall back
|
|
153
|
+
# to the interpreter, which reproduces exact diagnostics.
|
|
154
|
+
#
|
|
155
|
+
# Heavy-backtracking grammars get a second chance: the naive pass
|
|
156
|
+
# runs without memoization (zero overhead on the happy path), and
|
|
157
|
+
# only if it blows the step budget does a memoized pass run. The
|
|
158
|
+
# memo converts repeated rule+position work into single entries, so
|
|
159
|
+
# grammars that the interpreter needed (slow) memoization for now
|
|
160
|
+
# run at VM speed.
|
|
161
|
+
def run(program, input, consume_all)
|
|
162
|
+
executor = Executor.new(program, input, consume_all)
|
|
163
|
+
result = executor.execute
|
|
164
|
+
if result.equal?(BUDGET)
|
|
165
|
+
executor = Executor.new(program, input, consume_all, memoize: true)
|
|
166
|
+
result = executor.execute
|
|
167
|
+
result = BAIL if result.equal?(BUDGET)
|
|
168
|
+
end
|
|
169
|
+
result
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Grammar-aware entry used by Base#parse. Grammars that have shown
|
|
173
|
+
# heavy-backtracking behavior (budget bust, or a success that
|
|
174
|
+
# burned the step-density threshold) memoize from the first
|
|
175
|
+
# instruction on subsequent parses, skipping the doomed naive
|
|
176
|
+
# pass; a heavy success is retried memoized in the same call so
|
|
177
|
+
# the VM keeps the grammar instead of surrendering it to the
|
|
178
|
+
# interpreter.
|
|
179
|
+
def run_for(atom, program, input, consume_all)
|
|
180
|
+
root = atom.is_a?(Parsanol::Parser) ? atom.root : atom
|
|
181
|
+
id = root.object_id
|
|
182
|
+
heavy = (@heavy ||= {})
|
|
183
|
+
if heavy[id]
|
|
184
|
+
result = Executor.new(program, input, consume_all,
|
|
185
|
+
memoize: true).execute
|
|
186
|
+
return BAIL if result.equal?(BUDGET)
|
|
187
|
+
|
|
188
|
+
return result
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
result = Executor.new(program, input, consume_all).execute
|
|
192
|
+
if result.equal?(BUDGET) ||
|
|
193
|
+
(result.is_a?(Array) && result.first == :heavy)
|
|
194
|
+
heavy[id] = true
|
|
195
|
+
memo_result = Executor.new(program, input, consume_all,
|
|
196
|
+
memoize: true).execute
|
|
197
|
+
# A heavy naive success already answered; the memo pass only
|
|
198
|
+
# proves the grammar stays VM-viable (BUDGET would bail to the
|
|
199
|
+
# interpreter). When the naive pass never answered, the memo
|
|
200
|
+
# pass's outcome is the result.
|
|
201
|
+
return BAIL if memo_result.equal?(BUDGET)
|
|
202
|
+
|
|
203
|
+
result = memo_result if result.equal?(BUDGET)
|
|
204
|
+
end
|
|
205
|
+
result
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Converts the VM value tree into the interpreter's value tree.
|
|
209
|
+
def materialize(value, input)
|
|
210
|
+
case value
|
|
211
|
+
when Integer
|
|
212
|
+
pos = value >> 20
|
|
213
|
+
Slice.new(pos, input.byteslice(pos, value & MAX_PACK_LEN), input)
|
|
214
|
+
when NamedValue
|
|
215
|
+
{ value.name => Mat.flatten(materialize(value.value, input), true) }
|
|
216
|
+
when Array
|
|
217
|
+
value.map { |v| materialize(v, input) }
|
|
218
|
+
when Hash
|
|
219
|
+
value.transform_values { |v| materialize(v, input) }
|
|
220
|
+
else
|
|
221
|
+
value
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
module Mat
|
|
227
|
+
extend Parsanol::Atoms::CanFlatten
|
|
228
|
+
end
|
|
229
|
+
private_constant :Mat
|
|
230
|
+
|
|
231
|
+
# Compiles atoms into a flat stride-4 program. Entity bodies compile
|
|
232
|
+
# as subroutines, keyed by [body object_id, consume_all] so spine
|
|
233
|
+
# sites (consume_all=true) and inner sites stay semantically distinct.
|
|
234
|
+
class Compiler
|
|
235
|
+
def initialize(inline: true)
|
|
236
|
+
@ops = []
|
|
237
|
+
@inline = inline
|
|
238
|
+
@subs = {} # [obj_id, flag] => pc or :pending
|
|
239
|
+
@pending = {} # [obj_id, flag] => [[call_idx, body], ...]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
attr_reader :ops
|
|
243
|
+
|
|
244
|
+
def to_program
|
|
245
|
+
return :oversize if @ops.size > MAX_PROGRAM
|
|
246
|
+
|
|
247
|
+
@ops.flatten!(1)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def compile_atom(atom, consume_all)
|
|
251
|
+
case atom
|
|
252
|
+
when Parsanol::Atoms::Str
|
|
253
|
+
bytes = atom.str.unpack("C*")
|
|
254
|
+
emit(STR, bytes, bytes.size, atom.str.bytesize)
|
|
255
|
+
self
|
|
256
|
+
when Parsanol::Atoms::Re
|
|
257
|
+
emit(RE, atom.re, byte_table(atom.re), nil)
|
|
258
|
+
self
|
|
259
|
+
when Parsanol::Atoms::Sequence
|
|
260
|
+
parslets = atom.parslets
|
|
261
|
+
n = parslets.size
|
|
262
|
+
return nil if n.zero?
|
|
263
|
+
|
|
264
|
+
simple = simple_children(parslets, consume_all)
|
|
265
|
+
if simple
|
|
266
|
+
emit(SEQ_SIMPLE, simple, nil, nil)
|
|
267
|
+
return self
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
emit(SEQ_BEGIN)
|
|
271
|
+
last = n - 1
|
|
272
|
+
idx = 0
|
|
273
|
+
while idx < n
|
|
274
|
+
return nil unless compile_atom(parslets[idx], consume_all && idx == last)
|
|
275
|
+
|
|
276
|
+
idx += 1
|
|
277
|
+
end
|
|
278
|
+
emit(SEQ_END)
|
|
279
|
+
self
|
|
280
|
+
when Parsanol::Atoms::Alternative
|
|
281
|
+
alts = atom.alternatives
|
|
282
|
+
count = alts.size
|
|
283
|
+
return nil if count.zero?
|
|
284
|
+
|
|
285
|
+
jumps = []
|
|
286
|
+
idx = 0
|
|
287
|
+
while idx < count
|
|
288
|
+
last_alt = idx == count - 1
|
|
289
|
+
choice = (emit(CHOICE, nil, nil, nil) unless last_alt)
|
|
290
|
+
return nil unless compile_atom(alts[idx], consume_all)
|
|
291
|
+
|
|
292
|
+
unless last_alt
|
|
293
|
+
# The branch matched: drop this CHOICE's backtrack entry
|
|
294
|
+
# (ordered choice commits) before skipping the rest.
|
|
295
|
+
emit(POPBT)
|
|
296
|
+
jumps << emit(JMP, nil, nil, nil)
|
|
297
|
+
end
|
|
298
|
+
patch(choice, 1, flat(@ops.size)) if choice
|
|
299
|
+
idx += 1
|
|
300
|
+
end
|
|
301
|
+
jumps.each { |j| patch(j, 1, flat(@ops.size)) }
|
|
302
|
+
self
|
|
303
|
+
when Parsanol::Atoms::Repetition
|
|
304
|
+
compile_repetition(atom, consume_all)
|
|
305
|
+
when Parsanol::Atoms::Named
|
|
306
|
+
return nil unless compile_atom(atom.parslet, consume_all)
|
|
307
|
+
|
|
308
|
+
emit(NAME_END, atom.name, nil, nil)
|
|
309
|
+
self
|
|
310
|
+
when Parsanol::Atoms::Entity
|
|
311
|
+
compile_entity(atom, consume_all)
|
|
312
|
+
when Parsanol::Atoms::Lookahead
|
|
313
|
+
compile_lookahead(atom, consume_all)
|
|
314
|
+
when Parsanol::Atoms::Ignored
|
|
315
|
+
inner = atom.wrapped_atom
|
|
316
|
+
return nil if inner.nil?
|
|
317
|
+
return nil unless compile_atom(inner, consume_all)
|
|
318
|
+
|
|
319
|
+
emit(DROP)
|
|
320
|
+
self
|
|
321
|
+
when Parsanol::Atoms::Scope
|
|
322
|
+
# Scope only affects capture state; the result tree is the
|
|
323
|
+
# inner atom's tree unchanged. The block is pure DSL evaluated
|
|
324
|
+
# once at compile time. Any Capture/Dynamic inside still fails
|
|
325
|
+
# compilation on its own, so passthrough cannot diverge.
|
|
326
|
+
inner = begin
|
|
327
|
+
atom.block.call
|
|
328
|
+
rescue StandardError
|
|
329
|
+
nil
|
|
330
|
+
end
|
|
331
|
+
return nil if inner.nil?
|
|
332
|
+
return nil unless compile_atom(inner, consume_all)
|
|
333
|
+
|
|
334
|
+
self
|
|
335
|
+
# Dynamic, Capture, Cut, Custom, Infix, unknown
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def append_subroutines # rubocop:disable Naming/PredicateMethod -- mutating; returns compile success, not a predicate
|
|
340
|
+
until @pending.empty?
|
|
341
|
+
key, list = @pending.shift
|
|
342
|
+
body = list.first[1]
|
|
343
|
+
flag = key[1]
|
|
344
|
+
sub = @ops.size
|
|
345
|
+
# Register before compiling so recursive references resolve.
|
|
346
|
+
@subs[key] = sub
|
|
347
|
+
return false unless compile_atom(body, flag)
|
|
348
|
+
|
|
349
|
+
emit(RET)
|
|
350
|
+
list.each { |entry| patch(entry[0], 1, flat(sub)) } # rubocop:disable Style/HashEachMethods -- Array of [call_idx, body] pairs, not a Hash
|
|
351
|
+
end
|
|
352
|
+
true
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
MAX_PROGRAM = 40_000 # instructions; larger grammars fall back
|
|
356
|
+
|
|
357
|
+
private
|
|
358
|
+
|
|
359
|
+
def emit(op, a = nil, b = nil, c = nil) # rubocop:disable Naming/MethodParameterName -- opcode operand slots
|
|
360
|
+
@ops << [op, a, b, c]
|
|
361
|
+
@ops.size - 1
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def patch(idx, slot, value)
|
|
365
|
+
@ops[idx][slot] = value
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Compiler works in instruction indexes; the executor's pc indexes
|
|
369
|
+
# the flattened array (4 slots per instruction).
|
|
370
|
+
def flat(instr_index)
|
|
371
|
+
instr_index * 4
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Returns a flat [op, a, b, c, ...] descriptor array when every
|
|
375
|
+
# child compiles to a single-value fused op, else nil.
|
|
376
|
+
def simple_children(parslets, consume_all)
|
|
377
|
+
kids = []
|
|
378
|
+
last = parslets.size - 1
|
|
379
|
+
parslets.each_with_index do |child, idx|
|
|
380
|
+
spine = consume_all && idx == last
|
|
381
|
+
op = simple_child(child, spine)
|
|
382
|
+
return nil if op.nil?
|
|
383
|
+
|
|
384
|
+
kids.concat(op)
|
|
385
|
+
end
|
|
386
|
+
kids
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def simple_child(atom, spine)
|
|
390
|
+
case atom
|
|
391
|
+
when Parsanol::Atoms::Str
|
|
392
|
+
bytes = atom.str.unpack("C*")
|
|
393
|
+
[STR, bytes, bytes.size, atom.str.bytesize]
|
|
394
|
+
when Parsanol::Atoms::Re
|
|
395
|
+
[RE, atom.re, byte_table(atom.re), nil]
|
|
396
|
+
when Parsanol::Atoms::Repetition
|
|
397
|
+
min = atom.min
|
|
398
|
+
max = atom.max
|
|
399
|
+
return nil if max&.zero? || spine
|
|
400
|
+
|
|
401
|
+
body = atom.parslet
|
|
402
|
+
if min.zero? && max == 1
|
|
403
|
+
case body
|
|
404
|
+
when Parsanol::Atoms::Re
|
|
405
|
+
[MAYBE_RE, body.re, byte_table(body.re), nil]
|
|
406
|
+
when Parsanol::Atoms::Str
|
|
407
|
+
bytes = body.str.unpack("C*")
|
|
408
|
+
[MAYBE_STR, bytes, bytes.size, body.str.bytesize]
|
|
409
|
+
end
|
|
410
|
+
elsif min >= 1 && max.nil?
|
|
411
|
+
case body
|
|
412
|
+
when Parsanol::Atoms::Re
|
|
413
|
+
[RUN_RE, body.re, byte_table(body.re), min]
|
|
414
|
+
when Parsanol::Atoms::Str
|
|
415
|
+
bytes = body.str.unpack("C*")
|
|
416
|
+
[RUN_STR, bytes, bytes.size, min]
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Repetition compilation with terminal fusions:
|
|
423
|
+
# * maybe(0,1) of a single terminal -> one MAYBE_* op (no backtrack
|
|
424
|
+
# entry, no unwind cycle on the empty path)
|
|
425
|
+
# * unbounded-plus of a single terminal (non-spine sites) -> greedy
|
|
426
|
+
# RUN_* op
|
|
427
|
+
def compile_repetition(atom, consume_all)
|
|
428
|
+
min = atom.min
|
|
429
|
+
max = atom.max
|
|
430
|
+
return nil if max&.zero?
|
|
431
|
+
|
|
432
|
+
body = atom.parslet
|
|
433
|
+
if min.zero? && max == 1 && !consume_all
|
|
434
|
+
case body
|
|
435
|
+
when Parsanol::Atoms::Re
|
|
436
|
+
emit(MAYBE_RE, body.re, byte_table(body.re), nil)
|
|
437
|
+
return self
|
|
438
|
+
when Parsanol::Atoms::Str
|
|
439
|
+
bytes = body.str.unpack("C*")
|
|
440
|
+
emit(MAYBE_STR, bytes, bytes.size, body.str.bytesize)
|
|
441
|
+
return self
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
if min >= 1 && max.nil? && !consume_all
|
|
445
|
+
case body
|
|
446
|
+
when Parsanol::Atoms::Re
|
|
447
|
+
emit(RUN_RE, body.re, byte_table(body.re), min)
|
|
448
|
+
return self
|
|
449
|
+
when Parsanol::Atoms::Str
|
|
450
|
+
bytes = body.str.unpack("C*")
|
|
451
|
+
emit(RUN_STR, bytes, bytes.size, min)
|
|
452
|
+
return self
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
emit(REP_INIT, min, nil, nil)
|
|
457
|
+
test = emit(REP_TEST, nil, max, nil)
|
|
458
|
+
return nil unless compile_atom(body, false)
|
|
459
|
+
|
|
460
|
+
step = emit(REP_STEP, nil, nil, max)
|
|
461
|
+
exit_ = emit(REP_EXIT, atom.result_tag, consume_all ? 1 : 0, nil)
|
|
462
|
+
patch(test, 1, flat(exit_))
|
|
463
|
+
patch(step, 1, flat(test))
|
|
464
|
+
patch(step, 2, flat(exit_))
|
|
465
|
+
self
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def compile_entity(atom, consume_all)
|
|
469
|
+
body = begin
|
|
470
|
+
atom.parslet
|
|
471
|
+
rescue StandardError
|
|
472
|
+
nil
|
|
473
|
+
end
|
|
474
|
+
return nil if body.nil?
|
|
475
|
+
|
|
476
|
+
# Leaf entities are transparent: a rule whose body is a single
|
|
477
|
+
# terminal compiles inline, skipping the CALL/RET round trip.
|
|
478
|
+
case body
|
|
479
|
+
when Parsanol::Atoms::Str
|
|
480
|
+
bytes = body.str.unpack("C*")
|
|
481
|
+
emit(STR, bytes, bytes.size, body.str.bytesize)
|
|
482
|
+
return self
|
|
483
|
+
when Parsanol::Atoms::Re
|
|
484
|
+
emit(RE, body.re, byte_table(body.re), nil)
|
|
485
|
+
return self
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Non-recursive rules inline: the body's ops replace the CALL/RET
|
|
489
|
+
# round trip entirely. A body currently being compiled (directly
|
|
490
|
+
# or indirectly) is recursive and must stay a subroutine.
|
|
491
|
+
# rubocop:disable Lint/HashCompareByIdentity -- object_id keys; would otherwise pin every atom alive
|
|
492
|
+
if @inline && !(@compiling ||= {})[body.object_id]
|
|
493
|
+
@compiling[body.object_id] = true
|
|
494
|
+
# rubocop:enable Lint/HashCompareByIdentity
|
|
495
|
+
result = compile_atom(body, consume_all)
|
|
496
|
+
@compiling.delete(body.object_id)
|
|
497
|
+
return result if result
|
|
498
|
+
|
|
499
|
+
return nil
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
key = [body.object_id, consume_all]
|
|
503
|
+
sub = @subs[key]
|
|
504
|
+
if sub.is_a?(Integer)
|
|
505
|
+
emit(CALL, flat(sub), nil, nil)
|
|
506
|
+
else
|
|
507
|
+
call_idx = emit(CALL, nil, nil, nil)
|
|
508
|
+
(@pending[key] ||= []) << [call_idx, body]
|
|
509
|
+
@subs[key] = :pending if sub.nil?
|
|
510
|
+
end
|
|
511
|
+
self
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def compile_lookahead(atom, consume_all)
|
|
515
|
+
positive = atom.positive
|
|
516
|
+
bound = atom.bound_parslet
|
|
517
|
+
return nil if bound.nil?
|
|
518
|
+
|
|
519
|
+
entry = emit(positive ? LOOK_POS : LOOK_NEG, nil, nil, nil)
|
|
520
|
+
return nil unless compile_atom(bound, consume_all)
|
|
521
|
+
|
|
522
|
+
emit(positive ? LOOK_POS_END : LOOK_NEG_END)
|
|
523
|
+
patch(entry, 1, flat(@ops.size)) # continuation for K_NEG entries
|
|
524
|
+
self
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# 256-entry table: "regex matches a prefix consisting of exactly this
|
|
528
|
+
# ASCII byte" — only when the regex is provably a single-char class.
|
|
529
|
+
def byte_table(re) # rubocop:disable Naming/MethodParameterName
|
|
530
|
+
return nil unless SINGLE_CLASS_RE.match?(re.source)
|
|
531
|
+
return nil if re.match?("") # can match empty -> not byte-local
|
|
532
|
+
|
|
533
|
+
table = Array.new(256, false)
|
|
534
|
+
(1..127).each do |b|
|
|
535
|
+
table[b] = true if re.match?(b.chr)
|
|
536
|
+
end
|
|
537
|
+
table
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# One execution of a compiled program over one input string.
|
|
542
|
+
# When a parse succeeds but consumed a large fraction of the step
|
|
543
|
+
# budget, the grammar backtracks heavily and the tree interpreter
|
|
544
|
+
# (with its adaptive memoization) is the better engine; #run reports
|
|
545
|
+
# this via :heavy so Base#parse can skip the VM for the grammar.
|
|
546
|
+
class Executor
|
|
547
|
+
def initialize(program, input, consume_all, memoize: false)
|
|
548
|
+
@ops = program
|
|
549
|
+
@input = input
|
|
550
|
+
@consume_all = consume_all
|
|
551
|
+
@memoize = memoize
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def execute # rubocop:disable Metrics/MethodLength, Metrics/BlockLength, Metrics/BlockNesting -- single dispatch loop; hot path
|
|
555
|
+
ops = @ops
|
|
556
|
+
input = @input
|
|
557
|
+
bytes = input.unpack("C*")
|
|
558
|
+
# Regexp#match?(str, pos) searches FORWARD from pos (unanchored);
|
|
559
|
+
# StringScanner#match? is position-anchored, matching the
|
|
560
|
+
# interpreter's Source#matches? semantics exactly.
|
|
561
|
+
scanner = StringScanner.new(input)
|
|
562
|
+
n = bytes.size
|
|
563
|
+
# The memoized pass is polynomial (bounded by distinct rule ×
|
|
564
|
+
# position pairs); give it room proportional to grammar + input so
|
|
565
|
+
# packrat coverage of failing inputs isn't cut short.
|
|
566
|
+
budget = if @memoize
|
|
567
|
+
(MEMO_BUDGET_CELLS_FACTOR * ops.size) +
|
|
568
|
+
(MEMO_BUDGET_FACTOR * n) + STEP_BUDGET_FIXED
|
|
569
|
+
else
|
|
570
|
+
(STEP_BUDGET_FACTOR * n) + STEP_BUDGET_FIXED
|
|
571
|
+
end
|
|
572
|
+
steps = 0
|
|
573
|
+
|
|
574
|
+
pc = 0
|
|
575
|
+
pos = 0
|
|
576
|
+
rstack = []
|
|
577
|
+
bt = []
|
|
578
|
+
frames = []
|
|
579
|
+
calls = []
|
|
580
|
+
|
|
581
|
+
memo = @memoize ? {} : nil
|
|
582
|
+
memo_stack = []
|
|
583
|
+
trace = ENV["VM_TRACE"] ? [] : nil
|
|
584
|
+
|
|
585
|
+
# rubocop:disable-next Metrics/BlockLength -- the dispatch loop IS execute
|
|
586
|
+
loop do
|
|
587
|
+
steps += 1
|
|
588
|
+
if steps > budget
|
|
589
|
+
if trace
|
|
590
|
+
warn "BUDGET at steps=#{steps} pc=#{pc} pos=#{pos} rstack=#{rstack.size} bt=#{bt.size} calls=#{calls.size}"
|
|
591
|
+
warn "trace tail: #{trace.last(30).inspect}"
|
|
592
|
+
end
|
|
593
|
+
return BUDGET
|
|
594
|
+
end
|
|
595
|
+
trace << [pc, pos] if trace
|
|
596
|
+
|
|
597
|
+
if pc == FAIL
|
|
598
|
+
pc, pos = unwind(bt, rstack, frames, calls, memo, memo_stack)
|
|
599
|
+
return [false, nil] if pc == :fail
|
|
600
|
+
return BAIL if pc == :bail
|
|
601
|
+
|
|
602
|
+
next
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
case ops[pc]
|
|
606
|
+
when STR
|
|
607
|
+
lit = ops[pc + 1]
|
|
608
|
+
ln = ops[pc + 2]
|
|
609
|
+
i = 0
|
|
610
|
+
matched = true
|
|
611
|
+
while i < ln
|
|
612
|
+
if bytes[pos + i] != lit[i]
|
|
613
|
+
matched = false
|
|
614
|
+
break
|
|
615
|
+
end
|
|
616
|
+
i += 1
|
|
617
|
+
end
|
|
618
|
+
if matched
|
|
619
|
+
rstack << ((pos << 20) | ln)
|
|
620
|
+
pos += ops[pc + 3]
|
|
621
|
+
pc += 4
|
|
622
|
+
else
|
|
623
|
+
pc = FAIL
|
|
624
|
+
end
|
|
625
|
+
when RE
|
|
626
|
+
b = bytes[pos]
|
|
627
|
+
matched =
|
|
628
|
+
if (table = ops[pc + 2]) && b && b < 128
|
|
629
|
+
table[b]
|
|
630
|
+
elsif pos < n
|
|
631
|
+
(scanner.pos = pos) && scanner.match?(ops[pc + 1])
|
|
632
|
+
else
|
|
633
|
+
false
|
|
634
|
+
end
|
|
635
|
+
if matched
|
|
636
|
+
w = CHAR_WIDTH[b]
|
|
637
|
+
w = char_width_slow(input, pos) if w.zero?
|
|
638
|
+
rstack << ((pos << 20) | w)
|
|
639
|
+
pos += w
|
|
640
|
+
pc += 4
|
|
641
|
+
else
|
|
642
|
+
pc = FAIL
|
|
643
|
+
end
|
|
644
|
+
when ANY
|
|
645
|
+
if pos < n
|
|
646
|
+
w = CHAR_WIDTH[bytes[pos]]
|
|
647
|
+
w = char_width_slow(input, pos) if w.zero?
|
|
648
|
+
rstack << ((pos << 20) | w)
|
|
649
|
+
pos += w
|
|
650
|
+
pc += 4
|
|
651
|
+
else
|
|
652
|
+
pc = FAIL
|
|
653
|
+
end
|
|
654
|
+
when SEQ_BEGIN
|
|
655
|
+
rstack << SEQ_MARK
|
|
656
|
+
pc += 4
|
|
657
|
+
when SEQ_END
|
|
658
|
+
# Pop pushes in reverse; append and single reverse (O(n), no
|
|
659
|
+
# per-element unshift).
|
|
660
|
+
values = []
|
|
661
|
+
guard = rstack.size
|
|
662
|
+
ok_seq = false
|
|
663
|
+
while guard.positive?
|
|
664
|
+
v = rstack.pop
|
|
665
|
+
guard -= 1
|
|
666
|
+
if v == SEQ_MARK
|
|
667
|
+
ok_seq = true
|
|
668
|
+
break
|
|
669
|
+
end
|
|
670
|
+
values << v
|
|
671
|
+
end
|
|
672
|
+
return [false, nil] unless ok_seq
|
|
673
|
+
|
|
674
|
+
values << :sequence
|
|
675
|
+
values.reverse!
|
|
676
|
+
rstack << values
|
|
677
|
+
pc += 4
|
|
678
|
+
when CHOICE
|
|
679
|
+
bt << ops[pc + 1] << pos << rstack.size << frames.size << calls.size << K_ALT
|
|
680
|
+
pc += 4
|
|
681
|
+
when POPBT
|
|
682
|
+
bt.pop(BT_STRIDE)
|
|
683
|
+
pc += 4
|
|
684
|
+
when JMP
|
|
685
|
+
pc = ops[pc + 1]
|
|
686
|
+
when NAME_END
|
|
687
|
+
rstack << VM::NamedValue.new(ops[pc + 1], rstack.pop)
|
|
688
|
+
pc += 4
|
|
689
|
+
when CALL
|
|
690
|
+
sub = ops[pc + 1]
|
|
691
|
+
if memo
|
|
692
|
+
table = (memo[sub] ||= {})
|
|
693
|
+
hit = table[pos]
|
|
694
|
+
if hit
|
|
695
|
+
if hit.equal?(PENDING) || hit.equal?(:fail)
|
|
696
|
+
# :fail — the body already failed at this position;
|
|
697
|
+
# replay the failure without re-running it.
|
|
698
|
+
# PENDING — re-entry at the same rule+position (left
|
|
699
|
+
# recursion); the interpreter loops forever here, so
|
|
700
|
+
# failing this path keeps the VM bounded.
|
|
701
|
+
pc = FAIL
|
|
702
|
+
next
|
|
703
|
+
end
|
|
704
|
+
# Memo hit: replay the subroutine's stack effect without
|
|
705
|
+
# executing it. Values are immutable (packed spans,
|
|
706
|
+
# NamedValue, frozen-shape Arrays), so sharing is safe.
|
|
707
|
+
rstack.concat(hit[1])
|
|
708
|
+
pos = hit[0]
|
|
709
|
+
pc += 4
|
|
710
|
+
next
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
table[pos] = PENDING
|
|
714
|
+
# Lockstep with calls: unwind rolls both back by clen.
|
|
715
|
+
memo_stack << sub << pos << rstack.size
|
|
716
|
+
end
|
|
717
|
+
calls << (pc + 4)
|
|
718
|
+
pc = sub
|
|
719
|
+
when RET
|
|
720
|
+
pc = calls.pop
|
|
721
|
+
return BAIL if pc.nil?
|
|
722
|
+
|
|
723
|
+
if memo
|
|
724
|
+
mlen = memo_stack.pop
|
|
725
|
+
mpos = memo_stack.pop
|
|
726
|
+
msub = memo_stack.pop
|
|
727
|
+
memo[msub][mpos] = [pos, rstack[mlen..]]
|
|
728
|
+
end
|
|
729
|
+
when LOOK_POS
|
|
730
|
+
bt << ops[pc + 1] << pos << rstack.size << frames.size << calls.size << K_LOOK_FAIL
|
|
731
|
+
pc += 4
|
|
732
|
+
when LOOK_POS_END
|
|
733
|
+
# Body matched: restore entry's saved pos, drop entry and the
|
|
734
|
+
# body's value, push nil like Lookahead#try.
|
|
735
|
+
pos = bt[-BT_STRIDE + 1]
|
|
736
|
+
bt.pop(BT_STRIDE)
|
|
737
|
+
rstack.pop
|
|
738
|
+
rstack << nil
|
|
739
|
+
pc += 4
|
|
740
|
+
when LOOK_NEG
|
|
741
|
+
bt << ops[pc + 1] << pos << rstack.size << frames.size << calls.size << K_NEG
|
|
742
|
+
pc += 4
|
|
743
|
+
when LOOK_NEG_END
|
|
744
|
+
# Body matched: negative lookahead fails. Drop entry + body
|
|
745
|
+
# values, then propagate failure outward.
|
|
746
|
+
rlen = bt[-BT_STRIDE + 2]
|
|
747
|
+
rstack.slice!(rlen..) if rstack.size > rlen
|
|
748
|
+
bt.pop(BT_STRIDE)
|
|
749
|
+
pc = FAIL
|
|
750
|
+
when DROP
|
|
751
|
+
rstack.pop
|
|
752
|
+
rstack << nil
|
|
753
|
+
pc += 4
|
|
754
|
+
when REP_INIT
|
|
755
|
+
frames << 0 << pos << rstack.size << ops[pc + 1] # count,end,rbase,min
|
|
756
|
+
pc += 4
|
|
757
|
+
when REP_TEST
|
|
758
|
+
cbase = frames.size - FRAME_STRIDE
|
|
759
|
+
count = frames[cbase]
|
|
760
|
+
return BAIL if count.nil?
|
|
761
|
+
|
|
762
|
+
max = ops[pc + 2]
|
|
763
|
+
if max && count >= max
|
|
764
|
+
pc = ops[pc + 1] # exit
|
|
765
|
+
else
|
|
766
|
+
bt << ops[pc + 1] << frames[cbase + 1] << rstack.size << cbase << calls.size << K_REP
|
|
767
|
+
pc += 4 # body
|
|
768
|
+
end
|
|
769
|
+
when REP_STEP
|
|
770
|
+
bt.pop(BT_STRIDE)
|
|
771
|
+
cbase = frames.size - FRAME_STRIDE
|
|
772
|
+
count = frames[cbase]
|
|
773
|
+
return BAIL if count.nil?
|
|
774
|
+
|
|
775
|
+
count += 1
|
|
776
|
+
frames[cbase] = count
|
|
777
|
+
frames[cbase + 1] = pos
|
|
778
|
+
max = ops[pc + 3]
|
|
779
|
+
pc = if max && count >= max
|
|
780
|
+
ops[pc + 2] # exit
|
|
781
|
+
else
|
|
782
|
+
ops[pc + 1] # test
|
|
783
|
+
end
|
|
784
|
+
when REP_EXIT
|
|
785
|
+
cbase = frames.size - FRAME_STRIDE
|
|
786
|
+
return BAIL if frames[cbase].nil?
|
|
787
|
+
|
|
788
|
+
if ops[pc + 2] == 1 && pos != n
|
|
789
|
+
frames.slice!(cbase..)
|
|
790
|
+
pc = FAIL
|
|
791
|
+
else
|
|
792
|
+
rbase = frames[cbase + 2]
|
|
793
|
+
values = rstack.pop(rstack.size - rbase)
|
|
794
|
+
values.unshift(ops[pc + 1])
|
|
795
|
+
rstack << values
|
|
796
|
+
frames.slice!(cbase..)
|
|
797
|
+
pc += 4
|
|
798
|
+
end
|
|
799
|
+
when SEQ_SIMPLE
|
|
800
|
+
kids = ops[pc + 1]
|
|
801
|
+
kn = kids.size
|
|
802
|
+
values = [:sequence]
|
|
803
|
+
ki = 0
|
|
804
|
+
failed = false
|
|
805
|
+
while ki < kn
|
|
806
|
+
kop = kids[ki]
|
|
807
|
+
ka = kids[ki + 1]
|
|
808
|
+
kb = kids[ki + 2]
|
|
809
|
+
kc = kids[ki + 3]
|
|
810
|
+
case kop
|
|
811
|
+
when RE
|
|
812
|
+
b = bytes[pos]
|
|
813
|
+
m = if kb && b && b < 128
|
|
814
|
+
kb[b]
|
|
815
|
+
elsif pos < n
|
|
816
|
+
(scanner.pos = pos) && scanner.match?(ka)
|
|
817
|
+
else
|
|
818
|
+
false
|
|
819
|
+
end
|
|
820
|
+
if m
|
|
821
|
+
w = CHAR_WIDTH[b]
|
|
822
|
+
w = char_width_slow(input, pos) if w.zero?
|
|
823
|
+
values << ((pos << 20) | w)
|
|
824
|
+
pos += w
|
|
825
|
+
else
|
|
826
|
+
failed = true
|
|
827
|
+
end
|
|
828
|
+
when STR
|
|
829
|
+
i = 0
|
|
830
|
+
ln = kb
|
|
831
|
+
m = true
|
|
832
|
+
while i < ln
|
|
833
|
+
if bytes[pos + i] != ka[i] # rubocop:disable Metrics/BlockNesting -- fused byte-compare loop
|
|
834
|
+
m = false
|
|
835
|
+
break
|
|
836
|
+
end
|
|
837
|
+
i += 1
|
|
838
|
+
end
|
|
839
|
+
if m
|
|
840
|
+
values << ((pos << 20) | ln)
|
|
841
|
+
pos += kc
|
|
842
|
+
else
|
|
843
|
+
failed = true
|
|
844
|
+
end
|
|
845
|
+
when MAYBE_RE
|
|
846
|
+
b = bytes[pos]
|
|
847
|
+
m = if kb && b && b < 128
|
|
848
|
+
kb[b]
|
|
849
|
+
elsif pos < n
|
|
850
|
+
(scanner.pos = pos) && scanner.match?(ka)
|
|
851
|
+
else
|
|
852
|
+
false
|
|
853
|
+
end
|
|
854
|
+
if m
|
|
855
|
+
w = CHAR_WIDTH[b]
|
|
856
|
+
w = char_width_slow(input, pos) if w.zero?
|
|
857
|
+
values << [:maybe, (pos << 20) | w]
|
|
858
|
+
pos += w
|
|
859
|
+
else
|
|
860
|
+
values << [:maybe]
|
|
861
|
+
end
|
|
862
|
+
when MAYBE_STR
|
|
863
|
+
i = 0
|
|
864
|
+
ln = kb
|
|
865
|
+
m = true
|
|
866
|
+
while i < ln
|
|
867
|
+
if bytes[pos + i] != ka[i] # rubocop:disable Metrics/BlockNesting -- fused byte-compare loop
|
|
868
|
+
m = false
|
|
869
|
+
break
|
|
870
|
+
end
|
|
871
|
+
i += 1
|
|
872
|
+
end
|
|
873
|
+
if m
|
|
874
|
+
values << [:maybe, (pos << 20) | ln]
|
|
875
|
+
pos += kc
|
|
876
|
+
else
|
|
877
|
+
values << [:maybe]
|
|
878
|
+
end
|
|
879
|
+
when RUN_RE
|
|
880
|
+
elem_start = pos
|
|
881
|
+
count = 0
|
|
882
|
+
loop do
|
|
883
|
+
b = bytes[pos]
|
|
884
|
+
m = if kb && b && b < 128
|
|
885
|
+
kb[b]
|
|
886
|
+
elsif pos < n
|
|
887
|
+
(scanner.pos = pos) && scanner.match?(ka)
|
|
888
|
+
else
|
|
889
|
+
false
|
|
890
|
+
end
|
|
891
|
+
break unless m
|
|
892
|
+
|
|
893
|
+
count += 1
|
|
894
|
+
w = CHAR_WIDTH[b]
|
|
895
|
+
w = char_width_slow(input, pos) if w.zero?
|
|
896
|
+
values << ((pos << 20) | w)
|
|
897
|
+
pos += w
|
|
898
|
+
end
|
|
899
|
+
if count < kc
|
|
900
|
+
pos = elem_start
|
|
901
|
+
failed = true
|
|
902
|
+
end
|
|
903
|
+
when RUN_STR
|
|
904
|
+
elem_start = pos
|
|
905
|
+
count = 0
|
|
906
|
+
loop do
|
|
907
|
+
i = 0
|
|
908
|
+
ln = kb
|
|
909
|
+
m = true
|
|
910
|
+
while i < ln
|
|
911
|
+
if bytes[pos + i] != ka[i] # rubocop:disable Metrics/BlockNesting -- fused byte-compare loop
|
|
912
|
+
m = false
|
|
913
|
+
break
|
|
914
|
+
end
|
|
915
|
+
i += 1
|
|
916
|
+
end
|
|
917
|
+
break unless m
|
|
918
|
+
|
|
919
|
+
count += 1
|
|
920
|
+
values << ((pos << 20) | ln)
|
|
921
|
+
pos += ln
|
|
922
|
+
end
|
|
923
|
+
if count < kc
|
|
924
|
+
pos = elem_start
|
|
925
|
+
failed = true
|
|
926
|
+
end
|
|
927
|
+
end
|
|
928
|
+
break if failed
|
|
929
|
+
|
|
930
|
+
ki += 4
|
|
931
|
+
end
|
|
932
|
+
if failed
|
|
933
|
+
pc = FAIL
|
|
934
|
+
else
|
|
935
|
+
rstack << values
|
|
936
|
+
pc += 4
|
|
937
|
+
end
|
|
938
|
+
when MAYBE_RE
|
|
939
|
+
b = bytes[pos]
|
|
940
|
+
matched =
|
|
941
|
+
if (table = ops[pc + 2]) && b && b < 128
|
|
942
|
+
table[b]
|
|
943
|
+
elsif pos < n
|
|
944
|
+
(scanner.pos = pos) && scanner.match?(ops[pc + 1])
|
|
945
|
+
else
|
|
946
|
+
false
|
|
947
|
+
end
|
|
948
|
+
if matched
|
|
949
|
+
w = CHAR_WIDTH[b]
|
|
950
|
+
w = char_width_slow(input, pos) if w.zero?
|
|
951
|
+
rstack << [:maybe, (pos << 20) | w]
|
|
952
|
+
pos += w
|
|
953
|
+
else
|
|
954
|
+
rstack << [:maybe]
|
|
955
|
+
end
|
|
956
|
+
pc += 4
|
|
957
|
+
when MAYBE_STR
|
|
958
|
+
lit = ops[pc + 1]
|
|
959
|
+
ln = ops[pc + 2]
|
|
960
|
+
i = 0
|
|
961
|
+
matched = true
|
|
962
|
+
while i < ln
|
|
963
|
+
if bytes[pos + i] != lit[i]
|
|
964
|
+
matched = false
|
|
965
|
+
break
|
|
966
|
+
end
|
|
967
|
+
i += 1
|
|
968
|
+
end
|
|
969
|
+
if matched
|
|
970
|
+
rstack << [:maybe, (pos << 20) | ln]
|
|
971
|
+
pos += ops[pc + 3]
|
|
972
|
+
else
|
|
973
|
+
rstack << [:maybe]
|
|
974
|
+
end
|
|
975
|
+
pc += 4
|
|
976
|
+
when RUN_RE
|
|
977
|
+
re = ops[pc + 1]
|
|
978
|
+
table = ops[pc + 2]
|
|
979
|
+
min = ops[pc + 3]
|
|
980
|
+
values = [:repetition]
|
|
981
|
+
loop do
|
|
982
|
+
b = bytes[pos]
|
|
983
|
+
m = if table && b && b < 128
|
|
984
|
+
table[b]
|
|
985
|
+
elsif pos < n
|
|
986
|
+
(scanner.pos = pos) && scanner.match?(re)
|
|
987
|
+
else
|
|
988
|
+
false
|
|
989
|
+
end
|
|
990
|
+
break unless m
|
|
991
|
+
|
|
992
|
+
w = CHAR_WIDTH[b]
|
|
993
|
+
w = char_width_slow(input, pos) if w.zero?
|
|
994
|
+
values << ((pos << 20) | w)
|
|
995
|
+
pos += w
|
|
996
|
+
end
|
|
997
|
+
if min && values.size - 1 < min
|
|
998
|
+
pc = FAIL
|
|
999
|
+
else
|
|
1000
|
+
rstack << values
|
|
1001
|
+
pc += 4
|
|
1002
|
+
end
|
|
1003
|
+
when RUN_STR
|
|
1004
|
+
lit = ops[pc + 1]
|
|
1005
|
+
ln = ops[pc + 2]
|
|
1006
|
+
min = ops[pc + 3]
|
|
1007
|
+
values = [:repetition]
|
|
1008
|
+
loop do
|
|
1009
|
+
i = 0
|
|
1010
|
+
matched = true
|
|
1011
|
+
while i < ln
|
|
1012
|
+
if bytes[pos + i] != lit[i]
|
|
1013
|
+
matched = false
|
|
1014
|
+
break
|
|
1015
|
+
end
|
|
1016
|
+
i += 1
|
|
1017
|
+
end
|
|
1018
|
+
break unless matched
|
|
1019
|
+
|
|
1020
|
+
values << ((pos << 20) | ln)
|
|
1021
|
+
pos += ln
|
|
1022
|
+
end
|
|
1023
|
+
if min && values.size - 1 < min
|
|
1024
|
+
pc = FAIL
|
|
1025
|
+
else
|
|
1026
|
+
rstack << values
|
|
1027
|
+
pc += 4
|
|
1028
|
+
end
|
|
1029
|
+
when HALT
|
|
1030
|
+
if @consume_all && pos != n
|
|
1031
|
+
pc = FAIL
|
|
1032
|
+
next
|
|
1033
|
+
end
|
|
1034
|
+
value = VM.materialize(rstack[0], input)
|
|
1035
|
+
# More than ~100 steps per input byte means heavy
|
|
1036
|
+
# backtracking; the memoizing interpreter wins there.
|
|
1037
|
+
if @memoize
|
|
1038
|
+
return [true, value]
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
return steps > (n << 9) + 1000 ? [:heavy, value] : [true, value]
|
|
1042
|
+
else
|
|
1043
|
+
return BAIL
|
|
1044
|
+
end
|
|
1045
|
+
end
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
private
|
|
1049
|
+
|
|
1050
|
+
# Unwind one backtrack entry. Returns [pc, pos] or :fail. Repetition
|
|
1051
|
+
# entries with count >= min complete their tagged array and jump to
|
|
1052
|
+
# the exit continuation with the last iteration end position.
|
|
1053
|
+
def unwind(bt, rstack, frames, calls, memo, memo_stack) # rubocop:disable Naming/MethodParameterName -- stack register names
|
|
1054
|
+
return :fail if bt.empty?
|
|
1055
|
+
|
|
1056
|
+
# Everything above this entry belongs to constructs being
|
|
1057
|
+
# abandoned by the jump (inner alternatives, repetitions, or
|
|
1058
|
+
# subroutine state); discard it along with the entry itself.
|
|
1059
|
+
blen = bt.size - BT_STRIDE
|
|
1060
|
+
if ENV["VM_TRACE"]
|
|
1061
|
+
warn " UNWIND bt=#{bt.each_slice(6).map { |e| "[#{e[0]},#{e[1]},#{e[2]},#{e[3]},#{e[4]},#{e[5]}" }.join(' ')}"
|
|
1062
|
+
end
|
|
1063
|
+
kind = bt.pop
|
|
1064
|
+
clen = bt.pop
|
|
1065
|
+
cbase = bt.pop
|
|
1066
|
+
rlen = bt.pop
|
|
1067
|
+
epos = bt.pop
|
|
1068
|
+
epc = bt.pop
|
|
1069
|
+
bt.slice!(blen..) if bt.size > blen
|
|
1070
|
+
|
|
1071
|
+
rstack.slice!(rlen..) if rstack.size > rlen
|
|
1072
|
+
calls.slice!(clen..) if calls.size > clen
|
|
1073
|
+
if memo
|
|
1074
|
+
# memo_stack carries three slots per live call, so the frames of
|
|
1075
|
+
# every call abandoned by this unwind are the tail above 3*clen.
|
|
1076
|
+
# Every abandoned frame is a call whose body failed — cache that
|
|
1077
|
+
# failure too (full packrat memoization), so later explorations
|
|
1078
|
+
# that reach the same rule+position fail immediately instead of
|
|
1079
|
+
# re-running the body. Grammar inputs that don't parse explore
|
|
1080
|
+
# every alternative; without failure entries those explorations
|
|
1081
|
+
# repeat exponentially and blow the step budget.
|
|
1082
|
+
floor = clen * 3
|
|
1083
|
+
while memo_stack.size > floor
|
|
1084
|
+
memo_stack.pop
|
|
1085
|
+
mpos = memo_stack.pop
|
|
1086
|
+
msub = memo_stack.pop
|
|
1087
|
+
table = memo[msub]
|
|
1088
|
+
table[mpos] = :fail if table
|
|
1089
|
+
end
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
case kind
|
|
1093
|
+
when K_ALT
|
|
1094
|
+
frames.slice!(cbase..) if frames.size > cbase
|
|
1095
|
+
[epc, epos]
|
|
1096
|
+
when K_REP
|
|
1097
|
+
count = frames[cbase]
|
|
1098
|
+
min = frames[cbase + 3]
|
|
1099
|
+
if count.nil? || min.nil?
|
|
1100
|
+
return :bail
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
if count >= min
|
|
1104
|
+
# Repetition succeeds with the completed prefix.
|
|
1105
|
+
rbase = frames[cbase + 2]
|
|
1106
|
+
end_pos = frames[cbase + 1]
|
|
1107
|
+
tag = @ops[epc + 1]
|
|
1108
|
+
check_full = @ops[epc + 2]
|
|
1109
|
+
if check_full == 1 && end_pos != @input.bytesize
|
|
1110
|
+
frames.slice!(cbase..)
|
|
1111
|
+
unwind(bt, rstack, frames, calls, memo, memo_stack)
|
|
1112
|
+
else
|
|
1113
|
+
values = rstack.pop(rstack.size - rbase)
|
|
1114
|
+
values.unshift(tag)
|
|
1115
|
+
rstack << values
|
|
1116
|
+
frames.slice!(cbase..)
|
|
1117
|
+
[epc + 4, end_pos]
|
|
1118
|
+
end
|
|
1119
|
+
else
|
|
1120
|
+
frames.slice!(cbase..)
|
|
1121
|
+
unwind(bt, rstack, frames, calls, memo, memo_stack)
|
|
1122
|
+
end
|
|
1123
|
+
when K_LOOK_FAIL
|
|
1124
|
+
frames.slice!(cbase..) if frames.size > cbase
|
|
1125
|
+
unwind(bt, rstack, frames, calls, memo, memo_stack)
|
|
1126
|
+
when K_NEG
|
|
1127
|
+
# Negative lookahead body failed -> lookahead succeeds.
|
|
1128
|
+
frames.slice!(cbase..) if frames.size > cbase
|
|
1129
|
+
rstack << nil
|
|
1130
|
+
[epc, epos]
|
|
1131
|
+
end
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
def char_width_slow(input, pos)
|
|
1135
|
+
w = CHAR_WIDTH[input.getbyte(pos)]
|
|
1136
|
+
w.zero? ? 1 : w
|
|
1137
|
+
end
|
|
1138
|
+
end
|
|
1139
|
+
end
|
|
1140
|
+
end
|