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