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        /// Write the memory of an execution to the cache for reuse in mock
1404        /// execution.
1405        async fn write_old_memory(ctx: &ExecutorContext, exec_state: &ExecState, env_ref: EnvironmentRef) {
1406            if ctx.is_mock() {
1407                return;
1408            }
1409            let mut stack = exec_state.stack().deep_clone();
1410            stack.restore_env(env_ref);
1411            let state = cache::SketchModeState {
1412                stack,
1413                module_infos: exec_state.global.module_infos.clone(),
1414                #[cfg(feature = "artifact-graph")]
1415                scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1416                #[cfg(not(feature = "artifact-graph"))]
1417                scene_objects: Default::default(),
1418            };
1419            cache::write_old_memory(state).await;
1420        }
1421
1422        let env_ref = match result {
1423            Ok(env_ref) => env_ref,
1424            Err((err, env_ref)) => {
1425                // Preserve memory on execution failures so follow-up mock
1426                // execution can still reuse stable IDs before the error.
1427                if let Some(env_ref) = env_ref {
1428                    write_old_memory(self, exec_state, env_ref).await;
1429                }
1430                return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
1431            }
1432        };
1433
1434        write_old_memory(self, exec_state, env_ref).await;
1435
1436        let session_data = self.engine.get_session_data().await;
1437
1438        Ok((env_ref, session_data))
1439    }
1440
1441    /// Execute an AST's program and build auxiliary outputs like the artifact
1442    /// graph.
1443    async fn execute_and_build_graph(
1444        &self,
1445        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1446        exec_state: &mut ExecState,
1447        preserve_mem: PreserveMem,
1448    ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1449        // Don't early return!  We need to build other outputs regardless of
1450        // whether execution failed.
1451
1452        // Because of execution caching, we may start with operations from a
1453        // previous run.
1454        #[cfg(feature = "artifact-graph")]
1455        let start_op = exec_state.global.root_module_artifacts.operations.len();
1456
1457        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1458            .await
1459            .map_err(|e| (e, None))?;
1460
1461        let exec_result = self
1462            .exec_module_body(
1463                program,
1464                exec_state,
1465                preserve_mem,
1466                ModuleId::default(),
1467                &ModulePath::Main,
1468            )
1469            .await
1470            .map(
1471                |ModuleExecutionOutcome {
1472                     environment: env_ref,
1473                     artifacts: module_artifacts,
1474                     ..
1475                 }| {
1476                    // We need to extend because it may already have operations from
1477                    // imports.
1478                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1479                    env_ref
1480                },
1481            )
1482            .map_err(|(err, env_ref, module_artifacts)| {
1483                if let Some(module_artifacts) = module_artifacts {
1484                    // We need to extend because it may already have operations
1485                    // from imports.
1486                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1487                }
1488                (err, env_ref)
1489            });
1490
1491        #[cfg(feature = "artifact-graph")]
1492        {
1493            // Fill in NodePath for operations.
1494            let programs = &exec_state.build_program_lookup(program.clone());
1495            let cached_body_items = exec_state.global.artifacts.cached_body_items();
1496            for op in exec_state
1497                .global
1498                .root_module_artifacts
1499                .operations
1500                .iter_mut()
1501                .skip(start_op)
1502            {
1503                op.fill_node_paths(programs, cached_body_items);
1504            }
1505            for module in exec_state.global.module_infos.values_mut() {
1506                if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1507                    for op in &mut outcome.artifacts.operations {
1508                        op.fill_node_paths(programs, cached_body_items);
1509                    }
1510                }
1511            }
1512        }
1513
1514        // Ensure all the async commands completed.
1515        self.engine.ensure_async_commands_completed().await.map_err(|e| {
1516            match &exec_result {
1517                Ok(env_ref) => (e, Some(*env_ref)),
1518                // Prefer the execution error.
1519                Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1520            }
1521        })?;
1522
1523        // If we errored out and early-returned, there might be commands which haven't been executed
1524        // and should be dropped.
1525        self.engine.clear_queues().await;
1526
1527        match exec_state.build_artifact_graph(&self.engine, program).await {
1528            Ok(_) => exec_result,
1529            Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1530        }
1531    }
1532
1533    /// 'Import' std::prelude as the outermost scope.
1534    ///
1535    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
1536    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1537        if exec_state.stack().memory.requires_std() {
1538            #[cfg(feature = "artifact-graph")]
1539            let initial_ops = exec_state.mod_local.artifacts.operations.len();
1540
1541            let path = vec!["std".to_owned(), "prelude".to_owned()];
1542            let resolved_path = ModulePath::from_std_import_path(&path)?;
1543            let id = self
1544                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1545                .await?;
1546            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1547
1548            exec_state.mut_stack().memory.set_std(module_memory);
1549
1550            // Operations generated by the prelude are not useful, so clear them
1551            // out.
1552            //
1553            // TODO: Should we also clear them out of each module so that they
1554            // don't appear in test output?
1555            #[cfg(feature = "artifact-graph")]
1556            exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1557        }
1558
1559        Ok(())
1560    }
1561
1562    /// Get a snapshot of the current scene.
1563    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1564        // Zoom to fit.
1565        self.engine
1566            .send_modeling_cmd(
1567                uuid::Uuid::new_v4(),
1568                crate::execution::SourceRange::default(),
1569                &ModelingCmd::from(
1570                    mcmd::ZoomToFit::builder()
1571                        .object_ids(Default::default())
1572                        .animated(false)
1573                        .padding(0.1)
1574                        .build(),
1575                ),
1576            )
1577            .await
1578            .map_err(KclErrorWithOutputs::no_outputs)?;
1579
1580        // Send a snapshot request to the engine.
1581        let resp = self
1582            .engine
1583            .send_modeling_cmd(
1584                uuid::Uuid::new_v4(),
1585                crate::execution::SourceRange::default(),
1586                &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
1587            )
1588            .await
1589            .map_err(KclErrorWithOutputs::no_outputs)?;
1590
1591        let OkWebSocketResponseData::Modeling {
1592            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1593        } = resp
1594        else {
1595            return Err(ExecError::BadPng(format!(
1596                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1597            )));
1598        };
1599        Ok(contents)
1600    }
1601
1602    /// Export the current scene as a CAD file.
1603    pub async fn export(
1604        &self,
1605        format: kittycad_modeling_cmds::format::OutputFormat3d,
1606    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1607        let resp = self
1608            .engine
1609            .send_modeling_cmd(
1610                uuid::Uuid::new_v4(),
1611                crate::SourceRange::default(),
1612                &kittycad_modeling_cmds::ModelingCmd::Export(
1613                    kittycad_modeling_cmds::Export::builder()
1614                        .entity_ids(vec![])
1615                        .format(format)
1616                        .build(),
1617                ),
1618            )
1619            .await?;
1620
1621        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1622            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1623                format!("Expected Export response, got {resp:?}",),
1624                vec![SourceRange::default()],
1625            )));
1626        };
1627
1628        Ok(files)
1629    }
1630
1631    /// Export the current scene as a STEP file.
1632    pub async fn export_step(
1633        &self,
1634        deterministic_time: bool,
1635    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1636        let files = self
1637            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1638                kittycad_modeling_cmds::format::step::export::Options::builder()
1639                    .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
1640                    .maybe_created(if deterministic_time {
1641                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1642                            KclError::new_internal(crate::errors::KclErrorDetails::new(
1643                                format!("Failed to parse date: {e}"),
1644                                vec![SourceRange::default()],
1645                            ))
1646                        })?)
1647                    } else {
1648                        None
1649                    })
1650                    .build(),
1651            ))
1652            .await?;
1653
1654        Ok(files)
1655    }
1656
1657    pub async fn close(&self) {
1658        self.engine.close().await;
1659    }
1660}
1661
1662#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1663pub struct ArtifactId(Uuid);
1664
1665impl ArtifactId {
1666    pub fn new(uuid: Uuid) -> Self {
1667        Self(uuid)
1668    }
1669
1670    /// A placeholder artifact ID that will be filled in later.
1671    pub fn placeholder() -> Self {
1672        Self(Uuid::nil())
1673    }
1674}
1675
1676impl From<Uuid> for ArtifactId {
1677    fn from(uuid: Uuid) -> Self {
1678        Self::new(uuid)
1679    }
1680}
1681
1682impl From<&Uuid> for ArtifactId {
1683    fn from(uuid: &Uuid) -> Self {
1684        Self::new(*uuid)
1685    }
1686}
1687
1688impl From<ArtifactId> for Uuid {
1689    fn from(id: ArtifactId) -> Self {
1690        id.0
1691    }
1692}
1693
1694impl From<&ArtifactId> for Uuid {
1695    fn from(id: &ArtifactId) -> Self {
1696        id.0
1697    }
1698}
1699
1700impl From<ModelingCmdId> for ArtifactId {
1701    fn from(id: ModelingCmdId) -> Self {
1702        Self::new(*id.as_ref())
1703    }
1704}
1705
1706impl From<&ModelingCmdId> for ArtifactId {
1707    fn from(id: &ModelingCmdId) -> Self {
1708        Self::new(*id.as_ref())
1709    }
1710}
1711
1712#[cfg(test)]
1713pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1714    parse_execute_with_project_dir(code, None).await
1715}
1716
1717#[cfg(test)]
1718pub(crate) async fn parse_execute_with_project_dir(
1719    code: &str,
1720    project_directory: Option<TypedPath>,
1721) -> Result<ExecTestResults, KclError> {
1722    let program = crate::Program::parse_no_errs(code)?;
1723
1724    let exec_ctxt = ExecutorContext {
1725        engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
1726            |err| {
1727                KclError::new_internal(crate::errors::KclErrorDetails::new(
1728                    format!("Failed to create mock engine connection: {err}"),
1729                    vec![SourceRange::default()],
1730                ))
1731            },
1732        )?)),
1733        fs: Arc::new(crate::fs::FileManager::new()),
1734        settings: ExecutorSettings {
1735            project_directory,
1736            ..Default::default()
1737        },
1738        context_type: ContextType::Mock,
1739    };
1740    let mut exec_state = ExecState::new(&exec_ctxt);
1741    let result = exec_ctxt.run(&program, &mut exec_state).await?;
1742
1743    Ok(ExecTestResults {
1744        program,
1745        mem_env: result.0,
1746        exec_ctxt,
1747        exec_state,
1748    })
1749}
1750
1751#[cfg(test)]
1752#[derive(Debug)]
1753pub(crate) struct ExecTestResults {
1754    program: crate::Program,
1755    mem_env: EnvironmentRef,
1756    exec_ctxt: ExecutorContext,
1757    exec_state: ExecState,
1758}
1759
1760/// There are several places where we want to traverse a KCL program or find a symbol in it,
1761/// but because KCL modules can import each other, we need to traverse multiple programs.
1762/// This stores multiple programs, keyed by their module ID for quick access.
1763#[cfg(feature = "artifact-graph")]
1764pub struct ProgramLookup {
1765    programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
1766}
1767
1768#[cfg(feature = "artifact-graph")]
1769impl ProgramLookup {
1770    // TODO: Could this store a reference to KCL programs instead of owning them?
1771    // i.e. take &state::ModuleInfoMap instead?
1772    pub fn new(
1773        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1774        module_infos: state::ModuleInfoMap,
1775    ) -> Self {
1776        let mut programs = IndexMap::with_capacity(module_infos.len());
1777        for (id, info) in module_infos {
1778            if let ModuleRepr::Kcl(program, _) = info.repr {
1779                programs.insert(id, program);
1780            }
1781        }
1782        programs.insert(ModuleId::default(), current);
1783        Self { programs }
1784    }
1785
1786    pub fn program_for_module(
1787        &self,
1788        module_id: ModuleId,
1789    ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
1790        self.programs.get(&module_id)
1791    }
1792}
1793
1794#[cfg(test)]
1795mod tests {
1796    use pretty_assertions::assert_eq;
1797
1798    use super::*;
1799    use crate::{
1800        ModuleId,
1801        errors::{KclErrorDetails, Severity},
1802        exec::NumericType,
1803        execution::{memory::Stack, types::RuntimeType},
1804    };
1805
1806    /// Convenience function to get a JSON value from memory and unwrap.
1807    #[track_caller]
1808    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1809        memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1810    }
1811
1812    #[tokio::test(flavor = "multi_thread")]
1813    async fn test_execute_warn() {
1814        let text = "@blah";
1815        let result = parse_execute(text).await.unwrap();
1816        let errs = result.exec_state.errors();
1817        assert_eq!(errs.len(), 1);
1818        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1819        assert!(
1820            errs[0].message.contains("Unknown annotation"),
1821            "unexpected warning message: {}",
1822            errs[0].message
1823        );
1824    }
1825
1826    #[tokio::test(flavor = "multi_thread")]
1827    async fn test_execute_fn_definitions() {
1828        let ast = r#"fn def(@x) {
1829  return x
1830}
1831fn ghi(@x) {
1832  return x
1833}
1834fn jkl(@x) {
1835  return x
1836}
1837fn hmm(@x) {
1838  return x
1839}
1840
1841yo = 5 + 6
1842
1843abc = 3
1844identifierGuy = 5
1845part001 = startSketchOn(XY)
1846|> startProfile(at = [-1.2, 4.83])
1847|> line(end = [2.8, 0])
1848|> angledLine(angle = 100 + 100, length = 3.01)
1849|> angledLine(angle = abc, length = 3.02)
1850|> angledLine(angle = def(yo), length = 3.03)
1851|> angledLine(angle = ghi(2), length = 3.04)
1852|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1853|> close()
1854yo2 = hmm([identifierGuy + 5])"#;
1855
1856        parse_execute(ast).await.unwrap();
1857    }
1858
1859    #[tokio::test(flavor = "multi_thread")]
1860    async fn test_execute_with_pipe_substitutions_unary() {
1861        let ast = r#"myVar = 3
1862part001 = startSketchOn(XY)
1863  |> startProfile(at = [0, 0])
1864  |> line(end = [3, 4], tag = $seg01)
1865  |> line(end = [
1866  min([segLen(seg01), myVar]),
1867  -legLen(hypotenuse = segLen(seg01), leg = myVar)
1868])
1869"#;
1870
1871        parse_execute(ast).await.unwrap();
1872    }
1873
1874    #[tokio::test(flavor = "multi_thread")]
1875    async fn test_execute_with_pipe_substitutions() {
1876        let ast = r#"myVar = 3
1877part001 = startSketchOn(XY)
1878  |> startProfile(at = [0, 0])
1879  |> line(end = [3, 4], tag = $seg01)
1880  |> line(end = [
1881  min([segLen(seg01), myVar]),
1882  legLen(hypotenuse = segLen(seg01), leg = myVar)
1883])
1884"#;
1885
1886        parse_execute(ast).await.unwrap();
1887    }
1888
1889    #[tokio::test(flavor = "multi_thread")]
1890    async fn test_execute_with_inline_comment() {
1891        let ast = r#"baseThick = 1
1892armAngle = 60
1893
1894baseThickHalf = baseThick / 2
1895halfArmAngle = armAngle / 2
1896
1897arrExpShouldNotBeIncluded = [1, 2, 3]
1898objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1899
1900part001 = startSketchOn(XY)
1901  |> startProfile(at = [0, 0])
1902  |> yLine(endAbsolute = 1)
1903  |> xLine(length = 3.84) // selection-range-7ish-before-this
1904
1905variableBelowShouldNotBeIncluded = 3
1906"#;
1907
1908        parse_execute(ast).await.unwrap();
1909    }
1910
1911    #[tokio::test(flavor = "multi_thread")]
1912    async fn test_execute_with_function_literal_in_pipe() {
1913        let ast = r#"w = 20
1914l = 8
1915h = 10
1916
1917fn thing() {
1918  return -8
1919}
1920
1921firstExtrude = startSketchOn(XY)
1922  |> startProfile(at = [0,0])
1923  |> line(end = [0, l])
1924  |> line(end = [w, 0])
1925  |> line(end = [0, thing()])
1926  |> close()
1927  |> extrude(length = h)"#;
1928
1929        parse_execute(ast).await.unwrap();
1930    }
1931
1932    #[tokio::test(flavor = "multi_thread")]
1933    async fn test_execute_with_function_unary_in_pipe() {
1934        let ast = r#"w = 20
1935l = 8
1936h = 10
1937
1938fn thing(@x) {
1939  return -x
1940}
1941
1942firstExtrude = startSketchOn(XY)
1943  |> startProfile(at = [0,0])
1944  |> line(end = [0, l])
1945  |> line(end = [w, 0])
1946  |> line(end = [0, thing(8)])
1947  |> close()
1948  |> extrude(length = h)"#;
1949
1950        parse_execute(ast).await.unwrap();
1951    }
1952
1953    #[tokio::test(flavor = "multi_thread")]
1954    async fn test_execute_with_function_array_in_pipe() {
1955        let ast = r#"w = 20
1956l = 8
1957h = 10
1958
1959fn thing(@x) {
1960  return [0, -x]
1961}
1962
1963firstExtrude = startSketchOn(XY)
1964  |> startProfile(at = [0,0])
1965  |> line(end = [0, l])
1966  |> line(end = [w, 0])
1967  |> line(end = thing(8))
1968  |> close()
1969  |> extrude(length = h)"#;
1970
1971        parse_execute(ast).await.unwrap();
1972    }
1973
1974    #[tokio::test(flavor = "multi_thread")]
1975    async fn test_execute_with_function_call_in_pipe() {
1976        let ast = r#"w = 20
1977l = 8
1978h = 10
1979
1980fn other_thing(@y) {
1981  return -y
1982}
1983
1984fn thing(@x) {
1985  return other_thing(x)
1986}
1987
1988firstExtrude = startSketchOn(XY)
1989  |> startProfile(at = [0,0])
1990  |> line(end = [0, l])
1991  |> line(end = [w, 0])
1992  |> line(end = [0, thing(8)])
1993  |> close()
1994  |> extrude(length = h)"#;
1995
1996        parse_execute(ast).await.unwrap();
1997    }
1998
1999    #[tokio::test(flavor = "multi_thread")]
2000    async fn test_execute_with_function_sketch() {
2001        let ast = r#"fn box(h, l, w) {
2002 myBox = startSketchOn(XY)
2003    |> startProfile(at = [0,0])
2004    |> line(end = [0, l])
2005    |> line(end = [w, 0])
2006    |> line(end = [0, -l])
2007    |> close()
2008    |> extrude(length = h)
2009
2010  return myBox
2011}
2012
2013fnBox = box(h = 3, l = 6, w = 10)"#;
2014
2015        parse_execute(ast).await.unwrap();
2016    }
2017
2018    #[tokio::test(flavor = "multi_thread")]
2019    async fn test_get_member_of_object_with_function_period() {
2020        let ast = r#"fn box(@obj) {
2021 myBox = startSketchOn(XY)
2022    |> startProfile(at = obj.start)
2023    |> line(end = [0, obj.l])
2024    |> line(end = [obj.w, 0])
2025    |> line(end = [0, -obj.l])
2026    |> close()
2027    |> extrude(length = obj.h)
2028
2029  return myBox
2030}
2031
2032thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2033"#;
2034        parse_execute(ast).await.unwrap();
2035    }
2036
2037    #[tokio::test(flavor = "multi_thread")]
2038    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
2039    async fn test_object_member_starting_pipeline() {
2040        let ast = r#"
2041fn test2() {
2042  return {
2043    thing: startSketchOn(XY)
2044      |> startProfile(at = [0, 0])
2045      |> line(end = [0, 1])
2046      |> line(end = [1, 0])
2047      |> line(end = [0, -1])
2048      |> close()
2049  }
2050}
2051
2052x2 = test2()
2053
2054x2.thing
2055  |> extrude(length = 10)
2056"#;
2057        parse_execute(ast).await.unwrap();
2058    }
2059
2060    #[tokio::test(flavor = "multi_thread")]
2061    #[ignore] // ignore til we get loops
2062    async fn test_execute_with_function_sketch_loop_objects() {
2063        let ast = r#"fn box(obj) {
2064let myBox = startSketchOn(XY)
2065    |> startProfile(at = obj.start)
2066    |> line(end = [0, obj.l])
2067    |> line(end = [obj.w, 0])
2068    |> line(end = [0, -obj.l])
2069    |> close()
2070    |> extrude(length = obj.h)
2071
2072  return myBox
2073}
2074
2075for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2076  thisBox = box(var)
2077}"#;
2078
2079        parse_execute(ast).await.unwrap();
2080    }
2081
2082    #[tokio::test(flavor = "multi_thread")]
2083    #[ignore] // ignore til we get loops
2084    async fn test_execute_with_function_sketch_loop_array() {
2085        let ast = r#"fn box(h, l, w, start) {
2086 myBox = startSketchOn(XY)
2087    |> startProfile(at = [0,0])
2088    |> line(end = [0, l])
2089    |> line(end = [w, 0])
2090    |> line(end = [0, -l])
2091    |> close()
2092    |> extrude(length = h)
2093
2094  return myBox
2095}
2096
2097
2098for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2099  const thisBox = box(var[0], var[1], var[2], var[3])
2100}"#;
2101
2102        parse_execute(ast).await.unwrap();
2103    }
2104
2105    #[tokio::test(flavor = "multi_thread")]
2106    async fn test_get_member_of_array_with_function() {
2107        let ast = r#"fn box(@arr) {
2108 myBox =startSketchOn(XY)
2109    |> startProfile(at = arr[0])
2110    |> line(end = [0, arr[1]])
2111    |> line(end = [arr[2], 0])
2112    |> line(end = [0, -arr[1]])
2113    |> close()
2114    |> extrude(length = arr[3])
2115
2116  return myBox
2117}
2118
2119thisBox = box([[0,0], 6, 10, 3])
2120
2121"#;
2122        parse_execute(ast).await.unwrap();
2123    }
2124
2125    #[tokio::test(flavor = "multi_thread")]
2126    async fn test_function_cannot_access_future_definitions() {
2127        let ast = r#"
2128fn returnX() {
2129  // x shouldn't be defined yet.
2130  return x
2131}
2132
2133x = 5
2134
2135answer = returnX()"#;
2136
2137        let result = parse_execute(ast).await;
2138        let err = result.unwrap_err();
2139        assert_eq!(err.message(), "`x` is not defined");
2140    }
2141
2142    #[tokio::test(flavor = "multi_thread")]
2143    async fn test_override_prelude() {
2144        let text = "PI = 3.0";
2145        let result = parse_execute(text).await.unwrap();
2146        let errs = result.exec_state.errors();
2147        assert!(errs.is_empty());
2148    }
2149
2150    #[tokio::test(flavor = "multi_thread")]
2151    async fn type_aliases() {
2152        let text = r#"@settings(experimentalFeatures = allow)
2153type MyTy = [number; 2]
2154fn foo(@x: MyTy) {
2155    return x[0]
2156}
2157
2158foo([0, 1])
2159
2160type Other = MyTy | Helix
2161"#;
2162        let result = parse_execute(text).await.unwrap();
2163        let errs = result.exec_state.errors();
2164        assert!(errs.is_empty());
2165    }
2166
2167    #[tokio::test(flavor = "multi_thread")]
2168    async fn test_cannot_shebang_in_fn() {
2169        let ast = r#"
2170fn foo() {
2171  #!hello
2172  return true
2173}
2174
2175foo
2176"#;
2177
2178        let result = parse_execute(ast).await;
2179        let err = result.unwrap_err();
2180        assert_eq!(
2181            err,
2182            KclError::new_syntax(KclErrorDetails::new(
2183                "Unexpected token: #".to_owned(),
2184                vec![SourceRange::new(14, 15, ModuleId::default())],
2185            )),
2186        );
2187    }
2188
2189    #[tokio::test(flavor = "multi_thread")]
2190    async fn test_pattern_transform_function_cannot_access_future_definitions() {
2191        let ast = r#"
2192fn transform(@replicaId) {
2193  // x shouldn't be defined yet.
2194  scale = x
2195  return {
2196    translate = [0, 0, replicaId * 10],
2197    scale = [scale, 1, 0],
2198  }
2199}
2200
2201fn layer() {
2202  return startSketchOn(XY)
2203    |> circle( center= [0, 0], radius= 1, tag = $tag1)
2204    |> extrude(length = 10)
2205}
2206
2207x = 5
2208
2209// The 10 layers are replicas of each other, with a transform applied to each.
2210shape = layer() |> patternTransform(instances = 10, transform = transform)
2211"#;
2212
2213        let result = parse_execute(ast).await;
2214        let err = result.unwrap_err();
2215        assert_eq!(err.message(), "`x` is not defined",);
2216    }
2217
2218    // ADAM: Move some of these into simulation tests.
2219
2220    #[tokio::test(flavor = "multi_thread")]
2221    async fn test_math_execute_with_functions() {
2222        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
2223        let result = parse_execute(ast).await.unwrap();
2224        assert_eq!(
2225            5.0,
2226            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2227                .as_f64()
2228                .unwrap()
2229        );
2230    }
2231
2232    #[tokio::test(flavor = "multi_thread")]
2233    async fn test_math_execute() {
2234        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
2235        let result = parse_execute(ast).await.unwrap();
2236        assert_eq!(
2237            7.4,
2238            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2239                .as_f64()
2240                .unwrap()
2241        );
2242    }
2243
2244    #[tokio::test(flavor = "multi_thread")]
2245    async fn test_math_execute_start_negative() {
2246        let ast = r#"myVar = -5 + 6"#;
2247        let result = parse_execute(ast).await.unwrap();
2248        assert_eq!(
2249            1.0,
2250            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2251                .as_f64()
2252                .unwrap()
2253        );
2254    }
2255
2256    #[tokio::test(flavor = "multi_thread")]
2257    async fn test_math_execute_with_pi() {
2258        let ast = r#"myVar = PI * 2"#;
2259        let result = parse_execute(ast).await.unwrap();
2260        assert_eq!(
2261            std::f64::consts::TAU,
2262            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2263                .as_f64()
2264                .unwrap()
2265        );
2266    }
2267
2268    #[tokio::test(flavor = "multi_thread")]
2269    async fn test_math_define_decimal_without_leading_zero() {
2270        let ast = r#"thing = .4 + 7"#;
2271        let result = parse_execute(ast).await.unwrap();
2272        assert_eq!(
2273            7.4,
2274            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
2275                .as_f64()
2276                .unwrap()
2277        );
2278    }
2279
2280    #[tokio::test(flavor = "multi_thread")]
2281    async fn pass_std_to_std() {
2282        let ast = r#"sketch001 = startSketchOn(XY)
2283profile001 = circle(sketch001, center = [0, 0], radius = 2)
2284extrude001 = extrude(profile001, length = 5)
2285extrudes = patternLinear3d(
2286  extrude001,
2287  instances = 3,
2288  distance = 5,
2289  axis = [1, 1, 0],
2290)
2291clone001 = map(extrudes, f = clone)
2292"#;
2293        parse_execute(ast).await.unwrap();
2294    }
2295
2296    #[tokio::test(flavor = "multi_thread")]
2297    async fn test_array_reduce_nested_array() {
2298        let code = r#"
2299fn id(@el, accum)  { return accum }
2300
2301answer = reduce([], initial=[[[0,0]]], f=id)
2302"#;
2303        let result = parse_execute(code).await.unwrap();
2304        assert_eq!(
2305            mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2306            KclValue::HomArray {
2307                value: vec![KclValue::HomArray {
2308                    value: vec![KclValue::HomArray {
2309                        value: vec![
2310                            KclValue::Number {
2311                                value: 0.0,
2312                                ty: NumericType::default(),
2313                                meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2314                            },
2315                            KclValue::Number {
2316                                value: 0.0,
2317                                ty: NumericType::default(),
2318                                meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2319                            }
2320                        ],
2321                        ty: RuntimeType::any(),
2322                    }],
2323                    ty: RuntimeType::any(),
2324                }],
2325                ty: RuntimeType::any(),
2326            }
2327        );
2328    }
2329
2330    #[tokio::test(flavor = "multi_thread")]
2331    async fn test_zero_param_fn() {
2332        let ast = r#"sigmaAllow = 35000 // psi
2333leg1 = 5 // inches
2334leg2 = 8 // inches
2335fn thickness() { return 0.56 }
2336
2337bracket = startSketchOn(XY)
2338  |> startProfile(at = [0,0])
2339  |> line(end = [0, leg1])
2340  |> line(end = [leg2, 0])
2341  |> line(end = [0, -thickness()])
2342  |> line(end = [-leg2 + thickness(), 0])
2343"#;
2344        parse_execute(ast).await.unwrap();
2345    }
2346
2347    #[tokio::test(flavor = "multi_thread")]
2348    async fn test_unary_operator_not_succeeds() {
2349        let ast = r#"
2350fn returnTrue() { return !false }
2351t = true
2352f = false
2353notTrue = !t
2354notFalse = !f
2355c = !!true
2356d = !returnTrue()
2357
2358assertIs(!false, error = "expected to pass")
2359
2360fn check(x) {
2361  assertIs(!x, error = "expected argument to be false")
2362  return true
2363}
2364check(x = false)
2365"#;
2366        let result = parse_execute(ast).await.unwrap();
2367        assert_eq!(
2368            false,
2369            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2370                .as_bool()
2371                .unwrap()
2372        );
2373        assert_eq!(
2374            true,
2375            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2376                .as_bool()
2377                .unwrap()
2378        );
2379        assert_eq!(
2380            true,
2381            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2382                .as_bool()
2383                .unwrap()
2384        );
2385        assert_eq!(
2386            false,
2387            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2388                .as_bool()
2389                .unwrap()
2390        );
2391    }
2392
2393    #[tokio::test(flavor = "multi_thread")]
2394    async fn test_unary_operator_not_on_non_bool_fails() {
2395        let code1 = r#"
2396// Yup, this is null.
2397myNull = 0 / 0
2398notNull = !myNull
2399"#;
2400        assert_eq!(
2401            parse_execute(code1).await.unwrap_err().message(),
2402            "Cannot apply unary operator ! to non-boolean value: a number",
2403        );
2404
2405        let code2 = "notZero = !0";
2406        assert_eq!(
2407            parse_execute(code2).await.unwrap_err().message(),
2408            "Cannot apply unary operator ! to non-boolean value: a number",
2409        );
2410
2411        let code3 = r#"
2412notEmptyString = !""
2413"#;
2414        assert_eq!(
2415            parse_execute(code3).await.unwrap_err().message(),
2416            "Cannot apply unary operator ! to non-boolean value: a string",
2417        );
2418
2419        let code4 = r#"
2420obj = { a = 1 }
2421notMember = !obj.a
2422"#;
2423        assert_eq!(
2424            parse_execute(code4).await.unwrap_err().message(),
2425            "Cannot apply unary operator ! to non-boolean value: a number",
2426        );
2427
2428        let code5 = "
2429a = []
2430notArray = !a";
2431        assert_eq!(
2432            parse_execute(code5).await.unwrap_err().message(),
2433            "Cannot apply unary operator ! to non-boolean value: an empty array",
2434        );
2435
2436        let code6 = "
2437x = {}
2438notObject = !x";
2439        assert_eq!(
2440            parse_execute(code6).await.unwrap_err().message(),
2441            "Cannot apply unary operator ! to non-boolean value: an object",
2442        );
2443
2444        let code7 = "
2445fn x() { return 1 }
2446notFunction = !x";
2447        let fn_err = parse_execute(code7).await.unwrap_err();
2448        // These are currently printed out as JSON objects, so we don't want to
2449        // check the full error.
2450        assert!(
2451            fn_err
2452                .message()
2453                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2454            "Actual error: {fn_err:?}"
2455        );
2456
2457        let code8 = "
2458myTagDeclarator = $myTag
2459notTagDeclarator = !myTagDeclarator";
2460        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2461        // These are currently printed out as JSON objects, so we don't want to
2462        // check the full error.
2463        assert!(
2464            tag_declarator_err
2465                .message()
2466                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2467            "Actual error: {tag_declarator_err:?}"
2468        );
2469
2470        let code9 = "
2471myTagDeclarator = $myTag
2472notTagIdentifier = !myTag";
2473        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2474        // These are currently printed out as JSON objects, so we don't want to
2475        // check the full error.
2476        assert!(
2477            tag_identifier_err
2478                .message()
2479                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2480            "Actual error: {tag_identifier_err:?}"
2481        );
2482
2483        let code10 = "notPipe = !(1 |> 2)";
2484        assert_eq!(
2485            // TODO: We don't currently parse this, but we should.  It should be
2486            // a runtime error instead.
2487            parse_execute(code10).await.unwrap_err(),
2488            KclError::new_syntax(KclErrorDetails::new(
2489                "Unexpected token: !".to_owned(),
2490                vec![SourceRange::new(10, 11, ModuleId::default())],
2491            ))
2492        );
2493
2494        let code11 = "
2495fn identity(x) { return x }
2496notPipeSub = 1 |> identity(!%))";
2497        assert_eq!(
2498            // TODO: We don't currently parse this, but we should.  It should be
2499            // a runtime error instead.
2500            parse_execute(code11).await.unwrap_err(),
2501            KclError::new_syntax(KclErrorDetails::new(
2502                "There was an unexpected `!`. Try removing it.".to_owned(),
2503                vec![SourceRange::new(56, 57, ModuleId::default())],
2504            ))
2505        );
2506
2507        // TODO: Add these tests when we support these types.
2508        // let notNan = !NaN
2509        // let notInfinity = !Infinity
2510    }
2511
2512    #[tokio::test(flavor = "multi_thread")]
2513    async fn test_start_sketch_on_invalid_kwargs() {
2514        let current_dir = std::env::current_dir().unwrap();
2515        let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2516        let mut code = std::fs::read_to_string(&path).unwrap();
2517        assert_eq!(
2518            parse_execute(&code).await.unwrap_err().message(),
2519            "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2520        );
2521
2522        path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2523        code = std::fs::read_to_string(&path).unwrap();
2524
2525        assert_eq!(
2526            parse_execute(&code).await.unwrap_err().message(),
2527            "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2528        );
2529
2530        path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2531        code = std::fs::read_to_string(&path).unwrap();
2532
2533        assert_eq!(
2534            parse_execute(&code).await.unwrap_err().message(),
2535            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2536        );
2537
2538        path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2539        code = std::fs::read_to_string(&path).unwrap();
2540
2541        assert_eq!(
2542            parse_execute(&code).await.unwrap_err().message(),
2543            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2544        );
2545
2546        path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2547        code = std::fs::read_to_string(&path).unwrap();
2548
2549        assert_eq!(
2550            parse_execute(&code).await.unwrap_err().message(),
2551            "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2552        );
2553    }
2554
2555    #[tokio::test(flavor = "multi_thread")]
2556    async fn test_math_negative_variable_in_binary_expression() {
2557        let ast = r#"sigmaAllow = 35000 // psi
2558width = 1 // inch
2559
2560p = 150 // lbs
2561distance = 6 // inches
2562FOS = 2
2563
2564leg1 = 5 // inches
2565leg2 = 8 // inches
2566
2567thickness_squared = distance * p * FOS * 6 / sigmaAllow
2568thickness = 0.56 // inches. App does not support square root function yet
2569
2570bracket = startSketchOn(XY)
2571  |> startProfile(at = [0,0])
2572  |> line(end = [0, leg1])
2573  |> line(end = [leg2, 0])
2574  |> line(end = [0, -thickness])
2575  |> line(end = [-leg2 + thickness, 0])
2576"#;
2577        parse_execute(ast).await.unwrap();
2578    }
2579
2580    #[tokio::test(flavor = "multi_thread")]
2581    async fn test_execute_function_no_return() {
2582        let ast = r#"fn test(@origin) {
2583  origin
2584}
2585
2586test([0, 0])
2587"#;
2588        let result = parse_execute(ast).await;
2589        assert!(result.is_err());
2590        assert!(result.unwrap_err().to_string().contains("undefined"));
2591    }
2592
2593    #[tokio::test(flavor = "multi_thread")]
2594    async fn test_max_stack_size_exceeded_error() {
2595        let ast = r#"
2596fn forever(@n) {
2597  return 1 + forever(n)
2598}
2599
2600forever(1)
2601"#;
2602        let result = parse_execute(ast).await;
2603        let err = result.unwrap_err();
2604        assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
2605    }
2606
2607    #[tokio::test(flavor = "multi_thread")]
2608    async fn test_math_doubly_nested_parens() {
2609        let ast = r#"sigmaAllow = 35000 // psi
2610width = 4 // inch
2611p = 150 // Force on shelf - lbs
2612distance = 6 // inches
2613FOS = 2
2614leg1 = 5 // inches
2615leg2 = 8 // inches
2616thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2617thickness = 0.32 // inches. App does not support square root function yet
2618bracket = startSketchOn(XY)
2619  |> startProfile(at = [0,0])
2620    |> line(end = [0, leg1])
2621  |> line(end = [leg2, 0])
2622  |> line(end = [0, -thickness])
2623  |> line(end = [-1 * leg2 + thickness, 0])
2624  |> line(end = [0, -1 * leg1 + thickness])
2625  |> close()
2626  |> extrude(length = width)
2627"#;
2628        parse_execute(ast).await.unwrap();
2629    }
2630
2631    #[tokio::test(flavor = "multi_thread")]
2632    async fn test_math_nested_parens_one_less() {
2633        let ast = r#" sigmaAllow = 35000 // psi
2634width = 4 // inch
2635p = 150 // Force on shelf - lbs
2636distance = 6 // inches
2637FOS = 2
2638leg1 = 5 // inches
2639leg2 = 8 // inches
2640thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2641thickness = 0.32 // inches. App does not support square root function yet
2642bracket = startSketchOn(XY)
2643  |> startProfile(at = [0,0])
2644    |> line(end = [0, leg1])
2645  |> line(end = [leg2, 0])
2646  |> line(end = [0, -thickness])
2647  |> line(end = [-1 * leg2 + thickness, 0])
2648  |> line(end = [0, -1 * leg1 + thickness])
2649  |> close()
2650  |> extrude(length = width)
2651"#;
2652        parse_execute(ast).await.unwrap();
2653    }
2654
2655    #[tokio::test(flavor = "multi_thread")]
2656    async fn test_fn_as_operand() {
2657        let ast = r#"fn f() { return 1 }
2658x = f()
2659y = x + 1
2660z = f() + 1
2661w = f() + f()
2662"#;
2663        parse_execute(ast).await.unwrap();
2664    }
2665
2666    #[tokio::test(flavor = "multi_thread")]
2667    async fn kcl_test_ids_stable_between_executions() {
2668        let code = r#"sketch001 = startSketchOn(XZ)
2669|> startProfile(at = [61.74, 206.13])
2670|> xLine(length = 305.11, tag = $seg01)
2671|> yLine(length = -291.85)
2672|> xLine(length = -segLen(seg01))
2673|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2674|> close()
2675|> extrude(length = 40.14)
2676|> shell(
2677    thickness = 3.14,
2678    faces = [seg01]
2679)
2680"#;
2681
2682        let ctx = crate::test_server::new_context(true, None).await.unwrap();
2683        let old_program = crate::Program::parse_no_errs(code).unwrap();
2684
2685        // Execute the program.
2686        if let Err(err) = ctx.run_with_caching(old_program).await {
2687            let report = err.into_miette_report_with_outputs(code).unwrap();
2688            let report = miette::Report::new(report);
2689            panic!("Error executing program: {report:?}");
2690        }
2691
2692        // Get the id_generator from the first execution.
2693        let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2694
2695        let code = r#"sketch001 = startSketchOn(XZ)
2696|> startProfile(at = [62.74, 206.13])
2697|> xLine(length = 305.11, tag = $seg01)
2698|> yLine(length = -291.85)
2699|> xLine(length = -segLen(seg01))
2700|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2701|> close()
2702|> extrude(length = 40.14)
2703|> shell(
2704    faces = [seg01],
2705    thickness = 3.14,
2706)
2707"#;
2708
2709        // Execute a slightly different program again.
2710        let program = crate::Program::parse_no_errs(code).unwrap();
2711        // Execute the program.
2712        ctx.run_with_caching(program).await.unwrap();
2713
2714        let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2715
2716        assert_eq!(id_generator, new_id_generator);
2717    }
2718
2719    #[tokio::test(flavor = "multi_thread")]
2720    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2721        let code = r#"sketch001 = startSketchOn(XZ)
2722|> startProfile(at = [61.74, 206.13])
2723|> xLine(length = 305.11, tag = $seg01)
2724|> yLine(length = -291.85)
2725|> xLine(length = -segLen(seg01))
2726|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2727|> close()
2728|> extrude(length = 40.14)
2729|> shell(
2730    thickness = 3.14,
2731    faces = [seg01]
2732)
2733"#;
2734
2735        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2736        let old_program = crate::Program::parse_no_errs(code).unwrap();
2737
2738        // Execute the program.
2739        ctx.run_with_caching(old_program.clone()).await.unwrap();
2740
2741        let settings_state = cache::read_old_ast().await.unwrap().settings;
2742
2743        // Ensure the settings are as expected.
2744        assert_eq!(settings_state, ctx.settings);
2745
2746        // Change a setting.
2747        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2748
2749        // Execute the program.
2750        ctx.run_with_caching(old_program.clone()).await.unwrap();
2751
2752        let settings_state = cache::read_old_ast().await.unwrap().settings;
2753
2754        // Ensure the settings are as expected.
2755        assert_eq!(settings_state, ctx.settings);
2756
2757        // Change a setting.
2758        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2759
2760        // Execute the program.
2761        ctx.run_with_caching(old_program).await.unwrap();
2762
2763        let settings_state = cache::read_old_ast().await.unwrap().settings;
2764
2765        // Ensure the settings are as expected.
2766        assert_eq!(settings_state, ctx.settings);
2767
2768        ctx.close().await;
2769    }
2770
2771    #[tokio::test(flavor = "multi_thread")]
2772    async fn mock_after_not_mock() {
2773        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2774        let program = crate::Program::parse_no_errs("x = 2").unwrap();
2775        let result = ctx.run_with_caching(program).await.unwrap();
2776        assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2777
2778        let ctx2 = ExecutorContext::new_mock(None).await;
2779        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2780        let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
2781        assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2782
2783        ctx.close().await;
2784        ctx2.close().await;
2785    }
2786
2787    #[cfg(feature = "artifact-graph")]
2788    #[tokio::test(flavor = "multi_thread")]
2789    async fn mock_has_stable_ids() {
2790        let ctx = ExecutorContext::new_mock(None).await;
2791        let mock_config = MockConfig {
2792            use_prev_memory: false,
2793            ..Default::default()
2794        };
2795        let code = "sk = startSketchOn(XY)
2796        |> startProfile(at = [0, 0])";
2797        let program = crate::Program::parse_no_errs(code).unwrap();
2798        let result = ctx.run_mock(&program, &mock_config).await.unwrap();
2799        let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2800        assert!(!ids.is_empty(), "IDs should not be empty");
2801
2802        let ctx2 = ExecutorContext::new_mock(None).await;
2803        let program2 = crate::Program::parse_no_errs(code).unwrap();
2804        let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
2805        let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2806
2807        assert_eq!(ids, ids2, "Generated IDs should match");
2808        ctx.close().await;
2809        ctx2.close().await;
2810    }
2811
2812    #[cfg(feature = "artifact-graph")]
2813    #[tokio::test(flavor = "multi_thread")]
2814    async fn sim_sketch_mode_real_mock_real() {
2815        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2816        let code = r#"sketch001 = startSketchOn(XY)
2817profile001 = startProfile(sketch001, at = [0, 0])
2818  |> line(end = [10, 0])
2819  |> line(end = [0, 10])
2820  |> line(end = [-10, 0])
2821  |> line(end = [0, -10])
2822  |> close()
2823"#;
2824        let program = crate::Program::parse_no_errs(code).unwrap();
2825        let result = ctx.run_with_caching(program).await.unwrap();
2826        assert_eq!(result.operations.len(), 1);
2827
2828        let mock_ctx = ExecutorContext::new_mock(None).await;
2829        let mock_program = crate::Program::parse_no_errs(code).unwrap();
2830        let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
2831        assert_eq!(mock_result.operations.len(), 1);
2832
2833        let code2 = code.to_owned()
2834            + r#"
2835extrude001 = extrude(profile001, length = 10)
2836"#;
2837        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
2838        let result = ctx.run_with_caching(program2).await.unwrap();
2839        assert_eq!(result.operations.len(), 2);
2840
2841        ctx.close().await;
2842        mock_ctx.close().await;
2843    }
2844
2845    #[tokio::test(flavor = "multi_thread")]
2846    async fn read_tag_version() {
2847        let ast = r#"fn bar(@t) {
2848  return startSketchOn(XY)
2849    |> startProfile(at = [0,0])
2850    |> angledLine(
2851        angle = -60,
2852        length = segLen(t),
2853    )
2854    |> line(end = [0, 0])
2855    |> close()
2856}
2857
2858sketch = startSketchOn(XY)
2859  |> startProfile(at = [0,0])
2860  |> line(end = [0, 10])
2861  |> line(end = [10, 0], tag = $tag0)
2862  |> line(endAbsolute = [0, 0])
2863
2864fn foo() {
2865  // tag0 tags an edge
2866  return bar(tag0)
2867}
2868
2869solid = sketch |> extrude(length = 10)
2870// tag0 tags a face
2871sketch2 = startSketchOn(solid, face = tag0)
2872  |> startProfile(at = [0,0])
2873  |> line(end = [0, 1])
2874  |> line(end = [1, 0])
2875  |> line(end = [0, 0])
2876
2877foo() |> extrude(length = 1)
2878"#;
2879        parse_execute(ast).await.unwrap();
2880    }
2881
2882    #[tokio::test(flavor = "multi_thread")]
2883    async fn experimental() {
2884        let code = r#"
2885startSketchOn(XY)
2886  |> startProfile(at = [0, 0], tag = $start)
2887  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2888"#;
2889        let result = parse_execute(code).await.unwrap();
2890        let errors = result.exec_state.errors();
2891        assert_eq!(errors.len(), 1);
2892        assert_eq!(errors[0].severity, Severity::Error);
2893        let msg = &errors[0].message;
2894        assert!(msg.contains("experimental"), "found {msg}");
2895
2896        let code = r#"@settings(experimentalFeatures = allow)
2897startSketchOn(XY)
2898  |> startProfile(at = [0, 0], tag = $start)
2899  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2900"#;
2901        let result = parse_execute(code).await.unwrap();
2902        let errors = result.exec_state.errors();
2903        assert!(errors.is_empty());
2904
2905        let code = r#"@settings(experimentalFeatures = warn)
2906startSketchOn(XY)
2907  |> startProfile(at = [0, 0], tag = $start)
2908  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2909"#;
2910        let result = parse_execute(code).await.unwrap();
2911        let errors = result.exec_state.errors();
2912        assert_eq!(errors.len(), 1);
2913        assert_eq!(errors[0].severity, Severity::Warning);
2914        let msg = &errors[0].message;
2915        assert!(msg.contains("experimental"), "found {msg}");
2916
2917        let code = r#"@settings(experimentalFeatures = deny)
2918startSketchOn(XY)
2919  |> startProfile(at = [0, 0], tag = $start)
2920  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2921"#;
2922        let result = parse_execute(code).await.unwrap();
2923        let errors = result.exec_state.errors();
2924        assert_eq!(errors.len(), 1);
2925        assert_eq!(errors[0].severity, Severity::Error);
2926        let msg = &errors[0].message;
2927        assert!(msg.contains("experimental"), "found {msg}");
2928
2929        let code = r#"@settings(experimentalFeatures = foo)
2930startSketchOn(XY)
2931  |> startProfile(at = [0, 0], tag = $start)
2932  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2933"#;
2934        parse_execute(code).await.unwrap_err();
2935    }
2936
2937    #[tokio::test(flavor = "multi_thread")]
2938    async fn experimental_parameter() {
2939        let code = r#"
2940fn inc(@x, @(experimental = true) amount? = 1) {
2941  return x + amount
2942}
2943
2944answer = inc(5, amount = 2)
2945"#;
2946        let result = parse_execute(code).await.unwrap();
2947        let errors = result.exec_state.errors();
2948        assert_eq!(errors.len(), 1);
2949        assert_eq!(errors[0].severity, Severity::Error);
2950        let msg = &errors[0].message;
2951        assert!(msg.contains("experimental"), "found {msg}");
2952
2953        // If the parameter isn't used, there's no warning.
2954        let code = r#"
2955fn inc(@x, @(experimental = true) amount? = 1) {
2956  return x + amount
2957}
2958
2959answer = inc(5)
2960"#;
2961        let result = parse_execute(code).await.unwrap();
2962        let errors = result.exec_state.errors();
2963        assert_eq!(errors.len(), 0);
2964    }
2965}