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 &ModulePath::Main,
1048 &ModuleRepr::Kcl(program.ast.clone(), None),
1049 &mut universe,
1050 exec_state,
1051 )
1052 .await
1053 .map_err(|err| {
1054 println!("Error: {err:?}");
1055 let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
1056 .global
1057 .path_to_source_id
1058 .iter()
1059 .map(|(k, v)| ((*v), k.clone()))
1060 .collect();
1061
1062 KclErrorWithOutputs::new(
1063 err,
1064 exec_state.errors().to_vec(),
1065 #[cfg(feature = "artifact-graph")]
1066 exec_state.global.operations.clone(),
1067 #[cfg(feature = "artifact-graph")]
1068 exec_state.global.artifact_commands.clone(),
1069 #[cfg(feature = "artifact-graph")]
1070 exec_state.global.artifact_graph.clone(),
1071 module_id_to_module_path,
1072 exec_state.global.id_to_source.clone(),
1073 default_planes,
1074 )
1075 })?;
1076
1077 Ok((universe, root_imports))
1078 }
1079
1080 async fn inner_run(
1083 &self,
1084 program: &crate::Program,
1085 cached_body_items: usize,
1086 exec_state: &mut ExecState,
1087 preserve_mem: bool,
1088 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1089 let _stats = crate::log::LogPerfStats::new("Interpretation");
1090
1091 self.engine
1093 .reapply_settings(&self.settings, Default::default(), exec_state.id_generator())
1094 .await
1095 .map_err(KclErrorWithOutputs::no_outputs)?;
1096
1097 let default_planes = self.engine.get_default_planes().read().await.clone();
1098 let result = self
1099 .execute_and_build_graph(&program.ast, cached_body_items, exec_state, preserve_mem)
1100 .await;
1101
1102 crate::log::log(format!(
1103 "Post interpretation KCL memory stats: {:#?}",
1104 exec_state.stack().memory.stats
1105 ));
1106 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1107
1108 let env_ref = result.map_err(|e| {
1109 let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
1110 .global
1111 .path_to_source_id
1112 .iter()
1113 .map(|(k, v)| ((*v), k.clone()))
1114 .collect();
1115
1116 KclErrorWithOutputs::new(
1117 e,
1118 exec_state.errors().to_vec(),
1119 #[cfg(feature = "artifact-graph")]
1120 exec_state.global.operations.clone(),
1121 #[cfg(feature = "artifact-graph")]
1122 exec_state.global.artifact_commands.clone(),
1123 #[cfg(feature = "artifact-graph")]
1124 exec_state.global.artifact_graph.clone(),
1125 module_id_to_module_path,
1126 exec_state.global.id_to_source.clone(),
1127 default_planes.clone(),
1128 )
1129 })?;
1130
1131 if !self.is_mock() {
1132 let mut mem = exec_state.stack().deep_clone();
1133 mem.restore_env(env_ref);
1134 cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
1135 }
1136 let session_data = self.engine.get_session_data().await;
1137
1138 Ok((env_ref, session_data))
1139 }
1140
1141 async fn execute_and_build_graph(
1144 &self,
1145 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1146 #[cfg_attr(not(feature = "artifact-graph"), expect(unused))] cached_body_items: usize,
1147 exec_state: &mut ExecState,
1148 preserve_mem: bool,
1149 ) -> Result<EnvironmentRef, KclError> {
1150 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1154 .await?;
1155
1156 let exec_result = self
1157 .exec_module_body(
1158 program,
1159 exec_state,
1160 preserve_mem,
1161 ModuleId::default(),
1162 &ModulePath::Main,
1163 )
1164 .await;
1165
1166 self.engine.ensure_async_commands_completed().await?;
1168
1169 self.engine.clear_queues().await;
1172
1173 #[cfg(feature = "artifact-graph")]
1174 {
1175 let new_commands = self.engine.take_artifact_commands().await;
1176 let new_responses = self.engine.take_responses().await;
1177 let initial_graph = exec_state.global.artifact_graph.clone();
1178
1179 let graph_result = build_artifact_graph(
1181 &new_commands,
1182 &new_responses,
1183 program,
1184 cached_body_items,
1185 &mut exec_state.global.artifacts,
1186 initial_graph,
1187 );
1188 exec_state.global.artifact_commands.extend(new_commands);
1191 exec_state.global.artifact_responses.extend(new_responses);
1192
1193 match graph_result {
1194 Ok(artifact_graph) => {
1195 exec_state.global.artifact_graph = artifact_graph;
1196 exec_result.map(|(_, env_ref, _)| env_ref)
1197 }
1198 Err(err) => {
1199 exec_result.and(Err(err))
1201 }
1202 }
1203 }
1204 #[cfg(not(feature = "artifact-graph"))]
1205 {
1206 exec_result.map(|(_, env_ref, _)| env_ref)
1207 }
1208 }
1209
1210 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1214 if exec_state.stack().memory.requires_std() {
1215 let path = vec!["std".to_owned(), "prelude".to_owned()];
1216 let resolved_path = ModulePath::from_std_import_path(&path)?;
1217 let id = self
1218 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1219 .await?;
1220 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1221
1222 exec_state.mut_stack().memory.set_std(module_memory);
1223 }
1224
1225 Ok(())
1226 }
1227
1228 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1230 self.engine
1232 .send_modeling_cmd(
1233 uuid::Uuid::new_v4(),
1234 crate::execution::SourceRange::default(),
1235 &ModelingCmd::from(mcmd::ZoomToFit {
1236 object_ids: Default::default(),
1237 animated: false,
1238 padding: 0.1,
1239 }),
1240 )
1241 .await
1242 .map_err(KclErrorWithOutputs::no_outputs)?;
1243
1244 let resp = self
1246 .engine
1247 .send_modeling_cmd(
1248 uuid::Uuid::new_v4(),
1249 crate::execution::SourceRange::default(),
1250 &ModelingCmd::from(mcmd::TakeSnapshot {
1251 format: ImageFormat::Png,
1252 }),
1253 )
1254 .await
1255 .map_err(KclErrorWithOutputs::no_outputs)?;
1256
1257 let OkWebSocketResponseData::Modeling {
1258 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1259 } = resp
1260 else {
1261 return Err(ExecError::BadPng(format!(
1262 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1263 )));
1264 };
1265 Ok(contents)
1266 }
1267
1268 pub async fn export(
1270 &self,
1271 format: kittycad_modeling_cmds::format::OutputFormat3d,
1272 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1273 let resp = self
1274 .engine
1275 .send_modeling_cmd(
1276 uuid::Uuid::new_v4(),
1277 crate::SourceRange::default(),
1278 &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
1279 entity_ids: vec![],
1280 format,
1281 }),
1282 )
1283 .await?;
1284
1285 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1286 return Err(KclError::Internal(crate::errors::KclErrorDetails::new(
1287 format!("Expected Export response, got {resp:?}",),
1288 vec![SourceRange::default()],
1289 )));
1290 };
1291
1292 Ok(files)
1293 }
1294
1295 pub async fn export_step(
1297 &self,
1298 deterministic_time: bool,
1299 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1300 let files = self
1301 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1302 kittycad_modeling_cmds::format::step::export::Options {
1303 coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1304 created: if deterministic_time {
1305 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1306 KclError::Internal(crate::errors::KclErrorDetails::new(
1307 format!("Failed to parse date: {}", e),
1308 vec![SourceRange::default()],
1309 ))
1310 })?)
1311 } else {
1312 None
1313 },
1314 },
1315 ))
1316 .await?;
1317
1318 Ok(files)
1319 }
1320
1321 pub async fn close(&self) {
1322 self.engine.close().await;
1323 }
1324}
1325
1326#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS, JsonSchema)]
1327pub struct ArtifactId(Uuid);
1328
1329impl ArtifactId {
1330 pub fn new(uuid: Uuid) -> Self {
1331 Self(uuid)
1332 }
1333}
1334
1335impl From<Uuid> for ArtifactId {
1336 fn from(uuid: Uuid) -> Self {
1337 Self::new(uuid)
1338 }
1339}
1340
1341impl From<&Uuid> for ArtifactId {
1342 fn from(uuid: &Uuid) -> Self {
1343 Self::new(*uuid)
1344 }
1345}
1346
1347impl From<ArtifactId> for Uuid {
1348 fn from(id: ArtifactId) -> Self {
1349 id.0
1350 }
1351}
1352
1353impl From<&ArtifactId> for Uuid {
1354 fn from(id: &ArtifactId) -> Self {
1355 id.0
1356 }
1357}
1358
1359impl From<ModelingCmdId> for ArtifactId {
1360 fn from(id: ModelingCmdId) -> Self {
1361 Self::new(*id.as_ref())
1362 }
1363}
1364
1365impl From<&ModelingCmdId> for ArtifactId {
1366 fn from(id: &ModelingCmdId) -> Self {
1367 Self::new(*id.as_ref())
1368 }
1369}
1370
1371#[cfg(test)]
1372pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1373 parse_execute_with_project_dir(code, None).await
1374}
1375
1376#[cfg(test)]
1377pub(crate) async fn parse_execute_with_project_dir(
1378 code: &str,
1379 project_directory: Option<TypedPath>,
1380) -> Result<ExecTestResults, KclError> {
1381 let program = crate::Program::parse_no_errs(code)?;
1382
1383 let exec_ctxt = ExecutorContext {
1384 engine: Arc::new(Box::new(
1385 crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
1386 KclError::Internal(crate::errors::KclErrorDetails::new(
1387 format!("Failed to create mock engine connection: {}", err),
1388 vec![SourceRange::default()],
1389 ))
1390 })?,
1391 )),
1392 fs: Arc::new(crate::fs::FileManager::new()),
1393 stdlib: Arc::new(crate::std::StdLib::new()),
1394 settings: ExecutorSettings {
1395 project_directory,
1396 ..Default::default()
1397 },
1398 context_type: ContextType::Mock,
1399 };
1400 let mut exec_state = ExecState::new(&exec_ctxt);
1401 let result = exec_ctxt.run(&program, &mut exec_state).await?;
1402
1403 Ok(ExecTestResults {
1404 program,
1405 mem_env: result.0,
1406 exec_ctxt,
1407 exec_state,
1408 })
1409}
1410
1411#[cfg(test)]
1412#[derive(Debug)]
1413pub(crate) struct ExecTestResults {
1414 program: crate::Program,
1415 mem_env: EnvironmentRef,
1416 exec_ctxt: ExecutorContext,
1417 exec_state: ExecState,
1418}
1419
1420#[cfg(test)]
1421mod tests {
1422 use pretty_assertions::assert_eq;
1423
1424 use super::*;
1425 use crate::{errors::KclErrorDetails, execution::memory::Stack, ModuleId};
1426
1427 #[track_caller]
1429 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1430 memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1431 }
1432
1433 #[tokio::test(flavor = "multi_thread")]
1434 async fn test_execute_warn() {
1435 let text = "@blah";
1436 let result = parse_execute(text).await.unwrap();
1437 let errs = result.exec_state.errors();
1438 assert_eq!(errs.len(), 1);
1439 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1440 assert!(
1441 errs[0].message.contains("Unknown annotation"),
1442 "unexpected warning message: {}",
1443 errs[0].message
1444 );
1445 }
1446
1447 #[tokio::test(flavor = "multi_thread")]
1448 async fn test_execute_fn_definitions() {
1449 let ast = r#"fn def(@x) {
1450 return x
1451}
1452fn ghi(@x) {
1453 return x
1454}
1455fn jkl(@x) {
1456 return x
1457}
1458fn hmm(@x) {
1459 return x
1460}
1461
1462yo = 5 + 6
1463
1464abc = 3
1465identifierGuy = 5
1466part001 = startSketchOn(XY)
1467|> startProfile(at = [-1.2, 4.83])
1468|> line(end = [2.8, 0])
1469|> angledLine(angle = 100 + 100, length = 3.01)
1470|> angledLine(angle = abc, length = 3.02)
1471|> angledLine(angle = def(yo), length = 3.03)
1472|> angledLine(angle = ghi(2), length = 3.04)
1473|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1474|> close()
1475yo2 = hmm([identifierGuy + 5])"#;
1476
1477 parse_execute(ast).await.unwrap();
1478 }
1479
1480 #[tokio::test(flavor = "multi_thread")]
1481 async fn test_execute_with_pipe_substitutions_unary() {
1482 let ast = r#"myVar = 3
1483part001 = startSketchOn(XY)
1484 |> startProfile(at = [0, 0])
1485 |> line(end = [3, 4], tag = $seg01)
1486 |> line(end = [
1487 min([segLen(seg01), myVar]),
1488 -legLen(hypotenuse = segLen(seg01), leg = myVar)
1489])
1490"#;
1491
1492 parse_execute(ast).await.unwrap();
1493 }
1494
1495 #[tokio::test(flavor = "multi_thread")]
1496 async fn test_execute_with_pipe_substitutions() {
1497 let ast = r#"myVar = 3
1498part001 = startSketchOn(XY)
1499 |> startProfile(at = [0, 0])
1500 |> line(end = [3, 4], tag = $seg01)
1501 |> line(end = [
1502 min([segLen(seg01), myVar]),
1503 legLen(hypotenuse = segLen(seg01), leg = myVar)
1504])
1505"#;
1506
1507 parse_execute(ast).await.unwrap();
1508 }
1509
1510 #[tokio::test(flavor = "multi_thread")]
1511 async fn test_execute_with_inline_comment() {
1512 let ast = r#"baseThick = 1
1513armAngle = 60
1514
1515baseThickHalf = baseThick / 2
1516halfArmAngle = armAngle / 2
1517
1518arrExpShouldNotBeIncluded = [1, 2, 3]
1519objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1520
1521part001 = startSketchOn(XY)
1522 |> startProfile(at = [0, 0])
1523 |> yLine(endAbsolute = 1)
1524 |> xLine(length = 3.84) // selection-range-7ish-before-this
1525
1526variableBelowShouldNotBeIncluded = 3
1527"#;
1528
1529 parse_execute(ast).await.unwrap();
1530 }
1531
1532 #[tokio::test(flavor = "multi_thread")]
1533 async fn test_execute_with_function_literal_in_pipe() {
1534 let ast = r#"w = 20
1535l = 8
1536h = 10
1537
1538fn thing() {
1539 return -8
1540}
1541
1542firstExtrude = startSketchOn(XY)
1543 |> startProfile(at = [0,0])
1544 |> line(end = [0, l])
1545 |> line(end = [w, 0])
1546 |> line(end = [0, thing()])
1547 |> close()
1548 |> extrude(length = h)"#;
1549
1550 parse_execute(ast).await.unwrap();
1551 }
1552
1553 #[tokio::test(flavor = "multi_thread")]
1554 async fn test_execute_with_function_unary_in_pipe() {
1555 let ast = r#"w = 20
1556l = 8
1557h = 10
1558
1559fn thing(@x) {
1560 return -x
1561}
1562
1563firstExtrude = startSketchOn(XY)
1564 |> startProfile(at = [0,0])
1565 |> line(end = [0, l])
1566 |> line(end = [w, 0])
1567 |> line(end = [0, thing(8)])
1568 |> close()
1569 |> extrude(length = h)"#;
1570
1571 parse_execute(ast).await.unwrap();
1572 }
1573
1574 #[tokio::test(flavor = "multi_thread")]
1575 async fn test_execute_with_function_array_in_pipe() {
1576 let ast = r#"w = 20
1577l = 8
1578h = 10
1579
1580fn thing(@x) {
1581 return [0, -x]
1582}
1583
1584firstExtrude = startSketchOn(XY)
1585 |> startProfile(at = [0,0])
1586 |> line(end = [0, l])
1587 |> line(end = [w, 0])
1588 |> line(end = thing(8))
1589 |> close()
1590 |> extrude(length = h)"#;
1591
1592 parse_execute(ast).await.unwrap();
1593 }
1594
1595 #[tokio::test(flavor = "multi_thread")]
1596 async fn test_execute_with_function_call_in_pipe() {
1597 let ast = r#"w = 20
1598l = 8
1599h = 10
1600
1601fn other_thing(@y) {
1602 return -y
1603}
1604
1605fn thing(@x) {
1606 return other_thing(x)
1607}
1608
1609firstExtrude = startSketchOn(XY)
1610 |> startProfile(at = [0,0])
1611 |> line(end = [0, l])
1612 |> line(end = [w, 0])
1613 |> line(end = [0, thing(8)])
1614 |> close()
1615 |> extrude(length = h)"#;
1616
1617 parse_execute(ast).await.unwrap();
1618 }
1619
1620 #[tokio::test(flavor = "multi_thread")]
1621 async fn test_execute_with_function_sketch() {
1622 let ast = r#"fn box(h, l, w) {
1623 myBox = startSketchOn(XY)
1624 |> startProfile(at = [0,0])
1625 |> line(end = [0, l])
1626 |> line(end = [w, 0])
1627 |> line(end = [0, -l])
1628 |> close()
1629 |> extrude(length = h)
1630
1631 return myBox
1632}
1633
1634fnBox = box(h = 3, l = 6, w = 10)"#;
1635
1636 parse_execute(ast).await.unwrap();
1637 }
1638
1639 #[tokio::test(flavor = "multi_thread")]
1640 async fn test_get_member_of_object_with_function_period() {
1641 let ast = r#"fn box(@obj) {
1642 myBox = startSketchOn(XY)
1643 |> startProfile(at = obj.start)
1644 |> line(end = [0, obj.l])
1645 |> line(end = [obj.w, 0])
1646 |> line(end = [0, -obj.l])
1647 |> close()
1648 |> extrude(length = obj.h)
1649
1650 return myBox
1651}
1652
1653thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1654"#;
1655 parse_execute(ast).await.unwrap();
1656 }
1657
1658 #[tokio::test(flavor = "multi_thread")]
1659 #[ignore] async fn test_object_member_starting_pipeline() {
1661 let ast = r#"
1662fn test2() {
1663 return {
1664 thing: startSketchOn(XY)
1665 |> startProfile(at = [0, 0])
1666 |> line(end = [0, 1])
1667 |> line(end = [1, 0])
1668 |> line(end = [0, -1])
1669 |> close()
1670 }
1671}
1672
1673x2 = test2()
1674
1675x2.thing
1676 |> extrude(length = 10)
1677"#;
1678 parse_execute(ast).await.unwrap();
1679 }
1680
1681 #[tokio::test(flavor = "multi_thread")]
1682 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
1684 let ast = r#"fn box(obj) {
1685let myBox = startSketchOn(XY)
1686 |> startProfile(at = obj.start)
1687 |> line(end = [0, obj.l])
1688 |> line(end = [obj.w, 0])
1689 |> line(end = [0, -obj.l])
1690 |> close()
1691 |> extrude(length = obj.h)
1692
1693 return myBox
1694}
1695
1696for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1697 thisBox = box(var)
1698}"#;
1699
1700 parse_execute(ast).await.unwrap();
1701 }
1702
1703 #[tokio::test(flavor = "multi_thread")]
1704 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
1706 let ast = r#"fn box(h, l, w, start) {
1707 myBox = startSketchOn(XY)
1708 |> startProfile(at = [0,0])
1709 |> line(end = [0, l])
1710 |> line(end = [w, 0])
1711 |> line(end = [0, -l])
1712 |> close()
1713 |> extrude(length = h)
1714
1715 return myBox
1716}
1717
1718
1719for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1720 const thisBox = box(var[0], var[1], var[2], var[3])
1721}"#;
1722
1723 parse_execute(ast).await.unwrap();
1724 }
1725
1726 #[tokio::test(flavor = "multi_thread")]
1727 async fn test_get_member_of_array_with_function() {
1728 let ast = r#"fn box(@arr) {
1729 myBox =startSketchOn(XY)
1730 |> startProfile(at = arr[0])
1731 |> line(end = [0, arr[1]])
1732 |> line(end = [arr[2], 0])
1733 |> line(end = [0, -arr[1]])
1734 |> close()
1735 |> extrude(length = arr[3])
1736
1737 return myBox
1738}
1739
1740thisBox = box([[0,0], 6, 10, 3])
1741
1742"#;
1743 parse_execute(ast).await.unwrap();
1744 }
1745
1746 #[tokio::test(flavor = "multi_thread")]
1747 async fn test_function_cannot_access_future_definitions() {
1748 let ast = r#"
1749fn returnX() {
1750 // x shouldn't be defined yet.
1751 return x
1752}
1753
1754x = 5
1755
1756answer = returnX()"#;
1757
1758 let result = parse_execute(ast).await;
1759 let err = result.unwrap_err();
1760 assert_eq!(err.message(), "`x` is not defined");
1761 }
1762
1763 #[tokio::test(flavor = "multi_thread")]
1764 async fn test_override_prelude() {
1765 let text = "PI = 3.0";
1766 let result = parse_execute(text).await.unwrap();
1767 let errs = result.exec_state.errors();
1768 assert!(errs.is_empty());
1769 }
1770
1771 #[tokio::test(flavor = "multi_thread")]
1772 async fn type_aliases() {
1773 let text = r#"type MyTy = [number; 2]
1774fn foo(@x: MyTy) {
1775 return x[0]
1776}
1777
1778foo([0, 1])
1779
1780type Other = MyTy | Helix
1781"#;
1782 let result = parse_execute(text).await.unwrap();
1783 let errs = result.exec_state.errors();
1784 assert!(errs.is_empty());
1785 }
1786
1787 #[tokio::test(flavor = "multi_thread")]
1788 async fn test_cannot_shebang_in_fn() {
1789 let ast = r#"
1790fn foo() {
1791 #!hello
1792 return true
1793}
1794
1795foo
1796"#;
1797
1798 let result = parse_execute(ast).await;
1799 let err = result.unwrap_err();
1800 assert_eq!(
1801 err,
1802 KclError::Syntax(KclErrorDetails::new(
1803 "Unexpected token: #".to_owned(),
1804 vec![SourceRange::new(14, 15, ModuleId::default())],
1805 )),
1806 );
1807 }
1808
1809 #[tokio::test(flavor = "multi_thread")]
1810 async fn test_pattern_transform_function_cannot_access_future_definitions() {
1811 let ast = r#"
1812fn transform(@replicaId) {
1813 // x shouldn't be defined yet.
1814 scale = x
1815 return {
1816 translate = [0, 0, replicaId * 10],
1817 scale = [scale, 1, 0],
1818 }
1819}
1820
1821fn layer() {
1822 return startSketchOn(XY)
1823 |> circle( center= [0, 0], radius= 1, tag = $tag1)
1824 |> extrude(length = 10)
1825}
1826
1827x = 5
1828
1829// The 10 layers are replicas of each other, with a transform applied to each.
1830shape = layer() |> patternTransform(instances = 10, transform = transform)
1831"#;
1832
1833 let result = parse_execute(ast).await;
1834 let err = result.unwrap_err();
1835 assert_eq!(err.message(), "`x` is not defined",);
1836 }
1837
1838 #[tokio::test(flavor = "multi_thread")]
1841 async fn test_math_execute_with_functions() {
1842 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
1843 let result = parse_execute(ast).await.unwrap();
1844 assert_eq!(
1845 5.0,
1846 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1847 .as_f64()
1848 .unwrap()
1849 );
1850 }
1851
1852 #[tokio::test(flavor = "multi_thread")]
1853 async fn test_math_execute() {
1854 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
1855 let result = parse_execute(ast).await.unwrap();
1856 assert_eq!(
1857 7.4,
1858 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1859 .as_f64()
1860 .unwrap()
1861 );
1862 }
1863
1864 #[tokio::test(flavor = "multi_thread")]
1865 async fn test_math_execute_start_negative() {
1866 let ast = r#"myVar = -5 + 6"#;
1867 let result = parse_execute(ast).await.unwrap();
1868 assert_eq!(
1869 1.0,
1870 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1871 .as_f64()
1872 .unwrap()
1873 );
1874 }
1875
1876 #[tokio::test(flavor = "multi_thread")]
1877 async fn test_math_execute_with_pi() {
1878 let ast = r#"myVar = PI * 2"#;
1879 let result = parse_execute(ast).await.unwrap();
1880 assert_eq!(
1881 std::f64::consts::TAU,
1882 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1883 .as_f64()
1884 .unwrap()
1885 );
1886 }
1887
1888 #[tokio::test(flavor = "multi_thread")]
1889 async fn test_math_define_decimal_without_leading_zero() {
1890 let ast = r#"thing = .4 + 7"#;
1891 let result = parse_execute(ast).await.unwrap();
1892 assert_eq!(
1893 7.4,
1894 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
1895 .as_f64()
1896 .unwrap()
1897 );
1898 }
1899
1900 #[tokio::test(flavor = "multi_thread")]
1901 async fn test_zero_param_fn() {
1902 let ast = r#"sigmaAllow = 35000 // psi
1903leg1 = 5 // inches
1904leg2 = 8 // inches
1905fn thickness() { return 0.56 }
1906
1907bracket = startSketchOn(XY)
1908 |> startProfile(at = [0,0])
1909 |> line(end = [0, leg1])
1910 |> line(end = [leg2, 0])
1911 |> line(end = [0, -thickness()])
1912 |> line(end = [-leg2 + thickness(), 0])
1913"#;
1914 parse_execute(ast).await.unwrap();
1915 }
1916
1917 #[tokio::test(flavor = "multi_thread")]
1918 async fn test_unary_operator_not_succeeds() {
1919 let ast = r#"
1920fn returnTrue() { return !false }
1921t = true
1922f = false
1923notTrue = !t
1924notFalse = !f
1925c = !!true
1926d = !returnTrue()
1927
1928assertIs(!false, error = "expected to pass")
1929
1930fn check(x) {
1931 assertIs(!x, error = "expected argument to be false")
1932 return true
1933}
1934check(x = false)
1935"#;
1936 let result = parse_execute(ast).await.unwrap();
1937 assert_eq!(
1938 false,
1939 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
1940 .as_bool()
1941 .unwrap()
1942 );
1943 assert_eq!(
1944 true,
1945 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
1946 .as_bool()
1947 .unwrap()
1948 );
1949 assert_eq!(
1950 true,
1951 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
1952 .as_bool()
1953 .unwrap()
1954 );
1955 assert_eq!(
1956 false,
1957 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
1958 .as_bool()
1959 .unwrap()
1960 );
1961 }
1962
1963 #[tokio::test(flavor = "multi_thread")]
1964 async fn test_unary_operator_not_on_non_bool_fails() {
1965 let code1 = r#"
1966// Yup, this is null.
1967myNull = 0 / 0
1968notNull = !myNull
1969"#;
1970 assert_eq!(
1971 parse_execute(code1).await.unwrap_err().message(),
1972 "Cannot apply unary operator ! to non-boolean value: number(default units)",
1973 );
1974
1975 let code2 = "notZero = !0";
1976 assert_eq!(
1977 parse_execute(code2).await.unwrap_err().message(),
1978 "Cannot apply unary operator ! to non-boolean value: number(default units)",
1979 );
1980
1981 let code3 = r#"
1982notEmptyString = !""
1983"#;
1984 assert_eq!(
1985 parse_execute(code3).await.unwrap_err().message(),
1986 "Cannot apply unary operator ! to non-boolean value: string",
1987 );
1988
1989 let code4 = r#"
1990obj = { a = 1 }
1991notMember = !obj.a
1992"#;
1993 assert_eq!(
1994 parse_execute(code4).await.unwrap_err().message(),
1995 "Cannot apply unary operator ! to non-boolean value: number(default units)",
1996 );
1997
1998 let code5 = "
1999a = []
2000notArray = !a";
2001 assert_eq!(
2002 parse_execute(code5).await.unwrap_err().message(),
2003 "Cannot apply unary operator ! to non-boolean value: [any; 0]",
2004 );
2005
2006 let code6 = "
2007x = {}
2008notObject = !x";
2009 assert_eq!(
2010 parse_execute(code6).await.unwrap_err().message(),
2011 "Cannot apply unary operator ! to non-boolean value: { }",
2012 );
2013
2014 let code7 = "
2015fn x() { return 1 }
2016notFunction = !x";
2017 let fn_err = parse_execute(code7).await.unwrap_err();
2018 assert!(
2021 fn_err
2022 .message()
2023 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2024 "Actual error: {:?}",
2025 fn_err
2026 );
2027
2028 let code8 = "
2029myTagDeclarator = $myTag
2030notTagDeclarator = !myTagDeclarator";
2031 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2032 assert!(
2035 tag_declarator_err
2036 .message()
2037 .starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
2038 "Actual error: {:?}",
2039 tag_declarator_err
2040 );
2041
2042 let code9 = "
2043myTagDeclarator = $myTag
2044notTagIdentifier = !myTag";
2045 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2046 assert!(
2049 tag_identifier_err
2050 .message()
2051 .starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
2052 "Actual error: {:?}",
2053 tag_identifier_err
2054 );
2055
2056 let code10 = "notPipe = !(1 |> 2)";
2057 assert_eq!(
2058 parse_execute(code10).await.unwrap_err(),
2061 KclError::Syntax(KclErrorDetails::new(
2062 "Unexpected token: !".to_owned(),
2063 vec![SourceRange::new(10, 11, ModuleId::default())],
2064 ))
2065 );
2066
2067 let code11 = "
2068fn identity(x) { return x }
2069notPipeSub = 1 |> identity(!%))";
2070 assert_eq!(
2071 parse_execute(code11).await.unwrap_err(),
2074 KclError::Syntax(KclErrorDetails::new(
2075 "Unexpected token: |>".to_owned(),
2076 vec![SourceRange::new(44, 46, ModuleId::default())],
2077 ))
2078 );
2079
2080 }
2084
2085 #[tokio::test(flavor = "multi_thread")]
2086 async fn test_math_negative_variable_in_binary_expression() {
2087 let ast = r#"sigmaAllow = 35000 // psi
2088width = 1 // inch
2089
2090p = 150 // lbs
2091distance = 6 // inches
2092FOS = 2
2093
2094leg1 = 5 // inches
2095leg2 = 8 // inches
2096
2097thickness_squared = distance * p * FOS * 6 / sigmaAllow
2098thickness = 0.56 // inches. App does not support square root function yet
2099
2100bracket = startSketchOn(XY)
2101 |> startProfile(at = [0,0])
2102 |> line(end = [0, leg1])
2103 |> line(end = [leg2, 0])
2104 |> line(end = [0, -thickness])
2105 |> line(end = [-leg2 + thickness, 0])
2106"#;
2107 parse_execute(ast).await.unwrap();
2108 }
2109
2110 #[tokio::test(flavor = "multi_thread")]
2111 async fn test_execute_function_no_return() {
2112 let ast = r#"fn test(@origin) {
2113 origin
2114}
2115
2116test([0, 0])
2117"#;
2118 let result = parse_execute(ast).await;
2119 assert!(result.is_err());
2120 assert!(result.unwrap_err().to_string().contains("undefined"),);
2121 }
2122
2123 #[tokio::test(flavor = "multi_thread")]
2124 async fn test_math_doubly_nested_parens() {
2125 let ast = r#"sigmaAllow = 35000 // psi
2126width = 4 // inch
2127p = 150 // Force on shelf - lbs
2128distance = 6 // inches
2129FOS = 2
2130leg1 = 5 // inches
2131leg2 = 8 // inches
2132thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2133thickness = 0.32 // inches. App does not support square root function yet
2134bracket = startSketchOn(XY)
2135 |> startProfile(at = [0,0])
2136 |> line(end = [0, leg1])
2137 |> line(end = [leg2, 0])
2138 |> line(end = [0, -thickness])
2139 |> line(end = [-1 * leg2 + thickness, 0])
2140 |> line(end = [0, -1 * leg1 + thickness])
2141 |> close()
2142 |> extrude(length = width)
2143"#;
2144 parse_execute(ast).await.unwrap();
2145 }
2146
2147 #[tokio::test(flavor = "multi_thread")]
2148 async fn test_math_nested_parens_one_less() {
2149 let ast = r#" sigmaAllow = 35000 // psi
2150width = 4 // inch
2151p = 150 // Force on shelf - lbs
2152distance = 6 // inches
2153FOS = 2
2154leg1 = 5 // inches
2155leg2 = 8 // inches
2156thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2157thickness = 0.32 // inches. App does not support square root function yet
2158bracket = startSketchOn(XY)
2159 |> startProfile(at = [0,0])
2160 |> line(end = [0, leg1])
2161 |> line(end = [leg2, 0])
2162 |> line(end = [0, -thickness])
2163 |> line(end = [-1 * leg2 + thickness, 0])
2164 |> line(end = [0, -1 * leg1 + thickness])
2165 |> close()
2166 |> extrude(length = width)
2167"#;
2168 parse_execute(ast).await.unwrap();
2169 }
2170
2171 #[tokio::test(flavor = "multi_thread")]
2172 async fn test_fn_as_operand() {
2173 let ast = r#"fn f() { return 1 }
2174x = f()
2175y = x + 1
2176z = f() + 1
2177w = f() + f()
2178"#;
2179 parse_execute(ast).await.unwrap();
2180 }
2181
2182 #[tokio::test(flavor = "multi_thread")]
2183 async fn kcl_test_ids_stable_between_executions() {
2184 let code = r#"sketch001 = startSketchOn(XZ)
2185|> startProfile(at = [61.74, 206.13])
2186|> xLine(length = 305.11, tag = $seg01)
2187|> yLine(length = -291.85)
2188|> xLine(length = -segLen(seg01))
2189|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2190|> close()
2191|> extrude(length = 40.14)
2192|> shell(
2193 thickness = 3.14,
2194 faces = [seg01]
2195)
2196"#;
2197
2198 let ctx = crate::test_server::new_context(true, None).await.unwrap();
2199 let old_program = crate::Program::parse_no_errs(code).unwrap();
2200
2201 if let Err(err) = ctx.run_with_caching(old_program).await {
2203 let report = err.into_miette_report_with_outputs(code).unwrap();
2204 let report = miette::Report::new(report);
2205 panic!("Error executing program: {:?}", report);
2206 }
2207
2208 let id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
2210
2211 let code = r#"sketch001 = startSketchOn(XZ)
2212|> startProfile(at = [62.74, 206.13])
2213|> xLine(length = 305.11, tag = $seg01)
2214|> yLine(length = -291.85)
2215|> xLine(length = -segLen(seg01))
2216|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2217|> close()
2218|> extrude(length = 40.14)
2219|> shell(
2220 faces = [seg01],
2221 thickness = 3.14,
2222)
2223"#;
2224
2225 let program = crate::Program::parse_no_errs(code).unwrap();
2227 ctx.run_with_caching(program).await.unwrap();
2229
2230 let new_id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
2231
2232 assert_eq!(id_generator, new_id_generator);
2233 }
2234
2235 #[tokio::test(flavor = "multi_thread")]
2236 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2237 let code = r#"sketch001 = startSketchOn(XZ)
2238|> startProfile(at = [61.74, 206.13])
2239|> xLine(length = 305.11, tag = $seg01)
2240|> yLine(length = -291.85)
2241|> xLine(length = -segLen(seg01))
2242|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2243|> close()
2244|> extrude(length = 40.14)
2245|> shell(
2246 thickness = 3.14,
2247 faces = [seg01]
2248)
2249"#;
2250
2251 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2252 let old_program = crate::Program::parse_no_errs(code).unwrap();
2253
2254 ctx.run_with_caching(old_program.clone()).await.unwrap();
2256
2257 let settings_state = cache::read_old_ast().await.unwrap().settings;
2258
2259 assert_eq!(settings_state, ctx.settings);
2261
2262 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2264
2265 ctx.run_with_caching(old_program.clone()).await.unwrap();
2267
2268 let settings_state = cache::read_old_ast().await.unwrap().settings;
2269
2270 assert_eq!(settings_state, ctx.settings);
2272
2273 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2275
2276 ctx.run_with_caching(old_program).await.unwrap();
2278
2279 let settings_state = cache::read_old_ast().await.unwrap().settings;
2280
2281 assert_eq!(settings_state, ctx.settings);
2283
2284 ctx.close().await;
2285 }
2286
2287 #[tokio::test(flavor = "multi_thread")]
2288 async fn mock_after_not_mock() {
2289 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2290 let program = crate::Program::parse_no_errs("x = 2").unwrap();
2291 let result = ctx.run_with_caching(program).await.unwrap();
2292 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2293
2294 let ctx2 = ExecutorContext::new_mock(None).await;
2295 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2296 let result = ctx2.run_mock(program2, true).await.unwrap();
2297 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2298
2299 ctx.close().await;
2300 ctx2.close().await;
2301 }
2302
2303 #[tokio::test(flavor = "multi_thread")]
2304 async fn read_tag_version() {
2305 let ast = r#"fn bar(@t) {
2306 return startSketchOn(XY)
2307 |> startProfile(at = [0,0])
2308 |> angledLine(
2309 angle = -60,
2310 length = segLen(t),
2311 )
2312 |> line(end = [0, 0])
2313 |> close()
2314}
2315
2316sketch = startSketchOn(XY)
2317 |> startProfile(at = [0,0])
2318 |> line(end = [0, 10])
2319 |> line(end = [10, 0], tag = $tag0)
2320 |> line(end = [0, 0])
2321
2322fn foo() {
2323 // tag0 tags an edge
2324 return bar(tag0)
2325}
2326
2327solid = sketch |> extrude(length = 10)
2328// tag0 tags a face
2329sketch2 = startSketchOn(solid, face = tag0)
2330 |> startProfile(at = [0,0])
2331 |> line(end = [0, 1])
2332 |> line(end = [1, 0])
2333 |> line(end = [0, 0])
2334
2335foo() |> extrude(length = 1)
2336"#;
2337 parse_execute(ast).await.unwrap();
2338 }
2339}