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