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