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.
- checksums.yaml +4 -4
- data/Cargo.lock +2 -2
- data/lib/parsanol/atoms/base.rb +26 -1
- data/lib/parsanol/atoms/can_flatten.rb +64 -14
- data/lib/parsanol/atoms/repetition.rb +18 -21
- data/lib/parsanol/atoms/sequence.rb +10 -19
- data/lib/parsanol/native/parser.rb +32 -4
- data/lib/parsanol/native/transformer.rb +8 -7
- data/lib/parsanol/native.rb +43 -17
- data/lib/parsanol/parser.rb +27 -23
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +962 -0
- data/lib/parsanol.rb +1 -0
- metadata +2 -1
data/lib/parsanol/vm.rb
ADDED
|
@@ -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
|