kcl_lib/execution/
mod.rs

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