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) 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)]
82pub enum Value {
83    /// Null value (~).
84    Null,
85    /// Boolean value (true/false).
86    Bool(bool),
87    /// Integer value.
88    Int(i64),
89    /// Floating-point value.
90    Float(f64),
91    /// String value (boxed to reduce enum size).
92    String(Box<str>),
93    /// Tensor (multi-dimensional array, boxed to reduce enum size).
94    Tensor(Box<Tensor>),
95    /// Reference to another node.
96    Reference(Reference),
97    /// Parsed expression from $(...) (boxed to reduce enum size).
98    Expression(Box<Expression>),
99}
100
101impl Value {
102    /// Returns true if this value is null.
103    pub fn is_null(&self) -> bool {
104        matches!(self, Self::Null)
105    }
106
107    /// Returns true if this value is a reference.
108    pub fn is_reference(&self) -> bool {
109        matches!(self, Self::Reference(_))
110    }
111
112    /// Try to get the value as a string.
113    pub fn as_str(&self) -> Option<&str> {
114        match self {
115            Self::String(s) => Some(s),
116            _ => None,
117        }
118    }
119
120    /// Try to get the value as an integer.
121    pub fn as_int(&self) -> Option<i64> {
122        match self {
123            Self::Int(n) => Some(*n),
124            _ => None,
125        }
126    }
127
128    /// Try to get the value as a float.
129    pub fn as_float(&self) -> Option<f64> {
130        match self {
131            Self::Float(n) => Some(*n),
132            Self::Int(n) => Some(*n as f64),
133            _ => None,
134        }
135    }
136
137    /// Try to get the value as a boolean.
138    pub fn as_bool(&self) -> Option<bool> {
139        match self {
140            Self::Bool(b) => Some(*b),
141            _ => None,
142        }
143    }
144
145    /// Try to get the value as a reference.
146    pub fn as_reference(&self) -> Option<&Reference> {
147        match self {
148            Self::Reference(r) => Some(r),
149            _ => None,
150        }
151    }
152}
153
154impl std::fmt::Display for Value {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self {
157            Self::Null => write!(f, "~"),
158            Self::Bool(b) => write!(f, "{}", b),
159            Self::Int(n) => write!(f, "{}", n),
160            Self::Float(n) => write!(f, "{}", n),
161            Self::String(s) => write!(f, "{}", s),
162            Self::Tensor(_) => write!(f, "[tensor]"),
163            Self::Reference(r) => write!(f, "{}", r.to_ref_string()),
164            Self::Expression(e) => write!(f, "$({})", e),
165        }
166    }
167}
168
169impl Value {
170    /// Try to get the expression if this is an Expression variant.
171    pub fn as_expression(&self) -> Option<&Expression> {
172        match self {
173            Self::Expression(e) => Some(e),
174            _ => None,
175        }
176    }
177
178    /// Attempt to coerce this value to the expected type.
179    ///
180    /// Uses lenient mode (equivalent to Standard level) by default.
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// use hedl_core::Value;
186    /// use hedl_core::types::ExpectedType;
187    ///
188    /// let value = Value::String("42".to_string().into());
189    /// let result = value.coerce_to(&ExpectedType::Int);
190    /// assert!(result.is_ok());
191    /// ```
192    pub fn coerce_to(
193        &self,
194        expected: &crate::types::ExpectedType,
195    ) -> crate::coercion::CoercionResult {
196        crate::coercion::coerce(
197            self.clone(),
198            expected,
199            crate::coercion::CoercionMode::Lenient,
200        )
201    }
202
203    /// Check if this value can be coerced to the expected type.
204    ///
205    /// Returns true if coercion would succeed (either matched or coerced).
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// use hedl_core::Value;
211    /// use hedl_core::types::ExpectedType;
212    ///
213    /// let value = Value::Int(42);
214    /// assert!(value.can_coerce_to(&ExpectedType::Float));
215    ///
216    /// let value = Value::String("42".to_string().into());
217    /// assert!(value.can_coerce_to(&ExpectedType::Int));
218    /// ```
219    pub fn can_coerce_to(&self, expected: &crate::types::ExpectedType) -> bool {
220        !matches!(
221            self.coerce_to(expected),
222            crate::coercion::CoercionResult::Failed { .. }
223        )
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    // ==================== Reference tests ====================
232
233    #[test]
234    fn test_reference_local() {
235        let r = Reference::local("user-123");
236        assert_eq!(r.type_name, None);
237        assert_eq!(r.id.as_ref(), "user-123");
238    }
239
240    #[test]
241    fn test_reference_qualified() {
242        let r = Reference::qualified("User", "123");
243        assert_eq!(r.type_name.as_deref(), Some("User"));
244        assert_eq!(r.id.as_ref(), "123");
245    }
246
247    #[test]
248    fn test_reference_to_ref_string_local() {
249        let r = Reference::local("id-1");
250        assert_eq!(r.to_ref_string(), "@id-1");
251    }
252
253    #[test]
254    fn test_reference_to_ref_string_qualified() {
255        let r = Reference::qualified("User", "id-1");
256        assert_eq!(r.to_ref_string(), "@User:id-1");
257    }
258
259    #[test]
260    fn test_reference_equality() {
261        let a = Reference::qualified("User", "1");
262        let b = Reference::qualified("User", "1");
263        assert_eq!(a, b);
264    }
265
266    #[test]
267    fn test_reference_inequality() {
268        let a = Reference::qualified("User", "1");
269        let b = Reference::qualified("Post", "1");
270        assert_ne!(a, b);
271    }
272
273    #[test]
274    fn test_reference_clone() {
275        let original = Reference::qualified("Type", "id");
276        let cloned = original.clone();
277        assert_eq!(original, cloned);
278    }
279
280    #[test]
281    fn test_reference_debug() {
282        let r = Reference::qualified("User", "abc");
283        let debug = format!("{:?}", r);
284        assert!(debug.contains("User"));
285        assert!(debug.contains("abc"));
286    }
287
288    // ==================== Value::is_* tests ====================
289
290    #[test]
291    fn test_value_is_null() {
292        assert!(Value::Null.is_null());
293        assert!(!Value::Bool(true).is_null());
294        assert!(!Value::Int(0).is_null());
295    }
296
297    #[test]
298    fn test_value_is_reference() {
299        let r = Reference::local("id");
300        assert!(Value::Reference(r).is_reference());
301        assert!(!Value::Null.is_reference());
302        assert!(!Value::String("@ref".to_string().into()).is_reference());
303    }
304
305    // ==================== Value::as_* tests ====================
306
307    #[test]
308    fn test_value_as_str() {
309        let v = Value::String("hello".to_string().into());
310        assert_eq!(v.as_str(), Some("hello"));
311        assert_eq!(Value::Null.as_str(), None);
312        assert_eq!(Value::Int(42).as_str(), None);
313    }
314
315    #[test]
316    fn test_value_as_int() {
317        assert_eq!(Value::Int(42).as_int(), Some(42));
318        assert_eq!(Value::Int(-100).as_int(), Some(-100));
319        assert_eq!(Value::Float(3.5).as_int(), None);
320        assert_eq!(Value::String("42".to_string().into()).as_int(), None);
321    }
322
323    #[test]
324    fn test_value_as_float() {
325        assert_eq!(Value::Float(3.5).as_float(), Some(3.5));
326        // Int converts to float
327        assert_eq!(Value::Int(42).as_float(), Some(42.0));
328        assert_eq!(Value::String("3.5".to_string().into()).as_float(), None);
329    }
330
331    #[test]
332    fn test_value_as_bool() {
333        assert_eq!(Value::Bool(true).as_bool(), Some(true));
334        assert_eq!(Value::Bool(false).as_bool(), Some(false));
335        assert_eq!(Value::Int(1).as_bool(), None);
336        assert_eq!(Value::String("true".to_string().into()).as_bool(), None);
337    }
338
339    #[test]
340    fn test_value_as_reference() {
341        let r = Reference::local("id");
342        let v = Value::Reference(r.clone());
343        assert_eq!(v.as_reference(), Some(&r));
344        assert_eq!(Value::Null.as_reference(), None);
345    }
346
347    #[test]
348    fn test_value_as_expression() {
349        use crate::lex::{Expression, Span};
350        let expr = Expression::Identifier {
351            name: "x".to_string(),
352            span: Span::synthetic(),
353        };
354        let v = Value::Expression(Box::new(expr.clone()));
355        assert_eq!(v.as_expression(), Some(&expr));
356        assert_eq!(Value::Null.as_expression(), None);
357    }
358
359    // ==================== Value Display tests ====================
360
361    #[test]
362    fn test_value_display_null() {
363        assert_eq!(format!("{}", Value::Null), "~");
364    }
365
366    #[test]
367    fn test_value_display_bool() {
368        assert_eq!(format!("{}", Value::Bool(true)), "true");
369        assert_eq!(format!("{}", Value::Bool(false)), "false");
370    }
371
372    #[test]
373    fn test_value_display_int() {
374        assert_eq!(format!("{}", Value::Int(42)), "42");
375        assert_eq!(format!("{}", Value::Int(-100)), "-100");
376        assert_eq!(format!("{}", Value::Int(0)), "0");
377    }
378
379    #[test]
380    fn test_value_display_float() {
381        let s = format!("{}", Value::Float(3.5));
382        assert!(s.starts_with("3.5"));
383    }
384
385    #[test]
386    fn test_value_display_string() {
387        assert_eq!(
388            format!("{}", Value::String("hello".to_string().into())),
389            "hello"
390        );
391    }
392
393    #[test]
394    fn test_value_display_reference() {
395        let r = Reference::qualified("User", "123");
396        assert_eq!(format!("{}", Value::Reference(r)), "@User:123");
397    }
398
399    #[test]
400    fn test_value_display_expression() {
401        use crate::lex::{Expression, Span};
402        let expr = Expression::Identifier {
403            name: "x".to_string(),
404            span: Span::synthetic(),
405        };
406        assert_eq!(format!("{}", Value::Expression(Box::new(expr))), "$(x)");
407    }
408
409    #[test]
410    fn test_value_display_tensor() {
411        use crate::lex::Tensor;
412        let t = Tensor::Array(vec![Tensor::Scalar(1.0), Tensor::Scalar(2.0)]);
413        assert_eq!(format!("{}", Value::Tensor(Box::new(t))), "[tensor]");
414    }
415
416    // ==================== Value equality and clone ====================
417
418    #[test]
419    fn test_value_equality_null() {
420        assert_eq!(Value::Null, Value::Null);
421    }
422
423    #[test]
424    fn test_value_equality_bool() {
425        assert_eq!(Value::Bool(true), Value::Bool(true));
426        assert_ne!(Value::Bool(true), Value::Bool(false));
427    }
428
429    #[test]
430    fn test_value_equality_int() {
431        assert_eq!(Value::Int(42), Value::Int(42));
432        assert_ne!(Value::Int(42), Value::Int(43));
433    }
434
435    #[test]
436    fn test_value_equality_string() {
437        assert_eq!(
438            Value::String("test".to_string().into()),
439            Value::String("test".to_string().into())
440        );
441        assert_ne!(
442            Value::String("a".to_string().into()),
443            Value::String("b".to_string().into())
444        );
445    }
446
447    #[test]
448    fn test_value_inequality_different_types() {
449        assert_ne!(Value::Int(1), Value::Bool(true));
450        assert_ne!(Value::Null, Value::Bool(false));
451        assert_ne!(Value::String("42".to_string().into()), Value::Int(42));
452    }
453
454    #[test]
455    fn test_value_clone() {
456        let values = vec![
457            Value::Null,
458            Value::Bool(true),
459            Value::Int(42),
460            Value::Float(3.5),
461            Value::String("test".to_string().into()),
462            Value::Reference(Reference::local("id")),
463        ];
464
465        for v in values {
466            let cloned = v.clone();
467            assert_eq!(v, cloned);
468        }
469    }
470
471    #[test]
472    fn test_value_debug() {
473        let v = Value::Int(42);
474        let debug = format!("{:?}", v);
475        assert!(debug.contains("Int"));
476        assert!(debug.contains("42"));
477    }
478
479    // ==================== Edge cases ====================
480
481    #[test]
482    fn test_value_int_bounds() {
483        assert_eq!(Value::Int(i64::MAX).as_int(), Some(i64::MAX));
484        assert_eq!(Value::Int(i64::MIN).as_int(), Some(i64::MIN));
485    }
486
487    #[test]
488    fn test_value_empty_string() {
489        let v = Value::String(String::new().into());
490        assert_eq!(v.as_str(), Some(""));
491    }
492
493    #[test]
494    fn test_value_unicode_string() {
495        let v = Value::String("日本語 🎉".to_string().into());
496        assert_eq!(v.as_str(), Some("日本語 🎉"));
497    }
498
499    #[test]
500    fn test_value_float_special() {
501        let inf = Value::Float(f64::INFINITY);
502        assert!(inf.as_float().unwrap().is_infinite());
503
504        let nan = Value::Float(f64::NAN);
505        assert!(nan.as_float().unwrap().is_nan());
506    }
507
508    #[test]
509    fn test_reference_empty_id() {
510        let r = Reference::local("");
511        assert_eq!(r.id.as_ref(), "");
512        assert_eq!(r.to_ref_string(), "@");
513    }
514
515    #[test]
516    fn test_reference_with_special_chars() {
517        let r = Reference::local("id-with-hyphens-123");
518        assert_eq!(r.to_ref_string(), "@id-with-hyphens-123");
519    }
520}