Skip to main content

adze_ts_format_core/
lib.rs

1//! Shared constants and helpers for Tree-sitter-compatible parsing tables.
2//!
3//! This crate captures action tags and deterministic action selection behavior used by
4//! both runtime and tablegen/parsing code paths.
5
6#![forbid(unsafe_op_in_unsafe_fn)]
7#![deny(missing_docs)]
8#![cfg_attr(feature = "strict_api", deny(unreachable_pub))]
9#![cfg_attr(not(feature = "strict_api"), warn(unreachable_pub))]
10#![cfg_attr(feature = "strict_docs", deny(missing_docs))]
11#![cfg_attr(not(feature = "strict_docs"), allow(missing_docs))]
12
13use adze_glr_core::{Action, ParseTable};
14
15/// Tree-sitter action type tags.
16///
17/// # Examples
18///
19/// ```
20/// use adze_ts_format_core::TSActionTag;
21///
22/// assert_eq!(TSActionTag::Shift as u8, 1);
23/// assert!(TSActionTag::Reduce > TSActionTag::Shift);
24/// ```
25#[repr(u8)]
26#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
27pub enum TSActionTag {
28    /// Error action tag.
29    Error = 0,
30    /// Shift action tag.
31    Shift = 1,
32    /// Recover action tag.
33    Recover = 2,
34    /// Reduce action tag (Tree-sitter uses `3` for Reduce).
35    Reduce = 3,
36    /// Accept action tag (Tree-sitter uses `4` for Accept).
37    Accept = 4,
38}
39
40/// Choose a single action from a GLR cell deterministically.
41///
42/// Priority is `Accept > Shift > Reduce > Error`, matching existing runtime behavior.
43///
44/// # Examples
45///
46/// ```
47/// use adze_ts_format_core::choose_action;
48/// use adze_glr_core::{Action, StateId, RuleId};
49///
50/// let cell = vec![Action::Shift(StateId(1)), Action::Reduce(RuleId(0))];
51/// assert_eq!(choose_action(&cell), Some(Action::Shift(StateId(1))));
52///
53/// assert_eq!(choose_action(&[]), None);
54/// ```
55#[must_use]
56pub fn choose_action(cell: &[Action]) -> Option<Action> {
57    if cell.is_empty() {
58        return None;
59    }
60
61    if let Some(a) = cell.iter().find(|a| matches!(a, Action::Accept)) {
62        return Some(a.clone());
63    }
64    if let Some(a) = cell.iter().find(|a| matches!(a, Action::Shift(_))) {
65        return Some(a.clone());
66    }
67    if let Some(a) = cell.iter().find(|a| matches!(a, Action::Reduce(_))) {
68        return Some(a.clone());
69    }
70    Some(Action::Error)
71}
72
73/// Choose a single action from a GLR cell using precedence-aware scoring.
74///
75/// This is used when grammar/runtime policy needs tie-breaking with dynamic rule precedence.
76#[must_use]
77pub fn choose_action_with_precedence(cell: &[Action], parse_table: &ParseTable) -> Option<Action> {
78    if cell.is_empty() {
79        return None;
80    }
81
82    let mut sorted = cell.to_vec();
83    sorted.sort_by_key(|a| -action_priority(a, parse_table));
84    sorted.first().cloned()
85}
86
87#[inline]
88fn action_priority(action: &Action, parse_table: &ParseTable) -> i32 {
89    use Action::*;
90
91    if matches!(action, Accept) {
92        return 3_000_000;
93    }
94
95    let mut prec = 0i32;
96    if let Reduce(rid) = action {
97        if (rid.0 as usize) < parse_table.dynamic_prec_by_rule.len() {
98            prec = parse_table.dynamic_prec_by_rule[rid.0 as usize] as i32;
99        }
100
101        if (rid.0 as usize) < parse_table.rule_assoc_by_rule.len() {
102            prec = prec.saturating_add(parse_table.rule_assoc_by_rule[rid.0 as usize] as i32);
103        }
104
105        if prec > 0 {
106            return 2_000_000 + prec;
107        }
108        return 1_500_000 + prec;
109    }
110
111    if matches!(action, Shift(_)) {
112        return 2_000_000;
113    }
114
115    0
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn verify_action_tag_constants() {
124        assert_eq!(TSActionTag::Error as u8, 0, "Error tag must be 0");
125        assert_eq!(TSActionTag::Shift as u8, 1, "Shift tag must be 1");
126        assert_eq!(
127            TSActionTag::Reduce as u8,
128            3,
129            "Reduce tag must be 3 (Tree-sitter uses 2 for Recover)"
130        );
131        assert_eq!(TSActionTag::Accept as u8, 4, "Accept tag must be 4");
132
133        assert!(TSActionTag::Error < TSActionTag::Shift);
134        assert!(TSActionTag::Shift < TSActionTag::Reduce);
135        assert!(TSActionTag::Reduce < TSActionTag::Accept);
136    }
137
138    #[test]
139    fn choose_action_prefers_shift_over_reduce() {
140        use adze_glr_core::{RuleId, StateId};
141
142        let cell = vec![
143            Action::Shift(StateId(1)),
144            Action::Reduce(RuleId(0)),
145            Action::Accept,
146            Action::Error,
147        ];
148
149        let chosen = choose_action(&cell).expect("expected one action");
150        assert_eq!(chosen, Action::Accept);
151    }
152}