arena_terms_parser/
token.rs

1//! # Term Parser Tokens
2//!
3//! Concrete token value and token type used by the parser pipeline.
4//!
5//! Provides:
6//! - [`Value`]: the payload carried by lexical tokens (e.g., arena term or index),
7//! - [`TermToken`]: a concrete token implementation that pairs a [`TokenID`],
8//!   a [`Value`], and a source line number, implementing [`parlex::Token`].
9//!
10//! These are produced by the lexer and consumed by the parser and later stages.
11
12use crate::TokenID;
13use arena_terms::Term;
14use parlex::{ParlexError, Span, Token};
15
16/// Represents a generic value emitted by the lexer.
17///
18/// `Value` encapsulates auxiliary data produced during lexing, such as parsed
19/// terms or indices into the parser's term stack.
20///
21/// # Variants
22/// - [`Value::None`] — Indicates absence of a value.
23/// - [`Value::Term`] — Wraps a parsed [`Term`] instance.
24/// - [`Value::Index`] — Holds a numeric index to terms stack `TermsParserDriver::terms`.
25#[derive(Debug, Clone, Copy, Default)]
26pub enum Value {
27    #[default]
28    None,
29    Term(Term),
30    Index(usize),
31}
32
33/// Macro that implements [`TryFrom<Value>`] for selected target types.
34///
35/// Converts from a generic [`Value`] into concrete types such as [`Term`]
36/// or `usize`. If the variant does not match, returns an error.
37macro_rules! impl_tryfrom_value {
38    ( $( $Variant:ident => $ty:ty ),+ $(,)? ) => {
39        $(
40            impl ::core::convert::TryFrom<Value> for $ty {
41                type Error = ParlexError;
42                fn try_from(v: Value) -> Result<Self, ParlexError> {
43                    match v {
44                        Value::$Variant(x) => Ok(x),
45                        other => Err(ParlexError {
46                            message: format!("expected `Value::{}`, found {:?}", stringify!($Variant), other),
47                            span: None,
48                        }),
49                    }
50                }
51            }
52        )+
53    };
54}
55
56// Implements TryFrom<Value> for Term and usize.
57impl_tryfrom_value! {
58    Term => Term,
59    Index => usize,
60}
61
62// Implements TryFrom<Value> for Option<Term>
63impl TryFrom<Value> for Option<Term> {
64    type Error = ParlexError;
65    fn try_from(v: Value) -> Result<Self, ParlexError> {
66        match v {
67            Value::None => Ok(None),
68            Value::Term(x) => Ok(Some(x)),
69            other => Err(ParlexError {
70                message: format!("expected `Value::Term` or `Value::None`, found {:?}", other),
71                span: None,
72            }),
73        }
74    }
75}
76
77/// A token produced by the [`TermLexer`] for Prolog-like term parsing.
78///
79/// Each [`TermToken`] encapsulates:
80/// - the syntactic token kind (`token_id`),
81/// - an associated semantic [`Value`],
82/// - the line number on which it was recognized, and
83/// - an optional operator-table index used for precedence/associativity lookup.
84#[derive(Debug, Clone)]
85pub struct TermToken {
86    ///
87    pub token_id: TokenID,
88    /// The associated value (if any).
89    pub value: Value,
90    /// The line number where the token was recognized.
91    pub span: Option<Span>,
92    /// Optional operator definition index.
93    pub op_tab_index: Option<usize>,
94}
95
96impl TermToken {
97    /// Creates a new [`TermToken`] with the specified token ID, value, and line number.
98    ///
99    /// # Parameters
100    /// - `token_id`: Token identifier from the lexer’s token table.
101    /// - `value`: Optional value attached to the token.
102    /// - `line_no`: Source line number where this token was found.
103    ///
104    /// The `op_tab_index` field is initialized to `None` by default.
105    #[must_use]
106    pub fn new(token_id: TokenID, value: Value, span: Option<Span>) -> Self {
107        Self {
108            token_id,
109            value,
110            span,
111            op_tab_index: None,
112        }
113    }
114}
115
116impl TermToken {
117    pub fn merge_span(&mut self, other_token: &TermToken) {
118        match other_token.span() {
119            Some(other_span) => match &mut self.span {
120                Some(span) => {
121                    *span = span.merge(&other_span);
122                }
123                None => {
124                    self.span = Some(other_span);
125                }
126            },
127            None => (),
128        }
129    }
130}
131
132/// Implements the [`Token`] trait for [`TermToken`], allowing integration
133/// with the `parlex` core library.
134impl Token for TermToken {
135    type TokenID = TokenID;
136
137    fn token_id(&self) -> Self::TokenID {
138        self.token_id
139    }
140    fn span(&self) -> Option<Span> {
141        self.span
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use core::convert::TryFrom;
149
150    #[test]
151    fn try_from_value_to_usize_success() {
152        let v = Value::Index(42);
153        let got = usize::try_from(v).expect("Index -> usize should succeed");
154        assert_eq!(got, 42);
155    }
156
157    #[test]
158    fn try_from_value_to_usize_wrong_variant_err() {
159        // Using Value::None to avoid depending on Term
160        let v = Value::None;
161        let err = usize::try_from(v).expect_err("None -> usize must error");
162        let msg = format!("{err:#}");
163        // Should mention we expected Index and say what we actually got
164        assert!(msg.contains("expected `Value::Index`"), "msg={msg}");
165        assert!(msg.contains("None"), "msg={msg}");
166    }
167
168    #[test]
169    fn try_from_value_to_option_term_none_success() {
170        let v = Value::None;
171        let got = <Option<Term>>::try_from(v).expect("None -> Option<Term> should succeed");
172        assert!(got.is_none());
173    }
174
175    #[test]
176    fn try_from_value_to_option_term_wrong_variant_err() {
177        let v = Value::Index(7);
178        let err = <Option<Term>>::try_from(v).expect_err("Index -> Option<Term> must error");
179        let msg = format!("{err:#}");
180        dbg!(&msg);
181        // Message from the impl should be clear
182        assert!(
183            msg.contains("expected `Value::Term` or `Value::None`"),
184            "msg={msg}"
185        );
186    }
187
188    #[test]
189    fn roundtrip_index_via_value() {
190        let input = 123usize;
191        let got = usize::try_from(Value::Index(input)).unwrap();
192        assert_eq!(got, input);
193    }
194}