metrale_grammar/earley/
state.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// ParserState — the Earley item.
4// Port of `struct ParserState` from xgrammar `cpp/earley_parser.h`.
5
6/// A `sequence_id` of this value means the rule has not been expanded
7/// yet (used for the initial state pushed into the parser).
8pub const UNEXPANDED_RULE_START_SEQUENCE_ID: i32 = 128_000;
9
10/// A `rule_start_pos` of this value marks an item as a root of the
11/// parsing stack (no preceding input position).
12pub const NO_PREV_INPUT_POS: i32 = -1;
13
14/// An Earley item: a position inside a rule's body, paired with the
15/// input position the rule was predicted at.
16///
17/// In the grammar a rule is always either a `Choices` of `Sequence`s
18/// (or `EmptyStr`), or a `TagDispatch`. After FSM acceleration the body
19/// is walked as an FSM, so `element_id` doubles as the FSM node id.
20///
21/// Faithful port of the C++ fields:
22/// - `rule_id`        — which rule, or `-1` for the non-FSM body view.
23/// - `sequence_id`    — which choice/sequence (a `GrammarExpr` id).
24/// - `element_id`     — element index in the sequence, or FSM node id.
25/// - `rule_start_pos` — input position the rule was predicted at.
26/// - `sub_element_id` — sub-position inside the current element
27///   (byte index in a `ByteString`, or remaining UTF-8 bytes).
28/// - `repeat_count`   — times a `Repeat` element has matched.
29/// - `partial_codepoint` — UTF-8 codepoint accumulated mid-character.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub struct ParserState {
32    pub rule_id: i32,
33    pub sequence_id: i32,
34    pub element_id: i32,
35    pub rule_start_pos: i32,
36    pub sub_element_id: i32,
37    pub repeat_count: i32,
38    pub partial_codepoint: i32,
39}
40
41impl ParserState {
42    /// Construct an item. `repeat_count` and `partial_codepoint`
43    /// default to 0 — matching the C++ constructor defaults.
44    pub fn new(
45        rule_id: i32,
46        sequence_id: i32,
47        element_id: i32,
48        rule_start_pos: i32,
49        sub_element_id: i32,
50    ) -> Self {
51        Self {
52            rule_id,
53            sequence_id,
54            element_id,
55            rule_start_pos,
56            sub_element_id,
57            repeat_count: 0,
58            partial_codepoint: 0,
59        }
60    }
61
62    /// Construct an item with an explicit `repeat_count`.
63    pub fn with_repeat(
64        rule_id: i32,
65        sequence_id: i32,
66        element_id: i32,
67        rule_start_pos: i32,
68        sub_element_id: i32,
69        repeat_count: i32,
70    ) -> Self {
71        Self {
72            rule_id,
73            sequence_id,
74            element_id,
75            rule_start_pos,
76            sub_element_id,
77            repeat_count,
78            partial_codepoint: 0,
79        }
80    }
81
82    /// The invalid sentinel item (`sequence_id == -1`).
83    pub fn invalid() -> Self {
84        Self::new(-1, -1, -1, -1, -1)
85    }
86
87    /// True if this is the invalid sentinel.
88    pub fn is_invalid(&self) -> bool {
89        self.sequence_id == -1
90    }
91
92    /// True if this item is the root of its parsing stack.
93    pub fn is_root(&self) -> bool {
94        self.rule_start_pos == NO_PREV_INPUT_POS
95    }
96
97    /// True if the rule still needs expansion before parsing.
98    pub fn is_unexpanded(&self) -> bool {
99        self.sequence_id == UNEXPANDED_RULE_START_SEQUENCE_ID
100    }
101}
102
103/// The C++ uses two equality notions; we model the parsing one as
104/// `PartialEq` (all fields). The cache notion ignores `rule_start_pos`
105/// and `repeat_count` — see [`cache_key`].
106///
107/// `cache_key` returns the tuple the C++ `StateHashForCache` /
108/// `StateEqualForParsing`-minus-position uses to deduplicate states
109/// when computing the acceptable-token mask. Consumed by the W6
110/// `GrammarMatcher` token-mask cache.
111#[allow(dead_code)]
112pub fn cache_key(s: &ParserState) -> (i32, i32, i32, i32) {
113    (s.rule_id, s.sequence_id, s.element_id, s.sub_element_id)
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn invalid_sentinel() {
122        let s = ParserState::invalid();
123        assert!(s.is_invalid());
124        assert!(!ParserState::new(0, 0, 0, 0, 0).is_invalid());
125    }
126
127    #[test]
128    fn root_detection() {
129        assert!(ParserState::new(0, 0, 0, NO_PREV_INPUT_POS, 0).is_root());
130        assert!(!ParserState::new(0, 0, 0, 3, 0).is_root());
131    }
132
133    #[test]
134    fn unexpanded_detection() {
135        let s = ParserState::new(
136            0,
137            UNEXPANDED_RULE_START_SEQUENCE_ID,
138            0,
139            NO_PREV_INPUT_POS,
140            0,
141        );
142        assert!(s.is_unexpanded());
143        assert!(!ParserState::new(0, 5, 0, 0, 0).is_unexpanded());
144    }
145
146    #[test]
147    fn cache_key_ignores_position_and_repeat() {
148        let a = ParserState::with_repeat(1, 2, 3, 10, 4, 5);
149        let b = ParserState::with_repeat(1, 2, 3, 99, 4, 7);
150        assert_eq!(cache_key(&a), cache_key(&b));
151        assert_ne!(a, b);
152    }
153}