Skip to main content

kcl_lib/execution/
mod.rs

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