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