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::Artifact;
8pub use artifact::ArtifactCommand;
9pub use artifact::ArtifactGraph;
10pub use artifact::CapSubType;
11pub use artifact::CodeRef;
12pub use artifact::GdtAnnotationArtifact;
13pub use artifact::SketchBlock;
14pub use artifact::SketchBlockConstraint;
15pub use artifact::SketchBlockConstraintType;
16pub use artifact::StartSketchOnFace;
17pub use artifact::StartSketchOnPlane;
18use cache::GlobalState;
19pub use cache::bust_cache;
20pub use cache::clear_mem_cache;
21pub use cad_op::Group;
22pub use cad_op::Operation;
23pub use geometry::*;
24pub use id_generator::IdGenerator;
25pub(crate) use import::PreImportedGeometry;
26use indexmap::IndexMap;
27pub use kcl_value::KclObjectFields;
28pub use kcl_value::KclValue;
29use kcmc::ImageFormat;
30use kcmc::ModelingCmd;
31use kcmc::each_cmd as mcmd;
32use kcmc::ok_response::OkModelingCmdResponse;
33use kcmc::ok_response::output::TakeSnapshot;
34use kcmc::websocket::ModelingSessionData;
35use kcmc::websocket::OkWebSocketResponseData;
36use kittycad_modeling_cmds::id::ModelingCmdId;
37use kittycad_modeling_cmds::{self as kcmc};
38pub use memory::EnvironmentRef;
39pub(crate) use modeling::ModelingCmdMeta;
40use serde::Deserialize;
41use serde::Serialize;
42pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
43pub(crate) use sketch_solve::solver_numeric_type;
44pub use sketch_transpiler::pre_execute_transpile;
45pub use sketch_transpiler::transpile_all_old_sketches_to_new;
46pub use sketch_transpiler::transpile_old_sketch_to_new;
47pub use sketch_transpiler::transpile_old_sketch_to_new_ast;
48pub use sketch_transpiler::transpile_old_sketch_to_new_with_execution;
49pub(crate) use state::ConstraintKey;
50pub(crate) use state::ConstraintState;
51pub(crate) use state::ConsumedSolidInfo;
52pub(crate) use state::ConsumedSolidKey;
53pub(crate) use state::ConsumedSolidOperation;
54pub use state::ExecState;
55pub(crate) use state::KclVersion;
56pub use state::MetaSettings;
57pub(crate) use state::ModuleArtifactState;
58pub(crate) use state::TangencyMode;
59use uuid::Uuid;
60
61use crate::CompilationIssue;
62use crate::ExecError;
63use crate::KclErrorWithOutputs;
64use crate::NodePath;
65use crate::SourceRange;
66use crate::collections::AhashIndexSet;
67use crate::engine::EngineBatchContext;
68use crate::engine::EngineManager;
69use crate::engine::GridScaleBehavior;
70use crate::errors::KclError;
71use crate::errors::KclErrorDetails;
72use crate::execution::cache::CacheInformation;
73use crate::execution::cache::CacheResult;
74use crate::execution::import_graph::Universe;
75use crate::execution::import_graph::UniverseMap;
76use crate::execution::typed_path::TypedPath;
77use crate::front::Number;
78use crate::front::Object;
79use crate::front::ObjectId;
80use crate::fs::FileManager;
81use crate::modules::ModuleExecutionOutcome;
82use crate::modules::ModuleId;
83use crate::modules::ModulePath;
84use crate::modules::ModuleRepr;
85use crate::parsing::ast::types::Expr;
86use crate::parsing::ast::types::ImportPath;
87use crate::parsing::ast::types::NodeRef;
88
89#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
90#[ts(export)]
91pub struct OperationsByModule {
92    pub map: IndexMap<ModuleId, Vec<Operation>>,
93}
94
95impl OperationsByModule {
96    pub fn count(&self) -> usize {
97        self.map.values().map(Vec::len).sum()
98    }
99
100    pub fn is_empty(&self) -> bool {
101        self.map.values().all(Vec::is_empty)
102    }
103
104    pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
105        self.map.get(module_id)
106    }
107
108    pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
109        self.map.values()
110    }
111
112    pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
113        self.map.insert(module_id, operations);
114    }
115}
116
117pub(crate) mod annotations;
118mod artifact;
119pub(crate) mod cache;
120mod cad_op;
121mod exec_ast;
122pub mod fn_call;
123#[cfg(test)]
124mod freedom_analysis_tests;
125mod geometry;
126mod id_generator;
127mod import;
128mod import_graph;
129pub(crate) mod kcl_value;
130mod memory;
131mod modeling;
132mod sketch_solve;
133mod sketch_transpiler;
134mod state;
135pub mod typed_path;
136pub(crate) mod types;
137
138pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
139pub(crate) const SKETCH_OBJECT_META: &str = "meta";
140pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
141
142/// Convenience macro for handling [`KclValueControlFlow`] in execution by
143/// returning early if it is some kind of early return or stripping off the
144/// control flow otherwise. If it's an early return, it's returned as a
145/// `Result::Ok`.
146macro_rules! control_continue {
147    ($control_flow:expr) => {{
148        let cf = $control_flow;
149        if cf.is_some_return() {
150            return Ok(cf);
151        } else {
152            cf.into_value()
153        }
154    }};
155}
156// Expose the macro to other modules.
157pub(crate) use control_continue;
158
159/// Convenience macro for handling [`KclValueControlFlow`] in execution by
160/// returning early if it is some kind of early return or stripping off the
161/// control flow otherwise. If it's an early return, [`EarlyReturn`] is
162/// used to return it as a `Result::Err`.
163macro_rules! early_return {
164    ($control_flow:expr) => {{
165        let cf = $control_flow;
166        if cf.is_some_return() {
167            return Err(EarlyReturn::from(cf));
168        } else {
169            cf.into_value()
170        }
171    }};
172}
173// Expose the macro to other modules.
174pub(crate) use early_return;
175
176#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
177pub enum ControlFlowKind {
178    #[default]
179    Continue,
180    Exit,
181}
182
183impl ControlFlowKind {
184    /// Returns true if this is any kind of early return.
185    pub fn is_some_return(&self) -> bool {
186        match self {
187            ControlFlowKind::Continue => false,
188            ControlFlowKind::Exit => true,
189        }
190    }
191}
192
193#[must_use = "You should always handle the control flow value when it is returned"]
194#[derive(Debug, Clone, PartialEq, Serialize)]
195pub struct KclValueControlFlow {
196    /// Use [control_continue] or [Self::into_value] to get the value.
197    value: KclValue,
198    pub control: ControlFlowKind,
199}
200
201impl KclValue {
202    pub(crate) fn continue_(self) -> KclValueControlFlow {
203        KclValueControlFlow {
204            value: self,
205            control: ControlFlowKind::Continue,
206        }
207    }
208
209    pub(crate) fn exit(self) -> KclValueControlFlow {
210        KclValueControlFlow {
211            value: self,
212            control: ControlFlowKind::Exit,
213        }
214    }
215}
216
217impl KclValueControlFlow {
218    /// Returns true if this is any kind of early return.
219    pub fn is_some_return(&self) -> bool {
220        self.control.is_some_return()
221    }
222
223    pub(crate) fn into_value(self) -> KclValue {
224        self.value
225    }
226}
227
228/// A [`KclValueControlFlow`] or an error that needs to be returned early. This
229/// is useful for when functions might encounter either control flow or errors
230/// that need to bubble up early, but these aren't the primary return values of
231/// the function. We can use `EarlyReturn` as the error type in a `Result`.
232///
233/// Normally, you don't construct this directly. Use the `early_return!` macro.
234#[must_use = "You should always handle the control flow value when it is returned"]
235#[derive(Debug, Clone)]
236pub(crate) enum EarlyReturn {
237    /// A normal value with control flow.
238    Value(KclValueControlFlow),
239    /// An error that occurred during execution.
240    Error(KclError),
241}
242
243impl From<KclValueControlFlow> for EarlyReturn {
244    fn from(cf: KclValueControlFlow) -> Self {
245        EarlyReturn::Value(cf)
246    }
247}
248
249impl From<KclError> for EarlyReturn {
250    fn from(err: KclError) -> Self {
251        EarlyReturn::Error(err)
252    }
253}
254
255pub(crate) enum StatementKind<'a> {
256    Declaration { name: &'a str },
257    Expression,
258}
259
260#[derive(Debug, Clone, Copy)]
261pub enum PreserveMem {
262    Normal,
263    Always,
264}
265
266impl PreserveMem {
267    fn normal(self) -> bool {
268        match self {
269            PreserveMem::Normal => true,
270            PreserveMem::Always => false,
271        }
272    }
273}
274
275/// Outcome of executing a program.  This is used in TS.
276#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
277#[ts(export)]
278#[serde(rename_all = "camelCase")]
279pub struct ExecOutcome {
280    /// Variables in the top-level of the root module. Note that functions will have an invalid env ref.
281    pub variables: IndexMap<String, KclValue>,
282    /// Operations that have been performed in execution order, grouped by
283    /// owning module id, for display in the Feature Tree.
284    pub operations: OperationsByModule,
285    /// Output artifact graph.
286    pub artifact_graph: ArtifactGraph,
287    /// Objects in the scene, created from execution.
288    #[serde(skip)]
289    pub scene_objects: Vec<Object>,
290    /// Map from source range to object ID for lookup of objects by their source
291    /// range.
292    #[serde(skip)]
293    pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
294    #[serde(skip)]
295    pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
296    /// Non-fatal errors and warnings.
297    pub issues: Vec<CompilationIssue>,
298    /// File Names in module Id array index order
299    pub filenames: IndexMap<ModuleId, ModulePath>,
300    /// The default planes.
301    pub default_planes: Option<DefaultPlanes>,
302}
303
304/// Per-segment freedom used by the constraint report. Mirrors
305/// [`crate::front::Freedom`] but adds an `Error` variant for when
306/// a point lookup fails.
307#[derive(Debug, Clone, Copy, PartialEq)]
308enum SegmentFreedom {
309    Free,
310    Fixed,
311    Conflict,
312    /// A required point could not be found in the scene graph.
313    Error,
314}
315
316impl From<crate::front::Freedom> for SegmentFreedom {
317    fn from(f: crate::front::Freedom) -> Self {
318        match f {
319            crate::front::Freedom::Free => Self::Free,
320            crate::front::Freedom::Fixed => Self::Fixed,
321            crate::front::Freedom::Conflict => Self::Conflict,
322        }
323    }
324}
325
326/// Overall constraint status of a sketch.
327#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
328pub enum ConstraintKind {
329    FullyConstrained,
330    UnderConstrained,
331    OverConstrained,
332    /// Analysis could not determine constraint status (e.g., a point lookup
333    /// failed due to an inconsistent scene graph). Callers decide how to treat
334    /// this — as under-constrained, over-constrained, or something else.
335    Error,
336}
337
338/// Per-sketch summary of constraint freedom analysis.
339///
340/// A sketch with no countable segments (`total_count == 0`) is reported as
341/// [`ConstraintKind::FullyConstrained`]. This is vacuously true — there are
342/// no free or conflicting segments. Callers can check `total_count == 0` to
343/// distinguish this from a genuinely constrained sketch.
344#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
345pub struct SketchConstraintStatus {
346    /// The variable name of the sketch (e.g., "sketch001").
347    pub name: String,
348    /// Overall constraint status derived from per-segment freedom.
349    pub status: ConstraintKind,
350    /// Number of segments that are under-constrained (free to move).
351    pub free_count: usize,
352    /// Number of segments that are over-constrained (conflicting constraints).
353    pub conflict_count: usize,
354    /// Total number of segments analyzed.
355    pub total_count: usize,
356}
357
358/// Grouped report of all sketches by constraint status.
359#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
360pub struct SketchConstraintReport {
361    pub fully_constrained: Vec<SketchConstraintStatus>,
362    pub under_constrained: Vec<SketchConstraintStatus>,
363    pub over_constrained: Vec<SketchConstraintStatus>,
364    /// Sketches where analysis encountered an error (e.g., a point lookup
365    /// failed). Callers decide how to treat these.
366    pub errors: Vec<SketchConstraintStatus>,
367}
368
369/// Compute the constraint status for a single sketch object.
370///
371/// Returns `None` if `sketch_obj` is not a sketch.
372///
373/// Note: a sketch with no countable segments (`total_count == 0`) is reported
374/// as [`ConstraintKind::FullyConstrained`]. This is vacuously true — there are
375/// no free or conflicting segments. Callers can check `total_count == 0` to
376/// distinguish this from a genuinely constrained sketch.
377pub(crate) fn sketch_constraint_status_for_sketch(
378    scene_objects: &[Object],
379    sketch_obj: &Object,
380) -> Option<SketchConstraintStatus> {
381    use crate::front::ObjectKind;
382    use crate::front::Segment;
383
384    let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
385        return None;
386    };
387
388    // Closure to look up a point's freedom by ObjectId.
389    let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
390        let obj = scene_objects.get(id.0)?;
391        if let ObjectKind::Segment {
392            segment: Segment::Point(p),
393        } = &obj.kind
394        {
395            Some(p.freedom())
396        } else {
397            None
398        }
399    };
400
401    let mut free_count: usize = 0;
402    let mut conflict_count: usize = 0;
403    let mut error_count: usize = 0;
404    let mut total_count: usize = 0;
405
406    for &seg_id in &sketch.segments {
407        let Some(seg_obj) = scene_objects.get(seg_id.0) else {
408            continue;
409        };
410        let ObjectKind::Segment { segment } = &seg_obj.kind else {
411            continue;
412        };
413        // Skip owned points — their freedom is already captured by
414        // the parent geometry (Line/Arc/Circle) that looks them up.
415        if let Segment::Point(p) = segment
416            && p.owner.is_some()
417        {
418            continue;
419        }
420        let freedom = segment
421            .freedom(lookup)
422            .map(SegmentFreedom::from)
423            .unwrap_or(SegmentFreedom::Error);
424        total_count += 1;
425        match freedom {
426            SegmentFreedom::Free => free_count += 1,
427            SegmentFreedom::Conflict => conflict_count += 1,
428            SegmentFreedom::Error => error_count += 1,
429            SegmentFreedom::Fixed => {}
430        }
431    }
432
433    let status = if error_count > 0 {
434        ConstraintKind::Error
435    } else if conflict_count > 0 {
436        ConstraintKind::OverConstrained
437    } else if free_count > 0 {
438        ConstraintKind::UnderConstrained
439    } else {
440        ConstraintKind::FullyConstrained
441    };
442
443    Some(SketchConstraintStatus {
444        name: sketch_obj.label.clone(),
445        status,
446        free_count,
447        conflict_count,
448        total_count,
449    })
450}
451
452pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
453    let mut fully_constrained = Vec::new();
454    let mut under_constrained = Vec::new();
455    let mut over_constrained = Vec::new();
456    let mut errors = Vec::new();
457
458    for obj in scene_objects {
459        let Some(entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
460            continue;
461        };
462        match entry.status {
463            ConstraintKind::FullyConstrained => fully_constrained.push(entry),
464            ConstraintKind::UnderConstrained => under_constrained.push(entry),
465            ConstraintKind::OverConstrained => over_constrained.push(entry),
466            ConstraintKind::Error => errors.push(entry),
467        }
468    }
469
470    SketchConstraintReport {
471        fully_constrained,
472        under_constrained,
473        over_constrained,
474        errors,
475    }
476}
477
478impl ExecOutcome {
479    pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
480        debug_assert!(
481            id.0 < self.scene_objects.len(),
482            "Requested object ID {} but only have {} objects",
483            id.0,
484            self.scene_objects.len()
485        );
486        self.scene_objects.get(id.0)
487    }
488
489    /// Returns non-fatal errors. Warnings are not included.
490    pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
491        self.issues.iter().filter(|error| error.is_err())
492    }
493
494    /// Analyze all sketches in the execution result and group them by
495    /// constraint status (fully, under, or over constrained).
496    ///
497    /// Each segment in a sketch computes its own freedom by looking up the
498    /// freedom of its constituent points. Owned points (belonging to a
499    /// Line/Arc/Circle) are skipped to avoid double-counting.
500    pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
501        sketch_constraint_report_from_scene_objects(&self.scene_objects)
502    }
503}
504
505/// Configuration for mock execution.
506#[derive(Debug, Clone, PartialEq)]
507pub struct MockConfig {
508    pub use_prev_memory: bool,
509    /// The `ObjectId` of the sketch block to execute for sketch mode. Only the
510    /// specified sketch block will be executed. All other code is ignored.
511    pub sketch_block_id: Option<ObjectId>,
512    /// True to do more costly analysis of whether the sketch block segments are
513    /// under-constrained.
514    pub freedom_analysis: bool,
515    /// The segments that were edited that triggered this execution.
516    pub segment_ids_edited: AhashIndexSet<ObjectId>,
517    /// Segment-body drag anchors that temporarily pull a point on a segment toward the cursor.
518    pub drag_anchors: Vec<SegmentDragAnchor>,
519}
520
521#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
522#[ts(export, export_to = "FrontendApi.ts")]
523#[serde(rename_all = "camelCase")]
524pub struct SegmentDragAnchor {
525    pub segment_id: ObjectId,
526    pub target: crate::front::Point2d<Number>,
527}
528
529impl Default for MockConfig {
530    fn default() -> Self {
531        Self {
532            // By default, use previous memory. This is usually what you want.
533            use_prev_memory: true,
534            sketch_block_id: None,
535            freedom_analysis: true,
536            segment_ids_edited: AhashIndexSet::default(),
537            drag_anchors: Vec::new(),
538        }
539    }
540}
541
542impl MockConfig {
543    /// Create a new mock config for sketch mode.
544    pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
545        Self {
546            sketch_block_id: Some(sketch_block_id),
547            ..Default::default()
548        }
549    }
550
551    #[must_use]
552    pub(crate) fn no_freedom_analysis(mut self) -> Self {
553        self.freedom_analysis = false;
554        self
555    }
556}
557
558#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
559#[ts(export)]
560#[serde(rename_all = "camelCase")]
561pub struct DefaultPlanes {
562    pub xy: uuid::Uuid,
563    pub xz: uuid::Uuid,
564    pub yz: uuid::Uuid,
565    pub neg_xy: uuid::Uuid,
566    pub neg_xz: uuid::Uuid,
567    pub neg_yz: uuid::Uuid,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
571#[ts(export)]
572#[serde(tag = "type", rename_all = "camelCase")]
573pub struct TagIdentifier {
574    pub value: String,
575    // Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info
576    // was written.
577    #[serde(skip)]
578    pub info: Vec<(usize, TagEngineInfo)>,
579    #[serde(skip)]
580    pub meta: Vec<Metadata>,
581}
582
583impl TagIdentifier {
584    /// Get the tag info for this tag at a specified epoch.
585    pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
586        for (e, info) in self.info.iter().rev() {
587            if *e <= at_epoch {
588                return Some(info);
589            }
590        }
591
592        None
593    }
594
595    /// Get the most recent tag info for this tag.
596    pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
597        self.info.last().map(|i| &i.1)
598    }
599
600    /// Get all tag info entries at the most recent epoch.
601    /// For region-mapped tags, this returns multiple entries (one per region segment).
602    pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
603        let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
604            return vec![];
605        };
606        self.info
607            .iter()
608            .rev()
609            .take_while(|(e, _)| *e == cur_epoch)
610            .map(|(_, info)| info)
611            .collect()
612    }
613
614    /// Add info from a different instance of this tag.
615    pub fn merge_info(&mut self, other: &TagIdentifier) {
616        assert_eq!(&self.value, &other.value);
617        for (oe, ot) in &other.info {
618            if let Some((e, t)) = self.info.last_mut() {
619                // If there is newer info, then skip this iteration.
620                if *e > *oe {
621                    continue;
622                }
623                // If we're in the same epoch, then overwrite.
624                if e == oe {
625                    *t = ot.clone();
626                    continue;
627                }
628            }
629            self.info.push((*oe, ot.clone()));
630        }
631    }
632
633    pub fn geometry(&self) -> Option<Geometry> {
634        self.get_cur_info().map(|info| info.geometry.clone())
635    }
636}
637
638impl Eq for TagIdentifier {}
639
640impl std::fmt::Display for TagIdentifier {
641    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642        write!(f, "{}", self.value)
643    }
644}
645
646impl std::str::FromStr for TagIdentifier {
647    type Err = KclError;
648
649    fn from_str(s: &str) -> Result<Self, Self::Err> {
650        Ok(Self {
651            value: s.to_string(),
652            info: Vec::new(),
653            meta: Default::default(),
654        })
655    }
656}
657
658impl Ord for TagIdentifier {
659    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
660        self.value.cmp(&other.value)
661    }
662}
663
664impl PartialOrd for TagIdentifier {
665    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
666        Some(self.cmp(other))
667    }
668}
669
670impl std::hash::Hash for TagIdentifier {
671    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
672        self.value.hash(state);
673    }
674}
675
676/// Engine information for a tag.
677#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
678#[ts(export)]
679#[serde(tag = "type", rename_all = "camelCase")]
680pub struct TagEngineInfo {
681    /// The id of the tagged object.
682    pub id: uuid::Uuid,
683    /// The geometry the tag is on.
684    pub geometry: Geometry,
685    /// The path the tag is on.
686    pub path: Option<Path>,
687    /// The surface information for the tag.
688    pub surface: Option<ExtrudeSurface>,
689}
690
691#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
692pub enum BodyType {
693    Root,
694    Block,
695}
696
697/// Metadata.
698#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
699#[ts(export)]
700#[serde(rename_all = "camelCase")]
701pub struct Metadata {
702    /// The source range.
703    pub source_range: SourceRange,
704}
705
706impl From<Metadata> for Vec<SourceRange> {
707    fn from(meta: Metadata) -> Self {
708        vec![meta.source_range]
709    }
710}
711
712impl From<&Metadata> for SourceRange {
713    fn from(meta: &Metadata) -> Self {
714        meta.source_range
715    }
716}
717
718impl From<SourceRange> for Metadata {
719    fn from(source_range: SourceRange) -> Self {
720        Self { source_range }
721    }
722}
723
724impl<T> From<NodeRef<'_, T>> for Metadata {
725    fn from(node: NodeRef<'_, T>) -> Self {
726        Self {
727            source_range: SourceRange::new(node.start, node.end, node.module_id),
728        }
729    }
730}
731
732impl From<&Expr> for Metadata {
733    fn from(expr: &Expr) -> Self {
734        Self {
735            source_range: SourceRange::from(expr),
736        }
737    }
738}
739
740impl Metadata {
741    pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
742        if meta.len() == 1 {
743            let meta = &meta[0];
744            return crate::front::SourceRef::Simple {
745                range: meta.source_range,
746                node_path,
747            };
748        }
749        crate::front::SourceRef::BackTrace {
750            ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
751        }
752    }
753}
754
755/// The type of ExecutorContext being used
756#[derive(PartialEq, Debug, Default, Clone)]
757pub enum ContextType {
758    /// Live engine connection
759    #[default]
760    Live,
761
762    /// Completely mocked connection
763    /// Mock mode is only for the Design Studio when they just want to mock engine calls and not
764    /// actually make them.
765    Mock,
766
767    /// Handled by some other interpreter/conversion system
768    MockCustomForwarded,
769}
770
771/// The executor context.
772/// Cloning will return another handle to the same engine connection/session,
773/// as this uses `Arc` under the hood.
774#[derive(Debug, Clone)]
775pub struct ExecutorContext {
776    pub engine: Arc<Box<dyn EngineManager>>,
777    pub engine_batch: EngineBatchContext,
778    pub fs: Arc<FileManager>,
779    pub settings: ExecutorSettings,
780    pub context_type: ContextType,
781}
782
783/// The executor settings.
784#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
785#[ts(export)]
786pub struct ExecutorSettings {
787    /// Highlight edges of 3D objects?
788    pub highlight_edges: bool,
789    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
790    pub enable_ssao: bool,
791    /// Show grid?
792    pub show_grid: bool,
793    /// Should engine store this for replay?
794    /// If so, under what name?
795    pub replay: Option<String>,
796    /// The directory of the current project.  This is used for resolving import
797    /// paths.  If None is given, the current working directory is used.
798    pub project_directory: Option<TypedPath>,
799    /// This is the path to the current file being executed.
800    /// We use this for preventing cyclic imports.
801    pub current_file: Option<TypedPath>,
802    /// Whether or not to automatically scale the grid when user zooms.
803    pub fixed_size_grid: bool,
804    /// Skip sending the engine messages that are only needed to build the
805    /// artifact graph. When this is true, the artifact graph will be
806    /// incomplete. So you should only use this option if you know you don't
807    /// need the artifact graph or anything that depends on it. In that case,
808    /// skipping these commands can make execution slightly faster.
809    #[serde(default, skip_serializing_if = "is_false")]
810    pub skip_artifact_graph: bool,
811    /// If Some(N), sends a heartbeat to keep the WebSocket active, every N seconds.
812    /// If None, no heartbeats will be sent.
813    #[serde(default, skip_serializing_if = "Option::is_none")]
814    pub heartbeats: Option<u64>,
815    /// If given, sets the default backface colour.
816    /// If not, defaults to whatever the engine's default is.
817    #[serde(default, skip_serializing_if = "Option::is_none")]
818    pub default_backface_color: Option<String>,
819}
820
821fn is_false(b: &bool) -> bool {
822    !*b
823}
824
825impl Default for ExecutorSettings {
826    fn default() -> Self {
827        Self {
828            highlight_edges: true,
829            enable_ssao: false,
830            show_grid: false,
831            replay: None,
832            project_directory: None,
833            current_file: None,
834            fixed_size_grid: true,
835            skip_artifact_graph: false,
836            heartbeats: None,
837            default_backface_color: None,
838        }
839    }
840}
841
842impl From<crate::settings::types::Configuration> for ExecutorSettings {
843    fn from(config: crate::settings::types::Configuration) -> Self {
844        Self::from(config.settings)
845    }
846}
847
848impl From<crate::settings::types::Settings> for ExecutorSettings {
849    fn from(settings: crate::settings::types::Settings) -> Self {
850        let modeling_settings = settings.modeling.unwrap_or_default();
851        Self {
852            highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
853            enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
854            show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
855            replay: None,
856            project_directory: None,
857            current_file: None,
858            fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
859            skip_artifact_graph: false,
860            heartbeats: None,
861            default_backface_color: modeling_settings.backface_color.map(|color| color.0),
862        }
863    }
864}
865
866impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
867    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
868        Self::from(config.settings.modeling)
869    }
870}
871
872impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
873    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
874        Self {
875            highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
876            enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
877            show_grid: modeling.show_scale_grid.unwrap_or_default(),
878            replay: None,
879            project_directory: None,
880            current_file: None,
881            fixed_size_grid: true,
882            skip_artifact_graph: false,
883            heartbeats: None,
884            default_backface_color: modeling.backface_color.map(|color| color.0),
885        }
886    }
887}
888
889impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
890    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
891        Self {
892            highlight_edges: modeling.highlight_edges.into(),
893            enable_ssao: modeling.enable_ssao.into(),
894            show_grid: Default::default(),
895            replay: None,
896            project_directory: None,
897            current_file: None,
898            fixed_size_grid: true,
899            skip_artifact_graph: false,
900            heartbeats: None,
901            default_backface_color: None,
902        }
903    }
904}
905
906impl ExecutorSettings {
907    /// Add the current file path to the executor settings.
908    pub fn with_current_file(&mut self, current_file: TypedPath) {
909        // We want the parent directory of the file.
910        if current_file.extension() == Some("kcl") {
911            self.current_file = Some(current_file.clone());
912            // Get the parent directory.
913            if let Some(parent) = current_file.parent() {
914                self.project_directory = Some(parent);
915            } else {
916                self.project_directory = Some(TypedPath::from(""));
917            }
918        } else {
919            self.project_directory = Some(current_file);
920        }
921    }
922}
923
924impl ExecutorContext {
925    /// Create a new live executor context from an engine and file manager.
926    pub fn new_with_engine_and_fs(
927        engine: Arc<Box<dyn EngineManager>>,
928        fs: Arc<FileManager>,
929        settings: ExecutorSettings,
930    ) -> Self {
931        ExecutorContext {
932            engine,
933            engine_batch: EngineBatchContext::default(),
934            fs,
935            settings,
936            context_type: ContextType::Live,
937        }
938    }
939
940    fn clone_with_fresh_execution_batch(&self) -> Self {
941        Self {
942            engine: self.engine.clone(),
943            engine_batch: EngineBatchContext::new(),
944            fs: self.fs.clone(),
945            settings: self.settings.clone(),
946            context_type: self.context_type.clone(),
947        }
948    }
949
950    /// Create a new live executor context from an engine using the local file manager.
951    #[cfg(not(target_arch = "wasm32"))]
952    pub fn new_with_engine(engine: Arc<Box<dyn EngineManager>>, settings: ExecutorSettings) -> Self {
953        Self::new_with_engine_and_fs(engine, Arc::new(FileManager::new()), settings)
954    }
955
956    /// Create a new default executor context.
957    #[cfg(not(target_arch = "wasm32"))]
958    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
959        let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
960        let (ws, _headers) = client
961            .modeling()
962            .commands_ws(kittycad::modeling::CommandsWsParams {
963                api_call_id: None,
964                fps: None,
965                order_independent_transparency: None,
966                post_effect: if settings.enable_ssao {
967                    Some(kittycad::types::PostEffectType::Ssao)
968                } else {
969                    None
970                },
971                replay: settings.replay.clone(),
972                show_grid: if settings.show_grid { Some(true) } else { None },
973                pool: None,
974                pr,
975                unlocked_framerate: None,
976                webrtc: Some(false),
977                video_res_width: None,
978                video_res_height: None,
979            })
980            .await?;
981
982        let engine: Arc<Box<dyn EngineManager>> = Arc::new(Box::new(
983            crate::engine::conn::EngineConnection::new(ws, settings.heartbeats).await?,
984        ));
985
986        Ok(Self::new_with_engine(engine, settings))
987    }
988
989    #[cfg(target_arch = "wasm32")]
990    pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
991        Self::new_with_engine_and_fs(engine, fs, settings)
992    }
993
994    #[cfg(not(target_arch = "wasm32"))]
995    pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
996        ExecutorContext {
997            engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
998            engine_batch: EngineBatchContext::default(),
999            fs: Arc::new(FileManager::new()),
1000            settings: settings.unwrap_or_default(),
1001            context_type: ContextType::Mock,
1002        }
1003    }
1004
1005    #[cfg(target_arch = "wasm32")]
1006    pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
1007        ExecutorContext {
1008            engine,
1009            engine_batch: EngineBatchContext::default(),
1010            fs,
1011            settings,
1012            context_type: ContextType::Mock,
1013        }
1014    }
1015
1016    /// Create a new mock executor context for WASM LSP servers.
1017    /// This is a convenience function that creates a mock engine and FileManager from a FileSystemManager.
1018    #[cfg(target_arch = "wasm32")]
1019    pub fn new_mock_for_lsp(
1020        fs_manager: crate::fs::wasm::FileSystemManager,
1021        settings: ExecutorSettings,
1022    ) -> Result<Self, String> {
1023        use crate::mock_engine;
1024
1025        let mock_engine = Arc::new(Box::new(
1026            mock_engine::EngineConnection::new().map_err(|e| format!("Failed to create mock engine: {:?}", e))?,
1027        ) as Box<dyn EngineManager>);
1028
1029        let fs = Arc::new(FileManager::new(fs_manager));
1030
1031        Ok(ExecutorContext {
1032            engine: mock_engine,
1033            engine_batch: EngineBatchContext::default(),
1034            fs,
1035            settings,
1036            context_type: ContextType::Mock,
1037        })
1038    }
1039
1040    #[cfg(not(target_arch = "wasm32"))]
1041    pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
1042        ExecutorContext {
1043            engine,
1044            engine_batch: EngineBatchContext::default(),
1045            fs: Arc::new(FileManager::new()),
1046            settings: Default::default(),
1047            context_type: ContextType::MockCustomForwarded,
1048        }
1049    }
1050
1051    /// Create a new default executor context.
1052    /// With a kittycad client.
1053    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
1054    /// variables.
1055    /// But also allows for passing in a token and engine address directly.
1056    #[cfg(not(target_arch = "wasm32"))]
1057    pub async fn new_with_client(
1058        settings: ExecutorSettings,
1059        token: Option<String>,
1060        engine_addr: Option<String>,
1061    ) -> Result<Self> {
1062        // Create the client.
1063        let client = crate::engine::new_zoo_client(token, engine_addr)?;
1064
1065        let ctx = Self::new(&client, settings).await?;
1066        Ok(ctx)
1067    }
1068
1069    /// Create a new default executor context.
1070    /// With the default kittycad client.
1071    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
1072    /// variables.
1073    #[cfg(not(target_arch = "wasm32"))]
1074    pub async fn new_with_default_client() -> Result<Self> {
1075        // Create the client.
1076        let ctx = Self::new_with_client(Default::default(), None, None).await?;
1077        Ok(ctx)
1078    }
1079
1080    /// For executing unit tests.
1081    #[cfg(not(target_arch = "wasm32"))]
1082    pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
1083        let ctx = ExecutorContext::new_with_client(
1084            ExecutorSettings {
1085                highlight_edges: true,
1086                enable_ssao: false,
1087                show_grid: false,
1088                replay: None,
1089                project_directory: None,
1090                current_file: None,
1091                fixed_size_grid: false,
1092                skip_artifact_graph: false,
1093                heartbeats: None,
1094                default_backface_color: None,
1095            },
1096            None,
1097            engine_addr,
1098        )
1099        .await?;
1100        Ok(ctx)
1101    }
1102
1103    pub fn is_mock(&self) -> bool {
1104        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1105    }
1106
1107    /// Returns true if we should not send engine commands for any reason.
1108    pub async fn no_engine_commands(&self) -> bool {
1109        self.is_mock()
1110    }
1111
1112    pub async fn send_clear_scene(
1113        &self,
1114        exec_state: &mut ExecState,
1115        source_range: crate::execution::SourceRange,
1116    ) -> Result<(), KclError> {
1117        // Ensure artifacts are cleared so that we don't accumulate them across
1118        // runs.
1119        exec_state.mod_local.artifacts.clear();
1120        exec_state.global.root_module_artifacts.clear();
1121        exec_state.global.artifacts.clear();
1122
1123        self.engine
1124            .clear_scene(&self.engine_batch, &mut exec_state.mod_local.id_generator, source_range)
1125            .await?;
1126        // The engine errors out if you toggle OIT with SSAO off.
1127        // So ignore OIT settings if SSAO is off.
1128        if self.settings.enable_ssao {
1129            let cmd_id = exec_state.next_uuid();
1130            exec_state
1131                .batch_modeling_cmd(
1132                    ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1133                    ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1134                )
1135                .await?;
1136        }
1137        Ok(())
1138    }
1139
1140    pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1141        cache::bust_cache().await;
1142
1143        // Execute an empty program to clear and reset the scene.
1144        // We specifically want to be returned the objects after the scene is reset.
1145        // Like the default planes so it is easier to just execute an empty program
1146        // after the cache is busted.
1147        let outcome = self.run_with_caching(crate::Program::empty()).await?;
1148
1149        Ok(outcome)
1150    }
1151
1152    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1153        self.eval_prelude(exec_state, SourceRange::synthetic())
1154            .await
1155            .map_err(KclErrorWithOutputs::no_outputs)?;
1156        exec_state.mut_stack().push_new_root_env(true);
1157        Ok(())
1158    }
1159
1160    fn restore_mock_memory(
1161        exec_state: &mut ExecState,
1162        mem: cache::SketchModeState,
1163        _mock_config: &MockConfig,
1164    ) -> Result<(), KclErrorWithOutputs> {
1165        *exec_state.mut_stack() = mem.stack;
1166        exec_state.global.module_infos = mem.module_infos;
1167        exec_state.global.path_to_source_id = mem.path_to_source_id;
1168        exec_state.global.id_to_source = mem.id_to_source;
1169        exec_state.mod_local.constraint_state = mem.constraint_state;
1170        let len = _mock_config
1171            .sketch_block_id
1172            .map(|sketch_block_id| sketch_block_id.0)
1173            .unwrap_or(0);
1174        if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1175            exec_state
1176                .global
1177                .root_module_artifacts
1178                .restore_scene_objects(scene_objects);
1179        } else {
1180            let message = format!(
1181                "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1182                mem.scene_objects.len(),
1183                len
1184            );
1185            debug_assert!(false, "{message}");
1186            return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1187                KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1188            )));
1189        }
1190
1191        Ok(())
1192    }
1193
1194    pub async fn run_mock(
1195        &self,
1196        program: &crate::Program,
1197        mock_config: &MockConfig,
1198    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1199        assert!(
1200            self.is_mock(),
1201            "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1202        );
1203
1204        let use_prev_memory = mock_config.use_prev_memory;
1205        let mut exec_state = ExecState::new_mock(self, mock_config);
1206        if use_prev_memory {
1207            match cache::read_old_memory().await {
1208                Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
1209                None => self.prepare_mem(&mut exec_state).await?,
1210            }
1211        } else {
1212            self.prepare_mem(&mut exec_state).await?
1213        };
1214
1215        // Push a scope so that old variables can be overwritten (since we might be re-executing some
1216        // part of the scene).
1217        exec_state.mut_stack().push_new_env_for_scope();
1218
1219        let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1220
1221        // Restore any temporary variables, then save any newly created variables back to
1222        // memory in case another run wants to use them. Note this is just saved to the preserved
1223        // memory, not to the exec_state which is not cached for mock execution.
1224
1225        let mut stack = exec_state.stack().clone();
1226        let module_infos = exec_state.global.module_infos.clone();
1227        let path_to_source_id = exec_state.global.path_to_source_id.clone();
1228        let id_to_source = exec_state.global.id_to_source.clone();
1229        let constraint_state = exec_state.mod_local.constraint_state.clone();
1230        let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1231        let outcome = exec_state.into_exec_outcome(result.0, self).await;
1232
1233        stack.squash_env(result.0);
1234        let state = cache::SketchModeState {
1235            stack,
1236            module_infos,
1237            path_to_source_id,
1238            id_to_source,
1239            constraint_state,
1240            scene_objects,
1241        };
1242        cache::write_old_memory(state).await;
1243
1244        Ok(outcome)
1245    }
1246
1247    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1248        assert!(!self.is_mock());
1249        let grid_scale = if self.settings.fixed_size_grid {
1250            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1251        } else {
1252            GridScaleBehavior::ScaleWithZoom
1253        };
1254
1255        let original_program = program.clone();
1256
1257        let (_program, exec_state, result) = match cache::read_old_ast().await {
1258            Some(mut cached_state) => {
1259                let old = CacheInformation {
1260                    ast: &cached_state.main.ast,
1261                    settings: &cached_state.settings,
1262                };
1263                let new = CacheInformation {
1264                    ast: &program.ast,
1265                    settings: &self.settings,
1266                };
1267
1268                // Get the program that actually changed from the old and new information.
1269                let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1270                    CacheResult::ReExecute {
1271                        clear_scene,
1272                        reapply_settings,
1273                        program: changed_program,
1274                    } => {
1275                        if reapply_settings
1276                            && self
1277                                .engine
1278                                .reapply_settings(
1279                                    &self.engine_batch,
1280                                    &self.settings,
1281                                    Default::default(),
1282                                    &mut cached_state.main.exec_state.id_generator,
1283                                    grid_scale,
1284                                )
1285                                .await
1286                                .is_err()
1287                        {
1288                            (true, program, None)
1289                        } else {
1290                            (
1291                                clear_scene,
1292                                crate::Program {
1293                                    ast: changed_program,
1294                                    original_file_contents: program.original_file_contents,
1295                                },
1296                                None,
1297                            )
1298                        }
1299                    }
1300                    CacheResult::CheckImportsOnly {
1301                        reapply_settings,
1302                        ast: changed_program,
1303                    } => {
1304                        let mut reapply_failed = false;
1305                        if reapply_settings {
1306                            if self
1307                                .engine
1308                                .reapply_settings(
1309                                    &self.engine_batch,
1310                                    &self.settings,
1311                                    Default::default(),
1312                                    &mut cached_state.main.exec_state.id_generator,
1313                                    grid_scale,
1314                                )
1315                                .await
1316                                .is_ok()
1317                            {
1318                                cache::write_old_ast(GlobalState::with_settings(
1319                                    cached_state.clone(),
1320                                    self.settings.clone(),
1321                                ))
1322                                .await;
1323                            } else {
1324                                reapply_failed = true;
1325                            }
1326                        }
1327
1328                        if reapply_failed {
1329                            (true, program, None)
1330                        } else {
1331                            // We need to check our imports to see if they changed.
1332                            let mut new_exec_state = ExecState::new(self);
1333                            let (new_universe, new_universe_map) =
1334                                self.get_universe(&program, &mut new_exec_state).await?;
1335
1336                            let clear_scene = new_universe.values().any(|value| {
1337                                let id = value.1;
1338                                match (
1339                                    cached_state.exec_state.get_source(id),
1340                                    new_exec_state.global.get_source(id),
1341                                ) {
1342                                    (Some(s0), Some(s1)) => s0.source != s1.source,
1343                                    _ => false,
1344                                }
1345                            });
1346
1347                            if !clear_scene {
1348                                // Return early we don't need to clear the scene.
1349                                cache::write_old_memory(cached_state.mock_memory_state()).await;
1350                                return Ok(cached_state.into_exec_outcome(self).await);
1351                            }
1352
1353                            (
1354                                true,
1355                                crate::Program {
1356                                    ast: changed_program,
1357                                    original_file_contents: program.original_file_contents,
1358                                },
1359                                Some((new_universe, new_universe_map, new_exec_state)),
1360                            )
1361                        }
1362                    }
1363                    CacheResult::NoAction(true) => {
1364                        if self
1365                            .engine
1366                            .reapply_settings(
1367                                &self.engine_batch,
1368                                &self.settings,
1369                                Default::default(),
1370                                &mut cached_state.main.exec_state.id_generator,
1371                                grid_scale,
1372                            )
1373                            .await
1374                            .is_ok()
1375                        {
1376                            // We need to update the old ast state with the new settings!!
1377                            cache::write_old_ast(GlobalState::with_settings(
1378                                cached_state.clone(),
1379                                self.settings.clone(),
1380                            ))
1381                            .await;
1382
1383                            cache::write_old_memory(cached_state.mock_memory_state()).await;
1384                            return Ok(cached_state.into_exec_outcome(self).await);
1385                        }
1386                        (true, program, None)
1387                    }
1388                    CacheResult::NoAction(false) => {
1389                        cache::write_old_memory(cached_state.mock_memory_state()).await;
1390                        return Ok(cached_state.into_exec_outcome(self).await);
1391                    }
1392                };
1393
1394                let (exec_state, result) = match import_check_info {
1395                    Some((new_universe, new_universe_map, mut new_exec_state)) => {
1396                        // Clear the scene if the imports changed.
1397                        self.send_clear_scene(&mut new_exec_state, Default::default())
1398                            .await
1399                            .map_err(KclErrorWithOutputs::no_outputs)?;
1400
1401                        let result = self
1402                            .run_concurrent(
1403                                &program,
1404                                &mut new_exec_state,
1405                                Some((new_universe, new_universe_map)),
1406                                PreserveMem::Normal,
1407                            )
1408                            .await;
1409
1410                        (new_exec_state, result)
1411                    }
1412                    None if clear_scene => {
1413                        // Pop the execution state, since we are starting fresh.
1414                        let mut exec_state = cached_state.reconstitute_exec_state();
1415                        exec_state.reset(self);
1416
1417                        self.send_clear_scene(&mut exec_state, Default::default())
1418                            .await
1419                            .map_err(KclErrorWithOutputs::no_outputs)?;
1420
1421                        let result = self
1422                            .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1423                            .await;
1424
1425                        (exec_state, result)
1426                    }
1427                    None => {
1428                        let mut exec_state = cached_state.reconstitute_exec_state();
1429                        exec_state.mut_stack().restore_env(cached_state.main.result_env);
1430
1431                        let result = self
1432                            .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1433                            .await;
1434
1435                        (exec_state, result)
1436                    }
1437                };
1438
1439                (program, exec_state, result)
1440            }
1441            None => {
1442                let mut exec_state = ExecState::new(self);
1443                self.send_clear_scene(&mut exec_state, Default::default())
1444                    .await
1445                    .map_err(KclErrorWithOutputs::no_outputs)?;
1446
1447                let result = self
1448                    .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1449                    .await;
1450
1451                (program, exec_state, result)
1452            }
1453        };
1454
1455        if result.is_err() {
1456            cache::bust_cache().await;
1457        }
1458
1459        // Throw the error.
1460        let result = result?;
1461
1462        // Save this as the last successful execution to the cache.
1463        // Gotcha: `CacheResult::ReExecute.program` may be diff-based, do not save that AST
1464        // the last-successful AST. Instead, save in the full AST passed in.
1465        cache::write_old_ast(GlobalState::new(
1466            exec_state.clone(),
1467            self.settings.clone(),
1468            original_program.ast,
1469            result.0,
1470        ))
1471        .await;
1472
1473        let outcome = exec_state.into_exec_outcome(result.0, self).await;
1474        Ok(outcome)
1475    }
1476
1477    /// Perform the execution of a program.
1478    ///
1479    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
1480    pub async fn run(
1481        &self,
1482        program: &crate::Program,
1483        exec_state: &mut ExecState,
1484    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1485        self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1486            .await
1487    }
1488
1489    /// Perform the execution of a program using a concurrent
1490    /// execution model.
1491    ///
1492    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
1493    pub async fn run_concurrent(
1494        &self,
1495        program: &crate::Program,
1496        exec_state: &mut ExecState,
1497        universe_info: Option<(Universe, UniverseMap)>,
1498        preserve_mem: PreserveMem,
1499    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1500        // Reuse our cached universe if we have one.
1501
1502        let (universe, _universe_map) = if let Some((universe, universe_map)) = universe_info {
1503            (universe, universe_map)
1504        } else {
1505            self.get_universe(program, exec_state).await?
1506        };
1507
1508        let default_planes = self.engine.get_default_planes().read().await.clone();
1509
1510        // Run the prelude to set up the engine.
1511        self.eval_prelude(exec_state, SourceRange::synthetic())
1512            .await
1513            .map_err(KclErrorWithOutputs::no_outputs)?;
1514
1515        for modules in import_graph::import_graph(&universe, self)
1516            .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1517            .into_iter()
1518        {
1519            #[cfg(not(target_arch = "wasm32"))]
1520            let mut set = tokio::task::JoinSet::new();
1521
1522            #[allow(clippy::type_complexity)]
1523            let (results_tx, mut results_rx): (
1524                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1525                tokio::sync::mpsc::Receiver<_>,
1526            ) = tokio::sync::mpsc::channel(1);
1527
1528            for module in modules {
1529                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1530                    return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1531                        KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1532                    )));
1533                };
1534                let module_id = *module_id;
1535                let module_path = module_path.clone();
1536                let source_range = SourceRange::from(import_stmt);
1537                // Clone before mutating.
1538                let module_exec_state = exec_state.clone();
1539
1540                let repr = repr.clone();
1541                let exec_ctxt = self.clone_with_fresh_execution_batch();
1542                let results_tx = results_tx.clone();
1543
1544                let exec_module = async |exec_ctxt: &ExecutorContext,
1545                                         repr: &ModuleRepr,
1546                                         module_id: ModuleId,
1547                                         module_path: &ModulePath,
1548                                         exec_state: &mut ExecState,
1549                                         source_range: SourceRange|
1550                       -> Result<ModuleRepr, KclError> {
1551                    match repr {
1552                        ModuleRepr::Kcl(program, _) => {
1553                            let result = exec_ctxt
1554                                .exec_module_from_ast(
1555                                    program,
1556                                    module_id,
1557                                    module_path,
1558                                    exec_state,
1559                                    source_range,
1560                                    PreserveMem::Normal,
1561                                )
1562                                .await;
1563
1564                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1565                        }
1566                        ModuleRepr::Foreign(geom, _) => {
1567                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1568                                .await
1569                                .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1570
1571                            result.map(|val| {
1572                                ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
1573                            })
1574                        }
1575                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1576                            format!("Module {module_path} not found in universe"),
1577                            vec![source_range],
1578                        ))),
1579                    }
1580                };
1581
1582                #[cfg(target_arch = "wasm32")]
1583                {
1584                    wasm_bindgen_futures::spawn_local(async move {
1585                        let mut exec_state = module_exec_state;
1586                        let exec_ctxt = exec_ctxt;
1587
1588                        let result = exec_module(
1589                            &exec_ctxt,
1590                            &repr,
1591                            module_id,
1592                            &module_path,
1593                            &mut exec_state,
1594                            source_range,
1595                        )
1596                        .await;
1597
1598                        results_tx
1599                            .send((module_id, module_path, result))
1600                            .await
1601                            .unwrap_or_default();
1602                    });
1603                }
1604                #[cfg(not(target_arch = "wasm32"))]
1605                {
1606                    set.spawn(async move {
1607                        let mut exec_state = module_exec_state;
1608                        let exec_ctxt = exec_ctxt;
1609
1610                        let result = exec_module(
1611                            &exec_ctxt,
1612                            &repr,
1613                            module_id,
1614                            &module_path,
1615                            &mut exec_state,
1616                            source_range,
1617                        )
1618                        .await;
1619
1620                        results_tx
1621                            .send((module_id, module_path, result))
1622                            .await
1623                            .unwrap_or_default();
1624                    });
1625                }
1626            }
1627
1628            drop(results_tx);
1629
1630            while let Some((module_id, _, result)) = results_rx.recv().await {
1631                match result {
1632                    Ok(new_repr) => {
1633                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1634
1635                        match &mut repr {
1636                            ModuleRepr::Kcl(_, cache) => {
1637                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
1638                                    unreachable!();
1639                                };
1640                                *cache = session_data;
1641                            }
1642                            ModuleRepr::Foreign(_, cache) => {
1643                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
1644                                    unreachable!();
1645                                };
1646                                *cache = session_data;
1647                            }
1648                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1649                        }
1650
1651                        exec_state.global.module_infos[&module_id].restore_repr(repr);
1652                    }
1653                    Err(e) => {
1654                        return Err(exec_state.error_with_outputs(e, None, default_planes));
1655                    }
1656                }
1657            }
1658        }
1659
1660        // Since we haven't technically started executing the root module yet,
1661        // move any setup artifacts accumulated so far into the root state.
1662        exec_state
1663            .global
1664            .root_module_artifacts
1665            .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1666
1667        self.inner_run(program, exec_state, preserve_mem).await
1668    }
1669
1670    /// Get the universe & universe map of the program.
1671    /// And see if any of the imports changed.
1672    async fn get_universe(
1673        &self,
1674        program: &crate::Program,
1675        exec_state: &mut ExecState,
1676    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1677        exec_state.add_root_module_contents(program);
1678
1679        let mut universe = std::collections::HashMap::new();
1680
1681        let default_planes = self.engine.get_default_planes().read().await.clone();
1682
1683        let root_imports = import_graph::import_universe(
1684            self,
1685            &ModulePath::Main,
1686            &ModuleRepr::Kcl(program.ast.clone(), None),
1687            &mut universe,
1688            exec_state,
1689        )
1690        .await
1691        .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1692
1693        Ok((universe, root_imports))
1694    }
1695
1696    /// Perform the execution of a program.  Accept all possible parameters and
1697    /// output everything.
1698    async fn inner_run(
1699        &self,
1700        program: &crate::Program,
1701        exec_state: &mut ExecState,
1702        preserve_mem: PreserveMem,
1703    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1704        let _stats = crate::log::LogPerfStats::new("Interpretation");
1705
1706        // Re-apply the settings, in case the cache was busted.
1707        let grid_scale = if self.settings.fixed_size_grid {
1708            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1709        } else {
1710            GridScaleBehavior::ScaleWithZoom
1711        };
1712        self.engine
1713            .reapply_settings(
1714                &self.engine_batch,
1715                &self.settings,
1716                Default::default(),
1717                exec_state.id_generator(),
1718                grid_scale,
1719            )
1720            .await
1721            .map_err(KclErrorWithOutputs::no_outputs)?;
1722
1723        let default_planes = self.engine.get_default_planes().read().await.clone();
1724        let result = self
1725            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1726            .await;
1727
1728        crate::log::log(format!(
1729            "Post interpretation KCL memory stats: {:#?}",
1730            exec_state.stack().memory.stats
1731        ));
1732        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1733
1734        /// Write the memory of an execution to the cache for reuse in mock
1735        /// execution.
1736        async fn write_old_memory(ctx: &ExecutorContext, exec_state: &ExecState, env_ref: EnvironmentRef) {
1737            if ctx.is_mock() {
1738                return;
1739            }
1740            let mut stack = exec_state.stack().deep_clone();
1741            stack.restore_env(env_ref);
1742            let state = cache::SketchModeState {
1743                stack,
1744                module_infos: exec_state.global.module_infos.clone(),
1745                path_to_source_id: exec_state.global.path_to_source_id.clone(),
1746                id_to_source: exec_state.global.id_to_source.clone(),
1747                constraint_state: exec_state.mod_local.constraint_state.clone(),
1748                scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1749            };
1750            cache::write_old_memory(state).await;
1751        }
1752
1753        let env_ref = match result {
1754            Ok(env_ref) => env_ref,
1755            Err((err, env_ref)) => {
1756                // Preserve memory on execution failures so follow-up mock
1757                // execution can still reuse stable IDs before the error.
1758                if let Some(env_ref) = env_ref {
1759                    write_old_memory(self, exec_state, env_ref).await;
1760                }
1761                return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
1762            }
1763        };
1764
1765        write_old_memory(self, exec_state, env_ref).await;
1766
1767        let session_data = self.engine.get_session_data().await;
1768
1769        Ok((env_ref, session_data))
1770    }
1771
1772    /// Execute an AST's program and build auxiliary outputs like the artifact
1773    /// graph.
1774    async fn execute_and_build_graph(
1775        &self,
1776        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1777        exec_state: &mut ExecState,
1778        preserve_mem: PreserveMem,
1779    ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1780        // Don't early return!  We need to build other outputs regardless of
1781        // whether execution failed.
1782
1783        // Because of execution caching, we may start with operations from a
1784        // previous run.
1785        let start_op = exec_state.global.root_module_artifacts.operations.len();
1786
1787        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1788            .await
1789            .map_err(|e| (e, None))?;
1790
1791        let exec_result = self
1792            .exec_module_body(
1793                program,
1794                exec_state,
1795                preserve_mem,
1796                ModuleId::default(),
1797                &ModulePath::Main,
1798            )
1799            .await
1800            .map(
1801                |ModuleExecutionOutcome {
1802                     environment: env_ref,
1803                     artifacts: module_artifacts,
1804                     ..
1805                 }| {
1806                    // We need to extend because it may already have operations from
1807                    // imports.
1808                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1809                    env_ref
1810                },
1811            )
1812            .map_err(|(err, env_ref, module_artifacts)| {
1813                if let Some(module_artifacts) = module_artifacts {
1814                    // We need to extend because it may already have operations
1815                    // from imports.
1816                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1817                }
1818                (err, env_ref)
1819            });
1820
1821        // Fill in NodePath for operations.
1822        let programs = &exec_state.build_program_lookup(program.clone());
1823        let cached_body_items = exec_state.global.artifacts.cached_body_items();
1824        for op in exec_state
1825            .global
1826            .root_module_artifacts
1827            .operations
1828            .iter_mut()
1829            .skip(start_op)
1830        {
1831            op.fill_node_paths(programs, cached_body_items);
1832        }
1833        for module in exec_state.global.module_infos.values_mut() {
1834            if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1835                for op in &mut outcome.artifacts.operations {
1836                    op.fill_node_paths(programs, cached_body_items);
1837                }
1838            }
1839        }
1840
1841        // Ensure all the async commands completed.
1842        self.engine
1843            .ensure_async_commands_completed(&self.engine_batch)
1844            .await
1845            .map_err(|e| {
1846                match &exec_result {
1847                    Ok(env_ref) => (e, Some(*env_ref)),
1848                    // Prefer the execution error.
1849                    Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1850                }
1851            })?;
1852
1853        // If we errored out and early-returned, there might be commands which haven't been executed
1854        // and should be dropped.
1855        self.engine.clear_queues(&self.engine_batch).await;
1856
1857        match exec_state.build_artifact_graph(&self.engine, program).await {
1858            Ok(_) => exec_result,
1859            Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1860        }
1861    }
1862
1863    /// 'Import' std::prelude as the outermost scope.
1864    ///
1865    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
1866    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1867        if exec_state.stack().memory.requires_std() {
1868            let initial_ops = exec_state.mod_local.artifacts.operations.len();
1869
1870            let path = vec!["std".to_owned(), "prelude".to_owned()];
1871            let resolved_path = ModulePath::from_std_import_path(&path)?;
1872            let id = self
1873                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1874                .await?;
1875            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1876
1877            exec_state.mut_stack().memory.set_std(module_memory);
1878
1879            // Operations generated by the prelude are not useful, so clear them
1880            // out.
1881            //
1882            // TODO: Should we also clear them out of each module so that they
1883            // don't appear in test output?
1884            exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1885        }
1886
1887        Ok(())
1888    }
1889
1890    /// Get a snapshot of the current scene.
1891    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1892        // Zoom to fit.
1893        self.engine
1894            .send_modeling_cmd(
1895                &self.engine_batch,
1896                uuid::Uuid::new_v4(),
1897                crate::execution::SourceRange::default(),
1898                &ModelingCmd::from(
1899                    mcmd::ZoomToFit::builder()
1900                        .object_ids(Default::default())
1901                        .animated(false)
1902                        .padding(0.1)
1903                        .build(),
1904                ),
1905            )
1906            .await
1907            .map_err(KclErrorWithOutputs::no_outputs)?;
1908
1909        // Send a snapshot request to the engine.
1910        let resp = self
1911            .engine
1912            .send_modeling_cmd(
1913                &self.engine_batch,
1914                uuid::Uuid::new_v4(),
1915                crate::execution::SourceRange::default(),
1916                &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
1917            )
1918            .await
1919            .map_err(KclErrorWithOutputs::no_outputs)?;
1920
1921        let OkWebSocketResponseData::Modeling {
1922            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1923        } = resp
1924        else {
1925            return Err(ExecError::BadPng(format!(
1926                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1927            )));
1928        };
1929        Ok(contents)
1930    }
1931
1932    /// Export the current scene as a CAD file.
1933    pub async fn export(
1934        &self,
1935        format: kittycad_modeling_cmds::format::OutputFormat3d,
1936    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1937        let resp = self
1938            .engine
1939            .send_modeling_cmd(
1940                &self.engine_batch,
1941                uuid::Uuid::new_v4(),
1942                crate::SourceRange::default(),
1943                &kittycad_modeling_cmds::ModelingCmd::Export(
1944                    kittycad_modeling_cmds::Export::builder()
1945                        .entity_ids(vec![])
1946                        .format(format)
1947                        .build(),
1948                ),
1949            )
1950            .await?;
1951
1952        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1953            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1954                format!("Expected Export response, got {resp:?}",),
1955                vec![SourceRange::default()],
1956            )));
1957        };
1958
1959        Ok(files)
1960    }
1961
1962    /// Export the current scene as a STEP file.
1963    pub async fn export_step(
1964        &self,
1965        deterministic_time: bool,
1966    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1967        let files = self
1968            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1969                kittycad_modeling_cmds::format::step::export::Options::builder()
1970                    .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
1971                    .maybe_created(if deterministic_time {
1972                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1973                            KclError::new_internal(crate::errors::KclErrorDetails::new(
1974                                format!("Failed to parse date: {e}"),
1975                                vec![SourceRange::default()],
1976                            ))
1977                        })?)
1978                    } else {
1979                        None
1980                    })
1981                    .build(),
1982            ))
1983            .await?;
1984
1985        Ok(files)
1986    }
1987
1988    pub async fn close(&self) {
1989        self.engine.close().await;
1990    }
1991}
1992
1993#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1994pub struct ArtifactId(Uuid);
1995
1996impl ArtifactId {
1997    pub fn new(uuid: Uuid) -> Self {
1998        Self(uuid)
1999    }
2000
2001    /// A placeholder artifact ID that will be filled in later.
2002    pub fn placeholder() -> Self {
2003        Self(Uuid::nil())
2004    }
2005}
2006
2007impl From<Uuid> for ArtifactId {
2008    fn from(uuid: Uuid) -> Self {
2009        Self::new(uuid)
2010    }
2011}
2012
2013impl From<&Uuid> for ArtifactId {
2014    fn from(uuid: &Uuid) -> Self {
2015        Self::new(*uuid)
2016    }
2017}
2018
2019impl From<ArtifactId> for Uuid {
2020    fn from(id: ArtifactId) -> Self {
2021        id.0
2022    }
2023}
2024
2025impl From<&ArtifactId> for Uuid {
2026    fn from(id: &ArtifactId) -> Self {
2027        id.0
2028    }
2029}
2030
2031impl From<ModelingCmdId> for ArtifactId {
2032    fn from(id: ModelingCmdId) -> Self {
2033        Self::new(*id.as_ref())
2034    }
2035}
2036
2037impl From<&ModelingCmdId> for ArtifactId {
2038    fn from(id: &ModelingCmdId) -> Self {
2039        Self::new(*id.as_ref())
2040    }
2041}
2042
2043#[cfg(test)]
2044pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2045    parse_execute_with_project_dir(code, None).await
2046}
2047
2048#[cfg(test)]
2049pub(crate) async fn parse_execute_with_project_dir(
2050    code: &str,
2051    project_directory: Option<TypedPath>,
2052) -> Result<ExecTestResults, KclError> {
2053    let program = crate::Program::parse_no_errs(code)?;
2054
2055    let exec_ctxt = ExecutorContext {
2056        engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
2057            |err| {
2058                KclError::new_internal(crate::errors::KclErrorDetails::new(
2059                    format!("Failed to create mock engine connection: {err}"),
2060                    vec![SourceRange::default()],
2061                ))
2062            },
2063        )?)),
2064        engine_batch: EngineBatchContext::default(),
2065        fs: Arc::new(crate::fs::FileManager::new()),
2066        settings: ExecutorSettings {
2067            project_directory,
2068            ..Default::default()
2069        },
2070        context_type: ContextType::Mock,
2071    };
2072    let mut exec_state = ExecState::new(&exec_ctxt);
2073    let result = exec_ctxt.run(&program, &mut exec_state).await?;
2074
2075    Ok(ExecTestResults {
2076        program,
2077        mem_env: result.0,
2078        exec_ctxt,
2079        exec_state,
2080    })
2081}
2082
2083#[cfg(test)]
2084#[derive(Debug)]
2085pub(crate) struct ExecTestResults {
2086    program: crate::Program,
2087    mem_env: EnvironmentRef,
2088    exec_ctxt: ExecutorContext,
2089    exec_state: ExecState,
2090}
2091
2092#[cfg(test)]
2093impl ExecTestResults {
2094    pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2095        &self.exec_state.global.root_module_artifacts.commands
2096    }
2097}
2098
2099/// There are several places where we want to traverse a KCL program or find a symbol in it,
2100/// but because KCL modules can import each other, we need to traverse multiple programs.
2101/// This stores multiple programs, keyed by their module ID for quick access.
2102pub struct ProgramLookup {
2103    programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2104}
2105
2106impl ProgramLookup {
2107    // TODO: Could this store a reference to KCL programs instead of owning them?
2108    // i.e. take &state::ModuleInfoMap instead?
2109    pub fn new(
2110        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2111        module_infos: state::ModuleInfoMap,
2112    ) -> Self {
2113        let mut programs = IndexMap::with_capacity(module_infos.len());
2114        for (id, info) in module_infos {
2115            if let ModuleRepr::Kcl(program, _) = info.repr {
2116                programs.insert(id, program);
2117            }
2118        }
2119        programs.insert(ModuleId::default(), current);
2120        Self { programs }
2121    }
2122
2123    pub fn program_for_module(
2124        &self,
2125        module_id: ModuleId,
2126    ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2127        self.programs.get(&module_id)
2128    }
2129}
2130
2131#[cfg(test)]
2132mod tests {
2133    use pretty_assertions::assert_eq;
2134
2135    use super::*;
2136    use crate::ModuleId;
2137    use crate::errors::KclErrorDetails;
2138    use crate::errors::Severity;
2139    use crate::exec::NumericType;
2140    use crate::execution::memory::Stack;
2141    use crate::execution::types::RuntimeType;
2142
2143    /// Convenience function to get a JSON value from memory and unwrap.
2144    #[track_caller]
2145    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2146        memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
2147    }
2148
2149    #[tokio::test(flavor = "multi_thread")]
2150    async fn test_execute_warn() {
2151        let text = "@blah";
2152        let result = parse_execute(text).await.unwrap();
2153        let errs = result.exec_state.issues();
2154        assert_eq!(errs.len(), 1);
2155        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2156        assert!(
2157            errs[0].message.contains("Unknown annotation"),
2158            "unexpected warning message: {}",
2159            errs[0].message
2160        );
2161    }
2162
2163    #[tokio::test(flavor = "multi_thread")]
2164    async fn test_execute_fn_definitions() {
2165        let ast = r#"fn def(@x) {
2166  return x
2167}
2168fn ghi(@x) {
2169  return x
2170}
2171fn jkl(@x) {
2172  return x
2173}
2174fn hmm(@x) {
2175  return x
2176}
2177
2178yo = 5 + 6
2179
2180abc = 3
2181identifierGuy = 5
2182part001 = startSketchOn(XY)
2183|> startProfile(at = [-1.2, 4.83])
2184|> line(end = [2.8, 0])
2185|> angledLine(angle = 100 + 100, length = 3.01)
2186|> angledLine(angle = abc, length = 3.02)
2187|> angledLine(angle = def(yo), length = 3.03)
2188|> angledLine(angle = ghi(2), length = 3.04)
2189|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2190|> close()
2191yo2 = hmm([identifierGuy + 5])"#;
2192
2193        parse_execute(ast).await.unwrap();
2194    }
2195
2196    #[tokio::test(flavor = "multi_thread")]
2197    async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2198        let code = r#"
2199firstProfile = sketch(on = XY) {
2200  edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2201  edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2202  edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2203  edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2204  coincident([edge1.end, edge2.start])
2205  coincident([edge2.end, edge3.start])
2206  coincident([edge3.end, edge4.start])
2207  coincident([edge4.end, edge1.start])
2208}
2209
2210secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2211  edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2212  edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2213  edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2214  edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2215  coincident([edge5.end, edge6.start])
2216  coincident([edge6.end, edge7.start])
2217  coincident([edge7.end, edge8.start])
2218  coincident([edge8.end, edge5.start])
2219}
2220
2221firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2222secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2223"#;
2224
2225        let result = parse_execute(code).await.unwrap();
2226        assert!(result.exec_state.issues().is_empty());
2227    }
2228
2229    #[tokio::test(flavor = "multi_thread")]
2230    async fn sketch_block_artifact_preserves_standard_plane_name() {
2231        let code = r#"
2232sketch001 = sketch(on = -YZ) {
2233  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
2234}
2235"#;
2236
2237        let result = parse_execute(code).await.unwrap();
2238        let sketch_blocks = result
2239            .exec_state
2240            .global
2241            .artifacts
2242            .graph
2243            .values()
2244            .filter_map(|artifact| match artifact {
2245                Artifact::SketchBlock(block) => Some(block),
2246                _ => None,
2247            })
2248            .collect::<Vec<_>>();
2249
2250        assert_eq!(sketch_blocks.len(), 1);
2251        assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
2252    }
2253
2254    #[tokio::test(flavor = "multi_thread")]
2255    async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2256        let code = r#"
2257sketch001 = sketch(on = YZ) {
2258  line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2259  line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2260  line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2261  line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2262  coincident([line1.end, line2.start])
2263  coincident([line2.end, line3.start])
2264  coincident([line3.end, line4.start])
2265  coincident([line4.end, line1.start])
2266}
2267
2268sketch002 = sketch(on = -XZ) {
2269  line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2270  line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2271  line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2272  line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2273  coincident([line5.end, line6.start])
2274  coincident([line6.end, line7.start])
2275  coincident([line7.end, line8.start])
2276  coincident([line8.end, line5.start])
2277}
2278
2279region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2280extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2281region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2282extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2283
2284myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2285"#;
2286
2287        let result = parse_execute(code).await.unwrap();
2288        assert!(result.exec_state.issues().is_empty());
2289    }
2290
2291    #[tokio::test(flavor = "multi_thread")]
2292    async fn issue_10741_point_circle_coincident_executes() {
2293        let code = r#"
2294sketch001 = sketch(on = YZ) {
2295  circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
2296  line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
2297  coincident([line1.start, circle1])
2298}
2299"#;
2300
2301        let result = parse_execute(code).await.unwrap();
2302        assert!(
2303            result
2304                .exec_state
2305                .issues()
2306                .iter()
2307                .all(|issue| issue.severity != Severity::Error),
2308            "unexpected execution issues: {:#?}",
2309            result.exec_state.issues()
2310        );
2311    }
2312
2313    #[tokio::test(flavor = "multi_thread")]
2314    async fn test_execute_with_pipe_substitutions_unary() {
2315        let ast = r#"myVar = 3
2316part001 = startSketchOn(XY)
2317  |> startProfile(at = [0, 0])
2318  |> line(end = [3, 4], tag = $seg01)
2319  |> line(end = [
2320  min([segLen(seg01), myVar]),
2321  -legLen(hypotenuse = segLen(seg01), leg = myVar)
2322])
2323"#;
2324
2325        parse_execute(ast).await.unwrap();
2326    }
2327
2328    #[tokio::test(flavor = "multi_thread")]
2329    async fn test_execute_with_pipe_substitutions() {
2330        let ast = r#"myVar = 3
2331part001 = startSketchOn(XY)
2332  |> startProfile(at = [0, 0])
2333  |> line(end = [3, 4], tag = $seg01)
2334  |> line(end = [
2335  min([segLen(seg01), myVar]),
2336  legLen(hypotenuse = segLen(seg01), leg = myVar)
2337])
2338"#;
2339
2340        parse_execute(ast).await.unwrap();
2341    }
2342
2343    #[tokio::test(flavor = "multi_thread")]
2344    async fn test_execute_with_inline_comment() {
2345        let ast = r#"baseThick = 1
2346armAngle = 60
2347
2348baseThickHalf = baseThick / 2
2349halfArmAngle = armAngle / 2
2350
2351arrExpShouldNotBeIncluded = [1, 2, 3]
2352objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2353
2354part001 = startSketchOn(XY)
2355  |> startProfile(at = [0, 0])
2356  |> yLine(endAbsolute = 1)
2357  |> xLine(length = 3.84) // selection-range-7ish-before-this
2358
2359variableBelowShouldNotBeIncluded = 3
2360"#;
2361
2362        parse_execute(ast).await.unwrap();
2363    }
2364
2365    #[tokio::test(flavor = "multi_thread")]
2366    async fn test_execute_with_function_literal_in_pipe() {
2367        let ast = r#"w = 20
2368l = 8
2369h = 10
2370
2371fn thing() {
2372  return -8
2373}
2374
2375firstExtrude = startSketchOn(XY)
2376  |> startProfile(at = [0,0])
2377  |> line(end = [0, l])
2378  |> line(end = [w, 0])
2379  |> line(end = [0, thing()])
2380  |> close()
2381  |> extrude(length = h)"#;
2382
2383        parse_execute(ast).await.unwrap();
2384    }
2385
2386    #[tokio::test(flavor = "multi_thread")]
2387    async fn test_execute_with_function_unary_in_pipe() {
2388        let ast = r#"w = 20
2389l = 8
2390h = 10
2391
2392fn thing(@x) {
2393  return -x
2394}
2395
2396firstExtrude = startSketchOn(XY)
2397  |> startProfile(at = [0,0])
2398  |> line(end = [0, l])
2399  |> line(end = [w, 0])
2400  |> line(end = [0, thing(8)])
2401  |> close()
2402  |> extrude(length = h)"#;
2403
2404        parse_execute(ast).await.unwrap();
2405    }
2406
2407    #[tokio::test(flavor = "multi_thread")]
2408    async fn test_execute_with_function_array_in_pipe() {
2409        let ast = r#"w = 20
2410l = 8
2411h = 10
2412
2413fn thing(@x) {
2414  return [0, -x]
2415}
2416
2417firstExtrude = startSketchOn(XY)
2418  |> startProfile(at = [0,0])
2419  |> line(end = [0, l])
2420  |> line(end = [w, 0])
2421  |> line(end = thing(8))
2422  |> close()
2423  |> extrude(length = h)"#;
2424
2425        parse_execute(ast).await.unwrap();
2426    }
2427
2428    #[tokio::test(flavor = "multi_thread")]
2429    async fn test_execute_with_function_call_in_pipe() {
2430        let ast = r#"w = 20
2431l = 8
2432h = 10
2433
2434fn other_thing(@y) {
2435  return -y
2436}
2437
2438fn thing(@x) {
2439  return other_thing(x)
2440}
2441
2442firstExtrude = startSketchOn(XY)
2443  |> startProfile(at = [0,0])
2444  |> line(end = [0, l])
2445  |> line(end = [w, 0])
2446  |> line(end = [0, thing(8)])
2447  |> close()
2448  |> extrude(length = h)"#;
2449
2450        parse_execute(ast).await.unwrap();
2451    }
2452
2453    #[tokio::test(flavor = "multi_thread")]
2454    async fn test_execute_with_function_sketch() {
2455        let ast = r#"fn box(h, l, w) {
2456 myBox = startSketchOn(XY)
2457    |> startProfile(at = [0,0])
2458    |> line(end = [0, l])
2459    |> line(end = [w, 0])
2460    |> line(end = [0, -l])
2461    |> close()
2462    |> extrude(length = h)
2463
2464  return myBox
2465}
2466
2467fnBox = box(h = 3, l = 6, w = 10)"#;
2468
2469        parse_execute(ast).await.unwrap();
2470    }
2471
2472    #[tokio::test(flavor = "multi_thread")]
2473    async fn test_get_member_of_object_with_function_period() {
2474        let ast = r#"fn box(@obj) {
2475 myBox = startSketchOn(XY)
2476    |> startProfile(at = obj.start)
2477    |> line(end = [0, obj.l])
2478    |> line(end = [obj.w, 0])
2479    |> line(end = [0, -obj.l])
2480    |> close()
2481    |> extrude(length = obj.h)
2482
2483  return myBox
2484}
2485
2486thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2487"#;
2488        parse_execute(ast).await.unwrap();
2489    }
2490
2491    #[tokio::test(flavor = "multi_thread")]
2492    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
2493    async fn test_object_member_starting_pipeline() {
2494        let ast = r#"
2495fn test2() {
2496  return {
2497    thing: startSketchOn(XY)
2498      |> startProfile(at = [0, 0])
2499      |> line(end = [0, 1])
2500      |> line(end = [1, 0])
2501      |> line(end = [0, -1])
2502      |> close()
2503  }
2504}
2505
2506x2 = test2()
2507
2508x2.thing
2509  |> extrude(length = 10)
2510"#;
2511        parse_execute(ast).await.unwrap();
2512    }
2513
2514    #[tokio::test(flavor = "multi_thread")]
2515    #[ignore] // ignore til we get loops
2516    async fn test_execute_with_function_sketch_loop_objects() {
2517        let ast = r#"fn box(obj) {
2518let myBox = startSketchOn(XY)
2519    |> startProfile(at = obj.start)
2520    |> line(end = [0, obj.l])
2521    |> line(end = [obj.w, 0])
2522    |> line(end = [0, -obj.l])
2523    |> close()
2524    |> extrude(length = obj.h)
2525
2526  return myBox
2527}
2528
2529for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2530  thisBox = box(var)
2531}"#;
2532
2533        parse_execute(ast).await.unwrap();
2534    }
2535
2536    #[tokio::test(flavor = "multi_thread")]
2537    #[ignore] // ignore til we get loops
2538    async fn test_execute_with_function_sketch_loop_array() {
2539        let ast = r#"fn box(h, l, w, start) {
2540 myBox = startSketchOn(XY)
2541    |> startProfile(at = [0,0])
2542    |> line(end = [0, l])
2543    |> line(end = [w, 0])
2544    |> line(end = [0, -l])
2545    |> close()
2546    |> extrude(length = h)
2547
2548  return myBox
2549}
2550
2551
2552for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2553  const thisBox = box(var[0], var[1], var[2], var[3])
2554}"#;
2555
2556        parse_execute(ast).await.unwrap();
2557    }
2558
2559    #[tokio::test(flavor = "multi_thread")]
2560    async fn test_get_member_of_array_with_function() {
2561        let ast = r#"fn box(@arr) {
2562 myBox =startSketchOn(XY)
2563    |> startProfile(at = arr[0])
2564    |> line(end = [0, arr[1]])
2565    |> line(end = [arr[2], 0])
2566    |> line(end = [0, -arr[1]])
2567    |> close()
2568    |> extrude(length = arr[3])
2569
2570  return myBox
2571}
2572
2573thisBox = box([[0,0], 6, 10, 3])
2574
2575"#;
2576        parse_execute(ast).await.unwrap();
2577    }
2578
2579    #[tokio::test(flavor = "multi_thread")]
2580    async fn test_function_cannot_access_future_definitions() {
2581        let ast = r#"
2582fn returnX() {
2583  // x shouldn't be defined yet.
2584  return x
2585}
2586
2587x = 5
2588
2589answer = returnX()"#;
2590
2591        let result = parse_execute(ast).await;
2592        let err = result.unwrap_err();
2593        assert_eq!(err.message(), "`x` is not defined");
2594    }
2595
2596    #[tokio::test(flavor = "multi_thread")]
2597    async fn test_override_prelude() {
2598        let text = "PI = 3.0";
2599        let result = parse_execute(text).await.unwrap();
2600        let issues = result.exec_state.issues();
2601        assert!(issues.is_empty(), "issues={issues:#?}");
2602    }
2603
2604    #[tokio::test(flavor = "multi_thread")]
2605    async fn type_aliases() {
2606        let text = r#"@settings(experimentalFeatures = allow)
2607type MyTy = [number; 2]
2608fn foo(@x: MyTy) {
2609    return x[0]
2610}
2611
2612foo([0, 1])
2613
2614type Other = MyTy | Helix
2615"#;
2616        let result = parse_execute(text).await.unwrap();
2617        let issues = result.exec_state.issues();
2618        assert!(issues.is_empty(), "issues={issues:#?}");
2619    }
2620
2621    #[tokio::test(flavor = "multi_thread")]
2622    async fn test_cannot_shebang_in_fn() {
2623        let ast = r#"
2624fn foo() {
2625  #!hello
2626  return true
2627}
2628
2629foo
2630"#;
2631
2632        let result = parse_execute(ast).await;
2633        let err = result.unwrap_err();
2634        assert_eq!(
2635            err,
2636            KclError::new_syntax(KclErrorDetails::new(
2637                "Unexpected token: #".to_owned(),
2638                vec![SourceRange::new(14, 15, ModuleId::default())],
2639            )),
2640        );
2641    }
2642
2643    #[tokio::test(flavor = "multi_thread")]
2644    async fn test_pattern_transform_function_cannot_access_future_definitions() {
2645        let ast = r#"
2646fn transform(@replicaId) {
2647  // x shouldn't be defined yet.
2648  scale = x
2649  return {
2650    translate = [0, 0, replicaId * 10],
2651    scale = [scale, 1, 0],
2652  }
2653}
2654
2655fn layer() {
2656  return startSketchOn(XY)
2657    |> circle( center= [0, 0], radius= 1, tag = $tag1)
2658    |> extrude(length = 10)
2659}
2660
2661x = 5
2662
2663// The 10 layers are replicas of each other, with a transform applied to each.
2664shape = layer() |> patternTransform(instances = 10, transform = transform)
2665"#;
2666
2667        let result = parse_execute(ast).await;
2668        let err = result.unwrap_err();
2669        assert_eq!(err.message(), "`x` is not defined",);
2670    }
2671
2672    // ADAM: Move some of these into simulation tests.
2673
2674    #[tokio::test(flavor = "multi_thread")]
2675    async fn test_math_execute_with_functions() {
2676        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
2677        let result = parse_execute(ast).await.unwrap();
2678        assert_eq!(
2679            5.0,
2680            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2681                .as_f64()
2682                .unwrap()
2683        );
2684    }
2685
2686    #[tokio::test(flavor = "multi_thread")]
2687    async fn test_math_execute() {
2688        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
2689        let result = parse_execute(ast).await.unwrap();
2690        assert_eq!(
2691            7.4,
2692            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2693                .as_f64()
2694                .unwrap()
2695        );
2696    }
2697
2698    #[tokio::test(flavor = "multi_thread")]
2699    async fn test_math_execute_start_negative() {
2700        let ast = r#"myVar = -5 + 6"#;
2701        let result = parse_execute(ast).await.unwrap();
2702        assert_eq!(
2703            1.0,
2704            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2705                .as_f64()
2706                .unwrap()
2707        );
2708    }
2709
2710    #[tokio::test(flavor = "multi_thread")]
2711    async fn test_math_execute_with_pi() {
2712        let ast = r#"myVar = PI * 2"#;
2713        let result = parse_execute(ast).await.unwrap();
2714        assert_eq!(
2715            std::f64::consts::TAU,
2716            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2717                .as_f64()
2718                .unwrap()
2719        );
2720    }
2721
2722    #[tokio::test(flavor = "multi_thread")]
2723    async fn test_math_define_decimal_without_leading_zero() {
2724        let ast = r#"thing = .4 + 7"#;
2725        let result = parse_execute(ast).await.unwrap();
2726        assert_eq!(
2727            7.4,
2728            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
2729                .as_f64()
2730                .unwrap()
2731        );
2732    }
2733
2734    #[tokio::test(flavor = "multi_thread")]
2735    async fn pass_std_to_std() {
2736        let ast = r#"sketch001 = startSketchOn(XY)
2737profile001 = circle(sketch001, center = [0, 0], radius = 2)
2738extrude001 = extrude(profile001, length = 5)
2739extrudes = patternLinear3d(
2740  extrude001,
2741  instances = 3,
2742  distance = 5,
2743  axis = [1, 1, 0],
2744)
2745clone001 = map(extrudes, f = clone)
2746"#;
2747        parse_execute(ast).await.unwrap();
2748    }
2749
2750    #[tokio::test(flavor = "multi_thread")]
2751    async fn test_array_reduce_nested_array() {
2752        let code = r#"
2753fn id(@el, accum)  { return accum }
2754
2755answer = reduce([], initial=[[[0,0]]], f=id)
2756"#;
2757        let result = parse_execute(code).await.unwrap();
2758        assert_eq!(
2759            mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2760            KclValue::HomArray {
2761                value: vec![KclValue::HomArray {
2762                    value: vec![KclValue::HomArray {
2763                        value: vec![
2764                            KclValue::Number {
2765                                value: 0.0,
2766                                ty: NumericType::default(),
2767                                meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2768                            },
2769                            KclValue::Number {
2770                                value: 0.0,
2771                                ty: NumericType::default(),
2772                                meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2773                            }
2774                        ],
2775                        ty: RuntimeType::any(),
2776                    }],
2777                    ty: RuntimeType::any(),
2778                }],
2779                ty: RuntimeType::any(),
2780            }
2781        );
2782    }
2783
2784    #[tokio::test(flavor = "multi_thread")]
2785    async fn test_zero_param_fn() {
2786        let ast = r#"sigmaAllow = 35000 // psi
2787leg1 = 5 // inches
2788leg2 = 8 // inches
2789fn thickness() { return 0.56 }
2790
2791bracket = startSketchOn(XY)
2792  |> startProfile(at = [0,0])
2793  |> line(end = [0, leg1])
2794  |> line(end = [leg2, 0])
2795  |> line(end = [0, -thickness()])
2796  |> line(end = [-leg2 + thickness(), 0])
2797"#;
2798        parse_execute(ast).await.unwrap();
2799    }
2800
2801    #[tokio::test(flavor = "multi_thread")]
2802    async fn test_unary_operator_not_succeeds() {
2803        let ast = r#"
2804fn returnTrue() { return !false }
2805t = true
2806f = false
2807notTrue = !t
2808notFalse = !f
2809c = !!true
2810d = !returnTrue()
2811
2812assertIs(!false, error = "expected to pass")
2813
2814fn check(x) {
2815  assertIs(!x, error = "expected argument to be false")
2816  return true
2817}
2818check(x = false)
2819"#;
2820        let result = parse_execute(ast).await.unwrap();
2821        assert_eq!(
2822            false,
2823            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2824                .as_bool()
2825                .unwrap()
2826        );
2827        assert_eq!(
2828            true,
2829            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2830                .as_bool()
2831                .unwrap()
2832        );
2833        assert_eq!(
2834            true,
2835            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2836                .as_bool()
2837                .unwrap()
2838        );
2839        assert_eq!(
2840            false,
2841            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2842                .as_bool()
2843                .unwrap()
2844        );
2845    }
2846
2847    #[tokio::test(flavor = "multi_thread")]
2848    async fn test_unary_operator_not_on_non_bool_fails() {
2849        let code1 = r#"
2850// Yup, this is null.
2851myNull = 0 / 0
2852notNull = !myNull
2853"#;
2854        assert_eq!(
2855            parse_execute(code1).await.unwrap_err().message(),
2856            "Cannot apply unary operator ! to non-boolean value: a number",
2857        );
2858
2859        let code2 = "notZero = !0";
2860        assert_eq!(
2861            parse_execute(code2).await.unwrap_err().message(),
2862            "Cannot apply unary operator ! to non-boolean value: a number",
2863        );
2864
2865        let code3 = r#"
2866notEmptyString = !""
2867"#;
2868        assert_eq!(
2869            parse_execute(code3).await.unwrap_err().message(),
2870            "Cannot apply unary operator ! to non-boolean value: a string",
2871        );
2872
2873        let code4 = r#"
2874obj = { a = 1 }
2875notMember = !obj.a
2876"#;
2877        assert_eq!(
2878            parse_execute(code4).await.unwrap_err().message(),
2879            "Cannot apply unary operator ! to non-boolean value: a number",
2880        );
2881
2882        let code5 = "
2883a = []
2884notArray = !a";
2885        assert_eq!(
2886            parse_execute(code5).await.unwrap_err().message(),
2887            "Cannot apply unary operator ! to non-boolean value: an empty array",
2888        );
2889
2890        let code6 = "
2891x = {}
2892notObject = !x";
2893        assert_eq!(
2894            parse_execute(code6).await.unwrap_err().message(),
2895            "Cannot apply unary operator ! to non-boolean value: an object",
2896        );
2897
2898        let code7 = "
2899fn x() { return 1 }
2900notFunction = !x";
2901        let fn_err = parse_execute(code7).await.unwrap_err();
2902        // These are currently printed out as JSON objects, so we don't want to
2903        // check the full error.
2904        assert!(
2905            fn_err
2906                .message()
2907                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2908            "Actual error: {fn_err:?}"
2909        );
2910
2911        let code8 = "
2912myTagDeclarator = $myTag
2913notTagDeclarator = !myTagDeclarator";
2914        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2915        // These are currently printed out as JSON objects, so we don't want to
2916        // check the full error.
2917        assert!(
2918            tag_declarator_err
2919                .message()
2920                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2921            "Actual error: {tag_declarator_err:?}"
2922        );
2923
2924        let code9 = "
2925myTagDeclarator = $myTag
2926notTagIdentifier = !myTag";
2927        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2928        // These are currently printed out as JSON objects, so we don't want to
2929        // check the full error.
2930        assert!(
2931            tag_identifier_err
2932                .message()
2933                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2934            "Actual error: {tag_identifier_err:?}"
2935        );
2936
2937        let code10 = "notPipe = !(1 |> 2)";
2938        assert_eq!(
2939            // TODO: We don't currently parse this, but we should.  It should be
2940            // a runtime error instead.
2941            parse_execute(code10).await.unwrap_err(),
2942            KclError::new_syntax(KclErrorDetails::new(
2943                "Unexpected token: !".to_owned(),
2944                vec![SourceRange::new(10, 11, ModuleId::default())],
2945            ))
2946        );
2947
2948        let code11 = "
2949fn identity(x) { return x }
2950notPipeSub = 1 |> identity(!%))";
2951        assert_eq!(
2952            // TODO: We don't currently parse this, but we should.  It should be
2953            // a runtime error instead.
2954            parse_execute(code11).await.unwrap_err(),
2955            KclError::new_syntax(KclErrorDetails::new(
2956                "There was an unexpected `!`. Try removing it.".to_owned(),
2957                vec![SourceRange::new(56, 57, ModuleId::default())],
2958            ))
2959        );
2960
2961        // TODO: Add these tests when we support these types.
2962        // let notNan = !NaN
2963        // let notInfinity = !Infinity
2964    }
2965
2966    #[tokio::test(flavor = "multi_thread")]
2967    async fn test_start_sketch_on_invalid_kwargs() {
2968        let current_dir = std::env::current_dir().unwrap();
2969        let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2970        let mut code = std::fs::read_to_string(&path).unwrap();
2971        assert_eq!(
2972            parse_execute(&code).await.unwrap_err().message(),
2973            "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2974        );
2975
2976        path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2977        code = std::fs::read_to_string(&path).unwrap();
2978
2979        assert_eq!(
2980            parse_execute(&code).await.unwrap_err().message(),
2981            "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2982        );
2983
2984        path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2985        code = std::fs::read_to_string(&path).unwrap();
2986
2987        assert_eq!(
2988            parse_execute(&code).await.unwrap_err().message(),
2989            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2990        );
2991
2992        path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2993        code = std::fs::read_to_string(&path).unwrap();
2994
2995        assert_eq!(
2996            parse_execute(&code).await.unwrap_err().message(),
2997            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2998        );
2999
3000        path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
3001        code = std::fs::read_to_string(&path).unwrap();
3002
3003        assert_eq!(
3004            parse_execute(&code).await.unwrap_err().message(),
3005            "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
3006        );
3007    }
3008
3009    #[tokio::test(flavor = "multi_thread")]
3010    async fn test_math_negative_variable_in_binary_expression() {
3011        let ast = r#"sigmaAllow = 35000 // psi
3012width = 1 // inch
3013
3014p = 150 // lbs
3015distance = 6 // inches
3016FOS = 2
3017
3018leg1 = 5 // inches
3019leg2 = 8 // inches
3020
3021thickness_squared = distance * p * FOS * 6 / sigmaAllow
3022thickness = 0.56 // inches. App does not support square root function yet
3023
3024bracket = startSketchOn(XY)
3025  |> startProfile(at = [0,0])
3026  |> line(end = [0, leg1])
3027  |> line(end = [leg2, 0])
3028  |> line(end = [0, -thickness])
3029  |> line(end = [-leg2 + thickness, 0])
3030"#;
3031        parse_execute(ast).await.unwrap();
3032    }
3033
3034    #[tokio::test(flavor = "multi_thread")]
3035    async fn test_execute_function_no_return() {
3036        let ast = r#"fn test(@origin) {
3037  origin
3038}
3039
3040test([0, 0])
3041"#;
3042        let result = parse_execute(ast).await;
3043        assert!(result.is_err());
3044        assert!(result.unwrap_err().to_string().contains("undefined"));
3045    }
3046
3047    #[tokio::test(flavor = "multi_thread")]
3048    async fn test_max_stack_size_exceeded_error() {
3049        let ast = r#"
3050fn forever(@n) {
3051  return 1 + forever(n)
3052}
3053
3054forever(1)
3055"#;
3056        let result = parse_execute(ast).await;
3057        let err = result.unwrap_err();
3058        assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
3059    }
3060
3061    #[tokio::test(flavor = "multi_thread")]
3062    async fn test_math_doubly_nested_parens() {
3063        let ast = r#"sigmaAllow = 35000 // psi
3064width = 4 // inch
3065p = 150 // Force on shelf - lbs
3066distance = 6 // inches
3067FOS = 2
3068leg1 = 5 // inches
3069leg2 = 8 // inches
3070thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
3071thickness = 0.32 // inches. App does not support square root function yet
3072bracket = startSketchOn(XY)
3073  |> startProfile(at = [0,0])
3074    |> line(end = [0, leg1])
3075  |> line(end = [leg2, 0])
3076  |> line(end = [0, -thickness])
3077  |> line(end = [-1 * leg2 + thickness, 0])
3078  |> line(end = [0, -1 * leg1 + thickness])
3079  |> close()
3080  |> extrude(length = width)
3081"#;
3082        parse_execute(ast).await.unwrap();
3083    }
3084
3085    #[tokio::test(flavor = "multi_thread")]
3086    async fn test_math_nested_parens_one_less() {
3087        let ast = r#" sigmaAllow = 35000 // psi
3088width = 4 // inch
3089p = 150 // Force on shelf - lbs
3090distance = 6 // inches
3091FOS = 2
3092leg1 = 5 // inches
3093leg2 = 8 // inches
3094thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
3095thickness = 0.32 // inches. App does not support square root function yet
3096bracket = startSketchOn(XY)
3097  |> startProfile(at = [0,0])
3098    |> line(end = [0, leg1])
3099  |> line(end = [leg2, 0])
3100  |> line(end = [0, -thickness])
3101  |> line(end = [-1 * leg2 + thickness, 0])
3102  |> line(end = [0, -1 * leg1 + thickness])
3103  |> close()
3104  |> extrude(length = width)
3105"#;
3106        parse_execute(ast).await.unwrap();
3107    }
3108
3109    #[tokio::test(flavor = "multi_thread")]
3110    async fn test_fn_as_operand() {
3111        let ast = r#"fn f() { return 1 }
3112x = f()
3113y = x + 1
3114z = f() + 1
3115w = f() + f()
3116"#;
3117        parse_execute(ast).await.unwrap();
3118    }
3119
3120    #[tokio::test(flavor = "multi_thread")]
3121    async fn kcl_test_ids_stable_between_executions() {
3122        let code = r#"sketch001 = startSketchOn(XZ)
3123|> startProfile(at = [61.74, 206.13])
3124|> xLine(length = 305.11, tag = $seg01)
3125|> yLine(length = -291.85)
3126|> xLine(length = -segLen(seg01))
3127|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3128|> close()
3129|> extrude(length = 40.14)
3130|> shell(
3131    thickness = 3.14,
3132    faces = [seg01]
3133)
3134"#;
3135
3136        let ctx = crate::test_server::new_context(true, None).await.unwrap();
3137        let old_program = crate::Program::parse_no_errs(code).unwrap();
3138
3139        // Execute the program.
3140        if let Err(err) = ctx.run_with_caching(old_program).await {
3141            let report = err.into_miette_report_with_outputs(code).unwrap();
3142            let report = miette::Report::new(report);
3143            panic!("Error executing program: {report:?}");
3144        }
3145
3146        // Get the id_generator from the first execution.
3147        let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3148
3149        let code = r#"sketch001 = startSketchOn(XZ)
3150|> startProfile(at = [62.74, 206.13])
3151|> xLine(length = 305.11, tag = $seg01)
3152|> yLine(length = -291.85)
3153|> xLine(length = -segLen(seg01))
3154|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3155|> close()
3156|> extrude(length = 40.14)
3157|> shell(
3158    faces = [seg01],
3159    thickness = 3.14,
3160)
3161"#;
3162
3163        // Execute a slightly different program again.
3164        let program = crate::Program::parse_no_errs(code).unwrap();
3165        // Execute the program.
3166        ctx.run_with_caching(program).await.unwrap();
3167
3168        let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3169
3170        assert_eq!(id_generator, new_id_generator);
3171    }
3172
3173    #[tokio::test(flavor = "multi_thread")]
3174    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
3175        let code = r#"sketch001 = startSketchOn(XZ)
3176|> startProfile(at = [61.74, 206.13])
3177|> xLine(length = 305.11, tag = $seg01)
3178|> yLine(length = -291.85)
3179|> xLine(length = -segLen(seg01))
3180|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3181|> close()
3182|> extrude(length = 40.14)
3183|> shell(
3184    thickness = 3.14,
3185    faces = [seg01]
3186)
3187"#;
3188
3189        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
3190        let old_program = crate::Program::parse_no_errs(code).unwrap();
3191
3192        // Execute the program.
3193        ctx.run_with_caching(old_program.clone()).await.unwrap();
3194
3195        let settings_state = cache::read_old_ast().await.unwrap().settings;
3196
3197        // Ensure the settings are as expected.
3198        assert_eq!(settings_state, ctx.settings);
3199
3200        // Change a setting.
3201        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3202
3203        // Execute the program.
3204        ctx.run_with_caching(old_program.clone()).await.unwrap();
3205
3206        let settings_state = cache::read_old_ast().await.unwrap().settings;
3207
3208        // Ensure the settings are as expected.
3209        assert_eq!(settings_state, ctx.settings);
3210
3211        // Change a setting.
3212        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3213
3214        // Execute the program.
3215        ctx.run_with_caching(old_program).await.unwrap();
3216
3217        let settings_state = cache::read_old_ast().await.unwrap().settings;
3218
3219        // Ensure the settings are as expected.
3220        assert_eq!(settings_state, ctx.settings);
3221
3222        ctx.close().await;
3223    }
3224
3225    #[tokio::test(flavor = "multi_thread")]
3226    async fn mock_after_not_mock() {
3227        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3228        let program = crate::Program::parse_no_errs("x = 2").unwrap();
3229        let result = ctx.run_with_caching(program).await.unwrap();
3230        assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
3231
3232        let ctx2 = ExecutorContext::new_mock(None).await;
3233        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
3234        let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
3235        assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
3236
3237        ctx.close().await;
3238        ctx2.close().await;
3239    }
3240
3241    #[tokio::test(flavor = "multi_thread")]
3242    async fn mock_then_add_extrude_then_mock_again() {
3243        let code = "s = sketch(on = XY) {
3244    line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
3245    line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
3246    coincident([line1.end, line2.start])
3247    line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
3248    coincident([line2.end, line3.start])
3249    coincident([line1.start, line3.end])
3250}
3251    ";
3252        let ctx = ExecutorContext::new_mock(None).await;
3253        let program = crate::Program::parse_no_errs(code).unwrap();
3254        let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3255        assert!(result.variables.contains_key("s"), "actual: {:?}", &result.variables);
3256
3257        let code2 = code.to_owned()
3258            + "
3259region001 = region(point = [1mm, 1mm], sketch = s)
3260extrude001 = extrude(region001, length = 1)
3261    ";
3262        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3263        let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
3264        assert!(
3265            result.variables.contains_key("region001"),
3266            "actual: {:?}",
3267            &result.variables
3268        );
3269
3270        ctx.close().await;
3271    }
3272
3273    #[tokio::test(flavor = "multi_thread")]
3274    async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
3275        let code = format!(
3276            r#"{}
3277
3278face7 = faceOf(solid6, face = r6.tags.line1)
3279r7 = squareRegion(onSurface = face7)
3280solid7 = extrude(r7, length = width)
3281"#,
3282            include_str!("../../tests/endless_impeller/input.kcl")
3283        );
3284
3285        let result = parse_execute(&code).await.unwrap();
3286        let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
3287        assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
3288
3289        let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
3290            KclValue::Face { value } => value,
3291            value => panic!("expected face7 to be a Face, got {value:?}"),
3292        };
3293        assert!(face7.parent_solid.creator_sketch_id.is_some());
3294    }
3295
3296    #[tokio::test(flavor = "multi_thread")]
3297    async fn mock_has_stable_ids() {
3298        let ctx = ExecutorContext::new_mock(None).await;
3299        let mock_config = MockConfig {
3300            use_prev_memory: false,
3301            ..Default::default()
3302        };
3303        let code = "sk = startSketchOn(XY)
3304        |> startProfile(at = [0, 0])";
3305        let program = crate::Program::parse_no_errs(code).unwrap();
3306        let result = ctx.run_mock(&program, &mock_config).await.unwrap();
3307        let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3308        assert!(!ids.is_empty(), "IDs should not be empty");
3309
3310        let ctx2 = ExecutorContext::new_mock(None).await;
3311        let program2 = crate::Program::parse_no_errs(code).unwrap();
3312        let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
3313        let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3314
3315        assert_eq!(ids, ids2, "Generated IDs should match");
3316        ctx.close().await;
3317        ctx2.close().await;
3318    }
3319
3320    #[tokio::test(flavor = "multi_thread")]
3321    async fn mock_memory_restore_preserves_module_maps() {
3322        clear_mem_cache().await;
3323
3324        let ctx = ExecutorContext::new_mock(None).await;
3325        let cold_start = MockConfig {
3326            use_prev_memory: false,
3327            ..Default::default()
3328        };
3329        ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
3330
3331        let mut mem = cache::read_old_memory().await.unwrap();
3332        assert!(
3333            mem.path_to_source_id.len() > 3,
3334            "expected prelude imports to populate multiple modules, got {:?}",
3335            mem.path_to_source_id
3336        );
3337        mem.constraint_state.insert(
3338            crate::front::ObjectId(1),
3339            indexmap::indexmap! {
3340                crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
3341                    crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
3342            },
3343        );
3344
3345        let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
3346        ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
3347
3348        assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
3349        assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
3350        assert_eq!(exec_state.global.module_infos, mem.module_infos);
3351        assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
3352
3353        clear_mem_cache().await;
3354        ctx.close().await;
3355    }
3356
3357    #[tokio::test(flavor = "multi_thread")]
3358    async fn run_with_caching_no_action_refreshes_mock_memory() {
3359        cache::bust_cache().await;
3360        clear_mem_cache().await;
3361
3362        let ctx = ExecutorContext::new_with_engine(
3363            std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
3364            Default::default(),
3365        );
3366        let program = crate::Program::parse_no_errs(
3367            r#"sketch001 = sketch(on = XY) {
3368  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
3369}
3370"#,
3371        )
3372        .unwrap();
3373
3374        ctx.run_with_caching(program.clone()).await.unwrap();
3375        let baseline_memory = cache::read_old_memory().await.unwrap();
3376        assert!(
3377            !baseline_memory.scene_objects.is_empty(),
3378            "expected engine execution to persist full-scene mock memory"
3379        );
3380
3381        cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
3382        assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
3383
3384        ctx.run_with_caching(program).await.unwrap();
3385        let refreshed_memory = cache::read_old_memory().await.unwrap();
3386        assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
3387        assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
3388        assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
3389
3390        cache::bust_cache().await;
3391        clear_mem_cache().await;
3392        ctx.close().await;
3393    }
3394
3395    #[tokio::test(flavor = "multi_thread")]
3396    async fn sim_sketch_mode_real_mock_real() {
3397        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3398        let code = r#"sketch001 = startSketchOn(XY)
3399profile001 = startProfile(sketch001, at = [0, 0])
3400  |> line(end = [10, 0])
3401  |> line(end = [0, 10])
3402  |> line(end = [-10, 0])
3403  |> line(end = [0, -10])
3404  |> close()
3405"#;
3406        let program = crate::Program::parse_no_errs(code).unwrap();
3407        let result = ctx.run_with_caching(program).await.unwrap();
3408        assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
3409
3410        let mock_ctx = ExecutorContext::new_mock(None).await;
3411        let mock_program = crate::Program::parse_no_errs(code).unwrap();
3412        let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
3413        assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
3414
3415        let code2 = code.to_owned()
3416            + r#"
3417extrude001 = extrude(profile001, length = 10)
3418"#;
3419        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3420        let result = ctx.run_with_caching(program2).await.unwrap();
3421        assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
3422
3423        ctx.close().await;
3424        mock_ctx.close().await;
3425    }
3426
3427    #[tokio::test(flavor = "multi_thread")]
3428    async fn read_tag_version() {
3429        let ast = r#"fn bar(@t) {
3430  return startSketchOn(XY)
3431    |> startProfile(at = [0,0])
3432    |> angledLine(
3433        angle = -60,
3434        length = segLen(t),
3435    )
3436    |> line(end = [0, 0])
3437    |> close()
3438}
3439
3440sketch = startSketchOn(XY)
3441  |> startProfile(at = [0,0])
3442  |> line(end = [0, 10])
3443  |> line(end = [10, 0], tag = $tag0)
3444  |> line(endAbsolute = [0, 0])
3445
3446fn foo() {
3447  // tag0 tags an edge
3448  return bar(tag0)
3449}
3450
3451solid = sketch |> extrude(length = 10)
3452// tag0 tags a face
3453sketch2 = startSketchOn(solid, face = tag0)
3454  |> startProfile(at = [0,0])
3455  |> line(end = [0, 1])
3456  |> line(end = [1, 0])
3457  |> line(end = [0, 0])
3458
3459foo() |> extrude(length = 1)
3460"#;
3461        parse_execute(ast).await.unwrap();
3462    }
3463
3464    #[tokio::test(flavor = "multi_thread")]
3465    async fn experimental() {
3466        let code = r#"
3467startSketchOn(XY)
3468  |> startProfile(at = [0, 0], tag = $start)
3469  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3470"#;
3471        let result = parse_execute(code).await.unwrap();
3472        let issues = result.exec_state.issues();
3473        assert_eq!(issues.len(), 1);
3474        assert_eq!(issues[0].severity, Severity::Error);
3475        let msg = &issues[0].message;
3476        assert!(msg.contains("experimental"), "found {msg}");
3477
3478        let code = r#"@settings(experimentalFeatures = allow)
3479startSketchOn(XY)
3480  |> startProfile(at = [0, 0], tag = $start)
3481  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3482"#;
3483        let result = parse_execute(code).await.unwrap();
3484        let issues = result.exec_state.issues();
3485        assert!(issues.is_empty(), "issues={issues:#?}");
3486
3487        let code = r#"@settings(experimentalFeatures = warn)
3488startSketchOn(XY)
3489  |> startProfile(at = [0, 0], tag = $start)
3490  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3491"#;
3492        let result = parse_execute(code).await.unwrap();
3493        let issues = result.exec_state.issues();
3494        assert_eq!(issues.len(), 1);
3495        assert_eq!(issues[0].severity, Severity::Warning);
3496        let msg = &issues[0].message;
3497        assert!(msg.contains("experimental"), "found {msg}");
3498
3499        let code = r#"@settings(experimentalFeatures = deny)
3500startSketchOn(XY)
3501  |> startProfile(at = [0, 0], tag = $start)
3502  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3503"#;
3504        let result = parse_execute(code).await.unwrap();
3505        let issues = result.exec_state.issues();
3506        assert_eq!(issues.len(), 1);
3507        assert_eq!(issues[0].severity, Severity::Error);
3508        let msg = &issues[0].message;
3509        assert!(msg.contains("experimental"), "found {msg}");
3510
3511        let code = r#"@settings(experimentalFeatures = foo)
3512startSketchOn(XY)
3513  |> startProfile(at = [0, 0], tag = $start)
3514  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3515"#;
3516        parse_execute(code).await.unwrap_err();
3517    }
3518
3519    #[tokio::test(flavor = "multi_thread")]
3520    async fn experimental_parameter() {
3521        let code = r#"
3522fn inc(@x, @(experimental = true) amount? = 1) {
3523  return x + amount
3524}
3525
3526answer = inc(5, amount = 2)
3527"#;
3528        let result = parse_execute(code).await.unwrap();
3529        let issues = result.exec_state.issues();
3530        assert_eq!(issues.len(), 1);
3531        assert_eq!(issues[0].severity, Severity::Error);
3532        let msg = &issues[0].message;
3533        assert!(msg.contains("experimental"), "found {msg}");
3534
3535        // If the parameter isn't used, there's no warning.
3536        let code = r#"
3537fn inc(@x, @(experimental = true) amount? = 1) {
3538  return x + amount
3539}
3540
3541answer = inc(5)
3542"#;
3543        let result = parse_execute(code).await.unwrap();
3544        let issues = result.exec_state.issues();
3545        assert!(issues.is_empty(), "issues={issues:#?}");
3546    }
3547
3548    #[tokio::test(flavor = "multi_thread")]
3549    async fn experimental_scalar_fixed_constraint() {
3550        let code_left = r#"@settings(experimentalFeatures = warn)
3551sketch(on = XY) {
3552  point1 = point(at = [var 0mm, var 0mm])
3553  point1.at[0] == 1mm
3554}
3555"#;
3556        // It's symmetric. Flipping the binary operator has the same behavior.
3557        let code_right = r#"@settings(experimentalFeatures = warn)
3558sketch(on = XY) {
3559  point1 = point(at = [var 0mm, var 0mm])
3560  1mm == point1.at[0]
3561}
3562"#;
3563
3564        for code in [code_left, code_right] {
3565            let result = parse_execute(code).await.unwrap();
3566            let issues = result.exec_state.issues();
3567            let Some(error) = issues
3568                .iter()
3569                .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
3570            else {
3571                panic!("found {issues:#?}");
3572            };
3573            assert_eq!(error.severity, Severity::Warning);
3574        }
3575    }
3576
3577    // START Mock Execution tests
3578    // Ideally, we would do this as part of all sim tests and delete these one-off tests.
3579
3580    #[tokio::test(flavor = "multi_thread")]
3581    async fn test_tangent_line_arc_executes_with_mock_engine() {
3582        let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
3583        parse_execute(&code).await.unwrap();
3584    }
3585
3586    #[tokio::test(flavor = "multi_thread")]
3587    async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
3588        let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
3589        parse_execute(&code).await.unwrap();
3590    }
3591
3592    #[tokio::test(flavor = "multi_thread")]
3593    async fn test_tangent_line_circle_executes_with_mock_engine() {
3594        let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
3595        parse_execute(&code).await.unwrap();
3596    }
3597
3598    #[tokio::test(flavor = "multi_thread")]
3599    async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
3600        let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
3601        parse_execute(&code).await.unwrap();
3602    }
3603
3604    #[tokio::test(flavor = "multi_thread")]
3605    async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
3606        let code = r#"startX = 2
3607
3608baseSketch = sketch(on = XY) {
3609  yoyo = line(start = [startX, 0], end = [7, 6])
3610  line2 = line(start = [7, 6], end = [7, 12])
3611  hi = line(start = [7, 12], end = [startX, 0])
3612}
3613
3614baseRegion = region(point = [5.5, 6], sketch = baseSketch)
3615myExtrude = extrude(
3616  baseRegion,
3617  length = 5,
3618  tagEnd = $endCap,
3619  tagStart = $startCap,
3620)
3621yodawg = getCommonEdge(faces = [
3622  baseRegion.tags.hi,
3623  baseRegion.tags.yoyo
3624])
3625
3626cutSketch = sketch(on = YZ) {
3627  myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
3628  myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
3629  line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
3630}
3631
3632cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
3633extrude001 = extrude(cutRegion, length = 5)
3634solid001 = subtract(myExtrude, tools = extrude001)
3635
3636yoyo = getOppositeEdge(baseRegion.tags.hi)
3637fillet(solid001, radius = 0.1, tags = yoyo)
3638"#;
3639
3640        parse_execute(code).await.unwrap();
3641    }
3642
3643    // END Mock Execution tests
3644
3645    // Sketch constraint report tests
3646
3647    async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
3648        let program = crate::Program::parse_no_errs(kcl).unwrap();
3649        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3650        let mut exec_state = ExecState::new(&ctx);
3651        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3652        let outcome = exec_state.into_exec_outcome(env_ref, &ctx).await;
3653        let report = outcome.sketch_constraint_report();
3654        ctx.close().await;
3655        report
3656    }
3657
3658    #[tokio::test(flavor = "multi_thread")]
3659    async fn warn_when_sketch_is_over_constrained() {
3660        let code = r#"
3661sketch001 = sketch(on = XY) {
3662  line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
3663  fixed([line1.start, ORIGIN])
3664  fixed([line1.start, [20, 20]])
3665}
3666"#;
3667        let result = parse_execute(code).await.unwrap();
3668        let issues = result.exec_state.issues();
3669        let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
3670            panic!("expected over-constrained warning; found {issues:#?}");
3671        };
3672        assert_eq!(warning.severity, Severity::Warning);
3673    }
3674
3675    #[tokio::test(flavor = "multi_thread")]
3676    async fn no_warning_when_sketch_is_not_over_constrained() {
3677        // Under-constrained sketch should not emit the over-constrained warning.
3678        let code = r#"
3679sketch001 = sketch(on = XY) {
3680  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
3681}
3682"#;
3683        let result = parse_execute(code).await.unwrap();
3684        let issues = result.exec_state.issues();
3685        assert!(
3686            !issues.iter().any(|issue| issue.message.contains("over-constrained")),
3687            "did not expect over-constrained warning; found {issues:#?}"
3688        );
3689    }
3690
3691    #[tokio::test(flavor = "multi_thread")]
3692    async fn test_constraint_report_fully_constrained() {
3693        // All points are fully constrained via equality constraints.
3694        let kcl = r#"
3695@settings(experimentalFeatures = allow)
3696
3697sketch(on = YZ) {
3698  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
3699  line1.start.at[0] == 2
3700  line1.start.at[1] == 8
3701  line1.end.at[0] == 5
3702  line1.end.at[1] == 7
3703}
3704"#;
3705        let report = run_constraint_report(kcl).await;
3706        assert_eq!(report.fully_constrained.len(), 1);
3707        assert_eq!(report.under_constrained.len(), 0);
3708        assert_eq!(report.over_constrained.len(), 0);
3709        assert_eq!(report.errors.len(), 0);
3710        assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
3711    }
3712
3713    #[tokio::test(flavor = "multi_thread")]
3714    async fn test_constraint_report_under_constrained() {
3715        // No constraints at all — all points are free.
3716        let kcl = r#"
3717sketch(on = YZ) {
3718  line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
3719}
3720"#;
3721        let report = run_constraint_report(kcl).await;
3722        assert_eq!(report.fully_constrained.len(), 0);
3723        assert_eq!(report.under_constrained.len(), 1);
3724        assert_eq!(report.over_constrained.len(), 0);
3725        assert_eq!(report.errors.len(), 0);
3726        assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
3727        assert!(report.under_constrained[0].free_count > 0);
3728    }
3729
3730    #[tokio::test(flavor = "multi_thread")]
3731    async fn test_constraint_report_over_constrained() {
3732        // Conflicting distance constraints on the same pair of points.
3733        let kcl = r#"
3734@settings(experimentalFeatures = allow)
3735
3736sketch(on = YZ) {
3737  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
3738  line1.start.at[0] == 2
3739  line1.start.at[1] == 8
3740  line1.end.at[0] == 5
3741  line1.end.at[1] == 7
3742  distance([line1.start, line1.end]) == 100mm
3743}
3744"#;
3745        let report = run_constraint_report(kcl).await;
3746        assert_eq!(report.over_constrained.len(), 1);
3747        assert_eq!(report.errors.len(), 0);
3748        assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
3749        assert!(report.over_constrained[0].conflict_count > 0);
3750    }
3751
3752    #[tokio::test(flavor = "multi_thread")]
3753    async fn test_constraint_report_multiple_sketches() {
3754        // Two sketches: one fully constrained, one under-constrained.
3755        let kcl = r#"
3756@settings(experimentalFeatures = allow)
3757
3758s1 = sketch(on = YZ) {
3759  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
3760  line1.start.at[0] == 2
3761  line1.start.at[1] == 8
3762  line1.end.at[0] == 5
3763  line1.end.at[1] == 7
3764}
3765
3766s2 = sketch(on = XZ) {
3767  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
3768}
3769"#;
3770        let report = run_constraint_report(kcl).await;
3771        assert_eq!(
3772            report.fully_constrained.len()
3773                + report.under_constrained.len()
3774                + report.over_constrained.len()
3775                + report.errors.len(),
3776            2,
3777            "Expected 2 sketches total"
3778        );
3779        assert_eq!(report.fully_constrained.len(), 1);
3780        assert_eq!(report.under_constrained.len(), 1);
3781    }
3782}