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