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 geometry::*;
22pub use id_generator::IdGenerator;
23pub(crate) use import::PreImportedGeometry;
24use indexmap::IndexMap;
25pub use kcl_api::Operation;
26use kcl_api::ast::node_path::NodePath;
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;
63
64use crate::CompilationIssue;
65use crate::ExecError;
66use crate::KclErrorWithOutputs;
67use crate::NodePathExt;
68use crate::SourceRange;
69use crate::collections::AhashIndexSet;
70use crate::engine::EngineBatchContext;
71use crate::engine::GridScaleBehavior;
72use crate::engine::engine_manager::EngineManager;
73use crate::errors::KclError;
74use crate::errors::KclErrorDetails;
75use crate::execution::cache::CacheInformation;
76use crate::execution::cache::CacheResult;
77use crate::execution::cad_op::OperationExt;
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
2110pub use kcl_api::ArtifactId;
2111
2112pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
2113    ArtifactId::new(*id.as_ref())
2114}
2115
2116#[cfg(test)]
2117pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2118    parse_execute_with_project_dir(code, None).await
2119}
2120
2121#[cfg(test)]
2122pub(crate) async fn parse_execute_with_project_dir(
2123    code: &str,
2124    project_directory: Option<TypedPath>,
2125) -> Result<ExecTestResults, KclError> {
2126    let program = crate::Program::parse_no_errs(code)?;
2127
2128    let exec_ctxt = ExecutorContext {
2129        engine: Arc::new(EngineManager::new_mock()),
2130        engine_batch: EngineBatchContext::default(),
2131        fs: Arc::new(crate::fs::FileManager::new()),
2132        settings: ExecutorSettings {
2133            project_directory,
2134            ..Default::default()
2135        },
2136        context_type: ContextType::Mock,
2137        execution_callbacks: Default::default(),
2138    };
2139    let mut exec_state = ExecState::new(&exec_ctxt);
2140    let result = exec_ctxt.run(&program, &mut exec_state).await?;
2141
2142    Ok(ExecTestResults {
2143        program,
2144        mem_env: result.0,
2145        exec_ctxt,
2146        exec_state,
2147    })
2148}
2149
2150#[cfg(test)]
2151#[derive(Debug)]
2152pub(crate) struct ExecTestResults {
2153    program: crate::Program,
2154    mem_env: EnvironmentRef,
2155    exec_ctxt: ExecutorContext,
2156    exec_state: ExecState,
2157}
2158
2159#[cfg(test)]
2160impl ExecTestResults {
2161    pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2162        &self.exec_state.global.root_module_artifacts.commands
2163    }
2164}
2165
2166/// There are several places where we want to traverse a KCL program or find a symbol in it,
2167/// but because KCL modules can import each other, we need to traverse multiple programs.
2168/// This stores multiple programs, keyed by their module ID for quick access.
2169pub struct ProgramLookup {
2170    programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2171}
2172
2173impl ProgramLookup {
2174    // TODO: Could this store a reference to KCL programs instead of owning them?
2175    // i.e. take &state::ModuleInfoMap instead?
2176    pub fn new(
2177        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2178        module_infos: state::ModuleInfoMap,
2179    ) -> Self {
2180        let mut programs = IndexMap::with_capacity(module_infos.len());
2181        for (id, info) in module_infos {
2182            if let ModuleRepr::Kcl(program, _) = info.repr {
2183                programs.insert(id, program);
2184            }
2185        }
2186        programs.insert(ModuleId::default(), current);
2187        Self { programs }
2188    }
2189
2190    pub fn program_for_module(
2191        &self,
2192        module_id: ModuleId,
2193    ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2194        self.programs.get(&module_id)
2195    }
2196}
2197
2198#[cfg(test)]
2199mod tests {
2200    use kcl_api::NumericType;
2201    use pretty_assertions::assert_eq;
2202
2203    use super::*;
2204    use crate::ModuleId;
2205    use crate::errors::KclErrorDetails;
2206    use crate::errors::Severity;
2207    use crate::execution::memory::Stack;
2208    use crate::execution::types::RuntimeType;
2209
2210    /// Convenience function to get a JSON value from memory and unwrap.
2211    #[track_caller]
2212    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2213        memory.memory.get_from_unchecked(name, env).unwrap()
2214    }
2215
2216    async fn execute_variables_with_backend(
2217        code: &str,
2218        backend: memory::MemoryBackendKind,
2219    ) -> IndexMap<String, KclValueView> {
2220        execute_outcome_with_backend(code, backend).await.variables
2221    }
2222
2223    async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
2224        let program = crate::Program::parse_no_errs(code).unwrap();
2225        let ctx = ExecutorContext::new_mock(None).await;
2226        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2227        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2228        let outcome = exec_state
2229            .into_exec_outcome(env_ref, &ctx)
2230            .await
2231            .expect("test execution outcome should collect variables");
2232        ctx.close().await;
2233        outcome
2234    }
2235
2236    async fn execute_error_variables_with_backend(
2237        code: &str,
2238        backend: memory::MemoryBackendKind,
2239    ) -> IndexMap<String, KclValueView> {
2240        let program = crate::Program::parse_no_errs(code).unwrap();
2241        let ctx = ExecutorContext::new_mock(None).await;
2242        let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2243        let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
2244        ctx.close().await;
2245        error.variables
2246    }
2247
2248    async fn execute_project_variables_with_backend(
2249        main_code: &str,
2250        files: &[(&str, &str)],
2251        backend: memory::MemoryBackendKind,
2252    ) -> IndexMap<String, KclValueView> {
2253        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
2254        for (name, contents) in files {
2255            tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
2256        }
2257
2258        let program = crate::Program::parse_no_errs(main_code).unwrap();
2259        let ctx = ExecutorContext {
2260            engine: Arc::new(EngineManager::new_mock()),
2261            engine_batch: EngineBatchContext::default(),
2262            fs: Arc::new(crate::fs::FileManager::new()),
2263            settings: ExecutorSettings {
2264                project_directory: Some(crate::TypedPath(tmpdir.path().into())),
2265                ..Default::default()
2266            },
2267            context_type: ContextType::Mock,
2268            execution_callbacks: Default::default(),
2269        };
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.variables
2278    }
2279
2280    async fn run_with_caching_variables_with_backend(
2281        code: &str,
2282        backend: memory::MemoryBackendKind,
2283    ) -> IndexMap<String, KclValueView> {
2284        let _backend = memory::MemoryBackendKind::override_for_test(backend);
2285        cache::bust_cache().await;
2286        clear_mem_cache().await;
2287
2288        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
2289        let program = crate::Program::parse_no_errs(code).unwrap();
2290        ctx.run_with_caching(program.clone()).await.unwrap();
2291        let cached = ctx.run_with_caching(program).await.unwrap();
2292
2293        cache::bust_cache().await;
2294        clear_mem_cache().await;
2295        ctx.close().await;
2296        cached.variables
2297    }
2298
2299    async fn run_mock_variables_with_backend(
2300        code: &str,
2301        backend: memory::MemoryBackendKind,
2302    ) -> IndexMap<String, KclValueView> {
2303        let _backend = memory::MemoryBackendKind::override_for_test(backend);
2304        clear_mem_cache().await;
2305
2306        let ctx = ExecutorContext::new_mock(None).await;
2307        let first = crate::Program::parse_no_errs("x = 2").unwrap();
2308        ctx.run_mock(
2309            &first,
2310            &MockConfig {
2311                use_prev_memory: false,
2312                ..Default::default()
2313            },
2314        )
2315        .await
2316        .unwrap();
2317
2318        let program = crate::Program::parse_no_errs(code).unwrap();
2319        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
2320
2321        clear_mem_cache().await;
2322        ctx.close().await;
2323        outcome.variables
2324    }
2325
2326    fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
2327        let mut keys = variables.keys().cloned().collect::<Vec<_>>();
2328        keys.sort();
2329        keys
2330    }
2331
2332    fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
2333        let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
2334        let KclValueView::Number { value, .. } = value else {
2335            panic!("expected `{key}` to be a number, got {value:?}");
2336        };
2337        assert_eq!(*value, expected, "{key}: {value:?}");
2338    }
2339
2340    #[tokio::test(flavor = "multi_thread")]
2341    async fn exec_outcome_variables_match_between_memory_backends() {
2342        let code = "x = 2\ny = x + 1\narr = [x, y]";
2343
2344        let legacy = execute_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2345        let arena = execute_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2346
2347        assert_eq!(sorted_variable_keys(&legacy), vec!["arr", "x", "y"]);
2348        assert_eq!(arena, legacy);
2349    }
2350
2351    #[tokio::test(flavor = "multi_thread")]
2352    async fn error_output_variables_match_between_memory_backends() {
2353        let code = "x = 2\ny = missing + 1";
2354
2355        let legacy = execute_error_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2356        let arena = execute_error_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2357
2358        assert_eq!(sorted_variable_keys(&legacy), vec!["x"]);
2359        assert_eq!(arena, legacy);
2360    }
2361
2362    #[tokio::test(flavor = "multi_thread")]
2363    async fn cached_execution_variables_match_between_memory_backends() {
2364        let code = "x = 2\ny = x + 1";
2365
2366        let legacy = run_with_caching_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2367        let arena = run_with_caching_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2368
2369        assert_eq!(sorted_variable_keys(&legacy), vec!["x", "y"]);
2370        assert_eq!(arena, legacy);
2371    }
2372
2373    #[tokio::test(flavor = "multi_thread")]
2374    async fn mock_execution_variables_match_between_memory_backends() {
2375        let code = "y = x + 1";
2376
2377        let legacy = run_mock_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2378        let arena = run_mock_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2379
2380        assert_eq!(sorted_variable_keys(&legacy), vec!["y"]);
2381        assert_eq!(arena, legacy);
2382    }
2383
2384    #[tokio::test(flavor = "multi_thread")]
2385    async fn module_imports_and_exported_closures_match_between_memory_backends() {
2386        let module_code = r#"
2387export base = 40
2388
2389export fn addBase(n) {
2390  return n + base
2391}
2392"#;
2393        let main_code = r#"
2394import base, addBase from 'math.kcl'
2395import 'math.kcl'
2396
2397named = addBase(n = 2)
2398qualified = math::addBase(n = 1)
2399direct = math::base
2400"#;
2401
2402        let legacy = execute_project_variables_with_backend(
2403            main_code,
2404            &[("math.kcl", module_code)],
2405            memory::MemoryBackendKind::Legacy,
2406        )
2407        .await;
2408        let arena = execute_project_variables_with_backend(
2409            main_code,
2410            &[("math.kcl", module_code)],
2411            memory::MemoryBackendKind::Arena,
2412        )
2413        .await;
2414
2415        assert_number_variable(&legacy, "named", 42.0);
2416        assert_number_variable(&legacy, "qualified", 41.0);
2417        assert_number_variable(&legacy, "direct", 40.0);
2418        assert_eq!(arena, legacy);
2419    }
2420
2421    #[tokio::test(flavor = "multi_thread")]
2422    async fn sketch_block_variables_match_between_memory_backends() {
2423        let code = r#"
2424sketch001 = sketch(on = XY) {
2425  line1 = line(start = [0, 0], end = [1, 0])
2426  line2 = line(start = [1, 0], end = [0, 1])
2427}
2428lineCount = 2
2429"#;
2430
2431        let legacy = execute_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2432        let arena = execute_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2433
2434        assert!(legacy.contains_key("sketch001"), "actual: {legacy:?}");
2435        assert_number_variable(&legacy, "lineCount", 2.0);
2436        assert_eq!(arena, legacy);
2437    }
2438
2439    #[tokio::test(flavor = "multi_thread")]
2440    async fn tag_call_stack_lookup_matches_between_memory_backends() {
2441        let code = r#"
2442sketch001 = startSketchOn(XY)
2443  |> startProfile(at = [0, 0])
2444  |> xLine(length = 10, tag = $seg01)
2445
2446segLength = segLen(seg01)
2447"#;
2448
2449        let legacy = execute_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2450        let arena = execute_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2451
2452        assert_number_variable(&legacy, "segLength", 10.0);
2453        assert_eq!(arena, legacy);
2454    }
2455
2456    #[tokio::test(flavor = "multi_thread")]
2457    async fn sketch_transpiler_exec_outcome_variables_match_between_memory_backends() {
2458        let code = r#"
2459sketch001 = startSketchOn(XY)
2460  |> startProfile(at = [0, 0])
2461  |> line(end = [1, 0])
2462"#;
2463        let program = crate::Program::parse_no_errs(code).unwrap();
2464
2465        let legacy = execute_outcome_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2466        let arena = execute_outcome_with_backend(code, memory::MemoryBackendKind::Arena).await;
2467
2468        let legacy_transpiled = transpile_old_sketch_to_new(&legacy, &program, "sketch001").unwrap();
2469        let arena_transpiled = transpile_old_sketch_to_new(&arena, &program, "sketch001").unwrap();
2470        assert_eq!(arena_transpiled, legacy_transpiled);
2471    }
2472
2473    #[tokio::test(flavor = "multi_thread")]
2474    async fn test_execute_warn() {
2475        let text = "@blah";
2476        let result = parse_execute(text).await.unwrap();
2477        let errs = result.exec_state.issues();
2478        assert_eq!(errs.len(), 1);
2479        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2480        assert!(
2481            errs[0].message.contains("Unknown annotation"),
2482            "unexpected warning message: {}",
2483            errs[0].message
2484        );
2485    }
2486
2487    #[tokio::test(flavor = "multi_thread")]
2488    async fn test_execute_fn_definitions() {
2489        let ast = r#"fn def(@x) {
2490  return x
2491}
2492fn ghi(@x) {
2493  return x
2494}
2495fn jkl(@x) {
2496  return x
2497}
2498fn hmm(@x) {
2499  return x
2500}
2501
2502yo = 5 + 6
2503
2504abc = 3
2505identifierGuy = 5
2506part001 = startSketchOn(XY)
2507|> startProfile(at = [-1.2, 4.83])
2508|> line(end = [2.8, 0])
2509|> angledLine(angle = 100 + 100, length = 3.01)
2510|> angledLine(angle = abc, length = 3.02)
2511|> angledLine(angle = def(yo), length = 3.03)
2512|> angledLine(angle = ghi(2), length = 3.04)
2513|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2514|> close()
2515yo2 = hmm([identifierGuy + 5])"#;
2516
2517        parse_execute(ast).await.unwrap();
2518    }
2519
2520    #[tokio::test(flavor = "multi_thread")]
2521    async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2522        let code = r#"
2523firstProfile = sketch(on = XY) {
2524  edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2525  edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2526  edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2527  edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2528  coincident([edge1.end, edge2.start])
2529  coincident([edge2.end, edge3.start])
2530  coincident([edge3.end, edge4.start])
2531  coincident([edge4.end, edge1.start])
2532}
2533
2534secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2535  edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2536  edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2537  edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2538  edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2539  coincident([edge5.end, edge6.start])
2540  coincident([edge6.end, edge7.start])
2541  coincident([edge7.end, edge8.start])
2542  coincident([edge8.end, edge5.start])
2543}
2544
2545firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2546secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2547"#;
2548
2549        let result = parse_execute(code).await.unwrap();
2550        assert!(result.exec_state.issues().is_empty());
2551    }
2552
2553    #[tokio::test(flavor = "multi_thread")]
2554    async fn sketch_block_artifact_preserves_standard_plane_name() {
2555        let code = r#"
2556sketch001 = sketch(on = -YZ) {
2557  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
2558}
2559"#;
2560
2561        let result = parse_execute(code).await.unwrap();
2562        let sketch_blocks = result
2563            .exec_state
2564            .global
2565            .artifacts
2566            .graph
2567            .values()
2568            .filter_map(|artifact| match artifact {
2569                Artifact::SketchBlock(block) => Some(block),
2570                _ => None,
2571            })
2572            .collect::<Vec<_>>();
2573
2574        assert_eq!(sketch_blocks.len(), 1);
2575        assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
2576    }
2577
2578    #[tokio::test(flavor = "multi_thread")]
2579    async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2580        let code = r#"
2581sketch001 = sketch(on = YZ) {
2582  line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2583  line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2584  line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2585  line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2586  coincident([line1.end, line2.start])
2587  coincident([line2.end, line3.start])
2588  coincident([line3.end, line4.start])
2589  coincident([line4.end, line1.start])
2590}
2591
2592sketch002 = sketch(on = -XZ) {
2593  line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2594  line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2595  line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2596  line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2597  coincident([line5.end, line6.start])
2598  coincident([line6.end, line7.start])
2599  coincident([line7.end, line8.start])
2600  coincident([line8.end, line5.start])
2601}
2602
2603region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2604extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2605region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2606extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2607
2608myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2609"#;
2610
2611        let result = parse_execute(code).await.unwrap();
2612        assert!(result.exec_state.issues().is_empty());
2613    }
2614
2615    #[tokio::test(flavor = "multi_thread")]
2616    async fn issue_10741_point_circle_coincident_executes() {
2617        let code = r#"
2618sketch001 = sketch(on = YZ) {
2619  circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
2620  line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
2621  coincident([line1.start, circle1])
2622}
2623"#;
2624
2625        let result = parse_execute(code).await.unwrap();
2626        assert!(
2627            result
2628                .exec_state
2629                .issues()
2630                .iter()
2631                .all(|issue| issue.severity != Severity::Error),
2632            "unexpected execution issues: {:#?}",
2633            result.exec_state.issues()
2634        );
2635    }
2636
2637    #[tokio::test(flavor = "multi_thread")]
2638    async fn test_execute_with_pipe_substitutions_unary() {
2639        let ast = r#"myVar = 3
2640part001 = startSketchOn(XY)
2641  |> startProfile(at = [0, 0])
2642  |> line(end = [3, 4], tag = $seg01)
2643  |> line(end = [
2644  min([segLen(seg01), myVar]),
2645  -legLen(hypotenuse = segLen(seg01), leg = myVar)
2646])
2647"#;
2648
2649        parse_execute(ast).await.unwrap();
2650    }
2651
2652    #[tokio::test(flavor = "multi_thread")]
2653    async fn test_execute_with_pipe_substitutions() {
2654        let ast = r#"myVar = 3
2655part001 = startSketchOn(XY)
2656  |> startProfile(at = [0, 0])
2657  |> line(end = [3, 4], tag = $seg01)
2658  |> line(end = [
2659  min([segLen(seg01), myVar]),
2660  legLen(hypotenuse = segLen(seg01), leg = myVar)
2661])
2662"#;
2663
2664        parse_execute(ast).await.unwrap();
2665    }
2666
2667    #[tokio::test(flavor = "multi_thread")]
2668    async fn test_execute_with_inline_comment() {
2669        let ast = r#"baseThick = 1
2670armAngle = 60
2671
2672baseThickHalf = baseThick / 2
2673halfArmAngle = armAngle / 2
2674
2675arrExpShouldNotBeIncluded = [1, 2, 3]
2676objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2677
2678part001 = startSketchOn(XY)
2679  |> startProfile(at = [0, 0])
2680  |> yLine(endAbsolute = 1)
2681  |> xLine(length = 3.84) // selection-range-7ish-before-this
2682
2683variableBelowShouldNotBeIncluded = 3
2684"#;
2685
2686        parse_execute(ast).await.unwrap();
2687    }
2688
2689    #[tokio::test(flavor = "multi_thread")]
2690    async fn test_execute_with_function_literal_in_pipe() {
2691        let ast = r#"w = 20
2692l = 8
2693h = 10
2694
2695fn thing() {
2696  return -8
2697}
2698
2699firstExtrude = startSketchOn(XY)
2700  |> startProfile(at = [0,0])
2701  |> line(end = [0, l])
2702  |> line(end = [w, 0])
2703  |> line(end = [0, thing()])
2704  |> close()
2705  |> extrude(length = h)"#;
2706
2707        parse_execute(ast).await.unwrap();
2708    }
2709
2710    #[tokio::test(flavor = "multi_thread")]
2711    async fn test_execute_with_function_unary_in_pipe() {
2712        let ast = r#"w = 20
2713l = 8
2714h = 10
2715
2716fn thing(@x) {
2717  return -x
2718}
2719
2720firstExtrude = startSketchOn(XY)
2721  |> startProfile(at = [0,0])
2722  |> line(end = [0, l])
2723  |> line(end = [w, 0])
2724  |> line(end = [0, thing(8)])
2725  |> close()
2726  |> extrude(length = h)"#;
2727
2728        parse_execute(ast).await.unwrap();
2729    }
2730
2731    #[tokio::test(flavor = "multi_thread")]
2732    async fn test_execute_with_function_array_in_pipe() {
2733        let ast = r#"w = 20
2734l = 8
2735h = 10
2736
2737fn thing(@x) {
2738  return [0, -x]
2739}
2740
2741firstExtrude = startSketchOn(XY)
2742  |> startProfile(at = [0,0])
2743  |> line(end = [0, l])
2744  |> line(end = [w, 0])
2745  |> line(end = thing(8))
2746  |> close()
2747  |> extrude(length = h)"#;
2748
2749        parse_execute(ast).await.unwrap();
2750    }
2751
2752    #[tokio::test(flavor = "multi_thread")]
2753    async fn test_execute_with_function_call_in_pipe() {
2754        let ast = r#"w = 20
2755l = 8
2756h = 10
2757
2758fn other_thing(@y) {
2759  return -y
2760}
2761
2762fn thing(@x) {
2763  return other_thing(x)
2764}
2765
2766firstExtrude = startSketchOn(XY)
2767  |> startProfile(at = [0,0])
2768  |> line(end = [0, l])
2769  |> line(end = [w, 0])
2770  |> line(end = [0, thing(8)])
2771  |> close()
2772  |> extrude(length = h)"#;
2773
2774        parse_execute(ast).await.unwrap();
2775    }
2776
2777    #[tokio::test(flavor = "multi_thread")]
2778    async fn test_execute_with_function_sketch() {
2779        let ast = r#"fn box(h, l, w) {
2780 myBox = startSketchOn(XY)
2781    |> startProfile(at = [0,0])
2782    |> line(end = [0, l])
2783    |> line(end = [w, 0])
2784    |> line(end = [0, -l])
2785    |> close()
2786    |> extrude(length = h)
2787
2788  return myBox
2789}
2790
2791fnBox = box(h = 3, l = 6, w = 10)"#;
2792
2793        parse_execute(ast).await.unwrap();
2794    }
2795
2796    #[tokio::test(flavor = "multi_thread")]
2797    async fn test_get_member_of_object_with_function_period() {
2798        let ast = r#"fn box(@obj) {
2799 myBox = startSketchOn(XY)
2800    |> startProfile(at = obj.start)
2801    |> line(end = [0, obj.l])
2802    |> line(end = [obj.w, 0])
2803    |> line(end = [0, -obj.l])
2804    |> close()
2805    |> extrude(length = obj.h)
2806
2807  return myBox
2808}
2809
2810thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2811"#;
2812        parse_execute(ast).await.unwrap();
2813    }
2814
2815    #[tokio::test(flavor = "multi_thread")]
2816    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
2817    async fn test_object_member_starting_pipeline() {
2818        let ast = r#"
2819fn test2() {
2820  return {
2821    thing: startSketchOn(XY)
2822      |> startProfile(at = [0, 0])
2823      |> line(end = [0, 1])
2824      |> line(end = [1, 0])
2825      |> line(end = [0, -1])
2826      |> close()
2827  }
2828}
2829
2830x2 = test2()
2831
2832x2.thing
2833  |> extrude(length = 10)
2834"#;
2835        parse_execute(ast).await.unwrap();
2836    }
2837
2838    #[tokio::test(flavor = "multi_thread")]
2839    #[ignore] // ignore til we get loops
2840    async fn test_execute_with_function_sketch_loop_objects() {
2841        let ast = r#"fn box(obj) {
2842let myBox = startSketchOn(XY)
2843    |> startProfile(at = obj.start)
2844    |> line(end = [0, obj.l])
2845    |> line(end = [obj.w, 0])
2846    |> line(end = [0, -obj.l])
2847    |> close()
2848    |> extrude(length = obj.h)
2849
2850  return myBox
2851}
2852
2853for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2854  thisBox = box(var)
2855}"#;
2856
2857        parse_execute(ast).await.unwrap();
2858    }
2859
2860    #[tokio::test(flavor = "multi_thread")]
2861    #[ignore] // ignore til we get loops
2862    async fn test_execute_with_function_sketch_loop_array() {
2863        let ast = r#"fn box(h, l, w, start) {
2864 myBox = startSketchOn(XY)
2865    |> startProfile(at = [0,0])
2866    |> line(end = [0, l])
2867    |> line(end = [w, 0])
2868    |> line(end = [0, -l])
2869    |> close()
2870    |> extrude(length = h)
2871
2872  return myBox
2873}
2874
2875
2876for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2877  const thisBox = box(var[0], var[1], var[2], var[3])
2878}"#;
2879
2880        parse_execute(ast).await.unwrap();
2881    }
2882
2883    #[tokio::test(flavor = "multi_thread")]
2884    async fn test_get_member_of_array_with_function() {
2885        let ast = r#"fn box(@arr) {
2886 myBox =startSketchOn(XY)
2887    |> startProfile(at = arr[0])
2888    |> line(end = [0, arr[1]])
2889    |> line(end = [arr[2], 0])
2890    |> line(end = [0, -arr[1]])
2891    |> close()
2892    |> extrude(length = arr[3])
2893
2894  return myBox
2895}
2896
2897thisBox = box([[0,0], 6, 10, 3])
2898
2899"#;
2900        parse_execute(ast).await.unwrap();
2901    }
2902
2903    #[tokio::test(flavor = "multi_thread")]
2904    async fn test_function_cannot_access_future_definitions() {
2905        let ast = r#"
2906fn returnX() {
2907  // x shouldn't be defined yet.
2908  return x
2909}
2910
2911x = 5
2912
2913answer = returnX()"#;
2914
2915        let result = parse_execute(ast).await;
2916        let err = result.unwrap_err();
2917        assert_eq!(err.message(), "`x` is not defined");
2918    }
2919
2920    #[tokio::test(flavor = "multi_thread")]
2921    async fn test_override_prelude() {
2922        let text = "PI = 3.0";
2923        let result = parse_execute(text).await.unwrap();
2924        let issues = result.exec_state.issues();
2925        assert!(issues.is_empty(), "issues={issues:#?}");
2926    }
2927
2928    #[tokio::test(flavor = "multi_thread")]
2929    async fn type_aliases() {
2930        let text = r#"@settings(experimentalFeatures = allow)
2931type MyTy = [number; 2]
2932fn foo(@x: MyTy) {
2933    return x[0]
2934}
2935
2936foo([0, 1])
2937
2938type Other = MyTy | Helix
2939"#;
2940        let result = parse_execute(text).await.unwrap();
2941        let issues = result.exec_state.issues();
2942        assert!(issues.is_empty(), "issues={issues:#?}");
2943    }
2944
2945    #[tokio::test(flavor = "multi_thread")]
2946    async fn test_cannot_shebang_in_fn() {
2947        let ast = r#"
2948fn foo() {
2949  #!hello
2950  return true
2951}
2952
2953foo
2954"#;
2955
2956        let result = parse_execute(ast).await;
2957        let err = result.unwrap_err();
2958        assert_eq!(
2959            err,
2960            KclError::new_syntax(KclErrorDetails::new(
2961                "Unexpected token: #".to_owned(),
2962                vec![SourceRange::new(14, 15, ModuleId::default())],
2963            )),
2964        );
2965    }
2966
2967    #[tokio::test(flavor = "multi_thread")]
2968    async fn test_pattern_transform_function_cannot_access_future_definitions() {
2969        let ast = r#"
2970fn transform(@replicaId) {
2971  // x shouldn't be defined yet.
2972  scale = x
2973  return {
2974    translate = [0, 0, replicaId * 10],
2975    scale = [scale, 1, 0],
2976  }
2977}
2978
2979fn layer() {
2980  return startSketchOn(XY)
2981    |> circle( center= [0, 0], radius= 1, tag = $tag1)
2982    |> extrude(length = 10)
2983}
2984
2985x = 5
2986
2987// The 10 layers are replicas of each other, with a transform applied to each.
2988shape = layer() |> patternTransform(instances = 10, transform = transform)
2989"#;
2990
2991        let result = parse_execute(ast).await;
2992        let err = result.unwrap_err();
2993        assert_eq!(err.message(), "`x` is not defined",);
2994    }
2995
2996    // ADAM: Move some of these into simulation tests.
2997
2998    #[tokio::test(flavor = "multi_thread")]
2999    async fn test_math_execute_with_functions() {
3000        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
3001        let result = parse_execute(ast).await.unwrap();
3002        assert_eq!(
3003            5.0,
3004            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3005                .as_f64()
3006                .unwrap()
3007        );
3008    }
3009
3010    #[tokio::test(flavor = "multi_thread")]
3011    async fn test_math_execute() {
3012        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
3013        let result = parse_execute(ast).await.unwrap();
3014        assert_eq!(
3015            7.4,
3016            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3017                .as_f64()
3018                .unwrap()
3019        );
3020    }
3021
3022    #[tokio::test(flavor = "multi_thread")]
3023    async fn test_math_execute_start_negative() {
3024        let ast = r#"myVar = -5 + 6"#;
3025        let result = parse_execute(ast).await.unwrap();
3026        assert_eq!(
3027            1.0,
3028            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3029                .as_f64()
3030                .unwrap()
3031        );
3032    }
3033
3034    #[tokio::test(flavor = "multi_thread")]
3035    async fn test_math_execute_with_pi() {
3036        let ast = r#"myVar = PI * 2"#;
3037        let result = parse_execute(ast).await.unwrap();
3038        assert_eq!(
3039            std::f64::consts::TAU,
3040            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3041                .as_f64()
3042                .unwrap()
3043        );
3044    }
3045
3046    #[tokio::test(flavor = "multi_thread")]
3047    async fn test_math_define_decimal_without_leading_zero() {
3048        let ast = r#"thing = .4 + 7"#;
3049        let result = parse_execute(ast).await.unwrap();
3050        assert_eq!(
3051            7.4,
3052            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
3053                .as_f64()
3054                .unwrap()
3055        );
3056    }
3057
3058    #[tokio::test(flavor = "multi_thread")]
3059    async fn pass_std_to_std() {
3060        let ast = r#"sketch001 = startSketchOn(XY)
3061profile001 = circle(sketch001, center = [0, 0], radius = 2)
3062extrude001 = extrude(profile001, length = 5)
3063extrudes = patternLinear3d(
3064  extrude001,
3065  instances = 3,
3066  distance = 5,
3067  axis = [1, 1, 0],
3068)
3069clone001 = map(extrudes, f = clone)
3070"#;
3071        parse_execute(ast).await.unwrap();
3072    }
3073
3074    #[tokio::test(flavor = "multi_thread")]
3075    async fn test_array_reduce_nested_array() {
3076        let code = r#"
3077fn id(@el, accum)  { return accum }
3078
3079answer = reduce([], initial=[[[0,0]]], f=id)
3080"#;
3081        let result = parse_execute(code).await.unwrap();
3082        assert_eq!(
3083            mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
3084            KclValue::HomArray {
3085                value: vec![KclValue::HomArray {
3086                    value: vec![KclValue::HomArray {
3087                        value: vec![
3088                            KclValue::Number {
3089                                value: 0.0,
3090                                ty: NumericType::default(),
3091                                meta: vec![SourceRange::new(69, 70, Default::default()).into()],
3092                            },
3093                            KclValue::Number {
3094                                value: 0.0,
3095                                ty: NumericType::default(),
3096                                meta: vec![SourceRange::new(71, 72, Default::default()).into()],
3097                            }
3098                        ],
3099                        ty: RuntimeType::any(),
3100                    }],
3101                    ty: RuntimeType::any(),
3102                }],
3103                ty: RuntimeType::any(),
3104            }
3105        );
3106    }
3107
3108    #[tokio::test(flavor = "multi_thread")]
3109    async fn test_zero_param_fn() {
3110        let ast = r#"sigmaAllow = 35000 // psi
3111leg1 = 5 // inches
3112leg2 = 8 // inches
3113fn thickness() { return 0.56 }
3114
3115bracket = startSketchOn(XY)
3116  |> startProfile(at = [0,0])
3117  |> line(end = [0, leg1])
3118  |> line(end = [leg2, 0])
3119  |> line(end = [0, -thickness()])
3120  |> line(end = [-leg2 + thickness(), 0])
3121"#;
3122        parse_execute(ast).await.unwrap();
3123    }
3124
3125    #[tokio::test(flavor = "multi_thread")]
3126    async fn test_unary_operator_not_succeeds() {
3127        let ast = r#"
3128fn returnTrue() { return !false }
3129t = true
3130f = false
3131notTrue = !t
3132notFalse = !f
3133c = !!true
3134d = !returnTrue()
3135
3136assertIs(!false, error = "expected to pass")
3137
3138fn check(x) {
3139  assertIs(!x, error = "expected argument to be false")
3140  return true
3141}
3142check(x = false)
3143"#;
3144        let result = parse_execute(ast).await.unwrap();
3145        assert_eq!(
3146            false,
3147            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
3148                .as_bool()
3149                .unwrap()
3150        );
3151        assert_eq!(
3152            true,
3153            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
3154                .as_bool()
3155                .unwrap()
3156        );
3157        assert_eq!(
3158            true,
3159            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
3160                .as_bool()
3161                .unwrap()
3162        );
3163        assert_eq!(
3164            false,
3165            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
3166                .as_bool()
3167                .unwrap()
3168        );
3169    }
3170
3171    #[tokio::test(flavor = "multi_thread")]
3172    async fn test_unary_operator_not_on_non_bool_fails() {
3173        let code1 = r#"
3174// Yup, this is null.
3175myNull = 0 / 0
3176notNull = !myNull
3177"#;
3178        assert_eq!(
3179            parse_execute(code1).await.unwrap_err().message(),
3180            "Cannot apply unary operator ! to non-boolean value: a number",
3181        );
3182
3183        let code2 = "notZero = !0";
3184        assert_eq!(
3185            parse_execute(code2).await.unwrap_err().message(),
3186            "Cannot apply unary operator ! to non-boolean value: a number",
3187        );
3188
3189        let code3 = r#"
3190notEmptyString = !""
3191"#;
3192        assert_eq!(
3193            parse_execute(code3).await.unwrap_err().message(),
3194            "Cannot apply unary operator ! to non-boolean value: a string",
3195        );
3196
3197        let code4 = r#"
3198obj = { a = 1 }
3199notMember = !obj.a
3200"#;
3201        assert_eq!(
3202            parse_execute(code4).await.unwrap_err().message(),
3203            "Cannot apply unary operator ! to non-boolean value: a number",
3204        );
3205
3206        let code5 = "
3207a = []
3208notArray = !a";
3209        assert_eq!(
3210            parse_execute(code5).await.unwrap_err().message(),
3211            "Cannot apply unary operator ! to non-boolean value: an empty array",
3212        );
3213
3214        let code6 = "
3215x = {}
3216notObject = !x";
3217        assert_eq!(
3218            parse_execute(code6).await.unwrap_err().message(),
3219            "Cannot apply unary operator ! to non-boolean value: an object",
3220        );
3221
3222        let code7 = "
3223fn x() { return 1 }
3224notFunction = !x";
3225        let fn_err = parse_execute(code7).await.unwrap_err();
3226        // These are currently printed out as JSON objects, so we don't want to
3227        // check the full error.
3228        assert!(
3229            fn_err
3230                .message()
3231                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
3232            "Actual error: {fn_err:?}"
3233        );
3234
3235        let code8 = "
3236myTagDeclarator = $myTag
3237notTagDeclarator = !myTagDeclarator";
3238        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
3239        // These are currently printed out as JSON objects, so we don't want to
3240        // check the full error.
3241        assert!(
3242            tag_declarator_err
3243                .message()
3244                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
3245            "Actual error: {tag_declarator_err:?}"
3246        );
3247
3248        let code9 = "
3249myTagDeclarator = $myTag
3250notTagIdentifier = !myTag";
3251        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
3252        // These are currently printed out as JSON objects, so we don't want to
3253        // check the full error.
3254        assert!(
3255            tag_identifier_err
3256                .message()
3257                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
3258            "Actual error: {tag_identifier_err:?}"
3259        );
3260
3261        let code10 = "notPipe = !(1 |> 2)";
3262        assert_eq!(
3263            // TODO: We don't currently parse this, but we should.  It should be
3264            // a runtime error instead.
3265            parse_execute(code10).await.unwrap_err(),
3266            KclError::new_syntax(KclErrorDetails::new(
3267                "Unexpected token: !".to_owned(),
3268                vec![SourceRange::new(10, 11, ModuleId::default())],
3269            ))
3270        );
3271
3272        let code11 = "
3273fn identity(x) { return x }
3274notPipeSub = 1 |> identity(!%))";
3275        assert_eq!(
3276            // TODO: We don't currently parse this, but we should.  It should be
3277            // a runtime error instead.
3278            parse_execute(code11).await.unwrap_err(),
3279            KclError::new_syntax(KclErrorDetails::new(
3280                "There was an unexpected `!`. Try removing it.".to_owned(),
3281                vec![SourceRange::new(56, 57, ModuleId::default())],
3282            ))
3283        );
3284
3285        // TODO: Add these tests when we support these types.
3286        // let notNan = !NaN
3287        // let notInfinity = !Infinity
3288    }
3289
3290    #[tokio::test(flavor = "multi_thread")]
3291    async fn test_start_sketch_on_invalid_kwargs() {
3292        let current_dir = std::env::current_dir().unwrap();
3293        let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
3294        let mut code = std::fs::read_to_string(&path).unwrap();
3295        assert_eq!(
3296            parse_execute(&code).await.unwrap_err().message(),
3297            "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
3298        );
3299
3300        path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
3301        code = std::fs::read_to_string(&path).unwrap();
3302
3303        assert_eq!(
3304            parse_execute(&code).await.unwrap_err().message(),
3305            "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
3306        );
3307
3308        path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
3309        code = std::fs::read_to_string(&path).unwrap();
3310
3311        assert_eq!(
3312            parse_execute(&code).await.unwrap_err().message(),
3313            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
3314        );
3315
3316        path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
3317        code = std::fs::read_to_string(&path).unwrap();
3318
3319        assert_eq!(
3320            parse_execute(&code).await.unwrap_err().message(),
3321            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
3322        );
3323
3324        path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
3325        code = std::fs::read_to_string(&path).unwrap();
3326
3327        assert_eq!(
3328            parse_execute(&code).await.unwrap_err().message(),
3329            "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
3330        );
3331    }
3332
3333    #[tokio::test(flavor = "multi_thread")]
3334    async fn test_math_negative_variable_in_binary_expression() {
3335        let ast = r#"sigmaAllow = 35000 // psi
3336width = 1 // inch
3337
3338p = 150 // lbs
3339distance = 6 // inches
3340FOS = 2
3341
3342leg1 = 5 // inches
3343leg2 = 8 // inches
3344
3345thickness_squared = distance * p * FOS * 6 / sigmaAllow
3346thickness = 0.56 // inches. App does not support square root function yet
3347
3348bracket = startSketchOn(XY)
3349  |> startProfile(at = [0,0])
3350  |> line(end = [0, leg1])
3351  |> line(end = [leg2, 0])
3352  |> line(end = [0, -thickness])
3353  |> line(end = [-leg2 + thickness, 0])
3354"#;
3355        parse_execute(ast).await.unwrap();
3356    }
3357
3358    #[tokio::test(flavor = "multi_thread")]
3359    async fn test_execute_function_no_return() {
3360        let ast = r#"fn test(@origin) {
3361  origin
3362}
3363
3364test([0, 0])
3365"#;
3366        let result = parse_execute(ast).await;
3367        assert!(result.is_err());
3368        assert!(result.unwrap_err().to_string().contains("undefined"));
3369    }
3370
3371    #[tokio::test(flavor = "multi_thread")]
3372    async fn test_max_stack_size_exceeded_error() {
3373        let ast = r#"
3374fn forever(@n) {
3375  return 1 + forever(n)
3376}
3377
3378forever(1)
3379"#;
3380        let result = parse_execute(ast).await;
3381        let err = result.unwrap_err();
3382        assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
3383    }
3384
3385    #[tokio::test(flavor = "multi_thread")]
3386    async fn test_math_doubly_nested_parens() {
3387        let ast = r#"sigmaAllow = 35000 // psi
3388width = 4 // inch
3389p = 150 // Force on shelf - lbs
3390distance = 6 // inches
3391FOS = 2
3392leg1 = 5 // inches
3393leg2 = 8 // inches
3394thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
3395thickness = 0.32 // inches. App does not support square root function yet
3396bracket = startSketchOn(XY)
3397  |> startProfile(at = [0,0])
3398    |> line(end = [0, leg1])
3399  |> line(end = [leg2, 0])
3400  |> line(end = [0, -thickness])
3401  |> line(end = [-1 * leg2 + thickness, 0])
3402  |> line(end = [0, -1 * leg1 + thickness])
3403  |> close()
3404  |> extrude(length = width)
3405"#;
3406        parse_execute(ast).await.unwrap();
3407    }
3408
3409    #[tokio::test(flavor = "multi_thread")]
3410    async fn test_math_nested_parens_one_less() {
3411        let ast = r#" sigmaAllow = 35000 // psi
3412width = 4 // inch
3413p = 150 // Force on shelf - lbs
3414distance = 6 // inches
3415FOS = 2
3416leg1 = 5 // inches
3417leg2 = 8 // inches
3418thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
3419thickness = 0.32 // inches. App does not support square root function yet
3420bracket = startSketchOn(XY)
3421  |> startProfile(at = [0,0])
3422    |> line(end = [0, leg1])
3423  |> line(end = [leg2, 0])
3424  |> line(end = [0, -thickness])
3425  |> line(end = [-1 * leg2 + thickness, 0])
3426  |> line(end = [0, -1 * leg1 + thickness])
3427  |> close()
3428  |> extrude(length = width)
3429"#;
3430        parse_execute(ast).await.unwrap();
3431    }
3432
3433    #[tokio::test(flavor = "multi_thread")]
3434    async fn test_fn_as_operand() {
3435        let ast = r#"fn f() { return 1 }
3436x = f()
3437y = x + 1
3438z = f() + 1
3439w = f() + f()
3440"#;
3441        parse_execute(ast).await.unwrap();
3442    }
3443
3444    #[tokio::test(flavor = "multi_thread")]
3445    async fn kcl_test_ids_stable_between_executions() {
3446        let code = r#"sketch001 = startSketchOn(XZ)
3447|> startProfile(at = [61.74, 206.13])
3448|> xLine(length = 305.11, tag = $seg01)
3449|> yLine(length = -291.85)
3450|> xLine(length = -segLen(seg01))
3451|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3452|> close()
3453|> extrude(length = 40.14)
3454|> shell(
3455    thickness = 3.14,
3456    faces = [seg01]
3457)
3458"#;
3459
3460        let ctx = crate::test_server::new_context(true, None).await.unwrap();
3461        let old_program = crate::Program::parse_no_errs(code).unwrap();
3462
3463        // Execute the program.
3464        if let Err(err) = ctx.run_with_caching(old_program).await {
3465            let report = err.into_miette_report_with_outputs(code).unwrap();
3466            let report = miette::Report::new(report);
3467            panic!("Error executing program: {report:?}");
3468        }
3469
3470        // Get the id_generator from the first execution.
3471        let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3472
3473        let code = r#"sketch001 = startSketchOn(XZ)
3474|> startProfile(at = [62.74, 206.13])
3475|> xLine(length = 305.11, tag = $seg01)
3476|> yLine(length = -291.85)
3477|> xLine(length = -segLen(seg01))
3478|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3479|> close()
3480|> extrude(length = 40.14)
3481|> shell(
3482    faces = [seg01],
3483    thickness = 3.14,
3484)
3485"#;
3486
3487        // Execute a slightly different program again.
3488        let program = crate::Program::parse_no_errs(code).unwrap();
3489        // Execute the program.
3490        ctx.run_with_caching(program).await.unwrap();
3491
3492        let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3493
3494        assert_eq!(id_generator, new_id_generator);
3495    }
3496
3497    #[tokio::test(flavor = "multi_thread")]
3498    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
3499        let code = r#"sketch001 = startSketchOn(XZ)
3500|> startProfile(at = [61.74, 206.13])
3501|> xLine(length = 305.11, tag = $seg01)
3502|> yLine(length = -291.85)
3503|> xLine(length = -segLen(seg01))
3504|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3505|> close()
3506|> extrude(length = 40.14)
3507|> shell(
3508    thickness = 3.14,
3509    faces = [seg01]
3510)
3511"#;
3512
3513        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
3514        let old_program = crate::Program::parse_no_errs(code).unwrap();
3515
3516        // Execute the program.
3517        ctx.run_with_caching(old_program.clone()).await.unwrap();
3518
3519        let settings_state = cache::read_old_ast().await.unwrap().settings;
3520
3521        // Ensure the settings are as expected.
3522        assert_eq!(settings_state, ctx.settings);
3523
3524        // Change a setting.
3525        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3526
3527        // Execute the program.
3528        ctx.run_with_caching(old_program.clone()).await.unwrap();
3529
3530        let settings_state = cache::read_old_ast().await.unwrap().settings;
3531
3532        // Ensure the settings are as expected.
3533        assert_eq!(settings_state, ctx.settings);
3534
3535        // Change a setting.
3536        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3537
3538        // Execute the program.
3539        ctx.run_with_caching(old_program).await.unwrap();
3540
3541        let settings_state = cache::read_old_ast().await.unwrap().settings;
3542
3543        // Ensure the settings are as expected.
3544        assert_eq!(settings_state, ctx.settings);
3545
3546        ctx.close().await;
3547    }
3548
3549    #[tokio::test(flavor = "multi_thread")]
3550    async fn mock_after_not_mock() {
3551        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3552        let program = crate::Program::parse_no_errs("x = 2").unwrap();
3553        let result = ctx.run_with_caching(program).await.unwrap();
3554        assert_number_variable(&result.variables, "x", 2.0);
3555
3556        let ctx2 = ExecutorContext::new_mock(None).await;
3557        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
3558        let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
3559        assert_number_variable(&result.variables, "z", 3.0);
3560
3561        ctx.close().await;
3562        ctx2.close().await;
3563    }
3564
3565    #[tokio::test(flavor = "multi_thread")]
3566    async fn mock_then_add_extrude_then_mock_again() {
3567        let code = "s = sketch(on = XY) {
3568    line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
3569    line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
3570    coincident([line1.end, line2.start])
3571    line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
3572    coincident([line2.end, line3.start])
3573    coincident([line1.start, line3.end])
3574}
3575    ";
3576        let ctx = ExecutorContext::new_mock(None).await;
3577        let program = crate::Program::parse_no_errs(code).unwrap();
3578        let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3579        assert!(result.variables.contains_key("s"), "actual: {:?}", &result.variables);
3580
3581        let code2 = code.to_owned()
3582            + "
3583region001 = region(point = [1mm, 1mm], sketch = s)
3584extrude001 = extrude(region001, length = 1)
3585    ";
3586        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3587        let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
3588        assert!(
3589            result.variables.contains_key("region001"),
3590            "actual: {:?}",
3591            &result.variables
3592        );
3593
3594        ctx.close().await;
3595    }
3596
3597    #[tokio::test(flavor = "multi_thread")]
3598    async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
3599        let code = format!(
3600            r#"{}
3601
3602face7 = faceOf(solid6, face = r6.tags.line1)
3603r7 = squareRegion(onSurface = face7)
3604solid7 = extrude(r7, length = width)
3605"#,
3606            include_str!("../../tests/endless_impeller/input.kcl")
3607        );
3608
3609        let result = parse_execute(&code).await.unwrap();
3610        let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
3611        assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
3612
3613        let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
3614            KclValue::Face { value } => value,
3615            value => panic!("expected face7 to be a Face, got {value:?}"),
3616        };
3617        assert!(face7.parent_solid.creator_sketch_id.is_some());
3618    }
3619
3620    #[tokio::test(flavor = "multi_thread")]
3621    async fn mock_has_stable_ids() {
3622        let ctx = ExecutorContext::new_mock(None).await;
3623        let mock_config = MockConfig {
3624            use_prev_memory: false,
3625            ..Default::default()
3626        };
3627        let code = "sk = startSketchOn(XY)
3628        |> startProfile(at = [0, 0])";
3629        let program = crate::Program::parse_no_errs(code).unwrap();
3630        let result = ctx.run_mock(&program, &mock_config).await.unwrap();
3631        let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3632        assert!(!ids.is_empty(), "IDs should not be empty");
3633
3634        let ctx2 = ExecutorContext::new_mock(None).await;
3635        let program2 = crate::Program::parse_no_errs(code).unwrap();
3636        let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
3637        let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3638
3639        assert_eq!(ids, ids2, "Generated IDs should match");
3640        ctx.close().await;
3641        ctx2.close().await;
3642    }
3643
3644    #[tokio::test(flavor = "multi_thread")]
3645    async fn mock_memory_restore_preserves_module_maps() {
3646        clear_mem_cache().await;
3647
3648        let ctx = ExecutorContext::new_mock(None).await;
3649        let cold_start = MockConfig {
3650            use_prev_memory: false,
3651            ..Default::default()
3652        };
3653        ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
3654
3655        let mut mem = cache::read_old_memory().await.unwrap();
3656        assert!(
3657            mem.path_to_source_id.len() > 3,
3658            "expected prelude imports to populate multiple modules, got {:?}",
3659            mem.path_to_source_id
3660        );
3661        mem.constraint_state.insert(
3662            crate::front::ObjectId(1),
3663            indexmap::indexmap! {
3664                crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
3665                    crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
3666            },
3667        );
3668
3669        let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
3670        ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
3671
3672        assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
3673        assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
3674        assert_eq!(exec_state.global.module_infos, mem.module_infos);
3675        assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
3676
3677        clear_mem_cache().await;
3678        ctx.close().await;
3679    }
3680
3681    #[tokio::test(flavor = "multi_thread")]
3682    async fn run_with_caching_no_action_refreshes_mock_memory() {
3683        cache::bust_cache().await;
3684        clear_mem_cache().await;
3685
3686        let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
3687        let program = crate::Program::parse_no_errs(
3688            r#"sketch001 = sketch(on = XY) {
3689  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
3690}
3691"#,
3692        )
3693        .unwrap();
3694
3695        ctx.run_with_caching(program.clone()).await.unwrap();
3696        let baseline_memory = cache::read_old_memory().await.unwrap();
3697        assert!(
3698            !baseline_memory.scene_objects.is_empty(),
3699            "expected engine execution to persist full-scene mock memory"
3700        );
3701
3702        cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
3703        assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
3704
3705        ctx.run_with_caching(program).await.unwrap();
3706        let refreshed_memory = cache::read_old_memory().await.unwrap();
3707        assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
3708        assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
3709        assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
3710
3711        cache::bust_cache().await;
3712        clear_mem_cache().await;
3713        ctx.close().await;
3714    }
3715
3716    #[tokio::test(flavor = "multi_thread")]
3717    async fn sim_sketch_mode_real_mock_real() {
3718        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3719        let code = r#"sketch001 = startSketchOn(XY)
3720profile001 = startProfile(sketch001, at = [0, 0])
3721  |> line(end = [10, 0])
3722  |> line(end = [0, 10])
3723  |> line(end = [-10, 0])
3724  |> line(end = [0, -10])
3725  |> close()
3726"#;
3727        let program = crate::Program::parse_no_errs(code).unwrap();
3728        let result = ctx.run_with_caching(program).await.unwrap();
3729        assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
3730
3731        let mock_ctx = ExecutorContext::new_mock(None).await;
3732        let mock_program = crate::Program::parse_no_errs(code).unwrap();
3733        let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
3734        assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
3735
3736        let code2 = code.to_owned()
3737            + r#"
3738extrude001 = extrude(profile001, length = 10)
3739"#;
3740        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3741        let result = ctx.run_with_caching(program2).await.unwrap();
3742        assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
3743
3744        ctx.close().await;
3745        mock_ctx.close().await;
3746    }
3747
3748    #[tokio::test(flavor = "multi_thread")]
3749    async fn read_tag_version() {
3750        let ast = r#"fn bar(@t) {
3751  return startSketchOn(XY)
3752    |> startProfile(at = [0,0])
3753    |> angledLine(
3754        angle = -60,
3755        length = segLen(t),
3756    )
3757    |> line(end = [0, 0])
3758    |> close()
3759}
3760
3761sketch = startSketchOn(XY)
3762  |> startProfile(at = [0,0])
3763  |> line(end = [0, 10])
3764  |> line(end = [10, 0], tag = $tag0)
3765  |> line(endAbsolute = [0, 0])
3766
3767fn foo() {
3768  // tag0 tags an edge
3769  return bar(tag0)
3770}
3771
3772solid = sketch |> extrude(length = 10)
3773// tag0 tags a face
3774sketch2 = startSketchOn(solid, face = tag0)
3775  |> startProfile(at = [0,0])
3776  |> line(end = [0, 1])
3777  |> line(end = [1, 0])
3778  |> line(end = [0, 0])
3779
3780foo() |> extrude(length = 1)
3781"#;
3782        parse_execute(ast).await.unwrap();
3783    }
3784
3785    #[tokio::test(flavor = "multi_thread")]
3786    async fn experimental() {
3787        let code = r#"
3788startSketchOn(XY)
3789  |> startProfile(at = [0, 0], tag = $start)
3790  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3791"#;
3792        let result = parse_execute(code).await.unwrap();
3793        let issues = result.exec_state.issues();
3794        assert_eq!(issues.len(), 1);
3795        assert_eq!(issues[0].severity, Severity::Error);
3796        let msg = &issues[0].message;
3797        assert!(msg.contains("experimental"), "found {msg}");
3798
3799        let code = r#"@settings(experimentalFeatures = allow)
3800startSketchOn(XY)
3801  |> startProfile(at = [0, 0], tag = $start)
3802  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3803"#;
3804        let result = parse_execute(code).await.unwrap();
3805        let issues = result.exec_state.issues();
3806        assert!(issues.is_empty(), "issues={issues:#?}");
3807
3808        let code = r#"@settings(experimentalFeatures = warn)
3809startSketchOn(XY)
3810  |> startProfile(at = [0, 0], tag = $start)
3811  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3812"#;
3813        let result = parse_execute(code).await.unwrap();
3814        let issues = result.exec_state.issues();
3815        assert_eq!(issues.len(), 1);
3816        assert_eq!(issues[0].severity, Severity::Warning);
3817        let msg = &issues[0].message;
3818        assert!(msg.contains("experimental"), "found {msg}");
3819
3820        let code = r#"@settings(experimentalFeatures = deny)
3821startSketchOn(XY)
3822  |> startProfile(at = [0, 0], tag = $start)
3823  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3824"#;
3825        let result = parse_execute(code).await.unwrap();
3826        let issues = result.exec_state.issues();
3827        assert_eq!(issues.len(), 1);
3828        assert_eq!(issues[0].severity, Severity::Error);
3829        let msg = &issues[0].message;
3830        assert!(msg.contains("experimental"), "found {msg}");
3831
3832        let code = r#"@settings(experimentalFeatures = foo)
3833startSketchOn(XY)
3834  |> startProfile(at = [0, 0], tag = $start)
3835  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3836"#;
3837        parse_execute(code).await.unwrap_err();
3838    }
3839
3840    #[tokio::test(flavor = "multi_thread")]
3841    async fn experimental_parameter() {
3842        let code = r#"
3843fn inc(@x, @(experimental = true) amount? = 1) {
3844  return x + amount
3845}
3846
3847answer = inc(5, amount = 2)
3848"#;
3849        let result = parse_execute(code).await.unwrap();
3850        let issues = result.exec_state.issues();
3851        assert_eq!(issues.len(), 1);
3852        assert_eq!(issues[0].severity, Severity::Error);
3853        let msg = &issues[0].message;
3854        assert!(msg.contains("experimental"), "found {msg}");
3855
3856        // If the parameter isn't used, there's no warning.
3857        let code = r#"
3858fn inc(@x, @(experimental = true) amount? = 1) {
3859  return x + amount
3860}
3861
3862answer = inc(5)
3863"#;
3864        let result = parse_execute(code).await.unwrap();
3865        let issues = result.exec_state.issues();
3866        assert!(issues.is_empty(), "issues={issues:#?}");
3867    }
3868
3869    #[tokio::test(flavor = "multi_thread")]
3870    async fn experimental_scalar_fixed_constraint() {
3871        let code_left = r#"@settings(experimentalFeatures = warn)
3872sketch(on = XY) {
3873  point1 = point(at = [var 0mm, var 0mm])
3874  point1.at[0] == 1mm
3875}
3876"#;
3877        // It's symmetric. Flipping the binary operator has the same behavior.
3878        let code_right = r#"@settings(experimentalFeatures = warn)
3879sketch(on = XY) {
3880  point1 = point(at = [var 0mm, var 0mm])
3881  1mm == point1.at[0]
3882}
3883"#;
3884
3885        for code in [code_left, code_right] {
3886            let result = parse_execute(code).await.unwrap();
3887            let issues = result.exec_state.issues();
3888            let Some(error) = issues
3889                .iter()
3890                .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
3891            else {
3892                panic!("found {issues:#?}");
3893            };
3894            assert_eq!(error.severity, Severity::Warning);
3895        }
3896    }
3897
3898    // START Mock Execution tests
3899    // Ideally, we would do this as part of all sim tests and delete these one-off tests.
3900
3901    #[tokio::test(flavor = "multi_thread")]
3902    async fn test_tangent_line_arc_executes_with_mock_engine() {
3903        let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
3904        parse_execute(&code).await.unwrap();
3905    }
3906
3907    #[tokio::test(flavor = "multi_thread")]
3908    async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
3909        let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
3910        parse_execute(&code).await.unwrap();
3911    }
3912
3913    #[tokio::test(flavor = "multi_thread")]
3914    async fn test_tangent_line_circle_executes_with_mock_engine() {
3915        let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
3916        parse_execute(&code).await.unwrap();
3917    }
3918
3919    #[tokio::test(flavor = "multi_thread")]
3920    async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
3921        let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
3922        parse_execute(&code).await.unwrap();
3923    }
3924
3925    #[tokio::test(flavor = "multi_thread")]
3926    async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
3927        let code = r#"startX = 2
3928
3929baseSketch = sketch(on = XY) {
3930  yoyo = line(start = [startX, 0], end = [7, 6])
3931  line2 = line(start = [7, 6], end = [7, 12])
3932  hi = line(start = [7, 12], end = [startX, 0])
3933}
3934
3935baseRegion = region(point = [5.5, 6], sketch = baseSketch)
3936myExtrude = extrude(
3937  baseRegion,
3938  length = 5,
3939  tagEnd = $endCap,
3940  tagStart = $startCap,
3941)
3942yodawg = getCommonEdge(faces = [
3943  baseRegion.tags.hi,
3944  baseRegion.tags.yoyo
3945])
3946
3947cutSketch = sketch(on = YZ) {
3948  myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
3949  myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
3950  line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
3951}
3952
3953cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
3954extrude001 = extrude(cutRegion, length = 5)
3955solid001 = subtract(myExtrude, tools = extrude001)
3956
3957yoyo = getOppositeEdge(baseRegion.tags.hi)
3958fillet(solid001, radius = 0.1, tags = yoyo)
3959"#;
3960
3961        parse_execute(code).await.unwrap();
3962    }
3963
3964    // END Mock Execution tests
3965
3966    // Sketch constraint report tests
3967
3968    async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
3969        let program = crate::Program::parse_no_errs(kcl).unwrap();
3970        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3971        let mut exec_state = ExecState::new(&ctx);
3972        let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3973        let outcome = exec_state
3974            .into_exec_outcome(env_ref, &ctx)
3975            .await
3976            .expect("constraint report test outcome should collect variables");
3977        let report = outcome.sketch_constraint_report();
3978        ctx.close().await;
3979        report
3980    }
3981
3982    #[tokio::test(flavor = "multi_thread")]
3983    async fn warn_when_sketch_is_over_constrained() {
3984        let code = r#"
3985sketch001 = sketch(on = XY) {
3986  line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
3987  fixed([line1.start, ORIGIN])
3988  fixed([line1.start, [20, 20]])
3989}
3990"#;
3991        let result = parse_execute(code).await.unwrap();
3992        let issues = result.exec_state.issues();
3993        let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
3994            panic!("expected over-constrained warning; found {issues:#?}");
3995        };
3996        assert_eq!(warning.severity, Severity::Warning);
3997    }
3998
3999    #[tokio::test(flavor = "multi_thread")]
4000    async fn no_warning_when_sketch_is_not_over_constrained() {
4001        // Under-constrained sketch should not emit the over-constrained warning.
4002        let code = r#"
4003sketch001 = sketch(on = XY) {
4004  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4005}
4006"#;
4007        let result = parse_execute(code).await.unwrap();
4008        let issues = result.exec_state.issues();
4009        assert!(
4010            !issues.iter().any(|issue| issue.message.contains("over-constrained")),
4011            "did not expect over-constrained warning; found {issues:#?}"
4012        );
4013    }
4014
4015    #[tokio::test(flavor = "multi_thread")]
4016    async fn test_constraint_report_fully_constrained() {
4017        // All points are fully constrained via equality constraints.
4018        let kcl = r#"
4019@settings(experimentalFeatures = allow)
4020
4021sketch(on = YZ) {
4022  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4023  line1.start.at[0] == 2
4024  line1.start.at[1] == 8
4025  line1.end.at[0] == 5
4026  line1.end.at[1] == 7
4027}
4028"#;
4029        let report = run_constraint_report(kcl).await;
4030        assert_eq!(report.fully_constrained.len(), 1);
4031        assert_eq!(report.under_constrained.len(), 0);
4032        assert_eq!(report.over_constrained.len(), 0);
4033        assert_eq!(report.errors.len(), 0);
4034        assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
4035    }
4036
4037    #[tokio::test(flavor = "multi_thread")]
4038    async fn test_constraint_report_under_constrained() {
4039        // No constraints at all — all points are free.
4040        let kcl = r#"
4041sketch(on = YZ) {
4042  line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
4043}
4044"#;
4045        let report = run_constraint_report(kcl).await;
4046        assert_eq!(report.fully_constrained.len(), 0);
4047        assert_eq!(report.under_constrained.len(), 1);
4048        assert_eq!(report.over_constrained.len(), 0);
4049        assert_eq!(report.errors.len(), 0);
4050        assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
4051        assert!(report.under_constrained[0].free_count > 0);
4052    }
4053
4054    #[tokio::test(flavor = "multi_thread")]
4055    async fn test_constraint_report_over_constrained() {
4056        // Conflicting distance constraints on the same pair of points.
4057        let kcl = r#"
4058@settings(experimentalFeatures = allow)
4059
4060sketch(on = YZ) {
4061  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4062  line1.start.at[0] == 2
4063  line1.start.at[1] == 8
4064  line1.end.at[0] == 5
4065  line1.end.at[1] == 7
4066  distance([line1.start, line1.end]) == 100mm
4067}
4068"#;
4069        let report = run_constraint_report(kcl).await;
4070        assert_eq!(report.over_constrained.len(), 1);
4071        assert_eq!(report.errors.len(), 0);
4072        assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
4073        assert!(report.over_constrained[0].conflict_count > 0);
4074    }
4075
4076    #[tokio::test(flavor = "multi_thread")]
4077    async fn test_constraint_report_multiple_sketches() {
4078        // Two sketches: one fully constrained, one under-constrained.
4079        let kcl = r#"
4080@settings(experimentalFeatures = allow)
4081
4082s1 = sketch(on = YZ) {
4083  line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4084  line1.start.at[0] == 2
4085  line1.start.at[1] == 8
4086  line1.end.at[0] == 5
4087  line1.end.at[1] == 7
4088}
4089
4090s2 = sketch(on = XZ) {
4091  line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4092}
4093"#;
4094        let report = run_constraint_report(kcl).await;
4095        assert_eq!(
4096            report.fully_constrained.len()
4097                + report.under_constrained.len()
4098                + report.over_constrained.len()
4099                + report.errors.len(),
4100            2,
4101            "Expected 2 sketches total"
4102        );
4103        assert_eq!(report.fully_constrained.len(), 1);
4104        assert_eq!(report.under_constrained.len(), 1);
4105    }
4106}