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