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