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