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::sketch_block_constraint_type;
10use cache::GlobalState;
11pub use cache::bust_cache;
12pub use cache::clear_mem_cache;
13pub use geometry::*;
14pub use id_generator::IdGenerator;
15pub(crate) use import::PreImportedGeometry;
16use indexmap::IndexMap;
17pub use kcl_api::Operation;
18pub use kcl_api::artifact::Artifact;
19pub use kcl_api::artifact::ArtifactGraph;
20pub use kcl_api::artifact::CapSubType;
21pub use kcl_api::artifact::CodeRef;
22pub use kcl_api::artifact::GdtAnnotationArtifact;
23pub use kcl_api::artifact::SketchBlock;
24pub use kcl_api::artifact::SketchBlockConstraint;
25#[allow(unused_imports)]
26pub use kcl_api::artifact::SketchBlockConstraintType;
27pub use kcl_api::artifact::StartSketchOnFace;
28pub use kcl_api::artifact::StartSketchOnPlane;
29use kcl_api::ast::node_path::NodePath;
30pub use kcl_value::KclObjectFields;
31pub use kcl_value::KclObjectKind;
32pub use kcl_value::KclValue;
33pub use kcl_value_view::KclValueView;
34use kcmc::ImageFormat;
35use kcmc::ModelingCmd;
36use kcmc::each_cmd as mcmd;
37use kcmc::ok_response::OkModelingCmdResponse;
38use kcmc::ok_response::output::TakeSnapshot;
39use kcmc::websocket::ModelingSessionData;
40use kcmc::websocket::OkWebSocketResponseData;
41use kittycad_modeling_cmds::id::ModelingCmdId;
42use kittycad_modeling_cmds::{self as kcmc};
43pub use memory::EnvironmentRef;
44#[cfg(test)]
45pub(crate) use memory::MemoryBackendKind;
46pub(crate) use modeling::ModelingCmdMeta;
47use serde::Deserialize;
48use serde::Serialize;
49pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
50pub(crate) use sketch_solve::solver_numeric_type;
51pub use sketch_transpiler::pre_execute_transpile;
52pub use sketch_transpiler::transpile_all_old_sketches_to_new;
53pub use sketch_transpiler::transpile_old_sketch_to_new;
54pub use sketch_transpiler::transpile_old_sketch_to_new_ast;
55pub use sketch_transpiler::transpile_old_sketch_to_new_with_execution;
56pub(crate) use solver_arc::SolverArc;
57pub(crate) use state::ConstraintKey;
58pub(crate) use state::ConstraintState;
59pub(crate) use state::ConsumedRegionInfo;
60pub(crate) use state::ConsumedRegionOperation;
61pub(crate) use state::ConsumedSolidInfo;
62pub(crate) use state::ConsumedSolidKey;
63pub(crate) use state::ConsumedSolidOperation;
64pub use state::DirectTagFilletMeta;
65pub use state::DirectTagFilletTagEntry;
66pub use state::EdgeRefactorMeta;
67pub use state::EdgeRefactorStdlibFn;
68pub use state::ExecState;
69pub(crate) use state::KclVersion;
70pub use state::MetaSettings;
71pub(crate) use state::ModuleArtifactState;
72pub(crate) use state::PendingEdgeRefactorMeta;
73pub use state::RefactorMetadata;
74pub(crate) use state::TangencyMode;
75
76use crate::CompilationIssue;
77use crate::ExecError;
78use crate::KclErrorWithOutputs;
79use crate::NodePathExt;
80use crate::SourceRange;
81use crate::collections::AhashIndexSet;
82use crate::engine::EngineBatchContext;
83use crate::engine::GridScaleBehavior;
84use crate::engine::engine_manager::EngineManager;
85use crate::errors::KclError;
86use crate::errors::KclErrorDetails;
87use crate::execution::cache::CacheInformation;
88use crate::execution::cache::CacheResult;
89use crate::execution::cad_op::OperationExt;
90use crate::execution::import_graph::Universe;
91use crate::execution::import_graph::UniverseMap;
92use crate::execution::typed_path::TypedPath;
93use crate::front::Number;
94use crate::front::Object;
95use crate::front::ObjectId;
96use crate::fs::FileManager;
97use crate::fs::FileSystemHandle;
98use crate::modules::ModuleExecutionOutcome;
99use crate::modules::ModuleId;
100use crate::modules::ModulePath;
101use crate::modules::ModuleRepr;
102use crate::parsing::ast::types::Expr;
103use crate::parsing::ast::types::ImportPath;
104use crate::parsing::ast::types::NodeRef;
105
106#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
107#[ts(export)]
108pub struct OperationsByModule {
109    pub map: IndexMap<ModuleId, Vec<Operation>>,
110}
111
112#[derive(Clone, Serialize, ts_rs::TS)]
113#[ts(export)]
114#[serde(rename_all = "camelCase")]
115pub struct OperationCallbackArgs {
116    pub module_id: ModuleId,
117    pub operation: Operation,
118    pub index: usize,
119}
120
121pub trait ExecutionCallbacks: std::fmt::Debug + Send + Sync + 'static {
122    fn on_operation(&self, _args: OperationCallbackArgs) {}
123}
124
125impl OperationsByModule {
126    pub fn count(&self) -> usize {
127        self.map.values().map(Vec::len).sum()
128    }
129
130    pub fn is_empty(&self) -> bool {
131        self.map.values().all(Vec::is_empty)
132    }
133
134    pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
135        self.map.get(module_id)
136    }
137
138    pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
139        self.map.values()
140    }
141
142    pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
143        self.map.insert(module_id, operations);
144    }
145}
146
147pub(crate) mod annotations;
148mod artifact;
149#[cfg(test)]
150pub(crate) use artifact::mermaid_tests::ArtifactGraphMermaidExt;
151pub(crate) mod cache;
152mod cad_op;
153mod exec_ast;
154pub mod fn_call;
155#[cfg(test)]
156mod freedom_analysis_tests;
157mod geometry;
158mod id_generator;
159mod import;
160mod import_graph;
161pub(crate) mod kcl_value;
162pub(crate) mod kcl_value_view;
163mod memory;
164mod modeling;
165mod sketch_solve;
166mod sketch_transpiler;
167mod solver_arc;
168mod state;
169pub mod typed_path;
170pub(crate) mod types;
171
172pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
173pub(crate) const SKETCH_OBJECT_META: &str = "meta";
174pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
175
176/// Convenience macro for handling [`KclValueControlFlow`] in execution by
177/// returning early if it is some kind of early return or stripping off the
178/// control flow otherwise. If it's an early return, it's returned as a
179/// `Result::Ok`.
180macro_rules! control_continue {
181    ($control_flow:expr) => {{
182        let cf = $control_flow;
183        if cf.is_some_return() {
184            return Ok(cf);
185        } else {
186            cf.into_value()
187        }
188    }};
189}
190// Expose the macro to other modules.
191pub(crate) use control_continue;
192
193/// Convenience macro for handling [`KclValueControlFlow`] in execution by
194/// returning early if it is some kind of early return or stripping off the
195/// control flow otherwise. If it's an early return, [`EarlyReturn`] is
196/// used to return it as a `Result::Err`.
197macro_rules! early_return {
198    ($control_flow:expr) => {{
199        let cf = $control_flow;
200        if cf.is_some_return() {
201            return Err(EarlyReturn::from(cf));
202        } else {
203            cf.into_value()
204        }
205    }};
206}
207// Expose the macro to other modules.
208pub(crate) use early_return;
209
210#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
211pub enum ControlFlowKind {
212    #[default]
213    Continue,
214    Exit,
215}
216
217impl ControlFlowKind {
218    /// Returns true if this is any kind of early return.
219    pub fn is_some_return(&self) -> bool {
220        match self {
221            ControlFlowKind::Continue => false,
222            ControlFlowKind::Exit => true,
223        }
224    }
225}
226
227#[must_use = "You should always handle the control flow value when it is returned"]
228#[derive(Debug, Clone, PartialEq, Serialize)]
229pub struct KclValueControlFlow {
230    /// Use [control_continue] or [Self::into_value] to get the value.
231    value: Box<KclValue>,
232    pub control: ControlFlowKind,
233}
234
235impl KclValue {
236    pub(crate) fn continue_(self) -> KclValueControlFlow {
237        KclValueControlFlow {
238            value: Box::new(self),
239            control: ControlFlowKind::Continue,
240        }
241    }
242
243    pub(crate) fn exit(self) -> KclValueControlFlow {
244        KclValueControlFlow {
245            value: Box::new(self),
246            control: ControlFlowKind::Exit,
247        }
248    }
249}
250
251impl KclValueControlFlow {
252    /// Returns true if this is any kind of early return.
253    pub fn is_some_return(&self) -> bool {
254        self.control.is_some_return()
255    }
256
257    pub(crate) fn into_value(self) -> KclValue {
258        *self.value
259    }
260}
261
262/// A [`KclValueControlFlow`] or an error that needs to be returned early. This
263/// is useful for when functions might encounter either control flow or errors
264/// that need to bubble up early, but these aren't the primary return values of
265/// the function. We can use `EarlyReturn` as the error type in a `Result`.
266///
267/// Normally, you don't construct this directly. Use the `early_return!` macro.
268#[must_use = "You should always handle the control flow value when it is returned"]
269#[allow(clippy::large_enum_variant)]
270#[derive(Debug, Clone)]
271pub(crate) enum EarlyReturn {
272    /// A normal value with control flow.
273    Value(KclValueControlFlow),
274    /// An error that occurred during execution.
275    Error(KclError),
276}
277
278impl From<KclValueControlFlow> for EarlyReturn {
279    fn from(cf: KclValueControlFlow) -> Self {
280        EarlyReturn::Value(cf)
281    }
282}
283
284impl From<KclError> for EarlyReturn {
285    fn from(err: KclError) -> Self {
286        EarlyReturn::Error(err)
287    }
288}
289
290pub(crate) enum StatementKind<'a> {
291    Declaration { name: &'a str },
292    Expression,
293}
294
295#[derive(Debug, Clone, Copy)]
296pub enum PreserveMem {
297    Normal,
298    Always,
299}
300
301impl PreserveMem {
302    fn normal(self) -> bool {
303        match self {
304            PreserveMem::Normal => true,
305            PreserveMem::Always => false,
306        }
307    }
308}
309
310/// Outcome of executing a program.  This is used in TS.
311#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
312#[ts(export)]
313#[serde(rename_all = "camelCase")]
314pub struct ExecOutcome {
315    /// Variables in the top-level of the root module. Note that functions will have an invalid env ref.
316    pub variables: IndexMap<String, KclValueView>,
317    /// Operations that have been performed in execution order, grouped by
318    /// owning module id, for display in the Feature Tree.
319    pub operations: OperationsByModule,
320    /// Output artifact graph.
321    pub artifact_graph: ArtifactGraph,
322    /// Objects in the scene, created from execution.
323    #[serde(skip)]
324    pub scene_objects: Vec<Object>,
325    /// Map from source range to object ID for lookup of objects by their source
326    /// range.
327    #[serde(skip)]
328    pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
329    #[serde(skip)]
330    pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
331    /// Execution-backed metadata used by Z0006 and future auto-refactors.
332    pub refactor_metadata: Vec<RefactorMetadata>,
333    /// Non-fatal errors and warnings.
334    pub issues: Vec<CompilationIssue>,
335    /// File Names in module Id array index order
336    pub filenames: IndexMap<ModuleId, ModulePath>,
337    /// The default planes.
338    pub default_planes: Option<DefaultPlanes>,
339}
340
341/// Per-segment freedom used by the constraint report. Mirrors
342/// [`crate::front::Freedom`] but adds an `Error` variant for when
343/// a point lookup fails.
344#[derive(Debug, Clone, Copy, PartialEq)]
345enum SegmentFreedom {
346    Free,
347    Fixed,
348    Conflict,
349    /// A required point could not be found in the scene graph.
350    Error,
351}
352
353impl From<crate::front::Freedom> for SegmentFreedom {
354    fn from(f: crate::front::Freedom) -> Self {
355        match f {
356            crate::front::Freedom::Free => Self::Free,
357            crate::front::Freedom::Fixed => Self::Fixed,
358            crate::front::Freedom::Conflict => Self::Conflict,
359        }
360    }
361}
362
363/// Overall constraint status of a sketch.
364#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
365pub enum ConstraintKind {
366    FullyConstrained,
367    UnderConstrained,
368    OverConstrained,
369    /// Analysis could not determine constraint status (e.g., a point lookup
370    /// failed due to an inconsistent scene graph). Callers decide how to treat
371    /// this — as under-constrained, over-constrained, or something else.
372    Error,
373}
374
375/// Per-sketch summary of constraint freedom analysis.
376///
377/// A sketch with no countable segments (`total_count == 0`) is reported as
378/// [`ConstraintKind::FullyConstrained`]. This is vacuously true — there are
379/// no free or conflicting segments. Callers can check `total_count == 0` to
380/// distinguish this from a genuinely constrained sketch.
381#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
382pub struct SketchConstraintStatus {
383    /// The variable name of the sketch (e.g., "sketch001").
384    pub name: String,
385    /// Overall constraint status derived from per-segment freedom.
386    pub status: ConstraintKind,
387    /// Number of segments that are under-constrained (free to move).
388    pub free_count: usize,
389    /// Number of segments that are over-constrained (conflicting constraints).
390    pub conflict_count: usize,
391    /// Total number of segments analyzed.
392    pub total_count: usize,
393}
394
395/// Grouped report of all sketches by constraint status.
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
397pub struct SketchConstraintReport {
398    pub fully_constrained: Vec<SketchConstraintStatus>,
399    pub under_constrained: Vec<SketchConstraintStatus>,
400    pub over_constrained: Vec<SketchConstraintStatus>,
401    /// Sketches where analysis encountered an error (e.g., a point lookup
402    /// failed). Callers decide how to treat these.
403    pub errors: Vec<SketchConstraintStatus>,
404}
405
406/// Compute the constraint status for a single sketch object.
407///
408/// Returns `None` if `sketch_obj` is not a sketch.
409///
410/// Note: a sketch with no countable segments (`total_count == 0`) is reported
411/// as [`ConstraintKind::FullyConstrained`]. This is vacuously true — there are
412/// no free or conflicting segments. Callers can check `total_count == 0` to
413/// distinguish this from a genuinely constrained sketch.
414pub(crate) fn sketch_constraint_status_for_sketch(
415    scene_objects: &[Object],
416    sketch_obj: &Object,
417) -> Option<SketchConstraintStatus> {
418    use crate::front::ObjectKind;
419    use crate::front::Segment;
420
421    let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
422        return None;
423    };
424
425    // Closure to look up a point's freedom by ObjectId.
426    let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
427        let obj = scene_objects.get(id.0)?;
428        if let ObjectKind::Segment {
429            segment: Segment::Point(p),
430        } = &obj.kind
431        {
432            Some(p.freedom())
433        } else {
434            None
435        }
436    };
437
438    let mut free_count: usize = 0;
439    let mut conflict_count: usize = 0;
440    let mut error_count: usize = 0;
441    let mut total_count: usize = 0;
442
443    for &seg_id in &sketch.segments {
444        let Some(seg_obj) = scene_objects.get(seg_id.0) else {
445            continue;
446        };
447        let ObjectKind::Segment { segment } = &seg_obj.kind else {
448            continue;
449        };
450        // Skip owned points — their freedom is already captured by
451        // the parent geometry (Line/Arc/Circle) that looks them up.
452        if let Segment::Point(p) = segment
453            && p.owner.is_some()
454        {
455            continue;
456        }
457        let freedom = segment
458            .freedom(lookup)
459            .map(SegmentFreedom::from)
460            .unwrap_or(SegmentFreedom::Error);
461        total_count += 1;
462        match freedom {
463            SegmentFreedom::Free => free_count += 1,
464            SegmentFreedom::Conflict => conflict_count += 1,
465            SegmentFreedom::Error => error_count += 1,
466            SegmentFreedom::Fixed => {}
467        }
468    }
469
470    let status = if error_count > 0 {
471        ConstraintKind::Error
472    } else if conflict_count > 0 {
473        ConstraintKind::OverConstrained
474    } else if free_count > 0 {
475        ConstraintKind::UnderConstrained
476    } else {
477        ConstraintKind::FullyConstrained
478    };
479
480    Some(SketchConstraintStatus {
481        name: sketch_obj.label.clone(),
482        status,
483        free_count,
484        conflict_count,
485        total_count,
486    })
487}
488
489pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
490    let mut fully_constrained = Vec::new();
491    let mut under_constrained = Vec::new();
492    let mut over_constrained = Vec::new();
493    let mut errors = Vec::new();
494
495    for obj in scene_objects {
496        let Some(entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
497            continue;
498        };
499        match entry.status {
500            ConstraintKind::FullyConstrained => fully_constrained.push(entry),
501            ConstraintKind::UnderConstrained => under_constrained.push(entry),
502            ConstraintKind::OverConstrained => over_constrained.push(entry),
503            ConstraintKind::Error => errors.push(entry),
504        }
505    }
506
507    SketchConstraintReport {
508        fully_constrained,
509        under_constrained,
510        over_constrained,
511        errors,
512    }
513}
514
515impl ExecOutcome {
516    pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
517        debug_assert!(
518            id.0 < self.scene_objects.len(),
519            "Requested object ID {} but only have {} objects",
520            id.0,
521            self.scene_objects.len()
522        );
523        self.scene_objects.get(id.0)
524    }
525
526    /// Returns non-fatal errors. Warnings are not included.
527    pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
528        self.issues.iter().filter(|error| error.is_err())
529    }
530
531    /// Analyze all sketches in the execution result and group them by
532    /// constraint status (fully, under, or over constrained).
533    ///
534    /// Each segment in a sketch computes its own freedom by looking up the
535    /// freedom of its constituent points. Owned points (belonging to a
536    /// Line/Arc/Circle) are skipped to avoid double-counting.
537    pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
538        sketch_constraint_report_from_scene_objects(&self.scene_objects)
539    }
540}
541
542/// Configuration for mock execution.
543#[derive(Debug, Clone, PartialEq)]
544pub struct MockConfig {
545    pub use_prev_memory: bool,
546    /// The `ObjectId` of the sketch block to execute for sketch mode. Only the
547    /// specified sketch block will be executed. All other code is ignored.
548    pub sketch_block_id: Option<ObjectId>,
549    /// True to do more costly analysis of whether the sketch block segments are
550    /// under-constrained.
551    pub freedom_analysis: bool,
552    /// The segments that were edited that triggered this execution.
553    pub segment_ids_edited: AhashIndexSet<ObjectId>,
554    /// Segment-body drag anchors that temporarily pull a point on a segment toward the cursor.
555    pub drag_anchors: Vec<SegmentDragAnchor>,
556}
557
558#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
559#[ts(export, export_to = "FrontendApi.ts")]
560#[serde(rename_all = "camelCase")]
561pub struct SegmentDragAnchor {
562    pub segment_id: ObjectId,
563    pub target: crate::front::Point2d<Number>,
564}
565
566impl Default for MockConfig {
567    fn default() -> Self {
568        Self {
569            // By default, use previous memory. This is usually what you want.
570            use_prev_memory: true,
571            sketch_block_id: None,
572            freedom_analysis: true,
573            segment_ids_edited: AhashIndexSet::default(),
574            drag_anchors: Vec::new(),
575        }
576    }
577}
578
579impl MockConfig {
580    /// Create a new mock config for sketch mode.
581    pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
582        Self {
583            sketch_block_id: Some(sketch_block_id),
584            ..Default::default()
585        }
586    }
587
588    #[must_use]
589    pub(crate) fn no_freedom_analysis(mut self) -> Self {
590        self.freedom_analysis = false;
591        self
592    }
593}
594
595#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
596#[ts(export)]
597#[serde(rename_all = "camelCase")]
598pub struct DefaultPlanes {
599    pub xy: uuid::Uuid,
600    pub xz: uuid::Uuid,
601    pub yz: uuid::Uuid,
602    pub neg_xy: uuid::Uuid,
603    pub neg_xz: uuid::Uuid,
604    pub neg_yz: uuid::Uuid,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
608#[ts(export)]
609#[serde(tag = "type", rename_all = "camelCase")]
610pub struct TagIdentifier {
611    pub value: String,
612    // Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info
613    // was written.
614    #[serde(skip)]
615    pub info: Vec<(usize, TagEngineInfo)>,
616    #[serde(skip)]
617    pub meta: Vec<Metadata>,
618}
619
620impl TagIdentifier {
621    /// Get the tag info for this tag at a specified epoch.
622    pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
623        for (e, info) in self.info.iter().rev() {
624            if *e <= at_epoch {
625                return Some(info);
626            }
627        }
628
629        None
630    }
631
632    /// Get the most recent tag info for this tag.
633    pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
634        self.info.last().map(|i| &i.1)
635    }
636
637    /// Get all tag info entries at the most recent epoch.
638    /// For region-mapped tags, this returns multiple entries (one per region segment).
639    pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
640        let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
641            return vec![];
642        };
643        self.info
644            .iter()
645            .rev()
646            .take_while(|(e, _)| *e == cur_epoch)
647            .map(|(_, info)| info)
648            .collect()
649    }
650
651    /// Add info from a different instance of this tag.
652    pub fn merge_info(&mut self, other: &TagIdentifier) {
653        assert_eq!(&self.value, &other.value);
654        for (oe, ot) in &other.info {
655            if let Some((e, t)) = self.info.last_mut() {
656                // If there is newer info, then skip this iteration.
657                if *e > *oe {
658                    continue;
659                }
660                // If we're in the same epoch, then overwrite.
661                if e == oe {
662                    *t = ot.clone();
663                    continue;
664                }
665            }
666            self.info.push((*oe, ot.clone()));
667        }
668    }
669
670    pub fn geometry(&self) -> Option<Geometry> {
671        self.get_cur_info().map(|info| info.geometry.clone())
672    }
673
674    pub(crate) fn is_body_created_tag(&self) -> bool {
675        self.get_cur_info().is_some_and(|info| {
676            matches!(&info.geometry, Geometry::Solid(_)) && info.path.is_none() && info.surface.is_some()
677        })
678    }
679}
680
681impl Eq for TagIdentifier {}
682
683impl std::fmt::Display for TagIdentifier {
684    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685        write!(f, "{}", self.value)
686    }
687}
688
689impl std::str::FromStr for TagIdentifier {
690    type Err = KclError;
691
692    fn from_str(s: &str) -> Result<Self, Self::Err> {
693        Ok(Self {
694            value: s.to_string(),
695            info: Vec::new(),
696            meta: Default::default(),
697        })
698    }
699}
700
701impl Ord for TagIdentifier {
702    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
703        self.value.cmp(&other.value)
704    }
705}
706
707impl PartialOrd for TagIdentifier {
708    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
709        Some(self.cmp(other))
710    }
711}
712
713impl std::hash::Hash for TagIdentifier {
714    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
715        self.value.hash(state);
716    }
717}
718
719/// Engine information for a tag.
720#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
721#[ts(export)]
722#[serde(tag = "type", rename_all = "camelCase")]
723pub struct TagEngineInfo {
724    /// The id of the tagged object.
725    pub id: uuid::Uuid,
726    /// The geometry the tag is on.
727    pub geometry: Geometry,
728    /// The path the tag is on.
729    pub path: Option<Path>,
730    /// The surface information for the tag.
731    pub surface: Option<ExtrudeSurface>,
732}
733
734#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
735pub enum BodyType {
736    Root,
737    Block,
738}
739
740/// Metadata.
741#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
742#[ts(export)]
743#[serde(rename_all = "camelCase")]
744pub struct Metadata {
745    /// The source range.
746    pub source_range: SourceRange,
747}
748
749impl From<Metadata> for Vec<SourceRange> {
750    fn from(meta: Metadata) -> Self {
751        vec![meta.source_range]
752    }
753}
754
755impl From<&Metadata> for SourceRange {
756    fn from(meta: &Metadata) -> Self {
757        meta.source_range
758    }
759}
760
761impl From<SourceRange> for Metadata {
762    fn from(source_range: SourceRange) -> Self {
763        Self { source_range }
764    }
765}
766
767impl<T> From<NodeRef<'_, T>> for Metadata {
768    fn from(node: NodeRef<'_, T>) -> Self {
769        Self {
770            source_range: SourceRange::new(node.start, node.end, node.module_id),
771        }
772    }
773}
774
775impl From<&Expr> for Metadata {
776    fn from(expr: &Expr) -> Self {
777        Self {
778            source_range: SourceRange::from(expr),
779        }
780    }
781}
782
783impl Metadata {
784    pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
785        if meta.len() == 1 {
786            let meta = &meta[0];
787            return crate::front::SourceRef::Simple {
788                range: meta.source_range,
789                node_path,
790            };
791        }
792        crate::front::SourceRef::BackTrace {
793            ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
794        }
795    }
796}
797
798/// The type of ExecutorContext being used
799#[derive(PartialEq, Debug, Default, Clone)]
800pub enum ContextType {
801    /// Live engine connection
802    #[default]
803    Live,
804
805    /// Completely mocked connection
806    /// Mock mode is only for the Design Studio when they just want to mock engine calls and not
807    /// actually make them.
808    Mock,
809
810    /// Handled by some other interpreter/conversion system
811    MockCustomForwarded,
812}
813
814/// The executor context.
815/// Cloning will return another handle to the same engine connection/session,
816/// as this uses `Arc` under the hood.
817#[derive(Clone)]
818pub struct ExecutorContext {
819    pub engine: Arc<EngineManager>,
820    pub engine_batch: EngineBatchContext,
821    pub fs: FileSystemHandle,
822    pub settings: ExecutorSettings,
823    pub context_type: ContextType,
824    pub execution_callbacks: Option<Arc<dyn ExecutionCallbacks>>,
825}
826
827impl std::fmt::Debug for ExecutorContext {
828    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
829        f.debug_struct("ExecutorContext")
830            .field("engine", &self.engine)
831            .field("engine_batch", &self.engine_batch)
832            .field("settings", &self.settings)
833            .field("context_type", &self.context_type)
834            .field("execution_callbacks", &self.execution_callbacks)
835            .finish()
836    }
837}
838
839/// The executor settings.
840#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
841#[ts(export)]
842pub struct ExecutorSettings {
843    /// Highlight edges of 3D objects?
844    pub highlight_edges: bool,
845    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
846    pub enable_ssao: bool,
847    /// Show grid?
848    pub show_grid: bool,
849    /// Should engine store this for replay?
850    /// If so, under what name?
851    pub replay: Option<String>,
852    /// The directory of the current project.  This is used for resolving import
853    /// paths.  If None is given, the current working directory is used.
854    pub project_directory: Option<TypedPath>,
855    /// This is the path to the current file being executed.
856    /// We use this for preventing cyclic imports.
857    pub current_file: Option<TypedPath>,
858    /// Whether or not to automatically scale the grid when user zooms.
859    pub fixed_size_grid: bool,
860    /// Skip sending the engine messages that are only needed to build the
861    /// artifact graph. When this is true, the artifact graph will be
862    /// incomplete. So you should only use this option if you know you don't
863    /// need the artifact graph or anything that depends on it. In that case,
864    /// skipping these commands can make execution slightly faster.
865    #[serde(default, skip_serializing_if = "is_false")]
866    pub skip_artifact_graph: bool,
867    /// If Some(N), sends a heartbeat to keep the WebSocket active, every N seconds.
868    /// If None, no heartbeats will be sent.
869    #[serde(default, skip_serializing_if = "Option::is_none")]
870    pub heartbeats: Option<u64>,
871    /// If given, sets the default backface colour.
872    /// If not, defaults to whatever the engine's default is.
873    #[serde(default, skip_serializing_if = "Option::is_none")]
874    pub default_backface_color: Option<String>,
875}
876
877fn is_false(b: &bool) -> bool {
878    !*b
879}
880
881impl Default for ExecutorSettings {
882    fn default() -> Self {
883        Self {
884            highlight_edges: true,
885            enable_ssao: false,
886            show_grid: false,
887            replay: None,
888            project_directory: None,
889            current_file: None,
890            fixed_size_grid: true,
891            skip_artifact_graph: false,
892            heartbeats: None,
893            default_backface_color: None,
894        }
895    }
896}
897
898impl From<crate::settings::types::Configuration> for ExecutorSettings {
899    fn from(config: crate::settings::types::Configuration) -> Self {
900        Self::from(config.settings)
901    }
902}
903
904impl From<crate::settings::types::Settings> for ExecutorSettings {
905    fn from(settings: crate::settings::types::Settings) -> Self {
906        let modeling_settings = settings.modeling.unwrap_or_default();
907        Self {
908            highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
909            enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
910            show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
911            replay: None,
912            project_directory: None,
913            current_file: None,
914            fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
915            skip_artifact_graph: false,
916            heartbeats: None,
917            default_backface_color: modeling_settings.backface_color.map(|color| color.0),
918        }
919    }
920}
921
922impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
923    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
924        Self::from(config.settings.modeling)
925    }
926}
927
928impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
929    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
930        Self {
931            highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
932            enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
933            show_grid: modeling.show_scale_grid.unwrap_or_default(),
934            replay: None,
935            project_directory: None,
936            current_file: None,
937            fixed_size_grid: true,
938            skip_artifact_graph: false,
939            heartbeats: None,
940            default_backface_color: modeling.backface_color.map(|color| color.0),
941        }
942    }
943}
944
945impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
946    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
947        Self {
948            highlight_edges: modeling.highlight_edges.into(),
949            enable_ssao: modeling.enable_ssao.into(),
950            show_grid: Default::default(),
951            replay: None,
952            project_directory: None,
953            current_file: None,
954            fixed_size_grid: true,
955            skip_artifact_graph: false,
956            heartbeats: None,
957            default_backface_color: None,
958        }
959    }
960}
961
962impl ExecutorSettings {
963    /// Add the current file path to the executor settings.
964    pub fn with_current_file(&mut self, current_file: TypedPath) {
965        // We want the parent directory of the file.
966        if current_file.extension() == Some("kcl") {
967            self.current_file = Some(current_file.clone());
968            // Get the parent directory.
969            if let Some(parent) = current_file.parent() {
970                self.project_directory = Some(parent);
971            } else {
972                self.project_directory = Some(TypedPath::from(""));
973            }
974        } else {
975            self.project_directory = Some(current_file);
976        }
977    }
978}
979
980impl ExecutorContext {
981    /// Create a new live executor context from an engine and file manager.
982    pub fn new_with_engine_and_fs(
983        engine: Arc<EngineManager>,
984        fs: FileSystemHandle,
985        settings: ExecutorSettings,
986    ) -> Self {
987        ExecutorContext {
988            engine,
989            engine_batch: EngineBatchContext::default(),
990            fs,
991            settings,
992            context_type: ContextType::Live,
993            execution_callbacks: Default::default(),
994        }
995    }
996
997    fn clone_with_fresh_execution_batch(&self) -> Self {
998        Self {
999            engine: self.engine.clone(),
1000            engine_batch: EngineBatchContext::new(),
1001            fs: self.fs.clone(),
1002            settings: self.settings.clone(),
1003            context_type: self.context_type.clone(),
1004            execution_callbacks: self.execution_callbacks.clone(),
1005        }
1006    }
1007
1008    /// Create a new live executor context from an engine using the local file manager.
1009    #[cfg(not(target_arch = "wasm32"))]
1010    pub fn new_with_engine(engine: Arc<EngineManager>, settings: ExecutorSettings) -> Self {
1011        Self::new_with_engine_and_fs(engine, crate::fs::new_file_system_handle(FileManager::new()), settings)
1012    }
1013
1014    /// Create a new default executor context.
1015    #[cfg(not(target_arch = "wasm32"))]
1016    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
1017        let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
1018        let (ws, _headers) = client
1019            .modeling()
1020            .commands_ws(kittycad::modeling::CommandsWsParams {
1021                api_call_id: None,
1022                fps: None,
1023                order_independent_transparency: None,
1024                post_effect: if settings.enable_ssao {
1025                    Some(kittycad::types::PostEffectType::Ssao)
1026                } else {
1027                    None
1028                },
1029                replay: settings.replay.clone(),
1030                show_grid: if settings.show_grid { Some(true) } else { None },
1031                pool: None,
1032                pr,
1033                unlocked_framerate: None,
1034                webrtc: Some(false),
1035                video_res_width: None,
1036                video_res_height: None,
1037            })
1038            .await?;
1039
1040        let engine_conn = EngineManager::new_websocket_transport(ws, settings.heartbeats).await;
1041        let engine = Arc::new(engine_conn);
1042
1043        Ok(Self::new_with_engine(engine, settings))
1044    }
1045
1046    #[cfg(target_arch = "wasm32")]
1047    pub fn new(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1048        Self::new_with_engine_and_fs(engine, fs, settings)
1049    }
1050
1051    #[cfg(not(target_arch = "wasm32"))]
1052    pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
1053        ExecutorContext {
1054            engine: Arc::new(EngineManager::new_mock()),
1055            engine_batch: EngineBatchContext::default(),
1056            fs: crate::fs::new_file_system_handle(FileManager::new()),
1057            settings: settings.unwrap_or_default(),
1058            context_type: ContextType::Mock,
1059            execution_callbacks: Default::default(),
1060        }
1061    }
1062
1063    #[cfg(target_arch = "wasm32")]
1064    pub fn new_mock(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1065        ExecutorContext {
1066            engine,
1067            engine_batch: EngineBatchContext::default(),
1068            fs,
1069            settings,
1070            context_type: ContextType::Mock,
1071            execution_callbacks: Default::default(),
1072        }
1073    }
1074
1075    /// Create a new mock executor context for WASM LSP servers.
1076    /// This is a convenience function that creates a mock engine and FileManager from a FileSystemManager.
1077    #[cfg(target_arch = "wasm32")]
1078    pub fn new_mock_for_lsp(
1079        fs_manager: crate::fs::wasm::FileSystemManager,
1080        settings: ExecutorSettings,
1081    ) -> Result<Self, String> {
1082        let fs = crate::fs::new_file_system_handle(FileManager::new(fs_manager));
1083
1084        Ok(ExecutorContext {
1085            engine: Arc::new(EngineManager::new_mock()),
1086            engine_batch: EngineBatchContext::default(),
1087            fs,
1088            settings,
1089            context_type: ContextType::Mock,
1090            execution_callbacks: Default::default(),
1091        })
1092    }
1093
1094    #[cfg(not(target_arch = "wasm32"))]
1095    pub fn new_forwarded_mock(engine: Arc<EngineManager>) -> Self {
1096        ExecutorContext {
1097            engine,
1098            engine_batch: EngineBatchContext::default(),
1099            fs: crate::fs::new_file_system_handle(FileManager::new()),
1100            settings: Default::default(),
1101            context_type: ContextType::MockCustomForwarded,
1102            execution_callbacks: Default::default(),
1103        }
1104    }
1105
1106    /// Create a new default executor context.
1107    /// With a kittycad client.
1108    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
1109    /// variables.
1110    /// But also allows for passing in a token and engine address directly.
1111    #[cfg(not(target_arch = "wasm32"))]
1112    pub async fn new_with_client(
1113        settings: ExecutorSettings,
1114        token: Option<String>,
1115        engine_addr: Option<String>,
1116    ) -> Result<Self> {
1117        // Create the client.
1118        let client = crate::engine::new_zoo_client(token, engine_addr)?;
1119
1120        let ctx = Self::new(&client, settings).await?;
1121        Ok(ctx)
1122    }
1123
1124    /// Create a new default executor context.
1125    /// With the default kittycad client.
1126    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
1127    /// variables.
1128    #[cfg(not(target_arch = "wasm32"))]
1129    pub async fn new_with_default_client() -> Result<Self> {
1130        // Create the client.
1131        let ctx = Self::new_with_client(Default::default(), None, None).await?;
1132        Ok(ctx)
1133    }
1134
1135    /// For executing unit tests.
1136    #[cfg(not(target_arch = "wasm32"))]
1137    pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
1138        let ctx = ExecutorContext::new_with_client(
1139            ExecutorSettings {
1140                highlight_edges: true,
1141                enable_ssao: false,
1142                show_grid: false,
1143                replay: None,
1144                project_directory: None,
1145                current_file: None,
1146                fixed_size_grid: false,
1147                skip_artifact_graph: false,
1148                heartbeats: None,
1149                default_backface_color: None,
1150            },
1151            None,
1152            engine_addr,
1153        )
1154        .await?;
1155        Ok(ctx)
1156    }
1157
1158    pub fn is_mock(&self) -> bool {
1159        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1160    }
1161
1162    /// Returns true if we should not send engine commands for any reason.
1163    pub async fn no_engine_commands(&self) -> bool {
1164        self.is_mock()
1165    }
1166
1167    pub async fn send_clear_scene(
1168        &self,
1169        exec_state: &mut ExecState,
1170        source_range: crate::execution::SourceRange,
1171    ) -> Result<(), KclError> {
1172        // Ensure artifacts are cleared so that we don't accumulate them across
1173        // runs.
1174        exec_state.mod_local.artifacts.clear();
1175        exec_state.global.root_module_artifacts.clear();
1176        exec_state.global.artifacts.clear();
1177
1178        self.engine
1179            .clear_scene(&self.engine_batch, &mut exec_state.mod_local.id_generator, source_range)
1180            .await?;
1181        // The engine errors out if you toggle OIT with SSAO off.
1182        // So ignore OIT settings if SSAO is off.
1183        if self.settings.enable_ssao {
1184            let cmd_id = exec_state.next_uuid();
1185            exec_state
1186                .batch_modeling_cmd(
1187                    ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1188                    ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1189                )
1190                .await?;
1191        }
1192        Ok(())
1193    }
1194
1195    pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1196        cache::bust_cache().await;
1197
1198        // Execute an empty program to clear and reset the scene.
1199        // We specifically want to be returned the objects after the scene is reset.
1200        // Like the default planes so it is easier to just execute an empty program
1201        // after the cache is busted.
1202        let outcome = self.run_with_caching(crate::Program::empty()).await?;
1203
1204        Ok(outcome)
1205    }
1206
1207    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1208        self.eval_prelude(exec_state, SourceRange::synthetic())
1209            .await
1210            .map_err(KclErrorWithOutputs::no_outputs)?;
1211        exec_state
1212            .mut_stack()
1213            .push_new_root_env(true)
1214            .map_err(KclErrorWithOutputs::no_outputs)?;
1215        Ok(())
1216    }
1217
1218    fn restore_mock_memory(
1219        exec_state: &mut ExecState,
1220        mem: cache::SketchModeState,
1221        _mock_config: &MockConfig,
1222    ) -> Result<(), KclErrorWithOutputs> {
1223        *exec_state.mut_stack() = mem.stack;
1224        exec_state.global.module_infos = mem.module_infos;
1225        exec_state.global.path_to_source_id = mem.path_to_source_id;
1226        exec_state.global.id_to_source = mem.id_to_source;
1227        exec_state.mod_local.constraint_state = mem.constraint_state;
1228        let len = _mock_config
1229            .sketch_block_id
1230            .map(|sketch_block_id| sketch_block_id.0)
1231            .unwrap_or(0);
1232        if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1233            exec_state
1234                .global
1235                .root_module_artifacts
1236                .restore_scene_objects(scene_objects);
1237        } else {
1238            let message = format!(
1239                "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1240                mem.scene_objects.len(),
1241                len
1242            );
1243            debug_assert!(false, "{message}");
1244            return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1245                KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1246            )));
1247        }
1248
1249        Ok(())
1250    }
1251
1252    pub async fn run_mock(
1253        &self,
1254        program: &crate::Program,
1255        mock_config: &MockConfig,
1256    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1257        assert!(
1258            self.is_mock(),
1259            "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1260        );
1261
1262        let use_prev_memory = mock_config.use_prev_memory;
1263        let mut exec_state = ExecState::new_mock(self, mock_config);
1264        if use_prev_memory {
1265            match cache::read_old_memory().await {
1266                Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
1267                None => self.prepare_mem(&mut exec_state).await?,
1268            }
1269        } else {
1270            self.prepare_mem(&mut exec_state).await?
1271        };
1272
1273        // Push a scope so that old variables can be overwritten (since we might be re-executing some
1274        // part of the scene).
1275        exec_state
1276            .mut_stack()
1277            .push_new_env_for_scope()
1278            .map_err(KclErrorWithOutputs::no_outputs)?;
1279
1280        let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1281
1282        // Restore any temporary variables, then save any newly created variables back to
1283        // memory in case another run wants to use them. Note this is just saved to the preserved
1284        // memory, not to the exec_state which is not cached for mock execution.
1285
1286        let mut stack = exec_state.stack().clone();
1287        let module_infos = exec_state.global.module_infos.clone();
1288        let path_to_source_id = exec_state.global.path_to_source_id.clone();
1289        let id_to_source = exec_state.global.id_to_source.clone();
1290        let constraint_state = exec_state.mod_local.constraint_state.clone();
1291        let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1292        let outcome = exec_state
1293            .into_exec_outcome(result.0, self)
1294            .await
1295            .map_err(KclErrorWithOutputs::no_outputs)?;
1296
1297        stack.squash_env(result.0).map_err(KclErrorWithOutputs::no_outputs)?;
1298        let state = cache::SketchModeState {
1299            stack,
1300            module_infos,
1301            path_to_source_id,
1302            id_to_source,
1303            constraint_state,
1304            scene_objects,
1305        };
1306        cache::write_old_memory(state).await;
1307
1308        Ok(outcome)
1309    }
1310
1311    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1312        assert!(!self.is_mock());
1313        let grid_scale = if self.settings.fixed_size_grid {
1314            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1315        } else {
1316            GridScaleBehavior::ScaleWithZoom
1317        };
1318
1319        let original_program = program.clone();
1320
1321        let (_program, exec_state, result) = match cache::read_old_ast().await {
1322            Some(mut cached_state) => {
1323                let old = CacheInformation {
1324                    ast: &cached_state.main.ast,
1325                    settings: &cached_state.settings,
1326                };
1327                let new = CacheInformation {
1328                    ast: &program.ast,
1329                    settings: &self.settings,
1330                };
1331
1332                // Get the program that actually changed from the old and new information.
1333                let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1334                    CacheResult::ReExecute {
1335                        clear_scene,
1336                        reapply_settings,
1337                        program: changed_program,
1338                    } => {
1339                        if reapply_settings
1340                            && self
1341                                .engine
1342                                .reapply_settings(
1343                                    &self.engine_batch,
1344                                    &self.settings,
1345                                    Default::default(),
1346                                    &mut cached_state.main.exec_state.id_generator,
1347                                    grid_scale,
1348                                )
1349                                .await
1350                                .is_err()
1351                        {
1352                            (true, program, None)
1353                        } else {
1354                            (
1355                                clear_scene,
1356                                crate::Program {
1357                                    ast: changed_program,
1358                                    original_file_contents: program.original_file_contents,
1359                                },
1360                                None,
1361                            )
1362                        }
1363                    }
1364                    CacheResult::CheckImportsOnly {
1365                        reapply_settings,
1366                        ast: changed_program,
1367                    } => {
1368                        let mut reapply_failed = false;
1369                        if reapply_settings {
1370                            if self
1371                                .engine
1372                                .reapply_settings(
1373                                    &self.engine_batch,
1374                                    &self.settings,
1375                                    Default::default(),
1376                                    &mut cached_state.main.exec_state.id_generator,
1377                                    grid_scale,
1378                                )
1379                                .await
1380                                .is_ok()
1381                            {
1382                                cache::write_old_ast(GlobalState::with_settings(
1383                                    cached_state.clone(),
1384                                    self.settings.clone(),
1385                                ))
1386                                .await;
1387                            } else {
1388                                reapply_failed = true;
1389                            }
1390                        }
1391
1392                        if reapply_failed {
1393                            (true, program, None)
1394                        } else {
1395                            // We need to check our imports to see if they changed.
1396                            let mut new_exec_state = ExecState::new(self);
1397                            let (new_universe, new_universe_map) =
1398                                self.get_universe(&program, &mut new_exec_state).await?;
1399
1400                            let clear_scene = new_universe.values().any(|value| {
1401                                let id = value.1;
1402                                match (
1403                                    cached_state.exec_state.get_source(id),
1404                                    new_exec_state.global.get_source(id),
1405                                ) {
1406                                    (Some(s0), Some(s1)) => s0.source != s1.source,
1407                                    _ => false,
1408                                }
1409                            });
1410
1411                            if !clear_scene {
1412                                // Return early we don't need to clear the scene.
1413                                cache::write_old_memory(
1414                                    cached_state
1415                                        .mock_memory_state()
1416                                        .map_err(KclErrorWithOutputs::no_outputs)?,
1417                                )
1418                                .await;
1419                                return cached_state
1420                                    .into_exec_outcome(self)
1421                                    .await
1422                                    .map_err(KclErrorWithOutputs::no_outputs);
1423                            }
1424
1425                            (
1426                                true,
1427                                crate::Program {
1428                                    ast: changed_program,
1429                                    original_file_contents: program.original_file_contents,
1430                                },
1431                                Some((new_universe, new_universe_map, new_exec_state)),
1432                            )
1433                        }
1434                    }
1435                    CacheResult::NoAction(true) => {
1436                        if self
1437                            .engine
1438                            .reapply_settings(
1439                                &self.engine_batch,
1440                                &self.settings,
1441                                Default::default(),
1442                                &mut cached_state.main.exec_state.id_generator,
1443                                grid_scale,
1444                            )
1445                            .await
1446                            .is_ok()
1447                        {
1448                            // We need to update the old ast state with the new settings!!
1449                            cache::write_old_ast(GlobalState::with_settings(
1450                                cached_state.clone(),
1451                                self.settings.clone(),
1452                            ))
1453                            .await;
1454
1455                            cache::write_old_memory(
1456                                cached_state
1457                                    .mock_memory_state()
1458                                    .map_err(KclErrorWithOutputs::no_outputs)?,
1459                            )
1460                            .await;
1461                            return cached_state
1462                                .into_exec_outcome(self)
1463                                .await
1464                                .map_err(KclErrorWithOutputs::no_outputs);
1465                        }
1466                        (true, program, None)
1467                    }
1468                    CacheResult::NoAction(false) => {
1469                        cache::write_old_memory(
1470                            cached_state
1471                                .mock_memory_state()
1472                                .map_err(KclErrorWithOutputs::no_outputs)?,
1473                        )
1474                        .await;
1475                        return cached_state
1476                            .into_exec_outcome(self)
1477                            .await
1478                            .map_err(KclErrorWithOutputs::no_outputs);
1479                    }
1480                };
1481
1482                let (exec_state, result) = match import_check_info {
1483                    Some((new_universe, new_universe_map, mut new_exec_state)) => {
1484                        // Clear the scene if the imports changed.
1485                        self.send_clear_scene(&mut new_exec_state, Default::default())
1486                            .await
1487                            .map_err(KclErrorWithOutputs::no_outputs)?;
1488
1489                        let result = self
1490                            .run_concurrent(
1491                                &program,
1492                                &mut new_exec_state,
1493                                Some((new_universe, new_universe_map)),
1494                                PreserveMem::Normal,
1495                            )
1496                            .await;
1497
1498                        (new_exec_state, result)
1499                    }
1500                    None if clear_scene => {
1501                        // Pop the execution state, since we are starting fresh.
1502                        let mut exec_state = cached_state.reconstitute_exec_state(self);
1503                        exec_state.reset(self);
1504
1505                        self.send_clear_scene(&mut exec_state, Default::default())
1506                            .await
1507                            .map_err(KclErrorWithOutputs::no_outputs)?;
1508
1509                        let result = self
1510                            .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1511                            .await;
1512
1513                        (exec_state, result)
1514                    }
1515                    None => {
1516                        let mut exec_state = cached_state.reconstitute_exec_state(self);
1517                        exec_state
1518                            .mut_stack()
1519                            .restore_env(cached_state.main.result_env)
1520                            .map_err(KclErrorWithOutputs::no_outputs)?;
1521
1522                        let result = self
1523                            .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1524                            .await;
1525
1526                        (exec_state, result)
1527                    }
1528                };
1529
1530                (program, exec_state, result)
1531            }
1532            None => {
1533                let mut exec_state = ExecState::new(self);
1534                self.send_clear_scene(&mut exec_state, Default::default())
1535                    .await
1536                    .map_err(KclErrorWithOutputs::no_outputs)?;
1537
1538                let result = self
1539                    .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1540                    .await;
1541
1542                (program, exec_state, result)
1543            }
1544        };
1545
1546        if result.is_err() {
1547            cache::bust_cache().await;
1548        }
1549
1550        // Throw the error.
1551        let result = result?;
1552
1553        // Save this as the last successful execution to the cache.
1554        // Gotcha: `CacheResult::ReExecute.program` may be diff-based, do not save that AST
1555        // the last-successful AST. Instead, save in the full AST passed in.
1556        cache::write_old_ast(GlobalState::new(
1557            exec_state.clone(),
1558            self.settings.clone(),
1559            original_program.ast,
1560            result.0,
1561        ))
1562        .await;
1563
1564        let outcome = exec_state
1565            .into_exec_outcome(result.0, self)
1566            .await
1567            .map_err(KclErrorWithOutputs::no_outputs)?;
1568        Ok(outcome)
1569    }
1570
1571    /// Perform the execution of a program.
1572    ///
1573    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
1574    pub async fn run(
1575        &self,
1576        program: &crate::Program,
1577        exec_state: &mut ExecState,
1578    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1579        self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1580            .await
1581    }
1582
1583    /// Perform the execution of a program using a concurrent
1584    /// execution model.
1585    ///
1586    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
1587    pub async fn run_concurrent(
1588        &self,
1589        program: &crate::Program,
1590        exec_state: &mut ExecState,
1591        universe_info: Option<(Universe, UniverseMap)>,
1592        preserve_mem: PreserveMem,
1593    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1594        // Reuse our cached universe if we have one.
1595
1596        let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1597            (universe, universe_map)
1598        } else {
1599            self.get_universe(program, exec_state).await?
1600        };
1601
1602        // Push ModuleInstance ops for the root module's direct imports before
1603        // child modules execute. This lets the live feature tree show module
1604        // names immediately rather than waiting for the root module body to run.
1605        // Sort by source position so they appear in source-code order (the
1606        // universe_map is a HashMap with non-deterministic iteration order).
1607        let mut sorted_imports: Vec<_> = universe_map.iter().collect();
1608        sorted_imports.sort_by_key(|(_, import_stmt)| SourceRange::from(*import_stmt));
1609        for (_path, import_stmt) in sorted_imports {
1610            // Look up by the raw import filename (e.g. "car-wheel.kcl") which
1611            // is the key format used by Universe, NOT the resolved absolute
1612            // TypedPath that UniverseMap uses as its key.
1613            let filename = match &import_stmt.path {
1614                ImportPath::Kcl { filename } => filename.to_string(),
1615                ImportPath::Foreign { path } => path.to_string(),
1616                ImportPath::Std { .. } => continue,
1617            };
1618            if let Some((_, module_id, module_path, _)) = universe.get(&filename)
1619                && let ModulePath::Local { value, .. } = module_path
1620            {
1621                let name = import_stmt
1622                    .module_name()
1623                    .unwrap_or_else(|| value.file_name().unwrap_or_default());
1624                let source_range = SourceRange::from(import_stmt);
1625                exec_state.push_op(crate::execution::cad_op::Operation::ModuleInstance {
1626                    name,
1627                    module_id: *module_id,
1628                    glob: matches!(
1629                        import_stmt.selector,
1630                        crate::parsing::ast::types::ImportSelector::Glob(_)
1631                    ),
1632                    node_path: crate::NodePath::placeholder(),
1633                    source_range,
1634                });
1635            }
1636        }
1637
1638        let default_planes = self.engine.get_default_planes().read().await.clone();
1639
1640        // Run the prelude to set up the engine.
1641        self.eval_prelude(exec_state, SourceRange::synthetic())
1642            .await
1643            .map_err(KclErrorWithOutputs::no_outputs)?;
1644
1645        for modules in import_graph::import_graph(&universe, self)
1646            .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1647            .into_iter()
1648        {
1649            #[cfg(not(target_arch = "wasm32"))]
1650            let mut set = tokio::task::JoinSet::new();
1651
1652            #[allow(clippy::type_complexity)]
1653            let (results_tx, mut results_rx): (
1654                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1655                tokio::sync::mpsc::Receiver<_>,
1656            ) = tokio::sync::mpsc::channel(1);
1657
1658            for module in modules {
1659                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1660                    return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1661                        KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1662                    )));
1663                };
1664                let module_id = *module_id;
1665                let module_path = module_path.clone();
1666                let source_range = SourceRange::from(import_stmt);
1667                // Clone before mutating.
1668                let module_exec_state = exec_state.clone();
1669
1670                let repr = repr.clone();
1671                let exec_ctxt = self.clone_with_fresh_execution_batch();
1672                let results_tx = results_tx.clone();
1673
1674                let exec_module = async |exec_ctxt: &ExecutorContext,
1675                                         repr: &ModuleRepr,
1676                                         module_id: ModuleId,
1677                                         module_path: &ModulePath,
1678                                         exec_state: &mut ExecState,
1679                                         source_range: SourceRange|
1680                       -> Result<ModuleRepr, KclError> {
1681                    match repr {
1682                        ModuleRepr::Kcl(program, _) => {
1683                            let result = exec_ctxt
1684                                .exec_module_from_ast(
1685                                    program,
1686                                    module_id,
1687                                    module_path,
1688                                    exec_state,
1689                                    source_range,
1690                                    PreserveMem::Normal,
1691                                )
1692                                .await;
1693
1694                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1695                        }
1696                        ModuleRepr::Foreign(geom, _) => {
1697                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1698                                .await
1699                                .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1700
1701                            // Foreign modules don't produce their own operations;
1702                            // use a fresh artifact state instead of capturing the
1703                            // cloned root module's artifacts (which may contain
1704                            // early-pushed ModuleInstance operations).
1705                            result.map(|val| ModuleRepr::Foreign(geom.clone(), Some((val, Default::default()))))
1706                        }
1707                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1708                            format!("Module {module_path} not found in universe"),
1709                            vec![source_range],
1710                        ))),
1711                    }
1712                };
1713
1714                #[cfg(target_arch = "wasm32")]
1715                {
1716                    wasm_bindgen_futures::spawn_local(async move {
1717                        let mut exec_state = module_exec_state;
1718                        let exec_ctxt = exec_ctxt;
1719
1720                        let result = exec_module(
1721                            &exec_ctxt,
1722                            &repr,
1723                            module_id,
1724                            &module_path,
1725                            &mut exec_state,
1726                            source_range,
1727                        )
1728                        .await;
1729
1730                        results_tx
1731                            .send((module_id, module_path, result))
1732                            .await
1733                            .unwrap_or_default();
1734                    });
1735                }
1736                #[cfg(not(target_arch = "wasm32"))]
1737                {
1738                    set.spawn(async move {
1739                        let mut exec_state = module_exec_state;
1740                        let exec_ctxt = exec_ctxt;
1741
1742                        let result = exec_module(
1743                            &exec_ctxt,
1744                            &repr,
1745                            module_id,
1746                            &module_path,
1747                            &mut exec_state,
1748                            source_range,
1749                        )
1750                        .await;
1751
1752                        results_tx
1753                            .send((module_id, module_path, result))
1754                            .await
1755                            .unwrap_or_default();
1756                    });
1757                }
1758            }
1759
1760            drop(results_tx);
1761
1762            while let Some((module_id, _, result)) = results_rx.recv().await {
1763                match result {
1764                    Ok(new_repr) => {
1765                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1766
1767                        match &mut repr {
1768                            ModuleRepr::Kcl(_, cache) => {
1769                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
1770                                    unreachable!();
1771                                };
1772                                *cache = session_data;
1773                            }
1774                            ModuleRepr::Foreign(_, cache) => {
1775                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
1776                                    unreachable!();
1777                                };
1778                                *cache = session_data;
1779                            }
1780                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1781                        }
1782
1783                        exec_state.global.module_infos[&module_id].restore_repr(repr);
1784                    }
1785                    Err(e) => {
1786                        return Err(exec_state.error_with_outputs(e, None, default_planes));
1787                    }
1788                }
1789            }
1790        }
1791
1792        // The early-pushed ModuleInstance operations have already served their
1793        // purpose (firing onOperation callbacks for the live feature tree).
1794        // Clear them so they don't duplicate the operations the root module
1795        // body will produce when it actually executes its import statements.
1796        exec_state.mod_local.artifacts.operations.clear();
1797
1798        // Move any remaining setup artifacts (non-operation data from the
1799        // prelude, etc.) into the root state.
1800        exec_state
1801            .global
1802            .root_module_artifacts
1803            .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1804
1805        self.inner_run(program, exec_state, preserve_mem).await
1806    }
1807
1808    /// Get the universe & universe map of the program.
1809    /// And see if any of the imports changed.
1810    async fn get_universe(
1811        &self,
1812        program: &crate::Program,
1813        exec_state: &mut ExecState,
1814    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1815        exec_state.add_root_module_contents(program);
1816
1817        let mut universe = std::collections::HashMap::new();
1818
1819        let default_planes = self.engine.get_default_planes().read().await.clone();
1820
1821        let root_imports = import_graph::import_universe(
1822            self,
1823            &ModulePath::Main,
1824            &ModuleRepr::Kcl(program.ast.clone(), None),
1825            &mut universe,
1826            exec_state,
1827        )
1828        .await
1829        .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1830
1831        Ok((universe, root_imports))
1832    }
1833
1834    /// Perform the execution of a program.  Accept all possible parameters and
1835    /// output everything.
1836    async fn inner_run(
1837        &self,
1838        program: &crate::Program,
1839        exec_state: &mut ExecState,
1840        preserve_mem: PreserveMem,
1841    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1842        let _stats = crate::log::LogPerfStats::new("Interpretation");
1843
1844        // Re-apply the settings, in case the cache was busted.
1845        let grid_scale = if self.settings.fixed_size_grid {
1846            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1847        } else {
1848            GridScaleBehavior::ScaleWithZoom
1849        };
1850        self.engine
1851            .reapply_settings(
1852                &self.engine_batch,
1853                &self.settings,
1854                Default::default(),
1855                exec_state.id_generator(),
1856                grid_scale,
1857            )
1858            .await
1859            .map_err(KclErrorWithOutputs::no_outputs)?;
1860
1861        let default_planes = self.engine.get_default_planes().read().await.clone();
1862        let result = self
1863            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1864            .await;
1865
1866        crate::log::log(format!(
1867            "Post interpretation KCL memory stats: {:#?}",
1868            exec_state.stack().memory.stats()
1869        ));
1870        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1871
1872        /// Write the memory of an execution to the cache for reuse in mock
1873        /// execution.
1874        async fn write_old_memory(
1875            ctx: &ExecutorContext,
1876            exec_state: &ExecState,
1877            env_ref: EnvironmentRef,
1878        ) -> Result<(), KclError> {
1879            if ctx.is_mock() {
1880                return Ok(());
1881            }
1882            let mut stack = exec_state.stack().deep_clone()?;
1883            stack.restore_env(env_ref)?;
1884            let state = cache::SketchModeState {
1885                stack,
1886                module_infos: exec_state.global.module_infos.clone(),
1887                path_to_source_id: exec_state.global.path_to_source_id.clone(),
1888                id_to_source: exec_state.global.id_to_source.clone(),
1889                constraint_state: exec_state.mod_local.constraint_state.clone(),
1890                scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1891            };
1892            cache::write_old_memory(state).await;
1893            Ok(())
1894        }
1895
1896        let env_ref = match result {
1897            Ok(env_ref) => env_ref,
1898            Err((err, env_ref)) => {
1899                // Preserve memory on execution failures so follow-up mock
1900                // execution can still reuse stable IDs before the error.
1901                if let Some(env_ref) = env_ref {
1902                    write_old_memory(self, exec_state, env_ref)
1903                        .await
1904                        .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
1905                }
1906                return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
1907            }
1908        };
1909
1910        write_old_memory(self, exec_state, env_ref)
1911            .await
1912            .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
1913
1914        let session_data = self.engine.get_session_data().await;
1915
1916        Ok((env_ref, session_data))
1917    }
1918
1919    /// Execute an AST's program and build auxiliary outputs like the artifact
1920    /// graph.
1921    async fn execute_and_build_graph(
1922        &self,
1923        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1924        exec_state: &mut ExecState,
1925        preserve_mem: PreserveMem,
1926    ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1927        // Don't early return!  We need to build other outputs regardless of
1928        // whether execution failed.
1929
1930        // Because of execution caching, we may start with operations from a
1931        // previous run.
1932        let start_op = exec_state.global.root_module_artifacts.operations.len();
1933
1934        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1935            .await
1936            .map_err(|e| (e, None))?;
1937
1938        let exec_result = self
1939            .exec_module_body(
1940                program,
1941                exec_state,
1942                preserve_mem,
1943                ModuleId::default(),
1944                &ModulePath::Main,
1945            )
1946            .await
1947            .map(
1948                |ModuleExecutionOutcome {
1949                     environment: env_ref,
1950                     artifacts: module_artifacts,
1951                     ..
1952                 }| {
1953                    // We need to extend because it may already have operations from
1954                    // imports.
1955                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1956                    env_ref
1957                },
1958            )
1959            .map_err(|(err, env_ref, module_artifacts)| {
1960                if let Some(module_artifacts) = module_artifacts {
1961                    // We need to extend because it may already have operations
1962                    // from imports.
1963                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1964                }
1965                (err, env_ref)
1966            });
1967
1968        // Fill in NodePath for operations.
1969        let programs = &exec_state.build_program_lookup(program.clone());
1970        let cached_body_items = exec_state.global.artifacts.cached_body_items();
1971        for op in exec_state
1972            .global
1973            .root_module_artifacts
1974            .operations
1975            .iter_mut()
1976            .skip(start_op)
1977        {
1978            op.fill_node_paths(programs, cached_body_items);
1979        }
1980        for module in exec_state.global.module_infos.values_mut() {
1981            if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1982                for op in &mut outcome.artifacts.operations {
1983                    op.fill_node_paths(programs, cached_body_items);
1984                }
1985            }
1986        }
1987
1988        // Ensure all the async commands completed.
1989        self.engine
1990            .ensure_async_commands_completed(&self.engine_batch)
1991            .await
1992            .map_err(|e| {
1993                match &exec_result {
1994                    Ok(env_ref) => (e, Some(*env_ref)),
1995                    // Prefer the execution error.
1996                    Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1997                }
1998            })?;
1999
2000        // If we errored out and early-returned, there might be commands which haven't been executed
2001        // and should be dropped.
2002        self.engine.clear_queues(&self.engine_batch).await;
2003
2004        match exec_state.build_artifact_graph(&self.engine, program).await {
2005            Ok(_) => exec_result,
2006            Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
2007        }
2008    }
2009
2010    /// 'Import' std::prelude as the outermost scope.
2011    ///
2012    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
2013    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
2014        if exec_state.stack().memory.requires_std() {
2015            let initial_ops = exec_state.mod_local.artifacts.operations.len();
2016
2017            let path = vec!["std".to_owned(), "prelude".to_owned()];
2018            let resolved_path = ModulePath::from_std_import_path(&path)?;
2019            let id = self
2020                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
2021                .await?;
2022            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
2023
2024            exec_state.mut_stack().memory.set_std(module_memory)?;
2025
2026            // Operations generated by the prelude are not useful, so clear them
2027            // out.
2028            //
2029            // TODO: Should we also clear them out of each module so that they
2030            // don't appear in test output?
2031            exec_state.mod_local.artifacts.operations.truncate(initial_ops);
2032        }
2033
2034        Ok(())
2035    }
2036
2037    /// Get a snapshot of the current scene.
2038    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
2039        // Zoom to fit.
2040        self.engine
2041            .send_modeling_cmd(
2042                &self.engine_batch,
2043                uuid::Uuid::new_v4(),
2044                crate::execution::SourceRange::default(),
2045                &ModelingCmd::from(
2046                    mcmd::ZoomToFit::builder()
2047                        .object_ids(Default::default())
2048                        .animated(false)
2049                        .padding(0.1)
2050                        .build(),
2051                ),
2052            )
2053            .await
2054            .map_err(KclErrorWithOutputs::no_outputs)?;
2055
2056        // Send a snapshot request to the engine.
2057        let resp = self
2058            .engine
2059            .send_modeling_cmd(
2060                &self.engine_batch,
2061                uuid::Uuid::new_v4(),
2062                crate::execution::SourceRange::default(),
2063                &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
2064            )
2065            .await
2066            .map_err(KclErrorWithOutputs::no_outputs)?;
2067
2068        let OkWebSocketResponseData::Modeling {
2069            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
2070        } = resp
2071        else {
2072            return Err(ExecError::BadPng(format!(
2073                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
2074            )));
2075        };
2076        Ok(contents)
2077    }
2078
2079    /// Export the current scene as a CAD file.
2080    pub async fn export(
2081        &self,
2082        format: kittycad_modeling_cmds::format::OutputFormat3d,
2083    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2084        let resp = self
2085            .engine
2086            .send_modeling_cmd(
2087                &self.engine_batch,
2088                uuid::Uuid::new_v4(),
2089                crate::SourceRange::default(),
2090                &kittycad_modeling_cmds::ModelingCmd::Export(
2091                    kittycad_modeling_cmds::Export::builder()
2092                        .entity_ids(vec![])
2093                        .format(format)
2094                        .build(),
2095                ),
2096            )
2097            .await?;
2098
2099        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
2100            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
2101                format!("Expected Export response, got {resp:?}",),
2102                vec![SourceRange::default()],
2103            )));
2104        };
2105
2106        Ok(files)
2107    }
2108
2109    /// Export the current scene as a STEP file.
2110    pub async fn export_step(
2111        &self,
2112        deterministic_time: bool,
2113    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2114        let files = self
2115            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
2116                kittycad_modeling_cmds::format::step::export::Options::builder()
2117                    .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
2118                    .maybe_created(if deterministic_time {
2119                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
2120                            KclError::new_internal(crate::errors::KclErrorDetails::new(
2121                                format!("Failed to parse date: {e}"),
2122                                vec![SourceRange::default()],
2123                            ))
2124                        })?)
2125                    } else {
2126                        None
2127                    })
2128                    .build(),
2129            ))
2130            .await?;
2131
2132        Ok(files)
2133    }
2134
2135    pub async fn close(&self) {
2136        self.engine.close().await;
2137    }
2138}
2139
2140pub use kcl_api::ArtifactId;
2141
2142pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
2143    ArtifactId::new(*id.as_ref())
2144}
2145
2146#[cfg(test)]
2147pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2148    parse_execute_with_project_dir(code, None).await
2149}
2150
2151#[cfg(test)]
2152pub(crate) async fn parse_execute_with_project_dir(
2153    code: &str,
2154    project_directory: Option<TypedPath>,
2155) -> Result<ExecTestResults, KclError> {
2156    let program = crate::Program::parse_no_errs(code)?;
2157
2158    let exec_ctxt = ExecutorContext {
2159        engine: Arc::new(EngineManager::new_mock()),
2160        engine_batch: EngineBatchContext::default(),
2161        fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2162        settings: ExecutorSettings {
2163            project_directory,
2164            ..Default::default()
2165        },
2166        context_type: ContextType::Mock,
2167        execution_callbacks: Default::default(),
2168    };
2169    let mut exec_state = ExecState::new(&exec_ctxt);
2170    let result = exec_ctxt.run(&program, &mut exec_state).await?;
2171
2172    Ok(ExecTestResults {
2173        program,
2174        mem_env: result.0,
2175        exec_ctxt,
2176        exec_state,
2177    })
2178}
2179
2180#[cfg(test)]
2181#[derive(Debug)]
2182pub(crate) struct ExecTestResults {
2183    program: crate::Program,
2184    mem_env: EnvironmentRef,
2185    exec_ctxt: ExecutorContext,
2186    exec_state: ExecState,
2187}
2188
2189#[cfg(test)]
2190impl ExecTestResults {
2191    pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2192        &self.exec_state.global.root_module_artifacts.commands
2193    }
2194}
2195
2196/// There are several places where we want to traverse a KCL program or find a symbol in it,
2197/// but because KCL modules can import each other, we need to traverse multiple programs.
2198/// This stores multiple programs, keyed by their module ID for quick access.
2199pub struct ProgramLookup {
2200    programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2201}
2202
2203impl ProgramLookup {
2204    // TODO: Could this store a reference to KCL programs instead of owning them?
2205    // i.e. take &state::ModuleInfoMap instead?
2206    pub fn new(
2207        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2208        module_infos: state::ModuleInfoMap,
2209    ) -> Self {
2210        let mut programs = IndexMap::with_capacity(module_infos.len());
2211        for (id, info) in module_infos {
2212            if let ModuleRepr::Kcl(program, _) = info.repr {
2213                programs.insert(id, program);
2214            }
2215        }
2216        programs.insert(ModuleId::default(), current);
2217        Self { programs }
2218    }
2219
2220    pub fn program_for_module(
2221        &self,
2222        module_id: ModuleId,
2223    ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2224        self.programs.get(&module_id)
2225    }
2226}
2227
2228#[cfg(test)]
2229mod tests {
2230    use kcl_api::NumericType;
2231    use pretty_assertions::assert_eq;
2232
2233    use super::*;
2234    use crate::ModuleId;
2235    use crate::errors::KclErrorDetails;
2236    use crate::errors::Severity;
2237    use crate::execution::memory::Stack;
2238    use crate::execution::types::RuntimeType;
2239
2240    macro_rules! kcl_input {
2241        ($file:literal) => {
2242            include_str!(concat!("../../e2e/executor/inputs/", $file, ".kcl"))
2243        };
2244    }
2245
2246    /// Convenience function to get a JSON value from memory and unwrap.
2247    #[track_caller]
2248    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2249        memory.memory.get_from_unchecked(name, env).unwrap()
2250    }
2251
2252    async fn execute_variables_with_backend(
2253        code: &str,
2254        backend: memory::MemoryBackendKind,
2255    ) -> IndexMap<String, KclValueView> {
2256        execute_outcome_with_backend(code, backend).await.variables
2257    }
2258
2259    async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
2260        let program = crate::Program::parse_no_errs(code).unwrap();
2261        let ctx = ExecutorContext::new_mock(None).await;
2262        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2263        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2264        let outcome = exec_state
2265            .into_exec_outcome(env_ref, &ctx)
2266            .await
2267            .expect("test execution outcome should collect variables");
2268        ctx.close().await;
2269        outcome
2270    }
2271
2272    async fn execute_error_variables_with_backend(
2273        code: &str,
2274        backend: memory::MemoryBackendKind,
2275    ) -> IndexMap<String, KclValueView> {
2276        let program = crate::Program::parse_no_errs(code).unwrap();
2277        let ctx = ExecutorContext::new_mock(None).await;
2278        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2279        let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
2280        ctx.close().await;
2281        error.variables
2282    }
2283
2284    async fn execute_project_variables_with_backend(
2285        main_code: &str,
2286        files: &[(&str, &str)],
2287        backend: memory::MemoryBackendKind,
2288    ) -> IndexMap<String, KclValueView> {
2289        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
2290        for (name, contents) in files {
2291            tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
2292        }
2293
2294        let program = crate::Program::parse_no_errs(main_code).unwrap();
2295        let ctx = ExecutorContext {
2296            engine: Arc::new(EngineManager::new_mock()),
2297            engine_batch: EngineBatchContext::default(),
2298            fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2299            settings: ExecutorSettings {
2300                project_directory: Some(crate::TypedPath(tmpdir.path().into())),
2301                ..Default::default()
2302            },
2303            context_type: ContextType::Mock,
2304            execution_callbacks: Default::default(),
2305        };
2306        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2307        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2308        let outcome = exec_state
2309            .into_exec_outcome(env_ref, &ctx)
2310            .await
2311            .expect("test execution outcome should collect variables");
2312        ctx.close().await;
2313        outcome.variables
2314    }
2315
2316    async fn run_with_caching_variables_with_backend(
2317        code: &str,
2318        backend: memory::MemoryBackendKind,
2319    ) -> IndexMap<String, KclValueView> {
2320        let _backend = memory::MemoryBackendKind::override_for_test(backend);
2321        cache::bust_cache().await;
2322        clear_mem_cache().await;
2323
2324        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
2325        let program = crate::Program::parse_no_errs(code).unwrap();
2326        ctx.run_with_caching(program.clone()).await.unwrap();
2327        let cached = ctx.run_with_caching(program).await.unwrap();
2328
2329        cache::bust_cache().await;
2330        clear_mem_cache().await;
2331        ctx.close().await;
2332        cached.variables
2333    }
2334
2335    async fn run_mock_variables_with_backend(
2336        code: &str,
2337        backend: memory::MemoryBackendKind,
2338    ) -> IndexMap<String, KclValueView> {
2339        let _backend = memory::MemoryBackendKind::override_for_test(backend);
2340        clear_mem_cache().await;
2341
2342        let ctx = ExecutorContext::new_mock(None).await;
2343        let first = crate::Program::parse_no_errs("x = 2").unwrap();
2344        ctx.run_mock(
2345            &first,
2346            &MockConfig {
2347                use_prev_memory: false,
2348                ..Default::default()
2349            },
2350        )
2351        .await
2352        .unwrap();
2353
2354        let program = crate::Program::parse_no_errs(code).unwrap();
2355        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
2356
2357        clear_mem_cache().await;
2358        ctx.close().await;
2359        outcome.variables
2360    }
2361
2362    fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
2363        let mut keys = variables.keys().cloned().collect::<Vec<_>>();
2364        keys.sort();
2365        keys
2366    }
2367
2368    async fn collect_backend_results<T, Fut>(
2369        mut run: impl FnMut(memory::MemoryBackendKind) -> Fut,
2370    ) -> Vec<(memory::MemoryBackendKind, T)>
2371    where
2372        Fut: std::future::Future<Output = T>,
2373    {
2374        let all = memory::MemoryBackendKind::all();
2375        let mut results = Vec::with_capacity(all.len());
2376        for &kind in all {
2377            results.push((kind, run(kind).await));
2378        }
2379        results
2380    }
2381
2382    fn assert_backend_results_match<T>(results: &[(memory::MemoryBackendKind, T)])
2383    where
2384        T: std::fmt::Debug + PartialEq,
2385    {
2386        let (first, rest) = results.split_first().expect("expected at least one memory backend");
2387        let (first_kind, first_result) = first;
2388        for (kind, result) in rest {
2389            assert_eq!(
2390                result, first_result,
2391                "memory kind {kind:?} doesn't match {first_kind:?}"
2392            );
2393        }
2394    }
2395
2396    fn assert_backend_variable_results_match_expected_keys(
2397        results: &[(memory::MemoryBackendKind, IndexMap<String, KclValueView>)],
2398        expected_keys: &[&str],
2399    ) {
2400        let (first_kind, first_variables) = results.first().expect("expected at least one memory backend");
2401        let expected_keys = expected_keys.iter().map(|key| (*key).to_owned()).collect::<Vec<_>>();
2402        assert_eq!(
2403            sorted_variable_keys(first_variables),
2404            expected_keys,
2405            "memory kind {first_kind:?} doesn't match expected variables"
2406        );
2407        assert_backend_results_match(results);
2408    }
2409
2410    fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
2411        let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
2412        let KclValueView::Number { value, .. } = value else {
2413            panic!("expected `{key}` to be a number, got {value:?}");
2414        };
2415        assert_eq!(*value, expected, "{key}: {value:?}");
2416    }
2417
2418    #[tokio::test(flavor = "multi_thread")]
2419    async fn exec_outcome_variables_match_between_memory_backends() {
2420        let code = "x = 2\ny = x + 1\narr = [x, y]";
2421
2422        let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2423
2424        assert_backend_variable_results_match_expected_keys(&results, &["arr", "x", "y"]);
2425    }
2426
2427    #[tokio::test(flavor = "multi_thread")]
2428    async fn error_output_variables_match_between_memory_backends() {
2429        let code = "x = 2\ny = missing + 1";
2430
2431        let results = collect_backend_results(|kind| execute_error_variables_with_backend(code, kind)).await;
2432
2433        assert_backend_variable_results_match_expected_keys(&results, &["x"]);
2434    }
2435
2436    #[tokio::test(flavor = "multi_thread")]
2437    async fn cached_execution_variables_match_between_memory_backends() {
2438        let code = "x = 2\ny = x + 1";
2439
2440        let results = collect_backend_results(|kind| run_with_caching_variables_with_backend(code, kind)).await;
2441
2442        assert_backend_variable_results_match_expected_keys(&results, &["x", "y"]);
2443    }
2444
2445    #[tokio::test(flavor = "multi_thread")]
2446    async fn mock_execution_variables_match_between_memory_backends() {
2447        let code = "y = x + 1";
2448
2449        let results = collect_backend_results(|kind| run_mock_variables_with_backend(code, kind)).await;
2450
2451        assert_backend_variable_results_match_expected_keys(&results, &["y"]);
2452    }
2453
2454    #[tokio::test(flavor = "multi_thread")]
2455    async fn module_imports_and_exported_closures_match_between_memory_backends() {
2456        let module_code = r#"
2457export base = 40
2458
2459export fn addBase(n) {
2460  return n + base
2461}
2462"#;
2463        let main_code = r#"
2464import base, addBase from 'math.kcl'
2465import 'math.kcl'
2466
2467named = addBase(n = 2)
2468qualified = math::addBase(n = 1)
2469direct = math::base
2470"#;
2471
2472        let files = [("math.kcl", module_code)];
2473        let results =
2474            collect_backend_results(|kind| execute_project_variables_with_backend(main_code, &files, kind)).await;
2475
2476        let (_, first_variables) = results.first().expect("expected at least one memory backend");
2477        assert_number_variable(first_variables, "named", 42.0);
2478        assert_number_variable(first_variables, "qualified", 41.0);
2479        assert_number_variable(first_variables, "direct", 40.0);
2480        assert_backend_results_match(&results);
2481    }
2482
2483    #[tokio::test(flavor = "multi_thread")]
2484    async fn sketch_block_variables_match_between_memory_backends() {
2485        let code = r#"
2486sketch001 = sketch(on = XY) {
2487  line1 = line(start = [0, 0], end = [1, 0])
2488  line2 = line(start = [1, 0], end = [0, 1])
2489}
2490lineCount = 2
2491"#;
2492
2493        let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2494
2495        let (_, first_variables) = results.first().expect("expected at least one memory backend");
2496        assert!(first_variables.contains_key("sketch001"), "actual: {first_variables:?}");
2497        assert_number_variable(first_variables, "lineCount", 2.0);
2498        assert_backend_results_match(&results);
2499    }
2500
2501    #[tokio::test(flavor = "multi_thread")]
2502    async fn tag_call_stack_lookup_matches_between_memory_backends() {
2503        let code = r#"
2504sketch001 = startSketchOn(XY)
2505  |> startProfile(at = [0, 0])
2506  |> xLine(length = 10, tag = $seg01)
2507
2508segLength = segLen(seg01)
2509"#;
2510
2511        let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2512
2513        let (_, first_variables) = results.first().expect("expected at least one memory backend");
2514        assert_number_variable(first_variables, "segLength", 10.0);
2515        assert_backend_results_match(&results);
2516    }
2517
2518    #[tokio::test(flavor = "multi_thread")]
2519    async fn sketch_transpiler_exec_outcome_variables_match_between_memory_backends() {
2520        let code = r#"
2521sketch001 = startSketchOn(XY)
2522  |> startProfile(at = [0, 0])
2523  |> line(end = [1, 0])
2524"#;
2525        let program = crate::Program::parse_no_errs(code).unwrap();
2526
2527        let outcomes = collect_backend_results(|kind| execute_outcome_with_backend(code, kind)).await;
2528        let mut transpiled = Vec::with_capacity(outcomes.len());
2529        for (kind, outcome) in &outcomes {
2530            let sketch = transpile_old_sketch_to_new(outcome, &program, "sketch001").unwrap();
2531            transpiled.push((*kind, sketch));
2532        }
2533
2534        assert_backend_results_match(&transpiled);
2535    }
2536
2537    #[tokio::test(flavor = "multi_thread")]
2538    async fn test_execute_warn() {
2539        let text = "@blah";
2540        let result = parse_execute(text).await.unwrap();
2541        let errs = result.exec_state.issues();
2542        assert_eq!(errs.len(), 1);
2543        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2544        assert!(
2545            errs[0].message.contains("Unknown annotation"),
2546            "unexpected warning message: {}",
2547            errs[0].message
2548        );
2549    }
2550
2551    #[tokio::test(flavor = "multi_thread")]
2552    async fn test_execute_fn_definitions() {
2553        let ast = r#"fn def(@x) {
2554  return x
2555}
2556fn ghi(@x) {
2557  return x
2558}
2559fn jkl(@x) {
2560  return x
2561}
2562fn hmm(@x) {
2563  return x
2564}
2565
2566yo = 5 + 6
2567
2568abc = 3
2569identifierGuy = 5
2570part001 = startSketchOn(XY)
2571|> startProfile(at = [-1.2, 4.83])
2572|> line(end = [2.8, 0])
2573|> angledLine(angle = 100 + 100, length = 3.01)
2574|> angledLine(angle = abc, length = 3.02)
2575|> angledLine(angle = def(yo), length = 3.03)
2576|> angledLine(angle = ghi(2), length = 3.04)
2577|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2578|> close()
2579yo2 = hmm([identifierGuy + 5])"#;
2580
2581        parse_execute(ast).await.unwrap();
2582    }
2583
2584    #[tokio::test(flavor = "multi_thread")]
2585    async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2586        let code = r#"
2587firstProfile = sketch(on = XY) {
2588  edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2589  edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2590  edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2591  edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2592  coincident([edge1.end, edge2.start])
2593  coincident([edge2.end, edge3.start])
2594  coincident([edge3.end, edge4.start])
2595  coincident([edge4.end, edge1.start])
2596}
2597
2598secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2599  edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2600  edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2601  edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2602  edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2603  coincident([edge5.end, edge6.start])
2604  coincident([edge6.end, edge7.start])
2605  coincident([edge7.end, edge8.start])
2606  coincident([edge8.end, edge5.start])
2607}
2608
2609firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2610secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2611"#;
2612
2613        let result = parse_execute(code).await.unwrap();
2614        assert!(result.exec_state.issues().is_empty());
2615    }
2616
2617    #[tokio::test(flavor = "multi_thread")]
2618    async fn sketch_block_artifact_preserves_standard_plane_name() {
2619        let code = r#"
2620sketch001 = sketch(on = -YZ) {
2621  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
2622}
2623"#;
2624
2625        let result = parse_execute(code).await.unwrap();
2626        let sketch_blocks = result
2627            .exec_state
2628            .global
2629            .artifacts
2630            .graph
2631            .values()
2632            .filter_map(|artifact| match artifact {
2633                Artifact::SketchBlock(block) => Some(block),
2634                _ => None,
2635            })
2636            .collect::<Vec<_>>();
2637
2638        assert_eq!(sketch_blocks.len(), 1);
2639        assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
2640    }
2641
2642    #[tokio::test(flavor = "multi_thread")]
2643    async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2644        let code = r#"
2645sketch001 = sketch(on = YZ) {
2646  line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2647  line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2648  line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2649  line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2650  coincident([line1.end, line2.start])
2651  coincident([line2.end, line3.start])
2652  coincident([line3.end, line4.start])
2653  coincident([line4.end, line1.start])
2654}
2655
2656sketch002 = sketch(on = -XZ) {
2657  line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2658  line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2659  line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2660  line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2661  coincident([line5.end, line6.start])
2662  coincident([line6.end, line7.start])
2663  coincident([line7.end, line8.start])
2664  coincident([line8.end, line5.start])
2665}
2666
2667region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2668extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2669region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2670extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2671
2672myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2673"#;
2674
2675        let result = parse_execute(code).await.unwrap();
2676        assert!(result.exec_state.issues().is_empty());
2677    }
2678
2679    #[tokio::test(flavor = "multi_thread")]
2680    async fn issue_10741_point_circle_coincident_executes() {
2681        let code = r#"
2682sketch001 = sketch(on = YZ) {
2683  circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
2684  line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
2685  coincident([line1.start, circle1])
2686}
2687"#;
2688
2689        let result = parse_execute(code).await.unwrap();
2690        assert!(
2691            result
2692                .exec_state
2693                .issues()
2694                .iter()
2695                .all(|issue| issue.severity != Severity::Error),
2696            "unexpected execution issues: {:#?}",
2697            result.exec_state.issues()
2698        );
2699    }
2700
2701    #[tokio::test(flavor = "multi_thread")]
2702    async fn test_execute_with_pipe_substitutions_unary() {
2703        let ast = r#"myVar = 3
2704part001 = startSketchOn(XY)
2705  |> startProfile(at = [0, 0])
2706  |> line(end = [3, 4], tag = $seg01)
2707  |> line(end = [
2708  min([segLen(seg01), myVar]),
2709  -legLen(hypotenuse = segLen(seg01), leg = myVar)
2710])
2711"#;
2712
2713        parse_execute(ast).await.unwrap();
2714    }
2715
2716    #[tokio::test(flavor = "multi_thread")]
2717    async fn test_execute_with_pipe_substitutions() {
2718        let ast = r#"myVar = 3
2719part001 = startSketchOn(XY)
2720  |> startProfile(at = [0, 0])
2721  |> line(end = [3, 4], tag = $seg01)
2722  |> line(end = [
2723  min([segLen(seg01), myVar]),
2724  legLen(hypotenuse = segLen(seg01), leg = myVar)
2725])
2726"#;
2727
2728        parse_execute(ast).await.unwrap();
2729    }
2730
2731    #[tokio::test(flavor = "multi_thread")]
2732    async fn test_execute_with_inline_comment() {
2733        let ast = r#"baseThick = 1
2734armAngle = 60
2735
2736baseThickHalf = baseThick / 2
2737halfArmAngle = armAngle / 2
2738
2739arrExpShouldNotBeIncluded = [1, 2, 3]
2740objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2741
2742part001 = startSketchOn(XY)
2743  |> startProfile(at = [0, 0])
2744  |> yLine(endAbsolute = 1)
2745  |> xLine(length = 3.84) // selection-range-7ish-before-this
2746
2747variableBelowShouldNotBeIncluded = 3
2748"#;
2749
2750        parse_execute(ast).await.unwrap();
2751    }
2752
2753    #[tokio::test(flavor = "multi_thread")]
2754    async fn test_execute_with_function_literal_in_pipe() {
2755        let ast = r#"w = 20
2756l = 8
2757h = 10
2758
2759fn thing() {
2760  return -8
2761}
2762
2763firstExtrude = startSketchOn(XY)
2764  |> startProfile(at = [0,0])
2765  |> line(end = [0, l])
2766  |> line(end = [w, 0])
2767  |> line(end = [0, thing()])
2768  |> close()
2769  |> extrude(length = h)"#;
2770
2771        parse_execute(ast).await.unwrap();
2772    }
2773
2774    #[tokio::test(flavor = "multi_thread")]
2775    async fn test_execute_with_function_unary_in_pipe() {
2776        let ast = r#"w = 20
2777l = 8
2778h = 10
2779
2780fn thing(@x) {
2781  return -x
2782}
2783
2784firstExtrude = startSketchOn(XY)
2785  |> startProfile(at = [0,0])
2786  |> line(end = [0, l])
2787  |> line(end = [w, 0])
2788  |> line(end = [0, thing(8)])
2789  |> close()
2790  |> extrude(length = h)"#;
2791
2792        parse_execute(ast).await.unwrap();
2793    }
2794
2795    #[tokio::test(flavor = "multi_thread")]
2796    async fn test_execute_with_function_array_in_pipe() {
2797        let ast = r#"w = 20
2798l = 8
2799h = 10
2800
2801fn thing(@x) {
2802  return [0, -x]
2803}
2804
2805firstExtrude = startSketchOn(XY)
2806  |> startProfile(at = [0,0])
2807  |> line(end = [0, l])
2808  |> line(end = [w, 0])
2809  |> line(end = thing(8))
2810  |> close()
2811  |> extrude(length = h)"#;
2812
2813        parse_execute(ast).await.unwrap();
2814    }
2815
2816    #[tokio::test(flavor = "multi_thread")]
2817    async fn test_execute_with_function_call_in_pipe() {
2818        let ast = r#"w = 20
2819l = 8
2820h = 10
2821
2822fn other_thing(@y) {
2823  return -y
2824}
2825
2826fn thing(@x) {
2827  return other_thing(x)
2828}
2829
2830firstExtrude = startSketchOn(XY)
2831  |> startProfile(at = [0,0])
2832  |> line(end = [0, l])
2833  |> line(end = [w, 0])
2834  |> line(end = [0, thing(8)])
2835  |> close()
2836  |> extrude(length = h)"#;
2837
2838        parse_execute(ast).await.unwrap();
2839    }
2840
2841    #[tokio::test(flavor = "multi_thread")]
2842    async fn test_execute_with_function_sketch() {
2843        let ast = r#"fn box(h, l, w) {
2844 myBox = startSketchOn(XY)
2845    |> startProfile(at = [0,0])
2846    |> line(end = [0, l])
2847    |> line(end = [w, 0])
2848    |> line(end = [0, -l])
2849    |> close()
2850    |> extrude(length = h)
2851
2852  return myBox
2853}
2854
2855fnBox = box(h = 3, l = 6, w = 10)"#;
2856
2857        parse_execute(ast).await.unwrap();
2858    }
2859
2860    #[tokio::test(flavor = "multi_thread")]
2861    async fn test_get_member_of_object_with_function_period() {
2862        let ast = r#"fn box(@obj) {
2863 myBox = startSketchOn(XY)
2864    |> startProfile(at = obj.start)
2865    |> line(end = [0, obj.l])
2866    |> line(end = [obj.w, 0])
2867    |> line(end = [0, -obj.l])
2868    |> close()
2869    |> extrude(length = obj.h)
2870
2871  return myBox
2872}
2873
2874thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2875"#;
2876        parse_execute(ast).await.unwrap();
2877    }
2878
2879    #[tokio::test(flavor = "multi_thread")]
2880    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
2881    async fn test_object_member_starting_pipeline() {
2882        let ast = r#"
2883fn test2() {
2884  return {
2885    thing: startSketchOn(XY)
2886      |> startProfile(at = [0, 0])
2887      |> line(end = [0, 1])
2888      |> line(end = [1, 0])
2889      |> line(end = [0, -1])
2890      |> close()
2891  }
2892}
2893
2894x2 = test2()
2895
2896x2.thing
2897  |> extrude(length = 10)
2898"#;
2899        parse_execute(ast).await.unwrap();
2900    }
2901
2902    #[tokio::test(flavor = "multi_thread")]
2903    #[ignore] // ignore til we get loops
2904    async fn test_execute_with_function_sketch_loop_objects() {
2905        let ast = r#"fn box(obj) {
2906let myBox = startSketchOn(XY)
2907    |> startProfile(at = obj.start)
2908    |> line(end = [0, obj.l])
2909    |> line(end = [obj.w, 0])
2910    |> line(end = [0, -obj.l])
2911    |> close()
2912    |> extrude(length = obj.h)
2913
2914  return myBox
2915}
2916
2917for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2918  thisBox = box(var)
2919}"#;
2920
2921        parse_execute(ast).await.unwrap();
2922    }
2923
2924    #[tokio::test(flavor = "multi_thread")]
2925    #[ignore] // ignore til we get loops
2926    async fn test_execute_with_function_sketch_loop_array() {
2927        let ast = r#"fn box(h, l, w, start) {
2928 myBox = startSketchOn(XY)
2929    |> startProfile(at = [0,0])
2930    |> line(end = [0, l])
2931    |> line(end = [w, 0])
2932    |> line(end = [0, -l])
2933    |> close()
2934    |> extrude(length = h)
2935
2936  return myBox
2937}
2938
2939
2940for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2941  const thisBox = box(var[0], var[1], var[2], var[3])
2942}"#;
2943
2944        parse_execute(ast).await.unwrap();
2945    }
2946
2947    #[tokio::test(flavor = "multi_thread")]
2948    async fn test_get_member_of_array_with_function() {
2949        let ast = r#"fn box(@arr) {
2950 myBox =startSketchOn(XY)
2951    |> startProfile(at = arr[0])
2952    |> line(end = [0, arr[1]])
2953    |> line(end = [arr[2], 0])
2954    |> line(end = [0, -arr[1]])
2955    |> close()
2956    |> extrude(length = arr[3])
2957
2958  return myBox
2959}
2960
2961thisBox = box([[0,0], 6, 10, 3])
2962
2963"#;
2964        parse_execute(ast).await.unwrap();
2965    }
2966
2967    #[tokio::test(flavor = "multi_thread")]
2968    async fn test_function_cannot_access_future_definitions() {
2969        let ast = r#"
2970fn returnX() {
2971  // x shouldn't be defined yet.
2972  return x
2973}
2974
2975x = 5
2976
2977answer = returnX()"#;
2978
2979        let result = parse_execute(ast).await;
2980        let err = result.unwrap_err();
2981        assert_eq!(err.message(), "`x` is not defined");
2982    }
2983
2984    #[tokio::test(flavor = "multi_thread")]
2985    async fn test_override_prelude() {
2986        let text = "PI = 3.0";
2987        let result = parse_execute(text).await.unwrap();
2988        let issues = result.exec_state.issues();
2989        assert!(issues.is_empty(), "issues={issues:#?}");
2990    }
2991
2992    #[tokio::test(flavor = "multi_thread")]
2993    async fn type_aliases() {
2994        let text = r#"@settings(experimentalFeatures = allow)
2995type MyTy = [number; 2]
2996fn foo(@x: MyTy) {
2997    return x[0]
2998}
2999
3000foo([0, 1])
3001
3002type Other = MyTy | Helix
3003"#;
3004        let result = parse_execute(text).await.unwrap();
3005        let issues = result.exec_state.issues();
3006        assert!(issues.is_empty(), "issues={issues:#?}");
3007    }
3008
3009    #[tokio::test(flavor = "multi_thread")]
3010    async fn test_cannot_shebang_in_fn() {
3011        let ast = r#"
3012fn foo() {
3013  #!hello
3014  return true
3015}
3016
3017foo
3018"#;
3019
3020        let result = parse_execute(ast).await;
3021        let err = result.unwrap_err();
3022        assert_eq!(
3023            err,
3024            KclError::new_syntax(KclErrorDetails::new(
3025                "Unexpected token: #".to_owned(),
3026                vec![SourceRange::new(14, 15, ModuleId::default())],
3027            )),
3028        );
3029    }
3030
3031    #[tokio::test(flavor = "multi_thread")]
3032    async fn test_pattern_transform_function_cannot_access_future_definitions() {
3033        let ast = r#"
3034fn transform(@replicaId) {
3035  // x shouldn't be defined yet.
3036  scale = x
3037  return {
3038    translate = [0, 0, replicaId * 10],
3039    scale = [scale, 1, 0],
3040  }
3041}
3042
3043fn layer() {
3044  return startSketchOn(XY)
3045    |> circle( center= [0, 0], radius= 1, tag = $tag1)
3046    |> extrude(length = 10)
3047}
3048
3049x = 5
3050
3051// The 10 layers are replicas of each other, with a transform applied to each.
3052shape = layer() |> patternTransform(instances = 10, transform = transform)
3053"#;
3054
3055        let result = parse_execute(ast).await;
3056        let err = result.unwrap_err();
3057        assert_eq!(err.message(), "`x` is not defined",);
3058    }
3059
3060    // ADAM: Move some of these into simulation tests.
3061
3062    #[tokio::test(flavor = "multi_thread")]
3063    async fn test_math_execute_with_functions() {
3064        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
3065        let result = parse_execute(ast).await.unwrap();
3066        assert_eq!(
3067            5.0,
3068            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3069                .as_f64()
3070                .unwrap()
3071        );
3072    }
3073
3074    #[tokio::test(flavor = "multi_thread")]
3075    async fn test_math_execute() {
3076        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
3077        let result = parse_execute(ast).await.unwrap();
3078        assert_eq!(
3079            7.4,
3080            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3081                .as_f64()
3082                .unwrap()
3083        );
3084    }
3085
3086    #[tokio::test(flavor = "multi_thread")]
3087    async fn test_string_uppercase() {
3088        let composed = "\u{e9}";
3089        let uppercase_composed = "\u{c9}";
3090        let decomposed = "e\u{301}";
3091        let uppercase_decomposed = "E\u{301}";
3092        let code = format!(
3093            r#"
3094ascii = string::uppercase("Kcl")
3095unicode_expansion = string::uppercase("Straße")
3096uncased = string::uppercase("東京")
3097empty = string::uppercase("")
3098composed = string::uppercase("{composed}")
3099decomposed = string::uppercase("{decomposed}")
3100piped = "ready" |> string::uppercase()
3101"#
3102        );
3103
3104        let result = parse_execute(&code).await.unwrap();
3105        for (name, expected) in [
3106            ("ascii", "KCL"),
3107            ("unicode_expansion", "STRASSE"),
3108            ("uncased", "東京"),
3109            ("empty", ""),
3110            ("composed", uppercase_composed),
3111            ("decomposed", uppercase_decomposed),
3112            ("piped", "READY"),
3113        ] {
3114            assert_eq!(
3115                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3116                    .as_str()
3117                    .unwrap(),
3118                expected,
3119                "{name}"
3120            );
3121        }
3122    }
3123
3124    #[tokio::test(flavor = "multi_thread")]
3125    async fn test_string_lowercase() {
3126        let composed = "\u{c9}";
3127        let lowercase_composed = "\u{e9}";
3128        let decomposed = "E\u{301}";
3129        let lowercase_decomposed = "e\u{301}";
3130        let expanded = "i\u{307}";
3131        let code = format!(
3132            r#"
3133ascii = string::lowercase("KCL")
3134final_sigma = string::lowercase("ΟΣ")
3135medial_sigma = string::lowercase("ΟΣΑ")
3136unicode_expansion = string::lowercase("İ")
3137uncased = string::lowercase("東京")
3138empty = string::lowercase("")
3139composed = string::lowercase("{composed}")
3140decomposed = string::lowercase("{decomposed}")
3141piped = "READY" |> string::lowercase()
3142"#
3143        );
3144
3145        let result = parse_execute(&code).await.unwrap();
3146        for (name, expected) in [
3147            ("ascii", "kcl"),
3148            ("final_sigma", "ος"),
3149            ("medial_sigma", "οσα"),
3150            ("unicode_expansion", expanded),
3151            ("uncased", "東京"),
3152            ("empty", ""),
3153            ("composed", lowercase_composed),
3154            ("decomposed", lowercase_decomposed),
3155            ("piped", "ready"),
3156        ] {
3157            assert_eq!(
3158                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3159                    .as_str()
3160                    .unwrap(),
3161                expected,
3162                "{name}"
3163            );
3164        }
3165    }
3166
3167    #[tokio::test(flavor = "multi_thread")]
3168    async fn test_string_is_equal() {
3169        let composed = "\u{e9}";
3170        let decomposed = "e\u{301}";
3171        let code = format!(
3172            r#"
3173exact_same = string::isEqual("KCL", to = "KCL")
3174exact_different_case = string::isEqual("KCL", to = "kcl")
3175explicit_case_sensitive = string::isEqual("KCL", to = "kcl", caseInsensitive = false)
3176case_insensitive_ascii = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3177case_fold_expansion = string::isEqual("Straße", to = "STRASSE", caseInsensitive = true)
3178case_fold_expansion_reversed = string::isEqual("STRASSE", to = "Straße", caseInsensitive = true)
3179case_fold_sigma = string::isEqual("ος", to = "οσ", caseInsensitive = true)
3180case_fold_non_turkic = string::isEqual("I", to = "i", caseInsensitive = true)
3181case_fold_not_turkic = string::isEqual("I", to = "ı", caseInsensitive = true)
3182empty_same = string::isEqual("", to = "")
3183empty_different = string::isEqual("", to = "KCL")
3184exact_without_normalization = string::isEqual("{composed}", to = "{decomposed}")
3185case_fold_without_normalization = string::isEqual("{composed}", to = "{decomposed}", caseInsensitive = true)
3186piped = "ready" |> string::isEqual(to = "READY", caseInsensitive = true)
3187"#
3188        );
3189
3190        let result = parse_execute(&code).await.unwrap();
3191        for (name, expected) in [
3192            ("exact_same", true),
3193            ("exact_different_case", false),
3194            ("explicit_case_sensitive", false),
3195            ("case_insensitive_ascii", true),
3196            ("case_fold_expansion", true),
3197            ("case_fold_expansion_reversed", true),
3198            ("case_fold_sigma", true),
3199            ("case_fold_non_turkic", true),
3200            ("case_fold_not_turkic", false),
3201            ("empty_same", true),
3202            ("empty_different", false),
3203            ("exact_without_normalization", false),
3204            ("case_fold_without_normalization", false),
3205            ("piped", true),
3206        ] {
3207            assert_eq!(
3208                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3209                    .as_bool()
3210                    .unwrap(),
3211                expected,
3212                "{name}"
3213            );
3214        }
3215    }
3216
3217    #[tokio::test(flavor = "multi_thread")]
3218    async fn test_string_is_equal_inside_sketch_block_is_predicate() {
3219        let code = r#"
3220@settings(experimentalFeatures = allow)
3221
3222sketch(on = XY) {
3223  stringsAreEqual = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3224}
3225"#;
3226
3227        parse_execute(code).await.unwrap();
3228    }
3229
3230    #[tokio::test(flavor = "multi_thread")]
3231    async fn test_string_trim() {
3232        let ascii_whitespace = " \t\n";
3233        let tab = "\t";
3234        let non_breaking_space = "\u{a0}";
3235        let em_space = "\u{2003}";
3236        let ideographic_space = "\u{3000}";
3237        let zero_width_space = "\u{200b}";
3238        let decomposed = "e\u{301}";
3239        let code = format!(
3240            r#"
3241ascii = string::trim("{ascii_whitespace}KCL{ascii_whitespace}")
3242internal = string::trim("  KCL{tab}strings  ")
3243unicode = string::trim("{non_breaking_space}{em_space}KCL{ideographic_space}")
3244all_whitespace = string::trim("{ascii_whitespace}{non_breaking_space}")
3245empty = string::trim("")
3246unchanged = string::trim("KCL")
3247without_normalization = string::trim(" {decomposed} ")
3248non_whitespace = string::trim("{zero_width_space}KCL{zero_width_space}")
3249piped = "  ready  " |> string::trim()
3250"#
3251        );
3252
3253        let result = parse_execute(&code).await.unwrap();
3254        let non_whitespace = format!("{zero_width_space}KCL{zero_width_space}");
3255        for (name, expected) in [
3256            ("ascii", "KCL"),
3257            ("internal", "KCL\tstrings"),
3258            ("unicode", "KCL"),
3259            ("all_whitespace", ""),
3260            ("empty", ""),
3261            ("unchanged", "KCL"),
3262            ("without_normalization", decomposed),
3263            ("non_whitespace", non_whitespace.as_str()),
3264            ("piped", "ready"),
3265        ] {
3266            assert_eq!(
3267                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3268                    .as_str()
3269                    .unwrap(),
3270                expected,
3271                "{name}"
3272            );
3273        }
3274    }
3275
3276    #[tokio::test(flavor = "multi_thread")]
3277    async fn test_string_trim_start() {
3278        let ascii_whitespace = " \t\n";
3279        let tab = "\t";
3280        let non_breaking_space = "\u{a0}";
3281        let em_space = "\u{2003}";
3282        let ideographic_space = "\u{3000}";
3283        let zero_width_space = "\u{200b}";
3284        let decomposed = "e\u{301}";
3285        let code = format!(
3286            r#"
3287ascii = string::trimStart("{ascii_whitespace}KCL{ascii_whitespace}")
3288internal = string::trimStart("  KCL{tab}strings")
3289unicode = string::trimStart("{non_breaking_space}{em_space}KCL{ideographic_space}")
3290all_whitespace = string::trimStart("{ascii_whitespace}{non_breaking_space}")
3291empty = string::trimStart("")
3292unchanged = string::trimStart("KCL")
3293without_normalization = string::trimStart(" {decomposed}")
3294non_whitespace_prefix = string::trimStart("{zero_width_space}{ascii_whitespace}KCL")
3295piped = "  ready  " |> string::trimStart()
3296"#
3297        );
3298
3299        let result = parse_execute(&code).await.unwrap();
3300        let ascii = format!("KCL{ascii_whitespace}");
3301        let unicode = format!("KCL{ideographic_space}");
3302        let non_whitespace_prefix = format!("{zero_width_space}{ascii_whitespace}KCL");
3303        for (name, expected) in [
3304            ("ascii", ascii.as_str()),
3305            ("internal", "KCL\tstrings"),
3306            ("unicode", unicode.as_str()),
3307            ("all_whitespace", ""),
3308            ("empty", ""),
3309            ("unchanged", "KCL"),
3310            ("without_normalization", decomposed),
3311            ("non_whitespace_prefix", non_whitespace_prefix.as_str()),
3312            ("piped", "ready  "),
3313        ] {
3314            assert_eq!(
3315                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3316                    .as_str()
3317                    .unwrap(),
3318                expected,
3319                "{name}"
3320            );
3321        }
3322    }
3323
3324    #[tokio::test(flavor = "multi_thread")]
3325    async fn test_string_trim_end() {
3326        let ascii_whitespace = " \t\n";
3327        let tab = "\t";
3328        let non_breaking_space = "\u{a0}";
3329        let em_space = "\u{2003}";
3330        let ideographic_space = "\u{3000}";
3331        let zero_width_space = "\u{200b}";
3332        let decomposed = "e\u{301}";
3333        let code = format!(
3334            r#"
3335ascii = string::trimEnd("{ascii_whitespace}KCL{ascii_whitespace}")
3336internal = string::trimEnd("KCL{tab}strings  ")
3337unicode = string::trimEnd("{non_breaking_space}KCL{em_space}{ideographic_space}")
3338all_whitespace = string::trimEnd("{ascii_whitespace}{non_breaking_space}")
3339empty = string::trimEnd("")
3340unchanged = string::trimEnd("KCL")
3341without_normalization = string::trimEnd("{decomposed} ")
3342non_whitespace_suffix = string::trimEnd("KCL{ascii_whitespace}{zero_width_space}")
3343piped = "  ready  " |> string::trimEnd()
3344"#
3345        );
3346
3347        let result = parse_execute(&code).await.unwrap();
3348        let ascii = format!("{ascii_whitespace}KCL");
3349        let unicode = format!("{non_breaking_space}KCL");
3350        let non_whitespace_suffix = format!("KCL{ascii_whitespace}{zero_width_space}");
3351        for (name, expected) in [
3352            ("ascii", ascii.as_str()),
3353            ("internal", "KCL\tstrings"),
3354            ("unicode", unicode.as_str()),
3355            ("all_whitespace", ""),
3356            ("empty", ""),
3357            ("unchanged", "KCL"),
3358            ("without_normalization", decomposed),
3359            ("non_whitespace_suffix", non_whitespace_suffix.as_str()),
3360            ("piped", "  ready"),
3361        ] {
3362            assert_eq!(
3363                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3364                    .as_str()
3365                    .unwrap(),
3366                expected,
3367                "{name}"
3368            );
3369        }
3370    }
3371
3372    #[tokio::test(flavor = "multi_thread")]
3373    async fn test_string_to_string() {
3374        // Each case runs on its own so a failure names the expression that
3375        // produced it rather than collapsing the whole table.
3376        for (name, expr, expected) in [
3377            // Every row of the table in the `toString` doc comment appears
3378            // here, so the documentation cannot drift from the behaviour.
3379            ("unitless integer", "12", "12"),
3380            ("unitless fractional", "1.5", "1.5"),
3381            ("no digits dropped", "0.1 + 0.2", "0.30000000000000004"),
3382            ("unitless negative", "-7", "-7"),
3383            ("unitless zero", "0", "0"),
3384            ("negative zero", "-0", "0"),
3385            ("count", "3_", "3_"),
3386            ("millimeters", "12mm", "12mm"),
3387            ("centimeters", "12cm", "12cm"),
3388            ("meters", "12m", "12m"),
3389            ("inches", "1.5in", "1.5in"),
3390            ("feet", "2ft", "2ft"),
3391            ("yards", "3yd", "3yd"),
3392            ("degrees", "90deg", "90deg"),
3393            ("radians", "1.5rad", "1.5rad"),
3394            // Arithmetic keeps the unit it started with.
3395            ("length arithmetic", "2mm + 10mm", "12mm"),
3396            // Multiplying two lengths exceeds what the type system tracks, so
3397            // only the numeric component survives.
3398            ("units the type system loses", "2mm * 10mm", "20"),
3399            ("unitless arithmetic", "1 + 2", "3"),
3400        ] {
3401            let code = format!("actual = string::toString({expr})");
3402            let result = parse_execute(&code).await.unwrap();
3403
3404            assert_eq!(
3405                mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3406                    .as_str()
3407                    .unwrap(),
3408                expected,
3409                "case: {name}"
3410            );
3411        }
3412    }
3413
3414    #[tokio::test(flavor = "multi_thread")]
3415    async fn test_string_to_string_ignores_the_files_default_unit() {
3416        // A value with no suffix has the file's default unit attached, but that
3417        // unit was never written down, so neither is it in the output. Reading
3418        // the result back in a file with a different default gives a different
3419        // quantity; the guarantee is about the number, not the measurement.
3420        let code = "@settings(defaultLengthUnit = inch)\nactual = string::toString(12)";
3421        let result = parse_execute(code).await.unwrap();
3422
3423        assert_eq!(
3424            mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3425                .as_str()
3426                .unwrap(),
3427            "12"
3428        );
3429    }
3430
3431    #[tokio::test(flavor = "multi_thread")]
3432    async fn test_string_to_string_rejects_a_non_number() {
3433        let error = parse_execute(r#"actual = string::toString("already text")"#)
3434            .await
3435            .unwrap_err();
3436
3437        // The declared signature rejects this before the implementation runs,
3438        // so the diagnostic names the function and both types.
3439        assert_eq!(
3440            error.message(),
3441            "The input argument of `string::toString` requires a value with type `number`, but found a value with type `string`."
3442        );
3443        assert!(
3444            matches!(error, KclError::Argument { .. }),
3445            "expected an Argument error, found {error:?}"
3446        );
3447    }
3448
3449    #[tokio::test(flavor = "multi_thread")]
3450    async fn test_string_to_string_accepts_a_piped_argument() {
3451        let result = parse_execute("actual = 12mm |> string::toString()").await.unwrap();
3452
3453        assert_eq!(
3454            mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3455                .as_str()
3456                .unwrap(),
3457            "12mm"
3458        );
3459    }
3460
3461    #[tokio::test(flavor = "multi_thread")]
3462    async fn test_string_to_string_echoes_how_the_literal_was_written() {
3463        // Reading the output back is not a supported operation, but for a
3464        // literal that carries its own units the text still comes out looking
3465        // like what the author typed, which is what makes it readable.
3466        for literal in [
3467            "12",
3468            "1.5",
3469            "0.30000000000000004",
3470            "3_",
3471            // A fractional count and a negative both have to survive the trip,
3472            // since the formatter emits them.
3473            "2.5_",
3474            "-4_",
3475            "12mm",
3476            "-5mm",
3477            "1.5in",
3478            "90deg",
3479            "1.5rad",
3480        ] {
3481            let code = format!("actual = string::toString({literal})");
3482            let result = parse_execute(&code).await.unwrap();
3483
3484            assert_eq!(
3485                mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3486                    .as_str()
3487                    .unwrap(),
3488                literal,
3489                "literal: {literal}"
3490            );
3491        }
3492    }
3493
3494    #[tokio::test(flavor = "multi_thread")]
3495    async fn test_string_to_string_spells_out_non_finite_numbers() {
3496        // Division is unguarded, so these are reachable from ordinary KCL. They
3497        // convert like any other number: the point of the function is to build
3498        // a message, and a message about a NaN is exactly when you need one.
3499        for (name, expr, expected) in [
3500            ("positive infinity", "1 / 0", "Infinity"),
3501            ("negative infinity", "-1 / 0", "-Infinity"),
3502            ("nan", "0 / 0", "NaN"),
3503            // The unit is dropped: no length is described by "Infinitymm".
3504            ("infinity from a length", "1mm / 0", "Infinity"),
3505            ("nan from a length", "0mm / 0", "NaN"),
3506            ("infinity from an angle", "1deg / 0", "Infinity"),
3507        ] {
3508            let code = format!("actual = string::toString({expr})");
3509            let result = parse_execute(&code).await.unwrap();
3510
3511            assert_eq!(
3512                mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3513                    .as_str()
3514                    .unwrap(),
3515                expected,
3516                "case: {name}"
3517            );
3518        }
3519    }
3520
3521    #[tokio::test(flavor = "multi_thread")]
3522    async fn test_string_equality_operators() {
3523        let composed = "\u{e9}";
3524        let decomposed = "e\u{301}";
3525        let code = format!(
3526            r#"
3527equal_same_ascii = "KCL" == "KCL"
3528equal_different_case = "KCL" == "kcl"
3529not_equal_same_ascii = "KCL" != "KCL"
3530not_equal_different_case = "KCL" != "kcl"
3531equal_same_unicode = "{composed}" == "{composed}"
3532not_equal_same_unicode = "{composed}" != "{composed}"
3533equal_without_normalization = "{composed}" == "{decomposed}"
3534not_equal_without_normalization = "{composed}" != "{decomposed}"
3535"#
3536        );
3537
3538        let result = parse_execute(&code).await.unwrap();
3539        for (name, expected) in [
3540            ("equal_same_ascii", true),
3541            ("equal_different_case", false),
3542            ("not_equal_same_ascii", false),
3543            ("not_equal_different_case", true),
3544            ("equal_same_unicode", true),
3545            ("not_equal_same_unicode", false),
3546            ("equal_without_normalization", false),
3547            ("not_equal_without_normalization", true),
3548        ] {
3549            assert_eq!(
3550                mem_get_json(result.exec_state.stack(), result.mem_env, name)
3551                    .as_bool()
3552                    .unwrap(),
3553                expected,
3554                "{name}"
3555            );
3556        }
3557    }
3558
3559    #[tokio::test(flavor = "multi_thread")]
3560    async fn test_string_equality_inside_sketch_block_fails_like_number_equality() {
3561        let string_code = r#"
3562@settings(experimentalFeatures = allow)
3563
3564sketch(on = XY) {
3565  stringsAreEqual = "KCL" == "KCL"
3566}
3567"#;
3568        let number_code = r#"
3569@settings(experimentalFeatures = allow)
3570
3571sketch(on = XY) {
3572  numbersAreEqual = 1 == 1
3573}
3574"#;
3575
3576        assert_eq!(
3577            parse_execute(string_code).await.unwrap_err().message(),
3578            "Cannot create an equivalence constraint between values of these types: a string and a string"
3579        );
3580        assert_eq!(
3581            parse_execute(number_code).await.unwrap_err().message(),
3582            "Cannot create an equivalence constraint between values of these types: a number and a number"
3583        );
3584    }
3585
3586    #[tokio::test(flavor = "multi_thread")]
3587    async fn test_math_execute_start_negative() {
3588        let ast = r#"myVar = -5 + 6"#;
3589        let result = parse_execute(ast).await.unwrap();
3590        assert_eq!(
3591            1.0,
3592            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3593                .as_f64()
3594                .unwrap()
3595        );
3596    }
3597
3598    #[tokio::test(flavor = "multi_thread")]
3599    async fn test_math_execute_with_pi() {
3600        let ast = r#"myVar = PI * 2"#;
3601        let result = parse_execute(ast).await.unwrap();
3602        assert_eq!(
3603            std::f64::consts::TAU,
3604            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3605                .as_f64()
3606                .unwrap()
3607        );
3608    }
3609
3610    #[tokio::test(flavor = "multi_thread")]
3611    async fn test_math_define_decimal_without_leading_zero() {
3612        let ast = r#"thing = .4 + 7"#;
3613        let result = parse_execute(ast).await.unwrap();
3614        assert_eq!(
3615            7.4,
3616            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
3617                .as_f64()
3618                .unwrap()
3619        );
3620    }
3621
3622    #[tokio::test(flavor = "multi_thread")]
3623    async fn pass_std_to_std() {
3624        let ast = r#"sketch001 = startSketchOn(XY)
3625profile001 = circle(sketch001, center = [0, 0], radius = 2)
3626extrude001 = extrude(profile001, length = 5)
3627extrudes = patternLinear3d(
3628  extrude001,
3629  instances = 3,
3630  distance = 5,
3631  axis = [1, 1, 0],
3632)
3633clone001 = map(extrudes, f = clone)
3634"#;
3635        parse_execute(ast).await.unwrap();
3636    }
3637
3638    #[tokio::test(flavor = "multi_thread")]
3639    async fn test_array_reduce_nested_array() {
3640        let code = r#"
3641fn id(@el, accum)  { return accum }
3642
3643answer = reduce([], initial=[[[0,0]]], f=id)
3644"#;
3645        let result = parse_execute(code).await.unwrap();
3646        assert_eq!(
3647            mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
3648            KclValue::HomArray {
3649                value: vec![KclValue::HomArray {
3650                    value: vec![KclValue::HomArray {
3651                        value: vec![
3652                            KclValue::Number {
3653                                value: 0.0,
3654                                ty: NumericType::default(),
3655                                meta: vec![SourceRange::new(69, 70, Default::default()).into()],
3656                            },
3657                            KclValue::Number {
3658                                value: 0.0,
3659                                ty: NumericType::default(),
3660                                meta: vec![SourceRange::new(71, 72, Default::default()).into()],
3661                            }
3662                        ],
3663                        ty: RuntimeType::any(),
3664                    }],
3665                    ty: RuntimeType::any(),
3666                }],
3667                ty: RuntimeType::any(),
3668            }
3669        );
3670    }
3671
3672    #[tokio::test(flavor = "multi_thread")]
3673    async fn test_zero_param_fn() {
3674        let ast = r#"sigmaAllow = 35000 // psi
3675leg1 = 5 // inches
3676leg2 = 8 // inches
3677fn thickness() { return 0.56 }
3678
3679bracket = startSketchOn(XY)
3680  |> startProfile(at = [0,0])
3681  |> line(end = [0, leg1])
3682  |> line(end = [leg2, 0])
3683  |> line(end = [0, -thickness()])
3684  |> line(end = [-leg2 + thickness(), 0])
3685"#;
3686        parse_execute(ast).await.unwrap();
3687    }
3688
3689    #[tokio::test(flavor = "multi_thread")]
3690    async fn test_unary_operator_not_succeeds() {
3691        let ast = r#"
3692fn returnTrue() { return !false }
3693t = true
3694f = false
3695notTrue = !t
3696notFalse = !f
3697c = !!true
3698d = !returnTrue()
3699
3700assertIs(!false, error = "expected to pass")
3701
3702fn check(x) {
3703  assertIs(!x, error = "expected argument to be false")
3704  return true
3705}
3706check(x = false)
3707"#;
3708        let result = parse_execute(ast).await.unwrap();
3709        assert_eq!(
3710            false,
3711            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
3712                .as_bool()
3713                .unwrap()
3714        );
3715        assert_eq!(
3716            true,
3717            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
3718                .as_bool()
3719                .unwrap()
3720        );
3721        assert_eq!(
3722            true,
3723            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
3724                .as_bool()
3725                .unwrap()
3726        );
3727        assert_eq!(
3728            false,
3729            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
3730                .as_bool()
3731                .unwrap()
3732        );
3733    }
3734
3735    #[tokio::test(flavor = "multi_thread")]
3736    async fn test_unary_operator_not_on_non_bool_fails() {
3737        let code1 = r#"
3738// Yup, this is null.
3739myNull = 0 / 0
3740notNull = !myNull
3741"#;
3742        assert_eq!(
3743            parse_execute(code1).await.unwrap_err().message(),
3744            "Cannot apply unary operator ! to non-boolean value: a number",
3745        );
3746
3747        let code2 = "notZero = !0";
3748        assert_eq!(
3749            parse_execute(code2).await.unwrap_err().message(),
3750            "Cannot apply unary operator ! to non-boolean value: a number",
3751        );
3752
3753        let code3 = r#"
3754notEmptyString = !""
3755"#;
3756        assert_eq!(
3757            parse_execute(code3).await.unwrap_err().message(),
3758            "Cannot apply unary operator ! to non-boolean value: a string",
3759        );
3760
3761        let code4 = r#"
3762obj = { a = 1 }
3763notMember = !obj.a
3764"#;
3765        assert_eq!(
3766            parse_execute(code4).await.unwrap_err().message(),
3767            "Cannot apply unary operator ! to non-boolean value: a number",
3768        );
3769
3770        let code5 = "
3771a = []
3772notArray = !a";
3773        assert_eq!(
3774            parse_execute(code5).await.unwrap_err().message(),
3775            "Cannot apply unary operator ! to non-boolean value: an empty array",
3776        );
3777
3778        let code6 = "
3779x = {}
3780notObject = !x";
3781        assert_eq!(
3782            parse_execute(code6).await.unwrap_err().message(),
3783            "Cannot apply unary operator ! to non-boolean value: an object",
3784        );
3785
3786        let code7 = "
3787fn x() { return 1 }
3788notFunction = !x";
3789        let fn_err = parse_execute(code7).await.unwrap_err();
3790        // These are currently printed out as JSON objects, so we don't want to
3791        // check the full error.
3792        assert!(
3793            fn_err
3794                .message()
3795                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
3796            "Actual error: {fn_err:?}"
3797        );
3798
3799        let code8 = "
3800myTagDeclarator = $myTag
3801notTagDeclarator = !myTagDeclarator";
3802        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
3803        // These are currently printed out as JSON objects, so we don't want to
3804        // check the full error.
3805        assert!(
3806            tag_declarator_err
3807                .message()
3808                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
3809            "Actual error: {tag_declarator_err:?}"
3810        );
3811
3812        let code9 = "
3813myTagDeclarator = $myTag
3814notTagIdentifier = !myTag";
3815        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
3816        // These are currently printed out as JSON objects, so we don't want to
3817        // check the full error.
3818        assert!(
3819            tag_identifier_err
3820                .message()
3821                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
3822            "Actual error: {tag_identifier_err:?}"
3823        );
3824
3825        let code10 = "notPipe = !(1 |> 2)";
3826        assert_eq!(
3827            // TODO: We don't currently parse this, but we should.  It should be
3828            // a runtime error instead.
3829            parse_execute(code10).await.unwrap_err(),
3830            KclError::new_syntax(KclErrorDetails::new(
3831                "Unexpected token: !".to_owned(),
3832                vec![SourceRange::new(10, 11, ModuleId::default())],
3833            ))
3834        );
3835
3836        let code11 = "
3837fn identity(x) { return x }
3838notPipeSub = 1 |> identity(!%))";
3839        assert_eq!(
3840            // TODO: We don't currently parse this, but we should.  It should be
3841            // a runtime error instead.
3842            parse_execute(code11).await.unwrap_err(),
3843            KclError::new_syntax(KclErrorDetails::new(
3844                "There was an unexpected `!`. Try removing it.".to_owned(),
3845                vec![SourceRange::new(56, 57, ModuleId::default())],
3846            ))
3847        );
3848
3849        // TODO: Add these tests when we support these types.
3850        // let notNan = !NaN
3851        // let notInfinity = !Infinity
3852    }
3853
3854    #[tokio::test(flavor = "multi_thread")]
3855    async fn test_start_sketch_on_invalid_kwargs() {
3856        let current_dir = std::env::current_dir().unwrap();
3857        let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
3858        let mut code = std::fs::read_to_string(&path).unwrap();
3859        assert_eq!(
3860            parse_execute(&code).await.unwrap_err().message(),
3861            "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
3862        );
3863
3864        path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
3865        code = std::fs::read_to_string(&path).unwrap();
3866
3867        assert_eq!(
3868            parse_execute(&code).await.unwrap_err().message(),
3869            "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
3870        );
3871
3872        path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
3873        code = std::fs::read_to_string(&path).unwrap();
3874
3875        assert_eq!(
3876            parse_execute(&code).await.unwrap_err().message(),
3877            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
3878        );
3879
3880        path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
3881        code = std::fs::read_to_string(&path).unwrap();
3882
3883        assert_eq!(
3884            parse_execute(&code).await.unwrap_err().message(),
3885            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
3886        );
3887
3888        path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
3889        code = std::fs::read_to_string(&path).unwrap();
3890
3891        assert_eq!(
3892            parse_execute(&code).await.unwrap_err().message(),
3893            "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
3894        );
3895    }
3896
3897    #[tokio::test(flavor = "multi_thread")]
3898    async fn test_math_negative_variable_in_binary_expression() {
3899        let ast = r#"sigmaAllow = 35000 // psi
3900width = 1 // inch
3901
3902p = 150 // lbs
3903distance = 6 // inches
3904FOS = 2
3905
3906leg1 = 5 // inches
3907leg2 = 8 // inches
3908
3909thickness_squared = distance * p * FOS * 6 / sigmaAllow
3910thickness = 0.56 // inches. App does not support square root function yet
3911
3912bracket = startSketchOn(XY)
3913  |> startProfile(at = [0,0])
3914  |> line(end = [0, leg1])
3915  |> line(end = [leg2, 0])
3916  |> line(end = [0, -thickness])
3917  |> line(end = [-leg2 + thickness, 0])
3918"#;
3919        parse_execute(ast).await.unwrap();
3920    }
3921
3922    #[tokio::test(flavor = "multi_thread")]
3923    async fn test_execute_function_no_return() {
3924        let ast = r#"fn test(@origin) {
3925  origin
3926}
3927
3928test([0, 0])
3929"#;
3930        let result = parse_execute(ast).await;
3931        assert!(result.is_err());
3932        assert!(result.unwrap_err().to_string().contains("undefined"));
3933    }
3934
3935    #[tokio::test(flavor = "multi_thread")]
3936    async fn test_max_stack_size_exceeded_error() {
3937        let ast = r#"
3938fn forever(@n) {
3939  return 1 + forever(n)
3940}
3941
3942forever(1)
3943"#;
3944        let result = parse_execute(ast).await;
3945        let err = result.unwrap_err();
3946        assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
3947    }
3948
3949    #[tokio::test(flavor = "multi_thread")]
3950    async fn test_math_doubly_nested_parens() {
3951        let ast = r#"sigmaAllow = 35000 // psi
3952width = 4 // inch
3953p = 150 // Force on shelf - lbs
3954distance = 6 // inches
3955FOS = 2
3956leg1 = 5 // inches
3957leg2 = 8 // inches
3958thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
3959thickness = 0.32 // inches. App does not support square root function yet
3960bracket = startSketchOn(XY)
3961  |> startProfile(at = [0,0])
3962    |> line(end = [0, leg1])
3963  |> line(end = [leg2, 0])
3964  |> line(end = [0, -thickness])
3965  |> line(end = [-1 * leg2 + thickness, 0])
3966  |> line(end = [0, -1 * leg1 + thickness])
3967  |> close()
3968  |> extrude(length = width)
3969"#;
3970        parse_execute(ast).await.unwrap();
3971    }
3972
3973    #[tokio::test(flavor = "multi_thread")]
3974    async fn test_math_nested_parens_one_less() {
3975        let ast = r#" sigmaAllow = 35000 // psi
3976width = 4 // inch
3977p = 150 // Force on shelf - lbs
3978distance = 6 // inches
3979FOS = 2
3980leg1 = 5 // inches
3981leg2 = 8 // inches
3982thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
3983thickness = 0.32 // inches. App does not support square root function yet
3984bracket = startSketchOn(XY)
3985  |> startProfile(at = [0,0])
3986    |> line(end = [0, leg1])
3987  |> line(end = [leg2, 0])
3988  |> line(end = [0, -thickness])
3989  |> line(end = [-1 * leg2 + thickness, 0])
3990  |> line(end = [0, -1 * leg1 + thickness])
3991  |> close()
3992  |> extrude(length = width)
3993"#;
3994        parse_execute(ast).await.unwrap();
3995    }
3996
3997    #[tokio::test(flavor = "multi_thread")]
3998    async fn test_fn_as_operand() {
3999        let ast = r#"fn f() { return 1 }
4000x = f()
4001y = x + 1
4002z = f() + 1
4003w = f() + f()
4004"#;
4005        parse_execute(ast).await.unwrap();
4006    }
4007
4008    #[tokio::test(flavor = "multi_thread")]
4009    async fn kcl_test_ids_stable_between_executions() {
4010        let code = r#"sketch001 = startSketchOn(XZ)
4011|> startProfile(at = [61.74, 206.13])
4012|> xLine(length = 305.11, tag = $seg01)
4013|> yLine(length = -291.85)
4014|> xLine(length = -segLen(seg01))
4015|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4016|> close()
4017|> extrude(length = 40.14)
4018|> shell(
4019    thickness = 3.14,
4020    faces = [seg01]
4021)
4022"#;
4023
4024        let ctx = crate::test_server::new_context(true, None).await.unwrap();
4025        let old_program = crate::Program::parse_no_errs(code).unwrap();
4026
4027        // Execute the program.
4028        if let Err(err) = ctx.run_with_caching(old_program).await {
4029            let report = err.into_miette_report_with_outputs(code).unwrap();
4030            let report = miette::Report::new(report);
4031            panic!("Error executing program: {report:?}");
4032        }
4033
4034        // Get the id_generator from the first execution.
4035        let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4036
4037        let code = r#"sketch001 = startSketchOn(XZ)
4038|> startProfile(at = [62.74, 206.13])
4039|> xLine(length = 305.11, tag = $seg01)
4040|> yLine(length = -291.85)
4041|> xLine(length = -segLen(seg01))
4042|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4043|> close()
4044|> extrude(length = 40.14)
4045|> shell(
4046    faces = [seg01],
4047    thickness = 3.14,
4048)
4049"#;
4050
4051        // Execute a slightly different program again.
4052        let program = crate::Program::parse_no_errs(code).unwrap();
4053        // Execute the program.
4054        ctx.run_with_caching(program).await.unwrap();
4055
4056        let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4057
4058        assert_eq!(id_generator, new_id_generator);
4059    }
4060
4061    #[tokio::test(flavor = "multi_thread")]
4062    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
4063        let code = r#"sketch001 = startSketchOn(XZ)
4064|> startProfile(at = [61.74, 206.13])
4065|> xLine(length = 305.11, tag = $seg01)
4066|> yLine(length = -291.85)
4067|> xLine(length = -segLen(seg01))
4068|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4069|> close()
4070|> extrude(length = 40.14)
4071|> shell(
4072    thickness = 3.14,
4073    faces = [seg01]
4074)
4075"#;
4076
4077        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
4078        let old_program = crate::Program::parse_no_errs(code).unwrap();
4079
4080        // Execute the program.
4081        ctx.run_with_caching(old_program.clone()).await.unwrap();
4082
4083        let settings_state = cache::read_old_ast().await.unwrap().settings;
4084
4085        // Ensure the settings are as expected.
4086        assert_eq!(settings_state, ctx.settings);
4087
4088        // Change a setting.
4089        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4090
4091        // Execute the program.
4092        ctx.run_with_caching(old_program.clone()).await.unwrap();
4093
4094        let settings_state = cache::read_old_ast().await.unwrap().settings;
4095
4096        // Ensure the settings are as expected.
4097        assert_eq!(settings_state, ctx.settings);
4098
4099        // Change a setting.
4100        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4101
4102        // Execute the program.
4103        ctx.run_with_caching(old_program).await.unwrap();
4104
4105        let settings_state = cache::read_old_ast().await.unwrap().settings;
4106
4107        // Ensure the settings are as expected.
4108        assert_eq!(settings_state, ctx.settings);
4109
4110        ctx.close().await;
4111    }
4112
4113    #[tokio::test(flavor = "multi_thread")]
4114    async fn mock_after_not_mock() {
4115        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4116        let program = crate::Program::parse_no_errs("x = 2").unwrap();
4117        let result = ctx.run_with_caching(program).await.unwrap();
4118        assert_number_variable(&result.variables, "x", 2.0);
4119
4120        let ctx2 = ExecutorContext::new_mock(None).await;
4121        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
4122        let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
4123        assert_number_variable(&result.variables, "z", 3.0);
4124
4125        ctx.close().await;
4126        ctx2.close().await;
4127    }
4128
4129    /// Regression test for https://github.com/KittyCAD/modeling-app/issues/12498
4130    #[tokio::test(flavor = "multi_thread")]
4131    async fn mock_execution_succeeds_after_split() {
4132        let code = kcl_input!("repro_mock_extrude");
4133        let ctx = ExecutorContext::new_mock(None).await;
4134        let program = crate::Program::parse_no_errs(code).unwrap();
4135        let _result = match ctx.run_mock(&program, &MockConfig::default()).await {
4136            Ok(res) => res,
4137            Err(e) => panic!("{}", e.error),
4138        };
4139    }
4140
4141    #[tokio::test(flavor = "multi_thread")]
4142    async fn mock_then_add_extrude_then_mock_again() {
4143        let code = "s = sketch(on = XY) {
4144    line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
4145    line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
4146    coincident([line1.end, line2.start])
4147    line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
4148    coincident([line2.end, line3.start])
4149    coincident([line1.start, line3.end])
4150}
4151    ";
4152        let ctx = ExecutorContext::new_mock(None).await;
4153        let program = crate::Program::parse_no_errs(code).unwrap();
4154        let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
4155        assert!(result.variables.contains_key("s"), "actual: {:?}", result.variables);
4156
4157        let code2 = code.to_owned()
4158            + "
4159region001 = region(point = [1mm, 1mm], sketch = s)
4160extrude001 = extrude(region001, length = 1)
4161    ";
4162        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4163        let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
4164        assert!(
4165            result.variables.contains_key("region001"),
4166            "actual: {:?}",
4167            result.variables
4168        );
4169
4170        ctx.close().await;
4171    }
4172
4173    #[tokio::test(flavor = "multi_thread")]
4174    async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
4175        let code = format!(
4176            r#"{}
4177
4178face7 = faceOf(solid6, face = r6.tags.line1)
4179r7 = squareRegion(onSurface = face7)
4180solid7 = extrude(r7, length = width)
4181"#,
4182            include_str!("../../tests/endless_impeller/input.kcl")
4183        );
4184
4185        let result = parse_execute(&code).await.unwrap();
4186        let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
4187        assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
4188
4189        let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
4190            KclValue::Face { value } => value,
4191            value => panic!("expected face7 to be a Face, got {value:?}"),
4192        };
4193        assert!(face7.parent_solid.creator_sketch_id.is_some());
4194    }
4195
4196    #[tokio::test(flavor = "multi_thread")]
4197    async fn mock_has_stable_ids() {
4198        let ctx = ExecutorContext::new_mock(None).await;
4199        let mock_config = MockConfig {
4200            use_prev_memory: false,
4201            ..Default::default()
4202        };
4203        let code = "sk = startSketchOn(XY)
4204        |> startProfile(at = [0, 0])";
4205        let program = crate::Program::parse_no_errs(code).unwrap();
4206        let result = ctx.run_mock(&program, &mock_config).await.unwrap();
4207        let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4208        assert!(!ids.is_empty(), "IDs should not be empty");
4209
4210        let ctx2 = ExecutorContext::new_mock(None).await;
4211        let program2 = crate::Program::parse_no_errs(code).unwrap();
4212        let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
4213        let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4214
4215        assert_eq!(ids, ids2, "Generated IDs should match");
4216        ctx.close().await;
4217        ctx2.close().await;
4218    }
4219
4220    #[tokio::test(flavor = "multi_thread")]
4221    async fn mock_memory_restore_preserves_module_maps() {
4222        clear_mem_cache().await;
4223
4224        let ctx = ExecutorContext::new_mock(None).await;
4225        let cold_start = MockConfig {
4226            use_prev_memory: false,
4227            ..Default::default()
4228        };
4229        ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
4230
4231        let mut mem = cache::read_old_memory().await.unwrap();
4232        assert!(
4233            mem.path_to_source_id.len() > 3,
4234            "expected prelude imports to populate multiple modules, got {:?}",
4235            mem.path_to_source_id
4236        );
4237        mem.constraint_state.insert(
4238            crate::front::ObjectId(1),
4239            indexmap::indexmap! {
4240                crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
4241                    crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
4242            },
4243        );
4244
4245        let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
4246        ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
4247
4248        assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
4249        assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
4250        assert_eq!(exec_state.global.module_infos, mem.module_infos);
4251        assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
4252
4253        clear_mem_cache().await;
4254        ctx.close().await;
4255    }
4256
4257    #[tokio::test(flavor = "multi_thread")]
4258    async fn run_with_caching_no_action_refreshes_mock_memory() {
4259        cache::bust_cache().await;
4260        clear_mem_cache().await;
4261
4262        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
4263        let program = crate::Program::parse_no_errs(
4264            r#"sketch001 = sketch(on = XY) {
4265  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
4266}
4267"#,
4268        )
4269        .unwrap();
4270
4271        ctx.run_with_caching(program.clone()).await.unwrap();
4272        let baseline_memory = cache::read_old_memory().await.unwrap();
4273        assert!(
4274            !baseline_memory.scene_objects.is_empty(),
4275            "expected engine execution to persist full-scene mock memory"
4276        );
4277
4278        cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
4279        assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
4280
4281        ctx.run_with_caching(program).await.unwrap();
4282        let refreshed_memory = cache::read_old_memory().await.unwrap();
4283        assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
4284        assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
4285        assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
4286
4287        cache::bust_cache().await;
4288        clear_mem_cache().await;
4289        ctx.close().await;
4290    }
4291
4292    #[tokio::test(flavor = "multi_thread")]
4293    async fn sim_sketch_mode_real_mock_real() {
4294        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4295        let code = r#"sketch001 = startSketchOn(XY)
4296profile001 = startProfile(sketch001, at = [0, 0])
4297  |> line(end = [10, 0])
4298  |> line(end = [0, 10])
4299  |> line(end = [-10, 0])
4300  |> line(end = [0, -10])
4301  |> close()
4302"#;
4303        let program = crate::Program::parse_no_errs(code).unwrap();
4304        let result = ctx.run_with_caching(program).await.unwrap();
4305        assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4306
4307        let mock_ctx = ExecutorContext::new_mock(None).await;
4308        let mock_program = crate::Program::parse_no_errs(code).unwrap();
4309        let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
4310        assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4311
4312        let code2 = code.to_owned()
4313            + r#"
4314extrude001 = extrude(profile001, length = 10)
4315"#;
4316        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4317        let result = ctx.run_with_caching(program2).await.unwrap();
4318        assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
4319
4320        ctx.close().await;
4321        mock_ctx.close().await;
4322    }
4323
4324    #[tokio::test(flavor = "multi_thread")]
4325    async fn read_tag_version() {
4326        let ast = r#"fn bar(@t) {
4327  return startSketchOn(XY)
4328    |> startProfile(at = [0,0])
4329    |> angledLine(
4330        angle = -60,
4331        length = segLen(t),
4332    )
4333    |> line(end = [0, 0])
4334    |> close()
4335}
4336
4337sketch = startSketchOn(XY)
4338  |> startProfile(at = [0,0])
4339  |> line(end = [0, 10])
4340  |> line(end = [10, 0], tag = $tag0)
4341  |> line(endAbsolute = [0, 0])
4342
4343fn foo() {
4344  // tag0 tags an edge
4345  return bar(tag0)
4346}
4347
4348solid = sketch |> extrude(length = 10)
4349// tag0 tags a face
4350sketch2 = startSketchOn(solid, face = tag0)
4351  |> startProfile(at = [0,0])
4352  |> line(end = [0, 1])
4353  |> line(end = [1, 0])
4354  |> line(end = [0, 0])
4355
4356foo() |> extrude(length = 1)
4357"#;
4358        parse_execute(ast).await.unwrap();
4359    }
4360
4361    #[tokio::test(flavor = "multi_thread")]
4362    async fn experimental() {
4363        let code = r#"
4364startSketchOn(XY)
4365  |> startProfile(at = [0, 0], tag = $start)
4366  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4367"#;
4368        let result = parse_execute(code).await.unwrap();
4369        let issues = result.exec_state.issues();
4370        assert_eq!(issues.len(), 1);
4371        assert_eq!(issues[0].severity, Severity::Error);
4372        let msg = &issues[0].message;
4373        assert!(msg.contains("experimental"), "found {msg}");
4374
4375        let code = r#"@settings(experimentalFeatures = allow)
4376startSketchOn(XY)
4377  |> startProfile(at = [0, 0], tag = $start)
4378  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4379"#;
4380        let result = parse_execute(code).await.unwrap();
4381        let issues = result.exec_state.issues();
4382        assert!(issues.is_empty(), "issues={issues:#?}");
4383
4384        let code = r#"@settings(experimentalFeatures = warn)
4385startSketchOn(XY)
4386  |> startProfile(at = [0, 0], tag = $start)
4387  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4388"#;
4389        let result = parse_execute(code).await.unwrap();
4390        let issues = result.exec_state.issues();
4391        assert_eq!(issues.len(), 1);
4392        assert_eq!(issues[0].severity, Severity::Warning);
4393        let msg = &issues[0].message;
4394        assert!(msg.contains("experimental"), "found {msg}");
4395
4396        let code = r#"@settings(experimentalFeatures = deny)
4397startSketchOn(XY)
4398  |> startProfile(at = [0, 0], tag = $start)
4399  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4400"#;
4401        let result = parse_execute(code).await.unwrap();
4402        let issues = result.exec_state.issues();
4403        assert_eq!(issues.len(), 1);
4404        assert_eq!(issues[0].severity, Severity::Error);
4405        let msg = &issues[0].message;
4406        assert!(msg.contains("experimental"), "found {msg}");
4407
4408        let code = r#"@settings(experimentalFeatures = foo)
4409startSketchOn(XY)
4410  |> startProfile(at = [0, 0], tag = $start)
4411  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4412"#;
4413        parse_execute(code).await.unwrap_err();
4414    }
4415
4416    #[tokio::test(flavor = "multi_thread")]
4417    async fn experimental_parameter() {
4418        let code = r#"
4419fn inc(@x, @(experimental = true) amount? = 1) {
4420  return x + amount
4421}
4422
4423answer = inc(5, amount = 2)
4424"#;
4425        let result = parse_execute(code).await.unwrap();
4426        let issues = result.exec_state.issues();
4427        assert_eq!(issues.len(), 1);
4428        assert_eq!(issues[0].severity, Severity::Error);
4429        let msg = &issues[0].message;
4430        assert!(msg.contains("experimental"), "found {msg}");
4431
4432        // If the parameter isn't used, there's no warning.
4433        let code = r#"
4434fn inc(@x, @(experimental = true) amount? = 1) {
4435  return x + amount
4436}
4437
4438answer = inc(5)
4439"#;
4440        let result = parse_execute(code).await.unwrap();
4441        let issues = result.exec_state.issues();
4442        assert!(issues.is_empty(), "issues={issues:#?}");
4443    }
4444
4445    #[tokio::test(flavor = "multi_thread")]
4446    async fn experimental_scalar_fixed_constraint() {
4447        let code_left = r#"@settings(experimentalFeatures = warn)
4448sketch(on = XY) {
4449  point1 = point(at = [var 0mm, var 0mm])
4450  point1.at[0] == 1mm
4451}
4452"#;
4453        // It's symmetric. Flipping the binary operator has the same behavior.
4454        let code_right = r#"@settings(experimentalFeatures = warn)
4455sketch(on = XY) {
4456  point1 = point(at = [var 0mm, var 0mm])
4457  1mm == point1.at[0]
4458}
4459"#;
4460
4461        for code in [code_left, code_right] {
4462            let result = parse_execute(code).await.unwrap();
4463            let issues = result.exec_state.issues();
4464            let Some(error) = issues
4465                .iter()
4466                .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
4467            else {
4468                panic!("found {issues:#?}");
4469            };
4470            assert_eq!(error.severity, Severity::Warning);
4471        }
4472    }
4473
4474    // START Mock Execution tests
4475    // Ideally, we would do this as part of all sim tests and delete these one-off tests.
4476
4477    #[tokio::test(flavor = "multi_thread")]
4478    async fn test_tangent_line_arc_executes_with_mock_engine() {
4479        let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
4480        parse_execute(&code).await.unwrap();
4481    }
4482
4483    #[tokio::test(flavor = "multi_thread")]
4484    async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
4485        let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
4486        parse_execute(&code).await.unwrap();
4487    }
4488
4489    #[tokio::test(flavor = "multi_thread")]
4490    async fn test_tangent_line_circle_executes_with_mock_engine() {
4491        let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
4492        parse_execute(&code).await.unwrap();
4493    }
4494
4495    #[tokio::test(flavor = "multi_thread")]
4496    async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
4497        let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
4498        parse_execute(&code).await.unwrap();
4499    }
4500
4501    #[tokio::test(flavor = "multi_thread")]
4502    async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
4503        let code = r#"startX = 2
4504
4505baseSketch = sketch(on = XY) {
4506  yoyo = line(start = [startX, 0], end = [7, 6])
4507  line2 = line(start = [7, 6], end = [7, 12])
4508  hi = line(start = [7, 12], end = [startX, 0])
4509}
4510
4511baseRegion = region(point = [5.5, 6], sketch = baseSketch)
4512myExtrude = extrude(
4513  baseRegion,
4514  length = 5,
4515  tagEnd = $endCap,
4516  tagStart = $startCap,
4517)
4518yodawg = getCommonEdge(faces = [
4519  baseRegion.tags.hi,
4520  baseRegion.tags.yoyo
4521])
4522
4523cutSketch = sketch(on = YZ) {
4524  myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
4525  myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
4526  line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
4527}
4528
4529cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
4530extrude001 = extrude(cutRegion, length = 5)
4531solid001 = subtract(myExtrude, tools = extrude001)
4532
4533yoyo = getOppositeEdge(baseRegion.tags.hi)
4534fillet(solid001, radius = 0.1, tags = yoyo)
4535"#;
4536
4537        parse_execute(code).await.unwrap();
4538    }
4539
4540    // END Mock Execution tests
4541
4542    // Sketch constraint report tests
4543
4544    async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
4545        let program = crate::Program::parse_no_errs(kcl).unwrap();
4546        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4547        let mut exec_state = ExecState::new(&ctx);
4548        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
4549        let outcome = exec_state
4550            .into_exec_outcome(env_ref, &ctx)
4551            .await
4552            .expect("constraint report test outcome should collect variables");
4553        let report = outcome.sketch_constraint_report();
4554        ctx.close().await;
4555        report
4556    }
4557
4558    #[tokio::test(flavor = "multi_thread")]
4559    async fn warn_when_sketch_is_over_constrained() {
4560        let code = r#"
4561sketch001 = sketch(on = XY) {
4562  line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
4563  fixed([line1.start, ORIGIN])
4564  fixed([line1.start, [20, 20]])
4565}
4566"#;
4567        let result = parse_execute(code).await.unwrap();
4568        let issues = result.exec_state.issues();
4569        let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
4570            panic!("expected over-constrained warning; found {issues:#?}");
4571        };
4572        assert_eq!(warning.severity, Severity::Warning);
4573    }
4574
4575    #[tokio::test(flavor = "multi_thread")]
4576    async fn no_warning_when_sketch_is_not_over_constrained() {
4577        // Under-constrained sketch should not emit the over-constrained warning.
4578        let code = r#"
4579sketch001 = sketch(on = XY) {
4580  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4581}
4582"#;
4583        let result = parse_execute(code).await.unwrap();
4584        let issues = result.exec_state.issues();
4585        assert!(
4586            !issues.iter().any(|issue| issue.message.contains("over-constrained")),
4587            "did not expect over-constrained warning; found {issues:#?}"
4588        );
4589    }
4590
4591    #[tokio::test(flavor = "multi_thread")]
4592    async fn test_constraint_report_fully_constrained() {
4593        // All points are fully constrained via equality constraints.
4594        let kcl = r#"
4595@settings(experimentalFeatures = allow)
4596
4597sketch(on = YZ) {
4598  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4599  line1.start.at[0] == 2
4600  line1.start.at[1] == 8
4601  line1.end.at[0] == 5
4602  line1.end.at[1] == 7
4603}
4604"#;
4605        let report = run_constraint_report(kcl).await;
4606        assert_eq!(report.fully_constrained.len(), 1);
4607        assert_eq!(report.under_constrained.len(), 0);
4608        assert_eq!(report.over_constrained.len(), 0);
4609        assert_eq!(report.errors.len(), 0);
4610        assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
4611    }
4612
4613    #[tokio::test(flavor = "multi_thread")]
4614    async fn test_constraint_report_under_constrained() {
4615        // No constraints at all — all points are free.
4616        let kcl = r#"
4617sketch(on = YZ) {
4618  line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
4619}
4620"#;
4621        let report = run_constraint_report(kcl).await;
4622        assert_eq!(report.fully_constrained.len(), 0);
4623        assert_eq!(report.under_constrained.len(), 1);
4624        assert_eq!(report.over_constrained.len(), 0);
4625        assert_eq!(report.errors.len(), 0);
4626        assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
4627        assert!(report.under_constrained[0].free_count > 0);
4628    }
4629
4630    #[tokio::test(flavor = "multi_thread")]
4631    async fn test_constraint_report_over_constrained() {
4632        // Conflicting distance constraints on the same pair of points.
4633        let kcl = r#"
4634@settings(experimentalFeatures = allow)
4635
4636sketch(on = YZ) {
4637  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4638  line1.start.at[0] == 2
4639  line1.start.at[1] == 8
4640  line1.end.at[0] == 5
4641  line1.end.at[1] == 7
4642  distance([line1.start, line1.end]) == 100mm
4643}
4644"#;
4645        let report = run_constraint_report(kcl).await;
4646        assert_eq!(report.over_constrained.len(), 1);
4647        assert_eq!(report.errors.len(), 0);
4648        assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
4649        assert!(report.over_constrained[0].conflict_count > 0);
4650    }
4651
4652    #[tokio::test(flavor = "multi_thread")]
4653    async fn test_constraint_report_multiple_sketches() {
4654        // Two sketches: one fully constrained, one under-constrained.
4655        let kcl = r#"
4656@settings(experimentalFeatures = allow)
4657
4658s1 = sketch(on = YZ) {
4659  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4660  line1.start.at[0] == 2
4661  line1.start.at[1] == 8
4662  line1.end.at[0] == 5
4663  line1.end.at[1] == 7
4664}
4665
4666s2 = sketch(on = XZ) {
4667  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4668}
4669"#;
4670        let report = run_constraint_report(kcl).await;
4671        assert_eq!(
4672            report.fully_constrained.len()
4673                + report.under_constrained.len()
4674                + report.over_constrained.len()
4675                + report.errors.len(),
4676            2,
4677            "Expected 2 sketches total"
4678        );
4679        assert_eq!(report.fully_constrained.len(), 1);
4680        assert_eq!(report.under_constrained.len(), 1);
4681    }
4682
4683    #[tokio::test(flavor = "multi_thread")]
4684    async fn test_enum_declaration_is_experimental() {
4685        // Without opting in, executing a program with an enum declaration
4686        // fails at the parsing stage with the experimental diagnostic.
4687        let code = "type Color { | Red }";
4688        assert_eq!(
4689            parse_execute(code).await.unwrap_err().message(),
4690            "Use of enum declarations is experimental and may change or be removed."
4691        );
4692    }
4693
4694    #[tokio::test(flavor = "multi_thread")]
4695    async fn enum_declaration_registers_type() {
4696        // Plain and exported declarations both execute. Nothing references the
4697        // enum yet, so this only asserts that declaring one is no longer an
4698        // error; constructor use is exercised separately.
4699        let code = r#"@settings(experimentalFeatures = allow)
4700type Color { | Red | Green }
4701"#;
4702        parse_execute(code).await.unwrap();
4703
4704        let code = r#"@settings(experimentalFeatures = allow)
4705export type Color { | Red | Green }
4706"#;
4707        parse_execute(code).await.unwrap();
4708
4709        // A zero-variant enum is a valid declaration.
4710        let code = r#"@settings(experimentalFeatures = allow)
4711type Empty { | }
4712"#;
4713        parse_execute(code).await.unwrap();
4714    }
4715
4716    #[tokio::test(flavor = "multi_thread")]
4717    async fn enum_declaration_rejects_nested_scope() {
4718        // Identity is (module, declared name), so two same-named declarations in
4719        // one file would collide. The parser and formatter accept this shape, so
4720        // execution is the only thing that can reject it.
4721        //
4722        // The rule is about nesting, not about one kind of block, so both routes
4723        // to `BodyType::Block` are covered here.
4724        let allow = "@settings(experimentalFeatures = allow)\n";
4725        for (case, code) in [
4726            (
4727                "function body",
4728                format!("{allow}fn palette() {{\n  type Color {{ | Red }}\n  return 0\n}}\npalette()\n"),
4729            ),
4730            (
4731                "sketch block",
4732                format!(
4733                    "{allow}sketch(on = XY) {{\n  type Color {{ | Red }}\n  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
4734                ),
4735            ),
4736        ] {
4737            assert_eq!(
4738                parse_execute(&code).await.unwrap_err().message(),
4739                "Enum declarations are only supported at the top-level of a file. Move `type Color` to the top-level.",
4740                "case: {case}"
4741            );
4742        }
4743    }
4744
4745    #[tokio::test(flavor = "multi_thread")]
4746    async fn enum_alone_is_restricted_to_top_level() {
4747        // Pins the asymmetry the rule above creates: a type alias may be declared
4748        // in any block, an enum may not. The difference is required by enum
4749        // identity rather than chosen -- two nested aliases shadow each other
4750        // harmlessly, while two nested `type Color` declarations would be one type
4751        // with two variant sets. Tightening aliases to match, or relaxing enums,
4752        // has to break this test first.
4753        let allow = "@settings(experimentalFeatures = allow)\n";
4754        for (case, code) in [
4755            (
4756                "function body",
4757                format!("{allow}fn f() {{\n  type Temperature = number(_)\n  return 0\n}}\nx = f()\n"),
4758            ),
4759            (
4760                "sketch block",
4761                format!(
4762                    "{allow}sketch(on = XY) {{\n  type Temperature = number(_)\n  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
4763                ),
4764            ),
4765        ] {
4766            parse_execute(&code)
4767                .await
4768                .unwrap_or_else(|err| panic!("a type alias should be allowed in a {case}: {}", err.message()));
4769        }
4770    }
4771
4772    #[tokio::test(flavor = "multi_thread")]
4773    async fn enum_declaration_rejects_duplicate() {
4774        let code = r#"@settings(experimentalFeatures = allow)
4775type Color { | Red | Green | Red }
4776"#;
4777        assert_eq!(
4778            parse_execute(code).await.unwrap_err().message(),
4779            "Duplicate variant `Red` in enum `Color`."
4780        );
4781    }
4782
4783    /// Runs `main` with `modules` written beside it, so import paths resolve.
4784    async fn execute_with_modules(main: &str, modules: &[(&str, &str)]) -> Result<ExecTestResults, KclError> {
4785        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_enum_clash").unwrap();
4786        for (name, source) in modules {
4787            tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
4788        }
4789
4790        parse_execute_with_project_dir(main, Some(crate::TypedPath(tmpdir.path().into()))).await
4791    }
4792
4793    #[tokio::test(flavor = "multi_thread")]
4794    async fn enum_rejects_name_clash_with_module() {
4795        // One rule reached four ways: by declaring the enum second, by importing
4796        // the module second, and by importing the enum itself either by name or
4797        // through a glob, which arrive by different code paths because a glob
4798        // copies exported keys with their namespace prefix intact.
4799        let plain_module = ("Color.kcl", "export x = 1\n");
4800        let enum_module = (
4801            "enums.kcl",
4802            "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
4803        );
4804
4805        for (case, main, modules) in [
4806            (
4807                "module then enum",
4808                "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\ntype Color { | Red }\n",
4809                vec![plain_module],
4810            ),
4811            (
4812                "enum then module",
4813                "@settings(experimentalFeatures = allow)\ntype Color { | Red }\nimport \"Color.kcl\"\n",
4814                vec![plain_module],
4815            ),
4816            (
4817                "named import of an enum",
4818                "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport Color from 'enums.kcl'\n",
4819                vec![plain_module, enum_module],
4820            ),
4821            (
4822                "glob import of an enum",
4823                "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport * from 'enums.kcl'\n",
4824                vec![plain_module, enum_module],
4825            ),
4826        ] {
4827            let err = execute_with_modules(main, &modules).await.unwrap_err();
4828            assert_eq!(
4829                err.message(),
4830                "An enum and a module cannot share the name `Color` in the same scope, because `Color::x` would be ambiguous. Rename one of them.",
4831                "case: {case}"
4832            );
4833        }
4834    }
4835
4836    #[tokio::test(flavor = "multi_thread")]
4837    async fn enum_constructs_variant() {
4838        let allow = "@settings(experimentalFeatures = allow)\n";
4839        let colors = (
4840            "colors.kcl",
4841            "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
4842        );
4843
4844        for (case, main, modules) in [
4845            (
4846                "declared locally",
4847                format!("{allow}type Color {{ | Red | Green }}\nx = Color::Red\n"),
4848                vec![],
4849            ),
4850            (
4851                // Also the regression test for the export check: a module's exports
4852                // record the prefixed key `__ty_Color`, not the bare name.
4853                "reached through a module path",
4854                format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
4855                vec![colors],
4856            ),
4857            (
4858                "imported by name",
4859                format!("{allow}import Color from 'colors.kcl'\nx = Color::Red\n"),
4860                vec![colors],
4861            ),
4862            (
4863                // An import alias renames the binding, not the type, so identity
4864                // and therefore the reported name stay those of the declaration.
4865                "imported under an alias",
4866                format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade::Red\n"),
4867                vec![colors],
4868            ),
4869        ] {
4870            let result = execute_with_modules(&main, &modules)
4871                .await
4872                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
4873            let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
4874                panic!("case: {case}: `x` should hold an enum value");
4875            };
4876            assert_eq!(value.qualified_name(), "Color::Red", "case: {case}");
4877        }
4878    }
4879
4880    #[tokio::test(flavor = "multi_thread")]
4881    async fn enum_rejects_bad_variant_paths() {
4882        let allow = "@settings(experimentalFeatures = allow)\n";
4883
4884        for (case, main, modules, message) in [
4885            (
4886                "unknown variant",
4887                format!("{allow}type Color {{ | Red | Green }}\nx = Color::Blue\n"),
4888                vec![],
4889                "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
4890            ),
4891            (
4892                "enum with no variants",
4893                format!("{allow}type Empty {{ | }}\nx = Empty::Red\n"),
4894                vec![],
4895                "`Red` is not a variant of enum `Empty`. Enum `Empty` has no variants.",
4896            ),
4897            (
4898                "path continues past the enum",
4899                format!("{allow}type Color {{ | Red }}\nx = Color::Red::more\n"),
4900                vec![],
4901                "`Color` is an enum, so only a variant name can follow it. There is nothing to reach through `Color::Red`.",
4902            ),
4903            (
4904                "variant name is case sensitive",
4905                format!("{allow}type Color {{ | Red }}\nx = Color::red\n"),
4906                vec![],
4907                "`red` is not a variant of enum `Color`. Its variants are: Red.",
4908            ),
4909            (
4910                "enum not exported from its module",
4911                format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
4912                vec![(
4913                    "colors.kcl",
4914                    "@settings(experimentalFeatures = allow)\ntype Color { | Red }\n",
4915                )],
4916                "Item Color not found in module's exported items",
4917            ),
4918            (
4919                // The alias exemption seen from the use site: a type alias is not
4920                // an enum, so the segment is resolved as a module and fails.
4921                "a type alias cannot head a path",
4922                format!("{allow}type T = number(_)\nx = T::foo\n"),
4923                vec![],
4924                "`T` is not defined",
4925            ),
4926            (
4927                // The other half of allowing a value and an enum to share a name:
4928                // a value on its own can never head a path.
4929                "a value cannot head a path",
4930                "Color = 5\nx = Color::Red\n".to_owned(),
4931                vec![],
4932                "`Color` is not defined",
4933            ),
4934        ] {
4935            let err = execute_with_modules(&main, &modules).await.unwrap_err();
4936            assert_eq!(err.message(), message, "case: {case}");
4937        }
4938    }
4939
4940    #[tokio::test(flavor = "multi_thread")]
4941    async fn enum_compares_by_variant() {
4942        let code = r#"@settings(experimentalFeatures = allow)
4943type Color { | Red | Green }
4944sameEq = Color::Red == Color::Red
4945sameNeq = Color::Red != Color::Red
4946otherEq = Color::Red == Color::Green
4947otherNeq = Color::Red != Color::Green
4948"#;
4949        let result = parse_execute(code).await.unwrap();
4950
4951        for (name, expected) in [
4952            ("sameEq", true),
4953            ("sameNeq", false),
4954            ("otherEq", false),
4955            ("otherNeq", true),
4956        ] {
4957            let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
4958                panic!("`{name}` should hold a bool");
4959            };
4960            assert_eq!(value, expected, "variable: {name}");
4961        }
4962    }
4963
4964    #[tokio::test(flavor = "multi_thread")]
4965    async fn enum_usable_inside_sketch_block() {
4966        // Only enum declarations are restricted to the top level; uses are not
4967        // restricted at all. A sketch block executes its body with sketch-mode
4968        // skipping turned off, and memory lookups walk outward, so the enum
4969        // declared above resolves inside the block.
4970        //
4971        // `assertIs` runs inside the block because block-local bindings live in a
4972        // child scope that the root environment cannot read afterwards. A wrong
4973        // comparison therefore fails this test instead of passing unnoticed.
4974        let code = r#"@settings(experimentalFeatures = allow)
4975type Color { | Red | Green }
4976sketch(on = XY) {
4977  c = Color::Red
4978  assertIs(Color::Red != Color::Green)
4979  assertIs(!(Color::Red != Color::Red))
4980  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
4981}
4982"#;
4983        parse_execute(code)
4984            .await
4985            .unwrap_or_else(|err| panic!("enum use inside a sketch block should work: {}", err.message()));
4986    }
4987
4988    #[tokio::test(flavor = "multi_thread")]
4989    async fn enum_eq_reserved_inside_sketch_block() {
4990        // Inside a sketch block, `==` declares an equivalence constraint, so it is
4991        // not available for ordinary comparison. Enums are not singled out: the
4992        // interception happens before any value-comparison arm is reached, and
4993        // strings and numbers are refused in the same words. The string and number
4994        // rows are here to keep that visible -- if a later change makes enums
4995        // report something different from the other types, this test says so.
4996        //
4997        // `!=` is deliberately absent: the interception tests `Eq` only, so `!=`
4998        // still compares, which `enum_usable_inside_sketch_block` covers.
4999        let allow = "@settings(experimentalFeatures = allow)\n";
5000        let tail = "  l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}\n";
5001        for (case, declaration, comparison, types) in [
5002            (
5003                "enums",
5004                "type Color { | Red | Green }\n",
5005                "Color::Red == Color::Green",
5006                "a value of enum `Color` and a value of enum `Color`",
5007            ),
5008            ("strings", "", "\"a\" == \"b\"", "a string and a string"),
5009            ("numbers", "", "1 == 2", "a number and a number"),
5010        ] {
5011            let code = format!("{allow}{declaration}sketch(on = XY) {{\n  x = {comparison}\n{tail}");
5012            assert_eq!(
5013                parse_execute(&code).await.unwrap_err().message(),
5014                format!("Cannot create an equivalence constraint between values of these types: {types}"),
5015                "case: {case}"
5016            );
5017        }
5018    }
5019
5020    #[tokio::test(flavor = "multi_thread")]
5021    async fn enum_same_file_imported_twice_is_one_type() {
5022        // Two names for one declaration, so they are the same type and compare
5023        // equal. Identity is the declaration, not the binding, which is what makes
5024        // this different from two files that each declare a `Color`.
5025        let main = r#"@settings(experimentalFeatures = allow)
5026import Color as A from 'colors.kcl'
5027import Color as B from 'colors.kcl'
5028x = A::Red == B::Red
5029y = A::Red == B::Green
5030"#;
5031        let result = execute_with_modules(
5032            main,
5033            &[(
5034                "colors.kcl",
5035                "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
5036            )],
5037        )
5038        .await
5039        .unwrap();
5040
5041        for (name, expected) in [("x", true), ("y", false)] {
5042            let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
5043                panic!("`{name}` should hold a bool");
5044            };
5045            assert_eq!(value, expected, "variable: {name}");
5046        }
5047    }
5048
5049    #[tokio::test(flavor = "multi_thread")]
5050    async fn enum_rejects_comparison_across_types() {
5051        let allow = "@settings(experimentalFeatures = allow)\n";
5052        let color = (
5053            "a.kcl",
5054            "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
5055        );
5056        let other_color = (
5057            "b.kcl",
5058            "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
5059        );
5060
5061        for (case, main, modules, message) in [
5062            (
5063                "two enums declared separately",
5064                format!("{allow}type Color {{ | Red }}\ntype Shade {{ | Red }}\nx = Color::Red == Shade::Red\n"),
5065                vec![],
5066                "Cannot compare enum `Color` with enum `Shade`. They are different types.",
5067            ),
5068            (
5069                // Identity is the declaration, not the name, so two enums that
5070                // share a name are still different types. Pins that the message
5071                // says so rather than naming `Color` twice.
5072                "two enums sharing a name",
5073                format!(
5074                    "{allow}import Color as A from 'a.kcl'\nimport Color as B from 'b.kcl'\nx = A::Red == B::Red\n"
5075                ),
5076                vec![color, other_color],
5077                "Cannot compare two different enums that are both named `Color`. They come from separate declarations.",
5078            ),
5079            (
5080                "an enum and a number",
5081                format!("{allow}type Color {{ | Red }}\nx = Color::Red == 5\n"),
5082                vec![],
5083                "Cannot compare enum `Color::Red` with a number.",
5084            ),
5085            (
5086                "a number and an enum, in that order",
5087                format!("{allow}type Color {{ | Red }}\nx = 5 == Color::Red\n"),
5088                vec![],
5089                "Cannot compare enum `Color::Red` with a number.",
5090            ),
5091            (
5092                "an enum and a string",
5093                format!("{allow}type Color {{ | Red }}\nx = Color::Red == \"Red\"\n"),
5094                vec![],
5095                "Cannot compare enum `Color::Red` with a string.",
5096            ),
5097        ] {
5098            let err = execute_with_modules(&main, &modules).await.unwrap_err();
5099            assert_eq!(err.message(), message, "case: {case}");
5100        }
5101    }
5102
5103    #[tokio::test(flavor = "multi_thread")]
5104    async fn enum_rejects_bare_type_name_as_value() {
5105        let allow = "@settings(experimentalFeatures = allow)\n";
5106        let colors = (
5107            "colors.kcl",
5108            "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
5109        );
5110
5111        for (case, main, modules, message) in [
5112            (
5113                "enum suggests a variant",
5114                format!("{allow}type Color {{ | Red | Green }}\nx = Color\n"),
5115                vec![],
5116                "`Color` is a type, not a value. Use one of its variants, such as `Color::Red`.",
5117            ),
5118            (
5119                // The suggestion has to be pasteable into the file that produced
5120                // the error, so it uses the local name rather than the declared one.
5121                "suggestion uses the import alias",
5122                format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade\n"),
5123                vec![colors],
5124                "`Shade` is a type, not a value. Use one of its variants, such as `Shade::Red`.",
5125            ),
5126            (
5127                "enum with no variants suggests nothing",
5128                format!("{allow}type Empty {{ | }}\nx = Empty\n"),
5129                vec![],
5130                "`Empty` is a type, not a value.",
5131            ),
5132            (
5133                "a type alias reports the same way",
5134                format!("{allow}type T = number(_)\nx = T\n"),
5135                vec![],
5136                "`T` is a type, not a value.",
5137            ),
5138            (
5139                // Unchanged behavior: with no type of that name, the old message
5140                // is still the right one.
5141                "an unknown name is still undefined",
5142                "x = Nope\n".to_owned(),
5143                vec![],
5144                "`Nope` is not defined",
5145            ),
5146        ] {
5147            let err = execute_with_modules(&main, &modules).await.unwrap_err();
5148            assert_eq!(err.message(), message, "case: {case}");
5149        }
5150    }
5151
5152    #[tokio::test(flavor = "multi_thread")]
5153    async fn enum_use_gated_by_consuming_module() {
5154        // The declaring module allows experimental features; the consuming one
5155        // does not, so using the imported enum is what trips the gate. Pins that
5156        // the gate follows the consumer's settings rather than the declaration's.
5157        //
5158        // Experimental use is reported as a compilation issue rather than by
5159        // aborting the run, which is how `RuntimeType::from_alias` reports it too,
5160        // so execution succeeds and the diagnostic is what carries the complaint.
5161        let main = r#"import "colors.kcl"
5162x = colors::Color::Red
5163"#;
5164        let result = execute_with_modules(
5165            main,
5166            &[(
5167                "colors.kcl",
5168                "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
5169            )],
5170        )
5171        .await
5172        .unwrap();
5173
5174        let issues = &result.exec_state.global.issues;
5175        assert_eq!(issues.len(), 1, "issues: {issues:?}");
5176        assert_eq!(
5177            issues[0].message,
5178            "Use of the enum `Color` is experimental and may change or be removed."
5179        );
5180        assert_eq!(issues[0].severity, Severity::Error);
5181    }
5182
5183    #[tokio::test(flavor = "multi_thread")]
5184    async fn enum_use_not_gated_when_consumer_allows_it() {
5185        // The other half of the gate: with the setting present, using an enum
5186        // raises nothing at all.
5187        let code = r#"@settings(experimentalFeatures = allow)
5188type Color { | Red }
5189x = Color::Red
5190"#;
5191        let result = parse_execute(code).await.unwrap();
5192        assert!(
5193            result.exec_state.global.issues.is_empty(),
5194            "issues: {:?}",
5195            result.exec_state.global.issues
5196        );
5197    }
5198
5199    #[tokio::test(flavor = "multi_thread")]
5200    async fn enum_allows_name_sharing_outside_modules() {
5201        // Pins two deliberate exemptions from the clash rule above, so that
5202        // tightening it later has to be a decision rather than an accident.
5203        //
5204        // Only an enum or a module can head a `Color::Red` path, so only those two
5205        // can be ambiguous. A type alias cannot head a `::` path, and an ordinary
5206        // value is never looked up for a path head at all.
5207        for (case, main, modules) in [
5208            (
5209                // The module arrives second, which is the path carrying the
5210                // "only `TypeDef::Enum` conflicts" guard.
5211                "an alias may share a name with a module",
5212                "@settings(experimentalFeatures = allow)\ntype Temperature = number(_)\nimport \"Temperature.kcl\"\n",
5213                vec![("Temperature.kcl", "export x = 1\n")],
5214            ),
5215            (
5216                "a value may share a name with an enum",
5217                "@settings(experimentalFeatures = allow)\ntype Color { | Red }\nColor = 5\n",
5218                vec![],
5219            ),
5220        ] {
5221            if let Err(err) = execute_with_modules(main, &modules).await {
5222                panic!("case: {case}: {}", err.message());
5223            }
5224        }
5225    }
5226
5227    #[tokio::test(flavor = "multi_thread")]
5228    async fn enum_declaration_rejects_redefinition() {
5229        let code = r#"@settings(experimentalFeatures = allow)
5230type Color { | Red }
5231type Color { | Green }
5232"#;
5233        assert_eq!(
5234            parse_execute(code).await.unwrap_err().message(),
5235            "Redefinition of type Color."
5236        );
5237    }
5238
5239    /// Projection yields the variant's declared representation, which in V1 is
5240    /// always the variant name. Every row binds `x` so the rows differ only in the
5241    /// shape being projected, and the alias row is here because the target is
5242    /// resolved before projection decides anything, so an alias must behave
5243    /// exactly like the type it names.
5244    #[tokio::test(flavor = "multi_thread")]
5245    async fn enum_projects_to_string() {
5246        let header = r#"
5247            @settings(experimentalFeatures = allow)
5248            type Color { | Red | Green }
5249            type Label = string
5250        "#;
5251
5252        for (case, body, expected) in [
5253            ("a variant", "x = Color::Red: string", "Red"),
5254            ("another variant of the same enum", "x = Color::Green: string", "Green"),
5255            ("an alias of the target type", "x = Color::Red: Label", "Red"),
5256            (
5257                "an element of a projected array",
5258                r#"
5259                    pair = [Color::Red, Color::Green]: [string]
5260                    x = pair[1]
5261                "#,
5262                "Green",
5263            ),
5264            (
5265                "an element of a nested projected array",
5266                r#"
5267                    grid = [[Color::Green]]: [[string]]
5268                    x = grid[0][0]
5269                "#,
5270                "Green",
5271            ),
5272            (
5273                "a one-element array against a bare string",
5274                "x = [Color::Red]: string",
5275                "Red",
5276            ),
5277        ] {
5278            let result = parse_execute(&format!("{header}{body}\n"))
5279                .await
5280                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
5281            let KclValue::String { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
5282                panic!("case: {case}: `x` should hold a string");
5283            };
5284            assert_eq!(value, expected, "case: {case}");
5285        }
5286    }
5287
5288    /// Ascribing the enum's own type, directly or through an alias, is a check
5289    /// rather than a conversion: the value stays an enum and still compares equal
5290    /// to the variant it came from.
5291    #[tokio::test(flavor = "multi_thread")]
5292    async fn enum_ascription_keeps_the_enum() {
5293        let header = r#"
5294            @settings(experimentalFeatures = allow)
5295            type Color { | Red | Green }
5296            type Paint = Color
5297        "#;
5298
5299        for (case, expression, expected) in [
5300            ("its own type", "(Color::Red: Color) == Color::Red", true),
5301            ("an alias of its own type", "(Color::Red: Paint) == Color::Red", true),
5302            (
5303                "the ascription does not change which variant it is",
5304                "(Color::Red: Color) == Color::Green",
5305                false,
5306            ),
5307        ] {
5308            let result = parse_execute(&format!("{header}x = {expression}\n"))
5309                .await
5310                .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
5311            let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
5312                panic!("case: {case}: `x` should hold a bool");
5313            };
5314            assert_eq!(value, expected, "case: {case}");
5315        }
5316    }
5317
5318    /// A boundary the user did not write must not project, or a nominal parameter
5319    /// type would mean nothing. The rows are the separate coercion sites: the
5320    /// unlabeled argument, a labeled argument, and the return.
5321    #[tokio::test(flavor = "multi_thread")]
5322    async fn enum_projection_is_not_implicit() {
5323        let header = r#"
5324            @settings(experimentalFeatures = allow)
5325            type Color { | Red | Green }
5326        "#;
5327        let found = "but found a value of enum `Color` (with type `Color`).";
5328
5329        for (case, body, expected) in [
5330            (
5331                "unlabeled argument",
5332                r#"
5333                    fn label(@text: string) { return text }
5334                    x = label(Color::Red)
5335                "#,
5336                format!("The input argument of `label` requires a value with type `string`, {found}"),
5337            ),
5338            (
5339                "labeled argument",
5340                r#"
5341                    fn label(text: string) { return text }
5342                    x = label(text = Color::Red)
5343                "#,
5344                format!("text requires a value with type `string`, {found}"),
5345            ),
5346            (
5347                "return",
5348                r#"
5349                    fn label(): string { return Color::Red }
5350                    x = label()
5351                "#,
5352                format!("This function requires its result to be a value with type `string`, {found}"),
5353            ),
5354            (
5355                // The reported type is `[any; 1]` rather than `[Color; 1]` because
5356                // an array literal does not infer a homogeneous element type. That
5357                // is pre-existing and unrelated to enums; it is pinned here so the
5358                // row is not read as an enum-specific quirk.
5359                "inside an array at an argument boundary",
5360                r#"
5361                    fn labels(@text: [string]) { return text }
5362                    x = labels([Color::Red])
5363                "#,
5364                "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(),
5365            ),
5366        ] {
5367            assert_eq!(
5368                parse_execute(&format!("{header}{body}\n")).await.unwrap_err().message(),
5369                expected,
5370                "case: {case}"
5371            );
5372        }
5373    }
5374
5375    /// What an explicit ascription refuses, and what it says about it. The numeric
5376    /// rows deliberately do not name the mechanism a later version would use.
5377    #[tokio::test(flavor = "multi_thread")]
5378    async fn enum_ascription_rejections() {
5379        let header = r#"
5380            @settings(experimentalFeatures = allow)
5381            type Color { | Red }
5382            type Shade { | Red }
5383        "#;
5384        let no_number = "Cannot project enum `Color` to a number. An enum projects to `string`; projecting to a number is not supported yet.";
5385
5386        for (case, expression, expected) in [
5387            ("a number target", "Color::Red: number(_)", no_number.to_owned()),
5388            (
5389                "a number target reached through an array, so the reason survives the walk",
5390                "[Color::Red]: [number(_)]",
5391                no_number.to_owned(),
5392            ),
5393            (
5394                "a boolean target, which is not a projection at all",
5395                "Color::Red: bool",
5396                "could not coerce a value of enum `Color` (with type `Color`) to type `bool`".to_owned(),
5397            ),
5398            (
5399                "another enum whose variants happen to match",
5400                "Color::Red: Shade",
5401                "could not coerce a value of enum `Color` (with type `Color`) to type `Shade`".to_owned(),
5402            ),
5403        ] {
5404            assert_eq!(
5405                parse_execute(&format!("{header}x = {expression}\n"))
5406                    .await
5407                    .unwrap_err()
5408                    .message(),
5409                expected,
5410                "case: {case}"
5411            );
5412        }
5413    }
5414
5415    /// The mirror of `enum_projection_is_not_implicit`: where the declared type is
5416    /// the enum itself, a value flows through every boundary unchanged. Each row
5417    /// binds `x` to a comparison that must hold, so a value that arrived altered
5418    /// would fail rather than pass unnoticed. `Some(message)` marks a row that must
5419    /// be refused instead, which is what keeps the check nominal rather than
5420    /// merely permissive.
5421    #[tokio::test(flavor = "multi_thread")]
5422    async fn enum_flows_through_declared_types() {
5423        let header = r#"
5424            @settings(experimentalFeatures = allow)
5425            type Color { | Red | Green }
5426            type Shade { | Red }
5427        "#;
5428
5429        for (case, body, expected) in [
5430            (
5431                "an unlabeled parameter",
5432                r#"
5433                    fn paint(@c: Color) { return c }
5434                    x = paint(Color::Red) == Color::Red
5435                "#,
5436                None,
5437            ),
5438            (
5439                "a labeled parameter",
5440                r#"
5441                    fn paint(c: Color) { return c }
5442                    x = paint(c = Color::Green) == Color::Green
5443                "#,
5444                None,
5445            ),
5446            (
5447                "a declared return type",
5448                r#"
5449                    fn pick(): Color { return Color::Red }
5450                    x = pick() == Color::Red
5451                "#,
5452                None,
5453            ),
5454            (
5455                "an array parameter",
5456                r#"
5457                    fn firstOf(@cs: [Color]) { return cs[0] }
5458                    x = firstOf([Color::Red, Color::Green]) == Color::Red
5459                "#,
5460                None,
5461            ),
5462            (
5463                // The field check is `has_type`, which an enum satisfies, so an
5464                // object passes here while the projection row of
5465                // `enum_projects_by_target_shape` fails. Both behaviors come from
5466                // the same unfinished object coercion.
5467                "an object field",
5468                r#"
5469                    fn take(@o: { c: Color }) { return o.c }
5470                    x = take({ c = Color::Green }) == Color::Green
5471                "#,
5472                None,
5473            ),
5474            (
5475                "a union that names the enum",
5476                r#"
5477                    fn either(@v: Color | string) { return v }
5478                    x = either(Color::Red) == Color::Red
5479                "#,
5480                None,
5481            ),
5482            (
5483                "the same union given the other member",
5484                r#"
5485                    fn either(@v: Color | string) { return v }
5486                    x = either("plain") == "plain"
5487                "#,
5488                None,
5489            ),
5490            (
5491                "another declaration at the same boundary",
5492                r#"
5493                    fn paint(@c: Color) { return c }
5494                    x = paint(Shade::Red) == Shade::Red
5495                "#,
5496                Some(
5497                    "The input argument of `paint` requires a value with type `Color`, but found a value of enum `Shade` (with type `Shade`).",
5498                ),
5499            ),
5500        ] {
5501            let code = format!("{header}{body}\n");
5502            match expected {
5503                None => {
5504                    let result = parse_execute(&code)
5505                        .await
5506                        .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
5507                    let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x")
5508                    else {
5509                        panic!("case: {case}: `x` should hold a bool");
5510                    };
5511                    assert!(value, "case: {case}: the value did not survive the boundary");
5512                }
5513                Some(message) => assert_eq!(
5514                    parse_execute(&code).await.unwrap_err().message(),
5515                    message,
5516                    "case: {case}"
5517                ),
5518            }
5519        }
5520    }
5521}