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