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.
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/Cargo.lock +4 -4
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/Cargo.toml +2 -2
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/PKG-INFO +1 -1
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/rule.rs +31 -4
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/nodes.rs +27 -8
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/parsers.rs +80 -19
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/recursion.rs +41 -2
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/LICENSE.txt +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/README.md +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/pyproject.toml +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/Cargo.toml +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/alternation.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/cache.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/casefold.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/concatenation.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/core_rules.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/error.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/lib.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/literal.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/matcher.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/meta_grammar.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/node.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/option.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/parser.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/prose.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/registry.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/repetition.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/src/visitor.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/tests/combinators.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/tests/exclusion.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/tests/meta_grammar.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/core/tests/recursion.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/Cargo.toml +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/bootstrap.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/bridge.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/errors.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/external.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/hooks.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/iter.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/lib.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.8.3}/rust/pyo3/src/source.rs +0 -0
- {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.
|
|
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.
|
|
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.
|
|
103
|
+
version = "0.18.2"
|
|
104
104
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
105
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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 }
|
|
@@ -268,7 +268,17 @@ impl NamedRule {
|
|
|
268
268
|
self.name
|
|
269
269
|
),
|
|
270
270
|
});
|
|
271
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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.
|
|
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::
|
|
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<
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
390
|
-
// `
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
|
|
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
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|