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#[derive(Debug, Clone, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct Reference {
26    /// Optional type qualifier (e.g., "User" in "@User:id").
27    pub type_name: Option<String>,
28    /// The ID being referenced.
29    pub id: String,
30}
31
32impl Reference {
33    /// Create a local reference (no type qualifier).
34    pub fn local(id: impl Into<String>) -> Self {
35        Self {
36            type_name: None,
37            id: id.into(),
38        }
39    }
40
41    /// Create a qualified reference with type name.
42    pub fn qualified(type_name: impl Into<String>, id: impl Into<String>) -> Self {
43        Self {
44            type_name: Some(type_name.into()),
45            id: id.into(),
46        }
47    }
48
49    /// Format as a reference string (with @).
50    pub fn to_ref_string(&self) -> String {
51        match &self.type_name {
52            Some(t) => format!("@{}:{}", t, self.id),
53            None => format!("@{}", self.id),
54        }
55    }
56}
57
58/// A scalar value in HEDL.
59#[derive(Debug, Clone, PartialEq)]
60pub enum Value {
61    /// Null value (~).
62    Null,
63    /// Boolean value (true/false).
64    Bool(bool),
65    /// Integer value.
66    Int(i64),
67    /// Floating-point value.
68    Float(f64),
69    /// String value.
70    String(String),
71    /// Tensor (multi-dimensional array).
72    Tensor(Tensor),
73    /// Reference to another node.
74    Reference(Reference),
75    /// Parsed expression from $(...).
76    Expression(Expression),
77}
78
79impl Value {
80    /// Returns true if this value is null.
81    pub fn is_null(&self) -> bool {
82        matches!(self, Self::Null)
83    }
84
85    /// Returns true if this value is a reference.
86    pub fn is_reference(&self) -> bool {
87        matches!(self, Self::Reference(_))
88    }
89
90    /// Try to get the value as a string.
91    pub fn as_str(&self) -> Option<&str> {
92        match self {
93            Self::String(s) => Some(s),
94            _ => None,
95        }
96    }
97
98    /// Try to get the value as an integer.
99    pub fn as_int(&self) -> Option<i64> {
100        match self {
101            Self::Int(n) => Some(*n),
102            _ => None,
103        }
104    }
105
106    /// Try to get the value as a float.
107    pub fn as_float(&self) -> Option<f64> {
108        match self {
109            Self::Float(n) => Some(*n),
110            Self::Int(n) => Some(*n as f64),
111            _ => None,
112        }
113    }
114
115    /// Try to get the value as a boolean.
116    pub fn as_bool(&self) -> Option<bool> {
117        match self {
118            Self::Bool(b) => Some(*b),
119            _ => None,
120        }
121    }
122
123    /// Try to get the value as a reference.
124    pub fn as_reference(&self) -> Option<&Reference> {
125        match self {
126            Self::Reference(r) => Some(r),
127            _ => None,
128        }
129    }
130}
131
132impl std::fmt::Display for Value {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        match self {
135            Self::Null => write!(f, "~"),
136            Self::Bool(b) => write!(f, "{}", b),
137            Self::Int(n) => write!(f, "{}", n),
138            Self::Float(n) => write!(f, "{}", n),
139            Self::String(s) => write!(f, "{}", s),
140            Self::Tensor(_) => write!(f, "[tensor]"),
141            Self::Reference(r) => write!(f, "{}", r.to_ref_string()),
142            Self::Expression(e) => write!(f, "$({})", e),
143        }
144    }
145}
146
147impl Value {
148    /// Try to get the expression if this is an Expression variant.
149    pub fn as_expression(&self) -> Option<&Expression> {
150        match self {
151            Self::Expression(e) => Some(e),
152            _ => None,
153        }
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    // ==================== Reference tests ====================
162
163    #[test]
164    fn test_reference_local() {
165        let r = Reference::local("user-123");
166        assert_eq!(r.type_name, None);
167        assert_eq!(r.id, "user-123");
168    }
169
170    #[test]
171    fn test_reference_qualified() {
172        let r = Reference::qualified("User", "123");
173        assert_eq!(r.type_name, Some("User".to_string()));
174        assert_eq!(r.id, "123");
175    }
176
177    #[test]
178    fn test_reference_to_ref_string_local() {
179        let r = Reference::local("id-1");
180        assert_eq!(r.to_ref_string(), "@id-1");
181    }
182
183    #[test]
184    fn test_reference_to_ref_string_qualified() {
185        let r = Reference::qualified("User", "id-1");
186        assert_eq!(r.to_ref_string(), "@User:id-1");
187    }
188
189    #[test]
190    fn test_reference_equality() {
191        let a = Reference::qualified("User", "1");
192        let b = Reference::qualified("User", "1");
193        assert_eq!(a, b);
194    }
195
196    #[test]
197    fn test_reference_inequality() {
198        let a = Reference::qualified("User", "1");
199        let b = Reference::qualified("Post", "1");
200        assert_ne!(a, b);
201    }
202
203    #[test]
204    fn test_reference_clone() {
205        let original = Reference::qualified("Type", "id");
206        let cloned = original.clone();
207        assert_eq!(original, cloned);
208    }
209
210    #[test]
211    fn test_reference_debug() {
212        let r = Reference::qualified("User", "abc");
213        let debug = format!("{:?}", r);
214        assert!(debug.contains("User"));
215        assert!(debug.contains("abc"));
216    }
217
218    // ==================== Value::is_* tests ====================
219
220    #[test]
221    fn test_value_is_null() {
222        assert!(Value::Null.is_null());
223        assert!(!Value::Bool(true).is_null());
224        assert!(!Value::Int(0).is_null());
225    }
226
227    #[test]
228    fn test_value_is_reference() {
229        let r = Reference::local("id");
230        assert!(Value::Reference(r).is_reference());
231        assert!(!Value::Null.is_reference());
232        assert!(!Value::String("@ref".to_string()).is_reference());
233    }
234
235    // ==================== Value::as_* tests ====================
236
237    #[test]
238    fn test_value_as_str() {
239        let v = Value::String("hello".to_string());
240        assert_eq!(v.as_str(), Some("hello"));
241        assert_eq!(Value::Null.as_str(), None);
242        assert_eq!(Value::Int(42).as_str(), None);
243    }
244
245    #[test]
246    fn test_value_as_int() {
247        assert_eq!(Value::Int(42).as_int(), Some(42));
248        assert_eq!(Value::Int(-100).as_int(), Some(-100));
249        assert_eq!(Value::Float(3.5).as_int(), None);
250        assert_eq!(Value::String("42".to_string()).as_int(), None);
251    }
252
253    #[test]
254    fn test_value_as_float() {
255        assert_eq!(Value::Float(3.5).as_float(), Some(3.5));
256        // Int converts to float
257        assert_eq!(Value::Int(42).as_float(), Some(42.0));
258        assert_eq!(Value::String("3.5".to_string()).as_float(), None);
259    }
260
261    #[test]
262    fn test_value_as_bool() {
263        assert_eq!(Value::Bool(true).as_bool(), Some(true));
264        assert_eq!(Value::Bool(false).as_bool(), Some(false));
265        assert_eq!(Value::Int(1).as_bool(), None);
266        assert_eq!(Value::String("true".to_string()).as_bool(), None);
267    }
268
269    #[test]
270    fn test_value_as_reference() {
271        let r = Reference::local("id");
272        let v = Value::Reference(r.clone());
273        assert_eq!(v.as_reference(), Some(&r));
274        assert_eq!(Value::Null.as_reference(), None);
275    }
276
277    #[test]
278    fn test_value_as_expression() {
279        use crate::lex::{Expression, Span};
280        let expr = Expression::Identifier {
281            name: "x".to_string(),
282            span: Span::default(),
283        };
284        let v = Value::Expression(expr.clone());
285        assert_eq!(v.as_expression(), Some(&expr));
286        assert_eq!(Value::Null.as_expression(), None);
287    }
288
289    // ==================== Value Display tests ====================
290
291    #[test]
292    fn test_value_display_null() {
293        assert_eq!(format!("{}", Value::Null), "~");
294    }
295
296    #[test]
297    fn test_value_display_bool() {
298        assert_eq!(format!("{}", Value::Bool(true)), "true");
299        assert_eq!(format!("{}", Value::Bool(false)), "false");
300    }
301
302    #[test]
303    fn test_value_display_int() {
304        assert_eq!(format!("{}", Value::Int(42)), "42");
305        assert_eq!(format!("{}", Value::Int(-100)), "-100");
306        assert_eq!(format!("{}", Value::Int(0)), "0");
307    }
308
309    #[test]
310    fn test_value_display_float() {
311        let s = format!("{}", Value::Float(3.5));
312        assert!(s.starts_with("3.5"));
313    }
314
315    #[test]
316    fn test_value_display_string() {
317        assert_eq!(format!("{}", Value::String("hello".to_string())), "hello");
318    }
319
320    #[test]
321    fn test_value_display_reference() {
322        let r = Reference::qualified("User", "123");
323        assert_eq!(format!("{}", Value::Reference(r)), "@User:123");
324    }
325
326    #[test]
327    fn test_value_display_expression() {
328        use crate::lex::{Expression, Span};
329        let expr = Expression::Identifier {
330            name: "x".to_string(),
331            span: Span::default(),
332        };
333        assert_eq!(format!("{}", Value::Expression(expr)), "$(x)");
334    }
335
336    #[test]
337    fn test_value_display_tensor() {
338        use crate::lex::Tensor;
339        let t = Tensor::Array(vec![Tensor::Scalar(1.0), Tensor::Scalar(2.0)]);
340        assert_eq!(format!("{}", Value::Tensor(t)), "[tensor]");
341    }
342
343    // ==================== Value equality and clone ====================
344
345    #[test]
346    fn test_value_equality_null() {
347        assert_eq!(Value::Null, Value::Null);
348    }
349
350    #[test]
351    fn test_value_equality_bool() {
352        assert_eq!(Value::Bool(true), Value::Bool(true));
353        assert_ne!(Value::Bool(true), Value::Bool(false));
354    }
355
356    #[test]
357    fn test_value_equality_int() {
358        assert_eq!(Value::Int(42), Value::Int(42));
359        assert_ne!(Value::Int(42), Value::Int(43));
360    }
361
362    #[test]
363    fn test_value_equality_string() {
364        assert_eq!(
365            Value::String("test".to_string()),
366            Value::String("test".to_string())
367        );
368        assert_ne!(
369            Value::String("a".to_string()),
370            Value::String("b".to_string())
371        );
372    }
373
374    #[test]
375    fn test_value_inequality_different_types() {
376        assert_ne!(Value::Int(1), Value::Bool(true));
377        assert_ne!(Value::Null, Value::Bool(false));
378        assert_ne!(Value::String("42".to_string()), Value::Int(42));
379    }
380
381    #[test]
382    fn test_value_clone() {
383        let values = vec![
384            Value::Null,
385            Value::Bool(true),
386            Value::Int(42),
387            Value::Float(3.5),
388            Value::String("test".to_string()),
389            Value::Reference(Reference::local("id")),
390        ];
391
392        for v in values {
393            let cloned = v.clone();
394            assert_eq!(v, cloned);
395        }
396    }
397
398    #[test]
399    fn test_value_debug() {
400        let v = Value::Int(42);
401        let debug = format!("{:?}", v);
402        assert!(debug.contains("Int"));
403        assert!(debug.contains("42"));
404    }
405
406    // ==================== Edge cases ====================
407
408    #[test]
409    fn test_value_int_bounds() {
410        assert_eq!(Value::Int(i64::MAX).as_int(), Some(i64::MAX));
411        assert_eq!(Value::Int(i64::MIN).as_int(), Some(i64::MIN));
412    }
413
414    #[test]
415    fn test_value_empty_string() {
416        let v = Value::String(String::new());
417        assert_eq!(v.as_str(), Some(""));
418    }
419
420    #[test]
421    fn test_value_unicode_string() {
422        let v = Value::String("日本語 🎉".to_string());
423        assert_eq!(v.as_str(), Some("日本語 🎉"));
424    }
425
426    #[test]
427    fn test_value_float_special() {
428        let inf = Value::Float(f64::INFINITY);
429        assert!(inf.as_float().unwrap().is_infinite());
430
431        let nan = Value::Float(f64::NAN);
432        assert!(nan.as_float().unwrap().is_nan());
433    }
434
435    #[test]
436    fn test_reference_empty_id() {
437        let r = Reference::local("");
438        assert_eq!(r.id, "");
439        assert_eq!(r.to_ref_string(), "@");
440    }
441
442    #[test]
443    fn test_reference_with_special_chars() {
444        let r = Reference::local("id-with-hyphens-123");
445        assert_eq!(r.to_ref_string(), "@id-with-hyphens-123");
446    }
447}