abnf-rust 2.8.2__tar.gz → 2.8.3__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 (42) hide show
  1. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/Cargo.lock +4 -4
  2. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/Cargo.toml +2 -2
  3. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/PKG-INFO +1 -1
  4. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/rule.rs +31 -4
  5. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/nodes.rs +27 -8
  6. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/parsers.rs +80 -19
  7. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/recursion.rs +41 -2
  8. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/LICENSE.txt +0 -0
  9. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/README.md +0 -0
  10. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/pyproject.toml +0 -0
  11. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/Cargo.toml +0 -0
  12. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/alternation.rs +0 -0
  13. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/cache.rs +0 -0
  14. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/casefold.rs +0 -0
  15. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/concatenation.rs +0 -0
  16. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/core_rules.rs +0 -0
  17. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/error.rs +0 -0
  18. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/lib.rs +0 -0
  19. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/literal.rs +0 -0
  20. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/matcher.rs +0 -0
  21. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/meta_grammar.rs +0 -0
  22. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/node.rs +0 -0
  23. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/option.rs +0 -0
  24. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/parser.rs +0 -0
  25. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/prose.rs +0 -0
  26. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/registry.rs +0 -0
  27. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/repetition.rs +0 -0
  28. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/visitor.rs +0 -0
  29. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/tests/combinators.rs +0 -0
  30. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/tests/exclusion.rs +0 -0
  31. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/tests/meta_grammar.rs +0 -0
  32. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/tests/recursion.rs +0 -0
  33. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/Cargo.toml +0 -0
  34. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/bootstrap.rs +0 -0
  35. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/bridge.rs +0 -0
  36. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/errors.rs +0 -0
  37. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/external.rs +0 -0
  38. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/hooks.rs +0 -0
  39. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/iter.rs +0 -0
  40. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/lib.rs +0 -0
  41. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/source.rs +0 -0
  42. {abnf_rust-2.8.2 → abnf_rust-2.8.3}/src/abnf_rust/__init__.py +0 -0
@@ -4,7 +4,7 @@ version = 3
4
4
 
5
5
  [[package]]
6
6
  name = "abnf-core"
7
- version = "2.8.2"
7
+ version = "2.8.3"
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.8.2"
17
+ version = "2.8.3"
18
18
  dependencies = [
19
19
  "abnf-core",
20
20
  "mimalloc",
@@ -100,9 +100,9 @@ dependencies = [
100
100
 
101
101
  [[package]]
102
102
  name = "lru"
103
- version = "0.18.1"
103
+ version = "0.18.2"
104
104
  source = "registry+https://github.com/rust-lang/crates.io-index"
105
- checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6"
105
+ checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a"
106
106
  dependencies = [
107
107
  "hashbrown",
108
108
  ]
@@ -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.8.2"
9
+ version = "2.8.3"
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.8.2" }
17
+ abnf-core = { path = "rust/core", version = "2.8.3" }
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.8.2
3
+ Version: 2.8.3
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -268,7 +268,17 @@ impl NamedRule {
268
268
  self.name
269
269
  ),
270
270
  });
271
- let def = self.definition().ok_or_else(|| self.parse_error(start))?;
271
+ // A rule that was never defined is a broken grammar, not input
272
+ // that failed to match. Returning a `ParseError` here would be
273
+ // indistinguishable from "this alternative did not match", so
274
+ // an enclosing `Alternation` or `Repetition` would swallow it
275
+ // as backtracking and a typo'd rule name would silently delete
276
+ // that branch of the grammar. The pure-Python backend raises
277
+ // `GrammarError`; panic with the phrase the PyO3 layer maps to
278
+ // it, the same route `is_excluded` and the recursion guard use.
279
+ let Some(def) = self.definition() else {
280
+ panic!("Undefined rule \"{}\"", self.name);
281
+ };
272
282
  let inner = def.lparse(source, start)?;
273
283
 
274
284
  let excluded = self.exclude();
@@ -296,9 +306,26 @@ impl NamedRule {
296
306
  if let Some(ref excluded) = excluded {
297
307
  // `m.start` is the end of the match; the span is what
298
308
  // the Python side rebuilds by joining node values.
299
- if Self::is_excluded(excluded, &source[start..m.start]) {
300
- seen.remove(&m.start);
301
- continue;
309
+ //
310
+ // It is not necessarily a real span. When this rule's
311
+ // definition is a `PyCallbackParser`, the offset comes
312
+ // from a Python parser and can be anything -- past the
313
+ // end of the source, or before `start`. Slicing on it
314
+ // panicked, and a panic crosses the FFI as
315
+ // `PanicException`, a `BaseException` that `except
316
+ // ParseError` (or even `except Exception`) does not
317
+ // catch (issue #218).
318
+ //
319
+ // Text that is not in the source cannot be text the
320
+ // excluded rule matches, so a nonsensical span means
321
+ // "not excluded". The bad offset then fails naturally
322
+ // further up, which is what the pure-Python backend
323
+ // does with the same input.
324
+ if let Some(span) = source.get(start..m.start) {
325
+ if Self::is_excluded(excluded, span) {
326
+ seen.remove(&m.start);
327
+ continue;
328
+ }
302
329
  }
303
330
  }
304
331
  let node = Node::new(self.name.clone(), m.nodes.into_vec());
@@ -94,13 +94,16 @@ impl PyLiteralNode {
94
94
  && self.value.bind(py).as_any().eq(other.value.bind(py).as_any())?)
95
95
  }
96
96
 
97
- fn __hash__(&self, py: Python<'_>) -> PyResult<isize> {
98
- // Hash on the same fields `__eq__` compares, and let Python
99
- // hash the text so the two agree for every string it can hold.
100
- let mut h = self.value.bind(py).as_any().hash()?;
101
- h = h.wrapping_mul(1_000_003).wrapping_add(self.offset as isize);
102
- h = h.wrapping_mul(1_000_003).wrapping_add(self.length as isize);
103
- Ok(h)
97
+ /// Deliberately absent: defining `__eq__` without `__hash__`
98
+ /// makes a class unhashable in Python, and the pure-Python
99
+ /// `LiteralNode` does exactly that. `Node` is unhashable on both
100
+ /// backends too, so no parse-tree node is hashable anywhere.
101
+ /// Adding one here made `set(literal_nodes)` work under the Rust
102
+ /// backend and raise `TypeError` under the other.
103
+ #[classattr]
104
+ #[pyo3(name = "__hash__")]
105
+ fn __hash__() -> Option<Py<PyAny>> {
106
+ None
104
107
  }
105
108
  }
106
109
 
@@ -167,8 +170,24 @@ impl PyNode {
167
170
  }
168
171
 
169
172
  fn __richcmp__(&self, py: Python<'_>, other: &Self, op: CompareOp) -> PyResult<bool> {
173
+ // Structural, as the pure-Python `Node.__eq__` is: name and
174
+ // children, compared recursively. Comparing concatenated
175
+ // values instead called two different parse trees equal
176
+ // whenever they spanned the same text -- `"xy" / ("x" "y")`
177
+ // against `("x" "y") / "xy"`, say -- which silently changed
178
+ // what a user's assertions meant depending on the backend.
170
179
  let eq = self.name == other.name
171
- && self.value.bind(py).as_any().eq(other.value.bind(py).as_any())?;
180
+ && self.children.len() == other.children.len()
181
+ && {
182
+ let mut equal = true;
183
+ for (a, b) in self.children.iter().zip(other.children.iter()) {
184
+ if !a.bind(py).eq(b.bind(py))? {
185
+ equal = false;
186
+ break;
187
+ }
188
+ }
189
+ equal
190
+ };
172
191
  Ok(match op {
173
192
  CompareOp::Eq => eq,
174
193
  CompareOp::Ne => !eq,
@@ -8,7 +8,8 @@ use std::sync::Arc;
8
8
 
9
9
  use pyo3::exceptions::PyTypeError;
10
10
  use pyo3::prelude::*;
11
- use pyo3::types::{PyString, PyTuple};
11
+ use pyo3::sync::PyOnceLock;
12
+ use pyo3::types::{PyInt, PyString, PyTuple, PyType};
12
13
 
13
14
  use abnf_core::{
14
15
  arc, Alternation, ArcParser, Concatenation, Literal, LiteralKind, OptionParser, Parser, Prose,
@@ -38,7 +39,39 @@ pub struct PyRepeat {
38
39
  impl PyRepeat {
39
40
  #[new]
40
41
  #[pyo3(signature = (min=0, max=None))]
41
- fn new(py: Python<'_>, min: usize, max: Option<usize>) -> PyResult<Self> {
42
+ fn new(py: Python<'_>, min: usize, max: Option<&Bound<'_, PyAny>>) -> PyResult<Self> {
43
+ // A bound too large for `usize` saturates rather than raising.
44
+ // Python's ints are unbounded, so `2*99999999999999999999"x"`
45
+ // is odd but valid ABNF that the pure-Python backend parses
46
+ // happily -- the bound is simply never reached. Raising
47
+ // `OverflowError` here made the two backends disagree, and
48
+ // `OverflowError` is neither `GrammarError` nor `ParseError`,
49
+ // so it escaped the documented exception contract as well.
50
+ // No input can reach `usize::MAX` repetitions, so saturating
51
+ // is indistinguishable from the unbounded value.
52
+ let max: Option<usize> = match max {
53
+ None => None,
54
+ Some(obj) if obj.is_none() => None,
55
+ Some(obj) => match obj.extract::<usize>() {
56
+ Ok(value) => Some(value),
57
+ // Not an integer at all: report that, not a bound problem.
58
+ Err(err) if obj.cast::<PyInt>().is_err() => return Err(err),
59
+ // An integer outside `usize`. Negative falls through to
60
+ // the `max < min` check below, exactly as in Python;
61
+ // anything else is bigger than any input can reach.
62
+ Err(_) if obj.lt(0i64)? => {
63
+ let msg = format!(
64
+ "Repeat max ({}) is less than min ({min}); \
65
+ a repetition of {min}*{} can never match.",
66
+ obj.str()?,
67
+ obj.str()?
68
+ );
69
+ return Err(crate::errors::grammar_error(py, &msg));
70
+ }
71
+ Err(_) => Some(usize::MAX),
72
+ },
73
+ };
74
+
42
75
  // Mirrors the check in the pure-Python `Repeat.__init__`:
43
76
  // `3*2` is an impossible range, and leaving it unvalidated
44
77
  // silently behaves as `3*`. Both backends must reject it
@@ -97,7 +130,9 @@ impl PyAlternation {
97
130
  start: usize,
98
131
  ) -> PyResult<Py<LparseIter>> {
99
132
  let cps = CodePoints::new(source)?;
100
- let result = crate::recursion::call_lparse(|| self.inner.lparse(cps.as_slice(), start))?;
133
+ let result = crate::recursion::call_lparse(source.as_ptr() as usize, || {
134
+ self.inner.lparse(cps.as_slice(), start)
135
+ })?;
101
136
  lparse_iter(py, result, source)
102
137
  }
103
138
 
@@ -150,7 +185,9 @@ impl PyConcatenation {
150
185
  start: usize,
151
186
  ) -> PyResult<Py<LparseIter>> {
152
187
  let cps = CodePoints::new(source)?;
153
- let result = crate::recursion::call_lparse(|| self.inner.lparse(cps.as_slice(), start))?;
188
+ let result = crate::recursion::call_lparse(source.as_ptr() as usize, || {
189
+ self.inner.lparse(cps.as_slice(), start)
190
+ })?;
154
191
  lparse_iter(py, result, source)
155
192
  }
156
193
 
@@ -186,7 +223,9 @@ impl PyRepetition {
186
223
  start: usize,
187
224
  ) -> PyResult<Py<LparseIter>> {
188
225
  let cps = CodePoints::new(source)?;
189
- let result = crate::recursion::call_lparse(|| self.inner.lparse(cps.as_slice(), start))?;
226
+ let result = crate::recursion::call_lparse(source.as_ptr() as usize, || {
227
+ self.inner.lparse(cps.as_slice(), start)
228
+ })?;
190
229
  lparse_iter(py, result, source)
191
230
  }
192
231
 
@@ -222,7 +261,9 @@ impl PyOption {
222
261
  start: usize,
223
262
  ) -> PyResult<Py<LparseIter>> {
224
263
  let cps = CodePoints::new(source)?;
225
- let result = crate::recursion::call_lparse(|| self.inner.lparse(cps.as_slice(), start))?;
264
+ let result = crate::recursion::call_lparse(source.as_ptr() as usize, || {
265
+ self.inner.lparse(cps.as_slice(), start)
266
+ })?;
226
267
  lparse_iter(py, result, source)
227
268
  }
228
269
 
@@ -269,7 +310,11 @@ impl PyLiteral {
269
310
  };
270
311
  return Ok(Self {
271
312
  inner: Literal::range(*lo, *hi).into(),
272
- case_sensitive: true,
313
+ // A range compares by code point either way, so
314
+ // this attribute is inert -- but the pure-Python
315
+ // constructor leaves it at its default, and the
316
+ // two backends should read alike.
317
+ case_sensitive,
273
318
  });
274
319
  }
275
320
  }
@@ -285,7 +330,9 @@ impl PyLiteral {
285
330
  start: usize,
286
331
  ) -> PyResult<Py<LparseIter>> {
287
332
  let cps = CodePoints::new(source)?;
288
- let result = crate::recursion::call_lparse(|| self.inner.lparse(cps.as_slice(), start))?;
333
+ let result = crate::recursion::call_lparse(source.as_ptr() as usize, || {
334
+ self.inner.lparse(cps.as_slice(), start)
335
+ })?;
289
336
  lparse_iter(py, result, source)
290
337
  }
291
338
 
@@ -347,7 +394,9 @@ impl PyProse {
347
394
  start: usize,
348
395
  ) -> PyResult<Py<LparseIter>> {
349
396
  let cps = CodePoints::new(source)?;
350
- match crate::recursion::call_lparse(|| self.inner.lparse(cps.as_slice(), start))? {
397
+ match crate::recursion::call_lparse(source.as_ptr() as usize, || {
398
+ self.inner.lparse(cps.as_slice(), start)
399
+ })? {
351
400
  Ok(_) => lparse_iter(py, Ok(smallvec::SmallVec::new()), source),
352
401
  Err(err) => Err(parse_error_to_pyerr(py, err)),
353
402
  }
@@ -386,19 +435,31 @@ pub fn extract_parser(obj: &Bound<'_, PyAny>) -> PyResult<ArcParser> {
386
435
  if let Ok(p) = obj.cast::<PyProse>() {
387
436
  return Ok(p.borrow().inner.clone());
388
437
  }
389
- // If the value is a Python Rule (or anything else carrying a
390
- // `name`+`lparse` shape that fits the parser-by-name contract),
391
- // look up — or lazily create — its shadow Rust `NamedRule` in
392
- // the bridge registry. This is the fast path that keeps rule
393
- // references purely in Rust at parse time, instead of dispatching
394
- // every reference through Python.
395
- if obj.hasattr("name")? && obj.hasattr("lparse")? {
438
+ // If the value is a Python `Rule`, look up — or lazily create —
439
+ // its shadow Rust `NamedRule` in the bridge registry. This is the
440
+ // fast path that keeps rule references purely in Rust at parse
441
+ // time, instead of dispatching every reference through Python.
442
+ //
443
+ // The test is `isinstance`, not the presence of `name` + `lparse`.
444
+ // Attribute shape caught anything with a `name`, which was wrong
445
+ // twice over (issue #203): such an object became a
446
+ // definition-less `NamedRule` whose own `lparse` was then never
447
+ // called, and it was entered into a registry keyed by the object's
448
+ // *address* — sound only because `Rule._obj_map` makes rules
449
+ // immortal, which is not true of an arbitrary user object. A
450
+ // freed one's address could be handed to a new `Rule`, which then
451
+ // inherited its stale handle; defining that rule wrote into a
452
+ // parser tree built from an unrelated object.
453
+ static RULE_TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
454
+ let rule_type = RULE_TYPE.import(obj.py(), "abnf._parser_python", "Rule")?;
455
+ if obj.is_instance(rule_type)? {
396
456
  let handle = crate::bridge::get_or_create(obj)?;
397
457
  return Ok(arc(handle));
398
458
  }
399
- // Last-resort fallback: any other object with an `lparse` method
400
- // is wrapped as a `PyCallbackParser`. Reserved for callers that
401
- // pass duck-typed parsers without a `name` attribute.
459
+ // Any other object with an `lparse` method is wrapped as a
460
+ // `PyCallbackParser`, which holds a reference to it -- so unlike a
461
+ // bridge entry, it cannot outlive the object it refers to. This
462
+ // is where every duck-typed parser goes, with or without a `name`.
402
463
  if obj.hasattr("lparse")? {
403
464
  let callback = PyCallbackParser::new(obj.clone().unbind(), "PyCallback");
404
465
  let parser: Parser = Parser::External(Arc::new(callback));
@@ -24,7 +24,7 @@
24
24
  //! tags into the right Python exception. Other panics resume so
25
25
  //! they keep surfacing as `PanicException` via PyO3's defaults.
26
26
 
27
- use std::cell::RefCell;
27
+ use std::cell::{Cell, RefCell};
28
28
  use std::panic::{self, AssertUnwindSafe};
29
29
  use std::sync::Once;
30
30
 
@@ -48,6 +48,17 @@ const PYERR_PANIC_TAG: &str = "pycallback-python-error";
48
48
  const UNDEFINED_RULE_TAG: &str = "Undefined rule";
49
49
 
50
50
  thread_local! {
51
+ /// Identity of the source the innermost active parse is running
52
+ /// over: the address of the Python `str` object, which is what
53
+ /// the pure-Python backend compares (`ctx[0] is source`) before
54
+ /// trusting a memo entry. Zero when no parse is active.
55
+ ///
56
+ /// Not the code-point buffer's address: a nested call widens the
57
+ /// same `str` into a different pooled buffer, so that would report
58
+ /// "different source" for every re-entrant call and throw away the
59
+ /// enclosing parse's memo each time.
60
+ static CURRENT_SOURCE_ID: Cell<usize> = const { Cell::new(0) };
61
+
51
62
  /// Holds the most recent `PyErr` raised by a `PyCallbackParser`
52
63
  /// callback that should propagate (i.e. not a `ParseError`).
53
64
  /// Taken by `call_lparse` when it recognises the
@@ -57,6 +68,16 @@ thread_local! {
57
68
 
58
69
  static HOOK_INSTALLED: Once = Once::new();
59
70
 
71
+ /// Restores the enclosing parse's source identity on the way out, so
72
+ /// a nested call cannot leave the wrong one behind.
73
+ struct RestoreSourceId(usize);
74
+
75
+ impl Drop for RestoreSourceId {
76
+ fn drop(&mut self) {
77
+ CURRENT_SOURCE_ID.with(|id| id.set(self.0));
78
+ }
79
+ }
80
+
60
81
  /// Install the panic hook exactly once per process.
61
82
  pub fn install_panic_hook() {
62
83
  HOOK_INSTALLED.call_once(|| {
@@ -99,7 +120,7 @@ pub fn propagate_pyerr(err: PyErr) -> ! {
99
120
  /// PyCallbackParser-stash panic to the saved `PyErr`. Any other
100
121
  /// panic is re-raised so it surfaces normally via PyO3's
101
122
  /// `PanicException`.
102
- pub fn call_lparse<F>(f: F) -> PyResult<abnf_core::ParseResult>
123
+ pub fn call_lparse<F>(source_id: usize, f: F) -> PyResult<abnf_core::ParseResult>
103
124
  where
104
125
  F: FnOnce() -> abnf_core::ParseResult,
105
126
  {
@@ -112,6 +133,24 @@ where
112
133
  // identity. Nested entries -- a `PyCallbackParser` calling back
113
134
  // in -- share the epoch, so intra-parse reuse is preserved.
114
135
  let _scope = abnf_core::ParseScope::enter();
136
+
137
+ // ...but only while they are parsing the same text. A custom
138
+ // parser may call back in on any source it likes, and cache
139
+ // entries are keyed by position alone, so sharing an epoch across
140
+ // two sources let the inner parse answer the outer one's lookups
141
+ // and the outer parse returned a wrong result (issue #202).
142
+ //
143
+ // Compare the `str` object's identity, exactly as the pure-Python
144
+ // memo does, and give a genuinely different source its own epoch.
145
+ // Same-source re-entry -- the common case -- keeps sharing, which
146
+ // matters because an epoch change resets the caches it touches:
147
+ // claiming one unconditionally would wipe the enclosing parse's
148
+ // memo on every callback.
149
+ let previous_source = CURRENT_SOURCE_ID.with(Cell::get);
150
+ let _source_scope = (previous_source != source_id)
151
+ .then(abnf_core::SourceScope::enter);
152
+ CURRENT_SOURCE_ID.with(|id| id.set(source_id));
153
+ let _restore = RestoreSourceId(previous_source);
115
154
  match panic::catch_unwind(AssertUnwindSafe(f)) {
116
155
  Ok(result) => Ok(result),
117
156
  Err(payload) => {
File without changes
File without changes
File without changes