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