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.
Files changed (50) hide show
  1. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/Cargo.lock +2 -2
  2. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/Cargo.toml +2 -2
  3. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/PKG-INFO +1 -1
  4. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/alternation.rs +2 -2
  5. abnf_rust-2.8.2/rust/core/src/cache.rs +392 -0
  6. abnf_rust-2.8.2/rust/core/src/casefold.rs +47 -0
  7. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/concatenation.rs +2 -2
  8. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/core_rules.rs +7 -7
  9. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/lib.rs +2 -2
  10. abnf_rust-2.8.2/rust/core/src/literal.rs +226 -0
  11. abnf_rust-2.8.2/rust/core/src/matcher.rs +49 -0
  12. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/meta_grammar.rs +4 -4
  13. abnf_rust-2.8.2/rust/core/src/node.rs +125 -0
  14. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/option.rs +2 -2
  15. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/parser.rs +17 -2
  16. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/prose.rs +2 -2
  17. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/repetition.rs +34 -17
  18. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/rule.rs +67 -14
  19. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/visitor.rs +88 -80
  20. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/tests/combinators.rs +83 -39
  21. abnf_rust-2.8.2/rust/core/tests/exclusion.rs +101 -0
  22. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/tests/meta_grammar.rs +30 -18
  23. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/tests/recursion.rs +13 -4
  24. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/bridge.rs +35 -17
  25. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/errors.rs +6 -8
  26. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/external.rs +14 -8
  27. abnf_rust-2.8.2/rust/pyo3/src/hooks.rs +30 -0
  28. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/iter.rs +9 -7
  29. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/lib.rs +2 -2
  30. abnf_rust-2.8.2/rust/pyo3/src/nodes.rs +368 -0
  31. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/parsers.rs +86 -92
  32. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/src/recursion.rs +20 -1
  33. abnf_rust-2.8.2/rust/pyo3/src/source.rs +166 -0
  34. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/src/abnf_rust/__init__.py +2 -0
  35. abnf_rust-2.7.0/rust/core/src/cache.rs +0 -254
  36. abnf_rust-2.7.0/rust/core/src/casefold.rs +0 -29
  37. abnf_rust-2.7.0/rust/core/src/literal.rs +0 -329
  38. abnf_rust-2.7.0/rust/core/src/matcher.rs +0 -67
  39. abnf_rust-2.7.0/rust/core/src/node.rs +0 -86
  40. abnf_rust-2.7.0/rust/pyo3/src/hooks.rs +0 -21
  41. abnf_rust-2.7.0/rust/pyo3/src/nodes.rs +0 -341
  42. abnf_rust-2.7.0/rust/pyo3/src/offset.rs +0 -99
  43. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/LICENSE.txt +0 -0
  44. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/README.md +0 -0
  45. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/pyproject.toml +0 -0
  46. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/Cargo.toml +0 -0
  47. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/error.rs +0 -0
  48. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/core/src/registry.rs +0 -0
  49. {abnf_rust-2.7.0 → abnf_rust-2.8.2}/rust/pyo3/Cargo.toml +0 -0
  50. {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.0"
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.7.0"
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.7.0"
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.7.0" }
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 }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: abnf-rust
3
- Version: 2.7.0
3
+ Version: 2.8.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -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: &str, start: usize) -> ParseResult {
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: &str, start: usize) -> ParseResult {
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: char, hi: char) -> ArcParser {
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('\x41', '\x5A'), range('\x61', '\x7A')]).into(),
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('\x01', '\x7F'));
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('\x00', '\x1F'), lit_cs("\x7F")]).into(),
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('\x30', '\x39'));
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('\x00', '\u{00FF}'));
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('\x21', '\x7E'));
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};