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)]
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 if settings.enable_ssao {
391 Some(kittycad::types::PostEffectType::Ssao)
392 } else {
393 None
394 },
395 settings.replay.clone(),
396 if settings.show_grid { Some(true) } else { None },
397 None,
398 None,
399 None,
400 Some(false),
401 )
402 .await?;
403
404 let engine: Arc<Box<dyn EngineManager>> =
405 Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
406
407 Ok(Self {
408 engine,
409 fs: Arc::new(FileManager::new()),
410 stdlib: Arc::new(StdLib::new()),
411 settings,
412 context_type: ContextType::Live,
413 })
414 }
415
416 #[cfg(target_arch = "wasm32")]
417 pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
418 ExecutorContext {
419 engine,
420 fs,
421 stdlib: Arc::new(StdLib::new()),
422 settings,
423 context_type: ContextType::Live,
424 }
425 }
426
427 #[cfg(not(target_arch = "wasm32"))]
428 pub async fn new_mock() -> Self {
429 ExecutorContext {
430 engine: Arc::new(Box::new(
431 crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
432 )),
433 fs: Arc::new(FileManager::new()),
434 stdlib: Arc::new(StdLib::new()),
435 settings: Default::default(),
436 context_type: ContextType::Mock,
437 }
438 }
439
440 #[cfg(target_arch = "wasm32")]
441 pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
442 ExecutorContext {
443 engine,
444 fs,
445 stdlib: Arc::new(StdLib::new()),
446 settings,
447 context_type: ContextType::Mock,
448 }
449 }
450
451 #[cfg(not(target_arch = "wasm32"))]
452 pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
453 ExecutorContext {
454 engine,
455 fs: Arc::new(FileManager::new()),
456 stdlib: Arc::new(StdLib::new()),
457 settings: Default::default(),
458 context_type: ContextType::MockCustomForwarded,
459 }
460 }
461
462 #[cfg(not(target_arch = "wasm32"))]
468 pub async fn new_with_client(
469 settings: ExecutorSettings,
470 token: Option<String>,
471 engine_addr: Option<String>,
472 ) -> Result<Self> {
473 let client = crate::engine::new_zoo_client(token, engine_addr)?;
475
476 let ctx = Self::new(&client, settings).await?;
477 Ok(ctx)
478 }
479
480 #[cfg(not(target_arch = "wasm32"))]
485 pub async fn new_with_default_client() -> Result<Self> {
486 let ctx = Self::new_with_client(Default::default(), None, None).await?;
488 Ok(ctx)
489 }
490
491 #[cfg(not(target_arch = "wasm32"))]
493 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
494 let ctx = ExecutorContext::new_with_client(
495 ExecutorSettings {
496 highlight_edges: true,
497 enable_ssao: false,
498 show_grid: false,
499 replay: None,
500 project_directory: None,
501 current_file: None,
502 },
503 None,
504 engine_addr,
505 )
506 .await?;
507 Ok(ctx)
508 }
509
510 pub fn is_mock(&self) -> bool {
511 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
512 }
513
514 pub async fn no_engine_commands(&self) -> bool {
516 self.is_mock()
517 }
518
519 pub async fn send_clear_scene(
520 &self,
521 exec_state: &mut ExecState,
522 source_range: crate::execution::SourceRange,
523 ) -> Result<(), KclError> {
524 self.engine
525 .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
526 .await
527 }
528
529 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
530 cache::bust_cache().await;
531
532 let outcome = self.run_with_caching(crate::Program::empty()).await?;
537
538 Ok(outcome)
539 }
540
541 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
542 self.eval_prelude(exec_state, SourceRange::synthetic())
543 .await
544 .map_err(KclErrorWithOutputs::no_outputs)?;
545 exec_state.mut_stack().push_new_root_env(true);
546 Ok(())
547 }
548
549 pub async fn run_mock(
550 &self,
551 program: crate::Program,
552 use_prev_memory: bool,
553 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
554 assert!(self.is_mock());
555
556 let mut exec_state = ExecState::new(self);
557 if use_prev_memory {
558 match cache::read_old_memory().await {
559 Some(mem) => {
560 *exec_state.mut_stack() = mem.0;
561 exec_state.global.module_infos = mem.1;
562 }
563 None => self.prepare_mem(&mut exec_state).await?,
564 }
565 } else {
566 self.prepare_mem(&mut exec_state).await?
567 };
568
569 exec_state.mut_stack().push_new_env_for_scope();
572
573 let result = self.inner_run(&program, &mut exec_state, true).await?;
574
575 let mut mem = exec_state.stack().clone();
580 let module_infos = exec_state.global.module_infos.clone();
581 let outcome = exec_state.to_mock_exec_outcome(result.0).await;
582
583 mem.squash_env(result.0);
584 cache::write_old_memory((mem, module_infos)).await;
585
586 Ok(outcome)
587 }
588
589 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
590 assert!(!self.is_mock());
591
592 let (program, mut exec_state, preserve_mem, imports_info) = if let Some(OldAstState {
593 ast: old_ast,
594 exec_state: mut old_state,
595 settings: old_settings,
596 result_env,
597 }) = cache::read_old_ast().await
598 {
599 let old = CacheInformation {
600 ast: &old_ast,
601 settings: &old_settings,
602 };
603 let new = CacheInformation {
604 ast: &program.ast,
605 settings: &self.settings,
606 };
607
608 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
610 CacheResult::ReExecute {
611 clear_scene,
612 reapply_settings,
613 program: changed_program,
614 } => {
615 if reapply_settings
616 && self
617 .engine
618 .reapply_settings(&self.settings, Default::default(), old_state.id_generator())
619 .await
620 .is_err()
621 {
622 (true, program, None)
623 } else {
624 (
625 clear_scene,
626 crate::Program {
627 ast: changed_program,
628 original_file_contents: program.original_file_contents,
629 },
630 None,
631 )
632 }
633 }
634 CacheResult::CheckImportsOnly {
635 reapply_settings,
636 ast: changed_program,
637 } => {
638 if reapply_settings
639 && self
640 .engine
641 .reapply_settings(&self.settings, Default::default(), old_state.id_generator())
642 .await
643 .is_err()
644 {
645 (true, program, None)
646 } else {
647 let mut new_exec_state = ExecState::new(self);
649 let (new_universe, new_universe_map) = self.get_universe(&program, &mut new_exec_state).await?;
650 let mut clear_scene = false;
651
652 let mut keys = new_universe.keys().clone().collect::<Vec<_>>();
653 keys.sort();
654 for key in keys {
655 let (_, id, _, _) = &new_universe[key];
656 let old_source = old_state.get_source(*id);
657 let new_source = new_exec_state.get_source(*id);
658 if old_source != new_source {
659 clear_scene = true;
660 break;
661 }
662 }
663
664 (
665 clear_scene,
666 crate::Program {
667 ast: changed_program,
668 original_file_contents: program.original_file_contents,
669 },
670 if clear_scene {
672 Some((new_universe, new_universe_map, new_exec_state))
673 } else {
674 None
675 },
676 )
677 }
678 }
679 CacheResult::NoAction(true) => {
680 if self
681 .engine
682 .reapply_settings(&self.settings, Default::default(), old_state.id_generator())
683 .await
684 .is_ok()
685 {
686 cache::write_old_ast(OldAstState {
688 ast: old_ast,
689 exec_state: old_state.clone(),
690 settings: self.settings.clone(),
691 result_env,
692 })
693 .await;
694
695 let outcome = old_state.to_exec_outcome(result_env).await;
696 return Ok(outcome);
697 }
698 (true, program, None)
699 }
700 CacheResult::NoAction(false) => {
701 let outcome = old_state.to_exec_outcome(result_env).await;
702 return Ok(outcome);
703 }
704 };
705
706 let (exec_state, preserve_mem, universe_info) =
707 if let Some((new_universe, new_universe_map, mut new_exec_state)) = import_check_info {
708 self.send_clear_scene(&mut new_exec_state, Default::default())
710 .await
711 .map_err(KclErrorWithOutputs::no_outputs)?;
712
713 (new_exec_state, false, Some((new_universe, new_universe_map)))
714 } else if clear_scene {
715 let mut exec_state = old_state;
717 exec_state.reset(self);
718
719 self.send_clear_scene(&mut exec_state, Default::default())
720 .await
721 .map_err(KclErrorWithOutputs::no_outputs)?;
722
723 (exec_state, false, None)
724 } else {
725 old_state.mut_stack().restore_env(result_env);
726
727 (old_state, true, None)
728 };
729
730 (program, exec_state, preserve_mem, universe_info)
731 } else {
732 let mut exec_state = ExecState::new(self);
733 self.send_clear_scene(&mut exec_state, Default::default())
734 .await
735 .map_err(KclErrorWithOutputs::no_outputs)?;
736 (program, exec_state, false, None)
737 };
738
739 let result = self
740 .run_concurrent(&program, &mut exec_state, imports_info, preserve_mem)
741 .await;
742
743 if result.is_err() {
744 cache::bust_cache().await;
745 }
746
747 let result = result?;
749
750 cache::write_old_ast(OldAstState {
752 ast: program.ast,
753 exec_state: exec_state.clone(),
754 settings: self.settings.clone(),
755 result_env: result.0,
756 })
757 .await;
758
759 let outcome = exec_state.to_exec_outcome(result.0).await;
760 Ok(outcome)
761 }
762
763 pub async fn run(
770 &self,
771 program: &crate::Program,
772 exec_state: &mut ExecState,
773 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
774 self.run_concurrent(program, exec_state, None, false).await
775 }
776
777 pub async fn run_concurrent(
785 &self,
786 program: &crate::Program,
787 exec_state: &mut ExecState,
788 universe_info: Option<(Universe, UniverseMap)>,
789 preserve_mem: bool,
790 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
791 #[allow(unused_variables)]
793 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
794 (universe, universe_map)
795 } else {
796 self.get_universe(program, exec_state).await?
797 };
798
799 let default_planes = self.engine.get_default_planes().read().await.clone();
800
801 self.eval_prelude(exec_state, SourceRange::synthetic())
803 .await
804 .map_err(KclErrorWithOutputs::no_outputs)?;
805
806 for modules in crate::walk::import_graph(&universe, self)
807 .map_err(|err| {
808 let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
809 .global
810 .path_to_source_id
811 .iter()
812 .map(|(k, v)| ((*v), k.clone()))
813 .collect();
814
815 KclErrorWithOutputs::new(
816 err,
817 #[cfg(feature = "artifact-graph")]
818 exec_state.global.operations.clone(),
819 #[cfg(feature = "artifact-graph")]
820 exec_state.global.artifact_commands.clone(),
821 #[cfg(feature = "artifact-graph")]
822 exec_state.global.artifact_graph.clone(),
823 module_id_to_module_path,
824 exec_state.global.id_to_source.clone(),
825 default_planes.clone(),
826 )
827 })?
828 .into_iter()
829 {
830 #[cfg(not(target_arch = "wasm32"))]
831 let mut set = tokio::task::JoinSet::new();
832
833 #[allow(clippy::type_complexity)]
834 let (results_tx, mut results_rx): (
835 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
836 tokio::sync::mpsc::Receiver<_>,
837 ) = tokio::sync::mpsc::channel(1);
838
839 for module in modules {
840 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
841 return Err(KclErrorWithOutputs::no_outputs(KclError::Internal(KclErrorDetails {
842 message: format!("Module {module} not found in universe"),
843 source_ranges: Default::default(),
844 })));
845 };
846 let module_id = *module_id;
847 let module_path = module_path.clone();
848 let source_range = SourceRange::from(import_stmt);
849
850 #[cfg(feature = "artifact-graph")]
851 match &module_path {
852 ModulePath::Main => {
853 }
855 ModulePath::Local { value, .. } => {
856 if universe_map.contains_key(value) {
859 exec_state.global.operations.push(Operation::GroupBegin {
860 group: Group::ModuleInstance {
861 name: value.file_name().unwrap_or_default(),
862 module_id,
863 },
864 source_range,
865 });
866 exec_state.global.operations.push(Operation::GroupEnd);
870 }
871 }
872 ModulePath::Std { .. } => {
873 }
875 }
876
877 let repr = repr.clone();
878 let exec_state = exec_state.clone();
879 let exec_ctxt = self.clone();
880 let results_tx = results_tx.clone();
881
882 let exec_module = async |exec_ctxt: &ExecutorContext,
883 repr: &ModuleRepr,
884 module_id: ModuleId,
885 module_path: &ModulePath,
886 exec_state: &mut ExecState,
887 source_range: SourceRange|
888 -> Result<ModuleRepr, KclError> {
889 match repr {
890 ModuleRepr::Kcl(program, _) => {
891 let result = exec_ctxt
892 .exec_module_from_ast(program, module_id, module_path, exec_state, source_range, false)
893 .await;
894
895 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
896 }
897 ModuleRepr::Foreign(geom, _) => {
898 let result = crate::execution::import::send_to_engine(geom.clone(), exec_ctxt)
899 .await
900 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
901
902 result.map(|val| ModuleRepr::Foreign(geom.clone(), val))
903 }
904 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::Internal(KclErrorDetails {
905 message: format!("Module {module_path} not found in universe"),
906 source_ranges: vec![source_range],
907 })),
908 }
909 };
910
911 #[cfg(target_arch = "wasm32")]
912 {
913 wasm_bindgen_futures::spawn_local(async move {
914 let mut exec_state = exec_state;
916 let exec_ctxt = exec_ctxt;
917
918 let result = exec_module(
919 &exec_ctxt,
920 &repr,
921 module_id,
922 &module_path,
923 &mut exec_state,
924 source_range,
925 )
926 .await;
927
928 results_tx
929 .send((module_id, module_path, result))
930 .await
931 .unwrap_or_default();
932 });
933 }
934 #[cfg(not(target_arch = "wasm32"))]
935 {
936 set.spawn(async move {
937 let mut exec_state = exec_state;
938 let exec_ctxt = exec_ctxt;
939
940 let result = exec_module(
941 &exec_ctxt,
942 &repr,
943 module_id,
944 &module_path,
945 &mut exec_state,
946 source_range,
947 )
948 .await;
949
950 results_tx
951 .send((module_id, module_path, result))
952 .await
953 .unwrap_or_default();
954 });
955 }
956 }
957
958 drop(results_tx);
959
960 while let Some((module_id, _, result)) = results_rx.recv().await {
961 match result {
962 Ok(new_repr) => {
963 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
964
965 match &mut repr {
966 ModuleRepr::Kcl(_, cache) => {
967 let ModuleRepr::Kcl(_, session_data) = new_repr else {
968 unreachable!();
969 };
970 *cache = session_data;
971 }
972 ModuleRepr::Foreign(_, cache) => {
973 let ModuleRepr::Foreign(_, session_data) = new_repr else {
974 unreachable!();
975 };
976 *cache = session_data;
977 }
978 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
979 }
980
981 exec_state.global.module_infos[&module_id].restore_repr(repr);
982 }
983 Err(e) => {
984 let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
985 .global
986 .path_to_source_id
987 .iter()
988 .map(|(k, v)| ((*v), k.clone()))
989 .collect();
990
991 return Err(KclErrorWithOutputs::new(
992 e,
993 #[cfg(feature = "artifact-graph")]
994 exec_state.global.operations.clone(),
995 #[cfg(feature = "artifact-graph")]
996 exec_state.global.artifact_commands.clone(),
997 #[cfg(feature = "artifact-graph")]
998 exec_state.global.artifact_graph.clone(),
999 module_id_to_module_path,
1000 exec_state.global.id_to_source.clone(),
1001 default_planes,
1002 ));
1003 }
1004 }
1005 }
1006 }
1007
1008 self.inner_run(program, exec_state, preserve_mem).await
1009 }
1010
1011 async fn get_universe(
1014 &self,
1015 program: &crate::Program,
1016 exec_state: &mut ExecState,
1017 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1018 exec_state.add_root_module_contents(program);
1019
1020 let mut universe = std::collections::HashMap::new();
1021
1022 let default_planes = self.engine.get_default_planes().read().await.clone();
1023
1024 let root_imports = crate::walk::import_universe(
1025 self,
1026 &ModuleRepr::Kcl(program.ast.clone(), None),
1027 &mut universe,
1028 exec_state,
1029 )
1030 .await
1031 .map_err(|err| {
1032 println!("Error: {err:?}");
1033 let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
1034 .global
1035 .path_to_source_id
1036 .iter()
1037 .map(|(k, v)| ((*v), k.clone()))
1038 .collect();
1039
1040 KclErrorWithOutputs::new(
1041 err,
1042 #[cfg(feature = "artifact-graph")]
1043 exec_state.global.operations.clone(),
1044 #[cfg(feature = "artifact-graph")]
1045 exec_state.global.artifact_commands.clone(),
1046 #[cfg(feature = "artifact-graph")]
1047 exec_state.global.artifact_graph.clone(),
1048 module_id_to_module_path,
1049 exec_state.global.id_to_source.clone(),
1050 default_planes,
1051 )
1052 })?;
1053
1054 Ok((universe, root_imports))
1055 }
1056
1057 async fn inner_run(
1060 &self,
1061 program: &crate::Program,
1062 exec_state: &mut ExecState,
1063 preserve_mem: bool,
1064 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1065 let _stats = crate::log::LogPerfStats::new("Interpretation");
1066
1067 self.engine
1069 .reapply_settings(&self.settings, Default::default(), exec_state.id_generator())
1070 .await
1071 .map_err(KclErrorWithOutputs::no_outputs)?;
1072
1073 let default_planes = self.engine.get_default_planes().read().await.clone();
1074 let result = self
1075 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1076 .await;
1077
1078 crate::log::log(format!(
1079 "Post interpretation KCL memory stats: {:#?}",
1080 exec_state.stack().memory.stats
1081 ));
1082 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1083
1084 let env_ref = result.map_err(|e| {
1085 let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
1086 .global
1087 .path_to_source_id
1088 .iter()
1089 .map(|(k, v)| ((*v), k.clone()))
1090 .collect();
1091
1092 KclErrorWithOutputs::new(
1093 e,
1094 #[cfg(feature = "artifact-graph")]
1095 exec_state.global.operations.clone(),
1096 #[cfg(feature = "artifact-graph")]
1097 exec_state.global.artifact_commands.clone(),
1098 #[cfg(feature = "artifact-graph")]
1099 exec_state.global.artifact_graph.clone(),
1100 module_id_to_module_path,
1101 exec_state.global.id_to_source.clone(),
1102 default_planes.clone(),
1103 )
1104 })?;
1105
1106 if !self.is_mock() {
1107 let mut mem = exec_state.stack().deep_clone();
1108 mem.restore_env(env_ref);
1109 cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
1110 }
1111 let session_data = self.engine.get_session_data().await;
1112
1113 Ok((env_ref, session_data))
1114 }
1115
1116 async fn execute_and_build_graph(
1119 &self,
1120 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1121 exec_state: &mut ExecState,
1122 preserve_mem: bool,
1123 ) -> Result<EnvironmentRef, KclError> {
1124 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1128 .await?;
1129
1130 let exec_result = self
1131 .exec_module_body(
1132 program,
1133 exec_state,
1134 preserve_mem,
1135 ModuleId::default(),
1136 &ModulePath::Main,
1137 )
1138 .await;
1139
1140 self.engine.ensure_async_commands_completed().await?;
1142
1143 self.engine.clear_queues().await;
1146
1147 #[cfg(feature = "artifact-graph")]
1148 {
1149 exec_state
1152 .global
1153 .artifact_commands
1154 .extend(self.engine.take_artifact_commands().await);
1155 exec_state
1156 .global
1157 .artifact_responses
1158 .extend(self.engine.take_responses().await);
1159 match build_artifact_graph(
1161 &exec_state.global.artifact_commands,
1162 &exec_state.global.artifact_responses,
1163 program,
1164 &exec_state.global.artifacts,
1165 ) {
1166 Ok(artifact_graph) => {
1167 exec_state.global.artifact_graph = artifact_graph;
1168 exec_result.map(|(_, env_ref, _)| env_ref)
1169 }
1170 Err(err) => {
1171 exec_result.and(Err(err))
1173 }
1174 }
1175 }
1176 #[cfg(not(feature = "artifact-graph"))]
1177 {
1178 exec_result.map(|(_, env_ref, _)| env_ref)
1179 }
1180 }
1181
1182 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1186 if exec_state.stack().memory.requires_std() {
1187 let id = self
1188 .open_module(
1189 &ImportPath::Std {
1190 path: vec!["std".to_owned(), "prelude".to_owned()],
1191 },
1192 &[],
1193 exec_state,
1194 source_range,
1195 )
1196 .await?;
1197 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1198
1199 exec_state.mut_stack().memory.set_std(module_memory);
1200 }
1201
1202 Ok(())
1203 }
1204
1205 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1207 self.engine
1209 .send_modeling_cmd(
1210 uuid::Uuid::new_v4(),
1211 crate::execution::SourceRange::default(),
1212 &ModelingCmd::from(mcmd::ZoomToFit {
1213 object_ids: Default::default(),
1214 animated: false,
1215 padding: 0.1,
1216 }),
1217 )
1218 .await
1219 .map_err(KclErrorWithOutputs::no_outputs)?;
1220
1221 let resp = self
1223 .engine
1224 .send_modeling_cmd(
1225 uuid::Uuid::new_v4(),
1226 crate::execution::SourceRange::default(),
1227 &ModelingCmd::from(mcmd::TakeSnapshot {
1228 format: ImageFormat::Png,
1229 }),
1230 )
1231 .await
1232 .map_err(KclErrorWithOutputs::no_outputs)?;
1233
1234 let OkWebSocketResponseData::Modeling {
1235 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1236 } = resp
1237 else {
1238 return Err(ExecError::BadPng(format!(
1239 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1240 )));
1241 };
1242 Ok(contents)
1243 }
1244
1245 pub async fn export(
1247 &self,
1248 format: kittycad_modeling_cmds::format::OutputFormat3d,
1249 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1250 let resp = self
1251 .engine
1252 .send_modeling_cmd(
1253 uuid::Uuid::new_v4(),
1254 crate::SourceRange::default(),
1255 &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
1256 entity_ids: vec![],
1257 format,
1258 }),
1259 )
1260 .await?;
1261
1262 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1263 return Err(KclError::Internal(crate::errors::KclErrorDetails {
1264 message: format!("Expected Export response, got {resp:?}",),
1265 source_ranges: vec![SourceRange::default()],
1266 }));
1267 };
1268
1269 Ok(files)
1270 }
1271
1272 pub async fn export_step(
1274 &self,
1275 deterministic_time: bool,
1276 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1277 let files = self
1278 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1279 kittycad_modeling_cmds::format::step::export::Options {
1280 coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1281 created: if deterministic_time {
1282 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1283 KclError::Internal(crate::errors::KclErrorDetails {
1284 message: format!("Failed to parse date: {}", e),
1285 source_ranges: vec![SourceRange::default()],
1286 })
1287 })?)
1288 } else {
1289 None
1290 },
1291 },
1292 ))
1293 .await?;
1294
1295 Ok(files)
1296 }
1297
1298 pub async fn close(&self) {
1299 self.engine.close().await;
1300 }
1301}
1302
1303#[cfg(test)]
1304pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1305 parse_execute_with_project_dir(code, None).await
1306}
1307
1308#[cfg(test)]
1309pub(crate) async fn parse_execute_with_project_dir(
1310 code: &str,
1311 project_directory: Option<TypedPath>,
1312) -> Result<ExecTestResults, KclError> {
1313 let program = crate::Program::parse_no_errs(code)?;
1314
1315 let exec_ctxt = ExecutorContext {
1316 engine: Arc::new(Box::new(
1317 crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
1318 KclError::Internal(crate::errors::KclErrorDetails {
1319 message: format!("Failed to create mock engine connection: {}", err),
1320 source_ranges: vec![SourceRange::default()],
1321 })
1322 })?,
1323 )),
1324 fs: Arc::new(crate::fs::FileManager::new()),
1325 stdlib: Arc::new(crate::std::StdLib::new()),
1326 settings: ExecutorSettings {
1327 project_directory,
1328 ..Default::default()
1329 },
1330 context_type: ContextType::Mock,
1331 };
1332 let mut exec_state = ExecState::new(&exec_ctxt);
1333 let result = exec_ctxt.run(&program, &mut exec_state).await?;
1334
1335 Ok(ExecTestResults {
1336 program,
1337 mem_env: result.0,
1338 exec_ctxt,
1339 exec_state,
1340 })
1341}
1342
1343#[cfg(test)]
1344#[derive(Debug)]
1345pub(crate) struct ExecTestResults {
1346 program: crate::Program,
1347 mem_env: EnvironmentRef,
1348 exec_ctxt: ExecutorContext,
1349 exec_state: ExecState,
1350}
1351
1352#[cfg(test)]
1353mod tests {
1354 use pretty_assertions::assert_eq;
1355
1356 use super::*;
1357 use crate::{errors::KclErrorDetails, execution::memory::Stack, ModuleId};
1358
1359 #[track_caller]
1361 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1362 memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1363 }
1364
1365 #[tokio::test(flavor = "multi_thread")]
1366 async fn test_execute_warn() {
1367 let text = "@blah";
1368 let result = parse_execute(text).await.unwrap();
1369 let errs = result.exec_state.errors();
1370 assert_eq!(errs.len(), 1);
1371 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1372 assert!(
1373 errs[0].message.contains("Unknown annotation"),
1374 "unexpected warning message: {}",
1375 errs[0].message
1376 );
1377 }
1378
1379 #[tokio::test(flavor = "multi_thread")]
1380 async fn test_execute_fn_definitions() {
1381 let ast = r#"fn def(@x) {
1382 return x
1383}
1384fn ghi(@x) {
1385 return x
1386}
1387fn jkl(@x) {
1388 return x
1389}
1390fn hmm(@x) {
1391 return x
1392}
1393
1394yo = 5 + 6
1395
1396abc = 3
1397identifierGuy = 5
1398part001 = startSketchOn(XY)
1399|> startProfile(at = [-1.2, 4.83])
1400|> line(end = [2.8, 0])
1401|> angledLine(angle = 100 + 100, length = 3.01)
1402|> angledLine(angle = abc, length = 3.02)
1403|> angledLine(angle = def(yo), length = 3.03)
1404|> angledLine(angle = ghi(2), length = 3.04)
1405|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1406|> close()
1407yo2 = hmm([identifierGuy + 5])"#;
1408
1409 parse_execute(ast).await.unwrap();
1410 }
1411
1412 #[tokio::test(flavor = "multi_thread")]
1413 async fn test_execute_with_pipe_substitutions_unary() {
1414 let ast = r#"myVar = 3
1415part001 = startSketchOn(XY)
1416 |> startProfile(at = [0, 0])
1417 |> line(end = [3, 4], tag = $seg01)
1418 |> line(end = [
1419 min([segLen(seg01), myVar]),
1420 -legLen(hypotenuse = segLen(seg01), leg = myVar)
1421])
1422"#;
1423
1424 parse_execute(ast).await.unwrap();
1425 }
1426
1427 #[tokio::test(flavor = "multi_thread")]
1428 async fn test_execute_with_pipe_substitutions() {
1429 let ast = r#"myVar = 3
1430part001 = startSketchOn(XY)
1431 |> startProfile(at = [0, 0])
1432 |> line(end = [3, 4], tag = $seg01)
1433 |> line(end = [
1434 min([segLen(seg01), myVar]),
1435 legLen(hypotenuse = segLen(seg01), leg = myVar)
1436])
1437"#;
1438
1439 parse_execute(ast).await.unwrap();
1440 }
1441
1442 #[tokio::test(flavor = "multi_thread")]
1443 async fn test_execute_with_inline_comment() {
1444 let ast = r#"baseThick = 1
1445armAngle = 60
1446
1447baseThickHalf = baseThick / 2
1448halfArmAngle = armAngle / 2
1449
1450arrExpShouldNotBeIncluded = [1, 2, 3]
1451objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1452
1453part001 = startSketchOn(XY)
1454 |> startProfile(at = [0, 0])
1455 |> yLine(endAbsolute = 1)
1456 |> xLine(length = 3.84) // selection-range-7ish-before-this
1457
1458variableBelowShouldNotBeIncluded = 3
1459"#;
1460
1461 parse_execute(ast).await.unwrap();
1462 }
1463
1464 #[tokio::test(flavor = "multi_thread")]
1465 async fn test_execute_with_function_literal_in_pipe() {
1466 let ast = r#"w = 20
1467l = 8
1468h = 10
1469
1470fn thing() {
1471 return -8
1472}
1473
1474firstExtrude = startSketchOn(XY)
1475 |> startProfile(at = [0,0])
1476 |> line(end = [0, l])
1477 |> line(end = [w, 0])
1478 |> line(end = [0, thing()])
1479 |> close()
1480 |> extrude(length = h)"#;
1481
1482 parse_execute(ast).await.unwrap();
1483 }
1484
1485 #[tokio::test(flavor = "multi_thread")]
1486 async fn test_execute_with_function_unary_in_pipe() {
1487 let ast = r#"w = 20
1488l = 8
1489h = 10
1490
1491fn thing(@x) {
1492 return -x
1493}
1494
1495firstExtrude = startSketchOn(XY)
1496 |> startProfile(at = [0,0])
1497 |> line(end = [0, l])
1498 |> line(end = [w, 0])
1499 |> line(end = [0, thing(8)])
1500 |> close()
1501 |> extrude(length = h)"#;
1502
1503 parse_execute(ast).await.unwrap();
1504 }
1505
1506 #[tokio::test(flavor = "multi_thread")]
1507 async fn test_execute_with_function_array_in_pipe() {
1508 let ast = r#"w = 20
1509l = 8
1510h = 10
1511
1512fn thing(@x) {
1513 return [0, -x]
1514}
1515
1516firstExtrude = startSketchOn(XY)
1517 |> startProfile(at = [0,0])
1518 |> line(end = [0, l])
1519 |> line(end = [w, 0])
1520 |> line(end = thing(8))
1521 |> close()
1522 |> extrude(length = h)"#;
1523
1524 parse_execute(ast).await.unwrap();
1525 }
1526
1527 #[tokio::test(flavor = "multi_thread")]
1528 async fn test_execute_with_function_call_in_pipe() {
1529 let ast = r#"w = 20
1530l = 8
1531h = 10
1532
1533fn other_thing(@y) {
1534 return -y
1535}
1536
1537fn thing(@x) {
1538 return other_thing(x)
1539}
1540
1541firstExtrude = startSketchOn(XY)
1542 |> startProfile(at = [0,0])
1543 |> line(end = [0, l])
1544 |> line(end = [w, 0])
1545 |> line(end = [0, thing(8)])
1546 |> close()
1547 |> extrude(length = h)"#;
1548
1549 parse_execute(ast).await.unwrap();
1550 }
1551
1552 #[tokio::test(flavor = "multi_thread")]
1553 async fn test_execute_with_function_sketch() {
1554 let ast = r#"fn box(h, l, w) {
1555 myBox = startSketchOn(XY)
1556 |> startProfile(at = [0,0])
1557 |> line(end = [0, l])
1558 |> line(end = [w, 0])
1559 |> line(end = [0, -l])
1560 |> close()
1561 |> extrude(length = h)
1562
1563 return myBox
1564}
1565
1566fnBox = box(h = 3, l = 6, w = 10)"#;
1567
1568 parse_execute(ast).await.unwrap();
1569 }
1570
1571 #[tokio::test(flavor = "multi_thread")]
1572 async fn test_get_member_of_object_with_function_period() {
1573 let ast = r#"fn box(@obj) {
1574 myBox = startSketchOn(XY)
1575 |> startProfile(at = obj.start)
1576 |> line(end = [0, obj.l])
1577 |> line(end = [obj.w, 0])
1578 |> line(end = [0, -obj.l])
1579 |> close()
1580 |> extrude(length = obj.h)
1581
1582 return myBox
1583}
1584
1585thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1586"#;
1587 parse_execute(ast).await.unwrap();
1588 }
1589
1590 #[tokio::test(flavor = "multi_thread")]
1591 #[ignore] async fn test_object_member_starting_pipeline() {
1593 let ast = r#"
1594fn test2() {
1595 return {
1596 thing: startSketchOn(XY)
1597 |> startProfile(at = [0, 0])
1598 |> line(end = [0, 1])
1599 |> line(end = [1, 0])
1600 |> line(end = [0, -1])
1601 |> close()
1602 }
1603}
1604
1605x2 = test2()
1606
1607x2.thing
1608 |> extrude(length = 10)
1609"#;
1610 parse_execute(ast).await.unwrap();
1611 }
1612
1613 #[tokio::test(flavor = "multi_thread")]
1614 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
1616 let ast = r#"fn box(obj) {
1617let myBox = startSketchOn(XY)
1618 |> startProfile(at = obj.start)
1619 |> line(end = [0, obj.l])
1620 |> line(end = [obj.w, 0])
1621 |> line(end = [0, -obj.l])
1622 |> close()
1623 |> extrude(length = obj.h)
1624
1625 return myBox
1626}
1627
1628for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1629 thisBox = box(var)
1630}"#;
1631
1632 parse_execute(ast).await.unwrap();
1633 }
1634
1635 #[tokio::test(flavor = "multi_thread")]
1636 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
1638 let ast = r#"fn box(h, l, w, start) {
1639 myBox = startSketchOn(XY)
1640 |> startProfile(at = [0,0])
1641 |> line(end = [0, l])
1642 |> line(end = [w, 0])
1643 |> line(end = [0, -l])
1644 |> close()
1645 |> extrude(length = h)
1646
1647 return myBox
1648}
1649
1650
1651for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1652 const thisBox = box(var[0], var[1], var[2], var[3])
1653}"#;
1654
1655 parse_execute(ast).await.unwrap();
1656 }
1657
1658 #[tokio::test(flavor = "multi_thread")]
1659 async fn test_get_member_of_array_with_function() {
1660 let ast = r#"fn box(@arr) {
1661 myBox =startSketchOn(XY)
1662 |> startProfile(at = arr[0])
1663 |> line(end = [0, arr[1]])
1664 |> line(end = [arr[2], 0])
1665 |> line(end = [0, -arr[1]])
1666 |> close()
1667 |> extrude(length = arr[3])
1668
1669 return myBox
1670}
1671
1672thisBox = box([[0,0], 6, 10, 3])
1673
1674"#;
1675 parse_execute(ast).await.unwrap();
1676 }
1677
1678 #[tokio::test(flavor = "multi_thread")]
1679 async fn test_function_cannot_access_future_definitions() {
1680 let ast = r#"
1681fn returnX() {
1682 // x shouldn't be defined yet.
1683 return x
1684}
1685
1686x = 5
1687
1688answer = returnX()"#;
1689
1690 let result = parse_execute(ast).await;
1691 let err = result.unwrap_err();
1692 assert_eq!(err.message(), "`x` is not defined");
1693 }
1694
1695 #[tokio::test(flavor = "multi_thread")]
1696 async fn test_override_prelude() {
1697 let text = "PI = 3.0";
1698 let result = parse_execute(text).await.unwrap();
1699 let errs = result.exec_state.errors();
1700 assert!(errs.is_empty());
1701 }
1702
1703 #[tokio::test(flavor = "multi_thread")]
1704 async fn type_aliases() {
1705 let text = r#"type MyTy = [number; 2]
1706fn foo(@x: MyTy) {
1707 return x[0]
1708}
1709
1710foo([0, 1])
1711
1712type Other = MyTy | Helix
1713"#;
1714 let result = parse_execute(text).await.unwrap();
1715 let errs = result.exec_state.errors();
1716 assert!(errs.is_empty());
1717 }
1718
1719 #[tokio::test(flavor = "multi_thread")]
1720 async fn test_cannot_shebang_in_fn() {
1721 let ast = r#"
1722fn foo() {
1723 #!hello
1724 return true
1725}
1726
1727foo
1728"#;
1729
1730 let result = parse_execute(ast).await;
1731 let err = result.unwrap_err();
1732 assert_eq!(
1733 err,
1734 KclError::Syntax(KclErrorDetails {
1735 message: "Unexpected token: #".to_owned(),
1736 source_ranges: vec![SourceRange::new(14, 15, ModuleId::default())],
1737 }),
1738 );
1739 }
1740
1741 #[tokio::test(flavor = "multi_thread")]
1742 async fn test_pattern_transform_function_cannot_access_future_definitions() {
1743 let ast = r#"
1744fn transform(replicaId) {
1745 // x shouldn't be defined yet.
1746 scale = x
1747 return {
1748 translate = [0, 0, replicaId * 10],
1749 scale = [scale, 1, 0],
1750 }
1751}
1752
1753fn layer() {
1754 return startSketchOn(XY)
1755 |> circle( center= [0, 0], radius= 1, tag = $tag1)
1756 |> extrude(length = 10)
1757}
1758
1759x = 5
1760
1761// The 10 layers are replicas of each other, with a transform applied to each.
1762shape = layer() |> patternTransform(instances = 10, transform = transform)
1763"#;
1764
1765 let result = parse_execute(ast).await;
1766 let err = result.unwrap_err();
1767 assert_eq!(err.message(), "`x` is not defined",);
1768 }
1769
1770 #[tokio::test(flavor = "multi_thread")]
1773 async fn test_math_execute_with_functions() {
1774 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
1775 let result = parse_execute(ast).await.unwrap();
1776 assert_eq!(
1777 5.0,
1778 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1779 .as_f64()
1780 .unwrap()
1781 );
1782 }
1783
1784 #[tokio::test(flavor = "multi_thread")]
1785 async fn test_math_execute() {
1786 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
1787 let result = parse_execute(ast).await.unwrap();
1788 assert_eq!(
1789 7.4,
1790 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1791 .as_f64()
1792 .unwrap()
1793 );
1794 }
1795
1796 #[tokio::test(flavor = "multi_thread")]
1797 async fn test_math_execute_start_negative() {
1798 let ast = r#"myVar = -5 + 6"#;
1799 let result = parse_execute(ast).await.unwrap();
1800 assert_eq!(
1801 1.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_with_pi() {
1810 let ast = r#"myVar = PI * 2"#;
1811 let result = parse_execute(ast).await.unwrap();
1812 assert_eq!(
1813 std::f64::consts::TAU,
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_define_decimal_without_leading_zero() {
1822 let ast = r#"thing = .4 + 7"#;
1823 let result = parse_execute(ast).await.unwrap();
1824 assert_eq!(
1825 7.4,
1826 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
1827 .as_f64()
1828 .unwrap()
1829 );
1830 }
1831
1832 #[tokio::test(flavor = "multi_thread")]
1833 async fn test_zero_param_fn() {
1834 let ast = r#"sigmaAllow = 35000 // psi
1835leg1 = 5 // inches
1836leg2 = 8 // inches
1837fn thickness() { return 0.56 }
1838
1839bracket = startSketchOn(XY)
1840 |> startProfile(at = [0,0])
1841 |> line(end = [0, leg1])
1842 |> line(end = [leg2, 0])
1843 |> line(end = [0, -thickness()])
1844 |> line(end = [-leg2 + thickness(), 0])
1845"#;
1846 parse_execute(ast).await.unwrap();
1847 }
1848
1849 #[tokio::test(flavor = "multi_thread")]
1850 async fn test_unary_operator_not_succeeds() {
1851 let ast = r#"
1852fn returnTrue() { return !false }
1853t = true
1854f = false
1855notTrue = !t
1856notFalse = !f
1857c = !!true
1858d = !returnTrue()
1859
1860assertIs(!false, error = "expected to pass")
1861
1862fn check(x) {
1863 assertIs(!x, error = "expected argument to be false")
1864 return true
1865}
1866check(x = false)
1867"#;
1868 let result = parse_execute(ast).await.unwrap();
1869 assert_eq!(
1870 false,
1871 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
1872 .as_bool()
1873 .unwrap()
1874 );
1875 assert_eq!(
1876 true,
1877 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
1878 .as_bool()
1879 .unwrap()
1880 );
1881 assert_eq!(
1882 true,
1883 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
1884 .as_bool()
1885 .unwrap()
1886 );
1887 assert_eq!(
1888 false,
1889 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
1890 .as_bool()
1891 .unwrap()
1892 );
1893 }
1894
1895 #[tokio::test(flavor = "multi_thread")]
1896 async fn test_unary_operator_not_on_non_bool_fails() {
1897 let code1 = r#"
1898// Yup, this is null.
1899myNull = 0 / 0
1900notNull = !myNull
1901"#;
1902 assert_eq!(
1903 parse_execute(code1).await.unwrap_err().message(),
1904 "Cannot apply unary operator ! to non-boolean value: number",
1905 );
1906
1907 let code2 = "notZero = !0";
1908 assert_eq!(
1909 parse_execute(code2).await.unwrap_err().message(),
1910 "Cannot apply unary operator ! to non-boolean value: number",
1911 );
1912
1913 let code3 = r#"
1914notEmptyString = !""
1915"#;
1916 assert_eq!(
1917 parse_execute(code3).await.unwrap_err().message(),
1918 "Cannot apply unary operator ! to non-boolean value: string (text)",
1919 );
1920
1921 let code4 = r#"
1922obj = { a = 1 }
1923notMember = !obj.a
1924"#;
1925 assert_eq!(
1926 parse_execute(code4).await.unwrap_err().message(),
1927 "Cannot apply unary operator ! to non-boolean value: number",
1928 );
1929
1930 let code5 = "
1931a = []
1932notArray = !a";
1933 assert_eq!(
1934 parse_execute(code5).await.unwrap_err().message(),
1935 "Cannot apply unary operator ! to non-boolean value: array (list)",
1936 );
1937
1938 let code6 = "
1939x = {}
1940notObject = !x";
1941 assert_eq!(
1942 parse_execute(code6).await.unwrap_err().message(),
1943 "Cannot apply unary operator ! to non-boolean value: object",
1944 );
1945
1946 let code7 = "
1947fn x() { return 1 }
1948notFunction = !x";
1949 let fn_err = parse_execute(code7).await.unwrap_err();
1950 assert!(
1953 fn_err
1954 .message()
1955 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
1956 "Actual error: {:?}",
1957 fn_err
1958 );
1959
1960 let code8 = "
1961myTagDeclarator = $myTag
1962notTagDeclarator = !myTagDeclarator";
1963 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
1964 assert!(
1967 tag_declarator_err
1968 .message()
1969 .starts_with("Cannot apply unary operator ! to non-boolean value: TagDeclarator"),
1970 "Actual error: {:?}",
1971 tag_declarator_err
1972 );
1973
1974 let code9 = "
1975myTagDeclarator = $myTag
1976notTagIdentifier = !myTag";
1977 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
1978 assert!(
1981 tag_identifier_err
1982 .message()
1983 .starts_with("Cannot apply unary operator ! to non-boolean value: TagIdentifier"),
1984 "Actual error: {:?}",
1985 tag_identifier_err
1986 );
1987
1988 let code10 = "notPipe = !(1 |> 2)";
1989 assert_eq!(
1990 parse_execute(code10).await.unwrap_err(),
1993 KclError::Syntax(KclErrorDetails {
1994 message: "Unexpected token: !".to_owned(),
1995 source_ranges: vec![SourceRange::new(10, 11, ModuleId::default())],
1996 })
1997 );
1998
1999 let code11 = "
2000fn identity(x) { return x }
2001notPipeSub = 1 |> identity(!%))";
2002 assert_eq!(
2003 parse_execute(code11).await.unwrap_err(),
2006 KclError::Syntax(KclErrorDetails {
2007 message: "Unexpected token: |>".to_owned(),
2008 source_ranges: vec![SourceRange::new(44, 46, ModuleId::default())],
2009 })
2010 );
2011
2012 }
2016
2017 #[tokio::test(flavor = "multi_thread")]
2018 async fn test_math_negative_variable_in_binary_expression() {
2019 let ast = r#"sigmaAllow = 35000 // psi
2020width = 1 // inch
2021
2022p = 150 // lbs
2023distance = 6 // inches
2024FOS = 2
2025
2026leg1 = 5 // inches
2027leg2 = 8 // inches
2028
2029thickness_squared = distance * p * FOS * 6 / sigmaAllow
2030thickness = 0.56 // inches. App does not support square root function yet
2031
2032bracket = startSketchOn(XY)
2033 |> startProfile(at = [0,0])
2034 |> line(end = [0, leg1])
2035 |> line(end = [leg2, 0])
2036 |> line(end = [0, -thickness])
2037 |> line(end = [-leg2 + thickness, 0])
2038"#;
2039 parse_execute(ast).await.unwrap();
2040 }
2041
2042 #[tokio::test(flavor = "multi_thread")]
2043 async fn test_execute_function_no_return() {
2044 let ast = r#"fn test(@origin) {
2045 origin
2046}
2047
2048test([0, 0])
2049"#;
2050 let result = parse_execute(ast).await;
2051 assert!(result.is_err());
2052 assert!(result.unwrap_err().to_string().contains("undefined"),);
2053 }
2054
2055 #[tokio::test(flavor = "multi_thread")]
2056 async fn test_math_doubly_nested_parens() {
2057 let ast = r#"sigmaAllow = 35000 // psi
2058width = 4 // inch
2059p = 150 // Force on shelf - lbs
2060distance = 6 // inches
2061FOS = 2
2062leg1 = 5 // inches
2063leg2 = 8 // inches
2064thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2065thickness = 0.32 // inches. App does not support square root function yet
2066bracket = startSketchOn(XY)
2067 |> startProfile(at = [0,0])
2068 |> line(end = [0, leg1])
2069 |> line(end = [leg2, 0])
2070 |> line(end = [0, -thickness])
2071 |> line(end = [-1 * leg2 + thickness, 0])
2072 |> line(end = [0, -1 * leg1 + thickness])
2073 |> close()
2074 |> extrude(length = width)
2075"#;
2076 parse_execute(ast).await.unwrap();
2077 }
2078
2079 #[tokio::test(flavor = "multi_thread")]
2080 async fn test_math_nested_parens_one_less() {
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_fn_as_operand() {
2105 let ast = r#"fn f() { return 1 }
2106x = f()
2107y = x + 1
2108z = f() + 1
2109w = f() + f()
2110"#;
2111 parse_execute(ast).await.unwrap();
2112 }
2113
2114 #[tokio::test(flavor = "multi_thread")]
2115 async fn kcl_test_ids_stable_between_executions() {
2116 let code = r#"sketch001 = startSketchOn(XZ)
2117|> startProfile(at = [61.74, 206.13])
2118|> xLine(length = 305.11, tag = $seg01)
2119|> yLine(length = -291.85)
2120|> xLine(length = -segLen(seg01))
2121|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2122|> close()
2123|> extrude(length = 40.14)
2124|> shell(
2125 thickness = 3.14,
2126 faces = [seg01]
2127)
2128"#;
2129
2130 let ctx = crate::test_server::new_context(true, None).await.unwrap();
2131 let old_program = crate::Program::parse_no_errs(code).unwrap();
2132
2133 if let Err(err) = ctx.run_with_caching(old_program).await {
2135 let report = err.into_miette_report_with_outputs(code).unwrap();
2136 let report = miette::Report::new(report);
2137 panic!("Error executing program: {:?}", report);
2138 }
2139
2140 let id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
2142
2143 let code = r#"sketch001 = startSketchOn(XZ)
2144|> startProfile(at = [62.74, 206.13])
2145|> xLine(length = 305.11, tag = $seg01)
2146|> yLine(length = -291.85)
2147|> xLine(length = -segLen(seg01))
2148|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2149|> close()
2150|> extrude(length = 40.14)
2151|> shell(
2152 faces = [seg01],
2153 thickness = 3.14,
2154)
2155"#;
2156
2157 let program = crate::Program::parse_no_errs(code).unwrap();
2159 ctx.run_with_caching(program).await.unwrap();
2161
2162 let new_id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
2163
2164 assert_eq!(id_generator, new_id_generator);
2165 }
2166
2167 #[tokio::test(flavor = "multi_thread")]
2168 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2169 let code = r#"sketch001 = startSketchOn(XZ)
2170|> startProfile(at = [61.74, 206.13])
2171|> xLine(length = 305.11, tag = $seg01)
2172|> yLine(length = -291.85)
2173|> xLine(length = -segLen(seg01))
2174|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2175|> close()
2176|> extrude(length = 40.14)
2177|> shell(
2178 thickness = 3.14,
2179 faces = [seg01]
2180)
2181"#;
2182
2183 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2184 let old_program = crate::Program::parse_no_errs(code).unwrap();
2185
2186 ctx.run_with_caching(old_program.clone()).await.unwrap();
2188
2189 let settings_state = cache::read_old_ast().await.unwrap().settings;
2190
2191 assert_eq!(settings_state, ctx.settings);
2193
2194 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2196
2197 ctx.run_with_caching(old_program.clone()).await.unwrap();
2199
2200 let settings_state = cache::read_old_ast().await.unwrap().settings;
2201
2202 assert_eq!(settings_state, ctx.settings);
2204
2205 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2207
2208 ctx.run_with_caching(old_program).await.unwrap();
2210
2211 let settings_state = cache::read_old_ast().await.unwrap().settings;
2212
2213 assert_eq!(settings_state, ctx.settings);
2215
2216 ctx.close().await;
2217 }
2218
2219 #[tokio::test(flavor = "multi_thread")]
2220 async fn mock_after_not_mock() {
2221 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2222 let program = crate::Program::parse_no_errs("x = 2").unwrap();
2223 let result = ctx.run_with_caching(program).await.unwrap();
2224 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2225
2226 let ctx2 = ExecutorContext::new_mock().await;
2227 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2228 let result = ctx2.run_mock(program2, true).await.unwrap();
2229 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2230
2231 ctx.close().await;
2232 ctx2.close().await;
2233 }
2234
2235 #[tokio::test(flavor = "multi_thread")]
2236 async fn read_tag_version() {
2237 let ast = r#"fn bar(@t) {
2238 return startSketchOn(XY)
2239 |> startProfile(at = [0,0])
2240 |> angledLine(
2241 angle = -60,
2242 length = segLen(t),
2243 )
2244 |> line(end = [0, 0])
2245 |> close()
2246}
2247
2248sketch = startSketchOn(XY)
2249 |> startProfile(at = [0,0])
2250 |> line(end = [0, 10])
2251 |> line(end = [10, 0], tag = $tag0)
2252 |> line(end = [0, 0])
2253
2254fn foo() {
2255 // tag0 tags an edge
2256 return bar(tag0)
2257}
2258
2259solid = sketch |> extrude(length = 10)
2260// tag0 tags a face
2261sketch2 = startSketchOn(solid, face = tag0)
2262 |> startProfile(at = [0,0])
2263 |> line(end = [0, 1])
2264 |> line(end = [1, 0])
2265 |> line(end = [0, 0])
2266
2267foo() |> extrude(length = 1)
2268"#;
2269 parse_execute(ast).await.unwrap();
2270 }
2271}