Skip to main content

kcl_lib/execution/
mod.rs

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