kcl_lib/execution/
mod.rs

1//! The executor for the AST.
2
3use std::{path::PathBuf, sync::Arc};
4
5use anyhow::Result;
6pub use artifact::{
7    Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, CodeRef, StartSketchOnFace, StartSketchOnPlane,
8};
9use cache::OldAstState;
10pub use cache::{bust_cache, clear_mem_cache};
11pub use cad_op::Operation;
12pub use geometry::*;
13pub(crate) use import::{
14    import_foreign, send_to_engine as send_import_to_engine, PreImportedGeometry, ZOO_COORD_SYSTEM,
15};
16use indexmap::IndexMap;
17pub use kcl_value::{KclObjectFields, KclValue, PrimitiveType, UnitAngle, UnitLen};
18use kcmc::{
19    each_cmd as mcmd,
20    ok_response::{output::TakeSnapshot, OkModelingCmdResponse},
21    websocket::{ModelingSessionData, OkWebSocketResponseData},
22    ImageFormat, ModelingCmd,
23};
24use kittycad_modeling_cmds as kcmc;
25pub use memory::EnvironmentRef;
26use schemars::JsonSchema;
27use serde::{Deserialize, Serialize};
28pub use state::{ExecState, IdGenerator, MetaSettings};
29
30use crate::{
31    engine::EngineManager,
32    errors::KclError,
33    execution::{
34        artifact::build_artifact_graph,
35        cache::{CacheInformation, CacheResult},
36    },
37    fs::FileManager,
38    modules::{ModuleId, ModulePath},
39    parsing::ast::types::{Expr, ImportPath, NodeRef},
40    settings::types::UnitLength,
41    source_range::SourceRange,
42    std::StdLib,
43    CompilationError, ExecError, ExecutionKind, KclErrorWithOutputs,
44};
45
46pub(crate) mod annotations;
47mod artifact;
48pub(crate) mod cache;
49mod cad_op;
50mod exec_ast;
51mod geometry;
52mod import;
53pub(crate) mod kcl_value;
54mod memory;
55mod state;
56
57/// Outcome of executing a program.  This is used in TS.
58#[derive(Debug, Clone, Serialize, ts_rs::TS)]
59#[ts(export)]
60#[serde(rename_all = "camelCase")]
61pub struct ExecOutcome {
62    /// Variables in the top-level of the root module. Note that functions will have an invalid env ref.
63    pub variables: IndexMap<String, KclValue>,
64    /// Operations that have been performed in execution order, for display in
65    /// the Feature Tree.
66    pub operations: Vec<Operation>,
67    /// Output map of UUIDs to artifacts.
68    pub artifacts: IndexMap<ArtifactId, Artifact>,
69    /// Output commands to allow building the artifact graph by the caller.
70    pub artifact_commands: Vec<ArtifactCommand>,
71    /// Output artifact graph.
72    pub artifact_graph: ArtifactGraph,
73    /// Non-fatal errors and warnings.
74    pub errors: Vec<CompilationError>,
75    /// File Names in module Id array index order
76    pub filenames: IndexMap<ModuleId, ModulePath>,
77}
78
79#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
80#[ts(export)]
81#[serde(rename_all = "camelCase")]
82pub struct DefaultPlanes {
83    pub xy: uuid::Uuid,
84    pub xz: uuid::Uuid,
85    pub yz: uuid::Uuid,
86    pub neg_xy: uuid::Uuid,
87    pub neg_xz: uuid::Uuid,
88    pub neg_yz: uuid::Uuid,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
92#[ts(export)]
93#[serde(tag = "type", rename_all = "camelCase")]
94pub struct TagIdentifier {
95    pub value: String,
96    pub info: Option<TagEngineInfo>,
97    #[serde(rename = "__meta")]
98    pub meta: Vec<Metadata>,
99}
100
101impl Eq for TagIdentifier {}
102
103impl std::fmt::Display for TagIdentifier {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(f, "{}", self.value)
106    }
107}
108
109impl std::str::FromStr for TagIdentifier {
110    type Err = KclError;
111
112    fn from_str(s: &str) -> Result<Self, Self::Err> {
113        Ok(Self {
114            value: s.to_string(),
115            info: None,
116            meta: Default::default(),
117        })
118    }
119}
120
121impl Ord for TagIdentifier {
122    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
123        self.value.cmp(&other.value)
124    }
125}
126
127impl PartialOrd for TagIdentifier {
128    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
129        Some(self.cmp(other))
130    }
131}
132
133impl std::hash::Hash for TagIdentifier {
134    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
135        self.value.hash(state);
136    }
137}
138
139/// Engine information for a tag.
140#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
141#[ts(export)]
142#[serde(tag = "type", rename_all = "camelCase")]
143pub struct TagEngineInfo {
144    /// The id of the tagged object.
145    pub id: uuid::Uuid,
146    /// The sketch the tag is on.
147    pub sketch: uuid::Uuid,
148    /// The path the tag is on.
149    pub path: Option<Path>,
150    /// The surface information for the tag.
151    pub surface: Option<ExtrudeSurface>,
152}
153
154#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
155pub enum BodyType {
156    Root,
157    Block,
158}
159
160/// Metadata.
161#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
162#[ts(export)]
163#[serde(rename_all = "camelCase")]
164pub struct Metadata {
165    /// The source range.
166    pub source_range: SourceRange,
167}
168
169impl From<Metadata> for Vec<SourceRange> {
170    fn from(meta: Metadata) -> Self {
171        vec![meta.source_range]
172    }
173}
174
175impl From<SourceRange> for Metadata {
176    fn from(source_range: SourceRange) -> Self {
177        Self { source_range }
178    }
179}
180
181impl<T> From<NodeRef<'_, T>> for Metadata {
182    fn from(node: NodeRef<'_, T>) -> Self {
183        Self {
184            source_range: SourceRange::new(node.start, node.end, node.module_id),
185        }
186    }
187}
188
189impl From<&Expr> for Metadata {
190    fn from(expr: &Expr) -> Self {
191        Self {
192            source_range: SourceRange::from(expr),
193        }
194    }
195}
196
197/// The type of ExecutorContext being used
198#[derive(PartialEq, Debug, Default, Clone)]
199pub enum ContextType {
200    /// Live engine connection
201    #[default]
202    Live,
203
204    /// Completely mocked connection
205    /// Mock mode is only for the modeling app when they just want to mock engine calls and not
206    /// actually make them.
207    Mock,
208
209    /// Handled by some other interpreter/conversion system
210    MockCustomForwarded,
211}
212
213/// The executor context.
214/// Cloning will return another handle to the same engine connection/session,
215/// as this uses `Arc` under the hood.
216#[derive(Debug, Clone)]
217pub struct ExecutorContext {
218    pub engine: Arc<Box<dyn EngineManager>>,
219    pub fs: Arc<FileManager>,
220    pub stdlib: Arc<StdLib>,
221    pub settings: ExecutorSettings,
222    pub context_type: ContextType,
223}
224
225/// The executor settings.
226#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
227#[ts(export)]
228pub struct ExecutorSettings {
229    /// The project-default unit to use in modeling dimensions.
230    pub units: UnitLength,
231    /// Highlight edges of 3D objects?
232    pub highlight_edges: bool,
233    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
234    pub enable_ssao: bool,
235    /// Show grid?
236    pub show_grid: bool,
237    /// Should engine store this for replay?
238    /// If so, under what name?
239    pub replay: Option<String>,
240    /// The directory of the current project.  This is used for resolving import
241    /// paths.  If None is given, the current working directory is used.
242    pub project_directory: Option<PathBuf>,
243    /// This is the path to the current file being executed.
244    /// We use this for preventing cyclic imports.
245    pub current_file: Option<PathBuf>,
246}
247
248impl Default for ExecutorSettings {
249    fn default() -> Self {
250        Self {
251            units: Default::default(),
252            highlight_edges: true,
253            enable_ssao: false,
254            show_grid: false,
255            replay: None,
256            project_directory: None,
257            current_file: None,
258        }
259    }
260}
261
262impl From<crate::settings::types::Configuration> for ExecutorSettings {
263    fn from(config: crate::settings::types::Configuration) -> Self {
264        Self {
265            units: config.settings.modeling.base_unit,
266            highlight_edges: config.settings.modeling.highlight_edges.into(),
267            enable_ssao: config.settings.modeling.enable_ssao.into(),
268            show_grid: config.settings.modeling.show_scale_grid,
269            replay: None,
270            project_directory: None,
271            current_file: None,
272        }
273    }
274}
275
276impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
277    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
278        Self {
279            units: config.settings.modeling.base_unit,
280            highlight_edges: config.settings.modeling.highlight_edges.into(),
281            enable_ssao: config.settings.modeling.enable_ssao.into(),
282            show_grid: Default::default(),
283            replay: None,
284            project_directory: None,
285            current_file: None,
286        }
287    }
288}
289
290impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
291    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
292        Self {
293            units: modeling.base_unit,
294            highlight_edges: modeling.highlight_edges.into(),
295            enable_ssao: modeling.enable_ssao.into(),
296            show_grid: modeling.show_scale_grid,
297            replay: None,
298            project_directory: None,
299            current_file: None,
300        }
301    }
302}
303
304impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
305    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
306        Self {
307            units: modeling.base_unit,
308            highlight_edges: modeling.highlight_edges.into(),
309            enable_ssao: modeling.enable_ssao.into(),
310            show_grid: Default::default(),
311            replay: None,
312            project_directory: None,
313            current_file: None,
314        }
315    }
316}
317
318impl ExecutorSettings {
319    /// Add the current file path to the executor settings.
320    pub fn with_current_file(&mut self, current_file: PathBuf) {
321        // We want the parent directory of the file.
322        if current_file.extension() == Some(std::ffi::OsStr::new("kcl")) {
323            self.current_file = Some(current_file.clone());
324            // Get the parent directory.
325            if let Some(parent) = current_file.parent() {
326                self.project_directory = Some(parent.to_path_buf());
327            } else {
328                self.project_directory = Some(std::path::PathBuf::from(""));
329            }
330        } else {
331            self.project_directory = Some(current_file.clone());
332        }
333    }
334}
335
336impl ExecutorContext {
337    /// Create a new default executor context.
338    #[cfg(not(target_arch = "wasm32"))]
339    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
340        let (ws, _headers) = client
341            .modeling()
342            .commands_ws(
343                None,
344                None,
345                if settings.enable_ssao {
346                    Some(kittycad::types::PostEffectType::Ssao)
347                } else {
348                    None
349                },
350                settings.replay.clone(),
351                if settings.show_grid { Some(true) } else { None },
352                None,
353                None,
354                None,
355                Some(false),
356            )
357            .await?;
358
359        let engine: Arc<Box<dyn EngineManager>> =
360            Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
361
362        Ok(Self {
363            engine,
364            fs: Arc::new(FileManager::new()),
365            stdlib: Arc::new(StdLib::new()),
366            settings,
367            context_type: ContextType::Live,
368        })
369    }
370
371    #[cfg(target_arch = "wasm32")]
372    pub async fn new(
373        engine_manager: crate::engine::conn_wasm::EngineCommandManager,
374        fs_manager: crate::fs::wasm::FileSystemManager,
375        settings: ExecutorSettings,
376    ) -> Result<Self, String> {
377        Ok(ExecutorContext {
378            engine: Arc::new(Box::new(
379                crate::engine::conn_wasm::EngineConnection::new(engine_manager)
380                    .await
381                    .map_err(|e| format!("{:?}", e))?,
382            )),
383            fs: Arc::new(FileManager::new(fs_manager)),
384            stdlib: Arc::new(StdLib::new()),
385            settings,
386            context_type: ContextType::Live,
387        })
388    }
389
390    #[cfg(not(target_arch = "wasm32"))]
391    pub async fn new_mock() -> Self {
392        ExecutorContext {
393            engine: Arc::new(Box::new(
394                crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
395            )),
396            fs: Arc::new(FileManager::new()),
397            stdlib: Arc::new(StdLib::new()),
398            settings: Default::default(),
399            context_type: ContextType::Mock,
400        }
401    }
402
403    #[cfg(target_arch = "wasm32")]
404    pub async fn new_mock(
405        fs_manager: crate::fs::wasm::FileSystemManager,
406        settings: ExecutorSettings,
407    ) -> Result<Self, String> {
408        Ok(ExecutorContext {
409            engine: Arc::new(Box::new(
410                crate::engine::conn_mock::EngineConnection::new()
411                    .await
412                    .map_err(|e| format!("{:?}", e))?,
413            )),
414            fs: Arc::new(FileManager::new(fs_manager)),
415            stdlib: Arc::new(StdLib::new()),
416            settings,
417            context_type: ContextType::Mock,
418        })
419    }
420
421    #[cfg(not(target_arch = "wasm32"))]
422    pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
423        ExecutorContext {
424            engine,
425            fs: Arc::new(FileManager::new()),
426            stdlib: Arc::new(StdLib::new()),
427            settings: Default::default(),
428            context_type: ContextType::MockCustomForwarded,
429        }
430    }
431
432    /// Create a new default executor context.
433    /// With a kittycad client.
434    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
435    /// variables.
436    /// But also allows for passing in a token and engine address directly.
437    #[cfg(not(target_arch = "wasm32"))]
438    pub async fn new_with_client(
439        settings: ExecutorSettings,
440        token: Option<String>,
441        engine_addr: Option<String>,
442    ) -> Result<Self> {
443        // Create the client.
444        let client = crate::engine::new_zoo_client(token, engine_addr)?;
445
446        let ctx = Self::new(&client, settings).await?;
447        Ok(ctx)
448    }
449
450    /// Create a new default executor context.
451    /// With the default kittycad client.
452    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
453    /// variables.
454    #[cfg(not(target_arch = "wasm32"))]
455    pub async fn new_with_default_client(units: UnitLength) -> Result<Self> {
456        // Create the client.
457        let ctx = Self::new_with_client(
458            ExecutorSettings {
459                units,
460                ..Default::default()
461            },
462            None,
463            None,
464        )
465        .await?;
466        Ok(ctx)
467    }
468
469    /// For executing unit tests.
470    #[cfg(not(target_arch = "wasm32"))]
471    pub async fn new_for_unit_test(units: UnitLength, engine_addr: Option<String>) -> Result<Self> {
472        let ctx = ExecutorContext::new_with_client(
473            ExecutorSettings {
474                units,
475                highlight_edges: true,
476                enable_ssao: false,
477                show_grid: false,
478                replay: None,
479                project_directory: None,
480                current_file: None,
481            },
482            None,
483            engine_addr,
484        )
485        .await?;
486        Ok(ctx)
487    }
488
489    pub fn is_mock(&self) -> bool {
490        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
491    }
492
493    /// Returns true if we should not send engine commands for any reason.
494    pub async fn no_engine_commands(&self) -> bool {
495        self.is_mock() || self.engine.execution_kind().await.is_isolated()
496    }
497
498    pub async fn send_clear_scene(
499        &self,
500        exec_state: &mut ExecState,
501        source_range: crate::execution::SourceRange,
502    ) -> Result<(), KclError> {
503        self.engine
504            .clear_scene(&mut exec_state.global.id_generator, source_range)
505            .await
506    }
507
508    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
509        self.eval_prelude(exec_state, SourceRange::synthetic())
510            .await
511            .map_err(KclErrorWithOutputs::no_outputs)?;
512        exec_state.mut_stack().push_new_root_env(true);
513        Ok(())
514    }
515
516    pub async fn run_mock(
517        &self,
518        program: crate::Program,
519        use_prev_memory: bool,
520    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
521        assert!(self.is_mock());
522
523        let mut exec_state = ExecState::new(&self.settings);
524        if use_prev_memory {
525            match cache::read_old_memory().await {
526                Some(mem) => *exec_state.mut_stack() = mem,
527                None => self.prepare_mem(&mut exec_state).await?,
528            }
529        } else {
530            self.prepare_mem(&mut exec_state).await?
531        };
532
533        // Push a scope so that old variables can be overwritten (since we might be re-executing some
534        // part of the scene).
535        exec_state.mut_stack().push_new_env_for_scope();
536
537        let result = self.inner_run(&program, &mut exec_state, true).await?;
538
539        // Restore any temporary variables, then save any newly created variables back to
540        // memory in case another run wants to use them. Note this is just saved to the preserved
541        // memory, not to the exec_state which is not cached for mock execution.
542
543        let mut mem = exec_state.stack().clone();
544        let outcome = exec_state.to_mock_wasm_outcome(result.0);
545
546        mem.squash_env(result.0);
547        cache::write_old_memory(mem).await;
548
549        Ok(outcome)
550    }
551
552    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
553        assert!(!self.is_mock());
554
555        let (program, mut exec_state, preserve_mem) = if let Some(OldAstState {
556            ast: old_ast,
557            exec_state: mut old_state,
558            settings: old_settings,
559            result_env,
560        }) = cache::read_old_ast().await
561        {
562            let old = CacheInformation {
563                ast: &old_ast,
564                settings: &old_settings,
565            };
566            let new = CacheInformation {
567                ast: &program.ast,
568                settings: &self.settings,
569            };
570
571            // Get the program that actually changed from the old and new information.
572            let (clear_scene, program) = match cache::get_changed_program(old, new).await {
573                CacheResult::ReExecute {
574                    clear_scene,
575                    reapply_settings,
576                    program: changed_program,
577                } => {
578                    if reapply_settings
579                        && self
580                            .engine
581                            .reapply_settings(&self.settings, Default::default())
582                            .await
583                            .is_err()
584                    {
585                        (true, program)
586                    } else {
587                        (
588                            clear_scene,
589                            crate::Program {
590                                ast: changed_program,
591                                original_file_contents: program.original_file_contents,
592                            },
593                        )
594                    }
595                }
596                CacheResult::NoAction(true) => {
597                    if self
598                        .engine
599                        .reapply_settings(&self.settings, Default::default())
600                        .await
601                        .is_ok()
602                    {
603                        // We need to update the old ast state with the new settings!!
604                        cache::write_old_ast(OldAstState {
605                            ast: old_ast,
606                            exec_state: old_state.clone(),
607                            settings: self.settings.clone(),
608                            result_env,
609                        })
610                        .await;
611
612                        let outcome = old_state.to_wasm_outcome(result_env);
613                        return Ok(outcome);
614                    }
615                    (true, program)
616                }
617                CacheResult::NoAction(false) => {
618                    let outcome = old_state.to_wasm_outcome(result_env);
619                    return Ok(outcome);
620                }
621            };
622
623            let (exec_state, preserve_mem) = if clear_scene {
624                // Pop the execution state, since we are starting fresh.
625                let mut exec_state = old_state;
626                exec_state.reset(&self.settings);
627
628                // We don't do this in mock mode since there is no engine connection
629                // anyways and from the TS side we override memory and don't want to clear it.
630                self.send_clear_scene(&mut exec_state, Default::default())
631                    .await
632                    .map_err(KclErrorWithOutputs::no_outputs)?;
633
634                (exec_state, false)
635            } else {
636                old_state.mut_stack().restore_env(result_env);
637
638                (old_state, true)
639            };
640
641            (program, exec_state, preserve_mem)
642        } else {
643            let mut exec_state = ExecState::new(&self.settings);
644            self.send_clear_scene(&mut exec_state, Default::default())
645                .await
646                .map_err(KclErrorWithOutputs::no_outputs)?;
647            (program, exec_state, false)
648        };
649
650        let result = self.inner_run(&program, &mut exec_state, preserve_mem).await;
651
652        if result.is_err() {
653            cache::bust_cache().await;
654        }
655
656        // Throw the error.
657        let result = result?;
658
659        // Save this as the last successful execution to the cache.
660        cache::write_old_ast(OldAstState {
661            ast: program.ast,
662            exec_state: exec_state.clone(),
663            settings: self.settings.clone(),
664            result_env: result.0,
665        })
666        .await;
667
668        let outcome = exec_state.to_wasm_outcome(result.0);
669        Ok(outcome)
670    }
671
672    /// Perform the execution of a program.
673    ///
674    /// You can optionally pass in some initialization memory for partial
675    /// execution.
676    ///
677    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
678    pub async fn run(
679        &self,
680        program: &crate::Program,
681        exec_state: &mut ExecState,
682    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
683        self.inner_run(program, exec_state, false).await
684    }
685
686    /// Perform the execution of a program.  Accept all possible parameters and
687    /// output everything.
688    async fn inner_run(
689        &self,
690        program: &crate::Program,
691        exec_state: &mut ExecState,
692        preserve_mem: bool,
693    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
694        exec_state.add_root_module_contents(program);
695
696        let _stats = crate::log::LogPerfStats::new("Interpretation");
697
698        // Re-apply the settings, in case the cache was busted.
699        self.engine
700            .reapply_settings(&self.settings, Default::default())
701            .await
702            .map_err(KclErrorWithOutputs::no_outputs)?;
703
704        let env_ref = self
705            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
706            .await
707            .map_err(|e| {
708                let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
709                    .global
710                    .path_to_source_id
711                    .iter()
712                    .map(|(k, v)| ((*v), k.clone()))
713                    .collect();
714
715                KclErrorWithOutputs::new(
716                    e,
717                    exec_state.global.operations.clone(),
718                    exec_state.global.artifact_commands.clone(),
719                    exec_state.global.artifact_graph.clone(),
720                    module_id_to_module_path,
721                    exec_state.global.id_to_source.clone(),
722                )
723            })?;
724
725        crate::log::log(format!(
726            "Post interpretation KCL memory stats: {:#?}",
727            exec_state.stack().memory.stats
728        ));
729
730        if !self.is_mock() {
731            let mut mem = exec_state.stack().deep_clone();
732            mem.restore_env(env_ref);
733            cache::write_old_memory(mem).await;
734        }
735        let session_data = self.engine.get_session_data().await;
736        Ok((env_ref, session_data))
737    }
738
739    /// Execute an AST's program and build auxiliary outputs like the artifact
740    /// graph.
741    async fn execute_and_build_graph(
742        &self,
743        program: NodeRef<'_, crate::parsing::ast::types::Program>,
744        exec_state: &mut ExecState,
745        preserve_mem: bool,
746    ) -> Result<EnvironmentRef, KclError> {
747        // Don't early return!  We need to build other outputs regardless of
748        // whether execution failed.
749
750        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
751            .await?;
752
753        let exec_result = self
754            .exec_module_body(
755                program,
756                exec_state,
757                ExecutionKind::Normal,
758                preserve_mem,
759                &ModulePath::Main,
760            )
761            .await;
762
763        // Move the artifact commands and responses to simplify cache management
764        // and error creation.
765        exec_state
766            .global
767            .artifact_commands
768            .extend(self.engine.take_artifact_commands().await);
769        exec_state
770            .global
771            .artifact_responses
772            .extend(self.engine.take_responses().await);
773        // Build the artifact graph.
774        match build_artifact_graph(
775            &exec_state.global.artifact_commands,
776            &exec_state.global.artifact_responses,
777            program,
778            &exec_state.global.artifacts,
779        ) {
780            Ok(artifact_graph) => {
781                exec_state.global.artifact_graph = artifact_graph;
782                exec_result.map(|(_, env_ref, _)| env_ref)
783            }
784            Err(err) => {
785                // Prefer the exec error.
786                exec_result.and(Err(err))
787            }
788        }
789    }
790
791    /// 'Import' std::prelude as the outermost scope.
792    ///
793    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
794    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
795        if exec_state.stack().memory.requires_std() {
796            let id = self
797                .open_module(
798                    &ImportPath::Std {
799                        path: vec!["std".to_owned(), "prelude".to_owned()],
800                    },
801                    &[],
802                    exec_state,
803                    source_range,
804                )
805                .await?;
806            let (module_memory, _) = self
807                .exec_module_for_items(id, exec_state, ExecutionKind::Isolated, source_range)
808                .await
809                .unwrap();
810
811            exec_state.mut_stack().memory.set_std(module_memory);
812        }
813
814        Ok(())
815    }
816
817    /// Update the units for the executor.
818    pub(crate) fn update_units(&mut self, units: UnitLength) {
819        self.settings.units = units;
820    }
821
822    /// Get a snapshot of the current scene.
823    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
824        // Zoom to fit.
825        self.engine
826            .send_modeling_cmd(
827                uuid::Uuid::new_v4(),
828                crate::execution::SourceRange::default(),
829                &ModelingCmd::from(mcmd::ZoomToFit {
830                    object_ids: Default::default(),
831                    animated: false,
832                    padding: 0.1,
833                }),
834            )
835            .await
836            .map_err(KclErrorWithOutputs::no_outputs)?;
837
838        // Send a snapshot request to the engine.
839        let resp = self
840            .engine
841            .send_modeling_cmd(
842                uuid::Uuid::new_v4(),
843                crate::execution::SourceRange::default(),
844                &ModelingCmd::from(mcmd::TakeSnapshot {
845                    format: ImageFormat::Png,
846                }),
847            )
848            .await
849            .map_err(KclErrorWithOutputs::no_outputs)?;
850
851        let OkWebSocketResponseData::Modeling {
852            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
853        } = resp
854        else {
855            return Err(ExecError::BadPng(format!(
856                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
857            )));
858        };
859        Ok(contents)
860    }
861
862    /// Export the current scene as a CAD file.
863    pub async fn export(
864        &self,
865        format: kittycad_modeling_cmds::format::OutputFormat3d,
866    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
867        let resp = self
868            .engine
869            .send_modeling_cmd(
870                uuid::Uuid::new_v4(),
871                crate::SourceRange::default(),
872                &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
873                    entity_ids: vec![],
874                    format,
875                }),
876            )
877            .await?;
878
879        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
880            return Err(KclError::Internal(crate::errors::KclErrorDetails {
881                message: format!("Expected Export response, got {resp:?}",),
882                source_ranges: vec![SourceRange::default()],
883            }));
884        };
885
886        Ok(files)
887    }
888
889    /// Export the current scene as a STEP file.
890    pub async fn export_step(
891        &self,
892        deterministic_time: bool,
893    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
894        let mut files = self
895            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
896                kittycad_modeling_cmds::format::step::export::Options {
897                    coords: *kittycad_modeling_cmds::coord::KITTYCAD,
898                    created: None,
899                },
900            ))
901            .await?;
902
903        if deterministic_time {
904            for kittycad_modeling_cmds::websocket::RawFile { contents, .. } in &mut files {
905                use std::fmt::Write;
906                let utf8 = std::str::from_utf8(contents).unwrap();
907                let mut postprocessed = String::new();
908                for line in utf8.lines() {
909                    if line.starts_with("FILE_NAME") {
910                        let name = "test.step";
911                        let time = "2021-01-01T00:00:00Z";
912                        let author = "Test";
913                        let org = "Zoo";
914                        let version = "zoo.dev beta";
915                        let system = "zoo.dev";
916                        let authorization = "Test";
917                        writeln!(&mut postprocessed, "FILE_NAME('{name}', '{time}', ('{author}'), ('{org}'), '{version}', '{system}', '{authorization}');").unwrap();
918                    } else {
919                        writeln!(&mut postprocessed, "{line}").unwrap();
920                    }
921                }
922                *contents = postprocessed.into_bytes();
923            }
924        }
925
926        Ok(files)
927    }
928
929    pub async fn close(&self) {
930        self.engine.close().await;
931    }
932}
933
934#[cfg(test)]
935pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
936    let program = crate::Program::parse_no_errs(code)?;
937
938    let exec_ctxt = ExecutorContext {
939        engine: Arc::new(Box::new(
940            crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
941                KclError::Internal(crate::errors::KclErrorDetails {
942                    message: format!("Failed to create mock engine connection: {}", err),
943                    source_ranges: vec![SourceRange::default()],
944                })
945            })?,
946        )),
947        fs: Arc::new(crate::fs::FileManager::new()),
948        stdlib: Arc::new(crate::std::StdLib::new()),
949        settings: Default::default(),
950        context_type: ContextType::Mock,
951    };
952    let mut exec_state = ExecState::new(&exec_ctxt.settings);
953    let result = exec_ctxt.run(&program, &mut exec_state).await?;
954
955    Ok(ExecTestResults {
956        program,
957        mem_env: result.0,
958        exec_ctxt,
959        exec_state,
960    })
961}
962
963#[cfg(test)]
964#[derive(Debug)]
965pub(crate) struct ExecTestResults {
966    program: crate::Program,
967    mem_env: EnvironmentRef,
968    exec_ctxt: ExecutorContext,
969    exec_state: ExecState,
970}
971
972#[cfg(test)]
973mod tests {
974    use pretty_assertions::assert_eq;
975
976    use super::*;
977    use crate::{errors::KclErrorDetails, execution::memory::Stack, ModuleId};
978
979    /// Convenience function to get a JSON value from memory and unwrap.
980    #[track_caller]
981    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
982        memory
983            .memory
984            .get_from_unchecked(name, env, SourceRange::default())
985            .unwrap()
986            .to_owned()
987    }
988
989    #[tokio::test(flavor = "multi_thread")]
990    async fn test_execute_warn() {
991        let text = "@blah";
992        let result = parse_execute(text).await.unwrap();
993        let errs = result.exec_state.errors();
994        assert_eq!(errs.len(), 1);
995        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
996        assert!(
997            errs[0].message.contains("Unknown annotation"),
998            "unexpected warning message: {}",
999            errs[0].message
1000        );
1001    }
1002
1003    #[tokio::test(flavor = "multi_thread")]
1004    async fn test_warn_on_deprecated() {
1005        let text = "p = pi()";
1006        let result = parse_execute(text).await.unwrap();
1007        let errs = result.exec_state.errors();
1008        assert_eq!(errs.len(), 1);
1009        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1010        assert!(
1011            errs[0].message.contains("`pi` is deprecated"),
1012            "unexpected warning message: {}",
1013            errs[0].message
1014        );
1015    }
1016
1017    #[tokio::test(flavor = "multi_thread")]
1018    async fn test_execute_fn_definitions() {
1019        let ast = r#"fn def = (x) => {
1020  return x
1021}
1022fn ghi = (x) => {
1023  return x
1024}
1025fn jkl = (x) => {
1026  return x
1027}
1028fn hmm = (x) => {
1029  return x
1030}
1031
1032yo = 5 + 6
1033
1034abc = 3
1035identifierGuy = 5
1036part001 = startSketchOn(XY)
1037|> startProfileAt([-1.2, 4.83], %)
1038|> line(end = [2.8, 0])
1039|> angledLine([100 + 100, 3.01], %)
1040|> angledLine([abc, 3.02], %)
1041|> angledLine([def(yo), 3.03], %)
1042|> angledLine([ghi(2), 3.04], %)
1043|> angledLine([jkl(yo) + 2, 3.05], %)
1044|> close()
1045yo2 = hmm([identifierGuy + 5])"#;
1046
1047        parse_execute(ast).await.unwrap();
1048    }
1049
1050    #[tokio::test(flavor = "multi_thread")]
1051    async fn test_execute_with_pipe_substitutions_unary() {
1052        let ast = r#"const myVar = 3
1053const part001 = startSketchOn(XY)
1054  |> startProfileAt([0, 0], %)
1055  |> line(end = [3, 4], tag = $seg01)
1056  |> line(end = [
1057  min(segLen(seg01), myVar),
1058  -legLen(segLen(seg01), myVar)
1059])
1060"#;
1061
1062        parse_execute(ast).await.unwrap();
1063    }
1064
1065    #[tokio::test(flavor = "multi_thread")]
1066    async fn test_execute_with_pipe_substitutions() {
1067        let ast = r#"const myVar = 3
1068const part001 = startSketchOn(XY)
1069  |> startProfileAt([0, 0], %)
1070  |> line(end = [3, 4], tag = $seg01)
1071  |> line(end = [
1072  min(segLen(seg01), myVar),
1073  legLen(segLen(seg01), myVar)
1074])
1075"#;
1076
1077        parse_execute(ast).await.unwrap();
1078    }
1079
1080    #[tokio::test(flavor = "multi_thread")]
1081    async fn test_execute_with_inline_comment() {
1082        let ast = r#"const baseThick = 1
1083const armAngle = 60
1084
1085const baseThickHalf = baseThick / 2
1086const halfArmAngle = armAngle / 2
1087
1088const arrExpShouldNotBeIncluded = [1, 2, 3]
1089const objExpShouldNotBeIncluded = { a: 1, b: 2, c: 3 }
1090
1091const part001 = startSketchOn(XY)
1092  |> startProfileAt([0, 0], %)
1093  |> yLine(endAbsolute = 1)
1094  |> xLine(length = 3.84) // selection-range-7ish-before-this
1095
1096const variableBelowShouldNotBeIncluded = 3
1097"#;
1098
1099        parse_execute(ast).await.unwrap();
1100    }
1101
1102    #[tokio::test(flavor = "multi_thread")]
1103    async fn test_execute_with_function_literal_in_pipe() {
1104        let ast = r#"const w = 20
1105const l = 8
1106const h = 10
1107
1108fn thing = () => {
1109  return -8
1110}
1111
1112const firstExtrude = startSketchOn(XY)
1113  |> startProfileAt([0,0], %)
1114  |> line(end = [0, l])
1115  |> line(end = [w, 0])
1116  |> line(end = [0, thing()])
1117  |> close()
1118  |> extrude(length = h)"#;
1119
1120        parse_execute(ast).await.unwrap();
1121    }
1122
1123    #[tokio::test(flavor = "multi_thread")]
1124    async fn test_execute_with_function_unary_in_pipe() {
1125        let ast = r#"const w = 20
1126const l = 8
1127const h = 10
1128
1129fn thing = (x) => {
1130  return -x
1131}
1132
1133const firstExtrude = startSketchOn(XY)
1134  |> startProfileAt([0,0], %)
1135  |> line(end = [0, l])
1136  |> line(end = [w, 0])
1137  |> line(end = [0, thing(8)])
1138  |> close()
1139  |> extrude(length = h)"#;
1140
1141        parse_execute(ast).await.unwrap();
1142    }
1143
1144    #[tokio::test(flavor = "multi_thread")]
1145    async fn test_execute_with_function_array_in_pipe() {
1146        let ast = r#"const w = 20
1147const l = 8
1148const h = 10
1149
1150fn thing = (x) => {
1151  return [0, -x]
1152}
1153
1154const firstExtrude = startSketchOn(XY)
1155  |> startProfileAt([0,0], %)
1156  |> line(end = [0, l])
1157  |> line(end = [w, 0])
1158  |> line(end = thing(8))
1159  |> close()
1160  |> extrude(length = h)"#;
1161
1162        parse_execute(ast).await.unwrap();
1163    }
1164
1165    #[tokio::test(flavor = "multi_thread")]
1166    async fn test_execute_with_function_call_in_pipe() {
1167        let ast = r#"const w = 20
1168const l = 8
1169const h = 10
1170
1171fn other_thing = (y) => {
1172  return -y
1173}
1174
1175fn thing = (x) => {
1176  return other_thing(x)
1177}
1178
1179const firstExtrude = startSketchOn(XY)
1180  |> startProfileAt([0,0], %)
1181  |> line(end = [0, l])
1182  |> line(end = [w, 0])
1183  |> line(end = [0, thing(8)])
1184  |> close()
1185  |> extrude(length = h)"#;
1186
1187        parse_execute(ast).await.unwrap();
1188    }
1189
1190    #[tokio::test(flavor = "multi_thread")]
1191    async fn test_execute_with_function_sketch() {
1192        let ast = r#"fn box = (h, l, w) => {
1193 const myBox = startSketchOn(XY)
1194    |> startProfileAt([0,0], %)
1195    |> line(end = [0, l])
1196    |> line(end = [w, 0])
1197    |> line(end = [0, -l])
1198    |> close()
1199    |> extrude(length = h)
1200
1201  return myBox
1202}
1203
1204const fnBox = box(3, 6, 10)"#;
1205
1206        parse_execute(ast).await.unwrap();
1207    }
1208
1209    #[tokio::test(flavor = "multi_thread")]
1210    async fn test_get_member_of_object_with_function_period() {
1211        let ast = r#"fn box = (obj) => {
1212 let myBox = startSketchOn(XY)
1213    |> startProfileAt(obj.start, %)
1214    |> line(end = [0, obj.l])
1215    |> line(end = [obj.w, 0])
1216    |> line(end = [0, -obj.l])
1217    |> close()
1218    |> extrude(length = obj.h)
1219
1220  return myBox
1221}
1222
1223const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
1224"#;
1225        parse_execute(ast).await.unwrap();
1226    }
1227
1228    #[tokio::test(flavor = "multi_thread")]
1229    async fn test_get_member_of_object_with_function_brace() {
1230        let ast = r#"fn box = (obj) => {
1231 let myBox = startSketchOn(XY)
1232    |> startProfileAt(obj["start"], %)
1233    |> line(end = [0, obj["l"]])
1234    |> line(end = [obj["w"], 0])
1235    |> line(end = [0, -obj["l"]])
1236    |> close()
1237    |> extrude(length = obj["h"])
1238
1239  return myBox
1240}
1241
1242const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
1243"#;
1244        parse_execute(ast).await.unwrap();
1245    }
1246
1247    #[tokio::test(flavor = "multi_thread")]
1248    async fn test_get_member_of_object_with_function_mix_period_brace() {
1249        let ast = r#"fn box = (obj) => {
1250 let myBox = startSketchOn(XY)
1251    |> startProfileAt(obj["start"], %)
1252    |> line(end = [0, obj["l"]])
1253    |> line(end = [obj["w"], 0])
1254    |> line(end = [10 - obj["w"], -obj.l])
1255    |> close()
1256    |> extrude(length = obj["h"])
1257
1258  return myBox
1259}
1260
1261const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
1262"#;
1263        parse_execute(ast).await.unwrap();
1264    }
1265
1266    #[tokio::test(flavor = "multi_thread")]
1267    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
1268    async fn test_object_member_starting_pipeline() {
1269        let ast = r#"
1270fn test2 = () => {
1271  return {
1272    thing: startSketchOn(XY)
1273      |> startProfileAt([0, 0], %)
1274      |> line(end = [0, 1])
1275      |> line(end = [1, 0])
1276      |> line(end = [0, -1])
1277      |> close()
1278  }
1279}
1280
1281const x2 = test2()
1282
1283x2.thing
1284  |> extrude(length = 10)
1285"#;
1286        parse_execute(ast).await.unwrap();
1287    }
1288
1289    #[tokio::test(flavor = "multi_thread")]
1290    #[ignore] // ignore til we get loops
1291    async fn test_execute_with_function_sketch_loop_objects() {
1292        let ast = r#"fn box = (obj) => {
1293let myBox = startSketchOn(XY)
1294    |> startProfileAt(obj.start, %)
1295    |> line(end = [0, obj.l])
1296    |> line(end = [obj.w, 0])
1297    |> line(end = [0, -obj.l])
1298    |> close()
1299    |> extrude(length = obj.h)
1300
1301  return myBox
1302}
1303
1304for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1305  const thisBox = box(var)
1306}"#;
1307
1308        parse_execute(ast).await.unwrap();
1309    }
1310
1311    #[tokio::test(flavor = "multi_thread")]
1312    #[ignore] // ignore til we get loops
1313    async fn test_execute_with_function_sketch_loop_array() {
1314        let ast = r#"fn box = (h, l, w, start) => {
1315 const myBox = startSketchOn(XY)
1316    |> startProfileAt([0,0], %)
1317    |> line(end = [0, l])
1318    |> line(end = [w, 0])
1319    |> line(end = [0, -l])
1320    |> close()
1321    |> extrude(length = h)
1322
1323  return myBox
1324}
1325
1326
1327for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1328  const thisBox = box(var[0], var[1], var[2], var[3])
1329}"#;
1330
1331        parse_execute(ast).await.unwrap();
1332    }
1333
1334    #[tokio::test(flavor = "multi_thread")]
1335    async fn test_get_member_of_array_with_function() {
1336        let ast = r#"fn box = (arr) => {
1337 let myBox =startSketchOn(XY)
1338    |> startProfileAt(arr[0], %)
1339    |> line(end = [0, arr[1]])
1340    |> line(end = [arr[2], 0])
1341    |> line(end = [0, -arr[1]])
1342    |> close()
1343    |> extrude(length = arr[3])
1344
1345  return myBox
1346}
1347
1348const thisBox = box([[0,0], 6, 10, 3])
1349
1350"#;
1351        parse_execute(ast).await.unwrap();
1352    }
1353
1354    #[tokio::test(flavor = "multi_thread")]
1355    async fn test_function_cannot_access_future_definitions() {
1356        let ast = r#"
1357fn returnX = () => {
1358  // x shouldn't be defined yet.
1359  return x
1360}
1361
1362const x = 5
1363
1364const answer = returnX()"#;
1365
1366        let result = parse_execute(ast).await;
1367        let err = result.unwrap_err();
1368        assert_eq!(
1369            err,
1370            KclError::UndefinedValue(KclErrorDetails {
1371                message: "memory item key `x` is not defined".to_owned(),
1372                source_ranges: vec![
1373                    SourceRange::new(64, 65, ModuleId::default()),
1374                    SourceRange::new(97, 106, ModuleId::default())
1375                ],
1376            }),
1377        );
1378    }
1379
1380    #[tokio::test(flavor = "multi_thread")]
1381    async fn test_override_prelude() {
1382        let text = "PI = 3.0";
1383        let result = parse_execute(text).await.unwrap();
1384        let errs = result.exec_state.errors();
1385        assert!(errs.is_empty());
1386    }
1387
1388    #[tokio::test(flavor = "multi_thread")]
1389    async fn test_cannot_shebang_in_fn() {
1390        let ast = r#"
1391fn foo () {
1392  #!hello
1393  return true
1394}
1395
1396foo
1397"#;
1398
1399        let result = parse_execute(ast).await;
1400        let err = result.unwrap_err();
1401        assert_eq!(
1402            err,
1403            KclError::Syntax(KclErrorDetails {
1404                message: "Unexpected token: #".to_owned(),
1405                source_ranges: vec![SourceRange::new(15, 16, ModuleId::default())],
1406            }),
1407        );
1408    }
1409
1410    #[tokio::test(flavor = "multi_thread")]
1411    async fn test_pattern_transform_function_cannot_access_future_definitions() {
1412        let ast = r#"
1413fn transform = (replicaId) => {
1414  // x shouldn't be defined yet.
1415  let scale = x
1416  return {
1417    translate: [0, 0, replicaId * 10],
1418    scale: [scale, 1, 0],
1419  }
1420}
1421
1422fn layer = () => {
1423  return startSketchOn(XY)
1424    |> circle( center= [0, 0], radius= 1 , tag =$tag1)
1425    |> extrude(length = 10)
1426}
1427
1428const x = 5
1429
1430// The 10 layers are replicas of each other, with a transform applied to each.
1431let shape = layer() |> patternTransform(instances = 10, transform = transform)
1432"#;
1433
1434        let result = parse_execute(ast).await;
1435        let err = result.unwrap_err();
1436        assert_eq!(
1437            err,
1438            KclError::UndefinedValue(KclErrorDetails {
1439                message: "memory item key `x` is not defined".to_owned(),
1440                source_ranges: vec![SourceRange::new(80, 81, ModuleId::default())],
1441            }),
1442        );
1443    }
1444
1445    // ADAM: Move some of these into simulation tests.
1446
1447    #[tokio::test(flavor = "multi_thread")]
1448    async fn test_math_execute_with_functions() {
1449        let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;
1450        let result = parse_execute(ast).await.unwrap();
1451        assert_eq!(
1452            5.0,
1453            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1454                .as_f64()
1455                .unwrap()
1456        );
1457    }
1458
1459    #[tokio::test(flavor = "multi_thread")]
1460    async fn test_math_execute() {
1461        let ast = r#"const myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
1462        let result = parse_execute(ast).await.unwrap();
1463        assert_eq!(
1464            7.4,
1465            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1466                .as_f64()
1467                .unwrap()
1468        );
1469    }
1470
1471    #[tokio::test(flavor = "multi_thread")]
1472    async fn test_math_execute_start_negative() {
1473        let ast = r#"const myVar = -5 + 6"#;
1474        let result = parse_execute(ast).await.unwrap();
1475        assert_eq!(
1476            1.0,
1477            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1478                .as_f64()
1479                .unwrap()
1480        );
1481    }
1482
1483    #[tokio::test(flavor = "multi_thread")]
1484    async fn test_math_execute_with_pi() {
1485        let ast = r#"const myVar = PI * 2"#;
1486        let result = parse_execute(ast).await.unwrap();
1487        assert_eq!(
1488            std::f64::consts::TAU,
1489            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1490                .as_f64()
1491                .unwrap()
1492        );
1493    }
1494
1495    #[tokio::test(flavor = "multi_thread")]
1496    async fn test_math_define_decimal_without_leading_zero() {
1497        let ast = r#"let thing = .4 + 7"#;
1498        let result = parse_execute(ast).await.unwrap();
1499        assert_eq!(
1500            7.4,
1501            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
1502                .as_f64()
1503                .unwrap()
1504        );
1505    }
1506
1507    #[tokio::test(flavor = "multi_thread")]
1508    async fn test_unit_default() {
1509        let ast = r#"const inMm = 25.4 * mm()
1510const inInches = 1.0 * inch()"#;
1511        let result = parse_execute(ast).await.unwrap();
1512        assert_eq!(
1513            25.4,
1514            mem_get_json(result.exec_state.stack(), result.mem_env, "inMm")
1515                .as_f64()
1516                .unwrap()
1517        );
1518        assert_eq!(
1519            25.4,
1520            mem_get_json(result.exec_state.stack(), result.mem_env, "inInches")
1521                .as_f64()
1522                .unwrap()
1523        );
1524    }
1525
1526    #[tokio::test(flavor = "multi_thread")]
1527    async fn test_unit_overriden() {
1528        let ast = r#"@settings(defaultLengthUnit = inch)
1529const inMm = 25.4 * mm()
1530const inInches = 1.0 * inch()"#;
1531        let result = parse_execute(ast).await.unwrap();
1532        assert_eq!(
1533            1.0,
1534            mem_get_json(result.exec_state.stack(), result.mem_env, "inMm")
1535                .as_f64()
1536                .unwrap()
1537                .round()
1538        );
1539        assert_eq!(
1540            1.0,
1541            mem_get_json(result.exec_state.stack(), result.mem_env, "inInches")
1542                .as_f64()
1543                .unwrap()
1544        );
1545    }
1546
1547    #[tokio::test(flavor = "multi_thread")]
1548    async fn test_unit_overriden_in() {
1549        let ast = r#"@settings(defaultLengthUnit = in)
1550const inMm = 25.4 * mm()
1551const inInches = 2.0 * inch()"#;
1552        let result = parse_execute(ast).await.unwrap();
1553        assert_eq!(
1554            1.0,
1555            mem_get_json(result.exec_state.stack(), result.mem_env, "inMm")
1556                .as_f64()
1557                .unwrap()
1558                .round()
1559        );
1560        assert_eq!(
1561            2.0,
1562            mem_get_json(result.exec_state.stack(), result.mem_env, "inInches")
1563                .as_f64()
1564                .unwrap()
1565        );
1566    }
1567
1568    #[tokio::test(flavor = "multi_thread")]
1569    async fn test_zero_param_fn() {
1570        let ast = r#"const sigmaAllow = 35000 // psi
1571const leg1 = 5 // inches
1572const leg2 = 8 // inches
1573fn thickness = () => { return 0.56 }
1574
1575const bracket = startSketchOn(XY)
1576  |> startProfileAt([0,0], %)
1577  |> line(end = [0, leg1])
1578  |> line(end = [leg2, 0])
1579  |> line(end = [0, -thickness()])
1580  |> line(end = [-leg2 + thickness(), 0])
1581"#;
1582        parse_execute(ast).await.unwrap();
1583    }
1584
1585    #[tokio::test(flavor = "multi_thread")]
1586    async fn test_unary_operator_not_succeeds() {
1587        let ast = r#"
1588fn returnTrue = () => { return !false }
1589const t = true
1590const f = false
1591let notTrue = !t
1592let notFalse = !f
1593let c = !!true
1594let d = !returnTrue()
1595
1596assert(!false, "expected to pass")
1597
1598fn check = (x) => {
1599  assert(!x, "expected argument to be false")
1600  return true
1601}
1602check(false)
1603"#;
1604        let result = parse_execute(ast).await.unwrap();
1605        assert_eq!(
1606            false,
1607            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
1608                .as_bool()
1609                .unwrap()
1610        );
1611        assert_eq!(
1612            true,
1613            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
1614                .as_bool()
1615                .unwrap()
1616        );
1617        assert_eq!(
1618            true,
1619            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
1620                .as_bool()
1621                .unwrap()
1622        );
1623        assert_eq!(
1624            false,
1625            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
1626                .as_bool()
1627                .unwrap()
1628        );
1629    }
1630
1631    #[tokio::test(flavor = "multi_thread")]
1632    async fn test_unary_operator_not_on_non_bool_fails() {
1633        let code1 = r#"
1634// Yup, this is null.
1635let myNull = 0 / 0
1636let notNull = !myNull
1637"#;
1638        assert_eq!(
1639            parse_execute(code1).await.unwrap_err(),
1640            KclError::Semantic(KclErrorDetails {
1641                message: "Cannot apply unary operator ! to non-boolean value: number".to_owned(),
1642                source_ranges: vec![SourceRange::new(56, 63, ModuleId::default())],
1643            })
1644        );
1645
1646        let code2 = "let notZero = !0";
1647        assert_eq!(
1648            parse_execute(code2).await.unwrap_err(),
1649            KclError::Semantic(KclErrorDetails {
1650                message: "Cannot apply unary operator ! to non-boolean value: number".to_owned(),
1651                source_ranges: vec![SourceRange::new(14, 16, ModuleId::default())],
1652            })
1653        );
1654
1655        let code3 = r#"
1656let notEmptyString = !""
1657"#;
1658        assert_eq!(
1659            parse_execute(code3).await.unwrap_err(),
1660            KclError::Semantic(KclErrorDetails {
1661                message: "Cannot apply unary operator ! to non-boolean value: string (text)".to_owned(),
1662                source_ranges: vec![SourceRange::new(22, 25, ModuleId::default())],
1663            })
1664        );
1665
1666        let code4 = r#"
1667let obj = { a: 1 }
1668let notMember = !obj.a
1669"#;
1670        assert_eq!(
1671            parse_execute(code4).await.unwrap_err(),
1672            KclError::Semantic(KclErrorDetails {
1673                message: "Cannot apply unary operator ! to non-boolean value: number".to_owned(),
1674                source_ranges: vec![SourceRange::new(36, 42, ModuleId::default())],
1675            })
1676        );
1677
1678        let code5 = "
1679let a = []
1680let notArray = !a";
1681        assert_eq!(
1682            parse_execute(code5).await.unwrap_err(),
1683            KclError::Semantic(KclErrorDetails {
1684                message: "Cannot apply unary operator ! to non-boolean value: array (list)".to_owned(),
1685                source_ranges: vec![SourceRange::new(27, 29, ModuleId::default())],
1686            })
1687        );
1688
1689        let code6 = "
1690let x = {}
1691let notObject = !x";
1692        assert_eq!(
1693            parse_execute(code6).await.unwrap_err(),
1694            KclError::Semantic(KclErrorDetails {
1695                message: "Cannot apply unary operator ! to non-boolean value: object".to_owned(),
1696                source_ranges: vec![SourceRange::new(28, 30, ModuleId::default())],
1697            })
1698        );
1699
1700        let code7 = "
1701fn x = () => { return 1 }
1702let notFunction = !x";
1703        let fn_err = parse_execute(code7).await.unwrap_err();
1704        // These are currently printed out as JSON objects, so we don't want to
1705        // check the full error.
1706        assert!(
1707            fn_err
1708                .message()
1709                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
1710            "Actual error: {:?}",
1711            fn_err
1712        );
1713
1714        let code8 = "
1715let myTagDeclarator = $myTag
1716let notTagDeclarator = !myTagDeclarator";
1717        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
1718        // These are currently printed out as JSON objects, so we don't want to
1719        // check the full error.
1720        assert!(
1721            tag_declarator_err
1722                .message()
1723                .starts_with("Cannot apply unary operator ! to non-boolean value: TagDeclarator"),
1724            "Actual error: {:?}",
1725            tag_declarator_err
1726        );
1727
1728        let code9 = "
1729let myTagDeclarator = $myTag
1730let notTagIdentifier = !myTag";
1731        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
1732        // These are currently printed out as JSON objects, so we don't want to
1733        // check the full error.
1734        assert!(
1735            tag_identifier_err
1736                .message()
1737                .starts_with("Cannot apply unary operator ! to non-boolean value: TagIdentifier"),
1738            "Actual error: {:?}",
1739            tag_identifier_err
1740        );
1741
1742        let code10 = "let notPipe = !(1 |> 2)";
1743        assert_eq!(
1744            // TODO: We don't currently parse this, but we should.  It should be
1745            // a runtime error instead.
1746            parse_execute(code10).await.unwrap_err(),
1747            KclError::Syntax(KclErrorDetails {
1748                message: "Unexpected token: !".to_owned(),
1749                source_ranges: vec![SourceRange::new(14, 15, ModuleId::default())],
1750            })
1751        );
1752
1753        let code11 = "
1754fn identity = (x) => { return x }
1755let notPipeSub = 1 |> identity(!%))";
1756        assert_eq!(
1757            // TODO: We don't currently parse this, but we should.  It should be
1758            // a runtime error instead.
1759            parse_execute(code11).await.unwrap_err(),
1760            KclError::Syntax(KclErrorDetails {
1761                message: "Unexpected token: |>".to_owned(),
1762                source_ranges: vec![SourceRange::new(54, 56, ModuleId::default())],
1763            })
1764        );
1765
1766        // TODO: Add these tests when we support these types.
1767        // let notNan = !NaN
1768        // let notInfinity = !Infinity
1769    }
1770
1771    #[tokio::test(flavor = "multi_thread")]
1772    async fn test_math_negative_variable_in_binary_expression() {
1773        let ast = r#"const sigmaAllow = 35000 // psi
1774const width = 1 // inch
1775
1776const p = 150 // lbs
1777const distance = 6 // inches
1778const FOS = 2
1779
1780const leg1 = 5 // inches
1781const leg2 = 8 // inches
1782
1783const thickness_squared = distance * p * FOS * 6 / sigmaAllow
1784const thickness = 0.56 // inches. App does not support square root function yet
1785
1786const bracket = startSketchOn(XY)
1787  |> startProfileAt([0,0], %)
1788  |> line(end = [0, leg1])
1789  |> line(end = [leg2, 0])
1790  |> line(end = [0, -thickness])
1791  |> line(end = [-leg2 + thickness, 0])
1792"#;
1793        parse_execute(ast).await.unwrap();
1794    }
1795
1796    #[tokio::test(flavor = "multi_thread")]
1797    async fn test_execute_function_no_return() {
1798        let ast = r#"fn test = (origin) => {
1799  origin
1800}
1801
1802test([0, 0])
1803"#;
1804        let result = parse_execute(ast).await;
1805        assert!(result.is_err());
1806        assert!(result.unwrap_err().to_string().contains("undefined"),);
1807    }
1808
1809    #[tokio::test(flavor = "multi_thread")]
1810    async fn test_math_doubly_nested_parens() {
1811        let ast = r#"const sigmaAllow = 35000 // psi
1812const width = 4 // inch
1813const p = 150 // Force on shelf - lbs
1814const distance = 6 // inches
1815const FOS = 2
1816const leg1 = 5 // inches
1817const leg2 = 8 // inches
1818const thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
1819const thickness = 0.32 // inches. App does not support square root function yet
1820const bracket = startSketchOn(XY)
1821  |> startProfileAt([0,0], %)
1822    |> line(end = [0, leg1])
1823  |> line(end = [leg2, 0])
1824  |> line(end = [0, -thickness])
1825  |> line(end = [-1 * leg2 + thickness, 0])
1826  |> line(end = [0, -1 * leg1 + thickness])
1827  |> close()
1828  |> extrude(length = width)
1829"#;
1830        parse_execute(ast).await.unwrap();
1831    }
1832
1833    #[tokio::test(flavor = "multi_thread")]
1834    async fn test_math_nested_parens_one_less() {
1835        let ast = r#"const sigmaAllow = 35000 // psi
1836const width = 4 // inch
1837const p = 150 // Force on shelf - lbs
1838const distance = 6 // inches
1839const FOS = 2
1840const leg1 = 5 // inches
1841const leg2 = 8 // inches
1842const thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
1843const thickness = 0.32 // inches. App does not support square root function yet
1844const bracket = startSketchOn(XY)
1845  |> startProfileAt([0,0], %)
1846    |> line(end = [0, leg1])
1847  |> line(end = [leg2, 0])
1848  |> line(end = [0, -thickness])
1849  |> line(end = [-1 * leg2 + thickness, 0])
1850  |> line(end = [0, -1 * leg1 + thickness])
1851  |> close()
1852  |> extrude(length = width)
1853"#;
1854        parse_execute(ast).await.unwrap();
1855    }
1856
1857    #[tokio::test(flavor = "multi_thread")]
1858    async fn test_fn_as_operand() {
1859        let ast = r#"fn f = () => { return 1 }
1860let x = f()
1861let y = x + 1
1862let z = f() + 1
1863let w = f() + f()
1864"#;
1865        parse_execute(ast).await.unwrap();
1866    }
1867
1868    #[test]
1869    fn test_serialize_memory_item() {
1870        let mem = KclValue::Solids {
1871            value: Default::default(),
1872        };
1873        let json = serde_json::to_string(&mem).unwrap();
1874        assert_eq!(json, r#"{"type":"Solids","value":[]}"#);
1875    }
1876
1877    #[tokio::test(flavor = "multi_thread")]
1878    async fn kcl_test_ids_stable_between_executions() {
1879        let code = r#"sketch001 = startSketchOn(XZ)
1880|> startProfileAt([61.74, 206.13], %)
1881|> xLine(length = 305.11, tag = $seg01)
1882|> yLine(length = -291.85)
1883|> xLine(length = -segLen(seg01))
1884|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
1885|> close()
1886|> extrude(length = 40.14)
1887|> shell(
1888    thickness = 3.14,
1889    faces = [seg01]
1890)
1891"#;
1892
1893        let ctx = crate::test_server::new_context(UnitLength::Mm, true, None)
1894            .await
1895            .unwrap();
1896        let old_program = crate::Program::parse_no_errs(code).unwrap();
1897
1898        // Execute the program.
1899        ctx.run_with_caching(old_program).await.unwrap();
1900
1901        // Get the id_generator from the first execution.
1902        let id_generator = cache::read_old_ast().await.unwrap().exec_state.global.id_generator;
1903
1904        let code = r#"sketch001 = startSketchOn(XZ)
1905|> startProfileAt([62.74, 206.13], %)
1906|> xLine(length = 305.11, tag = $seg01)
1907|> yLine(length = -291.85)
1908|> xLine(length = -segLen(seg01))
1909|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
1910|> close()
1911|> extrude(length = 40.14)
1912|> shell(
1913    faces = [seg01],
1914    thickness = 3.14,
1915)
1916"#;
1917
1918        // Execute a slightly different program again.
1919        let program = crate::Program::parse_no_errs(code).unwrap();
1920        // Execute the program.
1921        ctx.run_with_caching(program).await.unwrap();
1922
1923        let new_id_generator = cache::read_old_ast().await.unwrap().exec_state.global.id_generator;
1924
1925        assert_eq!(id_generator, new_id_generator);
1926    }
1927
1928    #[tokio::test(flavor = "multi_thread")]
1929    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
1930        let code = r#"sketch001 = startSketchOn('XZ')
1931|> startProfileAt([61.74, 206.13], %)
1932|> xLine(length = 305.11, tag = $seg01)
1933|> yLine(length = -291.85)
1934|> xLine(length = -segLen(seg01))
1935|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
1936|> close()
1937|> extrude(length = 40.14)
1938|> shell(
1939    thickness = 3.14,
1940    faces = [seg01]
1941)
1942"#;
1943
1944        let mut ctx = crate::test_server::new_context(UnitLength::Mm, true, None)
1945            .await
1946            .unwrap();
1947        let old_program = crate::Program::parse_no_errs(code).unwrap();
1948
1949        // Execute the program.
1950        ctx.run_with_caching(old_program.clone()).await.unwrap();
1951
1952        // Get the id_generator from the first execution.
1953        let settings_state = cache::read_old_ast().await.unwrap().settings;
1954
1955        // Ensure the settings are as expected.
1956        assert_eq!(settings_state, ctx.settings);
1957
1958        // Change a setting.
1959        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
1960
1961        // Execute the program.
1962        ctx.run_with_caching(old_program.clone()).await.unwrap();
1963
1964        // Get the id_generator from the first execution.
1965        let settings_state = cache::read_old_ast().await.unwrap().settings;
1966
1967        // Ensure the settings are as expected.
1968        assert_eq!(settings_state, ctx.settings);
1969
1970        // Change a setting.
1971        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
1972
1973        // Execute the program.
1974        ctx.run_with_caching(old_program).await.unwrap();
1975
1976        // Get the id_generator from the first execution.
1977        let settings_state = cache::read_old_ast().await.unwrap().settings;
1978
1979        // Ensure the settings are as expected.
1980        assert_eq!(settings_state, ctx.settings);
1981    }
1982
1983    #[tokio::test(flavor = "multi_thread")]
1984    async fn mock_after_not_mock() {
1985        let ctx = ExecutorContext::new_with_default_client(UnitLength::Mm).await.unwrap();
1986        let program = crate::Program::parse_no_errs("x = 2").unwrap();
1987        let result = ctx.run_with_caching(program).await.unwrap();
1988        assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
1989
1990        let ctx2 = ExecutorContext::new_mock().await;
1991        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
1992        let result = ctx2.run_mock(program2, true).await.unwrap();
1993        assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
1994    }
1995}