abnf-rust 2.7.0__tar.gz → 2.8.2__tar.gz
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.
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/Cargo.lock +2 -2
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/Cargo.toml +2 -2
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/PKG-INFO +1 -1
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/alternation.rs +2 -2
- abnf_rust-2.8.2/rust/core/src/cache.rs +392 -0
- abnf_rust-2.8.2/rust/core/src/casefold.rs +47 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/concatenation.rs +2 -2
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/core_rules.rs +7 -7
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/lib.rs +2 -2
- abnf_rust-2.8.2/rust/core/src/literal.rs +226 -0
- abnf_rust-2.8.2/rust/core/src/matcher.rs +49 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/meta_grammar.rs +4 -4
- abnf_rust-2.8.2/rust/core/src/node.rs +125 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/option.rs +2 -2
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/parser.rs +17 -2
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/prose.rs +2 -2
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/repetition.rs +34 -17
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/rule.rs +67 -14
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/visitor.rs +88 -80
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/tests/combinators.rs +83 -39
- abnf_rust-2.8.2/rust/core/tests/exclusion.rs +101 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/tests/meta_grammar.rs +30 -18
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/tests/recursion.rs +13 -4
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/bridge.rs +35 -17
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/errors.rs +6 -8
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/external.rs +14 -8
- abnf_rust-2.8.2/rust/pyo3/src/hooks.rs +30 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/iter.rs +9 -7
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/lib.rs +2 -2
- abnf_rust-2.8.2/rust/pyo3/src/nodes.rs +368 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/parsers.rs +86 -92
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/recursion.rs +20 -1
- abnf_rust-2.8.2/rust/pyo3/src/source.rs +166 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/src/abnf_rust/__init__.py +2 -0
- abnf_rust-2.7.0/rust/core/src/cache.rs +0 -254
- abnf_rust-2.7.0/rust/core/src/casefold.rs +0 -29
- abnf_rust-2.7.0/rust/core/src/literal.rs +0 -329
- abnf_rust-2.7.0/rust/core/src/matcher.rs +0 -67
- abnf_rust-2.7.0/rust/core/src/node.rs +0 -86
- abnf_rust-2.7.0/rust/pyo3/src/hooks.rs +0 -21
- abnf_rust-2.7.0/rust/pyo3/src/nodes.rs +0 -341
- abnf_rust-2.7.0/rust/pyo3/src/offset.rs +0 -99
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/LICENSE.txt +0 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/README.md +0 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/pyproject.toml +0 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/Cargo.toml +0 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/error.rs +0 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/registry.rs +0 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/Cargo.toml +0 -0
- {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/bootstrap.rs +0 -0
|
@@ -4,7 +4,7 @@ version = 3
|
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "abnf-core"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.8.2"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"caseless",
|
|
10
10
|
"lru",
|
|
@@ -14,7 +14,7 @@ dependencies = [
|
|
|
14
14
|
|
|
15
15
|
[[package]]
|
|
16
16
|
name = "abnf-rust-ext"
|
|
17
|
-
version = "2.
|
|
17
|
+
version = "2.8.2"
|
|
18
18
|
dependencies = [
|
|
19
19
|
"abnf-core",
|
|
20
20
|
"mimalloc",
|
|
@@ -6,7 +6,7 @@ resolver = "2"
|
|
|
6
6
|
# Lock-stepped with the `abnf` distribution; both packages release
|
|
7
7
|
# under the same git tag so the wheels at `pip install abnf==X` and
|
|
8
8
|
# `pip install abnf-rust==X` always come from the same source state.
|
|
9
|
-
version = "2.
|
|
9
|
+
version = "2.8.2"
|
|
10
10
|
edition = "2021"
|
|
11
11
|
rust-version = "1.75"
|
|
12
12
|
license = "MIT"
|
|
@@ -14,7 +14,7 @@ authors = ["Charles Yeomans <charles@declaresub.com>"]
|
|
|
14
14
|
repository = "https://github.com/declaresub/abnf"
|
|
15
15
|
|
|
16
16
|
[workspace.dependencies]
|
|
17
|
-
abnf-core = { path = "rust/core", version = "2.
|
|
17
|
+
abnf-core = { path = "rust/core", version = "2.8.2" }
|
|
18
18
|
caseless = "0.2"
|
|
19
19
|
lru = "0.18"
|
|
20
20
|
mimalloc = { version = "0.1", default-features = false }
|
|
@@ -16,7 +16,7 @@ use smallvec::SmallVec;
|
|
|
16
16
|
|
|
17
17
|
use crate::concatenation::sort_by_longest;
|
|
18
18
|
use crate::error::ParseError;
|
|
19
|
-
use crate::parser::{ArcParser, MatchList, ParseResult};
|
|
19
|
+
use crate::parser::{ArcParser, MatchList, ParseResult, Src};
|
|
20
20
|
|
|
21
21
|
#[derive(Debug)]
|
|
22
22
|
pub struct Alternation {
|
|
@@ -47,7 +47,7 @@ impl Alternation {
|
|
|
47
47
|
self.first_match.store(value, Ordering::Relaxed);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
pub fn lparse(&self, source:
|
|
50
|
+
pub fn lparse(&self, source: Src<'_>, start: usize) -> ParseResult {
|
|
51
51
|
let mut all: MatchList = SmallVec::new();
|
|
52
52
|
let mut found = false;
|
|
53
53
|
let first_match = self.first_match();
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
//! `ParseCache` — cache used by `Repetition`.
|
|
2
|
+
//!
|
|
3
|
+
//! Mirrors `abnf.parser.ParseCache`: stores either the match-set
|
|
4
|
+
//! produced at a given start position or the `ParseError` raised
|
|
5
|
+
//! there; on a cache hit `Repetition` short-circuits the recursive
|
|
6
|
+
//! work.
|
|
7
|
+
//!
|
|
8
|
+
//! Two backing implementations behind a single interface:
|
|
9
|
+
//!
|
|
10
|
+
//! * `max_size = None` (Python default) → unbounded `HashMap`.
|
|
11
|
+
//! * `max_size = Some(n)` → `LruCache` with capacity `n`.
|
|
12
|
+
//!
|
|
13
|
+
//! Entries are scoped to a single parse, and exist only while one is
|
|
14
|
+
//! in progress: without a `ParseScope` the cache is inert. A `ParseScope` guard at the
|
|
15
|
+
//! FFI boundary bumps a thread-local epoch on entry to the outermost
|
|
16
|
+
//! `lparse`, and a cache whose stored epoch differs from the current
|
|
17
|
+
//! one is empty by definition. One parse sees one source, so `start`
|
|
18
|
+
//! alone identifies a position and nothing has to be inferred about
|
|
19
|
+
//! the source itself.
|
|
20
|
+
//!
|
|
21
|
+
//! This replaces an earlier scheme that remembered the source's
|
|
22
|
+
//! `(pointer, length)` and a sampled content fingerprint. That
|
|
23
|
+
//! sampled only the first and last 64 bytes, so two sources of equal
|
|
24
|
+
//! length differing anywhere in between — with the second landing at
|
|
25
|
+
//! the freed address of the first, which CPython's allocator does
|
|
26
|
+
//! routinely — compared equal and the second parse silently reused
|
|
27
|
+
//! the first one's matches.
|
|
28
|
+
|
|
29
|
+
use std::cell::Cell;
|
|
30
|
+
use std::collections::HashMap;
|
|
31
|
+
use std::num::NonZeroUsize;
|
|
32
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
33
|
+
|
|
34
|
+
use lru::LruCache;
|
|
35
|
+
|
|
36
|
+
use crate::error::ParseError;
|
|
37
|
+
use crate::parser::MatchList;
|
|
38
|
+
|
|
39
|
+
#[derive(Debug, Clone)]
|
|
40
|
+
pub enum CachedResult {
|
|
41
|
+
Matches(MatchList),
|
|
42
|
+
Failed(ParseError),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[derive(Debug)]
|
|
46
|
+
enum Backing {
|
|
47
|
+
Unbounded(HashMap<usize, CachedResult>),
|
|
48
|
+
Bounded(LruCache<usize, CachedResult>),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl Backing {
|
|
52
|
+
fn get(&mut self, key: usize) -> Option<CachedResult> {
|
|
53
|
+
match self {
|
|
54
|
+
Backing::Unbounded(m) => m.get(&key).cloned(),
|
|
55
|
+
Backing::Bounded(c) => c.get(&key).cloned(),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn put(&mut self, key: usize, value: CachedResult) {
|
|
60
|
+
match self {
|
|
61
|
+
Backing::Unbounded(m) => {
|
|
62
|
+
m.insert(key, value);
|
|
63
|
+
}
|
|
64
|
+
Backing::Bounded(c) => {
|
|
65
|
+
c.put(key, value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Reset to an empty backing. Avoids `HashMap::clear`'s
|
|
71
|
+
/// O(capacity) cost — large caches from a previous big-source
|
|
72
|
+
/// parse would otherwise impose a measurable cost on every
|
|
73
|
+
/// subsequent small parse that triggers invalidation.
|
|
74
|
+
fn reset(&mut self) {
|
|
75
|
+
match self {
|
|
76
|
+
Backing::Unbounded(m) => *m = HashMap::new(),
|
|
77
|
+
Backing::Bounded(c) => {
|
|
78
|
+
let cap = c.cap();
|
|
79
|
+
*c = LruCache::new(cap);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn len(&self) -> usize {
|
|
85
|
+
match self {
|
|
86
|
+
Backing::Unbounded(m) => m.len(),
|
|
87
|
+
Backing::Bounded(c) => c.len(),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#[derive(Debug)]
|
|
93
|
+
pub struct ParseCache {
|
|
94
|
+
inner: Backing,
|
|
95
|
+
/// Epoch these entries belong to. `0` means "never used"; live
|
|
96
|
+
/// epochs start at 1, so a fresh cache never matches.
|
|
97
|
+
epoch: u64,
|
|
98
|
+
pub hits: u64,
|
|
99
|
+
pub misses: u64,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
impl ParseCache {
|
|
103
|
+
pub fn new(max_size: Option<usize>) -> Self {
|
|
104
|
+
let inner = match max_size.and_then(NonZeroUsize::new) {
|
|
105
|
+
Some(cap) => Backing::Bounded(LruCache::new(cap)),
|
|
106
|
+
None => Backing::Unbounded(HashMap::new()),
|
|
107
|
+
};
|
|
108
|
+
Self {
|
|
109
|
+
inner,
|
|
110
|
+
epoch: 0,
|
|
111
|
+
hits: 0,
|
|
112
|
+
misses: 0,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Discard entries belonging to an earlier parse. Returns false
|
|
117
|
+
/// when there is no parse in progress, in which case the cache
|
|
118
|
+
/// must not be used at all: outside a `ParseScope` there is no
|
|
119
|
+
/// boundary to scope entries to, so retaining them across calls
|
|
120
|
+
/// would be the cross-source staleness this design removes.
|
|
121
|
+
fn bind(&mut self) -> bool {
|
|
122
|
+
if !in_parse() {
|
|
123
|
+
if self.epoch != 0 {
|
|
124
|
+
self.inner.reset();
|
|
125
|
+
self.epoch = 0;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
let now = current_epoch();
|
|
130
|
+
if self.epoch != now {
|
|
131
|
+
self.inner.reset();
|
|
132
|
+
self.epoch = now;
|
|
133
|
+
}
|
|
134
|
+
true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pub fn get(&mut self, start: usize) -> Option<CachedResult> {
|
|
138
|
+
if !self.bind() {
|
|
139
|
+
self.misses += 1;
|
|
140
|
+
return None;
|
|
141
|
+
}
|
|
142
|
+
if let Some(v) = self.inner.get(start) {
|
|
143
|
+
self.hits += 1;
|
|
144
|
+
Some(v)
|
|
145
|
+
} else {
|
|
146
|
+
self.misses += 1;
|
|
147
|
+
None
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
pub fn put(&mut self, start: usize, value: CachedResult) {
|
|
152
|
+
if !self.bind() {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
self.inner.put(start, value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
pub fn clear(&mut self) {
|
|
159
|
+
self.inner.reset();
|
|
160
|
+
self.epoch = 0;
|
|
161
|
+
self.hits = 0;
|
|
162
|
+
self.misses = 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
pub fn len(&self) -> usize {
|
|
166
|
+
self.inner.len()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
pub fn is_empty(&self) -> bool {
|
|
170
|
+
self.inner.len() == 0
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
impl Default for ParseCache {
|
|
175
|
+
fn default() -> Self {
|
|
176
|
+
Self::new(None)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Source of epoch numbers, shared by every thread.
|
|
181
|
+
///
|
|
182
|
+
/// It has to be global, not thread-local. A `Repetition` -- and so
|
|
183
|
+
/// its cache -- is shared across threads behind a `Mutex`, while a
|
|
184
|
+
/// parse belongs to one thread. With a per-thread counter, two
|
|
185
|
+
/// threads would independently reach epoch N, and the second would
|
|
186
|
+
/// find entries stamped N by the first and treat them as its own:
|
|
187
|
+
/// the same cross-source corruption this design removes, arriving by
|
|
188
|
+
/// a different route. A global counter makes every parse in the
|
|
189
|
+
/// process distinguishable. `Relaxed` suffices -- uniqueness is the
|
|
190
|
+
/// only requirement, and the `Mutex` around the cache provides the
|
|
191
|
+
/// happens-before edge for the entries themselves.
|
|
192
|
+
static EPOCH_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
193
|
+
|
|
194
|
+
thread_local! {
|
|
195
|
+
/// Epoch of the parse this thread is currently running, claimed
|
|
196
|
+
/// from `EPOCH_COUNTER` on entry to the outermost `lparse`.
|
|
197
|
+
static CURRENT_EPOCH: Cell<u64> = const { Cell::new(0) };
|
|
198
|
+
/// Nesting depth, so only the *outermost* entry claims a new
|
|
199
|
+
/// epoch. A `PyCallbackParser` that re-enters the engine is part
|
|
200
|
+
/// of the same parse and must keep using the same entries.
|
|
201
|
+
static PARSE_DEPTH: Cell<u32> = const { Cell::new(0) };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Marks the dynamic extent of one parse. Construct at the FFI
|
|
205
|
+
/// boundary; entries cached inside it are discarded once a later
|
|
206
|
+
/// parse begins.
|
|
207
|
+
pub struct ParseScope {
|
|
208
|
+
_private: (),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
impl ParseScope {
|
|
212
|
+
pub fn enter() -> Self {
|
|
213
|
+
PARSE_DEPTH.with(|depth| {
|
|
214
|
+
let current = depth.get();
|
|
215
|
+
if current == 0 {
|
|
216
|
+
// `fetch_add` returns the previous value; epochs start
|
|
217
|
+
// at 1 so that a never-used cache (epoch 0) never
|
|
218
|
+
// matches a live parse.
|
|
219
|
+
let claimed = EPOCH_COUNTER.fetch_add(1, Ordering::Relaxed) + 1;
|
|
220
|
+
CURRENT_EPOCH.with(|epoch| epoch.set(claimed));
|
|
221
|
+
}
|
|
222
|
+
depth.set(current + 1);
|
|
223
|
+
});
|
|
224
|
+
Self { _private: () }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
impl Drop for ParseScope {
|
|
229
|
+
fn drop(&mut self) {
|
|
230
|
+
PARSE_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// Marks a sub-parse over a *different* source inside the current
|
|
235
|
+
/// parse: rule exclusion re-parses a matched span against the
|
|
236
|
+
/// excluded rule.
|
|
237
|
+
///
|
|
238
|
+
/// Cache entries are keyed by position and scoped by epoch, which
|
|
239
|
+
/// assumes one epoch sees one source. A sub-parse over other text
|
|
240
|
+
/// therefore has to claim its own epoch, or its positions would
|
|
241
|
+
/// collide with the enclosing parse's. The outer epoch is restored
|
|
242
|
+
/// on drop; caches the sub-parse touched are reset when the outer
|
|
243
|
+
/// parse next reaches them, so it costs hit rate in grammars that use
|
|
244
|
+
/// exclusions and nothing at all in grammars that do not.
|
|
245
|
+
pub struct SourceScope {
|
|
246
|
+
previous: u64,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
impl SourceScope {
|
|
250
|
+
pub fn enter() -> Self {
|
|
251
|
+
let previous = CURRENT_EPOCH.with(Cell::get);
|
|
252
|
+
let claimed = EPOCH_COUNTER.fetch_add(1, Ordering::Relaxed) + 1;
|
|
253
|
+
CURRENT_EPOCH.with(|epoch| epoch.set(claimed));
|
|
254
|
+
Self { previous }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
impl Drop for SourceScope {
|
|
259
|
+
fn drop(&mut self) {
|
|
260
|
+
CURRENT_EPOCH.with(|epoch| epoch.set(self.previous));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// The epoch currently being parsed under.
|
|
265
|
+
pub fn current_epoch() -> u64 {
|
|
266
|
+
CURRENT_EPOCH.with(Cell::get)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// Whether a parse is in progress on this thread. Callers using
|
|
270
|
+
/// `abnf-core` directly, without a `ParseScope`, get no memoisation:
|
|
271
|
+
/// there is no boundary that would make entries safe to keep.
|
|
272
|
+
pub fn in_parse() -> bool {
|
|
273
|
+
PARSE_DEPTH.with(Cell::get) > 0
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#[cfg(test)]
|
|
277
|
+
mod tests {
|
|
278
|
+
use super::*;
|
|
279
|
+
use crate::error::ParseError;
|
|
280
|
+
use std::sync::Arc;
|
|
281
|
+
|
|
282
|
+
fn marker(label: &str) -> CachedResult {
|
|
283
|
+
CachedResult::Failed(ParseError::new(Arc::<str>::from(label), 0))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/// Entries must not survive into the next parse. Under the old
|
|
287
|
+
/// source-fingerprint scheme this was the corruption case: two
|
|
288
|
+
/// sources with identical 64-byte head and tail, the second at the
|
|
289
|
+
/// freed address of the first, compared equal and shared entries.
|
|
290
|
+
/// Scoping to an epoch makes the source irrelevant.
|
|
291
|
+
#[test]
|
|
292
|
+
fn entries_do_not_cross_parse_boundaries() {
|
|
293
|
+
let mut cache = ParseCache::new(None);
|
|
294
|
+
|
|
295
|
+
{
|
|
296
|
+
let _scope = ParseScope::enter();
|
|
297
|
+
cache.put(0, marker("first parse"));
|
|
298
|
+
assert!(cache.get(0).is_some(), "entry visible within its own parse");
|
|
299
|
+
}
|
|
300
|
+
{
|
|
301
|
+
let _scope = ParseScope::enter();
|
|
302
|
+
assert!(
|
|
303
|
+
cache.get(0).is_none(),
|
|
304
|
+
"an entry from a finished parse leaked into the next one"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/// Within one parse, entries are reused -- that is the whole point.
|
|
310
|
+
#[test]
|
|
311
|
+
fn entries_are_reused_within_one_parse() {
|
|
312
|
+
let mut cache = ParseCache::new(None);
|
|
313
|
+
let _scope = ParseScope::enter();
|
|
314
|
+
cache.put(7, marker("same parse"));
|
|
315
|
+
assert!(cache.get(7).is_some());
|
|
316
|
+
assert_eq!(cache.hits, 1);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/// A nested entry -- a `PyCallbackParser` re-entering the engine --
|
|
320
|
+
/// is part of the same parse and must not start a new epoch.
|
|
321
|
+
#[test]
|
|
322
|
+
fn nested_scopes_share_one_epoch() {
|
|
323
|
+
let mut cache = ParseCache::new(None);
|
|
324
|
+
let _outer = ParseScope::enter();
|
|
325
|
+
cache.put(3, marker("outer"));
|
|
326
|
+
{
|
|
327
|
+
let _inner = ParseScope::enter();
|
|
328
|
+
assert!(
|
|
329
|
+
cache.get(3).is_some(),
|
|
330
|
+
"a nested lparse started a new epoch and discarded the outer parse's work"
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
assert!(
|
|
334
|
+
cache.get(3).is_some(),
|
|
335
|
+
"outer entries survive the nested scope"
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/// Epochs must be unique across threads, not just within one.
|
|
340
|
+
/// The cache is shared behind a `Mutex` while a parse belongs to a
|
|
341
|
+
/// single thread, so a per-thread counter would let two threads
|
|
342
|
+
/// both reach epoch N and read each other's entries.
|
|
343
|
+
#[test]
|
|
344
|
+
fn epochs_are_unique_across_threads() {
|
|
345
|
+
use std::collections::HashSet;
|
|
346
|
+
use std::sync::mpsc;
|
|
347
|
+
|
|
348
|
+
let (tx, rx) = mpsc::channel();
|
|
349
|
+
let handles: Vec<_> = (0..8)
|
|
350
|
+
.map(|_| {
|
|
351
|
+
let tx = tx.clone();
|
|
352
|
+
std::thread::spawn(move || {
|
|
353
|
+
for _ in 0..50 {
|
|
354
|
+
let _scope = ParseScope::enter();
|
|
355
|
+
tx.send(current_epoch()).unwrap();
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
.collect();
|
|
360
|
+
drop(tx);
|
|
361
|
+
for h in handles {
|
|
362
|
+
h.join().unwrap();
|
|
363
|
+
}
|
|
364
|
+
let seen: Vec<u64> = rx.iter().collect();
|
|
365
|
+
let unique: HashSet<u64> = seen.iter().copied().collect();
|
|
366
|
+
assert_eq!(
|
|
367
|
+
seen.len(),
|
|
368
|
+
unique.len(),
|
|
369
|
+
"two parses shared an epoch; entries would cross between them"
|
|
370
|
+
);
|
|
371
|
+
assert!(!unique.contains(&0), "epoch 0 means never parsed");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/// A cache that has never seen a parse holds nothing.
|
|
375
|
+
#[test]
|
|
376
|
+
fn fresh_cache_is_empty_under_a_new_epoch() {
|
|
377
|
+
let mut cache = ParseCache::new(None);
|
|
378
|
+
let _scope = ParseScope::enter();
|
|
379
|
+
assert!(cache.get(0).is_none());
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/// Outside a `ParseScope` there is no parse boundary, so the cache
|
|
383
|
+
/// must not retain anything: a direct `abnf-core` caller would
|
|
384
|
+
/// otherwise see one call's results answer the next one's lookup.
|
|
385
|
+
#[test]
|
|
386
|
+
fn cache_is_inert_outside_a_parse_scope() {
|
|
387
|
+
let mut cache = ParseCache::new(None);
|
|
388
|
+
cache.put(0, marker("no scope"));
|
|
389
|
+
assert!(cache.get(0).is_none());
|
|
390
|
+
assert_eq!(cache.len(), 0);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//! Case-folding helpers.
|
|
2
|
+
//!
|
|
3
|
+
//! Two different questions, deliberately kept apart:
|
|
4
|
+
//!
|
|
5
|
+
//! * [`ascii_fold_cp`] folds literals, over US-ASCII only. RFC 5234 §2.3
|
|
6
|
+
//! makes literals case-insensitive and says their character set is
|
|
7
|
+
//! US-ASCII, so nothing outside ASCII takes part. It is also
|
|
8
|
+
//! length-preserving, which full folding is not.
|
|
9
|
+
//! * [`casefold`] matches Python's `str.casefold()` via the `caseless`
|
|
10
|
+
//! crate, and is used for *rule names*, where both backends have
|
|
11
|
+
//! always agreed on full folding.
|
|
12
|
+
|
|
13
|
+
use caseless::Caseless;
|
|
14
|
+
|
|
15
|
+
/// Fold one code point over US-ASCII only: `A-Z` map to `a-z`,
|
|
16
|
+
/// everything else is left exactly as it is -- including surrogates,
|
|
17
|
+
/// which are ordinary values here. Length-preserving by construction,
|
|
18
|
+
/// since it maps one code point to one code point.
|
|
19
|
+
#[inline]
|
|
20
|
+
pub fn ascii_fold_cp(cp: u32) -> u32 {
|
|
21
|
+
if cp.wrapping_sub(u32::from(b'A')) < 26 {
|
|
22
|
+
cp + 32
|
|
23
|
+
} else {
|
|
24
|
+
cp
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Returns `s.casefold()` as a new `String`. ASCII-only input takes a
|
|
29
|
+
/// fast path that just lowercases ASCII bytes.
|
|
30
|
+
pub fn casefold(s: &str) -> String {
|
|
31
|
+
if s.is_ascii() {
|
|
32
|
+
s.to_ascii_lowercase()
|
|
33
|
+
} else {
|
|
34
|
+
s.chars().default_case_fold().collect()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Returns `true` iff `a` and `b` are equal after case-folding. Avoids
|
|
39
|
+
/// allocating intermediate `String`s when both inputs are ASCII.
|
|
40
|
+
#[allow(dead_code)]
|
|
41
|
+
pub fn casefold_eq(a: &str, b: &str) -> bool {
|
|
42
|
+
if a.is_ascii() && b.is_ascii() {
|
|
43
|
+
a.eq_ignore_ascii_case(b)
|
|
44
|
+
} else {
|
|
45
|
+
a.chars().default_case_fold().eq(b.chars().default_case_fold())
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -6,7 +6,7 @@ use smallvec::{smallvec, SmallVec};
|
|
|
6
6
|
|
|
7
7
|
use crate::error::ParseError;
|
|
8
8
|
use crate::matcher::Match;
|
|
9
|
-
use crate::parser::{ArcParser, MatchList, NodeList, ParseResult};
|
|
9
|
+
use crate::parser::{ArcParser, MatchList, NodeList, ParseResult, Src};
|
|
10
10
|
|
|
11
11
|
#[derive(Debug)]
|
|
12
12
|
pub struct Concatenation {
|
|
@@ -18,7 +18,7 @@ impl Concatenation {
|
|
|
18
18
|
Self { parsers }
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
pub fn lparse(&self, source:
|
|
21
|
+
pub fn lparse(&self, source: Src<'_>, start: usize) -> ParseResult {
|
|
22
22
|
let mut match_list: MatchList = smallvec![Match::new(SmallVec::new(), start)];
|
|
23
23
|
for parser in &self.parsers {
|
|
24
24
|
let mut next: MatchList = SmallVec::new();
|
|
@@ -17,7 +17,7 @@ fn lit_ci(value: &str) -> ArcParser {
|
|
|
17
17
|
Literal::string(value, false).into()
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
fn range(lo:
|
|
20
|
+
fn range(lo: u32, hi: u32) -> ArcParser {
|
|
21
21
|
Literal::range(lo, hi).into()
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -26,7 +26,7 @@ pub fn install_core_rules(registry: &mut RuleRegistry) {
|
|
|
26
26
|
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
|
27
27
|
registry.define(
|
|
28
28
|
"ALPHA",
|
|
29
|
-
Alternation::new(vec![range(
|
|
29
|
+
Alternation::new(vec![range(0x41, 0x5A), range(0x61, 0x7A)]).into(),
|
|
30
30
|
);
|
|
31
31
|
|
|
32
32
|
// BIT = "0" / "1"
|
|
@@ -36,12 +36,12 @@ pub fn install_core_rules(registry: &mut RuleRegistry) {
|
|
|
36
36
|
);
|
|
37
37
|
|
|
38
38
|
// CHAR = %x01-7F
|
|
39
|
-
registry.define("CHAR", range(
|
|
39
|
+
registry.define("CHAR", range(0x01, 0x7F));
|
|
40
40
|
|
|
41
41
|
// CTL = %x00-1F / %x7F
|
|
42
42
|
registry.define(
|
|
43
43
|
"CTL",
|
|
44
|
-
Alternation::new(vec![range(
|
|
44
|
+
Alternation::new(vec![range(0x00, 0x1F), lit_cs("\x7F")]).into(),
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
// CR = %x0D
|
|
@@ -53,7 +53,7 @@ pub fn install_core_rules(registry: &mut RuleRegistry) {
|
|
|
53
53
|
registry.define("CRLF", Concatenation::new(vec![cr, lf_ref]).into());
|
|
54
54
|
|
|
55
55
|
// DIGIT = %x30-39
|
|
56
|
-
registry.define("DIGIT", range(
|
|
56
|
+
registry.define("DIGIT", range(0x30, 0x39));
|
|
57
57
|
|
|
58
58
|
// DQUOTE = %x22
|
|
59
59
|
registry.define("DQUOTE", lit_cs("\x22"));
|
|
@@ -81,13 +81,13 @@ pub fn install_core_rules(registry: &mut RuleRegistry) {
|
|
|
81
81
|
registry.define("LF", lit_cs("\x0A"));
|
|
82
82
|
|
|
83
83
|
// OCTET = %x00-FF
|
|
84
|
-
registry.define("OCTET", range(
|
|
84
|
+
registry.define("OCTET", range(0x00, 0xFF));
|
|
85
85
|
|
|
86
86
|
// SP = %x20
|
|
87
87
|
registry.define("SP", lit_cs("\x20"));
|
|
88
88
|
|
|
89
89
|
// VCHAR = %x21-7E
|
|
90
|
-
registry.define("VCHAR", range(
|
|
90
|
+
registry.define("VCHAR", range(0x21, 0x7E));
|
|
91
91
|
|
|
92
92
|
// WSP = SP / HTAB
|
|
93
93
|
let sp_ref = registry.get_or_create("SP");
|
|
@@ -40,7 +40,7 @@ mod rule;
|
|
|
40
40
|
mod visitor;
|
|
41
41
|
|
|
42
42
|
pub use alternation::Alternation;
|
|
43
|
-
pub use cache::ParseCache;
|
|
43
|
+
pub use cache::{ParseCache, ParseScope, SourceScope};
|
|
44
44
|
pub use concatenation::Concatenation;
|
|
45
45
|
pub use core_rules::install_core_rules;
|
|
46
46
|
pub use error::ParseError;
|
|
@@ -49,7 +49,7 @@ pub use matcher::Match;
|
|
|
49
49
|
pub use meta_grammar::{build_meta_grammar, install_meta_grammar};
|
|
50
50
|
pub use node::{LiteralNode, Node, NodeKind};
|
|
51
51
|
pub use option::OptionParser;
|
|
52
|
-
pub use parser::{arc, ArcParser, ExternalParser, MatchList, NodeList, ParseResult, Parser};
|
|
52
|
+
pub use parser::{arc, ArcParser, ExternalParser, MatchList, NodeList, ParseResult, Parser, Src};
|
|
53
53
|
pub use prose::Prose;
|
|
54
54
|
pub use registry::RuleRegistry;
|
|
55
55
|
pub use repetition::{Repeat, Repetition};
|