abnf-rust 2.8.2__tar.gz → 2.9.0__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.9.0}/Cargo.lock +4 -4
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/Cargo.toml +2 -2
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/PKG-INFO +1 -1
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/lib.rs +1 -1
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/literal.rs +25 -11
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/node.rs +47 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/rule.rs +31 -4
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/visitor.rs +3 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/pyo3/src/nodes.rs +140 -30
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/pyo3/src/parsers.rs +119 -20
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/pyo3/src/recursion.rs +41 -2
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/LICENSE.txt +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/README.md +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/pyproject.toml +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/Cargo.toml +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/alternation.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/cache.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/casefold.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/concatenation.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/core_rules.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/error.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/matcher.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/meta_grammar.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/option.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/parser.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/prose.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/registry.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/src/repetition.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/tests/combinators.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/tests/exclusion.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/tests/meta_grammar.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/core/tests/recursion.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/pyo3/Cargo.toml +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/pyo3/src/bootstrap.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/pyo3/src/bridge.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/pyo3/src/errors.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/pyo3/src/external.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/pyo3/src/hooks.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/pyo3/src/iter.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/pyo3/src/lib.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/rust/pyo3/src/source.rs +0 -0
- {abnf_rust-2.8.2 → abnf_rust-2.9.0}/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.
|
|
7
|
+
version = "2.9.0"
|
|
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.9.0"
|
|
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.
|
|
9
|
+
version = "2.9.0"
|
|
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.9.0" }
|
|
18
18
|
caseless = "0.2"
|
|
19
19
|
lru = "0.18"
|
|
20
20
|
mimalloc = { version = "0.1", default-features = false }
|
|
@@ -47,7 +47,7 @@ pub use error::ParseError;
|
|
|
47
47
|
pub use literal::{Literal, LiteralKind};
|
|
48
48
|
pub use matcher::Match;
|
|
49
49
|
pub use meta_grammar::{build_meta_grammar, install_meta_grammar};
|
|
50
|
-
pub use node::{LiteralNode, Node, NodeKind};
|
|
50
|
+
pub use node::{ForeignNode, LiteralNode, Node, NodeKind};
|
|
51
51
|
pub use option::OptionParser;
|
|
52
52
|
pub use parser::{arc, ArcParser, ExternalParser, MatchList, NodeList, ParseResult, Parser, Src};
|
|
53
53
|
pub use prose::Prose;
|
|
@@ -126,14 +126,17 @@ impl Literal {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
LiteralKind::String { value, pattern } => {
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
// Enough source must remain for the whole literal. This
|
|
130
|
+
// one check covers both cases: a non-empty literal needs
|
|
131
|
+
// room, and a zero-length one needs only `start <=
|
|
132
|
+
// source.len()`, which is what `end > source.len()` says
|
|
133
|
+
// when `plen` is 0.
|
|
134
|
+
//
|
|
135
|
+
// There used to be a `start >= source.len()` guard above
|
|
136
|
+
// this, mirroring Python's, which also refused a
|
|
137
|
+
// zero-length literal at end of input -- so `""` matched
|
|
138
|
+
// everywhere except there. See
|
|
139
|
+
// https://github.com/declaresub/abnf/issues/260 .
|
|
137
140
|
let plen = value.len();
|
|
138
141
|
let end = start + plen;
|
|
139
142
|
if end > source.len() {
|
|
@@ -210,11 +213,22 @@ mod tests {
|
|
|
210
213
|
assert!(lit.lparse(&[0xD800, 0x62], 0).is_err());
|
|
211
214
|
}
|
|
212
215
|
|
|
216
|
+
/// #260: a zero-length literal matches the empty string wherever the
|
|
217
|
+
/// source reaches, end of input included -- but not past it.
|
|
213
218
|
#[test]
|
|
214
|
-
fn
|
|
219
|
+
fn empty_literal_matches_at_eof_but_not_beyond() {
|
|
215
220
|
let lit = Literal::string("", false);
|
|
216
|
-
assert!(lit.lparse(&[], 0).
|
|
217
|
-
assert!(lit.lparse(&cps("x"), 0).is_ok());
|
|
221
|
+
assert!(lit.lparse(&[], 0).is_ok(), "empty source, offset 0");
|
|
222
|
+
assert!(lit.lparse(&cps("x"), 0).is_ok(), "start of input");
|
|
223
|
+
assert!(lit.lparse(&cps("x"), 1).is_ok(), "end of input");
|
|
224
|
+
assert!(lit.lparse(&cps("x"), 2).is_err(), "past end of input");
|
|
225
|
+
assert!(lit.lparse(&[], 1).is_err(), "past end of empty source");
|
|
226
|
+
|
|
227
|
+
// A non-empty literal still needs room for all of itself.
|
|
228
|
+
let lit = Literal::string("ab", false);
|
|
229
|
+
assert!(lit.lparse(&cps("ab"), 0).is_ok());
|
|
230
|
+
assert!(lit.lparse(&cps("a"), 0).is_err());
|
|
231
|
+
assert!(lit.lparse(&cps("ab"), 2).is_err());
|
|
218
232
|
}
|
|
219
233
|
|
|
220
234
|
#[test]
|
|
@@ -75,11 +75,45 @@ impl LiteralNode {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/// A terminal node supplied by an embedded Python parser, carrying the
|
|
79
|
+
/// text that parser reported.
|
|
80
|
+
///
|
|
81
|
+
/// Engine-built terminals are spans of the source ([`LiteralNode`]),
|
|
82
|
+
/// which is what lets the PyO3 layer produce their values by slicing.
|
|
83
|
+
/// A node handed *in* by a `PyCallbackParser` need not correspond to
|
|
84
|
+
/// any span: a custom parser may legitimately return a normalised or
|
|
85
|
+
/// synthesised value, and the pure-Python backend keeps it. Slicing
|
|
86
|
+
/// the source for one of these replaced it with unrelated text
|
|
87
|
+
/// (issue #220), so these carry their own.
|
|
88
|
+
///
|
|
89
|
+
/// Code points rather than a Rust string, for the same reason the
|
|
90
|
+
/// source is: the value may contain a lone surrogate.
|
|
91
|
+
#[derive(Debug, Clone)]
|
|
92
|
+
pub struct ForeignNode {
|
|
93
|
+
pub value: Box<[u32]>,
|
|
94
|
+
/// Offset the parser reported. Not necessarily where `value`
|
|
95
|
+
/// appears in the source -- it need not appear there at all.
|
|
96
|
+
pub offset: usize,
|
|
97
|
+
pub length: usize,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl ForeignNode {
|
|
101
|
+
pub fn new(value: Box<[u32]>, offset: usize, length: usize) -> Self {
|
|
102
|
+
Self { value, offset, length }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
78
106
|
/// Sum type for parse-tree children.
|
|
79
107
|
#[derive(Debug, Clone)]
|
|
80
108
|
pub enum NodeKind {
|
|
81
109
|
Internal(Node),
|
|
82
110
|
Literal(LiteralNode),
|
|
111
|
+
/// Behind an `Arc`, as `Node`'s children are, and for the same
|
|
112
|
+
/// reason: node lists are cloned on every match extension, so this
|
|
113
|
+
/// variant's clone lands on every parse whether or not a grammar
|
|
114
|
+
/// ever produces one. Inline it also made `NodeKind` 40 bytes
|
|
115
|
+
/// rather than 32.
|
|
116
|
+
Foreign(Arc<ForeignNode>),
|
|
83
117
|
}
|
|
84
118
|
|
|
85
119
|
impl NodeKind {
|
|
@@ -98,6 +132,13 @@ impl NodeKind {
|
|
|
98
132
|
.map(|cp| char::from_u32(*cp).unwrap_or(char::REPLACEMENT_CHARACTER)),
|
|
99
133
|
);
|
|
100
134
|
}
|
|
135
|
+
NodeKind::Foreign(f) => {
|
|
136
|
+
out.extend(
|
|
137
|
+
f.value
|
|
138
|
+
.iter()
|
|
139
|
+
.map(|cp| char::from_u32(*cp).unwrap_or(char::REPLACEMENT_CHARACTER)),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
101
142
|
}
|
|
102
143
|
}
|
|
103
144
|
|
|
@@ -113,6 +154,11 @@ impl NodeKind {
|
|
|
113
154
|
pub fn span_bounds(&self) -> Option<(usize, usize)> {
|
|
114
155
|
match self {
|
|
115
156
|
NodeKind::Literal(l) => Some((l.offset, l.offset + l.length)),
|
|
157
|
+
// A foreign node's text is its own, so its span says where
|
|
158
|
+
// the parser claims it sat, not where its value can be
|
|
159
|
+
// read from. Callers computing a value from a span must
|
|
160
|
+
// check `contains_foreign` first.
|
|
161
|
+
NodeKind::Foreign(f) => Some((f.offset, f.offset + f.length)),
|
|
116
162
|
NodeKind::Internal(n) => n.children.iter().fold(None, |acc, c| {
|
|
117
163
|
match (acc, c.span_bounds()) {
|
|
118
164
|
(None, s) => s,
|
|
@@ -123,3 +169,4 @@ impl NodeKind {
|
|
|
123
169
|
}
|
|
124
170
|
}
|
|
125
171
|
}
|
|
172
|
+
|
|
@@ -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());
|
|
@@ -153,6 +153,9 @@ fn visit_repeat(node: &Node, src: Src<'_>) -> Repeat {
|
|
|
153
153
|
saw_star = true;
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
+
// The meta-grammar builds its own trees, so no node here
|
|
157
|
+
// came from an embedded Python parser.
|
|
158
|
+
NodeKind::Foreign(_) => {}
|
|
156
159
|
}
|
|
157
160
|
}
|
|
158
161
|
let min = if min_src.is_empty() {
|
|
@@ -25,15 +25,15 @@ use pyo3::prelude::*;
|
|
|
25
25
|
use pyo3::types::PyString;
|
|
26
26
|
use pyo3::types::PyList;
|
|
27
27
|
|
|
28
|
-
use abnf_core::{LiteralNode, Match, Node, NodeKind};
|
|
28
|
+
use abnf_core::{ForeignNode, LiteralNode, Match, Node, NodeKind};
|
|
29
29
|
|
|
30
|
-
use crate::source::substring;
|
|
30
|
+
use crate::source::{from_code_points, substring, CodePoints};
|
|
31
31
|
|
|
32
32
|
// ----------------------------------------------------------------
|
|
33
33
|
// LiteralNode
|
|
34
34
|
// ----------------------------------------------------------------
|
|
35
35
|
|
|
36
|
-
#[pyclass(name = "LiteralNode", module = "abnf_rust._ext", from_py_object)]
|
|
36
|
+
#[pyclass(name = "LiteralNode", module = "abnf_rust._ext", from_py_object, subclass)]
|
|
37
37
|
#[derive(Debug)]
|
|
38
38
|
pub struct PyLiteralNode {
|
|
39
39
|
/// The matched text, as a Python `str` sliced out of the source.
|
|
@@ -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
|
|
|
@@ -122,7 +125,7 @@ impl PyLiteralNode {
|
|
|
122
125
|
// Node
|
|
123
126
|
// ----------------------------------------------------------------
|
|
124
127
|
|
|
125
|
-
#[pyclass(name = "Node", module = "abnf_rust._ext")]
|
|
128
|
+
#[pyclass(name = "Node", module = "abnf_rust._ext", subclass)]
|
|
126
129
|
#[derive(Debug)]
|
|
127
130
|
pub struct PyNode {
|
|
128
131
|
#[pyo3(get)]
|
|
@@ -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,
|
|
@@ -187,18 +206,55 @@ impl PyNode {
|
|
|
187
206
|
/// rather than a concatenation of the children's values: a node
|
|
188
207
|
/// covers a contiguous run of the source, so the two are the same
|
|
189
208
|
/// text.
|
|
190
|
-
pub fn from_rust(
|
|
209
|
+
pub fn from_rust(
|
|
210
|
+
py: Python<'_>,
|
|
211
|
+
n: &Node,
|
|
212
|
+
source: &Bound<'_, PyString>,
|
|
213
|
+
) -> PyResult<(Self, bool)> {
|
|
191
214
|
let mut children: Vec<Py<PyAny>> = Vec::with_capacity(n.children.len());
|
|
215
|
+
let mut foreign = false;
|
|
192
216
|
for child in n.children.iter() {
|
|
193
|
-
|
|
217
|
+
let (obj, child_foreign) = node_kind_to_py(py, child, source)?;
|
|
218
|
+
foreign |= child_foreign;
|
|
219
|
+
children.push(obj);
|
|
194
220
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
children
|
|
200
|
-
}
|
|
221
|
+
// One slice of the source covers this node -- unless something
|
|
222
|
+
// below it carries its own text, in which case join the parts,
|
|
223
|
+
// which the children have already computed (issue #220).
|
|
224
|
+
let value = if foreign {
|
|
225
|
+
join_child_values(py, &children)?
|
|
226
|
+
} else {
|
|
227
|
+
span_value(py, n.children.iter(), source)?
|
|
228
|
+
};
|
|
229
|
+
Ok((
|
|
230
|
+
Self {
|
|
231
|
+
name: n.name.as_ref().to_string(),
|
|
232
|
+
value: value.unbind(),
|
|
233
|
+
children,
|
|
234
|
+
},
|
|
235
|
+
foreign,
|
|
236
|
+
))
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// Concatenate the `value` of each already-converted child. Used only
|
|
241
|
+
/// for a node with a foreign descendant, where slicing the source
|
|
242
|
+
/// would not reproduce the text.
|
|
243
|
+
fn join_child_values<'py>(
|
|
244
|
+
py: Python<'py>,
|
|
245
|
+
children: &[Py<PyAny>],
|
|
246
|
+
) -> PyResult<Bound<'py, PyString>> {
|
|
247
|
+
let mut joined = PyString::new(py, "").unbind();
|
|
248
|
+
for child in children {
|
|
249
|
+
let part = child.bind(py).getattr("value")?;
|
|
250
|
+
joined = joined
|
|
251
|
+
.bind(py)
|
|
252
|
+
.as_any()
|
|
253
|
+
.add(part)?
|
|
254
|
+
.cast_into::<PyString>()?
|
|
255
|
+
.unbind();
|
|
201
256
|
}
|
|
257
|
+
Ok(joined.into_bound(py))
|
|
202
258
|
}
|
|
203
259
|
|
|
204
260
|
/// The text spanned by `nodes`: from the first descendant literal to
|
|
@@ -225,15 +281,44 @@ where
|
|
|
225
281
|
}
|
|
226
282
|
}
|
|
227
283
|
|
|
228
|
-
/// Convert a `NodeKind` to a Python object
|
|
284
|
+
/// Convert a `NodeKind` to a Python object, reporting whether the
|
|
285
|
+
/// subtree contains a node whose text is not a span of the source.
|
|
286
|
+
///
|
|
287
|
+
/// The flag is accumulated on the way up rather than recomputed by
|
|
288
|
+
/// walking each subtree, which would make building a tree quadratic --
|
|
289
|
+
/// measured at 13-17% on the parse benchmarks when this fix was first
|
|
290
|
+
/// written that way.
|
|
229
291
|
fn node_kind_to_py(
|
|
230
292
|
py: Python<'_>,
|
|
231
293
|
kind: &NodeKind,
|
|
232
294
|
source: &Bound<'_, PyString>,
|
|
233
|
-
) -> PyResult<Py<PyAny
|
|
295
|
+
) -> PyResult<(Py<PyAny>, bool)> {
|
|
234
296
|
Ok(match kind {
|
|
235
|
-
NodeKind::Internal(n) =>
|
|
236
|
-
|
|
297
|
+
NodeKind::Internal(n) => {
|
|
298
|
+
let (node, foreign) = PyNode::from_rust(py, n, source)?;
|
|
299
|
+
(Py::new(py, node)?.into_any(), foreign)
|
|
300
|
+
}
|
|
301
|
+
NodeKind::Literal(l) => (
|
|
302
|
+
Py::new(py, PyLiteralNode::from_rust(l, source)?)?.into_any(),
|
|
303
|
+
false,
|
|
304
|
+
),
|
|
305
|
+
// Carries its own text, so it is handed back verbatim rather
|
|
306
|
+
// than re-sliced from the source (issue #220).
|
|
307
|
+
NodeKind::Foreign(f) => {
|
|
308
|
+
let value = from_code_points(py, &f.value)?;
|
|
309
|
+
(
|
|
310
|
+
Py::new(
|
|
311
|
+
py,
|
|
312
|
+
PyLiteralNode {
|
|
313
|
+
value: value.unbind(),
|
|
314
|
+
offset: f.offset,
|
|
315
|
+
length: f.length,
|
|
316
|
+
},
|
|
317
|
+
)?
|
|
318
|
+
.into_any(),
|
|
319
|
+
true,
|
|
320
|
+
)
|
|
321
|
+
}
|
|
237
322
|
})
|
|
238
323
|
}
|
|
239
324
|
|
|
@@ -242,7 +327,7 @@ fn node_kind_to_py(
|
|
|
242
327
|
// Match
|
|
243
328
|
// ----------------------------------------------------------------
|
|
244
329
|
|
|
245
|
-
#[pyclass(name = "Match", module = "abnf_rust._ext")]
|
|
330
|
+
#[pyclass(name = "Match", module = "abnf_rust._ext", subclass)]
|
|
246
331
|
#[derive(Debug)]
|
|
247
332
|
pub struct PyMatch {
|
|
248
333
|
/// Match nodes as Python objects.
|
|
@@ -301,10 +386,17 @@ impl PyMatch {
|
|
|
301
386
|
impl PyMatch {
|
|
302
387
|
pub fn from_rust(py: Python<'_>, m: &Match, source: &Bound<'_, PyString>) -> PyResult<Self> {
|
|
303
388
|
let mut nodes = Vec::with_capacity(m.nodes.len());
|
|
389
|
+
let mut foreign = false;
|
|
304
390
|
for nk in &m.nodes {
|
|
305
|
-
|
|
391
|
+
let (obj, node_foreign) = node_kind_to_py(py, nk, source)?;
|
|
392
|
+
foreign |= node_foreign;
|
|
393
|
+
nodes.push(obj);
|
|
306
394
|
}
|
|
307
|
-
let cached_value =
|
|
395
|
+
let cached_value = if foreign {
|
|
396
|
+
join_child_values(py, &nodes)?
|
|
397
|
+
} else {
|
|
398
|
+
span_value(py, m.nodes.iter(), source)?
|
|
399
|
+
};
|
|
308
400
|
Ok(Self {
|
|
309
401
|
nodes,
|
|
310
402
|
start: m.start,
|
|
@@ -339,11 +431,23 @@ fn py_to_node_kind(obj: &Bound<'_, PyAny>) -> PyResult<NodeKind> {
|
|
|
339
431
|
//
|
|
340
432
|
// A terminal contributes only its span; the text itself is read
|
|
341
433
|
// back out of the source when the tree is rebuilt for Python.
|
|
434
|
+
// Terminals coming *in* from Python keep their own text. The
|
|
435
|
+
// engine's own terminals are spans of the source, which is what
|
|
436
|
+
// lets their values be produced by slicing -- but a custom parser
|
|
437
|
+
// may return a normalised or synthesised value, and discarding it
|
|
438
|
+
// silently replaced it with unrelated source text (issue #220).
|
|
342
439
|
if let Ok(lit) = obj.cast::<PyLiteralNode>() {
|
|
343
440
|
let lit = lit.borrow();
|
|
344
|
-
|
|
441
|
+
let value = CodePoints::new(lit.value.bind(obj.py()))?;
|
|
442
|
+
return Ok(NodeKind::Foreign(Arc::new(ForeignNode::new(
|
|
443
|
+
value.as_slice().into(),
|
|
444
|
+
lit.offset,
|
|
445
|
+
lit.length,
|
|
446
|
+
))));
|
|
345
447
|
}
|
|
346
|
-
if let (Ok(
|
|
448
|
+
if let (Ok(value_obj), Ok(offset_obj), Ok(length_obj)) =
|
|
449
|
+
(obj.getattr("value"), obj.getattr("offset"), obj.getattr("length"))
|
|
450
|
+
{
|
|
347
451
|
if let (Ok(offset), Ok(length)) = (
|
|
348
452
|
offset_obj.extract::<usize>(),
|
|
349
453
|
length_obj.extract::<usize>(),
|
|
@@ -353,7 +457,13 @@ fn py_to_node_kind(obj: &Bound<'_, PyAny>) -> PyResult<NodeKind> {
|
|
|
353
457
|
.and_then(|n| n.extract())
|
|
354
458
|
.unwrap_or_else(|_| "literal".to_string());
|
|
355
459
|
if name == "literal" {
|
|
356
|
-
|
|
460
|
+
let value_str = value_obj.cast::<PyString>()?;
|
|
461
|
+
let value = CodePoints::new(value_str)?;
|
|
462
|
+
return Ok(NodeKind::Foreign(Arc::new(ForeignNode::new(
|
|
463
|
+
value.as_slice().into(),
|
|
464
|
+
offset,
|
|
465
|
+
length,
|
|
466
|
+
))));
|
|
357
467
|
}
|
|
358
468
|
}
|
|
359
469
|
}
|
|
@@ -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::{PyFloat, PyInt, PyString, PyTuple, PyType};
|
|
12
13
|
|
|
13
14
|
use abnf_core::{
|
|
14
15
|
arc, Alternation, ArcParser, Concatenation, Literal, LiteralKind, OptionParser, Parser, Prose,
|
|
@@ -37,8 +38,78 @@ pub struct PyRepeat {
|
|
|
37
38
|
#[pymethods]
|
|
38
39
|
impl PyRepeat {
|
|
39
40
|
#[new]
|
|
40
|
-
#[pyo3(signature = (min=
|
|
41
|
-
fn new(
|
|
41
|
+
#[pyo3(signature = (min=None, max=None))]
|
|
42
|
+
fn new(
|
|
43
|
+
py: Python<'_>,
|
|
44
|
+
min: Option<&Bound<'_, PyAny>>,
|
|
45
|
+
max: Option<&Bound<'_, PyAny>>,
|
|
46
|
+
) -> PyResult<Self> {
|
|
47
|
+
// A negative `min` is not an error. The pure-Python `Repeat`
|
|
48
|
+
// accepts one, and `Repetition` then builds no mandatory
|
|
49
|
+
// prefix at all -- so it behaves as zero, which is what this
|
|
50
|
+
// clamps to (issue #221). Rejecting it made the backends
|
|
51
|
+
// disagree over a value neither treats as meaningful.
|
|
52
|
+
let min: usize = match min {
|
|
53
|
+
None => 0,
|
|
54
|
+
Some(obj) => match obj.extract::<usize>() {
|
|
55
|
+
Ok(value) => value,
|
|
56
|
+
Err(err) => {
|
|
57
|
+
if obj.cast::<PyInt>().is_ok() && obj.lt(0i64)? {
|
|
58
|
+
0
|
|
59
|
+
} else {
|
|
60
|
+
return Err(err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
// A bound too large for `usize` saturates rather than raising.
|
|
66
|
+
// Python's ints are unbounded, so `2*99999999999999999999"x"`
|
|
67
|
+
// is odd but valid ABNF that the pure-Python backend parses
|
|
68
|
+
// happily -- the bound is simply never reached. Raising
|
|
69
|
+
// `OverflowError` here made the two backends disagree, and
|
|
70
|
+
// `OverflowError` is neither `GrammarError` nor `ParseError`,
|
|
71
|
+
// so it escaped the documented exception contract as well.
|
|
72
|
+
// No input can reach `usize::MAX` repetitions, so saturating
|
|
73
|
+
// is indistinguishable from the unbounded value.
|
|
74
|
+
let max: Option<usize> = match max {
|
|
75
|
+
None => None,
|
|
76
|
+
Some(obj) if obj.is_none() => None,
|
|
77
|
+
Some(obj) => match obj.extract::<usize>() {
|
|
78
|
+
Ok(value) => Some(value),
|
|
79
|
+
// A float bound is not an error either. Pure Python
|
|
80
|
+
// compares `max` against the repetition count, so a
|
|
81
|
+
// non-integral float never equals it and the
|
|
82
|
+
// repetition is unbounded; an integral one caps where
|
|
83
|
+
// the integer would. Mirror both.
|
|
84
|
+
// Test for a *float* specifically: an int converts to
|
|
85
|
+
// f64 too, and routing negative ints through here would
|
|
86
|
+
// swallow the `max < min` check below.
|
|
87
|
+
Err(_) if obj.is_instance_of::<PyFloat>() => {
|
|
88
|
+
let value = obj.extract::<f64>()?;
|
|
89
|
+
if value.is_finite() && value.fract() == 0.0 && value >= 0.0 {
|
|
90
|
+
Some(value as usize)
|
|
91
|
+
} else {
|
|
92
|
+
None
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Not an integer at all: report that, not a bound problem.
|
|
96
|
+
Err(err) if obj.cast::<PyInt>().is_err() => return Err(err),
|
|
97
|
+
// An integer outside `usize`. Negative falls through to
|
|
98
|
+
// the `max < min` check below, exactly as in Python;
|
|
99
|
+
// anything else is bigger than any input can reach.
|
|
100
|
+
Err(_) if obj.lt(0i64)? => {
|
|
101
|
+
let msg = format!(
|
|
102
|
+
"Repeat max ({}) is less than min ({min}); \
|
|
103
|
+
a repetition of {min}*{} can never match.",
|
|
104
|
+
obj.str()?,
|
|
105
|
+
obj.str()?
|
|
106
|
+
);
|
|
107
|
+
return Err(crate::errors::grammar_error(py, &msg));
|
|
108
|
+
}
|
|
109
|
+
Err(_) => Some(usize::MAX),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
42
113
|
// Mirrors the check in the pure-Python `Repeat.__init__`:
|
|
43
114
|
// `3*2` is an impossible range, and leaving it unvalidated
|
|
44
115
|
// silently behaves as `3*`. Both backends must reject it
|
|
@@ -97,7 +168,9 @@ impl PyAlternation {
|
|
|
97
168
|
start: usize,
|
|
98
169
|
) -> PyResult<Py<LparseIter>> {
|
|
99
170
|
let cps = CodePoints::new(source)?;
|
|
100
|
-
let result = crate::recursion::call_lparse(
|
|
171
|
+
let result = crate::recursion::call_lparse(source.as_ptr() as usize, || {
|
|
172
|
+
self.inner.lparse(cps.as_slice(), start)
|
|
173
|
+
})?;
|
|
101
174
|
lparse_iter(py, result, source)
|
|
102
175
|
}
|
|
103
176
|
|
|
@@ -150,7 +223,9 @@ impl PyConcatenation {
|
|
|
150
223
|
start: usize,
|
|
151
224
|
) -> PyResult<Py<LparseIter>> {
|
|
152
225
|
let cps = CodePoints::new(source)?;
|
|
153
|
-
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
|
+
})?;
|
|
154
229
|
lparse_iter(py, result, source)
|
|
155
230
|
}
|
|
156
231
|
|
|
@@ -186,7 +261,9 @@ impl PyRepetition {
|
|
|
186
261
|
start: usize,
|
|
187
262
|
) -> PyResult<Py<LparseIter>> {
|
|
188
263
|
let cps = CodePoints::new(source)?;
|
|
189
|
-
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
|
+
})?;
|
|
190
267
|
lparse_iter(py, result, source)
|
|
191
268
|
}
|
|
192
269
|
|
|
@@ -222,7 +299,9 @@ impl PyOption {
|
|
|
222
299
|
start: usize,
|
|
223
300
|
) -> PyResult<Py<LparseIter>> {
|
|
224
301
|
let cps = CodePoints::new(source)?;
|
|
225
|
-
let result = crate::recursion::call_lparse(
|
|
302
|
+
let result = crate::recursion::call_lparse(source.as_ptr() as usize, || {
|
|
303
|
+
self.inner.lparse(cps.as_slice(), start)
|
|
304
|
+
})?;
|
|
226
305
|
lparse_iter(py, result, source)
|
|
227
306
|
}
|
|
228
307
|
|
|
@@ -269,7 +348,11 @@ impl PyLiteral {
|
|
|
269
348
|
};
|
|
270
349
|
return Ok(Self {
|
|
271
350
|
inner: Literal::range(*lo, *hi).into(),
|
|
272
|
-
|
|
351
|
+
// A range compares by code point either way, so
|
|
352
|
+
// this attribute is inert -- but the pure-Python
|
|
353
|
+
// constructor leaves it at its default, and the
|
|
354
|
+
// two backends should read alike.
|
|
355
|
+
case_sensitive,
|
|
273
356
|
});
|
|
274
357
|
}
|
|
275
358
|
}
|
|
@@ -285,7 +368,9 @@ impl PyLiteral {
|
|
|
285
368
|
start: usize,
|
|
286
369
|
) -> PyResult<Py<LparseIter>> {
|
|
287
370
|
let cps = CodePoints::new(source)?;
|
|
288
|
-
let result = crate::recursion::call_lparse(
|
|
371
|
+
let result = crate::recursion::call_lparse(source.as_ptr() as usize, || {
|
|
372
|
+
self.inner.lparse(cps.as_slice(), start)
|
|
373
|
+
})?;
|
|
289
374
|
lparse_iter(py, result, source)
|
|
290
375
|
}
|
|
291
376
|
|
|
@@ -347,7 +432,9 @@ impl PyProse {
|
|
|
347
432
|
start: usize,
|
|
348
433
|
) -> PyResult<Py<LparseIter>> {
|
|
349
434
|
let cps = CodePoints::new(source)?;
|
|
350
|
-
match crate::recursion::call_lparse(
|
|
435
|
+
match crate::recursion::call_lparse(source.as_ptr() as usize, || {
|
|
436
|
+
self.inner.lparse(cps.as_slice(), start)
|
|
437
|
+
})? {
|
|
351
438
|
Ok(_) => lparse_iter(py, Ok(smallvec::SmallVec::new()), source),
|
|
352
439
|
Err(err) => Err(parse_error_to_pyerr(py, err)),
|
|
353
440
|
}
|
|
@@ -386,19 +473,31 @@ pub fn extract_parser(obj: &Bound<'_, PyAny>) -> PyResult<ArcParser> {
|
|
|
386
473
|
if let Ok(p) = obj.cast::<PyProse>() {
|
|
387
474
|
return Ok(p.borrow().inner.clone());
|
|
388
475
|
}
|
|
389
|
-
// If the value is a Python Rule
|
|
390
|
-
// `
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
|
|
476
|
+
// If the value is a Python `Rule`, look up — or lazily create —
|
|
477
|
+
// its shadow Rust `NamedRule` in the bridge registry. This is the
|
|
478
|
+
// fast path that keeps rule references purely in Rust at parse
|
|
479
|
+
// time, instead of dispatching every reference through Python.
|
|
480
|
+
//
|
|
481
|
+
// The test is `isinstance`, not the presence of `name` + `lparse`.
|
|
482
|
+
// Attribute shape caught anything with a `name`, which was wrong
|
|
483
|
+
// twice over (issue #203): such an object became a
|
|
484
|
+
// definition-less `NamedRule` whose own `lparse` was then never
|
|
485
|
+
// called, and it was entered into a registry keyed by the object's
|
|
486
|
+
// *address* — sound only because `Rule._obj_map` makes rules
|
|
487
|
+
// immortal, which is not true of an arbitrary user object. A
|
|
488
|
+
// freed one's address could be handed to a new `Rule`, which then
|
|
489
|
+
// inherited its stale handle; defining that rule wrote into a
|
|
490
|
+
// parser tree built from an unrelated object.
|
|
491
|
+
static RULE_TYPE: PyOnceLock<Py<PyType>> = PyOnceLock::new();
|
|
492
|
+
let rule_type = RULE_TYPE.import(obj.py(), "abnf._parser_python", "Rule")?;
|
|
493
|
+
if obj.is_instance(rule_type)? {
|
|
396
494
|
let handle = crate::bridge::get_or_create(obj)?;
|
|
397
495
|
return Ok(arc(handle));
|
|
398
496
|
}
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
497
|
+
// Any other object with an `lparse` method is wrapped as a
|
|
498
|
+
// `PyCallbackParser`, which holds a reference to it -- so unlike a
|
|
499
|
+
// bridge entry, it cannot outlive the object it refers to. This
|
|
500
|
+
// is where every duck-typed parser goes, with or without a `name`.
|
|
402
501
|
if obj.hasattr("lparse")? {
|
|
403
502
|
let callback = PyCallbackParser::new(obj.clone().unbind(), "PyCallback");
|
|
404
503
|
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
|