Skip to main content

kcl_lib/execution/
mod.rs

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