Skip to main content

kcl_lib/execution/
mod.rs

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