1use std::sync::Arc;
4
5use anyhow::Result;
6#[cfg(feature = "artifact-graph")]
7pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketchOnFace, StartSketchOnPlane};
8use cache::GlobalState;
9pub use cache::{bust_cache, clear_mem_cache};
10#[cfg(feature = "artifact-graph")]
11pub use cad_op::Group;
12pub use cad_op::Operation;
13pub use geometry::*;
14pub use id_generator::IdGenerator;
15pub(crate) use import::PreImportedGeometry;
16use indexmap::IndexMap;
17pub use kcl_value::{KclObjectFields, KclValue};
18use kcmc::{
19 ImageFormat, ModelingCmd, each_cmd as mcmd,
20 ok_response::{OkModelingCmdResponse, output::TakeSnapshot},
21 websocket::{ModelingSessionData, OkWebSocketResponseData},
22};
23use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
24pub use memory::EnvironmentRef;
25pub(crate) use modeling::ModelingCmdMeta;
26use serde::{Deserialize, Serialize};
27pub(crate) use state::ModuleArtifactState;
28pub use state::{ExecState, MetaSettings};
29use uuid::Uuid;
30
31use crate::{
32 CompilationError, ExecError, KclErrorWithOutputs, SourceRange,
33 engine::{EngineManager, GridScaleBehavior},
34 errors::{KclError, KclErrorDetails},
35 execution::{
36 cache::{CacheInformation, CacheResult},
37 import_graph::{Universe, UniverseMap},
38 typed_path::TypedPath,
39 types::{UnitAngle, UnitLen},
40 },
41 fs::FileManager,
42 modules::{ModuleId, ModulePath, ModuleRepr},
43 parsing::ast::types::{Expr, ImportPath, NodeRef},
44};
45
46pub(crate) mod annotations;
47#[cfg(feature = "artifact-graph")]
48mod artifact;
49pub(crate) mod cache;
50mod cad_op;
51mod exec_ast;
52pub mod fn_call;
53mod geometry;
54mod id_generator;
55mod import;
56mod import_graph;
57pub(crate) mod kcl_value;
58mod memory;
59mod modeling;
60mod state;
61pub mod typed_path;
62pub(crate) mod types;
63
64enum StatementKind<'a> {
65 Declaration { name: &'a str },
66 Expression,
67}
68
69#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
71#[ts(export)]
72#[serde(rename_all = "camelCase")]
73pub struct ExecOutcome {
74 pub variables: IndexMap<String, KclValue>,
76 #[cfg(feature = "artifact-graph")]
79 pub operations: Vec<Operation>,
80 #[cfg(feature = "artifact-graph")]
82 pub artifact_graph: ArtifactGraph,
83 pub errors: Vec<CompilationError>,
85 pub filenames: IndexMap<ModuleId, ModulePath>,
87 pub default_planes: Option<DefaultPlanes>,
89}
90
91#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
92#[ts(export)]
93#[serde(rename_all = "camelCase")]
94pub struct DefaultPlanes {
95 pub xy: uuid::Uuid,
96 pub xz: uuid::Uuid,
97 pub yz: uuid::Uuid,
98 pub neg_xy: uuid::Uuid,
99 pub neg_xz: uuid::Uuid,
100 pub neg_yz: uuid::Uuid,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
104#[ts(export)]
105#[serde(tag = "type", rename_all = "camelCase")]
106pub struct TagIdentifier {
107 pub value: String,
108 #[serde(skip)]
111 pub info: Vec<(usize, TagEngineInfo)>,
112 #[serde(skip)]
113 pub meta: Vec<Metadata>,
114}
115
116impl TagIdentifier {
117 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
119 for (e, info) in self.info.iter().rev() {
120 if *e <= at_epoch {
121 return Some(info);
122 }
123 }
124
125 None
126 }
127
128 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
130 self.info.last().map(|i| &i.1)
131 }
132
133 pub fn merge_info(&mut self, other: &TagIdentifier) {
135 assert_eq!(&self.value, &other.value);
136 for (oe, ot) in &other.info {
137 if let Some((e, t)) = self.info.last_mut() {
138 if *e > *oe {
140 continue;
141 }
142 if e == oe {
144 *t = ot.clone();
145 continue;
146 }
147 }
148 self.info.push((*oe, ot.clone()));
149 }
150 }
151}
152
153impl Eq for TagIdentifier {}
154
155impl std::fmt::Display for TagIdentifier {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 write!(f, "{}", self.value)
158 }
159}
160
161impl std::str::FromStr for TagIdentifier {
162 type Err = KclError;
163
164 fn from_str(s: &str) -> Result<Self, Self::Err> {
165 Ok(Self {
166 value: s.to_string(),
167 info: Vec::new(),
168 meta: Default::default(),
169 })
170 }
171}
172
173impl Ord for TagIdentifier {
174 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
175 self.value.cmp(&other.value)
176 }
177}
178
179impl PartialOrd for TagIdentifier {
180 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
181 Some(self.cmp(other))
182 }
183}
184
185impl std::hash::Hash for TagIdentifier {
186 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
187 self.value.hash(state);
188 }
189}
190
191#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
193#[ts(export)]
194#[serde(tag = "type", rename_all = "camelCase")]
195pub struct TagEngineInfo {
196 pub id: uuid::Uuid,
198 pub sketch: uuid::Uuid,
200 pub path: Option<Path>,
202 pub surface: Option<ExtrudeSurface>,
204}
205
206#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
207pub enum BodyType {
208 Root,
209 Block,
210}
211
212#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
214#[ts(export)]
215#[serde(rename_all = "camelCase")]
216pub struct Metadata {
217 pub source_range: SourceRange,
219}
220
221impl From<Metadata> for Vec<SourceRange> {
222 fn from(meta: Metadata) -> Self {
223 vec![meta.source_range]
224 }
225}
226
227impl From<SourceRange> for Metadata {
228 fn from(source_range: SourceRange) -> Self {
229 Self { source_range }
230 }
231}
232
233impl<T> From<NodeRef<'_, T>> for Metadata {
234 fn from(node: NodeRef<'_, T>) -> Self {
235 Self {
236 source_range: SourceRange::new(node.start, node.end, node.module_id),
237 }
238 }
239}
240
241impl From<&Expr> for Metadata {
242 fn from(expr: &Expr) -> Self {
243 Self {
244 source_range: SourceRange::from(expr),
245 }
246 }
247}
248
249#[derive(PartialEq, Debug, Default, Clone)]
251pub enum ContextType {
252 #[default]
254 Live,
255
256 Mock,
260
261 MockCustomForwarded,
263}
264
265#[derive(Debug, Clone)]
269pub struct ExecutorContext {
270 pub engine: Arc<Box<dyn EngineManager>>,
271 pub fs: Arc<FileManager>,
272 pub settings: ExecutorSettings,
273 pub context_type: ContextType,
274}
275
276#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
278#[ts(export)]
279pub struct ExecutorSettings {
280 pub highlight_edges: bool,
282 pub enable_ssao: bool,
284 pub show_grid: bool,
286 pub replay: Option<String>,
289 pub project_directory: Option<TypedPath>,
292 pub current_file: Option<TypedPath>,
295 pub fixed_size_grid: bool,
297}
298
299impl Default for ExecutorSettings {
300 fn default() -> Self {
301 Self {
302 highlight_edges: true,
303 enable_ssao: false,
304 show_grid: false,
305 replay: None,
306 project_directory: None,
307 current_file: None,
308 fixed_size_grid: true,
309 }
310 }
311}
312
313impl From<crate::settings::types::Configuration> for ExecutorSettings {
314 fn from(config: crate::settings::types::Configuration) -> Self {
315 Self::from(config.settings)
316 }
317}
318
319impl From<crate::settings::types::Settings> for ExecutorSettings {
320 fn from(settings: crate::settings::types::Settings) -> Self {
321 Self {
322 highlight_edges: settings.modeling.highlight_edges.into(),
323 enable_ssao: settings.modeling.enable_ssao.into(),
324 show_grid: settings.modeling.show_scale_grid,
325 replay: None,
326 project_directory: None,
327 current_file: None,
328 fixed_size_grid: settings.modeling.fixed_size_grid,
329 }
330 }
331}
332
333impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
334 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
335 Self::from(config.settings.modeling)
336 }
337}
338
339impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
340 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
341 Self {
342 highlight_edges: modeling.highlight_edges.into(),
343 enable_ssao: modeling.enable_ssao.into(),
344 show_grid: modeling.show_scale_grid,
345 replay: None,
346 project_directory: None,
347 current_file: None,
348 fixed_size_grid: true,
349 }
350 }
351}
352
353impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
354 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
355 Self {
356 highlight_edges: modeling.highlight_edges.into(),
357 enable_ssao: modeling.enable_ssao.into(),
358 show_grid: Default::default(),
359 replay: None,
360 project_directory: None,
361 current_file: None,
362 fixed_size_grid: true,
363 }
364 }
365}
366
367impl ExecutorSettings {
368 pub fn with_current_file(&mut self, current_file: TypedPath) {
370 if current_file.extension() == Some("kcl") {
372 self.current_file = Some(current_file.clone());
373 if let Some(parent) = current_file.parent() {
375 self.project_directory = Some(parent);
376 } else {
377 self.project_directory = Some(TypedPath::from(""));
378 }
379 } else {
380 self.project_directory = Some(current_file.clone());
381 }
382 }
383}
384
385impl ExecutorContext {
386 #[cfg(not(target_arch = "wasm32"))]
388 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
389 let pool = std::env::var("ZOO_ENGINE_POOL").ok();
390 let (ws, _headers) = client
391 .modeling()
392 .commands_ws(
393 None,
394 None,
395 pool,
396 if settings.enable_ssao {
397 Some(kittycad::types::PostEffectType::Ssao)
398 } else {
399 None
400 },
401 settings.replay.clone(),
402 if settings.show_grid { Some(true) } else { None },
403 None,
404 None,
405 None,
406 Some(false),
407 )
408 .await?;
409
410 let engine: Arc<Box<dyn EngineManager>> =
411 Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
412
413 Ok(Self {
414 engine,
415 fs: Arc::new(FileManager::new()),
416 settings,
417 context_type: ContextType::Live,
418 })
419 }
420
421 #[cfg(target_arch = "wasm32")]
422 pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
423 ExecutorContext {
424 engine,
425 fs,
426 settings,
427 context_type: ContextType::Live,
428 }
429 }
430
431 #[cfg(not(target_arch = "wasm32"))]
432 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
433 ExecutorContext {
434 engine: Arc::new(Box::new(
435 crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
436 )),
437 fs: Arc::new(FileManager::new()),
438 settings: settings.unwrap_or_default(),
439 context_type: ContextType::Mock,
440 }
441 }
442
443 #[cfg(target_arch = "wasm32")]
444 pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
445 ExecutorContext {
446 engine,
447 fs,
448 settings,
449 context_type: ContextType::Mock,
450 }
451 }
452
453 #[cfg(not(target_arch = "wasm32"))]
454 pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
455 ExecutorContext {
456 engine,
457 fs: Arc::new(FileManager::new()),
458 settings: Default::default(),
459 context_type: ContextType::MockCustomForwarded,
460 }
461 }
462
463 #[cfg(not(target_arch = "wasm32"))]
469 pub async fn new_with_client(
470 settings: ExecutorSettings,
471 token: Option<String>,
472 engine_addr: Option<String>,
473 ) -> Result<Self> {
474 let client = crate::engine::new_zoo_client(token, engine_addr)?;
476
477 let ctx = Self::new(&client, settings).await?;
478 Ok(ctx)
479 }
480
481 #[cfg(not(target_arch = "wasm32"))]
486 pub async fn new_with_default_client() -> Result<Self> {
487 let ctx = Self::new_with_client(Default::default(), None, None).await?;
489 Ok(ctx)
490 }
491
492 #[cfg(not(target_arch = "wasm32"))]
494 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
495 let ctx = ExecutorContext::new_with_client(
496 ExecutorSettings {
497 highlight_edges: true,
498 enable_ssao: false,
499 show_grid: false,
500 replay: None,
501 project_directory: None,
502 current_file: None,
503 fixed_size_grid: false,
504 },
505 None,
506 engine_addr,
507 )
508 .await?;
509 Ok(ctx)
510 }
511
512 pub fn is_mock(&self) -> bool {
513 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
514 }
515
516 pub async fn no_engine_commands(&self) -> bool {
518 self.is_mock()
519 }
520
521 pub async fn send_clear_scene(
522 &self,
523 exec_state: &mut ExecState,
524 source_range: crate::execution::SourceRange,
525 ) -> Result<(), KclError> {
526 exec_state.mod_local.artifacts.clear();
529 exec_state.global.root_module_artifacts.clear();
530 exec_state.global.artifacts.clear();
531
532 self.engine
533 .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
534 .await
535 }
536
537 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
538 cache::bust_cache().await;
539
540 let outcome = self.run_with_caching(crate::Program::empty()).await?;
545
546 Ok(outcome)
547 }
548
549 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
550 self.eval_prelude(exec_state, SourceRange::synthetic())
551 .await
552 .map_err(KclErrorWithOutputs::no_outputs)?;
553 exec_state.mut_stack().push_new_root_env(true);
554 Ok(())
555 }
556
557 pub async fn run_mock(
558 &self,
559 program: &crate::Program,
560 use_prev_memory: bool,
561 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
562 assert!(
563 self.is_mock(),
564 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
565 );
566
567 let mut exec_state = ExecState::new(self);
568 if use_prev_memory {
569 match cache::read_old_memory().await {
570 Some(mem) => {
571 *exec_state.mut_stack() = mem.0;
572 exec_state.global.module_infos = mem.1;
573 }
574 None => self.prepare_mem(&mut exec_state).await?,
575 }
576 } else {
577 self.prepare_mem(&mut exec_state).await?
578 };
579
580 exec_state.mut_stack().push_new_env_for_scope();
583
584 let result = self.inner_run(program, &mut exec_state, true).await?;
585
586 let mut mem = exec_state.stack().clone();
591 let module_infos = exec_state.global.module_infos.clone();
592 let outcome = exec_state.into_exec_outcome(result.0, self).await;
593
594 mem.squash_env(result.0);
595 cache::write_old_memory((mem, module_infos)).await;
596
597 Ok(outcome)
598 }
599
600 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
601 assert!(!self.is_mock());
602 let grid_scale = if self.settings.fixed_size_grid {
603 GridScaleBehavior::Fixed(
604 program
605 .meta_settings()
606 .ok()
607 .flatten()
608 .map(|s| s.default_length_units)
609 .map(kcmc::units::UnitLength::from),
610 )
611 } else {
612 GridScaleBehavior::ScaleWithZoom
613 };
614
615 let (program, exec_state, result) = match cache::read_old_ast().await {
616 Some(mut cached_state) => {
617 let old = CacheInformation {
618 ast: &cached_state.main.ast,
619 settings: &cached_state.settings,
620 };
621 let new = CacheInformation {
622 ast: &program.ast,
623 settings: &self.settings,
624 };
625
626 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
628 CacheResult::ReExecute {
629 clear_scene,
630 reapply_settings,
631 program: changed_program,
632 } => {
633 if reapply_settings
634 && self
635 .engine
636 .reapply_settings(
637 &self.settings,
638 Default::default(),
639 &mut cached_state.main.exec_state.id_generator,
640 grid_scale,
641 )
642 .await
643 .is_err()
644 {
645 (true, program, None)
646 } else {
647 (
648 clear_scene,
649 crate::Program {
650 ast: changed_program,
651 original_file_contents: program.original_file_contents,
652 },
653 None,
654 )
655 }
656 }
657 CacheResult::CheckImportsOnly {
658 reapply_settings,
659 ast: changed_program,
660 } => {
661 if reapply_settings
662 && self
663 .engine
664 .reapply_settings(
665 &self.settings,
666 Default::default(),
667 &mut cached_state.main.exec_state.id_generator,
668 grid_scale,
669 )
670 .await
671 .is_err()
672 {
673 (true, program, None)
674 } else {
675 let mut new_exec_state = ExecState::new(self);
677 let (new_universe, new_universe_map) =
678 self.get_universe(&program, &mut new_exec_state).await?;
679
680 let clear_scene = new_universe.values().any(|value| {
681 let id = value.1;
682 match (
683 cached_state.exec_state.get_source(id),
684 new_exec_state.global.get_source(id),
685 ) {
686 (Some(s0), Some(s1)) => s0.source != s1.source,
687 _ => false,
688 }
689 });
690
691 if !clear_scene {
692 return Ok(cached_state.into_exec_outcome(self).await);
694 }
695
696 (
697 true,
698 crate::Program {
699 ast: changed_program,
700 original_file_contents: program.original_file_contents,
701 },
702 Some((new_universe, new_universe_map, new_exec_state)),
703 )
704 }
705 }
706 CacheResult::NoAction(true) => {
707 if self
708 .engine
709 .reapply_settings(
710 &self.settings,
711 Default::default(),
712 &mut cached_state.main.exec_state.id_generator,
713 grid_scale,
714 )
715 .await
716 .is_ok()
717 {
718 cache::write_old_ast(GlobalState::with_settings(
720 cached_state.clone(),
721 self.settings.clone(),
722 ))
723 .await;
724
725 return Ok(cached_state.into_exec_outcome(self).await);
726 }
727 (true, program, None)
728 }
729 CacheResult::NoAction(false) => {
730 return Ok(cached_state.into_exec_outcome(self).await);
731 }
732 };
733
734 let (exec_state, result) = match import_check_info {
735 Some((new_universe, new_universe_map, mut new_exec_state)) => {
736 self.send_clear_scene(&mut new_exec_state, Default::default())
738 .await
739 .map_err(KclErrorWithOutputs::no_outputs)?;
740
741 let result = self
742 .run_concurrent(
743 &program,
744 &mut new_exec_state,
745 Some((new_universe, new_universe_map)),
746 false,
747 )
748 .await;
749
750 (new_exec_state, result)
751 }
752 None if clear_scene => {
753 let mut exec_state = cached_state.reconstitute_exec_state();
755 exec_state.reset(self);
756
757 self.send_clear_scene(&mut exec_state, Default::default())
758 .await
759 .map_err(KclErrorWithOutputs::no_outputs)?;
760
761 let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
762
763 (exec_state, result)
764 }
765 None => {
766 let mut exec_state = cached_state.reconstitute_exec_state();
767 exec_state.mut_stack().restore_env(cached_state.main.result_env);
768
769 let result = self.run_concurrent(&program, &mut exec_state, None, true).await;
770
771 (exec_state, result)
772 }
773 };
774
775 (program, exec_state, result)
776 }
777 None => {
778 let mut exec_state = ExecState::new(self);
779 self.send_clear_scene(&mut exec_state, Default::default())
780 .await
781 .map_err(KclErrorWithOutputs::no_outputs)?;
782
783 let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
784
785 (program, exec_state, result)
786 }
787 };
788
789 if result.is_err() {
790 cache::bust_cache().await;
791 }
792
793 let result = result?;
795
796 cache::write_old_ast(GlobalState::new(
798 exec_state.clone(),
799 self.settings.clone(),
800 program.ast,
801 result.0,
802 ))
803 .await;
804
805 let outcome = exec_state.into_exec_outcome(result.0, self).await;
806 Ok(outcome)
807 }
808
809 pub async fn run(
813 &self,
814 program: &crate::Program,
815 exec_state: &mut ExecState,
816 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
817 self.run_concurrent(program, exec_state, None, false).await
818 }
819
820 pub async fn run_concurrent(
825 &self,
826 program: &crate::Program,
827 exec_state: &mut ExecState,
828 universe_info: Option<(Universe, UniverseMap)>,
829 preserve_mem: bool,
830 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
831 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
834 (universe, universe_map)
835 } else {
836 self.get_universe(program, exec_state).await?
837 };
838
839 let default_planes = self.engine.get_default_planes().read().await.clone();
840
841 self.eval_prelude(exec_state, SourceRange::synthetic())
843 .await
844 .map_err(KclErrorWithOutputs::no_outputs)?;
845
846 for modules in import_graph::import_graph(&universe, self)
847 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
848 .into_iter()
849 {
850 #[cfg(not(target_arch = "wasm32"))]
851 let mut set = tokio::task::JoinSet::new();
852
853 #[allow(clippy::type_complexity)]
854 let (results_tx, mut results_rx): (
855 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
856 tokio::sync::mpsc::Receiver<_>,
857 ) = tokio::sync::mpsc::channel(1);
858
859 for module in modules {
860 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
861 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
862 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
863 )));
864 };
865 let module_id = *module_id;
866 let module_path = module_path.clone();
867 let source_range = SourceRange::from(import_stmt);
868 let module_exec_state = exec_state.clone();
870
871 self.add_import_module_ops(
872 exec_state,
873 program,
874 module_id,
875 &module_path,
876 source_range,
877 &universe_map,
878 );
879
880 let repr = repr.clone();
881 let exec_ctxt = self.clone();
882 let results_tx = results_tx.clone();
883
884 let exec_module = async |exec_ctxt: &ExecutorContext,
885 repr: &ModuleRepr,
886 module_id: ModuleId,
887 module_path: &ModulePath,
888 exec_state: &mut ExecState,
889 source_range: SourceRange|
890 -> Result<ModuleRepr, KclError> {
891 match repr {
892 ModuleRepr::Kcl(program, _) => {
893 let result = exec_ctxt
894 .exec_module_from_ast(program, module_id, module_path, exec_state, source_range, false)
895 .await;
896
897 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
898 }
899 ModuleRepr::Foreign(geom, _) => {
900 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
901 .await
902 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
903
904 result.map(|val| {
905 ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
906 })
907 }
908 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
909 format!("Module {module_path} not found in universe"),
910 vec![source_range],
911 ))),
912 }
913 };
914
915 #[cfg(target_arch = "wasm32")]
916 {
917 wasm_bindgen_futures::spawn_local(async move {
918 let mut exec_state = module_exec_state;
919 let exec_ctxt = exec_ctxt;
920
921 let result = exec_module(
922 &exec_ctxt,
923 &repr,
924 module_id,
925 &module_path,
926 &mut exec_state,
927 source_range,
928 )
929 .await;
930
931 results_tx
932 .send((module_id, module_path, result))
933 .await
934 .unwrap_or_default();
935 });
936 }
937 #[cfg(not(target_arch = "wasm32"))]
938 {
939 set.spawn(async move {
940 let mut exec_state = module_exec_state;
941 let exec_ctxt = exec_ctxt;
942
943 let result = exec_module(
944 &exec_ctxt,
945 &repr,
946 module_id,
947 &module_path,
948 &mut exec_state,
949 source_range,
950 )
951 .await;
952
953 results_tx
954 .send((module_id, module_path, result))
955 .await
956 .unwrap_or_default();
957 });
958 }
959 }
960
961 drop(results_tx);
962
963 while let Some((module_id, _, result)) = results_rx.recv().await {
964 match result {
965 Ok(new_repr) => {
966 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
967
968 match &mut repr {
969 ModuleRepr::Kcl(_, cache) => {
970 let ModuleRepr::Kcl(_, session_data) = new_repr else {
971 unreachable!();
972 };
973 *cache = session_data;
974 }
975 ModuleRepr::Foreign(_, cache) => {
976 let ModuleRepr::Foreign(_, session_data) = new_repr else {
977 unreachable!();
978 };
979 *cache = session_data;
980 }
981 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
982 }
983
984 exec_state.global.module_infos[&module_id].restore_repr(repr);
985 }
986 Err(e) => {
987 return Err(exec_state.error_with_outputs(e, None, default_planes));
988 }
989 }
990 }
991 }
992
993 exec_state
997 .global
998 .root_module_artifacts
999 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1000
1001 self.inner_run(program, exec_state, preserve_mem).await
1002 }
1003
1004 async fn get_universe(
1007 &self,
1008 program: &crate::Program,
1009 exec_state: &mut ExecState,
1010 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1011 exec_state.add_root_module_contents(program);
1012
1013 let mut universe = std::collections::HashMap::new();
1014
1015 let default_planes = self.engine.get_default_planes().read().await.clone();
1016
1017 let root_imports = import_graph::import_universe(
1018 self,
1019 &ModulePath::Main,
1020 &ModuleRepr::Kcl(program.ast.clone(), None),
1021 &mut universe,
1022 exec_state,
1023 )
1024 .await
1025 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1026
1027 Ok((universe, root_imports))
1028 }
1029
1030 #[cfg(not(feature = "artifact-graph"))]
1031 fn add_import_module_ops(
1032 &self,
1033 _exec_state: &mut ExecState,
1034 _program: &crate::Program,
1035 _module_id: ModuleId,
1036 _module_path: &ModulePath,
1037 _source_range: SourceRange,
1038 _universe_map: &UniverseMap,
1039 ) {
1040 }
1041
1042 #[cfg(feature = "artifact-graph")]
1043 fn add_import_module_ops(
1044 &self,
1045 exec_state: &mut ExecState,
1046 program: &crate::Program,
1047 module_id: ModuleId,
1048 module_path: &ModulePath,
1049 source_range: SourceRange,
1050 universe_map: &UniverseMap,
1051 ) {
1052 match module_path {
1053 ModulePath::Main => {
1054 }
1056 ModulePath::Local { value, .. } => {
1057 if universe_map.contains_key(value) {
1060 use crate::NodePath;
1061
1062 let node_path = if source_range.is_top_level_module() {
1063 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1064 NodePath::from_range(&program.ast, cached_body_items, source_range).unwrap_or_default()
1065 } else {
1066 NodePath::placeholder()
1069 };
1070
1071 exec_state.push_op(Operation::GroupBegin {
1072 group: Group::ModuleInstance {
1073 name: value.file_name().unwrap_or_default(),
1074 module_id,
1075 },
1076 node_path,
1077 source_range,
1078 });
1079 exec_state.push_op(Operation::GroupEnd);
1083 }
1084 }
1085 ModulePath::Std { .. } => {
1086 }
1088 }
1089 }
1090
1091 async fn inner_run(
1094 &self,
1095 program: &crate::Program,
1096 exec_state: &mut ExecState,
1097 preserve_mem: bool,
1098 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1099 let _stats = crate::log::LogPerfStats::new("Interpretation");
1100
1101 let grid_scale = if self.settings.fixed_size_grid {
1103 GridScaleBehavior::Fixed(
1104 program
1105 .meta_settings()
1106 .ok()
1107 .flatten()
1108 .map(|s| s.default_length_units)
1109 .map(kcmc::units::UnitLength::from),
1110 )
1111 } else {
1112 GridScaleBehavior::ScaleWithZoom
1113 };
1114 self.engine
1115 .reapply_settings(
1116 &self.settings,
1117 Default::default(),
1118 exec_state.id_generator(),
1119 grid_scale,
1120 )
1121 .await
1122 .map_err(KclErrorWithOutputs::no_outputs)?;
1123
1124 let default_planes = self.engine.get_default_planes().read().await.clone();
1125 let result = self
1126 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1127 .await;
1128
1129 crate::log::log(format!(
1130 "Post interpretation KCL memory stats: {:#?}",
1131 exec_state.stack().memory.stats
1132 ));
1133 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1134
1135 let env_ref = result.map_err(|(err, env_ref)| exec_state.error_with_outputs(err, env_ref, default_planes))?;
1136
1137 if !self.is_mock() {
1138 let mut mem = exec_state.stack().deep_clone();
1139 mem.restore_env(env_ref);
1140 cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
1141 }
1142 let session_data = self.engine.get_session_data().await;
1143
1144 Ok((env_ref, session_data))
1145 }
1146
1147 async fn execute_and_build_graph(
1150 &self,
1151 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1152 exec_state: &mut ExecState,
1153 preserve_mem: bool,
1154 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1155 #[cfg(feature = "artifact-graph")]
1161 let start_op = exec_state.global.root_module_artifacts.operations.len();
1162
1163 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1164 .await
1165 .map_err(|e| (e, None))?;
1166
1167 let exec_result = self
1168 .exec_module_body(
1169 program,
1170 exec_state,
1171 preserve_mem,
1172 ModuleId::default(),
1173 &ModulePath::Main,
1174 )
1175 .await
1176 .map(|(_, env_ref, _, module_artifacts)| {
1177 exec_state.global.root_module_artifacts.extend(module_artifacts);
1180 env_ref
1181 })
1182 .map_err(|(err, env_ref, module_artifacts)| {
1183 if let Some(module_artifacts) = module_artifacts {
1184 exec_state.global.root_module_artifacts.extend(module_artifacts);
1187 }
1188 (err, env_ref)
1189 });
1190
1191 #[cfg(feature = "artifact-graph")]
1192 {
1193 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1195 for op in exec_state
1196 .global
1197 .root_module_artifacts
1198 .operations
1199 .iter_mut()
1200 .skip(start_op)
1201 {
1202 op.fill_node_paths(program, cached_body_items);
1203 }
1204 for module in exec_state.global.module_infos.values_mut() {
1205 if let ModuleRepr::Kcl(_, Some((_, _, _, module_artifacts))) = &mut module.repr {
1206 for op in &mut module_artifacts.operations {
1207 op.fill_node_paths(program, cached_body_items);
1208 }
1209 }
1210 }
1211 }
1212
1213 self.engine.ensure_async_commands_completed().await.map_err(|e| {
1215 match &exec_result {
1216 Ok(env_ref) => (e, Some(*env_ref)),
1217 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1219 }
1220 })?;
1221
1222 self.engine.clear_queues().await;
1225
1226 match exec_state.build_artifact_graph(&self.engine, program).await {
1227 Ok(_) => exec_result,
1228 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1229 }
1230 }
1231
1232 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1236 if exec_state.stack().memory.requires_std() {
1237 #[cfg(feature = "artifact-graph")]
1238 let initial_ops = exec_state.mod_local.artifacts.operations.len();
1239
1240 let path = vec!["std".to_owned(), "prelude".to_owned()];
1241 let resolved_path = ModulePath::from_std_import_path(&path)?;
1242 let id = self
1243 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1244 .await?;
1245 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1246
1247 exec_state.mut_stack().memory.set_std(module_memory);
1248
1249 #[cfg(feature = "artifact-graph")]
1255 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1256 }
1257
1258 Ok(())
1259 }
1260
1261 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1263 self.engine
1265 .send_modeling_cmd(
1266 uuid::Uuid::new_v4(),
1267 crate::execution::SourceRange::default(),
1268 &ModelingCmd::from(mcmd::ZoomToFit {
1269 object_ids: Default::default(),
1270 animated: false,
1271 padding: 0.1,
1272 }),
1273 )
1274 .await
1275 .map_err(KclErrorWithOutputs::no_outputs)?;
1276
1277 let resp = self
1279 .engine
1280 .send_modeling_cmd(
1281 uuid::Uuid::new_v4(),
1282 crate::execution::SourceRange::default(),
1283 &ModelingCmd::from(mcmd::TakeSnapshot {
1284 format: ImageFormat::Png,
1285 }),
1286 )
1287 .await
1288 .map_err(KclErrorWithOutputs::no_outputs)?;
1289
1290 let OkWebSocketResponseData::Modeling {
1291 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1292 } = resp
1293 else {
1294 return Err(ExecError::BadPng(format!(
1295 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1296 )));
1297 };
1298 Ok(contents)
1299 }
1300
1301 pub async fn export(
1303 &self,
1304 format: kittycad_modeling_cmds::format::OutputFormat3d,
1305 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1306 let resp = self
1307 .engine
1308 .send_modeling_cmd(
1309 uuid::Uuid::new_v4(),
1310 crate::SourceRange::default(),
1311 &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
1312 entity_ids: vec![],
1313 format,
1314 }),
1315 )
1316 .await?;
1317
1318 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1319 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1320 format!("Expected Export response, got {resp:?}",),
1321 vec![SourceRange::default()],
1322 )));
1323 };
1324
1325 Ok(files)
1326 }
1327
1328 pub async fn export_step(
1330 &self,
1331 deterministic_time: bool,
1332 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1333 let files = self
1334 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1335 kittycad_modeling_cmds::format::step::export::Options {
1336 coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1337 created: if deterministic_time {
1338 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1339 KclError::new_internal(crate::errors::KclErrorDetails::new(
1340 format!("Failed to parse date: {e}"),
1341 vec![SourceRange::default()],
1342 ))
1343 })?)
1344 } else {
1345 None
1346 },
1347 },
1348 ))
1349 .await?;
1350
1351 Ok(files)
1352 }
1353
1354 pub async fn close(&self) {
1355 self.engine.close().await;
1356 }
1357}
1358
1359#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1360pub struct ArtifactId(Uuid);
1361
1362impl ArtifactId {
1363 pub fn new(uuid: Uuid) -> Self {
1364 Self(uuid)
1365 }
1366}
1367
1368impl From<Uuid> for ArtifactId {
1369 fn from(uuid: Uuid) -> Self {
1370 Self::new(uuid)
1371 }
1372}
1373
1374impl From<&Uuid> for ArtifactId {
1375 fn from(uuid: &Uuid) -> Self {
1376 Self::new(*uuid)
1377 }
1378}
1379
1380impl From<ArtifactId> for Uuid {
1381 fn from(id: ArtifactId) -> Self {
1382 id.0
1383 }
1384}
1385
1386impl From<&ArtifactId> for Uuid {
1387 fn from(id: &ArtifactId) -> Self {
1388 id.0
1389 }
1390}
1391
1392impl From<ModelingCmdId> for ArtifactId {
1393 fn from(id: ModelingCmdId) -> Self {
1394 Self::new(*id.as_ref())
1395 }
1396}
1397
1398impl From<&ModelingCmdId> for ArtifactId {
1399 fn from(id: &ModelingCmdId) -> Self {
1400 Self::new(*id.as_ref())
1401 }
1402}
1403
1404#[cfg(test)]
1405pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1406 parse_execute_with_project_dir(code, None).await
1407}
1408
1409#[cfg(test)]
1410pub(crate) async fn parse_execute_with_project_dir(
1411 code: &str,
1412 project_directory: Option<TypedPath>,
1413) -> Result<ExecTestResults, KclError> {
1414 let program = crate::Program::parse_no_errs(code)?;
1415
1416 let exec_ctxt = ExecutorContext {
1417 engine: Arc::new(Box::new(
1418 crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
1419 KclError::new_internal(crate::errors::KclErrorDetails::new(
1420 format!("Failed to create mock engine connection: {err}"),
1421 vec![SourceRange::default()],
1422 ))
1423 })?,
1424 )),
1425 fs: Arc::new(crate::fs::FileManager::new()),
1426 settings: ExecutorSettings {
1427 project_directory,
1428 ..Default::default()
1429 },
1430 context_type: ContextType::Mock,
1431 };
1432 let mut exec_state = ExecState::new(&exec_ctxt);
1433 let result = exec_ctxt.run(&program, &mut exec_state).await?;
1434
1435 Ok(ExecTestResults {
1436 program,
1437 mem_env: result.0,
1438 exec_ctxt,
1439 exec_state,
1440 })
1441}
1442
1443#[cfg(test)]
1444#[derive(Debug)]
1445pub(crate) struct ExecTestResults {
1446 program: crate::Program,
1447 mem_env: EnvironmentRef,
1448 exec_ctxt: ExecutorContext,
1449 exec_state: ExecState,
1450}
1451
1452#[cfg(test)]
1453mod tests {
1454 use pretty_assertions::assert_eq;
1455
1456 use super::*;
1457 use crate::{
1458 ModuleId,
1459 errors::{KclErrorDetails, Severity},
1460 exec::NumericType,
1461 execution::{memory::Stack, types::RuntimeType},
1462 };
1463
1464 #[track_caller]
1466 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1467 memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1468 }
1469
1470 #[tokio::test(flavor = "multi_thread")]
1471 async fn test_execute_warn() {
1472 let text = "@blah";
1473 let result = parse_execute(text).await.unwrap();
1474 let errs = result.exec_state.errors();
1475 assert_eq!(errs.len(), 1);
1476 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1477 assert!(
1478 errs[0].message.contains("Unknown annotation"),
1479 "unexpected warning message: {}",
1480 errs[0].message
1481 );
1482 }
1483
1484 #[tokio::test(flavor = "multi_thread")]
1485 async fn test_execute_fn_definitions() {
1486 let ast = r#"fn def(@x) {
1487 return x
1488}
1489fn ghi(@x) {
1490 return x
1491}
1492fn jkl(@x) {
1493 return x
1494}
1495fn hmm(@x) {
1496 return x
1497}
1498
1499yo = 5 + 6
1500
1501abc = 3
1502identifierGuy = 5
1503part001 = startSketchOn(XY)
1504|> startProfile(at = [-1.2, 4.83])
1505|> line(end = [2.8, 0])
1506|> angledLine(angle = 100 + 100, length = 3.01)
1507|> angledLine(angle = abc, length = 3.02)
1508|> angledLine(angle = def(yo), length = 3.03)
1509|> angledLine(angle = ghi(2), length = 3.04)
1510|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1511|> close()
1512yo2 = hmm([identifierGuy + 5])"#;
1513
1514 parse_execute(ast).await.unwrap();
1515 }
1516
1517 #[tokio::test(flavor = "multi_thread")]
1518 async fn test_execute_with_pipe_substitutions_unary() {
1519 let ast = r#"myVar = 3
1520part001 = startSketchOn(XY)
1521 |> startProfile(at = [0, 0])
1522 |> line(end = [3, 4], tag = $seg01)
1523 |> line(end = [
1524 min([segLen(seg01), myVar]),
1525 -legLen(hypotenuse = segLen(seg01), leg = myVar)
1526])
1527"#;
1528
1529 parse_execute(ast).await.unwrap();
1530 }
1531
1532 #[tokio::test(flavor = "multi_thread")]
1533 async fn test_execute_with_pipe_substitutions() {
1534 let ast = r#"myVar = 3
1535part001 = startSketchOn(XY)
1536 |> startProfile(at = [0, 0])
1537 |> line(end = [3, 4], tag = $seg01)
1538 |> line(end = [
1539 min([segLen(seg01), myVar]),
1540 legLen(hypotenuse = segLen(seg01), leg = myVar)
1541])
1542"#;
1543
1544 parse_execute(ast).await.unwrap();
1545 }
1546
1547 #[tokio::test(flavor = "multi_thread")]
1548 async fn test_execute_with_inline_comment() {
1549 let ast = r#"baseThick = 1
1550armAngle = 60
1551
1552baseThickHalf = baseThick / 2
1553halfArmAngle = armAngle / 2
1554
1555arrExpShouldNotBeIncluded = [1, 2, 3]
1556objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1557
1558part001 = startSketchOn(XY)
1559 |> startProfile(at = [0, 0])
1560 |> yLine(endAbsolute = 1)
1561 |> xLine(length = 3.84) // selection-range-7ish-before-this
1562
1563variableBelowShouldNotBeIncluded = 3
1564"#;
1565
1566 parse_execute(ast).await.unwrap();
1567 }
1568
1569 #[tokio::test(flavor = "multi_thread")]
1570 async fn test_execute_with_function_literal_in_pipe() {
1571 let ast = r#"w = 20
1572l = 8
1573h = 10
1574
1575fn thing() {
1576 return -8
1577}
1578
1579firstExtrude = startSketchOn(XY)
1580 |> startProfile(at = [0,0])
1581 |> line(end = [0, l])
1582 |> line(end = [w, 0])
1583 |> line(end = [0, thing()])
1584 |> close()
1585 |> extrude(length = h)"#;
1586
1587 parse_execute(ast).await.unwrap();
1588 }
1589
1590 #[tokio::test(flavor = "multi_thread")]
1591 async fn test_execute_with_function_unary_in_pipe() {
1592 let ast = r#"w = 20
1593l = 8
1594h = 10
1595
1596fn thing(@x) {
1597 return -x
1598}
1599
1600firstExtrude = startSketchOn(XY)
1601 |> startProfile(at = [0,0])
1602 |> line(end = [0, l])
1603 |> line(end = [w, 0])
1604 |> line(end = [0, thing(8)])
1605 |> close()
1606 |> extrude(length = h)"#;
1607
1608 parse_execute(ast).await.unwrap();
1609 }
1610
1611 #[tokio::test(flavor = "multi_thread")]
1612 async fn test_execute_with_function_array_in_pipe() {
1613 let ast = r#"w = 20
1614l = 8
1615h = 10
1616
1617fn thing(@x) {
1618 return [0, -x]
1619}
1620
1621firstExtrude = startSketchOn(XY)
1622 |> startProfile(at = [0,0])
1623 |> line(end = [0, l])
1624 |> line(end = [w, 0])
1625 |> line(end = thing(8))
1626 |> close()
1627 |> extrude(length = h)"#;
1628
1629 parse_execute(ast).await.unwrap();
1630 }
1631
1632 #[tokio::test(flavor = "multi_thread")]
1633 async fn test_execute_with_function_call_in_pipe() {
1634 let ast = r#"w = 20
1635l = 8
1636h = 10
1637
1638fn other_thing(@y) {
1639 return -y
1640}
1641
1642fn thing(@x) {
1643 return other_thing(x)
1644}
1645
1646firstExtrude = startSketchOn(XY)
1647 |> startProfile(at = [0,0])
1648 |> line(end = [0, l])
1649 |> line(end = [w, 0])
1650 |> line(end = [0, thing(8)])
1651 |> close()
1652 |> extrude(length = h)"#;
1653
1654 parse_execute(ast).await.unwrap();
1655 }
1656
1657 #[tokio::test(flavor = "multi_thread")]
1658 async fn test_execute_with_function_sketch() {
1659 let ast = r#"fn box(h, l, w) {
1660 myBox = startSketchOn(XY)
1661 |> startProfile(at = [0,0])
1662 |> line(end = [0, l])
1663 |> line(end = [w, 0])
1664 |> line(end = [0, -l])
1665 |> close()
1666 |> extrude(length = h)
1667
1668 return myBox
1669}
1670
1671fnBox = box(h = 3, l = 6, w = 10)"#;
1672
1673 parse_execute(ast).await.unwrap();
1674 }
1675
1676 #[tokio::test(flavor = "multi_thread")]
1677 async fn test_get_member_of_object_with_function_period() {
1678 let ast = r#"fn box(@obj) {
1679 myBox = startSketchOn(XY)
1680 |> startProfile(at = obj.start)
1681 |> line(end = [0, obj.l])
1682 |> line(end = [obj.w, 0])
1683 |> line(end = [0, -obj.l])
1684 |> close()
1685 |> extrude(length = obj.h)
1686
1687 return myBox
1688}
1689
1690thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1691"#;
1692 parse_execute(ast).await.unwrap();
1693 }
1694
1695 #[tokio::test(flavor = "multi_thread")]
1696 #[ignore] async fn test_object_member_starting_pipeline() {
1698 let ast = r#"
1699fn test2() {
1700 return {
1701 thing: startSketchOn(XY)
1702 |> startProfile(at = [0, 0])
1703 |> line(end = [0, 1])
1704 |> line(end = [1, 0])
1705 |> line(end = [0, -1])
1706 |> close()
1707 }
1708}
1709
1710x2 = test2()
1711
1712x2.thing
1713 |> extrude(length = 10)
1714"#;
1715 parse_execute(ast).await.unwrap();
1716 }
1717
1718 #[tokio::test(flavor = "multi_thread")]
1719 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
1721 let ast = r#"fn box(obj) {
1722let myBox = startSketchOn(XY)
1723 |> startProfile(at = obj.start)
1724 |> line(end = [0, obj.l])
1725 |> line(end = [obj.w, 0])
1726 |> line(end = [0, -obj.l])
1727 |> close()
1728 |> extrude(length = obj.h)
1729
1730 return myBox
1731}
1732
1733for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1734 thisBox = box(var)
1735}"#;
1736
1737 parse_execute(ast).await.unwrap();
1738 }
1739
1740 #[tokio::test(flavor = "multi_thread")]
1741 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
1743 let ast = r#"fn box(h, l, w, start) {
1744 myBox = startSketchOn(XY)
1745 |> startProfile(at = [0,0])
1746 |> line(end = [0, l])
1747 |> line(end = [w, 0])
1748 |> line(end = [0, -l])
1749 |> close()
1750 |> extrude(length = h)
1751
1752 return myBox
1753}
1754
1755
1756for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1757 const thisBox = box(var[0], var[1], var[2], var[3])
1758}"#;
1759
1760 parse_execute(ast).await.unwrap();
1761 }
1762
1763 #[tokio::test(flavor = "multi_thread")]
1764 async fn test_get_member_of_array_with_function() {
1765 let ast = r#"fn box(@arr) {
1766 myBox =startSketchOn(XY)
1767 |> startProfile(at = arr[0])
1768 |> line(end = [0, arr[1]])
1769 |> line(end = [arr[2], 0])
1770 |> line(end = [0, -arr[1]])
1771 |> close()
1772 |> extrude(length = arr[3])
1773
1774 return myBox
1775}
1776
1777thisBox = box([[0,0], 6, 10, 3])
1778
1779"#;
1780 parse_execute(ast).await.unwrap();
1781 }
1782
1783 #[tokio::test(flavor = "multi_thread")]
1784 async fn test_function_cannot_access_future_definitions() {
1785 let ast = r#"
1786fn returnX() {
1787 // x shouldn't be defined yet.
1788 return x
1789}
1790
1791x = 5
1792
1793answer = returnX()"#;
1794
1795 let result = parse_execute(ast).await;
1796 let err = result.unwrap_err();
1797 assert_eq!(err.message(), "`x` is not defined");
1798 }
1799
1800 #[tokio::test(flavor = "multi_thread")]
1801 async fn test_override_prelude() {
1802 let text = "PI = 3.0";
1803 let result = parse_execute(text).await.unwrap();
1804 let errs = result.exec_state.errors();
1805 assert!(errs.is_empty());
1806 }
1807
1808 #[tokio::test(flavor = "multi_thread")]
1809 async fn type_aliases() {
1810 let text = r#"@settings(experimentalFeatures = allow)
1811type MyTy = [number; 2]
1812fn foo(@x: MyTy) {
1813 return x[0]
1814}
1815
1816foo([0, 1])
1817
1818type Other = MyTy | Helix
1819"#;
1820 let result = parse_execute(text).await.unwrap();
1821 let errs = result.exec_state.errors();
1822 assert!(errs.is_empty());
1823 }
1824
1825 #[tokio::test(flavor = "multi_thread")]
1826 async fn test_cannot_shebang_in_fn() {
1827 let ast = r#"
1828fn foo() {
1829 #!hello
1830 return true
1831}
1832
1833foo
1834"#;
1835
1836 let result = parse_execute(ast).await;
1837 let err = result.unwrap_err();
1838 assert_eq!(
1839 err,
1840 KclError::new_syntax(KclErrorDetails::new(
1841 "Unexpected token: #".to_owned(),
1842 vec![SourceRange::new(14, 15, ModuleId::default())],
1843 )),
1844 );
1845 }
1846
1847 #[tokio::test(flavor = "multi_thread")]
1848 async fn test_pattern_transform_function_cannot_access_future_definitions() {
1849 let ast = r#"
1850fn transform(@replicaId) {
1851 // x shouldn't be defined yet.
1852 scale = x
1853 return {
1854 translate = [0, 0, replicaId * 10],
1855 scale = [scale, 1, 0],
1856 }
1857}
1858
1859fn layer() {
1860 return startSketchOn(XY)
1861 |> circle( center= [0, 0], radius= 1, tag = $tag1)
1862 |> extrude(length = 10)
1863}
1864
1865x = 5
1866
1867// The 10 layers are replicas of each other, with a transform applied to each.
1868shape = layer() |> patternTransform(instances = 10, transform = transform)
1869"#;
1870
1871 let result = parse_execute(ast).await;
1872 let err = result.unwrap_err();
1873 assert_eq!(err.message(), "`x` is not defined",);
1874 }
1875
1876 #[tokio::test(flavor = "multi_thread")]
1879 async fn test_math_execute_with_functions() {
1880 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
1881 let result = parse_execute(ast).await.unwrap();
1882 assert_eq!(
1883 5.0,
1884 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1885 .as_f64()
1886 .unwrap()
1887 );
1888 }
1889
1890 #[tokio::test(flavor = "multi_thread")]
1891 async fn test_math_execute() {
1892 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
1893 let result = parse_execute(ast).await.unwrap();
1894 assert_eq!(
1895 7.4,
1896 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1897 .as_f64()
1898 .unwrap()
1899 );
1900 }
1901
1902 #[tokio::test(flavor = "multi_thread")]
1903 async fn test_math_execute_start_negative() {
1904 let ast = r#"myVar = -5 + 6"#;
1905 let result = parse_execute(ast).await.unwrap();
1906 assert_eq!(
1907 1.0,
1908 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1909 .as_f64()
1910 .unwrap()
1911 );
1912 }
1913
1914 #[tokio::test(flavor = "multi_thread")]
1915 async fn test_math_execute_with_pi() {
1916 let ast = r#"myVar = PI * 2"#;
1917 let result = parse_execute(ast).await.unwrap();
1918 assert_eq!(
1919 std::f64::consts::TAU,
1920 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1921 .as_f64()
1922 .unwrap()
1923 );
1924 }
1925
1926 #[tokio::test(flavor = "multi_thread")]
1927 async fn test_math_define_decimal_without_leading_zero() {
1928 let ast = r#"thing = .4 + 7"#;
1929 let result = parse_execute(ast).await.unwrap();
1930 assert_eq!(
1931 7.4,
1932 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
1933 .as_f64()
1934 .unwrap()
1935 );
1936 }
1937
1938 #[tokio::test(flavor = "multi_thread")]
1939 async fn pass_std_to_std() {
1940 let ast = r#"sketch001 = startSketchOn(XY)
1941profile001 = circle(sketch001, center = [0, 0], radius = 2)
1942extrude001 = extrude(profile001, length = 5)
1943extrudes = patternLinear3d(
1944 extrude001,
1945 instances = 3,
1946 distance = 5,
1947 axis = [1, 1, 0],
1948)
1949clone001 = map(extrudes, f = clone)
1950"#;
1951 parse_execute(ast).await.unwrap();
1952 }
1953
1954 #[tokio::test(flavor = "multi_thread")]
1955 async fn test_array_reduce_nested_array() {
1956 let code = r#"
1957fn id(@el, accum) { return accum }
1958
1959answer = reduce([], initial=[[[0,0]]], f=id)
1960"#;
1961 let result = parse_execute(code).await.unwrap();
1962 assert_eq!(
1963 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
1964 KclValue::HomArray {
1965 value: vec![KclValue::HomArray {
1966 value: vec![KclValue::HomArray {
1967 value: vec![
1968 KclValue::Number {
1969 value: 0.0,
1970 ty: NumericType::default(),
1971 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
1972 },
1973 KclValue::Number {
1974 value: 0.0,
1975 ty: NumericType::default(),
1976 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
1977 }
1978 ],
1979 ty: RuntimeType::any(),
1980 }],
1981 ty: RuntimeType::any(),
1982 }],
1983 ty: RuntimeType::any(),
1984 }
1985 );
1986 }
1987
1988 #[tokio::test(flavor = "multi_thread")]
1989 async fn test_zero_param_fn() {
1990 let ast = r#"sigmaAllow = 35000 // psi
1991leg1 = 5 // inches
1992leg2 = 8 // inches
1993fn thickness() { return 0.56 }
1994
1995bracket = startSketchOn(XY)
1996 |> startProfile(at = [0,0])
1997 |> line(end = [0, leg1])
1998 |> line(end = [leg2, 0])
1999 |> line(end = [0, -thickness()])
2000 |> line(end = [-leg2 + thickness(), 0])
2001"#;
2002 parse_execute(ast).await.unwrap();
2003 }
2004
2005 #[tokio::test(flavor = "multi_thread")]
2006 async fn test_unary_operator_not_succeeds() {
2007 let ast = r#"
2008fn returnTrue() { return !false }
2009t = true
2010f = false
2011notTrue = !t
2012notFalse = !f
2013c = !!true
2014d = !returnTrue()
2015
2016assertIs(!false, error = "expected to pass")
2017
2018fn check(x) {
2019 assertIs(!x, error = "expected argument to be false")
2020 return true
2021}
2022check(x = false)
2023"#;
2024 let result = parse_execute(ast).await.unwrap();
2025 assert_eq!(
2026 false,
2027 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2028 .as_bool()
2029 .unwrap()
2030 );
2031 assert_eq!(
2032 true,
2033 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2034 .as_bool()
2035 .unwrap()
2036 );
2037 assert_eq!(
2038 true,
2039 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2040 .as_bool()
2041 .unwrap()
2042 );
2043 assert_eq!(
2044 false,
2045 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2046 .as_bool()
2047 .unwrap()
2048 );
2049 }
2050
2051 #[tokio::test(flavor = "multi_thread")]
2052 async fn test_unary_operator_not_on_non_bool_fails() {
2053 let code1 = r#"
2054// Yup, this is null.
2055myNull = 0 / 0
2056notNull = !myNull
2057"#;
2058 assert_eq!(
2059 parse_execute(code1).await.unwrap_err().message(),
2060 "Cannot apply unary operator ! to non-boolean value: a number",
2061 );
2062
2063 let code2 = "notZero = !0";
2064 assert_eq!(
2065 parse_execute(code2).await.unwrap_err().message(),
2066 "Cannot apply unary operator ! to non-boolean value: a number",
2067 );
2068
2069 let code3 = r#"
2070notEmptyString = !""
2071"#;
2072 assert_eq!(
2073 parse_execute(code3).await.unwrap_err().message(),
2074 "Cannot apply unary operator ! to non-boolean value: a string",
2075 );
2076
2077 let code4 = r#"
2078obj = { a = 1 }
2079notMember = !obj.a
2080"#;
2081 assert_eq!(
2082 parse_execute(code4).await.unwrap_err().message(),
2083 "Cannot apply unary operator ! to non-boolean value: a number",
2084 );
2085
2086 let code5 = "
2087a = []
2088notArray = !a";
2089 assert_eq!(
2090 parse_execute(code5).await.unwrap_err().message(),
2091 "Cannot apply unary operator ! to non-boolean value: an empty array",
2092 );
2093
2094 let code6 = "
2095x = {}
2096notObject = !x";
2097 assert_eq!(
2098 parse_execute(code6).await.unwrap_err().message(),
2099 "Cannot apply unary operator ! to non-boolean value: an object",
2100 );
2101
2102 let code7 = "
2103fn x() { return 1 }
2104notFunction = !x";
2105 let fn_err = parse_execute(code7).await.unwrap_err();
2106 assert!(
2109 fn_err
2110 .message()
2111 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2112 "Actual error: {fn_err:?}"
2113 );
2114
2115 let code8 = "
2116myTagDeclarator = $myTag
2117notTagDeclarator = !myTagDeclarator";
2118 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2119 assert!(
2122 tag_declarator_err
2123 .message()
2124 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2125 "Actual error: {tag_declarator_err:?}"
2126 );
2127
2128 let code9 = "
2129myTagDeclarator = $myTag
2130notTagIdentifier = !myTag";
2131 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2132 assert!(
2135 tag_identifier_err
2136 .message()
2137 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2138 "Actual error: {tag_identifier_err:?}"
2139 );
2140
2141 let code10 = "notPipe = !(1 |> 2)";
2142 assert_eq!(
2143 parse_execute(code10).await.unwrap_err(),
2146 KclError::new_syntax(KclErrorDetails::new(
2147 "Unexpected token: !".to_owned(),
2148 vec![SourceRange::new(10, 11, ModuleId::default())],
2149 ))
2150 );
2151
2152 let code11 = "
2153fn identity(x) { return x }
2154notPipeSub = 1 |> identity(!%))";
2155 assert_eq!(
2156 parse_execute(code11).await.unwrap_err(),
2159 KclError::new_syntax(KclErrorDetails::new(
2160 "There was an unexpected `!`. Try removing it.".to_owned(),
2161 vec![SourceRange::new(56, 57, ModuleId::default())],
2162 ))
2163 );
2164
2165 }
2169
2170 #[tokio::test(flavor = "multi_thread")]
2171 async fn test_start_sketch_on_invalid_kwargs() {
2172 let current_dir = std::env::current_dir().unwrap();
2173 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2174 let mut code = std::fs::read_to_string(&path).unwrap();
2175 assert_eq!(
2176 parse_execute(&code).await.unwrap_err().message(),
2177 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2178 );
2179
2180 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2181 code = std::fs::read_to_string(&path).unwrap();
2182
2183 assert_eq!(
2184 parse_execute(&code).await.unwrap_err().message(),
2185 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2186 );
2187
2188 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2189 code = std::fs::read_to_string(&path).unwrap();
2190
2191 assert_eq!(
2192 parse_execute(&code).await.unwrap_err().message(),
2193 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2194 );
2195
2196 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2197 code = std::fs::read_to_string(&path).unwrap();
2198
2199 assert_eq!(
2200 parse_execute(&code).await.unwrap_err().message(),
2201 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2202 );
2203
2204 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2205 code = std::fs::read_to_string(&path).unwrap();
2206
2207 assert_eq!(
2208 parse_execute(&code).await.unwrap_err().message(),
2209 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2210 );
2211 }
2212
2213 #[tokio::test(flavor = "multi_thread")]
2214 async fn test_math_negative_variable_in_binary_expression() {
2215 let ast = r#"sigmaAllow = 35000 // psi
2216width = 1 // inch
2217
2218p = 150 // lbs
2219distance = 6 // inches
2220FOS = 2
2221
2222leg1 = 5 // inches
2223leg2 = 8 // inches
2224
2225thickness_squared = distance * p * FOS * 6 / sigmaAllow
2226thickness = 0.56 // inches. App does not support square root function yet
2227
2228bracket = startSketchOn(XY)
2229 |> startProfile(at = [0,0])
2230 |> line(end = [0, leg1])
2231 |> line(end = [leg2, 0])
2232 |> line(end = [0, -thickness])
2233 |> line(end = [-leg2 + thickness, 0])
2234"#;
2235 parse_execute(ast).await.unwrap();
2236 }
2237
2238 #[tokio::test(flavor = "multi_thread")]
2239 async fn test_execute_function_no_return() {
2240 let ast = r#"fn test(@origin) {
2241 origin
2242}
2243
2244test([0, 0])
2245"#;
2246 let result = parse_execute(ast).await;
2247 assert!(result.is_err());
2248 assert!(result.unwrap_err().to_string().contains("undefined"));
2249 }
2250
2251 #[tokio::test(flavor = "multi_thread")]
2252 async fn test_math_doubly_nested_parens() {
2253 let ast = r#"sigmaAllow = 35000 // psi
2254width = 4 // inch
2255p = 150 // Force on shelf - lbs
2256distance = 6 // inches
2257FOS = 2
2258leg1 = 5 // inches
2259leg2 = 8 // inches
2260thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2261thickness = 0.32 // inches. App does not support square root function yet
2262bracket = startSketchOn(XY)
2263 |> startProfile(at = [0,0])
2264 |> line(end = [0, leg1])
2265 |> line(end = [leg2, 0])
2266 |> line(end = [0, -thickness])
2267 |> line(end = [-1 * leg2 + thickness, 0])
2268 |> line(end = [0, -1 * leg1 + thickness])
2269 |> close()
2270 |> extrude(length = width)
2271"#;
2272 parse_execute(ast).await.unwrap();
2273 }
2274
2275 #[tokio::test(flavor = "multi_thread")]
2276 async fn test_math_nested_parens_one_less() {
2277 let ast = r#" sigmaAllow = 35000 // psi
2278width = 4 // inch
2279p = 150 // Force on shelf - lbs
2280distance = 6 // inches
2281FOS = 2
2282leg1 = 5 // inches
2283leg2 = 8 // inches
2284thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2285thickness = 0.32 // inches. App does not support square root function yet
2286bracket = startSketchOn(XY)
2287 |> startProfile(at = [0,0])
2288 |> line(end = [0, leg1])
2289 |> line(end = [leg2, 0])
2290 |> line(end = [0, -thickness])
2291 |> line(end = [-1 * leg2 + thickness, 0])
2292 |> line(end = [0, -1 * leg1 + thickness])
2293 |> close()
2294 |> extrude(length = width)
2295"#;
2296 parse_execute(ast).await.unwrap();
2297 }
2298
2299 #[tokio::test(flavor = "multi_thread")]
2300 async fn test_fn_as_operand() {
2301 let ast = r#"fn f() { return 1 }
2302x = f()
2303y = x + 1
2304z = f() + 1
2305w = f() + f()
2306"#;
2307 parse_execute(ast).await.unwrap();
2308 }
2309
2310 #[tokio::test(flavor = "multi_thread")]
2311 async fn kcl_test_ids_stable_between_executions() {
2312 let code = r#"sketch001 = startSketchOn(XZ)
2313|> startProfile(at = [61.74, 206.13])
2314|> xLine(length = 305.11, tag = $seg01)
2315|> yLine(length = -291.85)
2316|> xLine(length = -segLen(seg01))
2317|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2318|> close()
2319|> extrude(length = 40.14)
2320|> shell(
2321 thickness = 3.14,
2322 faces = [seg01]
2323)
2324"#;
2325
2326 let ctx = crate::test_server::new_context(true, None).await.unwrap();
2327 let old_program = crate::Program::parse_no_errs(code).unwrap();
2328
2329 if let Err(err) = ctx.run_with_caching(old_program).await {
2331 let report = err.into_miette_report_with_outputs(code).unwrap();
2332 let report = miette::Report::new(report);
2333 panic!("Error executing program: {report:?}");
2334 }
2335
2336 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2338
2339 let code = r#"sketch001 = startSketchOn(XZ)
2340|> startProfile(at = [62.74, 206.13])
2341|> xLine(length = 305.11, tag = $seg01)
2342|> yLine(length = -291.85)
2343|> xLine(length = -segLen(seg01))
2344|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2345|> close()
2346|> extrude(length = 40.14)
2347|> shell(
2348 faces = [seg01],
2349 thickness = 3.14,
2350)
2351"#;
2352
2353 let program = crate::Program::parse_no_errs(code).unwrap();
2355 ctx.run_with_caching(program).await.unwrap();
2357
2358 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2359
2360 assert_eq!(id_generator, new_id_generator);
2361 }
2362
2363 #[tokio::test(flavor = "multi_thread")]
2364 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2365 let code = r#"sketch001 = startSketchOn(XZ)
2366|> startProfile(at = [61.74, 206.13])
2367|> xLine(length = 305.11, tag = $seg01)
2368|> yLine(length = -291.85)
2369|> xLine(length = -segLen(seg01))
2370|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2371|> close()
2372|> extrude(length = 40.14)
2373|> shell(
2374 thickness = 3.14,
2375 faces = [seg01]
2376)
2377"#;
2378
2379 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2380 let old_program = crate::Program::parse_no_errs(code).unwrap();
2381
2382 ctx.run_with_caching(old_program.clone()).await.unwrap();
2384
2385 let settings_state = cache::read_old_ast().await.unwrap().settings;
2386
2387 assert_eq!(settings_state, ctx.settings);
2389
2390 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2392
2393 ctx.run_with_caching(old_program.clone()).await.unwrap();
2395
2396 let settings_state = cache::read_old_ast().await.unwrap().settings;
2397
2398 assert_eq!(settings_state, ctx.settings);
2400
2401 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2403
2404 ctx.run_with_caching(old_program).await.unwrap();
2406
2407 let settings_state = cache::read_old_ast().await.unwrap().settings;
2408
2409 assert_eq!(settings_state, ctx.settings);
2411
2412 ctx.close().await;
2413 }
2414
2415 #[tokio::test(flavor = "multi_thread")]
2416 async fn mock_after_not_mock() {
2417 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2418 let program = crate::Program::parse_no_errs("x = 2").unwrap();
2419 let result = ctx.run_with_caching(program).await.unwrap();
2420 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2421
2422 let ctx2 = ExecutorContext::new_mock(None).await;
2423 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2424 let result = ctx2.run_mock(&program2, true).await.unwrap();
2425 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2426
2427 ctx.close().await;
2428 ctx2.close().await;
2429 }
2430
2431 #[cfg(feature = "artifact-graph")]
2432 #[tokio::test(flavor = "multi_thread")]
2433 async fn mock_has_stable_ids() {
2434 let ctx = ExecutorContext::new_mock(None).await;
2435 let code = "sk = startSketchOn(XY)
2436 |> startProfile(at = [0, 0])";
2437 let program = crate::Program::parse_no_errs(code).unwrap();
2438 let result = ctx.run_mock(&program, false).await.unwrap();
2439 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2440 assert!(!ids.is_empty(), "IDs should not be empty");
2441
2442 let ctx2 = ExecutorContext::new_mock(None).await;
2443 let program2 = crate::Program::parse_no_errs(code).unwrap();
2444 let result = ctx2.run_mock(&program2, false).await.unwrap();
2445 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2446
2447 assert_eq!(ids, ids2, "Generated IDs should match");
2448 }
2449
2450 #[cfg(feature = "artifact-graph")]
2451 #[tokio::test(flavor = "multi_thread")]
2452 async fn sim_sketch_mode_real_mock_real() {
2453 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2454 let code = r#"sketch001 = startSketchOn(XY)
2455profile001 = startProfile(sketch001, at = [0, 0])
2456 |> line(end = [10, 0])
2457 |> line(end = [0, 10])
2458 |> line(end = [-10, 0])
2459 |> line(end = [0, -10])
2460 |> close()
2461"#;
2462 let program = crate::Program::parse_no_errs(code).unwrap();
2463 let result = ctx.run_with_caching(program).await.unwrap();
2464 assert_eq!(result.operations.len(), 1);
2465
2466 let mock_ctx = ExecutorContext::new_mock(None).await;
2467 let mock_program = crate::Program::parse_no_errs(code).unwrap();
2468 let mock_result = mock_ctx.run_mock(&mock_program, true).await.unwrap();
2469 assert_eq!(mock_result.operations.len(), 1);
2470
2471 let code2 = code.to_owned()
2472 + r#"
2473extrude001 = extrude(profile001, length = 10)
2474"#;
2475 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
2476 let result = ctx.run_with_caching(program2).await.unwrap();
2477 assert_eq!(result.operations.len(), 2);
2478
2479 ctx.close().await;
2480 mock_ctx.close().await;
2481 }
2482
2483 #[tokio::test(flavor = "multi_thread")]
2484 async fn read_tag_version() {
2485 let ast = r#"fn bar(@t) {
2486 return startSketchOn(XY)
2487 |> startProfile(at = [0,0])
2488 |> angledLine(
2489 angle = -60,
2490 length = segLen(t),
2491 )
2492 |> line(end = [0, 0])
2493 |> close()
2494}
2495
2496sketch = startSketchOn(XY)
2497 |> startProfile(at = [0,0])
2498 |> line(end = [0, 10])
2499 |> line(end = [10, 0], tag = $tag0)
2500 |> line(end = [0, 0])
2501
2502fn foo() {
2503 // tag0 tags an edge
2504 return bar(tag0)
2505}
2506
2507solid = sketch |> extrude(length = 10)
2508// tag0 tags a face
2509sketch2 = startSketchOn(solid, face = tag0)
2510 |> startProfile(at = [0,0])
2511 |> line(end = [0, 1])
2512 |> line(end = [1, 0])
2513 |> line(end = [0, 0])
2514
2515foo() |> extrude(length = 1)
2516"#;
2517 parse_execute(ast).await.unwrap();
2518 }
2519
2520 #[tokio::test(flavor = "multi_thread")]
2521 async fn experimental() {
2522 let code = r#"
2523startSketchOn(XY)
2524 |> startProfile(at = [0, 0], tag = $start)
2525 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2526"#;
2527 let result = parse_execute(code).await.unwrap();
2528 let errors = result.exec_state.errors();
2529 assert_eq!(errors.len(), 1);
2530 assert_eq!(errors[0].severity, Severity::Error);
2531 let msg = &errors[0].message;
2532 assert!(msg.contains("experimental"), "found {msg}");
2533
2534 let code = r#"@settings(experimentalFeatures = allow)
2535startSketchOn(XY)
2536 |> startProfile(at = [0, 0], tag = $start)
2537 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2538"#;
2539 let result = parse_execute(code).await.unwrap();
2540 let errors = result.exec_state.errors();
2541 assert!(errors.is_empty());
2542
2543 let code = r#"@settings(experimentalFeatures = warn)
2544startSketchOn(XY)
2545 |> startProfile(at = [0, 0], tag = $start)
2546 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2547"#;
2548 let result = parse_execute(code).await.unwrap();
2549 let errors = result.exec_state.errors();
2550 assert_eq!(errors.len(), 1);
2551 assert_eq!(errors[0].severity, Severity::Warning);
2552 let msg = &errors[0].message;
2553 assert!(msg.contains("experimental"), "found {msg}");
2554
2555 let code = r#"@settings(experimentalFeatures = deny)
2556startSketchOn(XY)
2557 |> startProfile(at = [0, 0], tag = $start)
2558 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2559"#;
2560 let result = parse_execute(code).await.unwrap();
2561 let errors = result.exec_state.errors();
2562 assert_eq!(errors.len(), 1);
2563 assert_eq!(errors[0].severity, Severity::Error);
2564 let msg = &errors[0].message;
2565 assert!(msg.contains("experimental"), "found {msg}");
2566
2567 let code = r#"@settings(experimentalFeatures = foo)
2568startSketchOn(XY)
2569 |> startProfile(at = [0, 0], tag = $start)
2570 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2571"#;
2572 parse_execute(code).await.unwrap_err();
2573 }
2574}