Skip to main content

kcl_lib/execution/
mod.rs

1//! The executor for the AST.
2
3#[cfg(feature = "artifact-graph")]
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7use anyhow::Result;
8#[cfg(feature = "artifact-graph")]
9pub use artifact::Artifact;
10#[cfg(feature = "artifact-graph")]
11pub use artifact::ArtifactCommand;
12#[cfg(feature = "artifact-graph")]
13pub use artifact::ArtifactGraph;
14#[cfg(feature = "artifact-graph")]
15pub use artifact::CapSubType;
16#[cfg(feature = "artifact-graph")]
17pub use artifact::CodeRef;
18#[cfg(feature = "artifact-graph")]
19pub use artifact::SketchBlock;
20#[cfg(feature = "artifact-graph")]
21pub use artifact::SketchBlockConstraint;
22#[cfg(feature = "artifact-graph")]
23pub use artifact::SketchBlockConstraintType;
24#[cfg(feature = "artifact-graph")]
25pub use artifact::StartSketchOnFace;
26#[cfg(feature = "artifact-graph")]
27pub use artifact::StartSketchOnPlane;
28use cache::GlobalState;
29pub use cache::bust_cache;
30pub use cache::clear_mem_cache;
31#[cfg(feature = "artifact-graph")]
32pub use cad_op::Group;
33pub use cad_op::Operation;
34pub use geometry::*;
35pub use id_generator::IdGenerator;
36pub(crate) use import::PreImportedGeometry;
37use indexmap::IndexMap;
38pub use kcl_value::KclObjectFields;
39pub use kcl_value::KclValue;
40use kcmc::ImageFormat;
41use kcmc::ModelingCmd;
42use kcmc::each_cmd as mcmd;
43use kcmc::ok_response::OkModelingCmdResponse;
44use kcmc::ok_response::output::TakeSnapshot;
45use kcmc::websocket::ModelingSessionData;
46use kcmc::websocket::OkWebSocketResponseData;
47use kittycad_modeling_cmds::id::ModelingCmdId;
48use kittycad_modeling_cmds::{self as kcmc};
49pub use memory::EnvironmentRef;
50pub(crate) use modeling::ModelingCmdMeta;
51use serde::Deserialize;
52use serde::Serialize;
53pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
54pub(crate) use sketch_solve::solver_numeric_type;
55pub use sketch_transpiler::pre_execute_transpile;
56pub use sketch_transpiler::transpile_all_old_sketches_to_new;
57pub use sketch_transpiler::transpile_old_sketch_to_new;
58pub use sketch_transpiler::transpile_old_sketch_to_new_ast;
59pub use sketch_transpiler::transpile_old_sketch_to_new_with_execution;
60pub use state::ExecState;
61pub use state::MetaSettings;
62pub(crate) use state::ModuleArtifactState;
63use uuid::Uuid;
64
65use crate::CompilationError;
66use crate::ExecError;
67use crate::KclErrorWithOutputs;
68use crate::SourceRange;
69#[cfg(feature = "artifact-graph")]
70use crate::collections::AhashIndexSet;
71use crate::engine::EngineManager;
72use crate::engine::GridScaleBehavior;
73use crate::errors::KclError;
74use crate::errors::KclErrorDetails;
75use crate::execution::cache::CacheInformation;
76use crate::execution::cache::CacheResult;
77use crate::execution::import_graph::Universe;
78use crate::execution::import_graph::UniverseMap;
79use crate::execution::typed_path::TypedPath;
80#[cfg(feature = "artifact-graph")]
81use crate::front::Number;
82use crate::front::Object;
83use crate::front::ObjectId;
84use crate::fs::FileManager;
85use crate::modules::ModuleExecutionOutcome;
86use crate::modules::ModuleId;
87use crate::modules::ModulePath;
88use crate::modules::ModuleRepr;
89use crate::parsing::ast::types::Expr;
90use crate::parsing::ast::types::ImportPath;
91use crate::parsing::ast::types::NodeRef;
92
93pub(crate) mod annotations;
94#[cfg(feature = "artifact-graph")]
95mod artifact;
96pub(crate) mod cache;
97mod cad_op;
98mod exec_ast;
99pub mod fn_call;
100#[cfg(test)]
101#[cfg(feature = "artifact-graph")]
102mod freedom_analysis_tests;
103mod geometry;
104mod id_generator;
105mod import;
106mod import_graph;
107pub(crate) mod kcl_value;
108mod memory;
109mod modeling;
110mod sketch_solve;
111mod sketch_transpiler;
112mod state;
113pub mod typed_path;
114pub(crate) mod types;
115
116pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
117pub(crate) const SKETCH_OBJECT_META: &str = "meta";
118pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
119
120/// Convenience macro for handling [`KclValueControlFlow`] in execution by
121/// returning early if it is some kind of early return or stripping off the
122/// control flow otherwise. If it's an early return, it's returned as a
123/// `Result::Ok`.
124macro_rules! control_continue {
125    ($control_flow:expr) => {{
126        let cf = $control_flow;
127        if cf.is_some_return() {
128            return Ok(cf);
129        } else {
130            cf.into_value()
131        }
132    }};
133}
134// Expose the macro to other modules.
135pub(crate) use control_continue;
136
137/// Convenience macro for handling [`KclValueControlFlow`] in execution by
138/// returning early if it is some kind of early return or stripping off the
139/// control flow otherwise. If it's an early return, [`EarlyReturn`] is
140/// used to return it as a `Result::Err`.
141macro_rules! early_return {
142    ($control_flow:expr) => {{
143        let cf = $control_flow;
144        if cf.is_some_return() {
145            return Err(EarlyReturn::from(cf));
146        } else {
147            cf.into_value()
148        }
149    }};
150}
151// Expose the macro to other modules.
152pub(crate) use early_return;
153
154#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
155pub enum ControlFlowKind {
156    #[default]
157    Continue,
158    Exit,
159}
160
161impl ControlFlowKind {
162    /// Returns true if this is any kind of early return.
163    pub fn is_some_return(&self) -> bool {
164        match self {
165            ControlFlowKind::Continue => false,
166            ControlFlowKind::Exit => true,
167        }
168    }
169}
170
171#[must_use = "You should always handle the control flow value when it is returned"]
172#[derive(Debug, Clone, PartialEq, Serialize)]
173pub struct KclValueControlFlow {
174    /// Use [control_continue] or [Self::into_value] to get the value.
175    value: KclValue,
176    pub control: ControlFlowKind,
177}
178
179impl KclValue {
180    pub(crate) fn continue_(self) -> KclValueControlFlow {
181        KclValueControlFlow {
182            value: self,
183            control: ControlFlowKind::Continue,
184        }
185    }
186
187    pub(crate) fn exit(self) -> KclValueControlFlow {
188        KclValueControlFlow {
189            value: self,
190            control: ControlFlowKind::Exit,
191        }
192    }
193}
194
195impl KclValueControlFlow {
196    /// Returns true if this is any kind of early return.
197    pub fn is_some_return(&self) -> bool {
198        self.control.is_some_return()
199    }
200
201    pub(crate) fn into_value(self) -> KclValue {
202        self.value
203    }
204}
205
206/// A [`KclValueControlFlow`] or an error that needs to be returned early. This
207/// is useful for when functions might encounter either control flow or errors
208/// that need to bubble up early, but these aren't the primary return values of
209/// the function. We can use `EarlyReturn` as the error type in a `Result`.
210///
211/// Normally, you don't construct this directly. Use the `early_return!` macro.
212#[must_use = "You should always handle the control flow value when it is returned"]
213#[derive(Debug, Clone)]
214pub(crate) enum EarlyReturn {
215    /// A normal value with control flow.
216    Value(KclValueControlFlow),
217    /// An error that occurred during execution.
218    Error(KclError),
219}
220
221impl From<KclValueControlFlow> for EarlyReturn {
222    fn from(cf: KclValueControlFlow) -> Self {
223        EarlyReturn::Value(cf)
224    }
225}
226
227impl From<KclError> for EarlyReturn {
228    fn from(err: KclError) -> Self {
229        EarlyReturn::Error(err)
230    }
231}
232
233pub(crate) enum StatementKind<'a> {
234    Declaration { name: &'a str },
235    Expression,
236}
237
238#[derive(Debug, Clone, Copy)]
239pub enum PreserveMem {
240    Normal,
241    Always,
242}
243
244impl PreserveMem {
245    fn normal(self) -> bool {
246        match self {
247            PreserveMem::Normal => true,
248            PreserveMem::Always => false,
249        }
250    }
251}
252
253/// Outcome of executing a program.  This is used in TS.
254#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
255#[ts(export)]
256#[serde(rename_all = "camelCase")]
257pub struct ExecOutcome {
258    /// Variables in the top-level of the root module. Note that functions will have an invalid env ref.
259    pub variables: IndexMap<String, KclValue>,
260    /// Operations that have been performed in execution order, for display in
261    /// the Feature Tree.
262    #[cfg(feature = "artifact-graph")]
263    pub operations: Vec<Operation>,
264    /// Output artifact graph.
265    #[cfg(feature = "artifact-graph")]
266    pub artifact_graph: ArtifactGraph,
267    /// Objects in the scene, created from execution.
268    #[cfg(feature = "artifact-graph")]
269    #[serde(skip)]
270    pub scene_objects: Vec<Object>,
271    /// Map from source range to object ID for lookup of objects by their source
272    /// range.
273    #[cfg(feature = "artifact-graph")]
274    #[serde(skip)]
275    pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
276    #[cfg(feature = "artifact-graph")]
277    #[serde(skip)]
278    pub var_solutions: Vec<(SourceRange, Number)>,
279    /// Non-fatal errors and warnings.
280    /// TODO: Rename this since it's confusing that it contains warnings.
281    pub errors: Vec<CompilationError>,
282    /// File Names in module Id array index order
283    pub filenames: IndexMap<ModuleId, ModulePath>,
284    /// The default planes.
285    pub default_planes: Option<DefaultPlanes>,
286}
287
288impl ExecOutcome {
289    pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
290        #[cfg(feature = "artifact-graph")]
291        {
292            debug_assert!(
293                id.0 < self.scene_objects.len(),
294                "Requested object ID {} but only have {} objects",
295                id.0,
296                self.scene_objects.len()
297            );
298            self.scene_objects.get(id.0)
299        }
300        #[cfg(not(feature = "artifact-graph"))]
301        {
302            let _ = id;
303            None
304        }
305    }
306
307    /// Returns non-fatal errors. Warnings are not included.
308    pub fn actual_errors(&self) -> impl Iterator<Item = &CompilationError> {
309        self.errors.iter().filter(|error| error.is_err())
310    }
311}
312
313/// Configuration for mock execution.
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct MockConfig {
316    pub use_prev_memory: bool,
317    /// The `ObjectId` of the sketch block to execute for sketch mode. Only the
318    /// specified sketch block will be executed. All other code is ignored.
319    pub sketch_block_id: Option<ObjectId>,
320    /// True to do more costly analysis of whether the sketch block segments are
321    /// under-constrained.
322    pub freedom_analysis: bool,
323    /// The segments that were edited that triggered this execution.
324    #[cfg(feature = "artifact-graph")]
325    pub segment_ids_edited: AhashIndexSet<ObjectId>,
326}
327
328impl Default for MockConfig {
329    fn default() -> Self {
330        Self {
331            // By default, use previous memory. This is usually what you want.
332            use_prev_memory: true,
333            sketch_block_id: None,
334            freedom_analysis: true,
335            #[cfg(feature = "artifact-graph")]
336            segment_ids_edited: AhashIndexSet::default(),
337        }
338    }
339}
340
341impl MockConfig {
342    /// Create a new mock config for sketch mode.
343    pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
344        Self {
345            sketch_block_id: Some(sketch_block_id),
346            ..Default::default()
347        }
348    }
349
350    #[must_use]
351    pub(crate) fn no_freedom_analysis(mut self) -> Self {
352        self.freedom_analysis = false;
353        self
354    }
355}
356
357#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
358#[ts(export)]
359#[serde(rename_all = "camelCase")]
360pub struct DefaultPlanes {
361    pub xy: uuid::Uuid,
362    pub xz: uuid::Uuid,
363    pub yz: uuid::Uuid,
364    pub neg_xy: uuid::Uuid,
365    pub neg_xz: uuid::Uuid,
366    pub neg_yz: uuid::Uuid,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
370#[ts(export)]
371#[serde(tag = "type", rename_all = "camelCase")]
372pub struct TagIdentifier {
373    pub value: String,
374    // Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info
375    // was written.
376    #[serde(skip)]
377    pub info: Vec<(usize, TagEngineInfo)>,
378    #[serde(skip)]
379    pub meta: Vec<Metadata>,
380}
381
382impl TagIdentifier {
383    /// Get the tag info for this tag at a specified epoch.
384    pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
385        for (e, info) in self.info.iter().rev() {
386            if *e <= at_epoch {
387                return Some(info);
388            }
389        }
390
391        None
392    }
393
394    /// Get the most recent tag info for this tag.
395    pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
396        self.info.last().map(|i| &i.1)
397    }
398
399    /// Get all tag info entries at the most recent epoch.
400    /// For region-mapped tags, this returns multiple entries (one per region segment).
401    pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
402        let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
403            return vec![];
404        };
405        self.info
406            .iter()
407            .rev()
408            .take_while(|(e, _)| *e == cur_epoch)
409            .map(|(_, info)| info)
410            .collect()
411    }
412
413    /// Add info from a different instance of this tag.
414    pub fn merge_info(&mut self, other: &TagIdentifier) {
415        assert_eq!(&self.value, &other.value);
416        for (oe, ot) in &other.info {
417            if let Some((e, t)) = self.info.last_mut() {
418                // If there is newer info, then skip this iteration.
419                if *e > *oe {
420                    continue;
421                }
422                // If we're in the same epoch, then overwrite.
423                if e == oe {
424                    *t = ot.clone();
425                    continue;
426                }
427            }
428            self.info.push((*oe, ot.clone()));
429        }
430    }
431
432    pub fn geometry(&self) -> Option<Geometry> {
433        self.get_cur_info().map(|info| info.geometry.clone())
434    }
435}
436
437impl Eq for TagIdentifier {}
438
439impl std::fmt::Display for TagIdentifier {
440    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441        write!(f, "{}", self.value)
442    }
443}
444
445impl std::str::FromStr for TagIdentifier {
446    type Err = KclError;
447
448    fn from_str(s: &str) -> Result<Self, Self::Err> {
449        Ok(Self {
450            value: s.to_string(),
451            info: Vec::new(),
452            meta: Default::default(),
453        })
454    }
455}
456
457impl Ord for TagIdentifier {
458    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
459        self.value.cmp(&other.value)
460    }
461}
462
463impl PartialOrd for TagIdentifier {
464    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
465        Some(self.cmp(other))
466    }
467}
468
469impl std::hash::Hash for TagIdentifier {
470    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
471        self.value.hash(state);
472    }
473}
474
475/// Engine information for a tag.
476#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
477#[ts(export)]
478#[serde(tag = "type", rename_all = "camelCase")]
479pub struct TagEngineInfo {
480    /// The id of the tagged object.
481    pub id: uuid::Uuid,
482    /// The geometry the tag is on.
483    pub geometry: Geometry,
484    /// The path the tag is on.
485    pub path: Option<Path>,
486    /// The surface information for the tag.
487    pub surface: Option<ExtrudeSurface>,
488}
489
490#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
491pub enum BodyType {
492    Root,
493    Block,
494}
495
496/// Metadata.
497#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
498#[ts(export)]
499#[serde(rename_all = "camelCase")]
500pub struct Metadata {
501    /// The source range.
502    pub source_range: SourceRange,
503}
504
505impl From<Metadata> for Vec<SourceRange> {
506    fn from(meta: Metadata) -> Self {
507        vec![meta.source_range]
508    }
509}
510
511impl From<&Metadata> for SourceRange {
512    fn from(meta: &Metadata) -> Self {
513        meta.source_range
514    }
515}
516
517impl From<SourceRange> for Metadata {
518    fn from(source_range: SourceRange) -> Self {
519        Self { source_range }
520    }
521}
522
523impl<T> From<NodeRef<'_, T>> for Metadata {
524    fn from(node: NodeRef<'_, T>) -> Self {
525        Self {
526            source_range: SourceRange::new(node.start, node.end, node.module_id),
527        }
528    }
529}
530
531impl From<&Expr> for Metadata {
532    fn from(expr: &Expr) -> Self {
533        Self {
534            source_range: SourceRange::from(expr),
535        }
536    }
537}
538
539impl Metadata {
540    pub fn to_source_ref(meta: &[Metadata]) -> crate::front::SourceRef {
541        if meta.len() == 1 {
542            let meta = &meta[0];
543            return crate::front::SourceRef::Simple {
544                range: meta.source_range,
545            };
546        }
547        crate::front::SourceRef::BackTrace {
548            ranges: meta.iter().map(|m| m.source_range).collect(),
549        }
550    }
551}
552
553/// The type of ExecutorContext being used
554#[derive(PartialEq, Debug, Default, Clone)]
555pub enum ContextType {
556    /// Live engine connection
557    #[default]
558    Live,
559
560    /// Completely mocked connection
561    /// Mock mode is only for the Design Studio when they just want to mock engine calls and not
562    /// actually make them.
563    Mock,
564
565    /// Handled by some other interpreter/conversion system
566    MockCustomForwarded,
567}
568
569/// The executor context.
570/// Cloning will return another handle to the same engine connection/session,
571/// as this uses `Arc` under the hood.
572#[derive(Debug, Clone)]
573pub struct ExecutorContext {
574    pub engine: Arc<Box<dyn EngineManager>>,
575    pub fs: Arc<FileManager>,
576    pub settings: ExecutorSettings,
577    pub context_type: ContextType,
578}
579
580/// The executor settings.
581#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
582#[ts(export)]
583pub struct ExecutorSettings {
584    /// Highlight edges of 3D objects?
585    pub highlight_edges: bool,
586    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
587    pub enable_ssao: bool,
588    /// Show grid?
589    pub show_grid: bool,
590    /// Should engine store this for replay?
591    /// If so, under what name?
592    pub replay: Option<String>,
593    /// The directory of the current project.  This is used for resolving import
594    /// paths.  If None is given, the current working directory is used.
595    pub project_directory: Option<TypedPath>,
596    /// This is the path to the current file being executed.
597    /// We use this for preventing cyclic imports.
598    pub current_file: Option<TypedPath>,
599    /// Whether or not to automatically scale the grid when user zooms.
600    pub fixed_size_grid: bool,
601}
602
603impl Default for ExecutorSettings {
604    fn default() -> Self {
605        Self {
606            highlight_edges: true,
607            enable_ssao: false,
608            show_grid: false,
609            replay: None,
610            project_directory: None,
611            current_file: None,
612            fixed_size_grid: true,
613        }
614    }
615}
616
617impl From<crate::settings::types::Configuration> for ExecutorSettings {
618    fn from(config: crate::settings::types::Configuration) -> Self {
619        Self::from(config.settings)
620    }
621}
622
623impl From<crate::settings::types::Settings> for ExecutorSettings {
624    fn from(settings: crate::settings::types::Settings) -> Self {
625        Self {
626            highlight_edges: settings.modeling.highlight_edges.into(),
627            enable_ssao: settings.modeling.enable_ssao.into(),
628            show_grid: settings.modeling.show_scale_grid,
629            replay: None,
630            project_directory: None,
631            current_file: None,
632            fixed_size_grid: settings.modeling.fixed_size_grid,
633        }
634    }
635}
636
637impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
638    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
639        Self::from(config.settings.modeling)
640    }
641}
642
643impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
644    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
645        Self {
646            highlight_edges: modeling.highlight_edges.into(),
647            enable_ssao: modeling.enable_ssao.into(),
648            show_grid: modeling.show_scale_grid,
649            replay: None,
650            project_directory: None,
651            current_file: None,
652            fixed_size_grid: true,
653        }
654    }
655}
656
657impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
658    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
659        Self {
660            highlight_edges: modeling.highlight_edges.into(),
661            enable_ssao: modeling.enable_ssao.into(),
662            show_grid: Default::default(),
663            replay: None,
664            project_directory: None,
665            current_file: None,
666            fixed_size_grid: true,
667        }
668    }
669}
670
671impl ExecutorSettings {
672    /// Add the current file path to the executor settings.
673    pub fn with_current_file(&mut self, current_file: TypedPath) {
674        // We want the parent directory of the file.
675        if current_file.extension() == Some("kcl") {
676            self.current_file = Some(current_file.clone());
677            // Get the parent directory.
678            if let Some(parent) = current_file.parent() {
679                self.project_directory = Some(parent);
680            } else {
681                self.project_directory = Some(TypedPath::from(""));
682            }
683        } else {
684            self.project_directory = Some(current_file);
685        }
686    }
687}
688
689impl ExecutorContext {
690    /// Create a new live executor context from an engine and file manager.
691    pub fn new_with_engine_and_fs(
692        engine: Arc<Box<dyn EngineManager>>,
693        fs: Arc<FileManager>,
694        settings: ExecutorSettings,
695    ) -> Self {
696        ExecutorContext {
697            engine,
698            fs,
699            settings,
700            context_type: ContextType::Live,
701        }
702    }
703
704    /// Create a new live executor context from an engine using the local file manager.
705    #[cfg(not(target_arch = "wasm32"))]
706    pub fn new_with_engine(engine: Arc<Box<dyn EngineManager>>, settings: ExecutorSettings) -> Self {
707        Self::new_with_engine_and_fs(engine, Arc::new(FileManager::new()), settings)
708    }
709
710    /// Create a new default executor context.
711    #[cfg(not(target_arch = "wasm32"))]
712    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
713        let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
714        let (ws, _headers) = client
715            .modeling()
716            .commands_ws(kittycad::modeling::CommandsWsParams {
717                api_call_id: None,
718                fps: None,
719                order_independent_transparency: None,
720                post_effect: if settings.enable_ssao {
721                    Some(kittycad::types::PostEffectType::Ssao)
722                } else {
723                    None
724                },
725                replay: settings.replay.clone(),
726                show_grid: if settings.show_grid { Some(true) } else { None },
727                pool: None,
728                pr,
729                unlocked_framerate: None,
730                webrtc: Some(false),
731                video_res_width: None,
732                video_res_height: None,
733            })
734            .await?;
735
736        let engine: Arc<Box<dyn EngineManager>> =
737            Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
738
739        Ok(Self::new_with_engine(engine, settings))
740    }
741
742    #[cfg(target_arch = "wasm32")]
743    pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
744        Self::new_with_engine_and_fs(engine, fs, settings)
745    }
746
747    #[cfg(not(target_arch = "wasm32"))]
748    pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
749        ExecutorContext {
750            engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
751            fs: Arc::new(FileManager::new()),
752            settings: settings.unwrap_or_default(),
753            context_type: ContextType::Mock,
754        }
755    }
756
757    #[cfg(target_arch = "wasm32")]
758    pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
759        ExecutorContext {
760            engine,
761            fs,
762            settings,
763            context_type: ContextType::Mock,
764        }
765    }
766
767    /// Create a new mock executor context for WASM LSP servers.
768    /// This is a convenience function that creates a mock engine and FileManager from a FileSystemManager.
769    #[cfg(target_arch = "wasm32")]
770    pub fn new_mock_for_lsp(
771        fs_manager: crate::fs::wasm::FileSystemManager,
772        settings: ExecutorSettings,
773    ) -> Result<Self, String> {
774        use crate::mock_engine;
775
776        let mock_engine = Arc::new(Box::new(
777            mock_engine::EngineConnection::new().map_err(|e| format!("Failed to create mock engine: {:?}", e))?,
778        ) as Box<dyn EngineManager>);
779
780        let fs = Arc::new(FileManager::new(fs_manager));
781
782        Ok(ExecutorContext {
783            engine: mock_engine,
784            fs,
785            settings,
786            context_type: ContextType::Mock,
787        })
788    }
789
790    #[cfg(not(target_arch = "wasm32"))]
791    pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
792        ExecutorContext {
793            engine,
794            fs: Arc::new(FileManager::new()),
795            settings: Default::default(),
796            context_type: ContextType::MockCustomForwarded,
797        }
798    }
799
800    /// Create a new default executor context.
801    /// With a kittycad client.
802    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
803    /// variables.
804    /// But also allows for passing in a token and engine address directly.
805    #[cfg(not(target_arch = "wasm32"))]
806    pub async fn new_with_client(
807        settings: ExecutorSettings,
808        token: Option<String>,
809        engine_addr: Option<String>,
810    ) -> Result<Self> {
811        // Create the client.
812        let client = crate::engine::new_zoo_client(token, engine_addr)?;
813
814        let ctx = Self::new(&client, settings).await?;
815        Ok(ctx)
816    }
817
818    /// Create a new default executor context.
819    /// With the default kittycad client.
820    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
821    /// variables.
822    #[cfg(not(target_arch = "wasm32"))]
823    pub async fn new_with_default_client() -> Result<Self> {
824        // Create the client.
825        let ctx = Self::new_with_client(Default::default(), None, None).await?;
826        Ok(ctx)
827    }
828
829    /// For executing unit tests.
830    #[cfg(not(target_arch = "wasm32"))]
831    pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
832        let ctx = ExecutorContext::new_with_client(
833            ExecutorSettings {
834                highlight_edges: true,
835                enable_ssao: false,
836                show_grid: false,
837                replay: None,
838                project_directory: None,
839                current_file: None,
840                fixed_size_grid: false,
841            },
842            None,
843            engine_addr,
844        )
845        .await?;
846        Ok(ctx)
847    }
848
849    pub fn is_mock(&self) -> bool {
850        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
851    }
852
853    /// Returns true if we should not send engine commands for any reason.
854    pub async fn no_engine_commands(&self) -> bool {
855        self.is_mock()
856    }
857
858    pub async fn send_clear_scene(
859        &self,
860        exec_state: &mut ExecState,
861        source_range: crate::execution::SourceRange,
862    ) -> Result<(), KclError> {
863        // Ensure artifacts are cleared so that we don't accumulate them across
864        // runs.
865        exec_state.mod_local.artifacts.clear();
866        exec_state.global.root_module_artifacts.clear();
867        exec_state.global.artifacts.clear();
868
869        self.engine
870            .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
871            .await?;
872        // The engine errors out if you toggle OIT with SSAO off.
873        // So ignore OIT settings if SSAO is off.
874        if self.settings.enable_ssao {
875            let cmd_id = exec_state.next_uuid();
876            exec_state
877                .batch_modeling_cmd(
878                    ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
879                    ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
880                )
881                .await?;
882        }
883        Ok(())
884    }
885
886    pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
887        cache::bust_cache().await;
888
889        // Execute an empty program to clear and reset the scene.
890        // We specifically want to be returned the objects after the scene is reset.
891        // Like the default planes so it is easier to just execute an empty program
892        // after the cache is busted.
893        let outcome = self.run_with_caching(crate::Program::empty()).await?;
894
895        Ok(outcome)
896    }
897
898    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
899        self.eval_prelude(exec_state, SourceRange::synthetic())
900            .await
901            .map_err(KclErrorWithOutputs::no_outputs)?;
902        exec_state.mut_stack().push_new_root_env(true);
903        Ok(())
904    }
905
906    pub async fn run_mock(
907        &self,
908        program: &crate::Program,
909        mock_config: &MockConfig,
910    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
911        assert!(
912            self.is_mock(),
913            "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
914        );
915
916        let use_prev_memory = mock_config.use_prev_memory;
917        let mut exec_state = ExecState::new_mock(self, mock_config);
918        if use_prev_memory {
919            match cache::read_old_memory().await {
920                Some(mem) => {
921                    *exec_state.mut_stack() = mem.stack;
922                    exec_state.global.module_infos = mem.module_infos;
923                    #[cfg(feature = "artifact-graph")]
924                    {
925                        let len = mock_config
926                            .sketch_block_id
927                            .map(|sketch_block_id| sketch_block_id.0)
928                            .unwrap_or(0);
929                        if let Some(scene_objects) = mem.scene_objects.get(0..len) {
930                            exec_state
931                                .global
932                                .root_module_artifacts
933                                .restore_scene_objects(scene_objects);
934                        } else {
935                            let message = format!(
936                                "Cached scene objects length {} is less than expected length from cached object ID generator {}",
937                                mem.scene_objects.len(),
938                                len
939                            );
940                            debug_assert!(false, "{message}");
941                            return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
942                                KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
943                            )));
944                        }
945                    }
946                }
947                None => self.prepare_mem(&mut exec_state).await?,
948            }
949        } else {
950            self.prepare_mem(&mut exec_state).await?
951        };
952
953        // Push a scope so that old variables can be overwritten (since we might be re-executing some
954        // part of the scene).
955        exec_state.mut_stack().push_new_env_for_scope();
956
957        let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
958
959        // Restore any temporary variables, then save any newly created variables back to
960        // memory in case another run wants to use them. Note this is just saved to the preserved
961        // memory, not to the exec_state which is not cached for mock execution.
962
963        let mut stack = exec_state.stack().clone();
964        let module_infos = exec_state.global.module_infos.clone();
965        #[cfg(feature = "artifact-graph")]
966        let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
967        #[cfg(not(feature = "artifact-graph"))]
968        let scene_objects = Default::default();
969        let outcome = exec_state.into_exec_outcome(result.0, self).await;
970
971        stack.squash_env(result.0);
972        let state = cache::SketchModeState {
973            stack,
974            module_infos,
975            scene_objects,
976        };
977        cache::write_old_memory(state).await;
978
979        Ok(outcome)
980    }
981
982    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
983        assert!(!self.is_mock());
984        let grid_scale = if self.settings.fixed_size_grid {
985            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
986        } else {
987            GridScaleBehavior::ScaleWithZoom
988        };
989
990        let original_program = program.clone();
991
992        let (_program, exec_state, result) = match cache::read_old_ast().await {
993            Some(mut cached_state) => {
994                let old = CacheInformation {
995                    ast: &cached_state.main.ast,
996                    settings: &cached_state.settings,
997                };
998                let new = CacheInformation {
999                    ast: &program.ast,
1000                    settings: &self.settings,
1001                };
1002
1003                // Get the program that actually changed from the old and new information.
1004                let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1005                    CacheResult::ReExecute {
1006                        clear_scene,
1007                        reapply_settings,
1008                        program: changed_program,
1009                    } => {
1010                        if reapply_settings
1011                            && self
1012                                .engine
1013                                .reapply_settings(
1014                                    &self.settings,
1015                                    Default::default(),
1016                                    &mut cached_state.main.exec_state.id_generator,
1017                                    grid_scale,
1018                                )
1019                                .await
1020                                .is_err()
1021                        {
1022                            (true, program, None)
1023                        } else {
1024                            (
1025                                clear_scene,
1026                                crate::Program {
1027                                    ast: changed_program,
1028                                    original_file_contents: program.original_file_contents,
1029                                },
1030                                None,
1031                            )
1032                        }
1033                    }
1034                    CacheResult::CheckImportsOnly {
1035                        reapply_settings,
1036                        ast: changed_program,
1037                    } => {
1038                        let mut reapply_failed = false;
1039                        if reapply_settings {
1040                            if self
1041                                .engine
1042                                .reapply_settings(
1043                                    &self.settings,
1044                                    Default::default(),
1045                                    &mut cached_state.main.exec_state.id_generator,
1046                                    grid_scale,
1047                                )
1048                                .await
1049                                .is_ok()
1050                            {
1051                                cache::write_old_ast(GlobalState::with_settings(
1052                                    cached_state.clone(),
1053                                    self.settings.clone(),
1054                                ))
1055                                .await;
1056                            } else {
1057                                reapply_failed = true;
1058                            }
1059                        }
1060
1061                        if reapply_failed {
1062                            (true, program, None)
1063                        } else {
1064                            // We need to check our imports to see if they changed.
1065                            let mut new_exec_state = ExecState::new(self);
1066                            let (new_universe, new_universe_map) =
1067                                self.get_universe(&program, &mut new_exec_state).await?;
1068
1069                            let clear_scene = new_universe.values().any(|value| {
1070                                let id = value.1;
1071                                match (
1072                                    cached_state.exec_state.get_source(id),
1073                                    new_exec_state.global.get_source(id),
1074                                ) {
1075                                    (Some(s0), Some(s1)) => s0.source != s1.source,
1076                                    _ => false,
1077                                }
1078                            });
1079
1080                            if !clear_scene {
1081                                // Return early we don't need to clear the scene.
1082                                return Ok(cached_state.into_exec_outcome(self).await);
1083                            }
1084
1085                            (
1086                                true,
1087                                crate::Program {
1088                                    ast: changed_program,
1089                                    original_file_contents: program.original_file_contents,
1090                                },
1091                                Some((new_universe, new_universe_map, new_exec_state)),
1092                            )
1093                        }
1094                    }
1095                    CacheResult::NoAction(true) => {
1096                        if self
1097                            .engine
1098                            .reapply_settings(
1099                                &self.settings,
1100                                Default::default(),
1101                                &mut cached_state.main.exec_state.id_generator,
1102                                grid_scale,
1103                            )
1104                            .await
1105                            .is_ok()
1106                        {
1107                            // We need to update the old ast state with the new settings!!
1108                            cache::write_old_ast(GlobalState::with_settings(
1109                                cached_state.clone(),
1110                                self.settings.clone(),
1111                            ))
1112                            .await;
1113
1114                            return Ok(cached_state.into_exec_outcome(self).await);
1115                        }
1116                        (true, program, None)
1117                    }
1118                    CacheResult::NoAction(false) => {
1119                        return Ok(cached_state.into_exec_outcome(self).await);
1120                    }
1121                };
1122
1123                let (exec_state, result) = match import_check_info {
1124                    Some((new_universe, new_universe_map, mut new_exec_state)) => {
1125                        // Clear the scene if the imports changed.
1126                        self.send_clear_scene(&mut new_exec_state, Default::default())
1127                            .await
1128                            .map_err(KclErrorWithOutputs::no_outputs)?;
1129
1130                        let result = self
1131                            .run_concurrent(
1132                                &program,
1133                                &mut new_exec_state,
1134                                Some((new_universe, new_universe_map)),
1135                                PreserveMem::Normal,
1136                            )
1137                            .await;
1138
1139                        (new_exec_state, result)
1140                    }
1141                    None if clear_scene => {
1142                        // Pop the execution state, since we are starting fresh.
1143                        let mut exec_state = cached_state.reconstitute_exec_state();
1144                        exec_state.reset(self);
1145
1146                        self.send_clear_scene(&mut exec_state, Default::default())
1147                            .await
1148                            .map_err(KclErrorWithOutputs::no_outputs)?;
1149
1150                        let result = self
1151                            .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1152                            .await;
1153
1154                        (exec_state, result)
1155                    }
1156                    None => {
1157                        let mut exec_state = cached_state.reconstitute_exec_state();
1158                        exec_state.mut_stack().restore_env(cached_state.main.result_env);
1159
1160                        let result = self
1161                            .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1162                            .await;
1163
1164                        (exec_state, result)
1165                    }
1166                };
1167
1168                (program, exec_state, result)
1169            }
1170            None => {
1171                let mut exec_state = ExecState::new(self);
1172                self.send_clear_scene(&mut exec_state, Default::default())
1173                    .await
1174                    .map_err(KclErrorWithOutputs::no_outputs)?;
1175
1176                let result = self
1177                    .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1178                    .await;
1179
1180                (program, exec_state, result)
1181            }
1182        };
1183
1184        if result.is_err() {
1185            cache::bust_cache().await;
1186        }
1187
1188        // Throw the error.
1189        let result = result?;
1190
1191        // Save this as the last successful execution to the cache.
1192        // Gotcha: `CacheResult::ReExecute.program` may be diff-based, do not save that AST
1193        // the last-successful AST. Instead, save in the full AST passed in.
1194        cache::write_old_ast(GlobalState::new(
1195            exec_state.clone(),
1196            self.settings.clone(),
1197            original_program.ast,
1198            result.0,
1199        ))
1200        .await;
1201
1202        let outcome = exec_state.into_exec_outcome(result.0, self).await;
1203        Ok(outcome)
1204    }
1205
1206    /// Perform the execution of a program.
1207    ///
1208    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
1209    pub async fn run(
1210        &self,
1211        program: &crate::Program,
1212        exec_state: &mut ExecState,
1213    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1214        self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1215            .await
1216    }
1217
1218    /// Perform the execution of a program using a concurrent
1219    /// execution model.
1220    ///
1221    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
1222    pub async fn run_concurrent(
1223        &self,
1224        program: &crate::Program,
1225        exec_state: &mut ExecState,
1226        universe_info: Option<(Universe, UniverseMap)>,
1227        preserve_mem: PreserveMem,
1228    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1229        // Reuse our cached universe if we have one.
1230
1231        let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1232            (universe, universe_map)
1233        } else {
1234            self.get_universe(program, exec_state).await?
1235        };
1236
1237        let default_planes = self.engine.get_default_planes().read().await.clone();
1238
1239        // Run the prelude to set up the engine.
1240        self.eval_prelude(exec_state, SourceRange::synthetic())
1241            .await
1242            .map_err(KclErrorWithOutputs::no_outputs)?;
1243
1244        for modules in import_graph::import_graph(&universe, self)
1245            .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1246            .into_iter()
1247        {
1248            #[cfg(not(target_arch = "wasm32"))]
1249            let mut set = tokio::task::JoinSet::new();
1250
1251            #[allow(clippy::type_complexity)]
1252            let (results_tx, mut results_rx): (
1253                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1254                tokio::sync::mpsc::Receiver<_>,
1255            ) = tokio::sync::mpsc::channel(1);
1256
1257            for module in modules {
1258                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1259                    return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1260                        KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1261                    )));
1262                };
1263                let module_id = *module_id;
1264                let module_path = module_path.clone();
1265                let source_range = SourceRange::from(import_stmt);
1266                // Clone before mutating.
1267                let module_exec_state = exec_state.clone();
1268
1269                self.add_import_module_ops(
1270                    exec_state,
1271                    &program.ast,
1272                    module_id,
1273                    &module_path,
1274                    source_range,
1275                    &universe_map,
1276                );
1277
1278                let repr = repr.clone();
1279                let exec_ctxt = self.clone();
1280                let results_tx = results_tx.clone();
1281
1282                let exec_module = async |exec_ctxt: &ExecutorContext,
1283                                         repr: &ModuleRepr,
1284                                         module_id: ModuleId,
1285                                         module_path: &ModulePath,
1286                                         exec_state: &mut ExecState,
1287                                         source_range: SourceRange|
1288                       -> Result<ModuleRepr, KclError> {
1289                    match repr {
1290                        ModuleRepr::Kcl(program, _) => {
1291                            let result = exec_ctxt
1292                                .exec_module_from_ast(
1293                                    program,
1294                                    module_id,
1295                                    module_path,
1296                                    exec_state,
1297                                    source_range,
1298                                    PreserveMem::Normal,
1299                                )
1300                                .await;
1301
1302                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1303                        }
1304                        ModuleRepr::Foreign(geom, _) => {
1305                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1306                                .await
1307                                .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1308
1309                            result.map(|val| {
1310                                ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
1311                            })
1312                        }
1313                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1314                            format!("Module {module_path} not found in universe"),
1315                            vec![source_range],
1316                        ))),
1317                    }
1318                };
1319
1320                #[cfg(target_arch = "wasm32")]
1321                {
1322                    wasm_bindgen_futures::spawn_local(async move {
1323                        let mut exec_state = module_exec_state;
1324                        let exec_ctxt = exec_ctxt;
1325
1326                        let result = exec_module(
1327                            &exec_ctxt,
1328                            &repr,
1329                            module_id,
1330                            &module_path,
1331                            &mut exec_state,
1332                            source_range,
1333                        )
1334                        .await;
1335
1336                        results_tx
1337                            .send((module_id, module_path, result))
1338                            .await
1339                            .unwrap_or_default();
1340                    });
1341                }
1342                #[cfg(not(target_arch = "wasm32"))]
1343                {
1344                    set.spawn(async move {
1345                        let mut exec_state = module_exec_state;
1346                        let exec_ctxt = exec_ctxt;
1347
1348                        let result = exec_module(
1349                            &exec_ctxt,
1350                            &repr,
1351                            module_id,
1352                            &module_path,
1353                            &mut exec_state,
1354                            source_range,
1355                        )
1356                        .await;
1357
1358                        results_tx
1359                            .send((module_id, module_path, result))
1360                            .await
1361                            .unwrap_or_default();
1362                    });
1363                }
1364            }
1365
1366            drop(results_tx);
1367
1368            while let Some((module_id, _, result)) = results_rx.recv().await {
1369                match result {
1370                    Ok(new_repr) => {
1371                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1372
1373                        match &mut repr {
1374                            ModuleRepr::Kcl(_, cache) => {
1375                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
1376                                    unreachable!();
1377                                };
1378                                *cache = session_data;
1379                            }
1380                            ModuleRepr::Foreign(_, cache) => {
1381                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
1382                                    unreachable!();
1383                                };
1384                                *cache = session_data;
1385                            }
1386                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1387                        }
1388
1389                        exec_state.global.module_infos[&module_id].restore_repr(repr);
1390                    }
1391                    Err(e) => {
1392                        return Err(exec_state.error_with_outputs(e, None, default_planes));
1393                    }
1394                }
1395            }
1396        }
1397
1398        // Since we haven't technically started executing the root module yet,
1399        // the operations corresponding to the imports will be missing unless we
1400        // track them here.
1401        exec_state
1402            .global
1403            .root_module_artifacts
1404            .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1405
1406        self.inner_run(program, exec_state, preserve_mem).await
1407    }
1408
1409    /// Get the universe & universe map of the program.
1410    /// And see if any of the imports changed.
1411    async fn get_universe(
1412        &self,
1413        program: &crate::Program,
1414        exec_state: &mut ExecState,
1415    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1416        exec_state.add_root_module_contents(program);
1417
1418        let mut universe = std::collections::HashMap::new();
1419
1420        let default_planes = self.engine.get_default_planes().read().await.clone();
1421
1422        let root_imports = import_graph::import_universe(
1423            self,
1424            &ModulePath::Main,
1425            &ModuleRepr::Kcl(program.ast.clone(), None),
1426            &mut universe,
1427            exec_state,
1428        )
1429        .await
1430        .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1431
1432        Ok((universe, root_imports))
1433    }
1434
1435    #[cfg(not(feature = "artifact-graph"))]
1436    fn add_import_module_ops(
1437        &self,
1438        _exec_state: &mut ExecState,
1439        _program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1440        _module_id: ModuleId,
1441        _module_path: &ModulePath,
1442        _source_range: SourceRange,
1443        _universe_map: &UniverseMap,
1444    ) {
1445    }
1446
1447    #[cfg(feature = "artifact-graph")]
1448    fn add_import_module_ops(
1449        &self,
1450        exec_state: &mut ExecState,
1451        program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1452        module_id: ModuleId,
1453        module_path: &ModulePath,
1454        source_range: SourceRange,
1455        universe_map: &UniverseMap,
1456    ) {
1457        match module_path {
1458            ModulePath::Main => {
1459                // This should never happen.
1460            }
1461            ModulePath::Local {
1462                value,
1463                original_import_path,
1464            } => {
1465                // We only want to display the top-level module imports in
1466                // the Feature Tree, not transitive imports.
1467                if universe_map.contains_key(value) {
1468                    use crate::NodePath;
1469
1470                    let node_path = if source_range.is_top_level_module() {
1471                        let cached_body_items = exec_state.global.artifacts.cached_body_items();
1472                        NodePath::from_range(
1473                            &exec_state.build_program_lookup(program.clone()),
1474                            cached_body_items,
1475                            source_range,
1476                        )
1477                        .unwrap_or_default()
1478                    } else {
1479                        // The frontend doesn't care about paths in
1480                        // files other than the top-level module.
1481                        NodePath::placeholder()
1482                    };
1483
1484                    let name = match original_import_path {
1485                        Some(value) => value.to_string_lossy(),
1486                        None => value.file_name().unwrap_or_default(),
1487                    };
1488                    exec_state.push_op(Operation::GroupBegin {
1489                        group: Group::ModuleInstance { name, module_id },
1490                        node_path,
1491                        source_range,
1492                    });
1493                    // Due to concurrent execution, we cannot easily
1494                    // group operations by module. So we leave the
1495                    // group empty and close it immediately.
1496                    exec_state.push_op(Operation::GroupEnd);
1497                }
1498            }
1499            ModulePath::Std { .. } => {
1500                // We don't want to display stdlib in the Feature Tree.
1501            }
1502        }
1503    }
1504
1505    /// Perform the execution of a program.  Accept all possible parameters and
1506    /// output everything.
1507    async fn inner_run(
1508        &self,
1509        program: &crate::Program,
1510        exec_state: &mut ExecState,
1511        preserve_mem: PreserveMem,
1512    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1513        let _stats = crate::log::LogPerfStats::new("Interpretation");
1514
1515        // Re-apply the settings, in case the cache was busted.
1516        let grid_scale = if self.settings.fixed_size_grid {
1517            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1518        } else {
1519            GridScaleBehavior::ScaleWithZoom
1520        };
1521        self.engine
1522            .reapply_settings(
1523                &self.settings,
1524                Default::default(),
1525                exec_state.id_generator(),
1526                grid_scale,
1527            )
1528            .await
1529            .map_err(KclErrorWithOutputs::no_outputs)?;
1530
1531        let default_planes = self.engine.get_default_planes().read().await.clone();
1532        let result = self
1533            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1534            .await;
1535
1536        crate::log::log(format!(
1537            "Post interpretation KCL memory stats: {:#?}",
1538            exec_state.stack().memory.stats
1539        ));
1540        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1541
1542        /// Write the memory of an execution to the cache for reuse in mock
1543        /// execution.
1544        async fn write_old_memory(ctx: &ExecutorContext, exec_state: &ExecState, env_ref: EnvironmentRef) {
1545            if ctx.is_mock() {
1546                return;
1547            }
1548            let mut stack = exec_state.stack().deep_clone();
1549            stack.restore_env(env_ref);
1550            let state = cache::SketchModeState {
1551                stack,
1552                module_infos: exec_state.global.module_infos.clone(),
1553                #[cfg(feature = "artifact-graph")]
1554                scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1555                #[cfg(not(feature = "artifact-graph"))]
1556                scene_objects: Default::default(),
1557            };
1558            cache::write_old_memory(state).await;
1559        }
1560
1561        let env_ref = match result {
1562            Ok(env_ref) => env_ref,
1563            Err((err, env_ref)) => {
1564                // Preserve memory on execution failures so follow-up mock
1565                // execution can still reuse stable IDs before the error.
1566                if let Some(env_ref) = env_ref {
1567                    write_old_memory(self, exec_state, env_ref).await;
1568                }
1569                return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
1570            }
1571        };
1572
1573        write_old_memory(self, exec_state, env_ref).await;
1574
1575        let session_data = self.engine.get_session_data().await;
1576
1577        Ok((env_ref, session_data))
1578    }
1579
1580    /// Execute an AST's program and build auxiliary outputs like the artifact
1581    /// graph.
1582    async fn execute_and_build_graph(
1583        &self,
1584        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1585        exec_state: &mut ExecState,
1586        preserve_mem: PreserveMem,
1587    ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1588        // Don't early return!  We need to build other outputs regardless of
1589        // whether execution failed.
1590
1591        // Because of execution caching, we may start with operations from a
1592        // previous run.
1593        #[cfg(feature = "artifact-graph")]
1594        let start_op = exec_state.global.root_module_artifacts.operations.len();
1595
1596        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1597            .await
1598            .map_err(|e| (e, None))?;
1599
1600        let exec_result = self
1601            .exec_module_body(
1602                program,
1603                exec_state,
1604                preserve_mem,
1605                ModuleId::default(),
1606                &ModulePath::Main,
1607            )
1608            .await
1609            .map(
1610                |ModuleExecutionOutcome {
1611                     environment: env_ref,
1612                     artifacts: module_artifacts,
1613                     ..
1614                 }| {
1615                    // We need to extend because it may already have operations from
1616                    // imports.
1617                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1618                    env_ref
1619                },
1620            )
1621            .map_err(|(err, env_ref, module_artifacts)| {
1622                if let Some(module_artifacts) = module_artifacts {
1623                    // We need to extend because it may already have operations
1624                    // from imports.
1625                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1626                }
1627                (err, env_ref)
1628            });
1629
1630        #[cfg(feature = "artifact-graph")]
1631        {
1632            // Fill in NodePath for operations.
1633            let programs = &exec_state.build_program_lookup(program.clone());
1634            let cached_body_items = exec_state.global.artifacts.cached_body_items();
1635            for op in exec_state
1636                .global
1637                .root_module_artifacts
1638                .operations
1639                .iter_mut()
1640                .skip(start_op)
1641            {
1642                op.fill_node_paths(programs, cached_body_items);
1643            }
1644            for module in exec_state.global.module_infos.values_mut() {
1645                if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1646                    for op in &mut outcome.artifacts.operations {
1647                        op.fill_node_paths(programs, cached_body_items);
1648                    }
1649                }
1650            }
1651        }
1652
1653        // Ensure all the async commands completed.
1654        self.engine.ensure_async_commands_completed().await.map_err(|e| {
1655            match &exec_result {
1656                Ok(env_ref) => (e, Some(*env_ref)),
1657                // Prefer the execution error.
1658                Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1659            }
1660        })?;
1661
1662        // If we errored out and early-returned, there might be commands which haven't been executed
1663        // and should be dropped.
1664        self.engine.clear_queues().await;
1665
1666        match exec_state.build_artifact_graph(&self.engine, program).await {
1667            Ok(_) => exec_result,
1668            Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1669        }
1670    }
1671
1672    /// 'Import' std::prelude as the outermost scope.
1673    ///
1674    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
1675    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1676        if exec_state.stack().memory.requires_std() {
1677            #[cfg(feature = "artifact-graph")]
1678            let initial_ops = exec_state.mod_local.artifacts.operations.len();
1679
1680            let path = vec!["std".to_owned(), "prelude".to_owned()];
1681            let resolved_path = ModulePath::from_std_import_path(&path)?;
1682            let id = self
1683                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1684                .await?;
1685            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1686
1687            exec_state.mut_stack().memory.set_std(module_memory);
1688
1689            // Operations generated by the prelude are not useful, so clear them
1690            // out.
1691            //
1692            // TODO: Should we also clear them out of each module so that they
1693            // don't appear in test output?
1694            #[cfg(feature = "artifact-graph")]
1695            exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1696        }
1697
1698        Ok(())
1699    }
1700
1701    /// Get a snapshot of the current scene.
1702    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1703        // Zoom to fit.
1704        self.engine
1705            .send_modeling_cmd(
1706                uuid::Uuid::new_v4(),
1707                crate::execution::SourceRange::default(),
1708                &ModelingCmd::from(
1709                    mcmd::ZoomToFit::builder()
1710                        .object_ids(Default::default())
1711                        .animated(false)
1712                        .padding(0.1)
1713                        .build(),
1714                ),
1715            )
1716            .await
1717            .map_err(KclErrorWithOutputs::no_outputs)?;
1718
1719        // Send a snapshot request to the engine.
1720        let resp = self
1721            .engine
1722            .send_modeling_cmd(
1723                uuid::Uuid::new_v4(),
1724                crate::execution::SourceRange::default(),
1725                &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
1726            )
1727            .await
1728            .map_err(KclErrorWithOutputs::no_outputs)?;
1729
1730        let OkWebSocketResponseData::Modeling {
1731            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1732        } = resp
1733        else {
1734            return Err(ExecError::BadPng(format!(
1735                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1736            )));
1737        };
1738        Ok(contents)
1739    }
1740
1741    /// Export the current scene as a CAD file.
1742    pub async fn export(
1743        &self,
1744        format: kittycad_modeling_cmds::format::OutputFormat3d,
1745    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1746        let resp = self
1747            .engine
1748            .send_modeling_cmd(
1749                uuid::Uuid::new_v4(),
1750                crate::SourceRange::default(),
1751                &kittycad_modeling_cmds::ModelingCmd::Export(
1752                    kittycad_modeling_cmds::Export::builder()
1753                        .entity_ids(vec![])
1754                        .format(format)
1755                        .build(),
1756                ),
1757            )
1758            .await?;
1759
1760        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1761            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1762                format!("Expected Export response, got {resp:?}",),
1763                vec![SourceRange::default()],
1764            )));
1765        };
1766
1767        Ok(files)
1768    }
1769
1770    /// Export the current scene as a STEP file.
1771    pub async fn export_step(
1772        &self,
1773        deterministic_time: bool,
1774    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1775        let files = self
1776            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1777                kittycad_modeling_cmds::format::step::export::Options::builder()
1778                    .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
1779                    .maybe_created(if deterministic_time {
1780                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1781                            KclError::new_internal(crate::errors::KclErrorDetails::new(
1782                                format!("Failed to parse date: {e}"),
1783                                vec![SourceRange::default()],
1784                            ))
1785                        })?)
1786                    } else {
1787                        None
1788                    })
1789                    .build(),
1790            ))
1791            .await?;
1792
1793        Ok(files)
1794    }
1795
1796    pub async fn close(&self) {
1797        self.engine.close().await;
1798    }
1799}
1800
1801#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1802pub struct ArtifactId(Uuid);
1803
1804impl ArtifactId {
1805    pub fn new(uuid: Uuid) -> Self {
1806        Self(uuid)
1807    }
1808
1809    /// A placeholder artifact ID that will be filled in later.
1810    pub fn placeholder() -> Self {
1811        Self(Uuid::nil())
1812    }
1813}
1814
1815impl From<Uuid> for ArtifactId {
1816    fn from(uuid: Uuid) -> Self {
1817        Self::new(uuid)
1818    }
1819}
1820
1821impl From<&Uuid> for ArtifactId {
1822    fn from(uuid: &Uuid) -> Self {
1823        Self::new(*uuid)
1824    }
1825}
1826
1827impl From<ArtifactId> for Uuid {
1828    fn from(id: ArtifactId) -> Self {
1829        id.0
1830    }
1831}
1832
1833impl From<&ArtifactId> for Uuid {
1834    fn from(id: &ArtifactId) -> Self {
1835        id.0
1836    }
1837}
1838
1839impl From<ModelingCmdId> for ArtifactId {
1840    fn from(id: ModelingCmdId) -> Self {
1841        Self::new(*id.as_ref())
1842    }
1843}
1844
1845impl From<&ModelingCmdId> for ArtifactId {
1846    fn from(id: &ModelingCmdId) -> Self {
1847        Self::new(*id.as_ref())
1848    }
1849}
1850
1851#[cfg(test)]
1852pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1853    parse_execute_with_project_dir(code, None).await
1854}
1855
1856#[cfg(test)]
1857pub(crate) async fn parse_execute_with_project_dir(
1858    code: &str,
1859    project_directory: Option<TypedPath>,
1860) -> Result<ExecTestResults, KclError> {
1861    let program = crate::Program::parse_no_errs(code)?;
1862
1863    let exec_ctxt = ExecutorContext {
1864        engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
1865            |err| {
1866                KclError::new_internal(crate::errors::KclErrorDetails::new(
1867                    format!("Failed to create mock engine connection: {err}"),
1868                    vec![SourceRange::default()],
1869                ))
1870            },
1871        )?)),
1872        fs: Arc::new(crate::fs::FileManager::new()),
1873        settings: ExecutorSettings {
1874            project_directory,
1875            ..Default::default()
1876        },
1877        context_type: ContextType::Mock,
1878    };
1879    let mut exec_state = ExecState::new(&exec_ctxt);
1880    let result = exec_ctxt.run(&program, &mut exec_state).await?;
1881
1882    Ok(ExecTestResults {
1883        program,
1884        mem_env: result.0,
1885        exec_ctxt,
1886        exec_state,
1887    })
1888}
1889
1890#[cfg(test)]
1891#[derive(Debug)]
1892pub(crate) struct ExecTestResults {
1893    program: crate::Program,
1894    mem_env: EnvironmentRef,
1895    exec_ctxt: ExecutorContext,
1896    exec_state: ExecState,
1897}
1898
1899/// There are several places where we want to traverse a KCL program or find a symbol in it,
1900/// but because KCL modules can import each other, we need to traverse multiple programs.
1901/// This stores multiple programs, keyed by their module ID for quick access.
1902#[cfg(feature = "artifact-graph")]
1903pub struct ProgramLookup {
1904    programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
1905}
1906
1907#[cfg(feature = "artifact-graph")]
1908impl ProgramLookup {
1909    // TODO: Could this store a reference to KCL programs instead of owning them?
1910    // i.e. take &state::ModuleInfoMap instead?
1911    pub fn new(
1912        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1913        module_infos: state::ModuleInfoMap,
1914    ) -> Self {
1915        let mut programs = IndexMap::with_capacity(module_infos.len());
1916        for (id, info) in module_infos {
1917            if let ModuleRepr::Kcl(program, _) = info.repr {
1918                programs.insert(id, program);
1919            }
1920        }
1921        programs.insert(ModuleId::default(), current);
1922        Self { programs }
1923    }
1924
1925    pub fn program_for_module(
1926        &self,
1927        module_id: ModuleId,
1928    ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
1929        self.programs.get(&module_id)
1930    }
1931}
1932
1933#[cfg(test)]
1934mod tests {
1935    use pretty_assertions::assert_eq;
1936
1937    use super::*;
1938    use crate::ModuleId;
1939    use crate::errors::KclErrorDetails;
1940    use crate::errors::Severity;
1941    use crate::exec::NumericType;
1942    use crate::execution::memory::Stack;
1943    use crate::execution::types::RuntimeType;
1944
1945    /// Convenience function to get a JSON value from memory and unwrap.
1946    #[track_caller]
1947    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1948        memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1949    }
1950
1951    #[tokio::test(flavor = "multi_thread")]
1952    async fn test_execute_warn() {
1953        let text = "@blah";
1954        let result = parse_execute(text).await.unwrap();
1955        let errs = result.exec_state.errors();
1956        assert_eq!(errs.len(), 1);
1957        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1958        assert!(
1959            errs[0].message.contains("Unknown annotation"),
1960            "unexpected warning message: {}",
1961            errs[0].message
1962        );
1963    }
1964
1965    #[tokio::test(flavor = "multi_thread")]
1966    async fn test_execute_fn_definitions() {
1967        let ast = r#"fn def(@x) {
1968  return x
1969}
1970fn ghi(@x) {
1971  return x
1972}
1973fn jkl(@x) {
1974  return x
1975}
1976fn hmm(@x) {
1977  return x
1978}
1979
1980yo = 5 + 6
1981
1982abc = 3
1983identifierGuy = 5
1984part001 = startSketchOn(XY)
1985|> startProfile(at = [-1.2, 4.83])
1986|> line(end = [2.8, 0])
1987|> angledLine(angle = 100 + 100, length = 3.01)
1988|> angledLine(angle = abc, length = 3.02)
1989|> angledLine(angle = def(yo), length = 3.03)
1990|> angledLine(angle = ghi(2), length = 3.04)
1991|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1992|> close()
1993yo2 = hmm([identifierGuy + 5])"#;
1994
1995        parse_execute(ast).await.unwrap();
1996    }
1997
1998    #[tokio::test(flavor = "multi_thread")]
1999    async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2000        let code = r#"@settings(experimentalFeatures = allow)
2001firstProfile = sketch(on = XY) {
2002  edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2003  edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2004  edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2005  edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2006  coincident([edge1.end, edge2.start])
2007  coincident([edge2.end, edge3.start])
2008  coincident([edge3.end, edge4.start])
2009  coincident([edge4.end, edge1.start])
2010}
2011
2012secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2013  edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2014  edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2015  edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2016  edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2017  coincident([edge5.end, edge6.start])
2018  coincident([edge6.end, edge7.start])
2019  coincident([edge7.end, edge8.start])
2020  coincident([edge8.end, edge5.start])
2021}
2022
2023firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2024secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2025"#;
2026
2027        let result = parse_execute(code).await.unwrap();
2028        assert!(result.exec_state.errors().is_empty());
2029    }
2030
2031    #[tokio::test(flavor = "multi_thread")]
2032    async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2033        let code = r#"@settings(experimentalFeatures = allow)
2034sketch001 = sketch(on = YZ) {
2035  line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2036  line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2037  line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2038  line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2039  coincident([line1.end, line2.start])
2040  coincident([line2.end, line3.start])
2041  coincident([line3.end, line4.start])
2042  coincident([line4.end, line1.start])
2043}
2044
2045sketch002 = sketch(on = -XZ) {
2046  line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2047  line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2048  line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2049  line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2050  coincident([line5.end, line6.start])
2051  coincident([line6.end, line7.start])
2052  coincident([line7.end, line8.start])
2053  coincident([line8.end, line5.start])
2054}
2055
2056region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2057extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2058region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2059extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2060
2061myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2062"#;
2063
2064        let result = parse_execute(code).await.unwrap();
2065        assert!(result.exec_state.errors().is_empty());
2066    }
2067
2068    #[tokio::test(flavor = "multi_thread")]
2069    async fn issue_10741_point_circle_coincident_executes() {
2070        let code = r#"@settings(experimentalFeatures = allow)
2071sketch001 = sketch(on = YZ) {
2072  circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
2073  line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
2074  coincident([line1.start, circle1])
2075}
2076"#;
2077
2078        let result = parse_execute(code).await.unwrap();
2079        assert!(
2080            result
2081                .exec_state
2082                .errors()
2083                .iter()
2084                .all(|error| error.severity != Severity::Error),
2085            "unexpected execution errors: {:#?}",
2086            result.exec_state.errors()
2087        );
2088    }
2089
2090    #[tokio::test(flavor = "multi_thread")]
2091    async fn test_execute_with_pipe_substitutions_unary() {
2092        let ast = r#"myVar = 3
2093part001 = startSketchOn(XY)
2094  |> startProfile(at = [0, 0])
2095  |> line(end = [3, 4], tag = $seg01)
2096  |> line(end = [
2097  min([segLen(seg01), myVar]),
2098  -legLen(hypotenuse = segLen(seg01), leg = myVar)
2099])
2100"#;
2101
2102        parse_execute(ast).await.unwrap();
2103    }
2104
2105    #[tokio::test(flavor = "multi_thread")]
2106    async fn test_execute_with_pipe_substitutions() {
2107        let ast = r#"myVar = 3
2108part001 = startSketchOn(XY)
2109  |> startProfile(at = [0, 0])
2110  |> line(end = [3, 4], tag = $seg01)
2111  |> line(end = [
2112  min([segLen(seg01), myVar]),
2113  legLen(hypotenuse = segLen(seg01), leg = myVar)
2114])
2115"#;
2116
2117        parse_execute(ast).await.unwrap();
2118    }
2119
2120    #[tokio::test(flavor = "multi_thread")]
2121    async fn test_execute_with_inline_comment() {
2122        let ast = r#"baseThick = 1
2123armAngle = 60
2124
2125baseThickHalf = baseThick / 2
2126halfArmAngle = armAngle / 2
2127
2128arrExpShouldNotBeIncluded = [1, 2, 3]
2129objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2130
2131part001 = startSketchOn(XY)
2132  |> startProfile(at = [0, 0])
2133  |> yLine(endAbsolute = 1)
2134  |> xLine(length = 3.84) // selection-range-7ish-before-this
2135
2136variableBelowShouldNotBeIncluded = 3
2137"#;
2138
2139        parse_execute(ast).await.unwrap();
2140    }
2141
2142    #[tokio::test(flavor = "multi_thread")]
2143    async fn test_execute_with_function_literal_in_pipe() {
2144        let ast = r#"w = 20
2145l = 8
2146h = 10
2147
2148fn thing() {
2149  return -8
2150}
2151
2152firstExtrude = startSketchOn(XY)
2153  |> startProfile(at = [0,0])
2154  |> line(end = [0, l])
2155  |> line(end = [w, 0])
2156  |> line(end = [0, thing()])
2157  |> close()
2158  |> extrude(length = h)"#;
2159
2160        parse_execute(ast).await.unwrap();
2161    }
2162
2163    #[tokio::test(flavor = "multi_thread")]
2164    async fn test_execute_with_function_unary_in_pipe() {
2165        let ast = r#"w = 20
2166l = 8
2167h = 10
2168
2169fn thing(@x) {
2170  return -x
2171}
2172
2173firstExtrude = startSketchOn(XY)
2174  |> startProfile(at = [0,0])
2175  |> line(end = [0, l])
2176  |> line(end = [w, 0])
2177  |> line(end = [0, thing(8)])
2178  |> close()
2179  |> extrude(length = h)"#;
2180
2181        parse_execute(ast).await.unwrap();
2182    }
2183
2184    #[tokio::test(flavor = "multi_thread")]
2185    async fn test_execute_with_function_array_in_pipe() {
2186        let ast = r#"w = 20
2187l = 8
2188h = 10
2189
2190fn thing(@x) {
2191  return [0, -x]
2192}
2193
2194firstExtrude = startSketchOn(XY)
2195  |> startProfile(at = [0,0])
2196  |> line(end = [0, l])
2197  |> line(end = [w, 0])
2198  |> line(end = thing(8))
2199  |> close()
2200  |> extrude(length = h)"#;
2201
2202        parse_execute(ast).await.unwrap();
2203    }
2204
2205    #[tokio::test(flavor = "multi_thread")]
2206    async fn test_execute_with_function_call_in_pipe() {
2207        let ast = r#"w = 20
2208l = 8
2209h = 10
2210
2211fn other_thing(@y) {
2212  return -y
2213}
2214
2215fn thing(@x) {
2216  return other_thing(x)
2217}
2218
2219firstExtrude = startSketchOn(XY)
2220  |> startProfile(at = [0,0])
2221  |> line(end = [0, l])
2222  |> line(end = [w, 0])
2223  |> line(end = [0, thing(8)])
2224  |> close()
2225  |> extrude(length = h)"#;
2226
2227        parse_execute(ast).await.unwrap();
2228    }
2229
2230    #[tokio::test(flavor = "multi_thread")]
2231    async fn test_execute_with_function_sketch() {
2232        let ast = r#"fn box(h, l, w) {
2233 myBox = startSketchOn(XY)
2234    |> startProfile(at = [0,0])
2235    |> line(end = [0, l])
2236    |> line(end = [w, 0])
2237    |> line(end = [0, -l])
2238    |> close()
2239    |> extrude(length = h)
2240
2241  return myBox
2242}
2243
2244fnBox = box(h = 3, l = 6, w = 10)"#;
2245
2246        parse_execute(ast).await.unwrap();
2247    }
2248
2249    #[tokio::test(flavor = "multi_thread")]
2250    async fn test_get_member_of_object_with_function_period() {
2251        let ast = r#"fn box(@obj) {
2252 myBox = startSketchOn(XY)
2253    |> startProfile(at = obj.start)
2254    |> line(end = [0, obj.l])
2255    |> line(end = [obj.w, 0])
2256    |> line(end = [0, -obj.l])
2257    |> close()
2258    |> extrude(length = obj.h)
2259
2260  return myBox
2261}
2262
2263thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2264"#;
2265        parse_execute(ast).await.unwrap();
2266    }
2267
2268    #[tokio::test(flavor = "multi_thread")]
2269    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
2270    async fn test_object_member_starting_pipeline() {
2271        let ast = r#"
2272fn test2() {
2273  return {
2274    thing: startSketchOn(XY)
2275      |> startProfile(at = [0, 0])
2276      |> line(end = [0, 1])
2277      |> line(end = [1, 0])
2278      |> line(end = [0, -1])
2279      |> close()
2280  }
2281}
2282
2283x2 = test2()
2284
2285x2.thing
2286  |> extrude(length = 10)
2287"#;
2288        parse_execute(ast).await.unwrap();
2289    }
2290
2291    #[tokio::test(flavor = "multi_thread")]
2292    #[ignore] // ignore til we get loops
2293    async fn test_execute_with_function_sketch_loop_objects() {
2294        let ast = r#"fn box(obj) {
2295let myBox = startSketchOn(XY)
2296    |> startProfile(at = obj.start)
2297    |> line(end = [0, obj.l])
2298    |> line(end = [obj.w, 0])
2299    |> line(end = [0, -obj.l])
2300    |> close()
2301    |> extrude(length = obj.h)
2302
2303  return myBox
2304}
2305
2306for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2307  thisBox = box(var)
2308}"#;
2309
2310        parse_execute(ast).await.unwrap();
2311    }
2312
2313    #[tokio::test(flavor = "multi_thread")]
2314    #[ignore] // ignore til we get loops
2315    async fn test_execute_with_function_sketch_loop_array() {
2316        let ast = r#"fn box(h, l, w, start) {
2317 myBox = startSketchOn(XY)
2318    |> startProfile(at = [0,0])
2319    |> line(end = [0, l])
2320    |> line(end = [w, 0])
2321    |> line(end = [0, -l])
2322    |> close()
2323    |> extrude(length = h)
2324
2325  return myBox
2326}
2327
2328
2329for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2330  const thisBox = box(var[0], var[1], var[2], var[3])
2331}"#;
2332
2333        parse_execute(ast).await.unwrap();
2334    }
2335
2336    #[tokio::test(flavor = "multi_thread")]
2337    async fn test_get_member_of_array_with_function() {
2338        let ast = r#"fn box(@arr) {
2339 myBox =startSketchOn(XY)
2340    |> startProfile(at = arr[0])
2341    |> line(end = [0, arr[1]])
2342    |> line(end = [arr[2], 0])
2343    |> line(end = [0, -arr[1]])
2344    |> close()
2345    |> extrude(length = arr[3])
2346
2347  return myBox
2348}
2349
2350thisBox = box([[0,0], 6, 10, 3])
2351
2352"#;
2353        parse_execute(ast).await.unwrap();
2354    }
2355
2356    #[tokio::test(flavor = "multi_thread")]
2357    async fn test_function_cannot_access_future_definitions() {
2358        let ast = r#"
2359fn returnX() {
2360  // x shouldn't be defined yet.
2361  return x
2362}
2363
2364x = 5
2365
2366answer = returnX()"#;
2367
2368        let result = parse_execute(ast).await;
2369        let err = result.unwrap_err();
2370        assert_eq!(err.message(), "`x` is not defined");
2371    }
2372
2373    #[tokio::test(flavor = "multi_thread")]
2374    async fn test_override_prelude() {
2375        let text = "PI = 3.0";
2376        let result = parse_execute(text).await.unwrap();
2377        let errs = result.exec_state.errors();
2378        assert!(errs.is_empty());
2379    }
2380
2381    #[tokio::test(flavor = "multi_thread")]
2382    async fn type_aliases() {
2383        let text = r#"@settings(experimentalFeatures = allow)
2384type MyTy = [number; 2]
2385fn foo(@x: MyTy) {
2386    return x[0]
2387}
2388
2389foo([0, 1])
2390
2391type Other = MyTy | Helix
2392"#;
2393        let result = parse_execute(text).await.unwrap();
2394        let errs = result.exec_state.errors();
2395        assert!(errs.is_empty());
2396    }
2397
2398    #[tokio::test(flavor = "multi_thread")]
2399    async fn test_cannot_shebang_in_fn() {
2400        let ast = r#"
2401fn foo() {
2402  #!hello
2403  return true
2404}
2405
2406foo
2407"#;
2408
2409        let result = parse_execute(ast).await;
2410        let err = result.unwrap_err();
2411        assert_eq!(
2412            err,
2413            KclError::new_syntax(KclErrorDetails::new(
2414                "Unexpected token: #".to_owned(),
2415                vec![SourceRange::new(14, 15, ModuleId::default())],
2416            )),
2417        );
2418    }
2419
2420    #[tokio::test(flavor = "multi_thread")]
2421    async fn test_pattern_transform_function_cannot_access_future_definitions() {
2422        let ast = r#"
2423fn transform(@replicaId) {
2424  // x shouldn't be defined yet.
2425  scale = x
2426  return {
2427    translate = [0, 0, replicaId * 10],
2428    scale = [scale, 1, 0],
2429  }
2430}
2431
2432fn layer() {
2433  return startSketchOn(XY)
2434    |> circle( center= [0, 0], radius= 1, tag = $tag1)
2435    |> extrude(length = 10)
2436}
2437
2438x = 5
2439
2440// The 10 layers are replicas of each other, with a transform applied to each.
2441shape = layer() |> patternTransform(instances = 10, transform = transform)
2442"#;
2443
2444        let result = parse_execute(ast).await;
2445        let err = result.unwrap_err();
2446        assert_eq!(err.message(), "`x` is not defined",);
2447    }
2448
2449    // ADAM: Move some of these into simulation tests.
2450
2451    #[tokio::test(flavor = "multi_thread")]
2452    async fn test_math_execute_with_functions() {
2453        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
2454        let result = parse_execute(ast).await.unwrap();
2455        assert_eq!(
2456            5.0,
2457            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2458                .as_f64()
2459                .unwrap()
2460        );
2461    }
2462
2463    #[tokio::test(flavor = "multi_thread")]
2464    async fn test_math_execute() {
2465        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
2466        let result = parse_execute(ast).await.unwrap();
2467        assert_eq!(
2468            7.4,
2469            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2470                .as_f64()
2471                .unwrap()
2472        );
2473    }
2474
2475    #[tokio::test(flavor = "multi_thread")]
2476    async fn test_math_execute_start_negative() {
2477        let ast = r#"myVar = -5 + 6"#;
2478        let result = parse_execute(ast).await.unwrap();
2479        assert_eq!(
2480            1.0,
2481            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2482                .as_f64()
2483                .unwrap()
2484        );
2485    }
2486
2487    #[tokio::test(flavor = "multi_thread")]
2488    async fn test_math_execute_with_pi() {
2489        let ast = r#"myVar = PI * 2"#;
2490        let result = parse_execute(ast).await.unwrap();
2491        assert_eq!(
2492            std::f64::consts::TAU,
2493            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2494                .as_f64()
2495                .unwrap()
2496        );
2497    }
2498
2499    #[tokio::test(flavor = "multi_thread")]
2500    async fn test_math_define_decimal_without_leading_zero() {
2501        let ast = r#"thing = .4 + 7"#;
2502        let result = parse_execute(ast).await.unwrap();
2503        assert_eq!(
2504            7.4,
2505            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
2506                .as_f64()
2507                .unwrap()
2508        );
2509    }
2510
2511    #[tokio::test(flavor = "multi_thread")]
2512    async fn pass_std_to_std() {
2513        let ast = r#"sketch001 = startSketchOn(XY)
2514profile001 = circle(sketch001, center = [0, 0], radius = 2)
2515extrude001 = extrude(profile001, length = 5)
2516extrudes = patternLinear3d(
2517  extrude001,
2518  instances = 3,
2519  distance = 5,
2520  axis = [1, 1, 0],
2521)
2522clone001 = map(extrudes, f = clone)
2523"#;
2524        parse_execute(ast).await.unwrap();
2525    }
2526
2527    #[tokio::test(flavor = "multi_thread")]
2528    async fn test_array_reduce_nested_array() {
2529        let code = r#"
2530fn id(@el, accum)  { return accum }
2531
2532answer = reduce([], initial=[[[0,0]]], f=id)
2533"#;
2534        let result = parse_execute(code).await.unwrap();
2535        assert_eq!(
2536            mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2537            KclValue::HomArray {
2538                value: vec![KclValue::HomArray {
2539                    value: vec![KclValue::HomArray {
2540                        value: vec![
2541                            KclValue::Number {
2542                                value: 0.0,
2543                                ty: NumericType::default(),
2544                                meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2545                            },
2546                            KclValue::Number {
2547                                value: 0.0,
2548                                ty: NumericType::default(),
2549                                meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2550                            }
2551                        ],
2552                        ty: RuntimeType::any(),
2553                    }],
2554                    ty: RuntimeType::any(),
2555                }],
2556                ty: RuntimeType::any(),
2557            }
2558        );
2559    }
2560
2561    #[tokio::test(flavor = "multi_thread")]
2562    async fn test_zero_param_fn() {
2563        let ast = r#"sigmaAllow = 35000 // psi
2564leg1 = 5 // inches
2565leg2 = 8 // inches
2566fn thickness() { return 0.56 }
2567
2568bracket = startSketchOn(XY)
2569  |> startProfile(at = [0,0])
2570  |> line(end = [0, leg1])
2571  |> line(end = [leg2, 0])
2572  |> line(end = [0, -thickness()])
2573  |> line(end = [-leg2 + thickness(), 0])
2574"#;
2575        parse_execute(ast).await.unwrap();
2576    }
2577
2578    #[tokio::test(flavor = "multi_thread")]
2579    async fn test_unary_operator_not_succeeds() {
2580        let ast = r#"
2581fn returnTrue() { return !false }
2582t = true
2583f = false
2584notTrue = !t
2585notFalse = !f
2586c = !!true
2587d = !returnTrue()
2588
2589assertIs(!false, error = "expected to pass")
2590
2591fn check(x) {
2592  assertIs(!x, error = "expected argument to be false")
2593  return true
2594}
2595check(x = false)
2596"#;
2597        let result = parse_execute(ast).await.unwrap();
2598        assert_eq!(
2599            false,
2600            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2601                .as_bool()
2602                .unwrap()
2603        );
2604        assert_eq!(
2605            true,
2606            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2607                .as_bool()
2608                .unwrap()
2609        );
2610        assert_eq!(
2611            true,
2612            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2613                .as_bool()
2614                .unwrap()
2615        );
2616        assert_eq!(
2617            false,
2618            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2619                .as_bool()
2620                .unwrap()
2621        );
2622    }
2623
2624    #[tokio::test(flavor = "multi_thread")]
2625    async fn test_unary_operator_not_on_non_bool_fails() {
2626        let code1 = r#"
2627// Yup, this is null.
2628myNull = 0 / 0
2629notNull = !myNull
2630"#;
2631        assert_eq!(
2632            parse_execute(code1).await.unwrap_err().message(),
2633            "Cannot apply unary operator ! to non-boolean value: a number",
2634        );
2635
2636        let code2 = "notZero = !0";
2637        assert_eq!(
2638            parse_execute(code2).await.unwrap_err().message(),
2639            "Cannot apply unary operator ! to non-boolean value: a number",
2640        );
2641
2642        let code3 = r#"
2643notEmptyString = !""
2644"#;
2645        assert_eq!(
2646            parse_execute(code3).await.unwrap_err().message(),
2647            "Cannot apply unary operator ! to non-boolean value: a string",
2648        );
2649
2650        let code4 = r#"
2651obj = { a = 1 }
2652notMember = !obj.a
2653"#;
2654        assert_eq!(
2655            parse_execute(code4).await.unwrap_err().message(),
2656            "Cannot apply unary operator ! to non-boolean value: a number",
2657        );
2658
2659        let code5 = "
2660a = []
2661notArray = !a";
2662        assert_eq!(
2663            parse_execute(code5).await.unwrap_err().message(),
2664            "Cannot apply unary operator ! to non-boolean value: an empty array",
2665        );
2666
2667        let code6 = "
2668x = {}
2669notObject = !x";
2670        assert_eq!(
2671            parse_execute(code6).await.unwrap_err().message(),
2672            "Cannot apply unary operator ! to non-boolean value: an object",
2673        );
2674
2675        let code7 = "
2676fn x() { return 1 }
2677notFunction = !x";
2678        let fn_err = parse_execute(code7).await.unwrap_err();
2679        // These are currently printed out as JSON objects, so we don't want to
2680        // check the full error.
2681        assert!(
2682            fn_err
2683                .message()
2684                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2685            "Actual error: {fn_err:?}"
2686        );
2687
2688        let code8 = "
2689myTagDeclarator = $myTag
2690notTagDeclarator = !myTagDeclarator";
2691        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2692        // These are currently printed out as JSON objects, so we don't want to
2693        // check the full error.
2694        assert!(
2695            tag_declarator_err
2696                .message()
2697                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2698            "Actual error: {tag_declarator_err:?}"
2699        );
2700
2701        let code9 = "
2702myTagDeclarator = $myTag
2703notTagIdentifier = !myTag";
2704        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2705        // These are currently printed out as JSON objects, so we don't want to
2706        // check the full error.
2707        assert!(
2708            tag_identifier_err
2709                .message()
2710                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2711            "Actual error: {tag_identifier_err:?}"
2712        );
2713
2714        let code10 = "notPipe = !(1 |> 2)";
2715        assert_eq!(
2716            // TODO: We don't currently parse this, but we should.  It should be
2717            // a runtime error instead.
2718            parse_execute(code10).await.unwrap_err(),
2719            KclError::new_syntax(KclErrorDetails::new(
2720                "Unexpected token: !".to_owned(),
2721                vec![SourceRange::new(10, 11, ModuleId::default())],
2722            ))
2723        );
2724
2725        let code11 = "
2726fn identity(x) { return x }
2727notPipeSub = 1 |> identity(!%))";
2728        assert_eq!(
2729            // TODO: We don't currently parse this, but we should.  It should be
2730            // a runtime error instead.
2731            parse_execute(code11).await.unwrap_err(),
2732            KclError::new_syntax(KclErrorDetails::new(
2733                "There was an unexpected `!`. Try removing it.".to_owned(),
2734                vec![SourceRange::new(56, 57, ModuleId::default())],
2735            ))
2736        );
2737
2738        // TODO: Add these tests when we support these types.
2739        // let notNan = !NaN
2740        // let notInfinity = !Infinity
2741    }
2742
2743    #[tokio::test(flavor = "multi_thread")]
2744    async fn test_start_sketch_on_invalid_kwargs() {
2745        let current_dir = std::env::current_dir().unwrap();
2746        let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2747        let mut code = std::fs::read_to_string(&path).unwrap();
2748        assert_eq!(
2749            parse_execute(&code).await.unwrap_err().message(),
2750            "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2751        );
2752
2753        path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2754        code = std::fs::read_to_string(&path).unwrap();
2755
2756        assert_eq!(
2757            parse_execute(&code).await.unwrap_err().message(),
2758            "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2759        );
2760
2761        path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2762        code = std::fs::read_to_string(&path).unwrap();
2763
2764        assert_eq!(
2765            parse_execute(&code).await.unwrap_err().message(),
2766            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2767        );
2768
2769        path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2770        code = std::fs::read_to_string(&path).unwrap();
2771
2772        assert_eq!(
2773            parse_execute(&code).await.unwrap_err().message(),
2774            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2775        );
2776
2777        path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2778        code = std::fs::read_to_string(&path).unwrap();
2779
2780        assert_eq!(
2781            parse_execute(&code).await.unwrap_err().message(),
2782            "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2783        );
2784    }
2785
2786    #[tokio::test(flavor = "multi_thread")]
2787    async fn test_math_negative_variable_in_binary_expression() {
2788        let ast = r#"sigmaAllow = 35000 // psi
2789width = 1 // inch
2790
2791p = 150 // lbs
2792distance = 6 // inches
2793FOS = 2
2794
2795leg1 = 5 // inches
2796leg2 = 8 // inches
2797
2798thickness_squared = distance * p * FOS * 6 / sigmaAllow
2799thickness = 0.56 // inches. App does not support square root function yet
2800
2801bracket = startSketchOn(XY)
2802  |> startProfile(at = [0,0])
2803  |> line(end = [0, leg1])
2804  |> line(end = [leg2, 0])
2805  |> line(end = [0, -thickness])
2806  |> line(end = [-leg2 + thickness, 0])
2807"#;
2808        parse_execute(ast).await.unwrap();
2809    }
2810
2811    #[tokio::test(flavor = "multi_thread")]
2812    async fn test_execute_function_no_return() {
2813        let ast = r#"fn test(@origin) {
2814  origin
2815}
2816
2817test([0, 0])
2818"#;
2819        let result = parse_execute(ast).await;
2820        assert!(result.is_err());
2821        assert!(result.unwrap_err().to_string().contains("undefined"));
2822    }
2823
2824    #[tokio::test(flavor = "multi_thread")]
2825    async fn test_max_stack_size_exceeded_error() {
2826        let ast = r#"
2827fn forever(@n) {
2828  return 1 + forever(n)
2829}
2830
2831forever(1)
2832"#;
2833        let result = parse_execute(ast).await;
2834        let err = result.unwrap_err();
2835        assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
2836    }
2837
2838    #[tokio::test(flavor = "multi_thread")]
2839    async fn test_math_doubly_nested_parens() {
2840        let ast = r#"sigmaAllow = 35000 // psi
2841width = 4 // inch
2842p = 150 // Force on shelf - lbs
2843distance = 6 // inches
2844FOS = 2
2845leg1 = 5 // inches
2846leg2 = 8 // inches
2847thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2848thickness = 0.32 // inches. App does not support square root function yet
2849bracket = startSketchOn(XY)
2850  |> startProfile(at = [0,0])
2851    |> line(end = [0, leg1])
2852  |> line(end = [leg2, 0])
2853  |> line(end = [0, -thickness])
2854  |> line(end = [-1 * leg2 + thickness, 0])
2855  |> line(end = [0, -1 * leg1 + thickness])
2856  |> close()
2857  |> extrude(length = width)
2858"#;
2859        parse_execute(ast).await.unwrap();
2860    }
2861
2862    #[tokio::test(flavor = "multi_thread")]
2863    async fn test_math_nested_parens_one_less() {
2864        let ast = r#" sigmaAllow = 35000 // psi
2865width = 4 // inch
2866p = 150 // Force on shelf - lbs
2867distance = 6 // inches
2868FOS = 2
2869leg1 = 5 // inches
2870leg2 = 8 // inches
2871thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2872thickness = 0.32 // inches. App does not support square root function yet
2873bracket = startSketchOn(XY)
2874  |> startProfile(at = [0,0])
2875    |> line(end = [0, leg1])
2876  |> line(end = [leg2, 0])
2877  |> line(end = [0, -thickness])
2878  |> line(end = [-1 * leg2 + thickness, 0])
2879  |> line(end = [0, -1 * leg1 + thickness])
2880  |> close()
2881  |> extrude(length = width)
2882"#;
2883        parse_execute(ast).await.unwrap();
2884    }
2885
2886    #[tokio::test(flavor = "multi_thread")]
2887    async fn test_fn_as_operand() {
2888        let ast = r#"fn f() { return 1 }
2889x = f()
2890y = x + 1
2891z = f() + 1
2892w = f() + f()
2893"#;
2894        parse_execute(ast).await.unwrap();
2895    }
2896
2897    #[tokio::test(flavor = "multi_thread")]
2898    async fn kcl_test_ids_stable_between_executions() {
2899        let code = r#"sketch001 = startSketchOn(XZ)
2900|> startProfile(at = [61.74, 206.13])
2901|> xLine(length = 305.11, tag = $seg01)
2902|> yLine(length = -291.85)
2903|> xLine(length = -segLen(seg01))
2904|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2905|> close()
2906|> extrude(length = 40.14)
2907|> shell(
2908    thickness = 3.14,
2909    faces = [seg01]
2910)
2911"#;
2912
2913        let ctx = crate::test_server::new_context(true, None).await.unwrap();
2914        let old_program = crate::Program::parse_no_errs(code).unwrap();
2915
2916        // Execute the program.
2917        if let Err(err) = ctx.run_with_caching(old_program).await {
2918            let report = err.into_miette_report_with_outputs(code).unwrap();
2919            let report = miette::Report::new(report);
2920            panic!("Error executing program: {report:?}");
2921        }
2922
2923        // Get the id_generator from the first execution.
2924        let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2925
2926        let code = r#"sketch001 = startSketchOn(XZ)
2927|> startProfile(at = [62.74, 206.13])
2928|> xLine(length = 305.11, tag = $seg01)
2929|> yLine(length = -291.85)
2930|> xLine(length = -segLen(seg01))
2931|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2932|> close()
2933|> extrude(length = 40.14)
2934|> shell(
2935    faces = [seg01],
2936    thickness = 3.14,
2937)
2938"#;
2939
2940        // Execute a slightly different program again.
2941        let program = crate::Program::parse_no_errs(code).unwrap();
2942        // Execute the program.
2943        ctx.run_with_caching(program).await.unwrap();
2944
2945        let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2946
2947        assert_eq!(id_generator, new_id_generator);
2948    }
2949
2950    #[tokio::test(flavor = "multi_thread")]
2951    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2952        let code = r#"sketch001 = startSketchOn(XZ)
2953|> startProfile(at = [61.74, 206.13])
2954|> xLine(length = 305.11, tag = $seg01)
2955|> yLine(length = -291.85)
2956|> xLine(length = -segLen(seg01))
2957|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2958|> close()
2959|> extrude(length = 40.14)
2960|> shell(
2961    thickness = 3.14,
2962    faces = [seg01]
2963)
2964"#;
2965
2966        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2967        let old_program = crate::Program::parse_no_errs(code).unwrap();
2968
2969        // Execute the program.
2970        ctx.run_with_caching(old_program.clone()).await.unwrap();
2971
2972        let settings_state = cache::read_old_ast().await.unwrap().settings;
2973
2974        // Ensure the settings are as expected.
2975        assert_eq!(settings_state, ctx.settings);
2976
2977        // Change a setting.
2978        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2979
2980        // Execute the program.
2981        ctx.run_with_caching(old_program.clone()).await.unwrap();
2982
2983        let settings_state = cache::read_old_ast().await.unwrap().settings;
2984
2985        // Ensure the settings are as expected.
2986        assert_eq!(settings_state, ctx.settings);
2987
2988        // Change a setting.
2989        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2990
2991        // Execute the program.
2992        ctx.run_with_caching(old_program).await.unwrap();
2993
2994        let settings_state = cache::read_old_ast().await.unwrap().settings;
2995
2996        // Ensure the settings are as expected.
2997        assert_eq!(settings_state, ctx.settings);
2998
2999        ctx.close().await;
3000    }
3001
3002    #[tokio::test(flavor = "multi_thread")]
3003    async fn mock_after_not_mock() {
3004        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3005        let program = crate::Program::parse_no_errs("x = 2").unwrap();
3006        let result = ctx.run_with_caching(program).await.unwrap();
3007        assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
3008
3009        let ctx2 = ExecutorContext::new_mock(None).await;
3010        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
3011        let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
3012        assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
3013
3014        ctx.close().await;
3015        ctx2.close().await;
3016    }
3017
3018    #[tokio::test(flavor = "multi_thread")]
3019    async fn mock_then_add_extrude_then_mock_again() {
3020        let code = "@settings(experimentalFeatures = allow)
3021    
3022s = sketch(on = XY) {
3023    line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
3024    line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
3025    coincident([line1.end, line2.start])
3026    line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
3027    coincident([line2.end, line3.start])
3028    coincident([line1.start, line3.end])
3029}
3030    ";
3031        let ctx = ExecutorContext::new_mock(None).await;
3032        let program = crate::Program::parse_no_errs(code).unwrap();
3033        let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3034        assert!(result.variables.contains_key("s"), "actual: {:?}", &result.variables);
3035
3036        let code2 = code.to_owned()
3037            + "
3038region001 = region(point = [1mm, 1mm], sketch = s)
3039extrude001 = extrude(region001, length = 1)
3040    ";
3041        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3042        let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
3043        assert!(
3044            result.variables.contains_key("region001"),
3045            "actual: {:?}",
3046            &result.variables
3047        );
3048
3049        ctx.close().await;
3050    }
3051
3052    #[cfg(feature = "artifact-graph")]
3053    #[tokio::test(flavor = "multi_thread")]
3054    async fn mock_has_stable_ids() {
3055        let ctx = ExecutorContext::new_mock(None).await;
3056        let mock_config = MockConfig {
3057            use_prev_memory: false,
3058            ..Default::default()
3059        };
3060        let code = "sk = startSketchOn(XY)
3061        |> startProfile(at = [0, 0])";
3062        let program = crate::Program::parse_no_errs(code).unwrap();
3063        let result = ctx.run_mock(&program, &mock_config).await.unwrap();
3064        let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3065        assert!(!ids.is_empty(), "IDs should not be empty");
3066
3067        let ctx2 = ExecutorContext::new_mock(None).await;
3068        let program2 = crate::Program::parse_no_errs(code).unwrap();
3069        let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
3070        let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3071
3072        assert_eq!(ids, ids2, "Generated IDs should match");
3073        ctx.close().await;
3074        ctx2.close().await;
3075    }
3076
3077    #[cfg(feature = "artifact-graph")]
3078    #[tokio::test(flavor = "multi_thread")]
3079    async fn sim_sketch_mode_real_mock_real() {
3080        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3081        let code = r#"sketch001 = startSketchOn(XY)
3082profile001 = startProfile(sketch001, at = [0, 0])
3083  |> line(end = [10, 0])
3084  |> line(end = [0, 10])
3085  |> line(end = [-10, 0])
3086  |> line(end = [0, -10])
3087  |> close()
3088"#;
3089        let program = crate::Program::parse_no_errs(code).unwrap();
3090        let result = ctx.run_with_caching(program).await.unwrap();
3091        assert_eq!(result.operations.len(), 1);
3092
3093        let mock_ctx = ExecutorContext::new_mock(None).await;
3094        let mock_program = crate::Program::parse_no_errs(code).unwrap();
3095        let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
3096        assert_eq!(mock_result.operations.len(), 1);
3097
3098        let code2 = code.to_owned()
3099            + r#"
3100extrude001 = extrude(profile001, length = 10)
3101"#;
3102        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3103        let result = ctx.run_with_caching(program2).await.unwrap();
3104        assert_eq!(result.operations.len(), 2);
3105
3106        ctx.close().await;
3107        mock_ctx.close().await;
3108    }
3109
3110    #[tokio::test(flavor = "multi_thread")]
3111    async fn read_tag_version() {
3112        let ast = r#"fn bar(@t) {
3113  return startSketchOn(XY)
3114    |> startProfile(at = [0,0])
3115    |> angledLine(
3116        angle = -60,
3117        length = segLen(t),
3118    )
3119    |> line(end = [0, 0])
3120    |> close()
3121}
3122
3123sketch = startSketchOn(XY)
3124  |> startProfile(at = [0,0])
3125  |> line(end = [0, 10])
3126  |> line(end = [10, 0], tag = $tag0)
3127  |> line(endAbsolute = [0, 0])
3128
3129fn foo() {
3130  // tag0 tags an edge
3131  return bar(tag0)
3132}
3133
3134solid = sketch |> extrude(length = 10)
3135// tag0 tags a face
3136sketch2 = startSketchOn(solid, face = tag0)
3137  |> startProfile(at = [0,0])
3138  |> line(end = [0, 1])
3139  |> line(end = [1, 0])
3140  |> line(end = [0, 0])
3141
3142foo() |> extrude(length = 1)
3143"#;
3144        parse_execute(ast).await.unwrap();
3145    }
3146
3147    #[tokio::test(flavor = "multi_thread")]
3148    async fn experimental() {
3149        let code = r#"
3150startSketchOn(XY)
3151  |> startProfile(at = [0, 0], tag = $start)
3152  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3153"#;
3154        let result = parse_execute(code).await.unwrap();
3155        let errors = result.exec_state.errors();
3156        assert_eq!(errors.len(), 1);
3157        assert_eq!(errors[0].severity, Severity::Error);
3158        let msg = &errors[0].message;
3159        assert!(msg.contains("experimental"), "found {msg}");
3160
3161        let code = r#"@settings(experimentalFeatures = allow)
3162startSketchOn(XY)
3163  |> startProfile(at = [0, 0], tag = $start)
3164  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3165"#;
3166        let result = parse_execute(code).await.unwrap();
3167        let errors = result.exec_state.errors();
3168        assert!(errors.is_empty());
3169
3170        let code = r#"@settings(experimentalFeatures = warn)
3171startSketchOn(XY)
3172  |> startProfile(at = [0, 0], tag = $start)
3173  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3174"#;
3175        let result = parse_execute(code).await.unwrap();
3176        let errors = result.exec_state.errors();
3177        assert_eq!(errors.len(), 1);
3178        assert_eq!(errors[0].severity, Severity::Warning);
3179        let msg = &errors[0].message;
3180        assert!(msg.contains("experimental"), "found {msg}");
3181
3182        let code = r#"@settings(experimentalFeatures = deny)
3183startSketchOn(XY)
3184  |> startProfile(at = [0, 0], tag = $start)
3185  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3186"#;
3187        let result = parse_execute(code).await.unwrap();
3188        let errors = result.exec_state.errors();
3189        assert_eq!(errors.len(), 1);
3190        assert_eq!(errors[0].severity, Severity::Error);
3191        let msg = &errors[0].message;
3192        assert!(msg.contains("experimental"), "found {msg}");
3193
3194        let code = r#"@settings(experimentalFeatures = foo)
3195startSketchOn(XY)
3196  |> startProfile(at = [0, 0], tag = $start)
3197  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3198"#;
3199        parse_execute(code).await.unwrap_err();
3200    }
3201
3202    #[tokio::test(flavor = "multi_thread")]
3203    async fn experimental_parameter() {
3204        let code = r#"
3205fn inc(@x, @(experimental = true) amount? = 1) {
3206  return x + amount
3207}
3208
3209answer = inc(5, amount = 2)
3210"#;
3211        let result = parse_execute(code).await.unwrap();
3212        let errors = result.exec_state.errors();
3213        assert_eq!(errors.len(), 1);
3214        assert_eq!(errors[0].severity, Severity::Error);
3215        let msg = &errors[0].message;
3216        assert!(msg.contains("experimental"), "found {msg}");
3217
3218        // If the parameter isn't used, there's no warning.
3219        let code = r#"
3220fn inc(@x, @(experimental = true) amount? = 1) {
3221  return x + amount
3222}
3223
3224answer = inc(5)
3225"#;
3226        let result = parse_execute(code).await.unwrap();
3227        let errors = result.exec_state.errors();
3228        assert_eq!(errors.len(), 0);
3229    }
3230
3231    // START Mock Execution tests
3232    // Ideally, we would do this as part of all sim tests and delete these one-off tests.
3233
3234    #[tokio::test(flavor = "multi_thread")]
3235    async fn test_tangent_line_arc_executes_with_mock_engine() {
3236        let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
3237        parse_execute(&code).await.unwrap();
3238    }
3239
3240    #[tokio::test(flavor = "multi_thread")]
3241    async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
3242        let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
3243        parse_execute(&code).await.unwrap();
3244    }
3245
3246    #[tokio::test(flavor = "multi_thread")]
3247    async fn test_tangent_line_circle_executes_with_mock_engine() {
3248        let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
3249        parse_execute(&code).await.unwrap();
3250    }
3251
3252    #[tokio::test(flavor = "multi_thread")]
3253    async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
3254        let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
3255        parse_execute(&code).await.unwrap();
3256    }
3257
3258    // END Mock Execution tests
3259}