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