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::{ModuleExecutionOutcome, 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);
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 original_program = program.clone();
606
607        let (_program, exec_state, result) = match cache::read_old_ast().await {
608            Some(mut cached_state) => {
609                let old = CacheInformation {
610                    ast: &cached_state.main.ast,
611                    settings: &cached_state.settings,
612                };
613                let new = CacheInformation {
614                    ast: &program.ast,
615                    settings: &self.settings,
616                };
617
618                // Get the program that actually changed from the old and new information.
619                let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
620                    CacheResult::ReExecute {
621                        clear_scene,
622                        reapply_settings,
623                        program: changed_program,
624                    } => {
625                        if reapply_settings
626                            && self
627                                .engine
628                                .reapply_settings(
629                                    &self.settings,
630                                    Default::default(),
631                                    &mut cached_state.main.exec_state.id_generator,
632                                    grid_scale,
633                                )
634                                .await
635                                .is_err()
636                        {
637                            (true, program, None)
638                        } else {
639                            (
640                                clear_scene,
641                                crate::Program {
642                                    ast: changed_program,
643                                    original_file_contents: program.original_file_contents,
644                                },
645                                None,
646                            )
647                        }
648                    }
649                    CacheResult::CheckImportsOnly {
650                        reapply_settings,
651                        ast: changed_program,
652                    } => {
653                        let mut reapply_failed = false;
654                        if reapply_settings {
655                            if self
656                                .engine
657                                .reapply_settings(
658                                    &self.settings,
659                                    Default::default(),
660                                    &mut cached_state.main.exec_state.id_generator,
661                                    grid_scale,
662                                )
663                                .await
664                                .is_ok()
665                            {
666                                cache::write_old_ast(GlobalState::with_settings(
667                                    cached_state.clone(),
668                                    self.settings.clone(),
669                                ))
670                                .await;
671                            } else {
672                                reapply_failed = true;
673                            }
674                        }
675
676                        if reapply_failed {
677                            (true, program, None)
678                        } else {
679                            // We need to check our imports to see if they changed.
680                            let mut new_exec_state = ExecState::new(self);
681                            let (new_universe, new_universe_map) =
682                                self.get_universe(&program, &mut new_exec_state).await?;
683
684                            let clear_scene = new_universe.values().any(|value| {
685                                let id = value.1;
686                                match (
687                                    cached_state.exec_state.get_source(id),
688                                    new_exec_state.global.get_source(id),
689                                ) {
690                                    (Some(s0), Some(s1)) => s0.source != s1.source,
691                                    _ => false,
692                                }
693                            });
694
695                            if !clear_scene {
696                                // Return early we don't need to clear the scene.
697                                return Ok(cached_state.into_exec_outcome(self).await);
698                            }
699
700                            (
701                                true,
702                                crate::Program {
703                                    ast: changed_program,
704                                    original_file_contents: program.original_file_contents,
705                                },
706                                Some((new_universe, new_universe_map, new_exec_state)),
707                            )
708                        }
709                    }
710                    CacheResult::NoAction(true) => {
711                        if self
712                            .engine
713                            .reapply_settings(
714                                &self.settings,
715                                Default::default(),
716                                &mut cached_state.main.exec_state.id_generator,
717                                grid_scale,
718                            )
719                            .await
720                            .is_ok()
721                        {
722                            // We need to update the old ast state with the new settings!!
723                            cache::write_old_ast(GlobalState::with_settings(
724                                cached_state.clone(),
725                                self.settings.clone(),
726                            ))
727                            .await;
728
729                            return Ok(cached_state.into_exec_outcome(self).await);
730                        }
731                        (true, program, None)
732                    }
733                    CacheResult::NoAction(false) => {
734                        return Ok(cached_state.into_exec_outcome(self).await);
735                    }
736                };
737
738                let (exec_state, result) = match import_check_info {
739                    Some((new_universe, new_universe_map, mut new_exec_state)) => {
740                        // Clear the scene if the imports changed.
741                        self.send_clear_scene(&mut new_exec_state, Default::default())
742                            .await
743                            .map_err(KclErrorWithOutputs::no_outputs)?;
744
745                        let result = self
746                            .run_concurrent(
747                                &program,
748                                &mut new_exec_state,
749                                Some((new_universe, new_universe_map)),
750                                false,
751                            )
752                            .await;
753
754                        (new_exec_state, result)
755                    }
756                    None if clear_scene => {
757                        // Pop the execution state, since we are starting fresh.
758                        let mut exec_state = cached_state.reconstitute_exec_state();
759                        exec_state.reset(self);
760
761                        self.send_clear_scene(&mut exec_state, Default::default())
762                            .await
763                            .map_err(KclErrorWithOutputs::no_outputs)?;
764
765                        let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
766
767                        (exec_state, result)
768                    }
769                    None => {
770                        let mut exec_state = cached_state.reconstitute_exec_state();
771                        exec_state.mut_stack().restore_env(cached_state.main.result_env);
772
773                        let result = self.run_concurrent(&program, &mut exec_state, None, true).await;
774
775                        (exec_state, result)
776                    }
777                };
778
779                (program, exec_state, result)
780            }
781            None => {
782                let mut exec_state = ExecState::new(self);
783                self.send_clear_scene(&mut exec_state, Default::default())
784                    .await
785                    .map_err(KclErrorWithOutputs::no_outputs)?;
786
787                let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
788
789                (program, exec_state, result)
790            }
791        };
792
793        if result.is_err() {
794            cache::bust_cache().await;
795        }
796
797        // Throw the error.
798        let result = result?;
799
800        // Save this as the last successful execution to the cache.
801        // Gotcha: `CacheResult::ReExecute.program` may be diff-based, do not save that AST
802        // the last-successful AST. Instead, save in the full AST passed in.
803        cache::write_old_ast(GlobalState::new(
804            exec_state.clone(),
805            self.settings.clone(),
806            original_program.ast,
807            result.0,
808        ))
809        .await;
810
811        let outcome = exec_state.into_exec_outcome(result.0, self).await;
812        Ok(outcome)
813    }
814
815    /// Perform the execution of a program.
816    ///
817    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
818    pub async fn run(
819        &self,
820        program: &crate::Program,
821        exec_state: &mut ExecState,
822    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
823        self.run_concurrent(program, exec_state, None, false).await
824    }
825
826    /// Perform the execution of a program using a concurrent
827    /// execution model.
828    ///
829    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
830    pub async fn run_concurrent(
831        &self,
832        program: &crate::Program,
833        exec_state: &mut ExecState,
834        universe_info: Option<(Universe, UniverseMap)>,
835        preserve_mem: bool,
836    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
837        // Reuse our cached universe if we have one.
838
839        let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
840            (universe, universe_map)
841        } else {
842            self.get_universe(program, exec_state).await?
843        };
844
845        let default_planes = self.engine.get_default_planes().read().await.clone();
846
847        // Run the prelude to set up the engine.
848        self.eval_prelude(exec_state, SourceRange::synthetic())
849            .await
850            .map_err(KclErrorWithOutputs::no_outputs)?;
851
852        for modules in import_graph::import_graph(&universe, self)
853            .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
854            .into_iter()
855        {
856            #[cfg(not(target_arch = "wasm32"))]
857            let mut set = tokio::task::JoinSet::new();
858
859            #[allow(clippy::type_complexity)]
860            let (results_tx, mut results_rx): (
861                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
862                tokio::sync::mpsc::Receiver<_>,
863            ) = tokio::sync::mpsc::channel(1);
864
865            for module in modules {
866                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
867                    return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
868                        KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
869                    )));
870                };
871                let module_id = *module_id;
872                let module_path = module_path.clone();
873                let source_range = SourceRange::from(import_stmt);
874                // Clone before mutating.
875                let module_exec_state = exec_state.clone();
876
877                self.add_import_module_ops(
878                    exec_state,
879                    &program.ast,
880                    module_id,
881                    &module_path,
882                    source_range,
883                    &universe_map,
884                );
885
886                let repr = repr.clone();
887                let exec_ctxt = self.clone();
888                let results_tx = results_tx.clone();
889
890                let exec_module = async |exec_ctxt: &ExecutorContext,
891                                         repr: &ModuleRepr,
892                                         module_id: ModuleId,
893                                         module_path: &ModulePath,
894                                         exec_state: &mut ExecState,
895                                         source_range: SourceRange|
896                       -> Result<ModuleRepr, KclError> {
897                    match repr {
898                        ModuleRepr::Kcl(program, _) => {
899                            let result = exec_ctxt
900                                .exec_module_from_ast(program, module_id, module_path, exec_state, source_range, false)
901                                .await;
902
903                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
904                        }
905                        ModuleRepr::Foreign(geom, _) => {
906                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
907                                .await
908                                .map(|geom| Some(KclValue::ImportedGeometry(geom)));
909
910                            result.map(|val| {
911                                ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
912                            })
913                        }
914                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
915                            format!("Module {module_path} not found in universe"),
916                            vec![source_range],
917                        ))),
918                    }
919                };
920
921                #[cfg(target_arch = "wasm32")]
922                {
923                    wasm_bindgen_futures::spawn_local(async move {
924                        let mut exec_state = module_exec_state;
925                        let exec_ctxt = exec_ctxt;
926
927                        let result = exec_module(
928                            &exec_ctxt,
929                            &repr,
930                            module_id,
931                            &module_path,
932                            &mut exec_state,
933                            source_range,
934                        )
935                        .await;
936
937                        results_tx
938                            .send((module_id, module_path, result))
939                            .await
940                            .unwrap_or_default();
941                    });
942                }
943                #[cfg(not(target_arch = "wasm32"))]
944                {
945                    set.spawn(async move {
946                        let mut exec_state = module_exec_state;
947                        let exec_ctxt = exec_ctxt;
948
949                        let result = exec_module(
950                            &exec_ctxt,
951                            &repr,
952                            module_id,
953                            &module_path,
954                            &mut exec_state,
955                            source_range,
956                        )
957                        .await;
958
959                        results_tx
960                            .send((module_id, module_path, result))
961                            .await
962                            .unwrap_or_default();
963                    });
964                }
965            }
966
967            drop(results_tx);
968
969            while let Some((module_id, _, result)) = results_rx.recv().await {
970                match result {
971                    Ok(new_repr) => {
972                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
973
974                        match &mut repr {
975                            ModuleRepr::Kcl(_, cache) => {
976                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
977                                    unreachable!();
978                                };
979                                *cache = session_data;
980                            }
981                            ModuleRepr::Foreign(_, cache) => {
982                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
983                                    unreachable!();
984                                };
985                                *cache = session_data;
986                            }
987                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
988                        }
989
990                        exec_state.global.module_infos[&module_id].restore_repr(repr);
991                    }
992                    Err(e) => {
993                        return Err(exec_state.error_with_outputs(e, None, default_planes));
994                    }
995                }
996            }
997        }
998
999        // Since we haven't technically started executing the root module yet,
1000        // the operations corresponding to the imports will be missing unless we
1001        // track them here.
1002        exec_state
1003            .global
1004            .root_module_artifacts
1005            .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1006
1007        self.inner_run(program, exec_state, preserve_mem).await
1008    }
1009
1010    /// Get the universe & universe map of the program.
1011    /// And see if any of the imports changed.
1012    async fn get_universe(
1013        &self,
1014        program: &crate::Program,
1015        exec_state: &mut ExecState,
1016    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1017        exec_state.add_root_module_contents(program);
1018
1019        let mut universe = std::collections::HashMap::new();
1020
1021        let default_planes = self.engine.get_default_planes().read().await.clone();
1022
1023        let root_imports = import_graph::import_universe(
1024            self,
1025            &ModulePath::Main,
1026            &ModuleRepr::Kcl(program.ast.clone(), None),
1027            &mut universe,
1028            exec_state,
1029        )
1030        .await
1031        .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1032
1033        Ok((universe, root_imports))
1034    }
1035
1036    #[cfg(not(feature = "artifact-graph"))]
1037    fn add_import_module_ops(
1038        &self,
1039        _exec_state: &mut ExecState,
1040        _program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1041        _module_id: ModuleId,
1042        _module_path: &ModulePath,
1043        _source_range: SourceRange,
1044        _universe_map: &UniverseMap,
1045    ) {
1046    }
1047
1048    #[cfg(feature = "artifact-graph")]
1049    fn add_import_module_ops(
1050        &self,
1051        exec_state: &mut ExecState,
1052        program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1053        module_id: ModuleId,
1054        module_path: &ModulePath,
1055        source_range: SourceRange,
1056        universe_map: &UniverseMap,
1057    ) {
1058        match module_path {
1059            ModulePath::Main => {
1060                // This should never happen.
1061            }
1062            ModulePath::Local {
1063                value,
1064                original_import_path,
1065            } => {
1066                // We only want to display the top-level module imports in
1067                // the Feature Tree, not transitive imports.
1068                if universe_map.contains_key(value) {
1069                    use crate::NodePath;
1070
1071                    let node_path = if source_range.is_top_level_module() {
1072                        let cached_body_items = exec_state.global.artifacts.cached_body_items();
1073                        NodePath::from_range(
1074                            &exec_state.build_program_lookup(program.clone()),
1075                            cached_body_items,
1076                            source_range,
1077                        )
1078                        .unwrap_or_default()
1079                    } else {
1080                        // The frontend doesn't care about paths in
1081                        // files other than the top-level module.
1082                        NodePath::placeholder()
1083                    };
1084
1085                    let name = match original_import_path {
1086                        Some(value) => value.to_string_lossy(),
1087                        None => value.file_name().unwrap_or_default(),
1088                    };
1089                    exec_state.push_op(Operation::GroupBegin {
1090                        group: Group::ModuleInstance { name, module_id },
1091                        node_path,
1092                        source_range,
1093                    });
1094                    // Due to concurrent execution, we cannot easily
1095                    // group operations by module. So we leave the
1096                    // group empty and close it immediately.
1097                    exec_state.push_op(Operation::GroupEnd);
1098                }
1099            }
1100            ModulePath::Std { .. } => {
1101                // We don't want to display stdlib in the Feature Tree.
1102            }
1103        }
1104    }
1105
1106    /// Perform the execution of a program.  Accept all possible parameters and
1107    /// output everything.
1108    async fn inner_run(
1109        &self,
1110        program: &crate::Program,
1111        exec_state: &mut ExecState,
1112        preserve_mem: bool,
1113    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1114        let _stats = crate::log::LogPerfStats::new("Interpretation");
1115
1116        // Re-apply the settings, in case the cache was busted.
1117        let grid_scale = if self.settings.fixed_size_grid {
1118            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1119        } else {
1120            GridScaleBehavior::ScaleWithZoom
1121        };
1122        self.engine
1123            .reapply_settings(
1124                &self.settings,
1125                Default::default(),
1126                exec_state.id_generator(),
1127                grid_scale,
1128            )
1129            .await
1130            .map_err(KclErrorWithOutputs::no_outputs)?;
1131
1132        let default_planes = self.engine.get_default_planes().read().await.clone();
1133        let result = self
1134            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1135            .await;
1136
1137        crate::log::log(format!(
1138            "Post interpretation KCL memory stats: {:#?}",
1139            exec_state.stack().memory.stats
1140        ));
1141        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1142
1143        let env_ref = result.map_err(|(err, env_ref)| exec_state.error_with_outputs(err, env_ref, default_planes))?;
1144
1145        if !self.is_mock() {
1146            let mut mem = exec_state.stack().deep_clone();
1147            mem.restore_env(env_ref);
1148            cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
1149        }
1150        let session_data = self.engine.get_session_data().await;
1151
1152        Ok((env_ref, session_data))
1153    }
1154
1155    /// Execute an AST's program and build auxiliary outputs like the artifact
1156    /// graph.
1157    async fn execute_and_build_graph(
1158        &self,
1159        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1160        exec_state: &mut ExecState,
1161        preserve_mem: bool,
1162    ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1163        // Don't early return!  We need to build other outputs regardless of
1164        // whether execution failed.
1165
1166        // Because of execution caching, we may start with operations from a
1167        // previous run.
1168        #[cfg(feature = "artifact-graph")]
1169        let start_op = exec_state.global.root_module_artifacts.operations.len();
1170
1171        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1172            .await
1173            .map_err(|e| (e, None))?;
1174
1175        let exec_result = self
1176            .exec_module_body(
1177                program,
1178                exec_state,
1179                preserve_mem,
1180                ModuleId::default(),
1181                &ModulePath::Main,
1182            )
1183            .await
1184            .map(
1185                |ModuleExecutionOutcome {
1186                     environment: env_ref,
1187                     artifacts: module_artifacts,
1188                     ..
1189                 }| {
1190                    // We need to extend because it may already have operations from
1191                    // imports.
1192                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1193                    env_ref
1194                },
1195            )
1196            .map_err(|(err, env_ref, module_artifacts)| {
1197                if let Some(module_artifacts) = module_artifacts {
1198                    // We need to extend because it may already have operations
1199                    // from imports.
1200                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1201                }
1202                (err, env_ref)
1203            });
1204
1205        #[cfg(feature = "artifact-graph")]
1206        {
1207            // Fill in NodePath for operations.
1208            let programs = &exec_state.build_program_lookup(program.clone());
1209            let cached_body_items = exec_state.global.artifacts.cached_body_items();
1210            for op in exec_state
1211                .global
1212                .root_module_artifacts
1213                .operations
1214                .iter_mut()
1215                .skip(start_op)
1216            {
1217                op.fill_node_paths(programs, cached_body_items);
1218            }
1219            for module in exec_state.global.module_infos.values_mut() {
1220                if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1221                    for op in &mut outcome.artifacts.operations {
1222                        op.fill_node_paths(programs, cached_body_items);
1223                    }
1224                }
1225            }
1226        }
1227
1228        // Ensure all the async commands completed.
1229        self.engine.ensure_async_commands_completed().await.map_err(|e| {
1230            match &exec_result {
1231                Ok(env_ref) => (e, Some(*env_ref)),
1232                // Prefer the execution error.
1233                Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1234            }
1235        })?;
1236
1237        // If we errored out and early-returned, there might be commands which haven't been executed
1238        // and should be dropped.
1239        self.engine.clear_queues().await;
1240
1241        match exec_state.build_artifact_graph(&self.engine, program).await {
1242            Ok(_) => exec_result,
1243            Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1244        }
1245    }
1246
1247    /// 'Import' std::prelude as the outermost scope.
1248    ///
1249    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
1250    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1251        if exec_state.stack().memory.requires_std() {
1252            #[cfg(feature = "artifact-graph")]
1253            let initial_ops = exec_state.mod_local.artifacts.operations.len();
1254
1255            let path = vec!["std".to_owned(), "prelude".to_owned()];
1256            let resolved_path = ModulePath::from_std_import_path(&path)?;
1257            let id = self
1258                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1259                .await?;
1260            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1261
1262            exec_state.mut_stack().memory.set_std(module_memory);
1263
1264            // Operations generated by the prelude are not useful, so clear them
1265            // out.
1266            //
1267            // TODO: Should we also clear them out of each module so that they
1268            // don't appear in test output?
1269            #[cfg(feature = "artifact-graph")]
1270            exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1271        }
1272
1273        Ok(())
1274    }
1275
1276    /// Get a snapshot of the current scene.
1277    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1278        // Zoom to fit.
1279        self.engine
1280            .send_modeling_cmd(
1281                uuid::Uuid::new_v4(),
1282                crate::execution::SourceRange::default(),
1283                &ModelingCmd::from(mcmd::ZoomToFit {
1284                    object_ids: Default::default(),
1285                    animated: false,
1286                    padding: 0.1,
1287                }),
1288            )
1289            .await
1290            .map_err(KclErrorWithOutputs::no_outputs)?;
1291
1292        // Send a snapshot request to the engine.
1293        let resp = self
1294            .engine
1295            .send_modeling_cmd(
1296                uuid::Uuid::new_v4(),
1297                crate::execution::SourceRange::default(),
1298                &ModelingCmd::from(mcmd::TakeSnapshot {
1299                    format: ImageFormat::Png,
1300                }),
1301            )
1302            .await
1303            .map_err(KclErrorWithOutputs::no_outputs)?;
1304
1305        let OkWebSocketResponseData::Modeling {
1306            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1307        } = resp
1308        else {
1309            return Err(ExecError::BadPng(format!(
1310                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1311            )));
1312        };
1313        Ok(contents)
1314    }
1315
1316    /// Export the current scene as a CAD file.
1317    pub async fn export(
1318        &self,
1319        format: kittycad_modeling_cmds::format::OutputFormat3d,
1320    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1321        let resp = self
1322            .engine
1323            .send_modeling_cmd(
1324                uuid::Uuid::new_v4(),
1325                crate::SourceRange::default(),
1326                &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
1327                    entity_ids: vec![],
1328                    format,
1329                }),
1330            )
1331            .await?;
1332
1333        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1334            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1335                format!("Expected Export response, got {resp:?}",),
1336                vec![SourceRange::default()],
1337            )));
1338        };
1339
1340        Ok(files)
1341    }
1342
1343    /// Export the current scene as a STEP file.
1344    pub async fn export_step(
1345        &self,
1346        deterministic_time: bool,
1347    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1348        let files = self
1349            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1350                kittycad_modeling_cmds::format::step::export::Options {
1351                    coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1352                    created: if deterministic_time {
1353                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1354                            KclError::new_internal(crate::errors::KclErrorDetails::new(
1355                                format!("Failed to parse date: {e}"),
1356                                vec![SourceRange::default()],
1357                            ))
1358                        })?)
1359                    } else {
1360                        None
1361                    },
1362                },
1363            ))
1364            .await?;
1365
1366        Ok(files)
1367    }
1368
1369    pub async fn close(&self) {
1370        self.engine.close().await;
1371    }
1372}
1373
1374#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1375pub struct ArtifactId(Uuid);
1376
1377impl ArtifactId {
1378    pub fn new(uuid: Uuid) -> Self {
1379        Self(uuid)
1380    }
1381}
1382
1383impl From<Uuid> for ArtifactId {
1384    fn from(uuid: Uuid) -> Self {
1385        Self::new(uuid)
1386    }
1387}
1388
1389impl From<&Uuid> for ArtifactId {
1390    fn from(uuid: &Uuid) -> Self {
1391        Self::new(*uuid)
1392    }
1393}
1394
1395impl From<ArtifactId> for Uuid {
1396    fn from(id: ArtifactId) -> Self {
1397        id.0
1398    }
1399}
1400
1401impl From<&ArtifactId> for Uuid {
1402    fn from(id: &ArtifactId) -> Self {
1403        id.0
1404    }
1405}
1406
1407impl From<ModelingCmdId> for ArtifactId {
1408    fn from(id: ModelingCmdId) -> Self {
1409        Self::new(*id.as_ref())
1410    }
1411}
1412
1413impl From<&ModelingCmdId> for ArtifactId {
1414    fn from(id: &ModelingCmdId) -> Self {
1415        Self::new(*id.as_ref())
1416    }
1417}
1418
1419#[cfg(test)]
1420pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1421    parse_execute_with_project_dir(code, None).await
1422}
1423
1424#[cfg(test)]
1425pub(crate) async fn parse_execute_with_project_dir(
1426    code: &str,
1427    project_directory: Option<TypedPath>,
1428) -> Result<ExecTestResults, KclError> {
1429    let program = crate::Program::parse_no_errs(code)?;
1430
1431    let exec_ctxt = ExecutorContext {
1432        engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
1433            |err| {
1434                KclError::new_internal(crate::errors::KclErrorDetails::new(
1435                    format!("Failed to create mock engine connection: {err}"),
1436                    vec![SourceRange::default()],
1437                ))
1438            },
1439        )?)),
1440        fs: Arc::new(crate::fs::FileManager::new()),
1441        settings: ExecutorSettings {
1442            project_directory,
1443            ..Default::default()
1444        },
1445        context_type: ContextType::Mock,
1446    };
1447    let mut exec_state = ExecState::new(&exec_ctxt);
1448    let result = exec_ctxt.run(&program, &mut exec_state).await?;
1449
1450    Ok(ExecTestResults {
1451        program,
1452        mem_env: result.0,
1453        exec_ctxt,
1454        exec_state,
1455    })
1456}
1457
1458#[cfg(test)]
1459#[derive(Debug)]
1460pub(crate) struct ExecTestResults {
1461    program: crate::Program,
1462    mem_env: EnvironmentRef,
1463    exec_ctxt: ExecutorContext,
1464    exec_state: ExecState,
1465}
1466
1467/// There are several places where we want to traverse a KCL program or find a symbol in it,
1468/// but because KCL modules can import each other, we need to traverse multiple programs.
1469/// This stores multiple programs, keyed by their module ID for quick access.
1470#[cfg(feature = "artifact-graph")]
1471pub struct ProgramLookup {
1472    programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
1473}
1474
1475#[cfg(feature = "artifact-graph")]
1476impl ProgramLookup {
1477    // TODO: Could this store a reference to KCL programs instead of owning them?
1478    // i.e. take &state::ModuleInfoMap instead?
1479    pub fn new(
1480        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1481        module_infos: state::ModuleInfoMap,
1482    ) -> Self {
1483        let mut programs = IndexMap::with_capacity(module_infos.len());
1484        for (id, info) in module_infos {
1485            if let ModuleRepr::Kcl(program, _) = info.repr {
1486                programs.insert(id, program);
1487            }
1488        }
1489        programs.insert(ModuleId::default(), current);
1490        Self { programs }
1491    }
1492
1493    pub fn program_for_module(
1494        &self,
1495        module_id: ModuleId,
1496    ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
1497        self.programs.get(&module_id)
1498    }
1499}
1500
1501#[cfg(test)]
1502mod tests {
1503    use pretty_assertions::assert_eq;
1504
1505    use super::*;
1506    use crate::{
1507        ModuleId,
1508        errors::{KclErrorDetails, Severity},
1509        exec::NumericType,
1510        execution::{memory::Stack, types::RuntimeType},
1511    };
1512
1513    /// Convenience function to get a JSON value from memory and unwrap.
1514    #[track_caller]
1515    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1516        memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1517    }
1518
1519    #[tokio::test(flavor = "multi_thread")]
1520    async fn test_execute_warn() {
1521        let text = "@blah";
1522        let result = parse_execute(text).await.unwrap();
1523        let errs = result.exec_state.errors();
1524        assert_eq!(errs.len(), 1);
1525        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1526        assert!(
1527            errs[0].message.contains("Unknown annotation"),
1528            "unexpected warning message: {}",
1529            errs[0].message
1530        );
1531    }
1532
1533    #[tokio::test(flavor = "multi_thread")]
1534    async fn test_execute_fn_definitions() {
1535        let ast = r#"fn def(@x) {
1536  return x
1537}
1538fn ghi(@x) {
1539  return x
1540}
1541fn jkl(@x) {
1542  return x
1543}
1544fn hmm(@x) {
1545  return x
1546}
1547
1548yo = 5 + 6
1549
1550abc = 3
1551identifierGuy = 5
1552part001 = startSketchOn(XY)
1553|> startProfile(at = [-1.2, 4.83])
1554|> line(end = [2.8, 0])
1555|> angledLine(angle = 100 + 100, length = 3.01)
1556|> angledLine(angle = abc, length = 3.02)
1557|> angledLine(angle = def(yo), length = 3.03)
1558|> angledLine(angle = ghi(2), length = 3.04)
1559|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1560|> close()
1561yo2 = hmm([identifierGuy + 5])"#;
1562
1563        parse_execute(ast).await.unwrap();
1564    }
1565
1566    #[tokio::test(flavor = "multi_thread")]
1567    async fn test_execute_with_pipe_substitutions_unary() {
1568        let ast = r#"myVar = 3
1569part001 = startSketchOn(XY)
1570  |> startProfile(at = [0, 0])
1571  |> line(end = [3, 4], tag = $seg01)
1572  |> line(end = [
1573  min([segLen(seg01), myVar]),
1574  -legLen(hypotenuse = segLen(seg01), leg = myVar)
1575])
1576"#;
1577
1578        parse_execute(ast).await.unwrap();
1579    }
1580
1581    #[tokio::test(flavor = "multi_thread")]
1582    async fn test_execute_with_pipe_substitutions() {
1583        let ast = r#"myVar = 3
1584part001 = startSketchOn(XY)
1585  |> startProfile(at = [0, 0])
1586  |> line(end = [3, 4], tag = $seg01)
1587  |> line(end = [
1588  min([segLen(seg01), myVar]),
1589  legLen(hypotenuse = segLen(seg01), leg = myVar)
1590])
1591"#;
1592
1593        parse_execute(ast).await.unwrap();
1594    }
1595
1596    #[tokio::test(flavor = "multi_thread")]
1597    async fn test_execute_with_inline_comment() {
1598        let ast = r#"baseThick = 1
1599armAngle = 60
1600
1601baseThickHalf = baseThick / 2
1602halfArmAngle = armAngle / 2
1603
1604arrExpShouldNotBeIncluded = [1, 2, 3]
1605objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1606
1607part001 = startSketchOn(XY)
1608  |> startProfile(at = [0, 0])
1609  |> yLine(endAbsolute = 1)
1610  |> xLine(length = 3.84) // selection-range-7ish-before-this
1611
1612variableBelowShouldNotBeIncluded = 3
1613"#;
1614
1615        parse_execute(ast).await.unwrap();
1616    }
1617
1618    #[tokio::test(flavor = "multi_thread")]
1619    async fn test_execute_with_function_literal_in_pipe() {
1620        let ast = r#"w = 20
1621l = 8
1622h = 10
1623
1624fn thing() {
1625  return -8
1626}
1627
1628firstExtrude = startSketchOn(XY)
1629  |> startProfile(at = [0,0])
1630  |> line(end = [0, l])
1631  |> line(end = [w, 0])
1632  |> line(end = [0, thing()])
1633  |> close()
1634  |> extrude(length = h)"#;
1635
1636        parse_execute(ast).await.unwrap();
1637    }
1638
1639    #[tokio::test(flavor = "multi_thread")]
1640    async fn test_execute_with_function_unary_in_pipe() {
1641        let ast = r#"w = 20
1642l = 8
1643h = 10
1644
1645fn thing(@x) {
1646  return -x
1647}
1648
1649firstExtrude = startSketchOn(XY)
1650  |> startProfile(at = [0,0])
1651  |> line(end = [0, l])
1652  |> line(end = [w, 0])
1653  |> line(end = [0, thing(8)])
1654  |> close()
1655  |> extrude(length = h)"#;
1656
1657        parse_execute(ast).await.unwrap();
1658    }
1659
1660    #[tokio::test(flavor = "multi_thread")]
1661    async fn test_execute_with_function_array_in_pipe() {
1662        let ast = r#"w = 20
1663l = 8
1664h = 10
1665
1666fn thing(@x) {
1667  return [0, -x]
1668}
1669
1670firstExtrude = startSketchOn(XY)
1671  |> startProfile(at = [0,0])
1672  |> line(end = [0, l])
1673  |> line(end = [w, 0])
1674  |> line(end = thing(8))
1675  |> close()
1676  |> extrude(length = h)"#;
1677
1678        parse_execute(ast).await.unwrap();
1679    }
1680
1681    #[tokio::test(flavor = "multi_thread")]
1682    async fn test_execute_with_function_call_in_pipe() {
1683        let ast = r#"w = 20
1684l = 8
1685h = 10
1686
1687fn other_thing(@y) {
1688  return -y
1689}
1690
1691fn thing(@x) {
1692  return other_thing(x)
1693}
1694
1695firstExtrude = startSketchOn(XY)
1696  |> startProfile(at = [0,0])
1697  |> line(end = [0, l])
1698  |> line(end = [w, 0])
1699  |> line(end = [0, thing(8)])
1700  |> close()
1701  |> extrude(length = h)"#;
1702
1703        parse_execute(ast).await.unwrap();
1704    }
1705
1706    #[tokio::test(flavor = "multi_thread")]
1707    async fn test_execute_with_function_sketch() {
1708        let ast = r#"fn box(h, l, w) {
1709 myBox = startSketchOn(XY)
1710    |> startProfile(at = [0,0])
1711    |> line(end = [0, l])
1712    |> line(end = [w, 0])
1713    |> line(end = [0, -l])
1714    |> close()
1715    |> extrude(length = h)
1716
1717  return myBox
1718}
1719
1720fnBox = box(h = 3, l = 6, w = 10)"#;
1721
1722        parse_execute(ast).await.unwrap();
1723    }
1724
1725    #[tokio::test(flavor = "multi_thread")]
1726    async fn test_get_member_of_object_with_function_period() {
1727        let ast = r#"fn box(@obj) {
1728 myBox = startSketchOn(XY)
1729    |> startProfile(at = obj.start)
1730    |> line(end = [0, obj.l])
1731    |> line(end = [obj.w, 0])
1732    |> line(end = [0, -obj.l])
1733    |> close()
1734    |> extrude(length = obj.h)
1735
1736  return myBox
1737}
1738
1739thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1740"#;
1741        parse_execute(ast).await.unwrap();
1742    }
1743
1744    #[tokio::test(flavor = "multi_thread")]
1745    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
1746    async fn test_object_member_starting_pipeline() {
1747        let ast = r#"
1748fn test2() {
1749  return {
1750    thing: startSketchOn(XY)
1751      |> startProfile(at = [0, 0])
1752      |> line(end = [0, 1])
1753      |> line(end = [1, 0])
1754      |> line(end = [0, -1])
1755      |> close()
1756  }
1757}
1758
1759x2 = test2()
1760
1761x2.thing
1762  |> extrude(length = 10)
1763"#;
1764        parse_execute(ast).await.unwrap();
1765    }
1766
1767    #[tokio::test(flavor = "multi_thread")]
1768    #[ignore] // ignore til we get loops
1769    async fn test_execute_with_function_sketch_loop_objects() {
1770        let ast = r#"fn box(obj) {
1771let myBox = startSketchOn(XY)
1772    |> startProfile(at = obj.start)
1773    |> line(end = [0, obj.l])
1774    |> line(end = [obj.w, 0])
1775    |> line(end = [0, -obj.l])
1776    |> close()
1777    |> extrude(length = obj.h)
1778
1779  return myBox
1780}
1781
1782for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1783  thisBox = box(var)
1784}"#;
1785
1786        parse_execute(ast).await.unwrap();
1787    }
1788
1789    #[tokio::test(flavor = "multi_thread")]
1790    #[ignore] // ignore til we get loops
1791    async fn test_execute_with_function_sketch_loop_array() {
1792        let ast = r#"fn box(h, l, w, start) {
1793 myBox = startSketchOn(XY)
1794    |> startProfile(at = [0,0])
1795    |> line(end = [0, l])
1796    |> line(end = [w, 0])
1797    |> line(end = [0, -l])
1798    |> close()
1799    |> extrude(length = h)
1800
1801  return myBox
1802}
1803
1804
1805for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1806  const thisBox = box(var[0], var[1], var[2], var[3])
1807}"#;
1808
1809        parse_execute(ast).await.unwrap();
1810    }
1811
1812    #[tokio::test(flavor = "multi_thread")]
1813    async fn test_get_member_of_array_with_function() {
1814        let ast = r#"fn box(@arr) {
1815 myBox =startSketchOn(XY)
1816    |> startProfile(at = arr[0])
1817    |> line(end = [0, arr[1]])
1818    |> line(end = [arr[2], 0])
1819    |> line(end = [0, -arr[1]])
1820    |> close()
1821    |> extrude(length = arr[3])
1822
1823  return myBox
1824}
1825
1826thisBox = box([[0,0], 6, 10, 3])
1827
1828"#;
1829        parse_execute(ast).await.unwrap();
1830    }
1831
1832    #[tokio::test(flavor = "multi_thread")]
1833    async fn test_function_cannot_access_future_definitions() {
1834        let ast = r#"
1835fn returnX() {
1836  // x shouldn't be defined yet.
1837  return x
1838}
1839
1840x = 5
1841
1842answer = returnX()"#;
1843
1844        let result = parse_execute(ast).await;
1845        let err = result.unwrap_err();
1846        assert_eq!(err.message(), "`x` is not defined");
1847    }
1848
1849    #[tokio::test(flavor = "multi_thread")]
1850    async fn test_override_prelude() {
1851        let text = "PI = 3.0";
1852        let result = parse_execute(text).await.unwrap();
1853        let errs = result.exec_state.errors();
1854        assert!(errs.is_empty());
1855    }
1856
1857    #[tokio::test(flavor = "multi_thread")]
1858    async fn type_aliases() {
1859        let text = r#"@settings(experimentalFeatures = allow)
1860type MyTy = [number; 2]
1861fn foo(@x: MyTy) {
1862    return x[0]
1863}
1864
1865foo([0, 1])
1866
1867type Other = MyTy | Helix
1868"#;
1869        let result = parse_execute(text).await.unwrap();
1870        let errs = result.exec_state.errors();
1871        assert!(errs.is_empty());
1872    }
1873
1874    #[tokio::test(flavor = "multi_thread")]
1875    async fn test_cannot_shebang_in_fn() {
1876        let ast = r#"
1877fn foo() {
1878  #!hello
1879  return true
1880}
1881
1882foo
1883"#;
1884
1885        let result = parse_execute(ast).await;
1886        let err = result.unwrap_err();
1887        assert_eq!(
1888            err,
1889            KclError::new_syntax(KclErrorDetails::new(
1890                "Unexpected token: #".to_owned(),
1891                vec![SourceRange::new(14, 15, ModuleId::default())],
1892            )),
1893        );
1894    }
1895
1896    #[tokio::test(flavor = "multi_thread")]
1897    async fn test_pattern_transform_function_cannot_access_future_definitions() {
1898        let ast = r#"
1899fn transform(@replicaId) {
1900  // x shouldn't be defined yet.
1901  scale = x
1902  return {
1903    translate = [0, 0, replicaId * 10],
1904    scale = [scale, 1, 0],
1905  }
1906}
1907
1908fn layer() {
1909  return startSketchOn(XY)
1910    |> circle( center= [0, 0], radius= 1, tag = $tag1)
1911    |> extrude(length = 10)
1912}
1913
1914x = 5
1915
1916// The 10 layers are replicas of each other, with a transform applied to each.
1917shape = layer() |> patternTransform(instances = 10, transform = transform)
1918"#;
1919
1920        let result = parse_execute(ast).await;
1921        let err = result.unwrap_err();
1922        assert_eq!(err.message(), "`x` is not defined",);
1923    }
1924
1925    // ADAM: Move some of these into simulation tests.
1926
1927    #[tokio::test(flavor = "multi_thread")]
1928    async fn test_math_execute_with_functions() {
1929        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
1930        let result = parse_execute(ast).await.unwrap();
1931        assert_eq!(
1932            5.0,
1933            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1934                .as_f64()
1935                .unwrap()
1936        );
1937    }
1938
1939    #[tokio::test(flavor = "multi_thread")]
1940    async fn test_math_execute() {
1941        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
1942        let result = parse_execute(ast).await.unwrap();
1943        assert_eq!(
1944            7.4,
1945            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1946                .as_f64()
1947                .unwrap()
1948        );
1949    }
1950
1951    #[tokio::test(flavor = "multi_thread")]
1952    async fn test_math_execute_start_negative() {
1953        let ast = r#"myVar = -5 + 6"#;
1954        let result = parse_execute(ast).await.unwrap();
1955        assert_eq!(
1956            1.0,
1957            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1958                .as_f64()
1959                .unwrap()
1960        );
1961    }
1962
1963    #[tokio::test(flavor = "multi_thread")]
1964    async fn test_math_execute_with_pi() {
1965        let ast = r#"myVar = PI * 2"#;
1966        let result = parse_execute(ast).await.unwrap();
1967        assert_eq!(
1968            std::f64::consts::TAU,
1969            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1970                .as_f64()
1971                .unwrap()
1972        );
1973    }
1974
1975    #[tokio::test(flavor = "multi_thread")]
1976    async fn test_math_define_decimal_without_leading_zero() {
1977        let ast = r#"thing = .4 + 7"#;
1978        let result = parse_execute(ast).await.unwrap();
1979        assert_eq!(
1980            7.4,
1981            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
1982                .as_f64()
1983                .unwrap()
1984        );
1985    }
1986
1987    #[tokio::test(flavor = "multi_thread")]
1988    async fn pass_std_to_std() {
1989        let ast = r#"sketch001 = startSketchOn(XY)
1990profile001 = circle(sketch001, center = [0, 0], radius = 2)
1991extrude001 = extrude(profile001, length = 5)
1992extrudes = patternLinear3d(
1993  extrude001,
1994  instances = 3,
1995  distance = 5,
1996  axis = [1, 1, 0],
1997)
1998clone001 = map(extrudes, f = clone)
1999"#;
2000        parse_execute(ast).await.unwrap();
2001    }
2002
2003    #[tokio::test(flavor = "multi_thread")]
2004    async fn test_array_reduce_nested_array() {
2005        let code = r#"
2006fn id(@el, accum)  { return accum }
2007
2008answer = reduce([], initial=[[[0,0]]], f=id)
2009"#;
2010        let result = parse_execute(code).await.unwrap();
2011        assert_eq!(
2012            mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2013            KclValue::HomArray {
2014                value: vec![KclValue::HomArray {
2015                    value: vec![KclValue::HomArray {
2016                        value: vec![
2017                            KclValue::Number {
2018                                value: 0.0,
2019                                ty: NumericType::default(),
2020                                meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2021                            },
2022                            KclValue::Number {
2023                                value: 0.0,
2024                                ty: NumericType::default(),
2025                                meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2026                            }
2027                        ],
2028                        ty: RuntimeType::any(),
2029                    }],
2030                    ty: RuntimeType::any(),
2031                }],
2032                ty: RuntimeType::any(),
2033            }
2034        );
2035    }
2036
2037    #[tokio::test(flavor = "multi_thread")]
2038    async fn test_zero_param_fn() {
2039        let ast = r#"sigmaAllow = 35000 // psi
2040leg1 = 5 // inches
2041leg2 = 8 // inches
2042fn thickness() { return 0.56 }
2043
2044bracket = startSketchOn(XY)
2045  |> startProfile(at = [0,0])
2046  |> line(end = [0, leg1])
2047  |> line(end = [leg2, 0])
2048  |> line(end = [0, -thickness()])
2049  |> line(end = [-leg2 + thickness(), 0])
2050"#;
2051        parse_execute(ast).await.unwrap();
2052    }
2053
2054    #[tokio::test(flavor = "multi_thread")]
2055    async fn test_unary_operator_not_succeeds() {
2056        let ast = r#"
2057fn returnTrue() { return !false }
2058t = true
2059f = false
2060notTrue = !t
2061notFalse = !f
2062c = !!true
2063d = !returnTrue()
2064
2065assertIs(!false, error = "expected to pass")
2066
2067fn check(x) {
2068  assertIs(!x, error = "expected argument to be false")
2069  return true
2070}
2071check(x = false)
2072"#;
2073        let result = parse_execute(ast).await.unwrap();
2074        assert_eq!(
2075            false,
2076            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2077                .as_bool()
2078                .unwrap()
2079        );
2080        assert_eq!(
2081            true,
2082            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2083                .as_bool()
2084                .unwrap()
2085        );
2086        assert_eq!(
2087            true,
2088            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2089                .as_bool()
2090                .unwrap()
2091        );
2092        assert_eq!(
2093            false,
2094            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2095                .as_bool()
2096                .unwrap()
2097        );
2098    }
2099
2100    #[tokio::test(flavor = "multi_thread")]
2101    async fn test_unary_operator_not_on_non_bool_fails() {
2102        let code1 = r#"
2103// Yup, this is null.
2104myNull = 0 / 0
2105notNull = !myNull
2106"#;
2107        assert_eq!(
2108            parse_execute(code1).await.unwrap_err().message(),
2109            "Cannot apply unary operator ! to non-boolean value: a number",
2110        );
2111
2112        let code2 = "notZero = !0";
2113        assert_eq!(
2114            parse_execute(code2).await.unwrap_err().message(),
2115            "Cannot apply unary operator ! to non-boolean value: a number",
2116        );
2117
2118        let code3 = r#"
2119notEmptyString = !""
2120"#;
2121        assert_eq!(
2122            parse_execute(code3).await.unwrap_err().message(),
2123            "Cannot apply unary operator ! to non-boolean value: a string",
2124        );
2125
2126        let code4 = r#"
2127obj = { a = 1 }
2128notMember = !obj.a
2129"#;
2130        assert_eq!(
2131            parse_execute(code4).await.unwrap_err().message(),
2132            "Cannot apply unary operator ! to non-boolean value: a number",
2133        );
2134
2135        let code5 = "
2136a = []
2137notArray = !a";
2138        assert_eq!(
2139            parse_execute(code5).await.unwrap_err().message(),
2140            "Cannot apply unary operator ! to non-boolean value: an empty array",
2141        );
2142
2143        let code6 = "
2144x = {}
2145notObject = !x";
2146        assert_eq!(
2147            parse_execute(code6).await.unwrap_err().message(),
2148            "Cannot apply unary operator ! to non-boolean value: an object",
2149        );
2150
2151        let code7 = "
2152fn x() { return 1 }
2153notFunction = !x";
2154        let fn_err = parse_execute(code7).await.unwrap_err();
2155        // These are currently printed out as JSON objects, so we don't want to
2156        // check the full error.
2157        assert!(
2158            fn_err
2159                .message()
2160                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2161            "Actual error: {fn_err:?}"
2162        );
2163
2164        let code8 = "
2165myTagDeclarator = $myTag
2166notTagDeclarator = !myTagDeclarator";
2167        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2168        // These are currently printed out as JSON objects, so we don't want to
2169        // check the full error.
2170        assert!(
2171            tag_declarator_err
2172                .message()
2173                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2174            "Actual error: {tag_declarator_err:?}"
2175        );
2176
2177        let code9 = "
2178myTagDeclarator = $myTag
2179notTagIdentifier = !myTag";
2180        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2181        // These are currently printed out as JSON objects, so we don't want to
2182        // check the full error.
2183        assert!(
2184            tag_identifier_err
2185                .message()
2186                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2187            "Actual error: {tag_identifier_err:?}"
2188        );
2189
2190        let code10 = "notPipe = !(1 |> 2)";
2191        assert_eq!(
2192            // TODO: We don't currently parse this, but we should.  It should be
2193            // a runtime error instead.
2194            parse_execute(code10).await.unwrap_err(),
2195            KclError::new_syntax(KclErrorDetails::new(
2196                "Unexpected token: !".to_owned(),
2197                vec![SourceRange::new(10, 11, ModuleId::default())],
2198            ))
2199        );
2200
2201        let code11 = "
2202fn identity(x) { return x }
2203notPipeSub = 1 |> identity(!%))";
2204        assert_eq!(
2205            // TODO: We don't currently parse this, but we should.  It should be
2206            // a runtime error instead.
2207            parse_execute(code11).await.unwrap_err(),
2208            KclError::new_syntax(KclErrorDetails::new(
2209                "There was an unexpected `!`. Try removing it.".to_owned(),
2210                vec![SourceRange::new(56, 57, ModuleId::default())],
2211            ))
2212        );
2213
2214        // TODO: Add these tests when we support these types.
2215        // let notNan = !NaN
2216        // let notInfinity = !Infinity
2217    }
2218
2219    #[tokio::test(flavor = "multi_thread")]
2220    async fn test_start_sketch_on_invalid_kwargs() {
2221        let current_dir = std::env::current_dir().unwrap();
2222        let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2223        let mut code = std::fs::read_to_string(&path).unwrap();
2224        assert_eq!(
2225            parse_execute(&code).await.unwrap_err().message(),
2226            "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2227        );
2228
2229        path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2230        code = std::fs::read_to_string(&path).unwrap();
2231
2232        assert_eq!(
2233            parse_execute(&code).await.unwrap_err().message(),
2234            "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2235        );
2236
2237        path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2238        code = std::fs::read_to_string(&path).unwrap();
2239
2240        assert_eq!(
2241            parse_execute(&code).await.unwrap_err().message(),
2242            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2243        );
2244
2245        path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2246        code = std::fs::read_to_string(&path).unwrap();
2247
2248        assert_eq!(
2249            parse_execute(&code).await.unwrap_err().message(),
2250            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2251        );
2252
2253        path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2254        code = std::fs::read_to_string(&path).unwrap();
2255
2256        assert_eq!(
2257            parse_execute(&code).await.unwrap_err().message(),
2258            "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2259        );
2260    }
2261
2262    #[tokio::test(flavor = "multi_thread")]
2263    async fn test_math_negative_variable_in_binary_expression() {
2264        let ast = r#"sigmaAllow = 35000 // psi
2265width = 1 // inch
2266
2267p = 150 // lbs
2268distance = 6 // inches
2269FOS = 2
2270
2271leg1 = 5 // inches
2272leg2 = 8 // inches
2273
2274thickness_squared = distance * p * FOS * 6 / sigmaAllow
2275thickness = 0.56 // inches. App does not support square root function yet
2276
2277bracket = startSketchOn(XY)
2278  |> startProfile(at = [0,0])
2279  |> line(end = [0, leg1])
2280  |> line(end = [leg2, 0])
2281  |> line(end = [0, -thickness])
2282  |> line(end = [-leg2 + thickness, 0])
2283"#;
2284        parse_execute(ast).await.unwrap();
2285    }
2286
2287    #[tokio::test(flavor = "multi_thread")]
2288    async fn test_execute_function_no_return() {
2289        let ast = r#"fn test(@origin) {
2290  origin
2291}
2292
2293test([0, 0])
2294"#;
2295        let result = parse_execute(ast).await;
2296        assert!(result.is_err());
2297        assert!(result.unwrap_err().to_string().contains("undefined"));
2298    }
2299
2300    #[tokio::test(flavor = "multi_thread")]
2301    async fn test_math_doubly_nested_parens() {
2302        let ast = r#"sigmaAllow = 35000 // psi
2303width = 4 // inch
2304p = 150 // Force on shelf - lbs
2305distance = 6 // inches
2306FOS = 2
2307leg1 = 5 // inches
2308leg2 = 8 // inches
2309thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2310thickness = 0.32 // inches. App does not support square root function yet
2311bracket = startSketchOn(XY)
2312  |> startProfile(at = [0,0])
2313    |> line(end = [0, leg1])
2314  |> line(end = [leg2, 0])
2315  |> line(end = [0, -thickness])
2316  |> line(end = [-1 * leg2 + thickness, 0])
2317  |> line(end = [0, -1 * leg1 + thickness])
2318  |> close()
2319  |> extrude(length = width)
2320"#;
2321        parse_execute(ast).await.unwrap();
2322    }
2323
2324    #[tokio::test(flavor = "multi_thread")]
2325    async fn test_math_nested_parens_one_less() {
2326        let ast = r#" sigmaAllow = 35000 // psi
2327width = 4 // inch
2328p = 150 // Force on shelf - lbs
2329distance = 6 // inches
2330FOS = 2
2331leg1 = 5 // inches
2332leg2 = 8 // inches
2333thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2334thickness = 0.32 // inches. App does not support square root function yet
2335bracket = startSketchOn(XY)
2336  |> startProfile(at = [0,0])
2337    |> line(end = [0, leg1])
2338  |> line(end = [leg2, 0])
2339  |> line(end = [0, -thickness])
2340  |> line(end = [-1 * leg2 + thickness, 0])
2341  |> line(end = [0, -1 * leg1 + thickness])
2342  |> close()
2343  |> extrude(length = width)
2344"#;
2345        parse_execute(ast).await.unwrap();
2346    }
2347
2348    #[tokio::test(flavor = "multi_thread")]
2349    async fn test_fn_as_operand() {
2350        let ast = r#"fn f() { return 1 }
2351x = f()
2352y = x + 1
2353z = f() + 1
2354w = f() + f()
2355"#;
2356        parse_execute(ast).await.unwrap();
2357    }
2358
2359    #[tokio::test(flavor = "multi_thread")]
2360    async fn kcl_test_ids_stable_between_executions() {
2361        let code = r#"sketch001 = startSketchOn(XZ)
2362|> startProfile(at = [61.74, 206.13])
2363|> xLine(length = 305.11, tag = $seg01)
2364|> yLine(length = -291.85)
2365|> xLine(length = -segLen(seg01))
2366|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2367|> close()
2368|> extrude(length = 40.14)
2369|> shell(
2370    thickness = 3.14,
2371    faces = [seg01]
2372)
2373"#;
2374
2375        let ctx = crate::test_server::new_context(true, None).await.unwrap();
2376        let old_program = crate::Program::parse_no_errs(code).unwrap();
2377
2378        // Execute the program.
2379        if let Err(err) = ctx.run_with_caching(old_program).await {
2380            let report = err.into_miette_report_with_outputs(code).unwrap();
2381            let report = miette::Report::new(report);
2382            panic!("Error executing program: {report:?}");
2383        }
2384
2385        // Get the id_generator from the first execution.
2386        let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2387
2388        let code = r#"sketch001 = startSketchOn(XZ)
2389|> startProfile(at = [62.74, 206.13])
2390|> xLine(length = 305.11, tag = $seg01)
2391|> yLine(length = -291.85)
2392|> xLine(length = -segLen(seg01))
2393|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2394|> close()
2395|> extrude(length = 40.14)
2396|> shell(
2397    faces = [seg01],
2398    thickness = 3.14,
2399)
2400"#;
2401
2402        // Execute a slightly different program again.
2403        let program = crate::Program::parse_no_errs(code).unwrap();
2404        // Execute the program.
2405        ctx.run_with_caching(program).await.unwrap();
2406
2407        let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2408
2409        assert_eq!(id_generator, new_id_generator);
2410    }
2411
2412    #[tokio::test(flavor = "multi_thread")]
2413    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2414        let code = r#"sketch001 = startSketchOn(XZ)
2415|> startProfile(at = [61.74, 206.13])
2416|> xLine(length = 305.11, tag = $seg01)
2417|> yLine(length = -291.85)
2418|> xLine(length = -segLen(seg01))
2419|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2420|> close()
2421|> extrude(length = 40.14)
2422|> shell(
2423    thickness = 3.14,
2424    faces = [seg01]
2425)
2426"#;
2427
2428        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2429        let old_program = crate::Program::parse_no_errs(code).unwrap();
2430
2431        // Execute the program.
2432        ctx.run_with_caching(old_program.clone()).await.unwrap();
2433
2434        let settings_state = cache::read_old_ast().await.unwrap().settings;
2435
2436        // Ensure the settings are as expected.
2437        assert_eq!(settings_state, ctx.settings);
2438
2439        // Change a setting.
2440        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2441
2442        // Execute the program.
2443        ctx.run_with_caching(old_program.clone()).await.unwrap();
2444
2445        let settings_state = cache::read_old_ast().await.unwrap().settings;
2446
2447        // Ensure the settings are as expected.
2448        assert_eq!(settings_state, ctx.settings);
2449
2450        // Change a setting.
2451        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2452
2453        // Execute the program.
2454        ctx.run_with_caching(old_program).await.unwrap();
2455
2456        let settings_state = cache::read_old_ast().await.unwrap().settings;
2457
2458        // Ensure the settings are as expected.
2459        assert_eq!(settings_state, ctx.settings);
2460
2461        ctx.close().await;
2462    }
2463
2464    #[tokio::test(flavor = "multi_thread")]
2465    async fn mock_after_not_mock() {
2466        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2467        let program = crate::Program::parse_no_errs("x = 2").unwrap();
2468        let result = ctx.run_with_caching(program).await.unwrap();
2469        assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2470
2471        let ctx2 = ExecutorContext::new_mock(None).await;
2472        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2473        let result = ctx2.run_mock(&program2, true).await.unwrap();
2474        assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2475
2476        ctx.close().await;
2477        ctx2.close().await;
2478    }
2479
2480    #[cfg(feature = "artifact-graph")]
2481    #[tokio::test(flavor = "multi_thread")]
2482    async fn mock_has_stable_ids() {
2483        let ctx = ExecutorContext::new_mock(None).await;
2484        let code = "sk = startSketchOn(XY)
2485        |> startProfile(at = [0, 0])";
2486        let program = crate::Program::parse_no_errs(code).unwrap();
2487        let result = ctx.run_mock(&program, false).await.unwrap();
2488        let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2489        assert!(!ids.is_empty(), "IDs should not be empty");
2490
2491        let ctx2 = ExecutorContext::new_mock(None).await;
2492        let program2 = crate::Program::parse_no_errs(code).unwrap();
2493        let result = ctx2.run_mock(&program2, false).await.unwrap();
2494        let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2495
2496        assert_eq!(ids, ids2, "Generated IDs should match");
2497        ctx.close().await;
2498        ctx2.close().await;
2499    }
2500
2501    #[cfg(feature = "artifact-graph")]
2502    #[tokio::test(flavor = "multi_thread")]
2503    async fn sim_sketch_mode_real_mock_real() {
2504        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2505        let code = r#"sketch001 = startSketchOn(XY)
2506profile001 = startProfile(sketch001, at = [0, 0])
2507  |> line(end = [10, 0])
2508  |> line(end = [0, 10])
2509  |> line(end = [-10, 0])
2510  |> line(end = [0, -10])
2511  |> close()
2512"#;
2513        let program = crate::Program::parse_no_errs(code).unwrap();
2514        let result = ctx.run_with_caching(program).await.unwrap();
2515        assert_eq!(result.operations.len(), 1);
2516
2517        let mock_ctx = ExecutorContext::new_mock(None).await;
2518        let mock_program = crate::Program::parse_no_errs(code).unwrap();
2519        let mock_result = mock_ctx.run_mock(&mock_program, true).await.unwrap();
2520        assert_eq!(mock_result.operations.len(), 1);
2521
2522        let code2 = code.to_owned()
2523            + r#"
2524extrude001 = extrude(profile001, length = 10)
2525"#;
2526        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
2527        let result = ctx.run_with_caching(program2).await.unwrap();
2528        assert_eq!(result.operations.len(), 2);
2529
2530        ctx.close().await;
2531        mock_ctx.close().await;
2532    }
2533
2534    #[tokio::test(flavor = "multi_thread")]
2535    async fn read_tag_version() {
2536        let ast = r#"fn bar(@t) {
2537  return startSketchOn(XY)
2538    |> startProfile(at = [0,0])
2539    |> angledLine(
2540        angle = -60,
2541        length = segLen(t),
2542    )
2543    |> line(end = [0, 0])
2544    |> close()
2545}
2546
2547sketch = startSketchOn(XY)
2548  |> startProfile(at = [0,0])
2549  |> line(end = [0, 10])
2550  |> line(end = [10, 0], tag = $tag0)
2551  |> line(end = [0, 0])
2552
2553fn foo() {
2554  // tag0 tags an edge
2555  return bar(tag0)
2556}
2557
2558solid = sketch |> extrude(length = 10)
2559// tag0 tags a face
2560sketch2 = startSketchOn(solid, face = tag0)
2561  |> startProfile(at = [0,0])
2562  |> line(end = [0, 1])
2563  |> line(end = [1, 0])
2564  |> line(end = [0, 0])
2565
2566foo() |> extrude(length = 1)
2567"#;
2568        parse_execute(ast).await.unwrap();
2569    }
2570
2571    #[tokio::test(flavor = "multi_thread")]
2572    async fn experimental() {
2573        let code = r#"
2574startSketchOn(XY)
2575  |> startProfile(at = [0, 0], tag = $start)
2576  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2577"#;
2578        let result = parse_execute(code).await.unwrap();
2579        let errors = result.exec_state.errors();
2580        assert_eq!(errors.len(), 1);
2581        assert_eq!(errors[0].severity, Severity::Error);
2582        let msg = &errors[0].message;
2583        assert!(msg.contains("experimental"), "found {msg}");
2584
2585        let code = r#"@settings(experimentalFeatures = allow)
2586startSketchOn(XY)
2587  |> startProfile(at = [0, 0], tag = $start)
2588  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2589"#;
2590        let result = parse_execute(code).await.unwrap();
2591        let errors = result.exec_state.errors();
2592        assert!(errors.is_empty());
2593
2594        let code = r#"@settings(experimentalFeatures = warn)
2595startSketchOn(XY)
2596  |> startProfile(at = [0, 0], tag = $start)
2597  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2598"#;
2599        let result = parse_execute(code).await.unwrap();
2600        let errors = result.exec_state.errors();
2601        assert_eq!(errors.len(), 1);
2602        assert_eq!(errors[0].severity, Severity::Warning);
2603        let msg = &errors[0].message;
2604        assert!(msg.contains("experimental"), "found {msg}");
2605
2606        let code = r#"@settings(experimentalFeatures = deny)
2607startSketchOn(XY)
2608  |> startProfile(at = [0, 0], tag = $start)
2609  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2610"#;
2611        let result = parse_execute(code).await.unwrap();
2612        let errors = result.exec_state.errors();
2613        assert_eq!(errors.len(), 1);
2614        assert_eq!(errors[0].severity, Severity::Error);
2615        let msg = &errors[0].message;
2616        assert!(msg.contains("experimental"), "found {msg}");
2617
2618        let code = r#"@settings(experimentalFeatures = foo)
2619startSketchOn(XY)
2620  |> startProfile(at = [0, 0], tag = $start)
2621  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2622"#;
2623        parse_execute(code).await.unwrap_err();
2624    }
2625}