Skip to main content

kcl_lib/execution/
mod.rs

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