Skip to main content

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