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