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