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