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