Skip to main content

panproto_expr/
literal.rs

1//! Literal values in the expression language.
2//!
3//! [`Literal`] is the expression language's own value type, independent of
4//! `panproto_inst::Value` to avoid dependency cycles. Downstream crates
5//! provide conversions between the two.
6
7use std::sync::Arc;
8
9/// A literal value in the expression language.
10///
11/// This is the result type of expression evaluation and the leaf node
12/// type for literal expressions. Kept minimal: just the primitives
13/// needed for schema transforms.
14#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
15pub enum Literal {
16    /// Boolean value.
17    Bool(bool),
18    /// 64-bit signed integer.
19    Int(i64),
20    /// 64-bit IEEE 754 float.
21    Float(f64),
22    /// UTF-8 string.
23    Str(String),
24    /// Raw bytes.
25    Bytes(Vec<u8>),
26    /// Null / absent value.
27    Null,
28    /// A record (ordered map of fields to values).
29    Record(Vec<(Arc<str>, Self)>),
30    /// A list of values.
31    List(Vec<Self>),
32    /// A closure: a lambda expression captured with its environment.
33    ///
34    /// Closures are first-class values produced by evaluating a `Lam` expression.
35    /// They capture the parameter name, body, and the environment at the point
36    /// of creation, enabling proper lexical scoping.
37    Closure {
38        /// The parameter name bound by this lambda.
39        param: Arc<str>,
40        /// The body expression (serialized as the AST).
41        body: Box<crate::Expr>,
42        /// The scope captured at the point of closure creation.
43        ///
44        /// Shared rather than copied, so producing a closure costs the same
45        /// however wide the scope it closes over is.
46        env: crate::Env,
47    },
48}
49
50impl Literal {
51    /// Returns a human-readable type name for error messages.
52    #[must_use]
53    pub const fn type_name(&self) -> &'static str {
54        match self {
55            Self::Bool(_) => "bool",
56            Self::Int(_) => "int",
57            Self::Float(_) => "float",
58            Self::Str(_) => "string",
59            Self::Bytes(_) => "bytes",
60            Self::Null => "null",
61            Self::Record(_) => "record",
62            Self::List(_) => "list",
63            Self::Closure { .. } => "function",
64        }
65    }
66
67    /// Returns `true` if this is a [`Literal::Null`].
68    #[must_use]
69    pub const fn is_null(&self) -> bool {
70        matches!(self, Self::Null)
71    }
72
73    /// Attempts to extract a boolean value.
74    #[must_use]
75    pub const fn as_bool(&self) -> Option<bool> {
76        match self {
77            Self::Bool(b) => Some(*b),
78            _ => None,
79        }
80    }
81
82    /// Attempts to extract an integer value.
83    #[must_use]
84    pub const fn as_int(&self) -> Option<i64> {
85        match self {
86            Self::Int(n) => Some(*n),
87            _ => None,
88        }
89    }
90
91    /// Attempts to extract a float value.
92    #[must_use]
93    pub const fn as_float(&self) -> Option<f64> {
94        match self {
95            Self::Float(f) => Some(*f),
96            _ => None,
97        }
98    }
99
100    /// Attempts to extract a string reference.
101    #[must_use]
102    pub fn as_str(&self) -> Option<&str> {
103        match self {
104            Self::Str(s) => Some(s),
105            _ => None,
106        }
107    }
108
109    /// Attempts to extract a record reference.
110    #[must_use]
111    pub fn as_record(&self) -> Option<&[(Arc<str>, Self)]> {
112        match self {
113            Self::Record(fields) => Some(fields),
114            _ => None,
115        }
116    }
117
118    /// Attempts to extract a list reference.
119    #[must_use]
120    pub fn as_list(&self) -> Option<&[Self]> {
121        match self {
122            Self::List(items) => Some(items),
123            _ => None,
124        }
125    }
126
127    /// Look up a field in a record by name.
128    #[must_use]
129    pub fn field(&self, name: &str) -> Option<&Self> {
130        match self {
131            Self::Record(fields) => fields.iter().find(|(k, _)| &**k == name).map(|(_, v)| v),
132            _ => None,
133        }
134    }
135}
136
137// Custom PartialEq that uses f64::to_bits for float comparison,
138// making it consistent with Eq and Hash.
139impl PartialEq for Literal {
140    fn eq(&self, other: &Self) -> bool {
141        match (self, other) {
142            (Self::Bool(a), Self::Bool(b)) => a == b,
143            (Self::Int(a), Self::Int(b)) => a == b,
144            (Self::Float(a), Self::Float(b)) => a.to_bits() == b.to_bits(),
145            (Self::Str(a), Self::Str(b)) => a == b,
146            (Self::Bytes(a), Self::Bytes(b)) => a == b,
147            (Self::Null, Self::Null) => true,
148            (Self::Record(a), Self::Record(b)) => a == b,
149            (Self::List(a), Self::List(b)) => a == b,
150            (
151                Self::Closure {
152                    param: p1,
153                    body: b1,
154                    env: e1,
155                },
156                Self::Closure {
157                    param: p2,
158                    body: b2,
159                    env: e2,
160                },
161            ) => p1 == p2 && b1 == b2 && e1 == e2,
162            _ => false,
163        }
164    }
165}
166
167impl Eq for Literal {}
168
169impl std::hash::Hash for Literal {
170    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
171        std::mem::discriminant(self).hash(state);
172        match self {
173            Self::Bool(b) => b.hash(state),
174            Self::Int(n) => n.hash(state),
175            Self::Float(f) => f.to_bits().hash(state),
176            Self::Str(s) => s.hash(state),
177            Self::Bytes(b) => b.hash(state),
178            Self::Null => {}
179            Self::Record(fields) => fields.hash(state),
180            Self::List(items) => items.hash(state),
181            Self::Closure { param, body, env } => {
182                param.hash(state);
183                body.hash(state);
184                env.hash(state);
185            }
186        }
187    }
188}
189
190impl std::fmt::Display for Literal {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        match self {
193            Self::Bool(b) => write!(f, "{b}"),
194            Self::Int(n) => write!(f, "{n}"),
195            Self::Float(v) => write!(f, "{v}"),
196            Self::Str(s) => write!(f, "\"{s}\""),
197            Self::Bytes(b) => write!(f, "<{} bytes>", b.len()),
198            Self::Null => write!(f, "null"),
199            Self::Record(fields) => {
200                write!(f, "{{ ")?;
201                for (i, (k, v)) in fields.iter().enumerate() {
202                    if i > 0 {
203                        write!(f, ", ")?;
204                    }
205                    write!(f, "{k}: {v}")?;
206                }
207                write!(f, " }}")
208            }
209            Self::List(items) => {
210                write!(f, "[")?;
211                for (i, v) in items.iter().enumerate() {
212                    if i > 0 {
213                        write!(f, ", ")?;
214                    }
215                    write!(f, "{v}")?;
216                }
217                write!(f, "]")
218            }
219            Self::Closure { param, .. } => write!(f, "<closure λ{param}>"),
220        }
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn float_equality_uses_bits() {
230        // NaN == NaN when using to_bits comparison
231        let a = Literal::Float(f64::NAN);
232        let b = Literal::Float(f64::NAN);
233        assert_eq!(a, b);
234    }
235
236    #[test]
237    fn type_names() {
238        assert_eq!(Literal::Bool(true).type_name(), "bool");
239        assert_eq!(Literal::Int(42).type_name(), "int");
240        assert_eq!(Literal::Null.type_name(), "null");
241        assert_eq!(Literal::Record(vec![]).type_name(), "record");
242        assert_eq!(Literal::List(vec![]).type_name(), "list");
243    }
244
245    #[test]
246    fn record_field_lookup() {
247        let rec = Literal::Record(vec![
248            (Arc::from("name"), Literal::Str("alice".into())),
249            (Arc::from("age"), Literal::Int(30)),
250        ]);
251        assert_eq!(rec.field("name"), Some(&Literal::Str("alice".into())));
252        assert_eq!(rec.field("age"), Some(&Literal::Int(30)));
253        assert_eq!(rec.field("missing"), None);
254    }
255}