kcl_lib/execution/
kcl_value.rs

1use std::collections::HashMap;
2
3use anyhow::Result;
4use schemars::JsonSchema;
5use serde::Serialize;
6
7use super::types::UnitType;
8use crate::{
9    errors::KclErrorDetails,
10    execution::{
11        annotations::{SETTINGS, SETTINGS_UNIT_LENGTH},
12        types::{NumericType, PrimitiveType, RuntimeType, UnitLen},
13        EnvironmentRef, ExecState, Face, Geometry, GeometryWithImportedGeometry, Helix, ImportedGeometry, MetaSettings,
14        Metadata, Plane, Sketch, Solid, TagIdentifier,
15    },
16    parsing::ast::types::{
17        DefaultParamVal, FunctionExpression, KclNone, Literal, LiteralValue, Node, TagDeclarator, TagNode,
18    },
19    std::{args::TyF64, StdFnProps},
20    CompilationError, KclError, ModuleId, SourceRange,
21};
22
23pub type KclObjectFields = HashMap<String, KclValue>;
24
25/// Any KCL value.
26#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
27#[ts(export)]
28#[serde(tag = "type")]
29pub enum KclValue {
30    Uuid {
31        value: ::uuid::Uuid,
32        #[serde(skip)]
33        meta: Vec<Metadata>,
34    },
35    Bool {
36        value: bool,
37        #[serde(skip)]
38        meta: Vec<Metadata>,
39    },
40    Number {
41        value: f64,
42        ty: NumericType,
43        #[serde(skip)]
44        meta: Vec<Metadata>,
45    },
46    String {
47        value: String,
48        #[serde(skip)]
49        meta: Vec<Metadata>,
50    },
51    Tuple {
52        value: Vec<KclValue>,
53        #[serde(skip)]
54        meta: Vec<Metadata>,
55    },
56    // An array where all values have a shared type (not necessarily the same principal type).
57    HomArray {
58        value: Vec<KclValue>,
59        // The type of values, not the array type.
60        #[serde(skip)]
61        ty: RuntimeType,
62    },
63    Object {
64        value: KclObjectFields,
65        #[serde(skip)]
66        meta: Vec<Metadata>,
67    },
68    TagIdentifier(Box<TagIdentifier>),
69    TagDeclarator(crate::parsing::ast::types::BoxNode<TagDeclarator>),
70    Plane {
71        value: Box<Plane>,
72    },
73    Face {
74        value: Box<Face>,
75    },
76    Sketch {
77        value: Box<Sketch>,
78    },
79    Solid {
80        value: Box<Solid>,
81    },
82    Helix {
83        value: Box<Helix>,
84    },
85    ImportedGeometry(ImportedGeometry),
86    Function {
87        #[serde(serialize_with = "function_value_stub")]
88        #[ts(type = "null")]
89        value: FunctionSource,
90        #[serde(skip)]
91        meta: Vec<Metadata>,
92    },
93    Module {
94        value: ModuleId,
95        #[serde(skip)]
96        meta: Vec<Metadata>,
97    },
98    #[ts(skip)]
99    Type {
100        #[serde(skip)]
101        value: TypeDef,
102        #[serde(skip)]
103        meta: Vec<Metadata>,
104    },
105    KclNone {
106        value: KclNone,
107        #[serde(skip)]
108        meta: Vec<Metadata>,
109    },
110}
111
112fn function_value_stub<S>(_value: &FunctionSource, serializer: S) -> Result<S::Ok, S::Error>
113where
114    S: serde::Serializer,
115{
116    serializer.serialize_unit()
117}
118
119#[derive(Debug, Clone, PartialEq, Default)]
120pub enum FunctionSource {
121    #[default]
122    None,
123    Std {
124        func: crate::std::StdFn,
125        ast: crate::parsing::ast::types::BoxNode<FunctionExpression>,
126        props: StdFnProps,
127    },
128    User {
129        ast: crate::parsing::ast::types::BoxNode<FunctionExpression>,
130        settings: MetaSettings,
131        memory: EnvironmentRef,
132    },
133}
134
135impl JsonSchema for FunctionSource {
136    fn schema_name() -> String {
137        "FunctionSource".to_owned()
138    }
139
140    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
141        // TODO: Actually generate a reasonable schema.
142        gen.subschema_for::<()>()
143    }
144}
145
146#[derive(Debug, Clone, PartialEq)]
147pub enum TypeDef {
148    RustRepr(PrimitiveType, StdFnProps),
149    Alias(RuntimeType),
150}
151
152impl From<Vec<Sketch>> for KclValue {
153    fn from(mut eg: Vec<Sketch>) -> Self {
154        if eg.len() == 1 {
155            KclValue::Sketch {
156                value: Box::new(eg.pop().unwrap()),
157            }
158        } else {
159            KclValue::HomArray {
160                value: eg
161                    .into_iter()
162                    .map(|s| KclValue::Sketch { value: Box::new(s) })
163                    .collect(),
164                ty: RuntimeType::Primitive(PrimitiveType::Sketch),
165            }
166        }
167    }
168}
169
170impl From<Vec<Solid>> for KclValue {
171    fn from(mut eg: Vec<Solid>) -> Self {
172        if eg.len() == 1 {
173            KclValue::Solid {
174                value: Box::new(eg.pop().unwrap()),
175            }
176        } else {
177            KclValue::HomArray {
178                value: eg.into_iter().map(|s| KclValue::Solid { value: Box::new(s) }).collect(),
179                ty: RuntimeType::Primitive(PrimitiveType::Solid),
180            }
181        }
182    }
183}
184
185impl From<KclValue> for Vec<SourceRange> {
186    fn from(item: KclValue) -> Self {
187        match item {
188            KclValue::TagDeclarator(t) => vec![SourceRange::new(t.start, t.end, t.module_id)],
189            KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
190            KclValue::Solid { value } => to_vec_sr(&value.meta),
191            KclValue::Sketch { value } => to_vec_sr(&value.meta),
192            KclValue::Helix { value } => to_vec_sr(&value.meta),
193            KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
194            KclValue::Function { meta, .. } => to_vec_sr(&meta),
195            KclValue::Plane { value } => to_vec_sr(&value.meta),
196            KclValue::Face { value } => to_vec_sr(&value.meta),
197            KclValue::Bool { meta, .. } => to_vec_sr(&meta),
198            KclValue::Number { meta, .. } => to_vec_sr(&meta),
199            KclValue::String { meta, .. } => to_vec_sr(&meta),
200            KclValue::Tuple { meta, .. } => to_vec_sr(&meta),
201            KclValue::HomArray { value, .. } => value.iter().flat_map(Into::<Vec<SourceRange>>::into).collect(),
202            KclValue::Object { meta, .. } => to_vec_sr(&meta),
203            KclValue::Module { meta, .. } => to_vec_sr(&meta),
204            KclValue::Uuid { meta, .. } => to_vec_sr(&meta),
205            KclValue::Type { meta, .. } => to_vec_sr(&meta),
206            KclValue::KclNone { meta, .. } => to_vec_sr(&meta),
207        }
208    }
209}
210
211fn to_vec_sr(meta: &[Metadata]) -> Vec<SourceRange> {
212    meta.iter().map(|m| m.source_range).collect()
213}
214
215impl From<&KclValue> for Vec<SourceRange> {
216    fn from(item: &KclValue) -> Self {
217        match item {
218            KclValue::TagDeclarator(t) => vec![SourceRange::new(t.start, t.end, t.module_id)],
219            KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
220            KclValue::Solid { value } => to_vec_sr(&value.meta),
221            KclValue::Sketch { value } => to_vec_sr(&value.meta),
222            KclValue::Helix { value } => to_vec_sr(&value.meta),
223            KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
224            KclValue::Function { meta, .. } => to_vec_sr(meta),
225            KclValue::Plane { value } => to_vec_sr(&value.meta),
226            KclValue::Face { value } => to_vec_sr(&value.meta),
227            KclValue::Bool { meta, .. } => to_vec_sr(meta),
228            KclValue::Number { meta, .. } => to_vec_sr(meta),
229            KclValue::String { meta, .. } => to_vec_sr(meta),
230            KclValue::Uuid { meta, .. } => to_vec_sr(meta),
231            KclValue::Tuple { meta, .. } => to_vec_sr(meta),
232            KclValue::HomArray { value, .. } => value.iter().flat_map(Into::<Vec<SourceRange>>::into).collect(),
233            KclValue::Object { meta, .. } => to_vec_sr(meta),
234            KclValue::Module { meta, .. } => to_vec_sr(meta),
235            KclValue::KclNone { meta, .. } => to_vec_sr(meta),
236            KclValue::Type { meta, .. } => to_vec_sr(meta),
237        }
238    }
239}
240
241impl From<&KclValue> for SourceRange {
242    fn from(item: &KclValue) -> Self {
243        let v: Vec<_> = item.into();
244        v.into_iter().next().unwrap_or_default()
245    }
246}
247
248impl KclValue {
249    pub(crate) fn metadata(&self) -> Vec<Metadata> {
250        match self {
251            KclValue::Uuid { value: _, meta } => meta.clone(),
252            KclValue::Bool { value: _, meta } => meta.clone(),
253            KclValue::Number { meta, .. } => meta.clone(),
254            KclValue::String { value: _, meta } => meta.clone(),
255            KclValue::Tuple { value: _, meta } => meta.clone(),
256            KclValue::HomArray { value, .. } => value.iter().flat_map(|v| v.metadata()).collect(),
257            KclValue::Object { value: _, meta } => meta.clone(),
258            KclValue::TagIdentifier(x) => x.meta.clone(),
259            KclValue::TagDeclarator(x) => vec![x.metadata()],
260            KclValue::Plane { value } => value.meta.clone(),
261            KclValue::Face { value } => value.meta.clone(),
262            KclValue::Sketch { value } => value.meta.clone(),
263            KclValue::Solid { value } => value.meta.clone(),
264            KclValue::Helix { value } => value.meta.clone(),
265            KclValue::ImportedGeometry(x) => x.meta.clone(),
266            KclValue::Function { meta, .. } => meta.clone(),
267            KclValue::Module { meta, .. } => meta.clone(),
268            KclValue::KclNone { meta, .. } => meta.clone(),
269            KclValue::Type { meta, .. } => meta.clone(),
270        }
271    }
272
273    #[allow(unused)]
274    pub(crate) fn none() -> Self {
275        Self::KclNone {
276            value: Default::default(),
277            meta: Default::default(),
278        }
279    }
280
281    /// Human readable type name used in error messages.  Should not be relied
282    /// on for program logic.
283    pub(crate) fn human_friendly_type(&self) -> String {
284        self.inner_human_friendly_type(1)
285    }
286
287    fn inner_human_friendly_type(&self, max_depth: usize) -> String {
288        if let Some(pt) = self.principal_type() {
289            if max_depth > 0 {
290                // The principal type of an array uses the array's element type,
291                // which is oftentimes `any`, and that's not a helpful message. So
292                // we show the actual elements.
293                if let KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } = self {
294                    // If it's empty, we want to show the type of the array.
295                    if !value.is_empty() {
296                        // A max of 3 is good because it's common to use 3D points.
297                        let max = 3;
298                        let len = value.len();
299                        let ellipsis = if len > max { ", ..." } else { "" };
300                        let element_label = if len == 1 { "value" } else { "values" };
301                        let element_tys = value
302                            .iter()
303                            .take(max)
304                            .map(|elem| elem.inner_human_friendly_type(max_depth - 1))
305                            .collect::<Vec<_>>()
306                            .join(", ");
307                        return format!("array of {element_tys}{ellipsis} with {len} {element_label}");
308                    }
309                }
310            }
311            return pt.to_string();
312        }
313        match self {
314            KclValue::Uuid { .. } => "Unique ID (uuid)",
315            KclValue::TagDeclarator(_) => "TagDeclarator",
316            KclValue::TagIdentifier(_) => "TagIdentifier",
317            KclValue::Solid { .. } => "Solid",
318            KclValue::Sketch { .. } => "Sketch",
319            KclValue::Helix { .. } => "Helix",
320            KclValue::ImportedGeometry(_) => "ImportedGeometry",
321            KclValue::Function { .. } => "Function",
322            KclValue::Plane { .. } => "Plane",
323            KclValue::Face { .. } => "Face",
324            KclValue::Bool { .. } => "boolean (true/false value)",
325            KclValue::Number {
326                ty: NumericType::Unknown,
327                ..
328            } => "number(unknown units)",
329            KclValue::Number {
330                ty: NumericType::Known(UnitType::Length(_)),
331                ..
332            } => "number(Length)",
333            KclValue::Number {
334                ty: NumericType::Known(UnitType::Angle(_)),
335                ..
336            } => "number(Angle)",
337            KclValue::Number { .. } => "number",
338            KclValue::String { .. } => "string (text)",
339            KclValue::Tuple { .. } => "tuple (list)",
340            KclValue::HomArray { .. } => "array (list)",
341            KclValue::Object { .. } => "object",
342            KclValue::Module { .. } => "module",
343            KclValue::Type { .. } => "type",
344            KclValue::KclNone { .. } => "None",
345        }
346        .to_owned()
347    }
348
349    pub(crate) fn from_literal(literal: Node<Literal>, exec_state: &mut ExecState) -> Self {
350        let meta = vec![literal.metadata()];
351        match literal.inner.value {
352            LiteralValue::Number { value, suffix } => {
353                let ty = NumericType::from_parsed(suffix, &exec_state.mod_local.settings);
354                if let NumericType::Default { len, .. } = &ty {
355                    if !exec_state.mod_local.explicit_length_units && *len != UnitLen::Mm {
356                        exec_state.warn(
357                            CompilationError::err(
358                                literal.as_source_range(),
359                                "Project-wide units are deprecated. Prefer to use per-file default units.",
360                            )
361                            .with_suggestion(
362                                "Fix by adding per-file settings",
363                                format!("@{SETTINGS}({SETTINGS_UNIT_LENGTH} = {len})\n"),
364                                // Insert at the start of the file.
365                                Some(SourceRange::new(0, 0, literal.module_id)),
366                                crate::errors::Tag::Deprecated,
367                            ),
368                        );
369                    }
370                }
371                KclValue::Number { value, meta, ty }
372            }
373            LiteralValue::String(value) => KclValue::String { value, meta },
374            LiteralValue::Bool(value) => KclValue::Bool { value, meta },
375        }
376    }
377
378    pub(crate) fn from_default_param(param: DefaultParamVal, exec_state: &mut ExecState) -> Self {
379        match param {
380            DefaultParamVal::Literal(lit) => Self::from_literal(lit, exec_state),
381            DefaultParamVal::KclNone(value) => KclValue::KclNone {
382                value,
383                meta: Default::default(),
384            },
385        }
386    }
387
388    pub(crate) fn map_env_ref(&self, old_env: usize, new_env: usize) -> Self {
389        let mut result = self.clone();
390        if let KclValue::Function {
391            value: FunctionSource::User { ref mut memory, .. },
392            ..
393        } = result
394        {
395            memory.replace_env(old_env, new_env);
396        }
397        result
398    }
399
400    pub const fn from_number_with_type(f: f64, ty: NumericType, meta: Vec<Metadata>) -> Self {
401        Self::Number { value: f, meta, ty }
402    }
403
404    /// Put the point into a KCL value.
405    pub fn from_point2d(p: [f64; 2], ty: NumericType, meta: Vec<Metadata>) -> Self {
406        Self::Tuple {
407            value: vec![
408                Self::Number {
409                    value: p[0],
410                    meta: meta.clone(),
411                    ty: ty.clone(),
412                },
413                Self::Number {
414                    value: p[1],
415                    meta: meta.clone(),
416                    ty,
417                },
418            ],
419            meta,
420        }
421    }
422
423    pub(crate) fn as_usize(&self) -> Option<usize> {
424        match self {
425            KclValue::Number { value, .. } => crate::try_f64_to_usize(*value),
426            _ => None,
427        }
428    }
429
430    pub fn as_int(&self) -> Option<i64> {
431        match self {
432            KclValue::Number { value, .. } => crate::try_f64_to_i64(*value),
433            _ => None,
434        }
435    }
436
437    pub fn as_int_with_ty(&self) -> Option<(i64, NumericType)> {
438        match self {
439            KclValue::Number { value, ty, .. } => crate::try_f64_to_i64(*value).map(|i| (i, ty.clone())),
440            _ => None,
441        }
442    }
443
444    pub fn as_object(&self) -> Option<&KclObjectFields> {
445        match self {
446            KclValue::Object { value, .. } => Some(value),
447            _ => None,
448        }
449    }
450
451    pub fn into_object(self) -> Option<KclObjectFields> {
452        match self {
453            KclValue::Object { value, .. } => Some(value),
454            _ => None,
455        }
456    }
457
458    pub fn as_str(&self) -> Option<&str> {
459        match self {
460            KclValue::String { value, .. } => Some(value),
461            _ => None,
462        }
463    }
464
465    pub fn into_array(self) -> Vec<KclValue> {
466        match self {
467            KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => value,
468            _ => vec![self],
469        }
470    }
471
472    pub fn as_point2d(&self) -> Option<[TyF64; 2]> {
473        let value = match self {
474            KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => value,
475            _ => return None,
476        };
477
478        if value.len() != 2 {
479            return None;
480        }
481        let x = value[0].as_ty_f64()?;
482        let y = value[1].as_ty_f64()?;
483        Some([x, y])
484    }
485
486    pub fn as_point3d(&self) -> Option<[TyF64; 3]> {
487        let value = match self {
488            KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => value,
489            _ => return None,
490        };
491
492        if value.len() != 3 {
493            return None;
494        }
495        let x = value[0].as_ty_f64()?;
496        let y = value[1].as_ty_f64()?;
497        let z = value[2].as_ty_f64()?;
498        Some([x, y, z])
499    }
500
501    pub fn as_uuid(&self) -> Option<uuid::Uuid> {
502        match self {
503            KclValue::Uuid { value, .. } => Some(*value),
504            _ => None,
505        }
506    }
507
508    pub fn as_plane(&self) -> Option<&Plane> {
509        match self {
510            KclValue::Plane { value, .. } => Some(value),
511            _ => None,
512        }
513    }
514
515    pub fn as_solid(&self) -> Option<&Solid> {
516        match self {
517            KclValue::Solid { value, .. } => Some(value),
518            _ => None,
519        }
520    }
521
522    pub fn as_sketch(&self) -> Option<&Sketch> {
523        match self {
524            KclValue::Sketch { value, .. } => Some(value),
525            _ => None,
526        }
527    }
528
529    pub fn as_mut_sketch(&mut self) -> Option<&mut Sketch> {
530        match self {
531            KclValue::Sketch { value } => Some(value),
532            _ => None,
533        }
534    }
535
536    pub fn as_mut_tag(&mut self) -> Option<&mut TagIdentifier> {
537        match self {
538            KclValue::TagIdentifier(value) => Some(value),
539            _ => None,
540        }
541    }
542
543    #[cfg(test)]
544    pub fn as_f64(&self) -> Option<f64> {
545        match self {
546            KclValue::Number { value, .. } => Some(*value),
547            _ => None,
548        }
549    }
550
551    pub fn as_ty_f64(&self) -> Option<TyF64> {
552        match self {
553            KclValue::Number { value, ty, .. } => Some(TyF64::new(*value, ty.clone())),
554            _ => None,
555        }
556    }
557
558    pub fn as_bool(&self) -> Option<bool> {
559        match self {
560            KclValue::Bool { value, .. } => Some(*value),
561            _ => None,
562        }
563    }
564
565    /// If this value is of type function, return it.
566    pub fn as_function(&self) -> Option<&FunctionSource> {
567        match self {
568            KclValue::Function { value, .. } => Some(value),
569            _ => None,
570        }
571    }
572
573    /// Get a tag identifier from a memory item.
574    pub fn get_tag_identifier(&self) -> Result<TagIdentifier, KclError> {
575        match self {
576            KclValue::TagIdentifier(t) => Ok(*t.clone()),
577            _ => Err(KclError::new_semantic(KclErrorDetails::new(
578                format!("Not a tag identifier: {:?}", self),
579                self.clone().into(),
580            ))),
581        }
582    }
583
584    /// Get a tag declarator from a memory item.
585    pub fn get_tag_declarator(&self) -> Result<TagNode, KclError> {
586        match self {
587            KclValue::TagDeclarator(t) => Ok((**t).clone()),
588            _ => Err(KclError::new_semantic(KclErrorDetails::new(
589                format!("Not a tag declarator: {:?}", self),
590                self.clone().into(),
591            ))),
592        }
593    }
594
595    /// If this KCL value is a bool, retrieve it.
596    pub fn get_bool(&self) -> Result<bool, KclError> {
597        self.as_bool().ok_or_else(|| {
598            KclError::new_type(KclErrorDetails::new(
599                format!("Expected bool, found {}", self.human_friendly_type()),
600                self.into(),
601            ))
602        })
603    }
604
605    pub fn value_str(&self) -> Option<String> {
606        match self {
607            KclValue::Bool { value, .. } => Some(format!("{value}")),
608            KclValue::Number { value, .. } => Some(format!("{value}")),
609            KclValue::String { value, .. } => Some(format!("'{value}'")),
610            KclValue::Uuid { value, .. } => Some(format!("{value}")),
611            KclValue::TagDeclarator(tag) => Some(format!("${}", tag.name)),
612            KclValue::TagIdentifier(tag) => Some(format!("${}", tag.value)),
613            // TODO better Array and Object stringification
614            KclValue::Tuple { .. } => Some("[...]".to_owned()),
615            KclValue::HomArray { .. } => Some("[...]".to_owned()),
616            KclValue::Object { .. } => Some("{ ... }".to_owned()),
617            KclValue::Module { .. }
618            | KclValue::Solid { .. }
619            | KclValue::Sketch { .. }
620            | KclValue::Helix { .. }
621            | KclValue::ImportedGeometry(_)
622            | KclValue::Function { .. }
623            | KclValue::Plane { .. }
624            | KclValue::Face { .. }
625            | KclValue::KclNone { .. }
626            | KclValue::Type { .. } => None,
627        }
628    }
629}
630
631impl From<Geometry> for KclValue {
632    fn from(value: Geometry) -> Self {
633        match value {
634            Geometry::Sketch(x) => Self::Sketch { value: Box::new(x) },
635            Geometry::Solid(x) => Self::Solid { value: Box::new(x) },
636        }
637    }
638}
639
640impl From<GeometryWithImportedGeometry> for KclValue {
641    fn from(value: GeometryWithImportedGeometry) -> Self {
642        match value {
643            GeometryWithImportedGeometry::Sketch(x) => Self::Sketch { value: Box::new(x) },
644            GeometryWithImportedGeometry::Solid(x) => Self::Solid { value: Box::new(x) },
645            GeometryWithImportedGeometry::ImportedGeometry(x) => Self::ImportedGeometry(*x),
646        }
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653
654    #[test]
655    fn test_human_friendly_type() {
656        let len = KclValue::Number {
657            value: 1.0,
658            ty: NumericType::Known(UnitType::Length(UnitLen::Unknown)),
659            meta: vec![],
660        };
661        assert_eq!(len.human_friendly_type(), "number(Length)".to_string());
662
663        let unknown = KclValue::Number {
664            value: 1.0,
665            ty: NumericType::Unknown,
666            meta: vec![],
667        };
668        assert_eq!(unknown.human_friendly_type(), "number(unknown units)".to_string());
669
670        let mm = KclValue::Number {
671            value: 1.0,
672            ty: NumericType::Known(UnitType::Length(UnitLen::Mm)),
673            meta: vec![],
674        };
675        assert_eq!(mm.human_friendly_type(), "number(mm)".to_string());
676
677        let array1_mm = KclValue::HomArray {
678            value: vec![mm.clone()],
679            ty: RuntimeType::any(),
680        };
681        assert_eq!(
682            array1_mm.human_friendly_type(),
683            "array of number(mm) with 1 value".to_string()
684        );
685
686        let array2_mm = KclValue::HomArray {
687            value: vec![mm.clone(), mm.clone()],
688            ty: RuntimeType::any(),
689        };
690        assert_eq!(
691            array2_mm.human_friendly_type(),
692            "array of number(mm), number(mm) with 2 values".to_string()
693        );
694
695        let array3_mm = KclValue::HomArray {
696            value: vec![mm.clone(), mm.clone(), mm.clone()],
697            ty: RuntimeType::any(),
698        };
699        assert_eq!(
700            array3_mm.human_friendly_type(),
701            "array of number(mm), number(mm), number(mm) with 3 values".to_string()
702        );
703
704        let inches = KclValue::Number {
705            value: 1.0,
706            ty: NumericType::Known(UnitType::Length(UnitLen::Inches)),
707            meta: vec![],
708        };
709        let array4 = KclValue::HomArray {
710            value: vec![mm.clone(), mm.clone(), inches.clone(), mm.clone()],
711            ty: RuntimeType::any(),
712        };
713        assert_eq!(
714            array4.human_friendly_type(),
715            "array of number(mm), number(mm), number(in), ... with 4 values".to_string()
716        );
717
718        let empty_array = KclValue::HomArray {
719            value: vec![],
720            ty: RuntimeType::any(),
721        };
722        assert_eq!(empty_array.human_friendly_type(), "[any; 0]".to_string());
723
724        let array_nested = KclValue::HomArray {
725            value: vec![array2_mm.clone()],
726            ty: RuntimeType::any(),
727        };
728        assert_eq!(
729            array_nested.human_friendly_type(),
730            "array of [any; 2] with 1 value".to_string()
731        );
732    }
733}