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::{ModuleExecutionOutcome, 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);
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 original_program = program.clone();
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(
792 exec_state.clone(),
793 self.settings.clone(),
794 original_program.ast,
795 result.0,
796 ))
797 .await;
798
799 let outcome = exec_state.into_exec_outcome(result.0, self).await;
800 Ok(outcome)
801 }
802
803 pub async fn run(
807 &self,
808 program: &crate::Program,
809 exec_state: &mut ExecState,
810 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
811 self.run_concurrent(program, exec_state, None, false).await
812 }
813
814 pub async fn run_concurrent(
819 &self,
820 program: &crate::Program,
821 exec_state: &mut ExecState,
822 universe_info: Option<(Universe, UniverseMap)>,
823 preserve_mem: bool,
824 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
825 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
828 (universe, universe_map)
829 } else {
830 self.get_universe(program, exec_state).await?
831 };
832
833 let default_planes = self.engine.get_default_planes().read().await.clone();
834
835 self.eval_prelude(exec_state, SourceRange::synthetic())
837 .await
838 .map_err(KclErrorWithOutputs::no_outputs)?;
839
840 for modules in import_graph::import_graph(&universe, self)
841 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
842 .into_iter()
843 {
844 #[cfg(not(target_arch = "wasm32"))]
845 let mut set = tokio::task::JoinSet::new();
846
847 #[allow(clippy::type_complexity)]
848 let (results_tx, mut results_rx): (
849 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
850 tokio::sync::mpsc::Receiver<_>,
851 ) = tokio::sync::mpsc::channel(1);
852
853 for module in modules {
854 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
855 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
856 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
857 )));
858 };
859 let module_id = *module_id;
860 let module_path = module_path.clone();
861 let source_range = SourceRange::from(import_stmt);
862 let module_exec_state = exec_state.clone();
864
865 self.add_import_module_ops(
866 exec_state,
867 &program.ast,
868 module_id,
869 &module_path,
870 source_range,
871 &universe_map,
872 );
873
874 let repr = repr.clone();
875 let exec_ctxt = self.clone();
876 let results_tx = results_tx.clone();
877
878 let exec_module = async |exec_ctxt: &ExecutorContext,
879 repr: &ModuleRepr,
880 module_id: ModuleId,
881 module_path: &ModulePath,
882 exec_state: &mut ExecState,
883 source_range: SourceRange|
884 -> Result<ModuleRepr, KclError> {
885 match repr {
886 ModuleRepr::Kcl(program, _) => {
887 let result = exec_ctxt
888 .exec_module_from_ast(program, module_id, module_path, exec_state, source_range, false)
889 .await;
890
891 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
892 }
893 ModuleRepr::Foreign(geom, _) => {
894 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
895 .await
896 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
897
898 result.map(|val| {
899 ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
900 })
901 }
902 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
903 format!("Module {module_path} not found in universe"),
904 vec![source_range],
905 ))),
906 }
907 };
908
909 #[cfg(target_arch = "wasm32")]
910 {
911 wasm_bindgen_futures::spawn_local(async move {
912 let mut exec_state = module_exec_state;
913 let exec_ctxt = exec_ctxt;
914
915 let result = exec_module(
916 &exec_ctxt,
917 &repr,
918 module_id,
919 &module_path,
920 &mut exec_state,
921 source_range,
922 )
923 .await;
924
925 results_tx
926 .send((module_id, module_path, result))
927 .await
928 .unwrap_or_default();
929 });
930 }
931 #[cfg(not(target_arch = "wasm32"))]
932 {
933 set.spawn(async move {
934 let mut exec_state = module_exec_state;
935 let exec_ctxt = exec_ctxt;
936
937 let result = exec_module(
938 &exec_ctxt,
939 &repr,
940 module_id,
941 &module_path,
942 &mut exec_state,
943 source_range,
944 )
945 .await;
946
947 results_tx
948 .send((module_id, module_path, result))
949 .await
950 .unwrap_or_default();
951 });
952 }
953 }
954
955 drop(results_tx);
956
957 while let Some((module_id, _, result)) = results_rx.recv().await {
958 match result {
959 Ok(new_repr) => {
960 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
961
962 match &mut repr {
963 ModuleRepr::Kcl(_, cache) => {
964 let ModuleRepr::Kcl(_, session_data) = new_repr else {
965 unreachable!();
966 };
967 *cache = session_data;
968 }
969 ModuleRepr::Foreign(_, cache) => {
970 let ModuleRepr::Foreign(_, session_data) = new_repr else {
971 unreachable!();
972 };
973 *cache = session_data;
974 }
975 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
976 }
977
978 exec_state.global.module_infos[&module_id].restore_repr(repr);
979 }
980 Err(e) => {
981 return Err(exec_state.error_with_outputs(e, None, default_planes));
982 }
983 }
984 }
985 }
986
987 exec_state
991 .global
992 .root_module_artifacts
993 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
994
995 self.inner_run(program, exec_state, preserve_mem).await
996 }
997
998 async fn get_universe(
1001 &self,
1002 program: &crate::Program,
1003 exec_state: &mut ExecState,
1004 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1005 exec_state.add_root_module_contents(program);
1006
1007 let mut universe = std::collections::HashMap::new();
1008
1009 let default_planes = self.engine.get_default_planes().read().await.clone();
1010
1011 let root_imports = import_graph::import_universe(
1012 self,
1013 &ModulePath::Main,
1014 &ModuleRepr::Kcl(program.ast.clone(), None),
1015 &mut universe,
1016 exec_state,
1017 )
1018 .await
1019 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1020
1021 Ok((universe, root_imports))
1022 }
1023
1024 #[cfg(not(feature = "artifact-graph"))]
1025 fn add_import_module_ops(
1026 &self,
1027 _exec_state: &mut ExecState,
1028 _program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1029 _module_id: ModuleId,
1030 _module_path: &ModulePath,
1031 _source_range: SourceRange,
1032 _universe_map: &UniverseMap,
1033 ) {
1034 }
1035
1036 #[cfg(feature = "artifact-graph")]
1037 fn add_import_module_ops(
1038 &self,
1039 exec_state: &mut ExecState,
1040 program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1041 module_id: ModuleId,
1042 module_path: &ModulePath,
1043 source_range: SourceRange,
1044 universe_map: &UniverseMap,
1045 ) {
1046 match module_path {
1047 ModulePath::Main => {
1048 }
1050 ModulePath::Local {
1051 value,
1052 original_import_path,
1053 } => {
1054 if universe_map.contains_key(value) {
1057 use crate::NodePath;
1058
1059 let node_path = if source_range.is_top_level_module() {
1060 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1061 NodePath::from_range(
1062 &exec_state.build_program_lookup(program.clone()),
1063 cached_body_items,
1064 source_range,
1065 )
1066 .unwrap_or_default()
1067 } else {
1068 NodePath::placeholder()
1071 };
1072
1073 let name = match original_import_path {
1074 Some(value) => value.to_string_lossy(),
1075 None => value.file_name().unwrap_or_default(),
1076 };
1077 exec_state.push_op(Operation::GroupBegin {
1078 group: Group::ModuleInstance { name, module_id },
1079 node_path,
1080 source_range,
1081 });
1082 exec_state.push_op(Operation::GroupEnd);
1086 }
1087 }
1088 ModulePath::Std { .. } => {
1089 }
1091 }
1092 }
1093
1094 async fn inner_run(
1097 &self,
1098 program: &crate::Program,
1099 exec_state: &mut ExecState,
1100 preserve_mem: bool,
1101 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1102 let _stats = crate::log::LogPerfStats::new("Interpretation");
1103
1104 let grid_scale = if self.settings.fixed_size_grid {
1106 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1107 } else {
1108 GridScaleBehavior::ScaleWithZoom
1109 };
1110 self.engine
1111 .reapply_settings(
1112 &self.settings,
1113 Default::default(),
1114 exec_state.id_generator(),
1115 grid_scale,
1116 )
1117 .await
1118 .map_err(KclErrorWithOutputs::no_outputs)?;
1119
1120 let default_planes = self.engine.get_default_planes().read().await.clone();
1121 let result = self
1122 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1123 .await;
1124
1125 crate::log::log(format!(
1126 "Post interpretation KCL memory stats: {:#?}",
1127 exec_state.stack().memory.stats
1128 ));
1129 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1130
1131 let env_ref = result.map_err(|(err, env_ref)| exec_state.error_with_outputs(err, env_ref, default_planes))?;
1132
1133 if !self.is_mock() {
1134 let mut mem = exec_state.stack().deep_clone();
1135 mem.restore_env(env_ref);
1136 cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
1137 }
1138 let session_data = self.engine.get_session_data().await;
1139
1140 Ok((env_ref, session_data))
1141 }
1142
1143 async fn execute_and_build_graph(
1146 &self,
1147 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1148 exec_state: &mut ExecState,
1149 preserve_mem: bool,
1150 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1151 #[cfg(feature = "artifact-graph")]
1157 let start_op = exec_state.global.root_module_artifacts.operations.len();
1158
1159 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1160 .await
1161 .map_err(|e| (e, None))?;
1162
1163 let exec_result = self
1164 .exec_module_body(
1165 program,
1166 exec_state,
1167 preserve_mem,
1168 ModuleId::default(),
1169 &ModulePath::Main,
1170 )
1171 .await
1172 .map(
1173 |ModuleExecutionOutcome {
1174 environment: env_ref,
1175 artifacts: module_artifacts,
1176 ..
1177 }| {
1178 exec_state.global.root_module_artifacts.extend(module_artifacts);
1181 env_ref
1182 },
1183 )
1184 .map_err(|(err, env_ref, module_artifacts)| {
1185 if let Some(module_artifacts) = module_artifacts {
1186 exec_state.global.root_module_artifacts.extend(module_artifacts);
1189 }
1190 (err, env_ref)
1191 });
1192
1193 #[cfg(feature = "artifact-graph")]
1194 {
1195 let programs = &exec_state.build_program_lookup(program.clone());
1197 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1198 for op in exec_state
1199 .global
1200 .root_module_artifacts
1201 .operations
1202 .iter_mut()
1203 .skip(start_op)
1204 {
1205 op.fill_node_paths(programs, cached_body_items);
1206 }
1207 for module in exec_state.global.module_infos.values_mut() {
1208 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1209 for op in &mut outcome.artifacts.operations {
1210 op.fill_node_paths(programs, cached_body_items);
1211 }
1212 }
1213 }
1214 }
1215
1216 self.engine.ensure_async_commands_completed().await.map_err(|e| {
1218 match &exec_result {
1219 Ok(env_ref) => (e, Some(*env_ref)),
1220 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1222 }
1223 })?;
1224
1225 self.engine.clear_queues().await;
1228
1229 match exec_state.build_artifact_graph(&self.engine, program).await {
1230 Ok(_) => exec_result,
1231 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1232 }
1233 }
1234
1235 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1239 if exec_state.stack().memory.requires_std() {
1240 #[cfg(feature = "artifact-graph")]
1241 let initial_ops = exec_state.mod_local.artifacts.operations.len();
1242
1243 let path = vec!["std".to_owned(), "prelude".to_owned()];
1244 let resolved_path = ModulePath::from_std_import_path(&path)?;
1245 let id = self
1246 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1247 .await?;
1248 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1249
1250 exec_state.mut_stack().memory.set_std(module_memory);
1251
1252 #[cfg(feature = "artifact-graph")]
1258 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1259 }
1260
1261 Ok(())
1262 }
1263
1264 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1266 self.engine
1268 .send_modeling_cmd(
1269 uuid::Uuid::new_v4(),
1270 crate::execution::SourceRange::default(),
1271 &ModelingCmd::from(mcmd::ZoomToFit {
1272 object_ids: Default::default(),
1273 animated: false,
1274 padding: 0.1,
1275 }),
1276 )
1277 .await
1278 .map_err(KclErrorWithOutputs::no_outputs)?;
1279
1280 let resp = self
1282 .engine
1283 .send_modeling_cmd(
1284 uuid::Uuid::new_v4(),
1285 crate::execution::SourceRange::default(),
1286 &ModelingCmd::from(mcmd::TakeSnapshot {
1287 format: ImageFormat::Png,
1288 }),
1289 )
1290 .await
1291 .map_err(KclErrorWithOutputs::no_outputs)?;
1292
1293 let OkWebSocketResponseData::Modeling {
1294 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1295 } = resp
1296 else {
1297 return Err(ExecError::BadPng(format!(
1298 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1299 )));
1300 };
1301 Ok(contents)
1302 }
1303
1304 pub async fn export(
1306 &self,
1307 format: kittycad_modeling_cmds::format::OutputFormat3d,
1308 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1309 let resp = self
1310 .engine
1311 .send_modeling_cmd(
1312 uuid::Uuid::new_v4(),
1313 crate::SourceRange::default(),
1314 &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
1315 entity_ids: vec![],
1316 format,
1317 }),
1318 )
1319 .await?;
1320
1321 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1322 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1323 format!("Expected Export response, got {resp:?}",),
1324 vec![SourceRange::default()],
1325 )));
1326 };
1327
1328 Ok(files)
1329 }
1330
1331 pub async fn export_step(
1333 &self,
1334 deterministic_time: bool,
1335 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1336 let files = self
1337 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1338 kittycad_modeling_cmds::format::step::export::Options {
1339 coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1340 created: if deterministic_time {
1341 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1342 KclError::new_internal(crate::errors::KclErrorDetails::new(
1343 format!("Failed to parse date: {e}"),
1344 vec![SourceRange::default()],
1345 ))
1346 })?)
1347 } else {
1348 None
1349 },
1350 },
1351 ))
1352 .await?;
1353
1354 Ok(files)
1355 }
1356
1357 pub async fn close(&self) {
1358 self.engine.close().await;
1359 }
1360}
1361
1362#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1363pub struct ArtifactId(Uuid);
1364
1365impl ArtifactId {
1366 pub fn new(uuid: Uuid) -> Self {
1367 Self(uuid)
1368 }
1369}
1370
1371impl From<Uuid> for ArtifactId {
1372 fn from(uuid: Uuid) -> Self {
1373 Self::new(uuid)
1374 }
1375}
1376
1377impl From<&Uuid> for ArtifactId {
1378 fn from(uuid: &Uuid) -> Self {
1379 Self::new(*uuid)
1380 }
1381}
1382
1383impl From<ArtifactId> for Uuid {
1384 fn from(id: ArtifactId) -> Self {
1385 id.0
1386 }
1387}
1388
1389impl From<&ArtifactId> for Uuid {
1390 fn from(id: &ArtifactId) -> Self {
1391 id.0
1392 }
1393}
1394
1395impl From<ModelingCmdId> for ArtifactId {
1396 fn from(id: ModelingCmdId) -> Self {
1397 Self::new(*id.as_ref())
1398 }
1399}
1400
1401impl From<&ModelingCmdId> for ArtifactId {
1402 fn from(id: &ModelingCmdId) -> Self {
1403 Self::new(*id.as_ref())
1404 }
1405}
1406
1407#[cfg(test)]
1408pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1409 parse_execute_with_project_dir(code, None).await
1410}
1411
1412#[cfg(test)]
1413pub(crate) async fn parse_execute_with_project_dir(
1414 code: &str,
1415 project_directory: Option<TypedPath>,
1416) -> Result<ExecTestResults, KclError> {
1417 let program = crate::Program::parse_no_errs(code)?;
1418
1419 let exec_ctxt = ExecutorContext {
1420 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
1421 |err| {
1422 KclError::new_internal(crate::errors::KclErrorDetails::new(
1423 format!("Failed to create mock engine connection: {err}"),
1424 vec![SourceRange::default()],
1425 ))
1426 },
1427 )?)),
1428 fs: Arc::new(crate::fs::FileManager::new()),
1429 settings: ExecutorSettings {
1430 project_directory,
1431 ..Default::default()
1432 },
1433 context_type: ContextType::Mock,
1434 };
1435 let mut exec_state = ExecState::new(&exec_ctxt);
1436 let result = exec_ctxt.run(&program, &mut exec_state).await?;
1437
1438 Ok(ExecTestResults {
1439 program,
1440 mem_env: result.0,
1441 exec_ctxt,
1442 exec_state,
1443 })
1444}
1445
1446#[cfg(test)]
1447#[derive(Debug)]
1448pub(crate) struct ExecTestResults {
1449 program: crate::Program,
1450 mem_env: EnvironmentRef,
1451 exec_ctxt: ExecutorContext,
1452 exec_state: ExecState,
1453}
1454
1455#[cfg(feature = "artifact-graph")]
1459pub struct ProgramLookup {
1460 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
1461}
1462
1463#[cfg(feature = "artifact-graph")]
1464impl ProgramLookup {
1465 pub fn new(
1468 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1469 module_infos: state::ModuleInfoMap,
1470 ) -> Self {
1471 let mut programs = IndexMap::with_capacity(module_infos.len());
1472 for (id, info) in module_infos {
1473 if let ModuleRepr::Kcl(program, _) = info.repr {
1474 programs.insert(id, program);
1475 }
1476 }
1477 programs.insert(ModuleId::default(), current);
1478 Self { programs }
1479 }
1480
1481 pub fn program_for_module(
1482 &self,
1483 module_id: ModuleId,
1484 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
1485 self.programs.get(&module_id)
1486 }
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491 use pretty_assertions::assert_eq;
1492
1493 use super::*;
1494 use crate::{
1495 ModuleId,
1496 errors::{KclErrorDetails, Severity},
1497 exec::NumericType,
1498 execution::{memory::Stack, types::RuntimeType},
1499 };
1500
1501 #[track_caller]
1503 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1504 memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1505 }
1506
1507 #[tokio::test(flavor = "multi_thread")]
1508 async fn test_execute_warn() {
1509 let text = "@blah";
1510 let result = parse_execute(text).await.unwrap();
1511 let errs = result.exec_state.errors();
1512 assert_eq!(errs.len(), 1);
1513 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1514 assert!(
1515 errs[0].message.contains("Unknown annotation"),
1516 "unexpected warning message: {}",
1517 errs[0].message
1518 );
1519 }
1520
1521 #[tokio::test(flavor = "multi_thread")]
1522 async fn test_execute_fn_definitions() {
1523 let ast = r#"fn def(@x) {
1524 return x
1525}
1526fn ghi(@x) {
1527 return x
1528}
1529fn jkl(@x) {
1530 return x
1531}
1532fn hmm(@x) {
1533 return x
1534}
1535
1536yo = 5 + 6
1537
1538abc = 3
1539identifierGuy = 5
1540part001 = startSketchOn(XY)
1541|> startProfile(at = [-1.2, 4.83])
1542|> line(end = [2.8, 0])
1543|> angledLine(angle = 100 + 100, length = 3.01)
1544|> angledLine(angle = abc, length = 3.02)
1545|> angledLine(angle = def(yo), length = 3.03)
1546|> angledLine(angle = ghi(2), length = 3.04)
1547|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1548|> close()
1549yo2 = hmm([identifierGuy + 5])"#;
1550
1551 parse_execute(ast).await.unwrap();
1552 }
1553
1554 #[tokio::test(flavor = "multi_thread")]
1555 async fn test_execute_with_pipe_substitutions_unary() {
1556 let ast = r#"myVar = 3
1557part001 = startSketchOn(XY)
1558 |> startProfile(at = [0, 0])
1559 |> line(end = [3, 4], tag = $seg01)
1560 |> line(end = [
1561 min([segLen(seg01), myVar]),
1562 -legLen(hypotenuse = segLen(seg01), leg = myVar)
1563])
1564"#;
1565
1566 parse_execute(ast).await.unwrap();
1567 }
1568
1569 #[tokio::test(flavor = "multi_thread")]
1570 async fn test_execute_with_pipe_substitutions() {
1571 let ast = r#"myVar = 3
1572part001 = startSketchOn(XY)
1573 |> startProfile(at = [0, 0])
1574 |> line(end = [3, 4], tag = $seg01)
1575 |> line(end = [
1576 min([segLen(seg01), myVar]),
1577 legLen(hypotenuse = segLen(seg01), leg = myVar)
1578])
1579"#;
1580
1581 parse_execute(ast).await.unwrap();
1582 }
1583
1584 #[tokio::test(flavor = "multi_thread")]
1585 async fn test_execute_with_inline_comment() {
1586 let ast = r#"baseThick = 1
1587armAngle = 60
1588
1589baseThickHalf = baseThick / 2
1590halfArmAngle = armAngle / 2
1591
1592arrExpShouldNotBeIncluded = [1, 2, 3]
1593objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1594
1595part001 = startSketchOn(XY)
1596 |> startProfile(at = [0, 0])
1597 |> yLine(endAbsolute = 1)
1598 |> xLine(length = 3.84) // selection-range-7ish-before-this
1599
1600variableBelowShouldNotBeIncluded = 3
1601"#;
1602
1603 parse_execute(ast).await.unwrap();
1604 }
1605
1606 #[tokio::test(flavor = "multi_thread")]
1607 async fn test_execute_with_function_literal_in_pipe() {
1608 let ast = r#"w = 20
1609l = 8
1610h = 10
1611
1612fn thing() {
1613 return -8
1614}
1615
1616firstExtrude = startSketchOn(XY)
1617 |> startProfile(at = [0,0])
1618 |> line(end = [0, l])
1619 |> line(end = [w, 0])
1620 |> line(end = [0, thing()])
1621 |> close()
1622 |> extrude(length = h)"#;
1623
1624 parse_execute(ast).await.unwrap();
1625 }
1626
1627 #[tokio::test(flavor = "multi_thread")]
1628 async fn test_execute_with_function_unary_in_pipe() {
1629 let ast = r#"w = 20
1630l = 8
1631h = 10
1632
1633fn thing(@x) {
1634 return -x
1635}
1636
1637firstExtrude = startSketchOn(XY)
1638 |> startProfile(at = [0,0])
1639 |> line(end = [0, l])
1640 |> line(end = [w, 0])
1641 |> line(end = [0, thing(8)])
1642 |> close()
1643 |> extrude(length = h)"#;
1644
1645 parse_execute(ast).await.unwrap();
1646 }
1647
1648 #[tokio::test(flavor = "multi_thread")]
1649 async fn test_execute_with_function_array_in_pipe() {
1650 let ast = r#"w = 20
1651l = 8
1652h = 10
1653
1654fn thing(@x) {
1655 return [0, -x]
1656}
1657
1658firstExtrude = startSketchOn(XY)
1659 |> startProfile(at = [0,0])
1660 |> line(end = [0, l])
1661 |> line(end = [w, 0])
1662 |> line(end = thing(8))
1663 |> close()
1664 |> extrude(length = h)"#;
1665
1666 parse_execute(ast).await.unwrap();
1667 }
1668
1669 #[tokio::test(flavor = "multi_thread")]
1670 async fn test_execute_with_function_call_in_pipe() {
1671 let ast = r#"w = 20
1672l = 8
1673h = 10
1674
1675fn other_thing(@y) {
1676 return -y
1677}
1678
1679fn thing(@x) {
1680 return other_thing(x)
1681}
1682
1683firstExtrude = startSketchOn(XY)
1684 |> startProfile(at = [0,0])
1685 |> line(end = [0, l])
1686 |> line(end = [w, 0])
1687 |> line(end = [0, thing(8)])
1688 |> close()
1689 |> extrude(length = h)"#;
1690
1691 parse_execute(ast).await.unwrap();
1692 }
1693
1694 #[tokio::test(flavor = "multi_thread")]
1695 async fn test_execute_with_function_sketch() {
1696 let ast = r#"fn box(h, l, w) {
1697 myBox = startSketchOn(XY)
1698 |> startProfile(at = [0,0])
1699 |> line(end = [0, l])
1700 |> line(end = [w, 0])
1701 |> line(end = [0, -l])
1702 |> close()
1703 |> extrude(length = h)
1704
1705 return myBox
1706}
1707
1708fnBox = box(h = 3, l = 6, w = 10)"#;
1709
1710 parse_execute(ast).await.unwrap();
1711 }
1712
1713 #[tokio::test(flavor = "multi_thread")]
1714 async fn test_get_member_of_object_with_function_period() {
1715 let ast = r#"fn box(@obj) {
1716 myBox = startSketchOn(XY)
1717 |> startProfile(at = obj.start)
1718 |> line(end = [0, obj.l])
1719 |> line(end = [obj.w, 0])
1720 |> line(end = [0, -obj.l])
1721 |> close()
1722 |> extrude(length = obj.h)
1723
1724 return myBox
1725}
1726
1727thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1728"#;
1729 parse_execute(ast).await.unwrap();
1730 }
1731
1732 #[tokio::test(flavor = "multi_thread")]
1733 #[ignore] async fn test_object_member_starting_pipeline() {
1735 let ast = r#"
1736fn test2() {
1737 return {
1738 thing: startSketchOn(XY)
1739 |> startProfile(at = [0, 0])
1740 |> line(end = [0, 1])
1741 |> line(end = [1, 0])
1742 |> line(end = [0, -1])
1743 |> close()
1744 }
1745}
1746
1747x2 = test2()
1748
1749x2.thing
1750 |> extrude(length = 10)
1751"#;
1752 parse_execute(ast).await.unwrap();
1753 }
1754
1755 #[tokio::test(flavor = "multi_thread")]
1756 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
1758 let ast = r#"fn box(obj) {
1759let myBox = startSketchOn(XY)
1760 |> startProfile(at = obj.start)
1761 |> line(end = [0, obj.l])
1762 |> line(end = [obj.w, 0])
1763 |> line(end = [0, -obj.l])
1764 |> close()
1765 |> extrude(length = obj.h)
1766
1767 return myBox
1768}
1769
1770for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1771 thisBox = box(var)
1772}"#;
1773
1774 parse_execute(ast).await.unwrap();
1775 }
1776
1777 #[tokio::test(flavor = "multi_thread")]
1778 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
1780 let ast = r#"fn box(h, l, w, start) {
1781 myBox = startSketchOn(XY)
1782 |> startProfile(at = [0,0])
1783 |> line(end = [0, l])
1784 |> line(end = [w, 0])
1785 |> line(end = [0, -l])
1786 |> close()
1787 |> extrude(length = h)
1788
1789 return myBox
1790}
1791
1792
1793for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1794 const thisBox = box(var[0], var[1], var[2], var[3])
1795}"#;
1796
1797 parse_execute(ast).await.unwrap();
1798 }
1799
1800 #[tokio::test(flavor = "multi_thread")]
1801 async fn test_get_member_of_array_with_function() {
1802 let ast = r#"fn box(@arr) {
1803 myBox =startSketchOn(XY)
1804 |> startProfile(at = arr[0])
1805 |> line(end = [0, arr[1]])
1806 |> line(end = [arr[2], 0])
1807 |> line(end = [0, -arr[1]])
1808 |> close()
1809 |> extrude(length = arr[3])
1810
1811 return myBox
1812}
1813
1814thisBox = box([[0,0], 6, 10, 3])
1815
1816"#;
1817 parse_execute(ast).await.unwrap();
1818 }
1819
1820 #[tokio::test(flavor = "multi_thread")]
1821 async fn test_function_cannot_access_future_definitions() {
1822 let ast = r#"
1823fn returnX() {
1824 // x shouldn't be defined yet.
1825 return x
1826}
1827
1828x = 5
1829
1830answer = returnX()"#;
1831
1832 let result = parse_execute(ast).await;
1833 let err = result.unwrap_err();
1834 assert_eq!(err.message(), "`x` is not defined");
1835 }
1836
1837 #[tokio::test(flavor = "multi_thread")]
1838 async fn test_override_prelude() {
1839 let text = "PI = 3.0";
1840 let result = parse_execute(text).await.unwrap();
1841 let errs = result.exec_state.errors();
1842 assert!(errs.is_empty());
1843 }
1844
1845 #[tokio::test(flavor = "multi_thread")]
1846 async fn type_aliases() {
1847 let text = r#"@settings(experimentalFeatures = allow)
1848type MyTy = [number; 2]
1849fn foo(@x: MyTy) {
1850 return x[0]
1851}
1852
1853foo([0, 1])
1854
1855type Other = MyTy | Helix
1856"#;
1857 let result = parse_execute(text).await.unwrap();
1858 let errs = result.exec_state.errors();
1859 assert!(errs.is_empty());
1860 }
1861
1862 #[tokio::test(flavor = "multi_thread")]
1863 async fn test_cannot_shebang_in_fn() {
1864 let ast = r#"
1865fn foo() {
1866 #!hello
1867 return true
1868}
1869
1870foo
1871"#;
1872
1873 let result = parse_execute(ast).await;
1874 let err = result.unwrap_err();
1875 assert_eq!(
1876 err,
1877 KclError::new_syntax(KclErrorDetails::new(
1878 "Unexpected token: #".to_owned(),
1879 vec![SourceRange::new(14, 15, ModuleId::default())],
1880 )),
1881 );
1882 }
1883
1884 #[tokio::test(flavor = "multi_thread")]
1885 async fn test_pattern_transform_function_cannot_access_future_definitions() {
1886 let ast = r#"
1887fn transform(@replicaId) {
1888 // x shouldn't be defined yet.
1889 scale = x
1890 return {
1891 translate = [0, 0, replicaId * 10],
1892 scale = [scale, 1, 0],
1893 }
1894}
1895
1896fn layer() {
1897 return startSketchOn(XY)
1898 |> circle( center= [0, 0], radius= 1, tag = $tag1)
1899 |> extrude(length = 10)
1900}
1901
1902x = 5
1903
1904// The 10 layers are replicas of each other, with a transform applied to each.
1905shape = layer() |> patternTransform(instances = 10, transform = transform)
1906"#;
1907
1908 let result = parse_execute(ast).await;
1909 let err = result.unwrap_err();
1910 assert_eq!(err.message(), "`x` is not defined",);
1911 }
1912
1913 #[tokio::test(flavor = "multi_thread")]
1916 async fn test_math_execute_with_functions() {
1917 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
1918 let result = parse_execute(ast).await.unwrap();
1919 assert_eq!(
1920 5.0,
1921 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1922 .as_f64()
1923 .unwrap()
1924 );
1925 }
1926
1927 #[tokio::test(flavor = "multi_thread")]
1928 async fn test_math_execute() {
1929 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
1930 let result = parse_execute(ast).await.unwrap();
1931 assert_eq!(
1932 7.4,
1933 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1934 .as_f64()
1935 .unwrap()
1936 );
1937 }
1938
1939 #[tokio::test(flavor = "multi_thread")]
1940 async fn test_math_execute_start_negative() {
1941 let ast = r#"myVar = -5 + 6"#;
1942 let result = parse_execute(ast).await.unwrap();
1943 assert_eq!(
1944 1.0,
1945 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1946 .as_f64()
1947 .unwrap()
1948 );
1949 }
1950
1951 #[tokio::test(flavor = "multi_thread")]
1952 async fn test_math_execute_with_pi() {
1953 let ast = r#"myVar = PI * 2"#;
1954 let result = parse_execute(ast).await.unwrap();
1955 assert_eq!(
1956 std::f64::consts::TAU,
1957 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1958 .as_f64()
1959 .unwrap()
1960 );
1961 }
1962
1963 #[tokio::test(flavor = "multi_thread")]
1964 async fn test_math_define_decimal_without_leading_zero() {
1965 let ast = r#"thing = .4 + 7"#;
1966 let result = parse_execute(ast).await.unwrap();
1967 assert_eq!(
1968 7.4,
1969 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
1970 .as_f64()
1971 .unwrap()
1972 );
1973 }
1974
1975 #[tokio::test(flavor = "multi_thread")]
1976 async fn pass_std_to_std() {
1977 let ast = r#"sketch001 = startSketchOn(XY)
1978profile001 = circle(sketch001, center = [0, 0], radius = 2)
1979extrude001 = extrude(profile001, length = 5)
1980extrudes = patternLinear3d(
1981 extrude001,
1982 instances = 3,
1983 distance = 5,
1984 axis = [1, 1, 0],
1985)
1986clone001 = map(extrudes, f = clone)
1987"#;
1988 parse_execute(ast).await.unwrap();
1989 }
1990
1991 #[tokio::test(flavor = "multi_thread")]
1992 async fn test_array_reduce_nested_array() {
1993 let code = r#"
1994fn id(@el, accum) { return accum }
1995
1996answer = reduce([], initial=[[[0,0]]], f=id)
1997"#;
1998 let result = parse_execute(code).await.unwrap();
1999 assert_eq!(
2000 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2001 KclValue::HomArray {
2002 value: vec![KclValue::HomArray {
2003 value: vec![KclValue::HomArray {
2004 value: vec![
2005 KclValue::Number {
2006 value: 0.0,
2007 ty: NumericType::default(),
2008 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2009 },
2010 KclValue::Number {
2011 value: 0.0,
2012 ty: NumericType::default(),
2013 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2014 }
2015 ],
2016 ty: RuntimeType::any(),
2017 }],
2018 ty: RuntimeType::any(),
2019 }],
2020 ty: RuntimeType::any(),
2021 }
2022 );
2023 }
2024
2025 #[tokio::test(flavor = "multi_thread")]
2026 async fn test_zero_param_fn() {
2027 let ast = r#"sigmaAllow = 35000 // psi
2028leg1 = 5 // inches
2029leg2 = 8 // inches
2030fn thickness() { return 0.56 }
2031
2032bracket = startSketchOn(XY)
2033 |> startProfile(at = [0,0])
2034 |> line(end = [0, leg1])
2035 |> line(end = [leg2, 0])
2036 |> line(end = [0, -thickness()])
2037 |> line(end = [-leg2 + thickness(), 0])
2038"#;
2039 parse_execute(ast).await.unwrap();
2040 }
2041
2042 #[tokio::test(flavor = "multi_thread")]
2043 async fn test_unary_operator_not_succeeds() {
2044 let ast = r#"
2045fn returnTrue() { return !false }
2046t = true
2047f = false
2048notTrue = !t
2049notFalse = !f
2050c = !!true
2051d = !returnTrue()
2052
2053assertIs(!false, error = "expected to pass")
2054
2055fn check(x) {
2056 assertIs(!x, error = "expected argument to be false")
2057 return true
2058}
2059check(x = false)
2060"#;
2061 let result = parse_execute(ast).await.unwrap();
2062 assert_eq!(
2063 false,
2064 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2065 .as_bool()
2066 .unwrap()
2067 );
2068 assert_eq!(
2069 true,
2070 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2071 .as_bool()
2072 .unwrap()
2073 );
2074 assert_eq!(
2075 true,
2076 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2077 .as_bool()
2078 .unwrap()
2079 );
2080 assert_eq!(
2081 false,
2082 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2083 .as_bool()
2084 .unwrap()
2085 );
2086 }
2087
2088 #[tokio::test(flavor = "multi_thread")]
2089 async fn test_unary_operator_not_on_non_bool_fails() {
2090 let code1 = r#"
2091// Yup, this is null.
2092myNull = 0 / 0
2093notNull = !myNull
2094"#;
2095 assert_eq!(
2096 parse_execute(code1).await.unwrap_err().message(),
2097 "Cannot apply unary operator ! to non-boolean value: a number",
2098 );
2099
2100 let code2 = "notZero = !0";
2101 assert_eq!(
2102 parse_execute(code2).await.unwrap_err().message(),
2103 "Cannot apply unary operator ! to non-boolean value: a number",
2104 );
2105
2106 let code3 = r#"
2107notEmptyString = !""
2108"#;
2109 assert_eq!(
2110 parse_execute(code3).await.unwrap_err().message(),
2111 "Cannot apply unary operator ! to non-boolean value: a string",
2112 );
2113
2114 let code4 = r#"
2115obj = { a = 1 }
2116notMember = !obj.a
2117"#;
2118 assert_eq!(
2119 parse_execute(code4).await.unwrap_err().message(),
2120 "Cannot apply unary operator ! to non-boolean value: a number",
2121 );
2122
2123 let code5 = "
2124a = []
2125notArray = !a";
2126 assert_eq!(
2127 parse_execute(code5).await.unwrap_err().message(),
2128 "Cannot apply unary operator ! to non-boolean value: an empty array",
2129 );
2130
2131 let code6 = "
2132x = {}
2133notObject = !x";
2134 assert_eq!(
2135 parse_execute(code6).await.unwrap_err().message(),
2136 "Cannot apply unary operator ! to non-boolean value: an object",
2137 );
2138
2139 let code7 = "
2140fn x() { return 1 }
2141notFunction = !x";
2142 let fn_err = parse_execute(code7).await.unwrap_err();
2143 assert!(
2146 fn_err
2147 .message()
2148 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2149 "Actual error: {fn_err:?}"
2150 );
2151
2152 let code8 = "
2153myTagDeclarator = $myTag
2154notTagDeclarator = !myTagDeclarator";
2155 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2156 assert!(
2159 tag_declarator_err
2160 .message()
2161 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2162 "Actual error: {tag_declarator_err:?}"
2163 );
2164
2165 let code9 = "
2166myTagDeclarator = $myTag
2167notTagIdentifier = !myTag";
2168 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2169 assert!(
2172 tag_identifier_err
2173 .message()
2174 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2175 "Actual error: {tag_identifier_err:?}"
2176 );
2177
2178 let code10 = "notPipe = !(1 |> 2)";
2179 assert_eq!(
2180 parse_execute(code10).await.unwrap_err(),
2183 KclError::new_syntax(KclErrorDetails::new(
2184 "Unexpected token: !".to_owned(),
2185 vec![SourceRange::new(10, 11, ModuleId::default())],
2186 ))
2187 );
2188
2189 let code11 = "
2190fn identity(x) { return x }
2191notPipeSub = 1 |> identity(!%))";
2192 assert_eq!(
2193 parse_execute(code11).await.unwrap_err(),
2196 KclError::new_syntax(KclErrorDetails::new(
2197 "There was an unexpected `!`. Try removing it.".to_owned(),
2198 vec![SourceRange::new(56, 57, ModuleId::default())],
2199 ))
2200 );
2201
2202 }
2206
2207 #[tokio::test(flavor = "multi_thread")]
2208 async fn test_start_sketch_on_invalid_kwargs() {
2209 let current_dir = std::env::current_dir().unwrap();
2210 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2211 let mut code = std::fs::read_to_string(&path).unwrap();
2212 assert_eq!(
2213 parse_execute(&code).await.unwrap_err().message(),
2214 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2215 );
2216
2217 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2218 code = std::fs::read_to_string(&path).unwrap();
2219
2220 assert_eq!(
2221 parse_execute(&code).await.unwrap_err().message(),
2222 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2223 );
2224
2225 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2226 code = std::fs::read_to_string(&path).unwrap();
2227
2228 assert_eq!(
2229 parse_execute(&code).await.unwrap_err().message(),
2230 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2231 );
2232
2233 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2234 code = std::fs::read_to_string(&path).unwrap();
2235
2236 assert_eq!(
2237 parse_execute(&code).await.unwrap_err().message(),
2238 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2239 );
2240
2241 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2242 code = std::fs::read_to_string(&path).unwrap();
2243
2244 assert_eq!(
2245 parse_execute(&code).await.unwrap_err().message(),
2246 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2247 );
2248 }
2249
2250 #[tokio::test(flavor = "multi_thread")]
2251 async fn test_math_negative_variable_in_binary_expression() {
2252 let ast = r#"sigmaAllow = 35000 // psi
2253width = 1 // inch
2254
2255p = 150 // lbs
2256distance = 6 // inches
2257FOS = 2
2258
2259leg1 = 5 // inches
2260leg2 = 8 // inches
2261
2262thickness_squared = distance * p * FOS * 6 / sigmaAllow
2263thickness = 0.56 // inches. App does not support square root function yet
2264
2265bracket = startSketchOn(XY)
2266 |> startProfile(at = [0,0])
2267 |> line(end = [0, leg1])
2268 |> line(end = [leg2, 0])
2269 |> line(end = [0, -thickness])
2270 |> line(end = [-leg2 + thickness, 0])
2271"#;
2272 parse_execute(ast).await.unwrap();
2273 }
2274
2275 #[tokio::test(flavor = "multi_thread")]
2276 async fn test_execute_function_no_return() {
2277 let ast = r#"fn test(@origin) {
2278 origin
2279}
2280
2281test([0, 0])
2282"#;
2283 let result = parse_execute(ast).await;
2284 assert!(result.is_err());
2285 assert!(result.unwrap_err().to_string().contains("undefined"));
2286 }
2287
2288 #[tokio::test(flavor = "multi_thread")]
2289 async fn test_math_doubly_nested_parens() {
2290 let ast = r#"sigmaAllow = 35000 // psi
2291width = 4 // inch
2292p = 150 // Force on shelf - lbs
2293distance = 6 // inches
2294FOS = 2
2295leg1 = 5 // inches
2296leg2 = 8 // inches
2297thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2298thickness = 0.32 // inches. App does not support square root function yet
2299bracket = startSketchOn(XY)
2300 |> startProfile(at = [0,0])
2301 |> line(end = [0, leg1])
2302 |> line(end = [leg2, 0])
2303 |> line(end = [0, -thickness])
2304 |> line(end = [-1 * leg2 + thickness, 0])
2305 |> line(end = [0, -1 * leg1 + thickness])
2306 |> close()
2307 |> extrude(length = width)
2308"#;
2309 parse_execute(ast).await.unwrap();
2310 }
2311
2312 #[tokio::test(flavor = "multi_thread")]
2313 async fn test_math_nested_parens_one_less() {
2314 let ast = r#" sigmaAllow = 35000 // psi
2315width = 4 // inch
2316p = 150 // Force on shelf - lbs
2317distance = 6 // inches
2318FOS = 2
2319leg1 = 5 // inches
2320leg2 = 8 // inches
2321thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2322thickness = 0.32 // inches. App does not support square root function yet
2323bracket = startSketchOn(XY)
2324 |> startProfile(at = [0,0])
2325 |> line(end = [0, leg1])
2326 |> line(end = [leg2, 0])
2327 |> line(end = [0, -thickness])
2328 |> line(end = [-1 * leg2 + thickness, 0])
2329 |> line(end = [0, -1 * leg1 + thickness])
2330 |> close()
2331 |> extrude(length = width)
2332"#;
2333 parse_execute(ast).await.unwrap();
2334 }
2335
2336 #[tokio::test(flavor = "multi_thread")]
2337 async fn test_fn_as_operand() {
2338 let ast = r#"fn f() { return 1 }
2339x = f()
2340y = x + 1
2341z = f() + 1
2342w = f() + f()
2343"#;
2344 parse_execute(ast).await.unwrap();
2345 }
2346
2347 #[tokio::test(flavor = "multi_thread")]
2348 async fn kcl_test_ids_stable_between_executions() {
2349 let code = r#"sketch001 = startSketchOn(XZ)
2350|> startProfile(at = [61.74, 206.13])
2351|> xLine(length = 305.11, tag = $seg01)
2352|> yLine(length = -291.85)
2353|> xLine(length = -segLen(seg01))
2354|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2355|> close()
2356|> extrude(length = 40.14)
2357|> shell(
2358 thickness = 3.14,
2359 faces = [seg01]
2360)
2361"#;
2362
2363 let ctx = crate::test_server::new_context(true, None).await.unwrap();
2364 let old_program = crate::Program::parse_no_errs(code).unwrap();
2365
2366 if let Err(err) = ctx.run_with_caching(old_program).await {
2368 let report = err.into_miette_report_with_outputs(code).unwrap();
2369 let report = miette::Report::new(report);
2370 panic!("Error executing program: {report:?}");
2371 }
2372
2373 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2375
2376 let code = r#"sketch001 = startSketchOn(XZ)
2377|> startProfile(at = [62.74, 206.13])
2378|> xLine(length = 305.11, tag = $seg01)
2379|> yLine(length = -291.85)
2380|> xLine(length = -segLen(seg01))
2381|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2382|> close()
2383|> extrude(length = 40.14)
2384|> shell(
2385 faces = [seg01],
2386 thickness = 3.14,
2387)
2388"#;
2389
2390 let program = crate::Program::parse_no_errs(code).unwrap();
2392 ctx.run_with_caching(program).await.unwrap();
2394
2395 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2396
2397 assert_eq!(id_generator, new_id_generator);
2398 }
2399
2400 #[tokio::test(flavor = "multi_thread")]
2401 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2402 let code = r#"sketch001 = startSketchOn(XZ)
2403|> startProfile(at = [61.74, 206.13])
2404|> xLine(length = 305.11, tag = $seg01)
2405|> yLine(length = -291.85)
2406|> xLine(length = -segLen(seg01))
2407|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2408|> close()
2409|> extrude(length = 40.14)
2410|> shell(
2411 thickness = 3.14,
2412 faces = [seg01]
2413)
2414"#;
2415
2416 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2417 let old_program = crate::Program::parse_no_errs(code).unwrap();
2418
2419 ctx.run_with_caching(old_program.clone()).await.unwrap();
2421
2422 let settings_state = cache::read_old_ast().await.unwrap().settings;
2423
2424 assert_eq!(settings_state, ctx.settings);
2426
2427 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2429
2430 ctx.run_with_caching(old_program.clone()).await.unwrap();
2432
2433 let settings_state = cache::read_old_ast().await.unwrap().settings;
2434
2435 assert_eq!(settings_state, ctx.settings);
2437
2438 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2440
2441 ctx.run_with_caching(old_program).await.unwrap();
2443
2444 let settings_state = cache::read_old_ast().await.unwrap().settings;
2445
2446 assert_eq!(settings_state, ctx.settings);
2448
2449 ctx.close().await;
2450 }
2451
2452 #[tokio::test(flavor = "multi_thread")]
2453 async fn mock_after_not_mock() {
2454 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2455 let program = crate::Program::parse_no_errs("x = 2").unwrap();
2456 let result = ctx.run_with_caching(program).await.unwrap();
2457 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2458
2459 let ctx2 = ExecutorContext::new_mock(None).await;
2460 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2461 let result = ctx2.run_mock(&program2, true).await.unwrap();
2462 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2463
2464 ctx.close().await;
2465 ctx2.close().await;
2466 }
2467
2468 #[cfg(feature = "artifact-graph")]
2469 #[tokio::test(flavor = "multi_thread")]
2470 async fn mock_has_stable_ids() {
2471 let ctx = ExecutorContext::new_mock(None).await;
2472 let code = "sk = startSketchOn(XY)
2473 |> startProfile(at = [0, 0])";
2474 let program = crate::Program::parse_no_errs(code).unwrap();
2475 let result = ctx.run_mock(&program, false).await.unwrap();
2476 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2477 assert!(!ids.is_empty(), "IDs should not be empty");
2478
2479 let ctx2 = ExecutorContext::new_mock(None).await;
2480 let program2 = crate::Program::parse_no_errs(code).unwrap();
2481 let result = ctx2.run_mock(&program2, false).await.unwrap();
2482 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2483
2484 assert_eq!(ids, ids2, "Generated IDs should match");
2485 }
2486
2487 #[cfg(feature = "artifact-graph")]
2488 #[tokio::test(flavor = "multi_thread")]
2489 async fn sim_sketch_mode_real_mock_real() {
2490 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2491 let code = r#"sketch001 = startSketchOn(XY)
2492profile001 = startProfile(sketch001, at = [0, 0])
2493 |> line(end = [10, 0])
2494 |> line(end = [0, 10])
2495 |> line(end = [-10, 0])
2496 |> line(end = [0, -10])
2497 |> close()
2498"#;
2499 let program = crate::Program::parse_no_errs(code).unwrap();
2500 let result = ctx.run_with_caching(program).await.unwrap();
2501 assert_eq!(result.operations.len(), 1);
2502
2503 let mock_ctx = ExecutorContext::new_mock(None).await;
2504 let mock_program = crate::Program::parse_no_errs(code).unwrap();
2505 let mock_result = mock_ctx.run_mock(&mock_program, true).await.unwrap();
2506 assert_eq!(mock_result.operations.len(), 1);
2507
2508 let code2 = code.to_owned()
2509 + r#"
2510extrude001 = extrude(profile001, length = 10)
2511"#;
2512 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
2513 let result = ctx.run_with_caching(program2).await.unwrap();
2514 assert_eq!(result.operations.len(), 2);
2515
2516 ctx.close().await;
2517 mock_ctx.close().await;
2518 }
2519
2520 #[tokio::test(flavor = "multi_thread")]
2521 async fn read_tag_version() {
2522 let ast = r#"fn bar(@t) {
2523 return startSketchOn(XY)
2524 |> startProfile(at = [0,0])
2525 |> angledLine(
2526 angle = -60,
2527 length = segLen(t),
2528 )
2529 |> line(end = [0, 0])
2530 |> close()
2531}
2532
2533sketch = startSketchOn(XY)
2534 |> startProfile(at = [0,0])
2535 |> line(end = [0, 10])
2536 |> line(end = [10, 0], tag = $tag0)
2537 |> line(end = [0, 0])
2538
2539fn foo() {
2540 // tag0 tags an edge
2541 return bar(tag0)
2542}
2543
2544solid = sketch |> extrude(length = 10)
2545// tag0 tags a face
2546sketch2 = startSketchOn(solid, face = tag0)
2547 |> startProfile(at = [0,0])
2548 |> line(end = [0, 1])
2549 |> line(end = [1, 0])
2550 |> line(end = [0, 0])
2551
2552foo() |> extrude(length = 1)
2553"#;
2554 parse_execute(ast).await.unwrap();
2555 }
2556
2557 #[tokio::test(flavor = "multi_thread")]
2558 async fn experimental() {
2559 let code = r#"
2560startSketchOn(XY)
2561 |> startProfile(at = [0, 0], tag = $start)
2562 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2563"#;
2564 let result = parse_execute(code).await.unwrap();
2565 let errors = result.exec_state.errors();
2566 assert_eq!(errors.len(), 1);
2567 assert_eq!(errors[0].severity, Severity::Error);
2568 let msg = &errors[0].message;
2569 assert!(msg.contains("experimental"), "found {msg}");
2570
2571 let code = r#"@settings(experimentalFeatures = allow)
2572startSketchOn(XY)
2573 |> startProfile(at = [0, 0], tag = $start)
2574 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2575"#;
2576 let result = parse_execute(code).await.unwrap();
2577 let errors = result.exec_state.errors();
2578 assert!(errors.is_empty());
2579
2580 let code = r#"@settings(experimentalFeatures = warn)
2581startSketchOn(XY)
2582 |> startProfile(at = [0, 0], tag = $start)
2583 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2584"#;
2585 let result = parse_execute(code).await.unwrap();
2586 let errors = result.exec_state.errors();
2587 assert_eq!(errors.len(), 1);
2588 assert_eq!(errors[0].severity, Severity::Warning);
2589 let msg = &errors[0].message;
2590 assert!(msg.contains("experimental"), "found {msg}");
2591
2592 let code = r#"@settings(experimentalFeatures = deny)
2593startSketchOn(XY)
2594 |> startProfile(at = [0, 0], tag = $start)
2595 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2596"#;
2597 let result = parse_execute(code).await.unwrap();
2598 let errors = result.exec_state.errors();
2599 assert_eq!(errors.len(), 1);
2600 assert_eq!(errors[0].severity, Severity::Error);
2601 let msg = &errors[0].message;
2602 assert!(msg.contains("experimental"), "found {msg}");
2603
2604 let code = r#"@settings(experimentalFeatures = foo)
2605startSketchOn(XY)
2606 |> startProfile(at = [0, 0], tag = $start)
2607 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2608"#;
2609 parse_execute(code).await.unwrap_err();
2610 }
2611}