Skip to main content

kcl_lib/execution/
mod.rs

1//! The executor for the AST.
2
3use std::collections::BTreeMap;
4use std::future::Future;
5use std::sync::Arc;
6
7use anyhow::Result;
8pub use artifact::ArtifactCommand;
9pub(crate) use artifact::EntityCloneInfo;
10pub(crate) use artifact::named_view_artifact;
11pub(crate) use artifact::sketch_block_constraint_type;
12pub use cache::Cache;
13use cache::GlobalState;
14use futures::future::BoxFuture;
15pub use geometry::*;
16pub use id_generator::IdGenerator;
17pub(crate) use import::PreImportedGeometry;
18use indexmap::IndexMap;
19pub use kcl_api::DefaultPlanes;
20use kcl_api::Number;
21use kcl_api::Object;
22use kcl_api::ObjectId;
23pub use kcl_api::Operation;
24pub use kcl_api::artifact::Artifact;
25pub use kcl_api::artifact::ArtifactGraph;
26pub use kcl_api::artifact::CapSubType;
27pub use kcl_api::artifact::CodeRef;
28pub use kcl_api::artifact::GdtAnnotationArtifact;
29pub use kcl_api::artifact::SketchBlock;
30pub use kcl_api::artifact::SketchBlockConstraint;
31#[allow(unused_imports)]
32pub use kcl_api::artifact::SketchBlockConstraintType;
33pub use kcl_api::artifact::StartSketchOnFace;
34pub use kcl_api::artifact::StartSketchOnPlane;
35use kcl_api::ast::node_path::NodePath;
36pub use kcl_value::KclObjectFields;
37pub use kcl_value::KclObjectKind;
38pub use kcl_value::KclValue;
39pub use kcl_value_view::EdgeCutViewExt;
40pub use kcl_value_view::ExtrudeSurfaceViewExt;
41pub use kcl_value_view::KclValueView;
42pub use kcl_value_view::PathViewExt;
43pub use kcl_value_view::SolidViewExt;
44use kcmc::ImageFormat;
45use kcmc::ModelingCmd;
46use kcmc::each_cmd as mcmd;
47use kcmc::ok_response::OkModelingCmdResponse;
48use kcmc::ok_response::output::TakeSnapshot;
49use kcmc::websocket::ModelingSessionData;
50use kcmc::websocket::OkWebSocketResponseData;
51use kittycad_modeling_cmds::id::ModelingCmdId;
52use kittycad_modeling_cmds::{self as kcmc};
53pub use memory::EnvironmentRef;
54#[cfg(test)]
55pub(crate) use memory::MemoryBackendKind;
56pub(crate) use modeling::ModelingCmdMeta;
57pub use named_views::*;
58use serde::Deserialize;
59use serde::Serialize;
60pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
61pub(crate) use sketch_solve::solver_numeric_type;
62pub(crate) use solver_arc::SolverArc;
63pub(crate) use state::ConstraintKey;
64pub(crate) use state::ConstraintState;
65pub(crate) use state::ConsumedRegionInfo;
66pub(crate) use state::ConsumedRegionOperation;
67pub(crate) use state::ConsumedSolidInfo;
68pub(crate) use state::ConsumedSolidKey;
69pub(crate) use state::ConsumedSolidOperation;
70pub use state::DirectTagFilletMeta;
71pub use state::DirectTagFilletTagEntry;
72pub use state::EdgeRefactorMeta;
73pub use state::EdgeRefactorStdlibFn;
74pub use state::ExecState;
75pub use state::KclVersion;
76pub use state::LegacyAngleRefactorMeta;
77pub use state::MetaSettings;
78pub(crate) use state::ModuleArtifactState;
79pub use state::NotYetAdded;
80pub(crate) use state::PendingEdgeRefactorMeta;
81pub(crate) use state::PendingLegacyAngleRefactorMeta;
82pub use state::RefactorMetadata;
83pub(crate) use state::TangencyMode;
84pub(crate) use state::computed_kcl_version;
85pub(crate) use state::declared_kcl_version;
86
87use crate::CompilationIssue;
88use crate::ExecError;
89use crate::KclErrorWithOutputs;
90use crate::NodePathExt;
91use crate::SourceRange;
92use crate::collections::AhashIndexSet;
93use crate::engine::EngineBatchContext;
94use crate::engine::GridScaleBehavior;
95use crate::engine::engine_manager::EngineManager;
96use crate::errors::KclError;
97use crate::errors::KclErrorDetails;
98use crate::execution::cache::CacheInformation;
99use crate::execution::cache::CacheResult;
100use crate::execution::cad_op::OperationExt;
101use crate::execution::import_graph::Universe;
102use crate::execution::import_graph::UniverseMap;
103use crate::execution::typed_path::TypedPath;
104use crate::front::PointExt;
105use crate::front::SegmentExt;
106use crate::fs::FileManager;
107use crate::fs::FileSystemHandle;
108use crate::modules::ModuleExecutionOutcome;
109use crate::modules::ModuleId;
110use crate::modules::ModulePath;
111use crate::modules::ModulePathExt as _;
112use crate::modules::ModuleRepr;
113use crate::modules::ModuleSource;
114use crate::named_views::values as named_views;
115use crate::parsing::ast::types::Expr;
116use crate::parsing::ast::types::ImportPath;
117use crate::parsing::ast::types::NodeRef;
118
119#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
120#[ts(export)]
121pub struct OperationsByModule {
122    pub map: IndexMap<ModuleId, Vec<Operation>>,
123}
124
125#[derive(Clone, Serialize, ts_rs::TS)]
126#[ts(export)]
127#[serde(rename_all = "camelCase")]
128pub struct OperationCallbackArgs {
129    pub module_id: ModuleId,
130    pub operation: Operation,
131    pub index: usize,
132}
133
134pub trait ExecutionCallbacks: std::fmt::Debug + Send + Sync + 'static {
135    fn on_operation(&self, _args: OperationCallbackArgs) {}
136}
137
138impl OperationsByModule {
139    pub fn count(&self) -> usize {
140        self.map.values().map(Vec::len).sum()
141    }
142
143    pub fn is_empty(&self) -> bool {
144        self.map.values().all(Vec::is_empty)
145    }
146
147    pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
148        self.map.get(module_id)
149    }
150
151    pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
152        self.map.values()
153    }
154
155    pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
156        self.map.insert(module_id, operations);
157    }
158}
159
160pub(crate) mod annotations;
161mod artifact;
162#[cfg(test)]
163pub(crate) use artifact::mermaid_tests::ArtifactGraphMermaidExt;
164pub(crate) mod cache;
165mod cad_op;
166pub(crate) mod exec_ast;
167pub mod fn_call;
168#[cfg(test)]
169mod freedom_analysis_tests;
170mod geometry;
171#[cfg(test)]
172mod hide_id_contract_kcl_test_pins;
173mod id_generator;
174mod import;
175mod import_graph;
176pub(crate) mod kcl_value;
177pub(crate) mod kcl_value_view;
178pub(crate) mod machine;
179mod memory;
180mod modeling;
181mod sketch_solve;
182mod solver_arc;
183mod state;
184pub use kcl_api::typed_path;
185pub(crate) mod types;
186
187pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
188pub(crate) const SKETCH_OBJECT_META: &str = "meta";
189pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
190
191/// Convenience macro for handling [`KclValueControlFlow`] in execution by
192/// returning early if it is some kind of early return or stripping off the
193/// control flow otherwise. If it's an early return, it's returned as a
194/// `Result::Ok`.
195macro_rules! control_continue {
196    ($control_flow:expr) => {{
197        let cf = $control_flow;
198        if cf.is_some_return() {
199            return Ok(cf);
200        } else {
201            cf.into_value()
202        }
203    }};
204}
205// Expose the macro to other modules.
206pub(crate) use control_continue;
207
208/// Convenience macro for handling [`KclValueControlFlow`] in execution by
209/// returning early if it is some kind of early return or stripping off the
210/// control flow otherwise. If it's an early return, [`EarlyReturn`] is
211/// used to return it as a `Result::Err`.
212macro_rules! early_return {
213    ($control_flow:expr) => {{
214        let cf = $control_flow;
215        if cf.is_some_return() {
216            return Err(EarlyReturn::from(cf));
217        } else {
218            cf.into_value()
219        }
220    }};
221}
222// Expose the macro to other modules.
223pub(crate) use early_return;
224
225#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
226pub enum ControlFlowKind {
227    /// Normal control flow. Continue to the next step.
228    #[default]
229    Continue,
230    /// A `return` statement executed under KCL 3.0: unwind to the nearest
231    /// function-call boundary, which absorbs it as the function's result. Never
232    /// constructed under older entry points, whose `return` uses
233    /// write-and-continue semantics instead; see `bind_return_value`.
234    Return,
235    /// `exit()` was called: unwind all the way to the program root, bypassing
236    /// function-call boundaries.
237    Exit,
238}
239
240impl ControlFlowKind {
241    /// Returns true if this is any kind of early return.
242    pub fn is_some_return(&self) -> bool {
243        match self {
244            ControlFlowKind::Continue => false,
245            ControlFlowKind::Return => true,
246            ControlFlowKind::Exit => true,
247        }
248    }
249}
250
251#[must_use = "You should always handle the control flow value when it is returned"]
252#[derive(Debug, Clone, PartialEq, Serialize)]
253pub struct KclValueControlFlow {
254    /// Use [control_continue] or [Self::into_value] to get the value.
255    value: Box<KclValue>,
256    pub control: ControlFlowKind,
257}
258
259impl KclValue {
260    pub(crate) fn continue_(self) -> KclValueControlFlow {
261        KclValueControlFlow {
262            value: Box::new(self),
263            control: ControlFlowKind::Continue,
264        }
265    }
266
267    pub(crate) fn return_(self) -> KclValueControlFlow {
268        KclValueControlFlow {
269            value: Box::new(self),
270            control: ControlFlowKind::Return,
271        }
272    }
273
274    pub(crate) fn exit(self) -> KclValueControlFlow {
275        KclValueControlFlow {
276            value: Box::new(self),
277            control: ControlFlowKind::Exit,
278        }
279    }
280}
281
282impl KclValueControlFlow {
283    /// Returns true if this is any kind of early return.
284    pub fn is_some_return(&self) -> bool {
285        self.control.is_some_return()
286    }
287
288    pub(crate) fn is_return(&self) -> bool {
289        matches!(self.control, ControlFlowKind::Return)
290    }
291
292    pub(crate) fn is_exit(&self) -> bool {
293        matches!(self.control, ControlFlowKind::Exit)
294    }
295
296    /// The source ranges of the wrapped value, for error reporting.
297    pub(crate) fn source_ranges(&self) -> Vec<SourceRange> {
298        let deduplicated = self
299            .value
300            .metadata()
301            .into_iter()
302            .map(|m| m.source_range)
303            .collect::<AhashIndexSet<_>>();
304        deduplicated.into_iter().collect()
305    }
306
307    pub(crate) fn into_value(self) -> KclValue {
308        *self.value
309    }
310}
311
312/// A [`KclValueControlFlow`] or an error that needs to be returned early. This
313/// is useful for when functions might encounter either control flow or errors
314/// that need to bubble up early, but these aren't the primary return values of
315/// the function. We can use `EarlyReturn` as the error type in a `Result`.
316///
317/// Normally, you don't construct this directly. Use the `early_return!` macro.
318#[must_use = "You should always handle the control flow value when it is returned"]
319#[allow(clippy::large_enum_variant)]
320#[derive(Debug, Clone)]
321pub(crate) enum EarlyReturn {
322    /// A normal value with control flow.
323    Value(KclValueControlFlow),
324    /// An error that occurred during execution.
325    Error(KclError),
326}
327
328impl From<KclValueControlFlow> for EarlyReturn {
329    fn from(cf: KclValueControlFlow) -> Self {
330        EarlyReturn::Value(cf)
331    }
332}
333
334impl From<KclError> for EarlyReturn {
335    fn from(err: KclError) -> Self {
336        EarlyReturn::Error(err)
337    }
338}
339
340pub(crate) enum StatementKind<'a> {
341    Declaration { name: &'a str },
342    Expression,
343}
344
345#[derive(Debug, Clone, Copy)]
346pub enum PreserveMem {
347    Normal,
348    Always,
349}
350
351impl PreserveMem {
352    fn normal(self) -> bool {
353        match self {
354            PreserveMem::Normal => true,
355            PreserveMem::Always => false,
356        }
357    }
358}
359
360/// Outcome of executing a program.  This is used in TS.
361#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
362#[ts(export)]
363#[serde(rename_all = "camelCase")]
364pub struct ExecOutcome {
365    /// Variables in the top-level of the root module. Note that functions will have an invalid env ref.
366    pub variables: IndexMap<String, KclValueView>,
367    /// Runtime memory retained only for tests that need to verify internal behavior.
368    #[cfg(test)]
369    #[serde(skip)]
370    #[ts(skip)]
371    pub(crate) test_program_memory: IndexMap<String, KclValue>,
372    /// Operations that have been performed in execution order, grouped by
373    /// owning module id, for display in the Feature Tree.
374    pub operations: OperationsByModule,
375    /// Output artifact graph.
376    pub artifact_graph: ArtifactGraph,
377    /// Objects in the scene, created from execution.
378    #[serde(skip)]
379    pub scene_objects: Vec<Object>,
380    /// Map from source range to object ID for lookup of objects by their source
381    /// range.
382    #[serde(skip)]
383    pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
384    #[serde(skip)]
385    pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
386    /// Execution-backed metadata used by Z0006 and future auto-refactors.
387    pub refactor_metadata: Vec<RefactorMetadata>,
388    /// Non-fatal errors and warnings.
389    pub issues: Vec<CompilationIssue>,
390    /// File Names in module Id array index order
391    pub filenames: IndexMap<ModuleId, ModulePath>,
392    /// Whether this outcome was reused from the previous run because the
393    /// program had not changed, which leaves the engine's geometry untouched.
394    pub from_cache: bool,
395    /// Source code of each module, for rendering issues against the module
396    /// their source range points into. Not serialized to keep the WASM
397    /// payload small; native callers (e.g. the Python bindings) read it
398    /// directly.
399    #[serde(skip)]
400    pub source_files: IndexMap<ModuleId, ModuleSource>,
401    /// The default planes.
402    pub default_planes: Option<DefaultPlanes>,
403}
404
405/// Per-segment freedom used by the constraint report. Mirrors
406/// [`kcl_api::Freedom`] but adds an `Error` variant for when
407/// a point lookup fails.
408#[derive(Debug, Clone, Copy, PartialEq)]
409enum SegmentFreedom {
410    Free,
411    Fixed,
412    Conflict,
413    /// A required point could not be found in the scene graph.
414    Error,
415}
416
417impl From<kcl_api::Freedom> for SegmentFreedom {
418    fn from(f: kcl_api::Freedom) -> Self {
419        match f {
420            kcl_api::Freedom::Free => Self::Free,
421            kcl_api::Freedom::Fixed => Self::Fixed,
422            kcl_api::Freedom::Conflict => Self::Conflict,
423        }
424    }
425}
426
427/// Overall constraint status of a sketch.
428#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
429pub enum ConstraintKind {
430    FullyConstrained,
431    UnderConstrained,
432    OverConstrained,
433    /// Analysis could not determine constraint status (e.g., a point lookup
434    /// failed due to an inconsistent scene graph). Callers decide how to treat
435    /// this — as under-constrained, over-constrained, or something else.
436    Error,
437}
438
439/// Per-sketch summary of constraint freedom analysis.
440///
441/// A sketch with no countable segments (`total_count == 0`) is reported as
442/// [`ConstraintKind::FullyConstrained`]. This is vacuously true — there are
443/// no free or conflicting segments. Callers can check `total_count == 0` to
444/// distinguish this from a genuinely constrained sketch.
445#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
446pub struct SketchConstraintStatus {
447    /// Name of the variable the sketch was assigned to, for example
448    /// "sketch001". This is the nearest enclosing declaration at the point the
449    /// sketch was created, which is not always the sketch's own name:
450    /// - Empty for a sketch written as an expression statement, because there
451    ///   is no enclosing declaration.
452    /// - The outer variable's name for a sketch passed straight into another
453    ///   call, as in `part = extrude(sketch(on = XY) { ... }, length = 10)`.
454    /// - The same name for two sketches, when a function body declares the
455    ///   sketch and is called more than once.
456    ///
457    /// This name is accepted by [`ExecOutcome::render_sketch_png_instance`].
458    pub name: String,
459    /// Zero-based creation order among sketches with this name, independent of
460    /// constraint-status grouping. Not stable across edits.
461    pub instance_index: usize,
462    /// Overall constraint status derived from per-segment freedom.
463    pub status: ConstraintKind,
464    /// Number of segments that are under-constrained (free to move).
465    pub free_count: usize,
466    /// Number of segments that are over-constrained (conflicting constraints).
467    pub conflict_count: usize,
468    /// Total number of segments analyzed.
469    pub total_count: usize,
470}
471
472/// Grouped report of all sketches by constraint status.
473#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
474pub struct SketchConstraintReport {
475    pub fully_constrained: Vec<SketchConstraintStatus>,
476    pub under_constrained: Vec<SketchConstraintStatus>,
477    pub over_constrained: Vec<SketchConstraintStatus>,
478    /// Sketches where analysis encountered an error (e.g., a point lookup
479    /// failed). Callers decide how to treat these.
480    pub errors: Vec<SketchConstraintStatus>,
481}
482
483/// Compute the constraint status for a single sketch object.
484///
485/// Returns `None` if `sketch_obj` is not a sketch.
486///
487/// Note: a sketch with no countable segments (`total_count == 0`) is reported
488/// as [`ConstraintKind::FullyConstrained`]. This is vacuously true — there are
489/// no free or conflicting segments. Callers can check `total_count == 0` to
490/// distinguish this from a genuinely constrained sketch.
491pub(crate) fn sketch_constraint_status_for_sketch(
492    scene_objects: &[Object],
493    sketch_obj: &Object,
494) -> Option<SketchConstraintStatus> {
495    use kcl_api::ObjectKind;
496    use kcl_api::Segment;
497
498    let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
499        return None;
500    };
501
502    // Closure to look up a point's freedom by ObjectId.
503    let lookup = |id: ObjectId| -> Option<kcl_api::Freedom> {
504        let obj = scene_objects.get(id.0)?;
505        if let ObjectKind::Segment {
506            segment: Segment::Point(p),
507        } = &obj.kind
508        {
509            Some(p.freedom())
510        } else {
511            None
512        }
513    };
514
515    let mut free_count: usize = 0;
516    let mut conflict_count: usize = 0;
517    let mut error_count: usize = 0;
518    let mut total_count: usize = 0;
519
520    for &seg_id in &sketch.segments {
521        let Some(seg_obj) = scene_objects.get(seg_id.0) else {
522            continue;
523        };
524        let ObjectKind::Segment { segment } = &seg_obj.kind else {
525            continue;
526        };
527        // Skip owned points — their freedom is already captured by
528        // the parent geometry (Line/Arc/Circle) that looks them up.
529        if let Segment::Point(p) = segment
530            && p.owner.is_some()
531        {
532            continue;
533        }
534        let freedom = segment
535            .freedom(lookup)
536            .map(SegmentFreedom::from)
537            .unwrap_or(SegmentFreedom::Error);
538        total_count += 1;
539        match freedom {
540            SegmentFreedom::Free => free_count += 1,
541            SegmentFreedom::Conflict => conflict_count += 1,
542            SegmentFreedom::Error => error_count += 1,
543            SegmentFreedom::Fixed => {}
544        }
545    }
546
547    let status = if error_count > 0 {
548        ConstraintKind::Error
549    } else if conflict_count > 0 {
550        ConstraintKind::OverConstrained
551    } else if free_count > 0 {
552        ConstraintKind::UnderConstrained
553    } else {
554        ConstraintKind::FullyConstrained
555    };
556
557    Some(SketchConstraintStatus {
558        name: sketch_obj.label.clone(),
559        instance_index: 0,
560        status,
561        free_count,
562        conflict_count,
563        total_count,
564    })
565}
566
567pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
568    let mut fully_constrained = Vec::new();
569    let mut under_constrained = Vec::new();
570    let mut over_constrained = Vec::new();
571    let mut errors = Vec::new();
572    let mut instance_counts = std::collections::HashMap::new();
573    for obj in scene_objects {
574        let Some(mut entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
575            continue;
576        };
577        let count = instance_counts.entry(entry.name.clone()).or_insert(0);
578        entry.instance_index = *count;
579        *count += 1;
580        match entry.status {
581            ConstraintKind::FullyConstrained => fully_constrained.push(entry),
582            ConstraintKind::UnderConstrained => under_constrained.push(entry),
583            ConstraintKind::OverConstrained => over_constrained.push(entry),
584            ConstraintKind::Error => errors.push(entry),
585        }
586    }
587
588    SketchConstraintReport {
589        fully_constrained,
590        under_constrained,
591        over_constrained,
592        errors,
593    }
594}
595
596impl ExecOutcome {
597    pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
598        debug_assert!(
599            id.0 < self.scene_objects.len(),
600            "Requested object ID {} but only have {} objects",
601            id.0,
602            self.scene_objects.len()
603        );
604        self.scene_objects.get(id.0)
605    }
606
607    /// Returns non-fatal errors. Warnings are not included.
608    pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
609        self.issues.iter().filter(|error| error.is_err())
610    }
611
612    /// Analyze all sketches in the execution result and group them by
613    /// constraint status (fully, under, or over constrained).
614    ///
615    /// Each segment in a sketch computes its own freedom by looking up the
616    /// freedom of its constituent points. Owned points (belonging to a
617    /// Line/Arc/Circle) are skipped to avoid double-counting.
618    pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
619        sketch_constraint_report_from_scene_objects(&self.scene_objects)
620    }
621
622    /// Render one sketch from this execution result as a PNG, colored by
623    /// solver freedom.
624    pub fn render_sketch_png(
625        &self,
626        sketch_name: &str,
627    ) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
628        self.render_sketch_png_instance(sketch_name, None)
629    }
630
631    /// Render a named sketch using an optional instance index from its report.
632    /// Without an index, the name must be unique.
633    pub fn render_sketch_png_instance(
634        &self,
635        sketch_name: &str,
636        instance_index: Option<usize>,
637    ) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
638        render_sketch_png_from_scene_objects(&self.scene_objects, sketch_name, instance_index)
639    }
640}
641
642pub(crate) fn render_sketch_png_from_scene_objects(
643    scene_objects: &[kcl_api::Object],
644    sketch_name: &str,
645    instance_index: Option<usize>,
646) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
647    use kcl_api::ObjectKind;
648
649    use crate::tooling::sketch_visualizer::SketchVisualizationError;
650
651    let sketches_matching_name = scene_objects
652        .iter()
653        .filter_map(|object| match &object.kind {
654            ObjectKind::Sketch(sketch) if object.label == sketch_name => Some(sketch),
655            _ => None,
656        })
657        .collect::<Vec<_>>();
658
659    // Select the correct sketch, from all sketches matching the name.
660    let sketch = match (sketches_matching_name.as_slice(), instance_index) {
661        // No sketches matched the name.
662        ([], _) => {
663            return Err(SketchVisualizationError::SketchNotFound {
664                name: sketch_name.to_owned(),
665            });
666        }
667        // At least one sketch matched the name, and the user gave an index.
668        (_nonempty, Some(index)) => {
669            *sketches_matching_name
670                .get(index)
671                .ok_or_else(|| SketchVisualizationError::InstanceNotFound {
672                    name: sketch_name.to_owned(),
673                    index,
674                    count: sketches_matching_name.len(),
675                })?
676        }
677        // Exactly one sketch matched the name, the user didn't need any
678        // index because there is no ambiguity about which sketch.
679        ([sketch], None) => *sketch,
680
681        // More than one sketch matched the name, but there's no index
682        // to disambiguate.
683        (_nonempty, None) => {
684            return Err(SketchVisualizationError::AmbiguousSketchName {
685                name: sketch_name.to_owned(),
686                count: sketches_matching_name.len(),
687            });
688        }
689    };
690
691    // Now that we've selected the right sketch, visualize it.
692    crate::tooling::sketch_visualizer::render_sketch_png(scene_objects, sketch)
693}
694
695/// Configuration for mock execution.
696#[derive(Debug, Clone, PartialEq)]
697pub struct MockConfig {
698    pub use_prev_memory: bool,
699    /// The `ObjectId` of the sketch block to execute for sketch mode. Only the
700    /// specified sketch block will be executed. All other code is ignored.
701    pub sketch_block_id: Option<ObjectId>,
702    /// True to do more costly analysis of whether the sketch block segments are
703    /// under-constrained.
704    pub freedom_analysis: bool,
705    /// The segments that were edited that triggered this execution.
706    pub segment_ids_edited: AhashIndexSet<ObjectId>,
707    /// Segment-body drag anchors that temporarily pull a point on a segment toward the cursor.
708    pub drag_anchors: Vec<SegmentDragAnchor>,
709}
710
711#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
712#[ts(export, export_to = "FrontendApi.ts")]
713#[serde(rename_all = "camelCase")]
714pub struct SegmentDragAnchor {
715    pub segment_id: ObjectId,
716    pub target: kcl_api::Point2d<Number>,
717}
718
719impl Default for MockConfig {
720    fn default() -> Self {
721        Self {
722            // By default, use previous memory. This is usually what you want.
723            use_prev_memory: true,
724            sketch_block_id: None,
725            freedom_analysis: true,
726            segment_ids_edited: AhashIndexSet::default(),
727            drag_anchors: Vec::new(),
728        }
729    }
730}
731
732impl MockConfig {
733    /// Create a new mock config for sketch mode.
734    pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
735        Self {
736            sketch_block_id: Some(sketch_block_id),
737            ..Default::default()
738        }
739    }
740
741    #[must_use]
742    pub(crate) fn no_freedom_analysis(mut self) -> Self {
743        self.freedom_analysis = false;
744        self
745    }
746}
747
748#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
749#[ts(export)]
750#[serde(tag = "type", rename_all = "camelCase")]
751pub struct TagIdentifier {
752    pub value: String,
753    // Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info
754    // was written.
755    #[serde(skip)]
756    pub info: Vec<(usize, TagEngineInfo)>,
757    #[serde(skip)]
758    pub meta: Vec<Metadata>,
759}
760
761impl TagIdentifier {
762    /// Get the tag info for this tag at a specified epoch.
763    pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
764        for (e, info) in self.info.iter().rev() {
765            if *e <= at_epoch {
766                return Some(info);
767            }
768        }
769
770        None
771    }
772
773    /// Get the most recent tag info for this tag.
774    pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
775        self.info.last().map(|i| &i.1)
776    }
777
778    /// Get all tag info entries at the most recent epoch.
779    /// For region-mapped tags, this returns multiple entries (one per region segment).
780    pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
781        let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
782            return vec![];
783        };
784        self.info
785            .iter()
786            .rev()
787            .take_while(|(e, _)| *e == cur_epoch)
788            .map(|(_, info)| info)
789            .collect()
790    }
791
792    /// Add info from a different instance of this tag.
793    pub fn merge_info(&mut self, other: &TagIdentifier) {
794        assert_eq!(&self.value, &other.value);
795        for (oe, ot) in &other.info {
796            if let Some((e, t)) = self.info.last_mut() {
797                // If there is newer info, then skip this iteration.
798                if *e > *oe {
799                    continue;
800                }
801                // If we're in the same epoch, then overwrite.
802                if e == oe {
803                    *t = ot.clone();
804                    continue;
805                }
806            }
807            self.info.push((*oe, ot.clone()));
808        }
809    }
810
811    pub fn geometry(&self) -> Option<Geometry> {
812        self.get_cur_info().map(|info| info.geometry.clone())
813    }
814
815    pub(crate) fn is_body_created_tag(&self) -> bool {
816        self.get_cur_info().is_some_and(|info| {
817            matches!(&info.geometry, Geometry::Solid(_)) && info.path.is_none() && info.surface.is_some()
818        })
819    }
820}
821
822impl Eq for TagIdentifier {}
823
824impl std::fmt::Display for TagIdentifier {
825    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
826        write!(f, "{}", self.value)
827    }
828}
829
830impl std::str::FromStr for TagIdentifier {
831    type Err = KclError;
832
833    fn from_str(s: &str) -> Result<Self, Self::Err> {
834        Ok(Self {
835            value: s.to_string(),
836            info: Vec::new(),
837            meta: Default::default(),
838        })
839    }
840}
841
842impl Ord for TagIdentifier {
843    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
844        self.value.cmp(&other.value)
845    }
846}
847
848impl PartialOrd for TagIdentifier {
849    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
850        Some(self.cmp(other))
851    }
852}
853
854impl std::hash::Hash for TagIdentifier {
855    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
856        self.value.hash(state);
857    }
858}
859
860/// Engine information for a tag.
861#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
862#[ts(export)]
863#[serde(tag = "type", rename_all = "camelCase")]
864pub struct TagEngineInfo {
865    /// The id of the tagged object.
866    pub id: uuid::Uuid,
867    /// The geometry the tag is on.
868    pub geometry: Geometry,
869    /// The path the tag is on.
870    pub path: Option<Path>,
871    /// The surface information for the tag.
872    pub surface: Option<ExtrudeSurface>,
873}
874
875#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
876pub enum BodyType {
877    Root,
878    Block,
879}
880
881/// Metadata.
882#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
883#[ts(export)]
884#[serde(rename_all = "camelCase")]
885pub struct Metadata {
886    /// The source range.
887    pub source_range: SourceRange,
888}
889
890impl From<Metadata> for Vec<SourceRange> {
891    fn from(meta: Metadata) -> Self {
892        vec![meta.source_range]
893    }
894}
895
896impl From<&Metadata> for SourceRange {
897    fn from(meta: &Metadata) -> Self {
898        meta.source_range
899    }
900}
901
902impl From<SourceRange> for Metadata {
903    fn from(source_range: SourceRange) -> Self {
904        Self { source_range }
905    }
906}
907
908impl<T> From<NodeRef<'_, T>> for Metadata {
909    fn from(node: NodeRef<'_, T>) -> Self {
910        Self {
911            source_range: SourceRange::new(node.start, node.end, node.module_id),
912        }
913    }
914}
915
916impl From<&Expr> for Metadata {
917    fn from(expr: &Expr) -> Self {
918        Self {
919            source_range: SourceRange::from(expr),
920        }
921    }
922}
923
924impl Metadata {
925    pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> kcl_api::SourceRef {
926        if meta.len() == 1 {
927            let meta = &meta[0];
928            return kcl_api::SourceRef::Simple {
929                range: meta.source_range,
930                node_path,
931            };
932        }
933        kcl_api::SourceRef::BackTrace {
934            ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
935        }
936    }
937}
938
939/// The type of ExecutorContext being used
940#[derive(PartialEq, Debug, Default, Clone)]
941pub enum ContextType {
942    /// Live engine connection
943    #[default]
944    Live,
945
946    /// Completely mocked connection
947    /// Mock mode is only for the Design Studio when they just want to mock engine calls and not
948    /// actually make them.
949    Mock,
950
951    /// Handled by some other interpreter/conversion system
952    MockCustomForwarded,
953}
954
955/// The executor context.
956/// Cloning will return another handle to the same engine connection/session,
957/// as this uses `Arc` under the hood.
958#[derive(Clone)]
959pub struct ExecutorContext {
960    pub engine: Arc<EngineManager>,
961    /// Shared session cache, also used by associated mock execution.
962    pub cache: Cache,
963    pub engine_batch: EngineBatchContext,
964    pub fs: FileSystemHandle,
965    pub settings: ExecutorSettings,
966    pub context_type: ContextType,
967    pub execution_callbacks: Option<Arc<dyn ExecutionCallbacks>>,
968    /// Which executor evaluates KCL. Crate-internal: set before the first
969    /// run and immutable during execution (run methods take &self). Cloned
970    /// contexts (fresh roots, Args) inherit the same executor.
971    pub(crate) executor_kind: machine::ExecutorKind,
972    /// Call-depth limit for the machine executor's runaway-recursion guard.
973    /// Crate-internal policy, not user configuration.
974    pub(crate) machine_call_depth_limit: usize,
975    /// If true, send BeginExecution and EndExecution before/after executing
976    /// KCL. This might need to be false if the engine server is disabling
977    /// rendering because it's in a headless context.
978    pub configure_engine_render: bool,
979}
980
981impl std::fmt::Debug for ExecutorContext {
982    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
983        f.debug_struct("ExecutorContext")
984            .field("engine", &self.engine)
985            .field("engine_batch", &self.engine_batch)
986            .field("settings", &self.settings)
987            .field("context_type", &self.context_type)
988            .field("execution_callbacks", &self.execution_callbacks)
989            .field("executor_kind", &self.executor_kind)
990            .field("machine_call_depth_limit", &self.machine_call_depth_limit)
991            .finish()
992    }
993}
994
995/// The executor settings.
996#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
997#[ts(export)]
998pub struct ExecutorSettings {
999    /// Highlight edges of 3D objects?
1000    pub highlight_edges: bool,
1001    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
1002    pub enable_ssao: bool,
1003    /// Show grid?
1004    pub show_grid: bool,
1005    /// Should engine store this for replay?
1006    /// If so, under what name?
1007    pub replay: Option<String>,
1008    /// The directory of the current project.  This is used for resolving import
1009    /// paths.  If None is given, the current working directory is used.
1010    pub project_directory: Option<TypedPath>,
1011    /// This is the path to the current file being executed.
1012    /// We use this for preventing cyclic imports.
1013    pub current_file: Option<TypedPath>,
1014    /// Whether or not to automatically scale the grid when user zooms.
1015    pub fixed_size_grid: bool,
1016    /// Skip sending the engine messages that are only needed to build the
1017    /// artifact graph. When this is true, the artifact graph will be
1018    /// incomplete. So you should only use this option if you know you don't
1019    /// need the artifact graph or anything that depends on it. In that case,
1020    /// skipping these commands can make execution slightly faster.
1021    #[serde(default, skip_serializing_if = "is_false")]
1022    pub skip_artifact_graph: bool,
1023    /// If Some(N), sends a heartbeat to keep the WebSocket active, every N seconds.
1024    /// If None, no heartbeats will be sent.
1025    #[serde(default, skip_serializing_if = "Option::is_none")]
1026    pub heartbeats: Option<u64>,
1027    /// If given, sets the default backface colour.
1028    /// If not, defaults to whatever the engine's default is.
1029    #[serde(default, skip_serializing_if = "Option::is_none")]
1030    pub default_backface_color: Option<String>,
1031    /// If given, sets a custom engine pool.
1032    #[serde(default, skip_serializing_if = "Option::is_none")]
1033    pub pool: Option<String>,
1034    /// If given, sets the Engine video width in pixels.
1035    #[serde(default, skip_serializing_if = "Option::is_none")]
1036    pub video_res_width: Option<u32>,
1037    /// If given, sets the Engine video height in pixels.
1038    #[serde(default, skip_serializing_if = "Option::is_none")]
1039    pub video_res_height: Option<u32>,
1040    /// asks the engine for geometry only mode - no video stream
1041    pub geometry_only: bool,
1042}
1043
1044fn is_false(b: &bool) -> bool {
1045    !*b
1046}
1047
1048impl Default for ExecutorSettings {
1049    fn default() -> Self {
1050        Self {
1051            highlight_edges: true,
1052            enable_ssao: false,
1053            show_grid: false,
1054            replay: None,
1055            project_directory: None,
1056            current_file: None,
1057            fixed_size_grid: true,
1058            skip_artifact_graph: false,
1059            heartbeats: None,
1060            default_backface_color: None,
1061            pool: None,
1062            video_res_width: None,
1063            video_res_height: None,
1064            geometry_only: false,
1065        }
1066    }
1067}
1068
1069impl From<crate::settings::types::Configuration> for ExecutorSettings {
1070    fn from(config: crate::settings::types::Configuration) -> Self {
1071        Self::from(config.settings)
1072    }
1073}
1074
1075impl From<crate::settings::types::Settings> for ExecutorSettings {
1076    fn from(settings: crate::settings::types::Settings) -> Self {
1077        let modeling_settings = settings.modeling.unwrap_or_default();
1078        Self {
1079            highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
1080            enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
1081            show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
1082            replay: None,
1083            project_directory: None,
1084            current_file: None,
1085            fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
1086            skip_artifact_graph: false,
1087            heartbeats: None,
1088            default_backface_color: modeling_settings.backface_color.map(|color| color.0),
1089            pool: None,
1090            video_res_width: None,
1091            video_res_height: None,
1092            geometry_only: false,
1093        }
1094    }
1095}
1096
1097impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
1098    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
1099        Self::from(config.settings.modeling)
1100    }
1101}
1102
1103impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
1104    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
1105        Self {
1106            highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
1107            enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
1108            show_grid: modeling.show_scale_grid.unwrap_or_default(),
1109            replay: None,
1110            project_directory: None,
1111            current_file: None,
1112            fixed_size_grid: true,
1113            skip_artifact_graph: false,
1114            heartbeats: None,
1115            default_backface_color: modeling.backface_color.map(|color| color.0),
1116            pool: None,
1117            video_res_width: None,
1118            video_res_height: None,
1119            geometry_only: false,
1120        }
1121    }
1122}
1123
1124impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
1125    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
1126        Self {
1127            highlight_edges: modeling.highlight_edges.into(),
1128            enable_ssao: modeling.enable_ssao.into(),
1129            show_grid: Default::default(),
1130            replay: None,
1131            project_directory: None,
1132            current_file: None,
1133            fixed_size_grid: true,
1134            skip_artifact_graph: false,
1135            heartbeats: None,
1136            default_backface_color: None,
1137            pool: None,
1138            video_res_width: None,
1139            video_res_height: None,
1140            geometry_only: false,
1141        }
1142    }
1143}
1144
1145impl ExecutorSettings {
1146    /// Add the current file path to the executor settings.
1147    pub fn with_current_file(&mut self, current_file: TypedPath) {
1148        // We want the parent directory of the file.
1149        if current_file.extension() == Some("kcl") {
1150            self.current_file = Some(current_file.clone());
1151            // Get the parent directory.
1152            if let Some(parent) = current_file.parent() {
1153                self.project_directory = Some(parent);
1154            } else {
1155                self.project_directory = Some(TypedPath::from(""));
1156            }
1157        } else {
1158            self.project_directory = Some(current_file);
1159        }
1160    }
1161}
1162
1163/// Either reuse a complete outcome or pass owned state into execution.
1164/// Keep the execution state boxed when transferring it between phases.
1165enum PreparedCachedExecution {
1166    Cached(Box<ExecOutcome>),
1167    Execute(Box<CachedExecution>),
1168}
1169
1170struct CachedExecution {
1171    program: crate::Program,
1172    exec_state: Box<ExecState>,
1173    universe_info: Option<(Universe, UniverseMap)>,
1174    preserve_mem: PreserveMem,
1175}
1176
1177impl ExecutorContext {
1178    /// Use an existing cache for another handle to the same session, or for
1179    /// associated mock execution. Independent sessions must use independent caches.
1180    pub fn with_cache(mut self, cache: Cache) -> Self {
1181        self.cache = cache;
1182        self
1183    }
1184
1185    /// Create a new live executor context from an engine and file manager.
1186    pub fn new_with_engine_and_fs(
1187        engine: Arc<EngineManager>,
1188        fs: FileSystemHandle,
1189        settings: ExecutorSettings,
1190    ) -> Self {
1191        ExecutorContext {
1192            engine,
1193            cache: Cache::default(),
1194            engine_batch: EngineBatchContext::default(),
1195            fs,
1196            settings,
1197            context_type: ContextType::Live,
1198            execution_callbacks: Default::default(),
1199            executor_kind: machine::ExecutorKind::resolve(),
1200            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1201            configure_engine_render: true,
1202        }
1203    }
1204
1205    fn clone_with_fresh_execution_batch(&self) -> Self {
1206        Self {
1207            engine: self.engine.clone(),
1208            cache: self.cache.clone(),
1209            engine_batch: EngineBatchContext::new(),
1210            fs: self.fs.clone(),
1211            settings: self.settings.clone(),
1212            context_type: self.context_type.clone(),
1213            execution_callbacks: self.execution_callbacks.clone(),
1214            // Imported modules execute on this cloned context; keep them on
1215            // the executor selected for the run instead of the default.
1216            executor_kind: self.executor_kind,
1217            machine_call_depth_limit: self.machine_call_depth_limit,
1218            configure_engine_render: true,
1219        }
1220    }
1221
1222    /// Create a new live executor context from an engine using the local file manager.
1223    #[cfg(not(target_arch = "wasm32"))]
1224    pub fn new_with_engine(engine: Arc<EngineManager>, settings: ExecutorSettings) -> Self {
1225        Self::new_with_engine_and_fs(engine, crate::fs::new_file_system_handle(FileManager::new()), settings)
1226    }
1227
1228    /// Toggle on engine graphics.
1229    /// Make sure to disable them again when you're done.
1230    pub async fn enable_engine_graphics(&self, exec_state: &mut ExecState) -> Result<()> {
1231        let source_range = Default::default();
1232        self.engine
1233            .send_modeling_cmd(
1234                &self.engine_batch,
1235                exec_state.next_uuid(),
1236                source_range,
1237                &ModelingCmd::ToggleGraphics(kcmc::ToggleGraphics::enabled(true)),
1238            )
1239            .await
1240            .map(|_| ())?;
1241
1242        Ok(())
1243    }
1244
1245    pub async fn enable_engine_graphics_settings(&self, exec_state: &mut ExecState) -> Result<()> {
1246        let source_range = Default::default();
1247        let grid_scale_unit = if self.settings.fixed_size_grid {
1248            GridScaleBehavior::Fixed(Some(exec_state.length_unit()))
1249        } else {
1250            GridScaleBehavior::ScaleWithZoom
1251        };
1252        self.engine
1253            .apply_graphics_settings(
1254                &self.engine_batch,
1255                &self.settings,
1256                source_range,
1257                exec_state.id_generator(),
1258                grid_scale_unit,
1259            )
1260            .await?;
1261        Ok(())
1262    }
1263
1264    pub async fn disable_engine_graphics(&self, exec_state: &mut ExecState) -> Result<()> {
1265        let source_range = Default::default();
1266        self.engine
1267            .send_modeling_cmd(
1268                &self.engine_batch,
1269                exec_state.next_uuid(),
1270                source_range,
1271                &ModelingCmd::ToggleGraphics(kcmc::ToggleGraphics::enabled(false)),
1272            )
1273            .await
1274            .map(|_| ())?;
1275        Ok(())
1276    }
1277
1278    /// Open an engine session for the entrypoint's resolved `Program::language_version()`.
1279    /// The version is fixed for the lifetime of this connection.
1280    #[cfg(not(target_arch = "wasm32"))]
1281    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings, kcl_version: KclVersion) -> Result<Self> {
1282        let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
1283        let (ws, headers) = client
1284            .modeling()
1285            .commands_ws(kittycad::modeling::CommandsWsParams {
1286                api_call_id: None,
1287                fps: None,
1288                order_independent_transparency: None,
1289                post_effect: if settings.enable_ssao {
1290                    Some(kittycad::types::PostEffectType::Ssao)
1291                } else {
1292                    None
1293                },
1294                replay: settings.replay.clone(),
1295                show_grid: if settings.show_grid { Some(true) } else { None },
1296                pool: if settings.geometry_only {
1297                    Some("cpu".to_string())
1298                } else {
1299                    settings.pool.clone()
1300                },
1301                geometry_only: Some(settings.geometry_only),
1302                pr,
1303                unlocked_framerate: None,
1304                webrtc: Some(false),
1305                video_res_width: settings.video_res_width,
1306                video_res_height: settings.video_res_height,
1307                kcl_version: Some(match kcl_version {
1308                    KclVersion::V1 => kittycad::types::KclVersion::One0,
1309                    KclVersion::V2 => kittycad::types::KclVersion::Two0,
1310                    KclVersion::V3Preview => kittycad::types::KclVersion::Three0Preview,
1311                    KclVersion::V3 => kittycad::types::KclVersion::Three0,
1312                    KclVersion::V4Preview => kittycad::types::KclVersion::Four0Preview,
1313                }),
1314            })
1315            .await?;
1316
1317        let request_id = headers
1318            .get("x-request-id")
1319            .and_then(|id| id.to_str().ok())
1320            .map(str::to_owned);
1321        let engine_conn =
1322            EngineManager::new_websocket_transport_with_request_id(ws, settings.heartbeats, request_id).await;
1323        let engine = Arc::new(engine_conn);
1324
1325        Ok(Self::new_with_engine(engine, settings))
1326    }
1327
1328    #[cfg(target_arch = "wasm32")]
1329    pub fn new(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1330        Self::new_with_engine_and_fs(engine, fs, settings)
1331    }
1332
1333    #[cfg(not(target_arch = "wasm32"))]
1334    pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
1335        ExecutorContext {
1336            engine: Arc::new(EngineManager::new_mock()),
1337            cache: Cache::default(),
1338            engine_batch: EngineBatchContext::default(),
1339            fs: crate::fs::new_file_system_handle(FileManager::new()),
1340            settings: settings.unwrap_or_default(),
1341            context_type: ContextType::Mock,
1342            execution_callbacks: Default::default(),
1343            executor_kind: machine::ExecutorKind::resolve(),
1344            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1345            configure_engine_render: true,
1346        }
1347    }
1348
1349    #[cfg(target_arch = "wasm32")]
1350    pub fn new_mock(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1351        ExecutorContext {
1352            engine,
1353            cache: Cache::default(),
1354            engine_batch: EngineBatchContext::default(),
1355            fs,
1356            settings,
1357            context_type: ContextType::Mock,
1358            execution_callbacks: Default::default(),
1359            executor_kind: machine::ExecutorKind::resolve(),
1360            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1361            configure_engine_render: true,
1362        }
1363    }
1364
1365    /// Create a new mock executor context for WASM LSP servers.
1366    /// This is a convenience function that creates a mock engine and FileManager from a FileSystemManager.
1367    #[cfg(target_arch = "wasm32")]
1368    pub fn new_mock_for_lsp(
1369        fs_manager: crate::fs::wasm::FileSystemManager,
1370        settings: ExecutorSettings,
1371    ) -> Result<Self, String> {
1372        let fs = crate::fs::new_file_system_handle(FileManager::new(fs_manager));
1373
1374        Ok(ExecutorContext {
1375            engine: Arc::new(EngineManager::new_mock()),
1376            cache: Cache::default(),
1377            engine_batch: EngineBatchContext::default(),
1378            fs,
1379            settings,
1380            context_type: ContextType::Mock,
1381            execution_callbacks: Default::default(),
1382            executor_kind: machine::ExecutorKind::resolve(),
1383            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1384            configure_engine_render: true,
1385        })
1386    }
1387
1388    #[cfg(not(target_arch = "wasm32"))]
1389    pub fn new_forwarded_mock(engine: Arc<EngineManager>) -> Self {
1390        ExecutorContext {
1391            engine,
1392            cache: Cache::default(),
1393            engine_batch: EngineBatchContext::default(),
1394            fs: crate::fs::new_file_system_handle(FileManager::new()),
1395            settings: Default::default(),
1396            context_type: ContextType::MockCustomForwarded,
1397            execution_callbacks: Default::default(),
1398            executor_kind: machine::ExecutorKind::resolve(),
1399            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1400            configure_engine_render: true,
1401        }
1402    }
1403
1404    /// Create a new default executor context.
1405    /// With a kittycad client.
1406    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
1407    /// variables.
1408    /// But also allows for passing in a token and engine address directly.
1409    #[cfg(not(target_arch = "wasm32"))]
1410    pub async fn new_with_client(
1411        settings: ExecutorSettings,
1412        token: Option<String>,
1413        engine_addr: Option<String>,
1414        kcl_version: KclVersion,
1415    ) -> Result<Self> {
1416        // Create the client.
1417        let client = crate::engine::new_zoo_client(token, engine_addr)?;
1418
1419        let ctx = Self::new(&client, settings, kcl_version).await?;
1420        Ok(ctx)
1421    }
1422
1423    /// Create a new default executor context.
1424    /// With a kittycad client.
1425    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
1426    /// variables.
1427    /// But also allows for passing in a token and engine address directly.
1428    #[cfg(not(target_arch = "wasm32"))]
1429    pub async fn new_with_version(kcl_version: KclVersion) -> Result<Self> {
1430        Self::new_with_client(Default::default(), None, None, kcl_version).await
1431    }
1432
1433    /// Create a new default executor context.
1434    /// With the default kittycad client and the default (unannotated) KCL version.
1435    /// For a versioned entrypoint, use `new_with_client` or `new_with_version`
1436    /// with `Program::language_version()`.
1437    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
1438    /// variables.
1439    #[cfg_attr(not(test), deprecated(note = "use fn new_with_version() instead"))]
1440    #[cfg(not(target_arch = "wasm32"))]
1441    pub async fn new_with_default_client() -> Result<Self> {
1442        Self::new_with_client(Default::default(), None, None, KclVersion::default()).await
1443    }
1444
1445    /// Create a geometry-only executor context with the default client.
1446    #[cfg(not(target_arch = "wasm32"))]
1447    pub async fn new_geometry_only_with_default_client() -> Result<Self> {
1448        Self::new_geometry_only_with_version(KclVersion::default()).await
1449    }
1450
1451    /// Create a geometry-only executor context for the entrypoint's KCL version.
1452    #[cfg(not(target_arch = "wasm32"))]
1453    pub async fn new_geometry_only_with_version(kcl_version: KclVersion) -> Result<Self> {
1454        Self::new_with_client(
1455            ExecutorSettings {
1456                geometry_only: true,
1457                ..Default::default()
1458            },
1459            None,
1460            None,
1461            kcl_version,
1462        )
1463        .await
1464    }
1465
1466    /// For executing unit tests.
1467    #[cfg(not(target_arch = "wasm32"))]
1468    pub async fn new_for_unit_test(engine_addr: Option<String>, kcl_version: KclVersion) -> Result<Self> {
1469        let ctx = ExecutorContext::new_with_client(
1470            ExecutorSettings {
1471                highlight_edges: true,
1472                enable_ssao: false,
1473                show_grid: false,
1474                replay: None,
1475                project_directory: None,
1476                current_file: None,
1477                fixed_size_grid: false,
1478                skip_artifact_graph: false,
1479                heartbeats: None,
1480                default_backface_color: None,
1481                pool: None,
1482                video_res_width: None,
1483                video_res_height: None,
1484                geometry_only: false,
1485            },
1486            None,
1487            engine_addr,
1488            kcl_version,
1489        )
1490        .await?;
1491        Ok(ctx)
1492    }
1493
1494    pub fn is_mock(&self) -> bool {
1495        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1496    }
1497
1498    /// Returns true if we should not send engine commands for any reason.
1499    pub async fn no_engine_commands(&self) -> bool {
1500        self.is_mock()
1501    }
1502
1503    pub async fn send_clear_scene(
1504        &self,
1505        kcl_version: Option<KclVersion>,
1506        exec_state: &mut ExecState,
1507        source_range: crate::execution::SourceRange,
1508    ) -> Result<(), KclError> {
1509        // Ensure artifacts are cleared so that we don't accumulate them across
1510        // runs.
1511        exec_state.mod_local.artifacts.clear();
1512        exec_state.global.root_module_artifacts.clear();
1513        exec_state.global.artifacts.clear();
1514
1515        self.engine
1516            .clear_scene(
1517                &self.engine_batch,
1518                &mut exec_state.mod_local.id_generator,
1519                source_range,
1520                kcl_version,
1521                self.settings.geometry_only,
1522            )
1523            .await?;
1524        // OIT requires a graphical context with SSAO enabled.
1525        if !self.settings.geometry_only && self.settings.enable_ssao {
1526            let cmd_id = exec_state.next_uuid();
1527            exec_state
1528                .batch_modeling_cmd(
1529                    ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1530                    ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1531                )
1532                .await?;
1533        }
1534        Ok(())
1535    }
1536
1537    /// Clear the KCL-lib internal cache and clear the entire engine scene.
1538    /// Only executors sharing this session cache are affected.
1539    pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1540        self.cache.bust_cache().await;
1541
1542        // Execute an empty program to clear and reset the scene.
1543        // We specifically want to be returned the objects after the scene is reset.
1544        // Like the default planes so it is easier to just execute an empty program
1545        // after the cache is busted.
1546        let outcome = self.run_with_caching(crate::Program::empty()).await?;
1547
1548        Ok(outcome)
1549    }
1550
1551    /// Clear the KCL-lib internal cache.
1552    /// Only executors sharing this session cache are affected.
1553    pub async fn bust_cache(&self) {
1554        self.cache.bust_cache().await;
1555    }
1556
1557    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1558        self.eval_prelude(exec_state, SourceRange::synthetic())
1559            .await
1560            .map_err(KclErrorWithOutputs::no_outputs)?;
1561        exec_state
1562            .mut_stack()
1563            .push_new_root_env(true)
1564            .map_err(KclErrorWithOutputs::no_outputs)?;
1565        Ok(())
1566    }
1567
1568    fn restore_mock_memory(
1569        exec_state: &mut ExecState,
1570        mem: cache::SketchModeState,
1571        _mock_config: &MockConfig,
1572    ) -> Result<(), KclErrorWithOutputs> {
1573        *exec_state.mut_stack() = mem.stack;
1574        exec_state.global.module_infos = mem.module_infos;
1575        exec_state.global.path_to_source_id = mem.path_to_source_id;
1576        exec_state.global.id_to_source = mem.id_to_source;
1577        exec_state.global.never_type_ranges = mem.never_type_ranges;
1578        exec_state.global.std_not_yet_added = mem.std_not_yet_added;
1579        exec_state.mod_local.constraint_state = mem.constraint_state;
1580        let len = _mock_config
1581            .sketch_block_id
1582            .map(|sketch_block_id| sketch_block_id.0)
1583            .unwrap_or(0);
1584        if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1585            exec_state
1586                .global
1587                .root_module_artifacts
1588                .restore_scene_objects(scene_objects);
1589        } else {
1590            let message = format!(
1591                "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1592                mem.scene_objects.len(),
1593                len
1594            );
1595            debug_assert!(false, "{message}");
1596            return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1597                KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1598            )));
1599        }
1600
1601        Ok(())
1602    }
1603
1604    pub async fn run_mock(
1605        &self,
1606        program: &crate::Program,
1607        mock_config: &MockConfig,
1608    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1609        let (exec_state, main_ref) = self.run_mock_returning_state(program, mock_config).await?;
1610
1611        // Restore any temporary variables, then save any newly created variables back to
1612        // memory in case another run wants to use them. Note this is just saved to the preserved
1613        // memory, not to the exec_state which is not cached for mock execution.
1614
1615        let mut stack = exec_state.stack().clone();
1616        let module_infos = exec_state.global.module_infos.clone();
1617        let path_to_source_id = exec_state.global.path_to_source_id.clone();
1618        let id_to_source = exec_state.global.id_to_source.clone();
1619        let never_type_ranges = exec_state.global.never_type_ranges.clone();
1620        let constraint_state = exec_state.mod_local.constraint_state.clone();
1621        let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1622        let std_not_yet_added = exec_state.global.std_not_yet_added.clone();
1623        let kcl_version = exec_state.entry_point_kcl_version();
1624        let outcome = exec_state
1625            .into_exec_outcome(main_ref, self)
1626            .await
1627            .map_err(KclErrorWithOutputs::no_outputs)?;
1628
1629        stack.squash_env(main_ref).map_err(KclErrorWithOutputs::no_outputs)?;
1630        let state = cache::SketchModeState {
1631            stack,
1632            module_infos,
1633            path_to_source_id,
1634            id_to_source,
1635            never_type_ranges,
1636            constraint_state,
1637            scene_objects,
1638            std_not_yet_added,
1639            kcl_version,
1640        };
1641        self.cache.write_old_memory(state).await;
1642
1643        Ok(outcome)
1644    }
1645
1646    /// The mock-execution pipeline through interpretation: set up mock state,
1647    /// restore or prepare memory, and execute. Split from [`Self::run_mock`],
1648    /// which converts the state to an [`ExecOutcome`], so that tests can
1649    /// inspect the [`ExecState`] after a mock run.
1650    async fn run_mock_returning_state(
1651        &self,
1652        program: &crate::Program,
1653        mock_config: &MockConfig,
1654    ) -> Result<(ExecState, EnvironmentRef), KclErrorWithOutputs> {
1655        assert!(
1656            self.is_mock(),
1657            "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1658        );
1659
1660        let use_prev_memory = mock_config.use_prev_memory;
1661        let mut exec_state = ExecState::new_mock(self, mock_config);
1662        // Before the prelude runs below, so that `added_in` gating in std sees
1663        // the program's version rather than the default.
1664        exec_state
1665            .set_entry_point_kcl_version(program)
1666            .map_err(KclErrorWithOutputs::no_outputs)?;
1667        if use_prev_memory {
1668            match self.cache.read_old_memory().await {
1669                Some(mem) if mem.reusable_for(exec_state.entry_point_kcl_version()) => {
1670                    Self::restore_mock_memory(&mut exec_state, mem, mock_config)?
1671                }
1672                // No memory, or memory from another effective kclVersion, whose
1673                // bindings, module outcomes, and prelude this program must not
1674                // see.
1675                _ => self.prepare_mem(&mut exec_state).await?,
1676            }
1677        } else {
1678            self.prepare_mem(&mut exec_state).await?
1679        };
1680
1681        // Push a scope so that old variables can be overwritten (since we might be re-executing some
1682        // part of the scene).
1683        exec_state
1684            .mut_stack()
1685            .push_new_env_for_scope()
1686            .map_err(KclErrorWithOutputs::no_outputs)?;
1687
1688        let (main_ref, _) = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1689
1690        Ok((exec_state, main_ref))
1691    }
1692
1693    /// Plan, execute, and save in separate phases so cache-management stack
1694    /// frames are not held underneath deep KCL execution in debug builds.
1695    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1696        assert!(!self.is_mock());
1697        let exec_fut = self.run_with_caching_inner(program);
1698        let result = if self.configure_engine_render {
1699            self.with_engine_execution(Box::pin(exec_fut)).await
1700        } else {
1701            exec_fut.await
1702        };
1703
1704        if result.is_err() {
1705            self.cache.bust_cache().await;
1706        }
1707        result
1708    }
1709
1710    async fn run_with_caching_inner(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1711        let original_program = program.clone();
1712        let CachedExecution {
1713            program,
1714            mut exec_state,
1715            universe_info,
1716            preserve_mem,
1717        } = *match self.prepare_cached_execution(program).await? {
1718            PreparedCachedExecution::Cached(outcome) => {
1719                return Ok(ExecOutcome {
1720                    from_cache: true,
1721                    ..*outcome
1722                });
1723            }
1724            PreparedCachedExecution::Execute(execution) => execution,
1725        };
1726        let result = self
1727            .run_concurrent_inner(&program, &mut exec_state, universe_info, preserve_mem)
1728            .await;
1729        self.finish_cached_execution(original_program, exec_state, result).await
1730    }
1731
1732    // Construct each phase's future here, rather than in run_with_caching,
1733    // so the coordinator does not reserve stack for its large temporaries.
1734    #[inline(never)]
1735    fn prepare_cached_execution(
1736        &self,
1737        program: crate::Program,
1738    ) -> BoxFuture<'_, Result<PreparedCachedExecution, KclErrorWithOutputs>> {
1739        Box::pin(async move {
1740            let grid_scale = if self.settings.fixed_size_grid {
1741                GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1742            } else {
1743                GridScaleBehavior::ScaleWithZoom
1744            };
1745
1746            let (program, exec_state, universe_info, preserve_mem) = match self.cache.read_old_ast().await {
1747                Some(mut cached_state) => {
1748                    let old = CacheInformation {
1749                        ast: &cached_state.main.ast,
1750                        settings: &cached_state.settings,
1751                        kcl_version: cached_state.kcl_version,
1752                    };
1753                    let new = CacheInformation {
1754                        ast: &program.ast,
1755                        settings: &self.settings,
1756                        kcl_version: program.kcl_version,
1757                    };
1758
1759                    // Get the program that actually changed from the old and new information.
1760                    let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1761                        CacheResult::ReExecute {
1762                            clear_scene,
1763                            reapply_settings,
1764                            program: changed_program,
1765                        } => {
1766                            if reapply_settings
1767                                && self
1768                                    .engine
1769                                    .reapply_settings(
1770                                        &self.engine_batch,
1771                                        &self.settings,
1772                                        Default::default(),
1773                                        &mut cached_state.main.exec_state.id_generator,
1774                                        grid_scale,
1775                                    )
1776                                    .await
1777                                    .is_err()
1778                            {
1779                                (true, program, None)
1780                            } else {
1781                                (
1782                                    clear_scene,
1783                                    crate::Program {
1784                                        kcl_version: program.kcl_version,
1785                                        ast: changed_program,
1786                                        original_file_contents: program.original_file_contents,
1787                                    },
1788                                    None,
1789                                )
1790                            }
1791                        }
1792                        CacheResult::CheckImportsOnly {
1793                            reapply_settings,
1794                            ast: changed_program,
1795                        } => {
1796                            let mut reapply_failed = false;
1797                            if reapply_settings {
1798                                if self
1799                                    .engine
1800                                    .reapply_settings(
1801                                        &self.engine_batch,
1802                                        &self.settings,
1803                                        Default::default(),
1804                                        &mut cached_state.main.exec_state.id_generator,
1805                                        grid_scale,
1806                                    )
1807                                    .await
1808                                    .is_ok()
1809                                {
1810                                    self.cache
1811                                        .write_old_ast(GlobalState::with_settings(
1812                                            cached_state.clone(),
1813                                            self.settings.clone(),
1814                                        ))
1815                                        .await;
1816                                } else {
1817                                    reapply_failed = true;
1818                                }
1819                            }
1820
1821                            if reapply_failed {
1822                                (true, program, None)
1823                            } else {
1824                                // We need to check our imports to see if they changed.
1825                                let mut new_exec_state = ExecState::new(self);
1826                                let (new_universe, new_universe_map) =
1827                                    self.get_universe(&program, &mut new_exec_state).await?;
1828
1829                                let clear_scene = new_universe.values().any(|value| {
1830                                    let id = value.1;
1831                                    match (
1832                                        cached_state.exec_state.get_source(id),
1833                                        new_exec_state.global.get_source(id),
1834                                    ) {
1835                                        (Some(s0), Some(s1)) => s0.source != s1.source,
1836                                        _ => false,
1837                                    }
1838                                });
1839
1840                                if !clear_scene {
1841                                    // Return early we don't need to clear the scene.
1842                                    self.cache
1843                                        .write_old_memory(
1844                                            cached_state
1845                                                .mock_memory_state()
1846                                                .map_err(KclErrorWithOutputs::no_outputs)?,
1847                                        )
1848                                        .await;
1849                                    return cached_state
1850                                        .into_exec_outcome(self)
1851                                        .await
1852                                        .map(|outcome| PreparedCachedExecution::Cached(Box::new(outcome)))
1853                                        .map_err(KclErrorWithOutputs::no_outputs);
1854                                }
1855
1856                                (
1857                                    true,
1858                                    crate::Program {
1859                                        kcl_version: program.kcl_version,
1860                                        ast: changed_program,
1861                                        original_file_contents: program.original_file_contents,
1862                                    },
1863                                    Some((new_universe, new_universe_map, new_exec_state)),
1864                                )
1865                            }
1866                        }
1867                        CacheResult::NoAction(true) => {
1868                            if self
1869                                .engine
1870                                .reapply_settings(
1871                                    &self.engine_batch,
1872                                    &self.settings,
1873                                    Default::default(),
1874                                    &mut cached_state.main.exec_state.id_generator,
1875                                    grid_scale,
1876                                )
1877                                .await
1878                                .is_ok()
1879                            {
1880                                // We need to update the old ast state with the new settings!!
1881                                self.cache
1882                                    .write_old_ast(GlobalState::with_settings(
1883                                        cached_state.clone(),
1884                                        self.settings.clone(),
1885                                    ))
1886                                    .await;
1887
1888                                self.cache
1889                                    .write_old_memory(
1890                                        cached_state
1891                                            .mock_memory_state()
1892                                            .map_err(KclErrorWithOutputs::no_outputs)?,
1893                                    )
1894                                    .await;
1895                                return cached_state
1896                                    .into_exec_outcome(self)
1897                                    .await
1898                                    .map(|outcome| PreparedCachedExecution::Cached(Box::new(outcome)))
1899                                    .map_err(KclErrorWithOutputs::no_outputs);
1900                            }
1901                            (true, program, None)
1902                        }
1903                        CacheResult::NoAction(false) => {
1904                            self.cache
1905                                .write_old_memory(
1906                                    cached_state
1907                                        .mock_memory_state()
1908                                        .map_err(KclErrorWithOutputs::no_outputs)?,
1909                                )
1910                                .await;
1911                            return cached_state
1912                                .into_exec_outcome(self)
1913                                .await
1914                                .map(|outcome| PreparedCachedExecution::Cached(Box::new(outcome)))
1915                                .map_err(KclErrorWithOutputs::no_outputs);
1916                        }
1917                    };
1918
1919                    let (exec_state, universe_info, preserve_mem) = match import_check_info {
1920                        Some((new_universe, new_universe_map, mut new_exec_state)) => {
1921                            // Clear the scene if the imports changed.
1922                            self.send_clear_scene(Some(program.kcl_version), &mut new_exec_state, Default::default())
1923                                .await
1924                                .map_err(KclErrorWithOutputs::no_outputs)?;
1925
1926                            (
1927                                new_exec_state,
1928                                Some((new_universe, new_universe_map)),
1929                                PreserveMem::Normal,
1930                            )
1931                        }
1932                        None if clear_scene => {
1933                            // Pop the execution state, since we are starting fresh.
1934                            let mut exec_state = cached_state.reconstitute_exec_state(self);
1935                            exec_state.reset(self);
1936
1937                            self.send_clear_scene(Some(program.kcl_version), &mut exec_state, Default::default())
1938                                .await
1939                                .map_err(KclErrorWithOutputs::no_outputs)?;
1940
1941                            (exec_state, None, PreserveMem::Normal)
1942                        }
1943                        None => {
1944                            let mut exec_state = cached_state.reconstitute_exec_state(self);
1945                            exec_state
1946                                .mut_stack()
1947                                .restore_env(cached_state.main.result_env)
1948                                .map_err(KclErrorWithOutputs::no_outputs)?;
1949
1950                            (exec_state, None, PreserveMem::Always)
1951                        }
1952                    };
1953
1954                    (program, exec_state, universe_info, preserve_mem)
1955                }
1956                None => {
1957                    let mut exec_state = ExecState::new(self);
1958                    self.send_clear_scene(Some(program.kcl_version), &mut exec_state, Default::default())
1959                        .await
1960                        .map_err(KclErrorWithOutputs::no_outputs)?;
1961
1962                    (program, exec_state, None, PreserveMem::Normal)
1963                }
1964            };
1965
1966            Ok(PreparedCachedExecution::Execute(Box::new(CachedExecution {
1967                program,
1968                exec_state: Box::new(exec_state),
1969                universe_info,
1970                preserve_mem,
1971            })))
1972        })
1973    }
1974
1975    #[inline(never)]
1976    fn finish_cached_execution(
1977        &self,
1978        original_program: crate::Program,
1979        exec_state: Box<ExecState>,
1980        result: Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs>,
1981    ) -> BoxFuture<'_, Result<ExecOutcome, KclErrorWithOutputs>> {
1982        Box::pin(async move {
1983            // Throw the error.
1984            let result = result?;
1985
1986            // Save this as the last successful execution to the cache.
1987            // Gotcha: `CacheResult::ReExecute.program` may be diff-based, do not save that AST
1988            // the last-successful AST. Instead, save in the full AST passed in.
1989            self.cache
1990                .write_old_ast(GlobalState::new(
1991                    (*exec_state).clone(),
1992                    self.settings.clone(),
1993                    original_program.kcl_version,
1994                    original_program.ast,
1995                    result.0,
1996                ))
1997                .await;
1998
1999            let outcome = exec_state
2000                .into_exec_outcome(result.0, self)
2001                .await
2002                .map_err(KclErrorWithOutputs::no_outputs)?;
2003            Ok(outcome)
2004        })
2005    }
2006
2007    /// Perform the execution of a program.
2008    ///
2009    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
2010    pub async fn run(
2011        &self,
2012        program: &crate::Program,
2013        exec_state: &mut ExecState,
2014    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
2015        self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
2016            .await
2017    }
2018
2019    /// Perform the execution of a program using a concurrent
2020    /// execution model.
2021    ///
2022    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
2023    pub async fn run_concurrent(
2024        &self,
2025        program: &crate::Program,
2026        exec_state: &mut ExecState,
2027        universe_info: Option<(Universe, UniverseMap)>,
2028        preserve_mem: PreserveMem,
2029    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
2030        let execution_fut = self.run_concurrent_inner(program, exec_state, universe_info, preserve_mem);
2031        if self.configure_engine_render {
2032            self.with_engine_execution(Box::pin(execution_fut)).await
2033        } else {
2034            execution_fut.await
2035        }
2036    }
2037
2038    /// Enclose the entire execution, including scene setup and cached settings updates.
2039    async fn with_engine_execution<T>(
2040        &self,
2041        execution: impl Future<Output = Result<T, KclErrorWithOutputs>>,
2042    ) -> Result<T, KclErrorWithOutputs> {
2043        self.send_execution_boundary(ModelingCmd::from(
2044            mcmd::BeginExecution::builder().enable_render(true).build(),
2045        ))
2046        .await
2047        .map_err(KclErrorWithOutputs::no_outputs)?;
2048
2049        let exec_res = execution.await;
2050        let flush_res = self.engine.ensure_async_commands_completed(&self.engine_batch).await;
2051        if flush_res.is_err() {
2052            // Do not carry unexecuted commands from a failed flush into the next execution.
2053            self.engine.clear_queues(&self.engine_batch).await;
2054        }
2055        // Always end execution, even when evaluation or the final flush failed.
2056        let end_res = self
2057            .send_execution_boundary(ModelingCmd::from(mcmd::EndExecution::default()))
2058            .await;
2059        match (exec_res, flush_res.and(end_res)) {
2060            (Ok(res), Ok(())) => Ok(res),
2061            (Err(e), _) => Err(e),
2062            (Ok(_), Err(e)) => Err(KclErrorWithOutputs::no_outputs(e)),
2063        }
2064    }
2065
2066    async fn send_execution_boundary(&self, cmd: ModelingCmd) -> Result<(), KclError> {
2067        // A separate, empty queue keeps these commands outside all modeling batches.
2068        // Their IDs must not consume or collide with the model's stable artifact IDs.
2069        self.engine
2070            .send_modeling_cmd(
2071                &EngineBatchContext::new(),
2072                uuid::Uuid::new_v4(),
2073                SourceRange::synthetic(),
2074                &cmd,
2075            )
2076            .await
2077            .map(|_| ())
2078    }
2079
2080    fn default_tolerance_command(exec_state: &ExecState) -> Option<ModelingCmd> {
2081        exec_state.entry_point_version_is_v3_or_higher().then(|| {
2082            let tolerance = kcmc::shared::Tolerance::builder()
2083                .point_point_2d_coincident(kcmc::length_unit::LengthUnit(
2084                    crate::std::solver::POINT_POINT_2D_COINCIDENT_EUCLIDEAN_TOLERANCE_MM,
2085                ))
2086                .build();
2087            ModelingCmd::from(mcmd::SetDefaultSystemProperties::builder().tolerance(tolerance).build())
2088        })
2089    }
2090
2091    async fn run_concurrent_inner(
2092        &self,
2093        program: &crate::Program,
2094        exec_state: &mut ExecState,
2095        universe_info: Option<(Universe, UniverseMap)>,
2096        preserve_mem: PreserveMem,
2097    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
2098        // Record the entry point's kclVersion before anything executes;
2099        // imported modules pre-execute on clones of this state below and must
2100        // inherit it.
2101        exec_state
2102            .set_entry_point_kcl_version(program)
2103            .map_err(KclErrorWithOutputs::no_outputs)?;
2104
2105        // Apply the physical tolerance before imported modules send geometry commands.
2106        if let Some(cmd) = Self::default_tolerance_command(exec_state) {
2107            self.send_execution_boundary(cmd)
2108                .await
2109                .map_err(KclErrorWithOutputs::no_outputs)?;
2110        }
2111
2112        // Reuse our cached universe if we have one.
2113
2114        let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
2115            (universe, universe_map)
2116        } else {
2117            self.get_universe(program, exec_state).await?
2118        };
2119
2120        // Push ModuleInstance ops for the root module's direct imports before
2121        // child modules execute. This lets the live feature tree show module
2122        // names immediately rather than waiting for the root module body to run.
2123        // Sort by source position so they appear in source-code order (the
2124        // universe_map is a HashMap with non-deterministic iteration order).
2125        let mut sorted_imports: Vec<_> = universe_map.iter().collect();
2126        sorted_imports.sort_by_key(|(_, import_stmt)| SourceRange::from(*import_stmt));
2127        for (_path, import_stmt) in sorted_imports {
2128            // Look up by the raw import filename (e.g. "car-wheel.kcl") which
2129            // is the key format used by Universe, NOT the resolved absolute
2130            // TypedPath that UniverseMap uses as its key.
2131            let filename = match &import_stmt.path {
2132                ImportPath::Kcl { filename } => filename.to_string(),
2133                ImportPath::Foreign { path } => path.to_string(),
2134                ImportPath::Std { .. } => continue,
2135            };
2136            if let Some((_, module_id, module_path, _)) = universe.get(&filename)
2137                && let ModulePath::Local { value, .. } = module_path
2138            {
2139                let name = import_stmt
2140                    .module_name()
2141                    .unwrap_or_else(|| value.file_name().unwrap_or_default());
2142                let source_range = SourceRange::from(import_stmt);
2143                exec_state.push_op(crate::execution::cad_op::Operation::ModuleInstance {
2144                    name,
2145                    module_id: *module_id,
2146                    glob: matches!(
2147                        import_stmt.selector,
2148                        crate::parsing::ast::types::ImportSelector::Glob(_)
2149                    ),
2150                    node_path: crate::NodePath::placeholder(),
2151                    source_range,
2152                });
2153            }
2154        }
2155
2156        let default_planes = self.engine.get_default_planes().read().await.clone();
2157
2158        // Run the prelude to set up the engine.
2159        self.eval_prelude(exec_state, SourceRange::synthetic())
2160            .await
2161            .map_err(KclErrorWithOutputs::no_outputs)?;
2162
2163        for modules in import_graph::import_graph(&universe, self)
2164            .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
2165            .into_iter()
2166        {
2167            #[cfg(not(target_arch = "wasm32"))]
2168            let mut set = tokio::task::JoinSet::new();
2169
2170            #[allow(clippy::type_complexity)]
2171            let (results_tx, mut results_rx): (
2172                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
2173                tokio::sync::mpsc::Receiver<_>,
2174            ) = tokio::sync::mpsc::channel(1);
2175
2176            for module in modules {
2177                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
2178                    return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
2179                        KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
2180                    )));
2181                };
2182                let module_id = *module_id;
2183                let module_path = module_path.clone();
2184                let source_range = SourceRange::from(import_stmt);
2185                // Clone before mutating.
2186                let module_exec_state = exec_state.clone();
2187
2188                let repr = repr.clone();
2189                let exec_ctxt = self.clone_with_fresh_execution_batch();
2190                let results_tx = results_tx.clone();
2191
2192                let exec_module = async |exec_ctxt: &ExecutorContext,
2193                                         repr: &ModuleRepr,
2194                                         module_id: ModuleId,
2195                                         module_path: &ModulePath,
2196                                         exec_state: &mut ExecState,
2197                                         source_range: SourceRange|
2198                       -> Result<ModuleRepr, KclError> {
2199                    match repr {
2200                        ModuleRepr::Kcl(program, _) => {
2201                            let result = exec_ctxt
2202                                .exec_module_from_ast(
2203                                    program,
2204                                    module_id,
2205                                    module_path,
2206                                    exec_state,
2207                                    source_range,
2208                                    PreserveMem::Normal,
2209                                )
2210                                .await;
2211
2212                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
2213                        }
2214                        ModuleRepr::Foreign(geom, _) => {
2215                            // The concurrent executor starts from a clone of the root module state.
2216                            // Use a fresh artifact state so the import command belongs only to the
2217                            // foreign module that issued it.
2218                            exec_state.mod_local.artifacts = Default::default();
2219                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
2220                                .await
2221                                .map(|geom| Some(KclValue::ImportedGeometry(geom)))
2222                                // Label the failure with the import so the
2223                                // backtrace names the foreign file (and so
2224                                // add_import_backtrace's assumption that the
2225                                // immediate frame is present holds).
2226                                .map_err(|err| err.add_import_location(&module_path.import_name(), source_range));
2227                            let module_artifacts = std::mem::take(&mut exec_state.mod_local.artifacts);
2228
2229                            result.map(|val| ModuleRepr::Foreign(geom.clone(), Some((val, module_artifacts))))
2230                        }
2231                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
2232                            format!("Module {module_path} not found in universe"),
2233                            vec![source_range],
2234                        ))),
2235                    }
2236                };
2237
2238                #[cfg(target_arch = "wasm32")]
2239                {
2240                    wasm_bindgen_futures::spawn_local(async move {
2241                        let mut exec_state = module_exec_state;
2242                        let exec_ctxt = exec_ctxt;
2243
2244                        let result = exec_module(
2245                            &exec_ctxt,
2246                            &repr,
2247                            module_id,
2248                            &module_path,
2249                            &mut exec_state,
2250                            source_range,
2251                        )
2252                        .await;
2253
2254                        results_tx
2255                            .send((module_id, module_path, result))
2256                            .await
2257                            .unwrap_or_default();
2258                    });
2259                }
2260                #[cfg(not(target_arch = "wasm32"))]
2261                {
2262                    set.spawn(async move {
2263                        let mut exec_state = module_exec_state;
2264                        let exec_ctxt = exec_ctxt;
2265
2266                        let result = exec_module(
2267                            &exec_ctxt,
2268                            &repr,
2269                            module_id,
2270                            &module_path,
2271                            &mut exec_state,
2272                            source_range,
2273                        )
2274                        .await;
2275
2276                        results_tx
2277                            .send((module_id, module_path, result))
2278                            .await
2279                            .unwrap_or_default();
2280                    });
2281                }
2282            }
2283
2284            drop(results_tx);
2285
2286            while let Some((module_id, _, result)) = results_rx.recv().await {
2287                match result {
2288                    Ok(new_repr) => {
2289                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
2290
2291                        match &mut repr {
2292                            ModuleRepr::Kcl(_, cache) => {
2293                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
2294                                    unreachable!();
2295                                };
2296                                *cache = session_data;
2297                            }
2298                            ModuleRepr::Foreign(_, cache) => {
2299                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
2300                                    unreachable!();
2301                                };
2302                                *cache = session_data;
2303                            }
2304                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
2305                        }
2306
2307                        exec_state.global.module_infos[&module_id].restore_repr(repr);
2308                    }
2309                    Err(e) => {
2310                        let e = import_graph::add_import_backtrace(e, module_id, &universe);
2311                        return Err(exec_state.error_with_outputs(e, None, default_planes));
2312                    }
2313                }
2314            }
2315        }
2316
2317        // The early-pushed ModuleInstance operations have already served their
2318        // purpose (firing onOperation callbacks for the live feature tree).
2319        // Clear them so they don't duplicate the operations the root module
2320        // body will produce when it actually executes its import statements.
2321        exec_state.mod_local.artifacts.operations.clear();
2322
2323        // Move any remaining setup artifacts (non-operation data from the
2324        // prelude, etc.) into the root state.
2325        exec_state
2326            .global
2327            .root_module_artifacts
2328            .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
2329
2330        self.inner_run(program, exec_state, preserve_mem)
2331            .await
2332            .map_err(|mut error| {
2333                // Engine rejections of async commands (e.g. foreign imports)
2334                // surface after module execution, so they miss the import
2335                // frames the eager loop attaches. Without a top-level range
2336                // the frontend cannot anchor the error in the root file;
2337                // rebuild the ancestry from the outermost range's module.
2338                let source_ranges = error.error.source_ranges();
2339                if !source_ranges.is_empty()
2340                    && !source_ranges.iter().any(|range| range.module_id().is_top_level())
2341                    && let Some(outermost) = source_ranges.last()
2342                {
2343                    error.error =
2344                        import_graph::add_import_backtrace_from(error.error.clone(), outermost.module_id(), &universe);
2345                }
2346                error
2347            })
2348    }
2349
2350    /// Get the universe & universe map of the program.
2351    /// And see if any of the imports changed.
2352    async fn get_universe(
2353        &self,
2354        program: &crate::Program,
2355        exec_state: &mut ExecState,
2356    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
2357        // Import validation needs the entry point's version even when a fresh
2358        // state is created only to check imports in a cached execution.
2359        exec_state
2360            .set_entry_point_kcl_version(program)
2361            .map_err(KclErrorWithOutputs::no_outputs)?;
2362        exec_state.add_root_module_contents(program);
2363
2364        let mut universe = std::collections::HashMap::new();
2365
2366        let default_planes = self.engine.get_default_planes().read().await.clone();
2367
2368        let root_imports = import_graph::import_universe(
2369            self,
2370            &ModulePath::Main,
2371            &ModuleRepr::Kcl(program.ast.clone(), None),
2372            &mut universe,
2373            exec_state,
2374        )
2375        .await
2376        .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
2377
2378        Ok((universe, root_imports))
2379    }
2380
2381    /// Perform the execution of a program.  Accept all possible parameters and
2382    /// output everything.
2383    async fn inner_run(
2384        &self,
2385        program: &crate::Program,
2386        exec_state: &mut ExecState,
2387        preserve_mem: PreserveMem,
2388    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
2389        let _stats = crate::log::LogPerfStats::new("Interpretation");
2390
2391        // Record the entry point's kclVersion. Mock execution reaches here
2392        // without going through run_concurrent; on the engine path this
2393        // re-assigns the same value, which is harmless.
2394        exec_state
2395            .set_entry_point_kcl_version(program)
2396            .map_err(KclErrorWithOutputs::no_outputs)?;
2397
2398        // Re-apply the settings, in case the cache was busted.
2399        let grid_scale = if self.settings.fixed_size_grid {
2400            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
2401        } else {
2402            GridScaleBehavior::ScaleWithZoom
2403        };
2404        self.engine
2405            .reapply_settings(
2406                &self.engine_batch,
2407                &self.settings,
2408                Default::default(),
2409                exec_state.id_generator(),
2410                grid_scale,
2411            )
2412            .await
2413            .map_err(KclErrorWithOutputs::no_outputs)?;
2414
2415        let default_planes = self.engine.get_default_planes().read().await.clone();
2416        let result = self
2417            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
2418            .await;
2419
2420        crate::log::log(format!(
2421            "Post interpretation KCL memory stats: {:#?}",
2422            exec_state.stack().memory.stats()
2423        ));
2424        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
2425
2426        /// Write the memory of an execution to the cache for reuse in mock
2427        /// execution.
2428        async fn write_old_memory(
2429            ctx: &ExecutorContext,
2430            exec_state: &ExecState,
2431            env_ref: EnvironmentRef,
2432        ) -> Result<(), KclError> {
2433            if ctx.is_mock() {
2434                return Ok(());
2435            }
2436            let mut stack = exec_state.stack().deep_clone()?;
2437            stack.restore_env(env_ref)?;
2438            let state = cache::SketchModeState {
2439                stack,
2440                module_infos: exec_state.global.module_infos.clone(),
2441                path_to_source_id: exec_state.global.path_to_source_id.clone(),
2442                id_to_source: exec_state.global.id_to_source.clone(),
2443                never_type_ranges: exec_state.global.never_type_ranges.clone(),
2444                constraint_state: exec_state.mod_local.constraint_state.clone(),
2445                scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
2446                std_not_yet_added: exec_state.global.std_not_yet_added.clone(),
2447                kcl_version: exec_state.entry_point_kcl_version(),
2448            };
2449            ctx.cache.write_old_memory(state).await;
2450            Ok(())
2451        }
2452
2453        let env_ref = match result {
2454            Ok(env_ref) => env_ref,
2455            Err((err, env_ref)) => {
2456                // Preserve memory on execution failures so follow-up mock
2457                // execution can still reuse stable IDs before the error.
2458                if let Some(env_ref) = env_ref {
2459                    write_old_memory(self, exec_state, env_ref)
2460                        .await
2461                        .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2462                }
2463                return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
2464            }
2465        };
2466
2467        write_old_memory(self, exec_state, env_ref)
2468            .await
2469            .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2470
2471        let session_data = self.engine.get_session_data().await;
2472
2473        Ok((env_ref, session_data))
2474    }
2475
2476    /// Execute an AST's program and build auxiliary outputs like the artifact
2477    /// graph.
2478    async fn execute_and_build_graph(
2479        &self,
2480        program: NodeRef<'_, crate::parsing::ast::types::Program>,
2481        exec_state: &mut ExecState,
2482        preserve_mem: PreserveMem,
2483    ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
2484        // Don't early return!  We need to build other outputs regardless of
2485        // whether execution failed.
2486
2487        // Because of execution caching, we may start with operations from a
2488        // previous run.
2489        let start_op = exec_state.global.root_module_artifacts.operations.len();
2490
2491        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
2492            .await
2493            .map_err(|e| (e, None))?;
2494
2495        let exec_result = self
2496            .exec_module_body(
2497                program,
2498                exec_state,
2499                preserve_mem,
2500                ModuleId::default(),
2501                &ModulePath::Main,
2502            )
2503            .await
2504            .map(
2505                |ModuleExecutionOutcome {
2506                     environment: env_ref,
2507                     artifacts: module_artifacts,
2508                     ..
2509                 }| {
2510                    // We need to extend because it may already have operations from
2511                    // imports.
2512                    exec_state.global.root_module_artifacts.extend(module_artifacts);
2513                    env_ref
2514                },
2515            )
2516            .map_err(|(err, env_ref, module_artifacts)| {
2517                if let Some(module_artifacts) = module_artifacts {
2518                    // We need to extend because it may already have operations
2519                    // from imports.
2520                    exec_state.global.root_module_artifacts.extend(module_artifacts);
2521                }
2522                (err, env_ref)
2523            });
2524
2525        // Fill in NodePath for operations.
2526        let programs = &exec_state.build_program_lookup(program.clone());
2527        let cached_body_items = exec_state.global.artifacts.cached_body_items();
2528        for op in exec_state
2529            .global
2530            .root_module_artifacts
2531            .operations
2532            .iter_mut()
2533            .skip(start_op)
2534        {
2535            op.fill_node_paths(programs, cached_body_items);
2536        }
2537        for module in exec_state.global.module_infos.values_mut() {
2538            if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
2539                for op in &mut outcome.artifacts.operations {
2540                    op.fill_node_paths(programs, cached_body_items);
2541                }
2542            }
2543        }
2544
2545        // Ensure all the async commands completed.
2546        self.engine
2547            .ensure_async_commands_completed(&self.engine_batch)
2548            .await
2549            .map_err(|e| {
2550                match &exec_result {
2551                    Ok(env_ref) => (e, Some(*env_ref)),
2552                    // Prefer the execution error.
2553                    Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
2554                }
2555            })?;
2556
2557        // If we errored out and early-returned, there might be commands which haven't been executed
2558        // and should be dropped.
2559        self.engine.clear_queues(&self.engine_batch).await;
2560
2561        match exec_state.build_artifact_graph(&self.engine, program).await {
2562            Ok(_) => exec_result,
2563            Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
2564        }
2565    }
2566
2567    /// 'Import' std::prelude as the outermost scope.
2568    ///
2569    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
2570    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
2571        if exec_state.stack().memory.requires_std() {
2572            let initial_ops = exec_state.mod_local.artifacts.operations.len();
2573
2574            let path = vec!["std".to_owned(), "prelude".to_owned()];
2575            let resolved_path = ModulePath::from_std_import_path(&path)?;
2576            let id = self
2577                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
2578                .await?;
2579            let prelude = self.exec_module_for_items(id, exec_state, source_range).await?;
2580
2581            exec_state.mut_stack().memory.set_std(prelude.environment)?;
2582            // Root environments reach the prelude without an import, so its records live here.
2583            exec_state.global.std_not_yet_added = prelude.not_yet_added;
2584
2585            // Operations generated by the prelude are not useful, so clear them
2586            // out.
2587            //
2588            // TODO: Should we also clear them out of each module so that they
2589            // don't appear in test output?
2590            exec_state.mod_local.artifacts.operations.truncate(initial_ops);
2591        }
2592
2593        Ok(())
2594    }
2595
2596    /// Get a snapshot of the current scene.
2597    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
2598        // Zoom to fit.
2599        self.engine
2600            .send_modeling_cmd(
2601                &self.engine_batch,
2602                uuid::Uuid::new_v4(),
2603                crate::execution::SourceRange::default(),
2604                &ModelingCmd::from(
2605                    mcmd::ZoomToFit::builder()
2606                        .object_ids(Default::default())
2607                        .animated(false)
2608                        .padding(0.1)
2609                        .build(),
2610                ),
2611            )
2612            .await
2613            .map_err(KclErrorWithOutputs::no_outputs)?;
2614
2615        // Send a snapshot request to the engine.
2616        let resp = self
2617            .engine
2618            .send_modeling_cmd(
2619                &self.engine_batch,
2620                uuid::Uuid::new_v4(),
2621                crate::execution::SourceRange::default(),
2622                &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
2623            )
2624            .await
2625            .map_err(KclErrorWithOutputs::no_outputs)?;
2626
2627        let OkWebSocketResponseData::Modeling {
2628            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
2629        } = resp
2630        else {
2631            return Err(ExecError::BadPng(format!(
2632                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
2633            )));
2634        };
2635        Ok(contents)
2636    }
2637
2638    /// Export the current scene as a CAD file.
2639    pub async fn export(
2640        &self,
2641        format: kittycad_modeling_cmds::format::OutputFormat3d,
2642    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2643        let resp = self
2644            .engine
2645            .send_modeling_cmd(
2646                &self.engine_batch,
2647                uuid::Uuid::new_v4(),
2648                crate::SourceRange::default(),
2649                &kittycad_modeling_cmds::ModelingCmd::Export(
2650                    kittycad_modeling_cmds::Export::builder()
2651                        .entity_ids(vec![])
2652                        .format(format)
2653                        .build(),
2654                ),
2655            )
2656            .await?;
2657
2658        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
2659            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
2660                format!("Expected Export response, got {resp:?}",),
2661                vec![SourceRange::default()],
2662            )));
2663        };
2664
2665        Ok(files)
2666    }
2667
2668    /// Export the current scene as a STEP file.
2669    pub async fn export_step(
2670        &self,
2671        deterministic_time: bool,
2672    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2673        let files = self
2674            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
2675                kittycad_modeling_cmds::format::step::export::Options::builder()
2676                    .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
2677                    .maybe_created(if deterministic_time {
2678                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
2679                            KclError::new_internal(crate::errors::KclErrorDetails::new(
2680                                format!("Failed to parse date: {e}"),
2681                                vec![SourceRange::default()],
2682                            ))
2683                        })?)
2684                    } else {
2685                        None
2686                    })
2687                    .build(),
2688            ))
2689            .await?;
2690
2691        Ok(files)
2692    }
2693
2694    pub async fn close(&self) {
2695        self.engine.close().await;
2696    }
2697}
2698
2699pub use kcl_api::ArtifactId;
2700
2701pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
2702    ArtifactId::new(*id.as_ref())
2703}
2704
2705#[cfg(test)]
2706pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2707    parse_execute_with_project_dir(code, None).await
2708}
2709
2710#[cfg(test)]
2711pub(crate) async fn parse_execute_with_project_dir(
2712    code: &str,
2713    project_directory: Option<TypedPath>,
2714) -> Result<ExecTestResults, KclError> {
2715    // Differential testing: unit tests run under both executors.
2716    parse_execute_with_executor_kind(code, project_directory, machine::ExecutorKind::resolve()).await
2717}
2718
2719/// A mock-engine executor context for tests that need to inspect the context
2720/// (e.g. the engine's batch queue) even when execution fails.
2721#[cfg(test)]
2722pub(crate) fn new_mock_executor_context(
2723    project_directory: Option<TypedPath>,
2724    executor_kind: machine::ExecutorKind,
2725) -> ExecutorContext {
2726    ExecutorContext {
2727        engine: Arc::new(EngineManager::new_mock()),
2728        cache: Cache::default(),
2729        engine_batch: EngineBatchContext::default(),
2730        fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2731        settings: ExecutorSettings {
2732            project_directory,
2733            ..Default::default()
2734        },
2735        context_type: ContextType::Mock,
2736        execution_callbacks: Default::default(),
2737        executor_kind,
2738        machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
2739        configure_engine_render: true,
2740    }
2741}
2742
2743#[cfg(test)]
2744pub(crate) async fn parse_execute_with_executor_kind(
2745    code: &str,
2746    project_directory: Option<TypedPath>,
2747    executor_kind: machine::ExecutorKind,
2748) -> Result<ExecTestResults, KclError> {
2749    parse_execute_inner(code, project_directory, executor_kind, false).await
2750}
2751
2752/// Like [`parse_execute_with_project_dir`], but missing names are explained
2753/// for user declarations too, not only std's. Only for tests of the `added_in`
2754/// hint itself, which std gives no declaration to exercise yet.
2755#[cfg(test)]
2756pub(crate) async fn parse_execute_hinting_all(
2757    code: &str,
2758    project_directory: Option<TypedPath>,
2759) -> Result<ExecTestResults, KclError> {
2760    parse_execute_inner(code, project_directory, machine::ExecutorKind::resolve(), true).await
2761}
2762
2763#[cfg(test)]
2764async fn parse_execute_inner(
2765    code: &str,
2766    project_directory: Option<TypedPath>,
2767    executor_kind: machine::ExecutorKind,
2768    hint_all_not_yet_added: bool,
2769) -> Result<ExecTestResults, KclError> {
2770    let program = crate::Program::parse_no_errs(code)?;
2771
2772    let exec_ctxt = new_mock_executor_context(project_directory, executor_kind);
2773    let mut exec_state = ExecState::new(&exec_ctxt);
2774    exec_state.global.hint_all_not_yet_added = hint_all_not_yet_added;
2775    let result = exec_ctxt.run(&program, &mut exec_state).await?;
2776
2777    Ok(ExecTestResults {
2778        program,
2779        mem_env: result.0,
2780        exec_ctxt,
2781        exec_state,
2782    })
2783}
2784
2785#[cfg(test)]
2786#[derive(Debug)]
2787pub(crate) struct ExecTestResults {
2788    program: crate::Program,
2789    mem_env: EnvironmentRef,
2790    exec_ctxt: ExecutorContext,
2791    exec_state: ExecState,
2792}
2793
2794#[cfg(test)]
2795impl ExecTestResults {
2796    pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2797        &self.exec_state.global.root_module_artifacts.commands
2798    }
2799
2800    /// The diagnostics the run reported. Non-fatal issues, such as use of an
2801    /// experimental feature without the opt-in, are recorded here rather than
2802    /// returned as an error, so this is the only place a test can see them.
2803    pub(crate) fn issues(&self) -> &[CompilationIssue] {
2804        self.exec_state.issues()
2805    }
2806
2807    /// The value bound to `name` after the run. Panics when the variable is
2808    /// absent, because a test that names a variable the program does not
2809    /// declare is broken rather than failing.
2810    #[track_caller]
2811    pub(crate) fn variable(&self, name: &str) -> KclValue {
2812        self.exec_state
2813            .stack()
2814            .memory
2815            .get_from_unchecked(name, self.mem_env)
2816            .unwrap()
2817    }
2818}
2819
2820/// There are several places where we want to traverse a KCL program or find a symbol in it,
2821/// but because KCL modules can import each other, we need to traverse multiple programs.
2822/// This stores multiple programs, keyed by their module ID for quick access.
2823pub struct ProgramLookup {
2824    programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2825}
2826
2827impl ProgramLookup {
2828    // TODO: Could this store a reference to KCL programs instead of owning them?
2829    // i.e. take &state::ModuleInfoMap instead?
2830    pub fn new(
2831        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2832        module_infos: state::ModuleInfoMap,
2833    ) -> Self {
2834        let mut programs = IndexMap::with_capacity(module_infos.len());
2835        for (id, info) in module_infos {
2836            if let ModuleRepr::Kcl(program, _) = info.repr {
2837                programs.insert(id, program);
2838            }
2839        }
2840        programs.insert(ModuleId::default(), current);
2841        Self { programs }
2842    }
2843
2844    pub fn program_for_module(
2845        &self,
2846        module_id: ModuleId,
2847    ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2848        self.programs.get(&module_id)
2849    }
2850}
2851
2852#[cfg(test)]
2853mod tests {
2854    use kcl_api::NumericType;
2855    use pretty_assertions::assert_eq;
2856
2857    use super::*;
2858    use crate::ModuleId;
2859    use crate::errors::KclErrorDetails;
2860    use crate::errors::Severity;
2861    use crate::execution::kcl_value::TypeDef;
2862    use crate::execution::memory::Stack;
2863    use crate::execution::types::RuntimeType;
2864
2865    macro_rules! kcl_input {
2866        ($file:literal) => {
2867            include_str!(concat!("../../e2e/executor/inputs/", $file, ".kcl"))
2868        };
2869    }
2870
2871    #[cfg(not(target_arch = "wasm32"))]
2872    #[tokio::test]
2873    async fn native_websocket_sends_entrypoint_kcl_version() {
2874        use tokio::io::AsyncReadExt;
2875        use tokio::io::AsyncWriteExt;
2876
2877        for (source, expected) in [
2878            ("", "1.0"),
2879            ("@settings(defaultLengthUnit = mm)", "1.0"),
2880            ("@settings(kclVersion = 2.0)", "2.0"),
2881            ("@settings(kclVersion = \"3.0-preview\")", "3.0-preview"),
2882            ("@settings(kclVersion = 3.0)", "3.0"),
2883            ("@settings(kclVersion = \"3.0\")", "3.0"),
2884        ] {
2885            let program = crate::Program::parse_no_errs(source).unwrap();
2886            let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2887            let address = listener.local_addr().unwrap();
2888            let request = tokio::spawn(async move {
2889                let (mut socket, _) = listener.accept().await.unwrap();
2890                let mut request = Vec::new();
2891                let mut buffer = [0; 1024];
2892                while !request.windows(4).any(|bytes| bytes == b"\r\n\r\n") {
2893                    let count = socket.read(&mut buffer).await.unwrap();
2894                    assert_ne!(count, 0, "connection closed before sending HTTP headers");
2895                    request.extend_from_slice(&buffer[..count]);
2896                }
2897                // Inspect the handshake without starting an engine session.
2898                socket
2899                    .write_all(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
2900                    .await
2901                    .unwrap();
2902                String::from_utf8(request).unwrap()
2903            });
2904            let mut client = kittycad::Client::new("test-token");
2905            client.set_base_url(format!("http://{address}"));
2906            let result = tokio::time::timeout(
2907                std::time::Duration::from_secs(5),
2908                ExecutorContext::new(&client, Default::default(), program.language_version().unwrap()),
2909            )
2910            .await
2911            .unwrap();
2912            assert!(result.is_err(), "the test server deliberately rejects the upgrade");
2913            let request = tokio::time::timeout(std::time::Duration::from_secs(5), request)
2914                .await
2915                .unwrap()
2916                .unwrap();
2917            let target = request.lines().next().unwrap().split_whitespace().nth(1).unwrap();
2918            let url = url::Url::parse(&format!("http://localhost{target}")).unwrap();
2919            let versions: Vec<_> = url
2920                .query_pairs()
2921                .filter(|(key, _)| key == "kcl_version")
2922                .map(|(_, value)| value.into_owned())
2923                .collect();
2924            assert_eq!(versions, vec![expected]);
2925        }
2926    }
2927
2928    #[cfg(not(target_arch = "wasm32"))]
2929    #[tokio::test]
2930    async fn scene_reset_sends_entrypoint_kcl_version() {
2931        use std::collections::HashMap;
2932
2933        use kcmc::websocket::WebSocketRequest;
2934        use kcmc::websocket::WebSocketResponse;
2935        use tokio::sync::RwLock;
2936        use uuid::Uuid;
2937
2938        use crate::engine::engine_manager::EngineTransport;
2939        use crate::engine::engine_manager::TransportCloseError;
2940
2941        struct RecordingTransport {
2942            inner: Arc<Box<dyn EngineTransport>>,
2943            requests: Arc<RwLock<Vec<WebSocketRequest>>>,
2944        }
2945
2946        #[async_trait::async_trait]
2947        impl EngineTransport for RecordingTransport {
2948            async fn inner_fire_modeling_cmd(
2949                &self,
2950                cmd_id: Uuid,
2951                source_range: SourceRange,
2952                cmd: WebSocketRequest,
2953                id_to_source_range: HashMap<Uuid, SourceRange>,
2954            ) -> Result<(), KclError> {
2955                self.requests.write().await.push(cmd.clone());
2956                self.inner
2957                    .inner_fire_modeling_cmd(cmd_id, source_range, cmd, id_to_source_range)
2958                    .await
2959            }
2960
2961            async fn inner_send_modeling_cmd(
2962                &self,
2963                cmd_id: Uuid,
2964                source_range: SourceRange,
2965                cmd: WebSocketRequest,
2966                id_to_source_range: HashMap<Uuid, SourceRange>,
2967            ) -> Result<WebSocketResponse, KclError> {
2968                self.requests.write().await.push(cmd.clone());
2969                self.inner
2970                    .inner_send_modeling_cmd(cmd_id, source_range, cmd, id_to_source_range)
2971                    .await
2972            }
2973
2974            async fn close(&self) -> Result<(), TransportCloseError> {
2975                self.inner.close().await
2976            }
2977        }
2978
2979        for (source, expected) in [
2980            ("", "1.0"),
2981            ("@settings(kclVersion = 2.0)", "2.0"),
2982            ("@settings(kclVersion = \"3.0-preview\")", "3.0-preview"),
2983            ("@settings(kclVersion = 3.0)", "3.0"),
2984        ] {
2985            let requests = Arc::new(RwLock::new(Vec::new()));
2986            let mut engine = EngineManager::new_mock();
2987            engine.transport = Arc::new(Box::new(RecordingTransport {
2988                inner: engine.transport.clone(),
2989                requests: requests.clone(),
2990            }));
2991            let ctx = ExecutorContext::new_with_engine(
2992                Arc::new(engine),
2993                ExecutorSettings {
2994                    geometry_only: true,
2995                    ..Default::default()
2996                },
2997            );
2998            let program = crate::Program::parse_no_errs(source).unwrap();
2999            let mut exec_state = ExecState::new(&ctx);
3000
3001            ctx.send_clear_scene(
3002                Some(program.language_version().unwrap()),
3003                &mut exec_state,
3004                Default::default(),
3005            )
3006            .await
3007            .unwrap();
3008
3009            let version_commands: Vec<_> = {
3010                let requests = requests.read().await;
3011                requests
3012                    .iter()
3013                    .flat_map(|request| match request {
3014                        WebSocketRequest::ModelingCmdReq(request) => vec![&request.cmd],
3015                        WebSocketRequest::ModelingCmdBatchReq(batch) => {
3016                            batch.requests.iter().map(|request| &request.cmd).collect()
3017                        }
3018                        _ => vec![],
3019                    })
3020                    .filter_map(|cmd| match cmd {
3021                        kcmc::ModelingCmd::SetKclVersion(_) => Some(serde_json::to_value(cmd).unwrap()),
3022                        _ => None,
3023                    })
3024                    .collect()
3025            };
3026            assert_eq!(
3027                version_commands,
3028                vec![serde_json::json!({"type": "set_kcl_version", "kcl_version": expected})],
3029                "{source}"
3030            );
3031            ctx.close().await;
3032        }
3033    }
3034
3035    #[test]
3036    fn clone_with_fresh_execution_batch_keeps_executor_selection() {
3037        // Imported modules execute on a context created by
3038        // clone_with_fresh_execution_batch. They must stay on the executor
3039        // selected for the run instead of silently reverting to the default.
3040        let mut ctx = new_mock_executor_context(None, machine::ExecutorKind::Machine);
3041        ctx.machine_call_depth_limit = 123;
3042        let cloned = ctx.clone_with_fresh_execution_batch();
3043        assert_eq!(cloned.executor_kind, machine::ExecutorKind::Machine);
3044        assert_eq!(cloned.machine_call_depth_limit, 123);
3045    }
3046
3047    #[tokio::test(flavor = "multi_thread")]
3048    async fn concurrent_foreign_import_preserves_artifact_command() {
3049        let tmpdir = tempfile::TempDir::with_prefix("zma_foreign_import_artifact").unwrap();
3050        tokio::fs::write(tmpdir.path().join("cube.obj"), "o cube\n")
3051            .await
3052            .unwrap();
3053
3054        let program = crate::Program::parse_no_errs("import \"cube.obj\" as cube\n\nmodel = cube\n").unwrap();
3055        let ctx = new_mock_executor_context(
3056            Some(crate::TypedPath(tmpdir.path().into())),
3057            machine::ExecutorKind::resolve(),
3058        );
3059        let mut exec_state = ExecState::new(&ctx);
3060        let (main_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3061        let outcome = exec_state
3062            .into_exec_outcome(main_ref, &ctx)
3063            .await
3064            .expect("foreign import execution should produce an outcome");
3065        ctx.close().await;
3066
3067        let KclValueView::ImportedGeometry(imported) = &outcome.variables["model"] else {
3068            panic!("model should be imported geometry");
3069        };
3070        let artifact_id = ArtifactId::new(imported.id);
3071        let Some(Artifact::ImportedGeometry(artifact)) = outcome.artifact_graph.get(&artifact_id) else {
3072            panic!("foreign import should produce an imported geometry artifact");
3073        };
3074        assert_eq!(artifact.id, artifact_id);
3075        assert!(!artifact.code_ref.node_path.is_empty());
3076    }
3077
3078    #[tokio::test(flavor = "multi_thread")]
3079    async fn nested_import_preserves_inner_error_and_backtrace() {
3080        // The imported modules live in an in-memory file system under a
3081        // synthetic project directory, so parallel tests share no on-disk
3082        // state and there is nothing to clean up even if the process is
3083        // killed.
3084        let project_dir = crate::TypedPath::new("/zma-kcl-import-error");
3085        let main_path = project_dir.join("main.kcl");
3086        let assembly_path = project_dir.join("assembly.kcl");
3087        let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
3088        // Key each module by the same join that import resolution performs, so
3089        // the lookup matches on every platform.
3090        let files = [
3091            (
3092                project_dir.join("broken.kcl").to_string(),
3093                b"export brokenValue = missingName + 1\n".to_vec(),
3094            ),
3095            (
3096                assembly_path.to_string(),
3097                b"import brokenValue from \"broken.kcl\"\n\nexport assemblyValue = brokenValue\n".to_vec(),
3098            ),
3099        ]
3100        .into_iter()
3101        .collect();
3102        let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
3103        let settings = ExecutorSettings {
3104            project_directory: Some(project_dir),
3105            current_file: Some(main_path.clone()),
3106            ..Default::default()
3107        };
3108        let program = crate::Program::parse_no_errs(main_code).unwrap();
3109
3110        let assert_error = |error: &KclErrorWithOutputs| {
3111            let KclError::UndefinedValue { details, name } = &error.error else {
3112                panic!("expected UndefinedValue, got {:#?}", error.error);
3113            };
3114            assert_eq!(name.as_deref(), Some("missingName"));
3115            assert_eq!(details.message, "`missingName` is not defined");
3116            assert_eq!(
3117                error
3118                    .error
3119                    .backtrace()
3120                    .iter()
3121                    .map(|frame| frame.fn_name.as_deref())
3122                    .collect::<Vec<_>>(),
3123                [Some("import broken.kcl"), Some("import assembly.kcl"), None]
3124            );
3125            assert_eq!(
3126                error
3127                    .error
3128                    .backtrace()
3129                    .iter()
3130                    .map(|frame| frame.kind)
3131                    .collect::<Vec<_>>(),
3132                [
3133                    kcl_error::BacktraceItemKind::Import,
3134                    kcl_error::BacktraceItemKind::Import,
3135                    kcl_error::BacktraceItemKind::Call
3136                ]
3137            );
3138
3139            let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
3140            assert!(report.filename.ends_with("broken.kcl"));
3141            assert_eq!(
3142                report
3143                    .related
3144                    .iter()
3145                    .map(|related| related.filename.as_str())
3146                    .collect::<Vec<_>>(),
3147                [assembly_path.to_string(), main_path.to_string()]
3148            );
3149
3150            let rendered = format!("{:?}", miette::Report::new(report));
3151            assert!(rendered.contains("broken.kcl"));
3152            assert!(rendered.contains("assembly.kcl"));
3153            assert!(rendered.contains("main.kcl"));
3154            assert!(rendered.contains("export brokenValue = missingName + 1"));
3155            assert!(!rendered.contains("Failed to read contents"));
3156        };
3157
3158        let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
3159        mock_ctx.fs = fs.clone();
3160        let mock_error = mock_ctx
3161            .run_mock(
3162                &program,
3163                &MockConfig {
3164                    use_prev_memory: false,
3165                    ..Default::default()
3166                },
3167            )
3168            .await
3169            .unwrap_err();
3170        mock_ctx.close().await;
3171        assert_error(&mock_error);
3172
3173        let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
3174        concurrent_ctx.fs = fs;
3175        let mut exec_state = ExecState::new(&concurrent_ctx);
3176        let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
3177        concurrent_ctx.close().await;
3178        assert_error(&concurrent_error);
3179    }
3180
3181    #[tokio::test(flavor = "multi_thread")]
3182    async fn function_error_across_import_keeps_backtrace_innermost_first() {
3183        // A function defined in an imported module fails when the importing
3184        // module calls it: function frames and the import frame must stay in
3185        // one innermost-first chain.
3186        let project_dir = crate::TypedPath::new("/zma-kcl-import-fn-error");
3187        let main_path = project_dir.join("main.kcl");
3188        let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
3189        let files = [
3190            (
3191                project_dir.join("helper.kcl").to_string(),
3192                b"export fn inner() { return missingName }\nexport fn outer() { return inner() }\n".to_vec(),
3193            ),
3194            (
3195                project_dir.join("assembly.kcl").to_string(),
3196                b"import outer from \"helper.kcl\"\n\nexport assemblyValue = outer()\n".to_vec(),
3197            ),
3198        ]
3199        .into_iter()
3200        .collect();
3201        let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
3202        let settings = ExecutorSettings {
3203            project_directory: Some(project_dir.clone()),
3204            current_file: Some(main_path),
3205            ..Default::default()
3206        };
3207        let program = crate::Program::parse_no_errs(main_code).unwrap();
3208
3209        let assert_error = |error: &KclErrorWithOutputs| {
3210            assert!(
3211                matches!(&error.error, KclError::UndefinedValue { .. }),
3212                "expected UndefinedValue, got {:#?}",
3213                error.error
3214            );
3215            assert_eq!(
3216                error
3217                    .error
3218                    .backtrace()
3219                    .iter()
3220                    .map(|frame| frame.fn_name.as_deref())
3221                    .collect::<Vec<_>>(),
3222                [Some("inner"), Some("outer"), Some("import assembly.kcl"), None]
3223            );
3224            assert_eq!(
3225                error
3226                    .error
3227                    .backtrace()
3228                    .iter()
3229                    .map(|frame| frame.kind)
3230                    .collect::<Vec<_>>(),
3231                [
3232                    kcl_error::BacktraceItemKind::Call,
3233                    kcl_error::BacktraceItemKind::Call,
3234                    kcl_error::BacktraceItemKind::Import,
3235                    kcl_error::BacktraceItemKind::Call
3236                ]
3237            );
3238
3239            let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
3240            assert!(report.filename.ends_with("helper.kcl"));
3241            assert_eq!(
3242                report
3243                    .related
3244                    .iter()
3245                    .map(|related| related.filename.as_str())
3246                    .collect::<Vec<_>>(),
3247                [
3248                    project_dir.join("assembly.kcl").to_string(),
3249                    project_dir.join("main.kcl").to_string()
3250                ]
3251            );
3252            let rendered = format!("{:?}", miette::Report::new(report));
3253            assert!(rendered.contains("return missingName"));
3254            assert!(!rendered.contains("Failed to read contents"));
3255        };
3256
3257        let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
3258        mock_ctx.fs = fs.clone();
3259        let mock_error = mock_ctx
3260            .run_mock(
3261                &program,
3262                &MockConfig {
3263                    use_prev_memory: false,
3264                    ..Default::default()
3265                },
3266            )
3267            .await
3268            .unwrap_err();
3269        mock_ctx.close().await;
3270        assert_error(&mock_error);
3271
3272        let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
3273        concurrent_ctx.fs = fs;
3274        let mut exec_state = ExecState::new(&concurrent_ctx);
3275        let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
3276        concurrent_ctx.close().await;
3277        assert_error(&concurrent_error);
3278    }
3279
3280    /// Convenience function to get a JSON value from memory and unwrap.
3281    #[track_caller]
3282    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
3283        memory.memory.get_from_unchecked(name, env).unwrap()
3284    }
3285
3286    async fn execute_variables_with_backend(
3287        code: &str,
3288        backend: memory::MemoryBackendKind,
3289    ) -> IndexMap<String, KclValueView> {
3290        execute_outcome_with_backend(code, backend).await.variables
3291    }
3292
3293    async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
3294        let program = crate::Program::parse_no_errs(code).unwrap();
3295        let ctx = ExecutorContext::new_mock(None).await;
3296        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
3297        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3298        let outcome = exec_state
3299            .into_exec_outcome(env_ref, &ctx)
3300            .await
3301            .expect("test execution outcome should collect variables");
3302        ctx.close().await;
3303        outcome
3304    }
3305
3306    async fn execute_error_variables_with_backend(
3307        code: &str,
3308        backend: memory::MemoryBackendKind,
3309    ) -> IndexMap<String, KclValueView> {
3310        let program = crate::Program::parse_no_errs(code).unwrap();
3311        let ctx = ExecutorContext::new_mock(None).await;
3312        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
3313        let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
3314        ctx.close().await;
3315        error.variables
3316    }
3317
3318    async fn execute_project_variables_with_backend(
3319        main_code: &str,
3320        files: &[(&str, &str)],
3321        backend: memory::MemoryBackendKind,
3322    ) -> IndexMap<String, KclValueView> {
3323        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
3324        for (name, contents) in files {
3325            tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
3326        }
3327
3328        let program = crate::Program::parse_no_errs(main_code).unwrap();
3329        let ctx = ExecutorContext {
3330            engine: Arc::new(EngineManager::new_mock()),
3331            cache: Cache::default(),
3332            engine_batch: EngineBatchContext::default(),
3333            fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
3334            settings: ExecutorSettings {
3335                project_directory: Some(crate::TypedPath(tmpdir.path().into())),
3336                ..Default::default()
3337            },
3338            context_type: ContextType::Mock,
3339            execution_callbacks: Default::default(),
3340            executor_kind: machine::ExecutorKind::resolve(),
3341            machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
3342            configure_engine_render: true,
3343        };
3344        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
3345        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3346        let outcome = exec_state
3347            .into_exec_outcome(env_ref, &ctx)
3348            .await
3349            .expect("test execution outcome should collect variables");
3350        ctx.close().await;
3351        outcome.variables
3352    }
3353
3354    async fn run_with_caching_variables_with_backend(
3355        code: &str,
3356        backend: memory::MemoryBackendKind,
3357    ) -> IndexMap<String, KclValueView> {
3358        let _backend = memory::MemoryBackendKind::override_for_test(backend);
3359
3360        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
3361        let program = crate::Program::parse_no_errs(code).unwrap();
3362        ctx.run_with_caching(program.clone()).await.unwrap();
3363        let cached = ctx.run_with_caching(program).await.unwrap();
3364
3365        ctx.close().await;
3366        cached.variables
3367    }
3368
3369    async fn run_mock_variables_with_backend(
3370        code: &str,
3371        backend: memory::MemoryBackendKind,
3372    ) -> IndexMap<String, KclValueView> {
3373        let _backend = memory::MemoryBackendKind::override_for_test(backend);
3374
3375        let ctx = ExecutorContext::new_mock(None).await;
3376        let first = crate::Program::parse_no_errs("x = 2").unwrap();
3377        ctx.run_mock(
3378            &first,
3379            &MockConfig {
3380                use_prev_memory: false,
3381                ..Default::default()
3382            },
3383        )
3384        .await
3385        .unwrap();
3386
3387        let program = crate::Program::parse_no_errs(code).unwrap();
3388        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3389
3390        ctx.close().await;
3391        outcome.variables
3392    }
3393
3394    fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
3395        let mut keys = variables.keys().cloned().collect::<Vec<_>>();
3396        keys.sort();
3397        keys
3398    }
3399
3400    async fn collect_backend_results<T, Fut>(
3401        mut run: impl FnMut(memory::MemoryBackendKind) -> Fut,
3402    ) -> Vec<(memory::MemoryBackendKind, T)>
3403    where
3404        Fut: std::future::Future<Output = T>,
3405    {
3406        let all = memory::MemoryBackendKind::all();
3407        let mut results = Vec::with_capacity(all.len());
3408        for &kind in all {
3409            results.push((kind, run(kind).await));
3410        }
3411        results
3412    }
3413
3414    fn assert_backend_results_match<T>(results: &[(memory::MemoryBackendKind, T)])
3415    where
3416        T: std::fmt::Debug + PartialEq,
3417    {
3418        let (first, rest) = results.split_first().expect("expected at least one memory backend");
3419        let (first_kind, first_result) = first;
3420        for (kind, result) in rest {
3421            assert_eq!(
3422                result, first_result,
3423                "memory kind {kind:?} doesn't match {first_kind:?}"
3424            );
3425        }
3426    }
3427
3428    fn assert_backend_variable_results_match_expected_keys(
3429        results: &[(memory::MemoryBackendKind, IndexMap<String, KclValueView>)],
3430        expected_keys: &[&str],
3431    ) {
3432        let (first_kind, first_variables) = results.first().expect("expected at least one memory backend");
3433        let expected_keys = expected_keys.iter().map(|key| (*key).to_owned()).collect::<Vec<_>>();
3434        assert_eq!(
3435            sorted_variable_keys(first_variables),
3436            expected_keys,
3437            "memory kind {first_kind:?} doesn't match expected variables"
3438        );
3439        assert_backend_results_match(results);
3440    }
3441
3442    fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
3443        let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
3444        let KclValueView::Number { value, .. } = value else {
3445            panic!("expected `{key}` to be a number, got {value:?}");
3446        };
3447        assert_eq!(*value, expected, "{key}: {value:?}");
3448    }
3449
3450    #[tokio::test(flavor = "multi_thread")]
3451    async fn exec_outcome_variables_match_between_memory_backends() {
3452        let code = "x = 2\ny = x + 1\narr = [x, y]";
3453
3454        let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
3455
3456        assert_backend_variable_results_match_expected_keys(&results, &["arr", "x", "y"]);
3457    }
3458
3459    #[tokio::test(flavor = "multi_thread")]
3460    async fn error_output_variables_match_between_memory_backends() {
3461        let code = "x = 2\ny = missing + 1";
3462
3463        let results = collect_backend_results(|kind| execute_error_variables_with_backend(code, kind)).await;
3464
3465        assert_backend_variable_results_match_expected_keys(&results, &["x"]);
3466    }
3467
3468    #[tokio::test(flavor = "multi_thread")]
3469    async fn cached_execution_variables_match_between_memory_backends() {
3470        let code = "x = 2\ny = x + 1";
3471
3472        let results = collect_backend_results(|kind| run_with_caching_variables_with_backend(code, kind)).await;
3473
3474        assert_backend_variable_results_match_expected_keys(&results, &["x", "y"]);
3475    }
3476
3477    #[tokio::test(flavor = "multi_thread")]
3478    async fn mock_execution_variables_match_between_memory_backends() {
3479        let code = "y = x + 1";
3480
3481        let results = collect_backend_results(|kind| run_mock_variables_with_backend(code, kind)).await;
3482
3483        assert_backend_variable_results_match_expected_keys(&results, &["y"]);
3484    }
3485
3486    #[tokio::test(flavor = "multi_thread")]
3487    async fn module_imports_and_exported_closures_match_between_memory_backends() {
3488        let module_code = r#"
3489export base = 40
3490
3491export fn addBase(n) {
3492  return n + base
3493}
3494"#;
3495        let main_code = r#"
3496import base, addBase from 'math.kcl'
3497import 'math.kcl'
3498
3499named = addBase(n = 2)
3500qualified = math::addBase(n = 1)
3501direct = math::base
3502"#;
3503
3504        let files = [("math.kcl", module_code)];
3505        let results =
3506            collect_backend_results(|kind| execute_project_variables_with_backend(main_code, &files, kind)).await;
3507
3508        let (_, first_variables) = results.first().expect("expected at least one memory backend");
3509        assert_number_variable(first_variables, "named", 42.0);
3510        assert_number_variable(first_variables, "qualified", 41.0);
3511        assert_number_variable(first_variables, "direct", 40.0);
3512        assert_backend_results_match(&results);
3513    }
3514
3515    #[tokio::test(flavor = "multi_thread")]
3516    async fn sketch_block_variables_match_between_memory_backends() {
3517        let code = r#"
3518sketch001 = sketch(on = XY) {
3519  line1 = line(start = [0, 0], end = [1, 0])
3520  line2 = line(start = [1, 0], end = [0, 1])
3521}
3522lineCount = 2
3523"#;
3524
3525        let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
3526
3527        let (_, first_variables) = results.first().expect("expected at least one memory backend");
3528        assert!(first_variables.contains_key("sketch001"), "actual: {first_variables:?}");
3529        assert_number_variable(first_variables, "lineCount", 2.0);
3530        assert_backend_results_match(&results);
3531    }
3532
3533    #[tokio::test(flavor = "multi_thread")]
3534    async fn tag_call_stack_lookup_matches_between_memory_backends() {
3535        let code = r#"
3536sketch001 = startSketchOn(XY)
3537  |> startProfile(at = [0, 0])
3538  |> xLine(length = 10, tag = $seg01)
3539
3540segLength = segLen(seg01)
3541"#;
3542
3543        let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
3544
3545        let (_, first_variables) = results.first().expect("expected at least one memory backend");
3546        assert_number_variable(first_variables, "segLength", 10.0);
3547        assert_backend_results_match(&results);
3548    }
3549
3550    #[tokio::test(flavor = "multi_thread")]
3551    async fn test_execute_warn() {
3552        let text = "@blah";
3553        let result = parse_execute(text).await.unwrap();
3554        let errs = result.exec_state.issues();
3555        assert_eq!(errs.len(), 1);
3556        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
3557        assert!(
3558            errs[0].message.contains("Unknown annotation"),
3559            "unexpected warning message: {}",
3560            errs[0].message
3561        );
3562    }
3563
3564    #[tokio::test(flavor = "multi_thread")]
3565    async fn test_execute_fn_definitions() {
3566        let ast = r#"fn def(@x) {
3567  return x
3568}
3569fn ghi(@x) {
3570  return x
3571}
3572fn jkl(@x) {
3573  return x
3574}
3575fn hmm(@x) {
3576  return x
3577}
3578
3579yo = 5 + 6
3580
3581abc = 3
3582identifierGuy = 5
3583part001 = startSketchOn(XY)
3584|> startProfile(at = [-1.2, 4.83])
3585|> line(end = [2.8, 0])
3586|> angledLine(angle = 100 + 100, length = 3.01)
3587|> angledLine(angle = abc, length = 3.02)
3588|> angledLine(angle = def(yo), length = 3.03)
3589|> angledLine(angle = ghi(2), length = 3.04)
3590|> angledLine(angle = jkl(yo) + 2, length = 3.05)
3591|> close()
3592yo2 = hmm([identifierGuy + 5])"#;
3593
3594        parse_execute(ast).await.unwrap();
3595    }
3596
3597    #[tokio::test(flavor = "multi_thread")]
3598    async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
3599        let code = r#"
3600firstProfile = sketch(on = XY) {
3601  edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
3602  edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
3603  edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
3604  edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
3605  coincident([edge1.end, edge2.start])
3606  coincident([edge2.end, edge3.start])
3607  coincident([edge3.end, edge4.start])
3608  coincident([edge4.end, edge1.start])
3609}
3610
3611secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
3612  edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
3613  edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
3614  edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
3615  edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
3616  coincident([edge5.end, edge6.start])
3617  coincident([edge6.end, edge7.start])
3618  coincident([edge7.end, edge8.start])
3619  coincident([edge8.end, edge5.start])
3620}
3621
3622firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
3623secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
3624"#;
3625
3626        let result = parse_execute(code).await.unwrap();
3627        assert!(result.exec_state.issues().is_empty());
3628    }
3629
3630    #[tokio::test(flavor = "multi_thread")]
3631    async fn sketch_block_artifact_preserves_standard_plane_name() {
3632        let code = r#"
3633sketch001 = sketch(on = -YZ) {
3634  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
3635}
3636"#;
3637
3638        let result = parse_execute(code).await.unwrap();
3639        let sketch_blocks = result
3640            .exec_state
3641            .global
3642            .artifacts
3643            .graph
3644            .values()
3645            .filter_map(|artifact| match artifact {
3646                Artifact::SketchBlock(block) => Some(block),
3647                _ => None,
3648            })
3649            .collect::<Vec<_>>();
3650
3651        assert_eq!(sketch_blocks.len(), 1);
3652        assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
3653    }
3654
3655    #[tokio::test(flavor = "multi_thread")]
3656    async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
3657        let code = r#"
3658sketch001 = sketch(on = YZ) {
3659  line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
3660  line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
3661  line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
3662  line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
3663  coincident([line1.end, line2.start])
3664  coincident([line2.end, line3.start])
3665  coincident([line3.end, line4.start])
3666  coincident([line4.end, line1.start])
3667}
3668
3669sketch002 = sketch(on = -XZ) {
3670  line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
3671  line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
3672  line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
3673  line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
3674  coincident([line5.end, line6.start])
3675  coincident([line6.end, line7.start])
3676  coincident([line7.end, line8.start])
3677  coincident([line8.end, line5.start])
3678}
3679
3680region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
3681extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
3682region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
3683extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
3684
3685myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
3686"#;
3687
3688        let result = parse_execute(code).await.unwrap();
3689        assert!(result.exec_state.issues().is_empty());
3690    }
3691
3692    #[tokio::test(flavor = "multi_thread")]
3693    async fn issue_10741_point_circle_coincident_executes() {
3694        let code = r#"
3695sketch001 = sketch(on = YZ) {
3696  circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
3697  line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
3698  coincident([line1.start, circle1])
3699}
3700"#;
3701
3702        let result = parse_execute(code).await.unwrap();
3703        assert!(
3704            result
3705                .exec_state
3706                .issues()
3707                .iter()
3708                .all(|issue| issue.severity != Severity::Error),
3709            "unexpected execution issues: {:#?}",
3710            result.exec_state.issues()
3711        );
3712    }
3713
3714    #[tokio::test(flavor = "multi_thread")]
3715    async fn test_execute_with_pipe_substitutions_unary() {
3716        let ast = r#"myVar = 3
3717part001 = startSketchOn(XY)
3718  |> startProfile(at = [0, 0])
3719  |> line(end = [3, 4], tag = $seg01)
3720  |> line(end = [
3721  min([segLen(seg01), myVar]),
3722  -legLen(hypotenuse = segLen(seg01), leg = myVar)
3723])
3724"#;
3725
3726        parse_execute(ast).await.unwrap();
3727    }
3728
3729    #[tokio::test(flavor = "multi_thread")]
3730    async fn test_execute_with_pipe_substitutions() {
3731        let ast = r#"myVar = 3
3732part001 = startSketchOn(XY)
3733  |> startProfile(at = [0, 0])
3734  |> line(end = [3, 4], tag = $seg01)
3735  |> line(end = [
3736  min([segLen(seg01), myVar]),
3737  legLen(hypotenuse = segLen(seg01), leg = myVar)
3738])
3739"#;
3740
3741        parse_execute(ast).await.unwrap();
3742    }
3743
3744    #[tokio::test(flavor = "multi_thread")]
3745    async fn test_execute_with_inline_comment() {
3746        let ast = r#"baseThick = 1
3747armAngle = 60
3748
3749baseThickHalf = baseThick / 2
3750halfArmAngle = armAngle / 2
3751
3752arrExpShouldNotBeIncluded = [1, 2, 3]
3753objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
3754
3755part001 = startSketchOn(XY)
3756  |> startProfile(at = [0, 0])
3757  |> yLine(endAbsolute = 1)
3758  |> xLine(length = 3.84) // selection-range-7ish-before-this
3759
3760variableBelowShouldNotBeIncluded = 3
3761"#;
3762
3763        parse_execute(ast).await.unwrap();
3764    }
3765
3766    #[tokio::test(flavor = "multi_thread")]
3767    async fn test_execute_with_function_literal_in_pipe() {
3768        let ast = r#"w = 20
3769l = 8
3770h = 10
3771
3772fn thing() {
3773  return -8
3774}
3775
3776firstExtrude = startSketchOn(XY)
3777  |> startProfile(at = [0,0])
3778  |> line(end = [0, l])
3779  |> line(end = [w, 0])
3780  |> line(end = [0, thing()])
3781  |> close()
3782  |> extrude(length = h)"#;
3783
3784        parse_execute(ast).await.unwrap();
3785    }
3786
3787    #[tokio::test(flavor = "multi_thread")]
3788    async fn test_execute_with_function_unary_in_pipe() {
3789        let ast = r#"w = 20
3790l = 8
3791h = 10
3792
3793fn thing(@x) {
3794  return -x
3795}
3796
3797firstExtrude = startSketchOn(XY)
3798  |> startProfile(at = [0,0])
3799  |> line(end = [0, l])
3800  |> line(end = [w, 0])
3801  |> line(end = [0, thing(8)])
3802  |> close()
3803  |> extrude(length = h)"#;
3804
3805        parse_execute(ast).await.unwrap();
3806    }
3807
3808    #[tokio::test(flavor = "multi_thread")]
3809    async fn test_execute_with_function_array_in_pipe() {
3810        let ast = r#"w = 20
3811l = 8
3812h = 10
3813
3814fn thing(@x) {
3815  return [0, -x]
3816}
3817
3818firstExtrude = startSketchOn(XY)
3819  |> startProfile(at = [0,0])
3820  |> line(end = [0, l])
3821  |> line(end = [w, 0])
3822  |> line(end = thing(8))
3823  |> close()
3824  |> extrude(length = h)"#;
3825
3826        parse_execute(ast).await.unwrap();
3827    }
3828
3829    #[tokio::test(flavor = "multi_thread")]
3830    async fn test_execute_with_function_call_in_pipe() {
3831        let ast = r#"w = 20
3832l = 8
3833h = 10
3834
3835fn other_thing(@y) {
3836  return -y
3837}
3838
3839fn thing(@x) {
3840  return other_thing(x)
3841}
3842
3843firstExtrude = startSketchOn(XY)
3844  |> startProfile(at = [0,0])
3845  |> line(end = [0, l])
3846  |> line(end = [w, 0])
3847  |> line(end = [0, thing(8)])
3848  |> close()
3849  |> extrude(length = h)"#;
3850
3851        parse_execute(ast).await.unwrap();
3852    }
3853
3854    #[tokio::test(flavor = "multi_thread")]
3855    async fn test_execute_with_function_sketch() {
3856        let ast = r#"fn box(h, l, w) {
3857 myBox = startSketchOn(XY)
3858    |> startProfile(at = [0,0])
3859    |> line(end = [0, l])
3860    |> line(end = [w, 0])
3861    |> line(end = [0, -l])
3862    |> close()
3863    |> extrude(length = h)
3864
3865  return myBox
3866}
3867
3868fnBox = box(h = 3, l = 6, w = 10)"#;
3869
3870        parse_execute(ast).await.unwrap();
3871    }
3872
3873    #[tokio::test(flavor = "multi_thread")]
3874    async fn test_get_member_of_object_with_function_period() {
3875        let ast = r#"fn box(@obj) {
3876 myBox = startSketchOn(XY)
3877    |> startProfile(at = obj.start)
3878    |> line(end = [0, obj.l])
3879    |> line(end = [obj.w, 0])
3880    |> line(end = [0, -obj.l])
3881    |> close()
3882    |> extrude(length = obj.h)
3883
3884  return myBox
3885}
3886
3887thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
3888"#;
3889        parse_execute(ast).await.unwrap();
3890    }
3891
3892    #[tokio::test(flavor = "multi_thread")]
3893    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
3894    async fn test_object_member_starting_pipeline() {
3895        let ast = r#"
3896fn test2() {
3897  return {
3898    thing: startSketchOn(XY)
3899      |> startProfile(at = [0, 0])
3900      |> line(end = [0, 1])
3901      |> line(end = [1, 0])
3902      |> line(end = [0, -1])
3903      |> close()
3904  }
3905}
3906
3907x2 = test2()
3908
3909x2.thing
3910  |> extrude(length = 10)
3911"#;
3912        parse_execute(ast).await.unwrap();
3913    }
3914
3915    #[tokio::test(flavor = "multi_thread")]
3916    #[ignore] // ignore til we get loops
3917    async fn test_execute_with_function_sketch_loop_objects() {
3918        let ast = r#"fn box(obj) {
3919let myBox = startSketchOn(XY)
3920    |> startProfile(at = obj.start)
3921    |> line(end = [0, obj.l])
3922    |> line(end = [obj.w, 0])
3923    |> line(end = [0, -obj.l])
3924    |> close()
3925    |> extrude(length = obj.h)
3926
3927  return myBox
3928}
3929
3930for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
3931  thisBox = box(var)
3932}"#;
3933
3934        parse_execute(ast).await.unwrap();
3935    }
3936
3937    #[tokio::test(flavor = "multi_thread")]
3938    #[ignore] // ignore til we get loops
3939    async fn test_execute_with_function_sketch_loop_array() {
3940        let ast = r#"fn box(h, l, w, start) {
3941 myBox = startSketchOn(XY)
3942    |> startProfile(at = [0,0])
3943    |> line(end = [0, l])
3944    |> line(end = [w, 0])
3945    |> line(end = [0, -l])
3946    |> close()
3947    |> extrude(length = h)
3948
3949  return myBox
3950}
3951
3952for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
3953  const thisBox = box(var[0], var[1], var[2], var[3])
3954}"#;
3955
3956        parse_execute(ast).await.unwrap();
3957    }
3958
3959    #[tokio::test(flavor = "multi_thread")]
3960    async fn test_get_member_of_array_with_function() {
3961        let ast = r#"fn box(@arr) {
3962 myBox =startSketchOn(XY)
3963    |> startProfile(at = arr[0])
3964    |> line(end = [0, arr[1]])
3965    |> line(end = [arr[2], 0])
3966    |> line(end = [0, -arr[1]])
3967    |> close()
3968    |> extrude(length = arr[3])
3969
3970  return myBox
3971}
3972
3973thisBox = box([[0,0], 6, 10, 3])
3974
3975"#;
3976        parse_execute(ast).await.unwrap();
3977    }
3978
3979    #[tokio::test(flavor = "multi_thread")]
3980    async fn test_function_cannot_access_future_definitions() {
3981        let ast = r#"
3982fn returnX() {
3983  // x shouldn't be defined yet.
3984  return x
3985}
3986
3987x = 5
3988
3989answer = returnX()"#;
3990
3991        let result = parse_execute(ast).await;
3992        let err = result.unwrap_err();
3993        assert_eq!(err.message(), "`x` is not defined");
3994    }
3995
3996    #[tokio::test(flavor = "multi_thread")]
3997    async fn test_override_prelude() {
3998        let text = "PI = 3.0";
3999        let result = parse_execute(text).await.unwrap();
4000        let issues = result.exec_state.issues();
4001        assert!(issues.is_empty(), "issues={issues:#?}");
4002    }
4003
4004    #[tokio::test(flavor = "multi_thread")]
4005    async fn type_aliases() {
4006        let text = r#"@settings(kclVersion = "3.0-preview")
4007type MyTy = [number; 2]
4008fn foo(@x: MyTy) {
4009    return x[0]
4010}
4011
4012foo([0, 1])
4013
4014type Other = MyTy | Helix
4015"#;
4016        let result = parse_execute(text).await.unwrap();
4017        let issues = result.exec_state.issues();
4018        assert!(issues.is_empty(), "issues={issues:#?}");
4019    }
4020
4021    #[tokio::test(flavor = "multi_thread")]
4022    async fn test_cannot_shebang_in_fn() {
4023        let ast = r#"
4024fn foo() {
4025  #!hello
4026  return true
4027}
4028
4029foo
4030"#;
4031
4032        let result = parse_execute(ast).await;
4033        let err = result.unwrap_err();
4034        assert_eq!(
4035            err,
4036            KclError::new_syntax(KclErrorDetails::new(
4037                "Unexpected token: #".to_owned(),
4038                vec![SourceRange::new(14, 15, ModuleId::default())],
4039            )),
4040        );
4041    }
4042
4043    #[tokio::test(flavor = "multi_thread")]
4044    async fn test_pattern_transform_function_cannot_access_future_definitions() {
4045        let ast = r#"
4046fn transform(@replicaId) {
4047  // x shouldn't be defined yet.
4048  scale = x
4049  return {
4050    translate = [0, 0, replicaId * 10],
4051    scale = [scale, 1, 0],
4052  }
4053}
4054
4055fn layer() {
4056  return startSketchOn(XY)
4057    |> circle( center= [0, 0], radius= 1, tag = $tag1)
4058    |> extrude(length = 10)
4059}
4060
4061x = 5
4062
4063// The 10 layers are replicas of each other, with a transform applied to each.
4064shape = layer() |> patternTransform(instances = 10, transform = transform)
4065"#;
4066
4067        let result = parse_execute(ast).await;
4068        let err = result.unwrap_err();
4069        assert_eq!(err.message(), "`x` is not defined",);
4070    }
4071
4072    // ADAM: Move some of these into simulation tests.
4073
4074    #[tokio::test(flavor = "multi_thread")]
4075    async fn test_math_execute_with_functions() {
4076        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
4077        let result = parse_execute(ast).await.unwrap();
4078        assert_eq!(
4079            5.0,
4080            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4081                .as_f64()
4082                .unwrap()
4083        );
4084    }
4085
4086    #[tokio::test(flavor = "multi_thread")]
4087    async fn test_math_execute() {
4088        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
4089        let result = parse_execute(ast).await.unwrap();
4090        assert_eq!(
4091            7.4,
4092            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4093                .as_f64()
4094                .unwrap()
4095        );
4096    }
4097
4098    #[tokio::test(flavor = "multi_thread")]
4099    async fn test_string_uppercase() {
4100        let composed = "\u{e9}";
4101        let uppercase_composed = "\u{c9}";
4102        let decomposed = "e\u{301}";
4103        let uppercase_decomposed = "E\u{301}";
4104        let code = format!(
4105            r#"
4106ascii = string::uppercase("Kcl")
4107unicode_expansion = string::uppercase("Straße")
4108uncased = string::uppercase("東京")
4109empty = string::uppercase("")
4110composed = string::uppercase("{composed}")
4111decomposed = string::uppercase("{decomposed}")
4112piped = "ready" |> string::uppercase()
4113"#
4114        );
4115
4116        let result = parse_execute(&code).await.unwrap();
4117        for (name, expected) in [
4118            ("ascii", "KCL"),
4119            ("unicode_expansion", "STRASSE"),
4120            ("uncased", "東京"),
4121            ("empty", ""),
4122            ("composed", uppercase_composed),
4123            ("decomposed", uppercase_decomposed),
4124            ("piped", "READY"),
4125        ] {
4126            assert_eq!(
4127                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4128                    .as_str()
4129                    .unwrap(),
4130                expected,
4131                "{name}"
4132            );
4133        }
4134    }
4135
4136    #[tokio::test(flavor = "multi_thread")]
4137    async fn test_string_lowercase() {
4138        let composed = "\u{c9}";
4139        let lowercase_composed = "\u{e9}";
4140        let decomposed = "E\u{301}";
4141        let lowercase_decomposed = "e\u{301}";
4142        let expanded = "i\u{307}";
4143        let code = format!(
4144            r#"
4145ascii = string::lowercase("KCL")
4146final_sigma = string::lowercase("ΟΣ")
4147medial_sigma = string::lowercase("ΟΣΑ")
4148unicode_expansion = string::lowercase("İ")
4149uncased = string::lowercase("東京")
4150empty = string::lowercase("")
4151composed = string::lowercase("{composed}")
4152decomposed = string::lowercase("{decomposed}")
4153piped = "READY" |> string::lowercase()
4154"#
4155        );
4156
4157        let result = parse_execute(&code).await.unwrap();
4158        for (name, expected) in [
4159            ("ascii", "kcl"),
4160            ("final_sigma", "ος"),
4161            ("medial_sigma", "οσα"),
4162            ("unicode_expansion", expanded),
4163            ("uncased", "東京"),
4164            ("empty", ""),
4165            ("composed", lowercase_composed),
4166            ("decomposed", lowercase_decomposed),
4167            ("piped", "ready"),
4168        ] {
4169            assert_eq!(
4170                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4171                    .as_str()
4172                    .unwrap(),
4173                expected,
4174                "{name}"
4175            );
4176        }
4177    }
4178
4179    #[tokio::test(flavor = "multi_thread")]
4180    async fn test_string_is_equal() {
4181        let composed = "\u{e9}";
4182        let decomposed = "e\u{301}";
4183        let code = format!(
4184            r#"
4185exact_same = string::isEqual("KCL", to = "KCL")
4186exact_different_case = string::isEqual("KCL", to = "kcl")
4187explicit_case_sensitive = string::isEqual("KCL", to = "kcl", caseInsensitive = false)
4188case_insensitive_ascii = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
4189case_fold_expansion = string::isEqual("Straße", to = "STRASSE", caseInsensitive = true)
4190case_fold_expansion_reversed = string::isEqual("STRASSE", to = "Straße", caseInsensitive = true)
4191case_fold_sigma = string::isEqual("ος", to = "οσ", caseInsensitive = true)
4192case_fold_non_turkic = string::isEqual("I", to = "i", caseInsensitive = true)
4193case_fold_not_turkic = string::isEqual("I", to = "ı", caseInsensitive = true)
4194empty_same = string::isEqual("", to = "")
4195empty_different = string::isEqual("", to = "KCL")
4196exact_without_normalization = string::isEqual("{composed}", to = "{decomposed}")
4197case_fold_without_normalization = string::isEqual("{composed}", to = "{decomposed}", caseInsensitive = true)
4198piped = "ready" |> string::isEqual(to = "READY", caseInsensitive = true)
4199"#
4200        );
4201
4202        let result = parse_execute(&code).await.unwrap();
4203        for (name, expected) in [
4204            ("exact_same", true),
4205            ("exact_different_case", false),
4206            ("explicit_case_sensitive", false),
4207            ("case_insensitive_ascii", true),
4208            ("case_fold_expansion", true),
4209            ("case_fold_expansion_reversed", true),
4210            ("case_fold_sigma", true),
4211            ("case_fold_non_turkic", true),
4212            ("case_fold_not_turkic", false),
4213            ("empty_same", true),
4214            ("empty_different", false),
4215            ("exact_without_normalization", false),
4216            ("case_fold_without_normalization", false),
4217            ("piped", true),
4218        ] {
4219            assert_eq!(
4220                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4221                    .as_bool()
4222                    .unwrap(),
4223                expected,
4224                "{name}"
4225            );
4226        }
4227    }
4228
4229    #[tokio::test(flavor = "multi_thread")]
4230    async fn test_string_is_equal_inside_sketch_block_is_predicate() {
4231        let code = r#"
4232@settings(experimentalFeatures = allow)
4233
4234sketch(on = XY) {
4235  stringsAreEqual = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
4236}
4237"#;
4238
4239        parse_execute(code).await.unwrap();
4240    }
4241
4242    #[tokio::test(flavor = "multi_thread")]
4243    async fn test_string_trim() {
4244        let ascii_whitespace = " \t\n";
4245        let tab = "\t";
4246        let non_breaking_space = "\u{a0}";
4247        let em_space = "\u{2003}";
4248        let ideographic_space = "\u{3000}";
4249        let zero_width_space = "\u{200b}";
4250        let decomposed = "e\u{301}";
4251        let code = format!(
4252            r#"
4253ascii = string::trim("{ascii_whitespace}KCL{ascii_whitespace}")
4254internal = string::trim("  KCL{tab}strings  ")
4255unicode = string::trim("{non_breaking_space}{em_space}KCL{ideographic_space}")
4256all_whitespace = string::trim("{ascii_whitespace}{non_breaking_space}")
4257empty = string::trim("")
4258unchanged = string::trim("KCL")
4259without_normalization = string::trim(" {decomposed} ")
4260non_whitespace = string::trim("{zero_width_space}KCL{zero_width_space}")
4261piped = "  ready  " |> string::trim()
4262"#
4263        );
4264
4265        let result = parse_execute(&code).await.unwrap();
4266        let non_whitespace = format!("{zero_width_space}KCL{zero_width_space}");
4267        for (name, expected) in [
4268            ("ascii", "KCL"),
4269            ("internal", "KCL\tstrings"),
4270            ("unicode", "KCL"),
4271            ("all_whitespace", ""),
4272            ("empty", ""),
4273            ("unchanged", "KCL"),
4274            ("without_normalization", decomposed),
4275            ("non_whitespace", non_whitespace.as_str()),
4276            ("piped", "ready"),
4277        ] {
4278            assert_eq!(
4279                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4280                    .as_str()
4281                    .unwrap(),
4282                expected,
4283                "{name}"
4284            );
4285        }
4286    }
4287
4288    #[tokio::test(flavor = "multi_thread")]
4289    async fn test_string_trim_start() {
4290        let ascii_whitespace = " \t\n";
4291        let tab = "\t";
4292        let non_breaking_space = "\u{a0}";
4293        let em_space = "\u{2003}";
4294        let ideographic_space = "\u{3000}";
4295        let zero_width_space = "\u{200b}";
4296        let decomposed = "e\u{301}";
4297        let code = format!(
4298            r#"
4299ascii = string::trimStart("{ascii_whitespace}KCL{ascii_whitespace}")
4300internal = string::trimStart("  KCL{tab}strings")
4301unicode = string::trimStart("{non_breaking_space}{em_space}KCL{ideographic_space}")
4302all_whitespace = string::trimStart("{ascii_whitespace}{non_breaking_space}")
4303empty = string::trimStart("")
4304unchanged = string::trimStart("KCL")
4305without_normalization = string::trimStart(" {decomposed}")
4306non_whitespace_prefix = string::trimStart("{zero_width_space}{ascii_whitespace}KCL")
4307piped = "  ready  " |> string::trimStart()
4308"#
4309        );
4310
4311        let result = parse_execute(&code).await.unwrap();
4312        let ascii = format!("KCL{ascii_whitespace}");
4313        let unicode = format!("KCL{ideographic_space}");
4314        let non_whitespace_prefix = format!("{zero_width_space}{ascii_whitespace}KCL");
4315        for (name, expected) in [
4316            ("ascii", ascii.as_str()),
4317            ("internal", "KCL\tstrings"),
4318            ("unicode", unicode.as_str()),
4319            ("all_whitespace", ""),
4320            ("empty", ""),
4321            ("unchanged", "KCL"),
4322            ("without_normalization", decomposed),
4323            ("non_whitespace_prefix", non_whitespace_prefix.as_str()),
4324            ("piped", "ready  "),
4325        ] {
4326            assert_eq!(
4327                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4328                    .as_str()
4329                    .unwrap(),
4330                expected,
4331                "{name}"
4332            );
4333        }
4334    }
4335
4336    #[tokio::test(flavor = "multi_thread")]
4337    async fn test_string_trim_end() {
4338        let ascii_whitespace = " \t\n";
4339        let tab = "\t";
4340        let non_breaking_space = "\u{a0}";
4341        let em_space = "\u{2003}";
4342        let ideographic_space = "\u{3000}";
4343        let zero_width_space = "\u{200b}";
4344        let decomposed = "e\u{301}";
4345        let code = format!(
4346            r#"
4347ascii = string::trimEnd("{ascii_whitespace}KCL{ascii_whitespace}")
4348internal = string::trimEnd("KCL{tab}strings  ")
4349unicode = string::trimEnd("{non_breaking_space}KCL{em_space}{ideographic_space}")
4350all_whitespace = string::trimEnd("{ascii_whitespace}{non_breaking_space}")
4351empty = string::trimEnd("")
4352unchanged = string::trimEnd("KCL")
4353without_normalization = string::trimEnd("{decomposed} ")
4354non_whitespace_suffix = string::trimEnd("KCL{ascii_whitespace}{zero_width_space}")
4355piped = "  ready  " |> string::trimEnd()
4356"#
4357        );
4358
4359        let result = parse_execute(&code).await.unwrap();
4360        let ascii = format!("{ascii_whitespace}KCL");
4361        let unicode = format!("{non_breaking_space}KCL");
4362        let non_whitespace_suffix = format!("KCL{ascii_whitespace}{zero_width_space}");
4363        for (name, expected) in [
4364            ("ascii", ascii.as_str()),
4365            ("internal", "KCL\tstrings"),
4366            ("unicode", unicode.as_str()),
4367            ("all_whitespace", ""),
4368            ("empty", ""),
4369            ("unchanged", "KCL"),
4370            ("without_normalization", decomposed),
4371            ("non_whitespace_suffix", non_whitespace_suffix.as_str()),
4372            ("piped", "  ready"),
4373        ] {
4374            assert_eq!(
4375                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4376                    .as_str()
4377                    .unwrap(),
4378                expected,
4379                "{name}"
4380            );
4381        }
4382    }
4383
4384    #[tokio::test(flavor = "multi_thread")]
4385    async fn test_string_to_string() {
4386        // Each case runs on its own so a failure names the expression that
4387        // produced it rather than collapsing the whole table.
4388        for (name, expr, expected) in [
4389            // Every row of the table in the `toString` doc comment appears
4390            // here, so the documentation cannot drift from the behaviour.
4391            ("unitless integer", "12", "12"),
4392            ("unitless fractional", "1.5", "1.5"),
4393            ("no digits dropped", "0.1 + 0.2", "0.30000000000000004"),
4394            ("unitless negative", "-7", "-7"),
4395            ("unitless zero", "0", "0"),
4396            ("negative zero", "-0", "0"),
4397            ("count", "3_", "3_"),
4398            ("millimeters", "12mm", "12mm"),
4399            ("centimeters", "12cm", "12cm"),
4400            ("meters", "12m", "12m"),
4401            ("inches", "1.5in", "1.5in"),
4402            ("feet", "2ft", "2ft"),
4403            ("yards", "3yd", "3yd"),
4404            ("degrees", "90deg", "90deg"),
4405            ("radians", "1.5rad", "1.5rad"),
4406            // Arithmetic keeps the unit it started with.
4407            ("length arithmetic", "2mm + 10mm", "12mm"),
4408            // Multiplying two lengths exceeds what the type system tracks, so
4409            // only the numeric component survives.
4410            ("units the type system loses", "2mm * 10mm", "20"),
4411            ("unitless arithmetic", "1 + 2", "3"),
4412        ] {
4413            let code = format!("actual = string::toString({expr})");
4414            let result = parse_execute(&code).await.unwrap();
4415
4416            assert_eq!(
4417                mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4418                    .as_str()
4419                    .unwrap(),
4420                expected,
4421                "case: {name}"
4422            );
4423        }
4424    }
4425
4426    #[tokio::test(flavor = "multi_thread")]
4427    async fn test_string_to_string_ignores_the_files_default_unit() {
4428        // A value with no suffix has the file's default unit attached, but that
4429        // unit was never written down, so neither is it in the output. Reading
4430        // the result back in a file with a different default gives a different
4431        // quantity; the guarantee is about the number, not the measurement.
4432        let code = "@settings(defaultLengthUnit = inch)\nactual = string::toString(12)";
4433        let result = parse_execute(code).await.unwrap();
4434
4435        assert_eq!(
4436            mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4437                .as_str()
4438                .unwrap(),
4439            "12"
4440        );
4441    }
4442
4443    #[tokio::test(flavor = "multi_thread")]
4444    async fn test_string_to_string_rejects_a_non_number() {
4445        let error = parse_execute(r#"actual = string::toString("already text")"#)
4446            .await
4447            .unwrap_err();
4448
4449        // The declared signature rejects this before the implementation runs,
4450        // so the diagnostic names the function and both types.
4451        assert_eq!(
4452            error.message(),
4453            "The input argument of `string::toString` requires a value with type `number`, but found a value with type `string`."
4454        );
4455        assert!(
4456            matches!(error, KclError::Argument { .. }),
4457            "expected an Argument error, found {error:?}"
4458        );
4459    }
4460
4461    #[tokio::test(flavor = "multi_thread")]
4462    async fn test_string_to_string_accepts_a_piped_argument() {
4463        let result = parse_execute("actual = 12mm |> string::toString()").await.unwrap();
4464
4465        assert_eq!(
4466            mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4467                .as_str()
4468                .unwrap(),
4469            "12mm"
4470        );
4471    }
4472
4473    #[tokio::test(flavor = "multi_thread")]
4474    async fn test_string_to_string_echoes_how_the_literal_was_written() {
4475        // Reading the output back is not a supported operation, but for a
4476        // literal that carries its own units the text still comes out looking
4477        // like what the author typed, which is what makes it readable.
4478        for literal in [
4479            "12",
4480            "1.5",
4481            "0.30000000000000004",
4482            "3_",
4483            // A fractional count and a negative both have to survive the trip,
4484            // since the formatter emits them.
4485            "2.5_",
4486            "-4_",
4487            "12mm",
4488            "-5mm",
4489            "1.5in",
4490            "90deg",
4491            "1.5rad",
4492        ] {
4493            let code = format!("actual = string::toString({literal})");
4494            let result = parse_execute(&code).await.unwrap();
4495
4496            assert_eq!(
4497                mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4498                    .as_str()
4499                    .unwrap(),
4500                literal,
4501                "literal: {literal}"
4502            );
4503        }
4504    }
4505
4506    #[tokio::test(flavor = "multi_thread")]
4507    async fn test_string_to_string_spells_out_non_finite_numbers() {
4508        // Division is unguarded, so these are reachable from ordinary KCL. They
4509        // convert like any other number: the point of the function is to build
4510        // a message, and a message about a NaN is exactly when you need one.
4511        for (name, expr, expected) in [
4512            ("positive infinity", "1 / 0", "Infinity"),
4513            ("negative infinity", "-1 / 0", "-Infinity"),
4514            ("nan", "0 / 0", "NaN"),
4515            // The unit is dropped: no length is described by "Infinitymm".
4516            ("infinity from a length", "1mm / 0", "Infinity"),
4517            ("nan from a length", "0mm / 0", "NaN"),
4518            ("infinity from an angle", "1deg / 0", "Infinity"),
4519        ] {
4520            let code = format!("actual = string::toString({expr})");
4521            let result = parse_execute(&code).await.unwrap();
4522
4523            assert_eq!(
4524                mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4525                    .as_str()
4526                    .unwrap(),
4527                expected,
4528                "case: {name}"
4529            );
4530        }
4531    }
4532
4533    #[tokio::test(flavor = "multi_thread")]
4534    async fn test_string_equality_operators() {
4535        let composed = "\u{e9}";
4536        let decomposed = "e\u{301}";
4537        let code = format!(
4538            r#"
4539equal_same_ascii = "KCL" == "KCL"
4540equal_different_case = "KCL" == "kcl"
4541not_equal_same_ascii = "KCL" != "KCL"
4542not_equal_different_case = "KCL" != "kcl"
4543equal_same_unicode = "{composed}" == "{composed}"
4544not_equal_same_unicode = "{composed}" != "{composed}"
4545equal_without_normalization = "{composed}" == "{decomposed}"
4546not_equal_without_normalization = "{composed}" != "{decomposed}"
4547"#
4548        );
4549
4550        let result = parse_execute(&code).await.unwrap();
4551        for (name, expected) in [
4552            ("equal_same_ascii", true),
4553            ("equal_different_case", false),
4554            ("not_equal_same_ascii", false),
4555            ("not_equal_different_case", true),
4556            ("equal_same_unicode", true),
4557            ("not_equal_same_unicode", false),
4558            ("equal_without_normalization", false),
4559            ("not_equal_without_normalization", true),
4560        ] {
4561            assert_eq!(
4562                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4563                    .as_bool()
4564                    .unwrap(),
4565                expected,
4566                "{name}"
4567            );
4568        }
4569    }
4570
4571    #[tokio::test(flavor = "multi_thread")]
4572    async fn test_string_equality_inside_sketch_block_fails_like_number_equality() {
4573        let string_code = r#"
4574@settings(experimentalFeatures = allow)
4575
4576sketch(on = XY) {
4577  stringsAreEqual = "KCL" == "KCL"
4578}
4579"#;
4580        let number_code = r#"
4581@settings(experimentalFeatures = allow)
4582
4583sketch(on = XY) {
4584  numbersAreEqual = 1 == 1
4585}
4586"#;
4587
4588        assert_eq!(
4589            parse_execute(string_code).await.unwrap_err().message(),
4590            "Cannot create an equivalence constraint between values of these types: a string and a string"
4591        );
4592        assert_eq!(
4593            parse_execute(number_code).await.unwrap_err().message(),
4594            "Cannot create an equivalence constraint between values of these types: a number and a number"
4595        );
4596    }
4597
4598    #[tokio::test(flavor = "multi_thread")]
4599    async fn test_math_execute_start_negative() {
4600        let ast = r#"myVar = -5 + 6"#;
4601        let result = parse_execute(ast).await.unwrap();
4602        assert_eq!(
4603            1.0,
4604            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4605                .as_f64()
4606                .unwrap()
4607        );
4608    }
4609
4610    #[tokio::test(flavor = "multi_thread")]
4611    async fn test_math_execute_with_pi() {
4612        let ast = r#"myVar = PI * 2"#;
4613        let result = parse_execute(ast).await.unwrap();
4614        assert_eq!(
4615            std::f64::consts::TAU,
4616            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4617                .as_f64()
4618                .unwrap()
4619        );
4620    }
4621
4622    #[tokio::test(flavor = "multi_thread")]
4623    async fn test_math_define_decimal_without_leading_zero() {
4624        let ast = r#"thing = .4 + 7"#;
4625        let result = parse_execute(ast).await.unwrap();
4626        assert_eq!(
4627            7.4,
4628            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
4629                .as_f64()
4630                .unwrap()
4631        );
4632    }
4633
4634    #[tokio::test(flavor = "multi_thread")]
4635    async fn pass_std_to_std() {
4636        let ast = r#"sketch001 = startSketchOn(XY)
4637profile001 = circle(sketch001, center = [0, 0], radius = 2)
4638extrude001 = extrude(profile001, length = 5)
4639extrudes = patternLinear3d(
4640  extrude001,
4641  instances = 3,
4642  distance = 5,
4643  axis = [1, 1, 0],
4644)
4645clone001 = map(extrudes, f = clone)
4646"#;
4647        parse_execute(ast).await.unwrap();
4648    }
4649
4650    #[tokio::test(flavor = "multi_thread")]
4651    async fn test_array_reduce_nested_array() {
4652        let code = r#"
4653fn id(@el, accum)  { return accum }
4654
4655answer = reduce([], initial=[[[0,0]]], f=id)
4656"#;
4657        let result = parse_execute(code).await.unwrap();
4658        assert_eq!(
4659            mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
4660            KclValue::HomArray {
4661                value: vec![KclValue::HomArray {
4662                    value: vec![KclValue::HomArray {
4663                        value: vec![
4664                            KclValue::Number {
4665                                value: 0.0,
4666                                ty: NumericType::default(),
4667                                meta: vec![SourceRange::new(69, 70, Default::default()).into()],
4668                            },
4669                            KclValue::Number {
4670                                value: 0.0,
4671                                ty: NumericType::default(),
4672                                meta: vec![SourceRange::new(71, 72, Default::default()).into()],
4673                            }
4674                        ],
4675                        ty: RuntimeType::any(),
4676                    }],
4677                    ty: RuntimeType::any(),
4678                }],
4679                ty: RuntimeType::any(),
4680            }
4681        );
4682    }
4683
4684    #[tokio::test(flavor = "multi_thread")]
4685    async fn test_zero_param_fn() {
4686        let ast = r#"sigmaAllow = 35000 // psi
4687leg1 = 5 // inches
4688leg2 = 8 // inches
4689fn thickness() { return 0.56 }
4690
4691bracket = startSketchOn(XY)
4692  |> startProfile(at = [0,0])
4693  |> line(end = [0, leg1])
4694  |> line(end = [leg2, 0])
4695  |> line(end = [0, -thickness()])
4696  |> line(end = [-leg2 + thickness(), 0])
4697"#;
4698        parse_execute(ast).await.unwrap();
4699    }
4700
4701    #[tokio::test(flavor = "multi_thread")]
4702    async fn test_unary_operator_not_succeeds() {
4703        let ast = r#"
4704fn returnTrue() { return !false }
4705t = true
4706f = false
4707notTrue = !t
4708notFalse = !f
4709c = !!true
4710d = !returnTrue()
4711
4712assertIs(!false, error = "expected to pass")
4713
4714fn check(x) {
4715  assertIs(!x, error = "expected argument to be false")
4716  return true
4717}
4718check(x = false)
4719"#;
4720        let result = parse_execute(ast).await.unwrap();
4721        assert_eq!(
4722            false,
4723            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
4724                .as_bool()
4725                .unwrap()
4726        );
4727        assert_eq!(
4728            true,
4729            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
4730                .as_bool()
4731                .unwrap()
4732        );
4733        assert_eq!(
4734            true,
4735            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
4736                .as_bool()
4737                .unwrap()
4738        );
4739        assert_eq!(
4740            false,
4741            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
4742                .as_bool()
4743                .unwrap()
4744        );
4745    }
4746
4747    #[tokio::test(flavor = "multi_thread")]
4748    async fn test_unary_operator_not_on_non_bool_fails() {
4749        let code1 = r#"
4750// Yup, this is null.
4751myNull = 0 / 0
4752notNull = !myNull
4753"#;
4754        assert_eq!(
4755            parse_execute(code1).await.unwrap_err().message(),
4756            "Cannot apply unary operator ! to non-boolean value: a number",
4757        );
4758
4759        let code2 = "notZero = !0";
4760        assert_eq!(
4761            parse_execute(code2).await.unwrap_err().message(),
4762            "Cannot apply unary operator ! to non-boolean value: a number",
4763        );
4764
4765        let code3 = r#"
4766notEmptyString = !""
4767"#;
4768        assert_eq!(
4769            parse_execute(code3).await.unwrap_err().message(),
4770            "Cannot apply unary operator ! to non-boolean value: a string",
4771        );
4772
4773        let code4 = r#"
4774obj = { a = 1 }
4775notMember = !obj.a
4776"#;
4777        assert_eq!(
4778            parse_execute(code4).await.unwrap_err().message(),
4779            "Cannot apply unary operator ! to non-boolean value: a number",
4780        );
4781
4782        let code5 = "
4783a = []
4784notArray = !a";
4785        assert_eq!(
4786            parse_execute(code5).await.unwrap_err().message(),
4787            "Cannot apply unary operator ! to non-boolean value: an empty array",
4788        );
4789
4790        let code6 = "
4791x = {}
4792notObject = !x";
4793        assert_eq!(
4794            parse_execute(code6).await.unwrap_err().message(),
4795            "Cannot apply unary operator ! to non-boolean value: an object",
4796        );
4797
4798        let code7 = "
4799fn x() { return 1 }
4800notFunction = !x";
4801        let fn_err = parse_execute(code7).await.unwrap_err();
4802        // These are currently printed out as JSON objects, so we don't want to
4803        // check the full error.
4804        assert!(
4805            fn_err
4806                .message()
4807                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
4808            "Actual error: {fn_err:?}"
4809        );
4810
4811        let code8 = "
4812myTagDeclarator = $myTag
4813notTagDeclarator = !myTagDeclarator";
4814        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
4815        // These are currently printed out as JSON objects, so we don't want to
4816        // check the full error.
4817        assert!(
4818            tag_declarator_err
4819                .message()
4820                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
4821            "Actual error: {tag_declarator_err:?}"
4822        );
4823
4824        let code9 = "
4825myTagDeclarator = $myTag
4826notTagIdentifier = !myTag";
4827        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
4828        // These are currently printed out as JSON objects, so we don't want to
4829        // check the full error.
4830        assert!(
4831            tag_identifier_err
4832                .message()
4833                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
4834            "Actual error: {tag_identifier_err:?}"
4835        );
4836
4837        let code10 = "notPipe = !(1 |> 2)";
4838        assert_eq!(
4839            // TODO: We don't currently parse this, but we should.  It should be
4840            // a runtime error instead.
4841            parse_execute(code10).await.unwrap_err(),
4842            KclError::new_syntax(KclErrorDetails::new(
4843                "Unexpected token: !".to_owned(),
4844                vec![SourceRange::new(10, 11, ModuleId::default())],
4845            ))
4846        );
4847
4848        let code11 = "
4849fn identity(x) { return x }
4850notPipeSub = 1 |> identity(!%))";
4851        assert_eq!(
4852            // TODO: We don't currently parse this, but we should.  It should be
4853            // a runtime error instead.
4854            parse_execute(code11).await.unwrap_err(),
4855            KclError::new_syntax(KclErrorDetails::new(
4856                "There was an unexpected `!`. Try removing it.".to_owned(),
4857                vec![SourceRange::new(56, 57, ModuleId::default())],
4858            ))
4859        );
4860
4861        // TODO: Add these tests when we support these types.
4862        // let notNan = !NaN
4863        // let notInfinity = !Infinity
4864    }
4865
4866    #[tokio::test(flavor = "multi_thread")]
4867    async fn test_start_sketch_on_invalid_kwargs() {
4868        let current_dir = std::env::current_dir().unwrap();
4869        let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
4870        let mut code = std::fs::read_to_string(&path).unwrap();
4871        assert_eq!(
4872            parse_execute(&code).await.unwrap_err().message(),
4873            "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
4874        );
4875
4876        path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
4877        code = std::fs::read_to_string(&path).unwrap();
4878
4879        assert_eq!(
4880            parse_execute(&code).await.unwrap_err().message(),
4881            "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
4882        );
4883
4884        path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
4885        code = std::fs::read_to_string(&path).unwrap();
4886
4887        assert_eq!(
4888            parse_execute(&code).await.unwrap_err().message(),
4889            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4890        );
4891
4892        path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
4893        code = std::fs::read_to_string(&path).unwrap();
4894
4895        assert_eq!(
4896            parse_execute(&code).await.unwrap_err().message(),
4897            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4898        );
4899
4900        path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
4901        code = std::fs::read_to_string(&path).unwrap();
4902
4903        assert_eq!(
4904            parse_execute(&code).await.unwrap_err().message(),
4905            "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
4906        );
4907    }
4908
4909    #[tokio::test(flavor = "multi_thread")]
4910    async fn test_math_negative_variable_in_binary_expression() {
4911        let ast = r#"sigmaAllow = 35000 // psi
4912width = 1 // inch
4913
4914p = 150 // lbs
4915distance = 6 // inches
4916FOS = 2
4917
4918leg1 = 5 // inches
4919leg2 = 8 // inches
4920
4921thickness_squared = distance * p * FOS * 6 / sigmaAllow
4922thickness = 0.56 // inches. App does not support square root function yet
4923
4924bracket = startSketchOn(XY)
4925  |> startProfile(at = [0,0])
4926  |> line(end = [0, leg1])
4927  |> line(end = [leg2, 0])
4928  |> line(end = [0, -thickness])
4929  |> line(end = [-leg2 + thickness, 0])
4930"#;
4931        parse_execute(ast).await.unwrap();
4932    }
4933
4934    #[tokio::test(flavor = "multi_thread")]
4935    async fn test_execute_function_no_return() {
4936        let ast = r#"fn test(@origin) {
4937  origin
4938}
4939
4940test([0, 0])
4941"#;
4942        let result = parse_execute(ast).await;
4943        assert!(result.is_err());
4944        assert!(result.unwrap_err().to_string().contains("undefined"));
4945    }
4946
4947    #[tokio::test(flavor = "multi_thread")]
4948    async fn test_max_stack_size_exceeded_error() {
4949        let ast = r#"
4950fn forever(@n) {
4951  return 1 + forever(n)
4952}
4953
4954forever(1)
4955"#;
4956        let result = parse_execute(ast).await;
4957        let err = result.unwrap_err();
4958        // The recursive executor's native-stack cap and the machine
4959        // executor's call-depth guard report differently.
4960        let msg = err.to_string();
4961        assert!(
4962            msg.contains("stack size exceeded") || msg.contains("Call depth limit"),
4963            "actual: {err:?}"
4964        );
4965    }
4966
4967    #[tokio::test(flavor = "multi_thread")]
4968    async fn test_math_doubly_nested_parens() {
4969        let ast = r#"sigmaAllow = 35000 // psi
4970width = 4 // inch
4971p = 150 // Force on shelf - lbs
4972distance = 6 // inches
4973FOS = 2
4974leg1 = 5 // inches
4975leg2 = 8 // inches
4976thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
4977thickness = 0.32 // inches. App does not support square root function yet
4978bracket = startSketchOn(XY)
4979  |> startProfile(at = [0,0])
4980    |> line(end = [0, leg1])
4981  |> line(end = [leg2, 0])
4982  |> line(end = [0, -thickness])
4983  |> line(end = [-1 * leg2 + thickness, 0])
4984  |> line(end = [0, -1 * leg1 + thickness])
4985  |> close()
4986  |> extrude(length = width)
4987"#;
4988        parse_execute(ast).await.unwrap();
4989    }
4990
4991    #[tokio::test(flavor = "multi_thread")]
4992    async fn test_math_nested_parens_one_less() {
4993        let ast = r#" sigmaAllow = 35000 // psi
4994width = 4 // inch
4995p = 150 // Force on shelf - lbs
4996distance = 6 // inches
4997FOS = 2
4998leg1 = 5 // inches
4999leg2 = 8 // inches
5000thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
5001thickness = 0.32 // inches. App does not support square root function yet
5002bracket = startSketchOn(XY)
5003  |> startProfile(at = [0,0])
5004    |> line(end = [0, leg1])
5005  |> line(end = [leg2, 0])
5006  |> line(end = [0, -thickness])
5007  |> line(end = [-1 * leg2 + thickness, 0])
5008  |> line(end = [0, -1 * leg1 + thickness])
5009  |> close()
5010  |> extrude(length = width)
5011"#;
5012        parse_execute(ast).await.unwrap();
5013    }
5014
5015    #[tokio::test(flavor = "multi_thread")]
5016    async fn test_fn_as_operand() {
5017        let ast = r#"fn f() { return 1 }
5018x = f()
5019y = x + 1
5020z = f() + 1
5021w = f() + f()
5022"#;
5023        parse_execute(ast).await.unwrap();
5024    }
5025
5026    #[tokio::test(flavor = "multi_thread")]
5027    async fn kcl_test_ids_stable_between_executions() {
5028        let code = r#"sketch001 = startSketchOn(XZ)
5029|> startProfile(at = [61.74, 206.13])
5030|> xLine(length = 305.11, tag = $seg01)
5031|> yLine(length = -291.85)
5032|> xLine(length = -segLen(seg01))
5033|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5034|> close()
5035|> extrude(length = 40.14)
5036|> shell(
5037    thickness = 3.14,
5038    faces = [seg01]
5039)
5040"#;
5041
5042        let old_program = crate::Program::parse_no_errs(code).unwrap();
5043        let ctx = crate::test_server::new_context(true, None, true, old_program.language_version().unwrap())
5044            .await
5045            .unwrap();
5046
5047        // Execute the program.
5048        if let Err(err) = ctx.run_with_caching(old_program).await {
5049            let report = err.into_miette_report_with_outputs(code).unwrap();
5050            let report = miette::Report::new(report);
5051            panic!("Error executing program: {report:?}");
5052        }
5053
5054        // Get the id_generator from the first execution.
5055        let id_generator = ctx.cache.read_old_ast().await.unwrap().main.exec_state.id_generator;
5056
5057        let code = r#"sketch001 = startSketchOn(XZ)
5058|> startProfile(at = [62.74, 206.13])
5059|> xLine(length = 305.11, tag = $seg01)
5060|> yLine(length = -291.85)
5061|> xLine(length = -segLen(seg01))
5062|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5063|> close()
5064|> extrude(length = 40.14)
5065|> shell(
5066    faces = [seg01],
5067    thickness = 3.14,
5068)
5069"#;
5070
5071        // Execute a slightly different program again.
5072        let program = crate::Program::parse_no_errs(code).unwrap();
5073        // Execute the program.
5074        ctx.run_with_caching(program).await.unwrap();
5075
5076        let new_id_generator = ctx.cache.read_old_ast().await.unwrap().main.exec_state.id_generator;
5077
5078        assert_eq!(id_generator, new_id_generator);
5079    }
5080
5081    #[tokio::test(flavor = "multi_thread")]
5082    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
5083        let code = r#"sketch001 = startSketchOn(XZ)
5084|> startProfile(at = [61.74, 206.13])
5085|> xLine(length = 305.11, tag = $seg01)
5086|> yLine(length = -291.85)
5087|> xLine(length = -segLen(seg01))
5088|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5089|> close()
5090|> extrude(length = 40.14)
5091|> shell(
5092    thickness = 3.14,
5093    faces = [seg01]
5094)
5095"#;
5096
5097        let old_program = crate::Program::parse_no_errs(code).unwrap();
5098        let mut ctx = crate::test_server::new_context(true, None, true, old_program.language_version().unwrap())
5099            .await
5100            .unwrap();
5101
5102        // Execute the program.
5103        ctx.run_with_caching(old_program.clone()).await.unwrap();
5104
5105        let settings_state = ctx.cache.read_old_ast().await.unwrap().settings;
5106
5107        // Ensure the settings are as expected.
5108        assert_eq!(settings_state, ctx.settings);
5109
5110        // Change a setting.
5111        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
5112
5113        // Execute the program.
5114        ctx.run_with_caching(old_program.clone()).await.unwrap();
5115
5116        let settings_state = ctx.cache.read_old_ast().await.unwrap().settings;
5117
5118        // Ensure the settings are as expected.
5119        assert_eq!(settings_state, ctx.settings);
5120
5121        // Change a setting.
5122        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
5123
5124        // Execute the program.
5125        ctx.run_with_caching(old_program).await.unwrap();
5126
5127        let settings_state = ctx.cache.read_old_ast().await.unwrap().settings;
5128
5129        // Ensure the settings are as expected.
5130        assert_eq!(settings_state, ctx.settings);
5131
5132        ctx.close().await;
5133    }
5134
5135    #[tokio::test(flavor = "multi_thread")]
5136    async fn mock_after_not_mock() {
5137        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
5138        let program = crate::Program::parse_no_errs("x = 2").unwrap();
5139        let result = ctx.run_with_caching(program).await.unwrap();
5140        assert_number_variable(&result.variables, "x", 2.0);
5141
5142        // Mock execution must explicitly reuse the live session's memory.
5143        let ctx2 = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
5144        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
5145        let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
5146        assert_number_variable(&result.variables, "z", 3.0);
5147
5148        ctx.close().await;
5149        ctx2.close().await;
5150    }
5151
5152    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/12498
5153    #[tokio::test(flavor = "multi_thread")]
5154    async fn mock_execution_succeeds_after_split() {
5155        let code = kcl_input!("repro_mock_extrude");
5156        let ctx = ExecutorContext::new_mock(None).await;
5157        let program = crate::Program::parse_no_errs(code).unwrap();
5158        let _result = match ctx.run_mock(&program, &MockConfig::default()).await {
5159            Ok(res) => res,
5160            Err(e) => panic!("{}", e.error),
5161        };
5162    }
5163
5164    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/13319
5165    #[tokio::test(flavor = "multi_thread")]
5166    async fn mock_execution_rejects_oob_on_frontend_array() {
5167        let code = r#"
5168values = [10, 20]
5169third = values[2]
5170"#;
5171        let ctx = ExecutorContext::new_mock(None).await;
5172        let program = crate::Program::parse_no_errs(code).unwrap();
5173        let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
5174        ctx.close().await;
5175
5176        assert!(
5177            err.error.message().contains("array doesn't have any item at index 2"),
5178            "{err:?}"
5179        );
5180    }
5181
5182    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/13103
5183    /// i.e.
5184    /// If you do a pattern circular 3d in mock execution mode,
5185    /// and you ask for 10 instances, you should get 10 instances.
5186    #[tokio::test(flavor = "multi_thread")]
5187    async fn mock_execution_pattern_circular_number() {
5188        let code = kcl_input!("repro_mock_pattern_circular");
5189        let ctx = ExecutorContext::new_mock(None).await;
5190        let program = crate::Program::parse_no_errs(code).unwrap();
5191        let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
5192        let copies = result
5193            .variables
5194            .get("copies")
5195            .expect("no variable called 'copies' found");
5196        let value = match copies {
5197            KclValueView::Solid { .. } => {
5198                panic!("One solid?");
5199            }
5200            KclValueView::HomArray { value } => value,
5201            other => panic!("{other:#?}"),
5202        };
5203        let actual_instances = value.len();
5204        let expected_instances = 10; // from the KCL `instances = `
5205        assert_eq!(actual_instances, expected_instances);
5206    }
5207
5208    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/13103
5209    /// i.e.
5210    /// If you do a pattern circular 3d in mock execution mode,
5211    /// and you ask for 10 instances, you should get 10 instances.
5212    #[tokio::test(flavor = "multi_thread")]
5213    async fn mock_execution_subtract() {
5214        // Run this KCL file, in mock execution.
5215        let code = kcl_input!("repro_mock_subtract");
5216        let ctx = ExecutorContext::new_mock(None).await;
5217        let program = crate::Program::parse_no_errs(code).unwrap();
5218        let result = ctx.run_mock(&program, &MockConfig::default()).await;
5219        ctx.close().await;
5220        let result = match result {
5221            Ok(x) => x,
5222            Err(e) => {
5223                let error = e.error;
5224                panic!("{error}");
5225            }
5226        };
5227
5228        // Get the variable we're interested in, from KCL program memory.
5229        let subtracted_parts = result
5230            .variables
5231            .get("subtractedParts")
5232            .expect("no variable called 'subtracted_parts' found");
5233        let subtracted_parts = match subtracted_parts {
5234            KclValueView::Solid { .. } => {
5235                panic!("One solid?");
5236            }
5237            KclValueView::HomArray { value } => value,
5238            other => panic!("{other:#?}"),
5239        };
5240
5241        // Validate the variable.
5242        // from the KCL, there's 2 parts being subtracted from.
5243        let expected_number_of_parts = 2;
5244        let actual_number_of_parts = subtracted_parts.len();
5245        assert_eq!(actual_number_of_parts, expected_number_of_parts);
5246    }
5247
5248    #[tokio::test(flavor = "multi_thread")]
5249    async fn mock_then_add_extrude_then_mock_again() {
5250        let code = "s = sketch(on = XY) {
5251    line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
5252    line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
5253    coincident([line1.end, line2.start])
5254    line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
5255    coincident([line2.end, line3.start])
5256    coincident([line1.start, line3.end])
5257}
5258    ";
5259        let ctx = ExecutorContext::new_mock(None).await;
5260        let program = crate::Program::parse_no_errs(code).unwrap();
5261        let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
5262        assert!(result.variables.contains_key("s"), "actual: {:?}", result.variables);
5263
5264        let code2 = code.to_owned()
5265            + "
5266region001 = region(point = [1mm, 1mm], sketch = s)
5267extrude001 = extrude(region001, length = 1)
5268    ";
5269        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
5270        let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
5271        assert!(
5272            result.variables.contains_key("region001"),
5273            "actual: {:?}",
5274            result.variables
5275        );
5276
5277        ctx.close().await;
5278    }
5279
5280    #[tokio::test(flavor = "multi_thread")]
5281    async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
5282        let code = format!(
5283            r#"{}
5284
5285face7 = faceOf(solid6, face = r6.tags.line1)
5286r7 = squareRegion(onSurface = face7)
5287solid7 = extrude(r7, length = width)
5288"#,
5289            include_str!("../../tests/endless_impeller/input.kcl")
5290        );
5291
5292        let result = parse_execute(&code).await.unwrap();
5293        let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
5294        assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
5295
5296        let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
5297            KclValue::Face { value } => value,
5298            value => panic!("expected face7 to be a Face, got {value:?}"),
5299        };
5300        assert!(face7.parent_solid.creator_sketch_id.is_some());
5301    }
5302
5303    #[tokio::test(flavor = "multi_thread")]
5304    async fn mock_has_stable_ids() {
5305        let ctx = ExecutorContext::new_mock(None).await;
5306        let mock_config = MockConfig {
5307            use_prev_memory: false,
5308            ..Default::default()
5309        };
5310        let code = "sk = startSketchOn(XY)
5311        |> startProfile(at = [0, 0])";
5312        let program = crate::Program::parse_no_errs(code).unwrap();
5313        let result = ctx.run_mock(&program, &mock_config).await.unwrap();
5314        let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
5315        assert!(!ids.is_empty(), "IDs should not be empty");
5316
5317        let ctx2 = ExecutorContext::new_mock(None).await;
5318        let program2 = crate::Program::parse_no_errs(code).unwrap();
5319        let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
5320        let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
5321
5322        assert_eq!(ids, ids2, "Generated IDs should match");
5323        ctx.close().await;
5324        ctx2.close().await;
5325    }
5326
5327    #[tokio::test(flavor = "multi_thread")]
5328    async fn mock_memory_restore_preserves_module_maps() {
5329        let ctx = ExecutorContext::new_mock(None).await;
5330        let cold_start = MockConfig {
5331            use_prev_memory: false,
5332            ..Default::default()
5333        };
5334        ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
5335
5336        let mut mem = ctx.cache.read_old_memory().await.unwrap();
5337        assert!(
5338            mem.path_to_source_id.len() > 3,
5339            "expected prelude imports to populate multiple modules, got {:?}",
5340            mem.path_to_source_id
5341        );
5342        mem.constraint_state.insert(
5343            kcl_api::ObjectId(1),
5344            indexmap::indexmap! {
5345                crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
5346                    crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
5347            },
5348        );
5349        let imported_id = ModuleId::from_usize(42);
5350        mem.never_type_ranges
5351            .insert(imported_id, vec![SourceRange::new(0, 5, imported_id)]);
5352
5353        let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
5354        ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
5355
5356        assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
5357        assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
5358        assert_eq!(exec_state.global.never_type_ranges, mem.never_type_ranges);
5359        assert_eq!(exec_state.global.module_infos, mem.module_infos);
5360        assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
5361
5362        ctx.close().await;
5363    }
5364
5365    // These use a live execution context with a mock engine so they exercise
5366    // the cache orchestration without a server or API token.
5367    #[tokio::test(flavor = "multi_thread")]
5368    async fn session_cache_isolates_concurrent_contexts() {
5369        let a = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5370        let b = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5371        let program_a = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 2").unwrap();
5372        let program_b = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 7").unwrap();
5373        let (outcome_a, outcome_b) = tokio::join!(
5374            a.run_with_caching(program_a.clone()),
5375            b.run_with_caching(program_b.clone()),
5376        );
5377        assert_number_variable(&outcome_a.unwrap().variables, "x", 2.0);
5378        assert_number_variable(&outcome_b.unwrap().variables, "x", 7.0);
5379
5380        // Both sessions must retain their own last execution, including when
5381        // the next request is a cache hit.
5382        assert_eq!(a.cache.read_old_ast().await.unwrap().main.ast, program_a.ast);
5383        assert_eq!(b.cache.read_old_ast().await.unwrap().main.ast, program_b.ast);
5384        assert_number_variable(&a.run_with_caching(program_a).await.unwrap().variables, "x", 2.0);
5385        assert_number_variable(&b.run_with_caching(program_b).await.unwrap().variables, "x", 7.0);
5386        a.bust_cache().await;
5387        assert!(a.cache.read_old_ast().await.is_none());
5388        assert!(b.cache.read_old_ast().await.is_some());
5389        a.close().await;
5390        b.close().await;
5391    }
5392
5393    #[tokio::test(flavor = "multi_thread")]
5394    async fn session_cache_shares_clones_and_associated_mock_memory() {
5395        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5396        let program = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 2").unwrap();
5397        ctx.run_with_caching(program).await.unwrap();
5398        let clone = ctx.clone();
5399        let import_ctx = ctx.clone_with_fresh_execution_batch();
5400        assert!(clone.cache.read_old_ast().await.is_some());
5401        import_ctx.bust_cache().await;
5402        assert!(clone.cache.read_old_ast().await.is_none());
5403
5404        let mock = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
5405        let expression = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\ny = x + 1").unwrap();
5406        let outcome = mock.run_mock(&expression, &MockConfig::default()).await.unwrap();
5407        assert_number_variable(&outcome.variables, "y", 3.0);
5408        assert!(ctx.cache.read_old_memory_var("y").await.is_some());
5409
5410        // An unrelated mock context cannot see the session's variables.
5411        let independent = ExecutorContext::new_mock(None).await;
5412        independent
5413            .run_mock(&expression, &MockConfig::default())
5414            .await
5415            .unwrap_err();
5416        mock.cache.clear_mem_cache().await;
5417        assert!(ctx.cache.read_old_memory().await.is_none());
5418        ctx.close().await;
5419        mock.close().await;
5420        independent.close().await;
5421    }
5422
5423    #[tokio::test(flavor = "multi_thread")]
5424    async fn session_cache_failure_preserves_partial_mock_memory() {
5425        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5426        let program = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 2\nbad = missing").unwrap();
5427        ctx.run_with_caching(program).await.unwrap_err();
5428        assert!(ctx.cache.read_old_ast().await.is_none());
5429        let mock = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
5430        let expression = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\ny = x + 1").unwrap();
5431        let outcome = mock.run_mock(&expression, &MockConfig::default()).await.unwrap();
5432        assert_number_variable(&outcome.variables, "y", 3.0);
5433        ctx.close().await;
5434        mock.close().await;
5435    }
5436
5437    #[tokio::test(flavor = "multi_thread")]
5438    async fn cached_execution_phases_preserve_edits_settings_and_error_recovery() {
5439        let mut ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5440        let original = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 2").unwrap();
5441        for _ in 0..2 {
5442            let outcome = ctx.run_with_caching(original.clone()).await.unwrap();
5443            assert_number_variable(&outcome.variables, "x", 2.0);
5444        }
5445
5446        // Appending a statement reuses the old environment. Finalization must
5447        // cache the full program, including the unchanged statement.
5448        let extended = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 2\ny = x + 1").unwrap();
5449        for _ in 0..2 {
5450            let outcome = ctx.run_with_caching(extended.clone()).await.unwrap();
5451            assert_number_variable(&outcome.variables, "x", 2.0);
5452            assert_number_variable(&outcome.variables, "y", 3.0);
5453            assert_eq!(ctx.cache.read_old_ast().await.unwrap().main.ast.body.len(), 2);
5454        }
5455
5456        // Changing an existing statement requires a fresh execution state.
5457        let changed = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 4\ny = x + 1").unwrap();
5458        let outcome = ctx.run_with_caching(changed.clone()).await.unwrap();
5459        assert_number_variable(&outcome.variables, "y", 5.0);
5460        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
5461        let outcome = ctx.run_with_caching(changed).await.unwrap();
5462        assert_number_variable(&outcome.variables, "y", 5.0);
5463        assert_eq!(ctx.cache.read_old_ast().await.unwrap().settings, ctx.settings);
5464
5465        let invalid = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = missing").unwrap();
5466        ctx.run_with_caching(invalid).await.unwrap_err();
5467        assert!(
5468            ctx.cache.read_old_ast().await.is_none(),
5469            "execution errors must invalidate the cache"
5470        );
5471        let outcome = ctx.run_with_caching(original).await.unwrap();
5472        assert_number_variable(&outcome.variables, "x", 2.0);
5473
5474        ctx.close().await;
5475    }
5476
5477    #[tokio::test(flavor = "multi_thread")]
5478    async fn cached_execution_phases_recheck_changed_imports() {
5479        let project = tempfile::TempDir::with_prefix("kcl_cache_phases").unwrap();
5480        let imported = project.path().join("values.kcl");
5481        tokio::fs::write(&imported, "export x = 2").await.unwrap();
5482        let mut ctx = ExecutorContext::new_with_engine(
5483            Arc::new(EngineManager::new_mock()),
5484            ExecutorSettings {
5485                project_directory: Some(crate::TypedPath(project.path().into())),
5486                ..Default::default()
5487            },
5488        );
5489        let program =
5490            crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nimport x from 'values.kcl'\ny = x + 1")
5491                .unwrap();
5492        for _ in 0..2 {
5493            let outcome = ctx.run_with_caching(program.clone()).await.unwrap();
5494            assert_number_variable(&outcome.variables, "y", 3.0);
5495        }
5496        // Exercise the unchanged-import early return with settings reapplied.
5497        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
5498        let outcome = ctx.run_with_caching(program.clone()).await.unwrap();
5499        assert_number_variable(&outcome.variables, "y", 3.0);
5500        assert_eq!(ctx.cache.read_old_ast().await.unwrap().settings, ctx.settings);
5501
5502        tokio::fs::write(&imported, "export x = 7").await.unwrap();
5503        for _ in 0..2 {
5504            let outcome = ctx.run_with_caching(program.clone()).await.unwrap();
5505            assert_number_variable(&outcome.variables, "y", 8.0);
5506        }
5507
5508        ctx.close().await;
5509    }
5510
5511    #[tokio::test(flavor = "multi_thread")]
5512    async fn run_with_caching_no_action_refreshes_mock_memory() {
5513        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5514        let program = crate::Program::parse_no_errs(
5515            r#"sketch001 = sketch(on = XY) {
5516  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
5517}
5518"#,
5519        )
5520        .unwrap();
5521
5522        ctx.run_with_caching(program.clone()).await.unwrap();
5523        let baseline_memory = ctx.cache.read_old_memory().await.unwrap();
5524        assert!(
5525            !baseline_memory.scene_objects.is_empty(),
5526            "expected engine execution to persist full-scene mock memory"
5527        );
5528
5529        ctx.cache
5530            .write_old_memory(cache::SketchModeState::new_for_tests())
5531            .await;
5532        assert_eq!(ctx.cache.read_old_memory().await.unwrap().scene_objects.len(), 0);
5533
5534        ctx.run_with_caching(program).await.unwrap();
5535        let refreshed_memory = ctx.cache.read_old_memory().await.unwrap();
5536        assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
5537        assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
5538        assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
5539
5540        ctx.close().await;
5541    }
5542
5543    #[tokio::test(flavor = "multi_thread")]
5544    async fn sim_sketch_mode_real_mock_real() {
5545        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
5546        let code = r#"sketch001 = startSketchOn(XY)
5547profile001 = startProfile(sketch001, at = [0, 0])
5548  |> line(end = [10, 0])
5549  |> line(end = [0, 10])
5550  |> line(end = [-10, 0])
5551  |> line(end = [0, -10])
5552  |> close()
5553"#;
5554        let program = crate::Program::parse_no_errs(code).unwrap();
5555        let result = ctx.run_with_caching(program).await.unwrap();
5556        assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
5557
5558        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
5559        let mock_program = crate::Program::parse_no_errs(code).unwrap();
5560        let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
5561        assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
5562
5563        let code2 = code.to_owned()
5564            + r#"
5565extrude001 = extrude(profile001, length = 10)
5566"#;
5567        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
5568        let result = ctx.run_with_caching(program2).await.unwrap();
5569        assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
5570
5571        ctx.close().await;
5572        mock_ctx.close().await;
5573    }
5574
5575    #[tokio::test(flavor = "multi_thread")]
5576    async fn read_tag_version() {
5577        let ast = r#"fn bar(@t) {
5578  return startSketchOn(XY)
5579    |> startProfile(at = [0,0])
5580    |> angledLine(
5581        angle = -60,
5582        length = segLen(t),
5583    )
5584    |> line(end = [0, 0])
5585    |> close()
5586}
5587
5588sketch = startSketchOn(XY)
5589  |> startProfile(at = [0,0])
5590  |> line(end = [0, 10])
5591  |> line(end = [10, 0], tag = $tag0)
5592  |> line(endAbsolute = [0, 0])
5593
5594fn foo() {
5595  // tag0 tags an edge
5596  return bar(tag0)
5597}
5598
5599solid = sketch |> extrude(length = 10)
5600// tag0 tags a face
5601sketch2 = startSketchOn(solid, face = tag0)
5602  |> startProfile(at = [0,0])
5603  |> line(end = [0, 1])
5604  |> line(end = [1, 0])
5605  |> line(end = [0, 0])
5606
5607foo() |> extrude(length = 1)
5608"#;
5609        parse_execute(ast).await.unwrap();
5610    }
5611
5612    #[tokio::test(flavor = "multi_thread")]
5613    async fn experimental() {
5614        let code = r#"
5615startSketchOn(XY)
5616  |> startProfile(at = [0, 0], tag = $start)
5617  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5618"#;
5619        let result = parse_execute(code).await.unwrap();
5620        let issues = result.exec_state.issues();
5621        assert_eq!(issues.len(), 1);
5622        assert_eq!(issues[0].severity, Severity::Error);
5623        let msg = &issues[0].message;
5624        assert!(msg.contains("experimental"), "found {msg}");
5625
5626        let code = r#"@settings(experimentalFeatures = allow)
5627startSketchOn(XY)
5628  |> startProfile(at = [0, 0], tag = $start)
5629  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5630"#;
5631        let result = parse_execute(code).await.unwrap();
5632        let issues = result.exec_state.issues();
5633        assert!(issues.is_empty(), "issues={issues:#?}");
5634
5635        let code = r#"@settings(experimentalFeatures = warn)
5636startSketchOn(XY)
5637  |> startProfile(at = [0, 0], tag = $start)
5638  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5639"#;
5640        let result = parse_execute(code).await.unwrap();
5641        let issues = result.exec_state.issues();
5642        assert_eq!(issues.len(), 1);
5643        assert_eq!(issues[0].severity, Severity::Warning);
5644        let msg = &issues[0].message;
5645        assert!(msg.contains("experimental"), "found {msg}");
5646
5647        let code = r#"@settings(experimentalFeatures = deny)
5648startSketchOn(XY)
5649  |> startProfile(at = [0, 0], tag = $start)
5650  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5651"#;
5652        let result = parse_execute(code).await.unwrap();
5653        let issues = result.exec_state.issues();
5654        assert_eq!(issues.len(), 1);
5655        assert_eq!(issues[0].severity, Severity::Error);
5656        let msg = &issues[0].message;
5657        assert!(msg.contains("experimental"), "found {msg}");
5658
5659        let code = r#"@settings(experimentalFeatures = foo)
5660startSketchOn(XY)
5661  |> startProfile(at = [0, 0], tag = $start)
5662  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5663"#;
5664        parse_execute(code).await.unwrap_err();
5665    }
5666
5667    #[tokio::test(flavor = "multi_thread")]
5668    async fn default_angle_unit_warns_in_legacy_kcl() {
5669        for version in ["", "kclVersion = 1.0, ", "kclVersion = 2.0, "] {
5670            for unit in ["deg", "rad"] {
5671                let code = format!("@settings({version}defaultAngleUnit = {unit})\nx = 1\n");
5672                let result = parse_execute(&code).await.unwrap();
5673                let issues = result.issues();
5674                assert_eq!(issues.len(), 1, "code={code}");
5675                assert_eq!(issues[0].severity, Severity::Warning, "code={code}");
5676                assert_eq!(
5677                    issues[0].message,
5678                    "The `defaultAngleUnit` setting is deprecated; use explicit units for angles"
5679                );
5680                assert_eq!(variable_f64(&result, "x"), 1.0);
5681            }
5682        }
5683    }
5684
5685    #[tokio::test(flavor = "multi_thread")]
5686    async fn default_angle_unit_errors_in_kcl_v3() {
5687        for settings in [
5688            "@settings(kclVersion = \"3.0-preview\", defaultAngleUnit = deg)",
5689            "@settings(defaultAngleUnit = rad, kclVersion = \"3.0-preview\")",
5690            "@settings(defaultAngleUnit = deg)\n@settings(kclVersion = \"3.0-preview\")",
5691            "@settings(kclVersion = \"3.0-preview\")\n@settings(defaultAngleUnit = rad)",
5692        ] {
5693            let code = format!("{settings}\nx = 1\n");
5694            let Err(error) = parse_execute(&code).await else {
5695                panic!("defaultAngleUnit must fail in KCL 3.0: {code}");
5696            };
5697            assert_eq!(
5698                error.message(),
5699                "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles",
5700                "code={code}"
5701            );
5702            let ranges = error.source_ranges();
5703            assert_eq!(ranges.len(), 1);
5704            assert!(code[ranges[0].start()..ranges[0].end()].contains("defaultAngleUnit"));
5705        }
5706    }
5707
5708    #[tokio::test(flavor = "multi_thread")]
5709    async fn default_angle_unit_error_cannot_be_suppressed() {
5710        for version in ["1.0", "2.0", "\"3.0-preview\""] {
5711            let code = format!(
5712                "@warnings(allow = angleUnits)\n@settings(kclVersion = {version}, defaultAngleUnit = deg)\nx = 1\n"
5713            );
5714            let result = parse_execute(&code).await;
5715            if version == "\"3.0-preview\"" {
5716                assert_eq!(
5717                    result.unwrap_err().message(),
5718                    "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
5719                );
5720            } else {
5721                assert!(result.unwrap().issues().is_empty(), "code={code}");
5722            }
5723        }
5724    }
5725
5726    /// The `defaultAngleUnit` gate in an imported module follows the effective
5727    /// kclVersion: the entry point's when it declares KCL 3.0, otherwise the
5728    /// module's own (undeclared here, so 1.0). The module declares no
5729    /// kclVersion because a KCL 3.0 entry point rejects an import declaring a
5730    /// different one before this gate is reached.
5731    #[tokio::test(flavor = "multi_thread")]
5732    async fn default_angle_unit_in_import_uses_effective_kcl_version() {
5733        let dep = "@settings(defaultAngleUnit = deg)\nexport x = 1\n";
5734        for version in ["1.0", "2.0", "\"3.0-preview\""] {
5735            let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5736            let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5737            if version == "\"3.0-preview\"" {
5738                assert_eq!(
5739                    result.unwrap_err().message(),
5740                    "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
5741                );
5742            } else {
5743                assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0);
5744            }
5745        }
5746    }
5747
5748    /// The attribute name recognized in an imported file follows the effective
5749    /// kclVersion: the entry point's when it declares KCL 3.0, otherwise the
5750    /// imported file's own (undeclared here, so 1.0). The imported file
5751    /// declares no kclVersion because a KCL 3.0 entry point rejects an import
5752    /// declaring a different one.
5753    ///
5754    /// Warnings raised while an imported file runs aren't observable here (it
5755    /// runs on a clone of the execution state), so this checks the gate with
5756    /// an unknown diagnostic name: a fatal error when the attribute is
5757    /// recognized, and an unknown annotation otherwise.
5758    #[tokio::test(flavor = "multi_thread")]
5759    async fn diagnostics_attribute_in_import_uses_effective_kcl_version() {
5760        let dep = "@diagnostics(allow = bogus)\nexport x = 1\n";
5761        for version in ["1.0", "2.0", "\"3.0-preview\""] {
5762            let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5763            let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5764            if version == "\"3.0-preview\"" {
5765                let error = result.unwrap_err();
5766                let message = error.message();
5767                assert!(
5768                    message.starts_with("Unexpected diagnostic value: `bogus`; accepted values: "),
5769                    "main={main}, message={message}"
5770                );
5771            } else {
5772                assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0, "main={main}");
5773            }
5774        }
5775
5776        // The old `@warnings` name follows the same version: recognized, and
5777        // so validated, before KCL 3.0, and an ignored attribute with a
5778        // non-fatal error under KCL 3.0.
5779        let dep = "@warnings(allow = bogus)\nexport x = 1\n";
5780        for version in ["1.0", "2.0", "\"3.0-preview\""] {
5781            let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5782            let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5783            if version == "\"3.0-preview\"" {
5784                assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0, "main={main}");
5785            } else {
5786                let error = result.unwrap_err();
5787                let message = error.message();
5788                assert!(
5789                    message.starts_with("Unexpected warning value: `bogus`; accepted values: "),
5790                    "main={main}, message={message}"
5791                );
5792            }
5793        }
5794    }
5795
5796    /// The entry point's declared kclVersion is recorded whatever it is, so
5797    /// that errors can name it. Only KCL 3.0 or later pins the version for the
5798    /// whole execution; see [`ExecState::kcl_version`].
5799    #[tokio::test(flavor = "multi_thread")]
5800    async fn entry_point_kcl_version_records_declared_version() {
5801        for (code, expected) in [
5802            ("x = 1\n", None),
5803            ("@settings(defaultLengthUnit = in)\nx = 1\n", None),
5804            ("@settings(kclVersion = 1.0)\nx = 1\n", Some(KclVersion::V1)),
5805            ("@settings(kclVersion = 2.0)\nx = 1\n", Some(KclVersion::V2)),
5806            (
5807                "@settings(kclVersion = \"3.0-preview\")\nx = 1\n",
5808                Some(KclVersion::V3Preview),
5809            ),
5810        ] {
5811            let result = parse_execute(code).await.unwrap();
5812            assert_eq!(
5813                result.exec_state.global.entry_point_kcl_version, expected,
5814                "code={code}"
5815            );
5816            assert_eq!(
5817                result.exec_state.entry_point_version_is_v3_or_higher(),
5818                expected == Some(KclVersion::V3Preview),
5819                "code={code}"
5820            );
5821        }
5822    }
5823
5824    #[tokio::test]
5825    async fn default_tolerance_command_is_sent_only_for_kcl_3() {
5826        let ctx = ExecutorContext::new_mock(None).await;
5827        let mut exec_state = ExecState::new(&ctx);
5828
5829        for version in [None, Some(KclVersion::V1), Some(KclVersion::V2)] {
5830            exec_state.global.entry_point_kcl_version = version;
5831            assert!(ExecutorContext::default_tolerance_command(&exec_state).is_none());
5832        }
5833
5834        exec_state.global.entry_point_kcl_version = Some(KclVersion::V3Preview);
5835        for unit in [kcl_api::UnitLength::Millimeters, kcl_api::UnitLength::Inches] {
5836            exec_state.mod_local.settings.default_length_units = unit;
5837            let Some(ModelingCmd::SetDefaultSystemProperties(properties)) =
5838                ExecutorContext::default_tolerance_command(&exec_state)
5839            else {
5840                panic!("expected KCL 3 default system properties command");
5841            };
5842            let tolerance = properties.tolerance.expect("expected KCL 3 tolerance");
5843            approx::assert_relative_eq!(
5844                tolerance.point_point_2d_coincident.0,
5845                1e-8 * std::f64::consts::SQRT_2,
5846                epsilon = 0.0,
5847                max_relative = 1e-12
5848            );
5849        }
5850    }
5851
5852    #[tokio::test(flavor = "multi_thread")]
5853    async fn kcl_version_lookup_prefers_entry_point_over_module_local() {
5854        let mut exec_state = parse_execute("x = 1\n").await.unwrap().exec_state;
5855
5856        // Legacy fallback: the module-local settings.
5857        exec_state.global.entry_point_kcl_version = None;
5858        exec_state.mod_local.settings.kcl_version = KclVersion::V2;
5859        assert_eq!(exec_state.kcl_version(), KclVersion::V2);
5860        assert_eq!(exec_state.legacy_caller_kcl_version(), KclVersion::V2);
5861
5862        // A pre-3.0 entry-point declaration does not pin the version: the
5863        // module-local settings still apply.
5864        exec_state.global.entry_point_kcl_version = Some(KclVersion::V1);
5865        assert_eq!(exec_state.kcl_version(), KclVersion::V2);
5866
5867        // An entry-point KCL 3.0 declaration overrides the module-local
5868        // settings for the unified lookup, but not for the legacy one.
5869        exec_state.global.entry_point_kcl_version = Some(KclVersion::V3Preview);
5870        assert_eq!(exec_state.kcl_version(), KclVersion::V3Preview);
5871        assert_eq!(exec_state.legacy_caller_kcl_version(), KclVersion::V2);
5872    }
5873
5874    /// Mock execution skips `run_concurrent`, so it relies on `inner_run` to
5875    /// record the entry point's kclVersion -- including re-recording it on
5876    /// every run when restoring memory preserved from a previous mock run,
5877    /// since the preserved memory must not pin the previous program's version.
5878    #[tokio::test(flavor = "multi_thread")]
5879    async fn mock_execution_records_entry_point_kcl_version() {
5880        use futures::FutureExt;
5881
5882        let ctx = ExecutorContext::new_mock(None).await;
5883        let fresh_memory = MockConfig {
5884            use_prev_memory: false,
5885            ..Default::default()
5886        };
5887        let prev_memory = MockConfig::default();
5888
5889        let v3_program = crate::Program::parse_no_errs("@settings(kclVersion = \"3.0-preview\")\nx = 1\n").unwrap();
5890        let v2_program = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 1\n").unwrap();
5891
5892        // Close the context even if an assertion panics,
5893        // then let the panic continue.
5894        let test_result = std::panic::AssertUnwindSafe(async {
5895            let (exec_state, _) = ctx.run_mock_returning_state(&v3_program, &fresh_memory).await.unwrap();
5896            assert_eq!(
5897                exec_state.global.entry_point_kcl_version,
5898                Some(KclVersion::V3Preview),
5899                "mock execution should record a 3.0-preview entry point"
5900            );
5901            assert!(exec_state.entry_point_version_is_v3_or_higher());
5902
5903            // Populate the preserved mock memory with a 3.0-preview run, then
5904            // check that a 2.0 run restoring that memory isn't pinned to
5905            // 3.0-preview...
5906            ctx.run_mock(&v3_program, &fresh_memory).await.unwrap();
5907            let (exec_state, _) = ctx.run_mock_returning_state(&v2_program, &prev_memory).await.unwrap();
5908            assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V2));
5909            assert!(!exec_state.entry_point_version_is_v3_or_higher());
5910
5911            // ...and that a 3.0-preview run restoring a 2.0 run's memory
5912            // records 3.0-preview.
5913            ctx.run_mock(&v2_program, &fresh_memory).await.unwrap();
5914            let (exec_state, _) = ctx.run_mock_returning_state(&v3_program, &prev_memory).await.unwrap();
5915            assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V3Preview));
5916        })
5917        .catch_unwind()
5918        .await;
5919
5920        ctx.close().await;
5921        if let Err(panic) = test_result {
5922            std::panic::resume_unwind(panic);
5923        }
5924    }
5925
5926    /// Mock execution applies the KCL 3.0 semantics -- early return and
5927    /// if-arm scoping -- since it records the entry point's kclVersion via
5928    /// `inner_run` rather than `run_concurrent`.
5929    #[tokio::test(flavor = "multi_thread")]
5930    async fn mock_execution_applies_v3_semantics() {
5931        use futures::FutureExt;
5932
5933        let ctx = ExecutorContext::new_mock(None).await;
5934        let fresh_memory = MockConfig {
5935            use_prev_memory: false,
5936            ..Default::default()
5937        };
5938        let program = crate::Program::parse_no_errs(
5939            r#"@settings(kclVersion = "3.0-preview")
5940fn f() {
5941  return 1
5942  assert(1, isEqualTo = 2, error = "code after return ran")
5943}
5944x = f()
5945outer = 1
5946y = if true {
5947  outer = 2
5948  outer + 10
5949} else {
5950  0
5951}
5952"#,
5953        )
5954        .unwrap();
5955
5956        // Close the context even if an assertion panics,
5957        // then let the panic continue.
5958        let test_result = std::panic::AssertUnwindSafe(async {
5959            let (exec_state, env) = ctx.run_mock_returning_state(&program, &fresh_memory).await.unwrap();
5960            let var = |name: &str| mem_get_json(exec_state.stack(), env, name).as_f64().unwrap();
5961            assert_eq!(var("x"), 1.0, "early return produces the function's value");
5962            assert_eq!(var("y"), 12.0, "the branch sees its own shadowing binding");
5963            assert_eq!(var("outer"), 1.0, "the outer binding is unchanged after the if");
5964        })
5965        .catch_unwind()
5966        .await;
5967
5968        ctx.close().await;
5969        if let Err(panic) = test_result {
5970            std::panic::resume_unwind(panic);
5971        }
5972    }
5973
5974    /// The modeling commands sent to the engine during the run, across the
5975    /// root module and every imported module.
5976    fn commands_everywhere(result: &ExecTestResults) -> impl Iterator<Item = &kittycad_modeling_cmds::ModelingCmd> {
5977        let module_commands = result
5978            .exec_state
5979            .global
5980            .module_infos
5981            .values()
5982            .filter_map(|info| match &info.repr {
5983                ModuleRepr::Kcl(_, Some(outcome)) => Some(outcome.artifacts.commands.iter()),
5984                _ => None,
5985            })
5986            .flatten();
5987        result
5988            .root_module_artifact_commands()
5989            .iter()
5990            .chain(module_commands)
5991            .map(|artifact_command| &artifact_command.command)
5992    }
5993
5994    /// All fillet algorithm versions sent to the engine during the run. The
5995    /// version emitted is the observable for which kclVersion governed the
5996    /// filleting code; see `default_edge_cut_version`.
5997    fn emitted_fillet_versions_everywhere(
5998        result: &ExecTestResults,
5999    ) -> Vec<kittycad_modeling_cmds::shared::EdgeCutVersion> {
6000        commands_everywhere(result)
6001            .filter_map(|command| match command {
6002                kittycad_modeling_cmds::ModelingCmd::Solid3dCutEdges(command) => Some(command.version),
6003                _ => None,
6004            })
6005            .collect()
6006    }
6007
6008    /// All region algorithm versions sent to the engine during the run. The
6009    /// version emitted is the observable for whether KCL 1.0 or 2.0 governed
6010    /// the region code, their only runtime difference; see `region_version`
6011    /// in `std::sketch`.
6012    fn emitted_region_versions_everywhere(
6013        result: &ExecTestResults,
6014    ) -> Vec<kittycad_modeling_cmds::shared::RegionVersion> {
6015        commands_everywhere(result)
6016            .filter_map(|command| match command {
6017                kittycad_modeling_cmds::ModelingCmd::CreateRegion(command) => Some(command.version.clone()),
6018                _ => None,
6019            })
6020            .collect()
6021    }
6022
6023    const FILLET_AT_MODULE_TOP_LEVEL: &str = r#"
6024profile = startSketchOn(XY)
6025  |> startProfile(at = [0, 0])
6026  |> line(end = [10, 0], tag = $edge)
6027  |> line(end = [0, 10])
6028  |> line(end = [-10, 0])
6029  |> close()
6030solid = extrude(profile, length = 10)
6031fillet(solid, tags = [edge], radius = 1)
6032"#;
6033
6034    const FILLET_IN_EXPORTED_FN: &str = r#"
6035export fn filletedBox() {
6036  profile = startSketchOn(XY)
6037    |> startProfile(at = [0, 0])
6038    |> line(end = [10, 0], tag = $edge)
6039    |> line(end = [0, 10])
6040    |> line(end = [-10, 0])
6041    |> close()
6042  solid = extrude(profile, length = 10)
6043  return fillet(solid, tags = [edge], radius = 1)
6044}
6045"#;
6046
6047    /// A KCL 3.0 entry point pins the kclVersion for the whole execution: an
6048    /// imported module that declares no kclVersion (1.0 under the legacy
6049    /// lookup) observes KCL 3.0 both in its module-level code and in its
6050    /// functions, wherever they are called from. An import declaring a
6051    /// different version is rejected instead; see
6052    /// [`imported_module_kcl_version_must_match_v3_entry_point`].
6053    #[tokio::test(flavor = "multi_thread")]
6054    async fn entry_point_v3_pins_kcl_version_for_imported_modules() {
6055        use kittycad_modeling_cmds::shared::EdgeCutVersion;
6056
6057        let dep = FILLET_AT_MODULE_TOP_LEVEL;
6058        let main = r#"@settings(kclVersion = "3.0-preview")
6059import "dep.kcl" as dep
6060"#;
6061        let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
6062        assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
6063
6064        let dep = FILLET_IN_EXPORTED_FN;
6065        let main = r#"@settings(kclVersion = "3.0-preview")
6066import filletedBox from "dep.kcl"
6067box = filletedBox()
6068"#;
6069        let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
6070        assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
6071    }
6072
6073    const REGION_AT_MODULE_TOP_LEVEL: &str = r#"
6074profile = sketch(on = XY) {
6075  outline = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6076}
6077disc = region(segments = [profile.outline])
6078"#;
6079
6080    const REGION_IN_EXPORTED_FN: &str = r#"
6081export fn disc() {
6082  profile = sketch(on = XY) {
6083    outline = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6084  }
6085  return region(segments = [profile.outline])
6086}
6087"#;
6088
6089    /// Without a KCL 3.0 entry point, the legacy lookup applies unchanged,
6090    /// including its quirk: an imported module's module-level code observes the
6091    /// module's own declared version, but its functions observe the CALLING
6092    /// module's version. Pinned with KCL 1.0 and 2.0, so that the pin does not
6093    /// depend on importing a KCL 3.0 file.
6094    #[tokio::test(flavor = "multi_thread")]
6095    async fn legacy_kcl_version_quirk_applies_without_v3_entry_point() {
6096        use kittycad_modeling_cmds::shared::RegionVersion;
6097
6098        let dep = format!("@settings(kclVersion = 1.0)\n{REGION_AT_MODULE_TOP_LEVEL}");
6099        let main = r#"@settings(kclVersion = 2.0)
6100import "dep.kcl" as dep
6101"#;
6102        let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
6103        assert_eq!(emitted_region_versions_everywhere(&result), vec![RegionVersion::V0]);
6104
6105        let dep = format!("@settings(kclVersion = 1.0)\n{REGION_IN_EXPORTED_FN}");
6106        let main = r#"@settings(kclVersion = 2.0)
6107import disc from "dep.kcl"
6108face = disc()
6109"#;
6110        let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
6111        assert_eq!(emitted_region_versions_everywhere(&result), vec![RegionVersion::V1]);
6112    }
6113
6114    /// Builds a mock-engine context whose project directory holds `modules`
6115    /// in an in-memory file system, with `main.kcl` as the current file so
6116    /// that errors can name the entry point's path. Nothing touches disk, so
6117    /// parallel tests share no state.
6118    fn versioned_modules_context(modules: &[(&str, &str)]) -> ExecutorContext {
6119        let project_dir = crate::TypedPath::new("/zma-kcl-version-mismatch");
6120        // Key each module by the same join that import resolution performs,
6121        // so the lookup matches on every platform.
6122        let files = modules
6123            .iter()
6124            .map(|(name, source)| (project_dir.join(name).to_string(), source.as_bytes().to_vec()))
6125            .collect();
6126        ExecutorContext {
6127            engine: Arc::new(EngineManager::new_mock()),
6128            cache: Cache::default(),
6129            engine_batch: EngineBatchContext::default(),
6130            fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
6131            settings: ExecutorSettings {
6132                current_file: Some(project_dir.join("main.kcl")),
6133                project_directory: Some(project_dir),
6134                ..Default::default()
6135            },
6136            context_type: ContextType::Mock,
6137            execution_callbacks: Default::default(),
6138            executor_kind: machine::ExecutorKind::resolve(),
6139            machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
6140            configure_engine_render: true,
6141        }
6142    }
6143
6144    /// Runs `main` with `modules` (see [`versioned_modules_context`]) the way
6145    /// engine execution does, which runs every imported module eagerly.
6146    async fn run_versioned_modules(main: &str, modules: &[(&str, &str)]) -> Result<(), KclError> {
6147        let ctx = versioned_modules_context(modules);
6148        let program = crate::Program::parse_no_errs(main).unwrap();
6149        let mut exec_state = ExecState::new(&ctx);
6150        let result = ctx.run(&program, &mut exec_state).await;
6151        ctx.close().await;
6152        result.map(|_| ()).map_err(|err| err.error)
6153    }
6154
6155    /// Runs `main` with `modules` (see [`versioned_modules_context`]) through
6156    /// mock execution, which runs imported modules lazily.
6157    async fn run_versioned_modules_mock(main: &str, modules: &[(&str, &str)]) -> Result<(), KclError> {
6158        let ctx = versioned_modules_context(modules);
6159        let program = crate::Program::parse_no_errs(main).unwrap();
6160        let mock_config = MockConfig {
6161            use_prev_memory: false,
6162            ..Default::default()
6163        };
6164        let result = ctx.run_mock_returning_state(&program, &mock_config).await;
6165        ctx.close().await;
6166        result.map(|_| ()).map_err(|err| err.error)
6167    }
6168
6169    const V3_MAIN_IMPORTING_DEP: &str =
6170        "@settings(kclVersion = \"3.0-preview\")\nimport width from \"dep.kcl\"\nx = width\n";
6171    const V3_STABLE_MAIN_IMPORTING_DEP: &str =
6172        "@settings(kclVersion = 3.0)\nimport width from \"dep.kcl\"\nx = width\n";
6173
6174    fn dep_declaring(version: &str) -> String {
6175        format!("@settings(kclVersion = {version})\nexport width = 10\n")
6176    }
6177
6178    /// The error a mismatch between `main.kcl` and `dep.kcl` must produce,
6179    /// with `expected_dep_version` as the imported file's version.
6180    #[track_caller]
6181    fn assert_kcl_version_mismatch(error: &KclError, expected_dep_version: &str) {
6182        assert!(matches!(error, KclError::Semantic { .. }), "{error:#?}");
6183        assert_eq!(
6184            error.message(),
6185            format!(
6186                "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."
6187            )
6188        );
6189    }
6190
6191    /// A KCL 3.0 entry point rejects an imported file that declares a
6192    /// different kclVersion. The error names both files and both versions,
6193    /// points at the declaration in the imported file, and carries the import
6194    /// site in the entry point as its outer frame.
6195    #[tokio::test(flavor = "multi_thread")]
6196    async fn imported_module_kcl_version_must_match_v3_entry_point() {
6197        for dep_version in ["2.0", "1.0", "3.0"] {
6198            let dep = dep_declaring(dep_version);
6199            let error = run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
6200                .await
6201                .expect_err("mismatched kclVersion should be rejected");
6202            assert_kcl_version_mismatch(&error, dep_version);
6203
6204            let ranges = error.source_ranges();
6205            assert_eq!(ranges.len(), 2, "{ranges:#?}");
6206            // The `kclVersion = ...` setting in dep.kcl.
6207            assert!(!ranges[0].module_id().is_top_level());
6208            let declaration = format!("kclVersion = {dep_version}");
6209            let start = dep.find(&declaration).unwrap();
6210            assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + declaration.len()));
6211            // The import statement in main.kcl.
6212            assert!(ranges[1].module_id().is_top_level());
6213            let import_stmt = "import width from \"dep.kcl\"";
6214            let start = V3_MAIN_IMPORTING_DEP.find(import_stmt).unwrap();
6215            assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
6216            assert_eq!(
6217                error
6218                    .backtrace()
6219                    .iter()
6220                    .map(|frame| frame.fn_name.as_deref())
6221                    .collect::<Vec<_>>(),
6222                [Some("import dep.kcl"), None]
6223            );
6224        }
6225    }
6226
6227    /// Imported files that declare no kclVersion are unaffected: they run
6228    /// under the entry point's version, as before.
6229    #[tokio::test(flavor = "multi_thread")]
6230    async fn imported_module_without_kcl_version_is_allowed_under_v3_entry_point() {
6231        for main in [V3_MAIN_IMPORTING_DEP, V3_STABLE_MAIN_IMPORTING_DEP] {
6232            for dep in [
6233                "export width = 10\n",
6234                "@settings(defaultLengthUnit = in)\nexport width = 10\n",
6235            ] {
6236                run_versioned_modules(main, &[("dep.kcl", dep)])
6237                    .await
6238                    .unwrap_or_else(|err| panic!("main={main:?}, dep={dep:?}: {err:#?}"));
6239            }
6240        }
6241    }
6242
6243    /// Declaring the entry point's own version, in any accepted spelling, is
6244    /// a match.
6245    #[tokio::test(flavor = "multi_thread")]
6246    async fn imported_module_matching_v3_kcl_version_is_allowed() {
6247        for (main, dep_versions) in [
6248            (
6249                V3_MAIN_IMPORTING_DEP,
6250                ["\"3.0-preview\"", "\"3-preview\"", "\"3.0.0-preview\""],
6251            ),
6252            (V3_STABLE_MAIN_IMPORTING_DEP, ["3.0", "3", "\"3.0.0\""]),
6253        ] {
6254            for dep_version in dep_versions {
6255                let dep = dep_declaring(dep_version);
6256                run_versioned_modules(main, &[("dep.kcl", &dep)])
6257                    .await
6258                    .unwrap_or_else(|err| panic!("main={main:?}, dep={dep_version}: {err:#?}"));
6259            }
6260        }
6261    }
6262
6263    #[tokio::test(flavor = "multi_thread")]
6264    async fn stable_v3_entry_point_rejects_other_import_versions() {
6265        for dep_version in ["1.0", "2.0", "\"3.0-preview\""] {
6266            let dep = dep_declaring(dep_version);
6267            let error = run_versioned_modules(V3_STABLE_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
6268                .await
6269                .expect_err("mismatched kclVersion should be rejected");
6270            assert!(matches!(error, KclError::Semantic { .. }), "{error:#?}");
6271            let expected_dep_version = dep_version.trim_matches('"');
6272            assert_eq!(
6273                error.message(),
6274                format!(
6275                    "Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` declares kclVersion 3.0, 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."
6276                )
6277            );
6278        }
6279    }
6280
6281    /// Without a KCL 3.0 entry point, KCL 1.0 and 2.0 may still be mixed, as
6282    /// before.
6283    #[tokio::test(flavor = "multi_thread")]
6284    async fn pre_v3_kcl_versions_may_be_mixed_without_v3_entry_point() {
6285        for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
6286            for dep_version in ["1.0", "2.0"] {
6287                let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6288                let dep = dep_declaring(dep_version);
6289                run_versioned_modules(&main, &[("dep.kcl", &dep)])
6290                    .await
6291                    .unwrap_or_else(|err| panic!("main={main_header:?} dep={dep_version}: {err:#?}"));
6292            }
6293        }
6294    }
6295
6296    /// The error an entry point `main.kcl` that declares no kclVersion must
6297    /// produce when it imports a 3.0-preview `dep.kcl`.
6298    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.";
6299
6300    /// Without a KCL 3.0 entry point, an imported file may not declare KCL
6301    /// 3.0: the legacy per-module lookup would otherwise apply KCL 3.0
6302    /// semantics to that file alone. The error names both files, says what
6303    /// the entry point declares (if anything), points at the declaration in
6304    /// the imported file, and carries the import site in the entry point as
6305    /// its outer frame.
6306    #[tokio::test(flavor = "multi_thread")]
6307    async fn v3_import_requires_v3_entry_point() {
6308        for (main_header, entry_point_declares, fix) in [
6309            (
6310                "",
6311                "does not declare a kclVersion",
6312                "Declare the same kclVersion in the entry point, or update the setting in the imported file.",
6313            ),
6314            (
6315                "@settings(kclVersion = 1.0)\n",
6316                "declares kclVersion 1.0",
6317                "Update the kclVersion setting in one of these files to match the other.",
6318            ),
6319            (
6320                "@settings(kclVersion = 2.0)\n",
6321                "declares kclVersion 2.0",
6322                "Update the kclVersion setting in one of these files to match the other.",
6323            ),
6324        ] {
6325            let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6326            let dep = dep_declaring("\"3.0-preview\"");
6327            let error = run_versioned_modules(&main, &[("dep.kcl", &dep)])
6328                .await
6329                .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
6330            assert!(matches!(error, KclError::Semantic { .. }), "{error:#?}");
6331            assert_eq!(
6332                error.message(),
6333                format!(
6334                    "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}"
6335                ),
6336                "main={main_header:?}"
6337            );
6338
6339            let ranges = error.source_ranges();
6340            assert_eq!(ranges.len(), 2, "{ranges:#?}");
6341            // The `kclVersion = ...` setting in dep.kcl.
6342            assert!(!ranges[0].module_id().is_top_level());
6343            let declaration = "kclVersion = \"3.0-preview\"";
6344            let start = dep.find(declaration).unwrap();
6345            assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + declaration.len()));
6346            // The import statement in main.kcl.
6347            assert!(ranges[1].module_id().is_top_level());
6348            let import_stmt = "import width from \"dep.kcl\"";
6349            let start = main.find(import_stmt).unwrap();
6350            assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
6351            assert_eq!(
6352                error
6353                    .backtrace()
6354                    .iter()
6355                    .map(|frame| frame.fn_name.as_deref())
6356                    .collect::<Vec<_>>(),
6357                [Some("import dep.kcl"), None]
6358            );
6359        }
6360    }
6361
6362    /// Every accepted spelling of 3.0-preview is rejected, and the message
6363    /// uses the canonical spelling.
6364    #[tokio::test(flavor = "multi_thread")]
6365    async fn v3_import_spellings_are_all_rejected_without_v3_entry_point() {
6366        let main = "import width from \"dep.kcl\"\nx = width\n";
6367        for dep_version in ["\"3-preview\"", "\"3.0.0-preview\""] {
6368            let dep = dep_declaring(dep_version);
6369            let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
6370                .await
6371                .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
6372            assert_eq!(
6373                error.message(),
6374                V3_DEP_UNDER_UNDECLARED_ENTRY_POINT,
6375                "dep={dep_version}"
6376            );
6377        }
6378    }
6379
6380    /// Mock execution runs a whole-module import's body only when the module
6381    /// is referenced, so the check also runs at the import site.
6382    #[tokio::test(flavor = "multi_thread")]
6383    async fn unreferenced_v3_whole_module_import_is_checked_in_mock_execution() {
6384        let main = "import \"dep.kcl\" as dep\nx = 1\n";
6385        let dep = dep_declaring("\"3.0-preview\"");
6386        let error = run_versioned_modules_mock(main, &[("dep.kcl", &dep)])
6387            .await
6388            .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected in mock execution");
6389        assert_eq!(error.message(), V3_DEP_UNDER_UNDECLARED_ENTRY_POINT);
6390        let ranges = error.source_ranges();
6391        assert_eq!(ranges.len(), 2, "{ranges:#?}");
6392        assert!(!ranges[0].module_id().is_top_level());
6393        assert!(ranges[1].module_id().is_top_level());
6394        let import_stmt = "import \"dep.kcl\" as dep";
6395        let start = main.find(import_stmt).unwrap();
6396        assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
6397
6398        // Engine execution runs the module eagerly and rejects it too.
6399        let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
6400            .await
6401            .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected in engine execution");
6402        assert_eq!(error.message(), V3_DEP_UNDER_UNDECLARED_ENTRY_POINT);
6403    }
6404
6405    /// The check covers transitive imports. The message names the entry point
6406    /// and the mismatched file; the file in between appears in the import
6407    /// backtrace.
6408    #[tokio::test(flavor = "multi_thread")]
6409    async fn transitive_v3_import_requires_v3_entry_point() {
6410        let main = "import doubled from \"a.kcl\"\nx = doubled\n";
6411        let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
6412        let b = dep_declaring("\"3.0-preview\"");
6413        let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", &b)])
6414            .await
6415            .expect_err("a transitive KCL 3.0 import without a KCL 3.0 entry point should be rejected");
6416        assert_eq!(
6417            error.message(),
6418            "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."
6419        );
6420        assert_eq!(
6421            error
6422                .backtrace()
6423                .iter()
6424                .map(|frame| frame.fn_name.as_deref())
6425                .collect::<Vec<_>>(),
6426            [Some("import b.kcl"), Some("import a.kcl"), None]
6427        );
6428    }
6429
6430    /// When execution was started without a file path, the message still
6431    /// describes the entry point, just without a path.
6432    #[tokio::test(flavor = "multi_thread")]
6433    async fn v3_import_without_v3_entry_point_or_entry_point_path() {
6434        let main = "import width from \"dep.kcl\"\nx = width\n";
6435        let dep = dep_declaring("\"3.0-preview\"");
6436        let error = execute_with_modules(main, &[("dep.kcl", &dep)])
6437            .await
6438            .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
6439        let message = error.message();
6440        assert!(
6441            message.starts_with(
6442                "Mixing KCL versions in a single program is not allowed. The entry point does not declare a kclVersion, but the imported file `"
6443            ),
6444            "{message}"
6445        );
6446        assert!(
6447            message.ends_with(
6448                "dep.kcl` declares kclVersion 3.0-preview. Declare the same kclVersion in the entry point, or update the setting in the imported file."
6449            ),
6450            "{message}"
6451        );
6452    }
6453
6454    /// The check covers transitive imports. The message names the entry point
6455    /// and the mismatched file; the file in between appears in the import
6456    /// backtrace.
6457    #[tokio::test(flavor = "multi_thread")]
6458    async fn transitive_import_kcl_version_mismatch_names_entry_point_and_mismatched_file() {
6459        let main = "@settings(kclVersion = \"3.0-preview\")\nimport doubled from \"a.kcl\"\nx = doubled\n";
6460        let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
6461        let b = dep_declaring("2.0");
6462        let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", &b)])
6463            .await
6464            .expect_err("mismatched kclVersion in a transitive import should be rejected");
6465        assert_eq!(
6466            error.message(),
6467            "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."
6468        );
6469        assert_eq!(
6470            error
6471                .backtrace()
6472                .iter()
6473                .map(|frame| frame.fn_name.as_deref())
6474                .collect::<Vec<_>>(),
6475            [Some("import b.kcl"), Some("import a.kcl"), None]
6476        );
6477        let ranges = error.source_ranges();
6478        assert_eq!(ranges.len(), 3, "{ranges:#?}");
6479        assert!(!ranges[0].module_id().is_top_level());
6480        assert!(!ranges[1].module_id().is_top_level());
6481        assert!(ranges[2].module_id().is_top_level());
6482    }
6483
6484    /// Mock execution runs a whole-module import's body only when the module
6485    /// is referenced, so the check also runs at the import site.
6486    #[tokio::test(flavor = "multi_thread")]
6487    async fn unreferenced_whole_module_import_kcl_version_is_checked_in_mock_execution() {
6488        let main = "@settings(kclVersion = \"3.0-preview\")\nimport \"dep.kcl\" as dep\nx = 1\n";
6489        let dep = dep_declaring("2.0");
6490        let error = run_versioned_modules_mock(main, &[("dep.kcl", &dep)])
6491            .await
6492            .expect_err("mismatched kclVersion should be rejected in mock execution");
6493        assert_kcl_version_mismatch(&error, "2.0");
6494        let ranges = error.source_ranges();
6495        assert_eq!(ranges.len(), 2, "{ranges:#?}");
6496        assert!(!ranges[0].module_id().is_top_level());
6497        assert!(ranges[1].module_id().is_top_level());
6498        let import_stmt = "import \"dep.kcl\" as dep";
6499        let start = main.find(import_stmt).unwrap();
6500        assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
6501
6502        // Engine execution runs the module eagerly and rejects it too.
6503        let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
6504            .await
6505            .expect_err("mismatched kclVersion should be rejected in engine execution");
6506        assert_kcl_version_mismatch(&error, "2.0");
6507
6508        // An undeclared version is fine in mock execution as well.
6509        run_versioned_modules_mock(main, &[("dep.kcl", "export width = 10\n")])
6510            .await
6511            .unwrap();
6512    }
6513
6514    #[tokio::test(flavor = "multi_thread")]
6515    async fn imported_use_keyword_follows_entry_point_version() {
6516        let dep = "use = 10\nexport width = use\n";
6517        for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
6518            let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6519            run_versioned_modules(&main, &[("dep.kcl", dep)])
6520                .await
6521                .unwrap_or_else(|error| panic!("main={main_header:?}: {error:#?}"));
6522        }
6523
6524        let main_v1 = "@settings(kclVersion = 1.0)\nimport width from \"dep.kcl\"\nx = width\n";
6525        let dep_v2 = format!("@settings(kclVersion = 2.0)\n{dep}");
6526        run_versioned_modules(main_v1, &[("dep.kcl", &dep_v2)]).await.unwrap();
6527
6528        for (main, version) in [
6529            (V3_MAIN_IMPORTING_DEP, "\"3.0-preview\""),
6530            (V3_STABLE_MAIN_IMPORTING_DEP, "3.0"),
6531        ] {
6532            for dep in [dep.to_owned(), format!("@settings(kclVersion = {version})\n{dep}")] {
6533                let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
6534                    .await
6535                    .expect_err("V3 imports must reject a use identifier");
6536                assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6537                assert_eq!(error.message(), crate::parsing::RESERVED_USE_MESSAGE);
6538                let ranges = error.source_ranges();
6539                assert_eq!(ranges.len(), 2, "{ranges:#?}");
6540                let start = dep.find("use =").unwrap();
6541                assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + 3));
6542                assert!(!ranges[0].module_id().is_top_level());
6543                assert!(ranges[1].module_id().is_top_level());
6544            }
6545        }
6546    }
6547
6548    #[tokio::test(flavor = "multi_thread")]
6549    async fn imported_never_type_follows_entry_point_version() {
6550        let dep = "export fn stop(): never {}\n";
6551        for version in ["1.0", "2.0"] {
6552            let main = format!("@settings(kclVersion = {version})\nimport stop from \"dep.kcl\"\nx = 1\n");
6553            for run_mock in [false, true] {
6554                let error = if run_mock {
6555                    run_versioned_modules_mock(&main, &[("dep.kcl", dep)]).await
6556                } else {
6557                    run_versioned_modules(&main, &[("dep.kcl", dep)]).await
6558                }
6559                .expect_err("older KCL versions must reject imported `never` types");
6560                assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6561                assert_eq!(
6562                    error.message(),
6563                    format!("The `never` type requires KCL 3.0-preview, but this program uses KCL {version}.")
6564                );
6565                let ranges = error.source_ranges();
6566                assert_eq!(ranges.len(), 2, "{ranges:#?}");
6567                let start = dep.find("never").unwrap();
6568                assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + "never".len()));
6569                assert!(!ranges[0].module_id().is_top_level());
6570                assert!(ranges[1].module_id().is_top_level());
6571            }
6572        }
6573
6574        let main_v3 = "@settings(kclVersion = \"3.0-preview\")\nimport stop from \"dep.kcl\"\nx = 1\n";
6575        run_versioned_modules(main_v3, &[("dep.kcl", dep)]).await.unwrap();
6576
6577        let dep_v2 = format!("@settings(kclVersion = 2.0)\n{dep}");
6578        let error = run_versioned_modules(main_v3, &[("dep.kcl", &dep_v2)])
6579            .await
6580            .expect_err("version mismatch must precede `never` validation");
6581        assert_kcl_version_mismatch(&error, "2.0");
6582        assert_eq!(
6583            error
6584                .backtrace()
6585                .iter()
6586                .map(|frame| frame.fn_name.as_deref())
6587                .collect::<Vec<_>>(),
6588            [Some("import dep.kcl"), None]
6589        );
6590
6591        let main_v2 = "@settings(kclVersion = 2.0)\nimport stop from \"dep.kcl\"\nx = 1\n";
6592        let dep_v3 = format!("@settings(kclVersion = \"3.0-preview\")\n{dep}");
6593        let error = run_versioned_modules(main_v2, &[("dep.kcl", &dep_v3)])
6594            .await
6595            .expect_err("version mismatch must precede `never` validation");
6596        assert!(
6597            error
6598                .message()
6599                .starts_with("Mixing KCL versions in a single program is not allowed.")
6600        );
6601
6602        // Mock execution must validate a whole-module import even when it is unused.
6603        let unused_import = "@settings(kclVersion = 2.0)\nimport \"dep.kcl\" as dep\nx = 1\n";
6604        let error = run_versioned_modules_mock(unused_import, &[("dep.kcl", dep)])
6605            .await
6606            .expect_err("an unused imported module must not bypass `never` validation");
6607        assert_eq!(
6608            error.message(),
6609            "The `never` type requires KCL 3.0-preview, but this program uses KCL 2.0."
6610        );
6611    }
6612
6613    #[tokio::test(flavor = "multi_thread")]
6614    async fn never_type_resolution_rejects_an_unvalidated_v2_ast() {
6615        let source = "@settings(kclVersion = 2.0)\nfn stop(): never {}\n";
6616        let (kcl_version, ast, _) = crate::parsing::parse_str_syntax(source, ModuleId::default()).unwrap();
6617        let program = crate::Program {
6618            kcl_version,
6619            ast,
6620            original_file_contents: source.to_owned(),
6621        };
6622        let ctx = versioned_modules_context(&[]);
6623        let mut exec_state = ExecState::new(&ctx);
6624        let error = ctx.run(&program, &mut exec_state).await.unwrap_err().error;
6625        ctx.close().await;
6626
6627        assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6628        assert_eq!(
6629            error.message(),
6630            "The `never` type requires KCL 3.0-preview, but this program uses KCL 2.0."
6631        );
6632    }
6633
6634    #[tokio::test(flavor = "multi_thread")]
6635    async fn import_universe_uses_entry_point_version_in_a_fresh_state() {
6636        let main = "@settings(kclVersion = \"3.0-preview\")\nimport stop from \"dep.kcl\"\nx = 1\n";
6637        let ctx = versioned_modules_context(&[("dep.kcl", "export fn stop(): never {}\n")]);
6638        let program = crate::Program::parse_no_errs(main).unwrap();
6639        let mut exec_state = ExecState::new(&ctx);
6640
6641        ctx.get_universe(&program, &mut exec_state).await.unwrap();
6642        assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V3Preview));
6643        ctx.close().await;
6644    }
6645
6646    #[tokio::test(flavor = "multi_thread")]
6647    async fn imported_enum_identifier_follows_entry_point_version() {
6648        let dep = "enum = 10\nexport width = enum\n";
6649        for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
6650            let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6651            run_versioned_modules(&main, &[("dep.kcl", dep)])
6652                .await
6653                .unwrap_or_else(|error| panic!("main={main_header:?}: {error:#?}"));
6654        }
6655
6656        for run_mock in [false, true] {
6657            let error = if run_mock {
6658                run_versioned_modules_mock(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep)]).await
6659            } else {
6660                run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep)]).await
6661            }
6662            .expect_err("V3 imports must reject an enum identifier");
6663            assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6664            assert_eq!(error.message(), crate::parsing::RESERVED_ENUM_MESSAGE);
6665            let ranges = error.source_ranges();
6666            assert_eq!(ranges.len(), 2, "{ranges:#?}");
6667            assert_eq!((ranges[0].start(), ranges[0].end()), (0, "enum".len()));
6668            assert!(!ranges[0].module_id().is_top_level());
6669            assert!(ranges[1].module_id().is_top_level());
6670        }
6671
6672        let dep_v2 = format!("@settings(kclVersion = 2.0)\n{dep}");
6673        let error = run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep_v2)])
6674            .await
6675            .expect_err("version mismatch must precede enum validation");
6676        assert_kcl_version_mismatch(&error, "2.0");
6677    }
6678
6679    #[tokio::test(flavor = "multi_thread")]
6680    async fn imported_import_modifiers_follow_entry_point_version() {
6681        for word in ["template", "lazy", "component"] {
6682            let dep = format!("import {word} from \"nested.kcl\"\nexport width = 10\n");
6683            let nested = format!("export {word} = 1\n");
6684            for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
6685                let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6686                run_versioned_modules(&main, &[("dep.kcl", &dep), ("nested.kcl", &nested)])
6687                    .await
6688                    .unwrap_or_else(|error| panic!("main={main_header:?}, word={word}: {error:#?}"));
6689            }
6690
6691            for run_mock in [false, true] {
6692                let error = if run_mock {
6693                    run_versioned_modules_mock(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep), ("nested.kcl", &nested)])
6694                        .await
6695                } else {
6696                    run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep), ("nested.kcl", &nested)]).await
6697                }
6698                .expect_err("V3 imports must reject a reserved import modifier");
6699                assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6700                assert_eq!(
6701                    error.message(),
6702                    format!(
6703                        "`{word}` is reserved as an import modifier in KCL 3.0 and cannot be the first imported item"
6704                    )
6705                );
6706                let ranges = error.source_ranges();
6707                assert_eq!(ranges.len(), 2, "{ranges:#?}");
6708                let start = dep.find(word).unwrap();
6709                assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + word.len()));
6710                assert!(!ranges[0].module_id().is_top_level());
6711                assert!(ranges[1].module_id().is_top_level());
6712            }
6713        }
6714    }
6715
6716    #[tokio::test(flavor = "multi_thread")]
6717    async fn import_version_mismatch_precedes_import_modifier_error() {
6718        let dep = "@settings(kclVersion = 2.0)\nimport lazy from \"nested.kcl\"\nexport width = 10\n";
6719        let nested = "export lazy = 1\n";
6720        for run_mock in [false, true] {
6721            let error = if run_mock {
6722                run_versioned_modules_mock(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep), ("nested.kcl", nested)]).await
6723            } else {
6724                run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep), ("nested.kcl", nested)]).await
6725            }
6726            .expect_err("version mismatch must precede import modifier validation");
6727            assert_kcl_version_mismatch(&error, "2.0");
6728        }
6729    }
6730
6731    #[tokio::test(flavor = "multi_thread")]
6732    async fn imported_use_function_name_is_allowed_under_v3() {
6733        let dep = "fn use() { return 10 }\nexport width = use()\n";
6734        run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep)])
6735            .await
6736            .unwrap();
6737    }
6738
6739    #[tokio::test(flavor = "multi_thread")]
6740    async fn import_version_mismatch_precedes_use_keyword_error() {
6741        let dep_v2 = "@settings(kclVersion = 2.0)\nuse = 10\nexport width = use\n";
6742        let error = run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep_v2)])
6743            .await
6744            .expect_err("version mismatch must precede the use keyword error");
6745        assert_kcl_version_mismatch(&error, "2.0");
6746
6747        let main_v2 = "@settings(kclVersion = 2.0)\nimport width from \"dep.kcl\"\nx = width\n";
6748        let dep_v3 = "@settings(kclVersion = \"3.0-preview\")\nuse = 10\nexport width = use\n";
6749        let error = run_versioned_modules(main_v2, &[("dep.kcl", dep_v3)])
6750            .await
6751            .expect_err("version mismatch must precede the use keyword error");
6752        assert!(
6753            error
6754                .message()
6755                .starts_with("Mixing KCL versions in a single program is not allowed.")
6756        );
6757    }
6758
6759    #[tokio::test(flavor = "multi_thread")]
6760    async fn unreferenced_whole_module_import_checks_use_keyword_in_mock_execution() {
6761        let main = "@settings(kclVersion = \"3.0-preview\")\nimport \"dep.kcl\" as dep\nx = 1\n";
6762        let dep = "use = 10\n";
6763        let error = run_versioned_modules_mock(main, &[("dep.kcl", dep)])
6764            .await
6765            .expect_err("mock import must reject a use identifier");
6766        assert_eq!(error.message(), crate::parsing::RESERVED_USE_MESSAGE);
6767        let ranges = error.source_ranges();
6768        assert_eq!(ranges.len(), 2, "{ranges:#?}");
6769        assert!(!ranges[0].module_id().is_top_level());
6770        assert!(ranges[1].module_id().is_top_level());
6771
6772        let dep_v2 = "@settings(kclVersion = 2.0)\nuse = 10\n";
6773        let error = run_versioned_modules_mock(main, &[("dep.kcl", dep_v2)])
6774            .await
6775            .expect_err("version mismatch must precede the use keyword error in mock execution");
6776        assert_kcl_version_mismatch(&error, "2.0");
6777    }
6778
6779    #[tokio::test(flavor = "multi_thread")]
6780    async fn transitive_import_checks_use_keyword_under_v3_entry_point() {
6781        let main = "@settings(kclVersion = \"3.0-preview\")\nimport doubled from \"a.kcl\"\nx = doubled\n";
6782        let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
6783        let b = "use = 10\nexport width = use\n";
6784        let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", b)])
6785            .await
6786            .expect_err("transitive import must reject a use identifier");
6787        assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6788        assert_eq!(error.message(), crate::parsing::RESERVED_USE_MESSAGE);
6789        let ranges = error.source_ranges();
6790        assert_eq!(ranges.len(), 3, "{ranges:#?}");
6791        assert!(!ranges[0].module_id().is_top_level());
6792        assert!(!ranges[1].module_id().is_top_level());
6793        assert!(ranges[2].module_id().is_top_level());
6794        assert_eq!(
6795            error
6796                .backtrace()
6797                .iter()
6798                .map(|frame| frame.fn_name.as_deref())
6799                .collect::<Vec<_>>(),
6800            [Some("import b.kcl"), Some("import a.kcl"), None]
6801        );
6802    }
6803
6804    /// Standard library modules declare kclVersion 1.0 but are exempt: they
6805    /// always run under the entry point's version, and the user cannot edit
6806    /// them. The prelude is imported implicitly; `std::turns` is imported
6807    /// explicitly here.
6808    #[tokio::test(flavor = "multi_thread")]
6809    async fn std_modules_are_exempt_from_kcl_version_matching() {
6810        let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport QUARTER_TURN from \"std::turns\"\nx = QUARTER_TURN\n";
6811        run_versioned_modules(main, &[]).await.unwrap();
6812        run_versioned_modules_mock(main, &[]).await.unwrap();
6813    }
6814
6815    /// When execution was started without a file path, the message still
6816    /// describes the entry point, just without a path.
6817    #[tokio::test(flavor = "multi_thread")]
6818    async fn kcl_version_mismatch_without_entry_point_path() {
6819        let dep = dep_declaring("2.0");
6820        let error = execute_with_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
6821            .await
6822            .expect_err("mismatched kclVersion should be rejected");
6823        let message = error.message();
6824        assert!(
6825            message.starts_with(
6826                "Mixing KCL versions in a single program is not allowed. The entry point declares kclVersion 3.0-preview, but the imported file `"
6827            ),
6828            "{message}"
6829        );
6830        assert!(
6831            message.ends_with(
6832                "dep.kcl` declares kclVersion 2.0. Update the kclVersion setting in one of these files to match the other."
6833            ),
6834            "{message}"
6835        );
6836    }
6837
6838    #[track_caller]
6839    fn variable_f64(result: &ExecTestResults, name: &str) -> f64 {
6840        mem_get_json(result.exec_state.stack(), result.mem_env, name)
6841            .as_f64()
6842            .unwrap()
6843    }
6844
6845    #[tokio::test(flavor = "multi_thread")]
6846    async fn return_terminates_function_early_in_v3() {
6847        let code = r#"@settings(kclVersion = "3.0-preview")
6848fn f() {
6849  return 1
6850  assert(1, isEqualTo = 2, error = "code after return ran")
6851}
6852x = f()
6853"#;
6854        let result = parse_execute(code).await.unwrap();
6855        assert_eq!(variable_f64(&result, "x"), 1.0);
6856    }
6857
6858    #[tokio::test(flavor = "multi_thread")]
6859    async fn second_return_is_unreachable_in_v3() {
6860        let code = r#"@settings(kclVersion = "3.0-preview")
6861fn f() {
6862  return 1
6863  return 2
6864}
6865x = f()
6866"#;
6867        let result = parse_execute(code).await.unwrap();
6868        assert_eq!(variable_f64(&result, "x"), 1.0);
6869    }
6870
6871    #[tokio::test(flavor = "multi_thread")]
6872    async fn return_inside_if_arm_returns_from_function_in_v3() {
6873        let code = r#"@settings(kclVersion = "3.0-preview")
6874fn f(@b) {
6875  dummy = if b {
6876    return 1
6877    0
6878  } else {
6879    0
6880  }
6881  return 2
6882}
6883x = f(true)
6884y = f(false)
6885"#;
6886        let result = parse_execute(code).await.unwrap();
6887        assert_eq!(variable_f64(&result, "x"), 1.0);
6888        assert_eq!(variable_f64(&result, "y"), 2.0);
6889    }
6890
6891    #[tokio::test(flavor = "multi_thread")]
6892    async fn return_inside_nested_if_returns_from_function_in_v3() {
6893        let code = r#"@settings(kclVersion = "3.0-preview")
6894fn f(@a, b) {
6895  dummy = if a {
6896    inner = if b {
6897      return 10
6898      0
6899    } else {
6900      1
6901    }
6902    inner + 1
6903  } else {
6904    2
6905  }
6906  return dummy * 100
6907}
6908x = f(true, b = true)
6909y = f(true, b = false)
6910z = f(false, b = false)
6911"#;
6912        let result = parse_execute(code).await.unwrap();
6913        assert_eq!(variable_f64(&result, "x"), 10.0);
6914        assert_eq!(variable_f64(&result, "y"), 200.0);
6915        assert_eq!(variable_f64(&result, "z"), 200.0);
6916    }
6917
6918    #[tokio::test(flavor = "multi_thread")]
6919    async fn return_inside_closure_returns_only_from_closure_in_v3() {
6920        let code = r#"@settings(kclVersion = "3.0-preview")
6921fn outer() {
6922  inner = fn() {
6923    return 5
6924    assert(1, isEqualTo = 2, error = "code after inner return ran")
6925  }
6926  v = inner()
6927  return v + 1
6928}
6929x = outer()
6930"#;
6931        let result = parse_execute(code).await.unwrap();
6932        assert_eq!(variable_f64(&result, "x"), 6.0);
6933    }
6934
6935    #[tokio::test(flavor = "multi_thread")]
6936    async fn return_type_coercion_applies_to_early_return_in_v3() {
6937        let code = r#"@settings(kclVersion = "3.0-preview")
6938fn f(): number(mm) {
6939  return 1
6940  assert(1, isEqualTo = 2, error = "code after return ran")
6941}
6942x = f()
6943"#;
6944        let result = parse_execute(code).await.unwrap();
6945        assert_eq!(variable_f64(&result, "x"), 1.0);
6946
6947        // A coercion failure surfaces as an error (on the machine, this
6948        // exercises unwind_return's error path).
6949        let code = r#"@settings(kclVersion = "3.0-preview")
6950fn f(): number(mm) {
6951  return "nope"
6952}
6953x = f()
6954"#;
6955        let err = parse_execute(code).await.expect_err("coercion failure should error");
6956        assert!(err.message().contains("type"), "unexpected message: {}", err.message());
6957    }
6958
6959    #[tokio::test(flavor = "multi_thread")]
6960    async fn return_at_top_level_errors() {
6961        // A return statement at the top level is rejected in all versions.
6962        for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
6963            let code = format!("{header}return 1\n");
6964            assert_eq!(
6965                parse_execute(&code).await.expect_err("should error").message(),
6966                "Cannot return from outside a function."
6967            );
6968        }
6969
6970        // Under KCL 3.0, a return escaping a top-level if-arm is also rejected
6971        // (without the setting it is silently ignored; see
6972        // top_level_if_arm_return_ignored_without_v3).
6973        let code = r#"@settings(kclVersion = "3.0-preview")
6974x = if true {
6975  return 1
6976  0
6977} else {
6978  0
6979}
6980"#;
6981        assert_eq!(
6982            parse_execute(code).await.expect_err("should error").message(),
6983            "Cannot return from outside a function."
6984        );
6985    }
6986
6987    #[tokio::test(flavor = "multi_thread")]
6988    async fn exit_inside_function_still_exits_program_in_v3() {
6989        let code = r#"@settings(kclVersion = "3.0-preview")
6990fn f() {
6991  exit()
6992  return 1
6993}
6994x = f()
6995assert(1, isEqualTo = 2, error = "code after exit ran")
6996"#;
6997        parse_execute(code).await.unwrap();
6998    }
6999
7000    #[tokio::test(flavor = "multi_thread")]
7001    async fn return_inside_sketch_block_terminates_function_in_v3() {
7002        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7003fn f() {
7004  sketch(on = XY) {
7005    l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7006    return 42
7007  }
7008  return 0
7009}
7010x = f()
7011"#;
7012        let result = parse_execute(code).await.unwrap();
7013        assert_eq!(variable_f64(&result, "x"), 42.0);
7014    }
7015
7016    #[tokio::test(flavor = "multi_thread")]
7017    async fn return_inside_sketch_block_ignored_without_v3() {
7018        // Pins the pre-KCL-3.0 behavior: `__return` binds in the sketch block's
7019        // child environment and is lost when it pops.
7020        let code = r#"@settings(experimentalFeatures = allow)
7021fn f() {
7022  sketch(on = XY) {
7023    l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7024    return 42
7025  }
7026  return 0
7027}
7028x = f()
7029"#;
7030        let result = parse_execute(code).await.unwrap();
7031        assert_eq!(variable_f64(&result, "x"), 0.0);
7032    }
7033
7034    #[tokio::test(flavor = "multi_thread")]
7035    async fn code_after_return_still_runs_without_v3() {
7036        let code = r#"fn f() {
7037  return 1
7038  assert(1, isEqualTo = 2, error = "ran past return")
7039}
7040x = f()
7041"#;
7042        let err = parse_execute(code).await.expect_err("should error");
7043        assert!(
7044            err.message().contains("ran past return"),
7045            "unexpected message: {}",
7046            err.message()
7047        );
7048    }
7049
7050    #[tokio::test(flavor = "multi_thread")]
7051    async fn multiple_returns_error_without_v3() {
7052        let code = r#"fn f() {
7053  return 1
7054  return 2
7055}
7056x = f()
7057"#;
7058        assert_eq!(
7059            parse_execute(code).await.expect_err("should error").message(),
7060            "Multiple returns from a single function."
7061        );
7062    }
7063
7064    #[tokio::test(flavor = "multi_thread")]
7065    async fn if_arm_return_plus_function_return_errors_without_v3() {
7066        // Pins the pre-KCL-3.0 behavior: the if-arm's `return` writes
7067        // `__return` into the function's environment, so the function-level
7068        // `return` is a second return.
7069        let code = r#"fn f() {
7070  dummy = if true {
7071    return 1
7072    0
7073  } else {
7074    0
7075  }
7076  return 2
7077}
7078x = f()
7079"#;
7080        assert_eq!(
7081            parse_execute(code).await.expect_err("should error").message(),
7082            "Multiple returns from a single function."
7083        );
7084    }
7085
7086    #[tokio::test(flavor = "multi_thread")]
7087    async fn top_level_if_arm_return_ignored_without_v3() {
7088        // Pins the pre-KCL-3.0 behavior: the return silently binds
7089        // `__return` in the root environment and the arm yields its trailing
7090        // expression.
7091        let code = r#"x = if true {
7092  return 1
7093  0
7094} else {
7095  0
7096}
7097"#;
7098        let result = parse_execute(code).await.unwrap();
7099        assert_eq!(variable_f64(&result, "x"), 0.0);
7100        assert_eq!(variable_f64(&result, memory::RETURN_NAME), 1.0);
7101    }
7102
7103    /// Early return is gated on the entry point's kclVersion, not the
7104    /// defining module's. A module cannot opt into KCL 3.0 on its own.
7105    #[tokio::test(flavor = "multi_thread")]
7106    async fn return_semantics_gated_on_entry_point_not_module() {
7107        // A 2.0 entry point importing a module that declares KCL 3.0 is
7108        // rejected as a version mismatch before the module runs, so its
7109        // function never gets to observe either return semantics.
7110        let dep = r#"@settings(kclVersion = "3.0-preview")
7111export fn f() {
7112  return 1
7113  assert(1, isEqualTo = 2, error = "ran past return")
7114}
7115"#;
7116        let main = r#"@settings(kclVersion = 2.0)
7117import f from "dep.kcl"
7118x = f()
7119"#;
7120        let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
7121        assert!(
7122            err.message()
7123                .starts_with("Mixing KCL versions in a single program is not allowed."),
7124            "unexpected message: {}",
7125            err.message()
7126        );
7127
7128        // A KCL 3.0 entry point applies early return everywhere, including
7129        // inside an imported module that declares no kclVersion (1.0 under the
7130        // legacy lookup). Declaring 2.0 there is rejected as a version mismatch
7131        // instead.
7132        let dep = r#"export fn f() {
7133  return 1
7134  assert(1, isEqualTo = 2, error = "ran past return")
7135}
7136"#;
7137        let main = r#"@settings(kclVersion = "3.0-preview")
7138import f from "dep.kcl"
7139x = f()
7140"#;
7141        let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
7142        assert_eq!(variable_f64(&result, "x"), 1.0);
7143    }
7144
7145    /// Early return inside a callback driven by a builtin terminates only
7146    /// that callback invocation; the builtin keeps iterating. On the machine
7147    /// executor, map/reduce callbacks run behind a Callback-completion call
7148    /// boundary, so this exercises unwind_return's resume-the-builtin path,
7149    /// unlike a directly called function.
7150    #[tokio::test(flavor = "multi_thread")]
7151    async fn return_inside_map_and_reduce_callbacks_in_v3() {
7152        let code = r#"@settings(kclVersion = "3.0-preview")
7153doubled = map([1, 2, 3], f = fn(@i) {
7154  return i * 2
7155  assert(1, isEqualTo = 2, error = "code after return ran in the map callback")
7156})
7157assert(doubled[0], isEqualTo = 2, error = "map result 0")
7158assert(doubled[1], isEqualTo = 4, error = "map result 1")
7159assert(doubled[2], isEqualTo = 6, error = "map result 2")
7160
7161total = reduce([1, 2, 3], initial = 0, f = fn(@i, accum) {
7162  return accum + i
7163  assert(1, isEqualTo = 2, error = "code after return ran in the reduce callback")
7164})
7165assert(total, isEqualTo = 6, error = "reduce total")
7166"#;
7167        let result = parse_execute(code).await.unwrap();
7168        assert_eq!(variable_f64(&result, "total"), 6.0);
7169    }
7170
7171    /// unwind_return must decrement the machine call depth like a normal
7172    /// call completion; otherwise sequential early-return calls would
7173    /// accumulate depth until the runaway guard trips. The recursive
7174    /// executor doesn't use the counter, so the bound is trivially true
7175    /// there.
7176    #[tokio::test(flavor = "multi_thread")]
7177    async fn early_returns_do_not_leak_machine_call_depth() {
7178        let code = r#"@settings(kclVersion = "3.0-preview")
7179fn one() {
7180  return 1
7181  assert(1, isEqualTo = 2, error = "code after return ran")
7182}
7183total = reduce([1..100], initial = 0, f = fn(@i, accum) {
7184  return accum + one()
7185})
7186assert(total, isEqualTo = 100, error = "each call returns 1")
7187"#;
7188        let result = parse_execute(code).await.unwrap();
7189        // Real nesting here is a few levels (reduce callback then one()).
7190        // If early returns leaked a level per call, the 100 sequential
7191        // calls would push the high water toward 100.
7192        let high_water = result.exec_state.global.machine_depth_high_water;
7193        assert!(high_water < 10, "high water: {high_water}");
7194    }
7195
7196    /// A return escaping to the top level of an imported module is rejected
7197    /// under a KCL 3.0 entry point. The entry module's version governs, so
7198    /// the imported module, which declares no kclVersion (1.0 under the
7199    /// legacy lookup), doesn't opt back out. Declaring 2.0 there would be
7200    /// rejected as a version mismatch instead. (Without a KCL 3.0 entry point
7201    /// the return is silently ignored; see
7202    /// top_level_if_arm_return_ignored_without_v3.)
7203    #[tokio::test(flavor = "multi_thread")]
7204    async fn top_level_if_arm_return_in_imported_module_errors_in_v3() {
7205        let dep = r#"x = if true {
7206  return 1
7207  0
7208} else {
7209  0
7210}
7211export y = x
7212"#;
7213        let main = r#"@settings(kclVersion = "3.0-preview")
7214import y from "dep.kcl"
7215z = y
7216"#;
7217        let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
7218        assert!(
7219            err.message().contains("Cannot return from outside a function."),
7220            "unexpected message: {}",
7221            err.message()
7222        );
7223    }
7224
7225    /// exit() in a return's argument still exits the whole program: the
7226    /// Exit control flow from evaluating the argument takes precedence over
7227    /// turning the statement into an early return. If it were mistakenly
7228    /// treated as the function's return value, execution would continue
7229    /// after the call and hit the failing assert.
7230    #[tokio::test(flavor = "multi_thread")]
7231    async fn return_of_exit_still_exits_program_in_v3() {
7232        let code = r#"@settings(kclVersion = "3.0-preview")
7233fn f() {
7234  return exit()
7235}
7236x = f()
7237assert(1, isEqualTo = 2, error = "code after exit ran")
7238"#;
7239        parse_execute(code).await.unwrap();
7240    }
7241
7242    #[tokio::test(flavor = "multi_thread")]
7243    async fn if_arm_bindings_do_not_leak_in_v3() {
7244        let code = r#"@settings(kclVersion = "3.0-preview")
7245x = if true {
7246  y = 1
7247  y
7248} else {
7249  0
7250}
7251z = y
7252"#;
7253        let err = parse_execute(code).await.expect_err("should error");
7254        assert!(
7255            err.message().contains("`y` is not defined"),
7256            "unexpected message: {}",
7257            err.message()
7258        );
7259    }
7260
7261    #[tokio::test(flavor = "multi_thread")]
7262    async fn if_arm_bindings_leak_without_v3() {
7263        // Pins the pre-KCL-3.0 behavior: arm bodies share the enclosing
7264        // environment, so arm bindings are visible after the if.
7265        for header in ["", "@settings(kclVersion = 2.0)\n"] {
7266            let code = format!(
7267                r#"{header}x = if true {{
7268  y = 1
7269  y
7270}} else {{
7271  0
7272}}
7273z = y
7274"#
7275            );
7276            let result = parse_execute(&code).await.unwrap();
7277            assert_eq!(variable_f64(&result, "z"), 1.0);
7278        }
7279    }
7280
7281    #[tokio::test(flavor = "multi_thread")]
7282    async fn if_arm_shadowing_allowed_in_v3() {
7283        let code = r#"@settings(kclVersion = "3.0-preview")
7284y = 1
7285x = if true {
7286  y = 2
7287  y + 10
7288} else {
7289  0
7290}
7291"#;
7292        let result = parse_execute(code).await.unwrap();
7293        assert_eq!(variable_f64(&result, "x"), 12.0);
7294        assert_eq!(variable_f64(&result, "y"), 1.0);
7295    }
7296
7297    #[tokio::test(flavor = "multi_thread")]
7298    async fn if_arm_shadowing_still_errors_without_v3() {
7299        // Pins the pre-KCL-3.0 behavior: the arm shares the enclosing
7300        // environment, so redeclaring an outer name is an error.
7301        for header in ["", "@settings(kclVersion = 2.0)\n"] {
7302            let code = format!(
7303                r#"{header}y = 1
7304x = if true {{
7305  y = 2
7306  y
7307}} else {{
7308  0
7309}}
7310"#
7311            );
7312            let err = parse_execute(&code).await.expect_err("should error");
7313            assert!(
7314                err.message().contains("Cannot redefine `y`"),
7315                "unexpected message: {}",
7316                err.message()
7317            );
7318        }
7319    }
7320
7321    #[tokio::test(flavor = "multi_thread")]
7322    async fn if_arm_closure_escape_in_v3() {
7323        // A closure declared in an arm captures arm-locals and stays valid
7324        // after the arm's scope is popped.
7325        let code = r#"@settings(kclVersion = "3.0-preview")
7326n = 1
7327f = if true {
7328  m = 41
7329  g = fn() {
7330    return m + n
7331  }
7332  g
7333} else {
7334  g = fn() {
7335    return 0
7336  }
7337  g
7338}
7339x = f()
7340"#;
7341        let result = parse_execute(code).await.unwrap();
7342        assert_eq!(variable_f64(&result, "x"), 42.0);
7343    }
7344
7345    #[tokio::test(flavor = "multi_thread")]
7346    async fn recursive_if_arm_closure_keeps_enclosing_function_frame_alive_in_v3() {
7347        // A named recursive closure takes a different snapshot path from an
7348        // anonymous closure. Escaping through an arm must retain both the arm
7349        // and its enclosing call frame.
7350        let code = r#"@settings(kclVersion = "3.0-preview")
7351fn makeCounter() {
7352  outer = 40
7353  selected = if true {
7354    inner = 2
7355    fn count(@n) {
7356      return if n == 0 {
7357        outer + inner
7358      } else {
7359        count(n - 1) + 1
7360      }
7361    }
7362    count
7363  } else {
7364    fn fallback(@n) {
7365      return n
7366    }
7367    fallback
7368  }
7369  return selected
7370}
7371counter = makeCounter()
7372x = counter(3)
7373"#;
7374        let result = parse_execute(code).await.unwrap();
7375        assert_eq!(variable_f64(&result, "x"), 45.0);
7376    }
7377
7378    #[tokio::test(flavor = "multi_thread")]
7379    async fn return_inside_scoped_if_arm_in_v3() {
7380        // Early return from inside a scoped arm pops the arm environment on
7381        // the way out.
7382        let code = r#"@settings(kclVersion = "3.0-preview")
7383fn f(@b) {
7384  local = if b {
7385    w = 1
7386    return w + 9
7387    0
7388  } else {
7389    0
7390  }
7391  return local
7392}
7393x = f(true)
7394y = f(false)
7395"#;
7396        let result = parse_execute(code).await.unwrap();
7397        assert_eq!(variable_f64(&result, "x"), 10.0);
7398        assert_eq!(variable_f64(&result, "y"), 0.0);
7399    }
7400
7401    #[tokio::test(flavor = "multi_thread")]
7402    async fn else_if_and_nested_if_scoping_in_v3() {
7403        let code = r#"@settings(kclVersion = "3.0-preview")
7404x = if false {
7405  0
7406} else if true {
7407  a = 1
7408  b = if true {
7409    c = 2
7410    a + c
7411  } else {
7412    0
7413  }
7414  a + b
7415} else {
7416  0
7417}
7418"#;
7419        let result = parse_execute(code).await.unwrap();
7420        assert_eq!(variable_f64(&result, "x"), 4.0);
7421
7422        // A nested arm's binding is not visible in the enclosing arm.
7423        let code = r#"@settings(kclVersion = "3.0-preview")
7424x = if true {
7425  b = if true {
7426    c = 2
7427    c
7428  } else {
7429    0
7430  }
7431  b + c
7432} else {
7433  0
7434}
7435"#;
7436        let err = parse_execute(code).await.expect_err("should error");
7437        assert!(
7438            err.message().contains("`c` is not defined"),
7439            "unexpected message: {}",
7440            err.message()
7441        );
7442    }
7443
7444    /// Else-if and final-else arms are isolated exactly like then-arms:
7445    /// their bindings are invisible after the if, and they may shadow outer
7446    /// bindings without changing them. Pinned per arm kind so a refactor of
7447    /// the shared arm dispatch can't silently drop one.
7448    #[tokio::test(flavor = "multi_thread")]
7449    async fn else_if_and_final_else_arms_are_isolated_in_v3() {
7450        // A taken else-if arm's binding doesn't leak.
7451        let code = r#"@settings(kclVersion = "3.0-preview")
7452x = if false {
7453  0
7454} else if true {
7455  y = 1
7456  y
7457} else {
7458  0
7459}
7460z = y
7461"#;
7462        let err = parse_execute(code).await.expect_err("should error");
7463        assert!(
7464            err.message().contains("`y` is not defined"),
7465            "unexpected message: {}",
7466            err.message()
7467        );
7468
7469        // A taken final-else arm's binding doesn't leak.
7470        let code = r#"@settings(kclVersion = "3.0-preview")
7471x = if false {
7472  0
7473} else if false {
7474  0
7475} else {
7476  y = 1
7477  y
7478}
7479z = y
7480"#;
7481        let err = parse_execute(code).await.expect_err("should error");
7482        assert!(
7483            err.message().contains("`y` is not defined"),
7484            "unexpected message: {}",
7485            err.message()
7486        );
7487
7488        // A taken else-if arm can shadow an outer binding without changing it.
7489        let code = r#"@settings(kclVersion = "3.0-preview")
7490outer = 1
7491x = if false {
7492  0
7493} else if true {
7494  outer = 2
7495  outer + 10
7496} else {
7497  0
7498}
7499"#;
7500        let result = parse_execute(code).await.unwrap();
7501        assert_eq!(variable_f64(&result, "x"), 12.0);
7502        assert_eq!(variable_f64(&result, "outer"), 1.0);
7503
7504        // Same from the final-else arm.
7505        let code = r#"@settings(kclVersion = "3.0-preview")
7506outer = 1
7507x = if false {
7508  0
7509} else if false {
7510  0
7511} else {
7512  outer = 2
7513  outer + 10
7514}
7515"#;
7516        let result = parse_execute(code).await.unwrap();
7517        assert_eq!(variable_f64(&result, "x"), 12.0);
7518        assert_eq!(variable_f64(&result, "outer"), 1.0);
7519    }
7520
7521    /// Pins the pre-KCL-3.0 behavior for else-if and final-else arms: their
7522    /// bindings leak into the enclosing environment, and shadowing an outer
7523    /// name is a redefinition error, matching then-arms.
7524    #[tokio::test(flavor = "multi_thread")]
7525    async fn else_if_and_final_else_arm_bindings_leak_without_v3() {
7526        for header in ["", "@settings(kclVersion = 2.0)\n"] {
7527            let code = format!(
7528                r#"{header}x = if false {{
7529  0
7530}} else if true {{
7531  y = 1
7532  y
7533}} else {{
7534  0
7535}}
7536z = y
7537"#
7538            );
7539            let result = parse_execute(&code).await.unwrap();
7540            assert_eq!(variable_f64(&result, "z"), 1.0, "code={code}");
7541
7542            let code = format!(
7543                r#"{header}x = if false {{
7544  0
7545}} else if false {{
7546  0
7547}} else {{
7548  y = 1
7549  y
7550}}
7551z = y
7552"#
7553            );
7554            let result = parse_execute(&code).await.unwrap();
7555            assert_eq!(variable_f64(&result, "z"), 1.0, "code={code}");
7556
7557            let code = format!(
7558                r#"{header}outer = 1
7559x = if false {{
7560  0
7561}} else if true {{
7562  outer = 2
7563  outer
7564}} else {{
7565  0
7566}}
7567"#
7568            );
7569            let err = parse_execute(&code).await.expect_err("should error");
7570            assert!(
7571                err.message().contains("Cannot redefine `outer`"),
7572                "unexpected message: {}",
7573                err.message()
7574            );
7575        }
7576    }
7577
7578    #[tokio::test(flavor = "multi_thread")]
7579    async fn error_inside_if_arm_unwinds_balanced_in_v3() {
7580        // The user's error surfaces (not an internal environment-imbalance
7581        // error), on both executors.
7582        let code = r#"@settings(kclVersion = "3.0-preview")
7583fn f() {
7584  dummy = if true {
7585    assert(1, isEqualTo = 2, error = "boom")
7586    0
7587  } else {
7588    0
7589  }
7590  return dummy
7591}
7592x = f()
7593"#;
7594        let err = parse_execute(code).await.expect_err("should error");
7595        assert!(err.message().contains("boom"), "unexpected message: {}", err.message());
7596    }
7597
7598    #[tokio::test(flavor = "multi_thread")]
7599    async fn exit_inside_scoped_if_arm_in_v3() {
7600        let code = r#"@settings(kclVersion = "3.0-preview")
7601fn f() {
7602  dummy = if true {
7603    exit()
7604    0
7605  } else {
7606    0
7607  }
7608  return dummy
7609}
7610x = f()
7611assert(1, isEqualTo = 2, error = "code after exit ran")
7612"#;
7613        parse_execute(code).await.unwrap();
7614    }
7615
7616    /// If-arm scoping is gated on the entry point's kclVersion, not the
7617    /// defining module's. A module cannot opt into KCL 3.0 on its own.
7618    #[tokio::test(flavor = "multi_thread")]
7619    async fn if_arm_scoping_gated_on_entry_point_not_module() {
7620        // A 2.0 entry point importing a module that declares KCL 3.0 is
7621        // rejected as a version mismatch before the module runs, so its arms
7622        // never get to leak or not leak.
7623        let dep = r#"@settings(kclVersion = "3.0-preview")
7624ignored = if true {
7625  leaked = 1
7626  leaked
7627} else {
7628  0
7629}
7630export leakCheck = leaked
7631"#;
7632        let main = r#"@settings(kclVersion = 2.0)
7633import leakCheck from "dep.kcl"
7634x = leakCheck
7635"#;
7636        let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
7637        assert!(
7638            err.message()
7639                .starts_with("Mixing KCL versions in a single program is not allowed."),
7640            "unexpected message: {}",
7641            err.message()
7642        );
7643
7644        // A KCL 3.0 entry point applies arm scoping everywhere, including
7645        // inside an imported module that declares no kclVersion (1.0 under the
7646        // legacy lookup). Declaring 2.0 there is rejected as a version mismatch
7647        // instead.
7648        let dep = r#"ignored = if true {
7649  arm = 1
7650  arm
7651} else {
7652  0
7653}
7654export fn leakCheck() {
7655  return arm
7656}
7657"#;
7658        let main = r#"@settings(kclVersion = "3.0-preview")
7659import leakCheck from "dep.kcl"
7660x = leakCheck()
7661"#;
7662        let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
7663        assert!(
7664            err.message().contains("`arm` is not defined"),
7665            "unexpected message: {}",
7666            err.message()
7667        );
7668    }
7669
7670    /// Unwinding out of a sketch block nested inside a scoped if-arm must
7671    /// run the sketch cleanup and then pop the arm's scope environment, in
7672    /// that order, on all three unwind paths: error, exit(), and early
7673    /// return.
7674    #[tokio::test(flavor = "multi_thread")]
7675    async fn unwind_through_sketch_block_inside_scoped_if_arm_in_v3() {
7676        // Error: the user's error surfaces, not an internal
7677        // environment-imbalance error.
7678        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7679fn f() {
7680  dummy = if true {
7681    s = sketch(on = XY) {
7682      l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7683      q = notDefinedAnywhere
7684    }
7685    0
7686  } else {
7687    0
7688  }
7689  return dummy
7690}
7691x = f()
7692"#;
7693        let err = parse_execute(code).await.unwrap_err();
7694        assert!(
7695            err.message().contains("`notDefinedAnywhere` is not defined"),
7696            "unexpected message: {}",
7697            err.message()
7698        );
7699
7700        // exit() terminates the program; nothing after it runs.
7701        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7702fn f() {
7703  dummy = if true {
7704    s = sketch(on = XY) {
7705      l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7706      e = exit()
7707    }
7708    0
7709  } else {
7710    0
7711  }
7712  return dummy
7713}
7714x = f()
7715assert(1, isEqualTo = 2, error = "code after exit ran")
7716"#;
7717        parse_execute(code).await.unwrap();
7718
7719        // Early return terminates the enclosing function with its value.
7720        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7721fn g() {
7722  dummy = if true {
7723    s = sketch(on = XY) {
7724      l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7725      return 42
7726    }
7727    0
7728  } else {
7729    0
7730  }
7731  return 0
7732}
7733y = g()
7734"#;
7735        let result = parse_execute(code).await.unwrap();
7736        assert_eq!(variable_f64(&result, "y"), 42.0);
7737    }
7738
7739    /// A tag declared inside an if-arm is bound like any arm-local: usable
7740    /// within its arm, and under KCL 3.0 not visible after the if. Without
7741    /// KCL 3.0 it leaks like other arm bindings.
7742    #[tokio::test(flavor = "multi_thread")]
7743    async fn tag_declared_inside_if_arm_is_arm_local_in_v3() {
7744        let arm_body = r#"p = if true {
7745  profile = startSketchOn(XY)
7746    |> startProfile(at = [0, 0])
7747    |> line(end = [10, 0], tag = $edge)
7748    |> line(end = [0, 10])
7749    |> line(end = [-10, 0])
7750    |> close()
7751  inArmLen = segLen(edge)
7752  assert(inArmLen, isEqualTo = 10, error = "tag is usable within its arm")
7753  profile
7754} else {
7755  startSketchOn(XY)
7756    |> startProfile(at = [0, 0])
7757    |> line(end = [5, 0])
7758    |> line(end = [0, 5])
7759    |> line(end = [-5, 0])
7760    |> close()
7761}
7762len = segLen(edge)
7763"#;
7764
7765        let code = format!("@settings(kclVersion = \"3.0-preview\")\n{arm_body}");
7766        let err = parse_execute(&code).await.unwrap_err();
7767        assert!(
7768            err.message().contains("`edge` is not defined"),
7769            "unexpected message: {}",
7770            err.message()
7771        );
7772
7773        // Pins the pre-KCL-3.0 behavior: the tag leaks out of the arm.
7774        let result = parse_execute(arm_body).await.unwrap();
7775        assert_eq!(variable_f64(&result, "len"), 10.0);
7776    }
7777
7778    /// Repeated calls to a function whose body evaluates an if-expression must
7779    /// not accumulate retained call frames when nothing escapes the arms: the
7780    /// arm's scope environment defers pinning its parent until it is itself
7781    /// referenced. Before deferred pinning, each of the 100 calls below
7782    /// permanently retained its frame.
7783    #[tokio::test(flavor = "multi_thread")]
7784    async fn if_arm_scopes_do_not_retain_function_frames_in_v3() {
7785        let code = r#"@settings(kclVersion = "3.0-preview")
7786fn pick(@i) {
7787  r = if i > 50 {
7788    a = i * 2
7789    a
7790  } else {
7791    b = i + 1
7792    b
7793  }
7794  return r
7795}
7796results = map([1..100], f = fn(@i) { return pick(i) })
7797assert(results[0], isEqualTo = 2, error = "pick(1) = 2")
7798assert(results[99], isEqualTo = 200, error = "pick(100) = 200")
7799"#;
7800        let result = parse_execute(code).await.unwrap();
7801        // Long-lived environments (std prelude modules, the root env, ...) are
7802        // a small constant independent of the call count.
7803        let retained = result.exec_state.stack().memory.envs_with_bindings();
7804        assert!(retained < 20, "retained environments: {retained}");
7805    }
7806
7807    /// An if-expression used as a pipe element gets arm scoping without
7808    /// disturbing the ambient pipe value: the arm's result feeds the next
7809    /// element's `%` (the parser doesn't accept `%` anywhere inside the if
7810    /// element itself), and arm-locals don't leak.
7811    #[tokio::test(flavor = "multi_thread")]
7812    async fn if_arm_scoping_inside_pipe_in_v3() {
7813        let code = r#"@settings(kclVersion = "3.0-preview")
7814cond = true
7815result = 5
7816  |> if cond {
7817    a = 20
7818    a
7819  } else {
7820    0
7821  }
7822  |> max([%, 1])
7823"#;
7824        let result = parse_execute(code).await.unwrap();
7825        // The then-arm's 20 must flow through the pipe into max's `%`. If
7826        // the arm's scope push/pop corrupted the ambient pipe value, this
7827        // would not be 20.
7828        assert_eq!(variable_f64(&result, "result"), 20.0);
7829
7830        // Arm-locals of a pipe element are invisible after the pipe.
7831        let code = r#"@settings(kclVersion = "3.0-preview")
7832cond = true
7833result = 5
7834  |> if cond {
7835    a = 20
7836    a
7837  } else {
7838    0
7839  }
7840leaked = a
7841"#;
7842        let err = parse_execute(code).await.unwrap_err();
7843        assert!(
7844            err.message().contains("`a` is not defined"),
7845            "unexpected message: {}",
7846            err.message()
7847        );
7848    }
7849
7850    #[tokio::test(flavor = "multi_thread")]
7851    async fn member_expression_evaluates_object_before_property_in_v3() {
7852        // Both operands are undefined, so the error names whichever one is
7853        // evaluated first. KCL 3.0 evaluates in source order: `a` before `b`.
7854        let code = r#"@settings(kclVersion = "3.0-preview")
7855x = a[b]
7856"#;
7857        let err = parse_execute(code).await.expect_err("should error");
7858        assert_eq!(err.message(), "`a` is not defined");
7859    }
7860
7861    #[tokio::test(flavor = "multi_thread")]
7862    async fn member_expression_evaluates_property_before_object_without_v3() {
7863        // Pre-KCL-3.0 order, preserved for compatibility: the computed
7864        // property is evaluated before the object.
7865        let code = r#"@settings(kclVersion = 2.0)
7866x = a[b]
7867"#;
7868        let err = parse_execute(code).await.expect_err("should error");
7869        assert_eq!(err.message(), "`b` is not defined");
7870    }
7871
7872    #[tokio::test(flavor = "multi_thread")]
7873    async fn member_expression_undefined_object_with_static_property_in_v3() {
7874        let code = r#"@settings(kclVersion = "3.0-preview")
7875x = a.b
7876"#;
7877        let err = parse_execute(code).await.expect_err("should error");
7878        assert_eq!(err.message(), "`a` is not defined");
7879    }
7880
7881    #[tokio::test(flavor = "multi_thread")]
7882    async fn member_expression_values_in_v3() {
7883        // The source-order path handles computed, non-computed, chained, and
7884        // call-result access.
7885        let code = r#"@settings(kclVersion = "3.0-preview")
7886fn xs() {
7887  return [10, 20, 30]
7888}
7889fn one() {
7890  return 1
7891}
7892obj = { inner = { xs = xs() } }
7893objs = [obj, obj]
7894a = obj.inner.xs[one()]
7895b = xs()[one() + 1]
7896c = objs[0].inner.xs[0]
7897"#;
7898        let result = parse_execute(code).await.unwrap();
7899        assert_eq!(variable_f64(&result, "a"), 20.0);
7900        assert_eq!(variable_f64(&result, "b"), 30.0);
7901        assert_eq!(variable_f64(&result, "c"), 10.0);
7902    }
7903
7904    #[tokio::test(flavor = "multi_thread")]
7905    async fn exit_inside_member_expression_in_v3() {
7906        // exit() propagates out of either half of a member expression and
7907        // terminates the program before the assert runs.
7908        for code in [
7909            r#"@settings(kclVersion = "3.0-preview")
7910x = exit()[0]
7911assert(1, isEqualTo = 2, error = "code after exit ran")
7912"#,
7913            r#"@settings(kclVersion = "3.0-preview")
7914arr = [1]
7915x = arr[exit()]
7916assert(1, isEqualTo = 2, error = "code after exit ran")
7917"#,
7918        ] {
7919            parse_execute(code).await.unwrap();
7920        }
7921    }
7922
7923    #[tokio::test(flavor = "multi_thread")]
7924    async fn experimental_parameter() {
7925        let code = r#"
7926fn inc(@x, @(experimental = true) amount? = 1) {
7927  return x + amount
7928}
7929
7930answer = inc(5, amount = 2)
7931"#;
7932        let result = parse_execute(code).await.unwrap();
7933        let issues = result.exec_state.issues();
7934        assert_eq!(issues.len(), 1);
7935        assert_eq!(issues[0].severity, Severity::Error);
7936        let msg = &issues[0].message;
7937        assert!(msg.contains("experimental"), "found {msg}");
7938
7939        // If the parameter isn't used, there's no warning.
7940        let code = r#"
7941fn inc(@x, @(experimental = true) amount? = 1) {
7942  return x + amount
7943}
7944
7945answer = inc(5)
7946"#;
7947        let result = parse_execute(code).await.unwrap();
7948        let issues = result.exec_state.issues();
7949        assert!(issues.is_empty(), "issues={issues:#?}");
7950    }
7951
7952    #[tokio::test(flavor = "multi_thread")]
7953    async fn experimental_scalar_fixed_constraint() {
7954        let code_left = r#"@settings(experimentalFeatures = warn)
7955sketch(on = XY) {
7956  point1 = point(at = [var 0mm, var 0mm])
7957  point1.at[0] == 1mm
7958}
7959"#;
7960        // It's symmetric. Flipping the binary operator has the same behavior.
7961        let code_right = r#"@settings(experimentalFeatures = warn)
7962sketch(on = XY) {
7963  point1 = point(at = [var 0mm, var 0mm])
7964  1mm == point1.at[0]
7965}
7966"#;
7967
7968        for code in [code_left, code_right] {
7969            let result = parse_execute(code).await.unwrap();
7970            let issues = result.exec_state.issues();
7971            let Some(error) = issues
7972                .iter()
7973                .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
7974            else {
7975                panic!("found {issues:#?}");
7976            };
7977            assert_eq!(error.severity, Severity::Warning);
7978        }
7979    }
7980
7981    // START Mock Execution tests
7982    // Ideally, we would do this as part of all sim tests and delete these one-off tests.
7983
7984    #[tokio::test(flavor = "multi_thread")]
7985    async fn test_tangent_line_arc_executes_with_mock_engine() {
7986        let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
7987        parse_execute(&code).await.unwrap();
7988    }
7989
7990    #[tokio::test(flavor = "multi_thread")]
7991    async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
7992        let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
7993        parse_execute(&code).await.unwrap();
7994    }
7995
7996    #[tokio::test(flavor = "multi_thread")]
7997    async fn test_tangent_line_circle_executes_with_mock_engine() {
7998        let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
7999        parse_execute(&code).await.unwrap();
8000    }
8001
8002    #[tokio::test(flavor = "multi_thread")]
8003    async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
8004        let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
8005        parse_execute(&code).await.unwrap();
8006    }
8007
8008    #[tokio::test(flavor = "multi_thread")]
8009    async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
8010        let code = r#"startX = 2
8011
8012baseSketch = sketch(on = XY) {
8013  yoyo = line(start = [startX, 0], end = [7, 6])
8014  line2 = line(start = [7, 6], end = [7, 12])
8015  hi = line(start = [7, 12], end = [startX, 0])
8016}
8017
8018baseRegion = region(point = [5.5, 6], sketch = baseSketch)
8019myExtrude = extrude(
8020  baseRegion,
8021  length = 5,
8022  tagEnd = $endCap,
8023  tagStart = $startCap,
8024)
8025yodawg = getCommonEdge(faces = [
8026  baseRegion.tags.hi,
8027  baseRegion.tags.yoyo
8028])
8029
8030cutSketch = sketch(on = YZ) {
8031  myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
8032  myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
8033  line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
8034}
8035
8036cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
8037extrude001 = extrude(cutRegion, length = 5)
8038solid001 = subtract(myExtrude, tools = extrude001)
8039
8040yoyo = getOppositeEdge(baseRegion.tags.hi)
8041fillet(solid001, radius = 0.1, tags = yoyo)
8042"#;
8043
8044        parse_execute(code).await.unwrap();
8045    }
8046
8047    // END Mock Execution tests
8048
8049    // Sketch constraint report tests
8050
8051    async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
8052        let program = crate::Program::parse_no_errs(kcl).unwrap();
8053        let ctx = ExecutorContext::new_geometry_only_with_version(program.language_version().unwrap())
8054            .await
8055            .unwrap();
8056        let mut exec_state = ExecState::new(&ctx);
8057        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
8058        let outcome = exec_state
8059            .into_exec_outcome(env_ref, &ctx)
8060            .await
8061            .expect("constraint report test outcome should collect variables");
8062        let report = outcome.sketch_constraint_report();
8063        ctx.close().await;
8064        report
8065    }
8066
8067    #[tokio::test(flavor = "multi_thread")]
8068    async fn warn_when_sketch_is_over_constrained() {
8069        let code = r#"
8070sketch001 = sketch(on = XY) {
8071  line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
8072  fixed([line1.start, ORIGIN])
8073  fixed([line1.start, [20, 20]])
8074}
8075"#;
8076        let result = parse_execute(code).await.unwrap();
8077        let issues = result.exec_state.issues();
8078        let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
8079            panic!("expected over-constrained warning; found {issues:#?}");
8080        };
8081        assert_eq!(warning.severity, Severity::Warning);
8082    }
8083
8084    #[tokio::test(flavor = "multi_thread")]
8085    async fn over_constrained_warning_identifies_signed_vertical_distance_direction() {
8086        let code = r#"
8087sketch001 = sketch(on = XY) {
8088  line1 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
8089  fixed([line1.start, [0mm, 10mm]])
8090  fixed([line1.end, ORIGIN])
8091  verticalDistance([line1.start, line1.end]) == 10mm
8092}
8093"#;
8094        let result = parse_execute(code).await.unwrap();
8095        let issues = result.exec_state.issues();
8096        let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
8097            panic!("expected over-constrained warning; found {issues:#?}");
8098        };
8099        assert!(
8100            warning.message.contains(
8101                "Unsatisfied signed verticalDistance constraint: a positive right-hand side requires the second point to be above the first"
8102            ),
8103            "expected signed-direction diagnostic; found {warning:#?}"
8104        );
8105    }
8106
8107    #[tokio::test(flavor = "multi_thread")]
8108    async fn no_warning_when_sketch_is_not_over_constrained() {
8109        // Under-constrained sketch should not emit the over-constrained warning.
8110        let code = r#"
8111sketch001 = sketch(on = XY) {
8112  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8113}
8114"#;
8115        let result = parse_execute(code).await.unwrap();
8116        let issues = result.exec_state.issues();
8117        assert!(
8118            !issues.iter().any(|issue| issue.message.contains("over-constrained")),
8119            "did not expect over-constrained warning; found {issues:#?}"
8120        );
8121    }
8122
8123    #[tokio::test(flavor = "multi_thread")]
8124    async fn test_constraint_report_fully_constrained() {
8125        // All points are fully constrained via equality constraints.
8126        let kcl = r#"
8127@settings(experimentalFeatures = allow)
8128
8129sketch(on = YZ) {
8130  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
8131  line1.start.at[0] == 2
8132  line1.start.at[1] == 8
8133  line1.end.at[0] == 5
8134  line1.end.at[1] == 7
8135}
8136"#;
8137        let report = run_constraint_report(kcl).await;
8138        assert_eq!(report.fully_constrained.len(), 1);
8139        assert_eq!(report.under_constrained.len(), 0);
8140        assert_eq!(report.over_constrained.len(), 0);
8141        assert_eq!(report.errors.len(), 0);
8142        assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
8143    }
8144
8145    #[tokio::test(flavor = "multi_thread")]
8146    async fn test_constraint_report_under_constrained() {
8147        // No constraints at all — all points are free.
8148        let kcl = r#"
8149sketch(on = YZ) {
8150  line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
8151}
8152"#;
8153        let report = run_constraint_report(kcl).await;
8154        assert_eq!(report.fully_constrained.len(), 0);
8155        assert_eq!(report.under_constrained.len(), 1);
8156        assert_eq!(report.over_constrained.len(), 0);
8157        assert_eq!(report.errors.len(), 0);
8158        assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
8159        assert!(report.under_constrained[0].free_count > 0);
8160    }
8161
8162    #[tokio::test(flavor = "multi_thread")]
8163    async fn test_constraint_report_over_constrained() {
8164        // Conflicting distance constraints on the same pair of points.
8165        let kcl = r#"
8166@settings(experimentalFeatures = allow)
8167
8168sketch(on = YZ) {
8169  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
8170  line1.start.at[0] == 2
8171  line1.start.at[1] == 8
8172  line1.end.at[0] == 5
8173  line1.end.at[1] == 7
8174  distance([line1.start, line1.end]) == 100mm
8175}
8176"#;
8177        let report = run_constraint_report(kcl).await;
8178        assert_eq!(report.over_constrained.len(), 1);
8179        assert_eq!(report.errors.len(), 0);
8180        assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
8181        assert!(report.over_constrained[0].conflict_count > 0);
8182    }
8183
8184    #[tokio::test(flavor = "multi_thread")]
8185    async fn test_constraint_report_multiple_sketches() {
8186        // Two sketches: one fully constrained, one under-constrained.
8187        let kcl = r#"
8188@settings(experimentalFeatures = allow)
8189
8190s1 = sketch(on = YZ) {
8191  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
8192  line1.start.at[0] == 2
8193  line1.start.at[1] == 8
8194  line1.end.at[0] == 5
8195  line1.end.at[1] == 7
8196}
8197
8198s2 = sketch(on = XZ) {
8199  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8200}
8201"#;
8202        let report = run_constraint_report(kcl).await;
8203        assert_eq!(
8204            report.fully_constrained.len()
8205                + report.under_constrained.len()
8206                + report.over_constrained.len()
8207                + report.errors.len(),
8208            2,
8209            "Expected 2 sketches total"
8210        );
8211        assert_eq!(report.fully_constrained.len(), 1);
8212        assert_eq!(report.under_constrained.len(), 1);
8213    }
8214
8215    #[tokio::test(flavor = "multi_thread")]
8216    async fn test_constraint_report_reports_sketch_names() {
8217        // One file holding a fully constrained, an under-constrained, and an
8218        // over-constrained sketch. Every entry carries the name of the
8219        // variable its sketch was assigned to, so a caller can say which
8220        // sketch needs correcting.
8221        let kcl = r#"
8222@settings(experimentalFeatures = allow)
8223
8224fixedSketch = sketch(on = YZ) {
8225  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
8226  line1.start.at[0] == 2
8227  line1.start.at[1] == 8
8228  line1.end.at[0] == 5
8229  line1.end.at[1] == 7
8230}
8231
8232looseSketch = sketch(on = XZ) {
8233  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8234}
8235
8236conflictSketch = sketch(on = XY) {
8237  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
8238  line1.start.at[0] == 2
8239  line1.start.at[1] == 8
8240  line1.end.at[0] == 5
8241  line1.end.at[1] == 7
8242  distance([line1.start, line1.end]) == 100mm
8243}
8244"#;
8245        let report = run_constraint_report(kcl).await;
8246        assert_eq!(report.errors.len(), 0);
8247        assert_eq!(report.fully_constrained.len(), 1);
8248        assert_eq!(report.under_constrained.len(), 1);
8249        assert_eq!(report.over_constrained.len(), 1);
8250        assert_eq!(report.fully_constrained[0].name, "fixedSketch");
8251        assert_eq!(report.under_constrained[0].name, "looseSketch");
8252        assert_eq!(report.over_constrained[0].name, "conflictSketch");
8253    }
8254
8255    #[tokio::test(flavor = "multi_thread")]
8256    async fn test_constraint_report_name_empty_without_declaration() {
8257        // A sketch written as an expression statement has no enclosing
8258        // variable declaration, so there is no name to report. This pins the
8259        // documented limitation of SketchConstraintStatus::name.
8260        let kcl = r#"
8261sketch(on = YZ) {
8262  line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
8263}
8264"#;
8265        let report = run_constraint_report(kcl).await;
8266        assert_eq!(report.under_constrained.len(), 1);
8267        assert_eq!(report.under_constrained[0].name, "");
8268    }
8269
8270    #[tokio::test(flavor = "multi_thread")]
8271    async fn test_constraint_report_names_repeat_across_calls() {
8272        // Both sketches come from the same declaration inside the function
8273        // body, so both entries carry that declaration's name and the report
8274        // cannot tell them apart. This pins the documented limitation of
8275        // SketchConstraintStatus::name.
8276        let kcl = r#"
8277fn makeSketch() {
8278  inner = sketch(on = XY) {
8279    line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8280  }
8281  return inner
8282}
8283
8284first = makeSketch()
8285second = makeSketch()
8286"#;
8287        let report = run_constraint_report(kcl).await;
8288        assert_eq!(report.under_constrained.len(), 2);
8289        assert_eq!(report.under_constrained[0].name, "inner");
8290        assert_eq!(report.under_constrained[1].name, "inner");
8291    }
8292
8293    #[tokio::test(flavor = "multi_thread")]
8294    async fn user_aliases_and_enums_require_v3_even_with_experimental_opt_in() {
8295        for version in ["1.0", "2.0"] {
8296            for opt_in in ["", ", experimentalFeatures = allow"] {
8297                for (feature, declaration) in [
8298                    ("Type aliases", "type Distance = number(mm)"),
8299                    ("Enum declarations", "type Color { | Red }"),
8300                ] {
8301                    let code = format!("@settings(kclVersion = \"{version}\"{opt_in})\n{declaration}\n");
8302                    assert_eq!(
8303                        parse_execute(&code).await.unwrap_err().message(),
8304                        format!("{feature} require KCL 3.0-preview, but this program uses KCL {version}."),
8305                        "code: {code}"
8306                    );
8307                }
8308            }
8309        }
8310    }
8311
8312    #[tokio::test(flavor = "multi_thread")]
8313    async fn user_aliases_and_enums_execute_in_v3_without_experimental_opt_in() {
8314        for code in [
8315            "@settings(kclVersion = \"3.0-preview\")\ntype Distance = number(mm)\nx = 1mm: Distance\n",
8316            "@settings(kclVersion = \"3.0-preview\")\ntype Color { | Red }\nx = Color::Red\n",
8317        ] {
8318            let result = parse_execute(code).await.unwrap();
8319            assert!(result.exec_state.issues().is_empty(), "code: {code}");
8320        }
8321    }
8322
8323    #[tokio::test(flavor = "multi_thread")]
8324    async fn standard_library_type_aliases_remain_available_before_v3() {
8325        for version in ["1.0", "2.0"] {
8326            let code = format!("@settings(kclVersion = \"{version}\")\nx = 1mm: mm\n");
8327            let result = parse_execute(&code).await.unwrap();
8328            assert!(result.exec_state.issues().is_empty(), "code: {code}");
8329        }
8330    }
8331
8332    #[tokio::test(flavor = "multi_thread")]
8333    async fn enum_declaration_registers_type() {
8334        // Plain and exported declarations both execute. Nothing references the
8335        // enum yet, so this only asserts that declaring one is no longer an
8336        // error; constructor use is exercised separately.
8337        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8338type Color { | Red | Green }
8339"#;
8340        parse_execute(code).await.unwrap();
8341
8342        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8343export type Color { | Red | Green }
8344"#;
8345        parse_execute(code).await.unwrap();
8346
8347        // A zero-variant enum is a valid declaration.
8348        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8349type Empty { | }
8350"#;
8351        parse_execute(code).await.unwrap();
8352    }
8353
8354    #[tokio::test(flavor = "multi_thread")]
8355    async fn enum_declaration_rejects_nested_scope() {
8356        // Identity is (module, declared name), so two same-named declarations in
8357        // one file would collide. The parser and formatter accept this shape, so
8358        // execution is the only thing that can reject it.
8359        //
8360        // The rule is about nesting, not about one kind of block, so all routes
8361        // to `BodyType::Block` are covered here.
8362        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8363        for (case, code) in [
8364            (
8365                "function body",
8366                format!("{allow}fn palette() {{\n  type Color {{ | Red }}\n  return 0\n}}\npalette()\n"),
8367            ),
8368            (
8369                "sketch block",
8370                format!(
8371                    "{allow}sketch(on = XY) {{\n  type Color {{ | Red }}\n  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
8372                ),
8373            ),
8374            (
8375                "if arm",
8376                format!("{allow}x = if true {{\n  type Color {{ | Red }}\n  0\n}} else {{\n  0\n}}\n"),
8377            ),
8378        ] {
8379            assert_eq!(
8380                parse_execute(&code).await.unwrap_err().message(),
8381                "Enum declarations are only supported at the top-level of a file. Move `type Color` to the top-level.",
8382                "case: {case}"
8383            );
8384        }
8385    }
8386
8387    #[tokio::test(flavor = "multi_thread")]
8388    async fn enum_alone_is_restricted_to_top_level() {
8389        // Pins the asymmetry the rule above creates: a type alias may be declared
8390        // in any block, an enum may not. The difference is required by enum
8391        // identity rather than chosen -- two nested aliases shadow each other
8392        // harmlessly, while two nested `type Color` declarations would be one type
8393        // with two variant sets. Tightening aliases to match, or relaxing enums,
8394        // has to break this test first.
8395        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8396        for (case, code) in [
8397            (
8398                "function body",
8399                format!("{allow}fn f() {{\n  type Temperature = number(_)\n  return 0\n}}\nx = f()\n"),
8400            ),
8401            (
8402                "sketch block",
8403                format!(
8404                    "{allow}sketch(on = XY) {{\n  type Temperature = number(_)\n  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
8405                ),
8406            ),
8407        ] {
8408            parse_execute(&code)
8409                .await
8410                .unwrap_or_else(|err| panic!("a type alias should be allowed in a {case}: {}", err.message()));
8411        }
8412    }
8413
8414    #[tokio::test(flavor = "multi_thread")]
8415    async fn enum_declaration_rejects_duplicate() {
8416        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8417type Color { | Red | Green | Red }
8418"#;
8419        assert_eq!(
8420            parse_execute(code).await.unwrap_err().message(),
8421            "Duplicate variant `Red` in enum `Color`."
8422        );
8423    }
8424
8425    /// Runs `main` with `modules` written beside it, so import paths resolve.
8426    async fn execute_with_modules(main: &str, modules: &[(&str, &str)]) -> Result<ExecTestResults, KclError> {
8427        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_enum_clash").unwrap();
8428        for (name, source) in modules {
8429            tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
8430        }
8431
8432        parse_execute_with_project_dir(main, Some(crate::TypedPath(tmpdir.path().into()))).await
8433    }
8434
8435    /// Like [`execute_with_modules`], with hints enabled for user declarations;
8436    /// see [`parse_execute_hinting_all`].
8437    async fn execute_with_modules_hinting_all(
8438        main: &str,
8439        modules: &[(&str, &str)],
8440    ) -> Result<ExecTestResults, KclError> {
8441        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_added_in").unwrap();
8442        for (name, source) in modules {
8443            tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
8444        }
8445
8446        parse_execute_hinting_all(main, Some(crate::TypedPath(tmpdir.path().into()))).await
8447    }
8448
8449    /// Runs `main` with an empty imported module named `m.kcl` in mock
8450    /// execution and returns the recorded compilation issues; the run may
8451    /// end in an error (e.g. from operating on the module's missing return
8452    /// value).
8453    ///
8454    /// The `m.kcl` module lives in an in-memory file system under a
8455    /// synthetic project directory, so parallel tests share no on-disk
8456    /// state and there is nothing to clean up even if the process is
8457    /// killed.
8458    async fn issues_with_empty_module(main: &str) -> Vec<crate::errors::CompilationIssue> {
8459        use futures::FutureExt;
8460
8461        let project_dir = crate::TypedPath::new("/zma-kcl-member-ranges");
8462        // Key the file by the same join that import resolution performs, so
8463        // the lookup matches on every platform.
8464        let files = [(project_dir.join("m.kcl").to_string(), Vec::new())]
8465            .into_iter()
8466            .collect();
8467
8468        let program = crate::Program::parse_no_errs(main).unwrap();
8469        let ctx = ExecutorContext {
8470            engine: Arc::new(EngineManager::new_mock()),
8471            cache: Cache::default(),
8472            engine_batch: EngineBatchContext::default(),
8473            fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
8474            settings: ExecutorSettings {
8475                project_directory: Some(project_dir),
8476                ..Default::default()
8477            },
8478            context_type: ContextType::Mock,
8479            execution_callbacks: Default::default(),
8480            executor_kind: machine::ExecutorKind::resolve(),
8481            machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
8482            configure_engine_render: true,
8483        };
8484        let mut exec_state = ExecState::new(&ctx);
8485        // Close the context even if execution panics, then let the panic
8486        // continue. An Err from the run itself is expected here (operating
8487        // on the module's missing return value) and is deliberately ignored.
8488        let run_result = std::panic::AssertUnwindSafe(ctx.run(&program, &mut exec_state))
8489            .catch_unwind()
8490            .await;
8491        ctx.close().await;
8492        if let Err(panic) = run_result {
8493            std::panic::resume_unwind(panic);
8494        }
8495        exec_state.issues().to_vec()
8496    }
8497
8498    #[tokio::test(flavor = "multi_thread")]
8499    async fn member_object_diagnostics_use_object_range() {
8500        // A diagnostic raised while evaluating a member expression's object
8501        // (here, the imported module's missing-return warning) points at the
8502        // object's own span, not the whole member expression.
8503        // Both member evaluation orders (pre-KCL-3.0 and KCL 3.0) must
8504        // attribute the diagnostic the same way.
8505        for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
8506            let main = format!("{header}import \"m.kcl\" as m\nx = m.field\n");
8507            let issues = issues_with_empty_module(&main).await;
8508            let warning = issues
8509                .iter()
8510                .find(|issue| issue.message.contains("no return value"))
8511                .expect("missing-return warning should be recorded");
8512            let object_start = main.rfind("m.field").unwrap();
8513            assert_eq!(
8514                (warning.source_range.start(), warning.source_range.end()),
8515                (object_start, object_start + 1),
8516                "warning should point at the object's span (header={header:?})"
8517            );
8518        }
8519    }
8520
8521    #[tokio::test(flavor = "multi_thread")]
8522    async fn member_property_diagnostics_use_property_range() {
8523        // Same for the computed property: the warning points at the index
8524        // expression's span inside the brackets.
8525        for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
8526            let main = format!("{header}import \"m.kcl\" as m\narr = [1]\nx = arr[m]\n");
8527            let issues = issues_with_empty_module(&main).await;
8528            let warning = issues
8529                .iter()
8530                .find(|issue| issue.message.contains("no return value"))
8531                .expect("missing-return warning should be recorded");
8532            let prop_start = main.rfind("[m]").unwrap() + 1;
8533            assert_eq!(
8534                (warning.source_range.start(), warning.source_range.end()),
8535                (prop_start, prop_start + 1),
8536                "warning should point at the property's span (header={header:?})"
8537            );
8538        }
8539    }
8540
8541    #[tokio::test(flavor = "multi_thread")]
8542    async fn backtrace_reports_fully_qualified_fn_names() {
8543        // An error inside a function called by a qualified name records the
8544        // full path (m::f), not just the final segment (f), in the
8545        // structured backtrace's unwind locations.
8546        let main = "import \"m.kcl\" as m\nx = m::f()\n";
8547        let modules = [("m.kcl", "export fn f() {\n  return undefinedVariable\n}\n")];
8548        let err = execute_with_modules(main, &modules).await.unwrap_err();
8549        let fn_names: Vec<_> = err.backtrace().into_iter().filter_map(|item| item.fn_name).collect();
8550        assert_eq!(fn_names, vec!["m::f".to_owned()]);
8551    }
8552
8553    #[tokio::test(flavor = "multi_thread")]
8554    async fn whole_module_name_executes_as_operand() {
8555        // A whole-module import used as a binary or unary operand executes
8556        // the module and operates on its final-expression value, exactly like
8557        // using the name in expression position (x = m).
8558        let main = r#"import "m.kcl" as m
8559sum = m + m
8560neg = -m
8561"#;
8562        let result = execute_with_modules(main, &[("m.kcl", "42\n")]).await.unwrap();
8563        assert_eq!(
8564            mem_get_json(result.exec_state.stack(), result.mem_env, "sum").as_f64(),
8565            Some(84.0)
8566        );
8567        assert_eq!(
8568            mem_get_json(result.exec_state.stack(), result.mem_env, "neg").as_f64(),
8569            Some(-42.0)
8570        );
8571    }
8572
8573    #[tokio::test(flavor = "multi_thread")]
8574    async fn whole_module_without_return_as_operand_errors() {
8575        // Matches expression-position behavior: the module still executes,
8576        // the missing-return fallback produces a KclNone, and the binary
8577        // operation then rejects it. (A trailing declaration would count as
8578        // the module's return value, so the module body must be empty.)
8579        let main = "import \"m.kcl\" as m
8580x = m + 1
8581";
8582        let err = execute_with_modules(main, &[("m.kcl", "")]).await.unwrap_err();
8583        assert!(
8584            err.message().contains("Expected a number, but found none"),
8585            "expected the operand to be the module's missing-return KclNone, got: {}",
8586            err.message()
8587        );
8588    }
8589
8590    #[tokio::test(flavor = "multi_thread")]
8591    async fn enum_rejects_name_clash_with_module() {
8592        // One rule reached six ways: by declaring the enum or an enum alias
8593        // second, by importing the module after either one, and by importing the
8594        // enum itself either by name or through a glob. A glob arrives by a
8595        // different code path because it copies exported keys with their
8596        // namespace prefix intact.
8597        let plain_module = ("Color.kcl", "export x = 1\n");
8598        let enum_module = (
8599            "enums.kcl",
8600            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red }\n",
8601        );
8602
8603        for (case, main, modules) in [
8604            (
8605                "module then enum",
8606                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"Color.kcl\"\ntype Color { | Red }\n",
8607                vec![plain_module],
8608            ),
8609            (
8610                "enum then module",
8611                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Color { | Red }\nimport \"Color.kcl\"\n",
8612                vec![plain_module],
8613            ),
8614            (
8615                "named import of an enum",
8616                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport Color from 'enums.kcl'\n",
8617                vec![plain_module, enum_module],
8618            ),
8619            (
8620                "glob import of an enum",
8621                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport * from 'enums.kcl'\n",
8622                vec![plain_module, enum_module],
8623            ),
8624            (
8625                "module then enum alias",
8626                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Base { | Red }\nimport \"Color.kcl\"\ntype Color = Base\n",
8627                vec![plain_module],
8628            ),
8629            (
8630                "enum alias then module",
8631                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Base { | Red }\ntype Color = Base\nimport \"Color.kcl\"\n",
8632                vec![plain_module],
8633            ),
8634        ] {
8635            let err = execute_with_modules(main, &modules).await.unwrap_err();
8636            assert_eq!(
8637                err.message(),
8638                "An enum and a module cannot share the name `Color` in the same scope, because `Color::x` would be ambiguous. Rename one of them.",
8639                "case: {case}"
8640            );
8641        }
8642    }
8643
8644    #[tokio::test(flavor = "multi_thread")]
8645    async fn enum_alias_can_shadow_module_from_outer_scope() {
8646        let main = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8647type Color { | Red }
8648import "Shade.kcl"
8649
8650fn pick(): Color {
8651  type Shade = Color
8652  return Shade::Red
8653}
8654
8655result = pick()
8656"#;
8657        let result = execute_with_modules(main, &[("Shade.kcl", "export value = 1\n")])
8658            .await
8659            .unwrap();
8660        let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "result") else {
8661            panic!("`result` should hold an enum value");
8662        };
8663        assert_eq!(value.qualified_name(), "Color::Red");
8664    }
8665
8666    #[tokio::test(flavor = "multi_thread")]
8667    async fn enum_constructs_variant() {
8668        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8669        let colors = (
8670            "colors.kcl",
8671            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
8672        );
8673
8674        for (case, main, modules) in [
8675            (
8676                "declared locally",
8677                format!("{allow}type Color {{ | Red | Green }}\nx = Color::Red\n"),
8678                vec![],
8679            ),
8680            (
8681                // Also the regression test for the export check: a module's exports
8682                // record the prefixed key `__ty_Color`, not the bare name.
8683                "reached through a module path",
8684                format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
8685                vec![colors],
8686            ),
8687            (
8688                "imported by name",
8689                format!("{allow}import Color from 'colors.kcl'\nx = Color::Red\n"),
8690                vec![colors],
8691            ),
8692            (
8693                // An import alias renames the binding, not the type, so identity
8694                // and therefore the reported name stay those of the declaration.
8695                "imported under an alias",
8696                format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade::Red\n"),
8697                vec![colors],
8698            ),
8699        ] {
8700            let result = execute_with_modules(&main, &modules)
8701                .await
8702                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
8703            let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
8704                panic!("case: {case}: `x` should hold an enum value");
8705            };
8706            assert_eq!(value.qualified_name(), "Color::Red", "case: {case}");
8707        }
8708    }
8709
8710    #[tokio::test(flavor = "multi_thread")]
8711    async fn enum_aliases_preserve_the_original_declaration() {
8712        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8713type Color { | Red | Green }
8714type C = Color
8715type D = C
8716
8717original = Color::Red
8718directAlias = C::Red
8719chainedAlias = D::Red
8720directEqualsOriginal = directAlias == original
8721chainEqualsOriginal = chainedAlias == original
8722
8723fn passThroughAlias(@color: C): Color {
8724  return color
8725}
8726
8727passed = passThroughAlias(D::Green)
8728"#;
8729
8730        let result = parse_execute(code).await.unwrap();
8731        let memory = result.exec_state.stack();
8732
8733        let KclValue::Type {
8734            value: TypeDef::Enum(original_def),
8735            ..
8736        } = mem_get_json(memory, result.mem_env, &format!("{}Color", memory::TYPE_PREFIX))
8737        else {
8738            panic!("`Color` should hold an enum definition");
8739        };
8740        for alias in ["C", "D"] {
8741            let KclValue::Type {
8742                value: TypeDef::Enum(alias_def),
8743                ..
8744            } = mem_get_json(memory, result.mem_env, &format!("{}{alias}", memory::TYPE_PREFIX))
8745            else {
8746                panic!("`{alias}` should hold an enum definition");
8747            };
8748            assert!(Arc::ptr_eq(&original_def, &alias_def), "alias: {alias}");
8749        }
8750
8751        for name in ["directEqualsOriginal", "chainEqualsOriginal"] {
8752            let KclValue::Bool { value, .. } = mem_get_json(memory, result.mem_env, name) else {
8753                panic!("`{name}` should hold a boolean");
8754            };
8755            assert!(value, "comparison: {name}");
8756        }
8757
8758        let KclValue::Enum { value: passed } = mem_get_json(memory, result.mem_env, "passed") else {
8759            panic!("`passed` should hold an enum value");
8760        };
8761        assert_eq!(passed.enum_id(), original_def.id());
8762        assert_eq!(passed.variant(), "Green");
8763    }
8764
8765    #[tokio::test(flavor = "multi_thread")]
8766    async fn enum_aliases_survive_qualified_imports_and_reexports() {
8767        let main = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8768import "colors.kcl"
8769import "aliases.kcl"
8770import "tones.kcl"
8771import Paint as Finish from "aliases.kcl"
8772import * from "aliases.kcl"
8773
8774original = colors::Color::Red
8775qualifiedAlias = aliases::Paint::Red
8776namedImportAlias = Finish::Green
8777globImportAlias = Paint::Red
8778namedTargetAlias = tones::Tint::Green
8779aliasesEqualOriginal = qualifiedAlias == original
8780
8781fn throughAlias(@color: Finish): colors::Color {
8782  return color
8783}
8784
8785fn throughOriginal(@color: colors::Color): Finish {
8786  return color
8787}
8788
8789fromAlias = throughAlias(Finish::Green)
8790originalIntoAlias = throughAlias(colors::Color::Green)
8791fromOriginal = throughOriginal(colors::Color::Red)
8792"#;
8793        let modules = [
8794            (
8795                "colors.kcl",
8796                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
8797            ),
8798            (
8799                "palette.kcl",
8800                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"colors.kcl\" as swatches\nexport type Shade = swatches::Color\n",
8801            ),
8802            (
8803                "aliases.kcl",
8804                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"palette.kcl\"\nexport type Paint = palette::Shade\n",
8805            ),
8806            (
8807                "tones.kcl",
8808                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport Color from \"colors.kcl\"\nexport type Tint = Color\n",
8809            ),
8810        ];
8811
8812        let result = execute_with_modules(main, &modules).await.unwrap();
8813        let memory = result.exec_state.stack();
8814        let KclValue::Enum { value: original } = mem_get_json(memory, result.mem_env, "original") else {
8815            panic!("`original` should hold an enum value");
8816        };
8817        let original_id = original.enum_id();
8818
8819        for (name, variant) in [
8820            ("qualifiedAlias", "Red"),
8821            ("namedImportAlias", "Green"),
8822            ("globImportAlias", "Red"),
8823            ("namedTargetAlias", "Green"),
8824            ("fromAlias", "Green"),
8825            ("originalIntoAlias", "Green"),
8826            ("fromOriginal", "Red"),
8827        ] {
8828            let KclValue::Enum { value } = mem_get_json(memory, result.mem_env, name) else {
8829                panic!("`{name}` should hold an enum value");
8830            };
8831            assert_eq!(value.enum_id(), original_id, "value: {name}");
8832            assert_eq!(value.variant(), variant, "value: {name}");
8833        }
8834
8835        let KclValue::Bool { value, .. } = mem_get_json(memory, result.mem_env, "aliasesEqualOriginal") else {
8836            panic!("`aliasesEqualOriginal` should hold a boolean");
8837        };
8838        assert!(value);
8839
8840        let KclValue::Type {
8841            value: TypeDef::Enum(finish_def),
8842            ..
8843        } = mem_get_json(memory, result.mem_env, &format!("{}Finish", memory::TYPE_PREFIX))
8844        else {
8845            panic!("`Finish` should hold an enum definition");
8846        };
8847        assert_eq!(finish_def.id(), original_id);
8848    }
8849
8850    // The next five tests pin lexical resolution of signature types: a type
8851    // name written in a function signature resolves in the scope where the
8852    // declaration executes, never in the caller's scope. Before
8853    // definition-time resolution, signature types were looked up at each call
8854    // in the caller's environment, so a std or user module whose exported
8855    // types a caller had not imported under their bare names was uncallable.
8856
8857    #[tokio::test(flavor = "multi_thread")]
8858    async fn signature_types_resolve_in_declaring_module() {
8859        let colors = (
8860            "colors.kcl",
8861            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n  return c\n}\n",
8862        );
8863        // The caller can reach `colors::Color` but never binds the bare name
8864        // `Color`, so resolving the signature in the caller's scope would fail.
8865        let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"colors.kcl\"\nr = colors::paint(colors::Color::Red)\n";
8866
8867        let result = execute_with_modules(main, &[colors]).await.unwrap();
8868        let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
8869            panic!("`r` should hold an enum value");
8870        };
8871        assert_eq!(value.qualified_name(), "Color::Red");
8872    }
8873
8874    #[tokio::test(flavor = "multi_thread")]
8875    async fn qualified_type_paths_resolve_in_aliases_and_ascriptions() {
8876        let main = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8877type ViewOrientation = view::Orientation
8878front = view::Orientation::Front: view::Orientation
8879"#;
8880
8881        parse_execute(main).await.unwrap();
8882    }
8883
8884    #[tokio::test(flavor = "multi_thread")]
8885    async fn unknown_qualified_type_reports_the_written_name() {
8886        let main = "fn f(@value: missing::Orientation) {}\n";
8887
8888        let err = parse_execute(main).await.unwrap_err();
8889        assert_eq!(err.message(), "Unknown type: missing::Orientation");
8890    }
8891
8892    #[tokio::test(flavor = "multi_thread")]
8893    async fn signature_types_resolve_under_import_alias() {
8894        // An import alias renames the caller's binding for the module. The
8895        // declaring module's scope is unaffected, so the signature must
8896        // resolve identically under any alias.
8897        let colors = (
8898            "colors.kcl",
8899            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n  return c\n}\n",
8900        );
8901        let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"colors.kcl\" as painter\nr = painter::paint(painter::Color::Red)\n";
8902
8903        let result = execute_with_modules(main, &[colors]).await.unwrap();
8904        let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
8905            panic!("`r` should hold an enum value");
8906        };
8907        assert_eq!(value.qualified_name(), "Color::Red");
8908    }
8909
8910    #[tokio::test(flavor = "multi_thread")]
8911    async fn signature_types_ignore_caller_scope() {
8912        // `broken.kcl` names a type it does not define. The caller defines
8913        // that name, which caller-scope resolution would have used. The
8914        // declaration must fail when the module loads, without consulting the
8915        // caller's binding.
8916        let broken = (
8917            "broken.kcl",
8918            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport fn f(@x: Missing) {\n  return x\n}\n",
8919        );
8920        let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Missing = string\nimport \"broken.kcl\"\nr = broken::f(\"hi\")\n";
8921
8922        let err = execute_with_modules(main, &[broken]).await.unwrap_err();
8923        assert!(
8924            err.message().contains("Unknown type: Missing"),
8925            "message: {}",
8926            err.message()
8927        );
8928    }
8929
8930    #[tokio::test(flavor = "multi_thread")]
8931    async fn signature_types_reject_forward_reference() {
8932        // Resolution happens when the declaration executes, so a type declared
8933        // later in the file is not visible. The function is never called; the
8934        // error must surface at the declaration itself.
8935        let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nfn f(@x: Later) {\n  return x\n}\ntype Later = string\n";
8936
8937        let err = parse_execute(main).await.unwrap_err();
8938        assert!(
8939            err.message().contains("Unknown type: Later"),
8940            "message: {}",
8941            err.message()
8942        );
8943    }
8944
8945    #[tokio::test(flavor = "multi_thread")]
8946    async fn signature_types_resolve_in_enclosing_scope() {
8947        // The declaring scope is the closure's scope, not merely the declaring
8948        // module: the anonymous function's signature must see the alias in the
8949        // enclosing function body. Caller-scope resolution would use the
8950        // module-level `Width = string` and fail to coerce `42`.
8951        let main = "@settings(kclVersion = \"3.0-preview\", 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";
8952
8953        let result = parse_execute(main).await.unwrap();
8954        let KclValue::Number { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
8955            panic!("`r` should hold a number");
8956        };
8957        assert_eq!(value, 42.0);
8958    }
8959
8960    // Pins that numeric types in signatures are settings-independent, so
8961    // definition-time resolution changed nothing for them: in type
8962    // annotations, bare `number` maps to `Any` before the settings-reading
8963    // path, and every explicit suffix maps to a settings-free type. A literal
8964    // argument therefore takes its unit from the CALLER's module defaults;
8965    // the declaring module's defaults (`in` here) must never leak in. If a
8966    // future change makes a signature's number type depend on module default
8967    // units, the declaring-module scope of definition-time resolution starts
8968    // to matter and this pin fails.
8969    #[tokio::test(flavor = "multi_thread")]
8970    async fn signature_number_types_ignore_module_default_units() {
8971        let units_in = (
8972            "units_in.kcl",
8973            "@settings(defaultLengthUnit = in)\nexport fn passThrough(@x: number(Length)) {\n  return x\n}\n",
8974        );
8975        // The caller's default length unit is mm (the test default), so the
8976        // unitless literal is 42 mm by the time it reaches the parameter.
8977        let main = "import \"units_in.kcl\"\na = units_in::passThrough(42)\nb = units_in::passThrough(42mm)\nc = units_in::passThrough(42in)\n";
8978
8979        let result = execute_with_modules(main, &[units_in]).await.unwrap();
8980        for (name, expected_ty) in [
8981            // The unitless literal keeps its `Default` type, and that type
8982            // records the CALLER's module settings. Declaring-module leakage
8983            // would show here as `len: Inches`.
8984            //
8985            // That the coercion to `number(Length)` leaves the type as
8986            // `Default` rather than concretizing it to `Known(Millimeters)`
8987            // is pre-existing coercion behavior which this test observes but
8988            // does not endorse. If coercion later concretizes, update the
8989            // expected type; the pin here is the settings provenance.
8990            (
8991                "a",
8992                kcl_api::NumericType::Default {
8993                    len: kcl_api::UnitLength::Millimeters,
8994                    angle: kcl_api::UnitAngle::Degrees,
8995                },
8996            ),
8997            (
8998                "b",
8999                kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Millimeters)),
9000            ),
9001            (
9002                "c",
9003                kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Inches)),
9004            ),
9005        ] {
9006            let KclValue::Number { value, ty, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name)
9007            else {
9008                panic!("`{name}` should hold a number");
9009            };
9010            assert_eq!(value, 42.0, "`{name}` should keep its magnitude");
9011            assert_eq!(ty, expected_ty, "`{name}` should keep the caller-side unit context");
9012        }
9013    }
9014
9015    // Pins the sharpest shadowing case, from a hand-written example during
9016    // review: BOTH scopes define the same type name with different meanings,
9017    // so the test observes which one the signature uses, not merely whether a
9018    // name is present. `m1.kcl`'s `A` is `string` and is NOT exported; the
9019    // caller's own `A` is `number(mm)`. The signature must use m1's `A`, so
9020    // passing `2mm` is a type error. Caller-scope resolution would have used
9021    // the caller's `A` and accepted the call.
9022    #[tokio::test(flavor = "multi_thread")]
9023    async fn signature_types_use_declaring_scope_when_both_scopes_define_the_name() {
9024        let m1 = (
9025            "m1.kcl",
9026            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype A = string\n\nexport fn test(@a: A) {\n  return a\n}\n",
9027        );
9028        let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport * from \"m1.kcl\"\ntype A = number(mm)\nx = test(2mm)\n";
9029
9030        let err = execute_with_modules(main, &[m1]).await.unwrap_err();
9031        assert_eq!(
9032            err.message(),
9033            "The input argument of `test` requires a value with type `A`, but found a number (mm) (with type `number(mm)`)."
9034        );
9035    }
9036
9037    #[tokio::test(flavor = "multi_thread")]
9038    async fn enum_rejects_bad_variant_paths() {
9039        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
9040
9041        for (case, main, modules, message) in [
9042            (
9043                "unknown variant",
9044                format!("{allow}type Color {{ | Red | Green }}\nx = Color::Blue\n"),
9045                vec![],
9046                "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
9047            ),
9048            (
9049                "unknown variant through an alias",
9050                format!("{allow}type Color {{ | Red | Green }}\ntype Paint = Color\nx = Paint::Blue\n"),
9051                vec![],
9052                "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
9053            ),
9054            (
9055                "enum with no variants",
9056                format!("{allow}type Empty {{ | }}\nx = Empty::Red\n"),
9057                vec![],
9058                "`Red` is not a variant of enum `Empty`. Enum `Empty` has no variants.",
9059            ),
9060            (
9061                "path continues past the enum",
9062                format!("{allow}type Color {{ | Red }}\nx = Color::Red::more\n"),
9063                vec![],
9064                "`Color` is an enum, so only a variant name can follow it. There is nothing to reach through `Color::Red`.",
9065            ),
9066            (
9067                "variant name is case sensitive",
9068                format!("{allow}type Color {{ | Red }}\nx = Color::red\n"),
9069                vec![],
9070                "`red` is not a variant of enum `Color`. Its variants are: Red.",
9071            ),
9072            (
9073                "enum not exported from its module",
9074                format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
9075                vec![(
9076                    "colors.kcl",
9077                    "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Color { | Red }\n",
9078                )],
9079                "Item Color not found in module's exported items",
9080            ),
9081            (
9082                "a non-enum type alias cannot head a path",
9083                format!("{allow}type T = number(_)\nx = T::foo\n"),
9084                vec![],
9085                "`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
9086            ),
9087            (
9088                "a chained non-enum type alias cannot head a path",
9089                format!("{allow}type T = number(_)\ntype U = T\nx = U::foo\n"),
9090                vec![],
9091                "`U` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
9092            ),
9093            (
9094                "a qualified non-enum type alias cannot head a path",
9095                format!("{allow}import \"types.kcl\"\nx = types::T::foo\n"),
9096                vec![(
9097                    "types.kcl",
9098                    "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type T = number(_)\n",
9099                )],
9100                "`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
9101            ),
9102            (
9103                "an alias containing an enum is not an enum alias",
9104                format!("{allow}type Color {{ | Red }}\ntype T = Color | string\nx = T::Red\n"),
9105                vec![],
9106                "`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
9107            ),
9108            (
9109                // The other half of allowing a value and an enum to share a name:
9110                // a value on its own can never head a path.
9111                "a value cannot head a path",
9112                "Color = 5\nx = Color::Red\n".to_owned(),
9113                vec![],
9114                "`Color` is not defined",
9115            ),
9116        ] {
9117            let err = execute_with_modules(&main, &modules).await.unwrap_err();
9118            assert_eq!(err.message(), message, "case: {case}");
9119        }
9120    }
9121
9122    #[tokio::test(flavor = "multi_thread")]
9123    async fn enum_compares_by_variant() {
9124        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9125type Color { | Red | Green }
9126sameEq = Color::Red == Color::Red
9127sameNeq = Color::Red != Color::Red
9128otherEq = Color::Red == Color::Green
9129otherNeq = Color::Red != Color::Green
9130"#;
9131        let result = parse_execute(code).await.unwrap();
9132
9133        for (name, expected) in [
9134            ("sameEq", true),
9135            ("sameNeq", false),
9136            ("otherEq", false),
9137            ("otherNeq", true),
9138        ] {
9139            let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
9140                panic!("`{name}` should hold a bool");
9141            };
9142            assert_eq!(value, expected, "variable: {name}");
9143        }
9144    }
9145
9146    #[tokio::test(flavor = "multi_thread")]
9147    async fn enum_usable_inside_sketch_block() {
9148        // Only enum declarations are restricted to the top level; uses are not
9149        // restricted at all. A sketch block executes its body with sketch-mode
9150        // skipping turned off, and memory lookups walk outward, so the enum
9151        // declared above resolves inside the block.
9152        //
9153        // `assertIs` runs inside the block because block-local bindings live in a
9154        // child scope that the root environment cannot read afterwards. A wrong
9155        // comparison therefore fails this test instead of passing unnoticed.
9156        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9157type Color { | Red | Green }
9158sketch(on = XY) {
9159  c = Color::Red
9160  assertIs(Color::Red != Color::Green)
9161  assertIs(!(Color::Red != Color::Red))
9162  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
9163}
9164"#;
9165        parse_execute(code)
9166            .await
9167            .unwrap_or_else(|err| panic!("enum use inside a sketch block should work: {}", err.message()));
9168    }
9169
9170    #[tokio::test(flavor = "multi_thread")]
9171    async fn enum_eq_reserved_inside_sketch_block() {
9172        // Inside a sketch block, `==` declares an equivalence constraint, so it is
9173        // not available for ordinary comparison. Enums are not singled out: the
9174        // interception happens before any value-comparison arm is reached, and
9175        // strings and numbers are refused in the same words. The string and number
9176        // rows are here to keep that visible -- if a later change makes enums
9177        // report something different from the other types, this test says so.
9178        //
9179        // `!=` is deliberately absent: the interception tests `Eq` only, so `!=`
9180        // still compares, which `enum_usable_inside_sketch_block` covers.
9181        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
9182        let tail = "  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}\n";
9183        for (case, declaration, comparison, types) in [
9184            (
9185                "enums",
9186                "type Color { | Red | Green }\n",
9187                "Color::Red == Color::Green",
9188                "a value of enum `Color` and a value of enum `Color`",
9189            ),
9190            ("strings", "", "\"a\" == \"b\"", "a string and a string"),
9191            ("numbers", "", "1 == 2", "a number and a number"),
9192        ] {
9193            let code = format!("{allow}{declaration}sketch(on = XY) {{\n  x = {comparison}\n{tail}");
9194            assert_eq!(
9195                parse_execute(&code).await.unwrap_err().message(),
9196                format!("Cannot create an equivalence constraint between values of these types: {types}"),
9197                "case: {case}"
9198            );
9199        }
9200    }
9201
9202    #[tokio::test(flavor = "multi_thread")]
9203    async fn enum_same_file_imported_twice_is_one_type() {
9204        // Two names for one declaration, so they are the same type and compare
9205        // equal. Identity is the declaration, not the binding, which is what makes
9206        // this different from two files that each declare a `Color`.
9207        let main = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9208import Color as A from 'colors.kcl'
9209import Color as B from 'colors.kcl'
9210x = A::Red == B::Red
9211y = A::Red == B::Green
9212"#;
9213        let result = execute_with_modules(
9214            main,
9215            &[(
9216                "colors.kcl",
9217                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
9218            )],
9219        )
9220        .await
9221        .unwrap();
9222
9223        for (name, expected) in [("x", true), ("y", false)] {
9224            let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
9225                panic!("`{name}` should hold a bool");
9226            };
9227            assert_eq!(value, expected, "variable: {name}");
9228        }
9229    }
9230
9231    #[tokio::test(flavor = "multi_thread")]
9232    async fn enum_rejects_comparison_across_types() {
9233        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
9234        let color = (
9235            "a.kcl",
9236            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red }\n",
9237        );
9238        let other_color = (
9239            "b.kcl",
9240            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red }\n",
9241        );
9242
9243        for (case, main, modules, message) in [
9244            (
9245                "two enums declared separately",
9246                format!("{allow}type Color {{ | Red }}\ntype Shade {{ | Red }}\nx = Color::Red == Shade::Red\n"),
9247                vec![],
9248                "Cannot compare enum `Color` with enum `Shade`. They are different types.",
9249            ),
9250            (
9251                // Identity is the declaration, not the name, so two enums that
9252                // share a name are still different types. Pins that the message
9253                // says so rather than naming `Color` twice.
9254                "two enums sharing a name",
9255                format!(
9256                    "{allow}import Color as A from 'a.kcl'\nimport Color as B from 'b.kcl'\nx = A::Red == B::Red\n"
9257                ),
9258                vec![color, other_color],
9259                "Cannot compare two different enums that are both named `Color`. They come from separate declarations.",
9260            ),
9261            (
9262                "an enum and a number",
9263                format!("{allow}type Color {{ | Red }}\nx = Color::Red == 5\n"),
9264                vec![],
9265                "Cannot compare enum `Color::Red` with a number.",
9266            ),
9267            (
9268                "a number and an enum, in that order",
9269                format!("{allow}type Color {{ | Red }}\nx = 5 == Color::Red\n"),
9270                vec![],
9271                "Cannot compare enum `Color::Red` with a number.",
9272            ),
9273            (
9274                "an enum and a string",
9275                format!("{allow}type Color {{ | Red }}\nx = Color::Red == \"Red\"\n"),
9276                vec![],
9277                "Cannot compare enum `Color::Red` with a string.",
9278            ),
9279        ] {
9280            let err = execute_with_modules(&main, &modules).await.unwrap_err();
9281            assert_eq!(err.message(), message, "case: {case}");
9282        }
9283    }
9284
9285    #[tokio::test(flavor = "multi_thread")]
9286    async fn enum_rejects_bare_type_name_as_value() {
9287        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
9288        let colors = (
9289            "colors.kcl",
9290            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
9291        );
9292
9293        for (case, main, modules, message) in [
9294            (
9295                "enum suggests a variant",
9296                format!("{allow}type Color {{ | Red | Green }}\nx = Color\n"),
9297                vec![],
9298                "`Color` is a type, not a value. Use one of its variants, such as `Color::Red`.",
9299            ),
9300            (
9301                // The suggestion has to be pasteable into the file that produced
9302                // the error, so it uses the local name rather than the declared one.
9303                "suggestion uses the import alias",
9304                format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade\n"),
9305                vec![colors],
9306                "`Shade` is a type, not a value. Use one of its variants, such as `Shade::Red`.",
9307            ),
9308            (
9309                "suggestion uses the type alias",
9310                format!("{allow}type Color {{ | Red | Green }}\ntype Paint = Color\nx = Paint\n"),
9311                vec![],
9312                "`Paint` is a type, not a value. Use one of its variants, such as `Paint::Red`.",
9313            ),
9314            (
9315                "enum with no variants suggests nothing",
9316                format!("{allow}type Empty {{ | }}\nx = Empty\n"),
9317                vec![],
9318                "`Empty` is a type, not a value.",
9319            ),
9320            (
9321                "a type alias reports the same way",
9322                format!("{allow}type T = number(_)\nx = T\n"),
9323                vec![],
9324                "`T` is a type, not a value.",
9325            ),
9326            (
9327                // Unchanged behavior: with no type of that name, the old message
9328                // is still the right one.
9329                "an unknown name is still undefined",
9330                "x = Nope\n".to_owned(),
9331                vec![],
9332                "`Nope` is not defined",
9333            ),
9334        ] {
9335            let err = execute_with_modules(&main, &modules).await.unwrap_err();
9336            assert_eq!(err.message(), message, "case: {case}");
9337        }
9338    }
9339
9340    #[tokio::test(flavor = "multi_thread")]
9341    async fn imported_user_type_declarations_require_v3_even_with_opt_in() {
9342        for version in ["1.0", "2.0"] {
9343            for module_settings in [format!("@settings(kclVersion = \"{version}\")\n"), String::new()] {
9344                for (feature, declaration, use_type) in [
9345                    (
9346                        "Type aliases",
9347                        "export type Distance = number(mm)",
9348                        "x = 1mm: types::Distance",
9349                    ),
9350                    (
9351                        "Enum declarations",
9352                        "export type Color { | Red }",
9353                        "x = types::Color::Red",
9354                    ),
9355                ] {
9356                    let main = format!(
9357                        "@settings(kclVersion = \"{version}\", experimentalFeatures = allow)\nimport \"types.kcl\"\n{use_type}\n"
9358                    );
9359                    let module = format!("{module_settings}{declaration}\n");
9360                    let err = execute_with_modules(&main, &[("types.kcl", &module)])
9361                        .await
9362                        .unwrap_err();
9363                    assert_eq!(
9364                        err.message(),
9365                        format!("{feature} require KCL 3.0-preview, but this program uses KCL {version}."),
9366                        "main: {main}; module: {module}"
9367                    );
9368                }
9369            }
9370        }
9371    }
9372
9373    #[tokio::test(flavor = "multi_thread")]
9374    async fn named_view_access_in_unversioned_import_uses_entry_point_version() {
9375        let dep = "export camera = view::directed([0, 1, -2])\n";
9376        for version in ["1.0", "2.0"] {
9377            let main = format!("@settings(kclVersion = \"{version}\")\nimport \"dep.kcl\"\nx = dep::camera\n");
9378            let err = execute_with_modules(&main, &[("dep.kcl", dep)]).await.unwrap_err();
9379            assert!(
9380                err.message()
9381                    .contains(&format!("added in KCL 3.0, but this program uses KCL {version}")),
9382                "error: {}",
9383                err.message()
9384            );
9385        }
9386
9387        let main = "@settings(kclVersion = \"3.0-preview\")\nimport \"dep.kcl\"\nx = dep::camera\n";
9388        let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
9389        assert!(result.exec_state.issues().is_empty());
9390    }
9391
9392    #[tokio::test(flavor = "multi_thread")]
9393    async fn enum_use_is_not_experimental_in_v3() {
9394        let code = r#"@settings(kclVersion = "3.0-preview")
9395type Color { | Red }
9396x = Color::Red
9397"#;
9398        let result = parse_execute(code).await.unwrap();
9399        assert!(
9400            result.exec_state.global.issues.is_empty(),
9401            "issues: {:?}",
9402            result.exec_state.global.issues
9403        );
9404    }
9405
9406    #[tokio::test(flavor = "multi_thread")]
9407    async fn v3_enum_variant_warning_follows_explicit_annotations() {
9408        for (case, declarations, binding, expected_warnings) in [
9409            ("unannotated enum", "type Color { | Red }", "Color", 0),
9410            (
9411                "annotated enum",
9412                "@(experimental = true)\ntype Color { | Red }",
9413                "Color",
9414                1,
9415            ),
9416            (
9417                "alias of annotated enum",
9418                "@(experimental = true)\ntype Color { | Red }\ntype Shade = Color",
9419                "Shade",
9420                1,
9421            ),
9422            (
9423                "annotated alias",
9424                "type Color { | Red }\n@(experimental = true)\ntype Shade = Color",
9425                "Shade",
9426                1,
9427            ),
9428        ] {
9429            let code = format!(
9430                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = warn)\n{declarations}\nx = {binding}::Red\n"
9431            );
9432            let result = parse_execute(&code)
9433                .await
9434                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
9435            let variant_warnings: Vec<_> = result
9436                .exec_state
9437                .issues()
9438                .iter()
9439                .filter(|issue| {
9440                    issue.message == "Use of the enum `Color` is experimental and may change or be removed."
9441                })
9442                .collect();
9443            assert_eq!(variant_warnings.len(), expected_warnings, "case: {case}");
9444            assert!(
9445                variant_warnings.iter().all(|issue| issue.severity == Severity::Warning),
9446                "case: {case}"
9447            );
9448        }
9449    }
9450
9451    #[tokio::test(flavor = "multi_thread")]
9452    async fn enum_allows_name_sharing_outside_modules() {
9453        // Pins two deliberate exemptions from the clash rule above, so that
9454        // tightening it later has to be a decision rather than an accident.
9455        //
9456        // Only a module or a type binding that resolves to an enum can head a
9457        // `Color::Red` path, so only those two can be ambiguous. A non-enum type
9458        // alias cannot head a `::` path, and an ordinary value is never looked up
9459        // for a path head at all.
9460        for (case, main, modules) in [
9461            (
9462                // The module arrives second, which is the path carrying the
9463                // "only `TypeDef::Enum` conflicts" guard.
9464                "an alias may share a name with a module",
9465                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Temperature = number(_)\nimport \"Temperature.kcl\"\nx = Temperature::x\n",
9466                vec![("Temperature.kcl", "export x = 1\n")],
9467            ),
9468            (
9469                "a value may share a name with an enum",
9470                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Color { | Red }\nColor = 5\n",
9471                vec![],
9472            ),
9473        ] {
9474            if let Err(err) = execute_with_modules(main, &modules).await {
9475                panic!("case: {case}: {}", err.message());
9476            }
9477        }
9478    }
9479
9480    #[tokio::test(flavor = "multi_thread")]
9481    async fn enum_declaration_rejects_redefinition() {
9482        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9483type Color { | Red }
9484type Color { | Green }
9485"#;
9486        assert_eq!(
9487            parse_execute(code).await.unwrap_err().message(),
9488            "Redefinition of type Color."
9489        );
9490    }
9491
9492    /// Projection yields the variant's declared representation, which in V1 is
9493    /// always the variant name. Every row binds `x` so the rows differ only in the
9494    /// shape being projected, and the alias row is here because the target is
9495    /// resolved before projection decides anything, so an alias must behave
9496    /// exactly like the type it names.
9497    #[tokio::test(flavor = "multi_thread")]
9498    async fn enum_projects_to_string() {
9499        let header = r#"
9500            @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9501            type Color { | Red | Green }
9502            type Label = string
9503        "#;
9504
9505        for (case, body, expected) in [
9506            ("a variant", "x = Color::Red: string", "Red"),
9507            ("another variant of the same enum", "x = Color::Green: string", "Green"),
9508            ("an alias of the target type", "x = Color::Red: Label", "Red"),
9509            (
9510                "an element of a projected array",
9511                r#"
9512                    pair = [Color::Red, Color::Green]: [string]
9513                    x = pair[1]
9514                "#,
9515                "Green",
9516            ),
9517            (
9518                "an element of a nested projected array",
9519                r#"
9520                    grid = [[Color::Green]]: [[string]]
9521                    x = grid[0][0]
9522                "#,
9523                "Green",
9524            ),
9525            (
9526                "a one-element array against a bare string",
9527                "x = [Color::Red]: string",
9528                "Red",
9529            ),
9530        ] {
9531            let result = parse_execute(&format!("{header}{body}\n"))
9532                .await
9533                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
9534            let KclValue::String { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
9535                panic!("case: {case}: `x` should hold a string");
9536            };
9537            assert_eq!(value, expected, "case: {case}");
9538        }
9539    }
9540
9541    /// Ascribing the enum's own type, directly or through an alias, is a check
9542    /// rather than a conversion: the value stays an enum and still compares equal
9543    /// to the variant it came from.
9544    #[tokio::test(flavor = "multi_thread")]
9545    async fn enum_ascription_keeps_the_enum() {
9546        let header = r#"
9547            @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9548            type Color { | Red | Green }
9549            type Paint = Color
9550        "#;
9551
9552        for (case, expression, expected) in [
9553            ("its own type", "(Color::Red: Color) == Color::Red", true),
9554            ("an alias of its own type", "(Color::Red: Paint) == Color::Red", true),
9555            (
9556                "the ascription does not change which variant it is",
9557                "(Color::Red: Color) == Color::Green",
9558                false,
9559            ),
9560        ] {
9561            let result = parse_execute(&format!("{header}x = {expression}\n"))
9562                .await
9563                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
9564            let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
9565                panic!("case: {case}: `x` should hold a bool");
9566            };
9567            assert_eq!(value, expected, "case: {case}");
9568        }
9569    }
9570
9571    /// A boundary the user did not write must not project, or a nominal parameter
9572    /// type would mean nothing. The rows are the separate coercion sites: the
9573    /// unlabeled argument, a labeled argument, and the return.
9574    #[tokio::test(flavor = "multi_thread")]
9575    async fn enum_projection_is_not_implicit() {
9576        let header = r#"
9577            @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9578            type Color { | Red | Green }
9579        "#;
9580        let found = "but found a value of enum `Color` (with type `Color`).";
9581
9582        for (case, body, expected) in [
9583            (
9584                "unlabeled argument",
9585                r#"
9586                    fn label(@text: string) { return text }
9587                    x = label(Color::Red)
9588                "#,
9589                format!("The input argument of `label` requires a value with type `string`, {found}"),
9590            ),
9591            (
9592                "labeled argument",
9593                r#"
9594                    fn label(text: string) { return text }
9595                    x = label(text = Color::Red)
9596                "#,
9597                format!("text requires a value with type `string`, {found}"),
9598            ),
9599            (
9600                "return",
9601                r#"
9602                    fn label(): string { return Color::Red }
9603                    x = label()
9604                "#,
9605                format!("This function requires its result to be a value with type `string`, {found}"),
9606            ),
9607            (
9608                // The reported type is `[any; 1]` rather than `[Color; 1]` because
9609                // an array literal does not infer a homogeneous element type. That
9610                // is pre-existing and unrelated to enums; it is pinned here so the
9611                // row is not read as an enum-specific quirk.
9612                "inside an array at an argument boundary",
9613                r#"
9614                    fn labels(@text: [string]) { return text }
9615                    x = labels([Color::Red])
9616                "#,
9617                "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(),
9618            ),
9619        ] {
9620            assert_eq!(
9621                parse_execute(&format!("{header}{body}\n")).await.unwrap_err().message(),
9622                expected,
9623                "case: {case}"
9624            );
9625        }
9626    }
9627
9628    /// What an explicit ascription refuses, and what it says about it. The numeric
9629    /// rows deliberately do not name the mechanism a later version would use.
9630    #[tokio::test(flavor = "multi_thread")]
9631    async fn enum_ascription_rejections() {
9632        let header = r#"
9633            @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9634            type Color { | Red }
9635            type Shade { | Red }
9636        "#;
9637        let no_number = "Cannot project enum `Color` to a number. An enum projects to `string`; projecting to a number is not supported yet.";
9638
9639        for (case, expression, expected) in [
9640            ("a number target", "Color::Red: number(_)", no_number.to_owned()),
9641            (
9642                "a number target reached through an array, so the reason survives the walk",
9643                "[Color::Red]: [number(_)]",
9644                no_number.to_owned(),
9645            ),
9646            (
9647                "a boolean target, which is not a projection at all",
9648                "Color::Red: bool",
9649                "could not coerce a value of enum `Color` (with type `Color`) to type `bool`".to_owned(),
9650            ),
9651            (
9652                "another enum whose variants happen to match",
9653                "Color::Red: Shade",
9654                "could not coerce a value of enum `Color` (with type `Color`) to type `Shade`".to_owned(),
9655            ),
9656        ] {
9657            assert_eq!(
9658                parse_execute(&format!("{header}x = {expression}\n"))
9659                    .await
9660                    .unwrap_err()
9661                    .message(),
9662                expected,
9663                "case: {case}"
9664            );
9665        }
9666    }
9667
9668    /// The mirror of `enum_projection_is_not_implicit`: where the declared type is
9669    /// the enum itself, a value flows through every boundary unchanged. Each row
9670    /// binds `x` to a comparison that must hold, so a value that arrived altered
9671    /// would fail rather than pass unnoticed. `Some(message)` marks a row that must
9672    /// be refused instead, which is what keeps the check nominal rather than
9673    /// merely permissive.
9674    #[tokio::test(flavor = "multi_thread")]
9675    async fn enum_flows_through_declared_types() {
9676        let header = r#"
9677            @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9678            type Color { | Red | Green }
9679            type Paint = Color
9680            type Shade { | Red }
9681        "#;
9682
9683        for (case, body, expected) in [
9684            (
9685                "an alias parameter accepts the original enum",
9686                r#"
9687                    fn paint(@c: Paint) { return c }
9688                    x = paint(Color::Red) == Paint::Red
9689                "#,
9690                None,
9691            ),
9692            (
9693                "an unlabeled parameter",
9694                r#"
9695                    fn paint(@c: Color) { return c }
9696                    x = paint(Color::Red) == Color::Red
9697                "#,
9698                None,
9699            ),
9700            (
9701                "a labeled parameter",
9702                r#"
9703                    fn paint(c: Color) { return c }
9704                    x = paint(c = Color::Green) == Color::Green
9705                "#,
9706                None,
9707            ),
9708            (
9709                "a declared return type",
9710                r#"
9711                    fn pick(): Color { return Color::Red }
9712                    x = pick() == Color::Red
9713                "#,
9714                None,
9715            ),
9716            (
9717                "an array parameter",
9718                r#"
9719                    fn firstOf(@cs: [Color]) { return cs[0] }
9720                    x = firstOf([Color::Red, Color::Green]) == Color::Red
9721                "#,
9722                None,
9723            ),
9724            (
9725                // The field check is `has_type`, which an enum satisfies, so an
9726                // object passes here while the projection row of
9727                // `enum_projects_by_target_shape` fails. Both behaviors come from
9728                // the same unfinished object coercion.
9729                "an object field",
9730                r#"
9731                    fn take(@o: { c: Color }) { return o.c }
9732                    x = take({ c = Color::Green }) == Color::Green
9733                "#,
9734                None,
9735            ),
9736            (
9737                "a union that names the enum",
9738                r#"
9739                    fn either(@v: Color | string) { return v }
9740                    x = either(Color::Red) == Color::Red
9741                "#,
9742                None,
9743            ),
9744            (
9745                "the same union given the other member",
9746                r#"
9747                    fn either(@v: Color | string) { return v }
9748                    x = either("plain") == "plain"
9749                "#,
9750                None,
9751            ),
9752            (
9753                "an alias parameter rejects a different enum",
9754                r#"
9755                    fn paint(@c: Paint) { return c }
9756                    x = paint(Shade::Red) == Shade::Red
9757                "#,
9758                Some(
9759                    "The input argument of `paint` requires a value with type `Paint`, but found a value of enum `Shade` (with type `Shade`).",
9760                ),
9761            ),
9762            (
9763                "another declaration at the same boundary",
9764                r#"
9765                    fn paint(@c: Color) { return c }
9766                    x = paint(Shade::Red) == Shade::Red
9767                "#,
9768                Some(
9769                    "The input argument of `paint` requires a value with type `Color`, but found a value of enum `Shade` (with type `Shade`).",
9770                ),
9771            ),
9772        ] {
9773            let code = format!("{header}{body}\n");
9774            match expected {
9775                None => {
9776                    let result = parse_execute(&code)
9777                        .await
9778                        .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
9779                    let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x")
9780                    else {
9781                        panic!("case: {case}: `x` should hold a bool");
9782                    };
9783                    assert!(value, "case: {case}: the value did not survive the boundary");
9784                }
9785                Some(message) => assert_eq!(
9786                    parse_execute(&code).await.unwrap_err().message(),
9787                    message,
9788                    "case: {case}"
9789                ),
9790            }
9791        }
9792    }
9793
9794    // ---- `added_in` on whole declarations ----
9795
9796    /// Runs `body` under `kcl_version` and returns the fatal error message, or
9797    /// `None` if it ran without issues. Experimental features are allowed for
9798    /// unrelated syntax in the test programs. Hints are enabled for user
9799    /// declarations, since in production only std gets them.
9800    async fn added_in_error(kcl_version: &str, body: &str) -> Option<String> {
9801        let program = format!("@settings(kclVersion = {kcl_version}, experimentalFeatures = allow)\n{body}");
9802        match parse_execute_hinting_all(&program, None).await {
9803            Ok(result) => {
9804                assert!(
9805                    result.issues().is_empty(),
9806                    "kclVersion {kcl_version}: unexpected issues: {:#?}",
9807                    result.issues()
9808                );
9809                None
9810            }
9811            Err(err) => Some(err.message().to_owned()),
9812        }
9813    }
9814
9815    #[tokio::test(flavor = "multi_thread")]
9816    async fn not_yet_added_function_is_undefined() {
9817        let body = r#"@(added_in = "3.0")
9818fn newFn() { return 1 }
9819x = newFn()
9820"#;
9821        assert_eq!(
9822            added_in_error("2.0", body).await.as_deref(),
9823            Some("`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0")
9824        );
9825        assert_eq!(added_in_error("\"3.0-preview\"", body).await, None);
9826    }
9827
9828    #[tokio::test(flavor = "multi_thread")]
9829    async fn not_yet_added_function_cannot_be_referenced_as_a_value() {
9830        // Any reference fails, not only a call: the name is simply not bound.
9831        let body = r#"@(added_in = "3.0")
9832fn newFn() { return 1 }
9833f = newFn
9834"#;
9835        assert_eq!(
9836            added_in_error("2.0", body).await.as_deref(),
9837            Some("`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0")
9838        );
9839    }
9840
9841    #[tokio::test(flavor = "multi_thread")]
9842    async fn added_in_boundary_is_inclusive_and_prerelease_counts_as_release() {
9843        for (kcl_version, added_in) in [
9844            ("1.0", "1.0"),
9845            ("2.0", "1.0"),
9846            ("2.0", "2.0"),
9847            ("\"3.0-preview\"", "3.0"),
9848            ("3.0", "3.0"),
9849        ] {
9850            let body = format!("@(added_in = \"{added_in}\")\nfn newFn() {{ return 1 }}\nx = newFn()\n");
9851            assert_eq!(
9852                added_in_error(kcl_version, &body).await,
9853                None,
9854                "kclVersion {kcl_version}, added_in {added_in}"
9855            );
9856        }
9857    }
9858
9859    #[tokio::test(flavor = "multi_thread")]
9860    async fn not_yet_added_error_reports_default_kcl_version() {
9861        // No `@settings(kclVersion = ...)`: the message names the default version.
9862        let program = r#"@(added_in = "2.0")
9863fn newFn() { return 1 }
9864x = newFn()
9865"#;
9866        assert_eq!(
9867            parse_execute_hinting_all(program, None).await.unwrap_err().message(),
9868            "`newFn` is not defined; it was added in KCL 2.0, but this program uses KCL 1.0"
9869        );
9870    }
9871
9872    #[tokio::test(flavor = "multi_thread")]
9873    async fn not_yet_added_constant_is_undefined() {
9874        let body = r#"@(added_in = "3.0")
9875newConst = 5
9876x = newConst
9877"#;
9878        assert_eq!(
9879            added_in_error("2.0", body).await.as_deref(),
9880            Some("`newConst` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0")
9881        );
9882        assert_eq!(added_in_error("\"3.0-preview\"", body).await, None);
9883    }
9884
9885    #[tokio::test(flavor = "multi_thread")]
9886    async fn not_yet_added_type_is_unknown() {
9887        let body = r#"@(added_in = "3.0")
9888type NewT = [number; 2]
9889x = [1, 2]: NewT
9890"#;
9891        assert_eq!(
9892            added_in_error("2.0", body).await.as_deref(),
9893            Some("Unknown type: NewT; it was added in KCL 3.0, but this program uses KCL 2.0")
9894        );
9895        assert_eq!(added_in_error("\"3.0-preview\"", body).await, None);
9896    }
9897
9898    /// A function naming a later-added type in its signature fails at its
9899    /// declaration with the type's error, unless it is gated at or above the type.
9900    #[tokio::test(flavor = "multi_thread")]
9901    async fn function_using_not_yet_added_type_fails_at_its_declaration() {
9902        let ty = "@(added_in = \"3.0\")\ntype NewT = [number; 2]\n";
9903        let type_error = Some("Unknown type: NewT; it was added in KCL 3.0, but this program uses KCL 2.0");
9904
9905        // Ungated function: the type is missing on 2.0.
9906        let body = format!("{ty}fn usesNew(@p: NewT) {{ return p }}\n");
9907        assert_eq!(added_in_error("2.0", &body).await.as_deref(), type_error);
9908        // Gated below the type: same.
9909        let body = format!("{ty}@(added_in = \"2.0\")\nfn usesNew(@p: NewT) {{ return p }}\n");
9910        assert_eq!(added_in_error("2.0", &body).await.as_deref(), type_error);
9911        // The return type is part of the signature too.
9912        let body = format!("{ty}fn makesNew(): NewT {{ return [1, 2] }}\n");
9913        assert_eq!(added_in_error("2.0", &body).await.as_deref(), type_error);
9914
9915        // Gated at the type's version: both are skipped.
9916        let body = format!("{ty}@(added_in = \"3.0\")\nfn usesNew(@p: NewT) {{ return p }}\n");
9917        assert_eq!(added_in_error("2.0", &body).await, None);
9918        let body = format!("{body}x = usesNew([1, 2])\n");
9919        assert_eq!(
9920            added_in_error("2.0", &body).await.as_deref(),
9921            Some("`usesNew` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0")
9922        );
9923
9924        // On a new enough program, nothing is gated.
9925        let body = format!("{ty}fn usesNew(@p: NewT) {{ return p }}\nx = usesNew([1, 2])\n");
9926        assert_eq!(added_in_error("\"3.0-preview\"", &body).await, None);
9927    }
9928
9929    #[tokio::test(flavor = "multi_thread")]
9930    async fn not_yet_added_declaration_is_absent_from_imports() {
9931        // The module runs on a cloned state, so these also cover the record's
9932        // trip through the module's execution outcome.
9933        let dep = "@(added_in = \"3.0\")\nexport fn newFn() { return 1 }\nexport fn oldFn() { return 2 }\n";
9934
9935        // An explicit import fails with the version help.
9936        let main = "@settings(kclVersion = 2.0)\nimport newFn from \"dep.kcl\"\nx = newFn()\n";
9937        assert_eq!(
9938            execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
9939                .await
9940                .unwrap_err()
9941                .message(),
9942            "newFn is not defined in module; it was added in KCL 3.0, but this program uses KCL 2.0"
9943        );
9944
9945        // So does a qualified path.
9946        let main = "@settings(kclVersion = 2.0)\nimport \"dep.kcl\"\nx = dep::newFn()\n";
9947        assert_eq!(
9948            execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
9949                .await
9950                .unwrap_err()
9951                .message(),
9952            "`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0"
9953        );
9954
9955        // A glob import silently omits it while the rest of the module works.
9956        let main = "@settings(kclVersion = 2.0)\nimport * from \"dep.kcl\"\nx = oldFn()\ny = newFn()\n";
9957        assert_eq!(
9958            execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
9959                .await
9960                .unwrap_err()
9961                .message(),
9962            "`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0"
9963        );
9964
9965        // On a new enough program the import works.
9966        let main = "@settings(kclVersion = \"3.0-preview\")\nimport newFn from \"dep.kcl\"\nx = newFn()\n";
9967        let result = execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
9968            .await
9969            .unwrap();
9970        assert_eq!(variable_f64(&result, "x"), 1.0);
9971    }
9972
9973    /// In production only std declarations get the version help; user
9974    /// declarations keep the plain message.
9975    #[tokio::test(flavor = "multi_thread")]
9976    async fn not_yet_added_user_declarations_get_no_hint() {
9977        let program = "@settings(kclVersion = 2.0)\n@(added_in = \"3.0\")\nfn newFn() { return 1 }\nx = newFn()\n";
9978        assert_eq!(
9979            parse_execute(program).await.unwrap_err().message(),
9980            "`newFn` is not defined"
9981        );
9982
9983        let dep = "@(added_in = \"3.0\")\nexport fn newFn() { return 1 }\n";
9984        let main = "@settings(kclVersion = 2.0)\nimport newFn from \"dep.kcl\"\n";
9985        assert_eq!(
9986            execute_with_modules(main, &[("dep.kcl", dep)])
9987                .await
9988                .unwrap_err()
9989                .message(),
9990            "newFn is not defined in module"
9991        );
9992    }
9993
9994    #[tokio::test(flavor = "multi_thread")]
9995    async fn not_yet_added_records_whether_the_declaration_is_std() {
9996        let ctx = new_mock_executor_context(None, machine::ExecutorKind::resolve());
9997        let mut exec_state = ExecState::new(&ctx);
9998        let version = |s: &str| annotations::VersionConstraint::parse(s).unwrap();
9999
10000        exec_state.record_not_yet_added("userFn".to_owned(), version("3.0"), false);
10001        exec_state.mod_local.path = ModulePath::Std {
10002            value: "solid".to_owned(),
10003        };
10004        exec_state.record_not_yet_added("stdFn".to_owned(), version("3.0"), true);
10005
10006        let records = &exec_state.mod_local.not_yet_added;
10007        assert!(!records["userFn"].item.is_std);
10008        assert!(!records["userFn"].exported);
10009        assert!(records["stdFn"].item.is_std);
10010        assert!(records["stdFn"].exported);
10011        ctx.close().await;
10012    }
10013
10014    /// `export import *` re-exports records along with names.
10015    #[tokio::test(flavor = "multi_thread")]
10016    async fn not_yet_added_records_follow_glob_re_exports() {
10017        let dep = "@(added_in = \"3.0\")\nexport fn newFn() { return 1 }\n";
10018        let mid = "export import * from \"dep.kcl\"\n";
10019        let modules = [("dep.kcl", dep), ("mid.kcl", mid)];
10020
10021        let main = "@settings(kclVersion = 2.0)\nimport * from \"mid.kcl\"\nx = newFn()\n";
10022        assert_eq!(
10023            execute_with_modules_hinting_all(main, &modules)
10024                .await
10025                .unwrap_err()
10026                .message(),
10027            "`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0"
10028        );
10029
10030        let main = "@settings(kclVersion = 2.0)\nimport newFn from \"mid.kcl\"\nx = newFn()\n";
10031        assert_eq!(
10032            execute_with_modules_hinting_all(main, &modules)
10033                .await
10034                .unwrap_err()
10035                .message(),
10036            "newFn is not defined in module; it was added in KCL 3.0, but this program uses KCL 2.0"
10037        );
10038    }
10039
10040    /// A record is as private as its name: no `export`, no hint for importers.
10041    #[tokio::test(flavor = "multi_thread")]
10042    async fn private_not_yet_added_records_stay_private() {
10043        let dep = "@(added_in = \"3.0\")\nfn privateFn() { return 1 }\nexport fn oldFn() { return 2 }\n";
10044
10045        let main = "@settings(kclVersion = 2.0)\nimport * from \"dep.kcl\"\nx = privateFn()\n";
10046        assert_eq!(
10047            execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
10048                .await
10049                .unwrap_err()
10050                .message(),
10051            "`privateFn` is not defined"
10052        );
10053
10054        let main = "@settings(kclVersion = 2.0)\nimport privateFn from \"dep.kcl\"\n";
10055        assert_eq!(
10056            execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
10057                .await
10058                .unwrap_err()
10059                .message(),
10060            "privateFn is not defined in module"
10061        );
10062
10063        // `mid` imports the record without `export`, so it does not pass it on.
10064        let dep = "@(added_in = \"3.0\")\nexport fn newFn() { return 1 }\n";
10065        let mid = "import * from \"dep.kcl\"\nexport fn other() { return 2 }\n";
10066        let main = "@settings(kclVersion = 2.0)\nimport * from \"mid.kcl\"\nx = newFn()\n";
10067        assert_eq!(
10068            execute_with_modules_hinting_all(main, &[("dep.kcl", dep), ("mid.kcl", mid)])
10069                .await
10070                .unwrap_err()
10071                .message(),
10072            "`newFn` is not defined"
10073        );
10074    }
10075
10076    /// The mock memory cache carries std's records, since a run reusing cached
10077    /// memory skips the prelude that writes them.
10078    #[tokio::test(flavor = "multi_thread")]
10079    async fn std_not_yet_added_records_survive_the_mock_memory_cache() {
10080        let ctx = new_mock_executor_context(None, machine::ExecutorKind::resolve());
10081        let mut exec_state = ExecState::new(&ctx);
10082        let version = |s: &str| annotations::VersionConstraint::parse(s).unwrap();
10083        let program = crate::Program::parse_no_errs("x = 1").unwrap();
10084        let (main_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
10085        exec_state.global.std_not_yet_added.insert(
10086            "cube".to_owned(),
10087            NotYetAdded {
10088                added_in: version("3.0"),
10089                is_std: true,
10090            },
10091        );
10092
10093        let cached = cache::GlobalState::new(
10094            exec_state,
10095            ctx.settings.clone(),
10096            program.kcl_version,
10097            program.ast.clone(),
10098            main_ref,
10099        );
10100        let mem = cached.mock_memory_state().unwrap();
10101        assert_eq!(mem.std_not_yet_added["cube"].added_in, version("3.0"));
10102
10103        let mock_config = MockConfig::default();
10104        let mut restored = ExecState::new_mock(&ctx, &mock_config);
10105        ExecutorContext::restore_mock_memory(&mut restored, mem, &mock_config).unwrap();
10106        assert!(
10107            !restored.stack().memory.requires_std(),
10108            "cached memory already has the prelude"
10109        );
10110        assert_eq!(
10111            restored.not_yet_added_in_scope("cube").map(|r| &r.added_in),
10112            Some(&version("3.0"))
10113        );
10114        // A std record explains a failed lookup without any test override.
10115        let err = KclError::new_undefined_value(
10116            KclErrorDetails::new("`cube` is not defined".to_owned(), Vec::new()),
10117            None,
10118        );
10119        assert_eq!(
10120            restored.with_not_yet_added_hint(&["cube"], err).message(),
10121            "`cube` is not defined; it was added in KCL 3.0, but this program uses KCL 1.0"
10122        );
10123        ctx.close().await;
10124    }
10125
10126    // ---- mock memory reuse across settings changes ----
10127
10128    /// Runs `code` in mock execution reusing the previous memory, as the LSP
10129    /// worker does for every execution.
10130    async fn run_mock_reusing_memory(ctx: &ExecutorContext, code: &str) -> Result<ExecOutcome, KclErrorWithOutputs> {
10131        let program = crate::Program::parse_no_errs(code).unwrap();
10132        ctx.run_mock(
10133            &program,
10134            &MockConfig {
10135                use_prev_memory: true,
10136                ..Default::default()
10137            },
10138        )
10139        .await
10140    }
10141
10142    #[track_caller]
10143    fn outcome_f64(outcome: &ExecOutcome, name: &str) -> f64 {
10144        let Some(KclValueView::Number { value, .. }) = outcome.variables.get(name) else {
10145            panic!(
10146                "expected `{name}` to be a number, got {:?}",
10147                outcome.variables.get(name)
10148            );
10149        };
10150        *value
10151    }
10152
10153    /// Memory written under one kclVersion still binds declarations the other
10154    /// version skips, so a mock run reusing it must rebuild instead. Nothing
10155    /// else invalidates the LSP worker's memory.
10156    #[tokio::test(flavor = "multi_thread")]
10157    async fn mock_memory_is_rebuilt_on_a_kcl_version_downgrade() {
10158        let ctx = ExecutorContext::new_mock(None).await;
10159        let body = "@(added_in = \"3.0\")\nfn newFn() {\n  return 42\n}\n\nanswer = newFn()\n";
10160
10161        let outcome = run_mock_reusing_memory(&ctx, &format!("@settings(kclVersion = \"3.0-preview\")\n{body}"))
10162            .await
10163            .unwrap();
10164        assert_eq!(outcome_f64(&outcome, "answer"), 42.0);
10165
10166        // Only the version changes; `newFn` is still bound in the cached memory.
10167        let err = run_mock_reusing_memory(&ctx, &format!("@settings(kclVersion = 2.0)\n{body}"))
10168            .await
10169            .unwrap_err();
10170        assert_eq!(err.error.message(), "`newFn` is not defined");
10171
10172        ctx.close().await;
10173    }
10174
10175    /// A mock execution context whose project directory holds `modules`.
10176    async fn mock_context_with_modules(modules: &[(&str, &str)]) -> (ExecutorContext, tempfile::TempDir) {
10177        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_mock_memory").unwrap();
10178        for (name, source) in modules {
10179            tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
10180        }
10181        let ctx = ExecutorContext::new_mock(Some(ExecutorSettings {
10182            project_directory: Some(crate::TypedPath(tmpdir.path().into())),
10183            ..Default::default()
10184        }))
10185        .await;
10186        (ctx, tmpdir)
10187    }
10188
10189    const GATED_DEP: &str =
10190        "@(added_in = \"3.0\")\nexport fn newFn() {\n  return 42\n}\nexport fn oldFn() {\n  return 1\n}\n";
10191
10192    /// A module's cached outcome keeps the exports of the version it ran
10193    /// under, so an upgrade must rebuild to see newly available functions.
10194    #[tokio::test(flavor = "multi_thread")]
10195    async fn mock_memory_is_rebuilt_on_a_kcl_version_upgrade_with_imports() {
10196        let (ctx, _tmpdir) = mock_context_with_modules(&[("dep.kcl", GATED_DEP)]).await;
10197
10198        let outcome = run_mock_reusing_memory(
10199            &ctx,
10200            "@settings(kclVersion = 2.0)\nimport * from \"dep.kcl\"\nx = oldFn()\n",
10201        )
10202        .await
10203        .unwrap();
10204        assert_eq!(outcome_f64(&outcome, "x"), 1.0);
10205
10206        let outcome = run_mock_reusing_memory(
10207            &ctx,
10208            "@settings(kclVersion = \"3.0-preview\")\nimport * from \"dep.kcl\"\nanswer = newFn()\n",
10209        )
10210        .await
10211        .unwrap();
10212        assert_eq!(outcome_f64(&outcome, "answer"), 42.0);
10213
10214        ctx.close().await;
10215    }
10216
10217    #[tokio::test(flavor = "multi_thread")]
10218    async fn mock_memory_is_rebuilt_on_a_kcl_version_downgrade_with_imports() {
10219        let (ctx, _tmpdir) = mock_context_with_modules(&[("dep.kcl", GATED_DEP)]).await;
10220
10221        let outcome = run_mock_reusing_memory(
10222            &ctx,
10223            "@settings(kclVersion = \"3.0-preview\")\nimport newFn from \"dep.kcl\"\nanswer = newFn()\n",
10224        )
10225        .await
10226        .unwrap();
10227        assert_eq!(outcome_f64(&outcome, "answer"), 42.0);
10228
10229        let err = run_mock_reusing_memory(
10230            &ctx,
10231            "@settings(kclVersion = 2.0)\nimport newFn from \"dep.kcl\"\nanswer = newFn()\n",
10232        )
10233        .await
10234        .unwrap_err();
10235        assert_eq!(err.error.message(), "newFn is not defined in module");
10236
10237        ctx.close().await;
10238    }
10239
10240    /// Only the version decides reuse. Adding the first spline makes the
10241    /// frontend add `experimentalFeatures = allow` and then execute just that
10242    /// sketch, which needs the cached memory and cannot rebuild it.
10243    #[tokio::test(flavor = "multi_thread")]
10244    async fn mock_memory_is_reused_when_only_other_settings_change() {
10245        let ctx = ExecutorContext::new_mock(None).await;
10246
10247        run_mock_reusing_memory(&ctx, "@settings(kclVersion = 2.0)\nx = 2\n")
10248            .await
10249            .unwrap();
10250        let outcome = run_mock_reusing_memory(
10251            &ctx,
10252            "@settings(kclVersion = 2.0, experimentalFeatures = allow)\ny = x\n",
10253        )
10254        .await
10255        .unwrap();
10256        assert_eq!(outcome_f64(&outcome, "y"), 2.0);
10257
10258        ctx.close().await;
10259    }
10260
10261    /// An undeclared version is the default version, so declaring the default
10262    /// explicitly is not a change, while declaring another version is.
10263    #[tokio::test(flavor = "multi_thread")]
10264    async fn mock_memory_treats_an_undeclared_kcl_version_as_the_default() {
10265        let ctx = ExecutorContext::new_mock(None).await;
10266
10267        run_mock_reusing_memory(&ctx, "x = 2\n").await.unwrap();
10268        let outcome = run_mock_reusing_memory(&ctx, "@settings(kclVersion = 1.0)\ny = x\n")
10269            .await
10270            .unwrap();
10271        assert_eq!(outcome_f64(&outcome, "y"), 2.0);
10272        let err = run_mock_reusing_memory(&ctx, "@settings(kclVersion = 2.0)\nz = x\n")
10273            .await
10274            .unwrap_err();
10275        assert_eq!(err.error.message(), "`x` is not defined");
10276
10277        ctx.close().await;
10278    }
10279}