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