Skip to main content

hedl_core/
value.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Value types for HEDL scalars.
19
20use crate::lex::{Expression, Tensor};
21
22/// A reference to another node.
23///
24/// Optimized memory layout:
25/// - 16 bytes on 64-bit systems (without heap allocation for short IDs)
26/// - Type names are interned during parsing to reduce duplication
27/// - Uses `Box<str>` to minimize heap overhead compared to String
28#[derive(Debug, Clone, PartialEq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct Reference {
31    /// Optional type qualifier (e.g., "User" in "@User:id").
32    /// Boxed to reduce size when None (common case).
33    pub type_name: Option<Box<str>>,
34    /// The ID being referenced.
35    /// Uses `Box<str>` for compact representation (16 bytes vs 24 for String).
36    pub id: Box<str>,
37}
38
39impl Reference {
40    /// Create a local reference (no type qualifier).
41    pub fn local(id: impl Into<String>) -> Self {
42        Self {
43            type_name: None,
44            id: id.into().into_boxed_str(),
45        }
46    }
47
48    /// Create a qualified reference with type name.
49    pub fn qualified(type_name: impl Into<String>, id: impl Into<String>) -> Self {
50        Self {
51            type_name: Some(type_name.into().into_boxed_str()),
52            id: id.into().into_boxed_str(),
53        }
54    }
55
56    /// Create an unqualified reference (alias for `local`).
57    ///
58    /// An unqualified reference has no type qualifier and will be resolved
59    /// based on context (current type in matrix lists, or global search
60    /// with ambiguity detection in key-value context).
61    pub fn unqualified(id: impl Into<String>) -> Self {
62        Self::local(id)
63    }
64
65    /// Format as a reference string (with @).
66    pub fn to_ref_string(&self) -> String {
67        match &self.type_name {
68            Some(t) => format!("@{}:{}", t, self.id),
69            None => format!("@{}", self.id),
70        }
71    }
72}
73
74/// A scalar value in HEDL.
75///
76/// Optimized memory layout:
77/// - Large variants (String, Tensor, Expression, List) are boxed to keep enum size small
78/// - Small values (Null, Bool, Int, Float) remain inline
79/// - Total enum size: 16 bytes (down from 32+ bytes)
80/// - Reduces memory usage by 40-50% for typical documents
81#[derive(Debug, Clone, PartialEq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83pub enum Value {
84    /// Null value (~).
85    Null,
86    /// Boolean value (true/false).
87    Bool(bool),
88    /// Integer value.
89    Int(i64),
90    /// Floating-point value.
91    Float(f64),
92    /// String value (boxed to reduce enum size).
93    String(Box<str>),
94    /// Tensor (multi-dimensional array, boxed to reduce enum size).
95    Tensor(Box<Tensor>),
96    /// Reference to another node.
97    Reference(Reference),
98    /// Parsed expression from $(...) (boxed to reduce enum size).
99    Expression(Box<Expression>),
100    /// List of scalar values (from `(...)` syntax).
101    ///
102    /// Distinct from Tensor: lists can contain any scalar types (strings, bools, refs, etc.),
103    /// while tensors are numeric-only.
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// use hedl_core::Value;
109    ///
110    /// // String list: (admin, editor, viewer)
111    /// let roles = Value::List(Box::new(vec![
112    ///     Value::String("admin".into()),
113    ///     Value::String("editor".into()),
114    ///     Value::String("viewer".into()),
115    /// ]));
116    ///
117    /// // Bool list: (true, false, true)
118    /// let flags = Value::List(Box::new(vec![
119    ///     Value::Bool(true),
120    ///     Value::Bool(false),
121    ///     Value::Bool(true),
122    /// ]));
123    ///
124    /// // Empty list: ()
125    /// let empty = Value::List(Box::new(vec![]));
126    /// ```
127    List(Box<Vec<Value>>),
128}
129
130impl Value {
131    /// Returns true if this value is null.
132    pub fn is_null(&self) -> bool {
133        matches!(self, Self::Null)
134    }
135
136    /// Returns true if this value is a reference.
137    pub fn is_reference(&self) -> bool {
138        matches!(self, Self::Reference(_))
139    }
140
141    /// Returns true if this value is a list.
142    pub fn is_list(&self) -> bool {
143        matches!(self, Self::List(_))
144    }
145
146    /// Try to get the value as a list.
147    pub fn as_list(&self) -> Option<&[Value]> {
148        match self {
149            Self::List(l) => Some(l),
150            _ => None,
151        }
152    }
153
154    /// Try to get the value as a string.
155    pub fn as_str(&self) -> Option<&str> {
156        match self {
157            Self::String(s) => Some(s),
158            _ => None,
159        }
160    }
161
162    /// Try to get the value as an integer.
163    pub fn as_int(&self) -> Option<i64> {
164        match self {
165            Self::Int(n) => Some(*n),
166            _ => None,
167        }
168    }
169
170    /// Try to get the value as a float.
171    pub fn as_float(&self) -> Option<f64> {
172        match self {
173            Self::Float(n) => Some(*n),
174            Self::Int(n) => Some(*n as f64),
175            _ => None,
176        }
177    }
178
179    /// Try to get the value as a boolean.
180    pub fn as_bool(&self) -> Option<bool> {
181        match self {
182            Self::Bool(b) => Some(*b),
183            _ => None,
184        }
185    }
186
187    /// Try to get the value as a reference.
188    pub fn as_reference(&self) -> Option<&Reference> {
189        match self {
190            Self::Reference(r) => Some(r),
191            _ => None,
192        }
193    }
194}
195
196impl std::fmt::Display for Value {
197    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198        match self {
199            Self::Null => write!(f, "~"),
200            Self::Bool(b) => write!(f, "{}", b),
201            Self::Int(n) => write!(f, "{}", n),
202            Self::Float(n) => write!(f, "{}", n),
203            Self::String(s) => write!(f, "{}", s),
204            Self::Tensor(_) => write!(f, "[tensor]"),
205            Self::Reference(r) => write!(f, "{}", r.to_ref_string()),
206            Self::Expression(e) => write!(f, "$({})", e),
207            Self::List(items) => {
208                write!(f, "(")?;
209                for (i, item) in items.iter().enumerate() {
210                    if i > 0 {
211                        write!(f, ", ")?;
212                    }
213                    write!(f, "{}", item)?;
214                }
215                write!(f, ")")
216            }
217        }
218    }
219}
220
221impl Value {
222    /// Try to get the expression if this is an Expression variant.
223    pub fn as_expression(&self) -> Option<&Expression> {
224        match self {
225            Self::Expression(e) => Some(e),
226            _ => None,
227        }
228    }
229
230    /// Attempt to coerce this value to the expected type.
231    ///
232    /// Uses lenient mode (equivalent to Standard level) by default.
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use hedl_core::Value;
238    /// use hedl_core::types::ExpectedType;
239    ///
240    /// let value = Value::String("42".to_string().into());
241    /// let result = value.coerce_to(&ExpectedType::Int);
242    /// assert!(result.is_ok());
243    /// ```
244    pub fn coerce_to(
245        &self,
246        expected: &crate::types::ExpectedType,
247    ) -> crate::coercion::CoercionResult {
248        crate::coercion::coerce(
249            self.clone(),
250            expected,
251            crate::coercion::CoercionMode::Lenient,
252        )
253    }
254
255    /// Check if this value can be coerced to the expected type.
256    ///
257    /// Returns true if coercion would succeed (either matched or coerced).
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// use hedl_core::Value;
263    /// use hedl_core::types::ExpectedType;
264    ///
265    /// let value = Value::Int(42);
266    /// assert!(value.can_coerce_to(&ExpectedType::Float));
267    ///
268    /// let value = Value::String("42".to_string().into());
269    /// assert!(value.can_coerce_to(&ExpectedType::Int));
270    /// ```
271    pub fn can_coerce_to(&self, expected: &crate::types::ExpectedType) -> bool {
272        !matches!(
273            self.coerce_to(expected),
274            crate::coercion::CoercionResult::Failed { .. }
275        )
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    // ==================== Reference tests ====================
284
285    #[test]
286    fn test_reference_local() {
287        let r = Reference::local("user-123");
288        assert_eq!(r.type_name, None);
289        assert_eq!(r.id.as_ref(), "user-123");
290    }
291
292    #[test]
293    fn test_reference_qualified() {
294        let r = Reference::qualified("User", "123");
295        assert_eq!(r.type_name.as_deref(), Some("User"));
296        assert_eq!(r.id.as_ref(), "123");
297    }
298
299    #[test]
300    fn test_reference_to_ref_string_local() {
301        let r = Reference::local("id-1");
302        assert_eq!(r.to_ref_string(), "@id-1");
303    }
304
305    #[test]
306    fn test_reference_to_ref_string_qualified() {
307        let r = Reference::qualified("User", "id-1");
308        assert_eq!(r.to_ref_string(), "@User:id-1");
309    }
310
311    #[test]
312    fn test_reference_equality() {
313        let a = Reference::qualified("User", "1");
314        let b = Reference::qualified("User", "1");
315        assert_eq!(a, b);
316    }
317
318    #[test]
319    fn test_reference_inequality() {
320        let a = Reference::qualified("User", "1");
321        let b = Reference::qualified("Post", "1");
322        assert_ne!(a, b);
323    }
324
325    #[test]
326    fn test_reference_clone() {
327        let original = Reference::qualified("Type", "id");
328        let cloned = original.clone();
329        assert_eq!(original, cloned);
330    }
331
332    #[test]
333    fn test_reference_debug() {
334        let r = Reference::qualified("User", "abc");
335        let debug = format!("{:?}", r);
336        assert!(debug.contains("User"));
337        assert!(debug.contains("abc"));
338    }
339
340    // ==================== Value::is_* tests ====================
341
342    #[test]
343    fn test_value_is_null() {
344        assert!(Value::Null.is_null());
345        assert!(!Value::Bool(true).is_null());
346        assert!(!Value::Int(0).is_null());
347    }
348
349    #[test]
350    fn test_value_is_reference() {
351        let r = Reference::local("id");
352        assert!(Value::Reference(r).is_reference());
353        assert!(!Value::Null.is_reference());
354        assert!(!Value::String("@ref".to_string().into()).is_reference());
355    }
356
357    // ==================== Value::as_* tests ====================
358
359    #[test]
360    fn test_value_as_str() {
361        let v = Value::String("hello".to_string().into());
362        assert_eq!(v.as_str(), Some("hello"));
363        assert_eq!(Value::Null.as_str(), None);
364        assert_eq!(Value::Int(42).as_str(), None);
365    }
366
367    #[test]
368    fn test_value_as_int() {
369        assert_eq!(Value::Int(42).as_int(), Some(42));
370        assert_eq!(Value::Int(-100).as_int(), Some(-100));
371        assert_eq!(Value::Float(3.5).as_int(), None);
372        assert_eq!(Value::String("42".to_string().into()).as_int(), None);
373    }
374
375    #[test]
376    fn test_value_as_float() {
377        assert_eq!(Value::Float(3.5).as_float(), Some(3.5));
378        // Int converts to float
379        assert_eq!(Value::Int(42).as_float(), Some(42.0));
380        assert_eq!(Value::String("3.5".to_string().into()).as_float(), None);
381    }
382
383    #[test]
384    fn test_value_as_bool() {
385        assert_eq!(Value::Bool(true).as_bool(), Some(true));
386        assert_eq!(Value::Bool(false).as_bool(), Some(false));
387        assert_eq!(Value::Int(1).as_bool(), None);
388        assert_eq!(Value::String("true".to_string().into()).as_bool(), None);
389    }
390
391    #[test]
392    fn test_value_as_reference() {
393        let r = Reference::local("id");
394        let v = Value::Reference(r.clone());
395        assert_eq!(v.as_reference(), Some(&r));
396        assert_eq!(Value::Null.as_reference(), None);
397    }
398
399    #[test]
400    fn test_value_as_expression() {
401        use crate::lex::{Expression, Span};
402        let expr = Expression::Identifier {
403            name: "x".to_string(),
404            span: Span::synthetic(),
405        };
406        let v = Value::Expression(Box::new(expr.clone()));
407        assert_eq!(v.as_expression(), Some(&expr));
408        assert_eq!(Value::Null.as_expression(), None);
409    }
410
411    // ==================== Value Display tests ====================
412
413    #[test]
414    fn test_value_display_null() {
415        assert_eq!(format!("{}", Value::Null), "~");
416    }
417
418    #[test]
419    fn test_value_display_bool() {
420        assert_eq!(format!("{}", Value::Bool(true)), "true");
421        assert_eq!(format!("{}", Value::Bool(false)), "false");
422    }
423
424    #[test]
425    fn test_value_display_int() {
426        assert_eq!(format!("{}", Value::Int(42)), "42");
427        assert_eq!(format!("{}", Value::Int(-100)), "-100");
428        assert_eq!(format!("{}", Value::Int(0)), "0");
429    }
430
431    #[test]
432    fn test_value_display_float() {
433        let s = format!("{}", Value::Float(3.5));
434        assert!(s.starts_with("3.5"));
435    }
436
437    #[test]
438    fn test_value_display_string() {
439        assert_eq!(
440            format!("{}", Value::String("hello".to_string().into())),
441            "hello"
442        );
443    }
444
445    #[test]
446    fn test_value_display_reference() {
447        let r = Reference::qualified("User", "123");
448        assert_eq!(format!("{}", Value::Reference(r)), "@User:123");
449    }
450
451    #[test]
452    fn test_value_display_expression() {
453        use crate::lex::{Expression, Span};
454        let expr = Expression::Identifier {
455            name: "x".to_string(),
456            span: Span::synthetic(),
457        };
458        assert_eq!(format!("{}", Value::Expression(Box::new(expr))), "$(x)");
459    }
460
461    #[test]
462    fn test_value_display_tensor() {
463        use crate::lex::Tensor;
464        let t = Tensor::Array(vec![Tensor::Scalar(1.0), Tensor::Scalar(2.0)]);
465        assert_eq!(format!("{}", Value::Tensor(Box::new(t))), "[tensor]");
466    }
467
468    // ==================== Value equality and clone ====================
469
470    #[test]
471    fn test_value_equality_null() {
472        assert_eq!(Value::Null, Value::Null);
473    }
474
475    #[test]
476    fn test_value_equality_bool() {
477        assert_eq!(Value::Bool(true), Value::Bool(true));
478        assert_ne!(Value::Bool(true), Value::Bool(false));
479    }
480
481    #[test]
482    fn test_value_equality_int() {
483        assert_eq!(Value::Int(42), Value::Int(42));
484        assert_ne!(Value::Int(42), Value::Int(43));
485    }
486
487    #[test]
488    fn test_value_equality_string() {
489        assert_eq!(
490            Value::String("test".to_string().into()),
491            Value::String("test".to_string().into())
492        );
493        assert_ne!(
494            Value::String("a".to_string().into()),
495            Value::String("b".to_string().into())
496        );
497    }
498
499    #[test]
500    fn test_value_inequality_different_types() {
501        assert_ne!(Value::Int(1), Value::Bool(true));
502        assert_ne!(Value::Null, Value::Bool(false));
503        assert_ne!(Value::String("42".to_string().into()), Value::Int(42));
504    }
505
506    #[test]
507    fn test_value_clone() {
508        let values = vec![
509            Value::Null,
510            Value::Bool(true),
511            Value::Int(42),
512            Value::Float(3.5),
513            Value::String("test".to_string().into()),
514            Value::Reference(Reference::local("id")),
515        ];
516
517        for v in values {
518            let cloned = v.clone();
519            assert_eq!(v, cloned);
520        }
521    }
522
523    #[test]
524    fn test_value_debug() {
525        let v = Value::Int(42);
526        let debug = format!("{:?}", v);
527        assert!(debug.contains("Int"));
528        assert!(debug.contains("42"));
529    }
530
531    // ==================== Edge cases ====================
532
533    #[test]
534    fn test_value_int_bounds() {
535        assert_eq!(Value::Int(i64::MAX).as_int(), Some(i64::MAX));
536        assert_eq!(Value::Int(i64::MIN).as_int(), Some(i64::MIN));
537    }
538
539    #[test]
540    fn test_value_empty_string() {
541        let v = Value::String(String::new().into());
542        assert_eq!(v.as_str(), Some(""));
543    }
544
545    #[test]
546    fn test_value_unicode_string() {
547        let v = Value::String("日本語 🎉".to_string().into());
548        assert_eq!(v.as_str(), Some("日本語 🎉"));
549    }
550
551    #[test]
552    fn test_value_float_special() {
553        let inf = Value::Float(f64::INFINITY);
554        assert!(inf.as_float().unwrap().is_infinite());
555
556        let nan = Value::Float(f64::NAN);
557        assert!(nan.as_float().unwrap().is_nan());
558    }
559
560    #[test]
561    fn test_reference_empty_id() {
562        let r = Reference::local("");
563        assert_eq!(r.id.as_ref(), "");
564        assert_eq!(r.to_ref_string(), "@");
565    }
566
567    #[test]
568    fn test_reference_with_special_chars() {
569        let r = Reference::local("id-with-hyphens-123");
570        assert_eq!(r.to_ref_string(), "@id-with-hyphens-123");
571    }
572}