kcl_lib/execution/
mod.rs

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