1use std::sync::Arc;
4
5use anyhow::Result;
6#[cfg(feature = "artifact-graph")]
7pub use artifact::{
8    Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, CodeRef, StartSketchOnFace, StartSketchOnPlane,
9};
10use cache::OldAstState;
11pub use cache::{bust_cache, clear_mem_cache};
12#[cfg(feature = "artifact-graph")]
13pub use cad_op::{Group, Operation};
14pub use geometry::*;
15pub use id_generator::IdGenerator;
16pub(crate) use import::PreImportedGeometry;
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
31#[cfg(feature = "artifact-graph")]
32use crate::execution::artifact::build_artifact_graph;
33use crate::{
34    engine::EngineManager,
35    errors::{KclError, KclErrorDetails},
36    execution::{
37        cache::{CacheInformation, CacheResult},
38        typed_path::TypedPath,
39        types::{UnitAngle, UnitLen},
40    },
41    fs::FileManager,
42    modules::{ModuleId, ModulePath, ModuleRepr},
43    parsing::ast::types::{Expr, ImportPath, NodeRef},
44    source_range::SourceRange,
45    std::StdLib,
46    walk::{Universe, UniverseMap},
47    CompilationError, ExecError, KclErrorWithOutputs,
48};
49
50pub(crate) mod annotations;
51#[cfg(feature = "artifact-graph")]
52mod artifact;
53pub(crate) mod cache;
54#[cfg(feature = "artifact-graph")]
55mod cad_op;
56mod exec_ast;
57mod geometry;
58mod id_generator;
59mod import;
60pub(crate) mod kcl_value;
61mod memory;
62mod state;
63pub mod typed_path;
64pub(crate) mod types;
65
66#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
68#[ts(export)]
69#[serde(rename_all = "camelCase")]
70pub struct ExecOutcome {
71    pub variables: IndexMap<String, KclValue>,
73    #[cfg(feature = "artifact-graph")]
76    pub operations: Vec<Operation>,
77    #[cfg(feature = "artifact-graph")]
79    pub artifact_commands: Vec<ArtifactCommand>,
80    #[cfg(feature = "artifact-graph")]
82    pub artifact_graph: ArtifactGraph,
83    pub errors: Vec<CompilationError>,
85    pub filenames: IndexMap<ModuleId, ModulePath>,
87    pub default_planes: Option<DefaultPlanes>,
89}
90
91#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
92#[ts(export)]
93#[serde(rename_all = "camelCase")]
94pub struct DefaultPlanes {
95    pub xy: uuid::Uuid,
96    pub xz: uuid::Uuid,
97    pub yz: uuid::Uuid,
98    pub neg_xy: uuid::Uuid,
99    pub neg_xz: uuid::Uuid,
100    pub neg_yz: uuid::Uuid,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
104#[ts(export)]
105#[serde(tag = "type", rename_all = "camelCase")]
106pub struct TagIdentifier {
107    pub value: String,
108    #[serde(skip)]
111    pub info: Vec<(usize, TagEngineInfo)>,
112    #[serde(skip)]
113    pub meta: Vec<Metadata>,
114}
115
116impl TagIdentifier {
117    pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
119        for (e, info) in self.info.iter().rev() {
120            if *e <= at_epoch {
121                return Some(info);
122            }
123        }
124
125        None
126    }
127
128    pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
130        self.info.last().map(|i| &i.1)
131    }
132
133    pub fn merge_info(&mut self, other: &TagIdentifier) {
135        assert_eq!(&self.value, &other.value);
136        for (oe, ot) in &other.info {
137            if let Some((e, t)) = self.info.last_mut() {
138                if *e > *oe {
140                    continue;
141                }
142                if e == oe {
144                    *t = ot.clone();
145                    continue;
146                }
147            }
148            self.info.push((*oe, ot.clone()));
149        }
150    }
151}
152
153impl Eq for TagIdentifier {}
154
155impl std::fmt::Display for TagIdentifier {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        write!(f, "{}", self.value)
158    }
159}
160
161impl std::str::FromStr for TagIdentifier {
162    type Err = KclError;
163
164    fn from_str(s: &str) -> Result<Self, Self::Err> {
165        Ok(Self {
166            value: s.to_string(),
167            info: Vec::new(),
168            meta: Default::default(),
169        })
170    }
171}
172
173impl Ord for TagIdentifier {
174    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
175        self.value.cmp(&other.value)
176    }
177}
178
179impl PartialOrd for TagIdentifier {
180    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
181        Some(self.cmp(other))
182    }
183}
184
185impl std::hash::Hash for TagIdentifier {
186    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
187        self.value.hash(state);
188    }
189}
190
191#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
193#[ts(export)]
194#[serde(tag = "type", rename_all = "camelCase")]
195pub struct TagEngineInfo {
196    pub id: uuid::Uuid,
198    pub sketch: uuid::Uuid,
200    pub path: Option<Path>,
202    pub surface: Option<ExtrudeSurface>,
204}
205
206#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
207pub enum BodyType {
208    Root,
209    Block,
210}
211
212#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
214#[ts(export)]
215#[serde(rename_all = "camelCase")]
216pub struct Metadata {
217    pub source_range: SourceRange,
219}
220
221impl From<Metadata> for Vec<SourceRange> {
222    fn from(meta: Metadata) -> Self {
223        vec![meta.source_range]
224    }
225}
226
227impl From<SourceRange> for Metadata {
228    fn from(source_range: SourceRange) -> Self {
229        Self { source_range }
230    }
231}
232
233impl<T> From<NodeRef<'_, T>> for Metadata {
234    fn from(node: NodeRef<'_, T>) -> Self {
235        Self {
236            source_range: SourceRange::new(node.start, node.end, node.module_id),
237        }
238    }
239}
240
241impl From<&Expr> for Metadata {
242    fn from(expr: &Expr) -> Self {
243        Self {
244            source_range: SourceRange::from(expr),
245        }
246    }
247}
248
249#[derive(PartialEq, Debug, Default, Clone)]
251pub enum ContextType {
252    #[default]
254    Live,
255
256    Mock,
260
261    MockCustomForwarded,
263}
264
265#[derive(Debug, Clone)]
269pub struct ExecutorContext {
270    pub engine: Arc<Box<dyn EngineManager>>,
271    pub fs: Arc<FileManager>,
272    pub stdlib: Arc<StdLib>,
273    pub settings: ExecutorSettings,
274    pub context_type: ContextType,
275}
276
277#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
279#[ts(export)]
280pub struct ExecutorSettings {
281    pub highlight_edges: bool,
283    pub enable_ssao: bool,
285    pub show_grid: bool,
287    pub replay: Option<String>,
290    pub project_directory: Option<TypedPath>,
293    pub current_file: Option<TypedPath>,
296}
297
298impl Default for ExecutorSettings {
299    fn default() -> Self {
300        Self {
301            highlight_edges: true,
302            enable_ssao: false,
303            show_grid: false,
304            replay: None,
305            project_directory: None,
306            current_file: None,
307        }
308    }
309}
310
311impl From<crate::settings::types::Configuration> for ExecutorSettings {
312    fn from(config: crate::settings::types::Configuration) -> Self {
313        Self {
314            highlight_edges: config.settings.modeling.highlight_edges.into(),
315            enable_ssao: config.settings.modeling.enable_ssao.into(),
316            show_grid: config.settings.modeling.show_scale_grid,
317            replay: None,
318            project_directory: None,
319            current_file: None,
320        }
321    }
322}
323
324impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
325    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
326        Self {
327            highlight_edges: config.settings.modeling.highlight_edges.into(),
328            enable_ssao: config.settings.modeling.enable_ssao.into(),
329            show_grid: Default::default(),
330            replay: None,
331            project_directory: None,
332            current_file: None,
333        }
334    }
335}
336
337impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
338    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
339        Self {
340            highlight_edges: modeling.highlight_edges.into(),
341            enable_ssao: modeling.enable_ssao.into(),
342            show_grid: modeling.show_scale_grid,
343            replay: None,
344            project_directory: None,
345            current_file: None,
346        }
347    }
348}
349
350impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
351    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
352        Self {
353            highlight_edges: modeling.highlight_edges.into(),
354            enable_ssao: modeling.enable_ssao.into(),
355            show_grid: Default::default(),
356            replay: None,
357            project_directory: None,
358            current_file: None,
359        }
360    }
361}
362
363impl ExecutorSettings {
364    pub fn with_current_file(&mut self, current_file: TypedPath) {
366        if current_file.extension() == Some("kcl") {
368            self.current_file = Some(current_file.clone());
369            if let Some(parent) = current_file.parent() {
371                self.project_directory = Some(parent);
372            } else {
373                self.project_directory = Some(TypedPath::from(""));
374            }
375        } else {
376            self.project_directory = Some(current_file.clone());
377        }
378    }
379}
380
381impl ExecutorContext {
382    #[cfg(not(target_arch = "wasm32"))]
384    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
385        let (ws, _headers) = client
386            .modeling()
387            .commands_ws(
388                None,
389                None,
390                None,
391                if settings.enable_ssao {
392                    Some(kittycad::types::PostEffectType::Ssao)
393                } else {
394                    None
395                },
396                settings.replay.clone(),
397                if settings.show_grid { Some(true) } else { None },
398                None,
399                None,
400                None,
401                Some(false),
402            )
403            .await?;
404
405        let engine: Arc<Box<dyn EngineManager>> =
406            Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
407
408        Ok(Self {
409            engine,
410            fs: Arc::new(FileManager::new()),
411            stdlib: Arc::new(StdLib::new()),
412            settings,
413            context_type: ContextType::Live,
414        })
415    }
416
417    #[cfg(target_arch = "wasm32")]
418    pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
419        ExecutorContext {
420            engine,
421            fs,
422            stdlib: Arc::new(StdLib::new()),
423            settings,
424            context_type: ContextType::Live,
425        }
426    }
427
428    #[cfg(not(target_arch = "wasm32"))]
429    pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
430        ExecutorContext {
431            engine: Arc::new(Box::new(
432                crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
433            )),
434            fs: Arc::new(FileManager::new()),
435            stdlib: Arc::new(StdLib::new()),
436            settings: settings.unwrap_or_default(),
437            context_type: ContextType::Mock,
438        }
439    }
440
441    #[cfg(target_arch = "wasm32")]
442    pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
443        ExecutorContext {
444            engine,
445            fs,
446            stdlib: Arc::new(StdLib::new()),
447            settings,
448            context_type: ContextType::Mock,
449        }
450    }
451
452    #[cfg(not(target_arch = "wasm32"))]
453    pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
454        ExecutorContext {
455            engine,
456            fs: Arc::new(FileManager::new()),
457            stdlib: Arc::new(StdLib::new()),
458            settings: Default::default(),
459            context_type: ContextType::MockCustomForwarded,
460        }
461    }
462
463    #[cfg(not(target_arch = "wasm32"))]
469    pub async fn new_with_client(
470        settings: ExecutorSettings,
471        token: Option<String>,
472        engine_addr: Option<String>,
473    ) -> Result<Self> {
474        let client = crate::engine::new_zoo_client(token, engine_addr)?;
476
477        let ctx = Self::new(&client, settings).await?;
478        Ok(ctx)
479    }
480
481    #[cfg(not(target_arch = "wasm32"))]
486    pub async fn new_with_default_client() -> Result<Self> {
487        let ctx = Self::new_with_client(Default::default(), None, None).await?;
489        Ok(ctx)
490    }
491
492    #[cfg(not(target_arch = "wasm32"))]
494    pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
495        let ctx = ExecutorContext::new_with_client(
496            ExecutorSettings {
497                highlight_edges: true,
498                enable_ssao: false,
499                show_grid: false,
500                replay: None,
501                project_directory: None,
502                current_file: None,
503            },
504            None,
505            engine_addr,
506        )
507        .await?;
508        Ok(ctx)
509    }
510
511    pub fn is_mock(&self) -> bool {
512        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
513    }
514
515    pub async fn no_engine_commands(&self) -> bool {
517        self.is_mock()
518    }
519
520    pub async fn send_clear_scene(
521        &self,
522        exec_state: &mut ExecState,
523        source_range: crate::execution::SourceRange,
524    ) -> Result<(), KclError> {
525        self.engine
526            .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
527            .await
528    }
529
530    pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
531        cache::bust_cache().await;
532
533        let outcome = self.run_with_caching(crate::Program::empty()).await?;
538
539        Ok(outcome)
540    }
541
542    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
543        self.eval_prelude(exec_state, SourceRange::synthetic())
544            .await
545            .map_err(KclErrorWithOutputs::no_outputs)?;
546        exec_state.mut_stack().push_new_root_env(true);
547        Ok(())
548    }
549
550    pub async fn run_mock(
551        &self,
552        program: crate::Program,
553        use_prev_memory: bool,
554    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
555        assert!(self.is_mock());
556
557        let mut exec_state = ExecState::new(self);
558        if use_prev_memory {
559            match cache::read_old_memory().await {
560                Some(mem) => {
561                    *exec_state.mut_stack() = mem.0;
562                    exec_state.global.module_infos = mem.1;
563                }
564                None => self.prepare_mem(&mut exec_state).await?,
565            }
566        } else {
567            self.prepare_mem(&mut exec_state).await?
568        };
569
570        exec_state.mut_stack().push_new_env_for_scope();
573
574        let result = self.inner_run(&program, 0, &mut exec_state, true).await?;
575
576        let mut mem = exec_state.stack().clone();
581        let module_infos = exec_state.global.module_infos.clone();
582        let outcome = exec_state.to_mock_exec_outcome(result.0).await;
583
584        mem.squash_env(result.0);
585        cache::write_old_memory((mem, module_infos)).await;
586
587        Ok(outcome)
588    }
589
590    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
591        assert!(!self.is_mock());
592
593        let (program, mut exec_state, preserve_mem, cached_body_items, imports_info) = if let Some(OldAstState {
594            ast: old_ast,
595            exec_state: mut old_state,
596            settings: old_settings,
597            result_env,
598        }) =
599            cache::read_old_ast().await
600        {
601            let old = CacheInformation {
602                ast: &old_ast,
603                settings: &old_settings,
604            };
605            let new = CacheInformation {
606                ast: &program.ast,
607                settings: &self.settings,
608            };
609
610            let (clear_scene, program, body_items, import_check_info) = match cache::get_changed_program(old, new).await
612            {
613                CacheResult::ReExecute {
614                    clear_scene,
615                    reapply_settings,
616                    program: changed_program,
617                    cached_body_items,
618                } => {
619                    if reapply_settings
620                        && self
621                            .engine
622                            .reapply_settings(&self.settings, Default::default(), old_state.id_generator())
623                            .await
624                            .is_err()
625                    {
626                        (true, program, cached_body_items, None)
627                    } else {
628                        (
629                            clear_scene,
630                            crate::Program {
631                                ast: changed_program,
632                                original_file_contents: program.original_file_contents,
633                            },
634                            cached_body_items,
635                            None,
636                        )
637                    }
638                }
639                CacheResult::CheckImportsOnly {
640                    reapply_settings,
641                    ast: changed_program,
642                } => {
643                    if reapply_settings
644                        && self
645                            .engine
646                            .reapply_settings(&self.settings, Default::default(), old_state.id_generator())
647                            .await
648                            .is_err()
649                    {
650                        (true, program, old_ast.body.len(), None)
651                    } else {
652                        let mut new_exec_state = ExecState::new(self);
654                        let (new_universe, new_universe_map) = self.get_universe(&program, &mut new_exec_state).await?;
655                        let mut clear_scene = false;
656
657                        let mut keys = new_universe.keys().clone().collect::<Vec<_>>();
658                        keys.sort();
659                        for key in keys {
660                            let (_, id, _, _) = &new_universe[key];
661                            if let (Some(source0), Some(source1)) =
662                                (old_state.get_source(*id), new_exec_state.get_source(*id))
663                            {
664                                if source0.source != source1.source {
665                                    clear_scene = true;
666                                    break;
667                                }
668                            }
669                        }
670
671                        if !clear_scene {
672                            let outcome = old_state.to_exec_outcome(result_env).await;
674                            return Ok(outcome);
675                        }
676
677                        (
678                            clear_scene,
679                            crate::Program {
680                                ast: changed_program,
681                                original_file_contents: program.original_file_contents,
682                            },
683                            old_ast.body.len(),
684                            if clear_scene {
686                                Some((new_universe, new_universe_map, new_exec_state))
687                            } else {
688                                None
689                            },
690                        )
691                    }
692                }
693                CacheResult::NoAction(true) => {
694                    if self
695                        .engine
696                        .reapply_settings(&self.settings, Default::default(), old_state.id_generator())
697                        .await
698                        .is_ok()
699                    {
700                        cache::write_old_ast(OldAstState {
702                            ast: old_ast,
703                            exec_state: old_state.clone(),
704                            settings: self.settings.clone(),
705                            result_env,
706                        })
707                        .await;
708
709                        let outcome = old_state.to_exec_outcome(result_env).await;
710                        return Ok(outcome);
711                    }
712                    (true, program, old_ast.body.len(), None)
713                }
714                CacheResult::NoAction(false) => {
715                    let outcome = old_state.to_exec_outcome(result_env).await;
716                    return Ok(outcome);
717                }
718            };
719
720            let (exec_state, preserve_mem, universe_info) =
721                if let Some((new_universe, new_universe_map, mut new_exec_state)) = import_check_info {
722                    self.send_clear_scene(&mut new_exec_state, Default::default())
724                        .await
725                        .map_err(KclErrorWithOutputs::no_outputs)?;
726
727                    (new_exec_state, false, Some((new_universe, new_universe_map)))
728                } else if clear_scene {
729                    let mut exec_state = old_state;
731                    exec_state.reset(self);
732
733                    self.send_clear_scene(&mut exec_state, Default::default())
734                        .await
735                        .map_err(KclErrorWithOutputs::no_outputs)?;
736
737                    (exec_state, false, None)
738                } else {
739                    old_state.mut_stack().restore_env(result_env);
740
741                    (old_state, true, None)
742                };
743
744            (program, exec_state, preserve_mem, body_items, universe_info)
745        } else {
746            let mut exec_state = ExecState::new(self);
747            self.send_clear_scene(&mut exec_state, Default::default())
748                .await
749                .map_err(KclErrorWithOutputs::no_outputs)?;
750            (program, exec_state, false, 0, None)
751        };
752
753        let result = self
754            .run_concurrent(&program, cached_body_items, &mut exec_state, imports_info, preserve_mem)
755            .await;
756
757        if result.is_err() {
758            cache::bust_cache().await;
759        }
760
761        let result = result?;
763
764        cache::write_old_ast(OldAstState {
766            ast: program.ast,
767            exec_state: exec_state.clone(),
768            settings: self.settings.clone(),
769            result_env: result.0,
770        })
771        .await;
772
773        let outcome = exec_state.to_exec_outcome(result.0).await;
774        Ok(outcome)
775    }
776
777    pub async fn run(
784        &self,
785        program: &crate::Program,
786        exec_state: &mut ExecState,
787    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
788        self.run_concurrent(program, 0, exec_state, None, false).await
789    }
790
791    pub async fn run_concurrent(
799        &self,
800        program: &crate::Program,
801        cached_body_items: usize,
802        exec_state: &mut ExecState,
803        universe_info: Option<(Universe, UniverseMap)>,
804        preserve_mem: bool,
805    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
806        #[allow(unused_variables)]
808        let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
809            (universe, universe_map)
810        } else {
811            self.get_universe(program, exec_state).await?
812        };
813
814        let default_planes = self.engine.get_default_planes().read().await.clone();
815
816        self.eval_prelude(exec_state, SourceRange::synthetic())
818            .await
819            .map_err(KclErrorWithOutputs::no_outputs)?;
820
821        for modules in crate::walk::import_graph(&universe, self)
822            .map_err(|err| {
823                let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
824                    .global
825                    .path_to_source_id
826                    .iter()
827                    .map(|(k, v)| ((*v), k.clone()))
828                    .collect();
829
830                KclErrorWithOutputs::new(
831                    err,
832                    exec_state.errors().to_vec(),
833                    #[cfg(feature = "artifact-graph")]
834                    exec_state.global.operations.clone(),
835                    #[cfg(feature = "artifact-graph")]
836                    exec_state.global.artifact_commands.clone(),
837                    #[cfg(feature = "artifact-graph")]
838                    exec_state.global.artifact_graph.clone(),
839                    module_id_to_module_path,
840                    exec_state.global.id_to_source.clone(),
841                    default_planes.clone(),
842                )
843            })?
844            .into_iter()
845        {
846            #[cfg(not(target_arch = "wasm32"))]
847            let mut set = tokio::task::JoinSet::new();
848
849            #[allow(clippy::type_complexity)]
850            let (results_tx, mut results_rx): (
851                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
852                tokio::sync::mpsc::Receiver<_>,
853            ) = tokio::sync::mpsc::channel(1);
854
855            for module in modules {
856                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
857                    return Err(KclErrorWithOutputs::no_outputs(KclError::Internal(KclErrorDetails {
858                        message: format!("Module {module} not found in universe"),
859                        source_ranges: Default::default(),
860                    })));
861                };
862                let module_id = *module_id;
863                let module_path = module_path.clone();
864                let source_range = SourceRange::from(import_stmt);
865
866                #[cfg(feature = "artifact-graph")]
867                match &module_path {
868                    ModulePath::Main => {
869                        }
871                    ModulePath::Local { value, .. } => {
872                        if universe_map.contains_key(value) {
875                            exec_state.global.operations.push(Operation::GroupBegin {
876                                group: Group::ModuleInstance {
877                                    name: value.file_name().unwrap_or_default(),
878                                    module_id,
879                                },
880                                source_range,
881                            });
882                            exec_state.global.operations.push(Operation::GroupEnd);
886                        }
887                    }
888                    ModulePath::Std { .. } => {
889                        }
891                }
892
893                let repr = repr.clone();
894                let exec_state = exec_state.clone();
895                let exec_ctxt = self.clone();
896                let results_tx = results_tx.clone();
897
898                let exec_module = async |exec_ctxt: &ExecutorContext,
899                                         repr: &ModuleRepr,
900                                         module_id: ModuleId,
901                                         module_path: &ModulePath,
902                                         exec_state: &mut ExecState,
903                                         source_range: SourceRange|
904                       -> Result<ModuleRepr, KclError> {
905                    match repr {
906                        ModuleRepr::Kcl(program, _) => {
907                            let result = exec_ctxt
908                                .exec_module_from_ast(program, module_id, module_path, exec_state, source_range, false)
909                                .await;
910
911                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
912                        }
913                        ModuleRepr::Foreign(geom, _) => {
914                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_ctxt)
915                                .await
916                                .map(|geom| Some(KclValue::ImportedGeometry(geom)));
917
918                            result.map(|val| ModuleRepr::Foreign(geom.clone(), val))
919                        }
920                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::Internal(KclErrorDetails {
921                            message: format!("Module {module_path} not found in universe"),
922                            source_ranges: vec![source_range],
923                        })),
924                    }
925                };
926
927                #[cfg(target_arch = "wasm32")]
928                {
929                    wasm_bindgen_futures::spawn_local(async move {
930                        let mut exec_state = exec_state;
932                        let exec_ctxt = exec_ctxt;
933
934                        let result = exec_module(
935                            &exec_ctxt,
936                            &repr,
937                            module_id,
938                            &module_path,
939                            &mut exec_state,
940                            source_range,
941                        )
942                        .await;
943
944                        results_tx
945                            .send((module_id, module_path, result))
946                            .await
947                            .unwrap_or_default();
948                    });
949                }
950                #[cfg(not(target_arch = "wasm32"))]
951                {
952                    set.spawn(async move {
953                        let mut exec_state = exec_state;
954                        let exec_ctxt = exec_ctxt;
955
956                        let result = exec_module(
957                            &exec_ctxt,
958                            &repr,
959                            module_id,
960                            &module_path,
961                            &mut exec_state,
962                            source_range,
963                        )
964                        .await;
965
966                        results_tx
967                            .send((module_id, module_path, result))
968                            .await
969                            .unwrap_or_default();
970                    });
971                }
972            }
973
974            drop(results_tx);
975
976            while let Some((module_id, _, result)) = results_rx.recv().await {
977                match result {
978                    Ok(new_repr) => {
979                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
980
981                        match &mut repr {
982                            ModuleRepr::Kcl(_, cache) => {
983                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
984                                    unreachable!();
985                                };
986                                *cache = session_data;
987                            }
988                            ModuleRepr::Foreign(_, cache) => {
989                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
990                                    unreachable!();
991                                };
992                                *cache = session_data;
993                            }
994                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
995                        }
996
997                        exec_state.global.module_infos[&module_id].restore_repr(repr);
998                    }
999                    Err(e) => {
1000                        let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
1001                            .global
1002                            .path_to_source_id
1003                            .iter()
1004                            .map(|(k, v)| ((*v), k.clone()))
1005                            .collect();
1006
1007                        return Err(KclErrorWithOutputs::new(
1008                            e,
1009                            exec_state.errors().to_vec(),
1010                            #[cfg(feature = "artifact-graph")]
1011                            exec_state.global.operations.clone(),
1012                            #[cfg(feature = "artifact-graph")]
1013                            exec_state.global.artifact_commands.clone(),
1014                            #[cfg(feature = "artifact-graph")]
1015                            exec_state.global.artifact_graph.clone(),
1016                            module_id_to_module_path,
1017                            exec_state.global.id_to_source.clone(),
1018                            default_planes,
1019                        ));
1020                    }
1021                }
1022            }
1023        }
1024
1025        self.inner_run(program, cached_body_items, exec_state, preserve_mem)
1026            .await
1027    }
1028
1029    async fn get_universe(
1032        &self,
1033        program: &crate::Program,
1034        exec_state: &mut ExecState,
1035    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1036        exec_state.add_root_module_contents(program);
1037
1038        let mut universe = std::collections::HashMap::new();
1039
1040        let default_planes = self.engine.get_default_planes().read().await.clone();
1041
1042        let root_imports = crate::walk::import_universe(
1043            self,
1044            &ModuleRepr::Kcl(program.ast.clone(), None),
1045            &mut universe,
1046            exec_state,
1047        )
1048        .await
1049        .map_err(|err| {
1050            println!("Error: {err:?}");
1051            let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
1052                .global
1053                .path_to_source_id
1054                .iter()
1055                .map(|(k, v)| ((*v), k.clone()))
1056                .collect();
1057
1058            KclErrorWithOutputs::new(
1059                err,
1060                exec_state.errors().to_vec(),
1061                #[cfg(feature = "artifact-graph")]
1062                exec_state.global.operations.clone(),
1063                #[cfg(feature = "artifact-graph")]
1064                exec_state.global.artifact_commands.clone(),
1065                #[cfg(feature = "artifact-graph")]
1066                exec_state.global.artifact_graph.clone(),
1067                module_id_to_module_path,
1068                exec_state.global.id_to_source.clone(),
1069                default_planes,
1070            )
1071        })?;
1072
1073        Ok((universe, root_imports))
1074    }
1075
1076    async fn inner_run(
1079        &self,
1080        program: &crate::Program,
1081        cached_body_items: usize,
1082        exec_state: &mut ExecState,
1083        preserve_mem: bool,
1084    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1085        let _stats = crate::log::LogPerfStats::new("Interpretation");
1086
1087        self.engine
1089            .reapply_settings(&self.settings, Default::default(), exec_state.id_generator())
1090            .await
1091            .map_err(KclErrorWithOutputs::no_outputs)?;
1092
1093        let default_planes = self.engine.get_default_planes().read().await.clone();
1094        let result = self
1095            .execute_and_build_graph(&program.ast, cached_body_items, exec_state, preserve_mem)
1096            .await;
1097
1098        crate::log::log(format!(
1099            "Post interpretation KCL memory stats: {:#?}",
1100            exec_state.stack().memory.stats
1101        ));
1102        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1103
1104        let env_ref = result.map_err(|e| {
1105            let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
1106                .global
1107                .path_to_source_id
1108                .iter()
1109                .map(|(k, v)| ((*v), k.clone()))
1110                .collect();
1111
1112            KclErrorWithOutputs::new(
1113                e,
1114                exec_state.errors().to_vec(),
1115                #[cfg(feature = "artifact-graph")]
1116                exec_state.global.operations.clone(),
1117                #[cfg(feature = "artifact-graph")]
1118                exec_state.global.artifact_commands.clone(),
1119                #[cfg(feature = "artifact-graph")]
1120                exec_state.global.artifact_graph.clone(),
1121                module_id_to_module_path,
1122                exec_state.global.id_to_source.clone(),
1123                default_planes.clone(),
1124            )
1125        })?;
1126
1127        if !self.is_mock() {
1128            let mut mem = exec_state.stack().deep_clone();
1129            mem.restore_env(env_ref);
1130            cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
1131        }
1132        let session_data = self.engine.get_session_data().await;
1133
1134        Ok((env_ref, session_data))
1135    }
1136
1137    async fn execute_and_build_graph(
1140        &self,
1141        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1142        #[cfg_attr(not(feature = "artifact-graph"), expect(unused))] cached_body_items: usize,
1143        exec_state: &mut ExecState,
1144        preserve_mem: bool,
1145    ) -> Result<EnvironmentRef, KclError> {
1146        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1150            .await?;
1151
1152        let exec_result = self
1153            .exec_module_body(
1154                program,
1155                exec_state,
1156                preserve_mem,
1157                ModuleId::default(),
1158                &ModulePath::Main,
1159            )
1160            .await;
1161
1162        self.engine.ensure_async_commands_completed().await?;
1164
1165        self.engine.clear_queues().await;
1168
1169        #[cfg(feature = "artifact-graph")]
1170        {
1171            let new_commands = self.engine.take_artifact_commands().await;
1172            let new_responses = self.engine.take_responses().await;
1173            let initial_graph = exec_state.global.artifact_graph.clone();
1174
1175            let graph_result = build_artifact_graph(
1177                &new_commands,
1178                &new_responses,
1179                program,
1180                cached_body_items,
1181                &mut exec_state.global.artifacts,
1182                initial_graph,
1183            );
1184            exec_state.global.artifact_commands.extend(new_commands);
1187            exec_state.global.artifact_responses.extend(new_responses);
1188
1189            match graph_result {
1190                Ok(artifact_graph) => {
1191                    exec_state.global.artifact_graph = artifact_graph;
1192                    exec_result.map(|(_, env_ref, _)| env_ref)
1193                }
1194                Err(err) => {
1195                    exec_result.and(Err(err))
1197                }
1198            }
1199        }
1200        #[cfg(not(feature = "artifact-graph"))]
1201        {
1202            exec_result.map(|(_, env_ref, _)| env_ref)
1203        }
1204    }
1205
1206    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1210        if exec_state.stack().memory.requires_std() {
1211            let id = self
1212                .open_module(
1213                    &ImportPath::Std {
1214                        path: vec!["std".to_owned(), "prelude".to_owned()],
1215                    },
1216                    &[],
1217                    exec_state,
1218                    source_range,
1219                )
1220                .await?;
1221            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1222
1223            exec_state.mut_stack().memory.set_std(module_memory);
1224        }
1225
1226        Ok(())
1227    }
1228
1229    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1231        self.engine
1233            .send_modeling_cmd(
1234                uuid::Uuid::new_v4(),
1235                crate::execution::SourceRange::default(),
1236                &ModelingCmd::from(mcmd::ZoomToFit {
1237                    object_ids: Default::default(),
1238                    animated: false,
1239                    padding: 0.1,
1240                }),
1241            )
1242            .await
1243            .map_err(KclErrorWithOutputs::no_outputs)?;
1244
1245        let resp = self
1247            .engine
1248            .send_modeling_cmd(
1249                uuid::Uuid::new_v4(),
1250                crate::execution::SourceRange::default(),
1251                &ModelingCmd::from(mcmd::TakeSnapshot {
1252                    format: ImageFormat::Png,
1253                }),
1254            )
1255            .await
1256            .map_err(KclErrorWithOutputs::no_outputs)?;
1257
1258        let OkWebSocketResponseData::Modeling {
1259            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1260        } = resp
1261        else {
1262            return Err(ExecError::BadPng(format!(
1263                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1264            )));
1265        };
1266        Ok(contents)
1267    }
1268
1269    pub async fn export(
1271        &self,
1272        format: kittycad_modeling_cmds::format::OutputFormat3d,
1273    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1274        let resp = self
1275            .engine
1276            .send_modeling_cmd(
1277                uuid::Uuid::new_v4(),
1278                crate::SourceRange::default(),
1279                &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
1280                    entity_ids: vec![],
1281                    format,
1282                }),
1283            )
1284            .await?;
1285
1286        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1287            return Err(KclError::Internal(crate::errors::KclErrorDetails {
1288                message: format!("Expected Export response, got {resp:?}",),
1289                source_ranges: vec![SourceRange::default()],
1290            }));
1291        };
1292
1293        Ok(files)
1294    }
1295
1296    pub async fn export_step(
1298        &self,
1299        deterministic_time: bool,
1300    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1301        let files = self
1302            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1303                kittycad_modeling_cmds::format::step::export::Options {
1304                    coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1305                    created: if deterministic_time {
1306                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1307                            KclError::Internal(crate::errors::KclErrorDetails {
1308                                message: format!("Failed to parse date: {}", e),
1309                                source_ranges: vec![SourceRange::default()],
1310                            })
1311                        })?)
1312                    } else {
1313                        None
1314                    },
1315                },
1316            ))
1317            .await?;
1318
1319        Ok(files)
1320    }
1321
1322    pub async fn close(&self) {
1323        self.engine.close().await;
1324    }
1325}
1326
1327#[cfg(test)]
1328pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1329    parse_execute_with_project_dir(code, None).await
1330}
1331
1332#[cfg(test)]
1333pub(crate) async fn parse_execute_with_project_dir(
1334    code: &str,
1335    project_directory: Option<TypedPath>,
1336) -> Result<ExecTestResults, KclError> {
1337    let program = crate::Program::parse_no_errs(code)?;
1338
1339    let exec_ctxt = ExecutorContext {
1340        engine: Arc::new(Box::new(
1341            crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
1342                KclError::Internal(crate::errors::KclErrorDetails {
1343                    message: format!("Failed to create mock engine connection: {}", err),
1344                    source_ranges: vec![SourceRange::default()],
1345                })
1346            })?,
1347        )),
1348        fs: Arc::new(crate::fs::FileManager::new()),
1349        stdlib: Arc::new(crate::std::StdLib::new()),
1350        settings: ExecutorSettings {
1351            project_directory,
1352            ..Default::default()
1353        },
1354        context_type: ContextType::Mock,
1355    };
1356    let mut exec_state = ExecState::new(&exec_ctxt);
1357    let result = exec_ctxt.run(&program, &mut exec_state).await?;
1358
1359    Ok(ExecTestResults {
1360        program,
1361        mem_env: result.0,
1362        exec_ctxt,
1363        exec_state,
1364    })
1365}
1366
1367#[cfg(test)]
1368#[derive(Debug)]
1369pub(crate) struct ExecTestResults {
1370    program: crate::Program,
1371    mem_env: EnvironmentRef,
1372    exec_ctxt: ExecutorContext,
1373    exec_state: ExecState,
1374}
1375
1376#[cfg(test)]
1377mod tests {
1378    use pretty_assertions::assert_eq;
1379
1380    use super::*;
1381    use crate::{errors::KclErrorDetails, execution::memory::Stack, ModuleId};
1382
1383    #[track_caller]
1385    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1386        memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1387    }
1388
1389    #[tokio::test(flavor = "multi_thread")]
1390    async fn test_execute_warn() {
1391        let text = "@blah";
1392        let result = parse_execute(text).await.unwrap();
1393        let errs = result.exec_state.errors();
1394        assert_eq!(errs.len(), 1);
1395        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1396        assert!(
1397            errs[0].message.contains("Unknown annotation"),
1398            "unexpected warning message: {}",
1399            errs[0].message
1400        );
1401    }
1402
1403    #[tokio::test(flavor = "multi_thread")]
1404    async fn test_execute_fn_definitions() {
1405        let ast = r#"fn def(@x) {
1406  return x
1407}
1408fn ghi(@x) {
1409  return x
1410}
1411fn jkl(@x) {
1412  return x
1413}
1414fn hmm(@x) {
1415  return x
1416}
1417
1418yo = 5 + 6
1419
1420abc = 3
1421identifierGuy = 5
1422part001 = startSketchOn(XY)
1423|> startProfile(at = [-1.2, 4.83])
1424|> line(end = [2.8, 0])
1425|> angledLine(angle = 100 + 100, length = 3.01)
1426|> angledLine(angle = abc, length = 3.02)
1427|> angledLine(angle = def(yo), length = 3.03)
1428|> angledLine(angle = ghi(2), length = 3.04)
1429|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1430|> close()
1431yo2 = hmm([identifierGuy + 5])"#;
1432
1433        parse_execute(ast).await.unwrap();
1434    }
1435
1436    #[tokio::test(flavor = "multi_thread")]
1437    async fn test_execute_with_pipe_substitutions_unary() {
1438        let ast = r#"myVar = 3
1439part001 = startSketchOn(XY)
1440  |> startProfile(at = [0, 0])
1441  |> line(end = [3, 4], tag = $seg01)
1442  |> line(end = [
1443  min([segLen(seg01), myVar]),
1444  -legLen(hypotenuse = segLen(seg01), leg = myVar)
1445])
1446"#;
1447
1448        parse_execute(ast).await.unwrap();
1449    }
1450
1451    #[tokio::test(flavor = "multi_thread")]
1452    async fn test_execute_with_pipe_substitutions() {
1453        let ast = r#"myVar = 3
1454part001 = startSketchOn(XY)
1455  |> startProfile(at = [0, 0])
1456  |> line(end = [3, 4], tag = $seg01)
1457  |> line(end = [
1458  min([segLen(seg01), myVar]),
1459  legLen(hypotenuse = segLen(seg01), leg = myVar)
1460])
1461"#;
1462
1463        parse_execute(ast).await.unwrap();
1464    }
1465
1466    #[tokio::test(flavor = "multi_thread")]
1467    async fn test_execute_with_inline_comment() {
1468        let ast = r#"baseThick = 1
1469armAngle = 60
1470
1471baseThickHalf = baseThick / 2
1472halfArmAngle = armAngle / 2
1473
1474arrExpShouldNotBeIncluded = [1, 2, 3]
1475objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1476
1477part001 = startSketchOn(XY)
1478  |> startProfile(at = [0, 0])
1479  |> yLine(endAbsolute = 1)
1480  |> xLine(length = 3.84) // selection-range-7ish-before-this
1481
1482variableBelowShouldNotBeIncluded = 3
1483"#;
1484
1485        parse_execute(ast).await.unwrap();
1486    }
1487
1488    #[tokio::test(flavor = "multi_thread")]
1489    async fn test_execute_with_function_literal_in_pipe() {
1490        let ast = r#"w = 20
1491l = 8
1492h = 10
1493
1494fn thing() {
1495  return -8
1496}
1497
1498firstExtrude = startSketchOn(XY)
1499  |> startProfile(at = [0,0])
1500  |> line(end = [0, l])
1501  |> line(end = [w, 0])
1502  |> line(end = [0, thing()])
1503  |> close()
1504  |> extrude(length = h)"#;
1505
1506        parse_execute(ast).await.unwrap();
1507    }
1508
1509    #[tokio::test(flavor = "multi_thread")]
1510    async fn test_execute_with_function_unary_in_pipe() {
1511        let ast = r#"w = 20
1512l = 8
1513h = 10
1514
1515fn thing(@x) {
1516  return -x
1517}
1518
1519firstExtrude = startSketchOn(XY)
1520  |> startProfile(at = [0,0])
1521  |> line(end = [0, l])
1522  |> line(end = [w, 0])
1523  |> line(end = [0, thing(8)])
1524  |> close()
1525  |> extrude(length = h)"#;
1526
1527        parse_execute(ast).await.unwrap();
1528    }
1529
1530    #[tokio::test(flavor = "multi_thread")]
1531    async fn test_execute_with_function_array_in_pipe() {
1532        let ast = r#"w = 20
1533l = 8
1534h = 10
1535
1536fn thing(@x) {
1537  return [0, -x]
1538}
1539
1540firstExtrude = startSketchOn(XY)
1541  |> startProfile(at = [0,0])
1542  |> line(end = [0, l])
1543  |> line(end = [w, 0])
1544  |> line(end = thing(8))
1545  |> close()
1546  |> extrude(length = h)"#;
1547
1548        parse_execute(ast).await.unwrap();
1549    }
1550
1551    #[tokio::test(flavor = "multi_thread")]
1552    async fn test_execute_with_function_call_in_pipe() {
1553        let ast = r#"w = 20
1554l = 8
1555h = 10
1556
1557fn other_thing(@y) {
1558  return -y
1559}
1560
1561fn thing(@x) {
1562  return other_thing(x)
1563}
1564
1565firstExtrude = startSketchOn(XY)
1566  |> startProfile(at = [0,0])
1567  |> line(end = [0, l])
1568  |> line(end = [w, 0])
1569  |> line(end = [0, thing(8)])
1570  |> close()
1571  |> extrude(length = h)"#;
1572
1573        parse_execute(ast).await.unwrap();
1574    }
1575
1576    #[tokio::test(flavor = "multi_thread")]
1577    async fn test_execute_with_function_sketch() {
1578        let ast = r#"fn box(h, l, w) {
1579 myBox = startSketchOn(XY)
1580    |> startProfile(at = [0,0])
1581    |> line(end = [0, l])
1582    |> line(end = [w, 0])
1583    |> line(end = [0, -l])
1584    |> close()
1585    |> extrude(length = h)
1586
1587  return myBox
1588}
1589
1590fnBox = box(h = 3, l = 6, w = 10)"#;
1591
1592        parse_execute(ast).await.unwrap();
1593    }
1594
1595    #[tokio::test(flavor = "multi_thread")]
1596    async fn test_get_member_of_object_with_function_period() {
1597        let ast = r#"fn box(@obj) {
1598 myBox = startSketchOn(XY)
1599    |> startProfile(at = obj.start)
1600    |> line(end = [0, obj.l])
1601    |> line(end = [obj.w, 0])
1602    |> line(end = [0, -obj.l])
1603    |> close()
1604    |> extrude(length = obj.h)
1605
1606  return myBox
1607}
1608
1609thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1610"#;
1611        parse_execute(ast).await.unwrap();
1612    }
1613
1614    #[tokio::test(flavor = "multi_thread")]
1615    #[ignore] async fn test_object_member_starting_pipeline() {
1617        let ast = r#"
1618fn test2() {
1619  return {
1620    thing: startSketchOn(XY)
1621      |> startProfile(at = [0, 0])
1622      |> line(end = [0, 1])
1623      |> line(end = [1, 0])
1624      |> line(end = [0, -1])
1625      |> close()
1626  }
1627}
1628
1629x2 = test2()
1630
1631x2.thing
1632  |> extrude(length = 10)
1633"#;
1634        parse_execute(ast).await.unwrap();
1635    }
1636
1637    #[tokio::test(flavor = "multi_thread")]
1638    #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
1640        let ast = r#"fn box(obj) {
1641let myBox = startSketchOn(XY)
1642    |> startProfile(at = obj.start)
1643    |> line(end = [0, obj.l])
1644    |> line(end = [obj.w, 0])
1645    |> line(end = [0, -obj.l])
1646    |> close()
1647    |> extrude(length = obj.h)
1648
1649  return myBox
1650}
1651
1652for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1653  thisBox = box(var)
1654}"#;
1655
1656        parse_execute(ast).await.unwrap();
1657    }
1658
1659    #[tokio::test(flavor = "multi_thread")]
1660    #[ignore] async fn test_execute_with_function_sketch_loop_array() {
1662        let ast = r#"fn box(h, l, w, start) {
1663 myBox = startSketchOn(XY)
1664    |> startProfile(at = [0,0])
1665    |> line(end = [0, l])
1666    |> line(end = [w, 0])
1667    |> line(end = [0, -l])
1668    |> close()
1669    |> extrude(length = h)
1670
1671  return myBox
1672}
1673
1674
1675for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1676  const thisBox = box(var[0], var[1], var[2], var[3])
1677}"#;
1678
1679        parse_execute(ast).await.unwrap();
1680    }
1681
1682    #[tokio::test(flavor = "multi_thread")]
1683    async fn test_get_member_of_array_with_function() {
1684        let ast = r#"fn box(@arr) {
1685 myBox =startSketchOn(XY)
1686    |> startProfile(at = arr[0])
1687    |> line(end = [0, arr[1]])
1688    |> line(end = [arr[2], 0])
1689    |> line(end = [0, -arr[1]])
1690    |> close()
1691    |> extrude(length = arr[3])
1692
1693  return myBox
1694}
1695
1696thisBox = box([[0,0], 6, 10, 3])
1697
1698"#;
1699        parse_execute(ast).await.unwrap();
1700    }
1701
1702    #[tokio::test(flavor = "multi_thread")]
1703    async fn test_function_cannot_access_future_definitions() {
1704        let ast = r#"
1705fn returnX() {
1706  // x shouldn't be defined yet.
1707  return x
1708}
1709
1710x = 5
1711
1712answer = returnX()"#;
1713
1714        let result = parse_execute(ast).await;
1715        let err = result.unwrap_err();
1716        assert_eq!(err.message(), "`x` is not defined");
1717    }
1718
1719    #[tokio::test(flavor = "multi_thread")]
1720    async fn test_override_prelude() {
1721        let text = "PI = 3.0";
1722        let result = parse_execute(text).await.unwrap();
1723        let errs = result.exec_state.errors();
1724        assert!(errs.is_empty());
1725    }
1726
1727    #[tokio::test(flavor = "multi_thread")]
1728    async fn type_aliases() {
1729        let text = r#"type MyTy = [number; 2]
1730fn foo(@x: MyTy) {
1731    return x[0]
1732}
1733
1734foo([0, 1])
1735
1736type Other = MyTy | Helix
1737"#;
1738        let result = parse_execute(text).await.unwrap();
1739        let errs = result.exec_state.errors();
1740        assert!(errs.is_empty());
1741    }
1742
1743    #[tokio::test(flavor = "multi_thread")]
1744    async fn test_cannot_shebang_in_fn() {
1745        let ast = r#"
1746fn foo() {
1747  #!hello
1748  return true
1749}
1750
1751foo
1752"#;
1753
1754        let result = parse_execute(ast).await;
1755        let err = result.unwrap_err();
1756        assert_eq!(
1757            err,
1758            KclError::Syntax(KclErrorDetails {
1759                message: "Unexpected token: #".to_owned(),
1760                source_ranges: vec![SourceRange::new(14, 15, ModuleId::default())],
1761            }),
1762        );
1763    }
1764
1765    #[tokio::test(flavor = "multi_thread")]
1766    async fn test_pattern_transform_function_cannot_access_future_definitions() {
1767        let ast = r#"
1768fn transform(@replicaId) {
1769  // x shouldn't be defined yet.
1770  scale = x
1771  return {
1772    translate = [0, 0, replicaId * 10],
1773    scale = [scale, 1, 0],
1774  }
1775}
1776
1777fn layer() {
1778  return startSketchOn(XY)
1779    |> circle( center= [0, 0], radius= 1, tag = $tag1)
1780    |> extrude(length = 10)
1781}
1782
1783x = 5
1784
1785// The 10 layers are replicas of each other, with a transform applied to each.
1786shape = layer() |> patternTransform(instances = 10, transform = transform)
1787"#;
1788
1789        let result = parse_execute(ast).await;
1790        let err = result.unwrap_err();
1791        assert_eq!(err.message(), "`x` is not defined",);
1792    }
1793
1794    #[tokio::test(flavor = "multi_thread")]
1797    async fn test_math_execute_with_functions() {
1798        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
1799        let result = parse_execute(ast).await.unwrap();
1800        assert_eq!(
1801            5.0,
1802            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1803                .as_f64()
1804                .unwrap()
1805        );
1806    }
1807
1808    #[tokio::test(flavor = "multi_thread")]
1809    async fn test_math_execute() {
1810        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
1811        let result = parse_execute(ast).await.unwrap();
1812        assert_eq!(
1813            7.4,
1814            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1815                .as_f64()
1816                .unwrap()
1817        );
1818    }
1819
1820    #[tokio::test(flavor = "multi_thread")]
1821    async fn test_math_execute_start_negative() {
1822        let ast = r#"myVar = -5 + 6"#;
1823        let result = parse_execute(ast).await.unwrap();
1824        assert_eq!(
1825            1.0,
1826            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1827                .as_f64()
1828                .unwrap()
1829        );
1830    }
1831
1832    #[tokio::test(flavor = "multi_thread")]
1833    async fn test_math_execute_with_pi() {
1834        let ast = r#"myVar = PI * 2"#;
1835        let result = parse_execute(ast).await.unwrap();
1836        assert_eq!(
1837            std::f64::consts::TAU,
1838            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1839                .as_f64()
1840                .unwrap()
1841        );
1842    }
1843
1844    #[tokio::test(flavor = "multi_thread")]
1845    async fn test_math_define_decimal_without_leading_zero() {
1846        let ast = r#"thing = .4 + 7"#;
1847        let result = parse_execute(ast).await.unwrap();
1848        assert_eq!(
1849            7.4,
1850            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
1851                .as_f64()
1852                .unwrap()
1853        );
1854    }
1855
1856    #[tokio::test(flavor = "multi_thread")]
1857    async fn test_zero_param_fn() {
1858        let ast = r#"sigmaAllow = 35000 // psi
1859leg1 = 5 // inches
1860leg2 = 8 // inches
1861fn thickness() { return 0.56 }
1862
1863bracket = startSketchOn(XY)
1864  |> startProfile(at = [0,0])
1865  |> line(end = [0, leg1])
1866  |> line(end = [leg2, 0])
1867  |> line(end = [0, -thickness()])
1868  |> line(end = [-leg2 + thickness(), 0])
1869"#;
1870        parse_execute(ast).await.unwrap();
1871    }
1872
1873    #[tokio::test(flavor = "multi_thread")]
1874    async fn test_unary_operator_not_succeeds() {
1875        let ast = r#"
1876fn returnTrue() { return !false }
1877t = true
1878f = false
1879notTrue = !t
1880notFalse = !f
1881c = !!true
1882d = !returnTrue()
1883
1884assertIs(!false, error = "expected to pass")
1885
1886fn check(x) {
1887  assertIs(!x, error = "expected argument to be false")
1888  return true
1889}
1890check(x = false)
1891"#;
1892        let result = parse_execute(ast).await.unwrap();
1893        assert_eq!(
1894            false,
1895            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
1896                .as_bool()
1897                .unwrap()
1898        );
1899        assert_eq!(
1900            true,
1901            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
1902                .as_bool()
1903                .unwrap()
1904        );
1905        assert_eq!(
1906            true,
1907            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
1908                .as_bool()
1909                .unwrap()
1910        );
1911        assert_eq!(
1912            false,
1913            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
1914                .as_bool()
1915                .unwrap()
1916        );
1917    }
1918
1919    #[tokio::test(flavor = "multi_thread")]
1920    async fn test_unary_operator_not_on_non_bool_fails() {
1921        let code1 = r#"
1922// Yup, this is null.
1923myNull = 0 / 0
1924notNull = !myNull
1925"#;
1926        assert_eq!(
1927            parse_execute(code1).await.unwrap_err().message(),
1928            "Cannot apply unary operator ! to non-boolean value: number(default units)",
1929        );
1930
1931        let code2 = "notZero = !0";
1932        assert_eq!(
1933            parse_execute(code2).await.unwrap_err().message(),
1934            "Cannot apply unary operator ! to non-boolean value: number(default units)",
1935        );
1936
1937        let code3 = r#"
1938notEmptyString = !""
1939"#;
1940        assert_eq!(
1941            parse_execute(code3).await.unwrap_err().message(),
1942            "Cannot apply unary operator ! to non-boolean value: string",
1943        );
1944
1945        let code4 = r#"
1946obj = { a = 1 }
1947notMember = !obj.a
1948"#;
1949        assert_eq!(
1950            parse_execute(code4).await.unwrap_err().message(),
1951            "Cannot apply unary operator ! to non-boolean value: number(default units)",
1952        );
1953
1954        let code5 = "
1955a = []
1956notArray = !a";
1957        assert_eq!(
1958            parse_execute(code5).await.unwrap_err().message(),
1959            "Cannot apply unary operator ! to non-boolean value: [any; 0]",
1960        );
1961
1962        let code6 = "
1963x = {}
1964notObject = !x";
1965        assert_eq!(
1966            parse_execute(code6).await.unwrap_err().message(),
1967            "Cannot apply unary operator ! to non-boolean value: {  }",
1968        );
1969
1970        let code7 = "
1971fn x() { return 1 }
1972notFunction = !x";
1973        let fn_err = parse_execute(code7).await.unwrap_err();
1974        assert!(
1977            fn_err
1978                .message()
1979                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
1980            "Actual error: {:?}",
1981            fn_err
1982        );
1983
1984        let code8 = "
1985myTagDeclarator = $myTag
1986notTagDeclarator = !myTagDeclarator";
1987        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
1988        assert!(
1991            tag_declarator_err
1992                .message()
1993                .starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
1994            "Actual error: {:?}",
1995            tag_declarator_err
1996        );
1997
1998        let code9 = "
1999myTagDeclarator = $myTag
2000notTagIdentifier = !myTag";
2001        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2002        assert!(
2005            tag_identifier_err
2006                .message()
2007                .starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
2008            "Actual error: {:?}",
2009            tag_identifier_err
2010        );
2011
2012        let code10 = "notPipe = !(1 |> 2)";
2013        assert_eq!(
2014            parse_execute(code10).await.unwrap_err(),
2017            KclError::Syntax(KclErrorDetails {
2018                message: "Unexpected token: !".to_owned(),
2019                source_ranges: vec![SourceRange::new(10, 11, ModuleId::default())],
2020            })
2021        );
2022
2023        let code11 = "
2024fn identity(x) { return x }
2025notPipeSub = 1 |> identity(!%))";
2026        assert_eq!(
2027            parse_execute(code11).await.unwrap_err(),
2030            KclError::Syntax(KclErrorDetails {
2031                message: "Unexpected token: |>".to_owned(),
2032                source_ranges: vec![SourceRange::new(44, 46, ModuleId::default())],
2033            })
2034        );
2035
2036        }
2040
2041    #[tokio::test(flavor = "multi_thread")]
2042    async fn test_math_negative_variable_in_binary_expression() {
2043        let ast = r#"sigmaAllow = 35000 // psi
2044width = 1 // inch
2045
2046p = 150 // lbs
2047distance = 6 // inches
2048FOS = 2
2049
2050leg1 = 5 // inches
2051leg2 = 8 // inches
2052
2053thickness_squared = distance * p * FOS * 6 / sigmaAllow
2054thickness = 0.56 // inches. App does not support square root function yet
2055
2056bracket = startSketchOn(XY)
2057  |> startProfile(at = [0,0])
2058  |> line(end = [0, leg1])
2059  |> line(end = [leg2, 0])
2060  |> line(end = [0, -thickness])
2061  |> line(end = [-leg2 + thickness, 0])
2062"#;
2063        parse_execute(ast).await.unwrap();
2064    }
2065
2066    #[tokio::test(flavor = "multi_thread")]
2067    async fn test_execute_function_no_return() {
2068        let ast = r#"fn test(@origin) {
2069  origin
2070}
2071
2072test([0, 0])
2073"#;
2074        let result = parse_execute(ast).await;
2075        assert!(result.is_err());
2076        assert!(result.unwrap_err().to_string().contains("undefined"),);
2077    }
2078
2079    #[tokio::test(flavor = "multi_thread")]
2080    async fn test_math_doubly_nested_parens() {
2081        let ast = r#"sigmaAllow = 35000 // psi
2082width = 4 // inch
2083p = 150 // Force on shelf - lbs
2084distance = 6 // inches
2085FOS = 2
2086leg1 = 5 // inches
2087leg2 = 8 // inches
2088thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2089thickness = 0.32 // inches. App does not support square root function yet
2090bracket = startSketchOn(XY)
2091  |> startProfile(at = [0,0])
2092    |> line(end = [0, leg1])
2093  |> line(end = [leg2, 0])
2094  |> line(end = [0, -thickness])
2095  |> line(end = [-1 * leg2 + thickness, 0])
2096  |> line(end = [0, -1 * leg1 + thickness])
2097  |> close()
2098  |> extrude(length = width)
2099"#;
2100        parse_execute(ast).await.unwrap();
2101    }
2102
2103    #[tokio::test(flavor = "multi_thread")]
2104    async fn test_math_nested_parens_one_less() {
2105        let ast = r#" sigmaAllow = 35000 // psi
2106width = 4 // inch
2107p = 150 // Force on shelf - lbs
2108distance = 6 // inches
2109FOS = 2
2110leg1 = 5 // inches
2111leg2 = 8 // inches
2112thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2113thickness = 0.32 // inches. App does not support square root function yet
2114bracket = startSketchOn(XY)
2115  |> startProfile(at = [0,0])
2116    |> line(end = [0, leg1])
2117  |> line(end = [leg2, 0])
2118  |> line(end = [0, -thickness])
2119  |> line(end = [-1 * leg2 + thickness, 0])
2120  |> line(end = [0, -1 * leg1 + thickness])
2121  |> close()
2122  |> extrude(length = width)
2123"#;
2124        parse_execute(ast).await.unwrap();
2125    }
2126
2127    #[tokio::test(flavor = "multi_thread")]
2128    async fn test_fn_as_operand() {
2129        let ast = r#"fn f() { return 1 }
2130x = f()
2131y = x + 1
2132z = f() + 1
2133w = f() + f()
2134"#;
2135        parse_execute(ast).await.unwrap();
2136    }
2137
2138    #[tokio::test(flavor = "multi_thread")]
2139    async fn kcl_test_ids_stable_between_executions() {
2140        let code = r#"sketch001 = startSketchOn(XZ)
2141|> startProfile(at = [61.74, 206.13])
2142|> xLine(length = 305.11, tag = $seg01)
2143|> yLine(length = -291.85)
2144|> xLine(length = -segLen(seg01))
2145|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2146|> close()
2147|> extrude(length = 40.14)
2148|> shell(
2149    thickness = 3.14,
2150    faces = [seg01]
2151)
2152"#;
2153
2154        let ctx = crate::test_server::new_context(true, None).await.unwrap();
2155        let old_program = crate::Program::parse_no_errs(code).unwrap();
2156
2157        if let Err(err) = ctx.run_with_caching(old_program).await {
2159            let report = err.into_miette_report_with_outputs(code).unwrap();
2160            let report = miette::Report::new(report);
2161            panic!("Error executing program: {:?}", report);
2162        }
2163
2164        let id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
2166
2167        let code = r#"sketch001 = startSketchOn(XZ)
2168|> startProfile(at = [62.74, 206.13])
2169|> xLine(length = 305.11, tag = $seg01)
2170|> yLine(length = -291.85)
2171|> xLine(length = -segLen(seg01))
2172|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2173|> close()
2174|> extrude(length = 40.14)
2175|> shell(
2176    faces = [seg01],
2177    thickness = 3.14,
2178)
2179"#;
2180
2181        let program = crate::Program::parse_no_errs(code).unwrap();
2183        ctx.run_with_caching(program).await.unwrap();
2185
2186        let new_id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
2187
2188        assert_eq!(id_generator, new_id_generator);
2189    }
2190
2191    #[tokio::test(flavor = "multi_thread")]
2192    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2193        let code = r#"sketch001 = startSketchOn(XZ)
2194|> startProfile(at = [61.74, 206.13])
2195|> xLine(length = 305.11, tag = $seg01)
2196|> yLine(length = -291.85)
2197|> xLine(length = -segLen(seg01))
2198|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2199|> close()
2200|> extrude(length = 40.14)
2201|> shell(
2202    thickness = 3.14,
2203    faces = [seg01]
2204)
2205"#;
2206
2207        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2208        let old_program = crate::Program::parse_no_errs(code).unwrap();
2209
2210        ctx.run_with_caching(old_program.clone()).await.unwrap();
2212
2213        let settings_state = cache::read_old_ast().await.unwrap().settings;
2214
2215        assert_eq!(settings_state, ctx.settings);
2217
2218        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2220
2221        ctx.run_with_caching(old_program.clone()).await.unwrap();
2223
2224        let settings_state = cache::read_old_ast().await.unwrap().settings;
2225
2226        assert_eq!(settings_state, ctx.settings);
2228
2229        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2231
2232        ctx.run_with_caching(old_program).await.unwrap();
2234
2235        let settings_state = cache::read_old_ast().await.unwrap().settings;
2236
2237        assert_eq!(settings_state, ctx.settings);
2239
2240        ctx.close().await;
2241    }
2242
2243    #[tokio::test(flavor = "multi_thread")]
2244    async fn mock_after_not_mock() {
2245        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2246        let program = crate::Program::parse_no_errs("x = 2").unwrap();
2247        let result = ctx.run_with_caching(program).await.unwrap();
2248        assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2249
2250        let ctx2 = ExecutorContext::new_mock(None).await;
2251        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2252        let result = ctx2.run_mock(program2, true).await.unwrap();
2253        assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2254
2255        ctx.close().await;
2256        ctx2.close().await;
2257    }
2258
2259    #[tokio::test(flavor = "multi_thread")]
2260    async fn read_tag_version() {
2261        let ast = r#"fn bar(@t) {
2262  return startSketchOn(XY)
2263    |> startProfile(at = [0,0])
2264    |> angledLine(
2265        angle = -60,
2266        length = segLen(t),
2267    )
2268    |> line(end = [0, 0])
2269    |> close()
2270}
2271
2272sketch = startSketchOn(XY)
2273  |> startProfile(at = [0,0])
2274  |> line(end = [0, 10])
2275  |> line(end = [10, 0], tag = $tag0)
2276  |> line(end = [0, 0])
2277
2278fn foo() {
2279  // tag0 tags an edge
2280  return bar(tag0)
2281}
2282
2283solid = sketch |> extrude(length = 10)
2284// tag0 tags a face
2285sketch2 = startSketchOn(solid, face = tag0)
2286  |> startProfile(at = [0,0])
2287  |> line(end = [0, 1])
2288  |> line(end = [1, 0])
2289  |> line(end = [0, 0])
2290
2291foo() |> extrude(length = 1)
2292"#;
2293        parse_execute(ast).await.unwrap();
2294    }
2295}