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