1#[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::CompilationIssue;
66use crate::ExecError;
67use crate::KclErrorWithOutputs;
68use crate::NodePath;
69use crate::SourceRange;
70#[cfg(feature = "artifact-graph")]
71use crate::collections::AhashIndexSet;
72use crate::engine::EngineManager;
73use crate::engine::GridScaleBehavior;
74use crate::errors::KclError;
75use crate::errors::KclErrorDetails;
76use crate::execution::cache::CacheInformation;
77use crate::execution::cache::CacheResult;
78use crate::execution::import_graph::Universe;
79use crate::execution::import_graph::UniverseMap;
80use crate::execution::typed_path::TypedPath;
81#[cfg(feature = "artifact-graph")]
82use crate::front::Number;
83use crate::front::Object;
84use crate::front::ObjectId;
85use crate::fs::FileManager;
86use crate::modules::ModuleExecutionOutcome;
87use crate::modules::ModuleId;
88use crate::modules::ModulePath;
89use crate::modules::ModuleRepr;
90use crate::parsing::ast::types::Expr;
91use crate::parsing::ast::types::ImportPath;
92use crate::parsing::ast::types::NodeRef;
93
94pub(crate) mod annotations;
95#[cfg(feature = "artifact-graph")]
96mod artifact;
97pub(crate) mod cache;
98mod cad_op;
99mod exec_ast;
100pub mod fn_call;
101#[cfg(test)]
102#[cfg(feature = "artifact-graph")]
103mod freedom_analysis_tests;
104mod geometry;
105mod id_generator;
106mod import;
107mod import_graph;
108pub(crate) mod kcl_value;
109mod memory;
110mod modeling;
111mod sketch_solve;
112mod sketch_transpiler;
113mod state;
114pub mod typed_path;
115pub(crate) mod types;
116
117pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
118pub(crate) const SKETCH_OBJECT_META: &str = "meta";
119pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
120
121macro_rules! control_continue {
126 ($control_flow:expr) => {{
127 let cf = $control_flow;
128 if cf.is_some_return() {
129 return Ok(cf);
130 } else {
131 cf.into_value()
132 }
133 }};
134}
135pub(crate) use control_continue;
137
138macro_rules! early_return {
143 ($control_flow:expr) => {{
144 let cf = $control_flow;
145 if cf.is_some_return() {
146 return Err(EarlyReturn::from(cf));
147 } else {
148 cf.into_value()
149 }
150 }};
151}
152pub(crate) use early_return;
154
155#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
156pub enum ControlFlowKind {
157 #[default]
158 Continue,
159 Exit,
160}
161
162impl ControlFlowKind {
163 pub fn is_some_return(&self) -> bool {
165 match self {
166 ControlFlowKind::Continue => false,
167 ControlFlowKind::Exit => true,
168 }
169 }
170}
171
172#[must_use = "You should always handle the control flow value when it is returned"]
173#[derive(Debug, Clone, PartialEq, Serialize)]
174pub struct KclValueControlFlow {
175 value: KclValue,
177 pub control: ControlFlowKind,
178}
179
180impl KclValue {
181 pub(crate) fn continue_(self) -> KclValueControlFlow {
182 KclValueControlFlow {
183 value: self,
184 control: ControlFlowKind::Continue,
185 }
186 }
187
188 pub(crate) fn exit(self) -> KclValueControlFlow {
189 KclValueControlFlow {
190 value: self,
191 control: ControlFlowKind::Exit,
192 }
193 }
194}
195
196impl KclValueControlFlow {
197 pub fn is_some_return(&self) -> bool {
199 self.control.is_some_return()
200 }
201
202 pub(crate) fn into_value(self) -> KclValue {
203 self.value
204 }
205}
206
207#[must_use = "You should always handle the control flow value when it is returned"]
214#[derive(Debug, Clone)]
215pub(crate) enum EarlyReturn {
216 Value(KclValueControlFlow),
218 Error(KclError),
220}
221
222impl From<KclValueControlFlow> for EarlyReturn {
223 fn from(cf: KclValueControlFlow) -> Self {
224 EarlyReturn::Value(cf)
225 }
226}
227
228impl From<KclError> for EarlyReturn {
229 fn from(err: KclError) -> Self {
230 EarlyReturn::Error(err)
231 }
232}
233
234pub(crate) enum StatementKind<'a> {
235 Declaration { name: &'a str },
236 Expression,
237}
238
239#[derive(Debug, Clone, Copy)]
240pub enum PreserveMem {
241 Normal,
242 Always,
243}
244
245impl PreserveMem {
246 fn normal(self) -> bool {
247 match self {
248 PreserveMem::Normal => true,
249 PreserveMem::Always => false,
250 }
251 }
252}
253
254#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
256#[ts(export)]
257#[serde(rename_all = "camelCase")]
258pub struct ExecOutcome {
259 pub variables: IndexMap<String, KclValue>,
261 #[cfg(feature = "artifact-graph")]
264 pub operations: Vec<Operation>,
265 #[cfg(feature = "artifact-graph")]
267 pub artifact_graph: ArtifactGraph,
268 #[cfg(feature = "artifact-graph")]
270 #[serde(skip)]
271 pub scene_objects: Vec<Object>,
272 #[cfg(feature = "artifact-graph")]
275 #[serde(skip)]
276 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
277 #[cfg(feature = "artifact-graph")]
278 #[serde(skip)]
279 pub var_solutions: Vec<(SourceRange, Number)>,
280 pub issues: Vec<CompilationIssue>,
282 pub filenames: IndexMap<ModuleId, ModulePath>,
284 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 pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
309 self.issues.iter().filter(|error| error.is_err())
310 }
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct MockConfig {
316 pub use_prev_memory: bool,
317 pub sketch_block_id: Option<ObjectId>,
320 pub freedom_analysis: bool,
323 #[cfg(feature = "artifact-graph")]
325 pub segment_ids_edited: AhashIndexSet<ObjectId>,
326}
327
328impl Default for MockConfig {
329 fn default() -> Self {
330 Self {
331 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 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 #[serde(skip)]
377 pub info: Vec<(usize, TagEngineInfo)>,
378 #[serde(skip)]
379 pub meta: Vec<Metadata>,
380}
381
382impl TagIdentifier {
383 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 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
396 self.info.last().map(|i| &i.1)
397 }
398
399 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 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 *e > *oe {
420 continue;
421 }
422 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#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
477#[ts(export)]
478#[serde(tag = "type", rename_all = "camelCase")]
479pub struct TagEngineInfo {
480 pub id: uuid::Uuid,
482 pub geometry: Geometry,
484 pub path: Option<Path>,
486 pub surface: Option<ExtrudeSurface>,
488}
489
490#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
491pub enum BodyType {
492 Root,
493 Block,
494}
495
496#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
498#[ts(export)]
499#[serde(rename_all = "camelCase")]
500pub struct Metadata {
501 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], node_path: Option<NodePath>) -> 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 node_path,
546 };
547 }
548 crate::front::SourceRef::BackTrace {
549 ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
550 }
551 }
552}
553
554#[derive(PartialEq, Debug, Default, Clone)]
556pub enum ContextType {
557 #[default]
559 Live,
560
561 Mock,
565
566 MockCustomForwarded,
568}
569
570#[derive(Debug, Clone)]
574pub struct ExecutorContext {
575 pub engine: Arc<Box<dyn EngineManager>>,
576 pub fs: Arc<FileManager>,
577 pub settings: ExecutorSettings,
578 pub context_type: ContextType,
579}
580
581#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
583#[ts(export)]
584pub struct ExecutorSettings {
585 pub highlight_edges: bool,
587 pub enable_ssao: bool,
589 pub show_grid: bool,
591 pub replay: Option<String>,
594 pub project_directory: Option<TypedPath>,
597 pub current_file: Option<TypedPath>,
600 pub fixed_size_grid: bool,
602}
603
604impl Default for ExecutorSettings {
605 fn default() -> Self {
606 Self {
607 highlight_edges: true,
608 enable_ssao: false,
609 show_grid: false,
610 replay: None,
611 project_directory: None,
612 current_file: None,
613 fixed_size_grid: true,
614 }
615 }
616}
617
618impl From<crate::settings::types::Configuration> for ExecutorSettings {
619 fn from(config: crate::settings::types::Configuration) -> Self {
620 Self::from(config.settings)
621 }
622}
623
624impl From<crate::settings::types::Settings> for ExecutorSettings {
625 fn from(settings: crate::settings::types::Settings) -> Self {
626 Self {
627 highlight_edges: settings.modeling.highlight_edges.into(),
628 enable_ssao: settings.modeling.enable_ssao.into(),
629 show_grid: settings.modeling.show_scale_grid,
630 replay: None,
631 project_directory: None,
632 current_file: None,
633 fixed_size_grid: settings.modeling.fixed_size_grid,
634 }
635 }
636}
637
638impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
639 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
640 Self::from(config.settings.modeling)
641 }
642}
643
644impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
645 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
646 Self {
647 highlight_edges: modeling.highlight_edges.into(),
648 enable_ssao: modeling.enable_ssao.into(),
649 show_grid: modeling.show_scale_grid,
650 replay: None,
651 project_directory: None,
652 current_file: None,
653 fixed_size_grid: true,
654 }
655 }
656}
657
658impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
659 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
660 Self {
661 highlight_edges: modeling.highlight_edges.into(),
662 enable_ssao: modeling.enable_ssao.into(),
663 show_grid: Default::default(),
664 replay: None,
665 project_directory: None,
666 current_file: None,
667 fixed_size_grid: true,
668 }
669 }
670}
671
672impl ExecutorSettings {
673 pub fn with_current_file(&mut self, current_file: TypedPath) {
675 if current_file.extension() == Some("kcl") {
677 self.current_file = Some(current_file.clone());
678 if let Some(parent) = current_file.parent() {
680 self.project_directory = Some(parent);
681 } else {
682 self.project_directory = Some(TypedPath::from(""));
683 }
684 } else {
685 self.project_directory = Some(current_file);
686 }
687 }
688}
689
690impl ExecutorContext {
691 pub fn new_with_engine_and_fs(
693 engine: Arc<Box<dyn EngineManager>>,
694 fs: Arc<FileManager>,
695 settings: ExecutorSettings,
696 ) -> Self {
697 ExecutorContext {
698 engine,
699 fs,
700 settings,
701 context_type: ContextType::Live,
702 }
703 }
704
705 #[cfg(not(target_arch = "wasm32"))]
707 pub fn new_with_engine(engine: Arc<Box<dyn EngineManager>>, settings: ExecutorSettings) -> Self {
708 Self::new_with_engine_and_fs(engine, Arc::new(FileManager::new()), settings)
709 }
710
711 #[cfg(not(target_arch = "wasm32"))]
713 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
714 let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
715 let (ws, _headers) = client
716 .modeling()
717 .commands_ws(kittycad::modeling::CommandsWsParams {
718 api_call_id: None,
719 fps: None,
720 order_independent_transparency: None,
721 post_effect: if settings.enable_ssao {
722 Some(kittycad::types::PostEffectType::Ssao)
723 } else {
724 None
725 },
726 replay: settings.replay.clone(),
727 show_grid: if settings.show_grid { Some(true) } else { None },
728 pool: None,
729 pr,
730 unlocked_framerate: None,
731 webrtc: Some(false),
732 video_res_width: None,
733 video_res_height: None,
734 })
735 .await?;
736
737 let engine: Arc<Box<dyn EngineManager>> =
738 Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
739
740 Ok(Self::new_with_engine(engine, settings))
741 }
742
743 #[cfg(target_arch = "wasm32")]
744 pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
745 Self::new_with_engine_and_fs(engine, fs, settings)
746 }
747
748 #[cfg(not(target_arch = "wasm32"))]
749 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
750 ExecutorContext {
751 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
752 fs: Arc::new(FileManager::new()),
753 settings: settings.unwrap_or_default(),
754 context_type: ContextType::Mock,
755 }
756 }
757
758 #[cfg(target_arch = "wasm32")]
759 pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
760 ExecutorContext {
761 engine,
762 fs,
763 settings,
764 context_type: ContextType::Mock,
765 }
766 }
767
768 #[cfg(target_arch = "wasm32")]
771 pub fn new_mock_for_lsp(
772 fs_manager: crate::fs::wasm::FileSystemManager,
773 settings: ExecutorSettings,
774 ) -> Result<Self, String> {
775 use crate::mock_engine;
776
777 let mock_engine = Arc::new(Box::new(
778 mock_engine::EngineConnection::new().map_err(|e| format!("Failed to create mock engine: {:?}", e))?,
779 ) as Box<dyn EngineManager>);
780
781 let fs = Arc::new(FileManager::new(fs_manager));
782
783 Ok(ExecutorContext {
784 engine: mock_engine,
785 fs,
786 settings,
787 context_type: ContextType::Mock,
788 })
789 }
790
791 #[cfg(not(target_arch = "wasm32"))]
792 pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
793 ExecutorContext {
794 engine,
795 fs: Arc::new(FileManager::new()),
796 settings: Default::default(),
797 context_type: ContextType::MockCustomForwarded,
798 }
799 }
800
801 #[cfg(not(target_arch = "wasm32"))]
807 pub async fn new_with_client(
808 settings: ExecutorSettings,
809 token: Option<String>,
810 engine_addr: Option<String>,
811 ) -> Result<Self> {
812 let client = crate::engine::new_zoo_client(token, engine_addr)?;
814
815 let ctx = Self::new(&client, settings).await?;
816 Ok(ctx)
817 }
818
819 #[cfg(not(target_arch = "wasm32"))]
824 pub async fn new_with_default_client() -> Result<Self> {
825 let ctx = Self::new_with_client(Default::default(), None, None).await?;
827 Ok(ctx)
828 }
829
830 #[cfg(not(target_arch = "wasm32"))]
832 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
833 let ctx = ExecutorContext::new_with_client(
834 ExecutorSettings {
835 highlight_edges: true,
836 enable_ssao: false,
837 show_grid: false,
838 replay: None,
839 project_directory: None,
840 current_file: None,
841 fixed_size_grid: false,
842 },
843 None,
844 engine_addr,
845 )
846 .await?;
847 Ok(ctx)
848 }
849
850 pub fn is_mock(&self) -> bool {
851 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
852 }
853
854 pub async fn no_engine_commands(&self) -> bool {
856 self.is_mock()
857 }
858
859 pub async fn send_clear_scene(
860 &self,
861 exec_state: &mut ExecState,
862 source_range: crate::execution::SourceRange,
863 ) -> Result<(), KclError> {
864 exec_state.mod_local.artifacts.clear();
867 exec_state.global.root_module_artifacts.clear();
868 exec_state.global.artifacts.clear();
869
870 self.engine
871 .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
872 .await?;
873 if self.settings.enable_ssao {
876 let cmd_id = exec_state.next_uuid();
877 exec_state
878 .batch_modeling_cmd(
879 ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
880 ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
881 )
882 .await?;
883 }
884 Ok(())
885 }
886
887 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
888 cache::bust_cache().await;
889
890 let outcome = self.run_with_caching(crate::Program::empty()).await?;
895
896 Ok(outcome)
897 }
898
899 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
900 self.eval_prelude(exec_state, SourceRange::synthetic())
901 .await
902 .map_err(KclErrorWithOutputs::no_outputs)?;
903 exec_state.mut_stack().push_new_root_env(true);
904 Ok(())
905 }
906
907 fn restore_mock_memory(
908 exec_state: &mut ExecState,
909 mem: cache::SketchModeState,
910 _mock_config: &MockConfig,
911 ) -> Result<(), KclErrorWithOutputs> {
912 *exec_state.mut_stack() = mem.stack;
913 exec_state.global.module_infos = mem.module_infos;
914 exec_state.global.path_to_source_id = mem.path_to_source_id;
915 exec_state.global.id_to_source = mem.id_to_source;
916 #[cfg(feature = "artifact-graph")]
917 {
918 let len = _mock_config
919 .sketch_block_id
920 .map(|sketch_block_id| sketch_block_id.0)
921 .unwrap_or(0);
922 if let Some(scene_objects) = mem.scene_objects.get(0..len) {
923 exec_state
924 .global
925 .root_module_artifacts
926 .restore_scene_objects(scene_objects);
927 } else {
928 let message = format!(
929 "Cached scene objects length {} is less than expected length from cached object ID generator {}",
930 mem.scene_objects.len(),
931 len
932 );
933 debug_assert!(false, "{message}");
934 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
935 KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
936 )));
937 }
938 }
939
940 Ok(())
941 }
942
943 pub async fn run_mock(
944 &self,
945 program: &crate::Program,
946 mock_config: &MockConfig,
947 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
948 assert!(
949 self.is_mock(),
950 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
951 );
952
953 let use_prev_memory = mock_config.use_prev_memory;
954 let mut exec_state = ExecState::new_mock(self, mock_config);
955 if use_prev_memory {
956 match cache::read_old_memory().await {
957 Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
958 None => self.prepare_mem(&mut exec_state).await?,
959 }
960 } else {
961 self.prepare_mem(&mut exec_state).await?
962 };
963
964 exec_state.mut_stack().push_new_env_for_scope();
967
968 let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
969
970 let mut stack = exec_state.stack().clone();
975 let module_infos = exec_state.global.module_infos.clone();
976 let path_to_source_id = exec_state.global.path_to_source_id.clone();
977 let id_to_source = exec_state.global.id_to_source.clone();
978 #[cfg(feature = "artifact-graph")]
979 let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
980 #[cfg(not(feature = "artifact-graph"))]
981 let scene_objects = Default::default();
982 let outcome = exec_state.into_exec_outcome(result.0, self).await;
983
984 stack.squash_env(result.0);
985 let state = cache::SketchModeState {
986 stack,
987 module_infos,
988 path_to_source_id,
989 id_to_source,
990 scene_objects,
991 };
992 cache::write_old_memory(state).await;
993
994 Ok(outcome)
995 }
996
997 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
998 assert!(!self.is_mock());
999 let grid_scale = if self.settings.fixed_size_grid {
1000 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1001 } else {
1002 GridScaleBehavior::ScaleWithZoom
1003 };
1004
1005 let original_program = program.clone();
1006
1007 let (_program, exec_state, result) = match cache::read_old_ast().await {
1008 Some(mut cached_state) => {
1009 let old = CacheInformation {
1010 ast: &cached_state.main.ast,
1011 settings: &cached_state.settings,
1012 };
1013 let new = CacheInformation {
1014 ast: &program.ast,
1015 settings: &self.settings,
1016 };
1017
1018 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1020 CacheResult::ReExecute {
1021 clear_scene,
1022 reapply_settings,
1023 program: changed_program,
1024 } => {
1025 if reapply_settings
1026 && self
1027 .engine
1028 .reapply_settings(
1029 &self.settings,
1030 Default::default(),
1031 &mut cached_state.main.exec_state.id_generator,
1032 grid_scale,
1033 )
1034 .await
1035 .is_err()
1036 {
1037 (true, program, None)
1038 } else {
1039 (
1040 clear_scene,
1041 crate::Program {
1042 ast: changed_program,
1043 original_file_contents: program.original_file_contents,
1044 },
1045 None,
1046 )
1047 }
1048 }
1049 CacheResult::CheckImportsOnly {
1050 reapply_settings,
1051 ast: changed_program,
1052 } => {
1053 let mut reapply_failed = false;
1054 if reapply_settings {
1055 if self
1056 .engine
1057 .reapply_settings(
1058 &self.settings,
1059 Default::default(),
1060 &mut cached_state.main.exec_state.id_generator,
1061 grid_scale,
1062 )
1063 .await
1064 .is_ok()
1065 {
1066 cache::write_old_ast(GlobalState::with_settings(
1067 cached_state.clone(),
1068 self.settings.clone(),
1069 ))
1070 .await;
1071 } else {
1072 reapply_failed = true;
1073 }
1074 }
1075
1076 if reapply_failed {
1077 (true, program, None)
1078 } else {
1079 let mut new_exec_state = ExecState::new(self);
1081 let (new_universe, new_universe_map) =
1082 self.get_universe(&program, &mut new_exec_state).await?;
1083
1084 let clear_scene = new_universe.values().any(|value| {
1085 let id = value.1;
1086 match (
1087 cached_state.exec_state.get_source(id),
1088 new_exec_state.global.get_source(id),
1089 ) {
1090 (Some(s0), Some(s1)) => s0.source != s1.source,
1091 _ => false,
1092 }
1093 });
1094
1095 if !clear_scene {
1096 return Ok(cached_state.into_exec_outcome(self).await);
1098 }
1099
1100 (
1101 true,
1102 crate::Program {
1103 ast: changed_program,
1104 original_file_contents: program.original_file_contents,
1105 },
1106 Some((new_universe, new_universe_map, new_exec_state)),
1107 )
1108 }
1109 }
1110 CacheResult::NoAction(true) => {
1111 if self
1112 .engine
1113 .reapply_settings(
1114 &self.settings,
1115 Default::default(),
1116 &mut cached_state.main.exec_state.id_generator,
1117 grid_scale,
1118 )
1119 .await
1120 .is_ok()
1121 {
1122 cache::write_old_ast(GlobalState::with_settings(
1124 cached_state.clone(),
1125 self.settings.clone(),
1126 ))
1127 .await;
1128
1129 return Ok(cached_state.into_exec_outcome(self).await);
1130 }
1131 (true, program, None)
1132 }
1133 CacheResult::NoAction(false) => {
1134 return Ok(cached_state.into_exec_outcome(self).await);
1135 }
1136 };
1137
1138 let (exec_state, result) = match import_check_info {
1139 Some((new_universe, new_universe_map, mut new_exec_state)) => {
1140 self.send_clear_scene(&mut new_exec_state, Default::default())
1142 .await
1143 .map_err(KclErrorWithOutputs::no_outputs)?;
1144
1145 let result = self
1146 .run_concurrent(
1147 &program,
1148 &mut new_exec_state,
1149 Some((new_universe, new_universe_map)),
1150 PreserveMem::Normal,
1151 )
1152 .await;
1153
1154 (new_exec_state, result)
1155 }
1156 None if clear_scene => {
1157 let mut exec_state = cached_state.reconstitute_exec_state();
1159 exec_state.reset(self);
1160
1161 self.send_clear_scene(&mut exec_state, Default::default())
1162 .await
1163 .map_err(KclErrorWithOutputs::no_outputs)?;
1164
1165 let result = self
1166 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1167 .await;
1168
1169 (exec_state, result)
1170 }
1171 None => {
1172 let mut exec_state = cached_state.reconstitute_exec_state();
1173 exec_state.mut_stack().restore_env(cached_state.main.result_env);
1174
1175 let result = self
1176 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1177 .await;
1178
1179 (exec_state, result)
1180 }
1181 };
1182
1183 (program, exec_state, result)
1184 }
1185 None => {
1186 let mut exec_state = ExecState::new(self);
1187 self.send_clear_scene(&mut exec_state, Default::default())
1188 .await
1189 .map_err(KclErrorWithOutputs::no_outputs)?;
1190
1191 let result = self
1192 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1193 .await;
1194
1195 (program, exec_state, result)
1196 }
1197 };
1198
1199 if result.is_err() {
1200 cache::bust_cache().await;
1201 }
1202
1203 let result = result?;
1205
1206 cache::write_old_ast(GlobalState::new(
1210 exec_state.clone(),
1211 self.settings.clone(),
1212 original_program.ast,
1213 result.0,
1214 ))
1215 .await;
1216
1217 let outcome = exec_state.into_exec_outcome(result.0, self).await;
1218 Ok(outcome)
1219 }
1220
1221 pub async fn run(
1225 &self,
1226 program: &crate::Program,
1227 exec_state: &mut ExecState,
1228 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1229 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1230 .await
1231 }
1232
1233 pub async fn run_concurrent(
1238 &self,
1239 program: &crate::Program,
1240 exec_state: &mut ExecState,
1241 universe_info: Option<(Universe, UniverseMap)>,
1242 preserve_mem: PreserveMem,
1243 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1244 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1247 (universe, universe_map)
1248 } else {
1249 self.get_universe(program, exec_state).await?
1250 };
1251
1252 let default_planes = self.engine.get_default_planes().read().await.clone();
1253
1254 self.eval_prelude(exec_state, SourceRange::synthetic())
1256 .await
1257 .map_err(KclErrorWithOutputs::no_outputs)?;
1258
1259 for modules in import_graph::import_graph(&universe, self)
1260 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1261 .into_iter()
1262 {
1263 #[cfg(not(target_arch = "wasm32"))]
1264 let mut set = tokio::task::JoinSet::new();
1265
1266 #[allow(clippy::type_complexity)]
1267 let (results_tx, mut results_rx): (
1268 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1269 tokio::sync::mpsc::Receiver<_>,
1270 ) = tokio::sync::mpsc::channel(1);
1271
1272 for module in modules {
1273 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1274 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1275 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1276 )));
1277 };
1278 let module_id = *module_id;
1279 let module_path = module_path.clone();
1280 let source_range = SourceRange::from(import_stmt);
1281 let module_exec_state = exec_state.clone();
1283
1284 self.add_import_module_ops(
1285 exec_state,
1286 &program.ast,
1287 module_id,
1288 &module_path,
1289 source_range,
1290 &universe_map,
1291 );
1292
1293 let repr = repr.clone();
1294 let exec_ctxt = self.clone();
1295 let results_tx = results_tx.clone();
1296
1297 let exec_module = async |exec_ctxt: &ExecutorContext,
1298 repr: &ModuleRepr,
1299 module_id: ModuleId,
1300 module_path: &ModulePath,
1301 exec_state: &mut ExecState,
1302 source_range: SourceRange|
1303 -> Result<ModuleRepr, KclError> {
1304 match repr {
1305 ModuleRepr::Kcl(program, _) => {
1306 let result = exec_ctxt
1307 .exec_module_from_ast(
1308 program,
1309 module_id,
1310 module_path,
1311 exec_state,
1312 source_range,
1313 PreserveMem::Normal,
1314 )
1315 .await;
1316
1317 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1318 }
1319 ModuleRepr::Foreign(geom, _) => {
1320 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1321 .await
1322 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1323
1324 result.map(|val| {
1325 ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
1326 })
1327 }
1328 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1329 format!("Module {module_path} not found in universe"),
1330 vec![source_range],
1331 ))),
1332 }
1333 };
1334
1335 #[cfg(target_arch = "wasm32")]
1336 {
1337 wasm_bindgen_futures::spawn_local(async move {
1338 let mut exec_state = module_exec_state;
1339 let exec_ctxt = exec_ctxt;
1340
1341 let result = exec_module(
1342 &exec_ctxt,
1343 &repr,
1344 module_id,
1345 &module_path,
1346 &mut exec_state,
1347 source_range,
1348 )
1349 .await;
1350
1351 results_tx
1352 .send((module_id, module_path, result))
1353 .await
1354 .unwrap_or_default();
1355 });
1356 }
1357 #[cfg(not(target_arch = "wasm32"))]
1358 {
1359 set.spawn(async move {
1360 let mut exec_state = module_exec_state;
1361 let exec_ctxt = exec_ctxt;
1362
1363 let result = exec_module(
1364 &exec_ctxt,
1365 &repr,
1366 module_id,
1367 &module_path,
1368 &mut exec_state,
1369 source_range,
1370 )
1371 .await;
1372
1373 results_tx
1374 .send((module_id, module_path, result))
1375 .await
1376 .unwrap_or_default();
1377 });
1378 }
1379 }
1380
1381 drop(results_tx);
1382
1383 while let Some((module_id, _, result)) = results_rx.recv().await {
1384 match result {
1385 Ok(new_repr) => {
1386 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1387
1388 match &mut repr {
1389 ModuleRepr::Kcl(_, cache) => {
1390 let ModuleRepr::Kcl(_, session_data) = new_repr else {
1391 unreachable!();
1392 };
1393 *cache = session_data;
1394 }
1395 ModuleRepr::Foreign(_, cache) => {
1396 let ModuleRepr::Foreign(_, session_data) = new_repr else {
1397 unreachable!();
1398 };
1399 *cache = session_data;
1400 }
1401 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1402 }
1403
1404 exec_state.global.module_infos[&module_id].restore_repr(repr);
1405 }
1406 Err(e) => {
1407 return Err(exec_state.error_with_outputs(e, None, default_planes));
1408 }
1409 }
1410 }
1411 }
1412
1413 exec_state
1417 .global
1418 .root_module_artifacts
1419 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1420
1421 self.inner_run(program, exec_state, preserve_mem).await
1422 }
1423
1424 async fn get_universe(
1427 &self,
1428 program: &crate::Program,
1429 exec_state: &mut ExecState,
1430 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1431 exec_state.add_root_module_contents(program);
1432
1433 let mut universe = std::collections::HashMap::new();
1434
1435 let default_planes = self.engine.get_default_planes().read().await.clone();
1436
1437 let root_imports = import_graph::import_universe(
1438 self,
1439 &ModulePath::Main,
1440 &ModuleRepr::Kcl(program.ast.clone(), None),
1441 &mut universe,
1442 exec_state,
1443 )
1444 .await
1445 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1446
1447 Ok((universe, root_imports))
1448 }
1449
1450 #[cfg(not(feature = "artifact-graph"))]
1451 fn add_import_module_ops(
1452 &self,
1453 _exec_state: &mut ExecState,
1454 _program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1455 _module_id: ModuleId,
1456 _module_path: &ModulePath,
1457 _source_range: SourceRange,
1458 _universe_map: &UniverseMap,
1459 ) {
1460 }
1461
1462 #[cfg(feature = "artifact-graph")]
1463 fn add_import_module_ops(
1464 &self,
1465 exec_state: &mut ExecState,
1466 program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1467 module_id: ModuleId,
1468 module_path: &ModulePath,
1469 source_range: SourceRange,
1470 universe_map: &UniverseMap,
1471 ) {
1472 match module_path {
1473 ModulePath::Main => {
1474 }
1476 ModulePath::Local {
1477 value,
1478 original_import_path,
1479 } => {
1480 if universe_map.contains_key(value) {
1483 use crate::NodePath;
1484
1485 let node_path = if source_range.is_top_level_module() {
1486 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1487 NodePath::from_range(
1488 &exec_state.build_program_lookup(program.clone()),
1489 cached_body_items,
1490 source_range,
1491 )
1492 .unwrap_or_default()
1493 } else {
1494 NodePath::placeholder()
1497 };
1498
1499 let name = match original_import_path {
1500 Some(value) => value.to_string_lossy(),
1501 None => value.file_name().unwrap_or_default(),
1502 };
1503 exec_state.push_op(Operation::GroupBegin {
1504 group: Group::ModuleInstance { name, module_id },
1505 node_path,
1506 source_range,
1507 });
1508 exec_state.push_op(Operation::GroupEnd);
1512 }
1513 }
1514 ModulePath::Std { .. } => {
1515 }
1517 }
1518 }
1519
1520 async fn inner_run(
1523 &self,
1524 program: &crate::Program,
1525 exec_state: &mut ExecState,
1526 preserve_mem: PreserveMem,
1527 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1528 let _stats = crate::log::LogPerfStats::new("Interpretation");
1529
1530 let grid_scale = if self.settings.fixed_size_grid {
1532 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1533 } else {
1534 GridScaleBehavior::ScaleWithZoom
1535 };
1536 self.engine
1537 .reapply_settings(
1538 &self.settings,
1539 Default::default(),
1540 exec_state.id_generator(),
1541 grid_scale,
1542 )
1543 .await
1544 .map_err(KclErrorWithOutputs::no_outputs)?;
1545
1546 let default_planes = self.engine.get_default_planes().read().await.clone();
1547 let result = self
1548 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1549 .await;
1550
1551 crate::log::log(format!(
1552 "Post interpretation KCL memory stats: {:#?}",
1553 exec_state.stack().memory.stats
1554 ));
1555 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1556
1557 async fn write_old_memory(ctx: &ExecutorContext, exec_state: &ExecState, env_ref: EnvironmentRef) {
1560 if ctx.is_mock() {
1561 return;
1562 }
1563 let mut stack = exec_state.stack().deep_clone();
1564 stack.restore_env(env_ref);
1565 let state = cache::SketchModeState {
1566 stack,
1567 module_infos: exec_state.global.module_infos.clone(),
1568 path_to_source_id: exec_state.global.path_to_source_id.clone(),
1569 id_to_source: exec_state.global.id_to_source.clone(),
1570 #[cfg(feature = "artifact-graph")]
1571 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1572 #[cfg(not(feature = "artifact-graph"))]
1573 scene_objects: Default::default(),
1574 };
1575 cache::write_old_memory(state).await;
1576 }
1577
1578 let env_ref = match result {
1579 Ok(env_ref) => env_ref,
1580 Err((err, env_ref)) => {
1581 if let Some(env_ref) = env_ref {
1584 write_old_memory(self, exec_state, env_ref).await;
1585 }
1586 return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
1587 }
1588 };
1589
1590 write_old_memory(self, exec_state, env_ref).await;
1591
1592 let session_data = self.engine.get_session_data().await;
1593
1594 Ok((env_ref, session_data))
1595 }
1596
1597 async fn execute_and_build_graph(
1600 &self,
1601 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1602 exec_state: &mut ExecState,
1603 preserve_mem: PreserveMem,
1604 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1605 #[cfg(feature = "artifact-graph")]
1611 let start_op = exec_state.global.root_module_artifacts.operations.len();
1612
1613 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1614 .await
1615 .map_err(|e| (e, None))?;
1616
1617 let exec_result = self
1618 .exec_module_body(
1619 program,
1620 exec_state,
1621 preserve_mem,
1622 ModuleId::default(),
1623 &ModulePath::Main,
1624 )
1625 .await
1626 .map(
1627 |ModuleExecutionOutcome {
1628 environment: env_ref,
1629 artifacts: module_artifacts,
1630 ..
1631 }| {
1632 exec_state.global.root_module_artifacts.extend(module_artifacts);
1635 env_ref
1636 },
1637 )
1638 .map_err(|(err, env_ref, module_artifacts)| {
1639 if let Some(module_artifacts) = module_artifacts {
1640 exec_state.global.root_module_artifacts.extend(module_artifacts);
1643 }
1644 (err, env_ref)
1645 });
1646
1647 #[cfg(feature = "artifact-graph")]
1648 {
1649 let programs = &exec_state.build_program_lookup(program.clone());
1651 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1652 for op in exec_state
1653 .global
1654 .root_module_artifacts
1655 .operations
1656 .iter_mut()
1657 .skip(start_op)
1658 {
1659 op.fill_node_paths(programs, cached_body_items);
1660 }
1661 for module in exec_state.global.module_infos.values_mut() {
1662 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1663 for op in &mut outcome.artifacts.operations {
1664 op.fill_node_paths(programs, cached_body_items);
1665 }
1666 }
1667 }
1668 }
1669
1670 self.engine.ensure_async_commands_completed().await.map_err(|e| {
1672 match &exec_result {
1673 Ok(env_ref) => (e, Some(*env_ref)),
1674 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1676 }
1677 })?;
1678
1679 self.engine.clear_queues().await;
1682
1683 match exec_state.build_artifact_graph(&self.engine, program).await {
1684 Ok(_) => exec_result,
1685 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1686 }
1687 }
1688
1689 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1693 if exec_state.stack().memory.requires_std() {
1694 #[cfg(feature = "artifact-graph")]
1695 let initial_ops = exec_state.mod_local.artifacts.operations.len();
1696
1697 let path = vec!["std".to_owned(), "prelude".to_owned()];
1698 let resolved_path = ModulePath::from_std_import_path(&path)?;
1699 let id = self
1700 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1701 .await?;
1702 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1703
1704 exec_state.mut_stack().memory.set_std(module_memory);
1705
1706 #[cfg(feature = "artifact-graph")]
1712 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1713 }
1714
1715 Ok(())
1716 }
1717
1718 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1720 self.engine
1722 .send_modeling_cmd(
1723 uuid::Uuid::new_v4(),
1724 crate::execution::SourceRange::default(),
1725 &ModelingCmd::from(
1726 mcmd::ZoomToFit::builder()
1727 .object_ids(Default::default())
1728 .animated(false)
1729 .padding(0.1)
1730 .build(),
1731 ),
1732 )
1733 .await
1734 .map_err(KclErrorWithOutputs::no_outputs)?;
1735
1736 let resp = self
1738 .engine
1739 .send_modeling_cmd(
1740 uuid::Uuid::new_v4(),
1741 crate::execution::SourceRange::default(),
1742 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
1743 )
1744 .await
1745 .map_err(KclErrorWithOutputs::no_outputs)?;
1746
1747 let OkWebSocketResponseData::Modeling {
1748 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1749 } = resp
1750 else {
1751 return Err(ExecError::BadPng(format!(
1752 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1753 )));
1754 };
1755 Ok(contents)
1756 }
1757
1758 pub async fn export(
1760 &self,
1761 format: kittycad_modeling_cmds::format::OutputFormat3d,
1762 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1763 let resp = self
1764 .engine
1765 .send_modeling_cmd(
1766 uuid::Uuid::new_v4(),
1767 crate::SourceRange::default(),
1768 &kittycad_modeling_cmds::ModelingCmd::Export(
1769 kittycad_modeling_cmds::Export::builder()
1770 .entity_ids(vec![])
1771 .format(format)
1772 .build(),
1773 ),
1774 )
1775 .await?;
1776
1777 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1778 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1779 format!("Expected Export response, got {resp:?}",),
1780 vec![SourceRange::default()],
1781 )));
1782 };
1783
1784 Ok(files)
1785 }
1786
1787 pub async fn export_step(
1789 &self,
1790 deterministic_time: bool,
1791 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1792 let files = self
1793 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1794 kittycad_modeling_cmds::format::step::export::Options::builder()
1795 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
1796 .maybe_created(if deterministic_time {
1797 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1798 KclError::new_internal(crate::errors::KclErrorDetails::new(
1799 format!("Failed to parse date: {e}"),
1800 vec![SourceRange::default()],
1801 ))
1802 })?)
1803 } else {
1804 None
1805 })
1806 .build(),
1807 ))
1808 .await?;
1809
1810 Ok(files)
1811 }
1812
1813 pub async fn close(&self) {
1814 self.engine.close().await;
1815 }
1816}
1817
1818#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1819pub struct ArtifactId(Uuid);
1820
1821impl ArtifactId {
1822 pub fn new(uuid: Uuid) -> Self {
1823 Self(uuid)
1824 }
1825
1826 pub fn placeholder() -> Self {
1828 Self(Uuid::nil())
1829 }
1830}
1831
1832impl From<Uuid> for ArtifactId {
1833 fn from(uuid: Uuid) -> Self {
1834 Self::new(uuid)
1835 }
1836}
1837
1838impl From<&Uuid> for ArtifactId {
1839 fn from(uuid: &Uuid) -> Self {
1840 Self::new(*uuid)
1841 }
1842}
1843
1844impl From<ArtifactId> for Uuid {
1845 fn from(id: ArtifactId) -> Self {
1846 id.0
1847 }
1848}
1849
1850impl From<&ArtifactId> for Uuid {
1851 fn from(id: &ArtifactId) -> Self {
1852 id.0
1853 }
1854}
1855
1856impl From<ModelingCmdId> for ArtifactId {
1857 fn from(id: ModelingCmdId) -> Self {
1858 Self::new(*id.as_ref())
1859 }
1860}
1861
1862impl From<&ModelingCmdId> for ArtifactId {
1863 fn from(id: &ModelingCmdId) -> Self {
1864 Self::new(*id.as_ref())
1865 }
1866}
1867
1868#[cfg(test)]
1869pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1870 parse_execute_with_project_dir(code, None).await
1871}
1872
1873#[cfg(test)]
1874pub(crate) async fn parse_execute_with_project_dir(
1875 code: &str,
1876 project_directory: Option<TypedPath>,
1877) -> Result<ExecTestResults, KclError> {
1878 let program = crate::Program::parse_no_errs(code)?;
1879
1880 let exec_ctxt = ExecutorContext {
1881 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
1882 |err| {
1883 KclError::new_internal(crate::errors::KclErrorDetails::new(
1884 format!("Failed to create mock engine connection: {err}"),
1885 vec![SourceRange::default()],
1886 ))
1887 },
1888 )?)),
1889 fs: Arc::new(crate::fs::FileManager::new()),
1890 settings: ExecutorSettings {
1891 project_directory,
1892 ..Default::default()
1893 },
1894 context_type: ContextType::Mock,
1895 };
1896 let mut exec_state = ExecState::new(&exec_ctxt);
1897 let result = exec_ctxt.run(&program, &mut exec_state).await?;
1898
1899 Ok(ExecTestResults {
1900 program,
1901 mem_env: result.0,
1902 exec_ctxt,
1903 exec_state,
1904 })
1905}
1906
1907#[cfg(test)]
1908#[derive(Debug)]
1909pub(crate) struct ExecTestResults {
1910 program: crate::Program,
1911 mem_env: EnvironmentRef,
1912 exec_ctxt: ExecutorContext,
1913 exec_state: ExecState,
1914}
1915
1916#[cfg(feature = "artifact-graph")]
1920pub struct ProgramLookup {
1921 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
1922}
1923
1924#[cfg(feature = "artifact-graph")]
1925impl ProgramLookup {
1926 pub fn new(
1929 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1930 module_infos: state::ModuleInfoMap,
1931 ) -> Self {
1932 let mut programs = IndexMap::with_capacity(module_infos.len());
1933 for (id, info) in module_infos {
1934 if let ModuleRepr::Kcl(program, _) = info.repr {
1935 programs.insert(id, program);
1936 }
1937 }
1938 programs.insert(ModuleId::default(), current);
1939 Self { programs }
1940 }
1941
1942 pub fn program_for_module(
1943 &self,
1944 module_id: ModuleId,
1945 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
1946 self.programs.get(&module_id)
1947 }
1948}
1949
1950#[cfg(test)]
1951mod tests {
1952 use pretty_assertions::assert_eq;
1953
1954 use super::*;
1955 use crate::ModuleId;
1956 use crate::errors::KclErrorDetails;
1957 use crate::errors::Severity;
1958 use crate::exec::NumericType;
1959 use crate::execution::memory::Stack;
1960 use crate::execution::types::RuntimeType;
1961
1962 #[track_caller]
1964 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1965 memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1966 }
1967
1968 #[tokio::test(flavor = "multi_thread")]
1969 async fn test_execute_warn() {
1970 let text = "@blah";
1971 let result = parse_execute(text).await.unwrap();
1972 let errs = result.exec_state.issues();
1973 assert_eq!(errs.len(), 1);
1974 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1975 assert!(
1976 errs[0].message.contains("Unknown annotation"),
1977 "unexpected warning message: {}",
1978 errs[0].message
1979 );
1980 }
1981
1982 #[tokio::test(flavor = "multi_thread")]
1983 async fn test_execute_fn_definitions() {
1984 let ast = r#"fn def(@x) {
1985 return x
1986}
1987fn ghi(@x) {
1988 return x
1989}
1990fn jkl(@x) {
1991 return x
1992}
1993fn hmm(@x) {
1994 return x
1995}
1996
1997yo = 5 + 6
1998
1999abc = 3
2000identifierGuy = 5
2001part001 = startSketchOn(XY)
2002|> startProfile(at = [-1.2, 4.83])
2003|> line(end = [2.8, 0])
2004|> angledLine(angle = 100 + 100, length = 3.01)
2005|> angledLine(angle = abc, length = 3.02)
2006|> angledLine(angle = def(yo), length = 3.03)
2007|> angledLine(angle = ghi(2), length = 3.04)
2008|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2009|> close()
2010yo2 = hmm([identifierGuy + 5])"#;
2011
2012 parse_execute(ast).await.unwrap();
2013 }
2014
2015 #[tokio::test(flavor = "multi_thread")]
2016 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2017 let code = r#"@settings(experimentalFeatures = allow)
2018firstProfile = sketch(on = XY) {
2019 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2020 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2021 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2022 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2023 coincident([edge1.end, edge2.start])
2024 coincident([edge2.end, edge3.start])
2025 coincident([edge3.end, edge4.start])
2026 coincident([edge4.end, edge1.start])
2027}
2028
2029secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2030 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2031 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2032 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2033 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2034 coincident([edge5.end, edge6.start])
2035 coincident([edge6.end, edge7.start])
2036 coincident([edge7.end, edge8.start])
2037 coincident([edge8.end, edge5.start])
2038}
2039
2040firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2041secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2042"#;
2043
2044 let result = parse_execute(code).await.unwrap();
2045 assert!(result.exec_state.issues().is_empty());
2046 }
2047
2048 #[tokio::test(flavor = "multi_thread")]
2049 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2050 let code = r#"@settings(experimentalFeatures = allow)
2051sketch001 = sketch(on = YZ) {
2052 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2053 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2054 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2055 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2056 coincident([line1.end, line2.start])
2057 coincident([line2.end, line3.start])
2058 coincident([line3.end, line4.start])
2059 coincident([line4.end, line1.start])
2060}
2061
2062sketch002 = sketch(on = -XZ) {
2063 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2064 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2065 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2066 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2067 coincident([line5.end, line6.start])
2068 coincident([line6.end, line7.start])
2069 coincident([line7.end, line8.start])
2070 coincident([line8.end, line5.start])
2071}
2072
2073region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2074extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2075region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2076extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2077
2078myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2079"#;
2080
2081 let result = parse_execute(code).await.unwrap();
2082 assert!(result.exec_state.issues().is_empty());
2083 }
2084
2085 #[tokio::test(flavor = "multi_thread")]
2086 async fn issue_10741_point_circle_coincident_executes() {
2087 let code = r#"@settings(experimentalFeatures = allow)
2088sketch001 = sketch(on = YZ) {
2089 circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
2090 line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
2091 coincident([line1.start, circle1])
2092}
2093"#;
2094
2095 let result = parse_execute(code).await.unwrap();
2096 assert!(
2097 result
2098 .exec_state
2099 .issues()
2100 .iter()
2101 .all(|issue| issue.severity != Severity::Error),
2102 "unexpected execution issues: {:#?}",
2103 result.exec_state.issues()
2104 );
2105 }
2106
2107 #[tokio::test(flavor = "multi_thread")]
2108 async fn test_execute_with_pipe_substitutions_unary() {
2109 let ast = r#"myVar = 3
2110part001 = startSketchOn(XY)
2111 |> startProfile(at = [0, 0])
2112 |> line(end = [3, 4], tag = $seg01)
2113 |> line(end = [
2114 min([segLen(seg01), myVar]),
2115 -legLen(hypotenuse = segLen(seg01), leg = myVar)
2116])
2117"#;
2118
2119 parse_execute(ast).await.unwrap();
2120 }
2121
2122 #[tokio::test(flavor = "multi_thread")]
2123 async fn test_execute_with_pipe_substitutions() {
2124 let ast = r#"myVar = 3
2125part001 = startSketchOn(XY)
2126 |> startProfile(at = [0, 0])
2127 |> line(end = [3, 4], tag = $seg01)
2128 |> line(end = [
2129 min([segLen(seg01), myVar]),
2130 legLen(hypotenuse = segLen(seg01), leg = myVar)
2131])
2132"#;
2133
2134 parse_execute(ast).await.unwrap();
2135 }
2136
2137 #[tokio::test(flavor = "multi_thread")]
2138 async fn test_execute_with_inline_comment() {
2139 let ast = r#"baseThick = 1
2140armAngle = 60
2141
2142baseThickHalf = baseThick / 2
2143halfArmAngle = armAngle / 2
2144
2145arrExpShouldNotBeIncluded = [1, 2, 3]
2146objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2147
2148part001 = startSketchOn(XY)
2149 |> startProfile(at = [0, 0])
2150 |> yLine(endAbsolute = 1)
2151 |> xLine(length = 3.84) // selection-range-7ish-before-this
2152
2153variableBelowShouldNotBeIncluded = 3
2154"#;
2155
2156 parse_execute(ast).await.unwrap();
2157 }
2158
2159 #[tokio::test(flavor = "multi_thread")]
2160 async fn test_execute_with_function_literal_in_pipe() {
2161 let ast = r#"w = 20
2162l = 8
2163h = 10
2164
2165fn thing() {
2166 return -8
2167}
2168
2169firstExtrude = startSketchOn(XY)
2170 |> startProfile(at = [0,0])
2171 |> line(end = [0, l])
2172 |> line(end = [w, 0])
2173 |> line(end = [0, thing()])
2174 |> close()
2175 |> extrude(length = h)"#;
2176
2177 parse_execute(ast).await.unwrap();
2178 }
2179
2180 #[tokio::test(flavor = "multi_thread")]
2181 async fn test_execute_with_function_unary_in_pipe() {
2182 let ast = r#"w = 20
2183l = 8
2184h = 10
2185
2186fn thing(@x) {
2187 return -x
2188}
2189
2190firstExtrude = startSketchOn(XY)
2191 |> startProfile(at = [0,0])
2192 |> line(end = [0, l])
2193 |> line(end = [w, 0])
2194 |> line(end = [0, thing(8)])
2195 |> close()
2196 |> extrude(length = h)"#;
2197
2198 parse_execute(ast).await.unwrap();
2199 }
2200
2201 #[tokio::test(flavor = "multi_thread")]
2202 async fn test_execute_with_function_array_in_pipe() {
2203 let ast = r#"w = 20
2204l = 8
2205h = 10
2206
2207fn thing(@x) {
2208 return [0, -x]
2209}
2210
2211firstExtrude = startSketchOn(XY)
2212 |> startProfile(at = [0,0])
2213 |> line(end = [0, l])
2214 |> line(end = [w, 0])
2215 |> line(end = thing(8))
2216 |> close()
2217 |> extrude(length = h)"#;
2218
2219 parse_execute(ast).await.unwrap();
2220 }
2221
2222 #[tokio::test(flavor = "multi_thread")]
2223 async fn test_execute_with_function_call_in_pipe() {
2224 let ast = r#"w = 20
2225l = 8
2226h = 10
2227
2228fn other_thing(@y) {
2229 return -y
2230}
2231
2232fn thing(@x) {
2233 return other_thing(x)
2234}
2235
2236firstExtrude = startSketchOn(XY)
2237 |> startProfile(at = [0,0])
2238 |> line(end = [0, l])
2239 |> line(end = [w, 0])
2240 |> line(end = [0, thing(8)])
2241 |> close()
2242 |> extrude(length = h)"#;
2243
2244 parse_execute(ast).await.unwrap();
2245 }
2246
2247 #[tokio::test(flavor = "multi_thread")]
2248 async fn test_execute_with_function_sketch() {
2249 let ast = r#"fn box(h, l, w) {
2250 myBox = startSketchOn(XY)
2251 |> startProfile(at = [0,0])
2252 |> line(end = [0, l])
2253 |> line(end = [w, 0])
2254 |> line(end = [0, -l])
2255 |> close()
2256 |> extrude(length = h)
2257
2258 return myBox
2259}
2260
2261fnBox = box(h = 3, l = 6, w = 10)"#;
2262
2263 parse_execute(ast).await.unwrap();
2264 }
2265
2266 #[tokio::test(flavor = "multi_thread")]
2267 async fn test_get_member_of_object_with_function_period() {
2268 let ast = r#"fn box(@obj) {
2269 myBox = startSketchOn(XY)
2270 |> startProfile(at = obj.start)
2271 |> line(end = [0, obj.l])
2272 |> line(end = [obj.w, 0])
2273 |> line(end = [0, -obj.l])
2274 |> close()
2275 |> extrude(length = obj.h)
2276
2277 return myBox
2278}
2279
2280thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2281"#;
2282 parse_execute(ast).await.unwrap();
2283 }
2284
2285 #[tokio::test(flavor = "multi_thread")]
2286 #[ignore] async fn test_object_member_starting_pipeline() {
2288 let ast = r#"
2289fn test2() {
2290 return {
2291 thing: startSketchOn(XY)
2292 |> startProfile(at = [0, 0])
2293 |> line(end = [0, 1])
2294 |> line(end = [1, 0])
2295 |> line(end = [0, -1])
2296 |> close()
2297 }
2298}
2299
2300x2 = test2()
2301
2302x2.thing
2303 |> extrude(length = 10)
2304"#;
2305 parse_execute(ast).await.unwrap();
2306 }
2307
2308 #[tokio::test(flavor = "multi_thread")]
2309 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
2311 let ast = r#"fn box(obj) {
2312let myBox = startSketchOn(XY)
2313 |> startProfile(at = obj.start)
2314 |> line(end = [0, obj.l])
2315 |> line(end = [obj.w, 0])
2316 |> line(end = [0, -obj.l])
2317 |> close()
2318 |> extrude(length = obj.h)
2319
2320 return myBox
2321}
2322
2323for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2324 thisBox = box(var)
2325}"#;
2326
2327 parse_execute(ast).await.unwrap();
2328 }
2329
2330 #[tokio::test(flavor = "multi_thread")]
2331 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
2333 let ast = r#"fn box(h, l, w, start) {
2334 myBox = startSketchOn(XY)
2335 |> startProfile(at = [0,0])
2336 |> line(end = [0, l])
2337 |> line(end = [w, 0])
2338 |> line(end = [0, -l])
2339 |> close()
2340 |> extrude(length = h)
2341
2342 return myBox
2343}
2344
2345
2346for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2347 const thisBox = box(var[0], var[1], var[2], var[3])
2348}"#;
2349
2350 parse_execute(ast).await.unwrap();
2351 }
2352
2353 #[tokio::test(flavor = "multi_thread")]
2354 async fn test_get_member_of_array_with_function() {
2355 let ast = r#"fn box(@arr) {
2356 myBox =startSketchOn(XY)
2357 |> startProfile(at = arr[0])
2358 |> line(end = [0, arr[1]])
2359 |> line(end = [arr[2], 0])
2360 |> line(end = [0, -arr[1]])
2361 |> close()
2362 |> extrude(length = arr[3])
2363
2364 return myBox
2365}
2366
2367thisBox = box([[0,0], 6, 10, 3])
2368
2369"#;
2370 parse_execute(ast).await.unwrap();
2371 }
2372
2373 #[tokio::test(flavor = "multi_thread")]
2374 async fn test_function_cannot_access_future_definitions() {
2375 let ast = r#"
2376fn returnX() {
2377 // x shouldn't be defined yet.
2378 return x
2379}
2380
2381x = 5
2382
2383answer = returnX()"#;
2384
2385 let result = parse_execute(ast).await;
2386 let err = result.unwrap_err();
2387 assert_eq!(err.message(), "`x` is not defined");
2388 }
2389
2390 #[tokio::test(flavor = "multi_thread")]
2391 async fn test_override_prelude() {
2392 let text = "PI = 3.0";
2393 let result = parse_execute(text).await.unwrap();
2394 let issues = result.exec_state.issues();
2395 assert!(issues.is_empty(), "issues={issues:#?}");
2396 }
2397
2398 #[tokio::test(flavor = "multi_thread")]
2399 async fn type_aliases() {
2400 let text = r#"@settings(experimentalFeatures = allow)
2401type MyTy = [number; 2]
2402fn foo(@x: MyTy) {
2403 return x[0]
2404}
2405
2406foo([0, 1])
2407
2408type Other = MyTy | Helix
2409"#;
2410 let result = parse_execute(text).await.unwrap();
2411 let issues = result.exec_state.issues();
2412 assert!(issues.is_empty(), "issues={issues:#?}");
2413 }
2414
2415 #[tokio::test(flavor = "multi_thread")]
2416 async fn test_cannot_shebang_in_fn() {
2417 let ast = r#"
2418fn foo() {
2419 #!hello
2420 return true
2421}
2422
2423foo
2424"#;
2425
2426 let result = parse_execute(ast).await;
2427 let err = result.unwrap_err();
2428 assert_eq!(
2429 err,
2430 KclError::new_syntax(KclErrorDetails::new(
2431 "Unexpected token: #".to_owned(),
2432 vec![SourceRange::new(14, 15, ModuleId::default())],
2433 )),
2434 );
2435 }
2436
2437 #[tokio::test(flavor = "multi_thread")]
2438 async fn test_pattern_transform_function_cannot_access_future_definitions() {
2439 let ast = r#"
2440fn transform(@replicaId) {
2441 // x shouldn't be defined yet.
2442 scale = x
2443 return {
2444 translate = [0, 0, replicaId * 10],
2445 scale = [scale, 1, 0],
2446 }
2447}
2448
2449fn layer() {
2450 return startSketchOn(XY)
2451 |> circle( center= [0, 0], radius= 1, tag = $tag1)
2452 |> extrude(length = 10)
2453}
2454
2455x = 5
2456
2457// The 10 layers are replicas of each other, with a transform applied to each.
2458shape = layer() |> patternTransform(instances = 10, transform = transform)
2459"#;
2460
2461 let result = parse_execute(ast).await;
2462 let err = result.unwrap_err();
2463 assert_eq!(err.message(), "`x` is not defined",);
2464 }
2465
2466 #[tokio::test(flavor = "multi_thread")]
2469 async fn test_math_execute_with_functions() {
2470 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
2471 let result = parse_execute(ast).await.unwrap();
2472 assert_eq!(
2473 5.0,
2474 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2475 .as_f64()
2476 .unwrap()
2477 );
2478 }
2479
2480 #[tokio::test(flavor = "multi_thread")]
2481 async fn test_math_execute() {
2482 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
2483 let result = parse_execute(ast).await.unwrap();
2484 assert_eq!(
2485 7.4,
2486 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2487 .as_f64()
2488 .unwrap()
2489 );
2490 }
2491
2492 #[tokio::test(flavor = "multi_thread")]
2493 async fn test_math_execute_start_negative() {
2494 let ast = r#"myVar = -5 + 6"#;
2495 let result = parse_execute(ast).await.unwrap();
2496 assert_eq!(
2497 1.0,
2498 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2499 .as_f64()
2500 .unwrap()
2501 );
2502 }
2503
2504 #[tokio::test(flavor = "multi_thread")]
2505 async fn test_math_execute_with_pi() {
2506 let ast = r#"myVar = PI * 2"#;
2507 let result = parse_execute(ast).await.unwrap();
2508 assert_eq!(
2509 std::f64::consts::TAU,
2510 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2511 .as_f64()
2512 .unwrap()
2513 );
2514 }
2515
2516 #[tokio::test(flavor = "multi_thread")]
2517 async fn test_math_define_decimal_without_leading_zero() {
2518 let ast = r#"thing = .4 + 7"#;
2519 let result = parse_execute(ast).await.unwrap();
2520 assert_eq!(
2521 7.4,
2522 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
2523 .as_f64()
2524 .unwrap()
2525 );
2526 }
2527
2528 #[tokio::test(flavor = "multi_thread")]
2529 async fn pass_std_to_std() {
2530 let ast = r#"sketch001 = startSketchOn(XY)
2531profile001 = circle(sketch001, center = [0, 0], radius = 2)
2532extrude001 = extrude(profile001, length = 5)
2533extrudes = patternLinear3d(
2534 extrude001,
2535 instances = 3,
2536 distance = 5,
2537 axis = [1, 1, 0],
2538)
2539clone001 = map(extrudes, f = clone)
2540"#;
2541 parse_execute(ast).await.unwrap();
2542 }
2543
2544 #[tokio::test(flavor = "multi_thread")]
2545 async fn test_array_reduce_nested_array() {
2546 let code = r#"
2547fn id(@el, accum) { return accum }
2548
2549answer = reduce([], initial=[[[0,0]]], f=id)
2550"#;
2551 let result = parse_execute(code).await.unwrap();
2552 assert_eq!(
2553 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2554 KclValue::HomArray {
2555 value: vec![KclValue::HomArray {
2556 value: vec![KclValue::HomArray {
2557 value: vec![
2558 KclValue::Number {
2559 value: 0.0,
2560 ty: NumericType::default(),
2561 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2562 },
2563 KclValue::Number {
2564 value: 0.0,
2565 ty: NumericType::default(),
2566 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2567 }
2568 ],
2569 ty: RuntimeType::any(),
2570 }],
2571 ty: RuntimeType::any(),
2572 }],
2573 ty: RuntimeType::any(),
2574 }
2575 );
2576 }
2577
2578 #[tokio::test(flavor = "multi_thread")]
2579 async fn test_zero_param_fn() {
2580 let ast = r#"sigmaAllow = 35000 // psi
2581leg1 = 5 // inches
2582leg2 = 8 // inches
2583fn thickness() { return 0.56 }
2584
2585bracket = startSketchOn(XY)
2586 |> startProfile(at = [0,0])
2587 |> line(end = [0, leg1])
2588 |> line(end = [leg2, 0])
2589 |> line(end = [0, -thickness()])
2590 |> line(end = [-leg2 + thickness(), 0])
2591"#;
2592 parse_execute(ast).await.unwrap();
2593 }
2594
2595 #[tokio::test(flavor = "multi_thread")]
2596 async fn test_unary_operator_not_succeeds() {
2597 let ast = r#"
2598fn returnTrue() { return !false }
2599t = true
2600f = false
2601notTrue = !t
2602notFalse = !f
2603c = !!true
2604d = !returnTrue()
2605
2606assertIs(!false, error = "expected to pass")
2607
2608fn check(x) {
2609 assertIs(!x, error = "expected argument to be false")
2610 return true
2611}
2612check(x = false)
2613"#;
2614 let result = parse_execute(ast).await.unwrap();
2615 assert_eq!(
2616 false,
2617 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2618 .as_bool()
2619 .unwrap()
2620 );
2621 assert_eq!(
2622 true,
2623 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2624 .as_bool()
2625 .unwrap()
2626 );
2627 assert_eq!(
2628 true,
2629 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2630 .as_bool()
2631 .unwrap()
2632 );
2633 assert_eq!(
2634 false,
2635 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2636 .as_bool()
2637 .unwrap()
2638 );
2639 }
2640
2641 #[tokio::test(flavor = "multi_thread")]
2642 async fn test_unary_operator_not_on_non_bool_fails() {
2643 let code1 = r#"
2644// Yup, this is null.
2645myNull = 0 / 0
2646notNull = !myNull
2647"#;
2648 assert_eq!(
2649 parse_execute(code1).await.unwrap_err().message(),
2650 "Cannot apply unary operator ! to non-boolean value: a number",
2651 );
2652
2653 let code2 = "notZero = !0";
2654 assert_eq!(
2655 parse_execute(code2).await.unwrap_err().message(),
2656 "Cannot apply unary operator ! to non-boolean value: a number",
2657 );
2658
2659 let code3 = r#"
2660notEmptyString = !""
2661"#;
2662 assert_eq!(
2663 parse_execute(code3).await.unwrap_err().message(),
2664 "Cannot apply unary operator ! to non-boolean value: a string",
2665 );
2666
2667 let code4 = r#"
2668obj = { a = 1 }
2669notMember = !obj.a
2670"#;
2671 assert_eq!(
2672 parse_execute(code4).await.unwrap_err().message(),
2673 "Cannot apply unary operator ! to non-boolean value: a number",
2674 );
2675
2676 let code5 = "
2677a = []
2678notArray = !a";
2679 assert_eq!(
2680 parse_execute(code5).await.unwrap_err().message(),
2681 "Cannot apply unary operator ! to non-boolean value: an empty array",
2682 );
2683
2684 let code6 = "
2685x = {}
2686notObject = !x";
2687 assert_eq!(
2688 parse_execute(code6).await.unwrap_err().message(),
2689 "Cannot apply unary operator ! to non-boolean value: an object",
2690 );
2691
2692 let code7 = "
2693fn x() { return 1 }
2694notFunction = !x";
2695 let fn_err = parse_execute(code7).await.unwrap_err();
2696 assert!(
2699 fn_err
2700 .message()
2701 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2702 "Actual error: {fn_err:?}"
2703 );
2704
2705 let code8 = "
2706myTagDeclarator = $myTag
2707notTagDeclarator = !myTagDeclarator";
2708 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2709 assert!(
2712 tag_declarator_err
2713 .message()
2714 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2715 "Actual error: {tag_declarator_err:?}"
2716 );
2717
2718 let code9 = "
2719myTagDeclarator = $myTag
2720notTagIdentifier = !myTag";
2721 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2722 assert!(
2725 tag_identifier_err
2726 .message()
2727 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2728 "Actual error: {tag_identifier_err:?}"
2729 );
2730
2731 let code10 = "notPipe = !(1 |> 2)";
2732 assert_eq!(
2733 parse_execute(code10).await.unwrap_err(),
2736 KclError::new_syntax(KclErrorDetails::new(
2737 "Unexpected token: !".to_owned(),
2738 vec![SourceRange::new(10, 11, ModuleId::default())],
2739 ))
2740 );
2741
2742 let code11 = "
2743fn identity(x) { return x }
2744notPipeSub = 1 |> identity(!%))";
2745 assert_eq!(
2746 parse_execute(code11).await.unwrap_err(),
2749 KclError::new_syntax(KclErrorDetails::new(
2750 "There was an unexpected `!`. Try removing it.".to_owned(),
2751 vec![SourceRange::new(56, 57, ModuleId::default())],
2752 ))
2753 );
2754
2755 }
2759
2760 #[tokio::test(flavor = "multi_thread")]
2761 async fn test_start_sketch_on_invalid_kwargs() {
2762 let current_dir = std::env::current_dir().unwrap();
2763 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2764 let mut code = std::fs::read_to_string(&path).unwrap();
2765 assert_eq!(
2766 parse_execute(&code).await.unwrap_err().message(),
2767 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2768 );
2769
2770 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2771 code = std::fs::read_to_string(&path).unwrap();
2772
2773 assert_eq!(
2774 parse_execute(&code).await.unwrap_err().message(),
2775 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2776 );
2777
2778 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2779 code = std::fs::read_to_string(&path).unwrap();
2780
2781 assert_eq!(
2782 parse_execute(&code).await.unwrap_err().message(),
2783 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2784 );
2785
2786 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2787 code = std::fs::read_to_string(&path).unwrap();
2788
2789 assert_eq!(
2790 parse_execute(&code).await.unwrap_err().message(),
2791 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2792 );
2793
2794 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2795 code = std::fs::read_to_string(&path).unwrap();
2796
2797 assert_eq!(
2798 parse_execute(&code).await.unwrap_err().message(),
2799 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2800 );
2801 }
2802
2803 #[tokio::test(flavor = "multi_thread")]
2804 async fn test_math_negative_variable_in_binary_expression() {
2805 let ast = r#"sigmaAllow = 35000 // psi
2806width = 1 // inch
2807
2808p = 150 // lbs
2809distance = 6 // inches
2810FOS = 2
2811
2812leg1 = 5 // inches
2813leg2 = 8 // inches
2814
2815thickness_squared = distance * p * FOS * 6 / sigmaAllow
2816thickness = 0.56 // inches. App does not support square root function yet
2817
2818bracket = startSketchOn(XY)
2819 |> startProfile(at = [0,0])
2820 |> line(end = [0, leg1])
2821 |> line(end = [leg2, 0])
2822 |> line(end = [0, -thickness])
2823 |> line(end = [-leg2 + thickness, 0])
2824"#;
2825 parse_execute(ast).await.unwrap();
2826 }
2827
2828 #[tokio::test(flavor = "multi_thread")]
2829 async fn test_execute_function_no_return() {
2830 let ast = r#"fn test(@origin) {
2831 origin
2832}
2833
2834test([0, 0])
2835"#;
2836 let result = parse_execute(ast).await;
2837 assert!(result.is_err());
2838 assert!(result.unwrap_err().to_string().contains("undefined"));
2839 }
2840
2841 #[tokio::test(flavor = "multi_thread")]
2842 async fn test_max_stack_size_exceeded_error() {
2843 let ast = r#"
2844fn forever(@n) {
2845 return 1 + forever(n)
2846}
2847
2848forever(1)
2849"#;
2850 let result = parse_execute(ast).await;
2851 let err = result.unwrap_err();
2852 assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
2853 }
2854
2855 #[tokio::test(flavor = "multi_thread")]
2856 async fn test_math_doubly_nested_parens() {
2857 let ast = r#"sigmaAllow = 35000 // psi
2858width = 4 // inch
2859p = 150 // Force on shelf - lbs
2860distance = 6 // inches
2861FOS = 2
2862leg1 = 5 // inches
2863leg2 = 8 // inches
2864thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2865thickness = 0.32 // inches. App does not support square root function yet
2866bracket = startSketchOn(XY)
2867 |> startProfile(at = [0,0])
2868 |> line(end = [0, leg1])
2869 |> line(end = [leg2, 0])
2870 |> line(end = [0, -thickness])
2871 |> line(end = [-1 * leg2 + thickness, 0])
2872 |> line(end = [0, -1 * leg1 + thickness])
2873 |> close()
2874 |> extrude(length = width)
2875"#;
2876 parse_execute(ast).await.unwrap();
2877 }
2878
2879 #[tokio::test(flavor = "multi_thread")]
2880 async fn test_math_nested_parens_one_less() {
2881 let ast = r#" sigmaAllow = 35000 // psi
2882width = 4 // inch
2883p = 150 // Force on shelf - lbs
2884distance = 6 // inches
2885FOS = 2
2886leg1 = 5 // inches
2887leg2 = 8 // inches
2888thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2889thickness = 0.32 // inches. App does not support square root function yet
2890bracket = startSketchOn(XY)
2891 |> startProfile(at = [0,0])
2892 |> line(end = [0, leg1])
2893 |> line(end = [leg2, 0])
2894 |> line(end = [0, -thickness])
2895 |> line(end = [-1 * leg2 + thickness, 0])
2896 |> line(end = [0, -1 * leg1 + thickness])
2897 |> close()
2898 |> extrude(length = width)
2899"#;
2900 parse_execute(ast).await.unwrap();
2901 }
2902
2903 #[tokio::test(flavor = "multi_thread")]
2904 async fn test_fn_as_operand() {
2905 let ast = r#"fn f() { return 1 }
2906x = f()
2907y = x + 1
2908z = f() + 1
2909w = f() + f()
2910"#;
2911 parse_execute(ast).await.unwrap();
2912 }
2913
2914 #[tokio::test(flavor = "multi_thread")]
2915 async fn kcl_test_ids_stable_between_executions() {
2916 let code = r#"sketch001 = startSketchOn(XZ)
2917|> startProfile(at = [61.74, 206.13])
2918|> xLine(length = 305.11, tag = $seg01)
2919|> yLine(length = -291.85)
2920|> xLine(length = -segLen(seg01))
2921|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2922|> close()
2923|> extrude(length = 40.14)
2924|> shell(
2925 thickness = 3.14,
2926 faces = [seg01]
2927)
2928"#;
2929
2930 let ctx = crate::test_server::new_context(true, None).await.unwrap();
2931 let old_program = crate::Program::parse_no_errs(code).unwrap();
2932
2933 if let Err(err) = ctx.run_with_caching(old_program).await {
2935 let report = err.into_miette_report_with_outputs(code).unwrap();
2936 let report = miette::Report::new(report);
2937 panic!("Error executing program: {report:?}");
2938 }
2939
2940 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2942
2943 let code = r#"sketch001 = startSketchOn(XZ)
2944|> startProfile(at = [62.74, 206.13])
2945|> xLine(length = 305.11, tag = $seg01)
2946|> yLine(length = -291.85)
2947|> xLine(length = -segLen(seg01))
2948|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2949|> close()
2950|> extrude(length = 40.14)
2951|> shell(
2952 faces = [seg01],
2953 thickness = 3.14,
2954)
2955"#;
2956
2957 let program = crate::Program::parse_no_errs(code).unwrap();
2959 ctx.run_with_caching(program).await.unwrap();
2961
2962 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2963
2964 assert_eq!(id_generator, new_id_generator);
2965 }
2966
2967 #[tokio::test(flavor = "multi_thread")]
2968 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2969 let code = r#"sketch001 = startSketchOn(XZ)
2970|> startProfile(at = [61.74, 206.13])
2971|> xLine(length = 305.11, tag = $seg01)
2972|> yLine(length = -291.85)
2973|> xLine(length = -segLen(seg01))
2974|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2975|> close()
2976|> extrude(length = 40.14)
2977|> shell(
2978 thickness = 3.14,
2979 faces = [seg01]
2980)
2981"#;
2982
2983 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2984 let old_program = crate::Program::parse_no_errs(code).unwrap();
2985
2986 ctx.run_with_caching(old_program.clone()).await.unwrap();
2988
2989 let settings_state = cache::read_old_ast().await.unwrap().settings;
2990
2991 assert_eq!(settings_state, ctx.settings);
2993
2994 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2996
2997 ctx.run_with_caching(old_program.clone()).await.unwrap();
2999
3000 let settings_state = cache::read_old_ast().await.unwrap().settings;
3001
3002 assert_eq!(settings_state, ctx.settings);
3004
3005 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3007
3008 ctx.run_with_caching(old_program).await.unwrap();
3010
3011 let settings_state = cache::read_old_ast().await.unwrap().settings;
3012
3013 assert_eq!(settings_state, ctx.settings);
3015
3016 ctx.close().await;
3017 }
3018
3019 #[tokio::test(flavor = "multi_thread")]
3020 async fn mock_after_not_mock() {
3021 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3022 let program = crate::Program::parse_no_errs("x = 2").unwrap();
3023 let result = ctx.run_with_caching(program).await.unwrap();
3024 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
3025
3026 let ctx2 = ExecutorContext::new_mock(None).await;
3027 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
3028 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
3029 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
3030
3031 ctx.close().await;
3032 ctx2.close().await;
3033 }
3034
3035 #[tokio::test(flavor = "multi_thread")]
3036 async fn mock_then_add_extrude_then_mock_again() {
3037 let code = "@settings(experimentalFeatures = allow)
3038
3039s = sketch(on = XY) {
3040 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
3041 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
3042 coincident([line1.end, line2.start])
3043 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
3044 coincident([line2.end, line3.start])
3045 coincident([line1.start, line3.end])
3046}
3047 ";
3048 let ctx = ExecutorContext::new_mock(None).await;
3049 let program = crate::Program::parse_no_errs(code).unwrap();
3050 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3051 assert!(result.variables.contains_key("s"), "actual: {:?}", &result.variables);
3052
3053 let code2 = code.to_owned()
3054 + "
3055region001 = region(point = [1mm, 1mm], sketch = s)
3056extrude001 = extrude(region001, length = 1)
3057 ";
3058 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3059 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
3060 assert!(
3061 result.variables.contains_key("region001"),
3062 "actual: {:?}",
3063 &result.variables
3064 );
3065
3066 ctx.close().await;
3067 }
3068
3069 #[cfg(feature = "artifact-graph")]
3070 #[tokio::test(flavor = "multi_thread")]
3071 async fn mock_has_stable_ids() {
3072 let ctx = ExecutorContext::new_mock(None).await;
3073 let mock_config = MockConfig {
3074 use_prev_memory: false,
3075 ..Default::default()
3076 };
3077 let code = "sk = startSketchOn(XY)
3078 |> startProfile(at = [0, 0])";
3079 let program = crate::Program::parse_no_errs(code).unwrap();
3080 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
3081 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3082 assert!(!ids.is_empty(), "IDs should not be empty");
3083
3084 let ctx2 = ExecutorContext::new_mock(None).await;
3085 let program2 = crate::Program::parse_no_errs(code).unwrap();
3086 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
3087 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3088
3089 assert_eq!(ids, ids2, "Generated IDs should match");
3090 ctx.close().await;
3091 ctx2.close().await;
3092 }
3093
3094 #[tokio::test(flavor = "multi_thread")]
3095 async fn mock_memory_restore_preserves_module_maps() {
3096 clear_mem_cache().await;
3097
3098 let ctx = ExecutorContext::new_mock(None).await;
3099 let cold_start = MockConfig {
3100 use_prev_memory: false,
3101 ..Default::default()
3102 };
3103 ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
3104
3105 let mem = cache::read_old_memory().await.unwrap();
3106 assert!(
3107 mem.path_to_source_id.len() > 3,
3108 "expected prelude imports to populate multiple modules, got {:?}",
3109 mem.path_to_source_id
3110 );
3111
3112 let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
3113 ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
3114
3115 assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
3116 assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
3117 assert_eq!(exec_state.global.module_infos, mem.module_infos);
3118
3119 clear_mem_cache().await;
3120 ctx.close().await;
3121 }
3122
3123 #[cfg(feature = "artifact-graph")]
3124 #[tokio::test(flavor = "multi_thread")]
3125 async fn sim_sketch_mode_real_mock_real() {
3126 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3127 let code = r#"sketch001 = startSketchOn(XY)
3128profile001 = startProfile(sketch001, at = [0, 0])
3129 |> line(end = [10, 0])
3130 |> line(end = [0, 10])
3131 |> line(end = [-10, 0])
3132 |> line(end = [0, -10])
3133 |> close()
3134"#;
3135 let program = crate::Program::parse_no_errs(code).unwrap();
3136 let result = ctx.run_with_caching(program).await.unwrap();
3137 assert_eq!(result.operations.len(), 1);
3138
3139 let mock_ctx = ExecutorContext::new_mock(None).await;
3140 let mock_program = crate::Program::parse_no_errs(code).unwrap();
3141 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
3142 assert_eq!(mock_result.operations.len(), 1);
3143
3144 let code2 = code.to_owned()
3145 + r#"
3146extrude001 = extrude(profile001, length = 10)
3147"#;
3148 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3149 let result = ctx.run_with_caching(program2).await.unwrap();
3150 assert_eq!(result.operations.len(), 2);
3151
3152 ctx.close().await;
3153 mock_ctx.close().await;
3154 }
3155
3156 #[tokio::test(flavor = "multi_thread")]
3157 async fn read_tag_version() {
3158 let ast = r#"fn bar(@t) {
3159 return startSketchOn(XY)
3160 |> startProfile(at = [0,0])
3161 |> angledLine(
3162 angle = -60,
3163 length = segLen(t),
3164 )
3165 |> line(end = [0, 0])
3166 |> close()
3167}
3168
3169sketch = startSketchOn(XY)
3170 |> startProfile(at = [0,0])
3171 |> line(end = [0, 10])
3172 |> line(end = [10, 0], tag = $tag0)
3173 |> line(endAbsolute = [0, 0])
3174
3175fn foo() {
3176 // tag0 tags an edge
3177 return bar(tag0)
3178}
3179
3180solid = sketch |> extrude(length = 10)
3181// tag0 tags a face
3182sketch2 = startSketchOn(solid, face = tag0)
3183 |> startProfile(at = [0,0])
3184 |> line(end = [0, 1])
3185 |> line(end = [1, 0])
3186 |> line(end = [0, 0])
3187
3188foo() |> extrude(length = 1)
3189"#;
3190 parse_execute(ast).await.unwrap();
3191 }
3192
3193 #[tokio::test(flavor = "multi_thread")]
3194 async fn experimental() {
3195 let code = r#"
3196startSketchOn(XY)
3197 |> startProfile(at = [0, 0], tag = $start)
3198 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3199"#;
3200 let result = parse_execute(code).await.unwrap();
3201 let issues = result.exec_state.issues();
3202 assert_eq!(issues.len(), 1);
3203 assert_eq!(issues[0].severity, Severity::Error);
3204 let msg = &issues[0].message;
3205 assert!(msg.contains("experimental"), "found {msg}");
3206
3207 let code = r#"@settings(experimentalFeatures = allow)
3208startSketchOn(XY)
3209 |> startProfile(at = [0, 0], tag = $start)
3210 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3211"#;
3212 let result = parse_execute(code).await.unwrap();
3213 let issues = result.exec_state.issues();
3214 assert!(issues.is_empty(), "issues={issues:#?}");
3215
3216 let code = r#"@settings(experimentalFeatures = warn)
3217startSketchOn(XY)
3218 |> startProfile(at = [0, 0], tag = $start)
3219 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3220"#;
3221 let result = parse_execute(code).await.unwrap();
3222 let issues = result.exec_state.issues();
3223 assert_eq!(issues.len(), 1);
3224 assert_eq!(issues[0].severity, Severity::Warning);
3225 let msg = &issues[0].message;
3226 assert!(msg.contains("experimental"), "found {msg}");
3227
3228 let code = r#"@settings(experimentalFeatures = deny)
3229startSketchOn(XY)
3230 |> startProfile(at = [0, 0], tag = $start)
3231 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3232"#;
3233 let result = parse_execute(code).await.unwrap();
3234 let issues = result.exec_state.issues();
3235 assert_eq!(issues.len(), 1);
3236 assert_eq!(issues[0].severity, Severity::Error);
3237 let msg = &issues[0].message;
3238 assert!(msg.contains("experimental"), "found {msg}");
3239
3240 let code = r#"@settings(experimentalFeatures = foo)
3241startSketchOn(XY)
3242 |> startProfile(at = [0, 0], tag = $start)
3243 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3244"#;
3245 parse_execute(code).await.unwrap_err();
3246 }
3247
3248 #[tokio::test(flavor = "multi_thread")]
3249 async fn experimental_parameter() {
3250 let code = r#"
3251fn inc(@x, @(experimental = true) amount? = 1) {
3252 return x + amount
3253}
3254
3255answer = inc(5, amount = 2)
3256"#;
3257 let result = parse_execute(code).await.unwrap();
3258 let issues = result.exec_state.issues();
3259 assert_eq!(issues.len(), 1);
3260 assert_eq!(issues[0].severity, Severity::Error);
3261 let msg = &issues[0].message;
3262 assert!(msg.contains("experimental"), "found {msg}");
3263
3264 let code = r#"
3266fn inc(@x, @(experimental = true) amount? = 1) {
3267 return x + amount
3268}
3269
3270answer = inc(5)
3271"#;
3272 let result = parse_execute(code).await.unwrap();
3273 let issues = result.exec_state.issues();
3274 assert!(issues.is_empty(), "issues={issues:#?}");
3275 }
3276
3277 #[tokio::test(flavor = "multi_thread")]
3278 async fn experimental_scalar_fixed_constraint() {
3279 let code_left = r#"@settings(experimentalFeatures = warn)
3280sketch(on = XY) {
3281 point1 = point(at = [var 0mm, var 0mm])
3282 point1.at[0] == 1mm
3283}
3284"#;
3285 let code_right = r#"@settings(experimentalFeatures = warn)
3287sketch(on = XY) {
3288 point1 = point(at = [var 0mm, var 0mm])
3289 1mm == point1.at[0]
3290}
3291"#;
3292
3293 for code in [code_left, code_right] {
3294 let result = parse_execute(code).await.unwrap();
3295 let issues = result.exec_state.issues();
3296 let Some(error) = issues
3297 .iter()
3298 .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
3299 else {
3300 panic!("found {issues:#?}");
3301 };
3302 assert_eq!(error.severity, Severity::Warning);
3303 }
3304 }
3305
3306 #[tokio::test(flavor = "multi_thread")]
3310 async fn test_tangent_line_arc_executes_with_mock_engine() {
3311 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
3312 parse_execute(&code).await.unwrap();
3313 }
3314
3315 #[tokio::test(flavor = "multi_thread")]
3316 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
3317 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
3318 parse_execute(&code).await.unwrap();
3319 }
3320
3321 #[tokio::test(flavor = "multi_thread")]
3322 async fn test_tangent_line_circle_executes_with_mock_engine() {
3323 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
3324 parse_execute(&code).await.unwrap();
3325 }
3326
3327 #[tokio::test(flavor = "multi_thread")]
3328 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
3329 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
3330 parse_execute(&code).await.unwrap();
3331 }
3332
3333 }