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