Skip to main content

kcl_lib/execution/
mod.rs

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