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;
6use std::sync::atomic::AtomicBool;
7use std::sync::atomic::Ordering;
8
9use anyhow::Result;
10pub use artifact::ArtifactCommand;
11pub(crate) use artifact::EntityCloneInfo;
12pub(crate) use artifact::named_view_artifact;
13pub(crate) use artifact::sketch_block_constraint_type;
14pub use cache::Cache;
15use cache::GlobalState;
16use futures::future::BoxFuture;
17pub use geometry::*;
18pub use id_generator::IdGenerator;
19pub(crate) use import::PreImportedGeometry;
20use indexmap::IndexMap;
21pub use kcl_api::DefaultPlanes;
22use kcl_api::Number;
23use kcl_api::Object;
24use kcl_api::ObjectId;
25pub use kcl_api::Operation;
26pub use kcl_api::artifact::Artifact;
27pub use kcl_api::artifact::ArtifactGraph;
28pub use kcl_api::artifact::CapSubType;
29pub use kcl_api::artifact::CodeRef;
30pub use kcl_api::artifact::GdtAnnotationArtifact;
31pub use kcl_api::artifact::SketchBlock;
32pub use kcl_api::artifact::SketchBlockConstraint;
33#[allow(unused_imports)]
34pub use kcl_api::artifact::SketchBlockConstraintType;
35pub use kcl_api::artifact::StartSketchOnFace;
36pub use kcl_api::artifact::StartSketchOnPlane;
37use kcl_api::ast::node_path::NodePath;
38pub use kcl_value::KclObjectFields;
39pub use kcl_value::KclObjectKind;
40pub use kcl_value::KclValue;
41pub use kcl_value_view::EdgeCutViewExt;
42pub use kcl_value_view::ExtrudeSurfaceViewExt;
43pub use kcl_value_view::KclValueView;
44pub use kcl_value_view::PathViewExt;
45pub use kcl_value_view::SolidViewExt;
46use kcmc::ImageFormat;
47use kcmc::ModelingCmd;
48use kcmc::each_cmd as mcmd;
49use kcmc::ok_response::OkModelingCmdResponse;
50use kcmc::ok_response::output::TakeSnapshot;
51use kcmc::websocket::ModelingSessionData;
52use kcmc::websocket::OkWebSocketResponseData;
53use kittycad_modeling_cmds::id::ModelingCmdId;
54use kittycad_modeling_cmds::{self as kcmc};
55pub use memory::EnvironmentRef;
56#[cfg(test)]
57pub(crate) use memory::MemoryBackendKind;
58pub(crate) use modeling::ModelingCmdMeta;
59pub use named_views::*;
60use serde::Deserialize;
61use serde::Serialize;
62pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
63pub(crate) use sketch_solve::solver_numeric_type;
64pub(crate) use solver_arc::SolverArc;
65pub(crate) use state::ConstraintKey;
66pub(crate) use state::ConstraintState;
67pub(crate) use state::ConsumedRegionInfo;
68pub(crate) use state::ConsumedRegionOperation;
69pub(crate) use state::ConsumedSolidInfo;
70pub(crate) use state::ConsumedSolidKey;
71pub(crate) use state::ConsumedSolidOperation;
72pub use state::DirectTagFilletMeta;
73pub use state::DirectTagFilletTagEntry;
74pub use state::EdgeRefactorMeta;
75pub use state::EdgeRefactorStdlibFn;
76pub use state::ExecState;
77pub use state::KclVersion;
78pub use state::LegacyAngleRefactorMeta;
79pub use state::MetaSettings;
80pub(crate) use state::ModuleArtifactState;
81pub use state::NotYetAdded;
82pub(crate) use state::PendingEdgeRefactorMeta;
83pub(crate) use state::PendingLegacyAngleRefactorMeta;
84pub use state::RefactorMetadata;
85pub(crate) use state::TangencyMode;
86pub(crate) use state::computed_kcl_version;
87pub(crate) use state::declared_kcl_version;
88
89use crate::CompilationIssue;
90use crate::ExecError;
91use crate::KclErrorWithOutputs;
92use crate::NodePathExt;
93use crate::SourceRange;
94use crate::collections::AhashIndexSet;
95use crate::engine::EngineBatchContext;
96use crate::engine::GridScaleBehavior;
97use crate::engine::engine_manager::EngineManager;
98use crate::errors::KclError;
99use crate::errors::KclErrorDetails;
100use crate::execution::cache::CacheInformation;
101use crate::execution::cache::CacheResult;
102use crate::execution::cad_op::OperationExt;
103use crate::execution::import_graph::Universe;
104use crate::execution::import_graph::UniverseMap;
105use crate::execution::typed_path::TypedPath;
106use crate::front::PointExt;
107use crate::front::SegmentExt;
108use crate::fs::FileManager;
109use crate::fs::FileSystemHandle;
110use crate::modules::ModuleExecutionOutcome;
111use crate::modules::ModuleId;
112use crate::modules::ModulePath;
113use crate::modules::ModulePathExt as _;
114use crate::modules::ModuleRepr;
115use crate::modules::ModuleSource;
116use crate::named_views::values as named_views;
117use crate::parsing::ast::types::Expr;
118use crate::parsing::ast::types::ImportPath;
119use crate::parsing::ast::types::NodeRef;
120
121#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
122#[ts(export)]
123pub struct OperationsByModule {
124    pub map: IndexMap<ModuleId, Vec<Operation>>,
125}
126
127#[derive(Clone, Serialize, ts_rs::TS)]
128#[ts(export)]
129#[serde(rename_all = "camelCase")]
130pub struct OperationCallbackArgs {
131    pub module_id: ModuleId,
132    pub operation: Operation,
133    pub index: usize,
134}
135
136pub trait ExecutionCallbacks: std::fmt::Debug + Send + Sync + 'static {
137    fn on_operation(&self, _args: OperationCallbackArgs) {}
138}
139
140impl OperationsByModule {
141    pub fn count(&self) -> usize {
142        self.map.values().map(Vec::len).sum()
143    }
144
145    pub fn is_empty(&self) -> bool {
146        self.map.values().all(Vec::is_empty)
147    }
148
149    pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
150        self.map.get(module_id)
151    }
152
153    pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
154        self.map.values()
155    }
156
157    pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
158        self.map.insert(module_id, operations);
159    }
160}
161
162pub(crate) mod annotations;
163mod artifact;
164#[cfg(test)]
165pub(crate) use artifact::mermaid_tests::ArtifactGraphMermaidExt;
166pub(crate) mod cache;
167mod cad_op;
168pub(crate) mod exec_ast;
169pub mod fn_call;
170#[cfg(test)]
171mod freedom_analysis_tests;
172mod geometry;
173#[cfg(test)]
174mod hide_id_contract_kcl_test_pins;
175mod id_generator;
176mod import;
177mod import_graph;
178pub(crate) mod kcl_value;
179pub(crate) mod kcl_value_view;
180pub(crate) mod machine;
181mod memory;
182mod modeling;
183#[cfg(not(target_arch = "wasm32"))]
184mod remote;
185mod sketch_solve;
186mod solver_arc;
187mod state;
188pub use kcl_api::typed_path;
189pub(crate) mod types;
190
191pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
192pub(crate) const SKETCH_OBJECT_META: &str = "meta";
193pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
194
195/// Convenience macro for handling [`KclValueControlFlow`] in execution by
196/// returning early if it is some kind of early return or stripping off the
197/// control flow otherwise. If it's an early return, it's returned as a
198/// `Result::Ok`.
199macro_rules! control_normal {
200    ($control_flow:expr) => {{
201        let cf = $control_flow;
202        if cf.is_some_return() {
203            return Ok(cf);
204        } else {
205            cf.into_value()
206        }
207    }};
208}
209// Expose the macro to other modules.
210pub(crate) use control_normal;
211
212/// Convenience macro for handling [`KclValueControlFlow`] in execution by
213/// returning early if it is some kind of early return or stripping off the
214/// control flow otherwise. If it's an early return, [`EarlyReturn`] is
215/// used to return it as a `Result::Err`.
216macro_rules! early_return {
217    ($control_flow:expr) => {{
218        let cf = $control_flow;
219        if cf.is_some_return() {
220            return Err(EarlyReturn::from(cf));
221        } else {
222            cf.into_value()
223        }
224    }};
225}
226// Expose the macro to other modules.
227pub(crate) use early_return;
228
229#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
230pub enum ControlFlowKind {
231    /// Normal control flow. Continue to the next step.
232    #[default]
233    Normal,
234    /// A `return` statement executed under KCL 3.0: unwind to the nearest
235    /// function-call boundary, which absorbs it as the function's result. Never
236    /// constructed under older entry points, whose `return` uses
237    /// write-and-continue semantics instead; see `bind_return_value`.
238    Return,
239    /// `exit()` was called: unwind all the way to the program root, bypassing
240    /// function-call boundaries.
241    Exit,
242}
243
244impl ControlFlowKind {
245    /// Returns true if this is any kind of early return.
246    pub fn is_some_return(&self) -> bool {
247        match self {
248            ControlFlowKind::Normal => false,
249            ControlFlowKind::Return => true,
250            ControlFlowKind::Exit => true,
251        }
252    }
253}
254
255#[must_use = "You should always handle the control flow value when it is returned"]
256#[derive(Debug, Clone, PartialEq, Serialize)]
257pub struct KclValueControlFlow {
258    /// Use [control_normal] or [Self::into_value] to get the value.
259    value: Box<KclValue>,
260    pub control: ControlFlowKind,
261}
262
263impl KclValue {
264    pub(crate) fn normal(self) -> KclValueControlFlow {
265        KclValueControlFlow {
266            value: Box::new(self),
267            control: ControlFlowKind::Normal,
268        }
269    }
270
271    pub(crate) fn return_(self) -> KclValueControlFlow {
272        KclValueControlFlow {
273            value: Box::new(self),
274            control: ControlFlowKind::Return,
275        }
276    }
277
278    pub(crate) fn exit(self) -> KclValueControlFlow {
279        KclValueControlFlow {
280            value: Box::new(self),
281            control: ControlFlowKind::Exit,
282        }
283    }
284}
285
286impl KclValueControlFlow {
287    /// Returns true if this is any kind of early return.
288    pub fn is_some_return(&self) -> bool {
289        self.control.is_some_return()
290    }
291
292    pub(crate) fn is_return(&self) -> bool {
293        matches!(self.control, ControlFlowKind::Return)
294    }
295
296    pub(crate) fn is_exit(&self) -> bool {
297        matches!(self.control, ControlFlowKind::Exit)
298    }
299
300    /// The source ranges of the wrapped value, for error reporting.
301    pub(crate) fn source_ranges(&self) -> Vec<SourceRange> {
302        let deduplicated = self
303            .value
304            .metadata()
305            .into_iter()
306            .map(|m| m.source_range)
307            .collect::<AhashIndexSet<_>>();
308        deduplicated.into_iter().collect()
309    }
310
311    pub(crate) fn into_value(self) -> KclValue {
312        *self.value
313    }
314}
315
316/// A [`KclValueControlFlow`] or an error that needs to be returned early. This
317/// is useful for when functions might encounter either control flow or errors
318/// that need to bubble up early, but these aren't the primary return values of
319/// the function. We can use `EarlyReturn` as the error type in a `Result`.
320///
321/// Normally, you don't construct this directly. Use the `early_return!` macro.
322#[must_use = "You should always handle the control flow value when it is returned"]
323#[allow(clippy::large_enum_variant)]
324#[derive(Debug, Clone)]
325pub(crate) enum EarlyReturn {
326    /// A normal value with control flow.
327    Value(KclValueControlFlow),
328    /// An error that occurred during execution.
329    Error(KclError),
330}
331
332impl From<KclValueControlFlow> for EarlyReturn {
333    fn from(cf: KclValueControlFlow) -> Self {
334        EarlyReturn::Value(cf)
335    }
336}
337
338impl From<KclError> for EarlyReturn {
339    fn from(err: KclError) -> Self {
340        EarlyReturn::Error(err)
341    }
342}
343
344pub(crate) enum StatementKind<'a> {
345    Declaration { name: &'a str },
346    Expression,
347}
348
349#[derive(Debug, Clone, Copy)]
350pub enum PreserveMem {
351    Normal,
352    Always,
353}
354
355impl PreserveMem {
356    fn normal(self) -> bool {
357        match self {
358            PreserveMem::Normal => true,
359            PreserveMem::Always => false,
360        }
361    }
362}
363
364/// Outcome of executing a program.  This is used in TS.
365#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
366#[ts(export)]
367#[serde(rename_all = "camelCase")]
368pub struct ExecOutcome {
369    /// Variables in the top-level of the root module. Note that functions will have an invalid env ref.
370    pub variables: IndexMap<String, KclValueView>,
371    /// Runtime memory retained only for tests that need to verify internal behavior.
372    #[cfg(test)]
373    #[serde(skip)]
374    #[ts(skip)]
375    pub(crate) test_program_memory: IndexMap<String, KclValue>,
376    /// Operations that have been performed in execution order, grouped by
377    /// owning module id, for display in the Feature Tree.
378    pub operations: OperationsByModule,
379    /// Output artifact graph.
380    pub artifact_graph: ArtifactGraph,
381    /// Objects in the scene, created from execution.
382    #[serde(skip)]
383    pub scene_objects: Vec<Object>,
384    /// Map from source range to object ID for lookup of objects by their source
385    /// range.
386    #[serde(skip)]
387    pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
388    #[serde(skip)]
389    pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
390    /// Execution-backed metadata used by Z0006 and future auto-refactors.
391    pub refactor_metadata: Vec<RefactorMetadata>,
392    /// Non-fatal errors and warnings.
393    pub issues: Vec<CompilationIssue>,
394    /// File Names in module Id array index order
395    pub filenames: IndexMap<ModuleId, ModulePath>,
396    /// The engine scene generation when this outcome was produced: the number of
397    /// runs so far that changed the engine scene. Runs answered from the cache
398    /// and mock runs leave it unchanged.
399    pub engine_scene_generation: usize,
400    /// Source code of each module, for rendering issues against the module
401    /// their source range points into. Not serialized to keep the WASM
402    /// payload small; native callers (e.g. the Python bindings) read it
403    /// directly.
404    #[serde(skip)]
405    pub source_files: IndexMap<ModuleId, ModuleSource>,
406    /// The default planes.
407    pub default_planes: Option<DefaultPlanes>,
408}
409
410/// Per-segment freedom used by the constraint report. Mirrors
411/// [`kcl_api::Freedom`] but adds an `Error` variant for when
412/// a point lookup fails.
413#[derive(Debug, Clone, Copy, PartialEq)]
414enum SegmentFreedom {
415    Free,
416    Fixed,
417    Conflict,
418    /// A required point could not be found in the scene graph.
419    Error,
420}
421
422impl From<kcl_api::Freedom> for SegmentFreedom {
423    fn from(f: kcl_api::Freedom) -> Self {
424        match f {
425            kcl_api::Freedom::Free => Self::Free,
426            kcl_api::Freedom::Fixed => Self::Fixed,
427            kcl_api::Freedom::Conflict => Self::Conflict,
428        }
429    }
430}
431
432/// Overall constraint status of a sketch.
433#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
434pub enum ConstraintKind {
435    FullyConstrained,
436    UnderConstrained,
437    OverConstrained,
438    /// Analysis could not determine constraint status (e.g., a point lookup
439    /// failed due to an inconsistent scene graph). Callers decide how to treat
440    /// this — as under-constrained, over-constrained, or something else.
441    Error,
442}
443
444/// Per-sketch summary of constraint freedom analysis.
445///
446/// A sketch with no countable segments (`total_count == 0`) is reported as
447/// [`ConstraintKind::FullyConstrained`]. This is vacuously true — there are
448/// no free or conflicting segments. Callers can check `total_count == 0` to
449/// distinguish this from a genuinely constrained sketch.
450#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
451pub struct SketchConstraintStatus {
452    /// Name of the variable the sketch was assigned to, for example
453    /// "sketch001". This is the nearest enclosing declaration at the point the
454    /// sketch was created, which is not always the sketch's own name:
455    /// - Empty for a sketch written as an expression statement, because there
456    ///   is no enclosing declaration.
457    /// - The outer variable's name for a sketch passed straight into another
458    ///   call, as in `part = extrude(sketch(on = XY) { ... }, length = 10)`.
459    /// - The same name for two sketches, when a function body declares the
460    ///   sketch and is called more than once.
461    ///
462    /// This name is accepted by [`ExecOutcome::render_sketch_png_instance`].
463    pub name: String,
464    /// Zero-based creation order among sketches with this name, independent of
465    /// constraint-status grouping. Not stable across edits.
466    pub instance_index: usize,
467    /// Overall constraint status derived from per-segment freedom.
468    pub status: ConstraintKind,
469    /// Number of segments that are under-constrained (free to move).
470    pub free_count: usize,
471    /// Number of segments that are over-constrained (conflicting constraints).
472    pub conflict_count: usize,
473    /// Total number of segments analyzed.
474    pub total_count: usize,
475}
476
477/// Grouped report of all sketches by constraint status.
478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
479pub struct SketchConstraintReport {
480    pub fully_constrained: Vec<SketchConstraintStatus>,
481    pub under_constrained: Vec<SketchConstraintStatus>,
482    pub over_constrained: Vec<SketchConstraintStatus>,
483    /// Sketches where analysis encountered an error (e.g., a point lookup
484    /// failed). Callers decide how to treat these.
485    pub errors: Vec<SketchConstraintStatus>,
486}
487
488/// Compute the constraint status for a single sketch object.
489///
490/// Returns `None` if `sketch_obj` is not a sketch.
491///
492/// Note: a sketch with no countable segments (`total_count == 0`) is reported
493/// as [`ConstraintKind::FullyConstrained`]. This is vacuously true — there are
494/// no free or conflicting segments. Callers can check `total_count == 0` to
495/// distinguish this from a genuinely constrained sketch.
496pub(crate) fn sketch_constraint_status_for_sketch(
497    scene_objects: &[Object],
498    sketch_obj: &Object,
499) -> Option<SketchConstraintStatus> {
500    use kcl_api::ObjectKind;
501    use kcl_api::Segment;
502
503    let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
504        return None;
505    };
506
507    // Closure to look up a point's freedom by ObjectId.
508    let lookup = |id: ObjectId| -> Option<kcl_api::Freedom> {
509        let obj = scene_objects.get(id.0)?;
510        if let ObjectKind::Segment {
511            segment: Segment::Point(p),
512        } = &obj.kind
513        {
514            Some(p.freedom())
515        } else {
516            None
517        }
518    };
519
520    let mut free_count: usize = 0;
521    let mut conflict_count: usize = 0;
522    let mut error_count: usize = 0;
523    let mut total_count: usize = 0;
524
525    for &seg_id in &sketch.segments {
526        let Some(seg_obj) = scene_objects.get(seg_id.0) else {
527            continue;
528        };
529        let ObjectKind::Segment { segment } = &seg_obj.kind else {
530            continue;
531        };
532        // Skip owned points — their freedom is already captured by
533        // the parent geometry (Line/Arc/Circle) that looks them up.
534        if let Segment::Point(p) = segment
535            && p.owner.is_some()
536        {
537            continue;
538        }
539        let freedom = segment
540            .freedom(lookup)
541            .map(SegmentFreedom::from)
542            .unwrap_or(SegmentFreedom::Error);
543        total_count += 1;
544        match freedom {
545            SegmentFreedom::Free => free_count += 1,
546            SegmentFreedom::Conflict => conflict_count += 1,
547            SegmentFreedom::Error => error_count += 1,
548            SegmentFreedom::Fixed => {}
549        }
550    }
551
552    let status = if error_count > 0 {
553        ConstraintKind::Error
554    } else if conflict_count > 0 {
555        ConstraintKind::OverConstrained
556    } else if free_count > 0 {
557        ConstraintKind::UnderConstrained
558    } else {
559        ConstraintKind::FullyConstrained
560    };
561
562    Some(SketchConstraintStatus {
563        name: sketch_obj.label.clone(),
564        instance_index: 0,
565        status,
566        free_count,
567        conflict_count,
568        total_count,
569    })
570}
571
572pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
573    let mut fully_constrained = Vec::new();
574    let mut under_constrained = Vec::new();
575    let mut over_constrained = Vec::new();
576    let mut errors = Vec::new();
577    let mut instance_counts = std::collections::HashMap::new();
578    for obj in scene_objects {
579        let Some(mut entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
580            continue;
581        };
582        let count = instance_counts.entry(entry.name.clone()).or_insert(0);
583        entry.instance_index = *count;
584        *count += 1;
585        match entry.status {
586            ConstraintKind::FullyConstrained => fully_constrained.push(entry),
587            ConstraintKind::UnderConstrained => under_constrained.push(entry),
588            ConstraintKind::OverConstrained => over_constrained.push(entry),
589            ConstraintKind::Error => errors.push(entry),
590        }
591    }
592
593    SketchConstraintReport {
594        fully_constrained,
595        under_constrained,
596        over_constrained,
597        errors,
598    }
599}
600
601impl ExecOutcome {
602    pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
603        debug_assert!(
604            id.0 < self.scene_objects.len(),
605            "Requested object ID {} but only have {} objects",
606            id.0,
607            self.scene_objects.len()
608        );
609        self.scene_objects.get(id.0)
610    }
611
612    /// Returns non-fatal errors. Warnings are not included.
613    pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
614        self.issues.iter().filter(|error| error.is_err())
615    }
616
617    /// Analyze all sketches in the execution result and group them by
618    /// constraint status (fully, under, or over constrained).
619    ///
620    /// Each segment in a sketch computes its own freedom by looking up the
621    /// freedom of its constituent points. Owned points (belonging to a
622    /// Line/Arc/Circle) are skipped to avoid double-counting.
623    pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
624        sketch_constraint_report_from_scene_objects(&self.scene_objects)
625    }
626
627    /// Render one sketch from this execution result as a PNG, colored by
628    /// solver freedom.
629    pub fn render_sketch_png(
630        &self,
631        sketch_name: &str,
632    ) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
633        self.render_sketch_png_instance(sketch_name, None)
634    }
635
636    /// Render a named sketch using an optional instance index from its report.
637    /// Without an index, the name must be unique.
638    pub fn render_sketch_png_instance(
639        &self,
640        sketch_name: &str,
641        instance_index: Option<usize>,
642    ) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
643        render_sketch_png_from_scene_objects(&self.scene_objects, sketch_name, instance_index)
644    }
645}
646
647pub(crate) fn render_sketch_png_from_scene_objects(
648    scene_objects: &[kcl_api::Object],
649    sketch_name: &str,
650    instance_index: Option<usize>,
651) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
652    use kcl_api::ObjectKind;
653
654    use crate::tooling::sketch_visualizer::SketchVisualizationError;
655
656    let sketches_matching_name = scene_objects
657        .iter()
658        .filter_map(|object| match &object.kind {
659            ObjectKind::Sketch(sketch) if object.label == sketch_name => Some(sketch),
660            _ => None,
661        })
662        .collect::<Vec<_>>();
663
664    // Select the correct sketch, from all sketches matching the name.
665    let sketch = match (sketches_matching_name.as_slice(), instance_index) {
666        // No sketches matched the name.
667        ([], _) => {
668            return Err(SketchVisualizationError::SketchNotFound {
669                name: sketch_name.to_owned(),
670            });
671        }
672        // At least one sketch matched the name, and the user gave an index.
673        (_nonempty, Some(index)) => {
674            *sketches_matching_name
675                .get(index)
676                .ok_or_else(|| SketchVisualizationError::InstanceNotFound {
677                    name: sketch_name.to_owned(),
678                    index,
679                    count: sketches_matching_name.len(),
680                })?
681        }
682        // Exactly one sketch matched the name, the user didn't need any
683        // index because there is no ambiguity about which sketch.
684        ([sketch], None) => *sketch,
685
686        // More than one sketch matched the name, but there's no index
687        // to disambiguate.
688        (_nonempty, None) => {
689            return Err(SketchVisualizationError::AmbiguousSketchName {
690                name: sketch_name.to_owned(),
691                count: sketches_matching_name.len(),
692            });
693        }
694    };
695
696    // Now that we've selected the right sketch, visualize it.
697    crate::tooling::sketch_visualizer::render_sketch_png(scene_objects, sketch)
698}
699
700/// Configuration for mock execution.
701#[derive(Debug, Clone, PartialEq)]
702pub struct MockConfig {
703    pub use_prev_memory: bool,
704    /// The `ObjectId` of the sketch block to execute for sketch mode. Only the
705    /// specified sketch block will be executed. All other code is ignored.
706    pub sketch_block_id: Option<ObjectId>,
707    /// True to do more costly analysis of whether the sketch block segments are
708    /// under-constrained.
709    pub freedom_analysis: bool,
710    /// The segments that were edited that triggered this execution.
711    pub segment_ids_edited: AhashIndexSet<ObjectId>,
712    /// Segment-body drag anchors that temporarily pull a point on a segment toward the cursor.
713    pub drag_anchors: Vec<SegmentDragAnchor>,
714}
715
716#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
717#[ts(export, export_to = "FrontendApi.ts")]
718#[serde(rename_all = "camelCase")]
719pub struct SegmentDragAnchor {
720    pub segment_id: ObjectId,
721    pub target: kcl_api::Point2d<Number>,
722}
723
724impl Default for MockConfig {
725    fn default() -> Self {
726        Self {
727            // By default, use previous memory. This is usually what you want.
728            use_prev_memory: true,
729            sketch_block_id: None,
730            freedom_analysis: true,
731            segment_ids_edited: AhashIndexSet::default(),
732            drag_anchors: Vec::new(),
733        }
734    }
735}
736
737impl MockConfig {
738    /// Create a new mock config for sketch mode.
739    pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
740        Self {
741            sketch_block_id: Some(sketch_block_id),
742            ..Default::default()
743        }
744    }
745
746    #[must_use]
747    pub(crate) fn no_freedom_analysis(mut self) -> Self {
748        self.freedom_analysis = false;
749        self
750    }
751}
752
753#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
754#[ts(export)]
755#[serde(tag = "type", rename_all = "camelCase")]
756pub struct TagIdentifier {
757    pub value: String,
758    // Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info
759    // was written.
760    #[serde(skip)]
761    pub info: Vec<(usize, TagEngineInfo)>,
762    #[serde(skip)]
763    pub meta: Vec<Metadata>,
764}
765
766impl TagIdentifier {
767    /// Get the tag info for this tag at a specified epoch.
768    pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
769        for (e, info) in self.info.iter().rev() {
770            if *e <= at_epoch {
771                return Some(info);
772            }
773        }
774
775        None
776    }
777
778    /// Get the most recent tag info for this tag.
779    pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
780        self.info.last().map(|i| &i.1)
781    }
782
783    /// Get all tag info entries at the most recent epoch.
784    /// For region-mapped tags, this returns multiple entries (one per region segment).
785    pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
786        let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
787            return vec![];
788        };
789        self.info
790            .iter()
791            .rev()
792            .take_while(|(e, _)| *e == cur_epoch)
793            .map(|(_, info)| info)
794            .collect()
795    }
796
797    /// Add info from a different instance of this tag.
798    pub fn merge_info(&mut self, other: &TagIdentifier) {
799        assert_eq!(&self.value, &other.value);
800        for (oe, ot) in &other.info {
801            if let Some((e, t)) = self.info.last_mut() {
802                // If there is newer info, then skip this iteration.
803                if *e > *oe {
804                    continue;
805                }
806                // If we're in the same epoch, then overwrite.
807                if e == oe {
808                    *t = ot.clone();
809                    continue;
810                }
811            }
812            self.info.push((*oe, ot.clone()));
813        }
814    }
815
816    pub fn geometry(&self) -> Option<Geometry> {
817        self.get_cur_info().map(|info| info.geometry.clone())
818    }
819
820    pub(crate) fn is_body_created_tag(&self) -> bool {
821        self.get_cur_info().is_some_and(|info| {
822            matches!(&info.geometry, Geometry::Solid(_)) && info.path.is_none() && info.surface.is_some()
823        })
824    }
825}
826
827impl Eq for TagIdentifier {}
828
829impl std::fmt::Display for TagIdentifier {
830    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
831        write!(f, "{}", self.value)
832    }
833}
834
835impl std::str::FromStr for TagIdentifier {
836    type Err = KclError;
837
838    fn from_str(s: &str) -> Result<Self, Self::Err> {
839        Ok(Self {
840            value: s.to_string(),
841            info: Vec::new(),
842            meta: Default::default(),
843        })
844    }
845}
846
847impl Ord for TagIdentifier {
848    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
849        self.value.cmp(&other.value)
850    }
851}
852
853impl PartialOrd for TagIdentifier {
854    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
855        Some(self.cmp(other))
856    }
857}
858
859impl std::hash::Hash for TagIdentifier {
860    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
861        self.value.hash(state);
862    }
863}
864
865/// Engine information for a tag.
866#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
867#[ts(export)]
868#[serde(tag = "type", rename_all = "camelCase")]
869pub struct TagEngineInfo {
870    /// The id of the tagged object.
871    pub id: uuid::Uuid,
872    /// The geometry the tag is on.
873    pub geometry: Geometry,
874    /// The path the tag is on.
875    pub path: Option<Path>,
876    /// The surface information for the tag.
877    pub surface: Option<ExtrudeSurface>,
878}
879
880#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
881pub enum BodyType {
882    Root,
883    Block,
884}
885
886/// Metadata.
887#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
888#[ts(export)]
889#[serde(rename_all = "camelCase")]
890pub struct Metadata {
891    /// The source range.
892    pub source_range: SourceRange,
893}
894
895impl From<Metadata> for Vec<SourceRange> {
896    fn from(meta: Metadata) -> Self {
897        vec![meta.source_range]
898    }
899}
900
901impl From<&Metadata> for SourceRange {
902    fn from(meta: &Metadata) -> Self {
903        meta.source_range
904    }
905}
906
907impl From<SourceRange> for Metadata {
908    fn from(source_range: SourceRange) -> Self {
909        Self { source_range }
910    }
911}
912
913impl<T> From<NodeRef<'_, T>> for Metadata {
914    fn from(node: NodeRef<'_, T>) -> Self {
915        Self {
916            source_range: SourceRange::new(node.start, node.end, node.module_id),
917        }
918    }
919}
920
921impl From<&Expr> for Metadata {
922    fn from(expr: &Expr) -> Self {
923        Self {
924            source_range: SourceRange::from(expr),
925        }
926    }
927}
928
929impl Metadata {
930    pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> kcl_api::SourceRef {
931        if meta.len() == 1 {
932            let meta = &meta[0];
933            return kcl_api::SourceRef::Simple {
934                range: meta.source_range,
935                node_path,
936            };
937        }
938        kcl_api::SourceRef::BackTrace {
939            ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
940        }
941    }
942}
943
944/// The type of ExecutorContext being used
945#[derive(PartialEq, Debug, Default, Clone)]
946pub enum ContextType {
947    /// Live engine connection
948    #[default]
949    Live,
950
951    /// Completely mocked connection
952    /// Mock mode is only for the Design Studio when they just want to mock engine calls and not
953    /// actually make them.
954    Mock,
955
956    /// Handled by some other interpreter/conversion system
957    MockCustomForwarded,
958}
959
960/// The executor context.
961/// Cloning will return another handle to the same engine connection/session,
962/// as this uses `Arc` under the hood.
963#[derive(Clone)]
964pub struct ExecutorContext {
965    pub engine: Arc<EngineManager>,
966    /// Shared session cache, also used by associated mock execution.
967    pub cache: Cache,
968    pub engine_batch: EngineBatchContext,
969    pub fs: FileSystemHandle,
970    pub settings: ExecutorSettings,
971    pub context_type: ContextType,
972    pub execution_callbacks: Option<Arc<dyn ExecutionCallbacks>>,
973    /// Call-depth limit for the machine executor's runaway-recursion guard.
974    /// Crate-internal policy, not user configuration.
975    pub(crate) machine_call_depth_limit: usize,
976    /// If true, send BeginExecution and EndExecution before/after executing
977    /// KCL. This might need to be false if the engine server is disabling
978    /// rendering because it's in a headless context.
979    pub configure_engine_render: bool,
980    /// If the last execution needed order-independent transparency (OIT).
981    /// In a CPU-only engine, this to see if you should enable OIT in the engine.
982    /// In a GPU-enabled engine, this is done for you by the KCL runtime during the `appearance` function.
983    pub(crate) needs_oit: Arc<AtomicBool>,
984}
985
986impl std::fmt::Debug for ExecutorContext {
987    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
988        f.debug_struct("ExecutorContext")
989            .field("engine", &self.engine)
990            .field("engine_batch", &self.engine_batch)
991            .field("settings", &self.settings)
992            .field("context_type", &self.context_type)
993            .field("needs_oit", &self.needs_oit)
994            .field("execution_callbacks", &self.execution_callbacks)
995            .field("machine_call_depth_limit", &self.machine_call_depth_limit)
996            .finish()
997    }
998}
999
1000/// The executor settings.
1001#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
1002#[ts(export)]
1003pub struct ExecutorSettings {
1004    /// Highlight edges of 3D objects?
1005    pub highlight_edges: bool,
1006    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
1007    pub enable_ssao: bool,
1008    /// Show grid?
1009    pub show_grid: bool,
1010    /// Should engine store this for replay?
1011    /// If so, under what name?
1012    pub replay: Option<String>,
1013    /// The directory of the current project.  This is used for resolving import
1014    /// paths.  If None is given, the current working directory is used.
1015    pub project_directory: Option<TypedPath>,
1016    /// This is the path to the current file being executed.
1017    /// We use this for preventing cyclic imports.
1018    pub current_file: Option<TypedPath>,
1019    /// Whether or not to automatically scale the grid when user zooms.
1020    pub fixed_size_grid: bool,
1021    /// Skip sending the engine messages that are only needed to build the
1022    /// artifact graph. When this is true, the artifact graph will be
1023    /// incomplete. So you should only use this option if you know you don't
1024    /// need the artifact graph or anything that depends on it. In that case,
1025    /// skipping these commands can make execution slightly faster.
1026    #[serde(default, skip_serializing_if = "is_false")]
1027    pub skip_artifact_graph: bool,
1028    /// If Some(N), sends a heartbeat to keep the WebSocket active, every N seconds.
1029    /// If None, no heartbeats will be sent.
1030    #[serde(default, skip_serializing_if = "Option::is_none")]
1031    pub heartbeats: Option<u64>,
1032    /// If given, sets the default backface colour.
1033    /// If not, defaults to whatever the engine's default is.
1034    #[serde(default, skip_serializing_if = "Option::is_none")]
1035    pub default_backface_color: Option<String>,
1036    /// If given, sets a custom engine pool.
1037    #[serde(default, skip_serializing_if = "Option::is_none")]
1038    pub pool: Option<String>,
1039    /// If given, sets the Engine video width in pixels.
1040    #[serde(default, skip_serializing_if = "Option::is_none")]
1041    pub video_res_width: Option<u32>,
1042    /// If given, sets the Engine video height in pixels.
1043    #[serde(default, skip_serializing_if = "Option::is_none")]
1044    pub video_res_height: Option<u32>,
1045    /// asks the engine for geometry only mode - no video stream
1046    pub geometry_only: bool,
1047}
1048
1049fn is_false(b: &bool) -> bool {
1050    !*b
1051}
1052
1053impl Default for ExecutorSettings {
1054    fn default() -> Self {
1055        Self {
1056            highlight_edges: true,
1057            enable_ssao: false,
1058            show_grid: false,
1059            replay: None,
1060            project_directory: None,
1061            current_file: None,
1062            fixed_size_grid: true,
1063            skip_artifact_graph: false,
1064            heartbeats: None,
1065            default_backface_color: None,
1066            pool: None,
1067            video_res_width: None,
1068            video_res_height: None,
1069            geometry_only: false,
1070        }
1071    }
1072}
1073
1074impl From<crate::settings::types::Configuration> for ExecutorSettings {
1075    fn from(config: crate::settings::types::Configuration) -> Self {
1076        Self::from(config.settings)
1077    }
1078}
1079
1080impl From<crate::settings::types::Settings> for ExecutorSettings {
1081    fn from(settings: crate::settings::types::Settings) -> Self {
1082        let modeling_settings = settings.modeling.unwrap_or_default();
1083        Self {
1084            highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
1085            enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
1086            show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
1087            replay: None,
1088            project_directory: None,
1089            current_file: None,
1090            fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
1091            skip_artifact_graph: false,
1092            heartbeats: None,
1093            default_backface_color: modeling_settings.backface_color.map(|color| color.0),
1094            pool: None,
1095            video_res_width: None,
1096            video_res_height: None,
1097            geometry_only: false,
1098        }
1099    }
1100}
1101
1102impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
1103    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
1104        Self::from(config.settings.modeling)
1105    }
1106}
1107
1108impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
1109    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
1110        Self {
1111            highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
1112            enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
1113            show_grid: modeling.show_scale_grid.unwrap_or_default(),
1114            replay: None,
1115            project_directory: None,
1116            current_file: None,
1117            fixed_size_grid: true,
1118            skip_artifact_graph: false,
1119            heartbeats: None,
1120            default_backface_color: modeling.backface_color.map(|color| color.0),
1121            pool: None,
1122            video_res_width: None,
1123            video_res_height: None,
1124            geometry_only: false,
1125        }
1126    }
1127}
1128
1129impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
1130    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
1131        Self {
1132            highlight_edges: modeling.highlight_edges.into(),
1133            enable_ssao: modeling.enable_ssao.into(),
1134            show_grid: Default::default(),
1135            replay: None,
1136            project_directory: None,
1137            current_file: None,
1138            fixed_size_grid: true,
1139            skip_artifact_graph: false,
1140            heartbeats: None,
1141            default_backface_color: None,
1142            pool: None,
1143            video_res_width: None,
1144            video_res_height: None,
1145            geometry_only: false,
1146        }
1147    }
1148}
1149
1150impl ExecutorSettings {
1151    /// Add the current file path to the executor settings.
1152    pub fn with_current_file(&mut self, current_file: TypedPath) {
1153        // We want the parent directory of the file.
1154        if current_file.extension() == Some("kcl") {
1155            self.current_file = Some(current_file.clone());
1156            // Get the parent directory.
1157            if let Some(parent) = current_file.parent() {
1158                self.project_directory = Some(parent);
1159            } else {
1160                self.project_directory = Some(TypedPath::from(""));
1161            }
1162        } else {
1163            self.project_directory = Some(current_file);
1164        }
1165    }
1166}
1167
1168/// Either reuse a complete outcome or pass owned state into execution.
1169/// Keep the execution state boxed when transferring it between phases.
1170enum PreparedCachedExecution {
1171    Cached(Box<ExecOutcome>),
1172    Execute(Box<CachedExecution>),
1173}
1174
1175struct CachedExecution {
1176    program: crate::Program,
1177    exec_state: Box<ExecState>,
1178    universe_info: Option<(Universe, UniverseMap)>,
1179    preserve_mem: PreserveMem,
1180}
1181
1182impl ExecutorContext {
1183    /// Use an existing cache for another handle to the same session, or for
1184    /// associated mock execution. Independent sessions must use independent caches.
1185    pub fn with_cache(mut self, cache: Cache) -> Self {
1186        self.cache = cache;
1187        self
1188    }
1189
1190    /// Create a new live executor context from an engine and file manager.
1191    pub fn new_with_engine_and_fs(
1192        engine: Arc<EngineManager>,
1193        fs: FileSystemHandle,
1194        settings: ExecutorSettings,
1195    ) -> Self {
1196        ExecutorContext {
1197            engine,
1198            cache: Cache::default(),
1199            engine_batch: EngineBatchContext::default(),
1200            fs,
1201            settings,
1202            context_type: ContextType::Live,
1203            execution_callbacks: Default::default(),
1204            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1205            configure_engine_render: true,
1206            needs_oit: Default::default(),
1207        }
1208    }
1209
1210    fn clone_with_fresh_execution_batch(&self) -> Self {
1211        Self {
1212            engine: self.engine.clone(),
1213            cache: self.cache.clone(),
1214            engine_batch: EngineBatchContext::new(),
1215            fs: self.fs.clone(),
1216            settings: self.settings.clone(),
1217            context_type: self.context_type.clone(),
1218            execution_callbacks: self.execution_callbacks.clone(),
1219            machine_call_depth_limit: self.machine_call_depth_limit,
1220            configure_engine_render: true,
1221            needs_oit: self.needs_oit.clone(),
1222        }
1223    }
1224
1225    /// Create a new live executor context from an engine using the local file manager.
1226    #[cfg(not(target_arch = "wasm32"))]
1227    pub fn new_with_engine(engine: Arc<EngineManager>, settings: ExecutorSettings) -> Self {
1228        Self::new_with_engine_and_fs(engine, crate::fs::new_file_system_handle(FileManager::new()), settings)
1229    }
1230
1231    /// Toggle on engine graphics.
1232    /// Make sure to disable them again when you're done.
1233    pub async fn enable_engine_graphics(&self, exec_state: &mut ExecState) -> Result<()> {
1234        let source_range = Default::default();
1235        self.engine
1236            .send_modeling_cmd(
1237                &self.engine_batch,
1238                exec_state.next_uuid(),
1239                source_range,
1240                &ModelingCmd::ToggleGraphics(kcmc::ToggleGraphics::enabled(true)),
1241            )
1242            .await
1243            .map(|_| ())?;
1244
1245        if self.needs_oit.load(Ordering::Relaxed) {
1246            self.engine
1247                .send_modeling_cmd(
1248                    &self.engine_batch,
1249                    exec_state.next_uuid(),
1250                    source_range,
1251                    &ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(true).build()),
1252                )
1253                .await?;
1254        }
1255
1256        Ok(())
1257    }
1258
1259    pub async fn enable_engine_graphics_settings(&self, exec_state: &mut ExecState) -> Result<()> {
1260        let source_range = Default::default();
1261        let grid_scale_unit = if self.settings.fixed_size_grid {
1262            GridScaleBehavior::Fixed(Some(exec_state.length_unit()))
1263        } else {
1264            GridScaleBehavior::ScaleWithZoom
1265        };
1266        self.engine
1267            .apply_graphics_settings(
1268                &self.engine_batch,
1269                &self.settings,
1270                source_range,
1271                exec_state.id_generator(),
1272                grid_scale_unit,
1273            )
1274            .await?;
1275        Ok(())
1276    }
1277
1278    pub async fn disable_engine_graphics(&self, exec_state: &mut ExecState) -> Result<()> {
1279        let source_range = Default::default();
1280        self.engine
1281            .send_modeling_cmd(
1282                &self.engine_batch,
1283                exec_state.next_uuid(),
1284                source_range,
1285                &ModelingCmd::ToggleGraphics(kcmc::ToggleGraphics::enabled(false)),
1286            )
1287            .await
1288            .map(|_| ())?;
1289        Ok(())
1290    }
1291
1292    /// Open an engine session for the entrypoint's resolved `Program::language_version()`.
1293    /// The version is fixed for the lifetime of this connection.
1294    #[cfg(not(target_arch = "wasm32"))]
1295    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings, kcl_version: KclVersion) -> Result<Self> {
1296        let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
1297        let (ws, headers) = client
1298            .modeling()
1299            .commands_ws(kittycad::modeling::CommandsWsParams {
1300                api_call_id: None,
1301                fps: None,
1302                order_independent_transparency: None,
1303                post_effect: if settings.enable_ssao {
1304                    Some(kittycad::types::PostEffectType::Ssao)
1305                } else {
1306                    None
1307                },
1308                replay: settings.replay.clone(),
1309                show_grid: if settings.show_grid { Some(true) } else { None },
1310                pool: settings.pool.clone(),
1311                geometry_only: Some(settings.geometry_only),
1312                pr,
1313                unlocked_framerate: None,
1314                webrtc: Some(false),
1315                video_res_width: settings.video_res_width,
1316                video_res_height: settings.video_res_height,
1317                kcl_version: Some(match kcl_version {
1318                    KclVersion::V1 => kittycad::types::KclVersion::One0,
1319                    KclVersion::V2 => kittycad::types::KclVersion::Two0,
1320                    KclVersion::V3Preview => kittycad::types::KclVersion::Three0Preview,
1321                    KclVersion::V3 => kittycad::types::KclVersion::Three0,
1322                    KclVersion::V4Preview => kittycad::types::KclVersion::Four0Preview,
1323                }),
1324            })
1325            .await?;
1326
1327        let request_id = headers
1328            .get("x-request-id")
1329            .and_then(|id| id.to_str().ok())
1330            .map(str::to_owned);
1331        let engine_conn =
1332            EngineManager::new_websocket_transport_with_request_id(ws, settings.heartbeats, request_id).await;
1333        let engine = Arc::new(engine_conn);
1334
1335        Ok(Self::new_with_engine(engine, settings))
1336    }
1337
1338    #[cfg(target_arch = "wasm32")]
1339    pub fn new(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1340        Self::new_with_engine_and_fs(engine, fs, settings)
1341    }
1342
1343    #[cfg(not(target_arch = "wasm32"))]
1344    pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
1345        ExecutorContext {
1346            engine: Arc::new(EngineManager::new_mock()),
1347            cache: Cache::default(),
1348            engine_batch: EngineBatchContext::default(),
1349            fs: crate::fs::new_file_system_handle(FileManager::new()),
1350            settings: settings.unwrap_or_default(),
1351            context_type: ContextType::Mock,
1352            execution_callbacks: Default::default(),
1353            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1354            configure_engine_render: true,
1355            needs_oit: Default::default(),
1356        }
1357    }
1358
1359    #[cfg(target_arch = "wasm32")]
1360    pub fn new_mock(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1361        ExecutorContext {
1362            engine,
1363            cache: Cache::default(),
1364            engine_batch: EngineBatchContext::default(),
1365            fs,
1366            settings,
1367            context_type: ContextType::Mock,
1368            execution_callbacks: Default::default(),
1369            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1370            configure_engine_render: true,
1371            needs_oit: Default::default(),
1372        }
1373    }
1374
1375    /// Create a new mock executor context for WASM LSP servers.
1376    /// This is a convenience function that creates a mock engine and FileManager from a FileSystemManager.
1377    #[cfg(target_arch = "wasm32")]
1378    pub fn new_mock_for_lsp(
1379        fs_manager: crate::fs::wasm::FileSystemManager,
1380        settings: ExecutorSettings,
1381    ) -> Result<Self, String> {
1382        let fs = crate::fs::new_file_system_handle(FileManager::new(fs_manager));
1383
1384        Ok(ExecutorContext {
1385            engine: Arc::new(EngineManager::new_mock()),
1386            cache: Cache::default(),
1387            engine_batch: EngineBatchContext::default(),
1388            fs,
1389            settings,
1390            context_type: ContextType::Mock,
1391            execution_callbacks: Default::default(),
1392            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1393            configure_engine_render: true,
1394            needs_oit: Default::default(),
1395        })
1396    }
1397
1398    #[cfg(not(target_arch = "wasm32"))]
1399    pub fn new_forwarded_mock(engine: Arc<EngineManager>) -> Self {
1400        ExecutorContext {
1401            engine,
1402            cache: Cache::default(),
1403            engine_batch: EngineBatchContext::default(),
1404            fs: crate::fs::new_file_system_handle(FileManager::new()),
1405            settings: Default::default(),
1406            context_type: ContextType::MockCustomForwarded,
1407            execution_callbacks: Default::default(),
1408            machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1409            configure_engine_render: true,
1410            needs_oit: Default::default(),
1411        }
1412    }
1413
1414    /// Create a new default executor context.
1415    /// With a kittycad client.
1416    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
1417    /// variables.
1418    /// But also allows for passing in a token and engine address directly.
1419    #[cfg(not(target_arch = "wasm32"))]
1420    pub async fn new_with_client(
1421        settings: ExecutorSettings,
1422        token: Option<String>,
1423        engine_addr: Option<String>,
1424        kcl_version: KclVersion,
1425    ) -> Result<Self> {
1426        // Create the client.
1427        let client = crate::engine::new_zoo_client(token, engine_addr)?;
1428
1429        let ctx = Self::new(&client, settings, kcl_version).await?;
1430        Ok(ctx)
1431    }
1432
1433    /// Create a new default executor context.
1434    /// With a kittycad client.
1435    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
1436    /// variables.
1437    /// But also allows for passing in a token and engine address directly.
1438    #[cfg(not(target_arch = "wasm32"))]
1439    pub async fn new_with_version(kcl_version: KclVersion) -> Result<Self> {
1440        Self::new_with_client(Default::default(), None, None, kcl_version).await
1441    }
1442
1443    /// Create a new default executor context.
1444    /// With the default kittycad client and the default (unannotated) KCL version.
1445    /// For a versioned entrypoint, use `new_with_client` or `new_with_version`
1446    /// with `Program::language_version()`.
1447    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
1448    /// variables.
1449    #[cfg_attr(not(test), deprecated(note = "use fn new_with_version() instead"))]
1450    #[cfg(not(target_arch = "wasm32"))]
1451    pub async fn new_with_default_client() -> Result<Self> {
1452        Self::new_with_client(Default::default(), None, None, KclVersion::default()).await
1453    }
1454
1455    /// Create a geometry-only executor context with the default client.
1456    #[cfg(not(target_arch = "wasm32"))]
1457    pub async fn new_geometry_only_with_default_client() -> Result<Self> {
1458        Self::new_geometry_only_with_version(KclVersion::default()).await
1459    }
1460
1461    /// Create a geometry-only executor context for the entrypoint's KCL version.
1462    #[cfg(not(target_arch = "wasm32"))]
1463    pub async fn new_geometry_only_with_version(kcl_version: KclVersion) -> Result<Self> {
1464        Self::new_with_client(
1465            ExecutorSettings {
1466                geometry_only: true,
1467                ..Default::default()
1468            },
1469            None,
1470            None,
1471            kcl_version,
1472        )
1473        .await
1474    }
1475
1476    /// For executing unit tests.
1477    #[cfg(not(target_arch = "wasm32"))]
1478    pub async fn new_for_unit_test(engine_addr: Option<String>, kcl_version: KclVersion) -> Result<Self> {
1479        let ctx = ExecutorContext::new_with_client(
1480            ExecutorSettings {
1481                highlight_edges: true,
1482                enable_ssao: false,
1483                show_grid: false,
1484                replay: None,
1485                project_directory: None,
1486                current_file: None,
1487                fixed_size_grid: false,
1488                skip_artifact_graph: false,
1489                heartbeats: None,
1490                default_backface_color: None,
1491                pool: None,
1492                video_res_width: None,
1493                video_res_height: None,
1494                geometry_only: false,
1495            },
1496            None,
1497            engine_addr,
1498            kcl_version,
1499        )
1500        .await?;
1501        Ok(ctx)
1502    }
1503
1504    pub fn is_mock(&self) -> bool {
1505        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1506    }
1507
1508    /// Returns true if we should not send engine commands for any reason.
1509    pub async fn no_engine_commands(&self) -> bool {
1510        self.is_mock()
1511    }
1512
1513    pub async fn send_clear_scene(
1514        &self,
1515        kcl_version: Option<KclVersion>,
1516        exec_state: &mut ExecState,
1517        source_range: crate::execution::SourceRange,
1518    ) -> Result<(), KclError> {
1519        // Ensure artifacts are cleared so that we don't accumulate them across
1520        // runs.
1521        exec_state.mod_local.artifacts.clear();
1522        exec_state.global.root_module_artifacts.clear();
1523        exec_state.global.artifacts.clear();
1524
1525        self.engine
1526            .clear_scene(
1527                &self.engine_batch,
1528                &mut exec_state.mod_local.id_generator,
1529                source_range,
1530                kcl_version,
1531                self.settings.geometry_only,
1532            )
1533            .await?;
1534        self.needs_oit.store(false, Ordering::Relaxed);
1535        // OIT requires a graphical context with SSAO enabled.
1536        if !self.settings.geometry_only && self.settings.enable_ssao {
1537            let cmd_id = exec_state.next_uuid();
1538            exec_state
1539                .batch_modeling_cmd(
1540                    ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1541                    ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1542                )
1543                .await?;
1544        }
1545        Ok(())
1546    }
1547
1548    /// Clear the KCL-lib internal cache and clear the entire engine scene.
1549    /// Only executors sharing this session cache are affected.
1550    pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1551        self.cache.bust_cache().await;
1552
1553        // Execute an empty program to clear and reset the scene.
1554        // We specifically want to be returned the objects after the scene is reset.
1555        // Like the default planes so it is easier to just execute an empty program
1556        // after the cache is busted.
1557        let outcome = self.run_with_caching(crate::Program::empty()).await?;
1558
1559        Ok(outcome)
1560    }
1561
1562    /// Clear the KCL-lib internal cache.
1563    /// Only executors sharing this session cache are affected.
1564    pub async fn bust_cache(&self) {
1565        self.cache.bust_cache().await;
1566    }
1567
1568    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1569        self.eval_prelude(exec_state, SourceRange::synthetic())
1570            .await
1571            .map_err(KclErrorWithOutputs::no_outputs)?;
1572        exec_state
1573            .mut_stack()
1574            .push_new_root_env(true)
1575            .map_err(KclErrorWithOutputs::no_outputs)?;
1576        Ok(())
1577    }
1578
1579    fn restore_mock_memory(
1580        exec_state: &mut ExecState,
1581        mem: cache::SketchModeState,
1582        _mock_config: &MockConfig,
1583    ) -> Result<(), KclErrorWithOutputs> {
1584        *exec_state.mut_stack() = mem.stack;
1585        exec_state.global.module_infos = mem.module_infos;
1586        exec_state.global.path_to_source_id = mem.path_to_source_id;
1587        exec_state.global.id_to_source = mem.id_to_source;
1588        exec_state.global.never_type_ranges = mem.never_type_ranges;
1589        exec_state.global.std_not_yet_added = mem.std_not_yet_added;
1590        exec_state.mod_local.constraint_state = mem.constraint_state;
1591        let len = _mock_config
1592            .sketch_block_id
1593            .map(|sketch_block_id| sketch_block_id.0)
1594            .unwrap_or(0);
1595        if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1596            exec_state
1597                .global
1598                .root_module_artifacts
1599                .restore_scene_objects(scene_objects);
1600        } else {
1601            let message = format!(
1602                "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1603                mem.scene_objects.len(),
1604                len
1605            );
1606            debug_assert!(false, "{message}");
1607            return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1608                KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1609            )));
1610        }
1611
1612        Ok(())
1613    }
1614
1615    pub async fn run_mock(
1616        &self,
1617        program: &crate::Program,
1618        mock_config: &MockConfig,
1619    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1620        let (exec_state, main_ref) = self.run_mock_returning_state(program, mock_config).await?;
1621
1622        // Restore any temporary variables, then save any newly created variables back to
1623        // memory in case another run wants to use them. Note this is just saved to the preserved
1624        // memory, not to the exec_state which is not cached for mock execution.
1625
1626        let mut stack = exec_state.stack().clone();
1627        let module_infos = exec_state.global.module_infos.clone();
1628        let path_to_source_id = exec_state.global.path_to_source_id.clone();
1629        let id_to_source = exec_state.global.id_to_source.clone();
1630        let never_type_ranges = exec_state.global.never_type_ranges.clone();
1631        let constraint_state = exec_state.mod_local.constraint_state.clone();
1632        let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1633        let std_not_yet_added = exec_state.global.std_not_yet_added.clone();
1634        let kcl_version = exec_state.entry_point_kcl_version();
1635        let outcome = exec_state
1636            .into_exec_outcome(main_ref, self)
1637            .await
1638            .map_err(KclErrorWithOutputs::no_outputs)?;
1639
1640        stack.squash_env(main_ref).map_err(KclErrorWithOutputs::no_outputs)?;
1641        let state = cache::SketchModeState {
1642            stack,
1643            module_infos,
1644            path_to_source_id,
1645            id_to_source,
1646            never_type_ranges,
1647            constraint_state,
1648            scene_objects,
1649            std_not_yet_added,
1650            kcl_version,
1651        };
1652        self.cache.write_old_memory(state).await;
1653
1654        Ok(outcome)
1655    }
1656
1657    /// The mock-execution pipeline through interpretation: set up mock state,
1658    /// restore or prepare memory, and execute. Split from [`Self::run_mock`],
1659    /// which converts the state to an [`ExecOutcome`], so that tests can
1660    /// inspect the [`ExecState`] after a mock run.
1661    async fn run_mock_returning_state(
1662        &self,
1663        program: &crate::Program,
1664        mock_config: &MockConfig,
1665    ) -> Result<(ExecState, EnvironmentRef), KclErrorWithOutputs> {
1666        assert!(
1667            self.is_mock(),
1668            "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1669        );
1670
1671        let use_prev_memory = mock_config.use_prev_memory;
1672        let mut exec_state = ExecState::new_mock(self, mock_config);
1673        // Before the prelude runs below, so that `added_in` gating in std sees
1674        // the program's version rather than the default.
1675        exec_state
1676            .set_entry_point_kcl_version(program)
1677            .map_err(KclErrorWithOutputs::no_outputs)?;
1678        if use_prev_memory {
1679            match self.cache.read_old_memory().await {
1680                Some(mem) if mem.reusable_for(exec_state.entry_point_kcl_version()) => {
1681                    Self::restore_mock_memory(&mut exec_state, mem, mock_config)?
1682                }
1683                // No memory, or memory from another effective kclVersion, whose
1684                // bindings, module outcomes, and prelude this program must not
1685                // see.
1686                _ => self.prepare_mem(&mut exec_state).await?,
1687            }
1688        } else {
1689            self.prepare_mem(&mut exec_state).await?
1690        };
1691
1692        // Push a scope so that old variables can be overwritten (since we might be re-executing some
1693        // part of the scene).
1694        exec_state
1695            .mut_stack()
1696            .push_new_env_for_scope()
1697            .map_err(KclErrorWithOutputs::no_outputs)?;
1698
1699        let (main_ref, _) = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1700
1701        Ok((exec_state, main_ref))
1702    }
1703
1704    /// Plan, execute, and save in separate phases so cache-management stack
1705    /// frames are not held underneath deep KCL execution in debug builds.
1706    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1707        assert!(!self.is_mock());
1708        let exec_fut = self.run_with_caching_inner(program);
1709        let result = if self.configure_engine_render {
1710            self.with_engine_execution(Box::pin(exec_fut)).await
1711        } else {
1712            exec_fut.await
1713        };
1714
1715        match result {
1716            Ok(outcome) => Ok(outcome),
1717            Err(mut err) => {
1718                self.cache.bust_cache().await;
1719                err.engine_scene_generation = Some(self.cache.engine_scene_generation().await);
1720                Err(err)
1721            }
1722        }
1723    }
1724
1725    async fn run_with_caching_inner(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1726        let original_program = program.clone();
1727        let prepared = self.prepare_cached_execution(program).await;
1728        if !matches!(prepared, Ok(PreparedCachedExecution::Cached(_))) {
1729            // Anything but a cache hit may have changed the engine scene, even
1730            // if it went on to fail.
1731            self.cache.bump_engine_scene_generation().await;
1732        }
1733        let CachedExecution {
1734            program,
1735            mut exec_state,
1736            universe_info,
1737            preserve_mem,
1738        } = *match prepared? {
1739            PreparedCachedExecution::Cached(outcome) => return Ok(*outcome),
1740            PreparedCachedExecution::Execute(execution) => execution,
1741        };
1742        let result = self
1743            .run_concurrent_inner(&program, &mut exec_state, universe_info, preserve_mem)
1744            .await;
1745        self.finish_cached_execution(original_program, exec_state, result).await
1746    }
1747
1748    // Construct each phase's future here, rather than in run_with_caching,
1749    // so the coordinator does not reserve stack for its large temporaries.
1750    #[inline(never)]
1751    fn prepare_cached_execution(
1752        &self,
1753        program: crate::Program,
1754    ) -> BoxFuture<'_, Result<PreparedCachedExecution, KclErrorWithOutputs>> {
1755        Box::pin(async move {
1756            let grid_scale = if self.settings.fixed_size_grid {
1757                GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1758            } else {
1759                GridScaleBehavior::ScaleWithZoom
1760            };
1761
1762            let (program, exec_state, universe_info, preserve_mem) = match self.cache.read_old_ast().await {
1763                Some(mut cached_state) => {
1764                    let old = CacheInformation {
1765                        ast: &cached_state.main.ast,
1766                        settings: &cached_state.settings,
1767                        kcl_version: cached_state.kcl_version,
1768                    };
1769                    let new = CacheInformation {
1770                        ast: &program.ast,
1771                        settings: &self.settings,
1772                        kcl_version: program.kcl_version,
1773                    };
1774
1775                    // Get the program that actually changed from the old and new information.
1776                    let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1777                        CacheResult::ReExecute {
1778                            clear_scene,
1779                            reapply_settings,
1780                            program: changed_program,
1781                        } => {
1782                            if reapply_settings
1783                                && self
1784                                    .engine
1785                                    .reapply_settings(
1786                                        &self.engine_batch,
1787                                        &self.settings,
1788                                        Default::default(),
1789                                        &mut cached_state.main.exec_state.id_generator,
1790                                        grid_scale,
1791                                    )
1792                                    .await
1793                                    .is_err()
1794                            {
1795                                (true, program, None)
1796                            } else {
1797                                (
1798                                    clear_scene,
1799                                    crate::Program {
1800                                        kcl_version: program.kcl_version,
1801                                        ast: changed_program,
1802                                        original_file_contents: program.original_file_contents,
1803                                    },
1804                                    None,
1805                                )
1806                            }
1807                        }
1808                        CacheResult::CheckImportsOnly {
1809                            reapply_settings,
1810                            ast: changed_program,
1811                        } => {
1812                            let mut reapply_failed = false;
1813                            if reapply_settings {
1814                                if self
1815                                    .engine
1816                                    .reapply_settings(
1817                                        &self.engine_batch,
1818                                        &self.settings,
1819                                        Default::default(),
1820                                        &mut cached_state.main.exec_state.id_generator,
1821                                        grid_scale,
1822                                    )
1823                                    .await
1824                                    .is_ok()
1825                                {
1826                                    self.cache
1827                                        .write_old_ast(GlobalState::with_settings(
1828                                            cached_state.clone(),
1829                                            self.settings.clone(),
1830                                        ))
1831                                        .await;
1832                                } else {
1833                                    reapply_failed = true;
1834                                }
1835                            }
1836
1837                            if reapply_failed {
1838                                (true, program, None)
1839                            } else {
1840                                // We need to check our imports to see if they changed.
1841                                let mut new_exec_state = ExecState::new(self);
1842                                let (new_universe, new_universe_map) =
1843                                    self.get_universe(&program, &mut new_exec_state).await?;
1844
1845                                let clear_scene = new_universe.values().any(|value| {
1846                                    let id = value.1;
1847                                    match (
1848                                        cached_state.exec_state.get_source(id),
1849                                        new_exec_state.global.get_source(id),
1850                                    ) {
1851                                        (Some(s0), Some(s1)) => s0.source != s1.source,
1852                                        _ => false,
1853                                    }
1854                                });
1855
1856                                if !clear_scene {
1857                                    // Return early we don't need to clear the scene.
1858                                    self.cache
1859                                        .write_old_memory(
1860                                            cached_state
1861                                                .mock_memory_state()
1862                                                .map_err(KclErrorWithOutputs::no_outputs)?,
1863                                        )
1864                                        .await;
1865                                    return cached_state
1866                                        .into_exec_outcome(self)
1867                                        .await
1868                                        .map(|outcome| PreparedCachedExecution::Cached(Box::new(outcome)))
1869                                        .map_err(KclErrorWithOutputs::no_outputs);
1870                                }
1871
1872                                (
1873                                    true,
1874                                    crate::Program {
1875                                        kcl_version: program.kcl_version,
1876                                        ast: changed_program,
1877                                        original_file_contents: program.original_file_contents,
1878                                    },
1879                                    Some((new_universe, new_universe_map, new_exec_state)),
1880                                )
1881                            }
1882                        }
1883                        CacheResult::NoAction(true) => {
1884                            if self
1885                                .engine
1886                                .reapply_settings(
1887                                    &self.engine_batch,
1888                                    &self.settings,
1889                                    Default::default(),
1890                                    &mut cached_state.main.exec_state.id_generator,
1891                                    grid_scale,
1892                                )
1893                                .await
1894                                .is_ok()
1895                            {
1896                                // We need to update the old ast state with the new settings!!
1897                                self.cache
1898                                    .write_old_ast(GlobalState::with_settings(
1899                                        cached_state.clone(),
1900                                        self.settings.clone(),
1901                                    ))
1902                                    .await;
1903
1904                                self.cache
1905                                    .write_old_memory(
1906                                        cached_state
1907                                            .mock_memory_state()
1908                                            .map_err(KclErrorWithOutputs::no_outputs)?,
1909                                    )
1910                                    .await;
1911                                return cached_state
1912                                    .into_exec_outcome(self)
1913                                    .await
1914                                    .map(|outcome| PreparedCachedExecution::Cached(Box::new(outcome)))
1915                                    .map_err(KclErrorWithOutputs::no_outputs);
1916                            }
1917                            (true, program, None)
1918                        }
1919                        CacheResult::NoAction(false) => {
1920                            self.cache
1921                                .write_old_memory(
1922                                    cached_state
1923                                        .mock_memory_state()
1924                                        .map_err(KclErrorWithOutputs::no_outputs)?,
1925                                )
1926                                .await;
1927                            return cached_state
1928                                .into_exec_outcome(self)
1929                                .await
1930                                .map(|outcome| PreparedCachedExecution::Cached(Box::new(outcome)))
1931                                .map_err(KclErrorWithOutputs::no_outputs);
1932                        }
1933                    };
1934
1935                    let (exec_state, universe_info, preserve_mem) = match import_check_info {
1936                        Some((new_universe, new_universe_map, mut new_exec_state)) => {
1937                            // Clear the scene if the imports changed.
1938                            self.send_clear_scene(Some(program.kcl_version), &mut new_exec_state, Default::default())
1939                                .await
1940                                .map_err(KclErrorWithOutputs::no_outputs)?;
1941
1942                            (
1943                                new_exec_state,
1944                                Some((new_universe, new_universe_map)),
1945                                PreserveMem::Normal,
1946                            )
1947                        }
1948                        None if clear_scene => {
1949                            // Pop the execution state, since we are starting fresh.
1950                            let mut exec_state = cached_state.reconstitute_exec_state(self);
1951                            exec_state.reset(self);
1952
1953                            self.send_clear_scene(Some(program.kcl_version), &mut exec_state, Default::default())
1954                                .await
1955                                .map_err(KclErrorWithOutputs::no_outputs)?;
1956
1957                            (exec_state, None, PreserveMem::Normal)
1958                        }
1959                        None => {
1960                            let mut exec_state = cached_state.reconstitute_exec_state(self);
1961                            exec_state
1962                                .mut_stack()
1963                                .restore_env(cached_state.main.result_env)
1964                                .map_err(KclErrorWithOutputs::no_outputs)?;
1965
1966                            (exec_state, None, PreserveMem::Always)
1967                        }
1968                    };
1969
1970                    (program, exec_state, universe_info, preserve_mem)
1971                }
1972                None => {
1973                    let mut exec_state = ExecState::new(self);
1974                    self.send_clear_scene(Some(program.kcl_version), &mut exec_state, Default::default())
1975                        .await
1976                        .map_err(KclErrorWithOutputs::no_outputs)?;
1977
1978                    (program, exec_state, None, PreserveMem::Normal)
1979                }
1980            };
1981
1982            Ok(PreparedCachedExecution::Execute(Box::new(CachedExecution {
1983                program,
1984                exec_state: Box::new(exec_state),
1985                universe_info,
1986                preserve_mem,
1987            })))
1988        })
1989    }
1990
1991    #[inline(never)]
1992    fn finish_cached_execution(
1993        &self,
1994        original_program: crate::Program,
1995        exec_state: Box<ExecState>,
1996        result: Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs>,
1997    ) -> BoxFuture<'_, Result<ExecOutcome, KclErrorWithOutputs>> {
1998        Box::pin(async move {
1999            // Throw the error.
2000            let result = result?;
2001
2002            // Save this as the last successful execution to the cache.
2003            // Gotcha: `CacheResult::ReExecute.program` may be diff-based, do not save that AST
2004            // the last-successful AST. Instead, save in the full AST passed in.
2005            self.cache
2006                .write_old_ast(GlobalState::new(
2007                    (*exec_state).clone(),
2008                    self.settings.clone(),
2009                    original_program.kcl_version,
2010                    original_program.ast,
2011                    result.0,
2012                ))
2013                .await;
2014
2015            let outcome = exec_state
2016                .into_exec_outcome(result.0, self)
2017                .await
2018                .map_err(KclErrorWithOutputs::no_outputs)?;
2019            Ok(outcome)
2020        })
2021    }
2022
2023    /// Perform the execution of a program.
2024    ///
2025    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
2026    pub async fn run(
2027        &self,
2028        program: &crate::Program,
2029        exec_state: &mut ExecState,
2030    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
2031        self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
2032            .await
2033    }
2034
2035    /// Perform the execution of a program using a concurrent
2036    /// execution model.
2037    ///
2038    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
2039    pub async fn run_concurrent(
2040        &self,
2041        program: &crate::Program,
2042        exec_state: &mut ExecState,
2043        universe_info: Option<(Universe, UniverseMap)>,
2044        preserve_mem: PreserveMem,
2045    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
2046        let execution_fut = self.run_concurrent_inner(program, exec_state, universe_info, preserve_mem);
2047        if self.configure_engine_render {
2048            self.with_engine_execution(Box::pin(execution_fut)).await
2049        } else {
2050            execution_fut.await
2051        }
2052    }
2053
2054    /// Enclose the entire execution, including scene setup and cached settings updates.
2055    async fn with_engine_execution<T>(
2056        &self,
2057        execution: impl Future<Output = Result<T, KclErrorWithOutputs>>,
2058    ) -> Result<T, KclErrorWithOutputs> {
2059        self.send_execution_boundary(ModelingCmd::from(
2060            mcmd::BeginExecution::builder().enable_render(true).build(),
2061        ))
2062        .await
2063        .map_err(KclErrorWithOutputs::no_outputs)?;
2064
2065        let exec_res = execution.await;
2066        let flush_res = self.engine.ensure_async_commands_completed(&self.engine_batch).await;
2067        if flush_res.is_err() {
2068            // Do not carry unexecuted commands from a failed flush into the next execution.
2069            self.engine.clear_queues(&self.engine_batch).await;
2070        }
2071        // Always end execution, even when evaluation or the final flush failed.
2072        let end_res = self
2073            .send_execution_boundary(ModelingCmd::from(mcmd::EndExecution::default()))
2074            .await;
2075        match (exec_res, flush_res.and(end_res)) {
2076            (Ok(res), Ok(())) => Ok(res),
2077            (Err(e), _) => Err(e),
2078            (Ok(_), Err(e)) => Err(KclErrorWithOutputs::no_outputs(e)),
2079        }
2080    }
2081
2082    async fn send_execution_boundary(&self, cmd: ModelingCmd) -> Result<(), KclError> {
2083        // A separate, empty queue keeps these commands outside all modeling batches.
2084        // Their IDs must not consume or collide with the model's stable artifact IDs.
2085        self.engine
2086            .send_modeling_cmd(
2087                &EngineBatchContext::new(),
2088                uuid::Uuid::new_v4(),
2089                SourceRange::synthetic(),
2090                &cmd,
2091            )
2092            .await
2093            .map(|_| ())
2094    }
2095
2096    fn default_tolerance_command(exec_state: &ExecState) -> Option<ModelingCmd> {
2097        exec_state.entry_point_version_is_v3_or_higher().then(|| {
2098            let tolerance = kcmc::shared::Tolerance::builder()
2099                .point_point_2d_coincident(kcmc::length_unit::LengthUnit(
2100                    crate::std::solver::POINT_POINT_2D_COINCIDENT_EUCLIDEAN_TOLERANCE_MM,
2101                ))
2102                .build();
2103            ModelingCmd::from(mcmd::SetDefaultSystemProperties::builder().tolerance(tolerance).build())
2104        })
2105    }
2106
2107    async fn run_concurrent_inner(
2108        &self,
2109        program: &crate::Program,
2110        exec_state: &mut ExecState,
2111        universe_info: Option<(Universe, UniverseMap)>,
2112        preserve_mem: PreserveMem,
2113    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
2114        // Record the entry point's kclVersion before anything executes;
2115        // imported modules pre-execute on clones of this state below and must
2116        // inherit it.
2117        exec_state
2118            .set_entry_point_kcl_version(program)
2119            .map_err(KclErrorWithOutputs::no_outputs)?;
2120
2121        // Apply the physical tolerance before imported modules send geometry commands.
2122        if let Some(cmd) = Self::default_tolerance_command(exec_state) {
2123            self.send_execution_boundary(cmd)
2124                .await
2125                .map_err(KclErrorWithOutputs::no_outputs)?;
2126        }
2127
2128        // Reuse our cached universe if we have one.
2129
2130        let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
2131            (universe, universe_map)
2132        } else {
2133            self.get_universe(program, exec_state).await?
2134        };
2135
2136        // Push ModuleInstance ops for the root module's direct imports before
2137        // child modules execute. This lets the live feature tree show module
2138        // names immediately rather than waiting for the root module body to run.
2139        // Sort by source position so they appear in source-code order (the
2140        // universe_map is a HashMap with non-deterministic iteration order).
2141        let mut sorted_imports: Vec<_> = universe_map.iter().collect();
2142        sorted_imports.sort_by_key(|(_, import_stmt)| SourceRange::from(*import_stmt));
2143        for (_path, import_stmt) in sorted_imports {
2144            // Look up by the raw import filename (e.g. "car-wheel.kcl") which
2145            // is the key format used by Universe, NOT the resolved absolute
2146            // TypedPath that UniverseMap uses as its key.
2147            let filename = match &import_stmt.path {
2148                ImportPath::Kcl { filename } => filename.to_string(),
2149                ImportPath::Foreign { path } => path.to_string(),
2150                ImportPath::Std { .. } => continue,
2151            };
2152            if let Some((_, module_id, module_path, _)) = universe.get(&filename)
2153                && let ModulePath::Local { value, .. } = module_path
2154            {
2155                let name = import_stmt
2156                    .module_name()
2157                    .unwrap_or_else(|| value.file_name().unwrap_or_default());
2158                let source_range = SourceRange::from(import_stmt);
2159                exec_state.push_op(crate::execution::cad_op::Operation::ModuleInstance {
2160                    name,
2161                    module_id: *module_id,
2162                    glob: matches!(
2163                        import_stmt.selector,
2164                        crate::parsing::ast::types::ImportSelector::Glob(_)
2165                    ),
2166                    node_path: crate::NodePath::placeholder(),
2167                    source_range,
2168                });
2169            }
2170        }
2171
2172        let default_planes = self.engine.get_default_planes().read().await.clone();
2173
2174        // Run the prelude to set up the engine.
2175        self.eval_prelude(exec_state, SourceRange::synthetic())
2176            .await
2177            .map_err(KclErrorWithOutputs::no_outputs)?;
2178
2179        for modules in import_graph::import_graph(&universe, self)
2180            .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
2181            .into_iter()
2182        {
2183            #[cfg(not(target_arch = "wasm32"))]
2184            let mut set = tokio::task::JoinSet::new();
2185
2186            #[allow(clippy::type_complexity)]
2187            let (results_tx, mut results_rx): (
2188                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
2189                tokio::sync::mpsc::Receiver<_>,
2190            ) = tokio::sync::mpsc::channel(1);
2191
2192            for module in modules {
2193                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
2194                    return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
2195                        KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
2196                    )));
2197                };
2198                let module_id = *module_id;
2199                let module_path = module_path.clone();
2200                let source_range = SourceRange::from(import_stmt);
2201                // Clone before mutating.
2202                let module_exec_state = exec_state.clone();
2203
2204                let repr = repr.clone();
2205                let exec_ctxt = self.clone_with_fresh_execution_batch();
2206                let results_tx = results_tx.clone();
2207
2208                let exec_module = async |exec_ctxt: &ExecutorContext,
2209                                         repr: &ModuleRepr,
2210                                         module_id: ModuleId,
2211                                         module_path: &ModulePath,
2212                                         exec_state: &mut ExecState,
2213                                         source_range: SourceRange|
2214                       -> Result<ModuleRepr, KclError> {
2215                    match repr {
2216                        ModuleRepr::Kcl(program, _) => {
2217                            let result = exec_ctxt
2218                                .exec_module_from_ast(
2219                                    program,
2220                                    module_id,
2221                                    module_path,
2222                                    exec_state,
2223                                    source_range,
2224                                    PreserveMem::Normal,
2225                                )
2226                                .await;
2227
2228                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
2229                        }
2230                        ModuleRepr::Foreign(geom, _) => {
2231                            // The concurrent executor starts from a clone of the root module state.
2232                            // Use a fresh artifact state so the import command belongs only to the
2233                            // foreign module that issued it.
2234                            exec_state.mod_local.artifacts = Default::default();
2235                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
2236                                .await
2237                                .map(|geom| Some(KclValue::ImportedGeometry(geom)))
2238                                // Label the failure with the import so the
2239                                // backtrace names the foreign file (and so
2240                                // add_import_backtrace's assumption that the
2241                                // immediate frame is present holds).
2242                                .map_err(|err| err.add_import_location(&module_path.import_name(), source_range));
2243                            let module_artifacts = std::mem::take(&mut exec_state.mod_local.artifacts);
2244
2245                            result.map(|val| ModuleRepr::Foreign(geom.clone(), Some((val, module_artifacts))))
2246                        }
2247                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
2248                            format!("Module {module_path} not found in universe"),
2249                            vec![source_range],
2250                        ))),
2251                    }
2252                };
2253
2254                #[cfg(target_arch = "wasm32")]
2255                {
2256                    wasm_bindgen_futures::spawn_local(async move {
2257                        let mut exec_state = module_exec_state;
2258                        let exec_ctxt = exec_ctxt;
2259
2260                        let result = exec_module(
2261                            &exec_ctxt,
2262                            &repr,
2263                            module_id,
2264                            &module_path,
2265                            &mut exec_state,
2266                            source_range,
2267                        )
2268                        .await;
2269
2270                        results_tx
2271                            .send((module_id, module_path, result))
2272                            .await
2273                            .unwrap_or_default();
2274                    });
2275                }
2276                #[cfg(not(target_arch = "wasm32"))]
2277                {
2278                    set.spawn(async move {
2279                        let mut exec_state = module_exec_state;
2280                        let exec_ctxt = exec_ctxt;
2281
2282                        let result = exec_module(
2283                            &exec_ctxt,
2284                            &repr,
2285                            module_id,
2286                            &module_path,
2287                            &mut exec_state,
2288                            source_range,
2289                        )
2290                        .await;
2291
2292                        results_tx
2293                            .send((module_id, module_path, result))
2294                            .await
2295                            .unwrap_or_default();
2296                    });
2297                }
2298            }
2299
2300            drop(results_tx);
2301
2302            while let Some((module_id, _, result)) = results_rx.recv().await {
2303                match result {
2304                    Ok(new_repr) => {
2305                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
2306
2307                        match &mut repr {
2308                            ModuleRepr::Kcl(_, cache) => {
2309                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
2310                                    unreachable!();
2311                                };
2312                                *cache = session_data;
2313                            }
2314                            ModuleRepr::Foreign(_, cache) => {
2315                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
2316                                    unreachable!();
2317                                };
2318                                *cache = session_data;
2319                            }
2320                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
2321                        }
2322
2323                        exec_state.global.module_infos[&module_id].restore_repr(repr);
2324                    }
2325                    Err(e) => {
2326                        let e = import_graph::add_import_backtrace(e, module_id, &universe);
2327                        return Err(exec_state.error_with_outputs(e, None, default_planes));
2328                    }
2329                }
2330            }
2331        }
2332
2333        // The early-pushed ModuleInstance operations have already served their
2334        // purpose (firing onOperation callbacks for the live feature tree).
2335        // Clear them so they don't duplicate the operations the root module
2336        // body will produce when it actually executes its import statements.
2337        exec_state.mod_local.artifacts.operations.clear();
2338
2339        // Move any remaining setup artifacts (non-operation data from the
2340        // prelude, etc.) into the root state.
2341        exec_state
2342            .global
2343            .root_module_artifacts
2344            .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
2345
2346        self.inner_run(program, exec_state, preserve_mem)
2347            .await
2348            .map_err(|mut error| {
2349                // Engine rejections of async commands (e.g. foreign imports)
2350                // surface after module execution, so they miss the import
2351                // frames the eager loop attaches. Without a top-level range
2352                // the frontend cannot anchor the error in the root file;
2353                // rebuild the ancestry from the outermost range's module.
2354                let source_ranges = error.error.source_ranges();
2355                if !source_ranges.is_empty()
2356                    && !source_ranges.iter().any(|range| range.module_id().is_top_level())
2357                    && let Some(outermost) = source_ranges.last()
2358                {
2359                    error.error =
2360                        import_graph::add_import_backtrace_from(error.error.clone(), outermost.module_id(), &universe);
2361                }
2362                error
2363            })
2364    }
2365
2366    /// Get the universe & universe map of the program.
2367    /// And see if any of the imports changed.
2368    async fn get_universe(
2369        &self,
2370        program: &crate::Program,
2371        exec_state: &mut ExecState,
2372    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
2373        // Import validation needs the entry point's version even when a fresh
2374        // state is created only to check imports in a cached execution.
2375        exec_state
2376            .set_entry_point_kcl_version(program)
2377            .map_err(KclErrorWithOutputs::no_outputs)?;
2378        exec_state.add_root_module_contents(program);
2379
2380        let mut universe = std::collections::HashMap::new();
2381
2382        let default_planes = self.engine.get_default_planes().read().await.clone();
2383
2384        let root_imports = import_graph::import_universe(
2385            self,
2386            &ModulePath::Main,
2387            &ModuleRepr::Kcl(program.ast.clone(), None),
2388            &mut universe,
2389            exec_state,
2390        )
2391        .await
2392        .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
2393
2394        Ok((universe, root_imports))
2395    }
2396
2397    /// Perform the execution of a program.  Accept all possible parameters and
2398    /// output everything.
2399    async fn inner_run(
2400        &self,
2401        program: &crate::Program,
2402        exec_state: &mut ExecState,
2403        preserve_mem: PreserveMem,
2404    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
2405        let _stats = crate::log::LogPerfStats::new("Interpretation");
2406
2407        // Record the entry point's kclVersion. Mock execution reaches here
2408        // without going through run_concurrent; on the engine path this
2409        // re-assigns the same value, which is harmless.
2410        exec_state
2411            .set_entry_point_kcl_version(program)
2412            .map_err(KclErrorWithOutputs::no_outputs)?;
2413
2414        // Re-apply the settings, in case the cache was busted.
2415        let grid_scale = if self.settings.fixed_size_grid {
2416            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
2417        } else {
2418            GridScaleBehavior::ScaleWithZoom
2419        };
2420        self.engine
2421            .reapply_settings(
2422                &self.engine_batch,
2423                &self.settings,
2424                Default::default(),
2425                exec_state.id_generator(),
2426                grid_scale,
2427            )
2428            .await
2429            .map_err(KclErrorWithOutputs::no_outputs)?;
2430
2431        let default_planes = self.engine.get_default_planes().read().await.clone();
2432        let result = self
2433            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
2434            .await;
2435
2436        crate::log::log(format!(
2437            "Post interpretation KCL memory stats: {:#?}",
2438            exec_state.stack().memory.stats()
2439        ));
2440        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
2441
2442        /// Write the memory of an execution to the cache for reuse in mock
2443        /// execution.
2444        async fn write_old_memory(
2445            ctx: &ExecutorContext,
2446            exec_state: &ExecState,
2447            env_ref: EnvironmentRef,
2448        ) -> Result<(), KclError> {
2449            if ctx.is_mock() {
2450                return Ok(());
2451            }
2452            let mut stack = exec_state.stack().deep_clone()?;
2453            stack.restore_env(env_ref)?;
2454            let state = cache::SketchModeState {
2455                stack,
2456                module_infos: exec_state.global.module_infos.clone(),
2457                path_to_source_id: exec_state.global.path_to_source_id.clone(),
2458                id_to_source: exec_state.global.id_to_source.clone(),
2459                never_type_ranges: exec_state.global.never_type_ranges.clone(),
2460                constraint_state: exec_state.mod_local.constraint_state.clone(),
2461                scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
2462                std_not_yet_added: exec_state.global.std_not_yet_added.clone(),
2463                kcl_version: exec_state.entry_point_kcl_version(),
2464            };
2465            ctx.cache.write_old_memory(state).await;
2466            Ok(())
2467        }
2468
2469        let env_ref = match result {
2470            Ok(env_ref) => env_ref,
2471            Err((err, env_ref)) => {
2472                // Preserve memory on execution failures so follow-up mock
2473                // execution can still reuse stable IDs before the error.
2474                if let Some(env_ref) = env_ref {
2475                    write_old_memory(self, exec_state, env_ref)
2476                        .await
2477                        .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2478                }
2479                return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
2480            }
2481        };
2482
2483        write_old_memory(self, exec_state, env_ref)
2484            .await
2485            .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2486
2487        let session_data = self.engine.get_session_data().await;
2488
2489        Ok((env_ref, session_data))
2490    }
2491
2492    /// Execute an AST's program and build auxiliary outputs like the artifact
2493    /// graph.
2494    async fn execute_and_build_graph(
2495        &self,
2496        program: NodeRef<'_, crate::parsing::ast::types::Program>,
2497        exec_state: &mut ExecState,
2498        preserve_mem: PreserveMem,
2499    ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
2500        // Don't early return!  We need to build other outputs regardless of
2501        // whether execution failed.
2502
2503        // Because of execution caching, we may start with operations from a
2504        // previous run.
2505        let start_op = exec_state.global.root_module_artifacts.operations.len();
2506
2507        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
2508            .await
2509            .map_err(|e| (e, None))?;
2510
2511        let exec_result = self
2512            .exec_module_body(
2513                program,
2514                exec_state,
2515                preserve_mem,
2516                ModuleId::default(),
2517                &ModulePath::Main,
2518            )
2519            .await
2520            .map(
2521                |ModuleExecutionOutcome {
2522                     environment: env_ref,
2523                     artifacts: module_artifacts,
2524                     ..
2525                 }| {
2526                    // We need to extend because it may already have operations from
2527                    // imports.
2528                    exec_state.global.root_module_artifacts.extend(module_artifacts);
2529                    env_ref
2530                },
2531            )
2532            .map_err(|(err, env_ref, module_artifacts)| {
2533                if let Some(module_artifacts) = module_artifacts {
2534                    // We need to extend because it may already have operations
2535                    // from imports.
2536                    exec_state.global.root_module_artifacts.extend(module_artifacts);
2537                }
2538                (err, env_ref)
2539            });
2540
2541        // Fill in NodePath for operations.
2542        let programs = &exec_state.build_program_lookup(program.clone());
2543        let cached_body_items = exec_state.global.artifacts.cached_body_items();
2544        for op in exec_state
2545            .global
2546            .root_module_artifacts
2547            .operations
2548            .iter_mut()
2549            .skip(start_op)
2550        {
2551            op.fill_node_paths(programs, cached_body_items);
2552        }
2553        for module in exec_state.global.module_infos.values_mut() {
2554            if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
2555                for op in &mut outcome.artifacts.operations {
2556                    op.fill_node_paths(programs, cached_body_items);
2557                }
2558            }
2559        }
2560
2561        // Ensure all the async commands completed.
2562        self.engine
2563            .ensure_async_commands_completed(&self.engine_batch)
2564            .await
2565            .map_err(|e| {
2566                match &exec_result {
2567                    Ok(env_ref) => (e, Some(*env_ref)),
2568                    // Prefer the execution error.
2569                    Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
2570                }
2571            })?;
2572
2573        // If we errored out and early-returned, there might be commands which haven't been executed
2574        // and should be dropped.
2575        self.engine.clear_queues(&self.engine_batch).await;
2576
2577        match exec_state.build_artifact_graph(&self.engine, program).await {
2578            Ok(_) => exec_result,
2579            Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
2580        }
2581    }
2582
2583    /// 'Import' std::prelude as the outermost scope.
2584    ///
2585    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
2586    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
2587        if exec_state.stack().memory.requires_std() {
2588            let initial_ops = exec_state.mod_local.artifacts.operations.len();
2589
2590            let path = vec!["std".to_owned(), "prelude".to_owned()];
2591            let resolved_path = ModulePath::from_std_import_path(&path)?;
2592            let id = self
2593                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
2594                .await?;
2595            let prelude = self.exec_module_for_items(id, exec_state, source_range).await?;
2596
2597            exec_state.mut_stack().memory.set_std(prelude.environment)?;
2598            // Root environments reach the prelude without an import, so its records live here.
2599            exec_state.global.std_not_yet_added = prelude.not_yet_added;
2600
2601            // Operations generated by the prelude are not useful, so clear them
2602            // out.
2603            //
2604            // TODO: Should we also clear them out of each module so that they
2605            // don't appear in test output?
2606            exec_state.mod_local.artifacts.operations.truncate(initial_ops);
2607        }
2608
2609        Ok(())
2610    }
2611
2612    /// Get a snapshot of the current scene.
2613    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
2614        // Zoom to fit.
2615        self.engine
2616            .send_modeling_cmd(
2617                &self.engine_batch,
2618                uuid::Uuid::new_v4(),
2619                crate::execution::SourceRange::default(),
2620                &ModelingCmd::from(
2621                    mcmd::ZoomToFit::builder()
2622                        .object_ids(Default::default())
2623                        .animated(false)
2624                        .padding(0.1)
2625                        .build(),
2626                ),
2627            )
2628            .await
2629            .map_err(KclErrorWithOutputs::no_outputs)?;
2630
2631        // Send a snapshot request to the engine.
2632        let resp = self
2633            .engine
2634            .send_modeling_cmd(
2635                &self.engine_batch,
2636                uuid::Uuid::new_v4(),
2637                crate::execution::SourceRange::default(),
2638                &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
2639            )
2640            .await
2641            .map_err(KclErrorWithOutputs::no_outputs)?;
2642
2643        let OkWebSocketResponseData::Modeling {
2644            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
2645        } = resp
2646        else {
2647            return Err(ExecError::BadPng(format!(
2648                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
2649            )));
2650        };
2651        Ok(contents)
2652    }
2653
2654    /// Export the current scene as a CAD file.
2655    pub async fn export(
2656        &self,
2657        format: kittycad_modeling_cmds::format::OutputFormat3d,
2658    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2659        let resp = self
2660            .engine
2661            .send_modeling_cmd(
2662                &self.engine_batch,
2663                uuid::Uuid::new_v4(),
2664                crate::SourceRange::default(),
2665                &kittycad_modeling_cmds::ModelingCmd::Export(
2666                    kittycad_modeling_cmds::Export::builder()
2667                        .entity_ids(vec![])
2668                        .format(format)
2669                        .build(),
2670                ),
2671            )
2672            .await?;
2673
2674        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
2675            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
2676                format!("Expected Export response, got {resp:?}",),
2677                vec![SourceRange::default()],
2678            )));
2679        };
2680
2681        Ok(files)
2682    }
2683
2684    /// Export the current scene as a STEP file.
2685    pub async fn export_step(
2686        &self,
2687        deterministic_time: bool,
2688    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2689        let files = self
2690            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
2691                kittycad_modeling_cmds::format::step::export::Options::builder()
2692                    .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
2693                    .maybe_created(if deterministic_time {
2694                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
2695                            KclError::new_internal(crate::errors::KclErrorDetails::new(
2696                                format!("Failed to parse date: {e}"),
2697                                vec![SourceRange::default()],
2698                            ))
2699                        })?)
2700                    } else {
2701                        None
2702                    })
2703                    .build(),
2704            ))
2705            .await?;
2706
2707        Ok(files)
2708    }
2709
2710    pub async fn close(&self) {
2711        self.engine.close().await;
2712    }
2713}
2714
2715pub use kcl_api::ArtifactId;
2716
2717pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
2718    ArtifactId::new(*id.as_ref())
2719}
2720
2721#[cfg(test)]
2722pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2723    parse_execute_with_project_dir(code, None).await
2724}
2725
2726#[cfg(test)]
2727pub(crate) async fn parse_execute_with_project_dir(
2728    code: &str,
2729    project_directory: Option<TypedPath>,
2730) -> Result<ExecTestResults, KclError> {
2731    parse_execute_inner(code, project_directory, false).await
2732}
2733
2734/// A mock-engine executor context for tests that need to inspect the context
2735/// (e.g. the engine's batch queue) even when execution fails.
2736#[cfg(test)]
2737pub(crate) fn new_mock_executor_context(project_directory: Option<TypedPath>) -> ExecutorContext {
2738    ExecutorContext {
2739        engine: Arc::new(EngineManager::new_mock()),
2740        cache: Cache::default(),
2741        engine_batch: EngineBatchContext::default(),
2742        fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2743        settings: ExecutorSettings {
2744            project_directory,
2745            ..Default::default()
2746        },
2747        context_type: ContextType::Mock,
2748        execution_callbacks: Default::default(),
2749        machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
2750        configure_engine_render: true,
2751        needs_oit: Default::default(),
2752    }
2753}
2754
2755/// Like [`parse_execute_with_project_dir`], but missing names are explained
2756/// for user declarations too, not only std's. Only for tests of the `added_in`
2757/// hint itself, which std gives no declaration to exercise yet.
2758#[cfg(test)]
2759pub(crate) async fn parse_execute_hinting_all(
2760    code: &str,
2761    project_directory: Option<TypedPath>,
2762) -> Result<ExecTestResults, KclError> {
2763    parse_execute_inner(code, project_directory, true).await
2764}
2765
2766#[cfg(test)]
2767async fn parse_execute_inner(
2768    code: &str,
2769    project_directory: Option<TypedPath>,
2770    hint_all_not_yet_added: bool,
2771) -> Result<ExecTestResults, KclError> {
2772    let program = crate::Program::parse_no_errs(code)?;
2773
2774    let exec_ctxt = new_mock_executor_context(project_directory);
2775    let mut exec_state = ExecState::new(&exec_ctxt);
2776    exec_state.global.hint_all_not_yet_added = hint_all_not_yet_added;
2777    let result = exec_ctxt.run(&program, &mut exec_state).await?;
2778
2779    Ok(ExecTestResults {
2780        program,
2781        mem_env: result.0,
2782        exec_ctxt,
2783        exec_state,
2784    })
2785}
2786
2787#[cfg(test)]
2788#[derive(Debug)]
2789pub(crate) struct ExecTestResults {
2790    program: crate::Program,
2791    mem_env: EnvironmentRef,
2792    exec_ctxt: ExecutorContext,
2793    exec_state: ExecState,
2794}
2795
2796#[cfg(test)]
2797impl ExecTestResults {
2798    pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2799        &self.exec_state.global.root_module_artifacts.commands
2800    }
2801
2802    /// The diagnostics the run reported. Non-fatal issues, such as use of an
2803    /// experimental feature without the opt-in, are recorded here rather than
2804    /// returned as an error, so this is the only place a test can see them.
2805    pub(crate) fn issues(&self) -> &[CompilationIssue] {
2806        self.exec_state.issues()
2807    }
2808
2809    /// The value bound to `name` after the run. Panics when the variable is
2810    /// absent, because a test that names a variable the program does not
2811    /// declare is broken rather than failing.
2812    #[track_caller]
2813    pub(crate) fn variable(&self, name: &str) -> KclValue {
2814        self.exec_state
2815            .stack()
2816            .memory
2817            .get_from_unchecked(name, self.mem_env)
2818            .unwrap()
2819    }
2820}
2821
2822/// There are several places where we want to traverse a KCL program or find a symbol in it,
2823/// but because KCL modules can import each other, we need to traverse multiple programs.
2824/// This stores multiple programs, keyed by their module ID for quick access.
2825pub struct ProgramLookup {
2826    programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2827}
2828
2829impl ProgramLookup {
2830    // TODO: Could this store a reference to KCL programs instead of owning them?
2831    // i.e. take &state::ModuleInfoMap instead?
2832    pub fn new(
2833        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2834        module_infos: state::ModuleInfoMap,
2835    ) -> Self {
2836        let mut programs = IndexMap::with_capacity(module_infos.len());
2837        for (id, info) in module_infos {
2838            if let ModuleRepr::Kcl(program, _) = info.repr {
2839                programs.insert(id, program);
2840            }
2841        }
2842        programs.insert(ModuleId::default(), current);
2843        Self { programs }
2844    }
2845
2846    pub fn program_for_module(
2847        &self,
2848        module_id: ModuleId,
2849    ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2850        self.programs.get(&module_id)
2851    }
2852}
2853
2854#[cfg(test)]
2855mod tests {
2856    use kcl_api::NumericType;
2857    use pretty_assertions::assert_eq;
2858
2859    use super::*;
2860    use crate::ModuleId;
2861    use crate::errors::KclErrorDetails;
2862    use crate::errors::Severity;
2863    use crate::execution::kcl_value::TypeDef;
2864    use crate::execution::memory::Stack;
2865    use crate::execution::types::RuntimeType;
2866
2867    macro_rules! kcl_input {
2868        ($file:literal) => {
2869            include_str!(concat!("../../e2e/executor/inputs/", $file, ".kcl"))
2870        };
2871    }
2872
2873    #[cfg(not(target_arch = "wasm32"))]
2874    #[tokio::test]
2875    async fn native_websocket_sends_entrypoint_kcl_version() {
2876        use tokio::io::AsyncReadExt;
2877        use tokio::io::AsyncWriteExt;
2878
2879        for (source, expected) in [
2880            ("", "1.0"),
2881            ("@settings(defaultLengthUnit = mm)", "1.0"),
2882            ("@settings(kclVersion = 2.0)", "2.0"),
2883            ("@settings(kclVersion = \"3.0-preview\")", "3.0-preview"),
2884            ("@settings(kclVersion = 3.0)", "3.0"),
2885            ("@settings(kclVersion = \"3.0\")", "3.0"),
2886        ] {
2887            let program = crate::Program::parse_no_errs(source).unwrap();
2888            let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2889            let address = listener.local_addr().unwrap();
2890            let request = tokio::spawn(async move {
2891                let (mut socket, _) = listener.accept().await.unwrap();
2892                let mut request = Vec::new();
2893                let mut buffer = [0; 1024];
2894                while !request.windows(4).any(|bytes| bytes == b"\r\n\r\n") {
2895                    let count = socket.read(&mut buffer).await.unwrap();
2896                    assert_ne!(count, 0, "connection closed before sending HTTP headers");
2897                    request.extend_from_slice(&buffer[..count]);
2898                }
2899                // Inspect the handshake without starting an engine session.
2900                socket
2901                    .write_all(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
2902                    .await
2903                    .unwrap();
2904                String::from_utf8(request).unwrap()
2905            });
2906            let mut client = kittycad::Client::new("test-token");
2907            client.set_base_url(format!("http://{address}"));
2908            let result = tokio::time::timeout(
2909                std::time::Duration::from_secs(5),
2910                ExecutorContext::new(&client, Default::default(), program.language_version().unwrap()),
2911            )
2912            .await
2913            .unwrap();
2914            assert!(result.is_err(), "the test server deliberately rejects the upgrade");
2915            let request = tokio::time::timeout(std::time::Duration::from_secs(5), request)
2916                .await
2917                .unwrap()
2918                .unwrap();
2919            let target = request.lines().next().unwrap().split_whitespace().nth(1).unwrap();
2920            let url = url::Url::parse(&format!("http://localhost{target}")).unwrap();
2921            let versions: Vec<_> = url
2922                .query_pairs()
2923                .filter(|(key, _)| key == "kcl_version")
2924                .map(|(_, value)| value.into_owned())
2925                .collect();
2926            assert_eq!(versions, vec![expected]);
2927        }
2928    }
2929
2930    #[cfg(not(target_arch = "wasm32"))]
2931    #[tokio::test]
2932    async fn scene_reset_sends_entrypoint_kcl_version() {
2933        use std::collections::HashMap;
2934
2935        use kcmc::websocket::WebSocketRequest;
2936        use kcmc::websocket::WebSocketResponse;
2937        use tokio::sync::RwLock;
2938        use uuid::Uuid;
2939
2940        use crate::engine::engine_manager::EngineTransport;
2941        use crate::engine::engine_manager::TransportCloseError;
2942
2943        struct RecordingTransport {
2944            inner: Arc<Box<dyn EngineTransport>>,
2945            requests: Arc<RwLock<Vec<WebSocketRequest>>>,
2946        }
2947
2948        #[async_trait::async_trait]
2949        impl EngineTransport for RecordingTransport {
2950            async fn inner_fire_modeling_cmd(
2951                &self,
2952                cmd_id: Uuid,
2953                source_range: SourceRange,
2954                cmd: WebSocketRequest,
2955                id_to_source_range: HashMap<Uuid, SourceRange>,
2956            ) -> Result<(), KclError> {
2957                self.requests.write().await.push(cmd.clone());
2958                self.inner
2959                    .inner_fire_modeling_cmd(cmd_id, source_range, cmd, id_to_source_range)
2960                    .await
2961            }
2962
2963            async fn inner_send_modeling_cmd(
2964                &self,
2965                cmd_id: Uuid,
2966                source_range: SourceRange,
2967                cmd: WebSocketRequest,
2968                id_to_source_range: HashMap<Uuid, SourceRange>,
2969            ) -> Result<WebSocketResponse, KclError> {
2970                self.requests.write().await.push(cmd.clone());
2971                self.inner
2972                    .inner_send_modeling_cmd(cmd_id, source_range, cmd, id_to_source_range)
2973                    .await
2974            }
2975
2976            async fn close(&self) -> Result<(), TransportCloseError> {
2977                self.inner.close().await
2978            }
2979        }
2980
2981        for (source, expected) in [
2982            ("", "1.0"),
2983            ("@settings(kclVersion = 2.0)", "2.0"),
2984            ("@settings(kclVersion = \"3.0-preview\")", "3.0-preview"),
2985            ("@settings(kclVersion = 3.0)", "3.0"),
2986        ] {
2987            let requests = Arc::new(RwLock::new(Vec::new()));
2988            let mut engine = EngineManager::new_mock();
2989            engine.transport = Arc::new(Box::new(RecordingTransport {
2990                inner: engine.transport.clone(),
2991                requests: requests.clone(),
2992            }));
2993            let ctx = ExecutorContext::new_with_engine(
2994                Arc::new(engine),
2995                ExecutorSettings {
2996                    geometry_only: true,
2997                    ..Default::default()
2998                },
2999            );
3000            let program = crate::Program::parse_no_errs(source).unwrap();
3001            let mut exec_state = ExecState::new(&ctx);
3002
3003            ctx.send_clear_scene(
3004                Some(program.language_version().unwrap()),
3005                &mut exec_state,
3006                Default::default(),
3007            )
3008            .await
3009            .unwrap();
3010
3011            let version_commands: Vec<_> = {
3012                let requests = requests.read().await;
3013                requests
3014                    .iter()
3015                    .flat_map(|request| match request {
3016                        WebSocketRequest::ModelingCmdReq(request) => vec![&request.cmd],
3017                        WebSocketRequest::ModelingCmdBatchReq(batch) => {
3018                            batch.requests.iter().map(|request| &request.cmd).collect()
3019                        }
3020                        _ => vec![],
3021                    })
3022                    .filter_map(|cmd| match cmd {
3023                        kcmc::ModelingCmd::SetKclVersion(_) => Some(serde_json::to_value(cmd).unwrap()),
3024                        _ => None,
3025                    })
3026                    .collect()
3027            };
3028            assert_eq!(
3029                version_commands,
3030                vec![serde_json::json!({"type": "set_kcl_version", "kcl_version": expected})],
3031                "{source}"
3032            );
3033            ctx.close().await;
3034        }
3035    }
3036
3037    #[test]
3038    fn clone_with_fresh_execution_batch_keeps_call_depth_limit() {
3039        let mut ctx = new_mock_executor_context(None);
3040        ctx.machine_call_depth_limit = 123;
3041        let cloned = ctx.clone_with_fresh_execution_batch();
3042        assert_eq!(cloned.machine_call_depth_limit, 123);
3043    }
3044
3045    #[tokio::test]
3046    async fn appearance_oit_deferral() {
3047        for (geometry_only, enable_ssao, opacity) in [
3048            (true, true, 60),
3049            (false, true, 60),
3050            (true, false, 60),
3051            (true, true, 100),
3052        ] {
3053            let ctx = ExecutorContext::new_mock(Some(ExecutorSettings {
3054                geometry_only,
3055                enable_ssao,
3056                ..Default::default()
3057            }))
3058            .await;
3059            let code = format!(
3060                r##"@settings(kclVersion = 3.0)
3061profile = sketch(on = XY) {{
3062  c = circle(center = [var 0mm, var 0mm], start = [var 1mm, var 0mm])
3063}}
3064body = extrude(region(segments = [profile.c]), length = 2mm)
3065appearance(body, color = "#dba09a", opacity = {opacity})
3066"##
3067            );
3068            let program = crate::Program::parse_no_errs(&code).unwrap();
3069            let mut state = ExecState::new(&ctx);
3070            ctx.run(&program, &mut state).await.unwrap();
3071
3072            let needs_oit = enable_ssao && opacity < 100;
3073            assert_eq!(ctx.needs_oit.load(Ordering::Relaxed), needs_oit);
3074            let sent_oit = state.global.root_module_artifacts.commands.iter().any(|command| {
3075                matches!(&command.command, ModelingCmd::SetOrderIndependentTransparency(command) if command.enabled == Some(true))
3076            });
3077            assert_eq!(sent_oit, needs_oit && !geometry_only);
3078
3079            let cloned = ctx.clone_with_fresh_execution_batch();
3080            assert!(Arc::ptr_eq(&ctx.needs_oit, &cloned.needs_oit));
3081            for _ in 0..2 {
3082                cloned.enable_engine_graphics(&mut state).await.unwrap();
3083                assert_eq!(state.geometry_only(), geometry_only);
3084                cloned.disable_engine_graphics(&mut state).await.unwrap();
3085                assert_eq!(state.geometry_only(), geometry_only);
3086                assert_eq!(cloned.needs_oit.load(Ordering::Relaxed), needs_oit);
3087            }
3088            ctx.send_clear_scene(Some(program.kcl_version), &mut state, SourceRange::synthetic())
3089                .await
3090                .unwrap();
3091            assert!(!cloned.needs_oit.load(Ordering::Relaxed));
3092            ctx.close().await;
3093        }
3094    }
3095
3096    #[tokio::test(flavor = "multi_thread")]
3097    async fn concurrent_foreign_import_preserves_artifact_command() {
3098        let tmpdir = tempfile::TempDir::with_prefix("zma_foreign_import_artifact").unwrap();
3099        tokio::fs::write(tmpdir.path().join("cube.obj"), "o cube\n")
3100            .await
3101            .unwrap();
3102
3103        let program = crate::Program::parse_no_errs("import \"cube.obj\" as cube\n\nmodel = cube\n").unwrap();
3104        let ctx = new_mock_executor_context(Some(crate::TypedPath(tmpdir.path().into())));
3105        let mut exec_state = ExecState::new(&ctx);
3106        let (main_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3107        let outcome = exec_state
3108            .into_exec_outcome(main_ref, &ctx)
3109            .await
3110            .expect("foreign import execution should produce an outcome");
3111        ctx.close().await;
3112
3113        let KclValueView::ImportedGeometry(imported) = &outcome.variables["model"] else {
3114            panic!("model should be imported geometry");
3115        };
3116        let artifact_id = ArtifactId::new(imported.id);
3117        let Some(Artifact::ImportedGeometry(artifact)) = outcome.artifact_graph.get(&artifact_id) else {
3118            panic!("foreign import should produce an imported geometry artifact");
3119        };
3120        assert_eq!(artifact.id, artifact_id);
3121        assert!(!artifact.code_ref.node_path.is_empty());
3122    }
3123
3124    #[tokio::test(flavor = "multi_thread")]
3125    async fn nested_import_preserves_inner_error_and_backtrace() {
3126        // The imported modules live in an in-memory file system under a
3127        // synthetic project directory, so parallel tests share no on-disk
3128        // state and there is nothing to clean up even if the process is
3129        // killed.
3130        let project_dir = crate::TypedPath::new("/zma-kcl-import-error");
3131        let main_path = project_dir.join("main.kcl");
3132        let assembly_path = project_dir.join("assembly.kcl");
3133        let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
3134        // Key each module by the same join that import resolution performs, so
3135        // the lookup matches on every platform.
3136        let files = [
3137            (
3138                project_dir.join("broken.kcl").to_string(),
3139                b"export brokenValue = missingName + 1\n".to_vec(),
3140            ),
3141            (
3142                assembly_path.to_string(),
3143                b"import brokenValue from \"broken.kcl\"\n\nexport assemblyValue = brokenValue\n".to_vec(),
3144            ),
3145        ]
3146        .into_iter()
3147        .collect();
3148        let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
3149        let settings = ExecutorSettings {
3150            project_directory: Some(project_dir),
3151            current_file: Some(main_path.clone()),
3152            ..Default::default()
3153        };
3154        let program = crate::Program::parse_no_errs(main_code).unwrap();
3155
3156        let assert_error = |error: &KclErrorWithOutputs| {
3157            let KclError::UndefinedValue { details, name } = &error.error else {
3158                panic!("expected UndefinedValue, got {:#?}", error.error);
3159            };
3160            assert_eq!(name.as_deref(), Some("missingName"));
3161            assert_eq!(details.message, "`missingName` is not defined");
3162            assert_eq!(
3163                error
3164                    .error
3165                    .backtrace()
3166                    .iter()
3167                    .map(|frame| frame.fn_name.as_deref())
3168                    .collect::<Vec<_>>(),
3169                [Some("import broken.kcl"), Some("import assembly.kcl"), None]
3170            );
3171            assert_eq!(
3172                error
3173                    .error
3174                    .backtrace()
3175                    .iter()
3176                    .map(|frame| frame.kind)
3177                    .collect::<Vec<_>>(),
3178                [
3179                    kcl_error::BacktraceItemKind::Import,
3180                    kcl_error::BacktraceItemKind::Import,
3181                    kcl_error::BacktraceItemKind::Call
3182                ]
3183            );
3184
3185            let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
3186            assert!(report.filename.ends_with("broken.kcl"));
3187            assert_eq!(
3188                report
3189                    .related
3190                    .iter()
3191                    .map(|related| related.filename.as_str())
3192                    .collect::<Vec<_>>(),
3193                [assembly_path.to_string(), main_path.to_string()]
3194            );
3195
3196            let rendered = format!("{:?}", miette::Report::new(report));
3197            assert!(rendered.contains("broken.kcl"));
3198            assert!(rendered.contains("assembly.kcl"));
3199            assert!(rendered.contains("main.kcl"));
3200            assert!(rendered.contains("export brokenValue = missingName + 1"));
3201            assert!(!rendered.contains("Failed to read contents"));
3202        };
3203
3204        let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
3205        mock_ctx.fs = fs.clone();
3206        let mock_error = mock_ctx
3207            .run_mock(
3208                &program,
3209                &MockConfig {
3210                    use_prev_memory: false,
3211                    ..Default::default()
3212                },
3213            )
3214            .await
3215            .unwrap_err();
3216        mock_ctx.close().await;
3217        assert_error(&mock_error);
3218
3219        let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
3220        concurrent_ctx.fs = fs;
3221        let mut exec_state = ExecState::new(&concurrent_ctx);
3222        let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
3223        concurrent_ctx.close().await;
3224        assert_error(&concurrent_error);
3225    }
3226
3227    #[tokio::test(flavor = "multi_thread")]
3228    async fn function_error_across_import_keeps_backtrace_innermost_first() {
3229        // A function defined in an imported module fails when the importing
3230        // module calls it: function frames and the import frame must stay in
3231        // one innermost-first chain.
3232        let project_dir = crate::TypedPath::new("/zma-kcl-import-fn-error");
3233        let main_path = project_dir.join("main.kcl");
3234        let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
3235        let files = [
3236            (
3237                project_dir.join("helper.kcl").to_string(),
3238                b"export fn inner() { return missingName }\nexport fn outer() { return inner() }\n".to_vec(),
3239            ),
3240            (
3241                project_dir.join("assembly.kcl").to_string(),
3242                b"import outer from \"helper.kcl\"\n\nexport assemblyValue = outer()\n".to_vec(),
3243            ),
3244        ]
3245        .into_iter()
3246        .collect();
3247        let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
3248        let settings = ExecutorSettings {
3249            project_directory: Some(project_dir.clone()),
3250            current_file: Some(main_path),
3251            ..Default::default()
3252        };
3253        let program = crate::Program::parse_no_errs(main_code).unwrap();
3254
3255        let assert_error = |error: &KclErrorWithOutputs| {
3256            assert!(
3257                matches!(&error.error, KclError::UndefinedValue { .. }),
3258                "expected UndefinedValue, got {:#?}",
3259                error.error
3260            );
3261            assert_eq!(
3262                error
3263                    .error
3264                    .backtrace()
3265                    .iter()
3266                    .map(|frame| frame.fn_name.as_deref())
3267                    .collect::<Vec<_>>(),
3268                [Some("inner"), Some("outer"), Some("import assembly.kcl"), None]
3269            );
3270            assert_eq!(
3271                error
3272                    .error
3273                    .backtrace()
3274                    .iter()
3275                    .map(|frame| frame.kind)
3276                    .collect::<Vec<_>>(),
3277                [
3278                    kcl_error::BacktraceItemKind::Call,
3279                    kcl_error::BacktraceItemKind::Call,
3280                    kcl_error::BacktraceItemKind::Import,
3281                    kcl_error::BacktraceItemKind::Call
3282                ]
3283            );
3284
3285            let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
3286            assert!(report.filename.ends_with("helper.kcl"));
3287            assert_eq!(
3288                report
3289                    .related
3290                    .iter()
3291                    .map(|related| related.filename.as_str())
3292                    .collect::<Vec<_>>(),
3293                [
3294                    project_dir.join("assembly.kcl").to_string(),
3295                    project_dir.join("main.kcl").to_string()
3296                ]
3297            );
3298            let rendered = format!("{:?}", miette::Report::new(report));
3299            assert!(rendered.contains("return missingName"));
3300            assert!(!rendered.contains("Failed to read contents"));
3301        };
3302
3303        let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
3304        mock_ctx.fs = fs.clone();
3305        let mock_error = mock_ctx
3306            .run_mock(
3307                &program,
3308                &MockConfig {
3309                    use_prev_memory: false,
3310                    ..Default::default()
3311                },
3312            )
3313            .await
3314            .unwrap_err();
3315        mock_ctx.close().await;
3316        assert_error(&mock_error);
3317
3318        let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
3319        concurrent_ctx.fs = fs;
3320        let mut exec_state = ExecState::new(&concurrent_ctx);
3321        let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
3322        concurrent_ctx.close().await;
3323        assert_error(&concurrent_error);
3324    }
3325
3326    /// Convenience function to get a JSON value from memory and unwrap.
3327    #[track_caller]
3328    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
3329        memory.memory.get_from_unchecked(name, env).unwrap()
3330    }
3331
3332    async fn execute_variables_with_backend(
3333        code: &str,
3334        backend: memory::MemoryBackendKind,
3335    ) -> IndexMap<String, KclValueView> {
3336        execute_outcome_with_backend(code, backend).await.variables
3337    }
3338
3339    async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
3340        let program = crate::Program::parse_no_errs(code).unwrap();
3341        let ctx = ExecutorContext::new_mock(None).await;
3342        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
3343        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3344        let outcome = exec_state
3345            .into_exec_outcome(env_ref, &ctx)
3346            .await
3347            .expect("test execution outcome should collect variables");
3348        ctx.close().await;
3349        outcome
3350    }
3351
3352    async fn execute_error_variables_with_backend(
3353        code: &str,
3354        backend: memory::MemoryBackendKind,
3355    ) -> IndexMap<String, KclValueView> {
3356        let program = crate::Program::parse_no_errs(code).unwrap();
3357        let ctx = ExecutorContext::new_mock(None).await;
3358        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
3359        let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
3360        ctx.close().await;
3361        error.variables
3362    }
3363
3364    async fn execute_project_variables_with_backend(
3365        main_code: &str,
3366        files: &[(&str, &str)],
3367        backend: memory::MemoryBackendKind,
3368    ) -> IndexMap<String, KclValueView> {
3369        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
3370        for (name, contents) in files {
3371            tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
3372        }
3373
3374        let program = crate::Program::parse_no_errs(main_code).unwrap();
3375        let ctx = ExecutorContext {
3376            engine: Arc::new(EngineManager::new_mock()),
3377            cache: Cache::default(),
3378            engine_batch: EngineBatchContext::default(),
3379            fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
3380            settings: ExecutorSettings {
3381                project_directory: Some(crate::TypedPath(tmpdir.path().into())),
3382                ..Default::default()
3383            },
3384            context_type: ContextType::Mock,
3385            execution_callbacks: Default::default(),
3386            machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
3387            configure_engine_render: true,
3388            needs_oit: Default::default(),
3389        };
3390        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
3391        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3392        let outcome = exec_state
3393            .into_exec_outcome(env_ref, &ctx)
3394            .await
3395            .expect("test execution outcome should collect variables");
3396        ctx.close().await;
3397        outcome.variables
3398    }
3399
3400    async fn run_with_caching_variables_with_backend(
3401        code: &str,
3402        backend: memory::MemoryBackendKind,
3403    ) -> IndexMap<String, KclValueView> {
3404        let _backend = memory::MemoryBackendKind::override_for_test(backend);
3405
3406        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
3407        let program = crate::Program::parse_no_errs(code).unwrap();
3408        ctx.run_with_caching(program.clone()).await.unwrap();
3409        let cached = ctx.run_with_caching(program).await.unwrap();
3410
3411        ctx.close().await;
3412        cached.variables
3413    }
3414
3415    async fn run_mock_variables_with_backend(
3416        code: &str,
3417        backend: memory::MemoryBackendKind,
3418    ) -> IndexMap<String, KclValueView> {
3419        let _backend = memory::MemoryBackendKind::override_for_test(backend);
3420
3421        let ctx = ExecutorContext::new_mock(None).await;
3422        let first = crate::Program::parse_no_errs("x = 2").unwrap();
3423        ctx.run_mock(
3424            &first,
3425            &MockConfig {
3426                use_prev_memory: false,
3427                ..Default::default()
3428            },
3429        )
3430        .await
3431        .unwrap();
3432
3433        let program = crate::Program::parse_no_errs(code).unwrap();
3434        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3435
3436        ctx.close().await;
3437        outcome.variables
3438    }
3439
3440    fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
3441        let mut keys = variables.keys().cloned().collect::<Vec<_>>();
3442        keys.sort();
3443        keys
3444    }
3445
3446    async fn collect_backend_results<T, Fut>(
3447        mut run: impl FnMut(memory::MemoryBackendKind) -> Fut,
3448    ) -> Vec<(memory::MemoryBackendKind, T)>
3449    where
3450        Fut: std::future::Future<Output = T>,
3451    {
3452        let all = memory::MemoryBackendKind::all();
3453        let mut results = Vec::with_capacity(all.len());
3454        for &kind in all {
3455            results.push((kind, run(kind).await));
3456        }
3457        results
3458    }
3459
3460    fn assert_backend_results_match<T>(results: &[(memory::MemoryBackendKind, T)])
3461    where
3462        T: std::fmt::Debug + PartialEq,
3463    {
3464        let (first, rest) = results.split_first().expect("expected at least one memory backend");
3465        let (first_kind, first_result) = first;
3466        for (kind, result) in rest {
3467            assert_eq!(
3468                result, first_result,
3469                "memory kind {kind:?} doesn't match {first_kind:?}"
3470            );
3471        }
3472    }
3473
3474    fn assert_backend_variable_results_match_expected_keys(
3475        results: &[(memory::MemoryBackendKind, IndexMap<String, KclValueView>)],
3476        expected_keys: &[&str],
3477    ) {
3478        let (first_kind, first_variables) = results.first().expect("expected at least one memory backend");
3479        let expected_keys = expected_keys.iter().map(|key| (*key).to_owned()).collect::<Vec<_>>();
3480        assert_eq!(
3481            sorted_variable_keys(first_variables),
3482            expected_keys,
3483            "memory kind {first_kind:?} doesn't match expected variables"
3484        );
3485        assert_backend_results_match(results);
3486    }
3487
3488    fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
3489        let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
3490        let KclValueView::Number { value, .. } = value else {
3491            panic!("expected `{key}` to be a number, got {value:?}");
3492        };
3493        assert_eq!(*value, expected, "{key}: {value:?}");
3494    }
3495
3496    #[tokio::test(flavor = "multi_thread")]
3497    async fn exec_outcome_variables_match_between_memory_backends() {
3498        let code = "x = 2\ny = x + 1\narr = [x, y]";
3499
3500        let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
3501
3502        assert_backend_variable_results_match_expected_keys(&results, &["arr", "x", "y"]);
3503    }
3504
3505    #[tokio::test(flavor = "multi_thread")]
3506    async fn error_output_variables_match_between_memory_backends() {
3507        let code = "x = 2\ny = missing + 1";
3508
3509        let results = collect_backend_results(|kind| execute_error_variables_with_backend(code, kind)).await;
3510
3511        assert_backend_variable_results_match_expected_keys(&results, &["x"]);
3512    }
3513
3514    #[tokio::test(flavor = "multi_thread")]
3515    async fn cached_execution_variables_match_between_memory_backends() {
3516        let code = "x = 2\ny = x + 1";
3517
3518        let results = collect_backend_results(|kind| run_with_caching_variables_with_backend(code, kind)).await;
3519
3520        assert_backend_variable_results_match_expected_keys(&results, &["x", "y"]);
3521    }
3522
3523    #[tokio::test(flavor = "multi_thread")]
3524    async fn mock_execution_variables_match_between_memory_backends() {
3525        let code = "y = x + 1";
3526
3527        let results = collect_backend_results(|kind| run_mock_variables_with_backend(code, kind)).await;
3528
3529        assert_backend_variable_results_match_expected_keys(&results, &["y"]);
3530    }
3531
3532    #[tokio::test(flavor = "multi_thread")]
3533    async fn module_imports_and_exported_closures_match_between_memory_backends() {
3534        let module_code = r#"
3535export base = 40
3536
3537export fn addBase(n) {
3538  return n + base
3539}
3540"#;
3541        let main_code = r#"
3542import base, addBase from 'math.kcl'
3543import 'math.kcl'
3544
3545named = addBase(n = 2)
3546qualified = math::addBase(n = 1)
3547direct = math::base
3548"#;
3549
3550        let files = [("math.kcl", module_code)];
3551        let results =
3552            collect_backend_results(|kind| execute_project_variables_with_backend(main_code, &files, kind)).await;
3553
3554        let (_, first_variables) = results.first().expect("expected at least one memory backend");
3555        assert_number_variable(first_variables, "named", 42.0);
3556        assert_number_variable(first_variables, "qualified", 41.0);
3557        assert_number_variable(first_variables, "direct", 40.0);
3558        assert_backend_results_match(&results);
3559    }
3560
3561    #[tokio::test(flavor = "multi_thread")]
3562    async fn sketch_block_variables_match_between_memory_backends() {
3563        let code = r#"
3564sketch001 = sketch(on = XY) {
3565  line1 = line(start = [0, 0], end = [1, 0])
3566  line2 = line(start = [1, 0], end = [0, 1])
3567}
3568lineCount = 2
3569"#;
3570
3571        let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
3572
3573        let (_, first_variables) = results.first().expect("expected at least one memory backend");
3574        assert!(first_variables.contains_key("sketch001"), "actual: {first_variables:?}");
3575        assert_number_variable(first_variables, "lineCount", 2.0);
3576        assert_backend_results_match(&results);
3577    }
3578
3579    #[tokio::test(flavor = "multi_thread")]
3580    async fn tag_call_stack_lookup_matches_between_memory_backends() {
3581        let code = r#"
3582sketch001 = startSketchOn(XY)
3583  |> startProfile(at = [0, 0])
3584  |> xLine(length = 10, tag = $seg01)
3585
3586segLength = segLen(seg01)
3587"#;
3588
3589        let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
3590
3591        let (_, first_variables) = results.first().expect("expected at least one memory backend");
3592        assert_number_variable(first_variables, "segLength", 10.0);
3593        assert_backend_results_match(&results);
3594    }
3595
3596    #[tokio::test(flavor = "multi_thread")]
3597    async fn test_execute_warn() {
3598        let text = "@blah";
3599        let result = parse_execute(text).await.unwrap();
3600        let errs = result.exec_state.issues();
3601        assert_eq!(errs.len(), 1);
3602        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
3603        assert!(
3604            errs[0].message.contains("Unknown annotation"),
3605            "unexpected warning message: {}",
3606            errs[0].message
3607        );
3608    }
3609
3610    #[tokio::test(flavor = "multi_thread")]
3611    async fn test_execute_fn_definitions() {
3612        let ast = r#"fn def(@x) {
3613  return x
3614}
3615fn ghi(@x) {
3616  return x
3617}
3618fn jkl(@x) {
3619  return x
3620}
3621fn hmm(@x) {
3622  return x
3623}
3624
3625yo = 5 + 6
3626
3627abc = 3
3628identifierGuy = 5
3629part001 = startSketchOn(XY)
3630|> startProfile(at = [-1.2, 4.83])
3631|> line(end = [2.8, 0])
3632|> angledLine(angle = 100 + 100, length = 3.01)
3633|> angledLine(angle = abc, length = 3.02)
3634|> angledLine(angle = def(yo), length = 3.03)
3635|> angledLine(angle = ghi(2), length = 3.04)
3636|> angledLine(angle = jkl(yo) + 2, length = 3.05)
3637|> close()
3638yo2 = hmm([identifierGuy + 5])"#;
3639
3640        parse_execute(ast).await.unwrap();
3641    }
3642
3643    #[tokio::test(flavor = "multi_thread")]
3644    async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
3645        let code = r#"
3646firstProfile = sketch(on = XY) {
3647  edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
3648  edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
3649  edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
3650  edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
3651  coincident([edge1.end, edge2.start])
3652  coincident([edge2.end, edge3.start])
3653  coincident([edge3.end, edge4.start])
3654  coincident([edge4.end, edge1.start])
3655}
3656
3657secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
3658  edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
3659  edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
3660  edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
3661  edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
3662  coincident([edge5.end, edge6.start])
3663  coincident([edge6.end, edge7.start])
3664  coincident([edge7.end, edge8.start])
3665  coincident([edge8.end, edge5.start])
3666}
3667
3668firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
3669secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
3670"#;
3671
3672        let result = parse_execute(code).await.unwrap();
3673        assert!(result.exec_state.issues().is_empty());
3674    }
3675
3676    #[tokio::test(flavor = "multi_thread")]
3677    async fn sketch_block_artifact_preserves_standard_plane_name() {
3678        let code = r#"
3679sketch001 = sketch(on = -YZ) {
3680  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
3681}
3682"#;
3683
3684        let result = parse_execute(code).await.unwrap();
3685        let sketch_blocks = result
3686            .exec_state
3687            .global
3688            .artifacts
3689            .graph
3690            .values()
3691            .filter_map(|artifact| match artifact {
3692                Artifact::SketchBlock(block) => Some(block),
3693                _ => None,
3694            })
3695            .collect::<Vec<_>>();
3696
3697        assert_eq!(sketch_blocks.len(), 1);
3698        assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
3699    }
3700
3701    #[tokio::test(flavor = "multi_thread")]
3702    async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
3703        let code = r#"
3704sketch001 = sketch(on = YZ) {
3705  line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
3706  line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
3707  line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
3708  line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
3709  coincident([line1.end, line2.start])
3710  coincident([line2.end, line3.start])
3711  coincident([line3.end, line4.start])
3712  coincident([line4.end, line1.start])
3713}
3714
3715sketch002 = sketch(on = -XZ) {
3716  line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
3717  line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
3718  line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
3719  line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
3720  coincident([line5.end, line6.start])
3721  coincident([line6.end, line7.start])
3722  coincident([line7.end, line8.start])
3723  coincident([line8.end, line5.start])
3724}
3725
3726region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
3727extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
3728region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
3729extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
3730
3731myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
3732"#;
3733
3734        let result = parse_execute(code).await.unwrap();
3735        assert!(result.exec_state.issues().is_empty());
3736    }
3737
3738    #[tokio::test(flavor = "multi_thread")]
3739    async fn issue_10741_point_circle_coincident_executes() {
3740        let code = r#"
3741sketch001 = sketch(on = YZ) {
3742  circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
3743  line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
3744  coincident([line1.start, circle1])
3745}
3746"#;
3747
3748        let result = parse_execute(code).await.unwrap();
3749        assert!(
3750            result
3751                .exec_state
3752                .issues()
3753                .iter()
3754                .all(|issue| issue.severity != Severity::Error),
3755            "unexpected execution issues: {:#?}",
3756            result.exec_state.issues()
3757        );
3758    }
3759
3760    #[tokio::test(flavor = "multi_thread")]
3761    async fn test_execute_with_pipe_substitutions_unary() {
3762        let ast = r#"myVar = 3
3763part001 = startSketchOn(XY)
3764  |> startProfile(at = [0, 0])
3765  |> line(end = [3, 4], tag = $seg01)
3766  |> line(end = [
3767  min([segLen(seg01), myVar]),
3768  -legLen(hypotenuse = segLen(seg01), leg = myVar)
3769])
3770"#;
3771
3772        parse_execute(ast).await.unwrap();
3773    }
3774
3775    #[tokio::test(flavor = "multi_thread")]
3776    async fn test_execute_with_pipe_substitutions() {
3777        let ast = r#"myVar = 3
3778part001 = startSketchOn(XY)
3779  |> startProfile(at = [0, 0])
3780  |> line(end = [3, 4], tag = $seg01)
3781  |> line(end = [
3782  min([segLen(seg01), myVar]),
3783  legLen(hypotenuse = segLen(seg01), leg = myVar)
3784])
3785"#;
3786
3787        parse_execute(ast).await.unwrap();
3788    }
3789
3790    #[tokio::test(flavor = "multi_thread")]
3791    async fn test_execute_with_inline_comment() {
3792        let ast = r#"baseThick = 1
3793armAngle = 60
3794
3795baseThickHalf = baseThick / 2
3796halfArmAngle = armAngle / 2
3797
3798arrExpShouldNotBeIncluded = [1, 2, 3]
3799objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
3800
3801part001 = startSketchOn(XY)
3802  |> startProfile(at = [0, 0])
3803  |> yLine(endAbsolute = 1)
3804  |> xLine(length = 3.84) // selection-range-7ish-before-this
3805
3806variableBelowShouldNotBeIncluded = 3
3807"#;
3808
3809        parse_execute(ast).await.unwrap();
3810    }
3811
3812    #[tokio::test(flavor = "multi_thread")]
3813    async fn test_execute_with_function_literal_in_pipe() {
3814        let ast = r#"w = 20
3815l = 8
3816h = 10
3817
3818fn thing() {
3819  return -8
3820}
3821
3822firstExtrude = startSketchOn(XY)
3823  |> startProfile(at = [0,0])
3824  |> line(end = [0, l])
3825  |> line(end = [w, 0])
3826  |> line(end = [0, thing()])
3827  |> close()
3828  |> extrude(length = h)"#;
3829
3830        parse_execute(ast).await.unwrap();
3831    }
3832
3833    #[tokio::test(flavor = "multi_thread")]
3834    async fn test_execute_with_function_unary_in_pipe() {
3835        let ast = r#"w = 20
3836l = 8
3837h = 10
3838
3839fn thing(@x) {
3840  return -x
3841}
3842
3843firstExtrude = startSketchOn(XY)
3844  |> startProfile(at = [0,0])
3845  |> line(end = [0, l])
3846  |> line(end = [w, 0])
3847  |> line(end = [0, thing(8)])
3848  |> close()
3849  |> extrude(length = h)"#;
3850
3851        parse_execute(ast).await.unwrap();
3852    }
3853
3854    #[tokio::test(flavor = "multi_thread")]
3855    async fn test_execute_with_function_array_in_pipe() {
3856        let ast = r#"w = 20
3857l = 8
3858h = 10
3859
3860fn thing(@x) {
3861  return [0, -x]
3862}
3863
3864firstExtrude = startSketchOn(XY)
3865  |> startProfile(at = [0,0])
3866  |> line(end = [0, l])
3867  |> line(end = [w, 0])
3868  |> line(end = thing(8))
3869  |> close()
3870  |> extrude(length = h)"#;
3871
3872        parse_execute(ast).await.unwrap();
3873    }
3874
3875    #[tokio::test(flavor = "multi_thread")]
3876    async fn test_execute_with_function_call_in_pipe() {
3877        let ast = r#"w = 20
3878l = 8
3879h = 10
3880
3881fn other_thing(@y) {
3882  return -y
3883}
3884
3885fn thing(@x) {
3886  return other_thing(x)
3887}
3888
3889firstExtrude = startSketchOn(XY)
3890  |> startProfile(at = [0,0])
3891  |> line(end = [0, l])
3892  |> line(end = [w, 0])
3893  |> line(end = [0, thing(8)])
3894  |> close()
3895  |> extrude(length = h)"#;
3896
3897        parse_execute(ast).await.unwrap();
3898    }
3899
3900    #[tokio::test(flavor = "multi_thread")]
3901    async fn test_execute_with_function_sketch() {
3902        let ast = r#"fn box(h, l, w) {
3903 myBox = startSketchOn(XY)
3904    |> startProfile(at = [0,0])
3905    |> line(end = [0, l])
3906    |> line(end = [w, 0])
3907    |> line(end = [0, -l])
3908    |> close()
3909    |> extrude(length = h)
3910
3911  return myBox
3912}
3913
3914fnBox = box(h = 3, l = 6, w = 10)"#;
3915
3916        parse_execute(ast).await.unwrap();
3917    }
3918
3919    #[tokio::test(flavor = "multi_thread")]
3920    async fn test_get_member_of_object_with_function_period() {
3921        let ast = r#"fn box(@obj) {
3922 myBox = startSketchOn(XY)
3923    |> startProfile(at = obj.start)
3924    |> line(end = [0, obj.l])
3925    |> line(end = [obj.w, 0])
3926    |> line(end = [0, -obj.l])
3927    |> close()
3928    |> extrude(length = obj.h)
3929
3930  return myBox
3931}
3932
3933thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
3934"#;
3935        parse_execute(ast).await.unwrap();
3936    }
3937
3938    #[tokio::test(flavor = "multi_thread")]
3939    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
3940    async fn test_object_member_starting_pipeline() {
3941        let ast = r#"
3942fn test2() {
3943  return {
3944    thing: startSketchOn(XY)
3945      |> startProfile(at = [0, 0])
3946      |> line(end = [0, 1])
3947      |> line(end = [1, 0])
3948      |> line(end = [0, -1])
3949      |> close()
3950  }
3951}
3952
3953x2 = test2()
3954
3955x2.thing
3956  |> extrude(length = 10)
3957"#;
3958        parse_execute(ast).await.unwrap();
3959    }
3960
3961    #[tokio::test(flavor = "multi_thread")]
3962    #[ignore] // ignore til we get loops
3963    async fn test_execute_with_function_sketch_loop_objects() {
3964        let ast = r#"fn box(obj) {
3965let myBox = startSketchOn(XY)
3966    |> startProfile(at = obj.start)
3967    |> line(end = [0, obj.l])
3968    |> line(end = [obj.w, 0])
3969    |> line(end = [0, -obj.l])
3970    |> close()
3971    |> extrude(length = obj.h)
3972
3973  return myBox
3974}
3975
3976for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
3977  thisBox = box(var)
3978}"#;
3979
3980        parse_execute(ast).await.unwrap();
3981    }
3982
3983    #[tokio::test(flavor = "multi_thread")]
3984    #[ignore] // ignore til we get loops
3985    async fn test_execute_with_function_sketch_loop_array() {
3986        let ast = r#"fn box(h, l, w, start) {
3987 myBox = startSketchOn(XY)
3988    |> startProfile(at = [0,0])
3989    |> line(end = [0, l])
3990    |> line(end = [w, 0])
3991    |> line(end = [0, -l])
3992    |> close()
3993    |> extrude(length = h)
3994
3995  return myBox
3996}
3997
3998for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
3999  const thisBox = box(var[0], var[1], var[2], var[3])
4000}"#;
4001
4002        parse_execute(ast).await.unwrap();
4003    }
4004
4005    #[tokio::test(flavor = "multi_thread")]
4006    async fn test_get_member_of_array_with_function() {
4007        let ast = r#"fn box(@arr) {
4008 myBox =startSketchOn(XY)
4009    |> startProfile(at = arr[0])
4010    |> line(end = [0, arr[1]])
4011    |> line(end = [arr[2], 0])
4012    |> line(end = [0, -arr[1]])
4013    |> close()
4014    |> extrude(length = arr[3])
4015
4016  return myBox
4017}
4018
4019thisBox = box([[0,0], 6, 10, 3])
4020
4021"#;
4022        parse_execute(ast).await.unwrap();
4023    }
4024
4025    #[tokio::test(flavor = "multi_thread")]
4026    async fn test_function_cannot_access_future_definitions() {
4027        let ast = r#"
4028fn returnX() {
4029  // x shouldn't be defined yet.
4030  return x
4031}
4032
4033x = 5
4034
4035answer = returnX()"#;
4036
4037        let result = parse_execute(ast).await;
4038        let err = result.unwrap_err();
4039        assert_eq!(err.message(), "`x` is not defined");
4040    }
4041
4042    #[tokio::test(flavor = "multi_thread")]
4043    async fn test_override_prelude() {
4044        let text = "PI = 3.0";
4045        let result = parse_execute(text).await.unwrap();
4046        let issues = result.exec_state.issues();
4047        assert!(issues.is_empty(), "issues={issues:#?}");
4048    }
4049
4050    #[tokio::test(flavor = "multi_thread")]
4051    async fn type_aliases() {
4052        let text = r#"@settings(kclVersion = "3.0-preview")
4053type MyTy = [number; 2]
4054fn foo(@x: MyTy) {
4055    return x[0]
4056}
4057
4058foo([0, 1])
4059
4060type Other = MyTy | Helix
4061"#;
4062        let result = parse_execute(text).await.unwrap();
4063        let issues = result.exec_state.issues();
4064        assert!(issues.is_empty(), "issues={issues:#?}");
4065    }
4066
4067    #[tokio::test(flavor = "multi_thread")]
4068    async fn test_cannot_shebang_in_fn() {
4069        let ast = r#"
4070fn foo() {
4071  #!hello
4072  return true
4073}
4074
4075foo
4076"#;
4077
4078        let result = parse_execute(ast).await;
4079        let err = result.unwrap_err();
4080        assert_eq!(
4081            err,
4082            KclError::new_syntax(KclErrorDetails::new(
4083                "Unexpected token: #".to_owned(),
4084                vec![SourceRange::new(14, 15, ModuleId::default())],
4085            )),
4086        );
4087    }
4088
4089    #[tokio::test(flavor = "multi_thread")]
4090    async fn test_pattern_transform_function_cannot_access_future_definitions() {
4091        let ast = r#"
4092fn transform(@replicaId) {
4093  // x shouldn't be defined yet.
4094  scale = x
4095  return {
4096    translate = [0, 0, replicaId * 10],
4097    scale = [scale, 1, 0],
4098  }
4099}
4100
4101fn layer() {
4102  return startSketchOn(XY)
4103    |> circle( center= [0, 0], radius= 1, tag = $tag1)
4104    |> extrude(length = 10)
4105}
4106
4107x = 5
4108
4109// The 10 layers are replicas of each other, with a transform applied to each.
4110shape = layer() |> patternTransform(instances = 10, transform = transform)
4111"#;
4112
4113        let result = parse_execute(ast).await;
4114        let err = result.unwrap_err();
4115        assert_eq!(err.message(), "`x` is not defined",);
4116    }
4117
4118    // ADAM: Move some of these into simulation tests.
4119
4120    #[tokio::test(flavor = "multi_thread")]
4121    async fn test_math_execute_with_functions() {
4122        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
4123        let result = parse_execute(ast).await.unwrap();
4124        assert_eq!(
4125            5.0,
4126            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4127                .as_f64()
4128                .unwrap()
4129        );
4130    }
4131
4132    #[tokio::test(flavor = "multi_thread")]
4133    async fn test_math_execute() {
4134        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
4135        let result = parse_execute(ast).await.unwrap();
4136        assert_eq!(
4137            7.4,
4138            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4139                .as_f64()
4140                .unwrap()
4141        );
4142    }
4143
4144    #[tokio::test(flavor = "multi_thread")]
4145    async fn test_string_uppercase() {
4146        let composed = "\u{e9}";
4147        let uppercase_composed = "\u{c9}";
4148        let decomposed = "e\u{301}";
4149        let uppercase_decomposed = "E\u{301}";
4150        let code = format!(
4151            r#"
4152ascii = string::uppercase("Kcl")
4153unicode_expansion = string::uppercase("Straße")
4154uncased = string::uppercase("東京")
4155empty = string::uppercase("")
4156composed = string::uppercase("{composed}")
4157decomposed = string::uppercase("{decomposed}")
4158piped = "ready" |> string::uppercase()
4159"#
4160        );
4161
4162        let result = parse_execute(&code).await.unwrap();
4163        for (name, expected) in [
4164            ("ascii", "KCL"),
4165            ("unicode_expansion", "STRASSE"),
4166            ("uncased", "東京"),
4167            ("empty", ""),
4168            ("composed", uppercase_composed),
4169            ("decomposed", uppercase_decomposed),
4170            ("piped", "READY"),
4171        ] {
4172            assert_eq!(
4173                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4174                    .as_str()
4175                    .unwrap(),
4176                expected,
4177                "{name}"
4178            );
4179        }
4180    }
4181
4182    #[tokio::test(flavor = "multi_thread")]
4183    async fn test_string_lowercase() {
4184        let composed = "\u{c9}";
4185        let lowercase_composed = "\u{e9}";
4186        let decomposed = "E\u{301}";
4187        let lowercase_decomposed = "e\u{301}";
4188        let expanded = "i\u{307}";
4189        let code = format!(
4190            r#"
4191ascii = string::lowercase("KCL")
4192final_sigma = string::lowercase("ΟΣ")
4193medial_sigma = string::lowercase("ΟΣΑ")
4194unicode_expansion = string::lowercase("İ")
4195uncased = string::lowercase("東京")
4196empty = string::lowercase("")
4197composed = string::lowercase("{composed}")
4198decomposed = string::lowercase("{decomposed}")
4199piped = "READY" |> string::lowercase()
4200"#
4201        );
4202
4203        let result = parse_execute(&code).await.unwrap();
4204        for (name, expected) in [
4205            ("ascii", "kcl"),
4206            ("final_sigma", "ος"),
4207            ("medial_sigma", "οσα"),
4208            ("unicode_expansion", expanded),
4209            ("uncased", "東京"),
4210            ("empty", ""),
4211            ("composed", lowercase_composed),
4212            ("decomposed", lowercase_decomposed),
4213            ("piped", "ready"),
4214        ] {
4215            assert_eq!(
4216                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4217                    .as_str()
4218                    .unwrap(),
4219                expected,
4220                "{name}"
4221            );
4222        }
4223    }
4224
4225    #[tokio::test(flavor = "multi_thread")]
4226    async fn test_string_is_equal() {
4227        let composed = "\u{e9}";
4228        let decomposed = "e\u{301}";
4229        let code = format!(
4230            r#"
4231exact_same = string::isEqual("KCL", to = "KCL")
4232exact_different_case = string::isEqual("KCL", to = "kcl")
4233explicit_case_sensitive = string::isEqual("KCL", to = "kcl", caseInsensitive = false)
4234case_insensitive_ascii = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
4235case_fold_expansion = string::isEqual("Straße", to = "STRASSE", caseInsensitive = true)
4236case_fold_expansion_reversed = string::isEqual("STRASSE", to = "Straße", caseInsensitive = true)
4237case_fold_sigma = string::isEqual("ος", to = "οσ", caseInsensitive = true)
4238case_fold_non_turkic = string::isEqual("I", to = "i", caseInsensitive = true)
4239case_fold_not_turkic = string::isEqual("I", to = "ı", caseInsensitive = true)
4240empty_same = string::isEqual("", to = "")
4241empty_different = string::isEqual("", to = "KCL")
4242exact_without_normalization = string::isEqual("{composed}", to = "{decomposed}")
4243case_fold_without_normalization = string::isEqual("{composed}", to = "{decomposed}", caseInsensitive = true)
4244piped = "ready" |> string::isEqual(to = "READY", caseInsensitive = true)
4245"#
4246        );
4247
4248        let result = parse_execute(&code).await.unwrap();
4249        for (name, expected) in [
4250            ("exact_same", true),
4251            ("exact_different_case", false),
4252            ("explicit_case_sensitive", false),
4253            ("case_insensitive_ascii", true),
4254            ("case_fold_expansion", true),
4255            ("case_fold_expansion_reversed", true),
4256            ("case_fold_sigma", true),
4257            ("case_fold_non_turkic", true),
4258            ("case_fold_not_turkic", false),
4259            ("empty_same", true),
4260            ("empty_different", false),
4261            ("exact_without_normalization", false),
4262            ("case_fold_without_normalization", false),
4263            ("piped", true),
4264        ] {
4265            assert_eq!(
4266                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4267                    .as_bool()
4268                    .unwrap(),
4269                expected,
4270                "{name}"
4271            );
4272        }
4273    }
4274
4275    #[tokio::test(flavor = "multi_thread")]
4276    async fn test_string_is_equal_inside_sketch_block_is_predicate() {
4277        let code = r#"
4278@settings(experimentalFeatures = allow)
4279
4280sketch(on = XY) {
4281  stringsAreEqual = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
4282}
4283"#;
4284
4285        parse_execute(code).await.unwrap();
4286    }
4287
4288    #[tokio::test(flavor = "multi_thread")]
4289    async fn test_string_trim() {
4290        let ascii_whitespace = " \t\n";
4291        let tab = "\t";
4292        let non_breaking_space = "\u{a0}";
4293        let em_space = "\u{2003}";
4294        let ideographic_space = "\u{3000}";
4295        let zero_width_space = "\u{200b}";
4296        let decomposed = "e\u{301}";
4297        let code = format!(
4298            r#"
4299ascii = string::trim("{ascii_whitespace}KCL{ascii_whitespace}")
4300internal = string::trim("  KCL{tab}strings  ")
4301unicode = string::trim("{non_breaking_space}{em_space}KCL{ideographic_space}")
4302all_whitespace = string::trim("{ascii_whitespace}{non_breaking_space}")
4303empty = string::trim("")
4304unchanged = string::trim("KCL")
4305without_normalization = string::trim(" {decomposed} ")
4306non_whitespace = string::trim("{zero_width_space}KCL{zero_width_space}")
4307piped = "  ready  " |> string::trim()
4308"#
4309        );
4310
4311        let result = parse_execute(&code).await.unwrap();
4312        let non_whitespace = format!("{zero_width_space}KCL{zero_width_space}");
4313        for (name, expected) in [
4314            ("ascii", "KCL"),
4315            ("internal", "KCL\tstrings"),
4316            ("unicode", "KCL"),
4317            ("all_whitespace", ""),
4318            ("empty", ""),
4319            ("unchanged", "KCL"),
4320            ("without_normalization", decomposed),
4321            ("non_whitespace", non_whitespace.as_str()),
4322            ("piped", "ready"),
4323        ] {
4324            assert_eq!(
4325                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4326                    .as_str()
4327                    .unwrap(),
4328                expected,
4329                "{name}"
4330            );
4331        }
4332    }
4333
4334    #[tokio::test(flavor = "multi_thread")]
4335    async fn test_string_trim_start() {
4336        let ascii_whitespace = " \t\n";
4337        let tab = "\t";
4338        let non_breaking_space = "\u{a0}";
4339        let em_space = "\u{2003}";
4340        let ideographic_space = "\u{3000}";
4341        let zero_width_space = "\u{200b}";
4342        let decomposed = "e\u{301}";
4343        let code = format!(
4344            r#"
4345ascii = string::trimStart("{ascii_whitespace}KCL{ascii_whitespace}")
4346internal = string::trimStart("  KCL{tab}strings")
4347unicode = string::trimStart("{non_breaking_space}{em_space}KCL{ideographic_space}")
4348all_whitespace = string::trimStart("{ascii_whitespace}{non_breaking_space}")
4349empty = string::trimStart("")
4350unchanged = string::trimStart("KCL")
4351without_normalization = string::trimStart(" {decomposed}")
4352non_whitespace_prefix = string::trimStart("{zero_width_space}{ascii_whitespace}KCL")
4353piped = "  ready  " |> string::trimStart()
4354"#
4355        );
4356
4357        let result = parse_execute(&code).await.unwrap();
4358        let ascii = format!("KCL{ascii_whitespace}");
4359        let unicode = format!("KCL{ideographic_space}");
4360        let non_whitespace_prefix = format!("{zero_width_space}{ascii_whitespace}KCL");
4361        for (name, expected) in [
4362            ("ascii", ascii.as_str()),
4363            ("internal", "KCL\tstrings"),
4364            ("unicode", unicode.as_str()),
4365            ("all_whitespace", ""),
4366            ("empty", ""),
4367            ("unchanged", "KCL"),
4368            ("without_normalization", decomposed),
4369            ("non_whitespace_prefix", non_whitespace_prefix.as_str()),
4370            ("piped", "ready  "),
4371        ] {
4372            assert_eq!(
4373                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4374                    .as_str()
4375                    .unwrap(),
4376                expected,
4377                "{name}"
4378            );
4379        }
4380    }
4381
4382    #[tokio::test(flavor = "multi_thread")]
4383    async fn test_string_trim_end() {
4384        let ascii_whitespace = " \t\n";
4385        let tab = "\t";
4386        let non_breaking_space = "\u{a0}";
4387        let em_space = "\u{2003}";
4388        let ideographic_space = "\u{3000}";
4389        let zero_width_space = "\u{200b}";
4390        let decomposed = "e\u{301}";
4391        let code = format!(
4392            r#"
4393ascii = string::trimEnd("{ascii_whitespace}KCL{ascii_whitespace}")
4394internal = string::trimEnd("KCL{tab}strings  ")
4395unicode = string::trimEnd("{non_breaking_space}KCL{em_space}{ideographic_space}")
4396all_whitespace = string::trimEnd("{ascii_whitespace}{non_breaking_space}")
4397empty = string::trimEnd("")
4398unchanged = string::trimEnd("KCL")
4399without_normalization = string::trimEnd("{decomposed} ")
4400non_whitespace_suffix = string::trimEnd("KCL{ascii_whitespace}{zero_width_space}")
4401piped = "  ready  " |> string::trimEnd()
4402"#
4403        );
4404
4405        let result = parse_execute(&code).await.unwrap();
4406        let ascii = format!("{ascii_whitespace}KCL");
4407        let unicode = format!("{non_breaking_space}KCL");
4408        let non_whitespace_suffix = format!("KCL{ascii_whitespace}{zero_width_space}");
4409        for (name, expected) in [
4410            ("ascii", ascii.as_str()),
4411            ("internal", "KCL\tstrings"),
4412            ("unicode", unicode.as_str()),
4413            ("all_whitespace", ""),
4414            ("empty", ""),
4415            ("unchanged", "KCL"),
4416            ("without_normalization", decomposed),
4417            ("non_whitespace_suffix", non_whitespace_suffix.as_str()),
4418            ("piped", "  ready"),
4419        ] {
4420            assert_eq!(
4421                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4422                    .as_str()
4423                    .unwrap(),
4424                expected,
4425                "{name}"
4426            );
4427        }
4428    }
4429
4430    #[tokio::test(flavor = "multi_thread")]
4431    async fn test_string_to_string() {
4432        // Each case runs on its own so a failure names the expression that
4433        // produced it rather than collapsing the whole table.
4434        for (name, expr, expected) in [
4435            // Every row of the table in the `toString` doc comment appears
4436            // here, so the documentation cannot drift from the behaviour.
4437            ("unitless integer", "12", "12"),
4438            ("unitless fractional", "1.5", "1.5"),
4439            ("no digits dropped", "0.1 + 0.2", "0.30000000000000004"),
4440            ("unitless negative", "-7", "-7"),
4441            ("unitless zero", "0", "0"),
4442            ("negative zero", "-0", "0"),
4443            ("count", "3_", "3_"),
4444            ("millimeters", "12mm", "12mm"),
4445            ("centimeters", "12cm", "12cm"),
4446            ("meters", "12m", "12m"),
4447            ("inches", "1.5in", "1.5in"),
4448            ("feet", "2ft", "2ft"),
4449            ("yards", "3yd", "3yd"),
4450            ("degrees", "90deg", "90deg"),
4451            ("radians", "1.5rad", "1.5rad"),
4452            // Arithmetic keeps the unit it started with.
4453            ("length arithmetic", "2mm + 10mm", "12mm"),
4454            // Multiplying two lengths exceeds what the type system tracks, so
4455            // only the numeric component survives.
4456            ("units the type system loses", "2mm * 10mm", "20"),
4457            ("unitless arithmetic", "1 + 2", "3"),
4458        ] {
4459            let code = format!("actual = string::toString({expr})");
4460            let result = parse_execute(&code).await.unwrap();
4461
4462            assert_eq!(
4463                mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4464                    .as_str()
4465                    .unwrap(),
4466                expected,
4467                "case: {name}"
4468            );
4469        }
4470    }
4471
4472    #[tokio::test(flavor = "multi_thread")]
4473    async fn test_string_to_string_ignores_the_files_default_unit() {
4474        // A value with no suffix has the file's default unit attached, but that
4475        // unit was never written down, so neither is it in the output. Reading
4476        // the result back in a file with a different default gives a different
4477        // quantity; the guarantee is about the number, not the measurement.
4478        let code = "@settings(defaultLengthUnit = inch)\nactual = string::toString(12)";
4479        let result = parse_execute(code).await.unwrap();
4480
4481        assert_eq!(
4482            mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4483                .as_str()
4484                .unwrap(),
4485            "12"
4486        );
4487    }
4488
4489    #[tokio::test(flavor = "multi_thread")]
4490    async fn test_string_to_string_rejects_a_non_number() {
4491        let error = parse_execute(r#"actual = string::toString("already text")"#)
4492            .await
4493            .unwrap_err();
4494
4495        // The declared signature rejects this before the implementation runs,
4496        // so the diagnostic names the function and both types.
4497        assert_eq!(
4498            error.message(),
4499            "The input argument of `string::toString` requires a value with type `number`, but found a value with type `string`."
4500        );
4501        assert!(
4502            matches!(error, KclError::Argument { .. }),
4503            "expected an Argument error, found {error:?}"
4504        );
4505    }
4506
4507    #[tokio::test(flavor = "multi_thread")]
4508    async fn test_string_to_string_accepts_a_piped_argument() {
4509        let result = parse_execute("actual = 12mm |> string::toString()").await.unwrap();
4510
4511        assert_eq!(
4512            mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4513                .as_str()
4514                .unwrap(),
4515            "12mm"
4516        );
4517    }
4518
4519    #[tokio::test(flavor = "multi_thread")]
4520    async fn test_string_to_string_echoes_how_the_literal_was_written() {
4521        // Reading the output back is not a supported operation, but for a
4522        // literal that carries its own units the text still comes out looking
4523        // like what the author typed, which is what makes it readable.
4524        for literal in [
4525            "12",
4526            "1.5",
4527            "0.30000000000000004",
4528            "3_",
4529            // A fractional count and a negative both have to survive the trip,
4530            // since the formatter emits them.
4531            "2.5_",
4532            "-4_",
4533            "12mm",
4534            "-5mm",
4535            "1.5in",
4536            "90deg",
4537            "1.5rad",
4538        ] {
4539            let code = format!("actual = string::toString({literal})");
4540            let result = parse_execute(&code).await.unwrap();
4541
4542            assert_eq!(
4543                mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4544                    .as_str()
4545                    .unwrap(),
4546                literal,
4547                "literal: {literal}"
4548            );
4549        }
4550    }
4551
4552    #[tokio::test(flavor = "multi_thread")]
4553    async fn test_string_to_string_spells_out_non_finite_numbers() {
4554        // Division is unguarded, so these are reachable from ordinary KCL. They
4555        // convert like any other number: the point of the function is to build
4556        // a message, and a message about a NaN is exactly when you need one.
4557        for (name, expr, expected) in [
4558            ("positive infinity", "1 / 0", "Infinity"),
4559            ("negative infinity", "-1 / 0", "-Infinity"),
4560            ("nan", "0 / 0", "NaN"),
4561            // The unit is dropped: no length is described by "Infinitymm".
4562            ("infinity from a length", "1mm / 0", "Infinity"),
4563            ("nan from a length", "0mm / 0", "NaN"),
4564            ("infinity from an angle", "1deg / 0", "Infinity"),
4565        ] {
4566            let code = format!("actual = string::toString({expr})");
4567            let result = parse_execute(&code).await.unwrap();
4568
4569            assert_eq!(
4570                mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4571                    .as_str()
4572                    .unwrap(),
4573                expected,
4574                "case: {name}"
4575            );
4576        }
4577    }
4578
4579    #[tokio::test(flavor = "multi_thread")]
4580    async fn test_string_equality_operators() {
4581        let composed = "\u{e9}";
4582        let decomposed = "e\u{301}";
4583        let code = format!(
4584            r#"
4585equal_same_ascii = "KCL" == "KCL"
4586equal_different_case = "KCL" == "kcl"
4587not_equal_same_ascii = "KCL" != "KCL"
4588not_equal_different_case = "KCL" != "kcl"
4589equal_same_unicode = "{composed}" == "{composed}"
4590not_equal_same_unicode = "{composed}" != "{composed}"
4591equal_without_normalization = "{composed}" == "{decomposed}"
4592not_equal_without_normalization = "{composed}" != "{decomposed}"
4593"#
4594        );
4595
4596        let result = parse_execute(&code).await.unwrap();
4597        for (name, expected) in [
4598            ("equal_same_ascii", true),
4599            ("equal_different_case", false),
4600            ("not_equal_same_ascii", false),
4601            ("not_equal_different_case", true),
4602            ("equal_same_unicode", true),
4603            ("not_equal_same_unicode", false),
4604            ("equal_without_normalization", false),
4605            ("not_equal_without_normalization", true),
4606        ] {
4607            assert_eq!(
4608                mem_get_json(result.exec_state.stack(), result.mem_env, name)
4609                    .as_bool()
4610                    .unwrap(),
4611                expected,
4612                "{name}"
4613            );
4614        }
4615    }
4616
4617    #[tokio::test(flavor = "multi_thread")]
4618    async fn test_string_equality_inside_sketch_block_fails_like_number_equality() {
4619        let string_code = r#"
4620@settings(experimentalFeatures = allow)
4621
4622sketch(on = XY) {
4623  stringsAreEqual = "KCL" == "KCL"
4624}
4625"#;
4626        let number_code = r#"
4627@settings(experimentalFeatures = allow)
4628
4629sketch(on = XY) {
4630  numbersAreEqual = 1 == 1
4631}
4632"#;
4633
4634        assert_eq!(
4635            parse_execute(string_code).await.unwrap_err().message(),
4636            "Cannot create an equivalence constraint between values of these types: a string and a string"
4637        );
4638        assert_eq!(
4639            parse_execute(number_code).await.unwrap_err().message(),
4640            "Cannot create an equivalence constraint between values of these types: a number and a number"
4641        );
4642    }
4643
4644    #[tokio::test(flavor = "multi_thread")]
4645    async fn test_math_execute_start_negative() {
4646        let ast = r#"myVar = -5 + 6"#;
4647        let result = parse_execute(ast).await.unwrap();
4648        assert_eq!(
4649            1.0,
4650            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4651                .as_f64()
4652                .unwrap()
4653        );
4654    }
4655
4656    #[tokio::test(flavor = "multi_thread")]
4657    async fn test_math_execute_with_pi() {
4658        let ast = r#"myVar = PI * 2"#;
4659        let result = parse_execute(ast).await.unwrap();
4660        assert_eq!(
4661            std::f64::consts::TAU,
4662            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4663                .as_f64()
4664                .unwrap()
4665        );
4666    }
4667
4668    #[tokio::test(flavor = "multi_thread")]
4669    async fn test_math_define_decimal_without_leading_zero() {
4670        let ast = r#"thing = .4 + 7"#;
4671        let result = parse_execute(ast).await.unwrap();
4672        assert_eq!(
4673            7.4,
4674            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
4675                .as_f64()
4676                .unwrap()
4677        );
4678    }
4679
4680    #[tokio::test(flavor = "multi_thread")]
4681    async fn pass_std_to_std() {
4682        let ast = r#"sketch001 = startSketchOn(XY)
4683profile001 = circle(sketch001, center = [0, 0], radius = 2)
4684extrude001 = extrude(profile001, length = 5)
4685extrudes = patternLinear3d(
4686  extrude001,
4687  instances = 3,
4688  distance = 5,
4689  axis = [1, 1, 0],
4690)
4691clone001 = map(extrudes, f = clone)
4692"#;
4693        parse_execute(ast).await.unwrap();
4694    }
4695
4696    #[tokio::test(flavor = "multi_thread")]
4697    async fn test_array_reduce_nested_array() {
4698        let code = r#"
4699fn id(@el, accum)  { return accum }
4700
4701answer = reduce([], initial=[[[0,0]]], f=id)
4702"#;
4703        let result = parse_execute(code).await.unwrap();
4704        assert_eq!(
4705            mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
4706            KclValue::HomArray {
4707                value: vec![KclValue::HomArray {
4708                    value: vec![KclValue::HomArray {
4709                        value: vec![
4710                            KclValue::Number {
4711                                value: 0.0,
4712                                ty: NumericType::default(),
4713                                meta: vec![SourceRange::new(69, 70, Default::default()).into()],
4714                            },
4715                            KclValue::Number {
4716                                value: 0.0,
4717                                ty: NumericType::default(),
4718                                meta: vec![SourceRange::new(71, 72, Default::default()).into()],
4719                            }
4720                        ],
4721                        ty: RuntimeType::any(),
4722                    }],
4723                    ty: RuntimeType::any(),
4724                }],
4725                ty: RuntimeType::any(),
4726            }
4727        );
4728    }
4729
4730    #[tokio::test(flavor = "multi_thread")]
4731    async fn test_zero_param_fn() {
4732        let ast = r#"sigmaAllow = 35000 // psi
4733leg1 = 5 // inches
4734leg2 = 8 // inches
4735fn thickness() { return 0.56 }
4736
4737bracket = startSketchOn(XY)
4738  |> startProfile(at = [0,0])
4739  |> line(end = [0, leg1])
4740  |> line(end = [leg2, 0])
4741  |> line(end = [0, -thickness()])
4742  |> line(end = [-leg2 + thickness(), 0])
4743"#;
4744        parse_execute(ast).await.unwrap();
4745    }
4746
4747    #[tokio::test(flavor = "multi_thread")]
4748    async fn test_unary_operator_not_succeeds() {
4749        let ast = r#"
4750fn returnTrue() { return !false }
4751t = true
4752f = false
4753notTrue = !t
4754notFalse = !f
4755c = !!true
4756d = !returnTrue()
4757
4758assertIs(!false, error = "expected to pass")
4759
4760fn check(x) {
4761  assertIs(!x, error = "expected argument to be false")
4762  return true
4763}
4764check(x = false)
4765"#;
4766        let result = parse_execute(ast).await.unwrap();
4767        assert_eq!(
4768            false,
4769            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
4770                .as_bool()
4771                .unwrap()
4772        );
4773        assert_eq!(
4774            true,
4775            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
4776                .as_bool()
4777                .unwrap()
4778        );
4779        assert_eq!(
4780            true,
4781            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
4782                .as_bool()
4783                .unwrap()
4784        );
4785        assert_eq!(
4786            false,
4787            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
4788                .as_bool()
4789                .unwrap()
4790        );
4791    }
4792
4793    #[tokio::test(flavor = "multi_thread")]
4794    async fn test_unary_operator_not_on_non_bool_fails() {
4795        let code1 = r#"
4796// Yup, this is null.
4797myNull = 0 / 0
4798notNull = !myNull
4799"#;
4800        assert_eq!(
4801            parse_execute(code1).await.unwrap_err().message(),
4802            "Cannot apply unary operator ! to non-boolean value: a number",
4803        );
4804
4805        let code2 = "notZero = !0";
4806        assert_eq!(
4807            parse_execute(code2).await.unwrap_err().message(),
4808            "Cannot apply unary operator ! to non-boolean value: a number",
4809        );
4810
4811        let code3 = r#"
4812notEmptyString = !""
4813"#;
4814        assert_eq!(
4815            parse_execute(code3).await.unwrap_err().message(),
4816            "Cannot apply unary operator ! to non-boolean value: a string",
4817        );
4818
4819        let code4 = r#"
4820obj = { a = 1 }
4821notMember = !obj.a
4822"#;
4823        assert_eq!(
4824            parse_execute(code4).await.unwrap_err().message(),
4825            "Cannot apply unary operator ! to non-boolean value: a number",
4826        );
4827
4828        let code5 = "
4829a = []
4830notArray = !a";
4831        assert_eq!(
4832            parse_execute(code5).await.unwrap_err().message(),
4833            "Cannot apply unary operator ! to non-boolean value: an empty array",
4834        );
4835
4836        let code6 = "
4837x = {}
4838notObject = !x";
4839        assert_eq!(
4840            parse_execute(code6).await.unwrap_err().message(),
4841            "Cannot apply unary operator ! to non-boolean value: an object",
4842        );
4843
4844        let code7 = "
4845fn x() { return 1 }
4846notFunction = !x";
4847        let fn_err = parse_execute(code7).await.unwrap_err();
4848        // These are currently printed out as JSON objects, so we don't want to
4849        // check the full error.
4850        assert!(
4851            fn_err
4852                .message()
4853                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
4854            "Actual error: {fn_err:?}"
4855        );
4856
4857        let code8 = "
4858myTagDeclarator = $myTag
4859notTagDeclarator = !myTagDeclarator";
4860        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
4861        // These are currently printed out as JSON objects, so we don't want to
4862        // check the full error.
4863        assert!(
4864            tag_declarator_err
4865                .message()
4866                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
4867            "Actual error: {tag_declarator_err:?}"
4868        );
4869
4870        let code9 = "
4871myTagDeclarator = $myTag
4872notTagIdentifier = !myTag";
4873        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
4874        // These are currently printed out as JSON objects, so we don't want to
4875        // check the full error.
4876        assert!(
4877            tag_identifier_err
4878                .message()
4879                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
4880            "Actual error: {tag_identifier_err:?}"
4881        );
4882
4883        let code10 = "notPipe = !(1 |> 2)";
4884        assert_eq!(
4885            // TODO: We don't currently parse this, but we should.  It should be
4886            // a runtime error instead.
4887            parse_execute(code10).await.unwrap_err(),
4888            KclError::new_syntax(KclErrorDetails::new(
4889                "Unexpected token: !".to_owned(),
4890                vec![SourceRange::new(10, 11, ModuleId::default())],
4891            ))
4892        );
4893
4894        let code11 = "
4895fn identity(x) { return x }
4896notPipeSub = 1 |> identity(!%))";
4897        assert_eq!(
4898            // TODO: We don't currently parse this, but we should.  It should be
4899            // a runtime error instead.
4900            parse_execute(code11).await.unwrap_err(),
4901            KclError::new_syntax(KclErrorDetails::new(
4902                "There was an unexpected `!`. Try removing it.".to_owned(),
4903                vec![SourceRange::new(56, 57, ModuleId::default())],
4904            ))
4905        );
4906
4907        // TODO: Add these tests when we support these types.
4908        // let notNan = !NaN
4909        // let notInfinity = !Infinity
4910    }
4911
4912    #[tokio::test(flavor = "multi_thread")]
4913    async fn test_start_sketch_on_invalid_kwargs() {
4914        let current_dir = std::env::current_dir().unwrap();
4915        let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
4916        let mut code = std::fs::read_to_string(&path).unwrap();
4917        assert_eq!(
4918            parse_execute(&code).await.unwrap_err().message(),
4919            "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
4920        );
4921
4922        path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
4923        code = std::fs::read_to_string(&path).unwrap();
4924
4925        assert_eq!(
4926            parse_execute(&code).await.unwrap_err().message(),
4927            "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
4928        );
4929
4930        path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
4931        code = std::fs::read_to_string(&path).unwrap();
4932
4933        assert_eq!(
4934            parse_execute(&code).await.unwrap_err().message(),
4935            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4936        );
4937
4938        path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
4939        code = std::fs::read_to_string(&path).unwrap();
4940
4941        assert_eq!(
4942            parse_execute(&code).await.unwrap_err().message(),
4943            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4944        );
4945
4946        path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
4947        code = std::fs::read_to_string(&path).unwrap();
4948
4949        assert_eq!(
4950            parse_execute(&code).await.unwrap_err().message(),
4951            "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
4952        );
4953    }
4954
4955    #[tokio::test(flavor = "multi_thread")]
4956    async fn test_math_negative_variable_in_binary_expression() {
4957        let ast = r#"sigmaAllow = 35000 // psi
4958width = 1 // inch
4959
4960p = 150 // lbs
4961distance = 6 // inches
4962FOS = 2
4963
4964leg1 = 5 // inches
4965leg2 = 8 // inches
4966
4967thickness_squared = distance * p * FOS * 6 / sigmaAllow
4968thickness = 0.56 // inches. App does not support square root function yet
4969
4970bracket = startSketchOn(XY)
4971  |> startProfile(at = [0,0])
4972  |> line(end = [0, leg1])
4973  |> line(end = [leg2, 0])
4974  |> line(end = [0, -thickness])
4975  |> line(end = [-leg2 + thickness, 0])
4976"#;
4977        parse_execute(ast).await.unwrap();
4978    }
4979
4980    #[tokio::test(flavor = "multi_thread")]
4981    async fn test_execute_function_no_return() {
4982        let ast = r#"fn test(@origin) {
4983  origin
4984}
4985
4986test([0, 0])
4987"#;
4988        let result = parse_execute(ast).await;
4989        assert!(result.is_err());
4990        assert!(result.unwrap_err().to_string().contains("undefined"));
4991    }
4992
4993    #[tokio::test(flavor = "multi_thread")]
4994    async fn test_max_stack_size_exceeded_error() {
4995        let ast = r#"
4996fn forever(@n) {
4997  return 1 + forever(n)
4998}
4999
5000forever(1)
5001"#;
5002        let result = parse_execute(ast).await;
5003        let err = result.unwrap_err();
5004        let msg = err.to_string();
5005        assert!(msg.contains("Call depth limit"), "actual: {err:?}");
5006    }
5007
5008    #[tokio::test(flavor = "multi_thread")]
5009    async fn test_math_doubly_nested_parens() {
5010        let ast = r#"sigmaAllow = 35000 // psi
5011width = 4 // inch
5012p = 150 // Force on shelf - lbs
5013distance = 6 // inches
5014FOS = 2
5015leg1 = 5 // inches
5016leg2 = 8 // inches
5017thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
5018thickness = 0.32 // inches. App does not support square root function yet
5019bracket = startSketchOn(XY)
5020  |> startProfile(at = [0,0])
5021    |> line(end = [0, leg1])
5022  |> line(end = [leg2, 0])
5023  |> line(end = [0, -thickness])
5024  |> line(end = [-1 * leg2 + thickness, 0])
5025  |> line(end = [0, -1 * leg1 + thickness])
5026  |> close()
5027  |> extrude(length = width)
5028"#;
5029        parse_execute(ast).await.unwrap();
5030    }
5031
5032    #[tokio::test(flavor = "multi_thread")]
5033    async fn test_math_nested_parens_one_less() {
5034        let ast = r#" sigmaAllow = 35000 // psi
5035width = 4 // inch
5036p = 150 // Force on shelf - lbs
5037distance = 6 // inches
5038FOS = 2
5039leg1 = 5 // inches
5040leg2 = 8 // inches
5041thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
5042thickness = 0.32 // inches. App does not support square root function yet
5043bracket = startSketchOn(XY)
5044  |> startProfile(at = [0,0])
5045    |> line(end = [0, leg1])
5046  |> line(end = [leg2, 0])
5047  |> line(end = [0, -thickness])
5048  |> line(end = [-1 * leg2 + thickness, 0])
5049  |> line(end = [0, -1 * leg1 + thickness])
5050  |> close()
5051  |> extrude(length = width)
5052"#;
5053        parse_execute(ast).await.unwrap();
5054    }
5055
5056    #[tokio::test(flavor = "multi_thread")]
5057    async fn test_fn_as_operand() {
5058        let ast = r#"fn f() { return 1 }
5059x = f()
5060y = x + 1
5061z = f() + 1
5062w = f() + f()
5063"#;
5064        parse_execute(ast).await.unwrap();
5065    }
5066
5067    #[tokio::test(flavor = "multi_thread")]
5068    async fn kcl_test_ids_stable_between_executions() {
5069        let code = r#"sketch001 = startSketchOn(XZ)
5070|> startProfile(at = [61.74, 206.13])
5071|> xLine(length = 305.11, tag = $seg01)
5072|> yLine(length = -291.85)
5073|> xLine(length = -segLen(seg01))
5074|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5075|> close()
5076|> extrude(length = 40.14)
5077|> shell(
5078    thickness = 3.14,
5079    faces = [seg01]
5080)
5081"#;
5082
5083        let old_program = crate::Program::parse_no_errs(code).unwrap();
5084        let ctx = crate::test_server::new_context(true, None, true, old_program.language_version().unwrap())
5085            .await
5086            .unwrap();
5087
5088        // Execute the program.
5089        if let Err(err) = ctx.run_with_caching(old_program).await {
5090            let report = err.into_miette_report_with_outputs(code).unwrap();
5091            let report = miette::Report::new(report);
5092            panic!("Error executing program: {report:?}");
5093        }
5094
5095        // Get the id_generator from the first execution.
5096        let id_generator = ctx.cache.read_old_ast().await.unwrap().main.exec_state.id_generator;
5097
5098        let code = r#"sketch001 = startSketchOn(XZ)
5099|> startProfile(at = [62.74, 206.13])
5100|> xLine(length = 305.11, tag = $seg01)
5101|> yLine(length = -291.85)
5102|> xLine(length = -segLen(seg01))
5103|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5104|> close()
5105|> extrude(length = 40.14)
5106|> shell(
5107    faces = [seg01],
5108    thickness = 3.14,
5109)
5110"#;
5111
5112        // Execute a slightly different program again.
5113        let program = crate::Program::parse_no_errs(code).unwrap();
5114        // Execute the program.
5115        ctx.run_with_caching(program).await.unwrap();
5116
5117        let new_id_generator = ctx.cache.read_old_ast().await.unwrap().main.exec_state.id_generator;
5118
5119        assert_eq!(id_generator, new_id_generator);
5120    }
5121
5122    #[tokio::test(flavor = "multi_thread")]
5123    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
5124        let code = r#"sketch001 = startSketchOn(XZ)
5125|> startProfile(at = [61.74, 206.13])
5126|> xLine(length = 305.11, tag = $seg01)
5127|> yLine(length = -291.85)
5128|> xLine(length = -segLen(seg01))
5129|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5130|> close()
5131|> extrude(length = 40.14)
5132|> shell(
5133    thickness = 3.14,
5134    faces = [seg01]
5135)
5136"#;
5137
5138        let old_program = crate::Program::parse_no_errs(code).unwrap();
5139        let mut ctx = crate::test_server::new_context(true, None, true, old_program.language_version().unwrap())
5140            .await
5141            .unwrap();
5142
5143        // Execute the program.
5144        ctx.run_with_caching(old_program.clone()).await.unwrap();
5145
5146        let settings_state = ctx.cache.read_old_ast().await.unwrap().settings;
5147
5148        // Ensure the settings are as expected.
5149        assert_eq!(settings_state, ctx.settings);
5150
5151        // Change a setting.
5152        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
5153
5154        // Execute the program.
5155        ctx.run_with_caching(old_program.clone()).await.unwrap();
5156
5157        let settings_state = ctx.cache.read_old_ast().await.unwrap().settings;
5158
5159        // Ensure the settings are as expected.
5160        assert_eq!(settings_state, ctx.settings);
5161
5162        // Change a setting.
5163        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
5164
5165        // Execute the program.
5166        ctx.run_with_caching(old_program).await.unwrap();
5167
5168        let settings_state = ctx.cache.read_old_ast().await.unwrap().settings;
5169
5170        // Ensure the settings are as expected.
5171        assert_eq!(settings_state, ctx.settings);
5172
5173        ctx.close().await;
5174    }
5175
5176    #[tokio::test(flavor = "multi_thread")]
5177    async fn mock_after_not_mock() {
5178        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
5179        let program = crate::Program::parse_no_errs("x = 2").unwrap();
5180        let result = ctx.run_with_caching(program).await.unwrap();
5181        assert_number_variable(&result.variables, "x", 2.0);
5182
5183        // Mock execution must explicitly reuse the live session's memory.
5184        let ctx2 = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
5185        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
5186        let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
5187        assert_number_variable(&result.variables, "z", 3.0);
5188
5189        ctx.close().await;
5190        ctx2.close().await;
5191    }
5192
5193    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/12498
5194    #[tokio::test(flavor = "multi_thread")]
5195    async fn mock_execution_succeeds_after_split() {
5196        let code = kcl_input!("repro_mock_extrude");
5197        let ctx = ExecutorContext::new_mock(None).await;
5198        let program = crate::Program::parse_no_errs(code).unwrap();
5199        let _result = match ctx.run_mock(&program, &MockConfig::default()).await {
5200            Ok(res) => res,
5201            Err(e) => panic!("{}", e.error),
5202        };
5203    }
5204
5205    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/13319
5206    #[tokio::test(flavor = "multi_thread")]
5207    async fn mock_execution_rejects_oob_on_frontend_array() {
5208        let code = r#"
5209values = [10, 20]
5210third = values[2]
5211"#;
5212        let ctx = ExecutorContext::new_mock(None).await;
5213        let program = crate::Program::parse_no_errs(code).unwrap();
5214        let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
5215        ctx.close().await;
5216
5217        assert!(
5218            err.error.message().contains("array doesn't have any item at index 2"),
5219            "{err:?}"
5220        );
5221    }
5222
5223    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/13103
5224    /// i.e.
5225    /// If you do a pattern circular 3d in mock execution mode,
5226    /// and you ask for 10 instances, you should get 10 instances.
5227    #[tokio::test(flavor = "multi_thread")]
5228    async fn mock_execution_pattern_circular_number() {
5229        let code = kcl_input!("repro_mock_pattern_circular");
5230        let ctx = ExecutorContext::new_mock(None).await;
5231        let program = crate::Program::parse_no_errs(code).unwrap();
5232        let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
5233        let copies = result
5234            .variables
5235            .get("copies")
5236            .expect("no variable called 'copies' found");
5237        let value = match copies {
5238            KclValueView::Solid { .. } => {
5239                panic!("One solid?");
5240            }
5241            KclValueView::HomArray { value } => value,
5242            other => panic!("{other:#?}"),
5243        };
5244        let actual_instances = value.len();
5245        let expected_instances = 10; // from the KCL `instances = `
5246        assert_eq!(actual_instances, expected_instances);
5247    }
5248
5249    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/13103
5250    /// i.e.
5251    /// If you do a pattern circular 3d in mock execution mode,
5252    /// and you ask for 10 instances, you should get 10 instances.
5253    #[tokio::test(flavor = "multi_thread")]
5254    async fn mock_execution_subtract() {
5255        // Run this KCL file, in mock execution.
5256        let code = kcl_input!("repro_mock_subtract");
5257        let ctx = ExecutorContext::new_mock(None).await;
5258        let program = crate::Program::parse_no_errs(code).unwrap();
5259        let result = ctx.run_mock(&program, &MockConfig::default()).await;
5260        ctx.close().await;
5261        let result = match result {
5262            Ok(x) => x,
5263            Err(e) => {
5264                let error = e.error;
5265                panic!("{error}");
5266            }
5267        };
5268
5269        // Get the variable we're interested in, from KCL program memory.
5270        let subtracted_parts = result
5271            .variables
5272            .get("subtractedParts")
5273            .expect("no variable called 'subtracted_parts' found");
5274        let subtracted_parts = match subtracted_parts {
5275            KclValueView::Solid { .. } => {
5276                panic!("One solid?");
5277            }
5278            KclValueView::HomArray { value } => value,
5279            other => panic!("{other:#?}"),
5280        };
5281
5282        // Validate the variable.
5283        // from the KCL, there's 2 parts being subtracted from.
5284        let expected_number_of_parts = 2;
5285        let actual_number_of_parts = subtracted_parts.len();
5286        assert_eq!(actual_number_of_parts, expected_number_of_parts);
5287    }
5288
5289    #[tokio::test(flavor = "multi_thread")]
5290    async fn mock_then_add_extrude_then_mock_again() {
5291        let code = "s = sketch(on = XY) {
5292    line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
5293    line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
5294    coincident([line1.end, line2.start])
5295    line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
5296    coincident([line2.end, line3.start])
5297    coincident([line1.start, line3.end])
5298}
5299    ";
5300        let ctx = ExecutorContext::new_mock(None).await;
5301        let program = crate::Program::parse_no_errs(code).unwrap();
5302        let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
5303        assert!(result.variables.contains_key("s"), "actual: {:?}", result.variables);
5304
5305        let code2 = code.to_owned()
5306            + "
5307region001 = region(point = [1mm, 1mm], sketch = s)
5308extrude001 = extrude(region001, length = 1)
5309    ";
5310        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
5311        let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
5312        assert!(
5313            result.variables.contains_key("region001"),
5314            "actual: {:?}",
5315            result.variables
5316        );
5317
5318        ctx.close().await;
5319    }
5320
5321    #[tokio::test(flavor = "multi_thread")]
5322    async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
5323        let code = format!(
5324            r#"{}
5325
5326face7 = faceOf(solid6, face = r6.tags.line1)
5327r7 = squareRegion(onSurface = face7)
5328solid7 = extrude(r7, length = width)
5329"#,
5330            include_str!("../../tests/endless_impeller/input.kcl")
5331        );
5332
5333        let result = parse_execute(&code).await.unwrap();
5334        let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
5335        assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
5336
5337        let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
5338            KclValue::Face { value } => value,
5339            value => panic!("expected face7 to be a Face, got {value:?}"),
5340        };
5341        assert!(face7.parent_solid.creator_sketch_id.is_some());
5342    }
5343
5344    #[tokio::test(flavor = "multi_thread")]
5345    async fn mock_has_stable_ids() {
5346        let ctx = ExecutorContext::new_mock(None).await;
5347        let mock_config = MockConfig {
5348            use_prev_memory: false,
5349            ..Default::default()
5350        };
5351        let code = "sk = startSketchOn(XY)
5352        |> startProfile(at = [0, 0])";
5353        let program = crate::Program::parse_no_errs(code).unwrap();
5354        let result = ctx.run_mock(&program, &mock_config).await.unwrap();
5355        let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
5356        assert!(!ids.is_empty(), "IDs should not be empty");
5357
5358        let ctx2 = ExecutorContext::new_mock(None).await;
5359        let program2 = crate::Program::parse_no_errs(code).unwrap();
5360        let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
5361        let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
5362
5363        assert_eq!(ids, ids2, "Generated IDs should match");
5364        ctx.close().await;
5365        ctx2.close().await;
5366    }
5367
5368    #[tokio::test(flavor = "multi_thread")]
5369    async fn mock_memory_restore_preserves_module_maps() {
5370        let ctx = ExecutorContext::new_mock(None).await;
5371        let cold_start = MockConfig {
5372            use_prev_memory: false,
5373            ..Default::default()
5374        };
5375        ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
5376
5377        let mut mem = ctx.cache.read_old_memory().await.unwrap();
5378        assert!(
5379            mem.path_to_source_id.len() > 3,
5380            "expected prelude imports to populate multiple modules, got {:?}",
5381            mem.path_to_source_id
5382        );
5383        mem.constraint_state.insert(
5384            kcl_api::ObjectId(1),
5385            indexmap::indexmap! {
5386                crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
5387                    crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
5388            },
5389        );
5390        let imported_id = ModuleId::from_usize(42);
5391        mem.never_type_ranges
5392            .insert(imported_id, vec![SourceRange::new(0, 5, imported_id)]);
5393
5394        let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
5395        ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
5396
5397        assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
5398        assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
5399        assert_eq!(exec_state.global.never_type_ranges, mem.never_type_ranges);
5400        assert_eq!(exec_state.global.module_infos, mem.module_infos);
5401        assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
5402
5403        ctx.close().await;
5404    }
5405
5406    // These use a live execution context with a mock engine so they exercise
5407    // the cache orchestration without a server or API token.
5408    #[tokio::test(flavor = "multi_thread")]
5409    async fn session_cache_isolates_concurrent_contexts() {
5410        let a = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5411        let b = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5412        let program_a = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 2").unwrap();
5413        let program_b = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 7").unwrap();
5414        let (outcome_a, outcome_b) = tokio::join!(
5415            a.run_with_caching(program_a.clone()),
5416            b.run_with_caching(program_b.clone()),
5417        );
5418        assert_number_variable(&outcome_a.unwrap().variables, "x", 2.0);
5419        assert_number_variable(&outcome_b.unwrap().variables, "x", 7.0);
5420
5421        // Both sessions must retain their own last execution, including when
5422        // the next request is a cache hit.
5423        assert_eq!(a.cache.read_old_ast().await.unwrap().main.ast, program_a.ast);
5424        assert_eq!(b.cache.read_old_ast().await.unwrap().main.ast, program_b.ast);
5425        assert_number_variable(&a.run_with_caching(program_a).await.unwrap().variables, "x", 2.0);
5426        assert_number_variable(&b.run_with_caching(program_b).await.unwrap().variables, "x", 7.0);
5427        a.bust_cache().await;
5428        assert!(a.cache.read_old_ast().await.is_none());
5429        assert!(b.cache.read_old_ast().await.is_some());
5430        a.close().await;
5431        b.close().await;
5432    }
5433
5434    #[tokio::test(flavor = "multi_thread")]
5435    async fn session_cache_shares_clones_and_associated_mock_memory() {
5436        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5437        let program = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 2").unwrap();
5438        ctx.run_with_caching(program).await.unwrap();
5439        let clone = ctx.clone();
5440        let import_ctx = ctx.clone_with_fresh_execution_batch();
5441        assert!(clone.cache.read_old_ast().await.is_some());
5442        import_ctx.bust_cache().await;
5443        assert!(clone.cache.read_old_ast().await.is_none());
5444
5445        let mock = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
5446        let expression = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\ny = x + 1").unwrap();
5447        let outcome = mock.run_mock(&expression, &MockConfig::default()).await.unwrap();
5448        assert_number_variable(&outcome.variables, "y", 3.0);
5449        assert!(ctx.cache.read_old_memory_var("y").await.is_some());
5450
5451        // An unrelated mock context cannot see the session's variables.
5452        let independent = ExecutorContext::new_mock(None).await;
5453        independent
5454            .run_mock(&expression, &MockConfig::default())
5455            .await
5456            .unwrap_err();
5457        mock.cache.clear_mem_cache().await;
5458        assert!(ctx.cache.read_old_memory().await.is_none());
5459        ctx.close().await;
5460        mock.close().await;
5461        independent.close().await;
5462    }
5463
5464    #[tokio::test(flavor = "multi_thread")]
5465    async fn session_cache_failure_preserves_partial_mock_memory() {
5466        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5467        let program = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 2\nbad = missing").unwrap();
5468        ctx.run_with_caching(program).await.unwrap_err();
5469        assert!(ctx.cache.read_old_ast().await.is_none());
5470        let mock = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
5471        let expression = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\ny = x + 1").unwrap();
5472        let outcome = mock.run_mock(&expression, &MockConfig::default()).await.unwrap();
5473        assert_number_variable(&outcome.variables, "y", 3.0);
5474        ctx.close().await;
5475        mock.close().await;
5476    }
5477
5478    #[tokio::test(flavor = "multi_thread")]
5479    async fn engine_scene_generation_skips_cache_hits_and_counts_failures() {
5480        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5481        let program = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = 2").unwrap();
5482        let first = ctx.run_with_caching(program.clone()).await.unwrap();
5483        assert_eq!(first.engine_scene_generation, 1);
5484        let cached = ctx.run_with_caching(program).await.unwrap();
5485        assert_eq!(cached.engine_scene_generation, 1, "a cache hit leaves the scene alone");
5486
5487        let invalid = crate::Program::parse_no_errs("@settings(kclVersion = 3.0)\nx = missing").unwrap();
5488        let err = ctx.run_with_caching(invalid).await.unwrap_err();
5489        assert_eq!(
5490            err.engine_scene_generation,
5491            Some(2),
5492            "a failed run may still change the scene"
5493        );
5494        ctx.close().await;
5495    }
5496
5497    #[tokio::test(flavor = "multi_thread")]
5498    async fn cached_execution_phases_preserve_edits_settings_and_error_recovery() {
5499        let mut ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5500        let original = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 2").unwrap();
5501        for _ in 0..2 {
5502            let outcome = ctx.run_with_caching(original.clone()).await.unwrap();
5503            assert_number_variable(&outcome.variables, "x", 2.0);
5504        }
5505
5506        // Appending a statement reuses the old environment. Finalization must
5507        // cache the full program, including the unchanged statement.
5508        let extended = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 2\ny = x + 1").unwrap();
5509        for _ in 0..2 {
5510            let outcome = ctx.run_with_caching(extended.clone()).await.unwrap();
5511            assert_number_variable(&outcome.variables, "x", 2.0);
5512            assert_number_variable(&outcome.variables, "y", 3.0);
5513            assert_eq!(ctx.cache.read_old_ast().await.unwrap().main.ast.body.len(), 2);
5514        }
5515
5516        // Changing an existing statement requires a fresh execution state.
5517        let changed = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 4\ny = x + 1").unwrap();
5518        let outcome = ctx.run_with_caching(changed.clone()).await.unwrap();
5519        assert_number_variable(&outcome.variables, "y", 5.0);
5520        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
5521        let outcome = ctx.run_with_caching(changed).await.unwrap();
5522        assert_number_variable(&outcome.variables, "y", 5.0);
5523        assert_eq!(ctx.cache.read_old_ast().await.unwrap().settings, ctx.settings);
5524
5525        let invalid = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = missing").unwrap();
5526        ctx.run_with_caching(invalid).await.unwrap_err();
5527        assert!(
5528            ctx.cache.read_old_ast().await.is_none(),
5529            "execution errors must invalidate the cache"
5530        );
5531        let outcome = ctx.run_with_caching(original).await.unwrap();
5532        assert_number_variable(&outcome.variables, "x", 2.0);
5533
5534        ctx.close().await;
5535    }
5536
5537    #[tokio::test(flavor = "multi_thread")]
5538    async fn cached_execution_phases_recheck_changed_imports() {
5539        let project = tempfile::TempDir::with_prefix("kcl_cache_phases").unwrap();
5540        let imported = project.path().join("values.kcl");
5541        tokio::fs::write(&imported, "export x = 2").await.unwrap();
5542        let mut ctx = ExecutorContext::new_with_engine(
5543            Arc::new(EngineManager::new_mock()),
5544            ExecutorSettings {
5545                project_directory: Some(crate::TypedPath(project.path().into())),
5546                ..Default::default()
5547            },
5548        );
5549        let program =
5550            crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nimport x from 'values.kcl'\ny = x + 1")
5551                .unwrap();
5552        for _ in 0..2 {
5553            let outcome = ctx.run_with_caching(program.clone()).await.unwrap();
5554            assert_number_variable(&outcome.variables, "y", 3.0);
5555        }
5556        // Exercise the unchanged-import early return with settings reapplied.
5557        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
5558        let outcome = ctx.run_with_caching(program.clone()).await.unwrap();
5559        assert_number_variable(&outcome.variables, "y", 3.0);
5560        assert_eq!(ctx.cache.read_old_ast().await.unwrap().settings, ctx.settings);
5561
5562        tokio::fs::write(&imported, "export x = 7").await.unwrap();
5563        for _ in 0..2 {
5564            let outcome = ctx.run_with_caching(program.clone()).await.unwrap();
5565            assert_number_variable(&outcome.variables, "y", 8.0);
5566        }
5567
5568        ctx.close().await;
5569    }
5570
5571    #[tokio::test(flavor = "multi_thread")]
5572    async fn run_with_caching_no_action_refreshes_mock_memory() {
5573        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5574        let program = crate::Program::parse_no_errs(
5575            r#"sketch001 = sketch(on = XY) {
5576  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
5577}
5578"#,
5579        )
5580        .unwrap();
5581
5582        ctx.run_with_caching(program.clone()).await.unwrap();
5583        let baseline_memory = ctx.cache.read_old_memory().await.unwrap();
5584        assert!(
5585            !baseline_memory.scene_objects.is_empty(),
5586            "expected engine execution to persist full-scene mock memory"
5587        );
5588
5589        ctx.cache
5590            .write_old_memory(cache::SketchModeState::new_for_tests())
5591            .await;
5592        assert_eq!(ctx.cache.read_old_memory().await.unwrap().scene_objects.len(), 0);
5593
5594        ctx.run_with_caching(program).await.unwrap();
5595        let refreshed_memory = ctx.cache.read_old_memory().await.unwrap();
5596        assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
5597        assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
5598        assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
5599
5600        ctx.close().await;
5601    }
5602
5603    #[tokio::test(flavor = "multi_thread")]
5604    async fn sim_sketch_mode_real_mock_real() {
5605        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
5606        let code = r#"sketch001 = startSketchOn(XY)
5607profile001 = startProfile(sketch001, at = [0, 0])
5608  |> line(end = [10, 0])
5609  |> line(end = [0, 10])
5610  |> line(end = [-10, 0])
5611  |> line(end = [0, -10])
5612  |> close()
5613"#;
5614        let program = crate::Program::parse_no_errs(code).unwrap();
5615        let result = ctx.run_with_caching(program).await.unwrap();
5616        assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
5617
5618        let mock_ctx = ExecutorContext::new_mock(None).await.with_cache(ctx.cache.clone());
5619        let mock_program = crate::Program::parse_no_errs(code).unwrap();
5620        let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
5621        assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
5622
5623        let code2 = code.to_owned()
5624            + r#"
5625extrude001 = extrude(profile001, length = 10)
5626"#;
5627        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
5628        let result = ctx.run_with_caching(program2).await.unwrap();
5629        assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
5630
5631        ctx.close().await;
5632        mock_ctx.close().await;
5633    }
5634
5635    #[tokio::test(flavor = "multi_thread")]
5636    async fn read_tag_version() {
5637        let ast = r#"fn bar(@t) {
5638  return startSketchOn(XY)
5639    |> startProfile(at = [0,0])
5640    |> angledLine(
5641        angle = -60,
5642        length = segLen(t),
5643    )
5644    |> line(end = [0, 0])
5645    |> close()
5646}
5647
5648sketch = startSketchOn(XY)
5649  |> startProfile(at = [0,0])
5650  |> line(end = [0, 10])
5651  |> line(end = [10, 0], tag = $tag0)
5652  |> line(endAbsolute = [0, 0])
5653
5654fn foo() {
5655  // tag0 tags an edge
5656  return bar(tag0)
5657}
5658
5659solid = sketch |> extrude(length = 10)
5660// tag0 tags a face
5661sketch2 = startSketchOn(solid, face = tag0)
5662  |> startProfile(at = [0,0])
5663  |> line(end = [0, 1])
5664  |> line(end = [1, 0])
5665  |> line(end = [0, 0])
5666
5667foo() |> extrude(length = 1)
5668"#;
5669        parse_execute(ast).await.unwrap();
5670    }
5671
5672    #[tokio::test(flavor = "multi_thread")]
5673    async fn experimental() {
5674        let code = r#"
5675startSketchOn(XY)
5676  |> startProfile(at = [0, 0], tag = $start)
5677  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5678"#;
5679        let result = parse_execute(code).await.unwrap();
5680        let issues = result.exec_state.issues();
5681        assert_eq!(issues.len(), 1);
5682        assert_eq!(issues[0].severity, Severity::Error);
5683        let msg = &issues[0].message;
5684        assert!(msg.contains("experimental"), "found {msg}");
5685
5686        let code = r#"@settings(experimentalFeatures = allow)
5687startSketchOn(XY)
5688  |> startProfile(at = [0, 0], tag = $start)
5689  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5690"#;
5691        let result = parse_execute(code).await.unwrap();
5692        let issues = result.exec_state.issues();
5693        assert!(issues.is_empty(), "issues={issues:#?}");
5694
5695        let code = r#"@settings(experimentalFeatures = warn)
5696startSketchOn(XY)
5697  |> startProfile(at = [0, 0], tag = $start)
5698  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5699"#;
5700        let result = parse_execute(code).await.unwrap();
5701        let issues = result.exec_state.issues();
5702        assert_eq!(issues.len(), 1);
5703        assert_eq!(issues[0].severity, Severity::Warning);
5704        let msg = &issues[0].message;
5705        assert!(msg.contains("experimental"), "found {msg}");
5706
5707        let code = r#"@settings(experimentalFeatures = deny)
5708startSketchOn(XY)
5709  |> startProfile(at = [0, 0], tag = $start)
5710  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5711"#;
5712        let result = parse_execute(code).await.unwrap();
5713        let issues = result.exec_state.issues();
5714        assert_eq!(issues.len(), 1);
5715        assert_eq!(issues[0].severity, Severity::Error);
5716        let msg = &issues[0].message;
5717        assert!(msg.contains("experimental"), "found {msg}");
5718
5719        let code = r#"@settings(experimentalFeatures = foo)
5720startSketchOn(XY)
5721  |> startProfile(at = [0, 0], tag = $start)
5722  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5723"#;
5724        parse_execute(code).await.unwrap_err();
5725    }
5726
5727    #[tokio::test(flavor = "multi_thread")]
5728    async fn default_angle_unit_warns_in_legacy_kcl() {
5729        for version in ["", "kclVersion = 1.0, ", "kclVersion = 2.0, "] {
5730            for unit in ["deg", "rad"] {
5731                let code = format!("@settings({version}defaultAngleUnit = {unit})\nx = 1\n");
5732                let result = parse_execute(&code).await.unwrap();
5733                let issues = result.issues();
5734                assert_eq!(issues.len(), 1, "code={code}");
5735                assert_eq!(issues[0].severity, Severity::Warning, "code={code}");
5736                assert_eq!(
5737                    issues[0].message,
5738                    "The `defaultAngleUnit` setting is deprecated; use explicit units for angles"
5739                );
5740                assert_eq!(variable_f64(&result, "x"), 1.0);
5741            }
5742        }
5743    }
5744
5745    #[tokio::test(flavor = "multi_thread")]
5746    async fn default_angle_unit_errors_in_kcl_v3() {
5747        for settings in [
5748            "@settings(kclVersion = \"3.0-preview\", defaultAngleUnit = deg)",
5749            "@settings(defaultAngleUnit = rad, kclVersion = \"3.0-preview\")",
5750            "@settings(defaultAngleUnit = deg)\n@settings(kclVersion = \"3.0-preview\")",
5751            "@settings(kclVersion = \"3.0-preview\")\n@settings(defaultAngleUnit = rad)",
5752        ] {
5753            let code = format!("{settings}\nx = 1\n");
5754            let Err(error) = parse_execute(&code).await else {
5755                panic!("defaultAngleUnit must fail in KCL 3.0: {code}");
5756            };
5757            assert_eq!(
5758                error.message(),
5759                "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles",
5760                "code={code}"
5761            );
5762            let ranges = error.source_ranges();
5763            assert_eq!(ranges.len(), 1);
5764            assert!(code[ranges[0].start()..ranges[0].end()].contains("defaultAngleUnit"));
5765        }
5766    }
5767
5768    #[tokio::test(flavor = "multi_thread")]
5769    async fn default_angle_unit_error_cannot_be_suppressed() {
5770        for version in ["1.0", "2.0", "\"3.0-preview\""] {
5771            let code = format!(
5772                "@warnings(allow = angleUnits)\n@settings(kclVersion = {version}, defaultAngleUnit = deg)\nx = 1\n"
5773            );
5774            let result = parse_execute(&code).await;
5775            if version == "\"3.0-preview\"" {
5776                assert_eq!(
5777                    result.unwrap_err().message(),
5778                    "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
5779                );
5780            } else {
5781                assert!(result.unwrap().issues().is_empty(), "code={code}");
5782            }
5783        }
5784    }
5785
5786    /// The `defaultAngleUnit` gate in an imported module follows the effective
5787    /// kclVersion: the entry point's when it declares KCL 3.0, otherwise the
5788    /// module's own (undeclared here, so 1.0). The module declares no
5789    /// kclVersion because a KCL 3.0 entry point rejects an import declaring a
5790    /// different one before this gate is reached.
5791    #[tokio::test(flavor = "multi_thread")]
5792    async fn default_angle_unit_in_import_uses_effective_kcl_version() {
5793        let dep = "@settings(defaultAngleUnit = deg)\nexport x = 1\n";
5794        for version in ["1.0", "2.0", "\"3.0-preview\""] {
5795            let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5796            let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5797            if version == "\"3.0-preview\"" {
5798                assert_eq!(
5799                    result.unwrap_err().message(),
5800                    "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
5801                );
5802            } else {
5803                assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0);
5804            }
5805        }
5806    }
5807
5808    /// The attribute name recognized in an imported file follows the effective
5809    /// kclVersion: the entry point's when it declares KCL 3.0, otherwise the
5810    /// imported file's own (undeclared here, so 1.0). The imported file
5811    /// declares no kclVersion because a KCL 3.0 entry point rejects an import
5812    /// declaring a different one.
5813    ///
5814    /// Warnings raised while an imported file runs aren't observable here (it
5815    /// runs on a clone of the execution state), so this checks the gate with
5816    /// an unknown diagnostic name: a fatal error when the attribute is
5817    /// recognized, and an unknown annotation otherwise.
5818    #[tokio::test(flavor = "multi_thread")]
5819    async fn diagnostics_attribute_in_import_uses_effective_kcl_version() {
5820        let dep = "@diagnostics(allow = bogus)\nexport x = 1\n";
5821        for version in ["1.0", "2.0", "\"3.0-preview\""] {
5822            let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5823            let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5824            if version == "\"3.0-preview\"" {
5825                let error = result.unwrap_err();
5826                let message = error.message();
5827                assert!(
5828                    message.starts_with("Unexpected diagnostic value: `bogus`; accepted values: "),
5829                    "main={main}, message={message}"
5830                );
5831            } else {
5832                assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0, "main={main}");
5833            }
5834        }
5835
5836        // The old `@warnings` name follows the same version: recognized, and
5837        // so validated, before KCL 3.0, and an ignored attribute with a
5838        // non-fatal error under KCL 3.0.
5839        let dep = "@warnings(allow = bogus)\nexport x = 1\n";
5840        for version in ["1.0", "2.0", "\"3.0-preview\""] {
5841            let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5842            let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5843            if version == "\"3.0-preview\"" {
5844                assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0, "main={main}");
5845            } else {
5846                let error = result.unwrap_err();
5847                let message = error.message();
5848                assert!(
5849                    message.starts_with("Unexpected warning value: `bogus`; accepted values: "),
5850                    "main={main}, message={message}"
5851                );
5852            }
5853        }
5854    }
5855
5856    /// The entry point's declared kclVersion is recorded whatever it is, so
5857    /// that errors can name it. Only KCL 3.0 or later pins the version for the
5858    /// whole execution; see [`ExecState::kcl_version`].
5859    #[tokio::test(flavor = "multi_thread")]
5860    async fn entry_point_kcl_version_records_declared_version() {
5861        for (code, expected) in [
5862            ("x = 1\n", None),
5863            ("@settings(defaultLengthUnit = in)\nx = 1\n", None),
5864            ("@settings(kclVersion = 1.0)\nx = 1\n", Some(KclVersion::V1)),
5865            ("@settings(kclVersion = 2.0)\nx = 1\n", Some(KclVersion::V2)),
5866            (
5867                "@settings(kclVersion = \"3.0-preview\")\nx = 1\n",
5868                Some(KclVersion::V3Preview),
5869            ),
5870        ] {
5871            let result = parse_execute(code).await.unwrap();
5872            assert_eq!(
5873                result.exec_state.global.entry_point_kcl_version, expected,
5874                "code={code}"
5875            );
5876            assert_eq!(
5877                result.exec_state.entry_point_version_is_v3_or_higher(),
5878                expected == Some(KclVersion::V3Preview),
5879                "code={code}"
5880            );
5881        }
5882    }
5883
5884    #[tokio::test]
5885    async fn default_tolerance_command_is_sent_only_for_kcl_3() {
5886        let ctx = ExecutorContext::new_mock(None).await;
5887        let mut exec_state = ExecState::new(&ctx);
5888
5889        for version in [None, Some(KclVersion::V1), Some(KclVersion::V2)] {
5890            exec_state.global.entry_point_kcl_version = version;
5891            assert!(ExecutorContext::default_tolerance_command(&exec_state).is_none());
5892        }
5893
5894        exec_state.global.entry_point_kcl_version = Some(KclVersion::V3Preview);
5895        for unit in [kcl_api::UnitLength::Millimeters, kcl_api::UnitLength::Inches] {
5896            exec_state.mod_local.settings.default_length_units = unit;
5897            let Some(ModelingCmd::SetDefaultSystemProperties(properties)) =
5898                ExecutorContext::default_tolerance_command(&exec_state)
5899            else {
5900                panic!("expected KCL 3 default system properties command");
5901            };
5902            let tolerance = properties.tolerance.expect("expected KCL 3 tolerance");
5903            approx::assert_relative_eq!(
5904                tolerance.point_point_2d_coincident.0,
5905                1e-8 * std::f64::consts::SQRT_2,
5906                epsilon = 0.0,
5907                max_relative = 1e-12
5908            );
5909        }
5910    }
5911
5912    #[tokio::test(flavor = "multi_thread")]
5913    async fn kcl_version_lookup_prefers_entry_point_over_module_local() {
5914        let mut exec_state = parse_execute("x = 1\n").await.unwrap().exec_state;
5915
5916        // Legacy fallback: the module-local settings.
5917        exec_state.global.entry_point_kcl_version = None;
5918        exec_state.mod_local.settings.kcl_version = KclVersion::V2;
5919        assert_eq!(exec_state.kcl_version(), KclVersion::V2);
5920        assert_eq!(exec_state.legacy_caller_kcl_version(), KclVersion::V2);
5921
5922        // A pre-3.0 entry-point declaration does not pin the version: the
5923        // module-local settings still apply.
5924        exec_state.global.entry_point_kcl_version = Some(KclVersion::V1);
5925        assert_eq!(exec_state.kcl_version(), KclVersion::V2);
5926
5927        // An entry-point KCL 3.0 declaration overrides the module-local
5928        // settings for the unified lookup, but not for the legacy one.
5929        exec_state.global.entry_point_kcl_version = Some(KclVersion::V3Preview);
5930        assert_eq!(exec_state.kcl_version(), KclVersion::V3Preview);
5931        assert_eq!(exec_state.legacy_caller_kcl_version(), KclVersion::V2);
5932    }
5933
5934    /// Mock execution skips `run_concurrent`, so it relies on `inner_run` to
5935    /// record the entry point's kclVersion -- including re-recording it on
5936    /// every run when restoring memory preserved from a previous mock run,
5937    /// since the preserved memory must not pin the previous program's version.
5938    #[tokio::test(flavor = "multi_thread")]
5939    async fn mock_execution_records_entry_point_kcl_version() {
5940        use futures::FutureExt;
5941
5942        let ctx = ExecutorContext::new_mock(None).await;
5943        let fresh_memory = MockConfig {
5944            use_prev_memory: false,
5945            ..Default::default()
5946        };
5947        let prev_memory = MockConfig::default();
5948
5949        let v3_program = crate::Program::parse_no_errs("@settings(kclVersion = \"3.0-preview\")\nx = 1\n").unwrap();
5950        let v2_program = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 1\n").unwrap();
5951
5952        // Close the context even if an assertion panics,
5953        // then let the panic continue.
5954        let test_result = std::panic::AssertUnwindSafe(async {
5955            let (exec_state, _) = ctx.run_mock_returning_state(&v3_program, &fresh_memory).await.unwrap();
5956            assert_eq!(
5957                exec_state.global.entry_point_kcl_version,
5958                Some(KclVersion::V3Preview),
5959                "mock execution should record a 3.0-preview entry point"
5960            );
5961            assert!(exec_state.entry_point_version_is_v3_or_higher());
5962
5963            // Populate the preserved mock memory with a 3.0-preview run, then
5964            // check that a 2.0 run restoring that memory isn't pinned to
5965            // 3.0-preview...
5966            ctx.run_mock(&v3_program, &fresh_memory).await.unwrap();
5967            let (exec_state, _) = ctx.run_mock_returning_state(&v2_program, &prev_memory).await.unwrap();
5968            assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V2));
5969            assert!(!exec_state.entry_point_version_is_v3_or_higher());
5970
5971            // ...and that a 3.0-preview run restoring a 2.0 run's memory
5972            // records 3.0-preview.
5973            ctx.run_mock(&v2_program, &fresh_memory).await.unwrap();
5974            let (exec_state, _) = ctx.run_mock_returning_state(&v3_program, &prev_memory).await.unwrap();
5975            assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V3Preview));
5976        })
5977        .catch_unwind()
5978        .await;
5979
5980        ctx.close().await;
5981        if let Err(panic) = test_result {
5982            std::panic::resume_unwind(panic);
5983        }
5984    }
5985
5986    /// Mock execution applies the KCL 3.0 semantics -- early return and
5987    /// if-arm scoping -- since it records the entry point's kclVersion via
5988    /// `inner_run` rather than `run_concurrent`.
5989    #[tokio::test(flavor = "multi_thread")]
5990    async fn mock_execution_applies_v3_semantics() {
5991        use futures::FutureExt;
5992
5993        let ctx = ExecutorContext::new_mock(None).await;
5994        let fresh_memory = MockConfig {
5995            use_prev_memory: false,
5996            ..Default::default()
5997        };
5998        let program = crate::Program::parse_no_errs(
5999            r#"@settings(kclVersion = "3.0-preview")
6000fn f() {
6001  return 1
6002  assert(1, isEqualTo = 2, error = "code after return ran")
6003}
6004x = f()
6005outer = 1
6006y = if true {
6007  outer = 2
6008  outer + 10
6009} else {
6010  0
6011}
6012"#,
6013        )
6014        .unwrap();
6015
6016        // Close the context even if an assertion panics,
6017        // then let the panic continue.
6018        let test_result = std::panic::AssertUnwindSafe(async {
6019            let (exec_state, env) = ctx.run_mock_returning_state(&program, &fresh_memory).await.unwrap();
6020            let var = |name: &str| mem_get_json(exec_state.stack(), env, name).as_f64().unwrap();
6021            assert_eq!(var("x"), 1.0, "early return produces the function's value");
6022            assert_eq!(var("y"), 12.0, "the branch sees its own shadowing binding");
6023            assert_eq!(var("outer"), 1.0, "the outer binding is unchanged after the if");
6024        })
6025        .catch_unwind()
6026        .await;
6027
6028        ctx.close().await;
6029        if let Err(panic) = test_result {
6030            std::panic::resume_unwind(panic);
6031        }
6032    }
6033
6034    /// The modeling commands sent to the engine during the run, across the
6035    /// root module and every imported module.
6036    fn commands_everywhere(result: &ExecTestResults) -> impl Iterator<Item = &kittycad_modeling_cmds::ModelingCmd> {
6037        let module_commands = result
6038            .exec_state
6039            .global
6040            .module_infos
6041            .values()
6042            .filter_map(|info| match &info.repr {
6043                ModuleRepr::Kcl(_, Some(outcome)) => Some(outcome.artifacts.commands.iter()),
6044                _ => None,
6045            })
6046            .flatten();
6047        result
6048            .root_module_artifact_commands()
6049            .iter()
6050            .chain(module_commands)
6051            .map(|artifact_command| &artifact_command.command)
6052    }
6053
6054    /// All fillet algorithm versions sent to the engine during the run. The
6055    /// version emitted is the observable for which kclVersion governed the
6056    /// filleting code; see `default_edge_cut_version`.
6057    fn emitted_fillet_versions_everywhere(
6058        result: &ExecTestResults,
6059    ) -> Vec<kittycad_modeling_cmds::shared::EdgeCutVersion> {
6060        commands_everywhere(result)
6061            .filter_map(|command| match command {
6062                kittycad_modeling_cmds::ModelingCmd::Solid3dCutEdges(command) => Some(command.version),
6063                _ => None,
6064            })
6065            .collect()
6066    }
6067
6068    /// All region algorithm versions sent to the engine during the run. The
6069    /// version emitted is the observable for whether KCL 1.0 or 2.0 governed
6070    /// the region code, their only runtime difference; see `region_version`
6071    /// in `std::sketch`.
6072    fn emitted_region_versions_everywhere(
6073        result: &ExecTestResults,
6074    ) -> Vec<kittycad_modeling_cmds::shared::RegionVersion> {
6075        commands_everywhere(result)
6076            .filter_map(|command| match command {
6077                kittycad_modeling_cmds::ModelingCmd::CreateRegion(command) => Some(command.version.clone()),
6078                _ => None,
6079            })
6080            .collect()
6081    }
6082
6083    const FILLET_AT_MODULE_TOP_LEVEL: &str = r#"
6084profile = startSketchOn(XY)
6085  |> startProfile(at = [0, 0])
6086  |> line(end = [10, 0], tag = $edge)
6087  |> line(end = [0, 10])
6088  |> line(end = [-10, 0])
6089  |> close()
6090solid = extrude(profile, length = 10)
6091fillet(solid, tags = [edge], radius = 1)
6092"#;
6093
6094    const FILLET_IN_EXPORTED_FN: &str = r#"
6095export fn filletedBox() {
6096  profile = startSketchOn(XY)
6097    |> startProfile(at = [0, 0])
6098    |> line(end = [10, 0], tag = $edge)
6099    |> line(end = [0, 10])
6100    |> line(end = [-10, 0])
6101    |> close()
6102  solid = extrude(profile, length = 10)
6103  return fillet(solid, tags = [edge], radius = 1)
6104}
6105"#;
6106
6107    /// A KCL 3.0 entry point pins the kclVersion for the whole execution: an
6108    /// imported module that declares no kclVersion (1.0 under the legacy
6109    /// lookup) observes KCL 3.0 both in its module-level code and in its
6110    /// functions, wherever they are called from. An import declaring a
6111    /// different version is rejected instead; see
6112    /// [`imported_module_kcl_version_must_match_v3_entry_point`].
6113    #[tokio::test(flavor = "multi_thread")]
6114    async fn entry_point_v3_pins_kcl_version_for_imported_modules() {
6115        use kittycad_modeling_cmds::shared::EdgeCutVersion;
6116
6117        let dep = FILLET_AT_MODULE_TOP_LEVEL;
6118        let main = r#"@settings(kclVersion = "3.0-preview")
6119import "dep.kcl" as dep
6120"#;
6121        let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
6122        assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
6123
6124        let dep = FILLET_IN_EXPORTED_FN;
6125        let main = r#"@settings(kclVersion = "3.0-preview")
6126import filletedBox from "dep.kcl"
6127box = filletedBox()
6128"#;
6129        let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
6130        assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
6131    }
6132
6133    const REGION_AT_MODULE_TOP_LEVEL: &str = r#"
6134profile = sketch(on = XY) {
6135  outline = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6136}
6137disc = region(segments = [profile.outline])
6138"#;
6139
6140    const REGION_IN_EXPORTED_FN: &str = r#"
6141export fn disc() {
6142  profile = sketch(on = XY) {
6143    outline = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
6144  }
6145  return region(segments = [profile.outline])
6146}
6147"#;
6148
6149    /// Without a KCL 3.0 entry point, the legacy lookup applies unchanged,
6150    /// including its quirk: an imported module's module-level code observes the
6151    /// module's own declared version, but its functions observe the CALLING
6152    /// module's version. Pinned with KCL 1.0 and 2.0, so that the pin does not
6153    /// depend on importing a KCL 3.0 file.
6154    #[tokio::test(flavor = "multi_thread")]
6155    async fn legacy_kcl_version_quirk_applies_without_v3_entry_point() {
6156        use kittycad_modeling_cmds::shared::RegionVersion;
6157
6158        let dep = format!("@settings(kclVersion = 1.0)\n{REGION_AT_MODULE_TOP_LEVEL}");
6159        let main = r#"@settings(kclVersion = 2.0)
6160import "dep.kcl" as dep
6161"#;
6162        let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
6163        assert_eq!(emitted_region_versions_everywhere(&result), vec![RegionVersion::V0]);
6164
6165        let dep = format!("@settings(kclVersion = 1.0)\n{REGION_IN_EXPORTED_FN}");
6166        let main = r#"@settings(kclVersion = 2.0)
6167import disc from "dep.kcl"
6168face = disc()
6169"#;
6170        let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
6171        assert_eq!(emitted_region_versions_everywhere(&result), vec![RegionVersion::V1]);
6172    }
6173
6174    /// Builds a mock-engine context whose project directory holds `modules`
6175    /// in an in-memory file system, with `main.kcl` as the current file so
6176    /// that errors can name the entry point's path. Nothing touches disk, so
6177    /// parallel tests share no state.
6178    fn versioned_modules_context(modules: &[(&str, &str)]) -> ExecutorContext {
6179        let project_dir = crate::TypedPath::new("/zma-kcl-version-mismatch");
6180        // Key each module by the same join that import resolution performs,
6181        // so the lookup matches on every platform.
6182        let files = modules
6183            .iter()
6184            .map(|(name, source)| (project_dir.join(name).to_string(), source.as_bytes().to_vec()))
6185            .collect();
6186        ExecutorContext {
6187            engine: Arc::new(EngineManager::new_mock()),
6188            cache: Cache::default(),
6189            engine_batch: EngineBatchContext::default(),
6190            fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
6191            settings: ExecutorSettings {
6192                current_file: Some(project_dir.join("main.kcl")),
6193                project_directory: Some(project_dir),
6194                ..Default::default()
6195            },
6196            context_type: ContextType::Mock,
6197            execution_callbacks: Default::default(),
6198            machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
6199            configure_engine_render: true,
6200            needs_oit: Default::default(),
6201        }
6202    }
6203
6204    /// Runs `main` with `modules` (see [`versioned_modules_context`]) the way
6205    /// engine execution does, which runs every imported module eagerly.
6206    async fn run_versioned_modules(main: &str, modules: &[(&str, &str)]) -> Result<(), KclError> {
6207        let ctx = versioned_modules_context(modules);
6208        let program = crate::Program::parse_no_errs(main).unwrap();
6209        let mut exec_state = ExecState::new(&ctx);
6210        let result = ctx.run(&program, &mut exec_state).await;
6211        ctx.close().await;
6212        result.map(|_| ()).map_err(|err| err.error)
6213    }
6214
6215    /// Runs `main` with `modules` (see [`versioned_modules_context`]) through
6216    /// mock execution, which runs imported modules lazily.
6217    async fn run_versioned_modules_mock(main: &str, modules: &[(&str, &str)]) -> Result<(), KclError> {
6218        let ctx = versioned_modules_context(modules);
6219        let program = crate::Program::parse_no_errs(main).unwrap();
6220        let mock_config = MockConfig {
6221            use_prev_memory: false,
6222            ..Default::default()
6223        };
6224        let result = ctx.run_mock_returning_state(&program, &mock_config).await;
6225        ctx.close().await;
6226        result.map(|_| ()).map_err(|err| err.error)
6227    }
6228
6229    const V3_MAIN_IMPORTING_DEP: &str =
6230        "@settings(kclVersion = \"3.0-preview\")\nimport width from \"dep.kcl\"\nx = width\n";
6231    const V3_STABLE_MAIN_IMPORTING_DEP: &str =
6232        "@settings(kclVersion = 3.0)\nimport width from \"dep.kcl\"\nx = width\n";
6233
6234    fn dep_declaring(version: &str) -> String {
6235        format!("@settings(kclVersion = {version})\nexport width = 10\n")
6236    }
6237
6238    /// The error a mismatch between `main.kcl` and `dep.kcl` must produce,
6239    /// with `expected_dep_version` as the imported file's version.
6240    #[track_caller]
6241    fn assert_kcl_version_mismatch(error: &KclError, expected_dep_version: &str) {
6242        assert!(matches!(error, KclError::Semantic { .. }), "{error:#?}");
6243        assert_eq!(
6244            error.message(),
6245            format!(
6246                "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."
6247            )
6248        );
6249    }
6250
6251    /// A KCL 3.0 entry point rejects an imported file that declares a
6252    /// different kclVersion. The error names both files and both versions,
6253    /// points at the declaration in the imported file, and carries the import
6254    /// site in the entry point as its outer frame.
6255    #[tokio::test(flavor = "multi_thread")]
6256    async fn imported_module_kcl_version_must_match_v3_entry_point() {
6257        for dep_version in ["2.0", "1.0", "3.0"] {
6258            let dep = dep_declaring(dep_version);
6259            let error = run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
6260                .await
6261                .expect_err("mismatched kclVersion should be rejected");
6262            assert_kcl_version_mismatch(&error, dep_version);
6263
6264            let ranges = error.source_ranges();
6265            assert_eq!(ranges.len(), 2, "{ranges:#?}");
6266            // The `kclVersion = ...` setting in dep.kcl.
6267            assert!(!ranges[0].module_id().is_top_level());
6268            let declaration = format!("kclVersion = {dep_version}");
6269            let start = dep.find(&declaration).unwrap();
6270            assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + declaration.len()));
6271            // The import statement in main.kcl.
6272            assert!(ranges[1].module_id().is_top_level());
6273            let import_stmt = "import width from \"dep.kcl\"";
6274            let start = V3_MAIN_IMPORTING_DEP.find(import_stmt).unwrap();
6275            assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
6276            assert_eq!(
6277                error
6278                    .backtrace()
6279                    .iter()
6280                    .map(|frame| frame.fn_name.as_deref())
6281                    .collect::<Vec<_>>(),
6282                [Some("import dep.kcl"), None]
6283            );
6284        }
6285    }
6286
6287    /// Imported files that declare no kclVersion are unaffected: they run
6288    /// under the entry point's version, as before.
6289    #[tokio::test(flavor = "multi_thread")]
6290    async fn imported_module_without_kcl_version_is_allowed_under_v3_entry_point() {
6291        for main in [V3_MAIN_IMPORTING_DEP, V3_STABLE_MAIN_IMPORTING_DEP] {
6292            for dep in [
6293                "export width = 10\n",
6294                "@settings(defaultLengthUnit = in)\nexport width = 10\n",
6295            ] {
6296                run_versioned_modules(main, &[("dep.kcl", dep)])
6297                    .await
6298                    .unwrap_or_else(|err| panic!("main={main:?}, dep={dep:?}: {err:#?}"));
6299            }
6300        }
6301    }
6302
6303    /// Declaring the entry point's own version, in any accepted spelling, is
6304    /// a match.
6305    #[tokio::test(flavor = "multi_thread")]
6306    async fn imported_module_matching_v3_kcl_version_is_allowed() {
6307        for (main, dep_versions) in [
6308            (
6309                V3_MAIN_IMPORTING_DEP,
6310                ["\"3.0-preview\"", "\"3-preview\"", "\"3.0.0-preview\""],
6311            ),
6312            (V3_STABLE_MAIN_IMPORTING_DEP, ["3.0", "3", "\"3.0.0\""]),
6313        ] {
6314            for dep_version in dep_versions {
6315                let dep = dep_declaring(dep_version);
6316                run_versioned_modules(main, &[("dep.kcl", &dep)])
6317                    .await
6318                    .unwrap_or_else(|err| panic!("main={main:?}, dep={dep_version}: {err:#?}"));
6319            }
6320        }
6321    }
6322
6323    #[tokio::test(flavor = "multi_thread")]
6324    async fn stable_v3_entry_point_rejects_other_import_versions() {
6325        for dep_version in ["1.0", "2.0", "\"3.0-preview\""] {
6326            let dep = dep_declaring(dep_version);
6327            let error = run_versioned_modules(V3_STABLE_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
6328                .await
6329                .expect_err("mismatched kclVersion should be rejected");
6330            assert!(matches!(error, KclError::Semantic { .. }), "{error:#?}");
6331            let expected_dep_version = dep_version.trim_matches('"');
6332            assert_eq!(
6333                error.message(),
6334                format!(
6335                    "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."
6336                )
6337            );
6338        }
6339    }
6340
6341    /// Without a KCL 3.0 entry point, KCL 1.0 and 2.0 may still be mixed, as
6342    /// before.
6343    #[tokio::test(flavor = "multi_thread")]
6344    async fn pre_v3_kcl_versions_may_be_mixed_without_v3_entry_point() {
6345        for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
6346            for dep_version in ["1.0", "2.0"] {
6347                let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6348                let dep = dep_declaring(dep_version);
6349                run_versioned_modules(&main, &[("dep.kcl", &dep)])
6350                    .await
6351                    .unwrap_or_else(|err| panic!("main={main_header:?} dep={dep_version}: {err:#?}"));
6352            }
6353        }
6354    }
6355
6356    /// The error an entry point `main.kcl` that declares no kclVersion must
6357    /// produce when it imports a 3.0-preview `dep.kcl`.
6358    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.";
6359
6360    /// Without a KCL 3.0 entry point, an imported file may not declare KCL
6361    /// 3.0: the legacy per-module lookup would otherwise apply KCL 3.0
6362    /// semantics to that file alone. The error names both files, says what
6363    /// the entry point declares (if anything), points at the declaration in
6364    /// the imported file, and carries the import site in the entry point as
6365    /// its outer frame.
6366    #[tokio::test(flavor = "multi_thread")]
6367    async fn v3_import_requires_v3_entry_point() {
6368        for (main_header, entry_point_declares, fix) in [
6369            (
6370                "",
6371                "does not declare a kclVersion",
6372                "Declare the same kclVersion in the entry point, or update the setting in the imported file.",
6373            ),
6374            (
6375                "@settings(kclVersion = 1.0)\n",
6376                "declares kclVersion 1.0",
6377                "Update the kclVersion setting in one of these files to match the other.",
6378            ),
6379            (
6380                "@settings(kclVersion = 2.0)\n",
6381                "declares kclVersion 2.0",
6382                "Update the kclVersion setting in one of these files to match the other.",
6383            ),
6384        ] {
6385            let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6386            let dep = dep_declaring("\"3.0-preview\"");
6387            let error = run_versioned_modules(&main, &[("dep.kcl", &dep)])
6388                .await
6389                .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
6390            assert!(matches!(error, KclError::Semantic { .. }), "{error:#?}");
6391            assert_eq!(
6392                error.message(),
6393                format!(
6394                    "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}"
6395                ),
6396                "main={main_header:?}"
6397            );
6398
6399            let ranges = error.source_ranges();
6400            assert_eq!(ranges.len(), 2, "{ranges:#?}");
6401            // The `kclVersion = ...` setting in dep.kcl.
6402            assert!(!ranges[0].module_id().is_top_level());
6403            let declaration = "kclVersion = \"3.0-preview\"";
6404            let start = dep.find(declaration).unwrap();
6405            assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + declaration.len()));
6406            // The import statement in main.kcl.
6407            assert!(ranges[1].module_id().is_top_level());
6408            let import_stmt = "import width from \"dep.kcl\"";
6409            let start = main.find(import_stmt).unwrap();
6410            assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
6411            assert_eq!(
6412                error
6413                    .backtrace()
6414                    .iter()
6415                    .map(|frame| frame.fn_name.as_deref())
6416                    .collect::<Vec<_>>(),
6417                [Some("import dep.kcl"), None]
6418            );
6419        }
6420    }
6421
6422    /// Every accepted spelling of 3.0-preview is rejected, and the message
6423    /// uses the canonical spelling.
6424    #[tokio::test(flavor = "multi_thread")]
6425    async fn v3_import_spellings_are_all_rejected_without_v3_entry_point() {
6426        let main = "import width from \"dep.kcl\"\nx = width\n";
6427        for dep_version in ["\"3-preview\"", "\"3.0.0-preview\""] {
6428            let dep = dep_declaring(dep_version);
6429            let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
6430                .await
6431                .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
6432            assert_eq!(
6433                error.message(),
6434                V3_DEP_UNDER_UNDECLARED_ENTRY_POINT,
6435                "dep={dep_version}"
6436            );
6437        }
6438    }
6439
6440    /// Mock execution runs a whole-module import's body only when the module
6441    /// is referenced, so the check also runs at the import site.
6442    #[tokio::test(flavor = "multi_thread")]
6443    async fn unreferenced_v3_whole_module_import_is_checked_in_mock_execution() {
6444        let main = "import \"dep.kcl\" as dep\nx = 1\n";
6445        let dep = dep_declaring("\"3.0-preview\"");
6446        let error = run_versioned_modules_mock(main, &[("dep.kcl", &dep)])
6447            .await
6448            .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected in mock execution");
6449        assert_eq!(error.message(), V3_DEP_UNDER_UNDECLARED_ENTRY_POINT);
6450        let ranges = error.source_ranges();
6451        assert_eq!(ranges.len(), 2, "{ranges:#?}");
6452        assert!(!ranges[0].module_id().is_top_level());
6453        assert!(ranges[1].module_id().is_top_level());
6454        let import_stmt = "import \"dep.kcl\" as dep";
6455        let start = main.find(import_stmt).unwrap();
6456        assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
6457
6458        // Engine execution runs the module eagerly and rejects it too.
6459        let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
6460            .await
6461            .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected in engine execution");
6462        assert_eq!(error.message(), V3_DEP_UNDER_UNDECLARED_ENTRY_POINT);
6463    }
6464
6465    /// The check covers transitive imports. The message names the entry point
6466    /// and the mismatched file; the file in between appears in the import
6467    /// backtrace.
6468    #[tokio::test(flavor = "multi_thread")]
6469    async fn transitive_v3_import_requires_v3_entry_point() {
6470        let main = "import doubled from \"a.kcl\"\nx = doubled\n";
6471        let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
6472        let b = dep_declaring("\"3.0-preview\"");
6473        let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", &b)])
6474            .await
6475            .expect_err("a transitive KCL 3.0 import without a KCL 3.0 entry point should be rejected");
6476        assert_eq!(
6477            error.message(),
6478            "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."
6479        );
6480        assert_eq!(
6481            error
6482                .backtrace()
6483                .iter()
6484                .map(|frame| frame.fn_name.as_deref())
6485                .collect::<Vec<_>>(),
6486            [Some("import b.kcl"), Some("import a.kcl"), None]
6487        );
6488    }
6489
6490    /// When execution was started without a file path, the message still
6491    /// describes the entry point, just without a path.
6492    #[tokio::test(flavor = "multi_thread")]
6493    async fn v3_import_without_v3_entry_point_or_entry_point_path() {
6494        let main = "import width from \"dep.kcl\"\nx = width\n";
6495        let dep = dep_declaring("\"3.0-preview\"");
6496        let error = execute_with_modules(main, &[("dep.kcl", &dep)])
6497            .await
6498            .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
6499        let message = error.message();
6500        assert!(
6501            message.starts_with(
6502                "Mixing KCL versions in a single program is not allowed. The entry point does not declare a kclVersion, but the imported file `"
6503            ),
6504            "{message}"
6505        );
6506        assert!(
6507            message.ends_with(
6508                "dep.kcl` declares kclVersion 3.0-preview. Declare the same kclVersion in the entry point, or update the setting in the imported file."
6509            ),
6510            "{message}"
6511        );
6512    }
6513
6514    /// The check covers transitive imports. The message names the entry point
6515    /// and the mismatched file; the file in between appears in the import
6516    /// backtrace.
6517    #[tokio::test(flavor = "multi_thread")]
6518    async fn transitive_import_kcl_version_mismatch_names_entry_point_and_mismatched_file() {
6519        let main = "@settings(kclVersion = \"3.0-preview\")\nimport doubled from \"a.kcl\"\nx = doubled\n";
6520        let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
6521        let b = dep_declaring("2.0");
6522        let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", &b)])
6523            .await
6524            .expect_err("mismatched kclVersion in a transitive import should be rejected");
6525        assert_eq!(
6526            error.message(),
6527            "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."
6528        );
6529        assert_eq!(
6530            error
6531                .backtrace()
6532                .iter()
6533                .map(|frame| frame.fn_name.as_deref())
6534                .collect::<Vec<_>>(),
6535            [Some("import b.kcl"), Some("import a.kcl"), None]
6536        );
6537        let ranges = error.source_ranges();
6538        assert_eq!(ranges.len(), 3, "{ranges:#?}");
6539        assert!(!ranges[0].module_id().is_top_level());
6540        assert!(!ranges[1].module_id().is_top_level());
6541        assert!(ranges[2].module_id().is_top_level());
6542    }
6543
6544    /// Mock execution runs a whole-module import's body only when the module
6545    /// is referenced, so the check also runs at the import site.
6546    #[tokio::test(flavor = "multi_thread")]
6547    async fn unreferenced_whole_module_import_kcl_version_is_checked_in_mock_execution() {
6548        let main = "@settings(kclVersion = \"3.0-preview\")\nimport \"dep.kcl\" as dep\nx = 1\n";
6549        let dep = dep_declaring("2.0");
6550        let error = run_versioned_modules_mock(main, &[("dep.kcl", &dep)])
6551            .await
6552            .expect_err("mismatched kclVersion should be rejected in mock execution");
6553        assert_kcl_version_mismatch(&error, "2.0");
6554        let ranges = error.source_ranges();
6555        assert_eq!(ranges.len(), 2, "{ranges:#?}");
6556        assert!(!ranges[0].module_id().is_top_level());
6557        assert!(ranges[1].module_id().is_top_level());
6558        let import_stmt = "import \"dep.kcl\" as dep";
6559        let start = main.find(import_stmt).unwrap();
6560        assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
6561
6562        // Engine execution runs the module eagerly and rejects it too.
6563        let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
6564            .await
6565            .expect_err("mismatched kclVersion should be rejected in engine execution");
6566        assert_kcl_version_mismatch(&error, "2.0");
6567
6568        // An undeclared version is fine in mock execution as well.
6569        run_versioned_modules_mock(main, &[("dep.kcl", "export width = 10\n")])
6570            .await
6571            .unwrap();
6572    }
6573
6574    #[tokio::test(flavor = "multi_thread")]
6575    async fn imported_use_keyword_follows_entry_point_version() {
6576        let dep = "use = 10\nexport width = use\n";
6577        for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
6578            let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6579            run_versioned_modules(&main, &[("dep.kcl", dep)])
6580                .await
6581                .unwrap_or_else(|error| panic!("main={main_header:?}: {error:#?}"));
6582        }
6583
6584        let main_v1 = "@settings(kclVersion = 1.0)\nimport width from \"dep.kcl\"\nx = width\n";
6585        let dep_v2 = format!("@settings(kclVersion = 2.0)\n{dep}");
6586        run_versioned_modules(main_v1, &[("dep.kcl", &dep_v2)]).await.unwrap();
6587
6588        for (main, version) in [
6589            (V3_MAIN_IMPORTING_DEP, "\"3.0-preview\""),
6590            (V3_STABLE_MAIN_IMPORTING_DEP, "3.0"),
6591        ] {
6592            for dep in [dep.to_owned(), format!("@settings(kclVersion = {version})\n{dep}")] {
6593                let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
6594                    .await
6595                    .expect_err("V3 imports must reject a use identifier");
6596                assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6597                assert_eq!(error.message(), crate::parsing::RESERVED_USE_MESSAGE);
6598                let ranges = error.source_ranges();
6599                assert_eq!(ranges.len(), 2, "{ranges:#?}");
6600                let start = dep.find("use =").unwrap();
6601                assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + 3));
6602                assert!(!ranges[0].module_id().is_top_level());
6603                assert!(ranges[1].module_id().is_top_level());
6604            }
6605        }
6606    }
6607
6608    #[tokio::test(flavor = "multi_thread")]
6609    async fn imported_never_type_follows_entry_point_version() {
6610        let dep = "export fn stop(): never {}\n";
6611        for version in ["1.0", "2.0"] {
6612            let main = format!("@settings(kclVersion = {version})\nimport stop from \"dep.kcl\"\nx = 1\n");
6613            for run_mock in [false, true] {
6614                let error = if run_mock {
6615                    run_versioned_modules_mock(&main, &[("dep.kcl", dep)]).await
6616                } else {
6617                    run_versioned_modules(&main, &[("dep.kcl", dep)]).await
6618                }
6619                .expect_err("older KCL versions must reject imported `never` types");
6620                assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6621                assert_eq!(
6622                    error.message(),
6623                    format!("The `never` type requires KCL 3.0-preview, but this program uses KCL {version}.")
6624                );
6625                let ranges = error.source_ranges();
6626                assert_eq!(ranges.len(), 2, "{ranges:#?}");
6627                let start = dep.find("never").unwrap();
6628                assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + "never".len()));
6629                assert!(!ranges[0].module_id().is_top_level());
6630                assert!(ranges[1].module_id().is_top_level());
6631            }
6632        }
6633
6634        let main_v3 = "@settings(kclVersion = \"3.0-preview\")\nimport stop from \"dep.kcl\"\nx = 1\n";
6635        run_versioned_modules(main_v3, &[("dep.kcl", dep)]).await.unwrap();
6636
6637        let dep_v2 = format!("@settings(kclVersion = 2.0)\n{dep}");
6638        let error = run_versioned_modules(main_v3, &[("dep.kcl", &dep_v2)])
6639            .await
6640            .expect_err("version mismatch must precede `never` validation");
6641        assert_kcl_version_mismatch(&error, "2.0");
6642        assert_eq!(
6643            error
6644                .backtrace()
6645                .iter()
6646                .map(|frame| frame.fn_name.as_deref())
6647                .collect::<Vec<_>>(),
6648            [Some("import dep.kcl"), None]
6649        );
6650
6651        let main_v2 = "@settings(kclVersion = 2.0)\nimport stop from \"dep.kcl\"\nx = 1\n";
6652        let dep_v3 = format!("@settings(kclVersion = \"3.0-preview\")\n{dep}");
6653        let error = run_versioned_modules(main_v2, &[("dep.kcl", &dep_v3)])
6654            .await
6655            .expect_err("version mismatch must precede `never` validation");
6656        assert!(
6657            error
6658                .message()
6659                .starts_with("Mixing KCL versions in a single program is not allowed.")
6660        );
6661
6662        // Mock execution must validate a whole-module import even when it is unused.
6663        let unused_import = "@settings(kclVersion = 2.0)\nimport \"dep.kcl\" as dep\nx = 1\n";
6664        let error = run_versioned_modules_mock(unused_import, &[("dep.kcl", dep)])
6665            .await
6666            .expect_err("an unused imported module must not bypass `never` validation");
6667        assert_eq!(
6668            error.message(),
6669            "The `never` type requires KCL 3.0-preview, but this program uses KCL 2.0."
6670        );
6671    }
6672
6673    #[tokio::test(flavor = "multi_thread")]
6674    async fn never_type_resolution_rejects_an_unvalidated_v2_ast() {
6675        let source = "@settings(kclVersion = 2.0)\nfn stop(): never {}\n";
6676        let (kcl_version, ast, _) = crate::parsing::parse_str_syntax(source, ModuleId::default()).unwrap();
6677        let program = crate::Program {
6678            kcl_version,
6679            ast,
6680            original_file_contents: source.to_owned(),
6681        };
6682        let ctx = versioned_modules_context(&[]);
6683        let mut exec_state = ExecState::new(&ctx);
6684        let error = ctx.run(&program, &mut exec_state).await.unwrap_err().error;
6685        ctx.close().await;
6686
6687        assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6688        assert_eq!(
6689            error.message(),
6690            "The `never` type requires KCL 3.0-preview, but this program uses KCL 2.0."
6691        );
6692    }
6693
6694    #[tokio::test(flavor = "multi_thread")]
6695    async fn import_universe_uses_entry_point_version_in_a_fresh_state() {
6696        let main = "@settings(kclVersion = \"3.0-preview\")\nimport stop from \"dep.kcl\"\nx = 1\n";
6697        let ctx = versioned_modules_context(&[("dep.kcl", "export fn stop(): never {}\n")]);
6698        let program = crate::Program::parse_no_errs(main).unwrap();
6699        let mut exec_state = ExecState::new(&ctx);
6700
6701        ctx.get_universe(&program, &mut exec_state).await.unwrap();
6702        assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V3Preview));
6703        ctx.close().await;
6704    }
6705
6706    #[tokio::test(flavor = "multi_thread")]
6707    async fn imported_enum_identifier_follows_entry_point_version() {
6708        let dep = "enum = 10\nexport width = enum\n";
6709        for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
6710            let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6711            run_versioned_modules(&main, &[("dep.kcl", dep)])
6712                .await
6713                .unwrap_or_else(|error| panic!("main={main_header:?}: {error:#?}"));
6714        }
6715
6716        for run_mock in [false, true] {
6717            let error = if run_mock {
6718                run_versioned_modules_mock(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep)]).await
6719            } else {
6720                run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep)]).await
6721            }
6722            .expect_err("V3 imports must reject an enum identifier");
6723            assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6724            assert_eq!(error.message(), crate::parsing::RESERVED_ENUM_MESSAGE);
6725            let ranges = error.source_ranges();
6726            assert_eq!(ranges.len(), 2, "{ranges:#?}");
6727            assert_eq!((ranges[0].start(), ranges[0].end()), (0, "enum".len()));
6728            assert!(!ranges[0].module_id().is_top_level());
6729            assert!(ranges[1].module_id().is_top_level());
6730        }
6731
6732        let dep_v2 = format!("@settings(kclVersion = 2.0)\n{dep}");
6733        let error = run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep_v2)])
6734            .await
6735            .expect_err("version mismatch must precede enum validation");
6736        assert_kcl_version_mismatch(&error, "2.0");
6737    }
6738
6739    #[tokio::test(flavor = "multi_thread")]
6740    async fn imported_import_modifiers_follow_entry_point_version() {
6741        for word in ["template", "lazy", "component"] {
6742            let dep = format!("import {word} from \"nested.kcl\"\nexport width = 10\n");
6743            let nested = format!("export {word} = 1\n");
6744            for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
6745                let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6746                run_versioned_modules(&main, &[("dep.kcl", &dep), ("nested.kcl", &nested)])
6747                    .await
6748                    .unwrap_or_else(|error| panic!("main={main_header:?}, word={word}: {error:#?}"));
6749            }
6750
6751            for run_mock in [false, true] {
6752                let error = if run_mock {
6753                    run_versioned_modules_mock(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep), ("nested.kcl", &nested)])
6754                        .await
6755                } else {
6756                    run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep), ("nested.kcl", &nested)]).await
6757                }
6758                .expect_err("V3 imports must reject a reserved import modifier");
6759                assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6760                assert_eq!(
6761                    error.message(),
6762                    format!(
6763                        "`{word}` is reserved as an import modifier in KCL 3.0 and cannot be the first imported item"
6764                    )
6765                );
6766                let ranges = error.source_ranges();
6767                assert_eq!(ranges.len(), 2, "{ranges:#?}");
6768                let start = dep.find(word).unwrap();
6769                assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + word.len()));
6770                assert!(!ranges[0].module_id().is_top_level());
6771                assert!(ranges[1].module_id().is_top_level());
6772            }
6773        }
6774    }
6775
6776    #[tokio::test(flavor = "multi_thread")]
6777    async fn import_version_mismatch_precedes_import_modifier_error() {
6778        let dep = "@settings(kclVersion = 2.0)\nimport lazy from \"nested.kcl\"\nexport width = 10\n";
6779        let nested = "export lazy = 1\n";
6780        for run_mock in [false, true] {
6781            let error = if run_mock {
6782                run_versioned_modules_mock(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep), ("nested.kcl", nested)]).await
6783            } else {
6784                run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep), ("nested.kcl", nested)]).await
6785            }
6786            .expect_err("version mismatch must precede import modifier validation");
6787            assert_kcl_version_mismatch(&error, "2.0");
6788        }
6789    }
6790
6791    #[tokio::test(flavor = "multi_thread")]
6792    async fn imported_use_function_name_is_allowed_under_v3() {
6793        let dep = "fn use() { return 10 }\nexport width = use()\n";
6794        run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep)])
6795            .await
6796            .unwrap();
6797    }
6798
6799    #[tokio::test(flavor = "multi_thread")]
6800    async fn import_version_mismatch_precedes_use_keyword_error() {
6801        let dep_v2 = "@settings(kclVersion = 2.0)\nuse = 10\nexport width = use\n";
6802        let error = run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep_v2)])
6803            .await
6804            .expect_err("version mismatch must precede the use keyword error");
6805        assert_kcl_version_mismatch(&error, "2.0");
6806
6807        let main_v2 = "@settings(kclVersion = 2.0)\nimport width from \"dep.kcl\"\nx = width\n";
6808        let dep_v3 = "@settings(kclVersion = \"3.0-preview\")\nuse = 10\nexport width = use\n";
6809        let error = run_versioned_modules(main_v2, &[("dep.kcl", dep_v3)])
6810            .await
6811            .expect_err("version mismatch must precede the use keyword error");
6812        assert!(
6813            error
6814                .message()
6815                .starts_with("Mixing KCL versions in a single program is not allowed.")
6816        );
6817    }
6818
6819    #[tokio::test(flavor = "multi_thread")]
6820    async fn unreferenced_whole_module_import_checks_use_keyword_in_mock_execution() {
6821        let main = "@settings(kclVersion = \"3.0-preview\")\nimport \"dep.kcl\" as dep\nx = 1\n";
6822        let dep = "use = 10\n";
6823        let error = run_versioned_modules_mock(main, &[("dep.kcl", dep)])
6824            .await
6825            .expect_err("mock import must reject a use identifier");
6826        assert_eq!(error.message(), crate::parsing::RESERVED_USE_MESSAGE);
6827        let ranges = error.source_ranges();
6828        assert_eq!(ranges.len(), 2, "{ranges:#?}");
6829        assert!(!ranges[0].module_id().is_top_level());
6830        assert!(ranges[1].module_id().is_top_level());
6831
6832        let dep_v2 = "@settings(kclVersion = 2.0)\nuse = 10\n";
6833        let error = run_versioned_modules_mock(main, &[("dep.kcl", dep_v2)])
6834            .await
6835            .expect_err("version mismatch must precede the use keyword error in mock execution");
6836        assert_kcl_version_mismatch(&error, "2.0");
6837    }
6838
6839    #[tokio::test(flavor = "multi_thread")]
6840    async fn transitive_import_checks_use_keyword_under_v3_entry_point() {
6841        let main = "@settings(kclVersion = \"3.0-preview\")\nimport doubled from \"a.kcl\"\nx = doubled\n";
6842        let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
6843        let b = "use = 10\nexport width = use\n";
6844        let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", b)])
6845            .await
6846            .expect_err("transitive import must reject a use identifier");
6847        assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6848        assert_eq!(error.message(), crate::parsing::RESERVED_USE_MESSAGE);
6849        let ranges = error.source_ranges();
6850        assert_eq!(ranges.len(), 3, "{ranges:#?}");
6851        assert!(!ranges[0].module_id().is_top_level());
6852        assert!(!ranges[1].module_id().is_top_level());
6853        assert!(ranges[2].module_id().is_top_level());
6854        assert_eq!(
6855            error
6856                .backtrace()
6857                .iter()
6858                .map(|frame| frame.fn_name.as_deref())
6859                .collect::<Vec<_>>(),
6860            [Some("import b.kcl"), Some("import a.kcl"), None]
6861        );
6862    }
6863
6864    /// Standard library modules declare kclVersion 1.0 but are exempt: they
6865    /// always run under the entry point's version, and the user cannot edit
6866    /// them. The prelude is imported implicitly; `std::turns` is imported
6867    /// explicitly here.
6868    #[tokio::test(flavor = "multi_thread")]
6869    async fn std_modules_are_exempt_from_kcl_version_matching() {
6870        let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport QUARTER_TURN from \"std::turns\"\nx = QUARTER_TURN\n";
6871        run_versioned_modules(main, &[]).await.unwrap();
6872        run_versioned_modules_mock(main, &[]).await.unwrap();
6873    }
6874
6875    /// When execution was started without a file path, the message still
6876    /// describes the entry point, just without a path.
6877    #[tokio::test(flavor = "multi_thread")]
6878    async fn kcl_version_mismatch_without_entry_point_path() {
6879        let dep = dep_declaring("2.0");
6880        let error = execute_with_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
6881            .await
6882            .expect_err("mismatched kclVersion should be rejected");
6883        let message = error.message();
6884        assert!(
6885            message.starts_with(
6886                "Mixing KCL versions in a single program is not allowed. The entry point declares kclVersion 3.0-preview, but the imported file `"
6887            ),
6888            "{message}"
6889        );
6890        assert!(
6891            message.ends_with(
6892                "dep.kcl` declares kclVersion 2.0. Update the kclVersion setting in one of these files to match the other."
6893            ),
6894            "{message}"
6895        );
6896    }
6897
6898    #[track_caller]
6899    fn variable_f64(result: &ExecTestResults, name: &str) -> f64 {
6900        mem_get_json(result.exec_state.stack(), result.mem_env, name)
6901            .as_f64()
6902            .unwrap()
6903    }
6904
6905    #[tokio::test(flavor = "multi_thread")]
6906    async fn return_terminates_function_early_in_v3() {
6907        let code = r#"@settings(kclVersion = "3.0-preview")
6908fn f() {
6909  return 1
6910  assert(1, isEqualTo = 2, error = "code after return ran")
6911}
6912x = f()
6913"#;
6914        let result = parse_execute(code).await.unwrap();
6915        assert_eq!(variable_f64(&result, "x"), 1.0);
6916    }
6917
6918    #[tokio::test(flavor = "multi_thread")]
6919    async fn second_return_is_unreachable_in_v3() {
6920        let code = r#"@settings(kclVersion = "3.0-preview")
6921fn f() {
6922  return 1
6923  return 2
6924}
6925x = f()
6926"#;
6927        let result = parse_execute(code).await.unwrap();
6928        assert_eq!(variable_f64(&result, "x"), 1.0);
6929    }
6930
6931    #[tokio::test(flavor = "multi_thread")]
6932    async fn return_inside_if_arm_returns_from_function_in_v3() {
6933        let code = r#"@settings(kclVersion = "3.0-preview")
6934fn f(@b) {
6935  dummy = if b {
6936    return 1
6937    0
6938  } else {
6939    0
6940  }
6941  return 2
6942}
6943x = f(true)
6944y = f(false)
6945"#;
6946        let result = parse_execute(code).await.unwrap();
6947        assert_eq!(variable_f64(&result, "x"), 1.0);
6948        assert_eq!(variable_f64(&result, "y"), 2.0);
6949    }
6950
6951    #[tokio::test(flavor = "multi_thread")]
6952    async fn return_inside_nested_if_returns_from_function_in_v3() {
6953        let code = r#"@settings(kclVersion = "3.0-preview")
6954fn f(@a, b) {
6955  dummy = if a {
6956    inner = if b {
6957      return 10
6958      0
6959    } else {
6960      1
6961    }
6962    inner + 1
6963  } else {
6964    2
6965  }
6966  return dummy * 100
6967}
6968x = f(true, b = true)
6969y = f(true, b = false)
6970z = f(false, b = false)
6971"#;
6972        let result = parse_execute(code).await.unwrap();
6973        assert_eq!(variable_f64(&result, "x"), 10.0);
6974        assert_eq!(variable_f64(&result, "y"), 200.0);
6975        assert_eq!(variable_f64(&result, "z"), 200.0);
6976    }
6977
6978    #[tokio::test(flavor = "multi_thread")]
6979    async fn return_inside_closure_returns_only_from_closure_in_v3() {
6980        let code = r#"@settings(kclVersion = "3.0-preview")
6981fn outer() {
6982  inner = fn() {
6983    return 5
6984    assert(1, isEqualTo = 2, error = "code after inner return ran")
6985  }
6986  v = inner()
6987  return v + 1
6988}
6989x = outer()
6990"#;
6991        let result = parse_execute(code).await.unwrap();
6992        assert_eq!(variable_f64(&result, "x"), 6.0);
6993    }
6994
6995    #[tokio::test(flavor = "multi_thread")]
6996    async fn return_type_coercion_applies_to_early_return_in_v3() {
6997        let code = r#"@settings(kclVersion = "3.0-preview")
6998fn f(): number(mm) {
6999  return 1
7000  assert(1, isEqualTo = 2, error = "code after return ran")
7001}
7002x = f()
7003"#;
7004        let result = parse_execute(code).await.unwrap();
7005        assert_eq!(variable_f64(&result, "x"), 1.0);
7006
7007        // A coercion failure surfaces as an error (on the machine, this
7008        // exercises unwind_return's error path).
7009        let code = r#"@settings(kclVersion = "3.0-preview")
7010fn f(): number(mm) {
7011  return "nope"
7012}
7013x = f()
7014"#;
7015        let err = parse_execute(code).await.expect_err("coercion failure should error");
7016        assert!(err.message().contains("type"), "unexpected message: {}", err.message());
7017    }
7018
7019    #[tokio::test(flavor = "multi_thread")]
7020    async fn return_at_top_level_errors() {
7021        // A return statement at the top level is rejected in all versions.
7022        for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
7023            let code = format!("{header}return 1\n");
7024            assert_eq!(
7025                parse_execute(&code).await.expect_err("should error").message(),
7026                "Cannot return from outside a function."
7027            );
7028        }
7029
7030        // Under KCL 3.0, a return escaping a top-level if-arm is also rejected
7031        // (without the setting it is silently ignored; see
7032        // top_level_if_arm_return_ignored_without_v3).
7033        let code = r#"@settings(kclVersion = "3.0-preview")
7034x = if true {
7035  return 1
7036  0
7037} else {
7038  0
7039}
7040"#;
7041        assert_eq!(
7042            parse_execute(code).await.expect_err("should error").message(),
7043            "Cannot return from outside a function."
7044        );
7045    }
7046
7047    #[tokio::test(flavor = "multi_thread")]
7048    async fn exit_inside_function_still_exits_program_in_v3() {
7049        let code = r#"@settings(kclVersion = "3.0-preview")
7050fn f() {
7051  exit()
7052  return 1
7053}
7054x = f()
7055assert(1, isEqualTo = 2, error = "code after exit ran")
7056"#;
7057        parse_execute(code).await.unwrap();
7058    }
7059
7060    #[tokio::test(flavor = "multi_thread")]
7061    async fn return_inside_sketch_block_terminates_function_in_v3() {
7062        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7063fn f() {
7064  sketch(on = XY) {
7065    l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7066    return 42
7067  }
7068  return 0
7069}
7070x = f()
7071"#;
7072        let result = parse_execute(code).await.unwrap();
7073        assert_eq!(variable_f64(&result, "x"), 42.0);
7074    }
7075
7076    #[tokio::test(flavor = "multi_thread")]
7077    async fn return_inside_sketch_block_ignored_without_v3() {
7078        // Pins the pre-KCL-3.0 behavior: `__return` binds in the sketch block's
7079        // child environment and is lost when it pops.
7080        let code = r#"@settings(experimentalFeatures = allow)
7081fn f() {
7082  sketch(on = XY) {
7083    l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7084    return 42
7085  }
7086  return 0
7087}
7088x = f()
7089"#;
7090        let result = parse_execute(code).await.unwrap();
7091        assert_eq!(variable_f64(&result, "x"), 0.0);
7092    }
7093
7094    #[tokio::test(flavor = "multi_thread")]
7095    async fn code_after_return_still_runs_without_v3() {
7096        let code = r#"fn f() {
7097  return 1
7098  assert(1, isEqualTo = 2, error = "ran past return")
7099}
7100x = f()
7101"#;
7102        let err = parse_execute(code).await.expect_err("should error");
7103        assert!(
7104            err.message().contains("ran past return"),
7105            "unexpected message: {}",
7106            err.message()
7107        );
7108    }
7109
7110    #[tokio::test(flavor = "multi_thread")]
7111    async fn multiple_returns_error_without_v3() {
7112        let code = r#"fn f() {
7113  return 1
7114  return 2
7115}
7116x = f()
7117"#;
7118        assert_eq!(
7119            parse_execute(code).await.expect_err("should error").message(),
7120            "Multiple returns from a single function."
7121        );
7122    }
7123
7124    #[tokio::test(flavor = "multi_thread")]
7125    async fn if_arm_return_plus_function_return_errors_without_v3() {
7126        // Pins the pre-KCL-3.0 behavior: the if-arm's `return` writes
7127        // `__return` into the function's environment, so the function-level
7128        // `return` is a second return.
7129        let code = r#"fn f() {
7130  dummy = if true {
7131    return 1
7132    0
7133  } else {
7134    0
7135  }
7136  return 2
7137}
7138x = f()
7139"#;
7140        assert_eq!(
7141            parse_execute(code).await.expect_err("should error").message(),
7142            "Multiple returns from a single function."
7143        );
7144    }
7145
7146    #[tokio::test(flavor = "multi_thread")]
7147    async fn top_level_if_arm_return_ignored_without_v3() {
7148        // Pins the pre-KCL-3.0 behavior: the return silently binds
7149        // `__return` in the root environment and the arm yields its trailing
7150        // expression.
7151        let code = r#"x = if true {
7152  return 1
7153  0
7154} else {
7155  0
7156}
7157"#;
7158        let result = parse_execute(code).await.unwrap();
7159        assert_eq!(variable_f64(&result, "x"), 0.0);
7160        assert_eq!(variable_f64(&result, memory::RETURN_NAME), 1.0);
7161    }
7162
7163    /// Early return is gated on the entry point's kclVersion, not the
7164    /// defining module's. A module cannot opt into KCL 3.0 on its own.
7165    #[tokio::test(flavor = "multi_thread")]
7166    async fn return_semantics_gated_on_entry_point_not_module() {
7167        // A 2.0 entry point importing a module that declares KCL 3.0 is
7168        // rejected as a version mismatch before the module runs, so its
7169        // function never gets to observe either return semantics.
7170        let dep = r#"@settings(kclVersion = "3.0-preview")
7171export fn f() {
7172  return 1
7173  assert(1, isEqualTo = 2, error = "ran past return")
7174}
7175"#;
7176        let main = r#"@settings(kclVersion = 2.0)
7177import f from "dep.kcl"
7178x = f()
7179"#;
7180        let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
7181        assert!(
7182            err.message()
7183                .starts_with("Mixing KCL versions in a single program is not allowed."),
7184            "unexpected message: {}",
7185            err.message()
7186        );
7187
7188        // A KCL 3.0 entry point applies early return everywhere, including
7189        // inside an imported module that declares no kclVersion (1.0 under the
7190        // legacy lookup). Declaring 2.0 there is rejected as a version mismatch
7191        // instead.
7192        let dep = r#"export fn f() {
7193  return 1
7194  assert(1, isEqualTo = 2, error = "ran past return")
7195}
7196"#;
7197        let main = r#"@settings(kclVersion = "3.0-preview")
7198import f from "dep.kcl"
7199x = f()
7200"#;
7201        let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
7202        assert_eq!(variable_f64(&result, "x"), 1.0);
7203    }
7204
7205    /// Early return inside a callback driven by a builtin terminates only
7206    /// that callback invocation; the builtin keeps iterating. On the machine
7207    /// executor, map/reduce callbacks run behind a Callback-completion call
7208    /// boundary, so this exercises unwind_return's resume-the-builtin path,
7209    /// unlike a directly called function.
7210    #[tokio::test(flavor = "multi_thread")]
7211    async fn return_inside_map_and_reduce_callbacks_in_v3() {
7212        let code = r#"@settings(kclVersion = "3.0-preview")
7213doubled = map([1, 2, 3], f = fn(@i) {
7214  return i * 2
7215  assert(1, isEqualTo = 2, error = "code after return ran in the map callback")
7216})
7217assert(doubled[0], isEqualTo = 2, error = "map result 0")
7218assert(doubled[1], isEqualTo = 4, error = "map result 1")
7219assert(doubled[2], isEqualTo = 6, error = "map result 2")
7220
7221total = reduce([1, 2, 3], initial = 0, f = fn(@i, accum) {
7222  return accum + i
7223  assert(1, isEqualTo = 2, error = "code after return ran in the reduce callback")
7224})
7225assert(total, isEqualTo = 6, error = "reduce total")
7226"#;
7227        let result = parse_execute(code).await.unwrap();
7228        assert_eq!(variable_f64(&result, "total"), 6.0);
7229    }
7230
7231    /// unwind_return must decrement the machine call depth like a normal
7232    /// call completion; otherwise sequential early-return calls would
7233    /// accumulate depth until the runaway guard trips.
7234    #[tokio::test(flavor = "multi_thread")]
7235    async fn early_returns_do_not_leak_machine_call_depth() {
7236        let code = r#"@settings(kclVersion = "3.0-preview")
7237fn one() {
7238  return 1
7239  assert(1, isEqualTo = 2, error = "code after return ran")
7240}
7241total = reduce([1..100], initial = 0, f = fn(@i, accum) {
7242  return accum + one()
7243})
7244assert(total, isEqualTo = 100, error = "each call returns 1")
7245"#;
7246        let result = parse_execute(code).await.unwrap();
7247        // Real nesting here is a few levels (reduce callback then one()).
7248        // If early returns leaked a level per call, the 100 sequential
7249        // calls would push the high water toward 100.
7250        let high_water = result.exec_state.global.machine_depth_high_water;
7251        assert!(high_water < 10, "high water: {high_water}");
7252    }
7253
7254    /// A return escaping to the top level of an imported module is rejected
7255    /// under a KCL 3.0 entry point. The entry module's version governs, so
7256    /// the imported module, which declares no kclVersion (1.0 under the
7257    /// legacy lookup), doesn't opt back out. Declaring 2.0 there would be
7258    /// rejected as a version mismatch instead. (Without a KCL 3.0 entry point
7259    /// the return is silently ignored; see
7260    /// top_level_if_arm_return_ignored_without_v3.)
7261    #[tokio::test(flavor = "multi_thread")]
7262    async fn top_level_if_arm_return_in_imported_module_errors_in_v3() {
7263        let dep = r#"x = if true {
7264  return 1
7265  0
7266} else {
7267  0
7268}
7269export y = x
7270"#;
7271        let main = r#"@settings(kclVersion = "3.0-preview")
7272import y from "dep.kcl"
7273z = y
7274"#;
7275        let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
7276        assert!(
7277            err.message().contains("Cannot return from outside a function."),
7278            "unexpected message: {}",
7279            err.message()
7280        );
7281    }
7282
7283    /// exit() in a return's argument still exits the whole program: the
7284    /// Exit control flow from evaluating the argument takes precedence over
7285    /// turning the statement into an early return. If it were mistakenly
7286    /// treated as the function's return value, execution would continue
7287    /// after the call and hit the failing assert.
7288    #[tokio::test(flavor = "multi_thread")]
7289    async fn return_of_exit_still_exits_program_in_v3() {
7290        let code = r#"@settings(kclVersion = "3.0-preview")
7291fn f() {
7292  return exit()
7293}
7294x = f()
7295assert(1, isEqualTo = 2, error = "code after exit ran")
7296"#;
7297        parse_execute(code).await.unwrap();
7298    }
7299
7300    #[tokio::test(flavor = "multi_thread")]
7301    async fn if_arm_bindings_do_not_leak_in_v3() {
7302        let code = r#"@settings(kclVersion = "3.0-preview")
7303x = if true {
7304  y = 1
7305  y
7306} else {
7307  0
7308}
7309z = y
7310"#;
7311        let err = parse_execute(code).await.expect_err("should error");
7312        assert!(
7313            err.message().contains("`y` is not defined"),
7314            "unexpected message: {}",
7315            err.message()
7316        );
7317    }
7318
7319    #[tokio::test(flavor = "multi_thread")]
7320    async fn if_arm_bindings_leak_without_v3() {
7321        // Pins the pre-KCL-3.0 behavior: arm bodies share the enclosing
7322        // environment, so arm bindings are visible after the if.
7323        for header in ["", "@settings(kclVersion = 2.0)\n"] {
7324            let code = format!(
7325                r#"{header}x = if true {{
7326  y = 1
7327  y
7328}} else {{
7329  0
7330}}
7331z = y
7332"#
7333            );
7334            let result = parse_execute(&code).await.unwrap();
7335            assert_eq!(variable_f64(&result, "z"), 1.0);
7336        }
7337    }
7338
7339    #[tokio::test(flavor = "multi_thread")]
7340    async fn if_arm_shadowing_allowed_in_v3() {
7341        let code = r#"@settings(kclVersion = "3.0-preview")
7342y = 1
7343x = if true {
7344  y = 2
7345  y + 10
7346} else {
7347  0
7348}
7349"#;
7350        let result = parse_execute(code).await.unwrap();
7351        assert_eq!(variable_f64(&result, "x"), 12.0);
7352        assert_eq!(variable_f64(&result, "y"), 1.0);
7353    }
7354
7355    #[tokio::test(flavor = "multi_thread")]
7356    async fn if_arm_shadowing_still_errors_without_v3() {
7357        // Pins the pre-KCL-3.0 behavior: the arm shares the enclosing
7358        // environment, so redeclaring an outer name is an error.
7359        for header in ["", "@settings(kclVersion = 2.0)\n"] {
7360            let code = format!(
7361                r#"{header}y = 1
7362x = if true {{
7363  y = 2
7364  y
7365}} else {{
7366  0
7367}}
7368"#
7369            );
7370            let err = parse_execute(&code).await.expect_err("should error");
7371            assert!(
7372                err.message().contains("Cannot redefine `y`"),
7373                "unexpected message: {}",
7374                err.message()
7375            );
7376        }
7377    }
7378
7379    #[tokio::test(flavor = "multi_thread")]
7380    async fn if_arm_closure_escape_in_v3() {
7381        // A closure declared in an arm captures arm-locals and stays valid
7382        // after the arm's scope is popped.
7383        let code = r#"@settings(kclVersion = "3.0-preview")
7384n = 1
7385f = if true {
7386  m = 41
7387  g = fn() {
7388    return m + n
7389  }
7390  g
7391} else {
7392  g = fn() {
7393    return 0
7394  }
7395  g
7396}
7397x = f()
7398"#;
7399        let result = parse_execute(code).await.unwrap();
7400        assert_eq!(variable_f64(&result, "x"), 42.0);
7401    }
7402
7403    #[tokio::test(flavor = "multi_thread")]
7404    async fn recursive_if_arm_closure_keeps_enclosing_function_frame_alive_in_v3() {
7405        // A named recursive closure takes a different snapshot path from an
7406        // anonymous closure. Escaping through an arm must retain both the arm
7407        // and its enclosing call frame.
7408        let code = r#"@settings(kclVersion = "3.0-preview")
7409fn makeCounter() {
7410  outer = 40
7411  selected = if true {
7412    inner = 2
7413    fn count(@n) {
7414      return if n == 0 {
7415        outer + inner
7416      } else {
7417        count(n - 1) + 1
7418      }
7419    }
7420    count
7421  } else {
7422    fn fallback(@n) {
7423      return n
7424    }
7425    fallback
7426  }
7427  return selected
7428}
7429counter = makeCounter()
7430x = counter(3)
7431"#;
7432        let result = parse_execute(code).await.unwrap();
7433        assert_eq!(variable_f64(&result, "x"), 45.0);
7434    }
7435
7436    #[tokio::test(flavor = "multi_thread")]
7437    async fn return_inside_scoped_if_arm_in_v3() {
7438        // Early return from inside a scoped arm pops the arm environment on
7439        // the way out.
7440        let code = r#"@settings(kclVersion = "3.0-preview")
7441fn f(@b) {
7442  local = if b {
7443    w = 1
7444    return w + 9
7445    0
7446  } else {
7447    0
7448  }
7449  return local
7450}
7451x = f(true)
7452y = f(false)
7453"#;
7454        let result = parse_execute(code).await.unwrap();
7455        assert_eq!(variable_f64(&result, "x"), 10.0);
7456        assert_eq!(variable_f64(&result, "y"), 0.0);
7457    }
7458
7459    #[tokio::test(flavor = "multi_thread")]
7460    async fn else_if_and_nested_if_scoping_in_v3() {
7461        let code = r#"@settings(kclVersion = "3.0-preview")
7462x = if false {
7463  0
7464} else if true {
7465  a = 1
7466  b = if true {
7467    c = 2
7468    a + c
7469  } else {
7470    0
7471  }
7472  a + b
7473} else {
7474  0
7475}
7476"#;
7477        let result = parse_execute(code).await.unwrap();
7478        assert_eq!(variable_f64(&result, "x"), 4.0);
7479
7480        // A nested arm's binding is not visible in the enclosing arm.
7481        let code = r#"@settings(kclVersion = "3.0-preview")
7482x = if true {
7483  b = if true {
7484    c = 2
7485    c
7486  } else {
7487    0
7488  }
7489  b + c
7490} else {
7491  0
7492}
7493"#;
7494        let err = parse_execute(code).await.expect_err("should error");
7495        assert!(
7496            err.message().contains("`c` is not defined"),
7497            "unexpected message: {}",
7498            err.message()
7499        );
7500    }
7501
7502    /// Else-if and final-else arms are isolated exactly like then-arms:
7503    /// their bindings are invisible after the if, and they may shadow outer
7504    /// bindings without changing them. Pinned per arm kind so a refactor of
7505    /// the shared arm dispatch can't silently drop one.
7506    #[tokio::test(flavor = "multi_thread")]
7507    async fn else_if_and_final_else_arms_are_isolated_in_v3() {
7508        // A taken else-if arm's binding doesn't leak.
7509        let code = r#"@settings(kclVersion = "3.0-preview")
7510x = if false {
7511  0
7512} else if true {
7513  y = 1
7514  y
7515} else {
7516  0
7517}
7518z = y
7519"#;
7520        let err = parse_execute(code).await.expect_err("should error");
7521        assert!(
7522            err.message().contains("`y` is not defined"),
7523            "unexpected message: {}",
7524            err.message()
7525        );
7526
7527        // A taken final-else arm's binding doesn't leak.
7528        let code = r#"@settings(kclVersion = "3.0-preview")
7529x = if false {
7530  0
7531} else if false {
7532  0
7533} else {
7534  y = 1
7535  y
7536}
7537z = y
7538"#;
7539        let err = parse_execute(code).await.expect_err("should error");
7540        assert!(
7541            err.message().contains("`y` is not defined"),
7542            "unexpected message: {}",
7543            err.message()
7544        );
7545
7546        // A taken else-if arm can shadow an outer binding without changing it.
7547        let code = r#"@settings(kclVersion = "3.0-preview")
7548outer = 1
7549x = if false {
7550  0
7551} else if true {
7552  outer = 2
7553  outer + 10
7554} else {
7555  0
7556}
7557"#;
7558        let result = parse_execute(code).await.unwrap();
7559        assert_eq!(variable_f64(&result, "x"), 12.0);
7560        assert_eq!(variable_f64(&result, "outer"), 1.0);
7561
7562        // Same from the final-else arm.
7563        let code = r#"@settings(kclVersion = "3.0-preview")
7564outer = 1
7565x = if false {
7566  0
7567} else if false {
7568  0
7569} else {
7570  outer = 2
7571  outer + 10
7572}
7573"#;
7574        let result = parse_execute(code).await.unwrap();
7575        assert_eq!(variable_f64(&result, "x"), 12.0);
7576        assert_eq!(variable_f64(&result, "outer"), 1.0);
7577    }
7578
7579    /// Pins the pre-KCL-3.0 behavior for else-if and final-else arms: their
7580    /// bindings leak into the enclosing environment, and shadowing an outer
7581    /// name is a redefinition error, matching then-arms.
7582    #[tokio::test(flavor = "multi_thread")]
7583    async fn else_if_and_final_else_arm_bindings_leak_without_v3() {
7584        for header in ["", "@settings(kclVersion = 2.0)\n"] {
7585            let code = format!(
7586                r#"{header}x = if false {{
7587  0
7588}} else if true {{
7589  y = 1
7590  y
7591}} else {{
7592  0
7593}}
7594z = y
7595"#
7596            );
7597            let result = parse_execute(&code).await.unwrap();
7598            assert_eq!(variable_f64(&result, "z"), 1.0, "code={code}");
7599
7600            let code = format!(
7601                r#"{header}x = if false {{
7602  0
7603}} else if false {{
7604  0
7605}} else {{
7606  y = 1
7607  y
7608}}
7609z = y
7610"#
7611            );
7612            let result = parse_execute(&code).await.unwrap();
7613            assert_eq!(variable_f64(&result, "z"), 1.0, "code={code}");
7614
7615            let code = format!(
7616                r#"{header}outer = 1
7617x = if false {{
7618  0
7619}} else if true {{
7620  outer = 2
7621  outer
7622}} else {{
7623  0
7624}}
7625"#
7626            );
7627            let err = parse_execute(&code).await.expect_err("should error");
7628            assert!(
7629                err.message().contains("Cannot redefine `outer`"),
7630                "unexpected message: {}",
7631                err.message()
7632            );
7633        }
7634    }
7635
7636    #[tokio::test(flavor = "multi_thread")]
7637    async fn error_inside_if_arm_unwinds_balanced_in_v3() {
7638        // The user's error surfaces (not an internal environment-imbalance
7639        // error).
7640        let code = r#"@settings(kclVersion = "3.0-preview")
7641fn f() {
7642  dummy = if true {
7643    assert(1, isEqualTo = 2, error = "boom")
7644    0
7645  } else {
7646    0
7647  }
7648  return dummy
7649}
7650x = f()
7651"#;
7652        let err = parse_execute(code).await.expect_err("should error");
7653        assert!(err.message().contains("boom"), "unexpected message: {}", err.message());
7654    }
7655
7656    #[tokio::test(flavor = "multi_thread")]
7657    async fn exit_inside_scoped_if_arm_in_v3() {
7658        let code = r#"@settings(kclVersion = "3.0-preview")
7659fn f() {
7660  dummy = if true {
7661    exit()
7662    0
7663  } else {
7664    0
7665  }
7666  return dummy
7667}
7668x = f()
7669assert(1, isEqualTo = 2, error = "code after exit ran")
7670"#;
7671        parse_execute(code).await.unwrap();
7672    }
7673
7674    /// If-arm scoping is gated on the entry point's kclVersion, not the
7675    /// defining module's. A module cannot opt into KCL 3.0 on its own.
7676    #[tokio::test(flavor = "multi_thread")]
7677    async fn if_arm_scoping_gated_on_entry_point_not_module() {
7678        // A 2.0 entry point importing a module that declares KCL 3.0 is
7679        // rejected as a version mismatch before the module runs, so its arms
7680        // never get to leak or not leak.
7681        let dep = r#"@settings(kclVersion = "3.0-preview")
7682ignored = if true {
7683  leaked = 1
7684  leaked
7685} else {
7686  0
7687}
7688export leakCheck = leaked
7689"#;
7690        let main = r#"@settings(kclVersion = 2.0)
7691import leakCheck from "dep.kcl"
7692x = leakCheck
7693"#;
7694        let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
7695        assert!(
7696            err.message()
7697                .starts_with("Mixing KCL versions in a single program is not allowed."),
7698            "unexpected message: {}",
7699            err.message()
7700        );
7701
7702        // A KCL 3.0 entry point applies arm scoping everywhere, including
7703        // inside an imported module that declares no kclVersion (1.0 under the
7704        // legacy lookup). Declaring 2.0 there is rejected as a version mismatch
7705        // instead.
7706        let dep = r#"ignored = if true {
7707  arm = 1
7708  arm
7709} else {
7710  0
7711}
7712export fn leakCheck() {
7713  return arm
7714}
7715"#;
7716        let main = r#"@settings(kclVersion = "3.0-preview")
7717import leakCheck from "dep.kcl"
7718x = leakCheck()
7719"#;
7720        let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
7721        assert!(
7722            err.message().contains("`arm` is not defined"),
7723            "unexpected message: {}",
7724            err.message()
7725        );
7726    }
7727
7728    /// Unwinding out of a sketch block nested inside a scoped if-arm must
7729    /// run the sketch cleanup and then pop the arm's scope environment, in
7730    /// that order, on all three unwind paths: error, exit(), and early
7731    /// return.
7732    #[tokio::test(flavor = "multi_thread")]
7733    async fn unwind_through_sketch_block_inside_scoped_if_arm_in_v3() {
7734        // Error: the user's error surfaces, not an internal
7735        // environment-imbalance error.
7736        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7737fn f() {
7738  dummy = if true {
7739    s = sketch(on = XY) {
7740      l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7741      q = notDefinedAnywhere
7742    }
7743    0
7744  } else {
7745    0
7746  }
7747  return dummy
7748}
7749x = f()
7750"#;
7751        let err = parse_execute(code).await.unwrap_err();
7752        assert!(
7753            err.message().contains("`notDefinedAnywhere` is not defined"),
7754            "unexpected message: {}",
7755            err.message()
7756        );
7757
7758        // exit() terminates the program; nothing after it runs.
7759        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7760fn f() {
7761  dummy = if true {
7762    s = sketch(on = XY) {
7763      l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7764      e = exit()
7765    }
7766    0
7767  } else {
7768    0
7769  }
7770  return dummy
7771}
7772x = f()
7773assert(1, isEqualTo = 2, error = "code after exit ran")
7774"#;
7775        parse_execute(code).await.unwrap();
7776
7777        // Early return terminates the enclosing function with its value.
7778        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7779fn g() {
7780  dummy = if true {
7781    s = sketch(on = XY) {
7782      l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7783      return 42
7784    }
7785    0
7786  } else {
7787    0
7788  }
7789  return 0
7790}
7791y = g()
7792"#;
7793        let result = parse_execute(code).await.unwrap();
7794        assert_eq!(variable_f64(&result, "y"), 42.0);
7795    }
7796
7797    /// A tag declared inside an if-arm is bound like any arm-local: usable
7798    /// within its arm, and under KCL 3.0 not visible after the if. Without
7799    /// KCL 3.0 it leaks like other arm bindings.
7800    #[tokio::test(flavor = "multi_thread")]
7801    async fn tag_declared_inside_if_arm_is_arm_local_in_v3() {
7802        let arm_body = r#"p = if true {
7803  profile = startSketchOn(XY)
7804    |> startProfile(at = [0, 0])
7805    |> line(end = [10, 0], tag = $edge)
7806    |> line(end = [0, 10])
7807    |> line(end = [-10, 0])
7808    |> close()
7809  inArmLen = segLen(edge)
7810  assert(inArmLen, isEqualTo = 10, error = "tag is usable within its arm")
7811  profile
7812} else {
7813  startSketchOn(XY)
7814    |> startProfile(at = [0, 0])
7815    |> line(end = [5, 0])
7816    |> line(end = [0, 5])
7817    |> line(end = [-5, 0])
7818    |> close()
7819}
7820len = segLen(edge)
7821"#;
7822
7823        let code = format!("@settings(kclVersion = \"3.0-preview\")\n{arm_body}");
7824        let err = parse_execute(&code).await.unwrap_err();
7825        assert!(
7826            err.message().contains("`edge` is not defined"),
7827            "unexpected message: {}",
7828            err.message()
7829        );
7830
7831        // Pins the pre-KCL-3.0 behavior: the tag leaks out of the arm.
7832        let result = parse_execute(arm_body).await.unwrap();
7833        assert_eq!(variable_f64(&result, "len"), 10.0);
7834    }
7835
7836    /// Repeated calls to a function whose body evaluates an if-expression must
7837    /// not accumulate retained call frames when nothing escapes the arms: the
7838    /// arm's scope environment defers pinning its parent until it is itself
7839    /// referenced. Before deferred pinning, each of the 100 calls below
7840    /// permanently retained its frame.
7841    #[tokio::test(flavor = "multi_thread")]
7842    async fn if_arm_scopes_do_not_retain_function_frames_in_v3() {
7843        let code = r#"@settings(kclVersion = "3.0-preview")
7844fn pick(@i) {
7845  r = if i > 50 {
7846    a = i * 2
7847    a
7848  } else {
7849    b = i + 1
7850    b
7851  }
7852  return r
7853}
7854results = map([1..100], f = fn(@i) { return pick(i) })
7855assert(results[0], isEqualTo = 2, error = "pick(1) = 2")
7856assert(results[99], isEqualTo = 200, error = "pick(100) = 200")
7857"#;
7858        let result = parse_execute(code).await.unwrap();
7859        // Long-lived environments (std prelude modules, the root env, ...) are
7860        // a small constant independent of the call count.
7861        let retained = result.exec_state.stack().memory.envs_with_bindings();
7862        assert!(retained < 20, "retained environments: {retained}");
7863    }
7864
7865    /// An if-expression used as a pipe element gets arm scoping without
7866    /// disturbing the ambient pipe value: the arm's result feeds the next
7867    /// element's `%` (the parser doesn't accept `%` anywhere inside the if
7868    /// element itself), and arm-locals don't leak.
7869    #[tokio::test(flavor = "multi_thread")]
7870    async fn if_arm_scoping_inside_pipe_in_v3() {
7871        let code = r#"@settings(kclVersion = "3.0-preview")
7872cond = true
7873result = 5
7874  |> if cond {
7875    a = 20
7876    a
7877  } else {
7878    0
7879  }
7880  |> max([%, 1])
7881"#;
7882        let result = parse_execute(code).await.unwrap();
7883        // The then-arm's 20 must flow through the pipe into max's `%`. If
7884        // the arm's scope push/pop corrupted the ambient pipe value, this
7885        // would not be 20.
7886        assert_eq!(variable_f64(&result, "result"), 20.0);
7887
7888        // Arm-locals of a pipe element are invisible after the pipe.
7889        let code = r#"@settings(kclVersion = "3.0-preview")
7890cond = true
7891result = 5
7892  |> if cond {
7893    a = 20
7894    a
7895  } else {
7896    0
7897  }
7898leaked = a
7899"#;
7900        let err = parse_execute(code).await.unwrap_err();
7901        assert!(
7902            err.message().contains("`a` is not defined"),
7903            "unexpected message: {}",
7904            err.message()
7905        );
7906    }
7907
7908    #[tokio::test(flavor = "multi_thread")]
7909    async fn member_expression_evaluates_object_before_property_in_v3() {
7910        // Both operands are undefined, so the error names whichever one is
7911        // evaluated first. KCL 3.0 evaluates in source order: `a` before `b`.
7912        let code = r#"@settings(kclVersion = "3.0-preview")
7913x = a[b]
7914"#;
7915        let err = parse_execute(code).await.expect_err("should error");
7916        assert_eq!(err.message(), "`a` is not defined");
7917    }
7918
7919    #[tokio::test(flavor = "multi_thread")]
7920    async fn member_expression_evaluates_property_before_object_without_v3() {
7921        // Pre-KCL-3.0 order, preserved for compatibility: the computed
7922        // property is evaluated before the object.
7923        let code = r#"@settings(kclVersion = 2.0)
7924x = a[b]
7925"#;
7926        let err = parse_execute(code).await.expect_err("should error");
7927        assert_eq!(err.message(), "`b` is not defined");
7928    }
7929
7930    #[tokio::test(flavor = "multi_thread")]
7931    async fn member_expression_undefined_object_with_static_property_in_v3() {
7932        let code = r#"@settings(kclVersion = "3.0-preview")
7933x = a.b
7934"#;
7935        let err = parse_execute(code).await.expect_err("should error");
7936        assert_eq!(err.message(), "`a` is not defined");
7937    }
7938
7939    #[tokio::test(flavor = "multi_thread")]
7940    async fn member_expression_values_in_v3() {
7941        // The source-order path handles computed, non-computed, chained, and
7942        // call-result access.
7943        let code = r#"@settings(kclVersion = "3.0-preview")
7944fn xs() {
7945  return [10, 20, 30]
7946}
7947fn one() {
7948  return 1
7949}
7950obj = { inner = { xs = xs() } }
7951objs = [obj, obj]
7952a = obj.inner.xs[one()]
7953b = xs()[one() + 1]
7954c = objs[0].inner.xs[0]
7955"#;
7956        let result = parse_execute(code).await.unwrap();
7957        assert_eq!(variable_f64(&result, "a"), 20.0);
7958        assert_eq!(variable_f64(&result, "b"), 30.0);
7959        assert_eq!(variable_f64(&result, "c"), 10.0);
7960    }
7961
7962    #[tokio::test(flavor = "multi_thread")]
7963    async fn exit_inside_member_expression_in_v3() {
7964        // exit() propagates out of either half of a member expression and
7965        // terminates the program before the assert runs.
7966        for code in [
7967            r#"@settings(kclVersion = "3.0-preview")
7968x = exit()[0]
7969assert(1, isEqualTo = 2, error = "code after exit ran")
7970"#,
7971            r#"@settings(kclVersion = "3.0-preview")
7972arr = [1]
7973x = arr[exit()]
7974assert(1, isEqualTo = 2, error = "code after exit ran")
7975"#,
7976        ] {
7977            parse_execute(code).await.unwrap();
7978        }
7979    }
7980
7981    #[tokio::test(flavor = "multi_thread")]
7982    async fn experimental_parameter() {
7983        let code = r#"
7984fn inc(@x, @(experimental = true) amount? = 1) {
7985  return x + amount
7986}
7987
7988answer = inc(5, amount = 2)
7989"#;
7990        let result = parse_execute(code).await.unwrap();
7991        let issues = result.exec_state.issues();
7992        assert_eq!(issues.len(), 1);
7993        assert_eq!(issues[0].severity, Severity::Error);
7994        let msg = &issues[0].message;
7995        assert!(msg.contains("experimental"), "found {msg}");
7996
7997        // If the parameter isn't used, there's no warning.
7998        let code = r#"
7999fn inc(@x, @(experimental = true) amount? = 1) {
8000  return x + amount
8001}
8002
8003answer = inc(5)
8004"#;
8005        let result = parse_execute(code).await.unwrap();
8006        let issues = result.exec_state.issues();
8007        assert!(issues.is_empty(), "issues={issues:#?}");
8008    }
8009
8010    #[tokio::test(flavor = "multi_thread")]
8011    async fn experimental_scalar_fixed_constraint() {
8012        let code_left = r#"@settings(experimentalFeatures = warn)
8013sketch(on = XY) {
8014  point1 = point(at = [var 0mm, var 0mm])
8015  point1.at[0] == 1mm
8016}
8017"#;
8018        // It's symmetric. Flipping the binary operator has the same behavior.
8019        let code_right = r#"@settings(experimentalFeatures = warn)
8020sketch(on = XY) {
8021  point1 = point(at = [var 0mm, var 0mm])
8022  1mm == point1.at[0]
8023}
8024"#;
8025
8026        for code in [code_left, code_right] {
8027            let result = parse_execute(code).await.unwrap();
8028            let issues = result.exec_state.issues();
8029            let Some(error) = issues
8030                .iter()
8031                .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
8032            else {
8033                panic!("found {issues:#?}");
8034            };
8035            assert_eq!(error.severity, Severity::Warning);
8036        }
8037    }
8038
8039    // START Mock Execution tests
8040    // Ideally, we would do this as part of all sim tests and delete these one-off tests.
8041
8042    #[tokio::test(flavor = "multi_thread")]
8043    async fn test_tangent_line_arc_executes_with_mock_engine() {
8044        let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
8045        parse_execute(&code).await.unwrap();
8046    }
8047
8048    #[tokio::test(flavor = "multi_thread")]
8049    async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
8050        let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
8051        parse_execute(&code).await.unwrap();
8052    }
8053
8054    #[tokio::test(flavor = "multi_thread")]
8055    async fn test_tangent_line_circle_executes_with_mock_engine() {
8056        let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
8057        parse_execute(&code).await.unwrap();
8058    }
8059
8060    #[tokio::test(flavor = "multi_thread")]
8061    async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
8062        let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
8063        parse_execute(&code).await.unwrap();
8064    }
8065
8066    #[tokio::test(flavor = "multi_thread")]
8067    async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
8068        let code = r#"startX = 2
8069
8070baseSketch = sketch(on = XY) {
8071  yoyo = line(start = [startX, 0], end = [7, 6])
8072  line2 = line(start = [7, 6], end = [7, 12])
8073  hi = line(start = [7, 12], end = [startX, 0])
8074}
8075
8076baseRegion = region(point = [5.5, 6], sketch = baseSketch)
8077myExtrude = extrude(
8078  baseRegion,
8079  length = 5,
8080  tagEnd = $endCap,
8081  tagStart = $startCap,
8082)
8083yodawg = getCommonEdge(faces = [
8084  baseRegion.tags.hi,
8085  baseRegion.tags.yoyo
8086])
8087
8088cutSketch = sketch(on = YZ) {
8089  myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
8090  myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
8091  line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
8092}
8093
8094cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
8095extrude001 = extrude(cutRegion, length = 5)
8096solid001 = subtract(myExtrude, tools = extrude001)
8097
8098yoyo = getOppositeEdge(baseRegion.tags.hi)
8099fillet(solid001, radius = 0.1, tags = yoyo)
8100"#;
8101
8102        parse_execute(code).await.unwrap();
8103    }
8104
8105    // END Mock Execution tests
8106
8107    // Sketch constraint report tests
8108
8109    async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
8110        let program = crate::Program::parse_no_errs(kcl).unwrap();
8111        let ctx = ExecutorContext::new_geometry_only_with_version(program.language_version().unwrap())
8112            .await
8113            .unwrap();
8114        let mut exec_state = ExecState::new(&ctx);
8115        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
8116        let outcome = exec_state
8117            .into_exec_outcome(env_ref, &ctx)
8118            .await
8119            .expect("constraint report test outcome should collect variables");
8120        let report = outcome.sketch_constraint_report();
8121        ctx.close().await;
8122        report
8123    }
8124
8125    #[tokio::test(flavor = "multi_thread")]
8126    async fn warn_when_sketch_is_over_constrained() {
8127        let code = r#"
8128sketch001 = sketch(on = XY) {
8129  line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
8130  fixed([line1.start, ORIGIN])
8131  fixed([line1.start, [20, 20]])
8132}
8133"#;
8134        let result = parse_execute(code).await.unwrap();
8135        let issues = result.exec_state.issues();
8136        let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
8137            panic!("expected over-constrained warning; found {issues:#?}");
8138        };
8139        assert_eq!(warning.severity, Severity::Warning);
8140    }
8141
8142    #[tokio::test(flavor = "multi_thread")]
8143    async fn over_constrained_warning_identifies_signed_vertical_distance_direction() {
8144        let code = r#"
8145sketch001 = sketch(on = XY) {
8146  line1 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
8147  fixed([line1.start, [0mm, 10mm]])
8148  fixed([line1.end, ORIGIN])
8149  verticalDistance([line1.start, line1.end]) == 10mm
8150}
8151"#;
8152        let result = parse_execute(code).await.unwrap();
8153        let issues = result.exec_state.issues();
8154        let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
8155            panic!("expected over-constrained warning; found {issues:#?}");
8156        };
8157        assert!(
8158            warning.message.contains(
8159                "Unsatisfied signed verticalDistance constraint: a positive right-hand side requires the second point to be above the first"
8160            ),
8161            "expected signed-direction diagnostic; found {warning:#?}"
8162        );
8163    }
8164
8165    #[tokio::test(flavor = "multi_thread")]
8166    async fn no_warning_when_sketch_is_not_over_constrained() {
8167        // Under-constrained sketch should not emit the over-constrained warning.
8168        let code = r#"
8169sketch001 = sketch(on = XY) {
8170  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8171}
8172"#;
8173        let result = parse_execute(code).await.unwrap();
8174        let issues = result.exec_state.issues();
8175        assert!(
8176            !issues.iter().any(|issue| issue.message.contains("over-constrained")),
8177            "did not expect over-constrained warning; found {issues:#?}"
8178        );
8179    }
8180
8181    #[tokio::test(flavor = "multi_thread")]
8182    async fn test_constraint_report_fully_constrained() {
8183        // All points are fully constrained via equality constraints.
8184        let kcl = r#"
8185@settings(experimentalFeatures = allow)
8186
8187sketch(on = YZ) {
8188  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
8189  line1.start.at[0] == 2
8190  line1.start.at[1] == 8
8191  line1.end.at[0] == 5
8192  line1.end.at[1] == 7
8193}
8194"#;
8195        let report = run_constraint_report(kcl).await;
8196        assert_eq!(report.fully_constrained.len(), 1);
8197        assert_eq!(report.under_constrained.len(), 0);
8198        assert_eq!(report.over_constrained.len(), 0);
8199        assert_eq!(report.errors.len(), 0);
8200        assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
8201    }
8202
8203    #[tokio::test(flavor = "multi_thread")]
8204    async fn test_constraint_report_under_constrained() {
8205        // No constraints at all — all points are free.
8206        let kcl = r#"
8207sketch(on = YZ) {
8208  line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
8209}
8210"#;
8211        let report = run_constraint_report(kcl).await;
8212        assert_eq!(report.fully_constrained.len(), 0);
8213        assert_eq!(report.under_constrained.len(), 1);
8214        assert_eq!(report.over_constrained.len(), 0);
8215        assert_eq!(report.errors.len(), 0);
8216        assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
8217        assert!(report.under_constrained[0].free_count > 0);
8218    }
8219
8220    #[tokio::test(flavor = "multi_thread")]
8221    async fn test_constraint_report_over_constrained() {
8222        // Conflicting distance constraints on the same pair of points.
8223        let kcl = r#"
8224@settings(experimentalFeatures = allow)
8225
8226sketch(on = YZ) {
8227  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
8228  line1.start.at[0] == 2
8229  line1.start.at[1] == 8
8230  line1.end.at[0] == 5
8231  line1.end.at[1] == 7
8232  distance([line1.start, line1.end]) == 100mm
8233}
8234"#;
8235        let report = run_constraint_report(kcl).await;
8236        assert_eq!(report.over_constrained.len(), 1);
8237        assert_eq!(report.errors.len(), 0);
8238        assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
8239        assert!(report.over_constrained[0].conflict_count > 0);
8240    }
8241
8242    #[tokio::test(flavor = "multi_thread")]
8243    async fn test_constraint_report_multiple_sketches() {
8244        // Two sketches: one fully constrained, one under-constrained.
8245        let kcl = r#"
8246@settings(experimentalFeatures = allow)
8247
8248s1 = sketch(on = YZ) {
8249  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
8250  line1.start.at[0] == 2
8251  line1.start.at[1] == 8
8252  line1.end.at[0] == 5
8253  line1.end.at[1] == 7
8254}
8255
8256s2 = sketch(on = XZ) {
8257  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8258}
8259"#;
8260        let report = run_constraint_report(kcl).await;
8261        assert_eq!(
8262            report.fully_constrained.len()
8263                + report.under_constrained.len()
8264                + report.over_constrained.len()
8265                + report.errors.len(),
8266            2,
8267            "Expected 2 sketches total"
8268        );
8269        assert_eq!(report.fully_constrained.len(), 1);
8270        assert_eq!(report.under_constrained.len(), 1);
8271    }
8272
8273    #[tokio::test(flavor = "multi_thread")]
8274    async fn test_constraint_report_reports_sketch_names() {
8275        // One file holding a fully constrained, an under-constrained, and an
8276        // over-constrained sketch. Every entry carries the name of the
8277        // variable its sketch was assigned to, so a caller can say which
8278        // sketch needs correcting.
8279        let kcl = r#"
8280@settings(experimentalFeatures = allow)
8281
8282fixedSketch = sketch(on = YZ) {
8283  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
8284  line1.start.at[0] == 2
8285  line1.start.at[1] == 8
8286  line1.end.at[0] == 5
8287  line1.end.at[1] == 7
8288}
8289
8290looseSketch = sketch(on = XZ) {
8291  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8292}
8293
8294conflictSketch = sketch(on = XY) {
8295  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
8296  line1.start.at[0] == 2
8297  line1.start.at[1] == 8
8298  line1.end.at[0] == 5
8299  line1.end.at[1] == 7
8300  distance([line1.start, line1.end]) == 100mm
8301}
8302"#;
8303        let report = run_constraint_report(kcl).await;
8304        assert_eq!(report.errors.len(), 0);
8305        assert_eq!(report.fully_constrained.len(), 1);
8306        assert_eq!(report.under_constrained.len(), 1);
8307        assert_eq!(report.over_constrained.len(), 1);
8308        assert_eq!(report.fully_constrained[0].name, "fixedSketch");
8309        assert_eq!(report.under_constrained[0].name, "looseSketch");
8310        assert_eq!(report.over_constrained[0].name, "conflictSketch");
8311    }
8312
8313    #[tokio::test(flavor = "multi_thread")]
8314    async fn test_constraint_report_name_empty_without_declaration() {
8315        // A sketch written as an expression statement has no enclosing
8316        // variable declaration, so there is no name to report. This pins the
8317        // documented limitation of SketchConstraintStatus::name.
8318        let kcl = r#"
8319sketch(on = YZ) {
8320  line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
8321}
8322"#;
8323        let report = run_constraint_report(kcl).await;
8324        assert_eq!(report.under_constrained.len(), 1);
8325        assert_eq!(report.under_constrained[0].name, "");
8326    }
8327
8328    #[tokio::test(flavor = "multi_thread")]
8329    async fn test_constraint_report_names_repeat_across_calls() {
8330        // Both sketches come from the same declaration inside the function
8331        // body, so both entries carry that declaration's name and the report
8332        // cannot tell them apart. This pins the documented limitation of
8333        // SketchConstraintStatus::name.
8334        let kcl = r#"
8335fn makeSketch() {
8336  inner = sketch(on = XY) {
8337    line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
8338  }
8339  return inner
8340}
8341
8342first = makeSketch()
8343second = makeSketch()
8344"#;
8345        let report = run_constraint_report(kcl).await;
8346        assert_eq!(report.under_constrained.len(), 2);
8347        assert_eq!(report.under_constrained[0].name, "inner");
8348        assert_eq!(report.under_constrained[1].name, "inner");
8349    }
8350
8351    #[tokio::test(flavor = "multi_thread")]
8352    async fn user_aliases_and_enums_require_v3_even_with_experimental_opt_in() {
8353        for version in ["1.0", "2.0"] {
8354            for opt_in in ["", ", experimentalFeatures = allow"] {
8355                for (feature, declaration) in [
8356                    ("Type aliases", "type Distance = number(mm)"),
8357                    ("Enum declarations", "type Color { | Red }"),
8358                ] {
8359                    let code = format!("@settings(kclVersion = \"{version}\"{opt_in})\n{declaration}\n");
8360                    assert_eq!(
8361                        parse_execute(&code).await.unwrap_err().message(),
8362                        format!("{feature} require KCL 3.0-preview, but this program uses KCL {version}."),
8363                        "code: {code}"
8364                    );
8365                }
8366            }
8367        }
8368    }
8369
8370    #[tokio::test(flavor = "multi_thread")]
8371    async fn user_aliases_and_enums_execute_in_v3_without_experimental_opt_in() {
8372        for code in [
8373            "@settings(kclVersion = \"3.0-preview\")\ntype Distance = number(mm)\nx = 1mm: Distance\n",
8374            "@settings(kclVersion = \"3.0-preview\")\ntype Color { | Red }\nx = Color::Red\n",
8375        ] {
8376            let result = parse_execute(code).await.unwrap();
8377            assert!(result.exec_state.issues().is_empty(), "code: {code}");
8378        }
8379    }
8380
8381    #[tokio::test(flavor = "multi_thread")]
8382    async fn standard_library_type_aliases_remain_available_before_v3() {
8383        for version in ["1.0", "2.0"] {
8384            let code = format!("@settings(kclVersion = \"{version}\")\nx = 1mm: mm\n");
8385            let result = parse_execute(&code).await.unwrap();
8386            assert!(result.exec_state.issues().is_empty(), "code: {code}");
8387        }
8388    }
8389
8390    #[tokio::test(flavor = "multi_thread")]
8391    async fn enum_declaration_registers_type() {
8392        // Plain and exported declarations both execute. Nothing references the
8393        // enum yet, so this only asserts that declaring one is no longer an
8394        // error; constructor use is exercised separately.
8395        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8396type Color { | Red | Green }
8397"#;
8398        parse_execute(code).await.unwrap();
8399
8400        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8401export type Color { | Red | Green }
8402"#;
8403        parse_execute(code).await.unwrap();
8404
8405        // A zero-variant enum is a valid declaration.
8406        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8407type Empty { | }
8408"#;
8409        parse_execute(code).await.unwrap();
8410    }
8411
8412    #[tokio::test(flavor = "multi_thread")]
8413    async fn enum_declaration_rejects_nested_scope() {
8414        // Identity is (module, declared name), so two same-named declarations in
8415        // one file would collide. The parser and formatter accept this shape, so
8416        // execution is the only thing that can reject it.
8417        //
8418        // The rule is about nesting, not about one kind of block, so all routes
8419        // to `BodyType::Block` are covered here.
8420        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8421        for (case, code) in [
8422            (
8423                "function body",
8424                format!("{allow}fn palette() {{\n  type Color {{ | Red }}\n  return 0\n}}\npalette()\n"),
8425            ),
8426            (
8427                "sketch block",
8428                format!(
8429                    "{allow}sketch(on = XY) {{\n  type Color {{ | Red }}\n  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
8430                ),
8431            ),
8432            (
8433                "if arm",
8434                format!("{allow}x = if true {{\n  type Color {{ | Red }}\n  0\n}} else {{\n  0\n}}\n"),
8435            ),
8436        ] {
8437            assert_eq!(
8438                parse_execute(&code).await.unwrap_err().message(),
8439                "Enum declarations are only supported at the top-level of a file. Move `type Color` to the top-level.",
8440                "case: {case}"
8441            );
8442        }
8443    }
8444
8445    #[tokio::test(flavor = "multi_thread")]
8446    async fn enum_alone_is_restricted_to_top_level() {
8447        // Pins the asymmetry the rule above creates: a type alias may be declared
8448        // in any block, an enum may not. The difference is required by enum
8449        // identity rather than chosen -- two nested aliases shadow each other
8450        // harmlessly, while two nested `type Color` declarations would be one type
8451        // with two variant sets. Tightening aliases to match, or relaxing enums,
8452        // has to break this test first.
8453        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8454        for (case, code) in [
8455            (
8456                "function body",
8457                format!("{allow}fn f() {{\n  type Temperature = number(_)\n  return 0\n}}\nx = f()\n"),
8458            ),
8459            (
8460                "sketch block",
8461                format!(
8462                    "{allow}sketch(on = XY) {{\n  type Temperature = number(_)\n  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
8463                ),
8464            ),
8465        ] {
8466            parse_execute(&code)
8467                .await
8468                .unwrap_or_else(|err| panic!("a type alias should be allowed in a {case}: {}", err.message()));
8469        }
8470    }
8471
8472    #[tokio::test(flavor = "multi_thread")]
8473    async fn enum_declaration_rejects_duplicate() {
8474        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8475type Color { | Red | Green | Red }
8476"#;
8477        assert_eq!(
8478            parse_execute(code).await.unwrap_err().message(),
8479            "Duplicate variant `Red` in enum `Color`."
8480        );
8481    }
8482
8483    /// Runs `main` with `modules` written beside it, so import paths resolve.
8484    async fn execute_with_modules(main: &str, modules: &[(&str, &str)]) -> Result<ExecTestResults, KclError> {
8485        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_enum_clash").unwrap();
8486        for (name, source) in modules {
8487            tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
8488        }
8489
8490        parse_execute_with_project_dir(main, Some(crate::TypedPath(tmpdir.path().into()))).await
8491    }
8492
8493    /// Like [`execute_with_modules`], with hints enabled for user declarations;
8494    /// see [`parse_execute_hinting_all`].
8495    async fn execute_with_modules_hinting_all(
8496        main: &str,
8497        modules: &[(&str, &str)],
8498    ) -> Result<ExecTestResults, KclError> {
8499        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_added_in").unwrap();
8500        for (name, source) in modules {
8501            tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
8502        }
8503
8504        parse_execute_hinting_all(main, Some(crate::TypedPath(tmpdir.path().into()))).await
8505    }
8506
8507    /// Runs `main` with an empty imported module named `m.kcl` in mock
8508    /// execution and returns the recorded compilation issues; the run may
8509    /// end in an error (e.g. from operating on the module's missing return
8510    /// value).
8511    ///
8512    /// The `m.kcl` module lives in an in-memory file system under a
8513    /// synthetic project directory, so parallel tests share no on-disk
8514    /// state and there is nothing to clean up even if the process is
8515    /// killed.
8516    async fn issues_with_empty_module(main: &str) -> Vec<crate::errors::CompilationIssue> {
8517        use futures::FutureExt;
8518
8519        let project_dir = crate::TypedPath::new("/zma-kcl-member-ranges");
8520        // Key the file by the same join that import resolution performs, so
8521        // the lookup matches on every platform.
8522        let files = [(project_dir.join("m.kcl").to_string(), Vec::new())]
8523            .into_iter()
8524            .collect();
8525
8526        let program = crate::Program::parse_no_errs(main).unwrap();
8527        let ctx = ExecutorContext {
8528            engine: Arc::new(EngineManager::new_mock()),
8529            cache: Cache::default(),
8530            engine_batch: EngineBatchContext::default(),
8531            fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
8532            settings: ExecutorSettings {
8533                project_directory: Some(project_dir),
8534                ..Default::default()
8535            },
8536            context_type: ContextType::Mock,
8537            execution_callbacks: Default::default(),
8538            machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
8539            configure_engine_render: true,
8540            needs_oit: Default::default(),
8541        };
8542        let mut exec_state = ExecState::new(&ctx);
8543        // Close the context even if execution panics, then let the panic
8544        // continue. An Err from the run itself is expected here (operating
8545        // on the module's missing return value) and is deliberately ignored.
8546        let run_result = std::panic::AssertUnwindSafe(ctx.run(&program, &mut exec_state))
8547            .catch_unwind()
8548            .await;
8549        ctx.close().await;
8550        if let Err(panic) = run_result {
8551            std::panic::resume_unwind(panic);
8552        }
8553        exec_state.issues().to_vec()
8554    }
8555
8556    #[tokio::test(flavor = "multi_thread")]
8557    async fn member_object_diagnostics_use_object_range() {
8558        // A diagnostic raised while evaluating a member expression's object
8559        // (here, the imported module's missing-return warning) points at the
8560        // object's own span, not the whole member expression.
8561        // Both member evaluation orders (pre-KCL-3.0 and KCL 3.0) must
8562        // attribute the diagnostic the same way.
8563        for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
8564            let main = format!("{header}import \"m.kcl\" as m\nx = m.field\n");
8565            let issues = issues_with_empty_module(&main).await;
8566            let warning = issues
8567                .iter()
8568                .find(|issue| issue.message.contains("no return value"))
8569                .expect("missing-return warning should be recorded");
8570            let object_start = main.rfind("m.field").unwrap();
8571            assert_eq!(
8572                (warning.source_range.start(), warning.source_range.end()),
8573                (object_start, object_start + 1),
8574                "warning should point at the object's span (header={header:?})"
8575            );
8576        }
8577    }
8578
8579    #[tokio::test(flavor = "multi_thread")]
8580    async fn member_property_diagnostics_use_property_range() {
8581        // Same for the computed property: the warning points at the index
8582        // expression's span inside the brackets.
8583        for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
8584            let main = format!("{header}import \"m.kcl\" as m\narr = [1]\nx = arr[m]\n");
8585            let issues = issues_with_empty_module(&main).await;
8586            let warning = issues
8587                .iter()
8588                .find(|issue| issue.message.contains("no return value"))
8589                .expect("missing-return warning should be recorded");
8590            let prop_start = main.rfind("[m]").unwrap() + 1;
8591            assert_eq!(
8592                (warning.source_range.start(), warning.source_range.end()),
8593                (prop_start, prop_start + 1),
8594                "warning should point at the property's span (header={header:?})"
8595            );
8596        }
8597    }
8598
8599    #[tokio::test(flavor = "multi_thread")]
8600    async fn backtrace_reports_fully_qualified_fn_names() {
8601        // An error inside a function called by a qualified name records the
8602        // full path (m::f), not just the final segment (f), in the
8603        // structured backtrace's unwind locations.
8604        let main = "import \"m.kcl\" as m\nx = m::f()\n";
8605        let modules = [("m.kcl", "export fn f() {\n  return undefinedVariable\n}\n")];
8606        let err = execute_with_modules(main, &modules).await.unwrap_err();
8607        let fn_names: Vec<_> = err.backtrace().into_iter().filter_map(|item| item.fn_name).collect();
8608        assert_eq!(fn_names, vec!["m::f".to_owned()]);
8609    }
8610
8611    #[tokio::test(flavor = "multi_thread")]
8612    async fn whole_module_name_executes_as_operand() {
8613        // A whole-module import used as a binary or unary operand executes
8614        // the module and operates on its final-expression value, exactly like
8615        // using the name in expression position (x = m).
8616        let main = r#"import "m.kcl" as m
8617sum = m + m
8618neg = -m
8619"#;
8620        let result = execute_with_modules(main, &[("m.kcl", "42\n")]).await.unwrap();
8621        assert_eq!(
8622            mem_get_json(result.exec_state.stack(), result.mem_env, "sum").as_f64(),
8623            Some(84.0)
8624        );
8625        assert_eq!(
8626            mem_get_json(result.exec_state.stack(), result.mem_env, "neg").as_f64(),
8627            Some(-42.0)
8628        );
8629    }
8630
8631    #[tokio::test(flavor = "multi_thread")]
8632    async fn whole_module_without_return_as_operand_errors() {
8633        // Matches expression-position behavior: the module still executes,
8634        // the missing-return fallback produces a KclNone, and the binary
8635        // operation then rejects it. (A trailing declaration would count as
8636        // the module's return value, so the module body must be empty.)
8637        let main = "import \"m.kcl\" as m
8638x = m + 1
8639";
8640        let err = execute_with_modules(main, &[("m.kcl", "")]).await.unwrap_err();
8641        assert!(
8642            err.message().contains("Expected a number, but found none"),
8643            "expected the operand to be the module's missing-return KclNone, got: {}",
8644            err.message()
8645        );
8646    }
8647
8648    #[tokio::test(flavor = "multi_thread")]
8649    async fn enum_rejects_name_clash_with_module() {
8650        // One rule reached six ways: by declaring the enum or an enum alias
8651        // second, by importing the module after either one, and by importing the
8652        // enum itself either by name or through a glob. A glob arrives by a
8653        // different code path because it copies exported keys with their
8654        // namespace prefix intact.
8655        let plain_module = ("Color.kcl", "export x = 1\n");
8656        let enum_module = (
8657            "enums.kcl",
8658            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red }\n",
8659        );
8660
8661        for (case, main, modules) in [
8662            (
8663                "module then enum",
8664                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"Color.kcl\"\ntype Color { | Red }\n",
8665                vec![plain_module],
8666            ),
8667            (
8668                "enum then module",
8669                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Color { | Red }\nimport \"Color.kcl\"\n",
8670                vec![plain_module],
8671            ),
8672            (
8673                "named import of an enum",
8674                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport Color from 'enums.kcl'\n",
8675                vec![plain_module, enum_module],
8676            ),
8677            (
8678                "glob import of an enum",
8679                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport * from 'enums.kcl'\n",
8680                vec![plain_module, enum_module],
8681            ),
8682            (
8683                "module then enum alias",
8684                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Base { | Red }\nimport \"Color.kcl\"\ntype Color = Base\n",
8685                vec![plain_module],
8686            ),
8687            (
8688                "enum alias then module",
8689                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Base { | Red }\ntype Color = Base\nimport \"Color.kcl\"\n",
8690                vec![plain_module],
8691            ),
8692        ] {
8693            let err = execute_with_modules(main, &modules).await.unwrap_err();
8694            assert_eq!(
8695                err.message(),
8696                "An enum and a module cannot share the name `Color` in the same scope, because `Color::x` would be ambiguous. Rename one of them.",
8697                "case: {case}"
8698            );
8699        }
8700    }
8701
8702    #[tokio::test(flavor = "multi_thread")]
8703    async fn enum_alias_can_shadow_module_from_outer_scope() {
8704        let main = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8705type Color { | Red }
8706import "Shade.kcl"
8707
8708fn pick(): Color {
8709  type Shade = Color
8710  return Shade::Red
8711}
8712
8713result = pick()
8714"#;
8715        let result = execute_with_modules(main, &[("Shade.kcl", "export value = 1\n")])
8716            .await
8717            .unwrap();
8718        let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "result") else {
8719            panic!("`result` should hold an enum value");
8720        };
8721        assert_eq!(value.qualified_name(), "Color::Red");
8722    }
8723
8724    #[tokio::test(flavor = "multi_thread")]
8725    async fn enum_constructs_variant() {
8726        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8727        let colors = (
8728            "colors.kcl",
8729            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
8730        );
8731
8732        for (case, main, modules) in [
8733            (
8734                "declared locally",
8735                format!("{allow}type Color {{ | Red | Green }}\nx = Color::Red\n"),
8736                vec![],
8737            ),
8738            (
8739                // Also the regression test for the export check: a module's exports
8740                // record the prefixed key `__ty_Color`, not the bare name.
8741                "reached through a module path",
8742                format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
8743                vec![colors],
8744            ),
8745            (
8746                "imported by name",
8747                format!("{allow}import Color from 'colors.kcl'\nx = Color::Red\n"),
8748                vec![colors],
8749            ),
8750            (
8751                // An import alias renames the binding, not the type, so identity
8752                // and therefore the reported name stay those of the declaration.
8753                "imported under an alias",
8754                format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade::Red\n"),
8755                vec![colors],
8756            ),
8757        ] {
8758            let result = execute_with_modules(&main, &modules)
8759                .await
8760                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
8761            let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
8762                panic!("case: {case}: `x` should hold an enum value");
8763            };
8764            assert_eq!(value.qualified_name(), "Color::Red", "case: {case}");
8765        }
8766    }
8767
8768    #[tokio::test(flavor = "multi_thread")]
8769    async fn enum_aliases_preserve_the_original_declaration() {
8770        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8771type Color { | Red | Green }
8772type C = Color
8773type D = C
8774
8775original = Color::Red
8776directAlias = C::Red
8777chainedAlias = D::Red
8778directEqualsOriginal = directAlias == original
8779chainEqualsOriginal = chainedAlias == original
8780
8781fn passThroughAlias(@color: C): Color {
8782  return color
8783}
8784
8785passed = passThroughAlias(D::Green)
8786"#;
8787
8788        let result = parse_execute(code).await.unwrap();
8789        let memory = result.exec_state.stack();
8790
8791        let KclValue::Type {
8792            value: TypeDef::Enum(original_def),
8793            ..
8794        } = mem_get_json(memory, result.mem_env, &format!("{}Color", memory::TYPE_PREFIX))
8795        else {
8796            panic!("`Color` should hold an enum definition");
8797        };
8798        for alias in ["C", "D"] {
8799            let KclValue::Type {
8800                value: TypeDef::Enum(alias_def),
8801                ..
8802            } = mem_get_json(memory, result.mem_env, &format!("{}{alias}", memory::TYPE_PREFIX))
8803            else {
8804                panic!("`{alias}` should hold an enum definition");
8805            };
8806            assert!(Arc::ptr_eq(&original_def, &alias_def), "alias: {alias}");
8807        }
8808
8809        for name in ["directEqualsOriginal", "chainEqualsOriginal"] {
8810            let KclValue::Bool { value, .. } = mem_get_json(memory, result.mem_env, name) else {
8811                panic!("`{name}` should hold a boolean");
8812            };
8813            assert!(value, "comparison: {name}");
8814        }
8815
8816        let KclValue::Enum { value: passed } = mem_get_json(memory, result.mem_env, "passed") else {
8817            panic!("`passed` should hold an enum value");
8818        };
8819        assert_eq!(passed.enum_id(), original_def.id());
8820        assert_eq!(passed.variant(), "Green");
8821    }
8822
8823    #[tokio::test(flavor = "multi_thread")]
8824    async fn enum_aliases_survive_qualified_imports_and_reexports() {
8825        let main = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8826import "colors.kcl"
8827import "aliases.kcl"
8828import "tones.kcl"
8829import Paint as Finish from "aliases.kcl"
8830import * from "aliases.kcl"
8831
8832original = colors::Color::Red
8833qualifiedAlias = aliases::Paint::Red
8834namedImportAlias = Finish::Green
8835globImportAlias = Paint::Red
8836namedTargetAlias = tones::Tint::Green
8837aliasesEqualOriginal = qualifiedAlias == original
8838
8839fn throughAlias(@color: Finish): colors::Color {
8840  return color
8841}
8842
8843fn throughOriginal(@color: colors::Color): Finish {
8844  return color
8845}
8846
8847fromAlias = throughAlias(Finish::Green)
8848originalIntoAlias = throughAlias(colors::Color::Green)
8849fromOriginal = throughOriginal(colors::Color::Red)
8850"#;
8851        let modules = [
8852            (
8853                "colors.kcl",
8854                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
8855            ),
8856            (
8857                "palette.kcl",
8858                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"colors.kcl\" as swatches\nexport type Shade = swatches::Color\n",
8859            ),
8860            (
8861                "aliases.kcl",
8862                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"palette.kcl\"\nexport type Paint = palette::Shade\n",
8863            ),
8864            (
8865                "tones.kcl",
8866                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport Color from \"colors.kcl\"\nexport type Tint = Color\n",
8867            ),
8868        ];
8869
8870        let result = execute_with_modules(main, &modules).await.unwrap();
8871        let memory = result.exec_state.stack();
8872        let KclValue::Enum { value: original } = mem_get_json(memory, result.mem_env, "original") else {
8873            panic!("`original` should hold an enum value");
8874        };
8875        let original_id = original.enum_id();
8876
8877        for (name, variant) in [
8878            ("qualifiedAlias", "Red"),
8879            ("namedImportAlias", "Green"),
8880            ("globImportAlias", "Red"),
8881            ("namedTargetAlias", "Green"),
8882            ("fromAlias", "Green"),
8883            ("originalIntoAlias", "Green"),
8884            ("fromOriginal", "Red"),
8885        ] {
8886            let KclValue::Enum { value } = mem_get_json(memory, result.mem_env, name) else {
8887                panic!("`{name}` should hold an enum value");
8888            };
8889            assert_eq!(value.enum_id(), original_id, "value: {name}");
8890            assert_eq!(value.variant(), variant, "value: {name}");
8891        }
8892
8893        let KclValue::Bool { value, .. } = mem_get_json(memory, result.mem_env, "aliasesEqualOriginal") else {
8894            panic!("`aliasesEqualOriginal` should hold a boolean");
8895        };
8896        assert!(value);
8897
8898        let KclValue::Type {
8899            value: TypeDef::Enum(finish_def),
8900            ..
8901        } = mem_get_json(memory, result.mem_env, &format!("{}Finish", memory::TYPE_PREFIX))
8902        else {
8903            panic!("`Finish` should hold an enum definition");
8904        };
8905        assert_eq!(finish_def.id(), original_id);
8906    }
8907
8908    // The next five tests pin lexical resolution of signature types: a type
8909    // name written in a function signature resolves in the scope where the
8910    // declaration executes, never in the caller's scope. Before
8911    // definition-time resolution, signature types were looked up at each call
8912    // in the caller's environment, so a std or user module whose exported
8913    // types a caller had not imported under their bare names was uncallable.
8914
8915    #[tokio::test(flavor = "multi_thread")]
8916    async fn signature_types_resolve_in_declaring_module() {
8917        let colors = (
8918            "colors.kcl",
8919            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n  return c\n}\n",
8920        );
8921        // The caller can reach `colors::Color` but never binds the bare name
8922        // `Color`, so resolving the signature in the caller's scope would fail.
8923        let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"colors.kcl\"\nr = colors::paint(colors::Color::Red)\n";
8924
8925        let result = execute_with_modules(main, &[colors]).await.unwrap();
8926        let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
8927            panic!("`r` should hold an enum value");
8928        };
8929        assert_eq!(value.qualified_name(), "Color::Red");
8930    }
8931
8932    #[tokio::test(flavor = "multi_thread")]
8933    async fn qualified_type_paths_resolve_in_aliases_and_ascriptions() {
8934        let main = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8935type ViewOrientation = view::Orientation
8936front = view::Orientation::Front: view::Orientation
8937"#;
8938
8939        parse_execute(main).await.unwrap();
8940    }
8941
8942    #[tokio::test(flavor = "multi_thread")]
8943    async fn unknown_qualified_type_reports_the_written_name() {
8944        let main = "fn f(@value: missing::Orientation) {}\n";
8945
8946        let err = parse_execute(main).await.unwrap_err();
8947        assert_eq!(err.message(), "Unknown type: missing::Orientation");
8948    }
8949
8950    #[tokio::test(flavor = "multi_thread")]
8951    async fn signature_types_resolve_under_import_alias() {
8952        // An import alias renames the caller's binding for the module. The
8953        // declaring module's scope is unaffected, so the signature must
8954        // resolve identically under any alias.
8955        let colors = (
8956            "colors.kcl",
8957            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n  return c\n}\n",
8958        );
8959        let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"colors.kcl\" as painter\nr = painter::paint(painter::Color::Red)\n";
8960
8961        let result = execute_with_modules(main, &[colors]).await.unwrap();
8962        let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
8963            panic!("`r` should hold an enum value");
8964        };
8965        assert_eq!(value.qualified_name(), "Color::Red");
8966    }
8967
8968    #[tokio::test(flavor = "multi_thread")]
8969    async fn signature_types_ignore_caller_scope() {
8970        // `broken.kcl` names a type it does not define. The caller defines
8971        // that name, which caller-scope resolution would have used. The
8972        // declaration must fail when the module loads, without consulting the
8973        // caller's binding.
8974        let broken = (
8975            "broken.kcl",
8976            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport fn f(@x: Missing) {\n  return x\n}\n",
8977        );
8978        let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Missing = string\nimport \"broken.kcl\"\nr = broken::f(\"hi\")\n";
8979
8980        let err = execute_with_modules(main, &[broken]).await.unwrap_err();
8981        assert!(
8982            err.message().contains("Unknown type: Missing"),
8983            "message: {}",
8984            err.message()
8985        );
8986    }
8987
8988    #[tokio::test(flavor = "multi_thread")]
8989    async fn signature_types_reject_forward_reference() {
8990        // Resolution happens when the declaration executes, so a type declared
8991        // later in the file is not visible. The function is never called; the
8992        // error must surface at the declaration itself.
8993        let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nfn f(@x: Later) {\n  return x\n}\ntype Later = string\n";
8994
8995        let err = parse_execute(main).await.unwrap_err();
8996        assert!(
8997            err.message().contains("Unknown type: Later"),
8998            "message: {}",
8999            err.message()
9000        );
9001    }
9002
9003    #[tokio::test(flavor = "multi_thread")]
9004    async fn signature_types_resolve_in_enclosing_scope() {
9005        // The declaring scope is the closure's scope, not merely the declaring
9006        // module: the anonymous function's signature must see the alias in the
9007        // enclosing function body. Caller-scope resolution would use the
9008        // module-level `Width = string` and fail to coerce `42`.
9009        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";
9010
9011        let result = parse_execute(main).await.unwrap();
9012        let KclValue::Number { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
9013            panic!("`r` should hold a number");
9014        };
9015        assert_eq!(value, 42.0);
9016    }
9017
9018    // Pins that numeric types in signatures are settings-independent, so
9019    // definition-time resolution changed nothing for them: in type
9020    // annotations, bare `number` maps to `Any` before the settings-reading
9021    // path, and every explicit suffix maps to a settings-free type. A literal
9022    // argument therefore takes its unit from the CALLER's module defaults;
9023    // the declaring module's defaults (`in` here) must never leak in. If a
9024    // future change makes a signature's number type depend on module default
9025    // units, the declaring-module scope of definition-time resolution starts
9026    // to matter and this pin fails.
9027    #[tokio::test(flavor = "multi_thread")]
9028    async fn signature_number_types_ignore_module_default_units() {
9029        let units_in = (
9030            "units_in.kcl",
9031            "@settings(defaultLengthUnit = in)\nexport fn passThrough(@x: number(Length)) {\n  return x\n}\n",
9032        );
9033        // The caller's default length unit is mm (the test default), so the
9034        // unitless literal is 42 mm by the time it reaches the parameter.
9035        let main = "import \"units_in.kcl\"\na = units_in::passThrough(42)\nb = units_in::passThrough(42mm)\nc = units_in::passThrough(42in)\n";
9036
9037        let result = execute_with_modules(main, &[units_in]).await.unwrap();
9038        for (name, expected_ty) in [
9039            // The unitless literal keeps its `Default` type, and that type
9040            // records the CALLER's module settings. Declaring-module leakage
9041            // would show here as `len: Inches`.
9042            //
9043            // That the coercion to `number(Length)` leaves the type as
9044            // `Default` rather than concretizing it to `Known(Millimeters)`
9045            // is pre-existing coercion behavior which this test observes but
9046            // does not endorse. If coercion later concretizes, update the
9047            // expected type; the pin here is the settings provenance.
9048            (
9049                "a",
9050                kcl_api::NumericType::Default {
9051                    len: kcl_api::UnitLength::Millimeters,
9052                    angle: kcl_api::UnitAngle::Degrees,
9053                },
9054            ),
9055            (
9056                "b",
9057                kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Millimeters)),
9058            ),
9059            (
9060                "c",
9061                kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Inches)),
9062            ),
9063        ] {
9064            let KclValue::Number { value, ty, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name)
9065            else {
9066                panic!("`{name}` should hold a number");
9067            };
9068            assert_eq!(value, 42.0, "`{name}` should keep its magnitude");
9069            assert_eq!(ty, expected_ty, "`{name}` should keep the caller-side unit context");
9070        }
9071    }
9072
9073    // Pins the sharpest shadowing case, from a hand-written example during
9074    // review: BOTH scopes define the same type name with different meanings,
9075    // so the test observes which one the signature uses, not merely whether a
9076    // name is present. `m1.kcl`'s `A` is `string` and is NOT exported; the
9077    // caller's own `A` is `number(mm)`. The signature must use m1's `A`, so
9078    // passing `2mm` is a type error. Caller-scope resolution would have used
9079    // the caller's `A` and accepted the call.
9080    #[tokio::test(flavor = "multi_thread")]
9081    async fn signature_types_use_declaring_scope_when_both_scopes_define_the_name() {
9082        let m1 = (
9083            "m1.kcl",
9084            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype A = string\n\nexport fn test(@a: A) {\n  return a\n}\n",
9085        );
9086        let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport * from \"m1.kcl\"\ntype A = number(mm)\nx = test(2mm)\n";
9087
9088        let err = execute_with_modules(main, &[m1]).await.unwrap_err();
9089        assert_eq!(
9090            err.message(),
9091            "The input argument of `test` requires a value with type `A`, but found a number (mm) (with type `number(mm)`)."
9092        );
9093    }
9094
9095    #[tokio::test(flavor = "multi_thread")]
9096    async fn enum_rejects_bad_variant_paths() {
9097        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
9098
9099        for (case, main, modules, message) in [
9100            (
9101                "unknown variant",
9102                format!("{allow}type Color {{ | Red | Green }}\nx = Color::Blue\n"),
9103                vec![],
9104                "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
9105            ),
9106            (
9107                "unknown variant through an alias",
9108                format!("{allow}type Color {{ | Red | Green }}\ntype Paint = Color\nx = Paint::Blue\n"),
9109                vec![],
9110                "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
9111            ),
9112            (
9113                "enum with no variants",
9114                format!("{allow}type Empty {{ | }}\nx = Empty::Red\n"),
9115                vec![],
9116                "`Red` is not a variant of enum `Empty`. Enum `Empty` has no variants.",
9117            ),
9118            (
9119                "path continues past the enum",
9120                format!("{allow}type Color {{ | Red }}\nx = Color::Red::more\n"),
9121                vec![],
9122                "`Color` is an enum, so only a variant name can follow it. There is nothing to reach through `Color::Red`.",
9123            ),
9124            (
9125                "variant name is case sensitive",
9126                format!("{allow}type Color {{ | Red }}\nx = Color::red\n"),
9127                vec![],
9128                "`red` is not a variant of enum `Color`. Its variants are: Red.",
9129            ),
9130            (
9131                "enum not exported from its module",
9132                format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
9133                vec![(
9134                    "colors.kcl",
9135                    "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Color { | Red }\n",
9136                )],
9137                "Item Color not found in module's exported items",
9138            ),
9139            (
9140                "a non-enum type alias cannot head a path",
9141                format!("{allow}type T = number(_)\nx = T::foo\n"),
9142                vec![],
9143                "`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
9144            ),
9145            (
9146                "a chained non-enum type alias cannot head a path",
9147                format!("{allow}type T = number(_)\ntype U = T\nx = U::foo\n"),
9148                vec![],
9149                "`U` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
9150            ),
9151            (
9152                "a qualified non-enum type alias cannot head a path",
9153                format!("{allow}import \"types.kcl\"\nx = types::T::foo\n"),
9154                vec![(
9155                    "types.kcl",
9156                    "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type T = number(_)\n",
9157                )],
9158                "`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
9159            ),
9160            (
9161                "an alias containing an enum is not an enum alias",
9162                format!("{allow}type Color {{ | Red }}\ntype T = Color | string\nx = T::Red\n"),
9163                vec![],
9164                "`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
9165            ),
9166            (
9167                // The other half of allowing a value and an enum to share a name:
9168                // a value on its own can never head a path.
9169                "a value cannot head a path",
9170                "Color = 5\nx = Color::Red\n".to_owned(),
9171                vec![],
9172                "`Color` is not defined",
9173            ),
9174        ] {
9175            let err = execute_with_modules(&main, &modules).await.unwrap_err();
9176            assert_eq!(err.message(), message, "case: {case}");
9177        }
9178    }
9179
9180    #[tokio::test(flavor = "multi_thread")]
9181    async fn enum_compares_by_variant() {
9182        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9183type Color { | Red | Green }
9184sameEq = Color::Red == Color::Red
9185sameNeq = Color::Red != Color::Red
9186otherEq = Color::Red == Color::Green
9187otherNeq = Color::Red != Color::Green
9188"#;
9189        let result = parse_execute(code).await.unwrap();
9190
9191        for (name, expected) in [
9192            ("sameEq", true),
9193            ("sameNeq", false),
9194            ("otherEq", false),
9195            ("otherNeq", true),
9196        ] {
9197            let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
9198                panic!("`{name}` should hold a bool");
9199            };
9200            assert_eq!(value, expected, "variable: {name}");
9201        }
9202    }
9203
9204    #[tokio::test(flavor = "multi_thread")]
9205    async fn enum_usable_inside_sketch_block() {
9206        // Only enum declarations are restricted to the top level; uses are not
9207        // restricted at all. A sketch block executes its body with sketch-mode
9208        // skipping turned off, and memory lookups walk outward, so the enum
9209        // declared above resolves inside the block.
9210        //
9211        // `assertIs` runs inside the block because block-local bindings live in a
9212        // child scope that the root environment cannot read afterwards. A wrong
9213        // comparison therefore fails this test instead of passing unnoticed.
9214        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9215type Color { | Red | Green }
9216sketch(on = XY) {
9217  c = Color::Red
9218  assertIs(Color::Red != Color::Green)
9219  assertIs(!(Color::Red != Color::Red))
9220  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
9221}
9222"#;
9223        parse_execute(code)
9224            .await
9225            .unwrap_or_else(|err| panic!("enum use inside a sketch block should work: {}", err.message()));
9226    }
9227
9228    #[tokio::test(flavor = "multi_thread")]
9229    async fn enum_eq_reserved_inside_sketch_block() {
9230        // Inside a sketch block, `==` declares an equivalence constraint, so it is
9231        // not available for ordinary comparison. Enums are not singled out: the
9232        // interception happens before any value-comparison arm is reached, and
9233        // strings and numbers are refused in the same words. The string and number
9234        // rows are here to keep that visible -- if a later change makes enums
9235        // report something different from the other types, this test says so.
9236        //
9237        // `!=` is deliberately absent: the interception tests `Eq` only, so `!=`
9238        // still compares, which `enum_usable_inside_sketch_block` covers.
9239        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
9240        let tail = "  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}\n";
9241        for (case, declaration, comparison, types) in [
9242            (
9243                "enums",
9244                "type Color { | Red | Green }\n",
9245                "Color::Red == Color::Green",
9246                "a value of enum `Color` and a value of enum `Color`",
9247            ),
9248            ("strings", "", "\"a\" == \"b\"", "a string and a string"),
9249            ("numbers", "", "1 == 2", "a number and a number"),
9250        ] {
9251            let code = format!("{allow}{declaration}sketch(on = XY) {{\n  x = {comparison}\n{tail}");
9252            assert_eq!(
9253                parse_execute(&code).await.unwrap_err().message(),
9254                format!("Cannot create an equivalence constraint between values of these types: {types}"),
9255                "case: {case}"
9256            );
9257        }
9258    }
9259
9260    #[tokio::test(flavor = "multi_thread")]
9261    async fn enum_same_file_imported_twice_is_one_type() {
9262        // Two names for one declaration, so they are the same type and compare
9263        // equal. Identity is the declaration, not the binding, which is what makes
9264        // this different from two files that each declare a `Color`.
9265        let main = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9266import Color as A from 'colors.kcl'
9267import Color as B from 'colors.kcl'
9268x = A::Red == B::Red
9269y = A::Red == B::Green
9270"#;
9271        let result = execute_with_modules(
9272            main,
9273            &[(
9274                "colors.kcl",
9275                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
9276            )],
9277        )
9278        .await
9279        .unwrap();
9280
9281        for (name, expected) in [("x", true), ("y", false)] {
9282            let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
9283                panic!("`{name}` should hold a bool");
9284            };
9285            assert_eq!(value, expected, "variable: {name}");
9286        }
9287    }
9288
9289    #[tokio::test(flavor = "multi_thread")]
9290    async fn enum_rejects_comparison_across_types() {
9291        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
9292        let color = (
9293            "a.kcl",
9294            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red }\n",
9295        );
9296        let other_color = (
9297            "b.kcl",
9298            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red }\n",
9299        );
9300
9301        for (case, main, modules, message) in [
9302            (
9303                "two enums declared separately",
9304                format!("{allow}type Color {{ | Red }}\ntype Shade {{ | Red }}\nx = Color::Red == Shade::Red\n"),
9305                vec![],
9306                "Cannot compare enum `Color` with enum `Shade`. They are different types.",
9307            ),
9308            (
9309                // Identity is the declaration, not the name, so two enums that
9310                // share a name are still different types. Pins that the message
9311                // says so rather than naming `Color` twice.
9312                "two enums sharing a name",
9313                format!(
9314                    "{allow}import Color as A from 'a.kcl'\nimport Color as B from 'b.kcl'\nx = A::Red == B::Red\n"
9315                ),
9316                vec![color, other_color],
9317                "Cannot compare two different enums that are both named `Color`. They come from separate declarations.",
9318            ),
9319            (
9320                "an enum and a number",
9321                format!("{allow}type Color {{ | Red }}\nx = Color::Red == 5\n"),
9322                vec![],
9323                "Cannot compare enum `Color::Red` with a number.",
9324            ),
9325            (
9326                "a number and an enum, in that order",
9327                format!("{allow}type Color {{ | Red }}\nx = 5 == Color::Red\n"),
9328                vec![],
9329                "Cannot compare enum `Color::Red` with a number.",
9330            ),
9331            (
9332                "an enum and a string",
9333                format!("{allow}type Color {{ | Red }}\nx = Color::Red == \"Red\"\n"),
9334                vec![],
9335                "Cannot compare enum `Color::Red` with a string.",
9336            ),
9337        ] {
9338            let err = execute_with_modules(&main, &modules).await.unwrap_err();
9339            assert_eq!(err.message(), message, "case: {case}");
9340        }
9341    }
9342
9343    #[tokio::test(flavor = "multi_thread")]
9344    async fn enum_rejects_bare_type_name_as_value() {
9345        let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
9346        let colors = (
9347            "colors.kcl",
9348            "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
9349        );
9350
9351        for (case, main, modules, message) in [
9352            (
9353                "enum suggests a variant",
9354                format!("{allow}type Color {{ | Red | Green }}\nx = Color\n"),
9355                vec![],
9356                "`Color` is a type, not a value. Use one of its variants, such as `Color::Red`.",
9357            ),
9358            (
9359                // The suggestion has to be pasteable into the file that produced
9360                // the error, so it uses the local name rather than the declared one.
9361                "suggestion uses the import alias",
9362                format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade\n"),
9363                vec![colors],
9364                "`Shade` is a type, not a value. Use one of its variants, such as `Shade::Red`.",
9365            ),
9366            (
9367                "suggestion uses the type alias",
9368                format!("{allow}type Color {{ | Red | Green }}\ntype Paint = Color\nx = Paint\n"),
9369                vec![],
9370                "`Paint` is a type, not a value. Use one of its variants, such as `Paint::Red`.",
9371            ),
9372            (
9373                "enum with no variants suggests nothing",
9374                format!("{allow}type Empty {{ | }}\nx = Empty\n"),
9375                vec![],
9376                "`Empty` is a type, not a value.",
9377            ),
9378            (
9379                "a type alias reports the same way",
9380                format!("{allow}type T = number(_)\nx = T\n"),
9381                vec![],
9382                "`T` is a type, not a value.",
9383            ),
9384            (
9385                // Unchanged behavior: with no type of that name, the old message
9386                // is still the right one.
9387                "an unknown name is still undefined",
9388                "x = Nope\n".to_owned(),
9389                vec![],
9390                "`Nope` is not defined",
9391            ),
9392        ] {
9393            let err = execute_with_modules(&main, &modules).await.unwrap_err();
9394            assert_eq!(err.message(), message, "case: {case}");
9395        }
9396    }
9397
9398    #[tokio::test(flavor = "multi_thread")]
9399    async fn imported_user_type_declarations_require_v3_even_with_opt_in() {
9400        for version in ["1.0", "2.0"] {
9401            for module_settings in [format!("@settings(kclVersion = \"{version}\")\n"), String::new()] {
9402                for (feature, declaration, use_type) in [
9403                    (
9404                        "Type aliases",
9405                        "export type Distance = number(mm)",
9406                        "x = 1mm: types::Distance",
9407                    ),
9408                    (
9409                        "Enum declarations",
9410                        "export type Color { | Red }",
9411                        "x = types::Color::Red",
9412                    ),
9413                ] {
9414                    let main = format!(
9415                        "@settings(kclVersion = \"{version}\", experimentalFeatures = allow)\nimport \"types.kcl\"\n{use_type}\n"
9416                    );
9417                    let module = format!("{module_settings}{declaration}\n");
9418                    let err = execute_with_modules(&main, &[("types.kcl", &module)])
9419                        .await
9420                        .unwrap_err();
9421                    assert_eq!(
9422                        err.message(),
9423                        format!("{feature} require KCL 3.0-preview, but this program uses KCL {version}."),
9424                        "main: {main}; module: {module}"
9425                    );
9426                }
9427            }
9428        }
9429    }
9430
9431    #[tokio::test(flavor = "multi_thread")]
9432    async fn named_view_access_in_unversioned_import_uses_entry_point_version() {
9433        let dep = "export camera = view::directed([0, 1, -2])\n";
9434        for version in ["1.0", "2.0"] {
9435            let main = format!("@settings(kclVersion = \"{version}\")\nimport \"dep.kcl\"\nx = dep::camera\n");
9436            let err = execute_with_modules(&main, &[("dep.kcl", dep)]).await.unwrap_err();
9437            assert!(
9438                err.message()
9439                    .contains(&format!("added in KCL 3.0, but this program uses KCL {version}")),
9440                "error: {}",
9441                err.message()
9442            );
9443        }
9444
9445        let main = "@settings(kclVersion = \"3.0-preview\")\nimport \"dep.kcl\"\nx = dep::camera\n";
9446        let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
9447        assert!(result.exec_state.issues().is_empty());
9448    }
9449
9450    #[tokio::test(flavor = "multi_thread")]
9451    async fn enum_use_is_not_experimental_in_v3() {
9452        let code = r#"@settings(kclVersion = "3.0-preview")
9453type Color { | Red }
9454x = Color::Red
9455"#;
9456        let result = parse_execute(code).await.unwrap();
9457        assert!(
9458            result.exec_state.global.issues.is_empty(),
9459            "issues: {:?}",
9460            result.exec_state.global.issues
9461        );
9462    }
9463
9464    #[tokio::test(flavor = "multi_thread")]
9465    async fn v3_enum_variant_warning_follows_explicit_annotations() {
9466        for (case, declarations, binding, expected_warnings) in [
9467            ("unannotated enum", "type Color { | Red }", "Color", 0),
9468            (
9469                "annotated enum",
9470                "@(experimental = true)\ntype Color { | Red }",
9471                "Color",
9472                1,
9473            ),
9474            (
9475                "alias of annotated enum",
9476                "@(experimental = true)\ntype Color { | Red }\ntype Shade = Color",
9477                "Shade",
9478                1,
9479            ),
9480            (
9481                "annotated alias",
9482                "type Color { | Red }\n@(experimental = true)\ntype Shade = Color",
9483                "Shade",
9484                1,
9485            ),
9486        ] {
9487            let code = format!(
9488                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = warn)\n{declarations}\nx = {binding}::Red\n"
9489            );
9490            let result = parse_execute(&code)
9491                .await
9492                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
9493            let variant_warnings: Vec<_> = result
9494                .exec_state
9495                .issues()
9496                .iter()
9497                .filter(|issue| {
9498                    issue.message == "Use of the enum `Color` is experimental and may change or be removed."
9499                })
9500                .collect();
9501            assert_eq!(variant_warnings.len(), expected_warnings, "case: {case}");
9502            assert!(
9503                variant_warnings.iter().all(|issue| issue.severity == Severity::Warning),
9504                "case: {case}"
9505            );
9506        }
9507    }
9508
9509    #[tokio::test(flavor = "multi_thread")]
9510    async fn enum_allows_name_sharing_outside_modules() {
9511        // Pins two deliberate exemptions from the clash rule above, so that
9512        // tightening it later has to be a decision rather than an accident.
9513        //
9514        // Only a module or a type binding that resolves to an enum can head a
9515        // `Color::Red` path, so only those two can be ambiguous. A non-enum type
9516        // alias cannot head a `::` path, and an ordinary value is never looked up
9517        // for a path head at all.
9518        for (case, main, modules) in [
9519            (
9520                // The module arrives second, which is the path carrying the
9521                // "only `TypeDef::Enum` conflicts" guard.
9522                "an alias may share a name with a module",
9523                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Temperature = number(_)\nimport \"Temperature.kcl\"\nx = Temperature::x\n",
9524                vec![("Temperature.kcl", "export x = 1\n")],
9525            ),
9526            (
9527                "a value may share a name with an enum",
9528                "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Color { | Red }\nColor = 5\n",
9529                vec![],
9530            ),
9531        ] {
9532            if let Err(err) = execute_with_modules(main, &modules).await {
9533                panic!("case: {case}: {}", err.message());
9534            }
9535        }
9536    }
9537
9538    #[tokio::test(flavor = "multi_thread")]
9539    async fn enum_declaration_rejects_redefinition() {
9540        let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9541type Color { | Red }
9542type Color { | Green }
9543"#;
9544        assert_eq!(
9545            parse_execute(code).await.unwrap_err().message(),
9546            "Redefinition of type Color."
9547        );
9548    }
9549
9550    /// Projection yields the variant's declared representation, which in V1 is
9551    /// always the variant name. Every row binds `x` so the rows differ only in the
9552    /// shape being projected, and the alias row is here because the target is
9553    /// resolved before projection decides anything, so an alias must behave
9554    /// exactly like the type it names.
9555    #[tokio::test(flavor = "multi_thread")]
9556    async fn enum_projects_to_string() {
9557        let header = r#"
9558            @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9559            type Color { | Red | Green }
9560            type Label = string
9561        "#;
9562
9563        for (case, body, expected) in [
9564            ("a variant", "x = Color::Red: string", "Red"),
9565            ("another variant of the same enum", "x = Color::Green: string", "Green"),
9566            ("an alias of the target type", "x = Color::Red: Label", "Red"),
9567            (
9568                "an element of a projected array",
9569                r#"
9570                    pair = [Color::Red, Color::Green]: [string]
9571                    x = pair[1]
9572                "#,
9573                "Green",
9574            ),
9575            (
9576                "an element of a nested projected array",
9577                r#"
9578                    grid = [[Color::Green]]: [[string]]
9579                    x = grid[0][0]
9580                "#,
9581                "Green",
9582            ),
9583            (
9584                "a one-element array against a bare string",
9585                "x = [Color::Red]: string",
9586                "Red",
9587            ),
9588        ] {
9589            let result = parse_execute(&format!("{header}{body}\n"))
9590                .await
9591                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
9592            let KclValue::String { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
9593                panic!("case: {case}: `x` should hold a string");
9594            };
9595            assert_eq!(value, expected, "case: {case}");
9596        }
9597    }
9598
9599    /// Ascribing the enum's own type, directly or through an alias, is a check
9600    /// rather than a conversion: the value stays an enum and still compares equal
9601    /// to the variant it came from.
9602    #[tokio::test(flavor = "multi_thread")]
9603    async fn enum_ascription_keeps_the_enum() {
9604        let header = r#"
9605            @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9606            type Color { | Red | Green }
9607            type Paint = Color
9608        "#;
9609
9610        for (case, expression, expected) in [
9611            ("its own type", "(Color::Red: Color) == Color::Red", true),
9612            ("an alias of its own type", "(Color::Red: Paint) == Color::Red", true),
9613            (
9614                "the ascription does not change which variant it is",
9615                "(Color::Red: Color) == Color::Green",
9616                false,
9617            ),
9618        ] {
9619            let result = parse_execute(&format!("{header}x = {expression}\n"))
9620                .await
9621                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
9622            let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
9623                panic!("case: {case}: `x` should hold a bool");
9624            };
9625            assert_eq!(value, expected, "case: {case}");
9626        }
9627    }
9628
9629    /// A boundary the user did not write must not project, or a nominal parameter
9630    /// type would mean nothing. The rows are the separate coercion sites: the
9631    /// unlabeled argument, a labeled argument, and the return.
9632    #[tokio::test(flavor = "multi_thread")]
9633    async fn enum_projection_is_not_implicit() {
9634        let header = r#"
9635            @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9636            type Color { | Red | Green }
9637        "#;
9638        let found = "but found a value of enum `Color` (with type `Color`).";
9639
9640        for (case, body, expected) in [
9641            (
9642                "unlabeled argument",
9643                r#"
9644                    fn label(@text: string) { return text }
9645                    x = label(Color::Red)
9646                "#,
9647                format!("The input argument of `label` requires a value with type `string`, {found}"),
9648            ),
9649            (
9650                "labeled argument",
9651                r#"
9652                    fn label(text: string) { return text }
9653                    x = label(text = Color::Red)
9654                "#,
9655                format!("text requires a value with type `string`, {found}"),
9656            ),
9657            (
9658                "return",
9659                r#"
9660                    fn label(): string { return Color::Red }
9661                    x = label()
9662                "#,
9663                format!("This function requires its result to be a value with type `string`, {found}"),
9664            ),
9665            (
9666                // The reported type is `[any; 1]` rather than `[Color; 1]` because
9667                // an array literal does not infer a homogeneous element type. That
9668                // is pre-existing and unrelated to enums; it is pinned here so the
9669                // row is not read as an enum-specific quirk.
9670                "inside an array at an argument boundary",
9671                r#"
9672                    fn labels(@text: [string]) { return text }
9673                    x = labels([Color::Red])
9674                "#,
9675                "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(),
9676            ),
9677        ] {
9678            assert_eq!(
9679                parse_execute(&format!("{header}{body}\n")).await.unwrap_err().message(),
9680                expected,
9681                "case: {case}"
9682            );
9683        }
9684    }
9685
9686    /// What an explicit ascription refuses, and what it says about it. The numeric
9687    /// rows deliberately do not name the mechanism a later version would use.
9688    #[tokio::test(flavor = "multi_thread")]
9689    async fn enum_ascription_rejections() {
9690        let header = r#"
9691            @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9692            type Color { | Red }
9693            type Shade { | Red }
9694        "#;
9695        let no_number = "Cannot project enum `Color` to a number. An enum projects to `string`; projecting to a number is not supported yet.";
9696
9697        for (case, expression, expected) in [
9698            ("a number target", "Color::Red: number(_)", no_number.to_owned()),
9699            (
9700                "a number target reached through an array, so the reason survives the walk",
9701                "[Color::Red]: [number(_)]",
9702                no_number.to_owned(),
9703            ),
9704            (
9705                "a boolean target, which is not a projection at all",
9706                "Color::Red: bool",
9707                "could not coerce a value of enum `Color` (with type `Color`) to type `bool`".to_owned(),
9708            ),
9709            (
9710                "another enum whose variants happen to match",
9711                "Color::Red: Shade",
9712                "could not coerce a value of enum `Color` (with type `Color`) to type `Shade`".to_owned(),
9713            ),
9714        ] {
9715            assert_eq!(
9716                parse_execute(&format!("{header}x = {expression}\n"))
9717                    .await
9718                    .unwrap_err()
9719                    .message(),
9720                expected,
9721                "case: {case}"
9722            );
9723        }
9724    }
9725
9726    /// The mirror of `enum_projection_is_not_implicit`: where the declared type is
9727    /// the enum itself, a value flows through every boundary unchanged. Each row
9728    /// binds `x` to a comparison that must hold, so a value that arrived altered
9729    /// would fail rather than pass unnoticed. `Some(message)` marks a row that must
9730    /// be refused instead, which is what keeps the check nominal rather than
9731    /// merely permissive.
9732    #[tokio::test(flavor = "multi_thread")]
9733    async fn enum_flows_through_declared_types() {
9734        let header = r#"
9735            @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9736            type Color { | Red | Green }
9737            type Paint = Color
9738            type Shade { | Red }
9739        "#;
9740
9741        for (case, body, expected) in [
9742            (
9743                "an alias parameter accepts the original enum",
9744                r#"
9745                    fn paint(@c: Paint) { return c }
9746                    x = paint(Color::Red) == Paint::Red
9747                "#,
9748                None,
9749            ),
9750            (
9751                "an unlabeled parameter",
9752                r#"
9753                    fn paint(@c: Color) { return c }
9754                    x = paint(Color::Red) == Color::Red
9755                "#,
9756                None,
9757            ),
9758            (
9759                "a labeled parameter",
9760                r#"
9761                    fn paint(c: Color) { return c }
9762                    x = paint(c = Color::Green) == Color::Green
9763                "#,
9764                None,
9765            ),
9766            (
9767                "a declared return type",
9768                r#"
9769                    fn pick(): Color { return Color::Red }
9770                    x = pick() == Color::Red
9771                "#,
9772                None,
9773            ),
9774            (
9775                "an array parameter",
9776                r#"
9777                    fn firstOf(@cs: [Color]) { return cs[0] }
9778                    x = firstOf([Color::Red, Color::Green]) == Color::Red
9779                "#,
9780                None,
9781            ),
9782            (
9783                // The field check is `has_type`, which an enum satisfies, so an
9784                // object passes here while the projection row of
9785                // `enum_projects_by_target_shape` fails. Both behaviors come from
9786                // the same unfinished object coercion.
9787                "an object field",
9788                r#"
9789                    fn take(@o: { c: Color }) { return o.c }
9790                    x = take({ c = Color::Green }) == Color::Green
9791                "#,
9792                None,
9793            ),
9794            (
9795                "a union that names the enum",
9796                r#"
9797                    fn either(@v: Color | string) { return v }
9798                    x = either(Color::Red) == Color::Red
9799                "#,
9800                None,
9801            ),
9802            (
9803                "the same union given the other member",
9804                r#"
9805                    fn either(@v: Color | string) { return v }
9806                    x = either("plain") == "plain"
9807                "#,
9808                None,
9809            ),
9810            (
9811                "an alias parameter rejects a different enum",
9812                r#"
9813                    fn paint(@c: Paint) { return c }
9814                    x = paint(Shade::Red) == Shade::Red
9815                "#,
9816                Some(
9817                    "The input argument of `paint` requires a value with type `Paint`, but found a value of enum `Shade` (with type `Shade`).",
9818                ),
9819            ),
9820            (
9821                "another declaration at the same boundary",
9822                r#"
9823                    fn paint(@c: Color) { return c }
9824                    x = paint(Shade::Red) == Shade::Red
9825                "#,
9826                Some(
9827                    "The input argument of `paint` requires a value with type `Color`, but found a value of enum `Shade` (with type `Shade`).",
9828                ),
9829            ),
9830        ] {
9831            let code = format!("{header}{body}\n");
9832            match expected {
9833                None => {
9834                    let result = parse_execute(&code)
9835                        .await
9836                        .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
9837                    let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x")
9838                    else {
9839                        panic!("case: {case}: `x` should hold a bool");
9840                    };
9841                    assert!(value, "case: {case}: the value did not survive the boundary");
9842                }
9843                Some(message) => assert_eq!(
9844                    parse_execute(&code).await.unwrap_err().message(),
9845                    message,
9846                    "case: {case}"
9847                ),
9848            }
9849        }
9850    }
9851
9852    // ---- `added_in` on whole declarations ----
9853
9854    /// Runs `body` under `kcl_version` and returns the fatal error message, or
9855    /// `None` if it ran without issues. Experimental features are allowed for
9856    /// unrelated syntax in the test programs. Hints are enabled for user
9857    /// declarations, since in production only std gets them.
9858    async fn added_in_error(kcl_version: &str, body: &str) -> Option<String> {
9859        let program = format!("@settings(kclVersion = {kcl_version}, experimentalFeatures = allow)\n{body}");
9860        match parse_execute_hinting_all(&program, None).await {
9861            Ok(result) => {
9862                assert!(
9863                    result.issues().is_empty(),
9864                    "kclVersion {kcl_version}: unexpected issues: {:#?}",
9865                    result.issues()
9866                );
9867                None
9868            }
9869            Err(err) => Some(err.message().to_owned()),
9870        }
9871    }
9872
9873    #[tokio::test(flavor = "multi_thread")]
9874    async fn not_yet_added_function_is_undefined() {
9875        let body = r#"@(added_in = "3.0")
9876fn newFn() { return 1 }
9877x = newFn()
9878"#;
9879        assert_eq!(
9880            added_in_error("2.0", body).await.as_deref(),
9881            Some("`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0")
9882        );
9883        assert_eq!(added_in_error("\"3.0-preview\"", body).await, None);
9884    }
9885
9886    #[tokio::test(flavor = "multi_thread")]
9887    async fn not_yet_added_function_cannot_be_referenced_as_a_value() {
9888        // Any reference fails, not only a call: the name is simply not bound.
9889        let body = r#"@(added_in = "3.0")
9890fn newFn() { return 1 }
9891f = newFn
9892"#;
9893        assert_eq!(
9894            added_in_error("2.0", body).await.as_deref(),
9895            Some("`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0")
9896        );
9897    }
9898
9899    #[tokio::test(flavor = "multi_thread")]
9900    async fn added_in_boundary_is_inclusive_and_prerelease_counts_as_release() {
9901        for (kcl_version, added_in) in [
9902            ("1.0", "1.0"),
9903            ("2.0", "1.0"),
9904            ("2.0", "2.0"),
9905            ("\"3.0-preview\"", "3.0"),
9906            ("3.0", "3.0"),
9907        ] {
9908            let body = format!("@(added_in = \"{added_in}\")\nfn newFn() {{ return 1 }}\nx = newFn()\n");
9909            assert_eq!(
9910                added_in_error(kcl_version, &body).await,
9911                None,
9912                "kclVersion {kcl_version}, added_in {added_in}"
9913            );
9914        }
9915    }
9916
9917    #[tokio::test(flavor = "multi_thread")]
9918    async fn not_yet_added_error_reports_default_kcl_version() {
9919        // No `@settings(kclVersion = ...)`: the message names the default version.
9920        let program = r#"@(added_in = "2.0")
9921fn newFn() { return 1 }
9922x = newFn()
9923"#;
9924        assert_eq!(
9925            parse_execute_hinting_all(program, None).await.unwrap_err().message(),
9926            "`newFn` is not defined; it was added in KCL 2.0, but this program uses KCL 1.0"
9927        );
9928    }
9929
9930    #[tokio::test(flavor = "multi_thread")]
9931    async fn not_yet_added_constant_is_undefined() {
9932        let body = r#"@(added_in = "3.0")
9933newConst = 5
9934x = newConst
9935"#;
9936        assert_eq!(
9937            added_in_error("2.0", body).await.as_deref(),
9938            Some("`newConst` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0")
9939        );
9940        assert_eq!(added_in_error("\"3.0-preview\"", body).await, None);
9941    }
9942
9943    #[tokio::test(flavor = "multi_thread")]
9944    async fn not_yet_added_type_is_unknown() {
9945        let body = r#"@(added_in = "3.0")
9946type NewT = [number; 2]
9947x = [1, 2]: NewT
9948"#;
9949        assert_eq!(
9950            added_in_error("2.0", body).await.as_deref(),
9951            Some("Unknown type: NewT; it was added in KCL 3.0, but this program uses KCL 2.0")
9952        );
9953        assert_eq!(added_in_error("\"3.0-preview\"", body).await, None);
9954    }
9955
9956    /// A function naming a later-added type in its signature fails at its
9957    /// declaration with the type's error, unless it is gated at or above the type.
9958    #[tokio::test(flavor = "multi_thread")]
9959    async fn function_using_not_yet_added_type_fails_at_its_declaration() {
9960        let ty = "@(added_in = \"3.0\")\ntype NewT = [number; 2]\n";
9961        let type_error = Some("Unknown type: NewT; it was added in KCL 3.0, but this program uses KCL 2.0");
9962
9963        // Ungated function: the type is missing on 2.0.
9964        let body = format!("{ty}fn usesNew(@p: NewT) {{ return p }}\n");
9965        assert_eq!(added_in_error("2.0", &body).await.as_deref(), type_error);
9966        // Gated below the type: same.
9967        let body = format!("{ty}@(added_in = \"2.0\")\nfn usesNew(@p: NewT) {{ return p }}\n");
9968        assert_eq!(added_in_error("2.0", &body).await.as_deref(), type_error);
9969        // The return type is part of the signature too.
9970        let body = format!("{ty}fn makesNew(): NewT {{ return [1, 2] }}\n");
9971        assert_eq!(added_in_error("2.0", &body).await.as_deref(), type_error);
9972
9973        // Gated at the type's version: both are skipped.
9974        let body = format!("{ty}@(added_in = \"3.0\")\nfn usesNew(@p: NewT) {{ return p }}\n");
9975        assert_eq!(added_in_error("2.0", &body).await, None);
9976        let body = format!("{body}x = usesNew([1, 2])\n");
9977        assert_eq!(
9978            added_in_error("2.0", &body).await.as_deref(),
9979            Some("`usesNew` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0")
9980        );
9981
9982        // On a new enough program, nothing is gated.
9983        let body = format!("{ty}fn usesNew(@p: NewT) {{ return p }}\nx = usesNew([1, 2])\n");
9984        assert_eq!(added_in_error("\"3.0-preview\"", &body).await, None);
9985    }
9986
9987    #[tokio::test(flavor = "multi_thread")]
9988    async fn not_yet_added_declaration_is_absent_from_imports() {
9989        // The module runs on a cloned state, so these also cover the record's
9990        // trip through the module's execution outcome.
9991        let dep = "@(added_in = \"3.0\")\nexport fn newFn() { return 1 }\nexport fn oldFn() { return 2 }\n";
9992
9993        // An explicit import fails with the version help.
9994        let main = "@settings(kclVersion = 2.0)\nimport newFn from \"dep.kcl\"\nx = newFn()\n";
9995        assert_eq!(
9996            execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
9997                .await
9998                .unwrap_err()
9999                .message(),
10000            "newFn is not defined in module; it was added in KCL 3.0, but this program uses KCL 2.0"
10001        );
10002
10003        // So does a qualified path.
10004        let main = "@settings(kclVersion = 2.0)\nimport \"dep.kcl\"\nx = dep::newFn()\n";
10005        assert_eq!(
10006            execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
10007                .await
10008                .unwrap_err()
10009                .message(),
10010            "`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0"
10011        );
10012
10013        // A glob import silently omits it while the rest of the module works.
10014        let main = "@settings(kclVersion = 2.0)\nimport * from \"dep.kcl\"\nx = oldFn()\ny = newFn()\n";
10015        assert_eq!(
10016            execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
10017                .await
10018                .unwrap_err()
10019                .message(),
10020            "`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0"
10021        );
10022
10023        // On a new enough program the import works.
10024        let main = "@settings(kclVersion = \"3.0-preview\")\nimport newFn from \"dep.kcl\"\nx = newFn()\n";
10025        let result = execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
10026            .await
10027            .unwrap();
10028        assert_eq!(variable_f64(&result, "x"), 1.0);
10029    }
10030
10031    /// In production only std declarations get the version help; user
10032    /// declarations keep the plain message.
10033    #[tokio::test(flavor = "multi_thread")]
10034    async fn not_yet_added_user_declarations_get_no_hint() {
10035        let program = "@settings(kclVersion = 2.0)\n@(added_in = \"3.0\")\nfn newFn() { return 1 }\nx = newFn()\n";
10036        assert_eq!(
10037            parse_execute(program).await.unwrap_err().message(),
10038            "`newFn` is not defined"
10039        );
10040
10041        let dep = "@(added_in = \"3.0\")\nexport fn newFn() { return 1 }\n";
10042        let main = "@settings(kclVersion = 2.0)\nimport newFn from \"dep.kcl\"\n";
10043        assert_eq!(
10044            execute_with_modules(main, &[("dep.kcl", dep)])
10045                .await
10046                .unwrap_err()
10047                .message(),
10048            "newFn is not defined in module"
10049        );
10050    }
10051
10052    #[tokio::test(flavor = "multi_thread")]
10053    async fn not_yet_added_records_whether_the_declaration_is_std() {
10054        let ctx = new_mock_executor_context(None);
10055        let mut exec_state = ExecState::new(&ctx);
10056        let version = |s: &str| annotations::VersionConstraint::parse(s).unwrap();
10057
10058        exec_state.record_not_yet_added("userFn".to_owned(), version("3.0"), false);
10059        exec_state.mod_local.path = ModulePath::Std {
10060            value: "solid".to_owned(),
10061        };
10062        exec_state.record_not_yet_added("stdFn".to_owned(), version("3.0"), true);
10063
10064        let records = &exec_state.mod_local.not_yet_added;
10065        assert!(!records["userFn"].item.is_std);
10066        assert!(!records["userFn"].exported);
10067        assert!(records["stdFn"].item.is_std);
10068        assert!(records["stdFn"].exported);
10069        ctx.close().await;
10070    }
10071
10072    /// `export import *` re-exports records along with names.
10073    #[tokio::test(flavor = "multi_thread")]
10074    async fn not_yet_added_records_follow_glob_re_exports() {
10075        let dep = "@(added_in = \"3.0\")\nexport fn newFn() { return 1 }\n";
10076        let mid = "export import * from \"dep.kcl\"\n";
10077        let modules = [("dep.kcl", dep), ("mid.kcl", mid)];
10078
10079        let main = "@settings(kclVersion = 2.0)\nimport * from \"mid.kcl\"\nx = newFn()\n";
10080        assert_eq!(
10081            execute_with_modules_hinting_all(main, &modules)
10082                .await
10083                .unwrap_err()
10084                .message(),
10085            "`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0"
10086        );
10087
10088        let main = "@settings(kclVersion = 2.0)\nimport newFn from \"mid.kcl\"\nx = newFn()\n";
10089        assert_eq!(
10090            execute_with_modules_hinting_all(main, &modules)
10091                .await
10092                .unwrap_err()
10093                .message(),
10094            "newFn is not defined in module; it was added in KCL 3.0, but this program uses KCL 2.0"
10095        );
10096    }
10097
10098    /// A record is as private as its name: no `export`, no hint for importers.
10099    #[tokio::test(flavor = "multi_thread")]
10100    async fn private_not_yet_added_records_stay_private() {
10101        let dep = "@(added_in = \"3.0\")\nfn privateFn() { return 1 }\nexport fn oldFn() { return 2 }\n";
10102
10103        let main = "@settings(kclVersion = 2.0)\nimport * from \"dep.kcl\"\nx = privateFn()\n";
10104        assert_eq!(
10105            execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
10106                .await
10107                .unwrap_err()
10108                .message(),
10109            "`privateFn` is not defined"
10110        );
10111
10112        let main = "@settings(kclVersion = 2.0)\nimport privateFn from \"dep.kcl\"\n";
10113        assert_eq!(
10114            execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
10115                .await
10116                .unwrap_err()
10117                .message(),
10118            "privateFn is not defined in module"
10119        );
10120
10121        // `mid` imports the record without `export`, so it does not pass it on.
10122        let dep = "@(added_in = \"3.0\")\nexport fn newFn() { return 1 }\n";
10123        let mid = "import * from \"dep.kcl\"\nexport fn other() { return 2 }\n";
10124        let main = "@settings(kclVersion = 2.0)\nimport * from \"mid.kcl\"\nx = newFn()\n";
10125        assert_eq!(
10126            execute_with_modules_hinting_all(main, &[("dep.kcl", dep), ("mid.kcl", mid)])
10127                .await
10128                .unwrap_err()
10129                .message(),
10130            "`newFn` is not defined"
10131        );
10132    }
10133
10134    /// The mock memory cache carries std's records, since a run reusing cached
10135    /// memory skips the prelude that writes them.
10136    #[tokio::test(flavor = "multi_thread")]
10137    async fn std_not_yet_added_records_survive_the_mock_memory_cache() {
10138        let ctx = new_mock_executor_context(None);
10139        let mut exec_state = ExecState::new(&ctx);
10140        let version = |s: &str| annotations::VersionConstraint::parse(s).unwrap();
10141        let program = crate::Program::parse_no_errs("x = 1").unwrap();
10142        let (main_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
10143        exec_state.global.std_not_yet_added.insert(
10144            "cube".to_owned(),
10145            NotYetAdded {
10146                added_in: version("3.0"),
10147                is_std: true,
10148            },
10149        );
10150
10151        let cached = cache::GlobalState::new(
10152            exec_state,
10153            ctx.settings.clone(),
10154            program.kcl_version,
10155            program.ast.clone(),
10156            main_ref,
10157        );
10158        let mem = cached.mock_memory_state().unwrap();
10159        assert_eq!(mem.std_not_yet_added["cube"].added_in, version("3.0"));
10160
10161        let mock_config = MockConfig::default();
10162        let mut restored = ExecState::new_mock(&ctx, &mock_config);
10163        ExecutorContext::restore_mock_memory(&mut restored, mem, &mock_config).unwrap();
10164        assert!(
10165            !restored.stack().memory.requires_std(),
10166            "cached memory already has the prelude"
10167        );
10168        assert_eq!(
10169            restored.not_yet_added_in_scope("cube").map(|r| &r.added_in),
10170            Some(&version("3.0"))
10171        );
10172        // A std record explains a failed lookup without any test override.
10173        let err = KclError::new_undefined_value(
10174            KclErrorDetails::new("`cube` is not defined".to_owned(), Vec::new()),
10175            None,
10176        );
10177        assert_eq!(
10178            restored.with_not_yet_added_hint(&["cube"], err).message(),
10179            "`cube` is not defined; it was added in KCL 3.0, but this program uses KCL 1.0"
10180        );
10181        ctx.close().await;
10182    }
10183
10184    // ---- mock memory reuse across settings changes ----
10185
10186    /// Runs `code` in mock execution reusing the previous memory, as the LSP
10187    /// worker does for every execution.
10188    async fn run_mock_reusing_memory(ctx: &ExecutorContext, code: &str) -> Result<ExecOutcome, KclErrorWithOutputs> {
10189        let program = crate::Program::parse_no_errs(code).unwrap();
10190        ctx.run_mock(
10191            &program,
10192            &MockConfig {
10193                use_prev_memory: true,
10194                ..Default::default()
10195            },
10196        )
10197        .await
10198    }
10199
10200    #[track_caller]
10201    fn outcome_f64(outcome: &ExecOutcome, name: &str) -> f64 {
10202        let Some(KclValueView::Number { value, .. }) = outcome.variables.get(name) else {
10203            panic!(
10204                "expected `{name}` to be a number, got {:?}",
10205                outcome.variables.get(name)
10206            );
10207        };
10208        *value
10209    }
10210
10211    /// Memory written under one kclVersion still binds declarations the other
10212    /// version skips, so a mock run reusing it must rebuild instead. Nothing
10213    /// else invalidates the LSP worker's memory.
10214    #[tokio::test(flavor = "multi_thread")]
10215    async fn mock_memory_is_rebuilt_on_a_kcl_version_downgrade() {
10216        let ctx = ExecutorContext::new_mock(None).await;
10217        let body = "@(added_in = \"3.0\")\nfn newFn() {\n  return 42\n}\n\nanswer = newFn()\n";
10218
10219        let outcome = run_mock_reusing_memory(&ctx, &format!("@settings(kclVersion = \"3.0-preview\")\n{body}"))
10220            .await
10221            .unwrap();
10222        assert_eq!(outcome_f64(&outcome, "answer"), 42.0);
10223
10224        // Only the version changes; `newFn` is still bound in the cached memory.
10225        let err = run_mock_reusing_memory(&ctx, &format!("@settings(kclVersion = 2.0)\n{body}"))
10226            .await
10227            .unwrap_err();
10228        assert_eq!(err.error.message(), "`newFn` is not defined");
10229
10230        ctx.close().await;
10231    }
10232
10233    /// A mock execution context whose project directory holds `modules`.
10234    async fn mock_context_with_modules(modules: &[(&str, &str)]) -> (ExecutorContext, tempfile::TempDir) {
10235        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_mock_memory").unwrap();
10236        for (name, source) in modules {
10237            tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
10238        }
10239        let ctx = ExecutorContext::new_mock(Some(ExecutorSettings {
10240            project_directory: Some(crate::TypedPath(tmpdir.path().into())),
10241            ..Default::default()
10242        }))
10243        .await;
10244        (ctx, tmpdir)
10245    }
10246
10247    const GATED_DEP: &str =
10248        "@(added_in = \"3.0\")\nexport fn newFn() {\n  return 42\n}\nexport fn oldFn() {\n  return 1\n}\n";
10249
10250    /// A module's cached outcome keeps the exports of the version it ran
10251    /// under, so an upgrade must rebuild to see newly available functions.
10252    #[tokio::test(flavor = "multi_thread")]
10253    async fn mock_memory_is_rebuilt_on_a_kcl_version_upgrade_with_imports() {
10254        let (ctx, _tmpdir) = mock_context_with_modules(&[("dep.kcl", GATED_DEP)]).await;
10255
10256        let outcome = run_mock_reusing_memory(
10257            &ctx,
10258            "@settings(kclVersion = 2.0)\nimport * from \"dep.kcl\"\nx = oldFn()\n",
10259        )
10260        .await
10261        .unwrap();
10262        assert_eq!(outcome_f64(&outcome, "x"), 1.0);
10263
10264        let outcome = run_mock_reusing_memory(
10265            &ctx,
10266            "@settings(kclVersion = \"3.0-preview\")\nimport * from \"dep.kcl\"\nanswer = newFn()\n",
10267        )
10268        .await
10269        .unwrap();
10270        assert_eq!(outcome_f64(&outcome, "answer"), 42.0);
10271
10272        ctx.close().await;
10273    }
10274
10275    #[tokio::test(flavor = "multi_thread")]
10276    async fn mock_memory_is_rebuilt_on_a_kcl_version_downgrade_with_imports() {
10277        let (ctx, _tmpdir) = mock_context_with_modules(&[("dep.kcl", GATED_DEP)]).await;
10278
10279        let outcome = run_mock_reusing_memory(
10280            &ctx,
10281            "@settings(kclVersion = \"3.0-preview\")\nimport newFn from \"dep.kcl\"\nanswer = newFn()\n",
10282        )
10283        .await
10284        .unwrap();
10285        assert_eq!(outcome_f64(&outcome, "answer"), 42.0);
10286
10287        let err = run_mock_reusing_memory(
10288            &ctx,
10289            "@settings(kclVersion = 2.0)\nimport newFn from \"dep.kcl\"\nanswer = newFn()\n",
10290        )
10291        .await
10292        .unwrap_err();
10293        assert_eq!(err.error.message(), "newFn is not defined in module");
10294
10295        ctx.close().await;
10296    }
10297
10298    /// Only the version decides reuse. Adding the first spline makes the
10299    /// frontend add `experimentalFeatures = allow` and then execute just that
10300    /// sketch, which needs the cached memory and cannot rebuild it.
10301    #[tokio::test(flavor = "multi_thread")]
10302    async fn mock_memory_is_reused_when_only_other_settings_change() {
10303        let ctx = ExecutorContext::new_mock(None).await;
10304
10305        run_mock_reusing_memory(&ctx, "@settings(kclVersion = 2.0)\nx = 2\n")
10306            .await
10307            .unwrap();
10308        let outcome = run_mock_reusing_memory(
10309            &ctx,
10310            "@settings(kclVersion = 2.0, experimentalFeatures = allow)\ny = x\n",
10311        )
10312        .await
10313        .unwrap();
10314        assert_eq!(outcome_f64(&outcome, "y"), 2.0);
10315
10316        ctx.close().await;
10317    }
10318
10319    /// An undeclared version is the default version, so declaring the default
10320    /// explicitly is not a change, while declaring another version is.
10321    #[tokio::test(flavor = "multi_thread")]
10322    async fn mock_memory_treats_an_undeclared_kcl_version_as_the_default() {
10323        let ctx = ExecutorContext::new_mock(None).await;
10324
10325        run_mock_reusing_memory(&ctx, "x = 2\n").await.unwrap();
10326        let outcome = run_mock_reusing_memory(&ctx, "@settings(kclVersion = 1.0)\ny = x\n")
10327            .await
10328            .unwrap();
10329        assert_eq!(outcome_f64(&outcome, "y"), 2.0);
10330        let err = run_mock_reusing_memory(&ctx, "@settings(kclVersion = 2.0)\nz = x\n")
10331            .await
10332            .unwrap_err();
10333        assert_eq!(err.error.message(), "`x` is not defined");
10334
10335        ctx.close().await;
10336    }
10337}