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