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