Skip to main content

kcl_lib/execution/
mod.rs

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