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