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