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