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