kcl_lib/execution/
mod.rs

1//! The executor for the AST.
2
3use std::sync::Arc;
4
5use anyhow::Result;
6#[cfg(feature = "artifact-graph")]
7pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketchOnFace, StartSketchOnPlane};
8use cache::GlobalState;
9pub use cache::{bust_cache, clear_mem_cache};
10#[cfg(feature = "artifact-graph")]
11pub use cad_op::Group;
12pub use cad_op::Operation;
13pub use geometry::*;
14pub use id_generator::IdGenerator;
15pub(crate) use import::PreImportedGeometry;
16use indexmap::IndexMap;
17pub use kcl_value::{KclObjectFields, KclValue};
18use kcmc::{
19    ImageFormat, ModelingCmd, each_cmd as mcmd,
20    ok_response::{OkModelingCmdResponse, output::TakeSnapshot},
21    websocket::{ModelingSessionData, OkWebSocketResponseData},
22};
23use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
24pub use memory::EnvironmentRef;
25pub(crate) use modeling::ModelingCmdMeta;
26use schemars::JsonSchema;
27use serde::{Deserialize, Serialize};
28pub(crate) use state::ModuleArtifactState;
29pub use state::{ExecState, MetaSettings};
30use uuid::Uuid;
31
32use crate::{
33    CompilationError, ExecError, KclErrorWithOutputs,
34    engine::{EngineManager, GridScaleBehavior},
35    errors::{KclError, KclErrorDetails},
36    execution::{
37        cache::{CacheInformation, CacheResult},
38        import_graph::{Universe, UniverseMap},
39        typed_path::TypedPath,
40        types::{UnitAngle, UnitLen},
41    },
42    fs::FileManager,
43    modules::{ModuleId, ModulePath, ModuleRepr},
44    parsing::ast::types::{Expr, ImportPath, NodeRef},
45    source_range::SourceRange,
46};
47
48pub(crate) mod annotations;
49#[cfg(feature = "artifact-graph")]
50mod artifact;
51pub(crate) mod cache;
52mod cad_op;
53mod exec_ast;
54pub mod fn_call;
55mod geometry;
56mod id_generator;
57mod import;
58mod import_graph;
59pub(crate) mod kcl_value;
60mod memory;
61mod modeling;
62mod state;
63pub mod typed_path;
64pub(crate) mod types;
65
66enum StatementKind<'a> {
67    Declaration { name: &'a str },
68    Expression,
69}
70
71/// Outcome of executing a program.  This is used in TS.
72#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
73#[ts(export)]
74#[serde(rename_all = "camelCase")]
75pub struct ExecOutcome {
76    /// Variables in the top-level of the root module. Note that functions will have an invalid env ref.
77    pub variables: IndexMap<String, KclValue>,
78    /// Operations that have been performed in execution order, for display in
79    /// the Feature Tree.
80    #[cfg(feature = "artifact-graph")]
81    pub operations: Vec<Operation>,
82    /// Output artifact graph.
83    #[cfg(feature = "artifact-graph")]
84    pub artifact_graph: ArtifactGraph,
85    /// Non-fatal errors and warnings.
86    pub errors: Vec<CompilationError>,
87    /// File Names in module Id array index order
88    pub filenames: IndexMap<ModuleId, ModulePath>,
89    /// The default planes.
90    pub default_planes: Option<DefaultPlanes>,
91}
92
93#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
94#[ts(export)]
95#[serde(rename_all = "camelCase")]
96pub struct DefaultPlanes {
97    pub xy: uuid::Uuid,
98    pub xz: uuid::Uuid,
99    pub yz: uuid::Uuid,
100    pub neg_xy: uuid::Uuid,
101    pub neg_xz: uuid::Uuid,
102    pub neg_yz: uuid::Uuid,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
106#[ts(export)]
107#[serde(tag = "type", rename_all = "camelCase")]
108pub struct TagIdentifier {
109    pub value: String,
110    // Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info
111    // was written.
112    #[serde(skip)]
113    pub info: Vec<(usize, TagEngineInfo)>,
114    #[serde(skip)]
115    pub meta: Vec<Metadata>,
116}
117
118impl TagIdentifier {
119    /// Get the tag info for this tag at a specified epoch.
120    pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
121        for (e, info) in self.info.iter().rev() {
122            if *e <= at_epoch {
123                return Some(info);
124            }
125        }
126
127        None
128    }
129
130    /// Get the most recent tag info for this tag.
131    pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
132        self.info.last().map(|i| &i.1)
133    }
134
135    /// Add info from a different instance of this tag.
136    pub fn merge_info(&mut self, other: &TagIdentifier) {
137        assert_eq!(&self.value, &other.value);
138        for (oe, ot) in &other.info {
139            if let Some((e, t)) = self.info.last_mut() {
140                // If there is newer info, then skip this iteration.
141                if *e > *oe {
142                    continue;
143                }
144                // If we're in the same epoch, then overwrite.
145                if e == oe {
146                    *t = ot.clone();
147                    continue;
148                }
149            }
150            self.info.push((*oe, ot.clone()));
151        }
152    }
153}
154
155impl Eq for TagIdentifier {}
156
157impl std::fmt::Display for TagIdentifier {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        write!(f, "{}", self.value)
160    }
161}
162
163impl std::str::FromStr for TagIdentifier {
164    type Err = KclError;
165
166    fn from_str(s: &str) -> Result<Self, Self::Err> {
167        Ok(Self {
168            value: s.to_string(),
169            info: Vec::new(),
170            meta: Default::default(),
171        })
172    }
173}
174
175impl Ord for TagIdentifier {
176    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
177        self.value.cmp(&other.value)
178    }
179}
180
181impl PartialOrd for TagIdentifier {
182    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
183        Some(self.cmp(other))
184    }
185}
186
187impl std::hash::Hash for TagIdentifier {
188    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
189        self.value.hash(state);
190    }
191}
192
193/// Engine information for a tag.
194#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
195#[ts(export)]
196#[serde(tag = "type", rename_all = "camelCase")]
197pub struct TagEngineInfo {
198    /// The id of the tagged object.
199    pub id: uuid::Uuid,
200    /// The sketch the tag is on.
201    pub sketch: uuid::Uuid,
202    /// The path the tag is on.
203    pub path: Option<Path>,
204    /// The surface information for the tag.
205    pub surface: Option<ExtrudeSurface>,
206}
207
208#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
209pub enum BodyType {
210    Root,
211    Block,
212}
213
214/// Metadata.
215#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
216#[ts(export)]
217#[serde(rename_all = "camelCase")]
218pub struct Metadata {
219    /// The source range.
220    pub source_range: SourceRange,
221}
222
223impl From<Metadata> for Vec<SourceRange> {
224    fn from(meta: Metadata) -> Self {
225        vec![meta.source_range]
226    }
227}
228
229impl From<SourceRange> for Metadata {
230    fn from(source_range: SourceRange) -> Self {
231        Self { source_range }
232    }
233}
234
235impl<T> From<NodeRef<'_, T>> for Metadata {
236    fn from(node: NodeRef<'_, T>) -> Self {
237        Self {
238            source_range: SourceRange::new(node.start, node.end, node.module_id),
239        }
240    }
241}
242
243impl From<&Expr> for Metadata {
244    fn from(expr: &Expr) -> Self {
245        Self {
246            source_range: SourceRange::from(expr),
247        }
248    }
249}
250
251/// The type of ExecutorContext being used
252#[derive(PartialEq, Debug, Default, Clone)]
253pub enum ContextType {
254    /// Live engine connection
255    #[default]
256    Live,
257
258    /// Completely mocked connection
259    /// Mock mode is only for the Design Studio when they just want to mock engine calls and not
260    /// actually make them.
261    Mock,
262
263    /// Handled by some other interpreter/conversion system
264    MockCustomForwarded,
265}
266
267/// The executor context.
268/// Cloning will return another handle to the same engine connection/session,
269/// as this uses `Arc` under the hood.
270#[derive(Debug, Clone)]
271pub struct ExecutorContext {
272    pub engine: Arc<Box<dyn EngineManager>>,
273    pub fs: Arc<FileManager>,
274    pub settings: ExecutorSettings,
275    pub context_type: ContextType,
276}
277
278/// The executor settings.
279#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
280#[ts(export)]
281pub struct ExecutorSettings {
282    /// Highlight edges of 3D objects?
283    pub highlight_edges: bool,
284    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
285    pub enable_ssao: bool,
286    /// Show grid?
287    pub show_grid: bool,
288    /// Should engine store this for replay?
289    /// If so, under what name?
290    pub replay: Option<String>,
291    /// The directory of the current project.  This is used for resolving import
292    /// paths.  If None is given, the current working directory is used.
293    pub project_directory: Option<TypedPath>,
294    /// This is the path to the current file being executed.
295    /// We use this for preventing cyclic imports.
296    pub current_file: Option<TypedPath>,
297    /// Whether or not to automatically scale the grid when user zooms.
298    pub fixed_size_grid: bool,
299}
300
301impl Default for ExecutorSettings {
302    fn default() -> Self {
303        Self {
304            highlight_edges: true,
305            enable_ssao: false,
306            show_grid: false,
307            replay: None,
308            project_directory: None,
309            current_file: None,
310            fixed_size_grid: true,
311        }
312    }
313}
314
315impl From<crate::settings::types::Configuration> for ExecutorSettings {
316    fn from(config: crate::settings::types::Configuration) -> Self {
317        Self::from(config.settings)
318    }
319}
320
321impl From<crate::settings::types::Settings> for ExecutorSettings {
322    fn from(settings: crate::settings::types::Settings) -> Self {
323        Self {
324            highlight_edges: settings.modeling.highlight_edges.into(),
325            enable_ssao: settings.modeling.enable_ssao.into(),
326            show_grid: settings.modeling.show_scale_grid,
327            replay: None,
328            project_directory: None,
329            current_file: None,
330            fixed_size_grid: settings.app.fixed_size_grid,
331        }
332    }
333}
334
335impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
336    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
337        Self::from(config.settings.modeling)
338    }
339}
340
341impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
342    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
343        Self {
344            highlight_edges: modeling.highlight_edges.into(),
345            enable_ssao: modeling.enable_ssao.into(),
346            show_grid: modeling.show_scale_grid,
347            replay: None,
348            project_directory: None,
349            current_file: None,
350            fixed_size_grid: true,
351        }
352    }
353}
354
355impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
356    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
357        Self {
358            highlight_edges: modeling.highlight_edges.into(),
359            enable_ssao: modeling.enable_ssao.into(),
360            show_grid: Default::default(),
361            replay: None,
362            project_directory: None,
363            current_file: None,
364            fixed_size_grid: true,
365        }
366    }
367}
368
369impl ExecutorSettings {
370    /// Add the current file path to the executor settings.
371    pub fn with_current_file(&mut self, current_file: TypedPath) {
372        // We want the parent directory of the file.
373        if current_file.extension() == Some("kcl") {
374            self.current_file = Some(current_file.clone());
375            // Get the parent directory.
376            if let Some(parent) = current_file.parent() {
377                self.project_directory = Some(parent);
378            } else {
379                self.project_directory = Some(TypedPath::from(""));
380            }
381        } else {
382            self.project_directory = Some(current_file.clone());
383        }
384    }
385}
386
387impl ExecutorContext {
388    /// Create a new default executor context.
389    #[cfg(not(target_arch = "wasm32"))]
390    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
391        let (ws, _headers) = client
392            .modeling()
393            .commands_ws(
394                None,
395                None,
396                None,
397                if settings.enable_ssao {
398                    Some(kittycad::types::PostEffectType::Ssao)
399                } else {
400                    None
401                },
402                settings.replay.clone(),
403                if settings.show_grid { Some(true) } else { None },
404                None,
405                None,
406                None,
407                Some(false),
408            )
409            .await?;
410
411        let engine: Arc<Box<dyn EngineManager>> =
412            Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
413
414        Ok(Self {
415            engine,
416            fs: Arc::new(FileManager::new()),
417            settings,
418            context_type: ContextType::Live,
419        })
420    }
421
422    #[cfg(target_arch = "wasm32")]
423    pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
424        ExecutorContext {
425            engine,
426            fs,
427            settings,
428            context_type: ContextType::Live,
429        }
430    }
431
432    #[cfg(not(target_arch = "wasm32"))]
433    pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
434        ExecutorContext {
435            engine: Arc::new(Box::new(
436                crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
437            )),
438            fs: Arc::new(FileManager::new()),
439            settings: settings.unwrap_or_default(),
440            context_type: ContextType::Mock,
441        }
442    }
443
444    #[cfg(target_arch = "wasm32")]
445    pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
446        ExecutorContext {
447            engine,
448            fs,
449            settings,
450            context_type: ContextType::Mock,
451        }
452    }
453
454    #[cfg(not(target_arch = "wasm32"))]
455    pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
456        ExecutorContext {
457            engine,
458            fs: Arc::new(FileManager::new()),
459            settings: Default::default(),
460            context_type: ContextType::MockCustomForwarded,
461        }
462    }
463
464    /// Create a new default executor context.
465    /// With a kittycad client.
466    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
467    /// variables.
468    /// But also allows for passing in a token and engine address directly.
469    #[cfg(not(target_arch = "wasm32"))]
470    pub async fn new_with_client(
471        settings: ExecutorSettings,
472        token: Option<String>,
473        engine_addr: Option<String>,
474    ) -> Result<Self> {
475        // Create the client.
476        let client = crate::engine::new_zoo_client(token, engine_addr)?;
477
478        let ctx = Self::new(&client, settings).await?;
479        Ok(ctx)
480    }
481
482    /// Create a new default executor context.
483    /// With the default kittycad client.
484    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
485    /// variables.
486    #[cfg(not(target_arch = "wasm32"))]
487    pub async fn new_with_default_client() -> Result<Self> {
488        // Create the client.
489        let ctx = Self::new_with_client(Default::default(), None, None).await?;
490        Ok(ctx)
491    }
492
493    /// For executing unit tests.
494    #[cfg(not(target_arch = "wasm32"))]
495    pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
496        let ctx = ExecutorContext::new_with_client(
497            ExecutorSettings {
498                highlight_edges: true,
499                enable_ssao: false,
500                show_grid: false,
501                replay: None,
502                project_directory: None,
503                current_file: None,
504                fixed_size_grid: false,
505            },
506            None,
507            engine_addr,
508        )
509        .await?;
510        Ok(ctx)
511    }
512
513    pub fn is_mock(&self) -> bool {
514        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
515    }
516
517    /// Returns true if we should not send engine commands for any reason.
518    pub async fn no_engine_commands(&self) -> bool {
519        self.is_mock()
520    }
521
522    pub async fn send_clear_scene(
523        &self,
524        exec_state: &mut ExecState,
525        source_range: crate::execution::SourceRange,
526    ) -> Result<(), KclError> {
527        // Ensure artifacts are cleared so that we don't accumulate them across
528        // runs.
529        exec_state.mod_local.artifacts.clear();
530        exec_state.global.root_module_artifacts.clear();
531        exec_state.global.artifacts.clear();
532
533        self.engine
534            .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
535            .await
536    }
537
538    pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
539        cache::bust_cache().await;
540
541        // Execute an empty program to clear and reset the scene.
542        // We specifically want to be returned the objects after the scene is reset.
543        // Like the default planes so it is easier to just execute an empty program
544        // after the cache is busted.
545        let outcome = self.run_with_caching(crate::Program::empty()).await?;
546
547        Ok(outcome)
548    }
549
550    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
551        self.eval_prelude(exec_state, SourceRange::synthetic())
552            .await
553            .map_err(KclErrorWithOutputs::no_outputs)?;
554        exec_state.mut_stack().push_new_root_env(true);
555        Ok(())
556    }
557
558    pub async fn run_mock(
559        &self,
560        program: &crate::Program,
561        use_prev_memory: bool,
562    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
563        assert!(
564            self.is_mock(),
565            "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
566        );
567
568        let mut exec_state = ExecState::new(self);
569        if use_prev_memory {
570            match cache::read_old_memory().await {
571                Some(mem) => {
572                    *exec_state.mut_stack() = mem.0;
573                    exec_state.global.module_infos = mem.1;
574                }
575                None => self.prepare_mem(&mut exec_state).await?,
576            }
577        } else {
578            self.prepare_mem(&mut exec_state).await?
579        };
580
581        // Push a scope so that old variables can be overwritten (since we might be re-executing some
582        // part of the scene).
583        exec_state.mut_stack().push_new_env_for_scope();
584
585        let result = self.inner_run(program, &mut exec_state, true).await?;
586
587        // Restore any temporary variables, then save any newly created variables back to
588        // memory in case another run wants to use them. Note this is just saved to the preserved
589        // memory, not to the exec_state which is not cached for mock execution.
590
591        let mut mem = exec_state.stack().clone();
592        let module_infos = exec_state.global.module_infos.clone();
593        let outcome = exec_state.into_exec_outcome(result.0, self).await;
594
595        mem.squash_env(result.0);
596        cache::write_old_memory((mem, module_infos)).await;
597
598        Ok(outcome)
599    }
600
601    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
602        assert!(!self.is_mock());
603        let grid_scale = if self.settings.fixed_size_grid {
604            GridScaleBehavior::Fixed(
605                program
606                    .meta_settings()
607                    .ok()
608                    .flatten()
609                    .map(|s| s.default_length_units)
610                    .map(kcmc::units::UnitLength::from),
611            )
612        } else {
613            GridScaleBehavior::ScaleWithZoom
614        };
615
616        let (program, exec_state, result) = match cache::read_old_ast().await {
617            Some(mut cached_state) => {
618                let old = CacheInformation {
619                    ast: &cached_state.main.ast,
620                    settings: &cached_state.settings,
621                };
622                let new = CacheInformation {
623                    ast: &program.ast,
624                    settings: &self.settings,
625                };
626
627                // Get the program that actually changed from the old and new information.
628                let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
629                    CacheResult::ReExecute {
630                        clear_scene,
631                        reapply_settings,
632                        program: changed_program,
633                    } => {
634                        if reapply_settings
635                            && self
636                                .engine
637                                .reapply_settings(
638                                    &self.settings,
639                                    Default::default(),
640                                    &mut cached_state.main.exec_state.id_generator,
641                                    grid_scale,
642                                )
643                                .await
644                                .is_err()
645                        {
646                            (true, program, None)
647                        } else {
648                            (
649                                clear_scene,
650                                crate::Program {
651                                    ast: changed_program,
652                                    original_file_contents: program.original_file_contents,
653                                },
654                                None,
655                            )
656                        }
657                    }
658                    CacheResult::CheckImportsOnly {
659                        reapply_settings,
660                        ast: changed_program,
661                    } => {
662                        if reapply_settings
663                            && self
664                                .engine
665                                .reapply_settings(
666                                    &self.settings,
667                                    Default::default(),
668                                    &mut cached_state.main.exec_state.id_generator,
669                                    grid_scale,
670                                )
671                                .await
672                                .is_err()
673                        {
674                            (true, program, None)
675                        } else {
676                            // We need to check our imports to see if they changed.
677                            let mut new_exec_state = ExecState::new(self);
678                            let (new_universe, new_universe_map) =
679                                self.get_universe(&program, &mut new_exec_state).await?;
680
681                            let clear_scene = new_universe.values().any(|value| {
682                                let id = value.1;
683                                match (
684                                    cached_state.exec_state.get_source(id),
685                                    new_exec_state.global.get_source(id),
686                                ) {
687                                    (Some(s0), Some(s1)) => s0.source != s1.source,
688                                    _ => false,
689                                }
690                            });
691
692                            if !clear_scene {
693                                // Return early we don't need to clear the scene.
694                                return Ok(cached_state.into_exec_outcome(self).await);
695                            }
696
697                            (
698                                true,
699                                crate::Program {
700                                    ast: changed_program,
701                                    original_file_contents: program.original_file_contents,
702                                },
703                                Some((new_universe, new_universe_map, new_exec_state)),
704                            )
705                        }
706                    }
707                    CacheResult::NoAction(true) => {
708                        if self
709                            .engine
710                            .reapply_settings(
711                                &self.settings,
712                                Default::default(),
713                                &mut cached_state.main.exec_state.id_generator,
714                                grid_scale,
715                            )
716                            .await
717                            .is_ok()
718                        {
719                            // We need to update the old ast state with the new settings!!
720                            cache::write_old_ast(GlobalState::with_settings(
721                                cached_state.clone(),
722                                self.settings.clone(),
723                            ))
724                            .await;
725
726                            return Ok(cached_state.into_exec_outcome(self).await);
727                        }
728                        (true, program, None)
729                    }
730                    CacheResult::NoAction(false) => {
731                        return Ok(cached_state.into_exec_outcome(self).await);
732                    }
733                };
734
735                let (exec_state, result) = match import_check_info {
736                    Some((new_universe, new_universe_map, mut new_exec_state)) => {
737                        // Clear the scene if the imports changed.
738                        self.send_clear_scene(&mut new_exec_state, Default::default())
739                            .await
740                            .map_err(KclErrorWithOutputs::no_outputs)?;
741
742                        let result = self
743                            .run_concurrent(
744                                &program,
745                                &mut new_exec_state,
746                                Some((new_universe, new_universe_map)),
747                                false,
748                            )
749                            .await;
750
751                        (new_exec_state, result)
752                    }
753                    None if clear_scene => {
754                        // Pop the execution state, since we are starting fresh.
755                        let mut exec_state = cached_state.reconstitute_exec_state();
756                        exec_state.reset(self);
757
758                        self.send_clear_scene(&mut exec_state, Default::default())
759                            .await
760                            .map_err(KclErrorWithOutputs::no_outputs)?;
761
762                        let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
763
764                        (exec_state, result)
765                    }
766                    None => {
767                        let mut exec_state = cached_state.reconstitute_exec_state();
768                        exec_state.mut_stack().restore_env(cached_state.main.result_env);
769
770                        let result = self.run_concurrent(&program, &mut exec_state, None, true).await;
771
772                        (exec_state, result)
773                    }
774                };
775
776                (program, exec_state, result)
777            }
778            None => {
779                let mut exec_state = ExecState::new(self);
780                self.send_clear_scene(&mut exec_state, Default::default())
781                    .await
782                    .map_err(KclErrorWithOutputs::no_outputs)?;
783
784                let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
785
786                (program, exec_state, result)
787            }
788        };
789
790        if result.is_err() {
791            cache::bust_cache().await;
792        }
793
794        // Throw the error.
795        let result = result?;
796
797        // Save this as the last successful execution to the cache.
798        cache::write_old_ast(GlobalState::new(
799            exec_state.clone(),
800            self.settings.clone(),
801            program.ast,
802            result.0,
803        ))
804        .await;
805
806        let outcome = exec_state.into_exec_outcome(result.0, self).await;
807        Ok(outcome)
808    }
809
810    /// Perform the execution of a program.
811    ///
812    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
813    pub async fn run(
814        &self,
815        program: &crate::Program,
816        exec_state: &mut ExecState,
817    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
818        self.run_concurrent(program, exec_state, None, false).await
819    }
820
821    /// Perform the execution of a program using a concurrent
822    /// execution model.
823    ///
824    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
825    pub async fn run_concurrent(
826        &self,
827        program: &crate::Program,
828        exec_state: &mut ExecState,
829        universe_info: Option<(Universe, UniverseMap)>,
830        preserve_mem: bool,
831    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
832        // Reuse our cached universe if we have one.
833
834        let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
835            (universe, universe_map)
836        } else {
837            self.get_universe(program, exec_state).await?
838        };
839
840        let default_planes = self.engine.get_default_planes().read().await.clone();
841
842        // Run the prelude to set up the engine.
843        self.eval_prelude(exec_state, SourceRange::synthetic())
844            .await
845            .map_err(KclErrorWithOutputs::no_outputs)?;
846
847        for modules in import_graph::import_graph(&universe, self)
848            .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
849            .into_iter()
850        {
851            #[cfg(not(target_arch = "wasm32"))]
852            let mut set = tokio::task::JoinSet::new();
853
854            #[allow(clippy::type_complexity)]
855            let (results_tx, mut results_rx): (
856                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
857                tokio::sync::mpsc::Receiver<_>,
858            ) = tokio::sync::mpsc::channel(1);
859
860            for module in modules {
861                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
862                    return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
863                        KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
864                    )));
865                };
866                let module_id = *module_id;
867                let module_path = module_path.clone();
868                let source_range = SourceRange::from(import_stmt);
869                // Clone before mutating.
870                let module_exec_state = exec_state.clone();
871
872                self.add_import_module_ops(
873                    exec_state,
874                    program,
875                    module_id,
876                    &module_path,
877                    source_range,
878                    &universe_map,
879                );
880
881                let repr = repr.clone();
882                let exec_ctxt = self.clone();
883                let results_tx = results_tx.clone();
884
885                let exec_module = async |exec_ctxt: &ExecutorContext,
886                                         repr: &ModuleRepr,
887                                         module_id: ModuleId,
888                                         module_path: &ModulePath,
889                                         exec_state: &mut ExecState,
890                                         source_range: SourceRange|
891                       -> Result<ModuleRepr, KclError> {
892                    match repr {
893                        ModuleRepr::Kcl(program, _) => {
894                            let result = exec_ctxt
895                                .exec_module_from_ast(program, module_id, module_path, exec_state, source_range, false)
896                                .await;
897
898                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
899                        }
900                        ModuleRepr::Foreign(geom, _) => {
901                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
902                                .await
903                                .map(|geom| Some(KclValue::ImportedGeometry(geom)));
904
905                            result.map(|val| {
906                                ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
907                            })
908                        }
909                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
910                            format!("Module {module_path} not found in universe"),
911                            vec![source_range],
912                        ))),
913                    }
914                };
915
916                #[cfg(target_arch = "wasm32")]
917                {
918                    wasm_bindgen_futures::spawn_local(async move {
919                        let mut exec_state = module_exec_state;
920                        let exec_ctxt = exec_ctxt;
921
922                        let result = exec_module(
923                            &exec_ctxt,
924                            &repr,
925                            module_id,
926                            &module_path,
927                            &mut exec_state,
928                            source_range,
929                        )
930                        .await;
931
932                        results_tx
933                            .send((module_id, module_path, result))
934                            .await
935                            .unwrap_or_default();
936                    });
937                }
938                #[cfg(not(target_arch = "wasm32"))]
939                {
940                    set.spawn(async move {
941                        let mut exec_state = module_exec_state;
942                        let exec_ctxt = exec_ctxt;
943
944                        let result = exec_module(
945                            &exec_ctxt,
946                            &repr,
947                            module_id,
948                            &module_path,
949                            &mut exec_state,
950                            source_range,
951                        )
952                        .await;
953
954                        results_tx
955                            .send((module_id, module_path, result))
956                            .await
957                            .unwrap_or_default();
958                    });
959                }
960            }
961
962            drop(results_tx);
963
964            while let Some((module_id, _, result)) = results_rx.recv().await {
965                match result {
966                    Ok(new_repr) => {
967                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
968
969                        match &mut repr {
970                            ModuleRepr::Kcl(_, cache) => {
971                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
972                                    unreachable!();
973                                };
974                                *cache = session_data;
975                            }
976                            ModuleRepr::Foreign(_, cache) => {
977                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
978                                    unreachable!();
979                                };
980                                *cache = session_data;
981                            }
982                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
983                        }
984
985                        exec_state.global.module_infos[&module_id].restore_repr(repr);
986                    }
987                    Err(e) => {
988                        return Err(exec_state.error_with_outputs(e, None, default_planes));
989                    }
990                }
991            }
992        }
993
994        // Since we haven't technically started executing the root module yet,
995        // the operations corresponding to the imports will be missing unless we
996        // track them here.
997        exec_state
998            .global
999            .root_module_artifacts
1000            .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1001
1002        self.inner_run(program, exec_state, preserve_mem).await
1003    }
1004
1005    /// Get the universe & universe map of the program.
1006    /// And see if any of the imports changed.
1007    async fn get_universe(
1008        &self,
1009        program: &crate::Program,
1010        exec_state: &mut ExecState,
1011    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1012        exec_state.add_root_module_contents(program);
1013
1014        let mut universe = std::collections::HashMap::new();
1015
1016        let default_planes = self.engine.get_default_planes().read().await.clone();
1017
1018        let root_imports = import_graph::import_universe(
1019            self,
1020            &ModulePath::Main,
1021            &ModuleRepr::Kcl(program.ast.clone(), None),
1022            &mut universe,
1023            exec_state,
1024        )
1025        .await
1026        .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1027
1028        Ok((universe, root_imports))
1029    }
1030
1031    #[cfg(not(feature = "artifact-graph"))]
1032    fn add_import_module_ops(
1033        &self,
1034        _exec_state: &mut ExecState,
1035        _program: &crate::Program,
1036        _module_id: ModuleId,
1037        _module_path: &ModulePath,
1038        _source_range: SourceRange,
1039        _universe_map: &UniverseMap,
1040    ) {
1041    }
1042
1043    #[cfg(feature = "artifact-graph")]
1044    fn add_import_module_ops(
1045        &self,
1046        exec_state: &mut ExecState,
1047        program: &crate::Program,
1048        module_id: ModuleId,
1049        module_path: &ModulePath,
1050        source_range: SourceRange,
1051        universe_map: &UniverseMap,
1052    ) {
1053        match module_path {
1054            ModulePath::Main => {
1055                // This should never happen.
1056            }
1057            ModulePath::Local { value, .. } => {
1058                // We only want to display the top-level module imports in
1059                // the Feature Tree, not transitive imports.
1060                if universe_map.contains_key(value) {
1061                    use crate::NodePath;
1062
1063                    let node_path = if source_range.is_top_level_module() {
1064                        let cached_body_items = exec_state.global.artifacts.cached_body_items();
1065                        NodePath::from_range(&program.ast, cached_body_items, source_range).unwrap_or_default()
1066                    } else {
1067                        // The frontend doesn't care about paths in
1068                        // files other than the top-level module.
1069                        NodePath::placeholder()
1070                    };
1071
1072                    exec_state.push_op(Operation::GroupBegin {
1073                        group: Group::ModuleInstance {
1074                            name: value.file_name().unwrap_or_default(),
1075                            module_id,
1076                        },
1077                        node_path,
1078                        source_range,
1079                    });
1080                    // Due to concurrent execution, we cannot easily
1081                    // group operations by module. So we leave the
1082                    // group empty and close it immediately.
1083                    exec_state.push_op(Operation::GroupEnd);
1084                }
1085            }
1086            ModulePath::Std { .. } => {
1087                // We don't want to display stdlib in the Feature Tree.
1088            }
1089        }
1090    }
1091
1092    /// Perform the execution of a program.  Accept all possible parameters and
1093    /// output everything.
1094    async fn inner_run(
1095        &self,
1096        program: &crate::Program,
1097        exec_state: &mut ExecState,
1098        preserve_mem: bool,
1099    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1100        let _stats = crate::log::LogPerfStats::new("Interpretation");
1101
1102        // Re-apply the settings, in case the cache was busted.
1103        let grid_scale = if self.settings.fixed_size_grid {
1104            GridScaleBehavior::Fixed(
1105                program
1106                    .meta_settings()
1107                    .ok()
1108                    .flatten()
1109                    .map(|s| s.default_length_units)
1110                    .map(kcmc::units::UnitLength::from),
1111            )
1112        } else {
1113            GridScaleBehavior::ScaleWithZoom
1114        };
1115        self.engine
1116            .reapply_settings(
1117                &self.settings,
1118                Default::default(),
1119                exec_state.id_generator(),
1120                grid_scale,
1121            )
1122            .await
1123            .map_err(KclErrorWithOutputs::no_outputs)?;
1124
1125        let default_planes = self.engine.get_default_planes().read().await.clone();
1126        let result = self
1127            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1128            .await;
1129
1130        crate::log::log(format!(
1131            "Post interpretation KCL memory stats: {:#?}",
1132            exec_state.stack().memory.stats
1133        ));
1134        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1135
1136        let env_ref = result.map_err(|(err, env_ref)| exec_state.error_with_outputs(err, env_ref, default_planes))?;
1137
1138        if !self.is_mock() {
1139            let mut mem = exec_state.stack().deep_clone();
1140            mem.restore_env(env_ref);
1141            cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
1142        }
1143        let session_data = self.engine.get_session_data().await;
1144
1145        Ok((env_ref, session_data))
1146    }
1147
1148    /// Execute an AST's program and build auxiliary outputs like the artifact
1149    /// graph.
1150    async fn execute_and_build_graph(
1151        &self,
1152        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1153        exec_state: &mut ExecState,
1154        preserve_mem: bool,
1155    ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1156        // Don't early return!  We need to build other outputs regardless of
1157        // whether execution failed.
1158
1159        // Because of execution caching, we may start with operations from a
1160        // previous run.
1161        #[cfg(feature = "artifact-graph")]
1162        let start_op = exec_state.global.root_module_artifacts.operations.len();
1163
1164        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1165            .await
1166            .map_err(|e| (e, None))?;
1167
1168        let exec_result = self
1169            .exec_module_body(
1170                program,
1171                exec_state,
1172                preserve_mem,
1173                ModuleId::default(),
1174                &ModulePath::Main,
1175            )
1176            .await
1177            .map(|(_, env_ref, _, module_artifacts)| {
1178                // We need to extend because it may already have operations from
1179                // imports.
1180                exec_state.global.root_module_artifacts.extend(module_artifacts);
1181                env_ref
1182            })
1183            .map_err(|(err, env_ref, module_artifacts)| {
1184                if let Some(module_artifacts) = module_artifacts {
1185                    // We need to extend because it may already have operations
1186                    // from imports.
1187                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1188                }
1189                (err, env_ref)
1190            });
1191
1192        #[cfg(feature = "artifact-graph")]
1193        {
1194            // Fill in NodePath for operations.
1195            let cached_body_items = exec_state.global.artifacts.cached_body_items();
1196            for op in exec_state
1197                .global
1198                .root_module_artifacts
1199                .operations
1200                .iter_mut()
1201                .skip(start_op)
1202            {
1203                op.fill_node_paths(program, cached_body_items);
1204            }
1205            for module in exec_state.global.module_infos.values_mut() {
1206                if let ModuleRepr::Kcl(_, Some((_, _, _, module_artifacts))) = &mut module.repr {
1207                    for op in &mut module_artifacts.operations {
1208                        op.fill_node_paths(program, cached_body_items);
1209                    }
1210                }
1211            }
1212        }
1213
1214        // Ensure all the async commands completed.
1215        self.engine.ensure_async_commands_completed().await.map_err(|e| {
1216            match &exec_result {
1217                Ok(env_ref) => (e, Some(*env_ref)),
1218                // Prefer the execution error.
1219                Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1220            }
1221        })?;
1222
1223        // If we errored out and early-returned, there might be commands which haven't been executed
1224        // and should be dropped.
1225        self.engine.clear_queues().await;
1226
1227        match exec_state.build_artifact_graph(&self.engine, program).await {
1228            Ok(_) => exec_result,
1229            Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1230        }
1231    }
1232
1233    /// 'Import' std::prelude as the outermost scope.
1234    ///
1235    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
1236    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1237        if exec_state.stack().memory.requires_std() {
1238            #[cfg(feature = "artifact-graph")]
1239            let initial_ops = exec_state.mod_local.artifacts.operations.len();
1240
1241            let path = vec!["std".to_owned(), "prelude".to_owned()];
1242            let resolved_path = ModulePath::from_std_import_path(&path)?;
1243            let id = self
1244                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1245                .await?;
1246            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1247
1248            exec_state.mut_stack().memory.set_std(module_memory);
1249
1250            // Operations generated by the prelude are not useful, so clear them
1251            // out.
1252            //
1253            // TODO: Should we also clear them out of each module so that they
1254            // don't appear in test output?
1255            #[cfg(feature = "artifact-graph")]
1256            exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1257        }
1258
1259        Ok(())
1260    }
1261
1262    /// Get a snapshot of the current scene.
1263    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1264        // Zoom to fit.
1265        self.engine
1266            .send_modeling_cmd(
1267                uuid::Uuid::new_v4(),
1268                crate::execution::SourceRange::default(),
1269                &ModelingCmd::from(mcmd::ZoomToFit {
1270                    object_ids: Default::default(),
1271                    animated: false,
1272                    padding: 0.1,
1273                }),
1274            )
1275            .await
1276            .map_err(KclErrorWithOutputs::no_outputs)?;
1277
1278        // Send a snapshot request to the engine.
1279        let resp = self
1280            .engine
1281            .send_modeling_cmd(
1282                uuid::Uuid::new_v4(),
1283                crate::execution::SourceRange::default(),
1284                &ModelingCmd::from(mcmd::TakeSnapshot {
1285                    format: ImageFormat::Png,
1286                }),
1287            )
1288            .await
1289            .map_err(KclErrorWithOutputs::no_outputs)?;
1290
1291        let OkWebSocketResponseData::Modeling {
1292            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1293        } = resp
1294        else {
1295            return Err(ExecError::BadPng(format!(
1296                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1297            )));
1298        };
1299        Ok(contents)
1300    }
1301
1302    /// Export the current scene as a CAD file.
1303    pub async fn export(
1304        &self,
1305        format: kittycad_modeling_cmds::format::OutputFormat3d,
1306    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1307        let resp = self
1308            .engine
1309            .send_modeling_cmd(
1310                uuid::Uuid::new_v4(),
1311                crate::SourceRange::default(),
1312                &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
1313                    entity_ids: vec![],
1314                    format,
1315                }),
1316            )
1317            .await?;
1318
1319        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1320            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1321                format!("Expected Export response, got {resp:?}",),
1322                vec![SourceRange::default()],
1323            )));
1324        };
1325
1326        Ok(files)
1327    }
1328
1329    /// Export the current scene as a STEP file.
1330    pub async fn export_step(
1331        &self,
1332        deterministic_time: bool,
1333    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1334        let files = self
1335            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1336                kittycad_modeling_cmds::format::step::export::Options {
1337                    coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1338                    created: if deterministic_time {
1339                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1340                            KclError::new_internal(crate::errors::KclErrorDetails::new(
1341                                format!("Failed to parse date: {e}"),
1342                                vec![SourceRange::default()],
1343                            ))
1344                        })?)
1345                    } else {
1346                        None
1347                    },
1348                },
1349            ))
1350            .await?;
1351
1352        Ok(files)
1353    }
1354
1355    pub async fn close(&self) {
1356        self.engine.close().await;
1357    }
1358}
1359
1360#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS, JsonSchema)]
1361pub struct ArtifactId(Uuid);
1362
1363impl ArtifactId {
1364    pub fn new(uuid: Uuid) -> Self {
1365        Self(uuid)
1366    }
1367}
1368
1369impl From<Uuid> for ArtifactId {
1370    fn from(uuid: Uuid) -> Self {
1371        Self::new(uuid)
1372    }
1373}
1374
1375impl From<&Uuid> for ArtifactId {
1376    fn from(uuid: &Uuid) -> Self {
1377        Self::new(*uuid)
1378    }
1379}
1380
1381impl From<ArtifactId> for Uuid {
1382    fn from(id: ArtifactId) -> Self {
1383        id.0
1384    }
1385}
1386
1387impl From<&ArtifactId> for Uuid {
1388    fn from(id: &ArtifactId) -> Self {
1389        id.0
1390    }
1391}
1392
1393impl From<ModelingCmdId> for ArtifactId {
1394    fn from(id: ModelingCmdId) -> Self {
1395        Self::new(*id.as_ref())
1396    }
1397}
1398
1399impl From<&ModelingCmdId> for ArtifactId {
1400    fn from(id: &ModelingCmdId) -> Self {
1401        Self::new(*id.as_ref())
1402    }
1403}
1404
1405#[cfg(test)]
1406pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1407    parse_execute_with_project_dir(code, None).await
1408}
1409
1410#[cfg(test)]
1411pub(crate) async fn parse_execute_with_project_dir(
1412    code: &str,
1413    project_directory: Option<TypedPath>,
1414) -> Result<ExecTestResults, KclError> {
1415    let program = crate::Program::parse_no_errs(code)?;
1416
1417    let exec_ctxt = ExecutorContext {
1418        engine: Arc::new(Box::new(
1419            crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
1420                KclError::new_internal(crate::errors::KclErrorDetails::new(
1421                    format!("Failed to create mock engine connection: {err}"),
1422                    vec![SourceRange::default()],
1423                ))
1424            })?,
1425        )),
1426        fs: Arc::new(crate::fs::FileManager::new()),
1427        settings: ExecutorSettings {
1428            project_directory,
1429            ..Default::default()
1430        },
1431        context_type: ContextType::Mock,
1432    };
1433    let mut exec_state = ExecState::new(&exec_ctxt);
1434    let result = exec_ctxt.run(&program, &mut exec_state).await?;
1435
1436    Ok(ExecTestResults {
1437        program,
1438        mem_env: result.0,
1439        exec_ctxt,
1440        exec_state,
1441    })
1442}
1443
1444#[cfg(test)]
1445#[derive(Debug)]
1446pub(crate) struct ExecTestResults {
1447    program: crate::Program,
1448    mem_env: EnvironmentRef,
1449    exec_ctxt: ExecutorContext,
1450    exec_state: ExecState,
1451}
1452
1453#[cfg(test)]
1454mod tests {
1455    use pretty_assertions::assert_eq;
1456
1457    use super::*;
1458    use crate::{
1459        ModuleId,
1460        errors::KclErrorDetails,
1461        exec::NumericType,
1462        execution::{memory::Stack, types::RuntimeType},
1463    };
1464
1465    /// Convenience function to get a JSON value from memory and unwrap.
1466    #[track_caller]
1467    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1468        memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1469    }
1470
1471    #[tokio::test(flavor = "multi_thread")]
1472    async fn test_execute_warn() {
1473        let text = "@blah";
1474        let result = parse_execute(text).await.unwrap();
1475        let errs = result.exec_state.errors();
1476        assert_eq!(errs.len(), 1);
1477        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1478        assert!(
1479            errs[0].message.contains("Unknown annotation"),
1480            "unexpected warning message: {}",
1481            errs[0].message
1482        );
1483    }
1484
1485    #[tokio::test(flavor = "multi_thread")]
1486    async fn test_execute_fn_definitions() {
1487        let ast = r#"fn def(@x) {
1488  return x
1489}
1490fn ghi(@x) {
1491  return x
1492}
1493fn jkl(@x) {
1494  return x
1495}
1496fn hmm(@x) {
1497  return x
1498}
1499
1500yo = 5 + 6
1501
1502abc = 3
1503identifierGuy = 5
1504part001 = startSketchOn(XY)
1505|> startProfile(at = [-1.2, 4.83])
1506|> line(end = [2.8, 0])
1507|> angledLine(angle = 100 + 100, length = 3.01)
1508|> angledLine(angle = abc, length = 3.02)
1509|> angledLine(angle = def(yo), length = 3.03)
1510|> angledLine(angle = ghi(2), length = 3.04)
1511|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1512|> close()
1513yo2 = hmm([identifierGuy + 5])"#;
1514
1515        parse_execute(ast).await.unwrap();
1516    }
1517
1518    #[tokio::test(flavor = "multi_thread")]
1519    async fn test_execute_with_pipe_substitutions_unary() {
1520        let ast = r#"myVar = 3
1521part001 = startSketchOn(XY)
1522  |> startProfile(at = [0, 0])
1523  |> line(end = [3, 4], tag = $seg01)
1524  |> line(end = [
1525  min([segLen(seg01), myVar]),
1526  -legLen(hypotenuse = segLen(seg01), leg = myVar)
1527])
1528"#;
1529
1530        parse_execute(ast).await.unwrap();
1531    }
1532
1533    #[tokio::test(flavor = "multi_thread")]
1534    async fn test_execute_with_pipe_substitutions() {
1535        let ast = r#"myVar = 3
1536part001 = startSketchOn(XY)
1537  |> startProfile(at = [0, 0])
1538  |> line(end = [3, 4], tag = $seg01)
1539  |> line(end = [
1540  min([segLen(seg01), myVar]),
1541  legLen(hypotenuse = segLen(seg01), leg = myVar)
1542])
1543"#;
1544
1545        parse_execute(ast).await.unwrap();
1546    }
1547
1548    #[tokio::test(flavor = "multi_thread")]
1549    async fn test_execute_with_inline_comment() {
1550        let ast = r#"baseThick = 1
1551armAngle = 60
1552
1553baseThickHalf = baseThick / 2
1554halfArmAngle = armAngle / 2
1555
1556arrExpShouldNotBeIncluded = [1, 2, 3]
1557objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1558
1559part001 = startSketchOn(XY)
1560  |> startProfile(at = [0, 0])
1561  |> yLine(endAbsolute = 1)
1562  |> xLine(length = 3.84) // selection-range-7ish-before-this
1563
1564variableBelowShouldNotBeIncluded = 3
1565"#;
1566
1567        parse_execute(ast).await.unwrap();
1568    }
1569
1570    #[tokio::test(flavor = "multi_thread")]
1571    async fn test_execute_with_function_literal_in_pipe() {
1572        let ast = r#"w = 20
1573l = 8
1574h = 10
1575
1576fn thing() {
1577  return -8
1578}
1579
1580firstExtrude = startSketchOn(XY)
1581  |> startProfile(at = [0,0])
1582  |> line(end = [0, l])
1583  |> line(end = [w, 0])
1584  |> line(end = [0, thing()])
1585  |> close()
1586  |> extrude(length = h)"#;
1587
1588        parse_execute(ast).await.unwrap();
1589    }
1590
1591    #[tokio::test(flavor = "multi_thread")]
1592    async fn test_execute_with_function_unary_in_pipe() {
1593        let ast = r#"w = 20
1594l = 8
1595h = 10
1596
1597fn thing(@x) {
1598  return -x
1599}
1600
1601firstExtrude = startSketchOn(XY)
1602  |> startProfile(at = [0,0])
1603  |> line(end = [0, l])
1604  |> line(end = [w, 0])
1605  |> line(end = [0, thing(8)])
1606  |> close()
1607  |> extrude(length = h)"#;
1608
1609        parse_execute(ast).await.unwrap();
1610    }
1611
1612    #[tokio::test(flavor = "multi_thread")]
1613    async fn test_execute_with_function_array_in_pipe() {
1614        let ast = r#"w = 20
1615l = 8
1616h = 10
1617
1618fn thing(@x) {
1619  return [0, -x]
1620}
1621
1622firstExtrude = startSketchOn(XY)
1623  |> startProfile(at = [0,0])
1624  |> line(end = [0, l])
1625  |> line(end = [w, 0])
1626  |> line(end = thing(8))
1627  |> close()
1628  |> extrude(length = h)"#;
1629
1630        parse_execute(ast).await.unwrap();
1631    }
1632
1633    #[tokio::test(flavor = "multi_thread")]
1634    async fn test_execute_with_function_call_in_pipe() {
1635        let ast = r#"w = 20
1636l = 8
1637h = 10
1638
1639fn other_thing(@y) {
1640  return -y
1641}
1642
1643fn thing(@x) {
1644  return other_thing(x)
1645}
1646
1647firstExtrude = startSketchOn(XY)
1648  |> startProfile(at = [0,0])
1649  |> line(end = [0, l])
1650  |> line(end = [w, 0])
1651  |> line(end = [0, thing(8)])
1652  |> close()
1653  |> extrude(length = h)"#;
1654
1655        parse_execute(ast).await.unwrap();
1656    }
1657
1658    #[tokio::test(flavor = "multi_thread")]
1659    async fn test_execute_with_function_sketch() {
1660        let ast = r#"fn box(h, l, w) {
1661 myBox = startSketchOn(XY)
1662    |> startProfile(at = [0,0])
1663    |> line(end = [0, l])
1664    |> line(end = [w, 0])
1665    |> line(end = [0, -l])
1666    |> close()
1667    |> extrude(length = h)
1668
1669  return myBox
1670}
1671
1672fnBox = box(h = 3, l = 6, w = 10)"#;
1673
1674        parse_execute(ast).await.unwrap();
1675    }
1676
1677    #[tokio::test(flavor = "multi_thread")]
1678    async fn test_get_member_of_object_with_function_period() {
1679        let ast = r#"fn box(@obj) {
1680 myBox = startSketchOn(XY)
1681    |> startProfile(at = obj.start)
1682    |> line(end = [0, obj.l])
1683    |> line(end = [obj.w, 0])
1684    |> line(end = [0, -obj.l])
1685    |> close()
1686    |> extrude(length = obj.h)
1687
1688  return myBox
1689}
1690
1691thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1692"#;
1693        parse_execute(ast).await.unwrap();
1694    }
1695
1696    #[tokio::test(flavor = "multi_thread")]
1697    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
1698    async fn test_object_member_starting_pipeline() {
1699        let ast = r#"
1700fn test2() {
1701  return {
1702    thing: startSketchOn(XY)
1703      |> startProfile(at = [0, 0])
1704      |> line(end = [0, 1])
1705      |> line(end = [1, 0])
1706      |> line(end = [0, -1])
1707      |> close()
1708  }
1709}
1710
1711x2 = test2()
1712
1713x2.thing
1714  |> extrude(length = 10)
1715"#;
1716        parse_execute(ast).await.unwrap();
1717    }
1718
1719    #[tokio::test(flavor = "multi_thread")]
1720    #[ignore] // ignore til we get loops
1721    async fn test_execute_with_function_sketch_loop_objects() {
1722        let ast = r#"fn box(obj) {
1723let myBox = startSketchOn(XY)
1724    |> startProfile(at = obj.start)
1725    |> line(end = [0, obj.l])
1726    |> line(end = [obj.w, 0])
1727    |> line(end = [0, -obj.l])
1728    |> close()
1729    |> extrude(length = obj.h)
1730
1731  return myBox
1732}
1733
1734for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1735  thisBox = box(var)
1736}"#;
1737
1738        parse_execute(ast).await.unwrap();
1739    }
1740
1741    #[tokio::test(flavor = "multi_thread")]
1742    #[ignore] // ignore til we get loops
1743    async fn test_execute_with_function_sketch_loop_array() {
1744        let ast = r#"fn box(h, l, w, start) {
1745 myBox = startSketchOn(XY)
1746    |> startProfile(at = [0,0])
1747    |> line(end = [0, l])
1748    |> line(end = [w, 0])
1749    |> line(end = [0, -l])
1750    |> close()
1751    |> extrude(length = h)
1752
1753  return myBox
1754}
1755
1756
1757for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1758  const thisBox = box(var[0], var[1], var[2], var[3])
1759}"#;
1760
1761        parse_execute(ast).await.unwrap();
1762    }
1763
1764    #[tokio::test(flavor = "multi_thread")]
1765    async fn test_get_member_of_array_with_function() {
1766        let ast = r#"fn box(@arr) {
1767 myBox =startSketchOn(XY)
1768    |> startProfile(at = arr[0])
1769    |> line(end = [0, arr[1]])
1770    |> line(end = [arr[2], 0])
1771    |> line(end = [0, -arr[1]])
1772    |> close()
1773    |> extrude(length = arr[3])
1774
1775  return myBox
1776}
1777
1778thisBox = box([[0,0], 6, 10, 3])
1779
1780"#;
1781        parse_execute(ast).await.unwrap();
1782    }
1783
1784    #[tokio::test(flavor = "multi_thread")]
1785    async fn test_function_cannot_access_future_definitions() {
1786        let ast = r#"
1787fn returnX() {
1788  // x shouldn't be defined yet.
1789  return x
1790}
1791
1792x = 5
1793
1794answer = returnX()"#;
1795
1796        let result = parse_execute(ast).await;
1797        let err = result.unwrap_err();
1798        assert_eq!(err.message(), "`x` is not defined");
1799    }
1800
1801    #[tokio::test(flavor = "multi_thread")]
1802    async fn test_override_prelude() {
1803        let text = "PI = 3.0";
1804        let result = parse_execute(text).await.unwrap();
1805        let errs = result.exec_state.errors();
1806        assert!(errs.is_empty());
1807    }
1808
1809    #[tokio::test(flavor = "multi_thread")]
1810    async fn type_aliases() {
1811        let text = r#"type 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}