Skip to main content

kcl_lib/execution/
mod.rs

1//! The executor for the AST.
2
3use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use anyhow::Result;
7pub use artifact::ArtifactCommand;
8pub(crate) use artifact::EntityCloneInfo;
9pub(crate) use artifact::named_view_artifact;
10pub(crate) use artifact::sketch_block_constraint_type;
11use cache::GlobalState;
12pub use cache::bust_cache;
13pub use cache::clear_mem_cache;
14pub use geometry::*;
15pub use id_generator::IdGenerator;
16pub(crate) use import::PreImportedGeometry;
17use indexmap::IndexMap;
18pub use kcl_api::Operation;
19pub use kcl_api::artifact::Artifact;
20pub use kcl_api::artifact::ArtifactGraph;
21pub use kcl_api::artifact::CapSubType;
22pub use kcl_api::artifact::CodeRef;
23pub use kcl_api::artifact::GdtAnnotationArtifact;
24pub use kcl_api::artifact::SketchBlock;
25pub use kcl_api::artifact::SketchBlockConstraint;
26#[allow(unused_imports)]
27pub use kcl_api::artifact::SketchBlockConstraintType;
28pub use kcl_api::artifact::StartSketchOnFace;
29pub use kcl_api::artifact::StartSketchOnPlane;
30use kcl_api::ast::node_path::NodePath;
31pub use kcl_value::KclObjectFields;
32pub use kcl_value::KclObjectKind;
33pub use kcl_value::KclValue;
34pub use kcl_value_view::KclValueView;
35use kcmc::ImageFormat;
36use kcmc::ModelingCmd;
37use kcmc::each_cmd as mcmd;
38use kcmc::ok_response::OkModelingCmdResponse;
39use kcmc::ok_response::output::TakeSnapshot;
40use kcmc::websocket::ModelingSessionData;
41use kcmc::websocket::OkWebSocketResponseData;
42use kittycad_modeling_cmds::id::ModelingCmdId;
43use kittycad_modeling_cmds::{self as kcmc};
44pub use memory::EnvironmentRef;
45#[cfg(test)]
46pub(crate) use memory::MemoryBackendKind;
47pub(crate) use modeling::ModelingCmdMeta;
48pub use named_views::*;
49use serde::Deserialize;
50use serde::Serialize;
51pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
52pub(crate) use sketch_solve::solver_numeric_type;
53pub use sketch_transpiler::pre_execute_transpile;
54pub use sketch_transpiler::transpile_all_old_sketches_to_new;
55pub use sketch_transpiler::transpile_old_sketch_to_new;
56pub use sketch_transpiler::transpile_old_sketch_to_new_ast;
57pub use sketch_transpiler::transpile_old_sketch_to_new_with_execution;
58pub(crate) use solver_arc::SolverArc;
59pub(crate) use state::ConstraintKey;
60pub(crate) use state::ConstraintState;
61pub(crate) use state::ConsumedRegionInfo;
62pub(crate) use state::ConsumedRegionOperation;
63pub(crate) use state::ConsumedSolidInfo;
64pub(crate) use state::ConsumedSolidKey;
65pub(crate) use state::ConsumedSolidOperation;
66pub use state::DirectTagFilletMeta;
67pub use state::DirectTagFilletTagEntry;
68pub use state::EdgeRefactorMeta;
69pub use state::EdgeRefactorStdlibFn;
70pub use state::ExecState;
71pub use state::KclVersion;
72pub use state::LegacyAngleRefactorMeta;
73pub use state::MetaSettings;
74pub(crate) use state::ModuleArtifactState;
75pub(crate) use state::PendingEdgeRefactorMeta;
76pub(crate) use state::PendingLegacyAngleRefactorMeta;
77pub use state::RefactorMetadata;
78pub(crate) use state::TangencyMode;
79
80use crate::CompilationIssue;
81use crate::ExecError;
82use crate::KclErrorWithOutputs;
83use crate::NodePathExt;
84use crate::SourceRange;
85use crate::collections::AhashIndexSet;
86use crate::engine::EngineBatchContext;
87use crate::engine::GridScaleBehavior;
88use crate::engine::engine_manager::EngineManager;
89use crate::errors::KclError;
90use crate::errors::KclErrorDetails;
91use crate::execution::cache::CacheInformation;
92use crate::execution::cache::CacheResult;
93use crate::execution::cad_op::OperationExt;
94use crate::execution::import_graph::Universe;
95use crate::execution::import_graph::UniverseMap;
96use crate::execution::typed_path::TypedPath;
97use crate::front::Number;
98use crate::front::Object;
99use crate::front::ObjectId;
100use crate::fs::FileManager;
101use crate::fs::FileSystemHandle;
102use crate::modules::ModuleExecutionOutcome;
103use crate::modules::ModuleId;
104use crate::modules::ModulePath;
105use crate::modules::ModuleRepr;
106use crate::parsing::ast::types::Expr;
107use crate::parsing::ast::types::ImportPath;
108use crate::parsing::ast::types::NodeRef;
109
110#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
111#[ts(export)]
112pub struct OperationsByModule {
113    pub map: IndexMap<ModuleId, Vec<Operation>>,
114}
115
116#[derive(Clone, Serialize, ts_rs::TS)]
117#[ts(export)]
118#[serde(rename_all = "camelCase")]
119pub struct OperationCallbackArgs {
120    pub module_id: ModuleId,
121    pub operation: Operation,
122    pub index: usize,
123}
124
125pub trait ExecutionCallbacks: std::fmt::Debug + Send + Sync + 'static {
126    fn on_operation(&self, _args: OperationCallbackArgs) {}
127}
128
129impl OperationsByModule {
130    pub fn count(&self) -> usize {
131        self.map.values().map(Vec::len).sum()
132    }
133
134    pub fn is_empty(&self) -> bool {
135        self.map.values().all(Vec::is_empty)
136    }
137
138    pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
139        self.map.get(module_id)
140    }
141
142    pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
143        self.map.values()
144    }
145
146    pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
147        self.map.insert(module_id, operations);
148    }
149}
150
151pub(crate) mod annotations;
152mod artifact;
153#[cfg(test)]
154pub(crate) use artifact::mermaid_tests::ArtifactGraphMermaidExt;
155pub(crate) mod cache;
156mod cad_op;
157pub(crate) mod exec_ast;
158pub mod fn_call;
159#[cfg(test)]
160mod freedom_analysis_tests;
161mod geometry;
162#[cfg(test)]
163mod hide_id_contract_kcl_test_pins;
164mod id_generator;
165mod import;
166mod import_graph;
167pub(crate) mod kcl_value;
168pub(crate) mod kcl_value_view;
169pub(crate) mod machine;
170mod memory;
171mod modeling;
172mod named_views;
173mod sketch_solve;
174mod sketch_transpiler;
175mod solver_arc;
176mod state;
177pub mod typed_path;
178pub(crate) mod types;
179
180pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
181pub(crate) const SKETCH_OBJECT_META: &str = "meta";
182pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
183
184/// Convenience macro for handling [`KclValueControlFlow`] in execution by
185/// returning early if it is some kind of early return or stripping off the
186/// control flow otherwise. If it's an early return, it's returned as a
187/// `Result::Ok`.
188macro_rules! control_continue {
189    ($control_flow:expr) => {{
190        let cf = $control_flow;
191        if cf.is_some_return() {
192            return Ok(cf);
193        } else {
194            cf.into_value()
195        }
196    }};
197}
198// Expose the macro to other modules.
199pub(crate) use control_continue;
200
201/// Convenience macro for handling [`KclValueControlFlow`] in execution by
202/// returning early if it is some kind of early return or stripping off the
203/// control flow otherwise. If it's an early return, [`EarlyReturn`] is
204/// used to return it as a `Result::Err`.
205macro_rules! early_return {
206    ($control_flow:expr) => {{
207        let cf = $control_flow;
208        if cf.is_some_return() {
209            return Err(EarlyReturn::from(cf));
210        } else {
211            cf.into_value()
212        }
213    }};
214}
215// Expose the macro to other modules.
216pub(crate) use early_return;
217
218#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
219pub enum ControlFlowKind {
220    #[default]
221    Continue,
222    Exit,
223}
224
225impl ControlFlowKind {
226    /// Returns true if this is any kind of early return.
227    pub fn is_some_return(&self) -> bool {
228        match self {
229            ControlFlowKind::Continue => false,
230            ControlFlowKind::Exit => true,
231        }
232    }
233}
234
235#[must_use = "You should always handle the control flow value when it is returned"]
236#[derive(Debug, Clone, PartialEq, Serialize)]
237pub struct KclValueControlFlow {
238    /// Use [control_continue] or [Self::into_value] to get the value.
239    value: Box<KclValue>,
240    pub control: ControlFlowKind,
241}
242
243impl KclValue {
244    pub(crate) fn continue_(self) -> KclValueControlFlow {
245        KclValueControlFlow {
246            value: Box::new(self),
247            control: ControlFlowKind::Continue,
248        }
249    }
250
251    pub(crate) fn exit(self) -> KclValueControlFlow {
252        KclValueControlFlow {
253            value: Box::new(self),
254            control: ControlFlowKind::Exit,
255        }
256    }
257}
258
259impl KclValueControlFlow {
260    /// Returns true if this is any kind of early return.
261    pub fn is_some_return(&self) -> bool {
262        self.control.is_some_return()
263    }
264
265    pub(crate) fn into_value(self) -> KclValue {
266        *self.value
267    }
268}
269
270/// A [`KclValueControlFlow`] or an error that needs to be returned early. This
271/// is useful for when functions might encounter either control flow or errors
272/// that need to bubble up early, but these aren't the primary return values of
273/// the function. We can use `EarlyReturn` as the error type in a `Result`.
274///
275/// Normally, you don't construct this directly. Use the `early_return!` macro.
276#[must_use = "You should always handle the control flow value when it is returned"]
277#[allow(clippy::large_enum_variant)]
278#[derive(Debug, Clone)]
279pub(crate) enum EarlyReturn {
280    /// A normal value with control flow.
281    Value(KclValueControlFlow),
282    /// An error that occurred during execution.
283    Error(KclError),
284}
285
286impl From<KclValueControlFlow> for EarlyReturn {
287    fn from(cf: KclValueControlFlow) -> Self {
288        EarlyReturn::Value(cf)
289    }
290}
291
292impl From<KclError> for EarlyReturn {
293    fn from(err: KclError) -> Self {
294        EarlyReturn::Error(err)
295    }
296}
297
298pub(crate) enum StatementKind<'a> {
299    Declaration { name: &'a str },
300    Expression,
301}
302
303#[derive(Debug, Clone, Copy)]
304pub enum PreserveMem {
305    Normal,
306    Always,
307}
308
309impl PreserveMem {
310    fn normal(self) -> bool {
311        match self {
312            PreserveMem::Normal => true,
313            PreserveMem::Always => false,
314        }
315    }
316}
317
318/// Outcome of executing a program.  This is used in TS.
319#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
320#[ts(export)]
321#[serde(rename_all = "camelCase")]
322pub struct ExecOutcome {
323    /// Variables in the top-level of the root module. Note that functions will have an invalid env ref.
324    pub variables: IndexMap<String, KclValueView>,
325    /// Operations that have been performed in execution order, grouped by
326    /// owning module id, for display in the Feature Tree.
327    pub operations: OperationsByModule,
328    /// Output artifact graph.
329    pub artifact_graph: ArtifactGraph,
330    /// Objects in the scene, created from execution.
331    #[serde(skip)]
332    pub scene_objects: Vec<Object>,
333    /// Map from source range to object ID for lookup of objects by their source
334    /// range.
335    #[serde(skip)]
336    pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
337    #[serde(skip)]
338    pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
339    /// Execution-backed metadata used by Z0006 and future auto-refactors.
340    pub refactor_metadata: Vec<RefactorMetadata>,
341    /// Non-fatal errors and warnings.
342    pub issues: Vec<CompilationIssue>,
343    /// File Names in module Id array index order
344    pub filenames: IndexMap<ModuleId, ModulePath>,
345    /// The default planes.
346    pub default_planes: Option<DefaultPlanes>,
347}
348
349/// Per-segment freedom used by the constraint report. Mirrors
350/// [`crate::front::Freedom`] but adds an `Error` variant for when
351/// a point lookup fails.
352#[derive(Debug, Clone, Copy, PartialEq)]
353enum SegmentFreedom {
354    Free,
355    Fixed,
356    Conflict,
357    /// A required point could not be found in the scene graph.
358    Error,
359}
360
361impl From<crate::front::Freedom> for SegmentFreedom {
362    fn from(f: crate::front::Freedom) -> Self {
363        match f {
364            crate::front::Freedom::Free => Self::Free,
365            crate::front::Freedom::Fixed => Self::Fixed,
366            crate::front::Freedom::Conflict => Self::Conflict,
367        }
368    }
369}
370
371/// Overall constraint status of a sketch.
372#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
373pub enum ConstraintKind {
374    FullyConstrained,
375    UnderConstrained,
376    OverConstrained,
377    /// Analysis could not determine constraint status (e.g., a point lookup
378    /// failed due to an inconsistent scene graph). Callers decide how to treat
379    /// this — as under-constrained, over-constrained, or something else.
380    Error,
381}
382
383/// Per-sketch summary of constraint freedom analysis.
384///
385/// A sketch with no countable segments (`total_count == 0`) is reported as
386/// [`ConstraintKind::FullyConstrained`]. This is vacuously true — there are
387/// no free or conflicting segments. Callers can check `total_count == 0` to
388/// distinguish this from a genuinely constrained sketch.
389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
390pub struct SketchConstraintStatus {
391    /// Name of the variable the sketch was assigned to, for example
392    /// "sketch001". This is the nearest enclosing declaration at the point the
393    /// sketch was created, which is not always the sketch's own name:
394    /// - Empty for a sketch written as an expression statement, because there
395    ///   is no enclosing declaration.
396    /// - The outer variable's name for a sketch passed straight into another
397    ///   call, as in `part = extrude(sketch(on = XY) { ... }, length = 10)`.
398    /// - The same name for two sketches, when a function body declares the
399    ///   sketch and is called more than once.
400    ///
401    /// This name is accepted by [`ExecOutcome::render_sketch_png`]. Because
402    /// the report carries no other sketch identifier, rendering returns an
403    /// ambiguity error when multiple sketches share a name.
404    pub name: String,
405    /// Overall constraint status derived from per-segment freedom.
406    pub status: ConstraintKind,
407    /// Number of segments that are under-constrained (free to move).
408    pub free_count: usize,
409    /// Number of segments that are over-constrained (conflicting constraints).
410    pub conflict_count: usize,
411    /// Total number of segments analyzed.
412    pub total_count: usize,
413}
414
415/// Grouped report of all sketches by constraint status.
416#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
417pub struct SketchConstraintReport {
418    pub fully_constrained: Vec<SketchConstraintStatus>,
419    pub under_constrained: Vec<SketchConstraintStatus>,
420    pub over_constrained: Vec<SketchConstraintStatus>,
421    /// Sketches where analysis encountered an error (e.g., a point lookup
422    /// failed). Callers decide how to treat these.
423    pub errors: Vec<SketchConstraintStatus>,
424}
425
426/// Compute the constraint status for a single sketch object.
427///
428/// Returns `None` if `sketch_obj` is not a sketch.
429///
430/// Note: a sketch with no countable segments (`total_count == 0`) is reported
431/// as [`ConstraintKind::FullyConstrained`]. This is vacuously true — there are
432/// no free or conflicting segments. Callers can check `total_count == 0` to
433/// distinguish this from a genuinely constrained sketch.
434pub(crate) fn sketch_constraint_status_for_sketch(
435    scene_objects: &[Object],
436    sketch_obj: &Object,
437) -> Option<SketchConstraintStatus> {
438    use crate::front::ObjectKind;
439    use crate::front::Segment;
440
441    let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
442        return None;
443    };
444
445    // Closure to look up a point's freedom by ObjectId.
446    let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
447        let obj = scene_objects.get(id.0)?;
448        if let ObjectKind::Segment {
449            segment: Segment::Point(p),
450        } = &obj.kind
451        {
452            Some(p.freedom())
453        } else {
454            None
455        }
456    };
457
458    let mut free_count: usize = 0;
459    let mut conflict_count: usize = 0;
460    let mut error_count: usize = 0;
461    let mut total_count: usize = 0;
462
463    for &seg_id in &sketch.segments {
464        let Some(seg_obj) = scene_objects.get(seg_id.0) else {
465            continue;
466        };
467        let ObjectKind::Segment { segment } = &seg_obj.kind else {
468            continue;
469        };
470        // Skip owned points — their freedom is already captured by
471        // the parent geometry (Line/Arc/Circle) that looks them up.
472        if let Segment::Point(p) = segment
473            && p.owner.is_some()
474        {
475            continue;
476        }
477        let freedom = segment
478            .freedom(lookup)
479            .map(SegmentFreedom::from)
480            .unwrap_or(SegmentFreedom::Error);
481        total_count += 1;
482        match freedom {
483            SegmentFreedom::Free => free_count += 1,
484            SegmentFreedom::Conflict => conflict_count += 1,
485            SegmentFreedom::Error => error_count += 1,
486            SegmentFreedom::Fixed => {}
487        }
488    }
489
490    let status = if error_count > 0 {
491        ConstraintKind::Error
492    } else if conflict_count > 0 {
493        ConstraintKind::OverConstrained
494    } else if free_count > 0 {
495        ConstraintKind::UnderConstrained
496    } else {
497        ConstraintKind::FullyConstrained
498    };
499
500    Some(SketchConstraintStatus {
501        name: sketch_obj.label.clone(),
502        status,
503        free_count,
504        conflict_count,
505        total_count,
506    })
507}
508
509pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
510    let mut fully_constrained = Vec::new();
511    let mut under_constrained = Vec::new();
512    let mut over_constrained = Vec::new();
513    let mut errors = Vec::new();
514    for obj in scene_objects {
515        let Some(entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
516            continue;
517        };
518        match entry.status {
519            ConstraintKind::FullyConstrained => fully_constrained.push(entry),
520            ConstraintKind::UnderConstrained => under_constrained.push(entry),
521            ConstraintKind::OverConstrained => over_constrained.push(entry),
522            ConstraintKind::Error => errors.push(entry),
523        }
524    }
525
526    SketchConstraintReport {
527        fully_constrained,
528        under_constrained,
529        over_constrained,
530        errors,
531    }
532}
533
534impl ExecOutcome {
535    pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
536        debug_assert!(
537            id.0 < self.scene_objects.len(),
538            "Requested object ID {} but only have {} objects",
539            id.0,
540            self.scene_objects.len()
541        );
542        self.scene_objects.get(id.0)
543    }
544
545    /// Returns non-fatal errors. Warnings are not included.
546    pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
547        self.issues.iter().filter(|error| error.is_err())
548    }
549
550    /// Analyze all sketches in the execution result and group them by
551    /// constraint status (fully, under, or over constrained).
552    ///
553    /// Each segment in a sketch computes its own freedom by looking up the
554    /// freedom of its constituent points. Owned points (belonging to a
555    /// Line/Arc/Circle) are skipped to avoid double-counting.
556    pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
557        sketch_constraint_report_from_scene_objects(&self.scene_objects)
558    }
559
560    /// Render one sketch from this execution result as a PNG, colored by
561    /// solver freedom.
562    pub fn render_sketch_png(
563        &self,
564        sketch_name: &str,
565    ) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
566        use crate::front::ObjectKind;
567        use crate::tooling::sketch_visualizer::SketchVisualizationError;
568
569        let sketches = self
570            .scene_objects
571            .iter()
572            .filter_map(|object| match &object.kind {
573                ObjectKind::Sketch(sketch) if object.label == sketch_name => Some(sketch),
574                _ => None,
575            })
576            .collect::<Vec<_>>();
577        let sketch = match sketches.as_slice() {
578            [] => {
579                return Err(SketchVisualizationError::SketchNotFound {
580                    name: sketch_name.to_owned(),
581                });
582            }
583            [sketch] => *sketch,
584            _ => {
585                return Err(SketchVisualizationError::AmbiguousSketchName {
586                    name: sketch_name.to_owned(),
587                    count: sketches.len(),
588                });
589            }
590        };
591
592        crate::tooling::sketch_visualizer::render_sketch_png(&self.scene_objects, sketch)
593    }
594}
595
596/// Configuration for mock execution.
597#[derive(Debug, Clone, PartialEq)]
598pub struct MockConfig {
599    pub use_prev_memory: bool,
600    /// The `ObjectId` of the sketch block to execute for sketch mode. Only the
601    /// specified sketch block will be executed. All other code is ignored.
602    pub sketch_block_id: Option<ObjectId>,
603    /// True to do more costly analysis of whether the sketch block segments are
604    /// under-constrained.
605    pub freedom_analysis: bool,
606    /// The segments that were edited that triggered this execution.
607    pub segment_ids_edited: AhashIndexSet<ObjectId>,
608    /// Segment-body drag anchors that temporarily pull a point on a segment toward the cursor.
609    pub drag_anchors: Vec<SegmentDragAnchor>,
610}
611
612#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
613#[ts(export, export_to = "FrontendApi.ts")]
614#[serde(rename_all = "camelCase")]
615pub struct SegmentDragAnchor {
616    pub segment_id: ObjectId,
617    pub target: crate::front::Point2d<Number>,
618}
619
620impl Default for MockConfig {
621    fn default() -> Self {
622        Self {
623            // By default, use previous memory. This is usually what you want.
624            use_prev_memory: true,
625            sketch_block_id: None,
626            freedom_analysis: true,
627            segment_ids_edited: AhashIndexSet::default(),
628            drag_anchors: Vec::new(),
629        }
630    }
631}
632
633impl MockConfig {
634    /// Create a new mock config for sketch mode.
635    pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
636        Self {
637            sketch_block_id: Some(sketch_block_id),
638            ..Default::default()
639        }
640    }
641
642    #[must_use]
643    pub(crate) fn no_freedom_analysis(mut self) -> Self {
644        self.freedom_analysis = false;
645        self
646    }
647}
648
649#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
650#[ts(export)]
651#[serde(rename_all = "camelCase")]
652pub struct DefaultPlanes {
653    pub xy: uuid::Uuid,
654    pub xz: uuid::Uuid,
655    pub yz: uuid::Uuid,
656    pub neg_xy: uuid::Uuid,
657    pub neg_xz: uuid::Uuid,
658    pub neg_yz: uuid::Uuid,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
662#[ts(export)]
663#[serde(tag = "type", rename_all = "camelCase")]
664pub struct TagIdentifier {
665    pub value: String,
666    // Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info
667    // was written.
668    #[serde(skip)]
669    pub info: Vec<(usize, TagEngineInfo)>,
670    #[serde(skip)]
671    pub meta: Vec<Metadata>,
672}
673
674impl TagIdentifier {
675    /// Get the tag info for this tag at a specified epoch.
676    pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
677        for (e, info) in self.info.iter().rev() {
678            if *e <= at_epoch {
679                return Some(info);
680            }
681        }
682
683        None
684    }
685
686    /// Get the most recent tag info for this tag.
687    pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
688        self.info.last().map(|i| &i.1)
689    }
690
691    /// Get all tag info entries at the most recent epoch.
692    /// For region-mapped tags, this returns multiple entries (one per region segment).
693    pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
694        let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
695            return vec![];
696        };
697        self.info
698            .iter()
699            .rev()
700            .take_while(|(e, _)| *e == cur_epoch)
701            .map(|(_, info)| info)
702            .collect()
703    }
704
705    /// Add info from a different instance of this tag.
706    pub fn merge_info(&mut self, other: &TagIdentifier) {
707        assert_eq!(&self.value, &other.value);
708        for (oe, ot) in &other.info {
709            if let Some((e, t)) = self.info.last_mut() {
710                // If there is newer info, then skip this iteration.
711                if *e > *oe {
712                    continue;
713                }
714                // If we're in the same epoch, then overwrite.
715                if e == oe {
716                    *t = ot.clone();
717                    continue;
718                }
719            }
720            self.info.push((*oe, ot.clone()));
721        }
722    }
723
724    pub fn geometry(&self) -> Option<Geometry> {
725        self.get_cur_info().map(|info| info.geometry.clone())
726    }
727
728    pub(crate) fn is_body_created_tag(&self) -> bool {
729        self.get_cur_info().is_some_and(|info| {
730            matches!(&info.geometry, Geometry::Solid(_)) && info.path.is_none() && info.surface.is_some()
731        })
732    }
733}
734
735impl Eq for TagIdentifier {}
736
737impl std::fmt::Display for TagIdentifier {
738    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
739        write!(f, "{}", self.value)
740    }
741}
742
743impl std::str::FromStr for TagIdentifier {
744    type Err = KclError;
745
746    fn from_str(s: &str) -> Result<Self, Self::Err> {
747        Ok(Self {
748            value: s.to_string(),
749            info: Vec::new(),
750            meta: Default::default(),
751        })
752    }
753}
754
755impl Ord for TagIdentifier {
756    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
757        self.value.cmp(&other.value)
758    }
759}
760
761impl PartialOrd for TagIdentifier {
762    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
763        Some(self.cmp(other))
764    }
765}
766
767impl std::hash::Hash for TagIdentifier {
768    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
769        self.value.hash(state);
770    }
771}
772
773/// Engine information for a tag.
774#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
775#[ts(export)]
776#[serde(tag = "type", rename_all = "camelCase")]
777pub struct TagEngineInfo {
778    /// The id of the tagged object.
779    pub id: uuid::Uuid,
780    /// The geometry the tag is on.
781    pub geometry: Geometry,
782    /// The path the tag is on.
783    pub path: Option<Path>,
784    /// The surface information for the tag.
785    pub surface: Option<ExtrudeSurface>,
786}
787
788#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
789pub enum BodyType {
790    Root,
791    Block,
792}
793
794/// Metadata.
795#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
796#[ts(export)]
797#[serde(rename_all = "camelCase")]
798pub struct Metadata {
799    /// The source range.
800    pub source_range: SourceRange,
801}
802
803impl From<Metadata> for Vec<SourceRange> {
804    fn from(meta: Metadata) -> Self {
805        vec![meta.source_range]
806    }
807}
808
809impl From<&Metadata> for SourceRange {
810    fn from(meta: &Metadata) -> Self {
811        meta.source_range
812    }
813}
814
815impl From<SourceRange> for Metadata {
816    fn from(source_range: SourceRange) -> Self {
817        Self { source_range }
818    }
819}
820
821impl<T> From<NodeRef<'_, T>> for Metadata {
822    fn from(node: NodeRef<'_, T>) -> Self {
823        Self {
824            source_range: SourceRange::new(node.start, node.end, node.module_id),
825        }
826    }
827}
828
829impl From<&Expr> for Metadata {
830    fn from(expr: &Expr) -> Self {
831        Self {
832            source_range: SourceRange::from(expr),
833        }
834    }
835}
836
837impl Metadata {
838    pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
839        if meta.len() == 1 {
840            let meta = &meta[0];
841            return crate::front::SourceRef::Simple {
842                range: meta.source_range,
843                node_path,
844            };
845        }
846        crate::front::SourceRef::BackTrace {
847            ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
848        }
849    }
850}
851
852/// The type of ExecutorContext being used
853#[derive(PartialEq, Debug, Default, Clone)]
854pub enum ContextType {
855    /// Live engine connection
856    #[default]
857    Live,
858
859    /// Completely mocked connection
860    /// Mock mode is only for the Design Studio when they just want to mock engine calls and not
861    /// actually make them.
862    Mock,
863
864    /// Handled by some other interpreter/conversion system
865    MockCustomForwarded,
866}
867
868/// The executor context.
869/// Cloning will return another handle to the same engine connection/session,
870/// as this uses `Arc` under the hood.
871#[derive(Clone)]
872pub struct ExecutorContext {
873    pub engine: Arc<EngineManager>,
874    pub engine_batch: EngineBatchContext,
875    pub fs: FileSystemHandle,
876    pub settings: ExecutorSettings,
877    pub context_type: ContextType,
878    pub execution_callbacks: Option<Arc<dyn ExecutionCallbacks>>,
879    /// Which executor evaluates KCL. Crate-internal: set before the first
880    /// run and immutable during execution (run methods take &self). Cloned
881    /// contexts (fresh roots, Args) inherit the same executor.
882    pub(crate) executor_kind: machine::ExecutorKind,
883    /// Call-depth limit for the machine executor's runaway-recursion guard.
884    /// Crate-internal policy, not user configuration.
885    pub(crate) machine_call_depth_limit: usize,
886}
887
888impl std::fmt::Debug for ExecutorContext {
889    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
890        f.debug_struct("ExecutorContext")
891            .field("engine", &self.engine)
892            .field("engine_batch", &self.engine_batch)
893            .field("settings", &self.settings)
894            .field("context_type", &self.context_type)
895            .field("execution_callbacks", &self.execution_callbacks)
896            .field("executor_kind", &self.executor_kind)
897            .field("machine_call_depth_limit", &self.machine_call_depth_limit)
898            .finish()
899    }
900}
901
902/// The executor settings.
903#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
904#[ts(export)]
905pub struct ExecutorSettings {
906    /// Highlight edges of 3D objects?
907    pub highlight_edges: bool,
908    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
909    pub enable_ssao: bool,
910    /// Show grid?
911    pub show_grid: bool,
912    /// Should engine store this for replay?
913    /// If so, under what name?
914    pub replay: Option<String>,
915    /// The directory of the current project.  This is used for resolving import
916    /// paths.  If None is given, the current working directory is used.
917    pub project_directory: Option<TypedPath>,
918    /// This is the path to the current file being executed.
919    /// We use this for preventing cyclic imports.
920    pub current_file: Option<TypedPath>,
921    /// Whether or not to automatically scale the grid when user zooms.
922    pub fixed_size_grid: bool,
923    /// Skip sending the engine messages that are only needed to build the
924    /// artifact graph. When this is true, the artifact graph will be
925    /// incomplete. So you should only use this option if you know you don't
926    /// need the artifact graph or anything that depends on it. In that case,
927    /// skipping these commands can make execution slightly faster.
928    #[serde(default, skip_serializing_if = "is_false")]
929    pub skip_artifact_graph: bool,
930    /// If Some(N), sends a heartbeat to keep the WebSocket active, every N seconds.
931    /// If None, no heartbeats will be sent.
932    #[serde(default, skip_serializing_if = "Option::is_none")]
933    pub heartbeats: Option<u64>,
934    /// If given, sets the default backface colour.
935    /// If not, defaults to whatever the engine's default is.
936    #[serde(default, skip_serializing_if = "Option::is_none")]
937    pub default_backface_color: Option<String>,
938}
939
940fn is_false(b: &bool) -> bool {
941    !*b
942}
943
944impl Default for ExecutorSettings {
945    fn default() -> Self {
946        Self {
947            highlight_edges: true,
948            enable_ssao: false,
949            show_grid: false,
950            replay: None,
951            project_directory: None,
952            current_file: None,
953            fixed_size_grid: true,
954            skip_artifact_graph: false,
955            heartbeats: None,
956            default_backface_color: None,
957        }
958    }
959}
960
961impl From<crate::settings::types::Configuration> for ExecutorSettings {
962    fn from(config: crate::settings::types::Configuration) -> Self {
963        Self::from(config.settings)
964    }
965}
966
967impl From<crate::settings::types::Settings> for ExecutorSettings {
968    fn from(settings: crate::settings::types::Settings) -> Self {
969        let modeling_settings = settings.modeling.unwrap_or_default();
970        Self {
971            highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
972            enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
973            show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
974            replay: None,
975            project_directory: None,
976            current_file: None,
977            fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
978            skip_artifact_graph: false,
979            heartbeats: None,
980            default_backface_color: modeling_settings.backface_color.map(|color| color.0),
981        }
982    }
983}
984
985impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
986    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
987        Self::from(config.settings.modeling)
988    }
989}
990
991impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
992    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
993        Self {
994            highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
995            enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
996            show_grid: modeling.show_scale_grid.unwrap_or_default(),
997            replay: None,
998            project_directory: None,
999            current_file: None,
1000            fixed_size_grid: true,
1001            skip_artifact_graph: false,
1002            heartbeats: None,
1003            default_backface_color: modeling.backface_color.map(|color| color.0),
1004        }
1005    }
1006}
1007
1008impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
1009    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
1010        Self {
1011            highlight_edges: modeling.highlight_edges.into(),
1012            enable_ssao: modeling.enable_ssao.into(),
1013            show_grid: Default::default(),
1014            replay: None,
1015            project_directory: None,
1016            current_file: None,
1017            fixed_size_grid: true,
1018            skip_artifact_graph: false,
1019            heartbeats: None,
1020            default_backface_color: None,
1021        }
1022    }
1023}
1024
1025impl ExecutorSettings {
1026    /// Add the current file path to the executor settings.
1027    pub fn with_current_file(&mut self, current_file: TypedPath) {
1028        // We want the parent directory of the file.
1029        if current_file.extension() == Some("kcl") {
1030            self.current_file = Some(current_file.clone());
1031            // Get the parent directory.
1032            if let Some(parent) = current_file.parent() {
1033                self.project_directory = Some(parent);
1034            } else {
1035                self.project_directory = Some(TypedPath::from(""));
1036            }
1037        } else {
1038            self.project_directory = Some(current_file);
1039        }
1040    }
1041}
1042
1043impl ExecutorContext {
1044    /// Create a new live executor context from an engine and file manager.
1045    pub fn new_with_engine_and_fs(
1046        engine: Arc<EngineManager>,
1047        fs: FileSystemHandle,
1048        settings: ExecutorSettings,
1049    ) -> Self {
1050        ExecutorContext {
1051            engine,
1052            engine_batch: EngineBatchContext::default(),
1053            fs,
1054            settings,
1055            context_type: ContextType::Live,
1056            execution_callbacks: Default::default(),
1057            executor_kind: machine::ExecutorKind::resolve(),
1058            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1059        }
1060    }
1061
1062    fn clone_with_fresh_execution_batch(&self) -> Self {
1063        Self {
1064            engine: self.engine.clone(),
1065            engine_batch: EngineBatchContext::new(),
1066            fs: self.fs.clone(),
1067            settings: self.settings.clone(),
1068            context_type: self.context_type.clone(),
1069            execution_callbacks: self.execution_callbacks.clone(),
1070            // Imported modules execute on this cloned context; keep them on
1071            // the executor selected for the run instead of the default.
1072            executor_kind: self.executor_kind,
1073            machine_call_depth_limit: self.machine_call_depth_limit,
1074        }
1075    }
1076
1077    /// Create a new live executor context from an engine using the local file manager.
1078    #[cfg(not(target_arch = "wasm32"))]
1079    pub fn new_with_engine(engine: Arc<EngineManager>, settings: ExecutorSettings) -> Self {
1080        Self::new_with_engine_and_fs(engine, crate::fs::new_file_system_handle(FileManager::new()), settings)
1081    }
1082
1083    /// Create a new default executor context.
1084    #[cfg(not(target_arch = "wasm32"))]
1085    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
1086        let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
1087        let (ws, _headers) = client
1088            .modeling()
1089            .commands_ws(kittycad::modeling::CommandsWsParams {
1090                api_call_id: None,
1091                fps: None,
1092                order_independent_transparency: None,
1093                post_effect: if settings.enable_ssao {
1094                    Some(kittycad::types::PostEffectType::Ssao)
1095                } else {
1096                    None
1097                },
1098                replay: settings.replay.clone(),
1099                show_grid: if settings.show_grid { Some(true) } else { None },
1100                pool: None,
1101                pr,
1102                unlocked_framerate: None,
1103                webrtc: Some(false),
1104                video_res_width: None,
1105                video_res_height: None,
1106            })
1107            .await?;
1108
1109        let engine_conn = EngineManager::new_websocket_transport(ws, settings.heartbeats).await;
1110        let engine = Arc::new(engine_conn);
1111
1112        Ok(Self::new_with_engine(engine, settings))
1113    }
1114
1115    #[cfg(target_arch = "wasm32")]
1116    pub fn new(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1117        Self::new_with_engine_and_fs(engine, fs, settings)
1118    }
1119
1120    #[cfg(not(target_arch = "wasm32"))]
1121    pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
1122        ExecutorContext {
1123            engine: Arc::new(EngineManager::new_mock()),
1124            engine_batch: EngineBatchContext::default(),
1125            fs: crate::fs::new_file_system_handle(FileManager::new()),
1126            settings: settings.unwrap_or_default(),
1127            context_type: ContextType::Mock,
1128            execution_callbacks: Default::default(),
1129            executor_kind: machine::ExecutorKind::resolve(),
1130            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1131        }
1132    }
1133
1134    #[cfg(target_arch = "wasm32")]
1135    pub fn new_mock(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1136        ExecutorContext {
1137            engine,
1138            engine_batch: EngineBatchContext::default(),
1139            fs,
1140            settings,
1141            context_type: ContextType::Mock,
1142            execution_callbacks: Default::default(),
1143            executor_kind: machine::ExecutorKind::resolve(),
1144            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1145        }
1146    }
1147
1148    /// Create a new mock executor context for WASM LSP servers.
1149    /// This is a convenience function that creates a mock engine and FileManager from a FileSystemManager.
1150    #[cfg(target_arch = "wasm32")]
1151    pub fn new_mock_for_lsp(
1152        fs_manager: crate::fs::wasm::FileSystemManager,
1153        settings: ExecutorSettings,
1154    ) -> Result<Self, String> {
1155        let fs = crate::fs::new_file_system_handle(FileManager::new(fs_manager));
1156
1157        Ok(ExecutorContext {
1158            engine: Arc::new(EngineManager::new_mock()),
1159            engine_batch: EngineBatchContext::default(),
1160            fs,
1161            settings,
1162            context_type: ContextType::Mock,
1163            execution_callbacks: Default::default(),
1164            executor_kind: machine::ExecutorKind::resolve(),
1165            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1166        })
1167    }
1168
1169    #[cfg(not(target_arch = "wasm32"))]
1170    pub fn new_forwarded_mock(engine: Arc<EngineManager>) -> Self {
1171        ExecutorContext {
1172            engine,
1173            engine_batch: EngineBatchContext::default(),
1174            fs: crate::fs::new_file_system_handle(FileManager::new()),
1175            settings: Default::default(),
1176            context_type: ContextType::MockCustomForwarded,
1177            execution_callbacks: Default::default(),
1178            executor_kind: machine::ExecutorKind::resolve(),
1179            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1180        }
1181    }
1182
1183    /// Create a new default executor context.
1184    /// With a kittycad client.
1185    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
1186    /// variables.
1187    /// But also allows for passing in a token and engine address directly.
1188    #[cfg(not(target_arch = "wasm32"))]
1189    pub async fn new_with_client(
1190        settings: ExecutorSettings,
1191        token: Option<String>,
1192        engine_addr: Option<String>,
1193    ) -> Result<Self> {
1194        // Create the client.
1195        let client = crate::engine::new_zoo_client(token, engine_addr)?;
1196
1197        let ctx = Self::new(&client, settings).await?;
1198        Ok(ctx)
1199    }
1200
1201    /// Create a new default executor context.
1202    /// With the default kittycad client.
1203    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
1204    /// variables.
1205    #[cfg(not(target_arch = "wasm32"))]
1206    pub async fn new_with_default_client() -> Result<Self> {
1207        // Create the client.
1208        let ctx = Self::new_with_client(Default::default(), None, None).await?;
1209        Ok(ctx)
1210    }
1211
1212    /// For executing unit tests.
1213    #[cfg(not(target_arch = "wasm32"))]
1214    pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
1215        let ctx = ExecutorContext::new_with_client(
1216            ExecutorSettings {
1217                highlight_edges: true,
1218                enable_ssao: false,
1219                show_grid: false,
1220                replay: None,
1221                project_directory: None,
1222                current_file: None,
1223                fixed_size_grid: false,
1224                skip_artifact_graph: false,
1225                heartbeats: None,
1226                default_backface_color: None,
1227            },
1228            None,
1229            engine_addr,
1230        )
1231        .await?;
1232        Ok(ctx)
1233    }
1234
1235    pub fn is_mock(&self) -> bool {
1236        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1237    }
1238
1239    /// Returns true if we should not send engine commands for any reason.
1240    pub async fn no_engine_commands(&self) -> bool {
1241        self.is_mock()
1242    }
1243
1244    pub async fn send_clear_scene(
1245        &self,
1246        exec_state: &mut ExecState,
1247        source_range: crate::execution::SourceRange,
1248    ) -> Result<(), KclError> {
1249        // Ensure artifacts are cleared so that we don't accumulate them across
1250        // runs.
1251        exec_state.mod_local.artifacts.clear();
1252        exec_state.global.root_module_artifacts.clear();
1253        exec_state.global.artifacts.clear();
1254
1255        self.engine
1256            .clear_scene(&self.engine_batch, &mut exec_state.mod_local.id_generator, source_range)
1257            .await?;
1258        // The engine errors out if you toggle OIT with SSAO off.
1259        // So ignore OIT settings if SSAO is off.
1260        if self.settings.enable_ssao {
1261            let cmd_id = exec_state.next_uuid();
1262            exec_state
1263                .batch_modeling_cmd(
1264                    ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1265                    ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1266                )
1267                .await?;
1268        }
1269        Ok(())
1270    }
1271
1272    pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1273        cache::bust_cache().await;
1274
1275        // Execute an empty program to clear and reset the scene.
1276        // We specifically want to be returned the objects after the scene is reset.
1277        // Like the default planes so it is easier to just execute an empty program
1278        // after the cache is busted.
1279        let outcome = self.run_with_caching(crate::Program::empty()).await?;
1280
1281        Ok(outcome)
1282    }
1283
1284    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1285        self.eval_prelude(exec_state, SourceRange::synthetic())
1286            .await
1287            .map_err(KclErrorWithOutputs::no_outputs)?;
1288        exec_state
1289            .mut_stack()
1290            .push_new_root_env(true)
1291            .map_err(KclErrorWithOutputs::no_outputs)?;
1292        Ok(())
1293    }
1294
1295    fn restore_mock_memory(
1296        exec_state: &mut ExecState,
1297        mem: cache::SketchModeState,
1298        _mock_config: &MockConfig,
1299    ) -> Result<(), KclErrorWithOutputs> {
1300        *exec_state.mut_stack() = mem.stack;
1301        exec_state.global.module_infos = mem.module_infos;
1302        exec_state.global.path_to_source_id = mem.path_to_source_id;
1303        exec_state.global.id_to_source = mem.id_to_source;
1304        exec_state.mod_local.constraint_state = mem.constraint_state;
1305        let len = _mock_config
1306            .sketch_block_id
1307            .map(|sketch_block_id| sketch_block_id.0)
1308            .unwrap_or(0);
1309        if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1310            exec_state
1311                .global
1312                .root_module_artifacts
1313                .restore_scene_objects(scene_objects);
1314        } else {
1315            let message = format!(
1316                "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1317                mem.scene_objects.len(),
1318                len
1319            );
1320            debug_assert!(false, "{message}");
1321            return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1322                KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1323            )));
1324        }
1325
1326        Ok(())
1327    }
1328
1329    pub async fn run_mock(
1330        &self,
1331        program: &crate::Program,
1332        mock_config: &MockConfig,
1333    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1334        assert!(
1335            self.is_mock(),
1336            "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1337        );
1338
1339        let use_prev_memory = mock_config.use_prev_memory;
1340        let mut exec_state = ExecState::new_mock(self, mock_config);
1341        if use_prev_memory {
1342            match cache::read_old_memory().await {
1343                Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
1344                None => self.prepare_mem(&mut exec_state).await?,
1345            }
1346        } else {
1347            self.prepare_mem(&mut exec_state).await?
1348        };
1349
1350        // Push a scope so that old variables can be overwritten (since we might be re-executing some
1351        // part of the scene).
1352        exec_state
1353            .mut_stack()
1354            .push_new_env_for_scope()
1355            .map_err(KclErrorWithOutputs::no_outputs)?;
1356
1357        let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1358
1359        // Restore any temporary variables, then save any newly created variables back to
1360        // memory in case another run wants to use them. Note this is just saved to the preserved
1361        // memory, not to the exec_state which is not cached for mock execution.
1362
1363        let mut stack = exec_state.stack().clone();
1364        let module_infos = exec_state.global.module_infos.clone();
1365        let path_to_source_id = exec_state.global.path_to_source_id.clone();
1366        let id_to_source = exec_state.global.id_to_source.clone();
1367        let constraint_state = exec_state.mod_local.constraint_state.clone();
1368        let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1369        let outcome = exec_state
1370            .into_exec_outcome(result.0, self)
1371            .await
1372            .map_err(KclErrorWithOutputs::no_outputs)?;
1373
1374        stack.squash_env(result.0).map_err(KclErrorWithOutputs::no_outputs)?;
1375        let state = cache::SketchModeState {
1376            stack,
1377            module_infos,
1378            path_to_source_id,
1379            id_to_source,
1380            constraint_state,
1381            scene_objects,
1382        };
1383        cache::write_old_memory(state).await;
1384
1385        Ok(outcome)
1386    }
1387
1388    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1389        assert!(!self.is_mock());
1390        let grid_scale = if self.settings.fixed_size_grid {
1391            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1392        } else {
1393            GridScaleBehavior::ScaleWithZoom
1394        };
1395
1396        let original_program = program.clone();
1397
1398        let (_program, exec_state, result) = match cache::read_old_ast().await {
1399            Some(mut cached_state) => {
1400                let old = CacheInformation {
1401                    ast: &cached_state.main.ast,
1402                    settings: &cached_state.settings,
1403                };
1404                let new = CacheInformation {
1405                    ast: &program.ast,
1406                    settings: &self.settings,
1407                };
1408
1409                // Get the program that actually changed from the old and new information.
1410                let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1411                    CacheResult::ReExecute {
1412                        clear_scene,
1413                        reapply_settings,
1414                        program: changed_program,
1415                    } => {
1416                        if reapply_settings
1417                            && self
1418                                .engine
1419                                .reapply_settings(
1420                                    &self.engine_batch,
1421                                    &self.settings,
1422                                    Default::default(),
1423                                    &mut cached_state.main.exec_state.id_generator,
1424                                    grid_scale,
1425                                )
1426                                .await
1427                                .is_err()
1428                        {
1429                            (true, program, None)
1430                        } else {
1431                            (
1432                                clear_scene,
1433                                crate::Program {
1434                                    ast: changed_program,
1435                                    original_file_contents: program.original_file_contents,
1436                                },
1437                                None,
1438                            )
1439                        }
1440                    }
1441                    CacheResult::CheckImportsOnly {
1442                        reapply_settings,
1443                        ast: changed_program,
1444                    } => {
1445                        let mut reapply_failed = false;
1446                        if reapply_settings {
1447                            if self
1448                                .engine
1449                                .reapply_settings(
1450                                    &self.engine_batch,
1451                                    &self.settings,
1452                                    Default::default(),
1453                                    &mut cached_state.main.exec_state.id_generator,
1454                                    grid_scale,
1455                                )
1456                                .await
1457                                .is_ok()
1458                            {
1459                                cache::write_old_ast(GlobalState::with_settings(
1460                                    cached_state.clone(),
1461                                    self.settings.clone(),
1462                                ))
1463                                .await;
1464                            } else {
1465                                reapply_failed = true;
1466                            }
1467                        }
1468
1469                        if reapply_failed {
1470                            (true, program, None)
1471                        } else {
1472                            // We need to check our imports to see if they changed.
1473                            let mut new_exec_state = ExecState::new(self);
1474                            let (new_universe, new_universe_map) =
1475                                self.get_universe(&program, &mut new_exec_state).await?;
1476
1477                            let clear_scene = new_universe.values().any(|value| {
1478                                let id = value.1;
1479                                match (
1480                                    cached_state.exec_state.get_source(id),
1481                                    new_exec_state.global.get_source(id),
1482                                ) {
1483                                    (Some(s0), Some(s1)) => s0.source != s1.source,
1484                                    _ => false,
1485                                }
1486                            });
1487
1488                            if !clear_scene {
1489                                // Return early we don't need to clear the scene.
1490                                cache::write_old_memory(
1491                                    cached_state
1492                                        .mock_memory_state()
1493                                        .map_err(KclErrorWithOutputs::no_outputs)?,
1494                                )
1495                                .await;
1496                                return cached_state
1497                                    .into_exec_outcome(self)
1498                                    .await
1499                                    .map_err(KclErrorWithOutputs::no_outputs);
1500                            }
1501
1502                            (
1503                                true,
1504                                crate::Program {
1505                                    ast: changed_program,
1506                                    original_file_contents: program.original_file_contents,
1507                                },
1508                                Some((new_universe, new_universe_map, new_exec_state)),
1509                            )
1510                        }
1511                    }
1512                    CacheResult::NoAction(true) => {
1513                        if self
1514                            .engine
1515                            .reapply_settings(
1516                                &self.engine_batch,
1517                                &self.settings,
1518                                Default::default(),
1519                                &mut cached_state.main.exec_state.id_generator,
1520                                grid_scale,
1521                            )
1522                            .await
1523                            .is_ok()
1524                        {
1525                            // We need to update the old ast state with the new settings!!
1526                            cache::write_old_ast(GlobalState::with_settings(
1527                                cached_state.clone(),
1528                                self.settings.clone(),
1529                            ))
1530                            .await;
1531
1532                            cache::write_old_memory(
1533                                cached_state
1534                                    .mock_memory_state()
1535                                    .map_err(KclErrorWithOutputs::no_outputs)?,
1536                            )
1537                            .await;
1538                            return cached_state
1539                                .into_exec_outcome(self)
1540                                .await
1541                                .map_err(KclErrorWithOutputs::no_outputs);
1542                        }
1543                        (true, program, None)
1544                    }
1545                    CacheResult::NoAction(false) => {
1546                        cache::write_old_memory(
1547                            cached_state
1548                                .mock_memory_state()
1549                                .map_err(KclErrorWithOutputs::no_outputs)?,
1550                        )
1551                        .await;
1552                        return cached_state
1553                            .into_exec_outcome(self)
1554                            .await
1555                            .map_err(KclErrorWithOutputs::no_outputs);
1556                    }
1557                };
1558
1559                let (exec_state, result) = match import_check_info {
1560                    Some((new_universe, new_universe_map, mut new_exec_state)) => {
1561                        // Clear the scene if the imports changed.
1562                        self.send_clear_scene(&mut new_exec_state, Default::default())
1563                            .await
1564                            .map_err(KclErrorWithOutputs::no_outputs)?;
1565
1566                        let result = self
1567                            .run_concurrent(
1568                                &program,
1569                                &mut new_exec_state,
1570                                Some((new_universe, new_universe_map)),
1571                                PreserveMem::Normal,
1572                            )
1573                            .await;
1574
1575                        (new_exec_state, result)
1576                    }
1577                    None if clear_scene => {
1578                        // Pop the execution state, since we are starting fresh.
1579                        let mut exec_state = cached_state.reconstitute_exec_state(self);
1580                        exec_state.reset(self);
1581
1582                        self.send_clear_scene(&mut exec_state, Default::default())
1583                            .await
1584                            .map_err(KclErrorWithOutputs::no_outputs)?;
1585
1586                        let result = self
1587                            .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1588                            .await;
1589
1590                        (exec_state, result)
1591                    }
1592                    None => {
1593                        let mut exec_state = cached_state.reconstitute_exec_state(self);
1594                        exec_state
1595                            .mut_stack()
1596                            .restore_env(cached_state.main.result_env)
1597                            .map_err(KclErrorWithOutputs::no_outputs)?;
1598
1599                        let result = self
1600                            .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1601                            .await;
1602
1603                        (exec_state, result)
1604                    }
1605                };
1606
1607                (program, exec_state, result)
1608            }
1609            None => {
1610                let mut exec_state = ExecState::new(self);
1611                self.send_clear_scene(&mut exec_state, Default::default())
1612                    .await
1613                    .map_err(KclErrorWithOutputs::no_outputs)?;
1614
1615                let result = self
1616                    .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1617                    .await;
1618
1619                (program, exec_state, result)
1620            }
1621        };
1622
1623        if result.is_err() {
1624            cache::bust_cache().await;
1625        }
1626
1627        // Throw the error.
1628        let result = result?;
1629
1630        // Save this as the last successful execution to the cache.
1631        // Gotcha: `CacheResult::ReExecute.program` may be diff-based, do not save that AST
1632        // the last-successful AST. Instead, save in the full AST passed in.
1633        cache::write_old_ast(GlobalState::new(
1634            exec_state.clone(),
1635            self.settings.clone(),
1636            original_program.ast,
1637            result.0,
1638        ))
1639        .await;
1640
1641        let outcome = exec_state
1642            .into_exec_outcome(result.0, self)
1643            .await
1644            .map_err(KclErrorWithOutputs::no_outputs)?;
1645        Ok(outcome)
1646    }
1647
1648    /// Perform the execution of a program.
1649    ///
1650    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
1651    pub async fn run(
1652        &self,
1653        program: &crate::Program,
1654        exec_state: &mut ExecState,
1655    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1656        self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1657            .await
1658    }
1659
1660    /// Perform the execution of a program using a concurrent
1661    /// execution model.
1662    ///
1663    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
1664    pub async fn run_concurrent(
1665        &self,
1666        program: &crate::Program,
1667        exec_state: &mut ExecState,
1668        universe_info: Option<(Universe, UniverseMap)>,
1669        preserve_mem: PreserveMem,
1670    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1671        // Reuse our cached universe if we have one.
1672
1673        let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1674            (universe, universe_map)
1675        } else {
1676            self.get_universe(program, exec_state).await?
1677        };
1678
1679        // Push ModuleInstance ops for the root module's direct imports before
1680        // child modules execute. This lets the live feature tree show module
1681        // names immediately rather than waiting for the root module body to run.
1682        // Sort by source position so they appear in source-code order (the
1683        // universe_map is a HashMap with non-deterministic iteration order).
1684        let mut sorted_imports: Vec<_> = universe_map.iter().collect();
1685        sorted_imports.sort_by_key(|(_, import_stmt)| SourceRange::from(*import_stmt));
1686        for (_path, import_stmt) in sorted_imports {
1687            // Look up by the raw import filename (e.g. "car-wheel.kcl") which
1688            // is the key format used by Universe, NOT the resolved absolute
1689            // TypedPath that UniverseMap uses as its key.
1690            let filename = match &import_stmt.path {
1691                ImportPath::Kcl { filename } => filename.to_string(),
1692                ImportPath::Foreign { path } => path.to_string(),
1693                ImportPath::Std { .. } => continue,
1694            };
1695            if let Some((_, module_id, module_path, _)) = universe.get(&filename)
1696                && let ModulePath::Local { value, .. } = module_path
1697            {
1698                let name = import_stmt
1699                    .module_name()
1700                    .unwrap_or_else(|| value.file_name().unwrap_or_default());
1701                let source_range = SourceRange::from(import_stmt);
1702                exec_state.push_op(crate::execution::cad_op::Operation::ModuleInstance {
1703                    name,
1704                    module_id: *module_id,
1705                    glob: matches!(
1706                        import_stmt.selector,
1707                        crate::parsing::ast::types::ImportSelector::Glob(_)
1708                    ),
1709                    node_path: crate::NodePath::placeholder(),
1710                    source_range,
1711                });
1712            }
1713        }
1714
1715        let default_planes = self.engine.get_default_planes().read().await.clone();
1716
1717        // Run the prelude to set up the engine.
1718        self.eval_prelude(exec_state, SourceRange::synthetic())
1719            .await
1720            .map_err(KclErrorWithOutputs::no_outputs)?;
1721
1722        for modules in import_graph::import_graph(&universe, self)
1723            .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1724            .into_iter()
1725        {
1726            #[cfg(not(target_arch = "wasm32"))]
1727            let mut set = tokio::task::JoinSet::new();
1728
1729            #[allow(clippy::type_complexity)]
1730            let (results_tx, mut results_rx): (
1731                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1732                tokio::sync::mpsc::Receiver<_>,
1733            ) = tokio::sync::mpsc::channel(1);
1734
1735            for module in modules {
1736                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1737                    return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1738                        KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1739                    )));
1740                };
1741                let module_id = *module_id;
1742                let module_path = module_path.clone();
1743                let source_range = SourceRange::from(import_stmt);
1744                // Clone before mutating.
1745                let module_exec_state = exec_state.clone();
1746
1747                let repr = repr.clone();
1748                let exec_ctxt = self.clone_with_fresh_execution_batch();
1749                let results_tx = results_tx.clone();
1750
1751                let exec_module = async |exec_ctxt: &ExecutorContext,
1752                                         repr: &ModuleRepr,
1753                                         module_id: ModuleId,
1754                                         module_path: &ModulePath,
1755                                         exec_state: &mut ExecState,
1756                                         source_range: SourceRange|
1757                       -> Result<ModuleRepr, KclError> {
1758                    match repr {
1759                        ModuleRepr::Kcl(program, _) => {
1760                            let result = exec_ctxt
1761                                .exec_module_from_ast(
1762                                    program,
1763                                    module_id,
1764                                    module_path,
1765                                    exec_state,
1766                                    source_range,
1767                                    PreserveMem::Normal,
1768                                )
1769                                .await;
1770
1771                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1772                        }
1773                        ModuleRepr::Foreign(geom, _) => {
1774                            // The concurrent executor starts from a clone of the root module state.
1775                            // Use a fresh artifact state so the import command belongs only to the
1776                            // foreign module that issued it.
1777                            exec_state.mod_local.artifacts = Default::default();
1778                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1779                                .await
1780                                .map(|geom| Some(KclValue::ImportedGeometry(geom)))
1781                                // Label the failure with the import so the
1782                                // backtrace names the foreign file (and so
1783                                // add_import_backtrace's assumption that the
1784                                // immediate frame is present holds).
1785                                .map_err(|err| err.add_import_location(&module_path.import_name(), source_range));
1786                            let module_artifacts = std::mem::take(&mut exec_state.mod_local.artifacts);
1787
1788                            result.map(|val| ModuleRepr::Foreign(geom.clone(), Some((val, module_artifacts))))
1789                        }
1790                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1791                            format!("Module {module_path} not found in universe"),
1792                            vec![source_range],
1793                        ))),
1794                    }
1795                };
1796
1797                #[cfg(target_arch = "wasm32")]
1798                {
1799                    wasm_bindgen_futures::spawn_local(async move {
1800                        let mut exec_state = module_exec_state;
1801                        let exec_ctxt = exec_ctxt;
1802
1803                        let result = exec_module(
1804                            &exec_ctxt,
1805                            &repr,
1806                            module_id,
1807                            &module_path,
1808                            &mut exec_state,
1809                            source_range,
1810                        )
1811                        .await;
1812
1813                        results_tx
1814                            .send((module_id, module_path, result))
1815                            .await
1816                            .unwrap_or_default();
1817                    });
1818                }
1819                #[cfg(not(target_arch = "wasm32"))]
1820                {
1821                    set.spawn(async move {
1822                        let mut exec_state = module_exec_state;
1823                        let exec_ctxt = exec_ctxt;
1824
1825                        let result = exec_module(
1826                            &exec_ctxt,
1827                            &repr,
1828                            module_id,
1829                            &module_path,
1830                            &mut exec_state,
1831                            source_range,
1832                        )
1833                        .await;
1834
1835                        results_tx
1836                            .send((module_id, module_path, result))
1837                            .await
1838                            .unwrap_or_default();
1839                    });
1840                }
1841            }
1842
1843            drop(results_tx);
1844
1845            while let Some((module_id, _, result)) = results_rx.recv().await {
1846                match result {
1847                    Ok(new_repr) => {
1848                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1849
1850                        match &mut repr {
1851                            ModuleRepr::Kcl(_, cache) => {
1852                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
1853                                    unreachable!();
1854                                };
1855                                *cache = session_data;
1856                            }
1857                            ModuleRepr::Foreign(_, cache) => {
1858                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
1859                                    unreachable!();
1860                                };
1861                                *cache = session_data;
1862                            }
1863                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1864                        }
1865
1866                        exec_state.global.module_infos[&module_id].restore_repr(repr);
1867                    }
1868                    Err(e) => {
1869                        let e = import_graph::add_import_backtrace(e, module_id, &universe);
1870                        return Err(exec_state.error_with_outputs(e, None, default_planes));
1871                    }
1872                }
1873            }
1874        }
1875
1876        // The early-pushed ModuleInstance operations have already served their
1877        // purpose (firing onOperation callbacks for the live feature tree).
1878        // Clear them so they don't duplicate the operations the root module
1879        // body will produce when it actually executes its import statements.
1880        exec_state.mod_local.artifacts.operations.clear();
1881
1882        // Move any remaining setup artifacts (non-operation data from the
1883        // prelude, etc.) into the root state.
1884        exec_state
1885            .global
1886            .root_module_artifacts
1887            .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1888
1889        self.inner_run(program, exec_state, preserve_mem)
1890            .await
1891            .map_err(|mut error| {
1892                // Engine rejections of async commands (e.g. foreign imports)
1893                // surface after module execution, so they miss the import
1894                // frames the eager loop attaches. Without a top-level range
1895                // the frontend cannot anchor the error in the root file;
1896                // rebuild the ancestry from the outermost range's module.
1897                let source_ranges = error.error.source_ranges();
1898                if !source_ranges.is_empty()
1899                    && !source_ranges.iter().any(|range| range.module_id().is_top_level())
1900                    && let Some(outermost) = source_ranges.last()
1901                {
1902                    error.error =
1903                        import_graph::add_import_backtrace_from(error.error.clone(), outermost.module_id(), &universe);
1904                }
1905                error
1906            })
1907    }
1908
1909    /// Get the universe & universe map of the program.
1910    /// And see if any of the imports changed.
1911    async fn get_universe(
1912        &self,
1913        program: &crate::Program,
1914        exec_state: &mut ExecState,
1915    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1916        exec_state.add_root_module_contents(program);
1917
1918        let mut universe = std::collections::HashMap::new();
1919
1920        let default_planes = self.engine.get_default_planes().read().await.clone();
1921
1922        let root_imports = import_graph::import_universe(
1923            self,
1924            &ModulePath::Main,
1925            &ModuleRepr::Kcl(program.ast.clone(), None),
1926            &mut universe,
1927            exec_state,
1928        )
1929        .await
1930        .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1931
1932        Ok((universe, root_imports))
1933    }
1934
1935    /// Perform the execution of a program.  Accept all possible parameters and
1936    /// output everything.
1937    async fn inner_run(
1938        &self,
1939        program: &crate::Program,
1940        exec_state: &mut ExecState,
1941        preserve_mem: PreserveMem,
1942    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1943        let _stats = crate::log::LogPerfStats::new("Interpretation");
1944
1945        // Re-apply the settings, in case the cache was busted.
1946        let grid_scale = if self.settings.fixed_size_grid {
1947            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1948        } else {
1949            GridScaleBehavior::ScaleWithZoom
1950        };
1951        self.engine
1952            .reapply_settings(
1953                &self.engine_batch,
1954                &self.settings,
1955                Default::default(),
1956                exec_state.id_generator(),
1957                grid_scale,
1958            )
1959            .await
1960            .map_err(KclErrorWithOutputs::no_outputs)?;
1961
1962        let default_planes = self.engine.get_default_planes().read().await.clone();
1963        let result = self
1964            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1965            .await;
1966
1967        crate::log::log(format!(
1968            "Post interpretation KCL memory stats: {:#?}",
1969            exec_state.stack().memory.stats()
1970        ));
1971        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1972
1973        /// Write the memory of an execution to the cache for reuse in mock
1974        /// execution.
1975        async fn write_old_memory(
1976            ctx: &ExecutorContext,
1977            exec_state: &ExecState,
1978            env_ref: EnvironmentRef,
1979        ) -> Result<(), KclError> {
1980            if ctx.is_mock() {
1981                return Ok(());
1982            }
1983            let mut stack = exec_state.stack().deep_clone()?;
1984            stack.restore_env(env_ref)?;
1985            let state = cache::SketchModeState {
1986                stack,
1987                module_infos: exec_state.global.module_infos.clone(),
1988                path_to_source_id: exec_state.global.path_to_source_id.clone(),
1989                id_to_source: exec_state.global.id_to_source.clone(),
1990                constraint_state: exec_state.mod_local.constraint_state.clone(),
1991                scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1992            };
1993            cache::write_old_memory(state).await;
1994            Ok(())
1995        }
1996
1997        let env_ref = match result {
1998            Ok(env_ref) => env_ref,
1999            Err((err, env_ref)) => {
2000                // Preserve memory on execution failures so follow-up mock
2001                // execution can still reuse stable IDs before the error.
2002                if let Some(env_ref) = env_ref {
2003                    write_old_memory(self, exec_state, env_ref)
2004                        .await
2005                        .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2006                }
2007                return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
2008            }
2009        };
2010
2011        write_old_memory(self, exec_state, env_ref)
2012            .await
2013            .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2014
2015        let session_data = self.engine.get_session_data().await;
2016
2017        Ok((env_ref, session_data))
2018    }
2019
2020    /// Execute an AST's program and build auxiliary outputs like the artifact
2021    /// graph.
2022    async fn execute_and_build_graph(
2023        &self,
2024        program: NodeRef<'_, crate::parsing::ast::types::Program>,
2025        exec_state: &mut ExecState,
2026        preserve_mem: PreserveMem,
2027    ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
2028        // Don't early return!  We need to build other outputs regardless of
2029        // whether execution failed.
2030
2031        // Because of execution caching, we may start with operations from a
2032        // previous run.
2033        let start_op = exec_state.global.root_module_artifacts.operations.len();
2034
2035        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
2036            .await
2037            .map_err(|e| (e, None))?;
2038
2039        let exec_result = self
2040            .exec_module_body(
2041                program,
2042                exec_state,
2043                preserve_mem,
2044                ModuleId::default(),
2045                &ModulePath::Main,
2046            )
2047            .await
2048            .map(
2049                |ModuleExecutionOutcome {
2050                     environment: env_ref,
2051                     artifacts: module_artifacts,
2052                     ..
2053                 }| {
2054                    // We need to extend because it may already have operations from
2055                    // imports.
2056                    exec_state.global.root_module_artifacts.extend(module_artifacts);
2057                    env_ref
2058                },
2059            )
2060            .map_err(|(err, env_ref, module_artifacts)| {
2061                if let Some(module_artifacts) = module_artifacts {
2062                    // We need to extend because it may already have operations
2063                    // from imports.
2064                    exec_state.global.root_module_artifacts.extend(module_artifacts);
2065                }
2066                (err, env_ref)
2067            });
2068
2069        // Fill in NodePath for operations.
2070        let programs = &exec_state.build_program_lookup(program.clone());
2071        let cached_body_items = exec_state.global.artifacts.cached_body_items();
2072        for op in exec_state
2073            .global
2074            .root_module_artifacts
2075            .operations
2076            .iter_mut()
2077            .skip(start_op)
2078        {
2079            op.fill_node_paths(programs, cached_body_items);
2080        }
2081        for module in exec_state.global.module_infos.values_mut() {
2082            if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
2083                for op in &mut outcome.artifacts.operations {
2084                    op.fill_node_paths(programs, cached_body_items);
2085                }
2086            }
2087        }
2088
2089        // Ensure all the async commands completed.
2090        self.engine
2091            .ensure_async_commands_completed(&self.engine_batch)
2092            .await
2093            .map_err(|e| {
2094                match &exec_result {
2095                    Ok(env_ref) => (e, Some(*env_ref)),
2096                    // Prefer the execution error.
2097                    Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
2098                }
2099            })?;
2100
2101        // If we errored out and early-returned, there might be commands which haven't been executed
2102        // and should be dropped.
2103        self.engine.clear_queues(&self.engine_batch).await;
2104
2105        match exec_state.build_artifact_graph(&self.engine, program).await {
2106            Ok(_) => exec_result,
2107            Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
2108        }
2109    }
2110
2111    /// 'Import' std::prelude as the outermost scope.
2112    ///
2113    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
2114    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
2115        if exec_state.stack().memory.requires_std() {
2116            let initial_ops = exec_state.mod_local.artifacts.operations.len();
2117
2118            let path = vec!["std".to_owned(), "prelude".to_owned()];
2119            let resolved_path = ModulePath::from_std_import_path(&path)?;
2120            let id = self
2121                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
2122                .await?;
2123            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
2124
2125            exec_state.mut_stack().memory.set_std(module_memory)?;
2126
2127            // Operations generated by the prelude are not useful, so clear them
2128            // out.
2129            //
2130            // TODO: Should we also clear them out of each module so that they
2131            // don't appear in test output?
2132            exec_state.mod_local.artifacts.operations.truncate(initial_ops);
2133        }
2134
2135        Ok(())
2136    }
2137
2138    /// Get a snapshot of the current scene.
2139    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
2140        // Zoom to fit.
2141        self.engine
2142            .send_modeling_cmd(
2143                &self.engine_batch,
2144                uuid::Uuid::new_v4(),
2145                crate::execution::SourceRange::default(),
2146                &ModelingCmd::from(
2147                    mcmd::ZoomToFit::builder()
2148                        .object_ids(Default::default())
2149                        .animated(false)
2150                        .padding(0.1)
2151                        .build(),
2152                ),
2153            )
2154            .await
2155            .map_err(KclErrorWithOutputs::no_outputs)?;
2156
2157        // Send a snapshot request to the engine.
2158        let resp = self
2159            .engine
2160            .send_modeling_cmd(
2161                &self.engine_batch,
2162                uuid::Uuid::new_v4(),
2163                crate::execution::SourceRange::default(),
2164                &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
2165            )
2166            .await
2167            .map_err(KclErrorWithOutputs::no_outputs)?;
2168
2169        let OkWebSocketResponseData::Modeling {
2170            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
2171        } = resp
2172        else {
2173            return Err(ExecError::BadPng(format!(
2174                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
2175            )));
2176        };
2177        Ok(contents)
2178    }
2179
2180    /// Export the current scene as a CAD file.
2181    pub async fn export(
2182        &self,
2183        format: kittycad_modeling_cmds::format::OutputFormat3d,
2184    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2185        let resp = self
2186            .engine
2187            .send_modeling_cmd(
2188                &self.engine_batch,
2189                uuid::Uuid::new_v4(),
2190                crate::SourceRange::default(),
2191                &kittycad_modeling_cmds::ModelingCmd::Export(
2192                    kittycad_modeling_cmds::Export::builder()
2193                        .entity_ids(vec![])
2194                        .format(format)
2195                        .build(),
2196                ),
2197            )
2198            .await?;
2199
2200        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
2201            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
2202                format!("Expected Export response, got {resp:?}",),
2203                vec![SourceRange::default()],
2204            )));
2205        };
2206
2207        Ok(files)
2208    }
2209
2210    /// Export the current scene as a STEP file.
2211    pub async fn export_step(
2212        &self,
2213        deterministic_time: bool,
2214    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2215        let files = self
2216            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
2217                kittycad_modeling_cmds::format::step::export::Options::builder()
2218                    .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
2219                    .maybe_created(if deterministic_time {
2220                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
2221                            KclError::new_internal(crate::errors::KclErrorDetails::new(
2222                                format!("Failed to parse date: {e}"),
2223                                vec![SourceRange::default()],
2224                            ))
2225                        })?)
2226                    } else {
2227                        None
2228                    })
2229                    .build(),
2230            ))
2231            .await?;
2232
2233        Ok(files)
2234    }
2235
2236    pub async fn close(&self) {
2237        self.engine.close().await;
2238    }
2239}
2240
2241pub use kcl_api::ArtifactId;
2242
2243pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
2244    ArtifactId::new(*id.as_ref())
2245}
2246
2247#[cfg(test)]
2248pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2249    parse_execute_with_project_dir(code, None).await
2250}
2251
2252#[cfg(test)]
2253pub(crate) async fn parse_execute_with_project_dir(
2254    code: &str,
2255    project_directory: Option<TypedPath>,
2256) -> Result<ExecTestResults, KclError> {
2257    // Differential testing: unit tests run under both executors.
2258    parse_execute_with_executor_kind(code, project_directory, machine::ExecutorKind::resolve()).await
2259}
2260
2261/// A mock-engine executor context for tests that need to inspect the context
2262/// (e.g. the engine's batch queue) even when execution fails.
2263#[cfg(test)]
2264pub(crate) fn new_mock_executor_context(
2265    project_directory: Option<TypedPath>,
2266    executor_kind: machine::ExecutorKind,
2267) -> ExecutorContext {
2268    ExecutorContext {
2269        engine: Arc::new(EngineManager::new_mock()),
2270        engine_batch: EngineBatchContext::default(),
2271        fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2272        settings: ExecutorSettings {
2273            project_directory,
2274            ..Default::default()
2275        },
2276        context_type: ContextType::Mock,
2277        execution_callbacks: Default::default(),
2278        executor_kind,
2279        machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
2280    }
2281}
2282
2283#[cfg(test)]
2284pub(crate) async fn parse_execute_with_executor_kind(
2285    code: &str,
2286    project_directory: Option<TypedPath>,
2287    executor_kind: machine::ExecutorKind,
2288) -> Result<ExecTestResults, KclError> {
2289    let program = crate::Program::parse_no_errs(code)?;
2290
2291    let exec_ctxt = new_mock_executor_context(project_directory, executor_kind);
2292    let mut exec_state = ExecState::new(&exec_ctxt);
2293    let result = exec_ctxt.run(&program, &mut exec_state).await?;
2294
2295    Ok(ExecTestResults {
2296        program,
2297        mem_env: result.0,
2298        exec_ctxt,
2299        exec_state,
2300    })
2301}
2302
2303#[cfg(test)]
2304#[derive(Debug)]
2305pub(crate) struct ExecTestResults {
2306    program: crate::Program,
2307    mem_env: EnvironmentRef,
2308    exec_ctxt: ExecutorContext,
2309    exec_state: ExecState,
2310}
2311
2312#[cfg(test)]
2313impl ExecTestResults {
2314    pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2315        &self.exec_state.global.root_module_artifacts.commands
2316    }
2317
2318    /// The diagnostics the run reported. Non-fatal issues, such as use of an
2319    /// experimental feature without the opt-in, are recorded here rather than
2320    /// returned as an error, so this is the only place a test can see them.
2321    pub(crate) fn issues(&self) -> &[CompilationIssue] {
2322        self.exec_state.issues()
2323    }
2324
2325    /// The value bound to `name` after the run. Panics when the variable is
2326    /// absent, because a test that names a variable the program does not
2327    /// declare is broken rather than failing.
2328    #[track_caller]
2329    pub(crate) fn variable(&self, name: &str) -> KclValue {
2330        self.exec_state
2331            .stack()
2332            .memory
2333            .get_from_unchecked(name, self.mem_env)
2334            .unwrap()
2335    }
2336}
2337
2338/// There are several places where we want to traverse a KCL program or find a symbol in it,
2339/// but because KCL modules can import each other, we need to traverse multiple programs.
2340/// This stores multiple programs, keyed by their module ID for quick access.
2341pub struct ProgramLookup {
2342    programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2343}
2344
2345impl ProgramLookup {
2346    // TODO: Could this store a reference to KCL programs instead of owning them?
2347    // i.e. take &state::ModuleInfoMap instead?
2348    pub fn new(
2349        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2350        module_infos: state::ModuleInfoMap,
2351    ) -> Self {
2352        let mut programs = IndexMap::with_capacity(module_infos.len());
2353        for (id, info) in module_infos {
2354            if let ModuleRepr::Kcl(program, _) = info.repr {
2355                programs.insert(id, program);
2356            }
2357        }
2358        programs.insert(ModuleId::default(), current);
2359        Self { programs }
2360    }
2361
2362    pub fn program_for_module(
2363        &self,
2364        module_id: ModuleId,
2365    ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2366        self.programs.get(&module_id)
2367    }
2368}
2369
2370#[cfg(test)]
2371mod tests {
2372    use kcl_api::NumericType;
2373    use pretty_assertions::assert_eq;
2374
2375    use super::*;
2376    use crate::ModuleId;
2377    use crate::errors::KclErrorDetails;
2378    use crate::errors::Severity;
2379    use crate::execution::memory::Stack;
2380    use crate::execution::types::RuntimeType;
2381
2382    macro_rules! kcl_input {
2383        ($file:literal) => {
2384            include_str!(concat!("../../e2e/executor/inputs/", $file, ".kcl"))
2385        };
2386    }
2387
2388    #[test]
2389    fn clone_with_fresh_execution_batch_keeps_executor_selection() {
2390        // Imported modules execute on a context created by
2391        // clone_with_fresh_execution_batch. They must stay on the executor
2392        // selected for the run instead of silently reverting to the default.
2393        let mut ctx = new_mock_executor_context(None, machine::ExecutorKind::Machine);
2394        ctx.machine_call_depth_limit = 123;
2395        let cloned = ctx.clone_with_fresh_execution_batch();
2396        assert_eq!(cloned.executor_kind, machine::ExecutorKind::Machine);
2397        assert_eq!(cloned.machine_call_depth_limit, 123);
2398    }
2399
2400    #[tokio::test(flavor = "multi_thread")]
2401    async fn concurrent_foreign_import_preserves_artifact_command() {
2402        let tmpdir = tempfile::TempDir::with_prefix("zma_foreign_import_artifact").unwrap();
2403        tokio::fs::write(tmpdir.path().join("cube.obj"), "o cube\n")
2404            .await
2405            .unwrap();
2406
2407        let program = crate::Program::parse_no_errs("import \"cube.obj\" as cube\n\nmodel = cube\n").unwrap();
2408        let ctx = new_mock_executor_context(
2409            Some(crate::TypedPath(tmpdir.path().into())),
2410            machine::ExecutorKind::resolve(),
2411        );
2412        let mut exec_state = ExecState::new(&ctx);
2413        let (main_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2414        let outcome = exec_state
2415            .into_exec_outcome(main_ref, &ctx)
2416            .await
2417            .expect("foreign import execution should produce an outcome");
2418        ctx.close().await;
2419
2420        let KclValueView::ImportedGeometry(imported) = &outcome.variables["model"] else {
2421            panic!("model should be imported geometry");
2422        };
2423        let artifact_id = ArtifactId::new(imported.id);
2424        let Some(Artifact::ImportedGeometry(artifact)) = outcome.artifact_graph.get(&artifact_id) else {
2425            panic!("foreign import should produce an imported geometry artifact");
2426        };
2427        assert_eq!(artifact.id, artifact_id);
2428        assert!(!artifact.code_ref.node_path.is_empty());
2429    }
2430
2431    #[tokio::test(flavor = "multi_thread")]
2432    async fn nested_import_preserves_inner_error_and_backtrace() {
2433        // The imported modules live in an in-memory file system under a
2434        // synthetic project directory, so parallel tests share no on-disk
2435        // state and there is nothing to clean up even if the process is
2436        // killed.
2437        let project_dir = crate::TypedPath::new("/zma-kcl-import-error");
2438        let main_path = project_dir.join("main.kcl");
2439        let assembly_path = project_dir.join("assembly.kcl");
2440        let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
2441        // Key each module by the same join that import resolution performs, so
2442        // the lookup matches on every platform.
2443        let files = [
2444            (
2445                project_dir.join("broken.kcl").to_string(),
2446                b"export brokenValue = missingName + 1\n".to_vec(),
2447            ),
2448            (
2449                assembly_path.to_string(),
2450                b"import brokenValue from \"broken.kcl\"\n\nexport assemblyValue = brokenValue\n".to_vec(),
2451            ),
2452        ]
2453        .into_iter()
2454        .collect();
2455        let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
2456        let settings = ExecutorSettings {
2457            project_directory: Some(project_dir),
2458            current_file: Some(main_path.clone()),
2459            ..Default::default()
2460        };
2461        let program = crate::Program::parse_no_errs(main_code).unwrap();
2462
2463        let assert_error = |error: &KclErrorWithOutputs| {
2464            let KclError::UndefinedValue { details, name } = &error.error else {
2465                panic!("expected UndefinedValue, got {:#?}", error.error);
2466            };
2467            assert_eq!(name.as_deref(), Some("missingName"));
2468            assert_eq!(details.message, "`missingName` is not defined");
2469            assert_eq!(
2470                error
2471                    .error
2472                    .backtrace()
2473                    .iter()
2474                    .map(|frame| frame.fn_name.as_deref())
2475                    .collect::<Vec<_>>(),
2476                [Some("import broken.kcl"), Some("import assembly.kcl"), None]
2477            );
2478            assert_eq!(
2479                error
2480                    .error
2481                    .backtrace()
2482                    .iter()
2483                    .map(|frame| frame.kind)
2484                    .collect::<Vec<_>>(),
2485                [
2486                    kcl_error::BacktraceItemKind::Import,
2487                    kcl_error::BacktraceItemKind::Import,
2488                    kcl_error::BacktraceItemKind::Call
2489                ]
2490            );
2491
2492            let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
2493            assert!(report.filename.ends_with("broken.kcl"));
2494            assert_eq!(
2495                report
2496                    .related
2497                    .iter()
2498                    .map(|related| related.filename.as_str())
2499                    .collect::<Vec<_>>(),
2500                [assembly_path.to_string(), main_path.to_string()]
2501            );
2502
2503            let rendered = format!("{:?}", miette::Report::new(report));
2504            assert!(rendered.contains("broken.kcl"));
2505            assert!(rendered.contains("assembly.kcl"));
2506            assert!(rendered.contains("main.kcl"));
2507            assert!(rendered.contains("export brokenValue = missingName + 1"));
2508            assert!(!rendered.contains("Failed to read contents"));
2509        };
2510
2511        let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
2512        mock_ctx.fs = fs.clone();
2513        let mock_error = mock_ctx
2514            .run_mock(
2515                &program,
2516                &MockConfig {
2517                    use_prev_memory: false,
2518                    ..Default::default()
2519                },
2520            )
2521            .await
2522            .unwrap_err();
2523        mock_ctx.close().await;
2524        assert_error(&mock_error);
2525
2526        let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
2527        concurrent_ctx.fs = fs;
2528        let mut exec_state = ExecState::new(&concurrent_ctx);
2529        let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
2530        concurrent_ctx.close().await;
2531        assert_error(&concurrent_error);
2532    }
2533
2534    #[tokio::test(flavor = "multi_thread")]
2535    async fn function_error_across_import_keeps_backtrace_innermost_first() {
2536        // A function defined in an imported module fails when the importing
2537        // module calls it: function frames and the import frame must stay in
2538        // one innermost-first chain.
2539        let project_dir = crate::TypedPath::new("/zma-kcl-import-fn-error");
2540        let main_path = project_dir.join("main.kcl");
2541        let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
2542        let files = [
2543            (
2544                project_dir.join("helper.kcl").to_string(),
2545                b"export fn inner() { return missingName }\nexport fn outer() { return inner() }\n".to_vec(),
2546            ),
2547            (
2548                project_dir.join("assembly.kcl").to_string(),
2549                b"import outer from \"helper.kcl\"\n\nexport assemblyValue = outer()\n".to_vec(),
2550            ),
2551        ]
2552        .into_iter()
2553        .collect();
2554        let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
2555        let settings = ExecutorSettings {
2556            project_directory: Some(project_dir.clone()),
2557            current_file: Some(main_path),
2558            ..Default::default()
2559        };
2560        let program = crate::Program::parse_no_errs(main_code).unwrap();
2561
2562        let assert_error = |error: &KclErrorWithOutputs| {
2563            assert!(
2564                matches!(&error.error, KclError::UndefinedValue { .. }),
2565                "expected UndefinedValue, got {:#?}",
2566                error.error
2567            );
2568            assert_eq!(
2569                error
2570                    .error
2571                    .backtrace()
2572                    .iter()
2573                    .map(|frame| frame.fn_name.as_deref())
2574                    .collect::<Vec<_>>(),
2575                [Some("inner"), Some("outer"), Some("import assembly.kcl"), None]
2576            );
2577            assert_eq!(
2578                error
2579                    .error
2580                    .backtrace()
2581                    .iter()
2582                    .map(|frame| frame.kind)
2583                    .collect::<Vec<_>>(),
2584                [
2585                    kcl_error::BacktraceItemKind::Call,
2586                    kcl_error::BacktraceItemKind::Call,
2587                    kcl_error::BacktraceItemKind::Import,
2588                    kcl_error::BacktraceItemKind::Call
2589                ]
2590            );
2591
2592            let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
2593            assert!(report.filename.ends_with("helper.kcl"));
2594            assert_eq!(
2595                report
2596                    .related
2597                    .iter()
2598                    .map(|related| related.filename.as_str())
2599                    .collect::<Vec<_>>(),
2600                [
2601                    project_dir.join("assembly.kcl").to_string(),
2602                    project_dir.join("main.kcl").to_string()
2603                ]
2604            );
2605            let rendered = format!("{:?}", miette::Report::new(report));
2606            assert!(rendered.contains("return missingName"));
2607            assert!(!rendered.contains("Failed to read contents"));
2608        };
2609
2610        let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
2611        mock_ctx.fs = fs.clone();
2612        let mock_error = mock_ctx
2613            .run_mock(
2614                &program,
2615                &MockConfig {
2616                    use_prev_memory: false,
2617                    ..Default::default()
2618                },
2619            )
2620            .await
2621            .unwrap_err();
2622        mock_ctx.close().await;
2623        assert_error(&mock_error);
2624
2625        let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
2626        concurrent_ctx.fs = fs;
2627        let mut exec_state = ExecState::new(&concurrent_ctx);
2628        let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
2629        concurrent_ctx.close().await;
2630        assert_error(&concurrent_error);
2631    }
2632
2633    /// Convenience function to get a JSON value from memory and unwrap.
2634    #[track_caller]
2635    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2636        memory.memory.get_from_unchecked(name, env).unwrap()
2637    }
2638
2639    async fn execute_variables_with_backend(
2640        code: &str,
2641        backend: memory::MemoryBackendKind,
2642    ) -> IndexMap<String, KclValueView> {
2643        execute_outcome_with_backend(code, backend).await.variables
2644    }
2645
2646    async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
2647        let program = crate::Program::parse_no_errs(code).unwrap();
2648        let ctx = ExecutorContext::new_mock(None).await;
2649        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2650        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2651        let outcome = exec_state
2652            .into_exec_outcome(env_ref, &ctx)
2653            .await
2654            .expect("test execution outcome should collect variables");
2655        ctx.close().await;
2656        outcome
2657    }
2658
2659    async fn execute_error_variables_with_backend(
2660        code: &str,
2661        backend: memory::MemoryBackendKind,
2662    ) -> IndexMap<String, KclValueView> {
2663        let program = crate::Program::parse_no_errs(code).unwrap();
2664        let ctx = ExecutorContext::new_mock(None).await;
2665        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2666        let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
2667        ctx.close().await;
2668        error.variables
2669    }
2670
2671    async fn execute_project_variables_with_backend(
2672        main_code: &str,
2673        files: &[(&str, &str)],
2674        backend: memory::MemoryBackendKind,
2675    ) -> IndexMap<String, KclValueView> {
2676        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
2677        for (name, contents) in files {
2678            tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
2679        }
2680
2681        let program = crate::Program::parse_no_errs(main_code).unwrap();
2682        let ctx = ExecutorContext {
2683            engine: Arc::new(EngineManager::new_mock()),
2684            engine_batch: EngineBatchContext::default(),
2685            fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2686            settings: ExecutorSettings {
2687                project_directory: Some(crate::TypedPath(tmpdir.path().into())),
2688                ..Default::default()
2689            },
2690            context_type: ContextType::Mock,
2691            execution_callbacks: Default::default(),
2692            executor_kind: machine::ExecutorKind::resolve(),
2693            machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
2694        };
2695        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2696        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2697        let outcome = exec_state
2698            .into_exec_outcome(env_ref, &ctx)
2699            .await
2700            .expect("test execution outcome should collect variables");
2701        ctx.close().await;
2702        outcome.variables
2703    }
2704
2705    async fn run_with_caching_variables_with_backend(
2706        code: &str,
2707        backend: memory::MemoryBackendKind,
2708    ) -> IndexMap<String, KclValueView> {
2709        let _backend = memory::MemoryBackendKind::override_for_test(backend);
2710        cache::bust_cache().await;
2711        clear_mem_cache().await;
2712
2713        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
2714        let program = crate::Program::parse_no_errs(code).unwrap();
2715        ctx.run_with_caching(program.clone()).await.unwrap();
2716        let cached = ctx.run_with_caching(program).await.unwrap();
2717
2718        cache::bust_cache().await;
2719        clear_mem_cache().await;
2720        ctx.close().await;
2721        cached.variables
2722    }
2723
2724    async fn run_mock_variables_with_backend(
2725        code: &str,
2726        backend: memory::MemoryBackendKind,
2727    ) -> IndexMap<String, KclValueView> {
2728        let _backend = memory::MemoryBackendKind::override_for_test(backend);
2729        clear_mem_cache().await;
2730
2731        let ctx = ExecutorContext::new_mock(None).await;
2732        let first = crate::Program::parse_no_errs("x = 2").unwrap();
2733        ctx.run_mock(
2734            &first,
2735            &MockConfig {
2736                use_prev_memory: false,
2737                ..Default::default()
2738            },
2739        )
2740        .await
2741        .unwrap();
2742
2743        let program = crate::Program::parse_no_errs(code).unwrap();
2744        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
2745
2746        clear_mem_cache().await;
2747        ctx.close().await;
2748        outcome.variables
2749    }
2750
2751    fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
2752        let mut keys = variables.keys().cloned().collect::<Vec<_>>();
2753        keys.sort();
2754        keys
2755    }
2756
2757    async fn collect_backend_results<T, Fut>(
2758        mut run: impl FnMut(memory::MemoryBackendKind) -> Fut,
2759    ) -> Vec<(memory::MemoryBackendKind, T)>
2760    where
2761        Fut: std::future::Future<Output = T>,
2762    {
2763        let all = memory::MemoryBackendKind::all();
2764        let mut results = Vec::with_capacity(all.len());
2765        for &kind in all {
2766            results.push((kind, run(kind).await));
2767        }
2768        results
2769    }
2770
2771    fn assert_backend_results_match<T>(results: &[(memory::MemoryBackendKind, T)])
2772    where
2773        T: std::fmt::Debug + PartialEq,
2774    {
2775        let (first, rest) = results.split_first().expect("expected at least one memory backend");
2776        let (first_kind, first_result) = first;
2777        for (kind, result) in rest {
2778            assert_eq!(
2779                result, first_result,
2780                "memory kind {kind:?} doesn't match {first_kind:?}"
2781            );
2782        }
2783    }
2784
2785    fn assert_backend_variable_results_match_expected_keys(
2786        results: &[(memory::MemoryBackendKind, IndexMap<String, KclValueView>)],
2787        expected_keys: &[&str],
2788    ) {
2789        let (first_kind, first_variables) = results.first().expect("expected at least one memory backend");
2790        let expected_keys = expected_keys.iter().map(|key| (*key).to_owned()).collect::<Vec<_>>();
2791        assert_eq!(
2792            sorted_variable_keys(first_variables),
2793            expected_keys,
2794            "memory kind {first_kind:?} doesn't match expected variables"
2795        );
2796        assert_backend_results_match(results);
2797    }
2798
2799    fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
2800        let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
2801        let KclValueView::Number { value, .. } = value else {
2802            panic!("expected `{key}` to be a number, got {value:?}");
2803        };
2804        assert_eq!(*value, expected, "{key}: {value:?}");
2805    }
2806
2807    #[tokio::test(flavor = "multi_thread")]
2808    async fn exec_outcome_variables_match_between_memory_backends() {
2809        let code = "x = 2\ny = x + 1\narr = [x, y]";
2810
2811        let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2812
2813        assert_backend_variable_results_match_expected_keys(&results, &["arr", "x", "y"]);
2814    }
2815
2816    #[tokio::test(flavor = "multi_thread")]
2817    async fn error_output_variables_match_between_memory_backends() {
2818        let code = "x = 2\ny = missing + 1";
2819
2820        let results = collect_backend_results(|kind| execute_error_variables_with_backend(code, kind)).await;
2821
2822        assert_backend_variable_results_match_expected_keys(&results, &["x"]);
2823    }
2824
2825    #[tokio::test(flavor = "multi_thread")]
2826    async fn cached_execution_variables_match_between_memory_backends() {
2827        let code = "x = 2\ny = x + 1";
2828
2829        let results = collect_backend_results(|kind| run_with_caching_variables_with_backend(code, kind)).await;
2830
2831        assert_backend_variable_results_match_expected_keys(&results, &["x", "y"]);
2832    }
2833
2834    #[tokio::test(flavor = "multi_thread")]
2835    async fn mock_execution_variables_match_between_memory_backends() {
2836        let code = "y = x + 1";
2837
2838        let results = collect_backend_results(|kind| run_mock_variables_with_backend(code, kind)).await;
2839
2840        assert_backend_variable_results_match_expected_keys(&results, &["y"]);
2841    }
2842
2843    #[tokio::test(flavor = "multi_thread")]
2844    async fn module_imports_and_exported_closures_match_between_memory_backends() {
2845        let module_code = r#"
2846export base = 40
2847
2848export fn addBase(n) {
2849  return n + base
2850}
2851"#;
2852        let main_code = r#"
2853import base, addBase from 'math.kcl'
2854import 'math.kcl'
2855
2856named = addBase(n = 2)
2857qualified = math::addBase(n = 1)
2858direct = math::base
2859"#;
2860
2861        let files = [("math.kcl", module_code)];
2862        let results =
2863            collect_backend_results(|kind| execute_project_variables_with_backend(main_code, &files, kind)).await;
2864
2865        let (_, first_variables) = results.first().expect("expected at least one memory backend");
2866        assert_number_variable(first_variables, "named", 42.0);
2867        assert_number_variable(first_variables, "qualified", 41.0);
2868        assert_number_variable(first_variables, "direct", 40.0);
2869        assert_backend_results_match(&results);
2870    }
2871
2872    #[tokio::test(flavor = "multi_thread")]
2873    async fn sketch_block_variables_match_between_memory_backends() {
2874        let code = r#"
2875sketch001 = sketch(on = XY) {
2876  line1 = line(start = [0, 0], end = [1, 0])
2877  line2 = line(start = [1, 0], end = [0, 1])
2878}
2879lineCount = 2
2880"#;
2881
2882        let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2883
2884        let (_, first_variables) = results.first().expect("expected at least one memory backend");
2885        assert!(first_variables.contains_key("sketch001"), "actual: {first_variables:?}");
2886        assert_number_variable(first_variables, "lineCount", 2.0);
2887        assert_backend_results_match(&results);
2888    }
2889
2890    #[tokio::test(flavor = "multi_thread")]
2891    async fn tag_call_stack_lookup_matches_between_memory_backends() {
2892        let code = r#"
2893sketch001 = startSketchOn(XY)
2894  |> startProfile(at = [0, 0])
2895  |> xLine(length = 10, tag = $seg01)
2896
2897segLength = segLen(seg01)
2898"#;
2899
2900        let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2901
2902        let (_, first_variables) = results.first().expect("expected at least one memory backend");
2903        assert_number_variable(first_variables, "segLength", 10.0);
2904        assert_backend_results_match(&results);
2905    }
2906
2907    #[tokio::test(flavor = "multi_thread")]
2908    async fn sketch_transpiler_exec_outcome_variables_match_between_memory_backends() {
2909        let code = r#"
2910sketch001 = startSketchOn(XY)
2911  |> startProfile(at = [0, 0])
2912  |> line(end = [1, 0])
2913"#;
2914        let program = crate::Program::parse_no_errs(code).unwrap();
2915
2916        let outcomes = collect_backend_results(|kind| execute_outcome_with_backend(code, kind)).await;
2917        let mut transpiled = Vec::with_capacity(outcomes.len());
2918        for (kind, outcome) in &outcomes {
2919            let sketch = transpile_old_sketch_to_new(outcome, &program, "sketch001").unwrap();
2920            transpiled.push((*kind, sketch));
2921        }
2922
2923        assert_backend_results_match(&transpiled);
2924    }
2925
2926    #[tokio::test(flavor = "multi_thread")]
2927    async fn test_execute_warn() {
2928        let text = "@blah";
2929        let result = parse_execute(text).await.unwrap();
2930        let errs = result.exec_state.issues();
2931        assert_eq!(errs.len(), 1);
2932        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2933        assert!(
2934            errs[0].message.contains("Unknown annotation"),
2935            "unexpected warning message: {}",
2936            errs[0].message
2937        );
2938    }
2939
2940    #[tokio::test(flavor = "multi_thread")]
2941    async fn test_execute_fn_definitions() {
2942        let ast = r#"fn def(@x) {
2943  return x
2944}
2945fn ghi(@x) {
2946  return x
2947}
2948fn jkl(@x) {
2949  return x
2950}
2951fn hmm(@x) {
2952  return x
2953}
2954
2955yo = 5 + 6
2956
2957abc = 3
2958identifierGuy = 5
2959part001 = startSketchOn(XY)
2960|> startProfile(at = [-1.2, 4.83])
2961|> line(end = [2.8, 0])
2962|> angledLine(angle = 100 + 100, length = 3.01)
2963|> angledLine(angle = abc, length = 3.02)
2964|> angledLine(angle = def(yo), length = 3.03)
2965|> angledLine(angle = ghi(2), length = 3.04)
2966|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2967|> close()
2968yo2 = hmm([identifierGuy + 5])"#;
2969
2970        parse_execute(ast).await.unwrap();
2971    }
2972
2973    #[tokio::test(flavor = "multi_thread")]
2974    async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2975        let code = r#"
2976firstProfile = sketch(on = XY) {
2977  edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2978  edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2979  edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2980  edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2981  coincident([edge1.end, edge2.start])
2982  coincident([edge2.end, edge3.start])
2983  coincident([edge3.end, edge4.start])
2984  coincident([edge4.end, edge1.start])
2985}
2986
2987secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2988  edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2989  edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2990  edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2991  edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2992  coincident([edge5.end, edge6.start])
2993  coincident([edge6.end, edge7.start])
2994  coincident([edge7.end, edge8.start])
2995  coincident([edge8.end, edge5.start])
2996}
2997
2998firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2999secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
3000"#;
3001
3002        let result = parse_execute(code).await.unwrap();
3003        assert!(result.exec_state.issues().is_empty());
3004    }
3005
3006    #[tokio::test(flavor = "multi_thread")]
3007    async fn sketch_block_artifact_preserves_standard_plane_name() {
3008        let code = r#"
3009sketch001 = sketch(on = -YZ) {
3010  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
3011}
3012"#;
3013
3014        let result = parse_execute(code).await.unwrap();
3015        let sketch_blocks = result
3016            .exec_state
3017            .global
3018            .artifacts
3019            .graph
3020            .values()
3021            .filter_map(|artifact| match artifact {
3022                Artifact::SketchBlock(block) => Some(block),
3023                _ => None,
3024            })
3025            .collect::<Vec<_>>();
3026
3027        assert_eq!(sketch_blocks.len(), 1);
3028        assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
3029    }
3030
3031    #[tokio::test(flavor = "multi_thread")]
3032    async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
3033        let code = r#"
3034sketch001 = sketch(on = YZ) {
3035  line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
3036  line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
3037  line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
3038  line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
3039  coincident([line1.end, line2.start])
3040  coincident([line2.end, line3.start])
3041  coincident([line3.end, line4.start])
3042  coincident([line4.end, line1.start])
3043}
3044
3045sketch002 = sketch(on = -XZ) {
3046  line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
3047  line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
3048  line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
3049  line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
3050  coincident([line5.end, line6.start])
3051  coincident([line6.end, line7.start])
3052  coincident([line7.end, line8.start])
3053  coincident([line8.end, line5.start])
3054}
3055
3056region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
3057extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
3058region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
3059extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
3060
3061myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
3062"#;
3063
3064        let result = parse_execute(code).await.unwrap();
3065        assert!(result.exec_state.issues().is_empty());
3066    }
3067
3068    #[tokio::test(flavor = "multi_thread")]
3069    async fn issue_10741_point_circle_coincident_executes() {
3070        let code = r#"
3071sketch001 = sketch(on = YZ) {
3072  circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
3073  line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
3074  coincident([line1.start, circle1])
3075}
3076"#;
3077
3078        let result = parse_execute(code).await.unwrap();
3079        assert!(
3080            result
3081                .exec_state
3082                .issues()
3083                .iter()
3084                .all(|issue| issue.severity != Severity::Error),
3085            "unexpected execution issues: {:#?}",
3086            result.exec_state.issues()
3087        );
3088    }
3089
3090    #[tokio::test(flavor = "multi_thread")]
3091    async fn test_execute_with_pipe_substitutions_unary() {
3092        let ast = r#"myVar = 3
3093part001 = startSketchOn(XY)
3094  |> startProfile(at = [0, 0])
3095  |> line(end = [3, 4], tag = $seg01)
3096  |> line(end = [
3097  min([segLen(seg01), myVar]),
3098  -legLen(hypotenuse = segLen(seg01), leg = myVar)
3099])
3100"#;
3101
3102        parse_execute(ast).await.unwrap();
3103    }
3104
3105    #[tokio::test(flavor = "multi_thread")]
3106    async fn test_execute_with_pipe_substitutions() {
3107        let ast = r#"myVar = 3
3108part001 = startSketchOn(XY)
3109  |> startProfile(at = [0, 0])
3110  |> line(end = [3, 4], tag = $seg01)
3111  |> line(end = [
3112  min([segLen(seg01), myVar]),
3113  legLen(hypotenuse = segLen(seg01), leg = myVar)
3114])
3115"#;
3116
3117        parse_execute(ast).await.unwrap();
3118    }
3119
3120    #[tokio::test(flavor = "multi_thread")]
3121    async fn test_execute_with_inline_comment() {
3122        let ast = r#"baseThick = 1
3123armAngle = 60
3124
3125baseThickHalf = baseThick / 2
3126halfArmAngle = armAngle / 2
3127
3128arrExpShouldNotBeIncluded = [1, 2, 3]
3129objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
3130
3131part001 = startSketchOn(XY)
3132  |> startProfile(at = [0, 0])
3133  |> yLine(endAbsolute = 1)
3134  |> xLine(length = 3.84) // selection-range-7ish-before-this
3135
3136variableBelowShouldNotBeIncluded = 3
3137"#;
3138
3139        parse_execute(ast).await.unwrap();
3140    }
3141
3142    #[tokio::test(flavor = "multi_thread")]
3143    async fn test_execute_with_function_literal_in_pipe() {
3144        let ast = r#"w = 20
3145l = 8
3146h = 10
3147
3148fn thing() {
3149  return -8
3150}
3151
3152firstExtrude = startSketchOn(XY)
3153  |> startProfile(at = [0,0])
3154  |> line(end = [0, l])
3155  |> line(end = [w, 0])
3156  |> line(end = [0, thing()])
3157  |> close()
3158  |> extrude(length = h)"#;
3159
3160        parse_execute(ast).await.unwrap();
3161    }
3162
3163    #[tokio::test(flavor = "multi_thread")]
3164    async fn test_execute_with_function_unary_in_pipe() {
3165        let ast = r#"w = 20
3166l = 8
3167h = 10
3168
3169fn thing(@x) {
3170  return -x
3171}
3172
3173firstExtrude = startSketchOn(XY)
3174  |> startProfile(at = [0,0])
3175  |> line(end = [0, l])
3176  |> line(end = [w, 0])
3177  |> line(end = [0, thing(8)])
3178  |> close()
3179  |> extrude(length = h)"#;
3180
3181        parse_execute(ast).await.unwrap();
3182    }
3183
3184    #[tokio::test(flavor = "multi_thread")]
3185    async fn test_execute_with_function_array_in_pipe() {
3186        let ast = r#"w = 20
3187l = 8
3188h = 10
3189
3190fn thing(@x) {
3191  return [0, -x]
3192}
3193
3194firstExtrude = startSketchOn(XY)
3195  |> startProfile(at = [0,0])
3196  |> line(end = [0, l])
3197  |> line(end = [w, 0])
3198  |> line(end = thing(8))
3199  |> close()
3200  |> extrude(length = h)"#;
3201
3202        parse_execute(ast).await.unwrap();
3203    }
3204
3205    #[tokio::test(flavor = "multi_thread")]
3206    async fn test_execute_with_function_call_in_pipe() {
3207        let ast = r#"w = 20
3208l = 8
3209h = 10
3210
3211fn other_thing(@y) {
3212  return -y
3213}
3214
3215fn thing(@x) {
3216  return other_thing(x)
3217}
3218
3219firstExtrude = startSketchOn(XY)
3220  |> startProfile(at = [0,0])
3221  |> line(end = [0, l])
3222  |> line(end = [w, 0])
3223  |> line(end = [0, thing(8)])
3224  |> close()
3225  |> extrude(length = h)"#;
3226
3227        parse_execute(ast).await.unwrap();
3228    }
3229
3230    #[tokio::test(flavor = "multi_thread")]
3231    async fn test_execute_with_function_sketch() {
3232        let ast = r#"fn box(h, l, w) {
3233 myBox = startSketchOn(XY)
3234    |> startProfile(at = [0,0])
3235    |> line(end = [0, l])
3236    |> line(end = [w, 0])
3237    |> line(end = [0, -l])
3238    |> close()
3239    |> extrude(length = h)
3240
3241  return myBox
3242}
3243
3244fnBox = box(h = 3, l = 6, w = 10)"#;
3245
3246        parse_execute(ast).await.unwrap();
3247    }
3248
3249    #[tokio::test(flavor = "multi_thread")]
3250    async fn test_get_member_of_object_with_function_period() {
3251        let ast = r#"fn box(@obj) {
3252 myBox = startSketchOn(XY)
3253    |> startProfile(at = obj.start)
3254    |> line(end = [0, obj.l])
3255    |> line(end = [obj.w, 0])
3256    |> line(end = [0, -obj.l])
3257    |> close()
3258    |> extrude(length = obj.h)
3259
3260  return myBox
3261}
3262
3263thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
3264"#;
3265        parse_execute(ast).await.unwrap();
3266    }
3267
3268    #[tokio::test(flavor = "multi_thread")]
3269    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
3270    async fn test_object_member_starting_pipeline() {
3271        let ast = r#"
3272fn test2() {
3273  return {
3274    thing: startSketchOn(XY)
3275      |> startProfile(at = [0, 0])
3276      |> line(end = [0, 1])
3277      |> line(end = [1, 0])
3278      |> line(end = [0, -1])
3279      |> close()
3280  }
3281}
3282
3283x2 = test2()
3284
3285x2.thing
3286  |> extrude(length = 10)
3287"#;
3288        parse_execute(ast).await.unwrap();
3289    }
3290
3291    #[tokio::test(flavor = "multi_thread")]
3292    #[ignore] // ignore til we get loops
3293    async fn test_execute_with_function_sketch_loop_objects() {
3294        let ast = r#"fn box(obj) {
3295let myBox = startSketchOn(XY)
3296    |> startProfile(at = obj.start)
3297    |> line(end = [0, obj.l])
3298    |> line(end = [obj.w, 0])
3299    |> line(end = [0, -obj.l])
3300    |> close()
3301    |> extrude(length = obj.h)
3302
3303  return myBox
3304}
3305
3306for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
3307  thisBox = box(var)
3308}"#;
3309
3310        parse_execute(ast).await.unwrap();
3311    }
3312
3313    #[tokio::test(flavor = "multi_thread")]
3314    #[ignore] // ignore til we get loops
3315    async fn test_execute_with_function_sketch_loop_array() {
3316        let ast = r#"fn box(h, l, w, start) {
3317 myBox = startSketchOn(XY)
3318    |> startProfile(at = [0,0])
3319    |> line(end = [0, l])
3320    |> line(end = [w, 0])
3321    |> line(end = [0, -l])
3322    |> close()
3323    |> extrude(length = h)
3324
3325  return myBox
3326}
3327
3328
3329for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
3330  const thisBox = box(var[0], var[1], var[2], var[3])
3331}"#;
3332
3333        parse_execute(ast).await.unwrap();
3334    }
3335
3336    #[tokio::test(flavor = "multi_thread")]
3337    async fn test_get_member_of_array_with_function() {
3338        let ast = r#"fn box(@arr) {
3339 myBox =startSketchOn(XY)
3340    |> startProfile(at = arr[0])
3341    |> line(end = [0, arr[1]])
3342    |> line(end = [arr[2], 0])
3343    |> line(end = [0, -arr[1]])
3344    |> close()
3345    |> extrude(length = arr[3])
3346
3347  return myBox
3348}
3349
3350thisBox = box([[0,0], 6, 10, 3])
3351
3352"#;
3353        parse_execute(ast).await.unwrap();
3354    }
3355
3356    #[tokio::test(flavor = "multi_thread")]
3357    async fn test_function_cannot_access_future_definitions() {
3358        let ast = r#"
3359fn returnX() {
3360  // x shouldn't be defined yet.
3361  return x
3362}
3363
3364x = 5
3365
3366answer = returnX()"#;
3367
3368        let result = parse_execute(ast).await;
3369        let err = result.unwrap_err();
3370        assert_eq!(err.message(), "`x` is not defined");
3371    }
3372
3373    #[tokio::test(flavor = "multi_thread")]
3374    async fn test_override_prelude() {
3375        let text = "PI = 3.0";
3376        let result = parse_execute(text).await.unwrap();
3377        let issues = result.exec_state.issues();
3378        assert!(issues.is_empty(), "issues={issues:#?}");
3379    }
3380
3381    #[tokio::test(flavor = "multi_thread")]
3382    async fn type_aliases() {
3383        let text = r#"@settings(experimentalFeatures = allow)
3384type MyTy = [number; 2]
3385fn foo(@x: MyTy) {
3386    return x[0]
3387}
3388
3389foo([0, 1])
3390
3391type Other = MyTy | Helix
3392"#;
3393        let result = parse_execute(text).await.unwrap();
3394        let issues = result.exec_state.issues();
3395        assert!(issues.is_empty(), "issues={issues:#?}");
3396    }
3397
3398    #[tokio::test(flavor = "multi_thread")]
3399    async fn test_cannot_shebang_in_fn() {
3400        let ast = r#"
3401fn foo() {
3402  #!hello
3403  return true
3404}
3405
3406foo
3407"#;
3408
3409        let result = parse_execute(ast).await;
3410        let err = result.unwrap_err();
3411        assert_eq!(
3412            err,
3413            KclError::new_syntax(KclErrorDetails::new(
3414                "Unexpected token: #".to_owned(),
3415                vec![SourceRange::new(14, 15, ModuleId::default())],
3416            )),
3417        );
3418    }
3419
3420    #[tokio::test(flavor = "multi_thread")]
3421    async fn test_pattern_transform_function_cannot_access_future_definitions() {
3422        let ast = r#"
3423fn transform(@replicaId) {
3424  // x shouldn't be defined yet.
3425  scale = x
3426  return {
3427    translate = [0, 0, replicaId * 10],
3428    scale = [scale, 1, 0],
3429  }
3430}
3431
3432fn layer() {
3433  return startSketchOn(XY)
3434    |> circle( center= [0, 0], radius= 1, tag = $tag1)
3435    |> extrude(length = 10)
3436}
3437
3438x = 5
3439
3440// The 10 layers are replicas of each other, with a transform applied to each.
3441shape = layer() |> patternTransform(instances = 10, transform = transform)
3442"#;
3443
3444        let result = parse_execute(ast).await;
3445        let err = result.unwrap_err();
3446        assert_eq!(err.message(), "`x` is not defined",);
3447    }
3448
3449    // ADAM: Move some of these into simulation tests.
3450
3451    #[tokio::test(flavor = "multi_thread")]
3452    async fn test_math_execute_with_functions() {
3453        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
3454        let result = parse_execute(ast).await.unwrap();
3455        assert_eq!(
3456            5.0,
3457            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3458                .as_f64()
3459                .unwrap()
3460        );
3461    }
3462
3463    #[tokio::test(flavor = "multi_thread")]
3464    async fn test_math_execute() {
3465        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
3466        let result = parse_execute(ast).await.unwrap();
3467        assert_eq!(
3468            7.4,
3469            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3470                .as_f64()
3471                .unwrap()
3472        );
3473    }
3474
3475    #[tokio::test(flavor = "multi_thread")]
3476    async fn test_string_uppercase() {
3477        let composed = "\u{e9}";
3478        let uppercase_composed = "\u{c9}";
3479        let decomposed = "e\u{301}";
3480        let uppercase_decomposed = "E\u{301}";
3481        let code = format!(
3482            r#"
3483ascii = string::uppercase("Kcl")
3484unicode_expansion = string::uppercase("Straße")
3485uncased = string::uppercase("東京")
3486empty = string::uppercase("")
3487composed = string::uppercase("{composed}")
3488decomposed = string::uppercase("{decomposed}")
3489piped = "ready" |> string::uppercase()
3490"#
3491        );
3492
3493        let result = parse_execute(&code).await.unwrap();
3494        for (name, expected) in [
3495            ("ascii", "KCL"),
3496            ("unicode_expansion", "STRASSE"),
3497            ("uncased", "東京"),
3498            ("empty", ""),
3499            ("composed", uppercase_composed),
3500            ("decomposed", uppercase_decomposed),
3501            ("piped", "READY"),
3502        ] {
3503            assert_eq!(
3504                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3505                    .as_str()
3506                    .unwrap(),
3507                expected,
3508                "{name}"
3509            );
3510        }
3511    }
3512
3513    #[tokio::test(flavor = "multi_thread")]
3514    async fn test_string_lowercase() {
3515        let composed = "\u{c9}";
3516        let lowercase_composed = "\u{e9}";
3517        let decomposed = "E\u{301}";
3518        let lowercase_decomposed = "e\u{301}";
3519        let expanded = "i\u{307}";
3520        let code = format!(
3521            r#"
3522ascii = string::lowercase("KCL")
3523final_sigma = string::lowercase("ΟΣ")
3524medial_sigma = string::lowercase("ΟΣΑ")
3525unicode_expansion = string::lowercase("İ")
3526uncased = string::lowercase("東京")
3527empty = string::lowercase("")
3528composed = string::lowercase("{composed}")
3529decomposed = string::lowercase("{decomposed}")
3530piped = "READY" |> string::lowercase()
3531"#
3532        );
3533
3534        let result = parse_execute(&code).await.unwrap();
3535        for (name, expected) in [
3536            ("ascii", "kcl"),
3537            ("final_sigma", "ος"),
3538            ("medial_sigma", "οσα"),
3539            ("unicode_expansion", expanded),
3540            ("uncased", "東京"),
3541            ("empty", ""),
3542            ("composed", lowercase_composed),
3543            ("decomposed", lowercase_decomposed),
3544            ("piped", "ready"),
3545        ] {
3546            assert_eq!(
3547                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3548                    .as_str()
3549                    .unwrap(),
3550                expected,
3551                "{name}"
3552            );
3553        }
3554    }
3555
3556    #[tokio::test(flavor = "multi_thread")]
3557    async fn test_string_is_equal() {
3558        let composed = "\u{e9}";
3559        let decomposed = "e\u{301}";
3560        let code = format!(
3561            r#"
3562exact_same = string::isEqual("KCL", to = "KCL")
3563exact_different_case = string::isEqual("KCL", to = "kcl")
3564explicit_case_sensitive = string::isEqual("KCL", to = "kcl", caseInsensitive = false)
3565case_insensitive_ascii = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3566case_fold_expansion = string::isEqual("Straße", to = "STRASSE", caseInsensitive = true)
3567case_fold_expansion_reversed = string::isEqual("STRASSE", to = "Straße", caseInsensitive = true)
3568case_fold_sigma = string::isEqual("ος", to = "οσ", caseInsensitive = true)
3569case_fold_non_turkic = string::isEqual("I", to = "i", caseInsensitive = true)
3570case_fold_not_turkic = string::isEqual("I", to = "ı", caseInsensitive = true)
3571empty_same = string::isEqual("", to = "")
3572empty_different = string::isEqual("", to = "KCL")
3573exact_without_normalization = string::isEqual("{composed}", to = "{decomposed}")
3574case_fold_without_normalization = string::isEqual("{composed}", to = "{decomposed}", caseInsensitive = true)
3575piped = "ready" |> string::isEqual(to = "READY", caseInsensitive = true)
3576"#
3577        );
3578
3579        let result = parse_execute(&code).await.unwrap();
3580        for (name, expected) in [
3581            ("exact_same", true),
3582            ("exact_different_case", false),
3583            ("explicit_case_sensitive", false),
3584            ("case_insensitive_ascii", true),
3585            ("case_fold_expansion", true),
3586            ("case_fold_expansion_reversed", true),
3587            ("case_fold_sigma", true),
3588            ("case_fold_non_turkic", true),
3589            ("case_fold_not_turkic", false),
3590            ("empty_same", true),
3591            ("empty_different", false),
3592            ("exact_without_normalization", false),
3593            ("case_fold_without_normalization", false),
3594            ("piped", true),
3595        ] {
3596            assert_eq!(
3597                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3598                    .as_bool()
3599                    .unwrap(),
3600                expected,
3601                "{name}"
3602            );
3603        }
3604    }
3605
3606    #[tokio::test(flavor = "multi_thread")]
3607    async fn test_string_is_equal_inside_sketch_block_is_predicate() {
3608        let code = r#"
3609@settings(experimentalFeatures = allow)
3610
3611sketch(on = XY) {
3612  stringsAreEqual = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3613}
3614"#;
3615
3616        parse_execute(code).await.unwrap();
3617    }
3618
3619    #[tokio::test(flavor = "multi_thread")]
3620    async fn test_string_trim() {
3621        let ascii_whitespace = " \t\n";
3622        let tab = "\t";
3623        let non_breaking_space = "\u{a0}";
3624        let em_space = "\u{2003}";
3625        let ideographic_space = "\u{3000}";
3626        let zero_width_space = "\u{200b}";
3627        let decomposed = "e\u{301}";
3628        let code = format!(
3629            r#"
3630ascii = string::trim("{ascii_whitespace}KCL{ascii_whitespace}")
3631internal = string::trim("  KCL{tab}strings  ")
3632unicode = string::trim("{non_breaking_space}{em_space}KCL{ideographic_space}")
3633all_whitespace = string::trim("{ascii_whitespace}{non_breaking_space}")
3634empty = string::trim("")
3635unchanged = string::trim("KCL")
3636without_normalization = string::trim(" {decomposed} ")
3637non_whitespace = string::trim("{zero_width_space}KCL{zero_width_space}")
3638piped = "  ready  " |> string::trim()
3639"#
3640        );
3641
3642        let result = parse_execute(&code).await.unwrap();
3643        let non_whitespace = format!("{zero_width_space}KCL{zero_width_space}");
3644        for (name, expected) in [
3645            ("ascii", "KCL"),
3646            ("internal", "KCL\tstrings"),
3647            ("unicode", "KCL"),
3648            ("all_whitespace", ""),
3649            ("empty", ""),
3650            ("unchanged", "KCL"),
3651            ("without_normalization", decomposed),
3652            ("non_whitespace", non_whitespace.as_str()),
3653            ("piped", "ready"),
3654        ] {
3655            assert_eq!(
3656                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3657                    .as_str()
3658                    .unwrap(),
3659                expected,
3660                "{name}"
3661            );
3662        }
3663    }
3664
3665    #[tokio::test(flavor = "multi_thread")]
3666    async fn test_string_trim_start() {
3667        let ascii_whitespace = " \t\n";
3668        let tab = "\t";
3669        let non_breaking_space = "\u{a0}";
3670        let em_space = "\u{2003}";
3671        let ideographic_space = "\u{3000}";
3672        let zero_width_space = "\u{200b}";
3673        let decomposed = "e\u{301}";
3674        let code = format!(
3675            r#"
3676ascii = string::trimStart("{ascii_whitespace}KCL{ascii_whitespace}")
3677internal = string::trimStart("  KCL{tab}strings")
3678unicode = string::trimStart("{non_breaking_space}{em_space}KCL{ideographic_space}")
3679all_whitespace = string::trimStart("{ascii_whitespace}{non_breaking_space}")
3680empty = string::trimStart("")
3681unchanged = string::trimStart("KCL")
3682without_normalization = string::trimStart(" {decomposed}")
3683non_whitespace_prefix = string::trimStart("{zero_width_space}{ascii_whitespace}KCL")
3684piped = "  ready  " |> string::trimStart()
3685"#
3686        );
3687
3688        let result = parse_execute(&code).await.unwrap();
3689        let ascii = format!("KCL{ascii_whitespace}");
3690        let unicode = format!("KCL{ideographic_space}");
3691        let non_whitespace_prefix = format!("{zero_width_space}{ascii_whitespace}KCL");
3692        for (name, expected) in [
3693            ("ascii", ascii.as_str()),
3694            ("internal", "KCL\tstrings"),
3695            ("unicode", unicode.as_str()),
3696            ("all_whitespace", ""),
3697            ("empty", ""),
3698            ("unchanged", "KCL"),
3699            ("without_normalization", decomposed),
3700            ("non_whitespace_prefix", non_whitespace_prefix.as_str()),
3701            ("piped", "ready  "),
3702        ] {
3703            assert_eq!(
3704                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3705                    .as_str()
3706                    .unwrap(),
3707                expected,
3708                "{name}"
3709            );
3710        }
3711    }
3712
3713    #[tokio::test(flavor = "multi_thread")]
3714    async fn test_string_trim_end() {
3715        let ascii_whitespace = " \t\n";
3716        let tab = "\t";
3717        let non_breaking_space = "\u{a0}";
3718        let em_space = "\u{2003}";
3719        let ideographic_space = "\u{3000}";
3720        let zero_width_space = "\u{200b}";
3721        let decomposed = "e\u{301}";
3722        let code = format!(
3723            r#"
3724ascii = string::trimEnd("{ascii_whitespace}KCL{ascii_whitespace}")
3725internal = string::trimEnd("KCL{tab}strings  ")
3726unicode = string::trimEnd("{non_breaking_space}KCL{em_space}{ideographic_space}")
3727all_whitespace = string::trimEnd("{ascii_whitespace}{non_breaking_space}")
3728empty = string::trimEnd("")
3729unchanged = string::trimEnd("KCL")
3730without_normalization = string::trimEnd("{decomposed} ")
3731non_whitespace_suffix = string::trimEnd("KCL{ascii_whitespace}{zero_width_space}")
3732piped = "  ready  " |> string::trimEnd()
3733"#
3734        );
3735
3736        let result = parse_execute(&code).await.unwrap();
3737        let ascii = format!("{ascii_whitespace}KCL");
3738        let unicode = format!("{non_breaking_space}KCL");
3739        let non_whitespace_suffix = format!("KCL{ascii_whitespace}{zero_width_space}");
3740        for (name, expected) in [
3741            ("ascii", ascii.as_str()),
3742            ("internal", "KCL\tstrings"),
3743            ("unicode", unicode.as_str()),
3744            ("all_whitespace", ""),
3745            ("empty", ""),
3746            ("unchanged", "KCL"),
3747            ("without_normalization", decomposed),
3748            ("non_whitespace_suffix", non_whitespace_suffix.as_str()),
3749            ("piped", "  ready"),
3750        ] {
3751            assert_eq!(
3752                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3753                    .as_str()
3754                    .unwrap(),
3755                expected,
3756                "{name}"
3757            );
3758        }
3759    }
3760
3761    #[tokio::test(flavor = "multi_thread")]
3762    async fn test_string_to_string() {
3763        // Each case runs on its own so a failure names the expression that
3764        // produced it rather than collapsing the whole table.
3765        for (name, expr, expected) in [
3766            // Every row of the table in the `toString` doc comment appears
3767            // here, so the documentation cannot drift from the behaviour.
3768            ("unitless integer", "12", "12"),
3769            ("unitless fractional", "1.5", "1.5"),
3770            ("no digits dropped", "0.1 + 0.2", "0.30000000000000004"),
3771            ("unitless negative", "-7", "-7"),
3772            ("unitless zero", "0", "0"),
3773            ("negative zero", "-0", "0"),
3774            ("count", "3_", "3_"),
3775            ("millimeters", "12mm", "12mm"),
3776            ("centimeters", "12cm", "12cm"),
3777            ("meters", "12m", "12m"),
3778            ("inches", "1.5in", "1.5in"),
3779            ("feet", "2ft", "2ft"),
3780            ("yards", "3yd", "3yd"),
3781            ("degrees", "90deg", "90deg"),
3782            ("radians", "1.5rad", "1.5rad"),
3783            // Arithmetic keeps the unit it started with.
3784            ("length arithmetic", "2mm + 10mm", "12mm"),
3785            // Multiplying two lengths exceeds what the type system tracks, so
3786            // only the numeric component survives.
3787            ("units the type system loses", "2mm * 10mm", "20"),
3788            ("unitless arithmetic", "1 + 2", "3"),
3789        ] {
3790            let code = format!("actual = string::toString({expr})");
3791            let result = parse_execute(&code).await.unwrap();
3792
3793            assert_eq!(
3794                mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3795                    .as_str()
3796                    .unwrap(),
3797                expected,
3798                "case: {name}"
3799            );
3800        }
3801    }
3802
3803    #[tokio::test(flavor = "multi_thread")]
3804    async fn test_string_to_string_ignores_the_files_default_unit() {
3805        // A value with no suffix has the file's default unit attached, but that
3806        // unit was never written down, so neither is it in the output. Reading
3807        // the result back in a file with a different default gives a different
3808        // quantity; the guarantee is about the number, not the measurement.
3809        let code = "@settings(defaultLengthUnit = inch)\nactual = string::toString(12)";
3810        let result = parse_execute(code).await.unwrap();
3811
3812        assert_eq!(
3813            mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3814                .as_str()
3815                .unwrap(),
3816            "12"
3817        );
3818    }
3819
3820    #[tokio::test(flavor = "multi_thread")]
3821    async fn test_string_to_string_rejects_a_non_number() {
3822        let error = parse_execute(r#"actual = string::toString("already text")"#)
3823            .await
3824            .unwrap_err();
3825
3826        // The declared signature rejects this before the implementation runs,
3827        // so the diagnostic names the function and both types.
3828        assert_eq!(
3829            error.message(),
3830            "The input argument of `string::toString` requires a value with type `number`, but found a value with type `string`."
3831        );
3832        assert!(
3833            matches!(error, KclError::Argument { .. }),
3834            "expected an Argument error, found {error:?}"
3835        );
3836    }
3837
3838    #[tokio::test(flavor = "multi_thread")]
3839    async fn test_string_to_string_accepts_a_piped_argument() {
3840        let result = parse_execute("actual = 12mm |> string::toString()").await.unwrap();
3841
3842        assert_eq!(
3843            mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3844                .as_str()
3845                .unwrap(),
3846            "12mm"
3847        );
3848    }
3849
3850    #[tokio::test(flavor = "multi_thread")]
3851    async fn test_string_to_string_echoes_how_the_literal_was_written() {
3852        // Reading the output back is not a supported operation, but for a
3853        // literal that carries its own units the text still comes out looking
3854        // like what the author typed, which is what makes it readable.
3855        for literal in [
3856            "12",
3857            "1.5",
3858            "0.30000000000000004",
3859            "3_",
3860            // A fractional count and a negative both have to survive the trip,
3861            // since the formatter emits them.
3862            "2.5_",
3863            "-4_",
3864            "12mm",
3865            "-5mm",
3866            "1.5in",
3867            "90deg",
3868            "1.5rad",
3869        ] {
3870            let code = format!("actual = string::toString({literal})");
3871            let result = parse_execute(&code).await.unwrap();
3872
3873            assert_eq!(
3874                mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3875                    .as_str()
3876                    .unwrap(),
3877                literal,
3878                "literal: {literal}"
3879            );
3880        }
3881    }
3882
3883    #[tokio::test(flavor = "multi_thread")]
3884    async fn test_string_to_string_spells_out_non_finite_numbers() {
3885        // Division is unguarded, so these are reachable from ordinary KCL. They
3886        // convert like any other number: the point of the function is to build
3887        // a message, and a message about a NaN is exactly when you need one.
3888        for (name, expr, expected) in [
3889            ("positive infinity", "1 / 0", "Infinity"),
3890            ("negative infinity", "-1 / 0", "-Infinity"),
3891            ("nan", "0 / 0", "NaN"),
3892            // The unit is dropped: no length is described by "Infinitymm".
3893            ("infinity from a length", "1mm / 0", "Infinity"),
3894            ("nan from a length", "0mm / 0", "NaN"),
3895            ("infinity from an angle", "1deg / 0", "Infinity"),
3896        ] {
3897            let code = format!("actual = string::toString({expr})");
3898            let result = parse_execute(&code).await.unwrap();
3899
3900            assert_eq!(
3901                mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3902                    .as_str()
3903                    .unwrap(),
3904                expected,
3905                "case: {name}"
3906            );
3907        }
3908    }
3909
3910    #[tokio::test(flavor = "multi_thread")]
3911    async fn test_string_equality_operators() {
3912        let composed = "\u{e9}";
3913        let decomposed = "e\u{301}";
3914        let code = format!(
3915            r#"
3916equal_same_ascii = "KCL" == "KCL"
3917equal_different_case = "KCL" == "kcl"
3918not_equal_same_ascii = "KCL" != "KCL"
3919not_equal_different_case = "KCL" != "kcl"
3920equal_same_unicode = "{composed}" == "{composed}"
3921not_equal_same_unicode = "{composed}" != "{composed}"
3922equal_without_normalization = "{composed}" == "{decomposed}"
3923not_equal_without_normalization = "{composed}" != "{decomposed}"
3924"#
3925        );
3926
3927        let result = parse_execute(&code).await.unwrap();
3928        for (name, expected) in [
3929            ("equal_same_ascii", true),
3930            ("equal_different_case", false),
3931            ("not_equal_same_ascii", false),
3932            ("not_equal_different_case", true),
3933            ("equal_same_unicode", true),
3934            ("not_equal_same_unicode", false),
3935            ("equal_without_normalization", false),
3936            ("not_equal_without_normalization", true),
3937        ] {
3938            assert_eq!(
3939                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3940                    .as_bool()
3941                    .unwrap(),
3942                expected,
3943                "{name}"
3944            );
3945        }
3946    }
3947
3948    #[tokio::test(flavor = "multi_thread")]
3949    async fn test_string_equality_inside_sketch_block_fails_like_number_equality() {
3950        let string_code = r#"
3951@settings(experimentalFeatures = allow)
3952
3953sketch(on = XY) {
3954  stringsAreEqual = "KCL" == "KCL"
3955}
3956"#;
3957        let number_code = r#"
3958@settings(experimentalFeatures = allow)
3959
3960sketch(on = XY) {
3961  numbersAreEqual = 1 == 1
3962}
3963"#;
3964
3965        assert_eq!(
3966            parse_execute(string_code).await.unwrap_err().message(),
3967            "Cannot create an equivalence constraint between values of these types: a string and a string"
3968        );
3969        assert_eq!(
3970            parse_execute(number_code).await.unwrap_err().message(),
3971            "Cannot create an equivalence constraint between values of these types: a number and a number"
3972        );
3973    }
3974
3975    #[tokio::test(flavor = "multi_thread")]
3976    async fn test_math_execute_start_negative() {
3977        let ast = r#"myVar = -5 + 6"#;
3978        let result = parse_execute(ast).await.unwrap();
3979        assert_eq!(
3980            1.0,
3981            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3982                .as_f64()
3983                .unwrap()
3984        );
3985    }
3986
3987    #[tokio::test(flavor = "multi_thread")]
3988    async fn test_math_execute_with_pi() {
3989        let ast = r#"myVar = PI * 2"#;
3990        let result = parse_execute(ast).await.unwrap();
3991        assert_eq!(
3992            std::f64::consts::TAU,
3993            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3994                .as_f64()
3995                .unwrap()
3996        );
3997    }
3998
3999    #[tokio::test(flavor = "multi_thread")]
4000    async fn test_math_define_decimal_without_leading_zero() {
4001        let ast = r#"thing = .4 + 7"#;
4002        let result = parse_execute(ast).await.unwrap();
4003        assert_eq!(
4004            7.4,
4005            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
4006                .as_f64()
4007                .unwrap()
4008        );
4009    }
4010
4011    #[tokio::test(flavor = "multi_thread")]
4012    async fn pass_std_to_std() {
4013        let ast = r#"sketch001 = startSketchOn(XY)
4014profile001 = circle(sketch001, center = [0, 0], radius = 2)
4015extrude001 = extrude(profile001, length = 5)
4016extrudes = patternLinear3d(
4017  extrude001,
4018  instances = 3,
4019  distance = 5,
4020  axis = [1, 1, 0],
4021)
4022clone001 = map(extrudes, f = clone)
4023"#;
4024        parse_execute(ast).await.unwrap();
4025    }
4026
4027    #[tokio::test(flavor = "multi_thread")]
4028    async fn test_array_reduce_nested_array() {
4029        let code = r#"
4030fn id(@el, accum)  { return accum }
4031
4032answer = reduce([], initial=[[[0,0]]], f=id)
4033"#;
4034        let result = parse_execute(code).await.unwrap();
4035        assert_eq!(
4036            mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
4037            KclValue::HomArray {
4038                value: vec![KclValue::HomArray {
4039                    value: vec![KclValue::HomArray {
4040                        value: vec![
4041                            KclValue::Number {
4042                                value: 0.0,
4043                                ty: NumericType::default(),
4044                                meta: vec![SourceRange::new(69, 70, Default::default()).into()],
4045                            },
4046                            KclValue::Number {
4047                                value: 0.0,
4048                                ty: NumericType::default(),
4049                                meta: vec![SourceRange::new(71, 72, Default::default()).into()],
4050                            }
4051                        ],
4052                        ty: RuntimeType::any(),
4053                    }],
4054                    ty: RuntimeType::any(),
4055                }],
4056                ty: RuntimeType::any(),
4057            }
4058        );
4059    }
4060
4061    #[tokio::test(flavor = "multi_thread")]
4062    async fn test_zero_param_fn() {
4063        let ast = r#"sigmaAllow = 35000 // psi
4064leg1 = 5 // inches
4065leg2 = 8 // inches
4066fn thickness() { return 0.56 }
4067
4068bracket = startSketchOn(XY)
4069  |> startProfile(at = [0,0])
4070  |> line(end = [0, leg1])
4071  |> line(end = [leg2, 0])
4072  |> line(end = [0, -thickness()])
4073  |> line(end = [-leg2 + thickness(), 0])
4074"#;
4075        parse_execute(ast).await.unwrap();
4076    }
4077
4078    #[tokio::test(flavor = "multi_thread")]
4079    async fn test_unary_operator_not_succeeds() {
4080        let ast = r#"
4081fn returnTrue() { return !false }
4082t = true
4083f = false
4084notTrue = !t
4085notFalse = !f
4086c = !!true
4087d = !returnTrue()
4088
4089assertIs(!false, error = "expected to pass")
4090
4091fn check(x) {
4092  assertIs(!x, error = "expected argument to be false")
4093  return true
4094}
4095check(x = false)
4096"#;
4097        let result = parse_execute(ast).await.unwrap();
4098        assert_eq!(
4099            false,
4100            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
4101                .as_bool()
4102                .unwrap()
4103        );
4104        assert_eq!(
4105            true,
4106            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
4107                .as_bool()
4108                .unwrap()
4109        );
4110        assert_eq!(
4111            true,
4112            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
4113                .as_bool()
4114                .unwrap()
4115        );
4116        assert_eq!(
4117            false,
4118            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
4119                .as_bool()
4120                .unwrap()
4121        );
4122    }
4123
4124    #[tokio::test(flavor = "multi_thread")]
4125    async fn test_unary_operator_not_on_non_bool_fails() {
4126        let code1 = r#"
4127// Yup, this is null.
4128myNull = 0 / 0
4129notNull = !myNull
4130"#;
4131        assert_eq!(
4132            parse_execute(code1).await.unwrap_err().message(),
4133            "Cannot apply unary operator ! to non-boolean value: a number",
4134        );
4135
4136        let code2 = "notZero = !0";
4137        assert_eq!(
4138            parse_execute(code2).await.unwrap_err().message(),
4139            "Cannot apply unary operator ! to non-boolean value: a number",
4140        );
4141
4142        let code3 = r#"
4143notEmptyString = !""
4144"#;
4145        assert_eq!(
4146            parse_execute(code3).await.unwrap_err().message(),
4147            "Cannot apply unary operator ! to non-boolean value: a string",
4148        );
4149
4150        let code4 = r#"
4151obj = { a = 1 }
4152notMember = !obj.a
4153"#;
4154        assert_eq!(
4155            parse_execute(code4).await.unwrap_err().message(),
4156            "Cannot apply unary operator ! to non-boolean value: a number",
4157        );
4158
4159        let code5 = "
4160a = []
4161notArray = !a";
4162        assert_eq!(
4163            parse_execute(code5).await.unwrap_err().message(),
4164            "Cannot apply unary operator ! to non-boolean value: an empty array",
4165        );
4166
4167        let code6 = "
4168x = {}
4169notObject = !x";
4170        assert_eq!(
4171            parse_execute(code6).await.unwrap_err().message(),
4172            "Cannot apply unary operator ! to non-boolean value: an object",
4173        );
4174
4175        let code7 = "
4176fn x() { return 1 }
4177notFunction = !x";
4178        let fn_err = parse_execute(code7).await.unwrap_err();
4179        // These are currently printed out as JSON objects, so we don't want to
4180        // check the full error.
4181        assert!(
4182            fn_err
4183                .message()
4184                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
4185            "Actual error: {fn_err:?}"
4186        );
4187
4188        let code8 = "
4189myTagDeclarator = $myTag
4190notTagDeclarator = !myTagDeclarator";
4191        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
4192        // These are currently printed out as JSON objects, so we don't want to
4193        // check the full error.
4194        assert!(
4195            tag_declarator_err
4196                .message()
4197                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
4198            "Actual error: {tag_declarator_err:?}"
4199        );
4200
4201        let code9 = "
4202myTagDeclarator = $myTag
4203notTagIdentifier = !myTag";
4204        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
4205        // These are currently printed out as JSON objects, so we don't want to
4206        // check the full error.
4207        assert!(
4208            tag_identifier_err
4209                .message()
4210                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
4211            "Actual error: {tag_identifier_err:?}"
4212        );
4213
4214        let code10 = "notPipe = !(1 |> 2)";
4215        assert_eq!(
4216            // TODO: We don't currently parse this, but we should.  It should be
4217            // a runtime error instead.
4218            parse_execute(code10).await.unwrap_err(),
4219            KclError::new_syntax(KclErrorDetails::new(
4220                "Unexpected token: !".to_owned(),
4221                vec![SourceRange::new(10, 11, ModuleId::default())],
4222            ))
4223        );
4224
4225        let code11 = "
4226fn identity(x) { return x }
4227notPipeSub = 1 |> identity(!%))";
4228        assert_eq!(
4229            // TODO: We don't currently parse this, but we should.  It should be
4230            // a runtime error instead.
4231            parse_execute(code11).await.unwrap_err(),
4232            KclError::new_syntax(KclErrorDetails::new(
4233                "There was an unexpected `!`. Try removing it.".to_owned(),
4234                vec![SourceRange::new(56, 57, ModuleId::default())],
4235            ))
4236        );
4237
4238        // TODO: Add these tests when we support these types.
4239        // let notNan = !NaN
4240        // let notInfinity = !Infinity
4241    }
4242
4243    #[tokio::test(flavor = "multi_thread")]
4244    async fn test_start_sketch_on_invalid_kwargs() {
4245        let current_dir = std::env::current_dir().unwrap();
4246        let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
4247        let mut code = std::fs::read_to_string(&path).unwrap();
4248        assert_eq!(
4249            parse_execute(&code).await.unwrap_err().message(),
4250            "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
4251        );
4252
4253        path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
4254        code = std::fs::read_to_string(&path).unwrap();
4255
4256        assert_eq!(
4257            parse_execute(&code).await.unwrap_err().message(),
4258            "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
4259        );
4260
4261        path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
4262        code = std::fs::read_to_string(&path).unwrap();
4263
4264        assert_eq!(
4265            parse_execute(&code).await.unwrap_err().message(),
4266            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4267        );
4268
4269        path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
4270        code = std::fs::read_to_string(&path).unwrap();
4271
4272        assert_eq!(
4273            parse_execute(&code).await.unwrap_err().message(),
4274            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4275        );
4276
4277        path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
4278        code = std::fs::read_to_string(&path).unwrap();
4279
4280        assert_eq!(
4281            parse_execute(&code).await.unwrap_err().message(),
4282            "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
4283        );
4284    }
4285
4286    #[tokio::test(flavor = "multi_thread")]
4287    async fn test_math_negative_variable_in_binary_expression() {
4288        let ast = r#"sigmaAllow = 35000 // psi
4289width = 1 // inch
4290
4291p = 150 // lbs
4292distance = 6 // inches
4293FOS = 2
4294
4295leg1 = 5 // inches
4296leg2 = 8 // inches
4297
4298thickness_squared = distance * p * FOS * 6 / sigmaAllow
4299thickness = 0.56 // inches. App does not support square root function yet
4300
4301bracket = startSketchOn(XY)
4302  |> startProfile(at = [0,0])
4303  |> line(end = [0, leg1])
4304  |> line(end = [leg2, 0])
4305  |> line(end = [0, -thickness])
4306  |> line(end = [-leg2 + thickness, 0])
4307"#;
4308        parse_execute(ast).await.unwrap();
4309    }
4310
4311    #[tokio::test(flavor = "multi_thread")]
4312    async fn test_execute_function_no_return() {
4313        let ast = r#"fn test(@origin) {
4314  origin
4315}
4316
4317test([0, 0])
4318"#;
4319        let result = parse_execute(ast).await;
4320        assert!(result.is_err());
4321        assert!(result.unwrap_err().to_string().contains("undefined"));
4322    }
4323
4324    #[tokio::test(flavor = "multi_thread")]
4325    async fn test_max_stack_size_exceeded_error() {
4326        let ast = r#"
4327fn forever(@n) {
4328  return 1 + forever(n)
4329}
4330
4331forever(1)
4332"#;
4333        let result = parse_execute(ast).await;
4334        let err = result.unwrap_err();
4335        // The recursive executor's native-stack cap and the machine
4336        // executor's call-depth guard report differently.
4337        let msg = err.to_string();
4338        assert!(
4339            msg.contains("stack size exceeded") || msg.contains("Call depth limit"),
4340            "actual: {err:?}"
4341        );
4342    }
4343
4344    #[tokio::test(flavor = "multi_thread")]
4345    async fn test_math_doubly_nested_parens() {
4346        let ast = r#"sigmaAllow = 35000 // psi
4347width = 4 // inch
4348p = 150 // Force on shelf - lbs
4349distance = 6 // inches
4350FOS = 2
4351leg1 = 5 // inches
4352leg2 = 8 // inches
4353thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
4354thickness = 0.32 // inches. App does not support square root function yet
4355bracket = startSketchOn(XY)
4356  |> startProfile(at = [0,0])
4357    |> line(end = [0, leg1])
4358  |> line(end = [leg2, 0])
4359  |> line(end = [0, -thickness])
4360  |> line(end = [-1 * leg2 + thickness, 0])
4361  |> line(end = [0, -1 * leg1 + thickness])
4362  |> close()
4363  |> extrude(length = width)
4364"#;
4365        parse_execute(ast).await.unwrap();
4366    }
4367
4368    #[tokio::test(flavor = "multi_thread")]
4369    async fn test_math_nested_parens_one_less() {
4370        let ast = r#" sigmaAllow = 35000 // psi
4371width = 4 // inch
4372p = 150 // Force on shelf - lbs
4373distance = 6 // inches
4374FOS = 2
4375leg1 = 5 // inches
4376leg2 = 8 // inches
4377thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
4378thickness = 0.32 // inches. App does not support square root function yet
4379bracket = startSketchOn(XY)
4380  |> startProfile(at = [0,0])
4381    |> line(end = [0, leg1])
4382  |> line(end = [leg2, 0])
4383  |> line(end = [0, -thickness])
4384  |> line(end = [-1 * leg2 + thickness, 0])
4385  |> line(end = [0, -1 * leg1 + thickness])
4386  |> close()
4387  |> extrude(length = width)
4388"#;
4389        parse_execute(ast).await.unwrap();
4390    }
4391
4392    #[tokio::test(flavor = "multi_thread")]
4393    async fn test_fn_as_operand() {
4394        let ast = r#"fn f() { return 1 }
4395x = f()
4396y = x + 1
4397z = f() + 1
4398w = f() + f()
4399"#;
4400        parse_execute(ast).await.unwrap();
4401    }
4402
4403    #[tokio::test(flavor = "multi_thread")]
4404    async fn kcl_test_ids_stable_between_executions() {
4405        let code = r#"sketch001 = startSketchOn(XZ)
4406|> startProfile(at = [61.74, 206.13])
4407|> xLine(length = 305.11, tag = $seg01)
4408|> yLine(length = -291.85)
4409|> xLine(length = -segLen(seg01))
4410|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4411|> close()
4412|> extrude(length = 40.14)
4413|> shell(
4414    thickness = 3.14,
4415    faces = [seg01]
4416)
4417"#;
4418
4419        let ctx = crate::test_server::new_context(true, None).await.unwrap();
4420        let old_program = crate::Program::parse_no_errs(code).unwrap();
4421
4422        // Execute the program.
4423        if let Err(err) = ctx.run_with_caching(old_program).await {
4424            let report = err.into_miette_report_with_outputs(code).unwrap();
4425            let report = miette::Report::new(report);
4426            panic!("Error executing program: {report:?}");
4427        }
4428
4429        // Get the id_generator from the first execution.
4430        let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4431
4432        let code = r#"sketch001 = startSketchOn(XZ)
4433|> startProfile(at = [62.74, 206.13])
4434|> xLine(length = 305.11, tag = $seg01)
4435|> yLine(length = -291.85)
4436|> xLine(length = -segLen(seg01))
4437|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4438|> close()
4439|> extrude(length = 40.14)
4440|> shell(
4441    faces = [seg01],
4442    thickness = 3.14,
4443)
4444"#;
4445
4446        // Execute a slightly different program again.
4447        let program = crate::Program::parse_no_errs(code).unwrap();
4448        // Execute the program.
4449        ctx.run_with_caching(program).await.unwrap();
4450
4451        let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4452
4453        assert_eq!(id_generator, new_id_generator);
4454    }
4455
4456    #[tokio::test(flavor = "multi_thread")]
4457    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
4458        let code = r#"sketch001 = startSketchOn(XZ)
4459|> startProfile(at = [61.74, 206.13])
4460|> xLine(length = 305.11, tag = $seg01)
4461|> yLine(length = -291.85)
4462|> xLine(length = -segLen(seg01))
4463|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4464|> close()
4465|> extrude(length = 40.14)
4466|> shell(
4467    thickness = 3.14,
4468    faces = [seg01]
4469)
4470"#;
4471
4472        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
4473        let old_program = crate::Program::parse_no_errs(code).unwrap();
4474
4475        // Execute the program.
4476        ctx.run_with_caching(old_program.clone()).await.unwrap();
4477
4478        let settings_state = cache::read_old_ast().await.unwrap().settings;
4479
4480        // Ensure the settings are as expected.
4481        assert_eq!(settings_state, ctx.settings);
4482
4483        // Change a setting.
4484        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4485
4486        // Execute the program.
4487        ctx.run_with_caching(old_program.clone()).await.unwrap();
4488
4489        let settings_state = cache::read_old_ast().await.unwrap().settings;
4490
4491        // Ensure the settings are as expected.
4492        assert_eq!(settings_state, ctx.settings);
4493
4494        // Change a setting.
4495        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4496
4497        // Execute the program.
4498        ctx.run_with_caching(old_program).await.unwrap();
4499
4500        let settings_state = cache::read_old_ast().await.unwrap().settings;
4501
4502        // Ensure the settings are as expected.
4503        assert_eq!(settings_state, ctx.settings);
4504
4505        ctx.close().await;
4506    }
4507
4508    #[tokio::test(flavor = "multi_thread")]
4509    async fn mock_after_not_mock() {
4510        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4511        let program = crate::Program::parse_no_errs("x = 2").unwrap();
4512        let result = ctx.run_with_caching(program).await.unwrap();
4513        assert_number_variable(&result.variables, "x", 2.0);
4514
4515        let ctx2 = ExecutorContext::new_mock(None).await;
4516        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
4517        let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
4518        assert_number_variable(&result.variables, "z", 3.0);
4519
4520        ctx.close().await;
4521        ctx2.close().await;
4522    }
4523
4524    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/12498
4525    #[tokio::test(flavor = "multi_thread")]
4526    async fn mock_execution_succeeds_after_split() {
4527        let code = kcl_input!("repro_mock_extrude");
4528        let ctx = ExecutorContext::new_mock(None).await;
4529        let program = crate::Program::parse_no_errs(code).unwrap();
4530        let _result = match ctx.run_mock(&program, &MockConfig::default()).await {
4531            Ok(res) => res,
4532            Err(e) => panic!("{}", e.error),
4533        };
4534    }
4535
4536    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/13319
4537    #[tokio::test(flavor = "multi_thread")]
4538    async fn mock_execution_rejects_oob_on_frontend_array() {
4539        let code = r#"
4540values = [10, 20]
4541third = values[2]
4542"#;
4543        let ctx = ExecutorContext::new_mock(None).await;
4544        let program = crate::Program::parse_no_errs(code).unwrap();
4545        let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
4546        ctx.close().await;
4547
4548        assert!(
4549            err.error.message().contains("array doesn't have any item at index 2"),
4550            "{err:?}"
4551        );
4552    }
4553
4554    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/13103
4555    /// i.e.
4556    /// If you do a pattern circular 3d in mock execution mode,
4557    /// and you ask for 10 instances, you should get 10 instances.
4558    #[tokio::test(flavor = "multi_thread")]
4559    async fn mock_execution_pattern_circular_number() {
4560        let code = kcl_input!("repro_mock_pattern_circular");
4561        let ctx = ExecutorContext::new_mock(None).await;
4562        let program = crate::Program::parse_no_errs(code).unwrap();
4563        let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
4564        let copies = result
4565            .variables
4566            .get("copies")
4567            .expect("no variable called 'copies' found");
4568        let value = match copies {
4569            KclValueView::Solid { .. } => {
4570                panic!("One solid?");
4571            }
4572            KclValueView::HomArray { value } => value,
4573            other => panic!("{other:#?}"),
4574        };
4575        let actual_instances = value.len();
4576        let expected_instances = 10; // from the KCL `instances = `
4577        assert_eq!(actual_instances, expected_instances);
4578    }
4579
4580    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/13103
4581    /// i.e.
4582    /// If you do a pattern circular 3d in mock execution mode,
4583    /// and you ask for 10 instances, you should get 10 instances.
4584    #[tokio::test(flavor = "multi_thread")]
4585    async fn mock_execution_subtract() {
4586        // Run this KCL file, in mock execution.
4587        let code = kcl_input!("repro_mock_subtract");
4588        let ctx = ExecutorContext::new_mock(None).await;
4589        let program = crate::Program::parse_no_errs(code).unwrap();
4590        let result = ctx.run_mock(&program, &MockConfig::default()).await;
4591        ctx.close().await;
4592        let result = match result {
4593            Ok(x) => x,
4594            Err(e) => {
4595                let error = e.error;
4596                panic!("{error}");
4597            }
4598        };
4599
4600        // Get the variable we're interested in, from KCL program memory.
4601        let subtracted_parts = result
4602            .variables
4603            .get("subtractedParts")
4604            .expect("no variable called 'subtracted_parts' found");
4605        let subtracted_parts = match subtracted_parts {
4606            KclValueView::Solid { .. } => {
4607                panic!("One solid?");
4608            }
4609            KclValueView::HomArray { value } => value,
4610            other => panic!("{other:#?}"),
4611        };
4612
4613        // Validate the variable.
4614        // from the KCL, there's 2 parts being subtracted from.
4615        let expected_number_of_parts = 2;
4616        let actual_number_of_parts = subtracted_parts.len();
4617        assert_eq!(actual_number_of_parts, expected_number_of_parts);
4618    }
4619
4620    #[tokio::test(flavor = "multi_thread")]
4621    async fn mock_then_add_extrude_then_mock_again() {
4622        let code = "s = sketch(on = XY) {
4623    line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
4624    line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
4625    coincident([line1.end, line2.start])
4626    line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
4627    coincident([line2.end, line3.start])
4628    coincident([line1.start, line3.end])
4629}
4630    ";
4631        let ctx = ExecutorContext::new_mock(None).await;
4632        let program = crate::Program::parse_no_errs(code).unwrap();
4633        let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
4634        assert!(result.variables.contains_key("s"), "actual: {:?}", result.variables);
4635
4636        let code2 = code.to_owned()
4637            + "
4638region001 = region(point = [1mm, 1mm], sketch = s)
4639extrude001 = extrude(region001, length = 1)
4640    ";
4641        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4642        let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
4643        assert!(
4644            result.variables.contains_key("region001"),
4645            "actual: {:?}",
4646            result.variables
4647        );
4648
4649        ctx.close().await;
4650    }
4651
4652    #[tokio::test(flavor = "multi_thread")]
4653    async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
4654        let code = format!(
4655            r#"{}
4656
4657face7 = faceOf(solid6, face = r6.tags.line1)
4658r7 = squareRegion(onSurface = face7)
4659solid7 = extrude(r7, length = width)
4660"#,
4661            include_str!("../../tests/endless_impeller/input.kcl")
4662        );
4663
4664        let result = parse_execute(&code).await.unwrap();
4665        let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
4666        assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
4667
4668        let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
4669            KclValue::Face { value } => value,
4670            value => panic!("expected face7 to be a Face, got {value:?}"),
4671        };
4672        assert!(face7.parent_solid.creator_sketch_id.is_some());
4673    }
4674
4675    #[tokio::test(flavor = "multi_thread")]
4676    async fn mock_has_stable_ids() {
4677        let ctx = ExecutorContext::new_mock(None).await;
4678        let mock_config = MockConfig {
4679            use_prev_memory: false,
4680            ..Default::default()
4681        };
4682        let code = "sk = startSketchOn(XY)
4683        |> startProfile(at = [0, 0])";
4684        let program = crate::Program::parse_no_errs(code).unwrap();
4685        let result = ctx.run_mock(&program, &mock_config).await.unwrap();
4686        let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4687        assert!(!ids.is_empty(), "IDs should not be empty");
4688
4689        let ctx2 = ExecutorContext::new_mock(None).await;
4690        let program2 = crate::Program::parse_no_errs(code).unwrap();
4691        let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
4692        let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4693
4694        assert_eq!(ids, ids2, "Generated IDs should match");
4695        ctx.close().await;
4696        ctx2.close().await;
4697    }
4698
4699    #[tokio::test(flavor = "multi_thread")]
4700    async fn mock_memory_restore_preserves_module_maps() {
4701        clear_mem_cache().await;
4702
4703        let ctx = ExecutorContext::new_mock(None).await;
4704        let cold_start = MockConfig {
4705            use_prev_memory: false,
4706            ..Default::default()
4707        };
4708        ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
4709
4710        let mut mem = cache::read_old_memory().await.unwrap();
4711        assert!(
4712            mem.path_to_source_id.len() > 3,
4713            "expected prelude imports to populate multiple modules, got {:?}",
4714            mem.path_to_source_id
4715        );
4716        mem.constraint_state.insert(
4717            crate::front::ObjectId(1),
4718            indexmap::indexmap! {
4719                crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
4720                    crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
4721            },
4722        );
4723
4724        let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
4725        ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
4726
4727        assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
4728        assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
4729        assert_eq!(exec_state.global.module_infos, mem.module_infos);
4730        assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
4731
4732        clear_mem_cache().await;
4733        ctx.close().await;
4734    }
4735
4736    #[tokio::test(flavor = "multi_thread")]
4737    async fn run_with_caching_no_action_refreshes_mock_memory() {
4738        cache::bust_cache().await;
4739        clear_mem_cache().await;
4740
4741        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
4742        let program = crate::Program::parse_no_errs(
4743            r#"sketch001 = sketch(on = XY) {
4744  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
4745}
4746"#,
4747        )
4748        .unwrap();
4749
4750        ctx.run_with_caching(program.clone()).await.unwrap();
4751        let baseline_memory = cache::read_old_memory().await.unwrap();
4752        assert!(
4753            !baseline_memory.scene_objects.is_empty(),
4754            "expected engine execution to persist full-scene mock memory"
4755        );
4756
4757        cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
4758        assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
4759
4760        ctx.run_with_caching(program).await.unwrap();
4761        let refreshed_memory = cache::read_old_memory().await.unwrap();
4762        assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
4763        assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
4764        assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
4765
4766        cache::bust_cache().await;
4767        clear_mem_cache().await;
4768        ctx.close().await;
4769    }
4770
4771    #[tokio::test(flavor = "multi_thread")]
4772    async fn sim_sketch_mode_real_mock_real() {
4773        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4774        let code = r#"sketch001 = startSketchOn(XY)
4775profile001 = startProfile(sketch001, at = [0, 0])
4776  |> line(end = [10, 0])
4777  |> line(end = [0, 10])
4778  |> line(end = [-10, 0])
4779  |> line(end = [0, -10])
4780  |> close()
4781"#;
4782        let program = crate::Program::parse_no_errs(code).unwrap();
4783        let result = ctx.run_with_caching(program).await.unwrap();
4784        assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4785
4786        let mock_ctx = ExecutorContext::new_mock(None).await;
4787        let mock_program = crate::Program::parse_no_errs(code).unwrap();
4788        let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
4789        assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4790
4791        let code2 = code.to_owned()
4792            + r#"
4793extrude001 = extrude(profile001, length = 10)
4794"#;
4795        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4796        let result = ctx.run_with_caching(program2).await.unwrap();
4797        assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
4798
4799        ctx.close().await;
4800        mock_ctx.close().await;
4801    }
4802
4803    #[tokio::test(flavor = "multi_thread")]
4804    async fn read_tag_version() {
4805        let ast = r#"fn bar(@t) {
4806  return startSketchOn(XY)
4807    |> startProfile(at = [0,0])
4808    |> angledLine(
4809        angle = -60,
4810        length = segLen(t),
4811    )
4812    |> line(end = [0, 0])
4813    |> close()
4814}
4815
4816sketch = startSketchOn(XY)
4817  |> startProfile(at = [0,0])
4818  |> line(end = [0, 10])
4819  |> line(end = [10, 0], tag = $tag0)
4820  |> line(endAbsolute = [0, 0])
4821
4822fn foo() {
4823  // tag0 tags an edge
4824  return bar(tag0)
4825}
4826
4827solid = sketch |> extrude(length = 10)
4828// tag0 tags a face
4829sketch2 = startSketchOn(solid, face = tag0)
4830  |> startProfile(at = [0,0])
4831  |> line(end = [0, 1])
4832  |> line(end = [1, 0])
4833  |> line(end = [0, 0])
4834
4835foo() |> extrude(length = 1)
4836"#;
4837        parse_execute(ast).await.unwrap();
4838    }
4839
4840    #[tokio::test(flavor = "multi_thread")]
4841    async fn experimental() {
4842        let code = r#"
4843startSketchOn(XY)
4844  |> startProfile(at = [0, 0], tag = $start)
4845  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4846"#;
4847        let result = parse_execute(code).await.unwrap();
4848        let issues = result.exec_state.issues();
4849        assert_eq!(issues.len(), 1);
4850        assert_eq!(issues[0].severity, Severity::Error);
4851        let msg = &issues[0].message;
4852        assert!(msg.contains("experimental"), "found {msg}");
4853
4854        let code = r#"@settings(experimentalFeatures = allow)
4855startSketchOn(XY)
4856  |> startProfile(at = [0, 0], tag = $start)
4857  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4858"#;
4859        let result = parse_execute(code).await.unwrap();
4860        let issues = result.exec_state.issues();
4861        assert!(issues.is_empty(), "issues={issues:#?}");
4862
4863        let code = r#"@settings(experimentalFeatures = warn)
4864startSketchOn(XY)
4865  |> startProfile(at = [0, 0], tag = $start)
4866  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4867"#;
4868        let result = parse_execute(code).await.unwrap();
4869        let issues = result.exec_state.issues();
4870        assert_eq!(issues.len(), 1);
4871        assert_eq!(issues[0].severity, Severity::Warning);
4872        let msg = &issues[0].message;
4873        assert!(msg.contains("experimental"), "found {msg}");
4874
4875        let code = r#"@settings(experimentalFeatures = deny)
4876startSketchOn(XY)
4877  |> startProfile(at = [0, 0], tag = $start)
4878  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4879"#;
4880        let result = parse_execute(code).await.unwrap();
4881        let issues = result.exec_state.issues();
4882        assert_eq!(issues.len(), 1);
4883        assert_eq!(issues[0].severity, Severity::Error);
4884        let msg = &issues[0].message;
4885        assert!(msg.contains("experimental"), "found {msg}");
4886
4887        let code = r#"@settings(experimentalFeatures = foo)
4888startSketchOn(XY)
4889  |> startProfile(at = [0, 0], tag = $start)
4890  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4891"#;
4892        parse_execute(code).await.unwrap_err();
4893    }
4894
4895    #[tokio::test(flavor = "multi_thread")]
4896    async fn experimental_parameter() {
4897        let code = r#"
4898fn inc(@x, @(experimental = true) amount? = 1) {
4899  return x + amount
4900}
4901
4902answer = inc(5, amount = 2)
4903"#;
4904        let result = parse_execute(code).await.unwrap();
4905        let issues = result.exec_state.issues();
4906        assert_eq!(issues.len(), 1);
4907        assert_eq!(issues[0].severity, Severity::Error);
4908        let msg = &issues[0].message;
4909        assert!(msg.contains("experimental"), "found {msg}");
4910
4911        // If the parameter isn't used, there's no warning.
4912        let code = r#"
4913fn inc(@x, @(experimental = true) amount? = 1) {
4914  return x + amount
4915}
4916
4917answer = inc(5)
4918"#;
4919        let result = parse_execute(code).await.unwrap();
4920        let issues = result.exec_state.issues();
4921        assert!(issues.is_empty(), "issues={issues:#?}");
4922    }
4923
4924    #[tokio::test(flavor = "multi_thread")]
4925    async fn experimental_scalar_fixed_constraint() {
4926        let code_left = r#"@settings(experimentalFeatures = warn)
4927sketch(on = XY) {
4928  point1 = point(at = [var 0mm, var 0mm])
4929  point1.at[0] == 1mm
4930}
4931"#;
4932        // It's symmetric. Flipping the binary operator has the same behavior.
4933        let code_right = r#"@settings(experimentalFeatures = warn)
4934sketch(on = XY) {
4935  point1 = point(at = [var 0mm, var 0mm])
4936  1mm == point1.at[0]
4937}
4938"#;
4939
4940        for code in [code_left, code_right] {
4941            let result = parse_execute(code).await.unwrap();
4942            let issues = result.exec_state.issues();
4943            let Some(error) = issues
4944                .iter()
4945                .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
4946            else {
4947                panic!("found {issues:#?}");
4948            };
4949            assert_eq!(error.severity, Severity::Warning);
4950        }
4951    }
4952
4953    // START Mock Execution tests
4954    // Ideally, we would do this as part of all sim tests and delete these one-off tests.
4955
4956    #[tokio::test(flavor = "multi_thread")]
4957    async fn test_tangent_line_arc_executes_with_mock_engine() {
4958        let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
4959        parse_execute(&code).await.unwrap();
4960    }
4961
4962    #[tokio::test(flavor = "multi_thread")]
4963    async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
4964        let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
4965        parse_execute(&code).await.unwrap();
4966    }
4967
4968    #[tokio::test(flavor = "multi_thread")]
4969    async fn test_tangent_line_circle_executes_with_mock_engine() {
4970        let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
4971        parse_execute(&code).await.unwrap();
4972    }
4973
4974    #[tokio::test(flavor = "multi_thread")]
4975    async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
4976        let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
4977        parse_execute(&code).await.unwrap();
4978    }
4979
4980    #[tokio::test(flavor = "multi_thread")]
4981    async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
4982        let code = r#"startX = 2
4983
4984baseSketch = sketch(on = XY) {
4985  yoyo = line(start = [startX, 0], end = [7, 6])
4986  line2 = line(start = [7, 6], end = [7, 12])
4987  hi = line(start = [7, 12], end = [startX, 0])
4988}
4989
4990baseRegion = region(point = [5.5, 6], sketch = baseSketch)
4991myExtrude = extrude(
4992  baseRegion,
4993  length = 5,
4994  tagEnd = $endCap,
4995  tagStart = $startCap,
4996)
4997yodawg = getCommonEdge(faces = [
4998  baseRegion.tags.hi,
4999  baseRegion.tags.yoyo
5000])
5001
5002cutSketch = sketch(on = YZ) {
5003  myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
5004  myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
5005  line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
5006}
5007
5008cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
5009extrude001 = extrude(cutRegion, length = 5)
5010solid001 = subtract(myExtrude, tools = extrude001)
5011
5012yoyo = getOppositeEdge(baseRegion.tags.hi)
5013fillet(solid001, radius = 0.1, tags = yoyo)
5014"#;
5015
5016        parse_execute(code).await.unwrap();
5017    }
5018
5019    // END Mock Execution tests
5020
5021    // Sketch constraint report tests
5022
5023    async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
5024        let program = crate::Program::parse_no_errs(kcl).unwrap();
5025        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5026        let mut exec_state = ExecState::new(&ctx);
5027        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
5028        let outcome = exec_state
5029            .into_exec_outcome(env_ref, &ctx)
5030            .await
5031            .expect("constraint report test outcome should collect variables");
5032        let report = outcome.sketch_constraint_report();
5033        ctx.close().await;
5034        report
5035    }
5036
5037    #[tokio::test(flavor = "multi_thread")]
5038    async fn warn_when_sketch_is_over_constrained() {
5039        let code = r#"
5040sketch001 = sketch(on = XY) {
5041  line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
5042  fixed([line1.start, ORIGIN])
5043  fixed([line1.start, [20, 20]])
5044}
5045"#;
5046        let result = parse_execute(code).await.unwrap();
5047        let issues = result.exec_state.issues();
5048        let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
5049            panic!("expected over-constrained warning; found {issues:#?}");
5050        };
5051        assert_eq!(warning.severity, Severity::Warning);
5052    }
5053
5054    #[tokio::test(flavor = "multi_thread")]
5055    async fn over_constrained_warning_identifies_signed_vertical_distance_direction() {
5056        let code = r#"
5057sketch001 = sketch(on = XY) {
5058  line1 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
5059  fixed([line1.start, [0mm, 10mm]])
5060  fixed([line1.end, ORIGIN])
5061  verticalDistance([line1.start, line1.end]) == 10mm
5062}
5063"#;
5064        let result = parse_execute(code).await.unwrap();
5065        let issues = result.exec_state.issues();
5066        let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
5067            panic!("expected over-constrained warning; found {issues:#?}");
5068        };
5069        assert!(
5070            warning.message.contains(
5071                "Unsatisfied signed verticalDistance constraint: a positive right-hand side requires the second point to be above the first"
5072            ),
5073            "expected signed-direction diagnostic; found {warning:#?}"
5074        );
5075    }
5076
5077    #[tokio::test(flavor = "multi_thread")]
5078    async fn no_warning_when_sketch_is_not_over_constrained() {
5079        // Under-constrained sketch should not emit the over-constrained warning.
5080        let code = r#"
5081sketch001 = sketch(on = XY) {
5082  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5083}
5084"#;
5085        let result = parse_execute(code).await.unwrap();
5086        let issues = result.exec_state.issues();
5087        assert!(
5088            !issues.iter().any(|issue| issue.message.contains("over-constrained")),
5089            "did not expect over-constrained warning; found {issues:#?}"
5090        );
5091    }
5092
5093    #[tokio::test(flavor = "multi_thread")]
5094    async fn test_constraint_report_fully_constrained() {
5095        // All points are fully constrained via equality constraints.
5096        let kcl = r#"
5097@settings(experimentalFeatures = allow)
5098
5099sketch(on = YZ) {
5100  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
5101  line1.start.at[0] == 2
5102  line1.start.at[1] == 8
5103  line1.end.at[0] == 5
5104  line1.end.at[1] == 7
5105}
5106"#;
5107        let report = run_constraint_report(kcl).await;
5108        assert_eq!(report.fully_constrained.len(), 1);
5109        assert_eq!(report.under_constrained.len(), 0);
5110        assert_eq!(report.over_constrained.len(), 0);
5111        assert_eq!(report.errors.len(), 0);
5112        assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
5113    }
5114
5115    #[tokio::test(flavor = "multi_thread")]
5116    async fn test_constraint_report_under_constrained() {
5117        // No constraints at all — all points are free.
5118        let kcl = r#"
5119sketch(on = YZ) {
5120  line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
5121}
5122"#;
5123        let report = run_constraint_report(kcl).await;
5124        assert_eq!(report.fully_constrained.len(), 0);
5125        assert_eq!(report.under_constrained.len(), 1);
5126        assert_eq!(report.over_constrained.len(), 0);
5127        assert_eq!(report.errors.len(), 0);
5128        assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
5129        assert!(report.under_constrained[0].free_count > 0);
5130    }
5131
5132    #[tokio::test(flavor = "multi_thread")]
5133    async fn test_constraint_report_over_constrained() {
5134        // Conflicting distance constraints on the same pair of points.
5135        let kcl = r#"
5136@settings(experimentalFeatures = allow)
5137
5138sketch(on = YZ) {
5139  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
5140  line1.start.at[0] == 2
5141  line1.start.at[1] == 8
5142  line1.end.at[0] == 5
5143  line1.end.at[1] == 7
5144  distance([line1.start, line1.end]) == 100mm
5145}
5146"#;
5147        let report = run_constraint_report(kcl).await;
5148        assert_eq!(report.over_constrained.len(), 1);
5149        assert_eq!(report.errors.len(), 0);
5150        assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
5151        assert!(report.over_constrained[0].conflict_count > 0);
5152    }
5153
5154    #[tokio::test(flavor = "multi_thread")]
5155    async fn test_constraint_report_multiple_sketches() {
5156        // Two sketches: one fully constrained, one under-constrained.
5157        let kcl = r#"
5158@settings(experimentalFeatures = allow)
5159
5160s1 = sketch(on = YZ) {
5161  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
5162  line1.start.at[0] == 2
5163  line1.start.at[1] == 8
5164  line1.end.at[0] == 5
5165  line1.end.at[1] == 7
5166}
5167
5168s2 = sketch(on = XZ) {
5169  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5170}
5171"#;
5172        let report = run_constraint_report(kcl).await;
5173        assert_eq!(
5174            report.fully_constrained.len()
5175                + report.under_constrained.len()
5176                + report.over_constrained.len()
5177                + report.errors.len(),
5178            2,
5179            "Expected 2 sketches total"
5180        );
5181        assert_eq!(report.fully_constrained.len(), 1);
5182        assert_eq!(report.under_constrained.len(), 1);
5183    }
5184
5185    #[tokio::test(flavor = "multi_thread")]
5186    async fn test_constraint_report_reports_sketch_names() {
5187        // One file holding a fully constrained, an under-constrained, and an
5188        // over-constrained sketch. Every entry carries the name of the
5189        // variable its sketch was assigned to, so a caller can say which
5190        // sketch needs correcting.
5191        let kcl = r#"
5192@settings(experimentalFeatures = allow)
5193
5194fixedSketch = sketch(on = YZ) {
5195  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
5196  line1.start.at[0] == 2
5197  line1.start.at[1] == 8
5198  line1.end.at[0] == 5
5199  line1.end.at[1] == 7
5200}
5201
5202looseSketch = sketch(on = XZ) {
5203  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5204}
5205
5206conflictSketch = sketch(on = XY) {
5207  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
5208  line1.start.at[0] == 2
5209  line1.start.at[1] == 8
5210  line1.end.at[0] == 5
5211  line1.end.at[1] == 7
5212  distance([line1.start, line1.end]) == 100mm
5213}
5214"#;
5215        let report = run_constraint_report(kcl).await;
5216        assert_eq!(report.errors.len(), 0);
5217        assert_eq!(report.fully_constrained.len(), 1);
5218        assert_eq!(report.under_constrained.len(), 1);
5219        assert_eq!(report.over_constrained.len(), 1);
5220        assert_eq!(report.fully_constrained[0].name, "fixedSketch");
5221        assert_eq!(report.under_constrained[0].name, "looseSketch");
5222        assert_eq!(report.over_constrained[0].name, "conflictSketch");
5223    }
5224
5225    #[tokio::test(flavor = "multi_thread")]
5226    async fn test_constraint_report_name_empty_without_declaration() {
5227        // A sketch written as an expression statement has no enclosing
5228        // variable declaration, so there is no name to report. This pins the
5229        // documented limitation of SketchConstraintStatus::name.
5230        let kcl = r#"
5231sketch(on = YZ) {
5232  line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
5233}
5234"#;
5235        let report = run_constraint_report(kcl).await;
5236        assert_eq!(report.under_constrained.len(), 1);
5237        assert_eq!(report.under_constrained[0].name, "");
5238    }
5239
5240    #[tokio::test(flavor = "multi_thread")]
5241    async fn test_constraint_report_names_repeat_across_calls() {
5242        // Both sketches come from the same declaration inside the function
5243        // body, so both entries carry that declaration's name and the report
5244        // cannot tell them apart. This pins the documented limitation of
5245        // SketchConstraintStatus::name.
5246        let kcl = r#"
5247fn makeSketch() {
5248  inner = sketch(on = XY) {
5249    line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5250  }
5251  return inner
5252}
5253
5254first = makeSketch()
5255second = makeSketch()
5256"#;
5257        let report = run_constraint_report(kcl).await;
5258        assert_eq!(report.under_constrained.len(), 2);
5259        assert_eq!(report.under_constrained[0].name, "inner");
5260        assert_eq!(report.under_constrained[1].name, "inner");
5261    }
5262
5263    #[tokio::test(flavor = "multi_thread")]
5264    async fn test_enum_declaration_is_experimental() {
5265        // Without opting in, executing a program with an enum declaration
5266        // fails at the parsing stage with the experimental diagnostic.
5267        let code = "type Color { | Red }";
5268        assert_eq!(
5269            parse_execute(code).await.unwrap_err().message(),
5270            "Use of enum declarations is experimental and may change or be removed."
5271        );
5272    }
5273
5274    #[tokio::test(flavor = "multi_thread")]
5275    async fn enum_declaration_registers_type() {
5276        // Plain and exported declarations both execute. Nothing references the
5277        // enum yet, so this only asserts that declaring one is no longer an
5278        // error; constructor use is exercised separately.
5279        let code = r#"@settings(experimentalFeatures = allow)
5280type Color { | Red | Green }
5281"#;
5282        parse_execute(code).await.unwrap();
5283
5284        let code = r#"@settings(experimentalFeatures = allow)
5285export type Color { | Red | Green }
5286"#;
5287        parse_execute(code).await.unwrap();
5288
5289        // A zero-variant enum is a valid declaration.
5290        let code = r#"@settings(experimentalFeatures = allow)
5291type Empty { | }
5292"#;
5293        parse_execute(code).await.unwrap();
5294    }
5295
5296    #[tokio::test(flavor = "multi_thread")]
5297    async fn enum_declaration_rejects_nested_scope() {
5298        // Identity is (module, declared name), so two same-named declarations in
5299        // one file would collide. The parser and formatter accept this shape, so
5300        // execution is the only thing that can reject it.
5301        //
5302        // The rule is about nesting, not about one kind of block, so both routes
5303        // to `BodyType::Block` are covered here.
5304        let allow = "@settings(experimentalFeatures = allow)\n";
5305        for (case, code) in [
5306            (
5307                "function body",
5308                format!("{allow}fn palette() {{\n  type Color {{ | Red }}\n  return 0\n}}\npalette()\n"),
5309            ),
5310            (
5311                "sketch block",
5312                format!(
5313                    "{allow}sketch(on = XY) {{\n  type Color {{ | Red }}\n  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
5314                ),
5315            ),
5316        ] {
5317            assert_eq!(
5318                parse_execute(&code).await.unwrap_err().message(),
5319                "Enum declarations are only supported at the top-level of a file. Move `type Color` to the top-level.",
5320                "case: {case}"
5321            );
5322        }
5323    }
5324
5325    #[tokio::test(flavor = "multi_thread")]
5326    async fn enum_alone_is_restricted_to_top_level() {
5327        // Pins the asymmetry the rule above creates: a type alias may be declared
5328        // in any block, an enum may not. The difference is required by enum
5329        // identity rather than chosen -- two nested aliases shadow each other
5330        // harmlessly, while two nested `type Color` declarations would be one type
5331        // with two variant sets. Tightening aliases to match, or relaxing enums,
5332        // has to break this test first.
5333        let allow = "@settings(experimentalFeatures = allow)\n";
5334        for (case, code) in [
5335            (
5336                "function body",
5337                format!("{allow}fn f() {{\n  type Temperature = number(_)\n  return 0\n}}\nx = f()\n"),
5338            ),
5339            (
5340                "sketch block",
5341                format!(
5342                    "{allow}sketch(on = XY) {{\n  type Temperature = number(_)\n  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
5343                ),
5344            ),
5345        ] {
5346            parse_execute(&code)
5347                .await
5348                .unwrap_or_else(|err| panic!("a type alias should be allowed in a {case}: {}", err.message()));
5349        }
5350    }
5351
5352    #[tokio::test(flavor = "multi_thread")]
5353    async fn enum_declaration_rejects_duplicate() {
5354        let code = r#"@settings(experimentalFeatures = allow)
5355type Color { | Red | Green | Red }
5356"#;
5357        assert_eq!(
5358            parse_execute(code).await.unwrap_err().message(),
5359            "Duplicate variant `Red` in enum `Color`."
5360        );
5361    }
5362
5363    /// Runs `main` with `modules` written beside it, so import paths resolve.
5364    async fn execute_with_modules(main: &str, modules: &[(&str, &str)]) -> Result<ExecTestResults, KclError> {
5365        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_enum_clash").unwrap();
5366        for (name, source) in modules {
5367            tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
5368        }
5369
5370        parse_execute_with_project_dir(main, Some(crate::TypedPath(tmpdir.path().into()))).await
5371    }
5372
5373    /// Runs `main` with an empty imported module named `m.kcl` in mock
5374    /// execution and returns the recorded compilation issues; the run may
5375    /// end in an error (e.g. from operating on the module's missing return
5376    /// value).
5377    ///
5378    /// The `m.kcl` module lives in an in-memory file system under a
5379    /// synthetic project directory, so parallel tests share no on-disk
5380    /// state and there is nothing to clean up even if the process is
5381    /// killed.
5382    async fn issues_with_empty_module(main: &str) -> Vec<crate::errors::CompilationIssue> {
5383        use futures::FutureExt;
5384
5385        let project_dir = crate::TypedPath::new("/zma-kcl-member-ranges");
5386        // Key the file by the same join that import resolution performs, so
5387        // the lookup matches on every platform.
5388        let files = [(project_dir.join("m.kcl").to_string(), Vec::new())]
5389            .into_iter()
5390            .collect();
5391
5392        let program = crate::Program::parse_no_errs(main).unwrap();
5393        let ctx = ExecutorContext {
5394            engine: Arc::new(EngineManager::new_mock()),
5395            engine_batch: EngineBatchContext::default(),
5396            fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
5397            settings: ExecutorSettings {
5398                project_directory: Some(project_dir),
5399                ..Default::default()
5400            },
5401            context_type: ContextType::Mock,
5402            execution_callbacks: Default::default(),
5403            executor_kind: machine::ExecutorKind::resolve(),
5404            machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
5405        };
5406        let mut exec_state = ExecState::new(&ctx);
5407        // Close the context even if execution panics, then let the panic
5408        // continue. An Err from the run itself is expected here (operating
5409        // on the module's missing return value) and is deliberately ignored.
5410        let run_result = std::panic::AssertUnwindSafe(ctx.run(&program, &mut exec_state))
5411            .catch_unwind()
5412            .await;
5413        ctx.close().await;
5414        if let Err(panic) = run_result {
5415            std::panic::resume_unwind(panic);
5416        }
5417        exec_state.issues().to_vec()
5418    }
5419
5420    #[tokio::test(flavor = "multi_thread")]
5421    async fn member_object_diagnostics_use_object_range() {
5422        // A diagnostic raised while evaluating a member expression's object
5423        // (here, the imported module's missing-return warning) points at the
5424        // object's own span, not the whole member expression.
5425        let main = "import \"m.kcl\" as m
5426x = m.field
5427";
5428        let issues = issues_with_empty_module(main).await;
5429        let warning = issues
5430            .iter()
5431            .find(|issue| issue.message.contains("no return value"))
5432            .expect("missing-return warning should be recorded");
5433        let object_start = main.rfind("m.field").unwrap();
5434        assert_eq!(
5435            (warning.source_range.start(), warning.source_range.end()),
5436            (object_start, object_start + 1),
5437            "warning should point at the object's span"
5438        );
5439    }
5440
5441    #[tokio::test(flavor = "multi_thread")]
5442    async fn member_property_diagnostics_use_property_range() {
5443        // Same for the computed property: the warning points at the index
5444        // expression's span inside the brackets.
5445        let main = "import \"m.kcl\" as m
5446arr = [1]
5447x = arr[m]
5448";
5449        let issues = issues_with_empty_module(main).await;
5450        let warning = issues
5451            .iter()
5452            .find(|issue| issue.message.contains("no return value"))
5453            .expect("missing-return warning should be recorded");
5454        let prop_start = main.rfind("[m]").unwrap() + 1;
5455        assert_eq!(
5456            (warning.source_range.start(), warning.source_range.end()),
5457            (prop_start, prop_start + 1),
5458            "warning should point at the property's span"
5459        );
5460    }
5461
5462    #[tokio::test(flavor = "multi_thread")]
5463    async fn backtrace_reports_fully_qualified_fn_names() {
5464        // An error inside a function called by a qualified name records the
5465        // full path (m::f), not just the final segment (f), in the
5466        // structured backtrace's unwind locations.
5467        let main = "import \"m.kcl\" as m\nx = m::f()\n";
5468        let modules = [("m.kcl", "export fn f() {\n  return undefinedVariable\n}\n")];
5469        let err = execute_with_modules(main, &modules).await.unwrap_err();
5470        let fn_names: Vec<_> = err.backtrace().into_iter().filter_map(|item| item.fn_name).collect();
5471        assert_eq!(fn_names, vec!["m::f".to_owned()]);
5472    }
5473
5474    #[tokio::test(flavor = "multi_thread")]
5475    async fn whole_module_name_executes_as_operand() {
5476        // A whole-module import used as a binary or unary operand executes
5477        // the module and operates on its final-expression value, exactly like
5478        // using the name in expression position (x = m).
5479        let main = r#"import "m.kcl" as m
5480sum = m + m
5481neg = -m
5482"#;
5483        let result = execute_with_modules(main, &[("m.kcl", "42\n")]).await.unwrap();
5484        assert_eq!(
5485            mem_get_json(result.exec_state.stack(), result.mem_env, "sum").as_f64(),
5486            Some(84.0)
5487        );
5488        assert_eq!(
5489            mem_get_json(result.exec_state.stack(), result.mem_env, "neg").as_f64(),
5490            Some(-42.0)
5491        );
5492    }
5493
5494    #[tokio::test(flavor = "multi_thread")]
5495    async fn whole_module_without_return_as_operand_errors() {
5496        // Matches expression-position behavior: the module still executes,
5497        // the missing-return fallback produces a KclNone, and the binary
5498        // operation then rejects it. (A trailing declaration would count as
5499        // the module's return value, so the module body must be empty.)
5500        let main = "import \"m.kcl\" as m
5501x = m + 1
5502";
5503        let err = execute_with_modules(main, &[("m.kcl", "")]).await.unwrap_err();
5504        assert!(
5505            err.message().contains("Expected a number, but found none"),
5506            "expected the operand to be the module's missing-return KclNone, got: {}",
5507            err.message()
5508        );
5509    }
5510
5511    #[tokio::test(flavor = "multi_thread")]
5512    async fn enum_rejects_name_clash_with_module() {
5513        // One rule reached four ways: by declaring the enum second, by importing
5514        // the module second, and by importing the enum itself either by name or
5515        // through a glob, which arrive by different code paths because a glob
5516        // copies exported keys with their namespace prefix intact.
5517        let plain_module = ("Color.kcl", "export x = 1\n");
5518        let enum_module = (
5519            "enums.kcl",
5520            "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
5521        );
5522
5523        for (case, main, modules) in [
5524            (
5525                "module then enum",
5526                "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\ntype Color { | Red }\n",
5527                vec![plain_module],
5528            ),
5529            (
5530                "enum then module",
5531                "@settings(experimentalFeatures = allow)\ntype Color { | Red }\nimport \"Color.kcl\"\n",
5532                vec![plain_module],
5533            ),
5534            (
5535                "named import of an enum",
5536                "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport Color from 'enums.kcl'\n",
5537                vec![plain_module, enum_module],
5538            ),
5539            (
5540                "glob import of an enum",
5541                "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport * from 'enums.kcl'\n",
5542                vec![plain_module, enum_module],
5543            ),
5544        ] {
5545            let err = execute_with_modules(main, &modules).await.unwrap_err();
5546            assert_eq!(
5547                err.message(),
5548                "An enum and a module cannot share the name `Color` in the same scope, because `Color::x` would be ambiguous. Rename one of them.",
5549                "case: {case}"
5550            );
5551        }
5552    }
5553
5554    #[tokio::test(flavor = "multi_thread")]
5555    async fn enum_constructs_variant() {
5556        let allow = "@settings(experimentalFeatures = allow)\n";
5557        let colors = (
5558            "colors.kcl",
5559            "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
5560        );
5561
5562        for (case, main, modules) in [
5563            (
5564                "declared locally",
5565                format!("{allow}type Color {{ | Red | Green }}\nx = Color::Red\n"),
5566                vec![],
5567            ),
5568            (
5569                // Also the regression test for the export check: a module's exports
5570                // record the prefixed key `__ty_Color`, not the bare name.
5571                "reached through a module path",
5572                format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
5573                vec![colors],
5574            ),
5575            (
5576                "imported by name",
5577                format!("{allow}import Color from 'colors.kcl'\nx = Color::Red\n"),
5578                vec![colors],
5579            ),
5580            (
5581                // An import alias renames the binding, not the type, so identity
5582                // and therefore the reported name stay those of the declaration.
5583                "imported under an alias",
5584                format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade::Red\n"),
5585                vec![colors],
5586            ),
5587        ] {
5588            let result = execute_with_modules(&main, &modules)
5589                .await
5590                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
5591            let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
5592                panic!("case: {case}: `x` should hold an enum value");
5593            };
5594            assert_eq!(value.qualified_name(), "Color::Red", "case: {case}");
5595        }
5596    }
5597
5598    // The next five tests pin lexical resolution of signature types: a type
5599    // name written in a function signature resolves in the scope where the
5600    // declaration executes, never in the caller's scope. Before
5601    // definition-time resolution, signature types were looked up at each call
5602    // in the caller's environment, so a std or user module whose exported
5603    // types a caller had not imported under their bare names was uncallable.
5604
5605    #[tokio::test(flavor = "multi_thread")]
5606    async fn signature_types_resolve_in_declaring_module() {
5607        let colors = (
5608            "colors.kcl",
5609            "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n  return c\n}\n",
5610        );
5611        // The caller can reach `colors::Color` but never binds the bare name
5612        // `Color`, so resolving the signature in the caller's scope would fail.
5613        let main =
5614            "@settings(experimentalFeatures = allow)\nimport \"colors.kcl\"\nr = colors::paint(colors::Color::Red)\n";
5615
5616        let result = execute_with_modules(main, &[colors]).await.unwrap();
5617        let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
5618            panic!("`r` should hold an enum value");
5619        };
5620        assert_eq!(value.qualified_name(), "Color::Red");
5621    }
5622
5623    #[tokio::test(flavor = "multi_thread")]
5624    async fn signature_types_resolve_under_import_alias() {
5625        // An import alias renames the caller's binding for the module. The
5626        // declaring module's scope is unaffected, so the signature must
5627        // resolve identically under any alias.
5628        let colors = (
5629            "colors.kcl",
5630            "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n  return c\n}\n",
5631        );
5632        let main = "@settings(experimentalFeatures = allow)\nimport \"colors.kcl\" as painter\nr = painter::paint(painter::Color::Red)\n";
5633
5634        let result = execute_with_modules(main, &[colors]).await.unwrap();
5635        let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
5636            panic!("`r` should hold an enum value");
5637        };
5638        assert_eq!(value.qualified_name(), "Color::Red");
5639    }
5640
5641    #[tokio::test(flavor = "multi_thread")]
5642    async fn signature_types_ignore_caller_scope() {
5643        // `broken.kcl` names a type it does not define. The caller defines
5644        // that name, which caller-scope resolution would have used. The
5645        // declaration must fail when the module loads, without consulting the
5646        // caller's binding.
5647        let broken = (
5648            "broken.kcl",
5649            "@settings(experimentalFeatures = allow)\nexport fn f(@x: Missing) {\n  return x\n}\n",
5650        );
5651        let main = "@settings(experimentalFeatures = allow)\ntype Missing = string\nimport \"broken.kcl\"\nr = broken::f(\"hi\")\n";
5652
5653        let err = execute_with_modules(main, &[broken]).await.unwrap_err();
5654        assert!(
5655            err.message().contains("Unknown type: Missing"),
5656            "message: {}",
5657            err.message()
5658        );
5659    }
5660
5661    #[tokio::test(flavor = "multi_thread")]
5662    async fn signature_types_reject_forward_reference() {
5663        // Resolution happens when the declaration executes, so a type declared
5664        // later in the file is not visible. The function is never called; the
5665        // error must surface at the declaration itself.
5666        let main = "@settings(experimentalFeatures = allow)\nfn f(@x: Later) {\n  return x\n}\ntype Later = string\n";
5667
5668        let err = parse_execute(main).await.unwrap_err();
5669        assert!(
5670            err.message().contains("Unknown type: Later"),
5671            "message: {}",
5672            err.message()
5673        );
5674    }
5675
5676    #[tokio::test(flavor = "multi_thread")]
5677    async fn signature_types_resolve_in_enclosing_scope() {
5678        // The declaring scope is the closure's scope, not merely the declaring
5679        // module: the anonymous function's signature must see the alias in the
5680        // enclosing function body. Caller-scope resolution would use the
5681        // module-level `Width = string` and fail to coerce `42`.
5682        let main = "@settings(experimentalFeatures = allow)\ntype Width = string\nfn makeMeasure() {\n  type Width = number(mm)\n  return fn(@w: Width) { return w }\n}\nmeasure = makeMeasure()\nr = measure(42)\n";
5683
5684        let result = parse_execute(main).await.unwrap();
5685        let KclValue::Number { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
5686            panic!("`r` should hold a number");
5687        };
5688        assert_eq!(value, 42.0);
5689    }
5690
5691    // Pins that numeric types in signatures are settings-independent, so
5692    // definition-time resolution changed nothing for them: in type
5693    // annotations, bare `number` maps to `Any` before the settings-reading
5694    // path, and every explicit suffix maps to a settings-free type. A literal
5695    // argument therefore takes its unit from the CALLER's module defaults;
5696    // the declaring module's defaults (`in` here) must never leak in. If a
5697    // future change makes a signature's number type depend on module default
5698    // units, the declaring-module scope of definition-time resolution starts
5699    // to matter and this pin fails.
5700    #[tokio::test(flavor = "multi_thread")]
5701    async fn signature_number_types_ignore_module_default_units() {
5702        let units_in = (
5703            "units_in.kcl",
5704            "@settings(defaultLengthUnit = in)\nexport fn passThrough(@x: number(Length)) {\n  return x\n}\n",
5705        );
5706        // The caller's default length unit is mm (the test default), so the
5707        // unitless literal is 42 mm by the time it reaches the parameter.
5708        let main = "import \"units_in.kcl\"\na = units_in::passThrough(42)\nb = units_in::passThrough(42mm)\nc = units_in::passThrough(42in)\n";
5709
5710        let result = execute_with_modules(main, &[units_in]).await.unwrap();
5711        for (name, expected_ty) in [
5712            // The unitless literal keeps its `Default` type, and that type
5713            // records the CALLER's module settings. Declaring-module leakage
5714            // would show here as `len: Inches`.
5715            //
5716            // That the coercion to `number(Length)` leaves the type as
5717            // `Default` rather than concretizing it to `Known(Millimeters)`
5718            // is pre-existing coercion behavior which this test observes but
5719            // does not endorse. If coercion later concretizes, update the
5720            // expected type; the pin here is the settings provenance.
5721            (
5722                "a",
5723                kcl_api::NumericType::Default {
5724                    len: kcl_api::UnitLength::Millimeters,
5725                    angle: kcl_api::UnitAngle::Degrees,
5726                },
5727            ),
5728            (
5729                "b",
5730                kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Millimeters)),
5731            ),
5732            (
5733                "c",
5734                kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Inches)),
5735            ),
5736        ] {
5737            let KclValue::Number { value, ty, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name)
5738            else {
5739                panic!("`{name}` should hold a number");
5740            };
5741            assert_eq!(value, 42.0, "`{name}` should keep its magnitude");
5742            assert_eq!(ty, expected_ty, "`{name}` should keep the caller-side unit context");
5743        }
5744    }
5745
5746    // Pins the sharpest shadowing case, from a hand-written example during
5747    // review: BOTH scopes define the same type name with different meanings,
5748    // so the test observes which one the signature uses, not merely whether a
5749    // name is present. `m1.kcl`'s `A` is `string` and is NOT exported; the
5750    // caller's own `A` is `number(mm)`. The signature must use m1's `A`, so
5751    // passing `2mm` is a type error. Caller-scope resolution would have used
5752    // the caller's `A` and accepted the call.
5753    #[tokio::test(flavor = "multi_thread")]
5754    async fn signature_types_use_declaring_scope_when_both_scopes_define_the_name() {
5755        let m1 = (
5756            "m1.kcl",
5757            "@settings(experimentalFeatures = allow)\ntype A = string\n\nexport fn test(@a: A) {\n  return a\n}\n",
5758        );
5759        let main =
5760            "@settings(experimentalFeatures = allow)\nimport * from \"m1.kcl\"\ntype A = number(mm)\nx = test(2mm)\n";
5761
5762        let err = execute_with_modules(main, &[m1]).await.unwrap_err();
5763        assert_eq!(
5764            err.message(),
5765            "The input argument of `test` requires a value with type `A`, but found a number (mm) (with type `number(mm)`)."
5766        );
5767    }
5768
5769    #[tokio::test(flavor = "multi_thread")]
5770    async fn enum_rejects_bad_variant_paths() {
5771        let allow = "@settings(experimentalFeatures = allow)\n";
5772
5773        for (case, main, modules, message) in [
5774            (
5775                "unknown variant",
5776                format!("{allow}type Color {{ | Red | Green }}\nx = Color::Blue\n"),
5777                vec![],
5778                "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
5779            ),
5780            (
5781                "enum with no variants",
5782                format!("{allow}type Empty {{ | }}\nx = Empty::Red\n"),
5783                vec![],
5784                "`Red` is not a variant of enum `Empty`. Enum `Empty` has no variants.",
5785            ),
5786            (
5787                "path continues past the enum",
5788                format!("{allow}type Color {{ | Red }}\nx = Color::Red::more\n"),
5789                vec![],
5790                "`Color` is an enum, so only a variant name can follow it. There is nothing to reach through `Color::Red`.",
5791            ),
5792            (
5793                "variant name is case sensitive",
5794                format!("{allow}type Color {{ | Red }}\nx = Color::red\n"),
5795                vec![],
5796                "`red` is not a variant of enum `Color`. Its variants are: Red.",
5797            ),
5798            (
5799                "enum not exported from its module",
5800                format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
5801                vec![(
5802                    "colors.kcl",
5803                    "@settings(experimentalFeatures = allow)\ntype Color { | Red }\n",
5804                )],
5805                "Item Color not found in module's exported items",
5806            ),
5807            (
5808                // The alias exemption seen from the use site: a type alias is not
5809                // an enum, so the segment is resolved as a module and fails.
5810                "a type alias cannot head a path",
5811                format!("{allow}type T = number(_)\nx = T::foo\n"),
5812                vec![],
5813                "`T` is not defined",
5814            ),
5815            (
5816                // The other half of allowing a value and an enum to share a name:
5817                // a value on its own can never head a path.
5818                "a value cannot head a path",
5819                "Color = 5\nx = Color::Red\n".to_owned(),
5820                vec![],
5821                "`Color` is not defined",
5822            ),
5823        ] {
5824            let err = execute_with_modules(&main, &modules).await.unwrap_err();
5825            assert_eq!(err.message(), message, "case: {case}");
5826        }
5827    }
5828
5829    #[tokio::test(flavor = "multi_thread")]
5830    async fn enum_compares_by_variant() {
5831        let code = r#"@settings(experimentalFeatures = allow)
5832type Color { | Red | Green }
5833sameEq = Color::Red == Color::Red
5834sameNeq = Color::Red != Color::Red
5835otherEq = Color::Red == Color::Green
5836otherNeq = Color::Red != Color::Green
5837"#;
5838        let result = parse_execute(code).await.unwrap();
5839
5840        for (name, expected) in [
5841            ("sameEq", true),
5842            ("sameNeq", false),
5843            ("otherEq", false),
5844            ("otherNeq", true),
5845        ] {
5846            let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
5847                panic!("`{name}` should hold a bool");
5848            };
5849            assert_eq!(value, expected, "variable: {name}");
5850        }
5851    }
5852
5853    #[tokio::test(flavor = "multi_thread")]
5854    async fn enum_usable_inside_sketch_block() {
5855        // Only enum declarations are restricted to the top level; uses are not
5856        // restricted at all. A sketch block executes its body with sketch-mode
5857        // skipping turned off, and memory lookups walk outward, so the enum
5858        // declared above resolves inside the block.
5859        //
5860        // `assertIs` runs inside the block because block-local bindings live in a
5861        // child scope that the root environment cannot read afterwards. A wrong
5862        // comparison therefore fails this test instead of passing unnoticed.
5863        let code = r#"@settings(experimentalFeatures = allow)
5864type Color { | Red | Green }
5865sketch(on = XY) {
5866  c = Color::Red
5867  assertIs(Color::Red != Color::Green)
5868  assertIs(!(Color::Red != Color::Red))
5869  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
5870}
5871"#;
5872        parse_execute(code)
5873            .await
5874            .unwrap_or_else(|err| panic!("enum use inside a sketch block should work: {}", err.message()));
5875    }
5876
5877    #[tokio::test(flavor = "multi_thread")]
5878    async fn enum_eq_reserved_inside_sketch_block() {
5879        // Inside a sketch block, `==` declares an equivalence constraint, so it is
5880        // not available for ordinary comparison. Enums are not singled out: the
5881        // interception happens before any value-comparison arm is reached, and
5882        // strings and numbers are refused in the same words. The string and number
5883        // rows are here to keep that visible -- if a later change makes enums
5884        // report something different from the other types, this test says so.
5885        //
5886        // `!=` is deliberately absent: the interception tests `Eq` only, so `!=`
5887        // still compares, which `enum_usable_inside_sketch_block` covers.
5888        let allow = "@settings(experimentalFeatures = allow)\n";
5889        let tail = "  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}\n";
5890        for (case, declaration, comparison, types) in [
5891            (
5892                "enums",
5893                "type Color { | Red | Green }\n",
5894                "Color::Red == Color::Green",
5895                "a value of enum `Color` and a value of enum `Color`",
5896            ),
5897            ("strings", "", "\"a\" == \"b\"", "a string and a string"),
5898            ("numbers", "", "1 == 2", "a number and a number"),
5899        ] {
5900            let code = format!("{allow}{declaration}sketch(on = XY) {{\n  x = {comparison}\n{tail}");
5901            assert_eq!(
5902                parse_execute(&code).await.unwrap_err().message(),
5903                format!("Cannot create an equivalence constraint between values of these types: {types}"),
5904                "case: {case}"
5905            );
5906        }
5907    }
5908
5909    #[tokio::test(flavor = "multi_thread")]
5910    async fn enum_same_file_imported_twice_is_one_type() {
5911        // Two names for one declaration, so they are the same type and compare
5912        // equal. Identity is the declaration, not the binding, which is what makes
5913        // this different from two files that each declare a `Color`.
5914        let main = r#"@settings(experimentalFeatures = allow)
5915import Color as A from 'colors.kcl'
5916import Color as B from 'colors.kcl'
5917x = A::Red == B::Red
5918y = A::Red == B::Green
5919"#;
5920        let result = execute_with_modules(
5921            main,
5922            &[(
5923                "colors.kcl",
5924                "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
5925            )],
5926        )
5927        .await
5928        .unwrap();
5929
5930        for (name, expected) in [("x", true), ("y", false)] {
5931            let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
5932                panic!("`{name}` should hold a bool");
5933            };
5934            assert_eq!(value, expected, "variable: {name}");
5935        }
5936    }
5937
5938    #[tokio::test(flavor = "multi_thread")]
5939    async fn enum_rejects_comparison_across_types() {
5940        let allow = "@settings(experimentalFeatures = allow)\n";
5941        let color = (
5942            "a.kcl",
5943            "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
5944        );
5945        let other_color = (
5946            "b.kcl",
5947            "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
5948        );
5949
5950        for (case, main, modules, message) in [
5951            (
5952                "two enums declared separately",
5953                format!("{allow}type Color {{ | Red }}\ntype Shade {{ | Red }}\nx = Color::Red == Shade::Red\n"),
5954                vec![],
5955                "Cannot compare enum `Color` with enum `Shade`. They are different types.",
5956            ),
5957            (
5958                // Identity is the declaration, not the name, so two enums that
5959                // share a name are still different types. Pins that the message
5960                // says so rather than naming `Color` twice.
5961                "two enums sharing a name",
5962                format!(
5963                    "{allow}import Color as A from 'a.kcl'\nimport Color as B from 'b.kcl'\nx = A::Red == B::Red\n"
5964                ),
5965                vec![color, other_color],
5966                "Cannot compare two different enums that are both named `Color`. They come from separate declarations.",
5967            ),
5968            (
5969                "an enum and a number",
5970                format!("{allow}type Color {{ | Red }}\nx = Color::Red == 5\n"),
5971                vec![],
5972                "Cannot compare enum `Color::Red` with a number.",
5973            ),
5974            (
5975                "a number and an enum, in that order",
5976                format!("{allow}type Color {{ | Red }}\nx = 5 == Color::Red\n"),
5977                vec![],
5978                "Cannot compare enum `Color::Red` with a number.",
5979            ),
5980            (
5981                "an enum and a string",
5982                format!("{allow}type Color {{ | Red }}\nx = Color::Red == \"Red\"\n"),
5983                vec![],
5984                "Cannot compare enum `Color::Red` with a string.",
5985            ),
5986        ] {
5987            let err = execute_with_modules(&main, &modules).await.unwrap_err();
5988            assert_eq!(err.message(), message, "case: {case}");
5989        }
5990    }
5991
5992    #[tokio::test(flavor = "multi_thread")]
5993    async fn enum_rejects_bare_type_name_as_value() {
5994        let allow = "@settings(experimentalFeatures = allow)\n";
5995        let colors = (
5996            "colors.kcl",
5997            "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
5998        );
5999
6000        for (case, main, modules, message) in [
6001            (
6002                "enum suggests a variant",
6003                format!("{allow}type Color {{ | Red | Green }}\nx = Color\n"),
6004                vec![],
6005                "`Color` is a type, not a value. Use one of its variants, such as `Color::Red`.",
6006            ),
6007            (
6008                // The suggestion has to be pasteable into the file that produced
6009                // the error, so it uses the local name rather than the declared one.
6010                "suggestion uses the import alias",
6011                format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade\n"),
6012                vec![colors],
6013                "`Shade` is a type, not a value. Use one of its variants, such as `Shade::Red`.",
6014            ),
6015            (
6016                "enum with no variants suggests nothing",
6017                format!("{allow}type Empty {{ | }}\nx = Empty\n"),
6018                vec![],
6019                "`Empty` is a type, not a value.",
6020            ),
6021            (
6022                "a type alias reports the same way",
6023                format!("{allow}type T = number(_)\nx = T\n"),
6024                vec![],
6025                "`T` is a type, not a value.",
6026            ),
6027            (
6028                // Unchanged behavior: with no type of that name, the old message
6029                // is still the right one.
6030                "an unknown name is still undefined",
6031                "x = Nope\n".to_owned(),
6032                vec![],
6033                "`Nope` is not defined",
6034            ),
6035        ] {
6036            let err = execute_with_modules(&main, &modules).await.unwrap_err();
6037            assert_eq!(err.message(), message, "case: {case}");
6038        }
6039    }
6040
6041    #[tokio::test(flavor = "multi_thread")]
6042    async fn enum_use_gated_by_consuming_module() {
6043        // The declaring module allows experimental features; the consuming one
6044        // does not, so using the imported enum is what trips the gate. Pins that
6045        // the gate follows the consumer's settings rather than the declaration's.
6046        //
6047        // Experimental use is reported as a compilation issue rather than by
6048        // aborting the run, which is how `RuntimeType::from_alias` reports it too,
6049        // so execution succeeds and the diagnostic is what carries the complaint.
6050        let main = r#"import "colors.kcl"
6051x = colors::Color::Red
6052"#;
6053        let result = execute_with_modules(
6054            main,
6055            &[(
6056                "colors.kcl",
6057                "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
6058            )],
6059        )
6060        .await
6061        .unwrap();
6062
6063        let issues = &result.exec_state.global.issues;
6064        assert_eq!(issues.len(), 1, "issues: {issues:?}");
6065        assert_eq!(
6066            issues[0].message,
6067            "Use of the enum `Color` is experimental and may change or be removed."
6068        );
6069        assert_eq!(issues[0].severity, Severity::Error);
6070    }
6071
6072    #[tokio::test(flavor = "multi_thread")]
6073    async fn enum_use_not_gated_when_consumer_allows_it() {
6074        // The other half of the gate: with the setting present, using an enum
6075        // raises nothing at all.
6076        let code = r#"@settings(experimentalFeatures = allow)
6077type Color { | Red }
6078x = Color::Red
6079"#;
6080        let result = parse_execute(code).await.unwrap();
6081        assert!(
6082            result.exec_state.global.issues.is_empty(),
6083            "issues: {:?}",
6084            result.exec_state.global.issues
6085        );
6086    }
6087
6088    #[tokio::test(flavor = "multi_thread")]
6089    async fn enum_allows_name_sharing_outside_modules() {
6090        // Pins two deliberate exemptions from the clash rule above, so that
6091        // tightening it later has to be a decision rather than an accident.
6092        //
6093        // Only an enum or a module can head a `Color::Red` path, so only those two
6094        // can be ambiguous. A type alias cannot head a `::` path, and an ordinary
6095        // value is never looked up for a path head at all.
6096        for (case, main, modules) in [
6097            (
6098                // The module arrives second, which is the path carrying the
6099                // "only `TypeDef::Enum` conflicts" guard.
6100                "an alias may share a name with a module",
6101                "@settings(experimentalFeatures = allow)\ntype Temperature = number(_)\nimport \"Temperature.kcl\"\n",
6102                vec![("Temperature.kcl", "export x = 1\n")],
6103            ),
6104            (
6105                "a value may share a name with an enum",
6106                "@settings(experimentalFeatures = allow)\ntype Color { | Red }\nColor = 5\n",
6107                vec![],
6108            ),
6109        ] {
6110            if let Err(err) = execute_with_modules(main, &modules).await {
6111                panic!("case: {case}: {}", err.message());
6112            }
6113        }
6114    }
6115
6116    #[tokio::test(flavor = "multi_thread")]
6117    async fn enum_declaration_rejects_redefinition() {
6118        let code = r#"@settings(experimentalFeatures = allow)
6119type Color { | Red }
6120type Color { | Green }
6121"#;
6122        assert_eq!(
6123            parse_execute(code).await.unwrap_err().message(),
6124            "Redefinition of type Color."
6125        );
6126    }
6127
6128    /// Projection yields the variant's declared representation, which in V1 is
6129    /// always the variant name. Every row binds `x` so the rows differ only in the
6130    /// shape being projected, and the alias row is here because the target is
6131    /// resolved before projection decides anything, so an alias must behave
6132    /// exactly like the type it names.
6133    #[tokio::test(flavor = "multi_thread")]
6134    async fn enum_projects_to_string() {
6135        let header = r#"
6136            @settings(experimentalFeatures = allow)
6137            type Color { | Red | Green }
6138            type Label = string
6139        "#;
6140
6141        for (case, body, expected) in [
6142            ("a variant", "x = Color::Red: string", "Red"),
6143            ("another variant of the same enum", "x = Color::Green: string", "Green"),
6144            ("an alias of the target type", "x = Color::Red: Label", "Red"),
6145            (
6146                "an element of a projected array",
6147                r#"
6148                    pair = [Color::Red, Color::Green]: [string]
6149                    x = pair[1]
6150                "#,
6151                "Green",
6152            ),
6153            (
6154                "an element of a nested projected array",
6155                r#"
6156                    grid = [[Color::Green]]: [[string]]
6157                    x = grid[0][0]
6158                "#,
6159                "Green",
6160            ),
6161            (
6162                "a one-element array against a bare string",
6163                "x = [Color::Red]: string",
6164                "Red",
6165            ),
6166        ] {
6167            let result = parse_execute(&format!("{header}{body}\n"))
6168                .await
6169                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
6170            let KclValue::String { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
6171                panic!("case: {case}: `x` should hold a string");
6172            };
6173            assert_eq!(value, expected, "case: {case}");
6174        }
6175    }
6176
6177    /// Ascribing the enum's own type, directly or through an alias, is a check
6178    /// rather than a conversion: the value stays an enum and still compares equal
6179    /// to the variant it came from.
6180    #[tokio::test(flavor = "multi_thread")]
6181    async fn enum_ascription_keeps_the_enum() {
6182        let header = r#"
6183            @settings(experimentalFeatures = allow)
6184            type Color { | Red | Green }
6185            type Paint = Color
6186        "#;
6187
6188        for (case, expression, expected) in [
6189            ("its own type", "(Color::Red: Color) == Color::Red", true),
6190            ("an alias of its own type", "(Color::Red: Paint) == Color::Red", true),
6191            (
6192                "the ascription does not change which variant it is",
6193                "(Color::Red: Color) == Color::Green",
6194                false,
6195            ),
6196        ] {
6197            let result = parse_execute(&format!("{header}x = {expression}\n"))
6198                .await
6199                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
6200            let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
6201                panic!("case: {case}: `x` should hold a bool");
6202            };
6203            assert_eq!(value, expected, "case: {case}");
6204        }
6205    }
6206
6207    /// A boundary the user did not write must not project, or a nominal parameter
6208    /// type would mean nothing. The rows are the separate coercion sites: the
6209    /// unlabeled argument, a labeled argument, and the return.
6210    #[tokio::test(flavor = "multi_thread")]
6211    async fn enum_projection_is_not_implicit() {
6212        let header = r#"
6213            @settings(experimentalFeatures = allow)
6214            type Color { | Red | Green }
6215        "#;
6216        let found = "but found a value of enum `Color` (with type `Color`).";
6217
6218        for (case, body, expected) in [
6219            (
6220                "unlabeled argument",
6221                r#"
6222                    fn label(@text: string) { return text }
6223                    x = label(Color::Red)
6224                "#,
6225                format!("The input argument of `label` requires a value with type `string`, {found}"),
6226            ),
6227            (
6228                "labeled argument",
6229                r#"
6230                    fn label(text: string) { return text }
6231                    x = label(text = Color::Red)
6232                "#,
6233                format!("text requires a value with type `string`, {found}"),
6234            ),
6235            (
6236                "return",
6237                r#"
6238                    fn label(): string { return Color::Red }
6239                    x = label()
6240                "#,
6241                format!("This function requires its result to be a value with type `string`, {found}"),
6242            ),
6243            (
6244                // The reported type is `[any; 1]` rather than `[Color; 1]` because
6245                // an array literal does not infer a homogeneous element type. That
6246                // is pre-existing and unrelated to enums; it is pinned here so the
6247                // row is not read as an enum-specific quirk.
6248                "inside an array at an argument boundary",
6249                r#"
6250                    fn labels(@text: [string]) { return text }
6251                    x = labels([Color::Red])
6252                "#,
6253                "The input argument of `labels` requires an array of strings (`[string]`), but found an array of `Color` with 1 value (with type `[any; 1]`).".to_owned(),
6254            ),
6255        ] {
6256            assert_eq!(
6257                parse_execute(&format!("{header}{body}\n")).await.unwrap_err().message(),
6258                expected,
6259                "case: {case}"
6260            );
6261        }
6262    }
6263
6264    /// What an explicit ascription refuses, and what it says about it. The numeric
6265    /// rows deliberately do not name the mechanism a later version would use.
6266    #[tokio::test(flavor = "multi_thread")]
6267    async fn enum_ascription_rejections() {
6268        let header = r#"
6269            @settings(experimentalFeatures = allow)
6270            type Color { | Red }
6271            type Shade { | Red }
6272        "#;
6273        let no_number = "Cannot project enum `Color` to a number. An enum projects to `string`; projecting to a number is not supported yet.";
6274
6275        for (case, expression, expected) in [
6276            ("a number target", "Color::Red: number(_)", no_number.to_owned()),
6277            (
6278                "a number target reached through an array, so the reason survives the walk",
6279                "[Color::Red]: [number(_)]",
6280                no_number.to_owned(),
6281            ),
6282            (
6283                "a boolean target, which is not a projection at all",
6284                "Color::Red: bool",
6285                "could not coerce a value of enum `Color` (with type `Color`) to type `bool`".to_owned(),
6286            ),
6287            (
6288                "another enum whose variants happen to match",
6289                "Color::Red: Shade",
6290                "could not coerce a value of enum `Color` (with type `Color`) to type `Shade`".to_owned(),
6291            ),
6292        ] {
6293            assert_eq!(
6294                parse_execute(&format!("{header}x = {expression}\n"))
6295                    .await
6296                    .unwrap_err()
6297                    .message(),
6298                expected,
6299                "case: {case}"
6300            );
6301        }
6302    }
6303
6304    /// The mirror of `enum_projection_is_not_implicit`: where the declared type is
6305    /// the enum itself, a value flows through every boundary unchanged. Each row
6306    /// binds `x` to a comparison that must hold, so a value that arrived altered
6307    /// would fail rather than pass unnoticed. `Some(message)` marks a row that must
6308    /// be refused instead, which is what keeps the check nominal rather than
6309    /// merely permissive.
6310    #[tokio::test(flavor = "multi_thread")]
6311    async fn enum_flows_through_declared_types() {
6312        let header = r#"
6313            @settings(experimentalFeatures = allow)
6314            type Color { | Red | Green }
6315            type Shade { | Red }
6316        "#;
6317
6318        for (case, body, expected) in [
6319            (
6320                "an unlabeled parameter",
6321                r#"
6322                    fn paint(@c: Color) { return c }
6323                    x = paint(Color::Red) == Color::Red
6324                "#,
6325                None,
6326            ),
6327            (
6328                "a labeled parameter",
6329                r#"
6330                    fn paint(c: Color) { return c }
6331                    x = paint(c = Color::Green) == Color::Green
6332                "#,
6333                None,
6334            ),
6335            (
6336                "a declared return type",
6337                r#"
6338                    fn pick(): Color { return Color::Red }
6339                    x = pick() == Color::Red
6340                "#,
6341                None,
6342            ),
6343            (
6344                "an array parameter",
6345                r#"
6346                    fn firstOf(@cs: [Color]) { return cs[0] }
6347                    x = firstOf([Color::Red, Color::Green]) == Color::Red
6348                "#,
6349                None,
6350            ),
6351            (
6352                // The field check is `has_type`, which an enum satisfies, so an
6353                // object passes here while the projection row of
6354                // `enum_projects_by_target_shape` fails. Both behaviors come from
6355                // the same unfinished object coercion.
6356                "an object field",
6357                r#"
6358                    fn take(@o: { c: Color }) { return o.c }
6359                    x = take({ c = Color::Green }) == Color::Green
6360                "#,
6361                None,
6362            ),
6363            (
6364                "a union that names the enum",
6365                r#"
6366                    fn either(@v: Color | string) { return v }
6367                    x = either(Color::Red) == Color::Red
6368                "#,
6369                None,
6370            ),
6371            (
6372                "the same union given the other member",
6373                r#"
6374                    fn either(@v: Color | string) { return v }
6375                    x = either("plain") == "plain"
6376                "#,
6377                None,
6378            ),
6379            (
6380                "another declaration at the same boundary",
6381                r#"
6382                    fn paint(@c: Color) { return c }
6383                    x = paint(Shade::Red) == Shade::Red
6384                "#,
6385                Some(
6386                    "The input argument of `paint` requires a value with type `Color`, but found a value of enum `Shade` (with type `Shade`).",
6387                ),
6388            ),
6389        ] {
6390            let code = format!("{header}{body}\n");
6391            match expected {
6392                None => {
6393                    let result = parse_execute(&code)
6394                        .await
6395                        .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
6396                    let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x")
6397                    else {
6398                        panic!("case: {case}: `x` should hold a bool");
6399                    };
6400                    assert!(value, "case: {case}: the value did not survive the boundary");
6401                }
6402                Some(message) => assert_eq!(
6403                    parse_execute(&code).await.unwrap_err().message(),
6404                    message,
6405                    "case: {case}"
6406                ),
6407            }
6408        }
6409    }
6410}