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