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