parsanol 1.3.13 → 1.3.15

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.
@@ -0,0 +1,962 @@
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
+
75
+ STEP_BUDGET_FACTOR = 200
76
+ STEP_BUDGET_FIXED = 10_000
77
+
78
+ class << self
79
+ @programs = {}
80
+
81
+ # Cached compile keyed by root-atom object identity. Atoms are
82
+ # effectively immutable once constructed; false caches grammars the
83
+ # VM cannot handle so we do not re-walk them on every parse.
84
+ #
85
+ # A grammar whose VM run failed once (unsupported backtracking shape
86
+ # or step budget) is marked :fallback so later parses skip the VM
87
+ # attempt entirely — the interpreter result is identical.
88
+ def program_for(atom)
89
+ root = atom.is_a?(Parsanol::Parser) ? atom.root : atom
90
+ key = root.object_id
91
+ cache = (@programs ||= {})
92
+ cached = cache[key]
93
+ return nil if cached == :fallback
94
+ return cached if key?(cache, key)
95
+
96
+ cache[key] = compile(atom)
97
+ end
98
+
99
+ # Marks a grammar as VM-incompatible after a runtime failure.
100
+ def disable_for!(atom)
101
+ root = atom.is_a?(Parsanol::Parser) ? atom.root : atom
102
+ (@programs ||= {})[root.object_id] = :fallback # rubocop:disable Lint/HashCompareByIdentity -- object_id keys avoid holding strong references to grammar atoms
103
+ end
104
+
105
+ def key?(cache, key) # rubocop:disable Naming/PredicateMethod -- mirrors Hash#key?
106
+ cache.key?(key)
107
+ end
108
+
109
+ def clear_program_cache
110
+ @programs&.clear
111
+ end
112
+
113
+ # Compiles the grammar rooted at +atom+. Returns the flat program
114
+ # Array, or nil when the grammar uses unsupported atoms.
115
+ def compile(atom)
116
+ root = atom.is_a?(Parsanol::Parser) ? atom.root : atom
117
+ compiler = Compiler.new
118
+ return nil unless compiler.compile_atom(root, true)
119
+
120
+ # HALT terminates the MAIN program; subroutines follow it so the
121
+ # root CALL's return address can never collide with a subroutine.
122
+ compiler.ops << [HALT, nil, nil, nil]
123
+ return nil unless compiler.append_subroutines
124
+
125
+ compiler.to_program
126
+ end
127
+
128
+ # Executes a compiled program. Returns [true, value] on success;
129
+ # [false, nil] on failure or budget exhaustion — callers fall back
130
+ # to the interpreter, which reproduces exact diagnostics.
131
+ def run(program, input, consume_all)
132
+ Executor.new(program, input, consume_all).execute
133
+ end
134
+
135
+ # Converts the VM value tree into the interpreter's value tree.
136
+ def materialize(value, input)
137
+ case value
138
+ when Integer
139
+ pos = value >> 20
140
+ Slice.new(pos, input.byteslice(pos, value & MAX_PACK_LEN), input)
141
+ when NamedValue
142
+ { value.name => Mat.flatten(materialize(value.value, input), true) }
143
+ when Array
144
+ value.map { |v| materialize(v, input) }
145
+ when Hash
146
+ value.transform_values { |v| materialize(v, input) }
147
+ else
148
+ value
149
+ end
150
+ end
151
+ end
152
+
153
+ module Mat
154
+ extend Parsanol::Atoms::CanFlatten
155
+ end
156
+ private_constant :Mat
157
+
158
+ # Compiles atoms into a flat stride-4 program. Entity bodies compile
159
+ # as subroutines, keyed by [body object_id, consume_all] so spine
160
+ # sites (consume_all=true) and inner sites stay semantically distinct.
161
+ class Compiler
162
+ def initialize
163
+ @ops = []
164
+ @subs = {} # [obj_id, flag] => pc or :pending
165
+ @pending = {} # [obj_id, flag] => [[call_idx, body], ...]
166
+ end
167
+
168
+ attr_reader :ops
169
+
170
+ def to_program
171
+ return nil if @ops.size > MAX_PROGRAM
172
+
173
+ @ops.flatten!(1)
174
+ end
175
+
176
+ def compile_atom(atom, consume_all)
177
+ case atom
178
+ when Parsanol::Atoms::Str
179
+ bytes = atom.str.unpack("C*")
180
+ emit(STR, bytes, bytes.size, atom.str.bytesize)
181
+ self
182
+ when Parsanol::Atoms::Re
183
+ emit(RE, atom.re, byte_table(atom.re), nil)
184
+ self
185
+ when Parsanol::Atoms::Sequence
186
+ parslets = atom.parslets
187
+ n = parslets.size
188
+ return nil if n.zero?
189
+
190
+ simple = simple_children(parslets, consume_all)
191
+ if simple
192
+ emit(SEQ_SIMPLE, simple, nil, nil)
193
+ return self
194
+ end
195
+
196
+ emit(SEQ_BEGIN)
197
+ last = n - 1
198
+ idx = 0
199
+ while idx < n
200
+ return nil unless compile_atom(parslets[idx], consume_all && idx == last)
201
+
202
+ idx += 1
203
+ end
204
+ emit(SEQ_END)
205
+ self
206
+ when Parsanol::Atoms::Alternative
207
+ alts = atom.alternatives
208
+ count = alts.size
209
+ return nil if count.zero?
210
+
211
+ jumps = []
212
+ idx = 0
213
+ while idx < count
214
+ last_alt = idx == count - 1
215
+ choice = (emit(CHOICE, nil, nil, nil) unless last_alt)
216
+ return nil unless compile_atom(alts[idx], consume_all)
217
+
218
+ unless last_alt
219
+ # The branch matched: drop this CHOICE's backtrack entry
220
+ # (ordered choice commits) before skipping the rest.
221
+ emit(POPBT)
222
+ jumps << emit(JMP, nil, nil, nil)
223
+ end
224
+ patch(choice, 1, flat(@ops.size)) if choice
225
+ idx += 1
226
+ end
227
+ jumps.each { |j| patch(j, 1, flat(@ops.size)) }
228
+ self
229
+ when Parsanol::Atoms::Repetition
230
+ compile_repetition(atom, consume_all)
231
+ when Parsanol::Atoms::Named
232
+ return nil unless compile_atom(atom.parslet, consume_all)
233
+
234
+ emit(NAME_END, atom.name, nil, nil)
235
+ self
236
+ when Parsanol::Atoms::Entity
237
+ compile_entity(atom, consume_all)
238
+ when Parsanol::Atoms::Lookahead
239
+ compile_lookahead(atom, consume_all)
240
+ when Parsanol::Atoms::Ignored
241
+ inner = atom.wrapped_atom
242
+ return nil if inner.nil?
243
+ return nil unless compile_atom(inner, consume_all)
244
+
245
+ emit(DROP)
246
+ self
247
+ # Dynamic, Capture, Scope, Cut, Custom, Infix, unknown
248
+ end
249
+ end
250
+
251
+ def append_subroutines # rubocop:disable Naming/PredicateMethod -- mutating; returns compile success, not a predicate
252
+ until @pending.empty?
253
+ key, list = @pending.shift
254
+ body = list.first[1]
255
+ flag = key[1]
256
+ sub = @ops.size
257
+ # Register before compiling so recursive references resolve.
258
+ @subs[key] = sub
259
+ return false unless compile_atom(body, flag)
260
+
261
+ emit(RET)
262
+ list.each { |entry| patch(entry[0], 1, flat(sub)) } # rubocop:disable Style/HashEachMethods -- Array of [call_idx, body] pairs, not a Hash
263
+ end
264
+ true
265
+ end
266
+
267
+ MAX_PROGRAM = 40_000 # instructions; larger grammars fall back
268
+
269
+ private
270
+
271
+ def emit(op, a = nil, b = nil, c = nil) # rubocop:disable Naming/MethodParameterName -- opcode operand slots
272
+ @ops << [op, a, b, c]
273
+ @ops.size - 1
274
+ end
275
+
276
+ def patch(idx, slot, value)
277
+ @ops[idx][slot] = value
278
+ end
279
+
280
+ # Compiler works in instruction indexes; the executor's pc indexes
281
+ # the flattened array (4 slots per instruction).
282
+ def flat(instr_index)
283
+ instr_index * 4
284
+ end
285
+
286
+ # Returns a flat [op, a, b, c, ...] descriptor array when every
287
+ # child compiles to a single-value fused op, else nil.
288
+ def simple_children(parslets, consume_all)
289
+ kids = []
290
+ last = parslets.size - 1
291
+ parslets.each_with_index do |child, idx|
292
+ spine = consume_all && idx == last
293
+ op = simple_child(child, spine)
294
+ return nil if op.nil?
295
+
296
+ kids.concat(op)
297
+ end
298
+ kids
299
+ end
300
+
301
+ def simple_child(atom, spine)
302
+ case atom
303
+ when Parsanol::Atoms::Str
304
+ bytes = atom.str.unpack("C*")
305
+ [STR, bytes, bytes.size, atom.str.bytesize]
306
+ when Parsanol::Atoms::Re
307
+ [RE, atom.re, byte_table(atom.re), nil]
308
+ when Parsanol::Atoms::Repetition
309
+ min = atom.min
310
+ max = atom.max
311
+ return nil if max&.zero? || spine
312
+
313
+ body = atom.parslet
314
+ if min.zero? && max == 1
315
+ case body
316
+ when Parsanol::Atoms::Re
317
+ [MAYBE_RE, body.re, byte_table(body.re), nil]
318
+ when Parsanol::Atoms::Str
319
+ bytes = body.str.unpack("C*")
320
+ [MAYBE_STR, bytes, bytes.size, body.str.bytesize]
321
+ end
322
+ elsif min >= 1 && max.nil?
323
+ case body
324
+ when Parsanol::Atoms::Re
325
+ [RUN_RE, body.re, byte_table(body.re), min]
326
+ when Parsanol::Atoms::Str
327
+ bytes = body.str.unpack("C*")
328
+ [RUN_STR, bytes, bytes.size, min]
329
+ end
330
+ end
331
+ end
332
+ end
333
+
334
+ # Repetition compilation with terminal fusions:
335
+ # * maybe(0,1) of a single terminal -> one MAYBE_* op (no backtrack
336
+ # entry, no unwind cycle on the empty path)
337
+ # * unbounded-plus of a single terminal (non-spine sites) -> greedy
338
+ # RUN_* op
339
+ def compile_repetition(atom, consume_all)
340
+ min = atom.min
341
+ max = atom.max
342
+ return nil if max&.zero?
343
+
344
+ body = atom.parslet
345
+ if min.zero? && max == 1 && !consume_all
346
+ case body
347
+ when Parsanol::Atoms::Re
348
+ emit(MAYBE_RE, body.re, byte_table(body.re), nil)
349
+ return self
350
+ when Parsanol::Atoms::Str
351
+ bytes = body.str.unpack("C*")
352
+ emit(MAYBE_STR, bytes, bytes.size, body.str.bytesize)
353
+ return self
354
+ end
355
+ end
356
+ if min >= 1 && max.nil? && !consume_all
357
+ case body
358
+ when Parsanol::Atoms::Re
359
+ emit(RUN_RE, body.re, byte_table(body.re), min)
360
+ return self
361
+ when Parsanol::Atoms::Str
362
+ bytes = body.str.unpack("C*")
363
+ emit(RUN_STR, bytes, bytes.size, min)
364
+ return self
365
+ end
366
+ end
367
+
368
+ emit(REP_INIT, min, nil, nil)
369
+ test = emit(REP_TEST, nil, max, nil)
370
+ return nil unless compile_atom(body, false)
371
+
372
+ step = emit(REP_STEP, nil, nil, max)
373
+ exit_ = emit(REP_EXIT, atom.result_tag, consume_all ? 1 : 0, nil)
374
+ patch(test, 1, flat(exit_))
375
+ patch(step, 1, flat(test))
376
+ patch(step, 2, flat(exit_))
377
+ self
378
+ end
379
+
380
+ def compile_entity(atom, consume_all)
381
+ body = begin
382
+ atom.parslet
383
+ rescue StandardError
384
+ nil
385
+ end
386
+ return nil if body.nil?
387
+
388
+ # Leaf entities are transparent: a rule whose body is a single
389
+ # terminal compiles inline, skipping the CALL/RET round trip.
390
+ case body
391
+ when Parsanol::Atoms::Str
392
+ bytes = body.str.unpack("C*")
393
+ emit(STR, bytes, bytes.size, body.str.bytesize)
394
+ return self
395
+ when Parsanol::Atoms::Re
396
+ emit(RE, body.re, byte_table(body.re), nil)
397
+ return self
398
+ end
399
+
400
+ # Non-recursive rules inline: the body's ops replace the CALL/RET
401
+ # round trip entirely. A body currently being compiled (directly
402
+ # or indirectly) is recursive and must stay a subroutine.
403
+ # rubocop:disable Lint/HashCompareByIdentity -- object_id keys; would otherwise pin every atom alive
404
+ unless (@compiling ||= {})[body.object_id]
405
+ @compiling[body.object_id] = true
406
+ # rubocop:enable Lint/HashCompareByIdentity
407
+ result = compile_atom(body, consume_all)
408
+ @compiling.delete(body.object_id)
409
+ return result if result
410
+
411
+ return nil
412
+ end
413
+
414
+ key = [body.object_id, consume_all]
415
+ sub = @subs[key]
416
+ if sub.is_a?(Integer)
417
+ emit(CALL, flat(sub), nil, nil)
418
+ else
419
+ call_idx = emit(CALL, nil, nil, nil)
420
+ (@pending[key] ||= []) << [call_idx, body]
421
+ @subs[key] = :pending if sub.nil?
422
+ end
423
+ self
424
+ end
425
+
426
+ def compile_lookahead(atom, consume_all)
427
+ positive = atom.positive
428
+ bound = atom.bound_parslet
429
+ return nil if bound.nil?
430
+
431
+ entry = emit(positive ? LOOK_POS : LOOK_NEG, nil, nil, nil)
432
+ return nil unless compile_atom(bound, consume_all)
433
+
434
+ emit(positive ? LOOK_POS_END : LOOK_NEG_END)
435
+ patch(entry, 1, flat(@ops.size)) # continuation for K_NEG entries
436
+ self
437
+ end
438
+
439
+ # 256-entry table: "regex matches a prefix consisting of exactly this
440
+ # ASCII byte" — only when the regex is provably a single-char class.
441
+ def byte_table(re) # rubocop:disable Naming/MethodParameterName
442
+ return nil unless SINGLE_CLASS_RE.match?(re.source)
443
+ return nil if re.match?("") # can match empty -> not byte-local
444
+
445
+ table = Array.new(256, false)
446
+ (1..127).each do |b|
447
+ table[b] = true if re.match?(b.chr)
448
+ end
449
+ table
450
+ end
451
+ end
452
+
453
+ # One execution of a compiled program over one input string.
454
+ # When a parse succeeds but consumed a large fraction of the step
455
+ # budget, the grammar backtracks heavily and the tree interpreter
456
+ # (with its adaptive memoization) is the better engine; #run reports
457
+ # this via :heavy so Base#parse can skip the VM for the grammar.
458
+ class Executor
459
+ def initialize(program, input, consume_all)
460
+ @ops = program
461
+ @input = input
462
+ @consume_all = consume_all
463
+ end
464
+
465
+ def execute # rubocop:disable Metrics/MethodLength, Metrics/BlockLength, Metrics/BlockNesting -- single dispatch loop; hot path
466
+ ops = @ops
467
+ input = @input
468
+ bytes = input.unpack("C*")
469
+ # Regexp#match?(str, pos) searches FORWARD from pos (unanchored);
470
+ # StringScanner#match? is position-anchored, matching the
471
+ # interpreter's Source#matches? semantics exactly.
472
+ scanner = StringScanner.new(input)
473
+ n = bytes.size
474
+ budget = (STEP_BUDGET_FACTOR * n) + STEP_BUDGET_FIXED
475
+ steps = 0
476
+
477
+ pc = 0
478
+ pos = 0
479
+ rstack = []
480
+ bt = []
481
+ frames = []
482
+ calls = []
483
+
484
+ # rubocop:disable-next Metrics/BlockLength -- the dispatch loop IS execute
485
+ loop do
486
+ steps += 1
487
+ return BAIL if steps > budget
488
+
489
+ if pc == FAIL
490
+ pc, pos = unwind(bt, rstack, frames, calls)
491
+ return [false, nil] if pc == :fail
492
+ return BAIL if pc == :bail
493
+
494
+ next
495
+ end
496
+
497
+ case ops[pc]
498
+ when STR
499
+ lit = ops[pc + 1]
500
+ ln = ops[pc + 2]
501
+ i = 0
502
+ matched = true
503
+ while i < ln
504
+ if bytes[pos + i] != lit[i]
505
+ matched = false
506
+ break
507
+ end
508
+ i += 1
509
+ end
510
+ if matched
511
+ rstack << ((pos << 20) | ln)
512
+ pos += ops[pc + 3]
513
+ pc += 4
514
+ else
515
+ pc = FAIL
516
+ end
517
+ when RE
518
+ b = bytes[pos]
519
+ matched =
520
+ if (table = ops[pc + 2]) && b && b < 128
521
+ table[b]
522
+ elsif pos < n
523
+ (scanner.pos = pos) && scanner.match?(ops[pc + 1])
524
+ else
525
+ false
526
+ end
527
+ if matched
528
+ w = CHAR_WIDTH[b]
529
+ w = char_width_slow(input, pos) if w.zero?
530
+ rstack << ((pos << 20) | w)
531
+ pos += w
532
+ pc += 4
533
+ else
534
+ pc = FAIL
535
+ end
536
+ when ANY
537
+ if pos < n
538
+ w = CHAR_WIDTH[bytes[pos]]
539
+ w = char_width_slow(input, pos) if w.zero?
540
+ rstack << ((pos << 20) | w)
541
+ pos += w
542
+ pc += 4
543
+ else
544
+ pc = FAIL
545
+ end
546
+ when SEQ_BEGIN
547
+ rstack << SEQ_MARK
548
+ pc += 4
549
+ when SEQ_END
550
+ # Pop pushes in reverse; append and single reverse (O(n), no
551
+ # per-element unshift).
552
+ values = []
553
+ guard = rstack.size
554
+ ok_seq = false
555
+ while guard.positive?
556
+ v = rstack.pop
557
+ guard -= 1
558
+ if v == SEQ_MARK
559
+ ok_seq = true
560
+ break
561
+ end
562
+ values << v
563
+ end
564
+ return [false, nil] unless ok_seq
565
+
566
+ values << :sequence
567
+ values.reverse!
568
+ rstack << values
569
+ pc += 4
570
+ when CHOICE
571
+ bt << ops[pc + 1] << pos << rstack.size << frames.size << calls.size << K_ALT
572
+ pc += 4
573
+ when POPBT
574
+ bt.pop(BT_STRIDE)
575
+ pc += 4
576
+ when JMP
577
+ pc = ops[pc + 1]
578
+ when NAME_END
579
+ rstack << VM::NamedValue.new(ops[pc + 1], rstack.pop)
580
+ pc += 4
581
+ when CALL
582
+ calls << (pc + 4)
583
+ pc = ops[pc + 1]
584
+ when RET
585
+ pc = calls.pop
586
+ return BAIL if pc.nil?
587
+ when LOOK_POS
588
+ bt << ops[pc + 1] << pos << rstack.size << frames.size << calls.size << K_LOOK_FAIL
589
+ pc += 4
590
+ when LOOK_POS_END
591
+ # Body matched: restore entry's saved pos, drop entry and the
592
+ # body's value, push nil like Lookahead#try.
593
+ pos = bt[-BT_STRIDE + 1]
594
+ bt.pop(BT_STRIDE)
595
+ rstack.pop
596
+ rstack << nil
597
+ pc += 4
598
+ when LOOK_NEG
599
+ bt << ops[pc + 1] << pos << rstack.size << frames.size << calls.size << K_NEG
600
+ pc += 4
601
+ when LOOK_NEG_END
602
+ # Body matched: negative lookahead fails. Drop entry + body
603
+ # values, then propagate failure outward.
604
+ rlen = bt[-BT_STRIDE + 2]
605
+ rstack.slice!(rlen..) if rstack.size > rlen
606
+ bt.pop(BT_STRIDE)
607
+ pc = FAIL
608
+ when DROP
609
+ rstack.pop
610
+ rstack << nil
611
+ pc += 4
612
+ when REP_INIT
613
+ frames << 0 << pos << rstack.size << ops[pc + 1] # count,end,rbase,min
614
+ pc += 4
615
+ when REP_TEST
616
+ cbase = frames.size - FRAME_STRIDE
617
+ count = frames[cbase]
618
+ return BAIL if count.nil?
619
+
620
+ max = ops[pc + 2]
621
+ if max && count >= max
622
+ pc = ops[pc + 1] # exit
623
+ else
624
+ bt << ops[pc + 1] << frames[cbase + 1] << rstack.size << cbase << calls.size << K_REP
625
+ pc += 4 # body
626
+ end
627
+ when REP_STEP
628
+ bt.pop(BT_STRIDE)
629
+ cbase = frames.size - FRAME_STRIDE
630
+ count = frames[cbase]
631
+ return BAIL if count.nil?
632
+
633
+ count += 1
634
+ frames[cbase] = count
635
+ frames[cbase + 1] = pos
636
+ max = ops[pc + 3]
637
+ pc = if max && count >= max
638
+ ops[pc + 2] # exit
639
+ else
640
+ ops[pc + 1] # test
641
+ end
642
+ when REP_EXIT
643
+ cbase = frames.size - FRAME_STRIDE
644
+ return BAIL if frames[cbase].nil?
645
+
646
+ if ops[pc + 2] == 1 && pos != n
647
+ frames.slice!(cbase..)
648
+ pc = FAIL
649
+ else
650
+ rbase = frames[cbase + 2]
651
+ values = rstack.pop(rstack.size - rbase)
652
+ values.unshift(ops[pc + 1])
653
+ rstack << values
654
+ frames.slice!(cbase..)
655
+ pc += 4
656
+ end
657
+ when SEQ_SIMPLE
658
+ kids = ops[pc + 1]
659
+ kn = kids.size
660
+ values = [:sequence]
661
+ ki = 0
662
+ failed = false
663
+ while ki < kn
664
+ kop = kids[ki]
665
+ ka = kids[ki + 1]
666
+ kb = kids[ki + 2]
667
+ kc = kids[ki + 3]
668
+ case kop
669
+ when RE
670
+ b = bytes[pos]
671
+ m = if kb && b && b < 128
672
+ kb[b]
673
+ elsif pos < n
674
+ (scanner.pos = pos) && scanner.match?(ka)
675
+ else
676
+ false
677
+ end
678
+ if m
679
+ w = CHAR_WIDTH[b]
680
+ w = char_width_slow(input, pos) if w.zero?
681
+ values << ((pos << 20) | w)
682
+ pos += w
683
+ else
684
+ failed = true
685
+ end
686
+ when STR
687
+ i = 0
688
+ ln = kb
689
+ m = true
690
+ while i < ln
691
+ if bytes[pos + i] != ka[i] # rubocop:disable Metrics/BlockNesting -- fused byte-compare loop
692
+ m = false
693
+ break
694
+ end
695
+ i += 1
696
+ end
697
+ if m
698
+ values << ((pos << 20) | ln)
699
+ pos += kc
700
+ else
701
+ failed = true
702
+ end
703
+ when MAYBE_RE
704
+ b = bytes[pos]
705
+ m = if kb && b && b < 128
706
+ kb[b]
707
+ elsif pos < n
708
+ (scanner.pos = pos) && scanner.match?(ka)
709
+ else
710
+ false
711
+ end
712
+ if m
713
+ w = CHAR_WIDTH[b]
714
+ w = char_width_slow(input, pos) if w.zero?
715
+ values << [:maybe, (pos << 20) | w]
716
+ pos += w
717
+ else
718
+ values << [:maybe]
719
+ end
720
+ when MAYBE_STR
721
+ i = 0
722
+ ln = kb
723
+ m = true
724
+ while i < ln
725
+ if bytes[pos + i] != ka[i] # rubocop:disable Metrics/BlockNesting -- fused byte-compare loop
726
+ m = false
727
+ break
728
+ end
729
+ i += 1
730
+ end
731
+ if m
732
+ values << [:maybe, (pos << 20) | ln]
733
+ pos += kc
734
+ else
735
+ values << [:maybe]
736
+ end
737
+ when RUN_RE
738
+ loop do
739
+ b = bytes[pos]
740
+ m = if kb && b && b < 128
741
+ kb[b]
742
+ elsif pos < n
743
+ (scanner.pos = pos) && scanner.match?(ka)
744
+ else
745
+ false
746
+ end
747
+ break unless m
748
+
749
+ w = CHAR_WIDTH[b]
750
+ w = char_width_slow(input, pos) if w.zero?
751
+ values << ((pos << 20) | w)
752
+ pos += w
753
+ end
754
+ when RUN_STR
755
+ loop do
756
+ i = 0
757
+ ln = kb
758
+ m = true
759
+ while i < ln
760
+ if bytes[pos + i] != ka[i] # rubocop:disable Metrics/BlockNesting -- fused byte-compare loop
761
+ m = false
762
+ break
763
+ end
764
+ i += 1
765
+ end
766
+ break unless m
767
+
768
+ values << ((pos << 20) | ln)
769
+ pos += ln
770
+ end
771
+ end
772
+ break if failed
773
+
774
+ ki += 4
775
+ end
776
+ if failed
777
+ pc = FAIL
778
+ else
779
+ rstack << values
780
+ pc += 4
781
+ end
782
+ when MAYBE_RE
783
+ b = bytes[pos]
784
+ matched =
785
+ if (table = ops[pc + 2]) && b && b < 128
786
+ table[b]
787
+ elsif pos < n
788
+ (scanner.pos = pos) && scanner.match?(ops[pc + 1])
789
+ else
790
+ false
791
+ end
792
+ if matched
793
+ w = CHAR_WIDTH[b]
794
+ w = char_width_slow(input, pos) if w.zero?
795
+ rstack << [:maybe, (pos << 20) | w]
796
+ pos += w
797
+ else
798
+ rstack << [:maybe]
799
+ end
800
+ pc += 4
801
+ when MAYBE_STR
802
+ lit = ops[pc + 1]
803
+ ln = ops[pc + 2]
804
+ i = 0
805
+ matched = true
806
+ while i < ln
807
+ if bytes[pos + i] != lit[i]
808
+ matched = false
809
+ break
810
+ end
811
+ i += 1
812
+ end
813
+ if matched
814
+ rstack << [:maybe, (pos << 20) | ln]
815
+ pos += ops[pc + 3]
816
+ else
817
+ rstack << [:maybe]
818
+ end
819
+ pc += 4
820
+ when RUN_RE
821
+ re = ops[pc + 1]
822
+ table = ops[pc + 2]
823
+ min = ops[pc + 3]
824
+ values = [:repetition]
825
+ loop do
826
+ b = bytes[pos]
827
+ m = if table && b && b < 128
828
+ table[b]
829
+ elsif pos < n
830
+ (scanner.pos = pos) && scanner.match?(re)
831
+ else
832
+ false
833
+ end
834
+ break unless m
835
+
836
+ w = CHAR_WIDTH[b]
837
+ w = char_width_slow(input, pos) if w.zero?
838
+ values << ((pos << 20) | w)
839
+ pos += w
840
+ end
841
+ if min && values.size - 1 < min
842
+ pc = FAIL
843
+ else
844
+ rstack << values
845
+ pc += 4
846
+ end
847
+ when RUN_STR
848
+ lit = ops[pc + 1]
849
+ ln = ops[pc + 2]
850
+ min = ops[pc + 3]
851
+ values = [:repetition]
852
+ loop do
853
+ i = 0
854
+ matched = true
855
+ while i < ln
856
+ if bytes[pos + i] != lit[i]
857
+ matched = false
858
+ break
859
+ end
860
+ i += 1
861
+ end
862
+ break unless matched
863
+
864
+ values << ((pos << 20) | ln)
865
+ pos += ln
866
+ end
867
+ if min && values.size - 1 < min
868
+ pc = FAIL
869
+ else
870
+ rstack << values
871
+ pc += 4
872
+ end
873
+ when HALT
874
+ if @consume_all && pos != n
875
+ pc = FAIL
876
+ next
877
+ end
878
+ value = VM.materialize(rstack[0], input)
879
+ # More than ~100 steps per input byte means heavy
880
+ # backtracking; the memoizing interpreter wins there.
881
+ return steps > (n << 9) + 1000 ? [:heavy, value] : [true, value]
882
+ else
883
+ return BAIL
884
+ end
885
+ end
886
+ end
887
+
888
+ private
889
+
890
+ # Unwind one backtrack entry. Returns [pc, pos] or :fail. Repetition
891
+ # entries with count >= min complete their tagged array and jump to
892
+ # the exit continuation with the last iteration end position.
893
+ def unwind(bt, rstack, frames, calls) # rubocop:disable Naming/MethodParameterName -- stack register names
894
+ return :fail if bt.empty?
895
+
896
+ # Everything above this entry belongs to constructs being
897
+ # abandoned by the jump (inner alternatives, repetitions, or
898
+ # subroutine state); discard it along with the entry itself.
899
+ blen = bt.size - BT_STRIDE
900
+ if ENV["VM_TRACE"]
901
+ warn " UNWIND bt=#{bt.each_slice(6).map { |e| "[#{e[0]},#{e[1]},#{e[2]},#{e[3]},#{e[4]},#{e[5]}" }.join(' ')}"
902
+ end
903
+ kind = bt.pop
904
+ clen = bt.pop
905
+ cbase = bt.pop
906
+ rlen = bt.pop
907
+ epos = bt.pop
908
+ epc = bt.pop
909
+ bt.slice!(blen..) if bt.size > blen
910
+
911
+ rstack.slice!(rlen..) if rstack.size > rlen
912
+ calls.slice!(clen..) if calls.size > clen
913
+
914
+ case kind
915
+ when K_ALT
916
+ frames.slice!(cbase..) if frames.size > cbase
917
+ [epc, epos]
918
+ when K_REP
919
+ count = frames[cbase]
920
+ min = frames[cbase + 3]
921
+ if count.nil? || min.nil?
922
+ return :bail
923
+ end
924
+
925
+ if count >= min
926
+ # Repetition succeeds with the completed prefix.
927
+ rbase = frames[cbase + 2]
928
+ end_pos = frames[cbase + 1]
929
+ tag = @ops[epc + 1]
930
+ check_full = @ops[epc + 2]
931
+ if check_full == 1 && end_pos != @input.bytesize
932
+ frames.slice!(cbase..)
933
+ unwind(bt, rstack, frames, calls)
934
+ else
935
+ values = rstack.pop(rstack.size - rbase)
936
+ values.unshift(tag)
937
+ rstack << values
938
+ frames.slice!(cbase..)
939
+ [epc + 4, end_pos]
940
+ end
941
+ else
942
+ frames.slice!(cbase..)
943
+ unwind(bt, rstack, frames, calls)
944
+ end
945
+ when K_LOOK_FAIL
946
+ frames.slice!(cbase..) if frames.size > cbase
947
+ unwind(bt, rstack, frames, calls)
948
+ when K_NEG
949
+ # Negative lookahead body failed -> lookahead succeeds.
950
+ frames.slice!(cbase..) if frames.size > cbase
951
+ rstack << nil
952
+ [epc, epos]
953
+ end
954
+ end
955
+
956
+ def char_width_slow(input, pos)
957
+ w = CHAR_WIDTH[input.getbyte(pos)]
958
+ w.zero? ? 1 : w
959
+ end
960
+ end
961
+ end
962
+ end