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