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