parsanol 1.3.67 → 1.3.68
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/context.rb +23 -4
- data/lib/parsanol/atoms/sequence.rb +5 -1
- data/lib/parsanol/atoms/trivia.rb +21 -0
- data/lib/parsanol/atoms.rb +1 -0
- data/lib/parsanol/cause.rb +38 -2
- data/lib/parsanol/native/ffi.rb +28 -3
- data/lib/parsanol/native/parser.rb +55 -5
- data/lib/parsanol/parg/compiler.rb +37 -15
- data/lib/parsanol/parg/document.rb +6 -1
- data/lib/parsanol/parg/parser.rb +7 -1
- data/lib/parsanol/version.rb +1 -1
- data/lib/parsanol/vm.rb +51 -20
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5917e9971751a101c27984a0ccec6aa6254813b77d18a45eea7f6a5538ead517
|
|
4
|
+
data.tar.gz: 9513f097d858d7c394a10c8002e372523212bf11629813867ec59a91dacbc41d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 65cf06e07d16879100effb50a7ed8012e95774ed6b8c86f97c997d3b6ce7a41525a07e3943d4670dc3c6f65e91d587953e719668decba35f90610bdf29a7c011
|
|
7
|
+
data.tar.gz: 84cd53317231f028e6510d90c7c426379566e47570594e960b022766ae45fffb8305afdcc5f044339fbbdb3a30c74e44e25ce03a735c7e3672221f70f26b0ab1
|
|
@@ -63,6 +63,10 @@ module Parsanol
|
|
|
63
63
|
|
|
64
64
|
# Cache eviction state
|
|
65
65
|
@furthest_pos = 0
|
|
66
|
+
|
|
67
|
+
# Injected-trivia depth: while positive, reported causes are
|
|
68
|
+
# marked trivia and never surface in diagnostics.
|
|
69
|
+
@trivia_depth = 0
|
|
66
70
|
@evict_threshold = 200
|
|
67
71
|
@evict_counter = 0
|
|
68
72
|
@evict_interval = 100
|
|
@@ -230,15 +234,17 @@ module Parsanol
|
|
|
230
234
|
# Pre-allocated result constants
|
|
231
235
|
SUCCESS_RESULT = [true, nil].freeze
|
|
232
236
|
ERROR_RESULT = [false, nil].freeze
|
|
237
|
+
TRIVIA_ERROR = [false, Cause::TriviaCause.instance].freeze
|
|
233
238
|
|
|
234
239
|
# Reports an error at a specific position.
|
|
235
240
|
#
|
|
236
241
|
# @return [Array(Boolean, Object)] error result tuple
|
|
237
242
|
#
|
|
238
243
|
def err_at(*)
|
|
239
|
-
return
|
|
244
|
+
return TRIVIA_ERROR if @trivia_depth.positive?
|
|
245
|
+
return ERROR_RESULT unless @reporter
|
|
240
246
|
|
|
241
|
-
|
|
247
|
+
[false, @reporter.err_at(*)]
|
|
242
248
|
end
|
|
243
249
|
|
|
244
250
|
# Reports an error at the current position.
|
|
@@ -246,9 +252,22 @@ module Parsanol
|
|
|
246
252
|
# @return [Array(Boolean, Object)] error result tuple
|
|
247
253
|
#
|
|
248
254
|
def err(*)
|
|
249
|
-
return
|
|
255
|
+
return TRIVIA_ERROR if @trivia_depth.positive?
|
|
256
|
+
return ERROR_RESULT unless @reporter
|
|
257
|
+
|
|
258
|
+
[false, @reporter.err(*)]
|
|
259
|
+
end
|
|
250
260
|
|
|
251
|
-
|
|
261
|
+
# Brackets a parse inside injected trivia: causes reported while
|
|
262
|
+
# the depth is positive carry the trivia flag and never surface
|
|
263
|
+
# in rendered trees or deepest-failure positions.
|
|
264
|
+
def with_trivia
|
|
265
|
+
@trivia_depth += 1
|
|
266
|
+
begin
|
|
267
|
+
yield
|
|
268
|
+
ensure
|
|
269
|
+
@trivia_depth -= 1
|
|
270
|
+
end
|
|
252
271
|
end
|
|
253
272
|
|
|
254
273
|
# Checks if this context is collecting diagnostic errors.
|
|
@@ -77,7 +77,11 @@ module Parsanol
|
|
|
77
77
|
# @param prec [Integer] precedence
|
|
78
78
|
# @return [String]
|
|
79
79
|
def to_s_inner(prec)
|
|
80
|
-
|
|
80
|
+
# Injected trivia is invisible in diagnostics: a failed
|
|
81
|
+
# sequence describes what the grammar asks for, not the skip
|
|
82
|
+
# machinery interleaved around it.
|
|
83
|
+
@parslets.grep_v(Parsanol::Atoms::Trivia)
|
|
84
|
+
.map { |p| p.to_s(prec) }.join(" ")
|
|
81
85
|
end
|
|
82
86
|
|
|
83
87
|
# FIRST set is first element's FIRST set (with epsilon propagation).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Parsanol
|
|
4
|
+
module Atoms
|
|
5
|
+
# The injected skip wrapper: Ignored semantics (matched bytes
|
|
6
|
+
# contribute nothing to values or spans) plus diagnostics
|
|
7
|
+
# transparency — failures inside trivia are marked and never
|
|
8
|
+
# surface in rendered trees or deepest-failure positions.
|
|
9
|
+
class Trivia < Ignored
|
|
10
|
+
def apply(source, context, consume_all)
|
|
11
|
+
context.with_trivia do
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_s_inner(prec)
|
|
17
|
+
"trivia(#{@wrapped_atom.to_s(prec)})"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/parsanol/atoms.rb
CHANGED
data/lib/parsanol/cause.rb
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
# cause.children # => []
|
|
17
17
|
# cause.to_s # => "Expected at least one"
|
|
18
18
|
#
|
|
19
|
+
require "singleton"
|
|
20
|
+
|
|
19
21
|
module Parsanol
|
|
20
22
|
class Cause
|
|
21
23
|
# @return [Array<String>] Error message parts
|
|
@@ -33,6 +35,15 @@ module Parsanol
|
|
|
33
35
|
# @return [Array<Cause>] Child causes
|
|
34
36
|
attr_reader :children
|
|
35
37
|
|
|
38
|
+
# True when this failure happened inside injected skip trivia:
|
|
39
|
+
# trivia positions never surface in diagnostics (rendered trees
|
|
40
|
+
# prune trivia causes; deepest-failure tracking ignores them).
|
|
41
|
+
attr_accessor :trivia
|
|
42
|
+
|
|
43
|
+
def trivia?
|
|
44
|
+
@trivia == true
|
|
45
|
+
end
|
|
46
|
+
|
|
36
47
|
# Creates a new cause for parse failure
|
|
37
48
|
#
|
|
38
49
|
# @param message [String, Array<String>] Error description
|
|
@@ -45,6 +56,7 @@ module Parsanol
|
|
|
45
56
|
@position = position
|
|
46
57
|
@children = children.nil? ? [] : children
|
|
47
58
|
@parsing_label = nil
|
|
59
|
+
@trivia = false
|
|
48
60
|
end
|
|
49
61
|
|
|
50
62
|
# Factory method for creating a cause
|
|
@@ -89,6 +101,24 @@ module Parsanol
|
|
|
89
101
|
output.string
|
|
90
102
|
end
|
|
91
103
|
|
|
104
|
+
# Stand-in for failures inside injected skip trivia: the reporting
|
|
105
|
+
# context returns it instead of calling the reporter (reporters and
|
|
106
|
+
# deepest-failure tracking never see trivia failures), and rendered
|
|
107
|
+
# trees prune it (visible).
|
|
108
|
+
class TriviaCause < Cause
|
|
109
|
+
include Singleton
|
|
110
|
+
|
|
111
|
+
def initialize
|
|
112
|
+
super("trivia", nil, 0)
|
|
113
|
+
@trivia = true
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Label propagation would mutate the shared instance.
|
|
117
|
+
def set_label(_label)
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
92
122
|
# Raises a ParseFailed exception with this cause's information
|
|
93
123
|
#
|
|
94
124
|
# @raise [Parsanol::ParseFailed] Always
|
|
@@ -99,12 +129,18 @@ module Parsanol
|
|
|
99
129
|
|
|
100
130
|
private
|
|
101
131
|
|
|
132
|
+
# Trivia causes stay out of rendered trees entirely.
|
|
133
|
+
def visible(node)
|
|
134
|
+
node.children.reject(&:trivia?)
|
|
135
|
+
end
|
|
136
|
+
|
|
102
137
|
def build_tree_recursive(node, stream, prefix_flags)
|
|
103
138
|
render_prefix(stream, prefix_flags)
|
|
104
139
|
stream.puts node.to_s
|
|
105
140
|
|
|
106
|
-
|
|
107
|
-
|
|
141
|
+
children = visible(node)
|
|
142
|
+
children.each do |child|
|
|
143
|
+
is_last_child = (children.last == child)
|
|
108
144
|
build_tree_recursive(child, stream, prefix_flags + [is_last_child])
|
|
109
145
|
end
|
|
110
146
|
end
|
data/lib/parsanol/native/ffi.rb
CHANGED
|
@@ -43,8 +43,18 @@ module Parsanol
|
|
|
43
43
|
json = ::Parsanol::Native::Parser.serialize_grammar(grammar)
|
|
44
44
|
atom = grammar
|
|
45
45
|
end
|
|
46
|
-
handle =
|
|
47
|
-
|
|
46
|
+
handle = @handles && @handles[json]
|
|
47
|
+
if handle
|
|
48
|
+
# Refresh recency: reinsertion moves the entry to the end,
|
|
49
|
+
# so churn cannot evict a hot grammar's handle.
|
|
50
|
+
@handles[json] = @handles.delete(json)
|
|
51
|
+
else
|
|
52
|
+
handle = @binding.c_register(json)
|
|
53
|
+
raise Error, "grammar registration failed" if handle.zero?
|
|
54
|
+
|
|
55
|
+
(@handles ||= {})[json] = handle
|
|
56
|
+
trim_handles
|
|
57
|
+
end
|
|
48
58
|
|
|
49
59
|
written = parse_into(handle, input)
|
|
50
60
|
if written.positive?
|
|
@@ -65,6 +75,19 @@ module Parsanol
|
|
|
65
75
|
|
|
66
76
|
private
|
|
67
77
|
|
|
78
|
+
# FIFO cap with recency refresh, mirroring Parser::CACHE_LIMIT.
|
|
79
|
+
# Without it, every grammar ever parsed kept its Rust-side
|
|
80
|
+
# HandleEntry (parsed Grammar + engine state) forever, and the
|
|
81
|
+
# map keys are full grammar JSON strings. Evicted handles are
|
|
82
|
+
# released through c_release; a later miss re-registers.
|
|
83
|
+
def trim_handles
|
|
84
|
+
while @handles.size > ::Parsanol::Native::Parser::CACHE_LIMIT
|
|
85
|
+
json, handle = @handles.first
|
|
86
|
+
@handles.delete(json)
|
|
87
|
+
@binding.c_release(handle)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
68
91
|
# Search order: explicit override, gem-vendored cdylib next to
|
|
69
92
|
# this file, then the system linker path.
|
|
70
93
|
def locate_library
|
|
@@ -110,8 +133,10 @@ module Parsanol
|
|
|
110
133
|
%i[uint64], :void
|
|
111
134
|
end
|
|
112
135
|
# Probe the symbols now so a mismatched library fails loudly
|
|
113
|
-
# at availability time, not at first parse.
|
|
136
|
+
# at availability time, not at first parse. c_release(0) is a
|
|
137
|
+
# no-op (unknown handle), so the probe has no side effect.
|
|
114
138
|
probe = binding_mod.c_last_error
|
|
139
|
+
binding_mod.c_release(0)
|
|
115
140
|
@binding = binding_mod
|
|
116
141
|
@buffer = nil
|
|
117
142
|
!probe.nil? || true
|
|
@@ -6,7 +6,21 @@ module Parsanol
|
|
|
6
6
|
module Native
|
|
7
7
|
# Core parsing functionality using Rust native extension
|
|
8
8
|
module Parser
|
|
9
|
-
|
|
9
|
+
# Cap for the grammar-keyed caches. The handle cache pins a
|
|
10
|
+
# Rust-side HandleEntry (parsed Grammar + compiled program) per
|
|
11
|
+
# live entry — without the cap, every grammar ever registered
|
|
12
|
+
# accumulates on both sides of the FFI boundary. 32 mirrors
|
|
13
|
+
# VM::PROGRAM_CACHE_LIMIT; eviction re-registers on demand (the
|
|
14
|
+
# Rust artifact cache short-circuits the re-compile).
|
|
15
|
+
CACHE_LIMIT = 32
|
|
16
|
+
|
|
17
|
+
# Identity-keyed memo: root atom object => structure hash. Keyed
|
|
18
|
+
# by the OBJECT, not object_id — a recycled id must never return a
|
|
19
|
+
# dead grammar's hash for a new grammar (through GRAMMAR_CACHE /
|
|
20
|
+
# HANDLE_CACHE that would alias the new grammar to the old
|
|
21
|
+
# grammar's serialized JSON and Rust handle: a silent wrong
|
|
22
|
+
# native parse).
|
|
23
|
+
GRAMMAR_HASH_CACHE = Hash.new.compare_by_identity
|
|
10
24
|
GRAMMAR_CACHE = Hash.new
|
|
11
25
|
# structure-hash => Rust-side grammar handle. Keyed by content so a
|
|
12
26
|
# recycled object_id can never alias a different grammar.
|
|
@@ -72,7 +86,18 @@ module Parsanol
|
|
|
72
86
|
# access — no JSON marshal, no re-hash inside Rust.
|
|
73
87
|
def grammar_handle(root_atom)
|
|
74
88
|
cache_key = grammar_cache_key(root_atom)
|
|
75
|
-
HANDLE_CACHE[cache_key]
|
|
89
|
+
handle = HANDLE_CACHE[cache_key]
|
|
90
|
+
if handle
|
|
91
|
+
# Refresh recency: reinsertion moves the entry to the end,
|
|
92
|
+
# so churn cannot evict a hot grammar's handle.
|
|
93
|
+
HANDLE_CACHE[cache_key] = HANDLE_CACHE.delete(cache_key)
|
|
94
|
+
return handle
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
handle = Native._register_grammar(grammar_json(root_atom))
|
|
98
|
+
HANDLE_CACHE[cache_key] = handle
|
|
99
|
+
trim_caches
|
|
100
|
+
handle
|
|
76
101
|
end
|
|
77
102
|
|
|
78
103
|
# Drop a handle whose Rust-side entry no longer exists; the next
|
|
@@ -82,6 +107,7 @@ module Parsanol
|
|
|
82
107
|
end
|
|
83
108
|
|
|
84
109
|
def clear_cache
|
|
110
|
+
HANDLE_CACHE.each_value { |handle| Native._release_grammar(handle) }
|
|
85
111
|
GRAMMAR_HASH_CACHE.clear
|
|
86
112
|
GRAMMAR_CACHE.clear
|
|
87
113
|
HANDLE_CACHE.clear
|
|
@@ -99,14 +125,38 @@ module Parsanol
|
|
|
99
125
|
|
|
100
126
|
def grammar_cache_key(root_atom)
|
|
101
127
|
root_atom = root_atom.root if root_atom.is_a?(::Parsanol::Parser)
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
cached = GRAMMAR_HASH_CACHE[root_atom]
|
|
129
|
+
return cached if cached
|
|
130
|
+
|
|
131
|
+
hash = grammar_structure_hash(root_atom)
|
|
132
|
+
GRAMMAR_HASH_CACHE[root_atom] = hash
|
|
133
|
+
GRAMMAR_HASH_CACHE.shift while GRAMMAR_HASH_CACHE.size > CACHE_LIMIT
|
|
134
|
+
hash
|
|
104
135
|
end
|
|
105
136
|
|
|
106
137
|
def grammar_json(root_atom)
|
|
107
138
|
root_atom = root_atom.root if root_atom.is_a?(::Parsanol::Parser)
|
|
108
139
|
cache_key = grammar_cache_key(root_atom)
|
|
109
|
-
GRAMMAR_CACHE[cache_key]
|
|
140
|
+
json = GRAMMAR_CACHE[cache_key]
|
|
141
|
+
return json if json
|
|
142
|
+
|
|
143
|
+
json = GrammarSerializer.serialize(root_atom)
|
|
144
|
+
GRAMMAR_CACHE[cache_key] = json
|
|
145
|
+
json
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# FIFO eviction past the cap; evicted handles are released
|
|
149
|
+
# Rust-side so HandleEntries (Grammar + program) do not
|
|
150
|
+
# accumulate. Re-registration on a later miss is cheap: the
|
|
151
|
+
# Rust artifact cache reloads the compiled program.
|
|
152
|
+
def trim_caches
|
|
153
|
+
while HANDLE_CACHE.size > CACHE_LIMIT
|
|
154
|
+
key, handle = HANDLE_CACHE.first
|
|
155
|
+
HANDLE_CACHE.delete(key)
|
|
156
|
+
GRAMMAR_CACHE.delete(key)
|
|
157
|
+
Native._release_grammar(handle)
|
|
158
|
+
end
|
|
159
|
+
GRAMMAR_CACHE.shift while GRAMMAR_CACHE.size > CACHE_LIMIT
|
|
110
160
|
end
|
|
111
161
|
|
|
112
162
|
def grammar_structure_hash(atom)
|
|
@@ -92,7 +92,11 @@ module Parsanol
|
|
|
92
92
|
def atom_for(name)
|
|
93
93
|
@atom_cache[name] ||= begin
|
|
94
94
|
atom = build_atom(fetch_rule(name))
|
|
95
|
-
|
|
95
|
+
# Atomic rules build without injection entirely: token-shaped
|
|
96
|
+
# rules match contiguously (no inter-iteration trivia, no
|
|
97
|
+
# entry-boundary skips).
|
|
98
|
+
if @document.skip_declaration && !skip_exempt_names.include?(name) &&
|
|
99
|
+
!@document.atomic_rules.include?(name)
|
|
96
100
|
atom = inject_skip_trivia(atom)
|
|
97
101
|
# Entry points take leading and trailing skips: tail trivia
|
|
98
102
|
# (the tail-comment case) has no following terminal to
|
|
@@ -120,7 +124,8 @@ module Parsanol
|
|
|
120
124
|
case node.kind
|
|
121
125
|
when :ref then names << node.a
|
|
122
126
|
when :alt, :seq then node.a.each { |b| walk_refs.call(b) }
|
|
123
|
-
when :cap then walk_refs.call(node.b)
|
|
127
|
+
when :cap, :pred then walk_refs.call(node.b)
|
|
128
|
+
when :rep, :opt then walk_refs.call(node.a)
|
|
124
129
|
end
|
|
125
130
|
end
|
|
126
131
|
walk_refs.call(@document.skip_declaration) if @document.skip_declaration
|
|
@@ -153,10 +158,12 @@ module Parsanol
|
|
|
153
158
|
"skip rule must be non-nullable: a skip that can match " \
|
|
154
159
|
"empty loops forever at injection points"
|
|
155
160
|
end
|
|
156
|
-
#
|
|
161
|
+
# Trivia: consumed but contributes nothing to the enclosing
|
|
157
162
|
# sequence/repetition value — trivia never leaks into
|
|
158
|
-
# span-joined captures
|
|
159
|
-
|
|
163
|
+
# span-joined captures — and failures inside it never surface
|
|
164
|
+
# in diagnostics (rendered trees and deepest-failure
|
|
165
|
+
# positions skip trivia causes).
|
|
166
|
+
wrapper = Atoms::Trivia.new(Atoms::Repetition.new(atom, 0, 1))
|
|
160
167
|
(@skip_wrapper_ids ||= {}.compare_by_identity)[wrapper] = true
|
|
161
168
|
wrapper
|
|
162
169
|
end
|
|
@@ -184,19 +191,34 @@ module Parsanol
|
|
|
184
191
|
when Atoms::Alternative
|
|
185
192
|
Atoms::Alternative.new(*atom.alternatives.map { |a| inject_skip_trivia(a) })
|
|
186
193
|
when Atoms::Repetition
|
|
187
|
-
|
|
188
|
-
|
|
194
|
+
# Inside repetition bodies, trivia is injected only before
|
|
195
|
+
# rule-reference children: structural iterations
|
|
196
|
+
# (`( sep item )*`) still span trivia between iterations,
|
|
197
|
+
# while char-scan spans (`( !"x" ANY )*` quoted-string bodies,
|
|
198
|
+
# WORD runs, comment bodies) stay verbatim — injecting there
|
|
199
|
+
# corrupted them (spaces inside strings eaten as whitespace,
|
|
200
|
+
# `//` inside a URL read as a comment). A single-ref body
|
|
201
|
+
# (`1*pair`) needs no interior injection: each iteration's own
|
|
202
|
+
# rule-level leading injection consumes the trivia.
|
|
203
|
+
body = atom.parslet
|
|
204
|
+
if body.is_a?(Atoms::Sequence) &&
|
|
205
|
+
body.parslets.any?(Atoms::Entity)
|
|
206
|
+
kids = body.parslets.flat_map do |child|
|
|
207
|
+
child.is_a?(Atoms::Entity) ? [skip_maybe_atom, child] : child
|
|
208
|
+
end
|
|
209
|
+
rebuilt = kids.length == 1 ? kids.first : Atoms::Sequence.new(*kids)
|
|
210
|
+
Atoms::Repetition.new(rebuilt, atom.min, atom.max, atom.result_tag)
|
|
189
211
|
else
|
|
190
|
-
|
|
191
|
-
atom.result_tag)
|
|
212
|
+
atom
|
|
192
213
|
end
|
|
193
214
|
when Atoms::Named
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
|
|
215
|
+
# Descend, keeping the capture's span clean: injected skips
|
|
216
|
+
# are Ignored-wrapped, so the bytes they consume never join
|
|
217
|
+
# span-joined captures. Composites must interleave around
|
|
218
|
+
# their own children (refs included) — the old pass-through
|
|
219
|
+
# relied on every referenced leaf self-injecting, which
|
|
220
|
+
# `atomic` rules deliberately stop doing.
|
|
221
|
+
Atoms::Named.new(inject_skip_trivia(atom.parslet), atom.name)
|
|
200
222
|
when Atoms::Lookahead
|
|
201
223
|
Atoms::Lookahead.new(inject_skip_trivia(atom.bound_parslet), atom.positive)
|
|
202
224
|
when Atoms::Str, Atoms::Re
|
|
@@ -9,7 +9,8 @@ module Parsanol
|
|
|
9
9
|
|
|
10
10
|
Test = Struct.new(:entry, :kind, :input, :expect)
|
|
11
11
|
|
|
12
|
-
attr_accessor :grammar_name, :version, :source, :uses, :skip_declaration, :skip_source
|
|
12
|
+
attr_accessor :grammar_name, :version, :source, :uses, :skip_declaration, :skip_source,
|
|
13
|
+
:atomic_rules
|
|
13
14
|
attr_reader :rules, :bindings, :preprocess, :entries, :docs, :tests, :render, :derive,
|
|
14
15
|
:customs, :states
|
|
15
16
|
# Entries authored in this file (pre-import); nil-entry tests resolve
|
|
@@ -32,6 +33,10 @@ module Parsanol
|
|
|
32
33
|
@own_entries = nil
|
|
33
34
|
@customs = {}
|
|
34
35
|
@states = {}
|
|
36
|
+
# Rules declared `atomic <name> = ...`: skip-trivia injection is
|
|
37
|
+
# suppressed inside their bodies (token-shaped rules match
|
|
38
|
+
# contiguously).
|
|
39
|
+
@atomic_rules = []
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def validate!
|
data/lib/parsanol/parg/parser.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Parsanol
|
|
|
21
21
|
STATE_KEYWORDS = %w[custom state].freeze
|
|
22
22
|
KEYWORDS = %w[grammar as alt from_table column bindings render derive
|
|
23
23
|
preprocess entry table_lookup custom state set switch
|
|
24
|
-
skip until].freeze
|
|
24
|
+
skip until atomic].freeze
|
|
25
25
|
|
|
26
26
|
def initialize(text)
|
|
27
27
|
@tokens = Lexer.new(text).tokens
|
|
@@ -188,6 +188,11 @@ module Parsanol
|
|
|
188
188
|
skip_newlines
|
|
189
189
|
next
|
|
190
190
|
end
|
|
191
|
+
# `atomic <rule> = ...` suppresses skip-trivia injection inside
|
|
192
|
+
# the rule body: token-shaped rules match their input
|
|
193
|
+
# contiguously instead of spanning trivia between iterations.
|
|
194
|
+
atomic = peek&.type == :ident && peek.value == "atomic"
|
|
195
|
+
advance if atomic
|
|
191
196
|
name = rule_name
|
|
192
197
|
document.docs[name] = docs.join("\n") unless docs.empty?
|
|
193
198
|
punct("=")
|
|
@@ -197,6 +202,7 @@ module Parsanol
|
|
|
197
202
|
end
|
|
198
203
|
|
|
199
204
|
document.rules[name] = node
|
|
205
|
+
document.atomic_rules << name if atomic
|
|
200
206
|
skip_newlines
|
|
201
207
|
end
|
|
202
208
|
punct("}")
|
data/lib/parsanol/version.rb
CHANGED
data/lib/parsanol/vm.rb
CHANGED
|
@@ -85,11 +85,26 @@ module Parsanol
|
|
|
85
85
|
MEMO_BUDGET_FACTOR = 4_096
|
|
86
86
|
MEMO_BUDGET_CELLS_FACTOR = 16
|
|
87
87
|
|
|
88
|
+
# Compiled-program and heavy-flag caches hold at most this many
|
|
89
|
+
# grammars. A process rarely uses more than a handful concurrently;
|
|
90
|
+
# the cap keeps worst-case retention explicit and bounded instead
|
|
91
|
+
# of growing with every grammar ever parsed.
|
|
92
|
+
PROGRAM_CACHE_LIMIT = 32
|
|
93
|
+
|
|
88
94
|
class << self
|
|
89
|
-
|
|
95
|
+
# Compiled-program and heavy-flag caches, keyed by the root atom
|
|
96
|
+
# object itself (identity semantics — two structurally identical
|
|
97
|
+
# grammars are distinct entries). This replaces the original
|
|
98
|
+
# object_id-keyed plain Hash, where entries outlived their
|
|
99
|
+
# grammars forever and a recycled object_id could serve a fresh
|
|
100
|
+
# grammar another grammar's program. Holding the object as the
|
|
101
|
+
# key does pin the grammar while cached — bounded by
|
|
102
|
+
# PROGRAM_CACHE_LIMIT, FIFO eviction.
|
|
103
|
+
@programs = {}.compare_by_identity
|
|
104
|
+
@heavy = {}.compare_by_identity
|
|
90
105
|
|
|
91
106
|
# Cached compile keyed by root-atom object identity. Atoms are
|
|
92
|
-
# effectively immutable once constructed;
|
|
107
|
+
# effectively immutable once constructed; nil caches grammars the
|
|
93
108
|
# VM cannot handle so we do not re-walk them on every parse.
|
|
94
109
|
#
|
|
95
110
|
# A grammar whose VM run failed once (unsupported backtracking shape
|
|
@@ -97,29 +112,32 @@ module Parsanol
|
|
|
97
112
|
# attempt entirely — the interpreter result is identical.
|
|
98
113
|
def program_for(atom)
|
|
99
114
|
root = atom.is_a?(Parsanol::Parser) ? atom.root : atom
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
104
|
-
return cached if key?(cache, key)
|
|
115
|
+
cache = (@programs ||= {}.compare_by_identity)
|
|
116
|
+
cached = cache[root]
|
|
117
|
+
return nil if cached.equal?(:fallback)
|
|
118
|
+
return cached if key?(cache, root)
|
|
105
119
|
|
|
106
120
|
# A grammar the compiler cannot compile — e.g. one containing a
|
|
107
121
|
# lazily resolved Entity that a select-first literal index would
|
|
108
122
|
# never select — falls back to the interpreter, the source of
|
|
109
123
|
# truth. The interpreter resolves such entities lazily, so an
|
|
110
124
|
# unselected branch simply never raises.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
program =
|
|
126
|
+
begin
|
|
127
|
+
compile(atom)
|
|
128
|
+
rescue NotImplementedError
|
|
129
|
+
:fallback
|
|
130
|
+
end
|
|
131
|
+
cache[root] = program
|
|
132
|
+
trim_cache(cache)
|
|
133
|
+
program.equal?(:fallback) ? nil : program
|
|
117
134
|
end
|
|
118
135
|
|
|
119
136
|
# Marks a grammar as VM-incompatible after a runtime failure.
|
|
120
137
|
def disable_for!(atom)
|
|
121
138
|
root = atom.is_a?(Parsanol::Parser) ? atom.root : atom
|
|
122
|
-
(@programs ||= {})[root
|
|
139
|
+
(@programs ||= {}.compare_by_identity)[root] = :fallback
|
|
140
|
+
trim_cache(@programs)
|
|
123
141
|
end
|
|
124
142
|
|
|
125
143
|
def key?(cache, key) # rubocop:disable Naming/PredicateMethod -- mirrors Hash#key?
|
|
@@ -131,6 +149,12 @@ module Parsanol
|
|
|
131
149
|
@heavy&.clear
|
|
132
150
|
end
|
|
133
151
|
|
|
152
|
+
# FIFO eviction past the cache limit.
|
|
153
|
+
def trim_cache(cache)
|
|
154
|
+
cache.shift while cache.size > PROGRAM_CACHE_LIMIT
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
134
158
|
# Compiles the grammar rooted at +atom+. Returns the flat program
|
|
135
159
|
# Array, or nil when the grammar uses unsupported atoms.
|
|
136
160
|
def compile(atom)
|
|
@@ -168,8 +192,8 @@ module Parsanol
|
|
|
168
192
|
# included — the seed is recomputed at every compile), so the
|
|
169
193
|
# cold-start never pays the doomed unmemoized exploration.
|
|
170
194
|
if compiler.backtracking_prone
|
|
171
|
-
|
|
172
|
-
(@heavy
|
|
195
|
+
(@heavy ||= {}.compare_by_identity)[root] = true
|
|
196
|
+
trim_cache(@heavy)
|
|
173
197
|
end
|
|
174
198
|
|
|
175
199
|
compiler.to_program
|
|
@@ -194,6 +218,13 @@ module Parsanol
|
|
|
194
218
|
break if walk_branch.nil?
|
|
195
219
|
end
|
|
196
220
|
next if walk_branch.nil?
|
|
221
|
+
# A bare zero-width assertion is a guard, not a content
|
|
222
|
+
# matcher: `&X / rest` commits to its empty match exactly
|
|
223
|
+
# where X succeeds, and `rest` is reachable exactly where X
|
|
224
|
+
# fails — conditional control flow, not accidental shadowing
|
|
225
|
+
# (#137). Both engines already stop repetitions on
|
|
226
|
+
# zero-width iterations, so guard branches cannot loop.
|
|
227
|
+
next if walk_branch.is_a?(Parsanol::Atoms::Lookahead)
|
|
197
228
|
next unless Compiler.new.nullable?(walk_branch)
|
|
198
229
|
|
|
199
230
|
"alternative branch #{idx} (#{branch.inspect}) can match " \
|
|
@@ -287,9 +318,8 @@ module Parsanol
|
|
|
287
318
|
# interpreter.
|
|
288
319
|
def run_for(atom, program, input, consume_all)
|
|
289
320
|
root = atom.is_a?(Parsanol::Parser) ? atom.root : atom
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if heavy[id]
|
|
321
|
+
heavy = (@heavy ||= {}.compare_by_identity)
|
|
322
|
+
if heavy[root]
|
|
293
323
|
result = Executor.new(program, input, consume_all,
|
|
294
324
|
memoize: true).execute
|
|
295
325
|
return BAIL if result.equal?(BUDGET)
|
|
@@ -300,7 +330,8 @@ module Parsanol
|
|
|
300
330
|
result = Executor.new(program, input, consume_all).execute
|
|
301
331
|
if result.equal?(BUDGET) ||
|
|
302
332
|
(result.is_a?(Array) && result.first == :heavy)
|
|
303
|
-
heavy[
|
|
333
|
+
heavy[root] = true
|
|
334
|
+
trim_cache(heavy)
|
|
304
335
|
memo_result = Executor.new(program, input, consume_all,
|
|
305
336
|
memoize: true).execute
|
|
306
337
|
# A heavy naive success already answered; the memo pass only
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.68
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -72,6 +72,7 @@ files:
|
|
|
72
72
|
- lib/parsanol/atoms/sequence.rb
|
|
73
73
|
- lib/parsanol/atoms/state.rb
|
|
74
74
|
- lib/parsanol/atoms/str.rb
|
|
75
|
+
- lib/parsanol/atoms/trivia.rb
|
|
75
76
|
- lib/parsanol/atoms/visitor.rb
|
|
76
77
|
- lib/parsanol/buffer.rb
|
|
77
78
|
- lib/parsanol/builder_callbacks.rb
|