Skip to main content

kcl_lib/execution/
mod.rs

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