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