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