kcl_lib/execution/
mod.rs

1//! The executor for the AST.
2
3use std::sync::Arc;
4
5use anyhow::Result;
6#[cfg(feature = "artifact-graph")]
7pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketchOnFace, StartSketchOnPlane};
8use cache::GlobalState;
9pub use cache::{bust_cache, clear_mem_cache};
10#[cfg(feature = "artifact-graph")]
11pub use cad_op::Group;
12pub use cad_op::Operation;
13pub use geometry::*;
14pub use id_generator::IdGenerator;
15pub(crate) use import::PreImportedGeometry;
16use indexmap::IndexMap;
17pub use kcl_value::{KclObjectFields, KclValue};
18use kcmc::{
19    ImageFormat, ModelingCmd, each_cmd as mcmd,
20    ok_response::{OkModelingCmdResponse, output::TakeSnapshot},
21    websocket::{ModelingSessionData, OkWebSocketResponseData},
22};
23use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
24pub use memory::EnvironmentRef;
25pub(crate) use modeling::ModelingCmdMeta;
26use schemars::JsonSchema;
27use serde::{Deserialize, Serialize};
28pub(crate) use state::ModuleArtifactState;
29pub use state::{ExecState, MetaSettings};
30use uuid::Uuid;
31
32use crate::{
33    CompilationError, ExecError, KclErrorWithOutputs,
34    engine::{EngineManager, GridScaleBehavior},
35    errors::{KclError, KclErrorDetails},
36    execution::{
37        cache::{CacheInformation, CacheResult},
38        import_graph::{Universe, UniverseMap},
39        typed_path::TypedPath,
40        types::{UnitAngle, UnitLen},
41    },
42    fs::FileManager,
43    modules::{ModuleId, ModulePath, ModuleRepr},
44    parsing::ast::types::{Expr, ImportPath, NodeRef},
45    source_range::SourceRange,
46};
47
48pub(crate) mod annotations;
49#[cfg(feature = "artifact-graph")]
50mod artifact;
51pub(crate) mod cache;
52mod cad_op;
53mod exec_ast;
54pub mod fn_call;
55mod geometry;
56mod id_generator;
57mod import;
58mod import_graph;
59pub(crate) mod kcl_value;
60mod memory;
61mod modeling;
62mod state;
63pub mod typed_path;
64pub(crate) mod types;
65
66enum StatementKind<'a> {
67    Declaration { name: &'a str },
68    Expression,
69}
70
71/// Outcome of executing a program.  This is used in TS.
72#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
73#[ts(export)]
74#[serde(rename_all = "camelCase")]
75pub struct ExecOutcome {
76    /// Variables in the top-level of the root module. Note that functions will have an invalid env ref.
77    pub variables: IndexMap<String, KclValue>,
78    /// Operations that have been performed in execution order, for display in
79    /// the Feature Tree.
80    #[cfg(feature = "artifact-graph")]
81    pub operations: Vec<Operation>,
82    /// Output artifact graph.
83    #[cfg(feature = "artifact-graph")]
84    pub artifact_graph: ArtifactGraph,
85    /// Non-fatal errors and warnings.
86    pub errors: Vec<CompilationError>,
87    /// File Names in module Id array index order
88    pub filenames: IndexMap<ModuleId, ModulePath>,
89    /// The default planes.
90    pub default_planes: Option<DefaultPlanes>,
91}
92
93#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
94#[ts(export)]
95#[serde(rename_all = "camelCase")]
96pub struct DefaultPlanes {
97    pub xy: uuid::Uuid,
98    pub xz: uuid::Uuid,
99    pub yz: uuid::Uuid,
100    pub neg_xy: uuid::Uuid,
101    pub neg_xz: uuid::Uuid,
102    pub neg_yz: uuid::Uuid,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
106#[ts(export)]
107#[serde(tag = "type", rename_all = "camelCase")]
108pub struct TagIdentifier {
109    pub value: String,
110    // Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info
111    // was written.
112    #[serde(skip)]
113    pub info: Vec<(usize, TagEngineInfo)>,
114    #[serde(skip)]
115    pub meta: Vec<Metadata>,
116}
117
118impl TagIdentifier {
119    /// Get the tag info for this tag at a specified epoch.
120    pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
121        for (e, info) in self.info.iter().rev() {
122            if *e <= at_epoch {
123                return Some(info);
124            }
125        }
126
127        None
128    }
129
130    /// Get the most recent tag info for this tag.
131    pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
132        self.info.last().map(|i| &i.1)
133    }
134
135    /// Add info from a different instance of this tag.
136    pub fn merge_info(&mut self, other: &TagIdentifier) {
137        assert_eq!(&self.value, &other.value);
138        for (oe, ot) in &other.info {
139            if let Some((e, t)) = self.info.last_mut() {
140                // If there is newer info, then skip this iteration.
141                if *e > *oe {
142                    continue;
143                }
144                // If we're in the same epoch, then overwrite.
145                if e == oe {
146                    *t = ot.clone();
147                    continue;
148                }
149            }
150            self.info.push((*oe, ot.clone()));
151        }
152    }
153}
154
155impl Eq for TagIdentifier {}
156
157impl std::fmt::Display for TagIdentifier {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        write!(f, "{}", self.value)
160    }
161}
162
163impl std::str::FromStr for TagIdentifier {
164    type Err = KclError;
165
166    fn from_str(s: &str) -> Result<Self, Self::Err> {
167        Ok(Self {
168            value: s.to_string(),
169            info: Vec::new(),
170            meta: Default::default(),
171        })
172    }
173}
174
175impl Ord for TagIdentifier {
176    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
177        self.value.cmp(&other.value)
178    }
179}
180
181impl PartialOrd for TagIdentifier {
182    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
183        Some(self.cmp(other))
184    }
185}
186
187impl std::hash::Hash for TagIdentifier {
188    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
189        self.value.hash(state);
190    }
191}
192
193/// Engine information for a tag.
194#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
195#[ts(export)]
196#[serde(tag = "type", rename_all = "camelCase")]
197pub struct TagEngineInfo {
198    /// The id of the tagged object.
199    pub id: uuid::Uuid,
200    /// The sketch the tag is on.
201    pub sketch: uuid::Uuid,
202    /// The path the tag is on.
203    pub path: Option<Path>,
204    /// The surface information for the tag.
205    pub surface: Option<ExtrudeSurface>,
206}
207
208#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
209pub enum BodyType {
210    Root,
211    Block,
212}
213
214/// Metadata.
215#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
216#[ts(export)]
217#[serde(rename_all = "camelCase")]
218pub struct Metadata {
219    /// The source range.
220    pub source_range: SourceRange,
221}
222
223impl From<Metadata> for Vec<SourceRange> {
224    fn from(meta: Metadata) -> Self {
225        vec![meta.source_range]
226    }
227}
228
229impl From<SourceRange> for Metadata {
230    fn from(source_range: SourceRange) -> Self {
231        Self { source_range }
232    }
233}
234
235impl<T> From<NodeRef<'_, T>> for Metadata {
236    fn from(node: NodeRef<'_, T>) -> Self {
237        Self {
238            source_range: SourceRange::new(node.start, node.end, node.module_id),
239        }
240    }
241}
242
243impl From<&Expr> for Metadata {
244    fn from(expr: &Expr) -> Self {
245        Self {
246            source_range: SourceRange::from(expr),
247        }
248    }
249}
250
251/// The type of ExecutorContext being used
252#[derive(PartialEq, Debug, Default, Clone)]
253pub enum ContextType {
254    /// Live engine connection
255    #[default]
256    Live,
257
258    /// Completely mocked connection
259    /// Mock mode is only for the Design Studio when they just want to mock engine calls and not
260    /// actually make them.
261    Mock,
262
263    /// Handled by some other interpreter/conversion system
264    MockCustomForwarded,
265}
266
267/// The executor context.
268/// Cloning will return another handle to the same engine connection/session,
269/// as this uses `Arc` under the hood.
270#[derive(Debug, Clone)]
271pub struct ExecutorContext {
272    pub engine: Arc<Box<dyn EngineManager>>,
273    pub fs: Arc<FileManager>,
274    pub settings: ExecutorSettings,
275    pub context_type: ContextType,
276}
277
278/// The executor settings.
279#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
280#[ts(export)]
281pub struct ExecutorSettings {
282    /// Highlight edges of 3D objects?
283    pub highlight_edges: bool,
284    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
285    pub enable_ssao: bool,
286    /// Show grid?
287    pub show_grid: bool,
288    /// Should engine store this for replay?
289    /// If so, under what name?
290    pub replay: Option<String>,
291    /// The directory of the current project.  This is used for resolving import
292    /// paths.  If None is given, the current working directory is used.
293    pub project_directory: Option<TypedPath>,
294    /// This is the path to the current file being executed.
295    /// We use this for preventing cyclic imports.
296    pub current_file: Option<TypedPath>,
297    /// Whether or not to automatically scale the grid when user zooms.
298    pub fixed_size_grid: bool,
299}
300
301impl Default for ExecutorSettings {
302    fn default() -> Self {
303        Self {
304            highlight_edges: true,
305            enable_ssao: false,
306            show_grid: false,
307            replay: None,
308            project_directory: None,
309            current_file: None,
310            fixed_size_grid: true,
311        }
312    }
313}
314
315impl From<crate::settings::types::Configuration> for ExecutorSettings {
316    fn from(config: crate::settings::types::Configuration) -> Self {
317        Self::from(config.settings)
318    }
319}
320
321impl From<crate::settings::types::Settings> for ExecutorSettings {
322    fn from(settings: crate::settings::types::Settings) -> Self {
323        Self {
324            highlight_edges: settings.modeling.highlight_edges.into(),
325            enable_ssao: settings.modeling.enable_ssao.into(),
326            show_grid: settings.modeling.show_scale_grid,
327            replay: None,
328            project_directory: None,
329            current_file: None,
330            fixed_size_grid: settings.app.fixed_size_grid,
331        }
332    }
333}
334
335impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
336    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
337        Self::from(config.settings.modeling)
338    }
339}
340
341impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
342    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
343        Self {
344            highlight_edges: modeling.highlight_edges.into(),
345            enable_ssao: modeling.enable_ssao.into(),
346            show_grid: modeling.show_scale_grid,
347            replay: None,
348            project_directory: None,
349            current_file: None,
350            fixed_size_grid: true,
351        }
352    }
353}
354
355impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
356    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
357        Self {
358            highlight_edges: modeling.highlight_edges.into(),
359            enable_ssao: modeling.enable_ssao.into(),
360            show_grid: Default::default(),
361            replay: None,
362            project_directory: None,
363            current_file: None,
364            fixed_size_grid: true,
365        }
366    }
367}
368
369impl ExecutorSettings {
370    /// Add the current file path to the executor settings.
371    pub fn with_current_file(&mut self, current_file: TypedPath) {
372        // We want the parent directory of the file.
373        if current_file.extension() == Some("kcl") {
374            self.current_file = Some(current_file.clone());
375            // Get the parent directory.
376            if let Some(parent) = current_file.parent() {
377                self.project_directory = Some(parent);
378            } else {
379                self.project_directory = Some(TypedPath::from(""));
380            }
381        } else {
382            self.project_directory = Some(current_file.clone());
383        }
384    }
385}
386
387impl ExecutorContext {
388    /// Create a new default executor context.
389    #[cfg(not(target_arch = "wasm32"))]
390    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
391        let pool = std::env::var("ZOO_ENGINE_POOL").ok();
392        let (ws, _headers) = client
393            .modeling()
394            .commands_ws(
395                None,
396                None,
397                pool,
398                if settings.enable_ssao {
399                    Some(kittycad::types::PostEffectType::Ssao)
400                } else {
401                    None
402                },
403                settings.replay.clone(),
404                if settings.show_grid { Some(true) } else { None },
405                None,
406                None,
407                None,
408                Some(false),
409            )
410            .await?;
411
412        let engine: Arc<Box<dyn EngineManager>> =
413            Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
414
415        Ok(Self {
416            engine,
417            fs: Arc::new(FileManager::new()),
418            settings,
419            context_type: ContextType::Live,
420        })
421    }
422
423    #[cfg(target_arch = "wasm32")]
424    pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
425        ExecutorContext {
426            engine,
427            fs,
428            settings,
429            context_type: ContextType::Live,
430        }
431    }
432
433    #[cfg(not(target_arch = "wasm32"))]
434    pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
435        ExecutorContext {
436            engine: Arc::new(Box::new(
437                crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
438            )),
439            fs: Arc::new(FileManager::new()),
440            settings: settings.unwrap_or_default(),
441            context_type: ContextType::Mock,
442        }
443    }
444
445    #[cfg(target_arch = "wasm32")]
446    pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
447        ExecutorContext {
448            engine,
449            fs,
450            settings,
451            context_type: ContextType::Mock,
452        }
453    }
454
455    #[cfg(not(target_arch = "wasm32"))]
456    pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
457        ExecutorContext {
458            engine,
459            fs: Arc::new(FileManager::new()),
460            settings: Default::default(),
461            context_type: ContextType::MockCustomForwarded,
462        }
463    }
464
465    /// Create a new default executor context.
466    /// With a kittycad client.
467    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
468    /// variables.
469    /// But also allows for passing in a token and engine address directly.
470    #[cfg(not(target_arch = "wasm32"))]
471    pub async fn new_with_client(
472        settings: ExecutorSettings,
473        token: Option<String>,
474        engine_addr: Option<String>,
475    ) -> Result<Self> {
476        // Create the client.
477        let client = crate::engine::new_zoo_client(token, engine_addr)?;
478
479        let ctx = Self::new(&client, settings).await?;
480        Ok(ctx)
481    }
482
483    /// Create a new default executor context.
484    /// With the default kittycad client.
485    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
486    /// variables.
487    #[cfg(not(target_arch = "wasm32"))]
488    pub async fn new_with_default_client() -> Result<Self> {
489        // Create the client.
490        let ctx = Self::new_with_client(Default::default(), None, None).await?;
491        Ok(ctx)
492    }
493
494    /// For executing unit tests.
495    #[cfg(not(target_arch = "wasm32"))]
496    pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
497        let ctx = ExecutorContext::new_with_client(
498            ExecutorSettings {
499                highlight_edges: true,
500                enable_ssao: false,
501                show_grid: false,
502                replay: None,
503                project_directory: None,
504                current_file: None,
505                fixed_size_grid: false,
506            },
507            None,
508            engine_addr,
509        )
510        .await?;
511        Ok(ctx)
512    }
513
514    pub fn is_mock(&self) -> bool {
515        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
516    }
517
518    /// Returns true if we should not send engine commands for any reason.
519    pub async fn no_engine_commands(&self) -> bool {
520        self.is_mock()
521    }
522
523    pub async fn send_clear_scene(
524        &self,
525        exec_state: &mut ExecState,
526        source_range: crate::execution::SourceRange,
527    ) -> Result<(), KclError> {
528        // Ensure artifacts are cleared so that we don't accumulate them across
529        // runs.
530        exec_state.mod_local.artifacts.clear();
531        exec_state.global.root_module_artifacts.clear();
532        exec_state.global.artifacts.clear();
533
534        self.engine
535            .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
536            .await
537    }
538
539    pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
540        cache::bust_cache().await;
541
542        // Execute an empty program to clear and reset the scene.
543        // We specifically want to be returned the objects after the scene is reset.
544        // Like the default planes so it is easier to just execute an empty program
545        // after the cache is busted.
546        let outcome = self.run_with_caching(crate::Program::empty()).await?;
547
548        Ok(outcome)
549    }
550
551    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
552        self.eval_prelude(exec_state, SourceRange::synthetic())
553            .await
554            .map_err(KclErrorWithOutputs::no_outputs)?;
555        exec_state.mut_stack().push_new_root_env(true);
556        Ok(())
557    }
558
559    pub async fn run_mock(
560        &self,
561        program: &crate::Program,
562        use_prev_memory: bool,
563    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
564        assert!(
565            self.is_mock(),
566            "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
567        );
568
569        let mut exec_state = ExecState::new(self);
570        if use_prev_memory {
571            match cache::read_old_memory().await {
572                Some(mem) => {
573                    *exec_state.mut_stack() = mem.0;
574                    exec_state.global.module_infos = mem.1;
575                }
576                None => self.prepare_mem(&mut exec_state).await?,
577            }
578        } else {
579            self.prepare_mem(&mut exec_state).await?
580        };
581
582        // Push a scope so that old variables can be overwritten (since we might be re-executing some
583        // part of the scene).
584        exec_state.mut_stack().push_new_env_for_scope();
585
586        let result = self.inner_run(program, &mut exec_state, true).await?;
587
588        // Restore any temporary variables, then save any newly created variables back to
589        // memory in case another run wants to use them. Note this is just saved to the preserved
590        // memory, not to the exec_state which is not cached for mock execution.
591
592        let mut mem = exec_state.stack().clone();
593        let module_infos = exec_state.global.module_infos.clone();
594        let outcome = exec_state.into_exec_outcome(result.0, self).await;
595
596        mem.squash_env(result.0);
597        cache::write_old_memory((mem, module_infos)).await;
598
599        Ok(outcome)
600    }
601
602    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
603        assert!(!self.is_mock());
604        let grid_scale = if self.settings.fixed_size_grid {
605            GridScaleBehavior::Fixed(
606                program
607                    .meta_settings()
608                    .ok()
609                    .flatten()
610                    .map(|s| s.default_length_units)
611                    .map(kcmc::units::UnitLength::from),
612            )
613        } else {
614            GridScaleBehavior::ScaleWithZoom
615        };
616
617        let (program, exec_state, result) = match cache::read_old_ast().await {
618            Some(mut cached_state) => {
619                let old = CacheInformation {
620                    ast: &cached_state.main.ast,
621                    settings: &cached_state.settings,
622                };
623                let new = CacheInformation {
624                    ast: &program.ast,
625                    settings: &self.settings,
626                };
627
628                // Get the program that actually changed from the old and new information.
629                let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
630                    CacheResult::ReExecute {
631                        clear_scene,
632                        reapply_settings,
633                        program: changed_program,
634                    } => {
635                        if reapply_settings
636                            && self
637                                .engine
638                                .reapply_settings(
639                                    &self.settings,
640                                    Default::default(),
641                                    &mut cached_state.main.exec_state.id_generator,
642                                    grid_scale,
643                                )
644                                .await
645                                .is_err()
646                        {
647                            (true, program, None)
648                        } else {
649                            (
650                                clear_scene,
651                                crate::Program {
652                                    ast: changed_program,
653                                    original_file_contents: program.original_file_contents,
654                                },
655                                None,
656                            )
657                        }
658                    }
659                    CacheResult::CheckImportsOnly {
660                        reapply_settings,
661                        ast: changed_program,
662                    } => {
663                        if reapply_settings
664                            && self
665                                .engine
666                                .reapply_settings(
667                                    &self.settings,
668                                    Default::default(),
669                                    &mut cached_state.main.exec_state.id_generator,
670                                    grid_scale,
671                                )
672                                .await
673                                .is_err()
674                        {
675                            (true, program, None)
676                        } else {
677                            // We need to check our imports to see if they changed.
678                            let mut new_exec_state = ExecState::new(self);
679                            let (new_universe, new_universe_map) =
680                                self.get_universe(&program, &mut new_exec_state).await?;
681
682                            let clear_scene = new_universe.values().any(|value| {
683                                let id = value.1;
684                                match (
685                                    cached_state.exec_state.get_source(id),
686                                    new_exec_state.global.get_source(id),
687                                ) {
688                                    (Some(s0), Some(s1)) => s0.source != s1.source,
689                                    _ => false,
690                                }
691                            });
692
693                            if !clear_scene {
694                                // Return early we don't need to clear the scene.
695                                return Ok(cached_state.into_exec_outcome(self).await);
696                            }
697
698                            (
699                                true,
700                                crate::Program {
701                                    ast: changed_program,
702                                    original_file_contents: program.original_file_contents,
703                                },
704                                Some((new_universe, new_universe_map, new_exec_state)),
705                            )
706                        }
707                    }
708                    CacheResult::NoAction(true) => {
709                        if self
710                            .engine
711                            .reapply_settings(
712                                &self.settings,
713                                Default::default(),
714                                &mut cached_state.main.exec_state.id_generator,
715                                grid_scale,
716                            )
717                            .await
718                            .is_ok()
719                        {
720                            // We need to update the old ast state with the new settings!!
721                            cache::write_old_ast(GlobalState::with_settings(
722                                cached_state.clone(),
723                                self.settings.clone(),
724                            ))
725                            .await;
726
727                            return Ok(cached_state.into_exec_outcome(self).await);
728                        }
729                        (true, program, None)
730                    }
731                    CacheResult::NoAction(false) => {
732                        return Ok(cached_state.into_exec_outcome(self).await);
733                    }
734                };
735
736                let (exec_state, result) = match import_check_info {
737                    Some((new_universe, new_universe_map, mut new_exec_state)) => {
738                        // Clear the scene if the imports changed.
739                        self.send_clear_scene(&mut new_exec_state, Default::default())
740                            .await
741                            .map_err(KclErrorWithOutputs::no_outputs)?;
742
743                        let result = self
744                            .run_concurrent(
745                                &program,
746                                &mut new_exec_state,
747                                Some((new_universe, new_universe_map)),
748                                false,
749                            )
750                            .await;
751
752                        (new_exec_state, result)
753                    }
754                    None if clear_scene => {
755                        // Pop the execution state, since we are starting fresh.
756                        let mut exec_state = cached_state.reconstitute_exec_state();
757                        exec_state.reset(self);
758
759                        self.send_clear_scene(&mut exec_state, Default::default())
760                            .await
761                            .map_err(KclErrorWithOutputs::no_outputs)?;
762
763                        let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
764
765                        (exec_state, result)
766                    }
767                    None => {
768                        let mut exec_state = cached_state.reconstitute_exec_state();
769                        exec_state.mut_stack().restore_env(cached_state.main.result_env);
770
771                        let result = self.run_concurrent(&program, &mut exec_state, None, true).await;
772
773                        (exec_state, result)
774                    }
775                };
776
777                (program, exec_state, result)
778            }
779            None => {
780                let mut exec_state = ExecState::new(self);
781                self.send_clear_scene(&mut exec_state, Default::default())
782                    .await
783                    .map_err(KclErrorWithOutputs::no_outputs)?;
784
785                let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
786
787                (program, exec_state, result)
788            }
789        };
790
791        if result.is_err() {
792            cache::bust_cache().await;
793        }
794
795        // Throw the error.
796        let result = result?;
797
798        // Save this as the last successful execution to the cache.
799        cache::write_old_ast(GlobalState::new(
800            exec_state.clone(),
801            self.settings.clone(),
802            program.ast,
803            result.0,
804        ))
805        .await;
806
807        let outcome = exec_state.into_exec_outcome(result.0, self).await;
808        Ok(outcome)
809    }
810
811    /// Perform the execution of a program.
812    ///
813    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
814    pub async fn run(
815        &self,
816        program: &crate::Program,
817        exec_state: &mut ExecState,
818    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
819        self.run_concurrent(program, exec_state, None, false).await
820    }
821
822    /// Perform the execution of a program using a concurrent
823    /// execution model.
824    ///
825    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
826    pub async fn run_concurrent(
827        &self,
828        program: &crate::Program,
829        exec_state: &mut ExecState,
830        universe_info: Option<(Universe, UniverseMap)>,
831        preserve_mem: bool,
832    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
833        // Reuse our cached universe if we have one.
834
835        let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
836            (universe, universe_map)
837        } else {
838            self.get_universe(program, exec_state).await?
839        };
840
841        let default_planes = self.engine.get_default_planes().read().await.clone();
842
843        // Run the prelude to set up the engine.
844        self.eval_prelude(exec_state, SourceRange::synthetic())
845            .await
846            .map_err(KclErrorWithOutputs::no_outputs)?;
847
848        for modules in import_graph::import_graph(&universe, self)
849            .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
850            .into_iter()
851        {
852            #[cfg(not(target_arch = "wasm32"))]
853            let mut set = tokio::task::JoinSet::new();
854
855            #[allow(clippy::type_complexity)]
856            let (results_tx, mut results_rx): (
857                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
858                tokio::sync::mpsc::Receiver<_>,
859            ) = tokio::sync::mpsc::channel(1);
860
861            for module in modules {
862                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
863                    return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
864                        KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
865                    )));
866                };
867                let module_id = *module_id;
868                let module_path = module_path.clone();
869                let source_range = SourceRange::from(import_stmt);
870                // Clone before mutating.
871                let module_exec_state = exec_state.clone();
872
873                self.add_import_module_ops(
874                    exec_state,
875                    program,
876                    module_id,
877                    &module_path,
878                    source_range,
879                    &universe_map,
880                );
881
882                let repr = repr.clone();
883                let exec_ctxt = self.clone();
884                let results_tx = results_tx.clone();
885
886                let exec_module = async |exec_ctxt: &ExecutorContext,
887                                         repr: &ModuleRepr,
888                                         module_id: ModuleId,
889                                         module_path: &ModulePath,
890                                         exec_state: &mut ExecState,
891                                         source_range: SourceRange|
892                       -> Result<ModuleRepr, KclError> {
893                    match repr {
894                        ModuleRepr::Kcl(program, _) => {
895                            let result = exec_ctxt
896                                .exec_module_from_ast(program, module_id, module_path, exec_state, source_range, false)
897                                .await;
898
899                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
900                        }
901                        ModuleRepr::Foreign(geom, _) => {
902                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
903                                .await
904                                .map(|geom| Some(KclValue::ImportedGeometry(geom)));
905
906                            result.map(|val| {
907                                ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
908                            })
909                        }
910                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
911                            format!("Module {module_path} not found in universe"),
912                            vec![source_range],
913                        ))),
914                    }
915                };
916
917                #[cfg(target_arch = "wasm32")]
918                {
919                    wasm_bindgen_futures::spawn_local(async move {
920                        let mut exec_state = module_exec_state;
921                        let exec_ctxt = exec_ctxt;
922
923                        let result = exec_module(
924                            &exec_ctxt,
925                            &repr,
926                            module_id,
927                            &module_path,
928                            &mut exec_state,
929                            source_range,
930                        )
931                        .await;
932
933                        results_tx
934                            .send((module_id, module_path, result))
935                            .await
936                            .unwrap_or_default();
937                    });
938                }
939                #[cfg(not(target_arch = "wasm32"))]
940                {
941                    set.spawn(async move {
942                        let mut exec_state = module_exec_state;
943                        let exec_ctxt = exec_ctxt;
944
945                        let result = exec_module(
946                            &exec_ctxt,
947                            &repr,
948                            module_id,
949                            &module_path,
950                            &mut exec_state,
951                            source_range,
952                        )
953                        .await;
954
955                        results_tx
956                            .send((module_id, module_path, result))
957                            .await
958                            .unwrap_or_default();
959                    });
960                }
961            }
962
963            drop(results_tx);
964
965            while let Some((module_id, _, result)) = results_rx.recv().await {
966                match result {
967                    Ok(new_repr) => {
968                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
969
970                        match &mut repr {
971                            ModuleRepr::Kcl(_, cache) => {
972                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
973                                    unreachable!();
974                                };
975                                *cache = session_data;
976                            }
977                            ModuleRepr::Foreign(_, cache) => {
978                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
979                                    unreachable!();
980                                };
981                                *cache = session_data;
982                            }
983                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
984                        }
985
986                        exec_state.global.module_infos[&module_id].restore_repr(repr);
987                    }
988                    Err(e) => {
989                        return Err(exec_state.error_with_outputs(e, None, default_planes));
990                    }
991                }
992            }
993        }
994
995        // Since we haven't technically started executing the root module yet,
996        // the operations corresponding to the imports will be missing unless we
997        // track them here.
998        exec_state
999            .global
1000            .root_module_artifacts
1001            .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1002
1003        self.inner_run(program, exec_state, preserve_mem).await
1004    }
1005
1006    /// Get the universe & universe map of the program.
1007    /// And see if any of the imports changed.
1008    async fn get_universe(
1009        &self,
1010        program: &crate::Program,
1011        exec_state: &mut ExecState,
1012    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1013        exec_state.add_root_module_contents(program);
1014
1015        let mut universe = std::collections::HashMap::new();
1016
1017        let default_planes = self.engine.get_default_planes().read().await.clone();
1018
1019        let root_imports = import_graph::import_universe(
1020            self,
1021            &ModulePath::Main,
1022            &ModuleRepr::Kcl(program.ast.clone(), None),
1023            &mut universe,
1024            exec_state,
1025        )
1026        .await
1027        .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1028
1029        Ok((universe, root_imports))
1030    }
1031
1032    #[cfg(not(feature = "artifact-graph"))]
1033    fn add_import_module_ops(
1034        &self,
1035        _exec_state: &mut ExecState,
1036        _program: &crate::Program,
1037        _module_id: ModuleId,
1038        _module_path: &ModulePath,
1039        _source_range: SourceRange,
1040        _universe_map: &UniverseMap,
1041    ) {
1042    }
1043
1044    #[cfg(feature = "artifact-graph")]
1045    fn add_import_module_ops(
1046        &self,
1047        exec_state: &mut ExecState,
1048        program: &crate::Program,
1049        module_id: ModuleId,
1050        module_path: &ModulePath,
1051        source_range: SourceRange,
1052        universe_map: &UniverseMap,
1053    ) {
1054        match module_path {
1055            ModulePath::Main => {
1056                // This should never happen.
1057            }
1058            ModulePath::Local { value, .. } => {
1059                // We only want to display the top-level module imports in
1060                // the Feature Tree, not transitive imports.
1061                if universe_map.contains_key(value) {
1062                    use crate::NodePath;
1063
1064                    let node_path = if source_range.is_top_level_module() {
1065                        let cached_body_items = exec_state.global.artifacts.cached_body_items();
1066                        NodePath::from_range(&program.ast, cached_body_items, source_range).unwrap_or_default()
1067                    } else {
1068                        // The frontend doesn't care about paths in
1069                        // files other than the top-level module.
1070                        NodePath::placeholder()
1071                    };
1072
1073                    exec_state.push_op(Operation::GroupBegin {
1074                        group: Group::ModuleInstance {
1075                            name: value.file_name().unwrap_or_default(),
1076                            module_id,
1077                        },
1078                        node_path,
1079                        source_range,
1080                    });
1081                    // Due to concurrent execution, we cannot easily
1082                    // group operations by module. So we leave the
1083                    // group empty and close it immediately.
1084                    exec_state.push_op(Operation::GroupEnd);
1085                }
1086            }
1087            ModulePath::Std { .. } => {
1088                // We don't want to display stdlib in the Feature Tree.
1089            }
1090        }
1091    }
1092
1093    /// Perform the execution of a program.  Accept all possible parameters and
1094    /// output everything.
1095    async fn inner_run(
1096        &self,
1097        program: &crate::Program,
1098        exec_state: &mut ExecState,
1099        preserve_mem: bool,
1100    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1101        let _stats = crate::log::LogPerfStats::new("Interpretation");
1102
1103        // Re-apply the settings, in case the cache was busted.
1104        let grid_scale = if self.settings.fixed_size_grid {
1105            GridScaleBehavior::Fixed(
1106                program
1107                    .meta_settings()
1108                    .ok()
1109                    .flatten()
1110                    .map(|s| s.default_length_units)
1111                    .map(kcmc::units::UnitLength::from),
1112            )
1113        } else {
1114            GridScaleBehavior::ScaleWithZoom
1115        };
1116        self.engine
1117            .reapply_settings(
1118                &self.settings,
1119                Default::default(),
1120                exec_state.id_generator(),
1121                grid_scale,
1122            )
1123            .await
1124            .map_err(KclErrorWithOutputs::no_outputs)?;
1125
1126        let default_planes = self.engine.get_default_planes().read().await.clone();
1127        let result = self
1128            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1129            .await;
1130
1131        crate::log::log(format!(
1132            "Post interpretation KCL memory stats: {:#?}",
1133            exec_state.stack().memory.stats
1134        ));
1135        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1136
1137        let env_ref = result.map_err(|(err, env_ref)| exec_state.error_with_outputs(err, env_ref, default_planes))?;
1138
1139        if !self.is_mock() {
1140            let mut mem = exec_state.stack().deep_clone();
1141            mem.restore_env(env_ref);
1142            cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
1143        }
1144        let session_data = self.engine.get_session_data().await;
1145
1146        Ok((env_ref, session_data))
1147    }
1148
1149    /// Execute an AST's program and build auxiliary outputs like the artifact
1150    /// graph.
1151    async fn execute_and_build_graph(
1152        &self,
1153        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1154        exec_state: &mut ExecState,
1155        preserve_mem: bool,
1156    ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1157        // Don't early return!  We need to build other outputs regardless of
1158        // whether execution failed.
1159
1160        // Because of execution caching, we may start with operations from a
1161        // previous run.
1162        #[cfg(feature = "artifact-graph")]
1163        let start_op = exec_state.global.root_module_artifacts.operations.len();
1164
1165        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1166            .await
1167            .map_err(|e| (e, None))?;
1168
1169        let exec_result = self
1170            .exec_module_body(
1171                program,
1172                exec_state,
1173                preserve_mem,
1174                ModuleId::default(),
1175                &ModulePath::Main,
1176            )
1177            .await
1178            .map(|(_, env_ref, _, module_artifacts)| {
1179                // We need to extend because it may already have operations from
1180                // imports.
1181                exec_state.global.root_module_artifacts.extend(module_artifacts);
1182                env_ref
1183            })
1184            .map_err(|(err, env_ref, module_artifacts)| {
1185                if let Some(module_artifacts) = module_artifacts {
1186                    // We need to extend because it may already have operations
1187                    // from imports.
1188                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1189                }
1190                (err, env_ref)
1191            });
1192
1193        #[cfg(feature = "artifact-graph")]
1194        {
1195            // Fill in NodePath for operations.
1196            let cached_body_items = exec_state.global.artifacts.cached_body_items();
1197            for op in exec_state
1198                .global
1199                .root_module_artifacts
1200                .operations
1201                .iter_mut()
1202                .skip(start_op)
1203            {
1204                op.fill_node_paths(program, cached_body_items);
1205            }
1206            for module in exec_state.global.module_infos.values_mut() {
1207                if let ModuleRepr::Kcl(_, Some((_, _, _, module_artifacts))) = &mut module.repr {
1208                    for op in &mut module_artifacts.operations {
1209                        op.fill_node_paths(program, cached_body_items);
1210                    }
1211                }
1212            }
1213        }
1214
1215        // Ensure all the async commands completed.
1216        self.engine.ensure_async_commands_completed().await.map_err(|e| {
1217            match &exec_result {
1218                Ok(env_ref) => (e, Some(*env_ref)),
1219                // Prefer the execution error.
1220                Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1221            }
1222        })?;
1223
1224        // If we errored out and early-returned, there might be commands which haven't been executed
1225        // and should be dropped.
1226        self.engine.clear_queues().await;
1227
1228        match exec_state.build_artifact_graph(&self.engine, program).await {
1229            Ok(_) => exec_result,
1230            Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1231        }
1232    }
1233
1234    /// 'Import' std::prelude as the outermost scope.
1235    ///
1236    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
1237    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1238        if exec_state.stack().memory.requires_std() {
1239            #[cfg(feature = "artifact-graph")]
1240            let initial_ops = exec_state.mod_local.artifacts.operations.len();
1241
1242            let path = vec!["std".to_owned(), "prelude".to_owned()];
1243            let resolved_path = ModulePath::from_std_import_path(&path)?;
1244            let id = self
1245                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1246                .await?;
1247            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1248
1249            exec_state.mut_stack().memory.set_std(module_memory);
1250
1251            // Operations generated by the prelude are not useful, so clear them
1252            // out.
1253            //
1254            // TODO: Should we also clear them out of each module so that they
1255            // don't appear in test output?
1256            #[cfg(feature = "artifact-graph")]
1257            exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1258        }
1259
1260        Ok(())
1261    }
1262
1263    /// Get a snapshot of the current scene.
1264    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1265        // Zoom to fit.
1266        self.engine
1267            .send_modeling_cmd(
1268                uuid::Uuid::new_v4(),
1269                crate::execution::SourceRange::default(),
1270                &ModelingCmd::from(mcmd::ZoomToFit {
1271                    object_ids: Default::default(),
1272                    animated: false,
1273                    padding: 0.1,
1274                }),
1275            )
1276            .await
1277            .map_err(KclErrorWithOutputs::no_outputs)?;
1278
1279        // Send a snapshot request to the engine.
1280        let resp = self
1281            .engine
1282            .send_modeling_cmd(
1283                uuid::Uuid::new_v4(),
1284                crate::execution::SourceRange::default(),
1285                &ModelingCmd::from(mcmd::TakeSnapshot {
1286                    format: ImageFormat::Png,
1287                }),
1288            )
1289            .await
1290            .map_err(KclErrorWithOutputs::no_outputs)?;
1291
1292        let OkWebSocketResponseData::Modeling {
1293            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1294        } = resp
1295        else {
1296            return Err(ExecError::BadPng(format!(
1297                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1298            )));
1299        };
1300        Ok(contents)
1301    }
1302
1303    /// Export the current scene as a CAD file.
1304    pub async fn export(
1305        &self,
1306        format: kittycad_modeling_cmds::format::OutputFormat3d,
1307    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1308        let resp = self
1309            .engine
1310            .send_modeling_cmd(
1311                uuid::Uuid::new_v4(),
1312                crate::SourceRange::default(),
1313                &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
1314                    entity_ids: vec![],
1315                    format,
1316                }),
1317            )
1318            .await?;
1319
1320        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1321            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1322                format!("Expected Export response, got {resp:?}",),
1323                vec![SourceRange::default()],
1324            )));
1325        };
1326
1327        Ok(files)
1328    }
1329
1330    /// Export the current scene as a STEP file.
1331    pub async fn export_step(
1332        &self,
1333        deterministic_time: bool,
1334    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1335        let files = self
1336            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1337                kittycad_modeling_cmds::format::step::export::Options {
1338                    coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1339                    created: if deterministic_time {
1340                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1341                            KclError::new_internal(crate::errors::KclErrorDetails::new(
1342                                format!("Failed to parse date: {e}"),
1343                                vec![SourceRange::default()],
1344                            ))
1345                        })?)
1346                    } else {
1347                        None
1348                    },
1349                },
1350            ))
1351            .await?;
1352
1353        Ok(files)
1354    }
1355
1356    pub async fn close(&self) {
1357        self.engine.close().await;
1358    }
1359}
1360
1361#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS, JsonSchema)]
1362pub struct ArtifactId(Uuid);
1363
1364impl ArtifactId {
1365    pub fn new(uuid: Uuid) -> Self {
1366        Self(uuid)
1367    }
1368}
1369
1370impl From<Uuid> for ArtifactId {
1371    fn from(uuid: Uuid) -> Self {
1372        Self::new(uuid)
1373    }
1374}
1375
1376impl From<&Uuid> for ArtifactId {
1377    fn from(uuid: &Uuid) -> Self {
1378        Self::new(*uuid)
1379    }
1380}
1381
1382impl From<ArtifactId> for Uuid {
1383    fn from(id: ArtifactId) -> Self {
1384        id.0
1385    }
1386}
1387
1388impl From<&ArtifactId> for Uuid {
1389    fn from(id: &ArtifactId) -> Self {
1390        id.0
1391    }
1392}
1393
1394impl From<ModelingCmdId> for ArtifactId {
1395    fn from(id: ModelingCmdId) -> Self {
1396        Self::new(*id.as_ref())
1397    }
1398}
1399
1400impl From<&ModelingCmdId> for ArtifactId {
1401    fn from(id: &ModelingCmdId) -> Self {
1402        Self::new(*id.as_ref())
1403    }
1404}
1405
1406#[cfg(test)]
1407pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1408    parse_execute_with_project_dir(code, None).await
1409}
1410
1411#[cfg(test)]
1412pub(crate) async fn parse_execute_with_project_dir(
1413    code: &str,
1414    project_directory: Option<TypedPath>,
1415) -> Result<ExecTestResults, KclError> {
1416    let program = crate::Program::parse_no_errs(code)?;
1417
1418    let exec_ctxt = ExecutorContext {
1419        engine: Arc::new(Box::new(
1420            crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
1421                KclError::new_internal(crate::errors::KclErrorDetails::new(
1422                    format!("Failed to create mock engine connection: {err}"),
1423                    vec![SourceRange::default()],
1424                ))
1425            })?,
1426        )),
1427        fs: Arc::new(crate::fs::FileManager::new()),
1428        settings: ExecutorSettings {
1429            project_directory,
1430            ..Default::default()
1431        },
1432        context_type: ContextType::Mock,
1433    };
1434    let mut exec_state = ExecState::new(&exec_ctxt);
1435    let result = exec_ctxt.run(&program, &mut exec_state).await?;
1436
1437    Ok(ExecTestResults {
1438        program,
1439        mem_env: result.0,
1440        exec_ctxt,
1441        exec_state,
1442    })
1443}
1444
1445#[cfg(test)]
1446#[derive(Debug)]
1447pub(crate) struct ExecTestResults {
1448    program: crate::Program,
1449    mem_env: EnvironmentRef,
1450    exec_ctxt: ExecutorContext,
1451    exec_state: ExecState,
1452}
1453
1454#[cfg(test)]
1455mod tests {
1456    use pretty_assertions::assert_eq;
1457
1458    use super::*;
1459    use crate::{
1460        ModuleId,
1461        errors::{KclErrorDetails, Severity},
1462        exec::NumericType,
1463        execution::{memory::Stack, types::RuntimeType},
1464    };
1465
1466    /// Convenience function to get a JSON value from memory and unwrap.
1467    #[track_caller]
1468    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1469        memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1470    }
1471
1472    #[tokio::test(flavor = "multi_thread")]
1473    async fn test_execute_warn() {
1474        let text = "@blah";
1475        let result = parse_execute(text).await.unwrap();
1476        let errs = result.exec_state.errors();
1477        assert_eq!(errs.len(), 1);
1478        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1479        assert!(
1480            errs[0].message.contains("Unknown annotation"),
1481            "unexpected warning message: {}",
1482            errs[0].message
1483        );
1484    }
1485
1486    #[tokio::test(flavor = "multi_thread")]
1487    async fn test_execute_fn_definitions() {
1488        let ast = r#"fn def(@x) {
1489  return x
1490}
1491fn ghi(@x) {
1492  return x
1493}
1494fn jkl(@x) {
1495  return x
1496}
1497fn hmm(@x) {
1498  return x
1499}
1500
1501yo = 5 + 6
1502
1503abc = 3
1504identifierGuy = 5
1505part001 = startSketchOn(XY)
1506|> startProfile(at = [-1.2, 4.83])
1507|> line(end = [2.8, 0])
1508|> angledLine(angle = 100 + 100, length = 3.01)
1509|> angledLine(angle = abc, length = 3.02)
1510|> angledLine(angle = def(yo), length = 3.03)
1511|> angledLine(angle = ghi(2), length = 3.04)
1512|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1513|> close()
1514yo2 = hmm([identifierGuy + 5])"#;
1515
1516        parse_execute(ast).await.unwrap();
1517    }
1518
1519    #[tokio::test(flavor = "multi_thread")]
1520    async fn test_execute_with_pipe_substitutions_unary() {
1521        let ast = r#"myVar = 3
1522part001 = startSketchOn(XY)
1523  |> startProfile(at = [0, 0])
1524  |> line(end = [3, 4], tag = $seg01)
1525  |> line(end = [
1526  min([segLen(seg01), myVar]),
1527  -legLen(hypotenuse = segLen(seg01), leg = myVar)
1528])
1529"#;
1530
1531        parse_execute(ast).await.unwrap();
1532    }
1533
1534    #[tokio::test(flavor = "multi_thread")]
1535    async fn test_execute_with_pipe_substitutions() {
1536        let ast = r#"myVar = 3
1537part001 = startSketchOn(XY)
1538  |> startProfile(at = [0, 0])
1539  |> line(end = [3, 4], tag = $seg01)
1540  |> line(end = [
1541  min([segLen(seg01), myVar]),
1542  legLen(hypotenuse = segLen(seg01), leg = myVar)
1543])
1544"#;
1545
1546        parse_execute(ast).await.unwrap();
1547    }
1548
1549    #[tokio::test(flavor = "multi_thread")]
1550    async fn test_execute_with_inline_comment() {
1551        let ast = r#"baseThick = 1
1552armAngle = 60
1553
1554baseThickHalf = baseThick / 2
1555halfArmAngle = armAngle / 2
1556
1557arrExpShouldNotBeIncluded = [1, 2, 3]
1558objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1559
1560part001 = startSketchOn(XY)
1561  |> startProfile(at = [0, 0])
1562  |> yLine(endAbsolute = 1)
1563  |> xLine(length = 3.84) // selection-range-7ish-before-this
1564
1565variableBelowShouldNotBeIncluded = 3
1566"#;
1567
1568        parse_execute(ast).await.unwrap();
1569    }
1570
1571    #[tokio::test(flavor = "multi_thread")]
1572    async fn test_execute_with_function_literal_in_pipe() {
1573        let ast = r#"w = 20
1574l = 8
1575h = 10
1576
1577fn thing() {
1578  return -8
1579}
1580
1581firstExtrude = startSketchOn(XY)
1582  |> startProfile(at = [0,0])
1583  |> line(end = [0, l])
1584  |> line(end = [w, 0])
1585  |> line(end = [0, thing()])
1586  |> close()
1587  |> extrude(length = h)"#;
1588
1589        parse_execute(ast).await.unwrap();
1590    }
1591
1592    #[tokio::test(flavor = "multi_thread")]
1593    async fn test_execute_with_function_unary_in_pipe() {
1594        let ast = r#"w = 20
1595l = 8
1596h = 10
1597
1598fn thing(@x) {
1599  return -x
1600}
1601
1602firstExtrude = startSketchOn(XY)
1603  |> startProfile(at = [0,0])
1604  |> line(end = [0, l])
1605  |> line(end = [w, 0])
1606  |> line(end = [0, thing(8)])
1607  |> close()
1608  |> extrude(length = h)"#;
1609
1610        parse_execute(ast).await.unwrap();
1611    }
1612
1613    #[tokio::test(flavor = "multi_thread")]
1614    async fn test_execute_with_function_array_in_pipe() {
1615        let ast = r#"w = 20
1616l = 8
1617h = 10
1618
1619fn thing(@x) {
1620  return [0, -x]
1621}
1622
1623firstExtrude = startSketchOn(XY)
1624  |> startProfile(at = [0,0])
1625  |> line(end = [0, l])
1626  |> line(end = [w, 0])
1627  |> line(end = thing(8))
1628  |> close()
1629  |> extrude(length = h)"#;
1630
1631        parse_execute(ast).await.unwrap();
1632    }
1633
1634    #[tokio::test(flavor = "multi_thread")]
1635    async fn test_execute_with_function_call_in_pipe() {
1636        let ast = r#"w = 20
1637l = 8
1638h = 10
1639
1640fn other_thing(@y) {
1641  return -y
1642}
1643
1644fn thing(@x) {
1645  return other_thing(x)
1646}
1647
1648firstExtrude = startSketchOn(XY)
1649  |> startProfile(at = [0,0])
1650  |> line(end = [0, l])
1651  |> line(end = [w, 0])
1652  |> line(end = [0, thing(8)])
1653  |> close()
1654  |> extrude(length = h)"#;
1655
1656        parse_execute(ast).await.unwrap();
1657    }
1658
1659    #[tokio::test(flavor = "multi_thread")]
1660    async fn test_execute_with_function_sketch() {
1661        let ast = r#"fn box(h, l, w) {
1662 myBox = startSketchOn(XY)
1663    |> startProfile(at = [0,0])
1664    |> line(end = [0, l])
1665    |> line(end = [w, 0])
1666    |> line(end = [0, -l])
1667    |> close()
1668    |> extrude(length = h)
1669
1670  return myBox
1671}
1672
1673fnBox = box(h = 3, l = 6, w = 10)"#;
1674
1675        parse_execute(ast).await.unwrap();
1676    }
1677
1678    #[tokio::test(flavor = "multi_thread")]
1679    async fn test_get_member_of_object_with_function_period() {
1680        let ast = r#"fn box(@obj) {
1681 myBox = startSketchOn(XY)
1682    |> startProfile(at = obj.start)
1683    |> line(end = [0, obj.l])
1684    |> line(end = [obj.w, 0])
1685    |> line(end = [0, -obj.l])
1686    |> close()
1687    |> extrude(length = obj.h)
1688
1689  return myBox
1690}
1691
1692thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1693"#;
1694        parse_execute(ast).await.unwrap();
1695    }
1696
1697    #[tokio::test(flavor = "multi_thread")]
1698    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
1699    async fn test_object_member_starting_pipeline() {
1700        let ast = r#"
1701fn test2() {
1702  return {
1703    thing: startSketchOn(XY)
1704      |> startProfile(at = [0, 0])
1705      |> line(end = [0, 1])
1706      |> line(end = [1, 0])
1707      |> line(end = [0, -1])
1708      |> close()
1709  }
1710}
1711
1712x2 = test2()
1713
1714x2.thing
1715  |> extrude(length = 10)
1716"#;
1717        parse_execute(ast).await.unwrap();
1718    }
1719
1720    #[tokio::test(flavor = "multi_thread")]
1721    #[ignore] // ignore til we get loops
1722    async fn test_execute_with_function_sketch_loop_objects() {
1723        let ast = r#"fn box(obj) {
1724let myBox = startSketchOn(XY)
1725    |> startProfile(at = obj.start)
1726    |> line(end = [0, obj.l])
1727    |> line(end = [obj.w, 0])
1728    |> line(end = [0, -obj.l])
1729    |> close()
1730    |> extrude(length = obj.h)
1731
1732  return myBox
1733}
1734
1735for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1736  thisBox = box(var)
1737}"#;
1738
1739        parse_execute(ast).await.unwrap();
1740    }
1741
1742    #[tokio::test(flavor = "multi_thread")]
1743    #[ignore] // ignore til we get loops
1744    async fn test_execute_with_function_sketch_loop_array() {
1745        let ast = r#"fn box(h, l, w, start) {
1746 myBox = startSketchOn(XY)
1747    |> startProfile(at = [0,0])
1748    |> line(end = [0, l])
1749    |> line(end = [w, 0])
1750    |> line(end = [0, -l])
1751    |> close()
1752    |> extrude(length = h)
1753
1754  return myBox
1755}
1756
1757
1758for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1759  const thisBox = box(var[0], var[1], var[2], var[3])
1760}"#;
1761
1762        parse_execute(ast).await.unwrap();
1763    }
1764
1765    #[tokio::test(flavor = "multi_thread")]
1766    async fn test_get_member_of_array_with_function() {
1767        let ast = r#"fn box(@arr) {
1768 myBox =startSketchOn(XY)
1769    |> startProfile(at = arr[0])
1770    |> line(end = [0, arr[1]])
1771    |> line(end = [arr[2], 0])
1772    |> line(end = [0, -arr[1]])
1773    |> close()
1774    |> extrude(length = arr[3])
1775
1776  return myBox
1777}
1778
1779thisBox = box([[0,0], 6, 10, 3])
1780
1781"#;
1782        parse_execute(ast).await.unwrap();
1783    }
1784
1785    #[tokio::test(flavor = "multi_thread")]
1786    async fn test_function_cannot_access_future_definitions() {
1787        let ast = r#"
1788fn returnX() {
1789  // x shouldn't be defined yet.
1790  return x
1791}
1792
1793x = 5
1794
1795answer = returnX()"#;
1796
1797        let result = parse_execute(ast).await;
1798        let err = result.unwrap_err();
1799        assert_eq!(err.message(), "`x` is not defined");
1800    }
1801
1802    #[tokio::test(flavor = "multi_thread")]
1803    async fn test_override_prelude() {
1804        let text = "PI = 3.0";
1805        let result = parse_execute(text).await.unwrap();
1806        let errs = result.exec_state.errors();
1807        assert!(errs.is_empty());
1808    }
1809
1810    #[tokio::test(flavor = "multi_thread")]
1811    async fn type_aliases() {
1812        let text = r#"@settings(experimentalFeatures = allow)
1813type MyTy = [number; 2]
1814fn foo(@x: MyTy) {
1815    return x[0]
1816}
1817
1818foo([0, 1])
1819
1820type Other = MyTy | Helix
1821"#;
1822        let result = parse_execute(text).await.unwrap();
1823        let errs = result.exec_state.errors();
1824        assert!(errs.is_empty());
1825    }
1826
1827    #[tokio::test(flavor = "multi_thread")]
1828    async fn test_cannot_shebang_in_fn() {
1829        let ast = r#"
1830fn foo() {
1831  #!hello
1832  return true
1833}
1834
1835foo
1836"#;
1837
1838        let result = parse_execute(ast).await;
1839        let err = result.unwrap_err();
1840        assert_eq!(
1841            err,
1842            KclError::new_syntax(KclErrorDetails::new(
1843                "Unexpected token: #".to_owned(),
1844                vec![SourceRange::new(14, 15, ModuleId::default())],
1845            )),
1846        );
1847    }
1848
1849    #[tokio::test(flavor = "multi_thread")]
1850    async fn test_pattern_transform_function_cannot_access_future_definitions() {
1851        let ast = r#"
1852fn transform(@replicaId) {
1853  // x shouldn't be defined yet.
1854  scale = x
1855  return {
1856    translate = [0, 0, replicaId * 10],
1857    scale = [scale, 1, 0],
1858  }
1859}
1860
1861fn layer() {
1862  return startSketchOn(XY)
1863    |> circle( center= [0, 0], radius= 1, tag = $tag1)
1864    |> extrude(length = 10)
1865}
1866
1867x = 5
1868
1869// The 10 layers are replicas of each other, with a transform applied to each.
1870shape = layer() |> patternTransform(instances = 10, transform = transform)
1871"#;
1872
1873        let result = parse_execute(ast).await;
1874        let err = result.unwrap_err();
1875        assert_eq!(err.message(), "`x` is not defined",);
1876    }
1877
1878    // ADAM: Move some of these into simulation tests.
1879
1880    #[tokio::test(flavor = "multi_thread")]
1881    async fn test_math_execute_with_functions() {
1882        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
1883        let result = parse_execute(ast).await.unwrap();
1884        assert_eq!(
1885            5.0,
1886            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1887                .as_f64()
1888                .unwrap()
1889        );
1890    }
1891
1892    #[tokio::test(flavor = "multi_thread")]
1893    async fn test_math_execute() {
1894        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
1895        let result = parse_execute(ast).await.unwrap();
1896        assert_eq!(
1897            7.4,
1898            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1899                .as_f64()
1900                .unwrap()
1901        );
1902    }
1903
1904    #[tokio::test(flavor = "multi_thread")]
1905    async fn test_math_execute_start_negative() {
1906        let ast = r#"myVar = -5 + 6"#;
1907        let result = parse_execute(ast).await.unwrap();
1908        assert_eq!(
1909            1.0,
1910            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1911                .as_f64()
1912                .unwrap()
1913        );
1914    }
1915
1916    #[tokio::test(flavor = "multi_thread")]
1917    async fn test_math_execute_with_pi() {
1918        let ast = r#"myVar = PI * 2"#;
1919        let result = parse_execute(ast).await.unwrap();
1920        assert_eq!(
1921            std::f64::consts::TAU,
1922            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1923                .as_f64()
1924                .unwrap()
1925        );
1926    }
1927
1928    #[tokio::test(flavor = "multi_thread")]
1929    async fn test_math_define_decimal_without_leading_zero() {
1930        let ast = r#"thing = .4 + 7"#;
1931        let result = parse_execute(ast).await.unwrap();
1932        assert_eq!(
1933            7.4,
1934            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
1935                .as_f64()
1936                .unwrap()
1937        );
1938    }
1939
1940    #[tokio::test(flavor = "multi_thread")]
1941    async fn pass_std_to_std() {
1942        let ast = r#"sketch001 = startSketchOn(XY)
1943profile001 = circle(sketch001, center = [0, 0], radius = 2)
1944extrude001 = extrude(profile001, length = 5)
1945extrudes = patternLinear3d(
1946  extrude001,
1947  instances = 3,
1948  distance = 5,
1949  axis = [1, 1, 0],
1950)
1951clone001 = map(extrudes, f = clone)
1952"#;
1953        parse_execute(ast).await.unwrap();
1954    }
1955
1956    #[tokio::test(flavor = "multi_thread")]
1957    async fn test_array_reduce_nested_array() {
1958        let code = r#"
1959fn id(@el, accum)  { return accum }
1960
1961answer = reduce([], initial=[[[0,0]]], f=id)
1962"#;
1963        let result = parse_execute(code).await.unwrap();
1964        assert_eq!(
1965            mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
1966            KclValue::HomArray {
1967                value: vec![KclValue::HomArray {
1968                    value: vec![KclValue::HomArray {
1969                        value: vec![
1970                            KclValue::Number {
1971                                value: 0.0,
1972                                ty: NumericType::default(),
1973                                meta: vec![SourceRange::new(69, 70, Default::default()).into()],
1974                            },
1975                            KclValue::Number {
1976                                value: 0.0,
1977                                ty: NumericType::default(),
1978                                meta: vec![SourceRange::new(71, 72, Default::default()).into()],
1979                            }
1980                        ],
1981                        ty: RuntimeType::any(),
1982                    }],
1983                    ty: RuntimeType::any(),
1984                }],
1985                ty: RuntimeType::any(),
1986            }
1987        );
1988    }
1989
1990    #[tokio::test(flavor = "multi_thread")]
1991    async fn test_zero_param_fn() {
1992        let ast = r#"sigmaAllow = 35000 // psi
1993leg1 = 5 // inches
1994leg2 = 8 // inches
1995fn thickness() { return 0.56 }
1996
1997bracket = startSketchOn(XY)
1998  |> startProfile(at = [0,0])
1999  |> line(end = [0, leg1])
2000  |> line(end = [leg2, 0])
2001  |> line(end = [0, -thickness()])
2002  |> line(end = [-leg2 + thickness(), 0])
2003"#;
2004        parse_execute(ast).await.unwrap();
2005    }
2006
2007    #[tokio::test(flavor = "multi_thread")]
2008    async fn test_unary_operator_not_succeeds() {
2009        let ast = r#"
2010fn returnTrue() { return !false }
2011t = true
2012f = false
2013notTrue = !t
2014notFalse = !f
2015c = !!true
2016d = !returnTrue()
2017
2018assertIs(!false, error = "expected to pass")
2019
2020fn check(x) {
2021  assertIs(!x, error = "expected argument to be false")
2022  return true
2023}
2024check(x = false)
2025"#;
2026        let result = parse_execute(ast).await.unwrap();
2027        assert_eq!(
2028            false,
2029            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2030                .as_bool()
2031                .unwrap()
2032        );
2033        assert_eq!(
2034            true,
2035            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2036                .as_bool()
2037                .unwrap()
2038        );
2039        assert_eq!(
2040            true,
2041            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2042                .as_bool()
2043                .unwrap()
2044        );
2045        assert_eq!(
2046            false,
2047            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2048                .as_bool()
2049                .unwrap()
2050        );
2051    }
2052
2053    #[tokio::test(flavor = "multi_thread")]
2054    async fn test_unary_operator_not_on_non_bool_fails() {
2055        let code1 = r#"
2056// Yup, this is null.
2057myNull = 0 / 0
2058notNull = !myNull
2059"#;
2060        assert_eq!(
2061            parse_execute(code1).await.unwrap_err().message(),
2062            "Cannot apply unary operator ! to non-boolean value: a number",
2063        );
2064
2065        let code2 = "notZero = !0";
2066        assert_eq!(
2067            parse_execute(code2).await.unwrap_err().message(),
2068            "Cannot apply unary operator ! to non-boolean value: a number",
2069        );
2070
2071        let code3 = r#"
2072notEmptyString = !""
2073"#;
2074        assert_eq!(
2075            parse_execute(code3).await.unwrap_err().message(),
2076            "Cannot apply unary operator ! to non-boolean value: a string",
2077        );
2078
2079        let code4 = r#"
2080obj = { a = 1 }
2081notMember = !obj.a
2082"#;
2083        assert_eq!(
2084            parse_execute(code4).await.unwrap_err().message(),
2085            "Cannot apply unary operator ! to non-boolean value: a number",
2086        );
2087
2088        let code5 = "
2089a = []
2090notArray = !a";
2091        assert_eq!(
2092            parse_execute(code5).await.unwrap_err().message(),
2093            "Cannot apply unary operator ! to non-boolean value: an empty array",
2094        );
2095
2096        let code6 = "
2097x = {}
2098notObject = !x";
2099        assert_eq!(
2100            parse_execute(code6).await.unwrap_err().message(),
2101            "Cannot apply unary operator ! to non-boolean value: an object",
2102        );
2103
2104        let code7 = "
2105fn x() { return 1 }
2106notFunction = !x";
2107        let fn_err = parse_execute(code7).await.unwrap_err();
2108        // These are currently printed out as JSON objects, so we don't want to
2109        // check the full error.
2110        assert!(
2111            fn_err
2112                .message()
2113                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2114            "Actual error: {fn_err:?}"
2115        );
2116
2117        let code8 = "
2118myTagDeclarator = $myTag
2119notTagDeclarator = !myTagDeclarator";
2120        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2121        // These are currently printed out as JSON objects, so we don't want to
2122        // check the full error.
2123        assert!(
2124            tag_declarator_err
2125                .message()
2126                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2127            "Actual error: {tag_declarator_err:?}"
2128        );
2129
2130        let code9 = "
2131myTagDeclarator = $myTag
2132notTagIdentifier = !myTag";
2133        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2134        // These are currently printed out as JSON objects, so we don't want to
2135        // check the full error.
2136        assert!(
2137            tag_identifier_err
2138                .message()
2139                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2140            "Actual error: {tag_identifier_err:?}"
2141        );
2142
2143        let code10 = "notPipe = !(1 |> 2)";
2144        assert_eq!(
2145            // TODO: We don't currently parse this, but we should.  It should be
2146            // a runtime error instead.
2147            parse_execute(code10).await.unwrap_err(),
2148            KclError::new_syntax(KclErrorDetails::new(
2149                "Unexpected token: !".to_owned(),
2150                vec![SourceRange::new(10, 11, ModuleId::default())],
2151            ))
2152        );
2153
2154        let code11 = "
2155fn identity(x) { return x }
2156notPipeSub = 1 |> identity(!%))";
2157        assert_eq!(
2158            // TODO: We don't currently parse this, but we should.  It should be
2159            // a runtime error instead.
2160            parse_execute(code11).await.unwrap_err(),
2161            KclError::new_syntax(KclErrorDetails::new(
2162                "There was an unexpected `!`. Try removing it.".to_owned(),
2163                vec![SourceRange::new(56, 57, ModuleId::default())],
2164            ))
2165        );
2166
2167        // TODO: Add these tests when we support these types.
2168        // let notNan = !NaN
2169        // let notInfinity = !Infinity
2170    }
2171
2172    #[tokio::test(flavor = "multi_thread")]
2173    async fn test_start_sketch_on_invalid_kwargs() {
2174        let current_dir = std::env::current_dir().unwrap();
2175        let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2176        let mut code = std::fs::read_to_string(&path).unwrap();
2177        assert_eq!(
2178            parse_execute(&code).await.unwrap_err().message(),
2179            "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2180        );
2181
2182        path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2183        code = std::fs::read_to_string(&path).unwrap();
2184
2185        assert_eq!(
2186            parse_execute(&code).await.unwrap_err().message(),
2187            "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2188        );
2189
2190        path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2191        code = std::fs::read_to_string(&path).unwrap();
2192
2193        assert_eq!(
2194            parse_execute(&code).await.unwrap_err().message(),
2195            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2196        );
2197
2198        path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2199        code = std::fs::read_to_string(&path).unwrap();
2200
2201        assert_eq!(
2202            parse_execute(&code).await.unwrap_err().message(),
2203            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2204        );
2205
2206        path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2207        code = std::fs::read_to_string(&path).unwrap();
2208
2209        assert_eq!(
2210            parse_execute(&code).await.unwrap_err().message(),
2211            "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2212        );
2213    }
2214
2215    #[tokio::test(flavor = "multi_thread")]
2216    async fn test_math_negative_variable_in_binary_expression() {
2217        let ast = r#"sigmaAllow = 35000 // psi
2218width = 1 // inch
2219
2220p = 150 // lbs
2221distance = 6 // inches
2222FOS = 2
2223
2224leg1 = 5 // inches
2225leg2 = 8 // inches
2226
2227thickness_squared = distance * p * FOS * 6 / sigmaAllow
2228thickness = 0.56 // inches. App does not support square root function yet
2229
2230bracket = startSketchOn(XY)
2231  |> startProfile(at = [0,0])
2232  |> line(end = [0, leg1])
2233  |> line(end = [leg2, 0])
2234  |> line(end = [0, -thickness])
2235  |> line(end = [-leg2 + thickness, 0])
2236"#;
2237        parse_execute(ast).await.unwrap();
2238    }
2239
2240    #[tokio::test(flavor = "multi_thread")]
2241    async fn test_execute_function_no_return() {
2242        let ast = r#"fn test(@origin) {
2243  origin
2244}
2245
2246test([0, 0])
2247"#;
2248        let result = parse_execute(ast).await;
2249        assert!(result.is_err());
2250        assert!(result.unwrap_err().to_string().contains("undefined"));
2251    }
2252
2253    #[tokio::test(flavor = "multi_thread")]
2254    async fn test_math_doubly_nested_parens() {
2255        let ast = r#"sigmaAllow = 35000 // psi
2256width = 4 // inch
2257p = 150 // Force on shelf - lbs
2258distance = 6 // inches
2259FOS = 2
2260leg1 = 5 // inches
2261leg2 = 8 // inches
2262thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2263thickness = 0.32 // inches. App does not support square root function yet
2264bracket = startSketchOn(XY)
2265  |> startProfile(at = [0,0])
2266    |> line(end = [0, leg1])
2267  |> line(end = [leg2, 0])
2268  |> line(end = [0, -thickness])
2269  |> line(end = [-1 * leg2 + thickness, 0])
2270  |> line(end = [0, -1 * leg1 + thickness])
2271  |> close()
2272  |> extrude(length = width)
2273"#;
2274        parse_execute(ast).await.unwrap();
2275    }
2276
2277    #[tokio::test(flavor = "multi_thread")]
2278    async fn test_math_nested_parens_one_less() {
2279        let ast = r#" sigmaAllow = 35000 // psi
2280width = 4 // inch
2281p = 150 // Force on shelf - lbs
2282distance = 6 // inches
2283FOS = 2
2284leg1 = 5 // inches
2285leg2 = 8 // inches
2286thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2287thickness = 0.32 // inches. App does not support square root function yet
2288bracket = startSketchOn(XY)
2289  |> startProfile(at = [0,0])
2290    |> line(end = [0, leg1])
2291  |> line(end = [leg2, 0])
2292  |> line(end = [0, -thickness])
2293  |> line(end = [-1 * leg2 + thickness, 0])
2294  |> line(end = [0, -1 * leg1 + thickness])
2295  |> close()
2296  |> extrude(length = width)
2297"#;
2298        parse_execute(ast).await.unwrap();
2299    }
2300
2301    #[tokio::test(flavor = "multi_thread")]
2302    async fn test_fn_as_operand() {
2303        let ast = r#"fn f() { return 1 }
2304x = f()
2305y = x + 1
2306z = f() + 1
2307w = f() + f()
2308"#;
2309        parse_execute(ast).await.unwrap();
2310    }
2311
2312    #[tokio::test(flavor = "multi_thread")]
2313    async fn kcl_test_ids_stable_between_executions() {
2314        let code = r#"sketch001 = startSketchOn(XZ)
2315|> startProfile(at = [61.74, 206.13])
2316|> xLine(length = 305.11, tag = $seg01)
2317|> yLine(length = -291.85)
2318|> xLine(length = -segLen(seg01))
2319|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2320|> close()
2321|> extrude(length = 40.14)
2322|> shell(
2323    thickness = 3.14,
2324    faces = [seg01]
2325)
2326"#;
2327
2328        let ctx = crate::test_server::new_context(true, None).await.unwrap();
2329        let old_program = crate::Program::parse_no_errs(code).unwrap();
2330
2331        // Execute the program.
2332        if let Err(err) = ctx.run_with_caching(old_program).await {
2333            let report = err.into_miette_report_with_outputs(code).unwrap();
2334            let report = miette::Report::new(report);
2335            panic!("Error executing program: {report:?}");
2336        }
2337
2338        // Get the id_generator from the first execution.
2339        let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2340
2341        let code = r#"sketch001 = startSketchOn(XZ)
2342|> startProfile(at = [62.74, 206.13])
2343|> xLine(length = 305.11, tag = $seg01)
2344|> yLine(length = -291.85)
2345|> xLine(length = -segLen(seg01))
2346|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2347|> close()
2348|> extrude(length = 40.14)
2349|> shell(
2350    faces = [seg01],
2351    thickness = 3.14,
2352)
2353"#;
2354
2355        // Execute a slightly different program again.
2356        let program = crate::Program::parse_no_errs(code).unwrap();
2357        // Execute the program.
2358        ctx.run_with_caching(program).await.unwrap();
2359
2360        let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2361
2362        assert_eq!(id_generator, new_id_generator);
2363    }
2364
2365    #[tokio::test(flavor = "multi_thread")]
2366    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2367        let code = r#"sketch001 = startSketchOn(XZ)
2368|> startProfile(at = [61.74, 206.13])
2369|> xLine(length = 305.11, tag = $seg01)
2370|> yLine(length = -291.85)
2371|> xLine(length = -segLen(seg01))
2372|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2373|> close()
2374|> extrude(length = 40.14)
2375|> shell(
2376    thickness = 3.14,
2377    faces = [seg01]
2378)
2379"#;
2380
2381        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2382        let old_program = crate::Program::parse_no_errs(code).unwrap();
2383
2384        // Execute the program.
2385        ctx.run_with_caching(old_program.clone()).await.unwrap();
2386
2387        let settings_state = cache::read_old_ast().await.unwrap().settings;
2388
2389        // Ensure the settings are as expected.
2390        assert_eq!(settings_state, ctx.settings);
2391
2392        // Change a setting.
2393        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2394
2395        // Execute the program.
2396        ctx.run_with_caching(old_program.clone()).await.unwrap();
2397
2398        let settings_state = cache::read_old_ast().await.unwrap().settings;
2399
2400        // Ensure the settings are as expected.
2401        assert_eq!(settings_state, ctx.settings);
2402
2403        // Change a setting.
2404        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2405
2406        // Execute the program.
2407        ctx.run_with_caching(old_program).await.unwrap();
2408
2409        let settings_state = cache::read_old_ast().await.unwrap().settings;
2410
2411        // Ensure the settings are as expected.
2412        assert_eq!(settings_state, ctx.settings);
2413
2414        ctx.close().await;
2415    }
2416
2417    #[tokio::test(flavor = "multi_thread")]
2418    async fn mock_after_not_mock() {
2419        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2420        let program = crate::Program::parse_no_errs("x = 2").unwrap();
2421        let result = ctx.run_with_caching(program).await.unwrap();
2422        assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2423
2424        let ctx2 = ExecutorContext::new_mock(None).await;
2425        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2426        let result = ctx2.run_mock(&program2, true).await.unwrap();
2427        assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2428
2429        ctx.close().await;
2430        ctx2.close().await;
2431    }
2432
2433    #[cfg(feature = "artifact-graph")]
2434    #[tokio::test(flavor = "multi_thread")]
2435    async fn mock_has_stable_ids() {
2436        let ctx = ExecutorContext::new_mock(None).await;
2437        let code = "sk = startSketchOn(XY)
2438        |> startProfile(at = [0, 0])";
2439        let program = crate::Program::parse_no_errs(code).unwrap();
2440        let result = ctx.run_mock(&program, false).await.unwrap();
2441        let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2442        assert!(!ids.is_empty(), "IDs should not be empty");
2443
2444        let ctx2 = ExecutorContext::new_mock(None).await;
2445        let program2 = crate::Program::parse_no_errs(code).unwrap();
2446        let result = ctx2.run_mock(&program2, false).await.unwrap();
2447        let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2448
2449        assert_eq!(ids, ids2, "Generated IDs should match");
2450    }
2451
2452    #[cfg(feature = "artifact-graph")]
2453    #[tokio::test(flavor = "multi_thread")]
2454    async fn sim_sketch_mode_real_mock_real() {
2455        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2456        let code = r#"sketch001 = startSketchOn(XY)
2457profile001 = startProfile(sketch001, at = [0, 0])
2458  |> line(end = [10, 0])
2459  |> line(end = [0, 10])
2460  |> line(end = [-10, 0])
2461  |> line(end = [0, -10])
2462  |> close()
2463"#;
2464        let program = crate::Program::parse_no_errs(code).unwrap();
2465        let result = ctx.run_with_caching(program).await.unwrap();
2466        assert_eq!(result.operations.len(), 1);
2467
2468        let mock_ctx = ExecutorContext::new_mock(None).await;
2469        let mock_program = crate::Program::parse_no_errs(code).unwrap();
2470        let mock_result = mock_ctx.run_mock(&mock_program, true).await.unwrap();
2471        assert_eq!(mock_result.operations.len(), 1);
2472
2473        let code2 = code.to_owned()
2474            + r#"
2475extrude001 = extrude(profile001, length = 10)
2476"#;
2477        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
2478        let result = ctx.run_with_caching(program2).await.unwrap();
2479        assert_eq!(result.operations.len(), 2);
2480
2481        ctx.close().await;
2482        mock_ctx.close().await;
2483    }
2484
2485    #[tokio::test(flavor = "multi_thread")]
2486    async fn read_tag_version() {
2487        let ast = r#"fn bar(@t) {
2488  return startSketchOn(XY)
2489    |> startProfile(at = [0,0])
2490    |> angledLine(
2491        angle = -60,
2492        length = segLen(t),
2493    )
2494    |> line(end = [0, 0])
2495    |> close()
2496}
2497
2498sketch = startSketchOn(XY)
2499  |> startProfile(at = [0,0])
2500  |> line(end = [0, 10])
2501  |> line(end = [10, 0], tag = $tag0)
2502  |> line(end = [0, 0])
2503
2504fn foo() {
2505  // tag0 tags an edge
2506  return bar(tag0)
2507}
2508
2509solid = sketch |> extrude(length = 10)
2510// tag0 tags a face
2511sketch2 = startSketchOn(solid, face = tag0)
2512  |> startProfile(at = [0,0])
2513  |> line(end = [0, 1])
2514  |> line(end = [1, 0])
2515  |> line(end = [0, 0])
2516
2517foo() |> extrude(length = 1)
2518"#;
2519        parse_execute(ast).await.unwrap();
2520    }
2521
2522    #[tokio::test(flavor = "multi_thread")]
2523    async fn experimental() {
2524        let code = r#"
2525startSketchOn(XY)
2526  |> startProfile(at = [0, 0], tag = $start)
2527  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2528"#;
2529        let result = parse_execute(code).await.unwrap();
2530        let errors = result.exec_state.errors();
2531        assert_eq!(errors.len(), 1);
2532        assert_eq!(errors[0].severity, Severity::Error);
2533        let msg = &errors[0].message;
2534        assert!(msg.contains("experimental"), "found {msg}");
2535
2536        let code = r#"@settings(experimentalFeatures = allow)
2537startSketchOn(XY)
2538  |> startProfile(at = [0, 0], tag = $start)
2539  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2540"#;
2541        let result = parse_execute(code).await.unwrap();
2542        let errors = result.exec_state.errors();
2543        assert!(errors.is_empty());
2544
2545        let code = r#"@settings(experimentalFeatures = warn)
2546startSketchOn(XY)
2547  |> startProfile(at = [0, 0], tag = $start)
2548  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2549"#;
2550        let result = parse_execute(code).await.unwrap();
2551        let errors = result.exec_state.errors();
2552        assert_eq!(errors.len(), 1);
2553        assert_eq!(errors[0].severity, Severity::Warning);
2554        let msg = &errors[0].message;
2555        assert!(msg.contains("experimental"), "found {msg}");
2556
2557        let code = r#"@settings(experimentalFeatures = deny)
2558startSketchOn(XY)
2559  |> startProfile(at = [0, 0], tag = $start)
2560  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2561"#;
2562        let result = parse_execute(code).await.unwrap();
2563        let errors = result.exec_state.errors();
2564        assert_eq!(errors.len(), 1);
2565        assert_eq!(errors[0].severity, Severity::Error);
2566        let msg = &errors[0].message;
2567        assert!(msg.contains("experimental"), "found {msg}");
2568
2569        let code = r#"@settings(experimentalFeatures = foo)
2570startSketchOn(XY)
2571  |> startProfile(at = [0, 0], tag = $start)
2572  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2573"#;
2574        parse_execute(code).await.unwrap_err();
2575    }
2576}