parsanol 1.3.14-arm-linux → 1.3.15-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 -1
- data/lib/parsanol/native/transformer.rb +8 -7
- data/lib/parsanol/native.rb +20 -13
- 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 +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e98e448bc4374451a62e175387759b4db66c90160e5df3204e652c17a751ddd7
|
|
4
|
+
data.tar.gz: 7f081c2cd9e6fac46cd4e43d15c91a6755af3aad8b01c35b5cb542d190cf2786
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 682268466119952e99a678a79270d6fe69a449630665a8e082e7f01f709903f6190552053e6328ec74f47fd452d1b946bedd5b89687a20594debbe33a6e9a2d2
|
|
7
|
+
data.tar.gz: 386e99e3a11788991e7b1fc5fb0ae55a6c95caa0b60508c62c3f997492c213887135d9d7b4bcb04a72b7d9d8c8bd20a9b20b105b6a2e31cc78b814db32380306
|
data/lib/parsanol/atoms/base.rb
CHANGED
|
@@ -30,9 +30,34 @@ module Parsanol
|
|
|
30
30
|
# @return [Object] the parsed result
|
|
31
31
|
# @raise [Parsanol::ParseFailed] on parse failure
|
|
32
32
|
def parse(source, options = {})
|
|
33
|
-
input = normalize_input(source)
|
|
34
33
|
must_consume_all = !options[:prefix]
|
|
35
34
|
|
|
35
|
+
# Compiled-VM fast path: String input, full-consumption semantics.
|
|
36
|
+
# On failure (or unsupported grammar / budget exhaustion) fall
|
|
37
|
+
# through to the interpreter, which also produces the exact
|
|
38
|
+
# cause-tree diagnostics.
|
|
39
|
+
if must_consume_all && source.is_a?(String) &&
|
|
40
|
+
(program = VM.program_for(self))
|
|
41
|
+
result = VM.run(program, source, true)
|
|
42
|
+
if result == VM::BAIL
|
|
43
|
+
# Internal bail: fall back to the interpreter and skip the VM
|
|
44
|
+
# for this grammar from now on.
|
|
45
|
+
VM.disable_for!(self)
|
|
46
|
+
elsif result.first == :heavy
|
|
47
|
+
# Succeeded but burned >1/8 of the step budget —
|
|
48
|
+
# heavy-backtracking grammar; the memoizing interpreter is
|
|
49
|
+
# the better engine from now on.
|
|
50
|
+
VM.disable_for!(self)
|
|
51
|
+
return finalize_result(result[1])
|
|
52
|
+
elsif result.first
|
|
53
|
+
return finalize_result(result[1])
|
|
54
|
+
end
|
|
55
|
+
# Clean [false, nil]: the input does not parse — the interpreter
|
|
56
|
+
# reparse below produces the detailed cause tree.
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
input = normalize_input(source)
|
|
60
|
+
|
|
36
61
|
# Initial parse attempt (no error collection)
|
|
37
62
|
success, value = run_with_context(input, nil, must_consume_all)
|
|
38
63
|
return finalize_result(value) if success
|
|
@@ -132,9 +132,11 @@ module Parsanol
|
|
|
132
132
|
# Transform the value
|
|
133
133
|
transformed = transform(value, named: true)
|
|
134
134
|
|
|
135
|
-
# Check if value is a tagged repetition from native parser
|
|
135
|
+
# Check if value is a tagged repetition from native parser.
|
|
136
|
+
# The Rust handle path tags with Symbols, the batch decoder with
|
|
137
|
+
# ":repetition" Strings — accept both.
|
|
136
138
|
is_tagged_repetition = value.is_a?(Array) && !value.empty? &&
|
|
137
|
-
|
|
139
|
+
[REPETITION_SYM, REPETITION_TAG].include?(value.first)
|
|
138
140
|
|
|
139
141
|
# Check RAW value for repetition pattern BEFORE transformation
|
|
140
142
|
# Array with items that all have the parent key
|
|
@@ -203,13 +205,12 @@ module Parsanol
|
|
|
203
205
|
# Empty array from repetition stays as empty array
|
|
204
206
|
if transformed.empty?
|
|
205
207
|
{ sym_key => EMPTY_ARRAY }
|
|
206
|
-
#
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
# Hash items already carry their own capture names — a
|
|
209
|
+
# repetition of named captures keeps them as-is (parslet
|
|
210
|
+
# semantics); only unnamed items get the parent name per item.
|
|
211
|
+
elsif transformed.all?(Hash)
|
|
210
212
|
{ sym_key => transformed }
|
|
211
213
|
else
|
|
212
|
-
# Wrap each item with the name
|
|
213
214
|
{ sym_key => transformed.map { |item| { sym_key => item } } }
|
|
214
215
|
end
|
|
215
216
|
elsif transformed.is_a?(::Parsanol::Slice) && transformed.empty?
|
data/lib/parsanol/native.rb
CHANGED
|
@@ -38,14 +38,14 @@ module Parsanol
|
|
|
38
38
|
def parse(grammar, input)
|
|
39
39
|
raise LoadError, "Native parser not available" unless available?
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
# Both sub-methods return the final decoded tree; on native failure
|
|
42
|
+
# they fall back to the pure-Ruby parser, whose result is already
|
|
43
|
+
# final and must not be transformed again.
|
|
44
|
+
if grammar.is_a?(String)
|
|
45
|
+
parse_json_grammar(grammar, input)
|
|
46
|
+
else
|
|
47
|
+
parse_atom_grammar(grammar, input)
|
|
48
|
+
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Memory-bounded parsing without packrat cache.
|
|
@@ -65,12 +65,14 @@ module Parsanol
|
|
|
65
65
|
Parser.serialize_grammar(grammar)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# Decode here, not around the fallback: raise_native_parse_error
|
|
69
|
+
# returns the Ruby parser's already-final tree on success.
|
|
68
70
|
begin
|
|
69
|
-
|
|
71
|
+
BatchDecoder.decode_and_flatten(_parse_fresh_raw(grammar_json, input),
|
|
72
|
+
input, Parsanol::Slice)
|
|
70
73
|
rescue RuntimeError => e
|
|
71
74
|
raise_native_parse_error(e, grammar, input)
|
|
72
75
|
end
|
|
73
|
-
BatchDecoder.decode_and_flatten(raw_ast, input, Parsanol::Slice)
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
# Parse and return RAW AST without transformation.
|
|
@@ -201,7 +203,8 @@ module Parsanol
|
|
|
201
203
|
|
|
202
204
|
# Pre-serialized JSON grammar path (library authors with cached JSON).
|
|
203
205
|
def parse_json_grammar(grammar_json, input)
|
|
204
|
-
_parse_raw(grammar_json, input)
|
|
206
|
+
BatchDecoder.decode_and_flatten(_parse_raw(grammar_json, input),
|
|
207
|
+
input, Parsanol::Slice)
|
|
205
208
|
rescue RuntimeError => e
|
|
206
209
|
raise_native_parse_error(e, grammar_json, input)
|
|
207
210
|
end
|
|
@@ -212,12 +215,16 @@ module Parsanol
|
|
|
212
215
|
handle = Parser.grammar_handle(grammar)
|
|
213
216
|
|
|
214
217
|
begin
|
|
215
|
-
_parse_handle(handle, input)
|
|
218
|
+
BatchDecoder.decode_and_flatten(_parse_handle(handle, input),
|
|
219
|
+
input, Parsanol::Slice)
|
|
216
220
|
rescue ArgumentError
|
|
217
221
|
# Handle dropped Rust-side (e.g. cache cleared): re-register once.
|
|
218
222
|
Parser.invalidate_handle(handle)
|
|
219
223
|
begin
|
|
220
|
-
|
|
224
|
+
BatchDecoder.decode_and_flatten(
|
|
225
|
+
_parse_handle(Parser.grammar_handle(grammar), input),
|
|
226
|
+
input, Parsanol::Slice
|
|
227
|
+
)
|
|
221
228
|
rescue RuntimeError => e
|
|
222
229
|
raise_native_parse_error(e, grammar, input)
|
|
223
230
|
end
|
data/lib/parsanol/parser.rb
CHANGED
|
@@ -109,32 +109,36 @@ module Parsanol
|
|
|
109
109
|
# result[:name].to_s # => "hello"
|
|
110
110
|
#
|
|
111
111
|
def parse(input, mode_or_opts = {}, **kwargs)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
# Native backend by default; falls back to pure Ruby inside
|
|
118
|
-
# parse_native when the extension is missing.
|
|
119
|
-
parse_native(input, merged)
|
|
112
|
+
opts =
|
|
113
|
+
if mode_or_opts.is_a?(Hash)
|
|
114
|
+
mode_or_opts.merge(kwargs)
|
|
115
|
+
elsif mode_or_opts.nil?
|
|
116
|
+
kwargs
|
|
120
117
|
else
|
|
121
|
-
|
|
118
|
+
# New API: parse(input, :mode, **options)
|
|
119
|
+
kwargs[:mode] = mode_or_opts
|
|
120
|
+
kwargs
|
|
122
121
|
end
|
|
122
|
+
# parse(input, {mode: :ruby, ...}) is the same call shape a subclass
|
|
123
|
+
# forwarding options via super produces; :mode must be honored
|
|
124
|
+
# wherever it appears, or those callers silently get the native path.
|
|
125
|
+
mode = opts.delete(:mode) ||
|
|
126
|
+
(if Parsanol::Native.available? && !opts.key?(:prefix) &&
|
|
127
|
+
!opts.key?(:reporter)
|
|
128
|
+
:native
|
|
129
|
+
else
|
|
130
|
+
:ruby
|
|
131
|
+
end)
|
|
132
|
+
case mode
|
|
133
|
+
when :ruby
|
|
134
|
+
super(input, opts)
|
|
135
|
+
when :native
|
|
136
|
+
parse_native(input, opts)
|
|
137
|
+
when :json
|
|
138
|
+
parse_json(input, opts)
|
|
123
139
|
else
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
(Parsanol::Native.available? ? :native : :ruby)
|
|
127
|
-
case mode
|
|
128
|
-
when :ruby
|
|
129
|
-
super(input, kwargs)
|
|
130
|
-
when :native
|
|
131
|
-
parse_native(input, kwargs)
|
|
132
|
-
when :json
|
|
133
|
-
parse_json(input, kwargs)
|
|
134
|
-
else
|
|
135
|
-
raise ArgumentError,
|
|
136
|
-
"Unknown mode: #{mode}. Valid modes: :ruby, :native, :json"
|
|
137
|
-
end
|
|
140
|
+
raise ArgumentError,
|
|
141
|
+
"Unknown mode: #{mode}. Valid modes: :ruby, :native, :json"
|
|
138
142
|
end
|
|
139
143
|
end
|
|
140
144
|
|
data/lib/parsanol/version.rb
CHANGED
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
|
data/lib/parsanol.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parsanol
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.15
|
|
5
5
|
platform: arm-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -238,6 +238,7 @@ files:
|
|
|
238
238
|
- lib/parsanol/string_view.rb
|
|
239
239
|
- lib/parsanol/transform.rb
|
|
240
240
|
- lib/parsanol/version.rb
|
|
241
|
+
- lib/parsanol/vm.rb
|
|
241
242
|
- lib/parsanol/wasm/README.md
|
|
242
243
|
- lib/parsanol/wasm/package.json
|
|
243
244
|
- lib/parsanol/wasm/parsanol.js
|