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