1#[cfg(feature = "artifact-graph")]
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7use anyhow::Result;
8#[cfg(feature = "artifact-graph")]
9pub use artifact::{
10 Artifact, ArtifactCommand, ArtifactGraph, CodeRef, SketchBlock, StartSketchOnFace, StartSketchOnPlane,
11};
12use cache::GlobalState;
13pub use cache::{bust_cache, clear_mem_cache};
14#[cfg(feature = "artifact-graph")]
15pub use cad_op::Group;
16pub use cad_op::Operation;
17pub use geometry::*;
18pub use id_generator::IdGenerator;
19pub(crate) use import::PreImportedGeometry;
20use indexmap::IndexMap;
21pub use kcl_value::{KclObjectFields, KclValue};
22use kcmc::{
23 ImageFormat, ModelingCmd, each_cmd as mcmd,
24 ok_response::{OkModelingCmdResponse, output::TakeSnapshot},
25 websocket::{ModelingSessionData, OkWebSocketResponseData},
26};
27use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
28pub use memory::EnvironmentRef;
29pub(crate) use modeling::ModelingCmdMeta;
30use serde::{Deserialize, Serialize};
31pub(crate) use sketch_solve::normalize_to_solver_unit;
32pub(crate) use state::ModuleArtifactState;
33pub use state::{ExecState, MetaSettings};
34use uuid::Uuid;
35
36use crate::{
37 CompilationError, ExecError, KclErrorWithOutputs, SourceRange,
38 engine::{EngineManager, GridScaleBehavior},
39 errors::{KclError, KclErrorDetails},
40 execution::{
41 cache::{CacheInformation, CacheResult},
42 import_graph::{Universe, UniverseMap},
43 typed_path::TypedPath,
44 },
45 front::{Object, ObjectId},
46 fs::FileManager,
47 modules::{ModuleExecutionOutcome, ModuleId, ModulePath, ModuleRepr},
48 parsing::ast::types::{Expr, ImportPath, NodeRef},
49};
50#[cfg(feature = "artifact-graph")]
51use crate::{collections::AhashIndexSet, front::Number};
52
53pub(crate) mod annotations;
54#[cfg(feature = "artifact-graph")]
55mod artifact;
56pub(crate) mod cache;
57mod cad_op;
58mod exec_ast;
59pub mod fn_call;
60#[cfg(test)]
61#[cfg(feature = "artifact-graph")]
62mod freedom_analysis_tests;
63mod geometry;
64mod id_generator;
65mod import;
66mod import_graph;
67pub(crate) mod kcl_value;
68mod memory;
69mod modeling;
70mod sketch_solve;
71mod state;
72pub mod typed_path;
73pub(crate) mod types;
74
75macro_rules! control_continue {
79 ($control_flow:expr) => {{
80 let cf = $control_flow;
81 if cf.is_some_return() {
82 return Ok(cf);
83 } else {
84 cf.into_value()
85 }
86 }};
87}
88pub(crate) use control_continue;
90
91#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
92pub enum ControlFlowKind {
93 #[default]
94 Continue,
95 Exit,
96}
97
98impl ControlFlowKind {
99 pub fn is_some_return(&self) -> bool {
101 match self {
102 ControlFlowKind::Continue => false,
103 ControlFlowKind::Exit => true,
104 }
105 }
106}
107
108#[must_use = "You should always handle the control flow value when it is returned"]
109#[derive(Debug, Clone, PartialEq, Serialize)]
110pub struct KclValueControlFlow {
111 value: KclValue,
113 pub control: ControlFlowKind,
114}
115
116impl KclValue {
117 pub(crate) fn continue_(self) -> KclValueControlFlow {
118 KclValueControlFlow {
119 value: self,
120 control: ControlFlowKind::Continue,
121 }
122 }
123
124 pub(crate) fn exit(self) -> KclValueControlFlow {
125 KclValueControlFlow {
126 value: self,
127 control: ControlFlowKind::Exit,
128 }
129 }
130}
131
132impl KclValueControlFlow {
133 pub fn is_some_return(&self) -> bool {
135 self.control.is_some_return()
136 }
137
138 pub(crate) fn into_value(self) -> KclValue {
139 self.value
140 }
141}
142
143pub(crate) enum StatementKind<'a> {
144 Declaration { name: &'a str },
145 Expression,
146}
147
148#[derive(Debug, Clone, Copy)]
149pub enum PreserveMem {
150 Normal,
151 Always,
152}
153
154impl PreserveMem {
155 fn normal(self) -> bool {
156 match self {
157 PreserveMem::Normal => true,
158 PreserveMem::Always => false,
159 }
160 }
161}
162
163#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
165#[ts(export)]
166#[serde(rename_all = "camelCase")]
167pub struct ExecOutcome {
168 pub variables: IndexMap<String, KclValue>,
170 #[cfg(feature = "artifact-graph")]
173 pub operations: Vec<Operation>,
174 #[cfg(feature = "artifact-graph")]
176 pub artifact_graph: ArtifactGraph,
177 #[cfg(feature = "artifact-graph")]
179 #[serde(skip)]
180 pub scene_objects: Vec<Object>,
181 #[cfg(feature = "artifact-graph")]
184 #[serde(skip)]
185 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
186 #[cfg(feature = "artifact-graph")]
187 #[serde(skip)]
188 pub var_solutions: Vec<(SourceRange, Number)>,
189 pub errors: Vec<CompilationError>,
191 pub filenames: IndexMap<ModuleId, ModulePath>,
193 pub default_planes: Option<DefaultPlanes>,
195}
196
197impl ExecOutcome {
198 pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
199 #[cfg(feature = "artifact-graph")]
200 {
201 debug_assert!(
202 id.0 < self.scene_objects.len(),
203 "Requested object ID {} but only have {} objects",
204 id.0,
205 self.scene_objects.len()
206 );
207 self.scene_objects.get(id.0)
208 }
209 #[cfg(not(feature = "artifact-graph"))]
210 {
211 let _ = id;
212 None
213 }
214 }
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct MockConfig {
220 pub use_prev_memory: bool,
221 pub sketch_block_id: Option<ObjectId>,
224 pub freedom_analysis: bool,
227 #[cfg(feature = "artifact-graph")]
229 pub segment_ids_edited: AhashIndexSet<ObjectId>,
230}
231
232impl Default for MockConfig {
233 fn default() -> Self {
234 Self {
235 use_prev_memory: true,
237 sketch_block_id: None,
238 freedom_analysis: true,
239 #[cfg(feature = "artifact-graph")]
240 segment_ids_edited: AhashIndexSet::default(),
241 }
242 }
243}
244
245impl MockConfig {
246 pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
248 Self {
249 sketch_block_id: Some(sketch_block_id),
250 ..Default::default()
251 }
252 }
253
254 #[must_use]
255 pub(crate) fn no_freedom_analysis(mut self) -> Self {
256 self.freedom_analysis = false;
257 self
258 }
259}
260
261#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
262#[ts(export)]
263#[serde(rename_all = "camelCase")]
264pub struct DefaultPlanes {
265 pub xy: uuid::Uuid,
266 pub xz: uuid::Uuid,
267 pub yz: uuid::Uuid,
268 pub neg_xy: uuid::Uuid,
269 pub neg_xz: uuid::Uuid,
270 pub neg_yz: uuid::Uuid,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
274#[ts(export)]
275#[serde(tag = "type", rename_all = "camelCase")]
276pub struct TagIdentifier {
277 pub value: String,
278 #[serde(skip)]
281 pub info: Vec<(usize, TagEngineInfo)>,
282 #[serde(skip)]
283 pub meta: Vec<Metadata>,
284}
285
286impl TagIdentifier {
287 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
289 for (e, info) in self.info.iter().rev() {
290 if *e <= at_epoch {
291 return Some(info);
292 }
293 }
294
295 None
296 }
297
298 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
300 self.info.last().map(|i| &i.1)
301 }
302
303 pub fn merge_info(&mut self, other: &TagIdentifier) {
305 assert_eq!(&self.value, &other.value);
306 for (oe, ot) in &other.info {
307 if let Some((e, t)) = self.info.last_mut() {
308 if *e > *oe {
310 continue;
311 }
312 if e == oe {
314 *t = ot.clone();
315 continue;
316 }
317 }
318 self.info.push((*oe, ot.clone()));
319 }
320 }
321}
322
323impl Eq for TagIdentifier {}
324
325impl std::fmt::Display for TagIdentifier {
326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327 write!(f, "{}", self.value)
328 }
329}
330
331impl std::str::FromStr for TagIdentifier {
332 type Err = KclError;
333
334 fn from_str(s: &str) -> Result<Self, Self::Err> {
335 Ok(Self {
336 value: s.to_string(),
337 info: Vec::new(),
338 meta: Default::default(),
339 })
340 }
341}
342
343impl Ord for TagIdentifier {
344 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
345 self.value.cmp(&other.value)
346 }
347}
348
349impl PartialOrd for TagIdentifier {
350 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
351 Some(self.cmp(other))
352 }
353}
354
355impl std::hash::Hash for TagIdentifier {
356 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
357 self.value.hash(state);
358 }
359}
360
361#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
363#[ts(export)]
364#[serde(tag = "type", rename_all = "camelCase")]
365pub struct TagEngineInfo {
366 pub id: uuid::Uuid,
368 pub sketch: uuid::Uuid,
370 pub path: Option<Path>,
372 pub surface: Option<ExtrudeSurface>,
374}
375
376#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
377pub enum BodyType {
378 Root,
379 Block,
380}
381
382#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
384#[ts(export)]
385#[serde(rename_all = "camelCase")]
386pub struct Metadata {
387 pub source_range: SourceRange,
389}
390
391impl From<Metadata> for Vec<SourceRange> {
392 fn from(meta: Metadata) -> Self {
393 vec![meta.source_range]
394 }
395}
396
397impl From<SourceRange> for Metadata {
398 fn from(source_range: SourceRange) -> Self {
399 Self { source_range }
400 }
401}
402
403impl<T> From<NodeRef<'_, T>> for Metadata {
404 fn from(node: NodeRef<'_, T>) -> Self {
405 Self {
406 source_range: SourceRange::new(node.start, node.end, node.module_id),
407 }
408 }
409}
410
411impl From<&Expr> for Metadata {
412 fn from(expr: &Expr) -> Self {
413 Self {
414 source_range: SourceRange::from(expr),
415 }
416 }
417}
418
419impl Metadata {
420 pub fn to_source_ref(meta: &[Metadata]) -> crate::front::SourceRef {
421 if meta.len() == 1 {
422 let meta = &meta[0];
423 return crate::front::SourceRef::Simple {
424 range: meta.source_range,
425 };
426 }
427 crate::front::SourceRef::BackTrace {
428 ranges: meta.iter().map(|m| m.source_range).collect(),
429 }
430 }
431}
432
433#[derive(PartialEq, Debug, Default, Clone)]
435pub enum ContextType {
436 #[default]
438 Live,
439
440 Mock,
444
445 MockCustomForwarded,
447}
448
449#[derive(Debug, Clone)]
453pub struct ExecutorContext {
454 pub engine: Arc<Box<dyn EngineManager>>,
455 pub fs: Arc<FileManager>,
456 pub settings: ExecutorSettings,
457 pub context_type: ContextType,
458}
459
460#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
462#[ts(export)]
463pub struct ExecutorSettings {
464 pub highlight_edges: bool,
466 pub enable_ssao: bool,
468 pub show_grid: bool,
470 pub replay: Option<String>,
473 pub project_directory: Option<TypedPath>,
476 pub current_file: Option<TypedPath>,
479 pub fixed_size_grid: bool,
481}
482
483impl Default for ExecutorSettings {
484 fn default() -> Self {
485 Self {
486 highlight_edges: true,
487 enable_ssao: false,
488 show_grid: false,
489 replay: None,
490 project_directory: None,
491 current_file: None,
492 fixed_size_grid: true,
493 }
494 }
495}
496
497impl From<crate::settings::types::Configuration> for ExecutorSettings {
498 fn from(config: crate::settings::types::Configuration) -> Self {
499 Self::from(config.settings)
500 }
501}
502
503impl From<crate::settings::types::Settings> for ExecutorSettings {
504 fn from(settings: crate::settings::types::Settings) -> Self {
505 Self {
506 highlight_edges: settings.modeling.highlight_edges.into(),
507 enable_ssao: settings.modeling.enable_ssao.into(),
508 show_grid: settings.modeling.show_scale_grid,
509 replay: None,
510 project_directory: None,
511 current_file: None,
512 fixed_size_grid: settings.modeling.fixed_size_grid,
513 }
514 }
515}
516
517impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
518 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
519 Self::from(config.settings.modeling)
520 }
521}
522
523impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
524 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
525 Self {
526 highlight_edges: modeling.highlight_edges.into(),
527 enable_ssao: modeling.enable_ssao.into(),
528 show_grid: modeling.show_scale_grid,
529 replay: None,
530 project_directory: None,
531 current_file: None,
532 fixed_size_grid: true,
533 }
534 }
535}
536
537impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
538 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
539 Self {
540 highlight_edges: modeling.highlight_edges.into(),
541 enable_ssao: modeling.enable_ssao.into(),
542 show_grid: Default::default(),
543 replay: None,
544 project_directory: None,
545 current_file: None,
546 fixed_size_grid: true,
547 }
548 }
549}
550
551impl ExecutorSettings {
552 pub fn with_current_file(&mut self, current_file: TypedPath) {
554 if current_file.extension() == Some("kcl") {
556 self.current_file = Some(current_file.clone());
557 if let Some(parent) = current_file.parent() {
559 self.project_directory = Some(parent);
560 } else {
561 self.project_directory = Some(TypedPath::from(""));
562 }
563 } else {
564 self.project_directory = Some(current_file);
565 }
566 }
567}
568
569impl ExecutorContext {
570 #[cfg(not(target_arch = "wasm32"))]
572 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
573 let (ws, _headers) = client
574 .modeling()
575 .commands_ws(
576 None,
577 None,
578 None,
579 if settings.enable_ssao {
580 Some(kittycad::types::PostEffectType::Ssao)
581 } else {
582 None
583 },
584 settings.replay.clone(),
585 if settings.show_grid { Some(true) } else { None },
586 None,
587 None,
588 None,
589 Some(false),
590 )
591 .await?;
592
593 let engine: Arc<Box<dyn EngineManager>> =
594 Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
595
596 Ok(Self {
597 engine,
598 fs: Arc::new(FileManager::new()),
599 settings,
600 context_type: ContextType::Live,
601 })
602 }
603
604 #[cfg(target_arch = "wasm32")]
605 pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
606 ExecutorContext {
607 engine,
608 fs,
609 settings,
610 context_type: ContextType::Live,
611 }
612 }
613
614 #[cfg(not(target_arch = "wasm32"))]
615 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
616 ExecutorContext {
617 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
618 fs: Arc::new(FileManager::new()),
619 settings: settings.unwrap_or_default(),
620 context_type: ContextType::Mock,
621 }
622 }
623
624 #[cfg(target_arch = "wasm32")]
625 pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
626 ExecutorContext {
627 engine,
628 fs,
629 settings,
630 context_type: ContextType::Mock,
631 }
632 }
633
634 #[cfg(not(target_arch = "wasm32"))]
635 pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
636 ExecutorContext {
637 engine,
638 fs: Arc::new(FileManager::new()),
639 settings: Default::default(),
640 context_type: ContextType::MockCustomForwarded,
641 }
642 }
643
644 #[cfg(not(target_arch = "wasm32"))]
650 pub async fn new_with_client(
651 settings: ExecutorSettings,
652 token: Option<String>,
653 engine_addr: Option<String>,
654 ) -> Result<Self> {
655 let client = crate::engine::new_zoo_client(token, engine_addr)?;
657
658 let ctx = Self::new(&client, settings).await?;
659 Ok(ctx)
660 }
661
662 #[cfg(not(target_arch = "wasm32"))]
667 pub async fn new_with_default_client() -> Result<Self> {
668 let ctx = Self::new_with_client(Default::default(), None, None).await?;
670 Ok(ctx)
671 }
672
673 #[cfg(not(target_arch = "wasm32"))]
675 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
676 let ctx = ExecutorContext::new_with_client(
677 ExecutorSettings {
678 highlight_edges: true,
679 enable_ssao: false,
680 show_grid: false,
681 replay: None,
682 project_directory: None,
683 current_file: None,
684 fixed_size_grid: false,
685 },
686 None,
687 engine_addr,
688 )
689 .await?;
690 Ok(ctx)
691 }
692
693 pub fn is_mock(&self) -> bool {
694 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
695 }
696
697 pub async fn no_engine_commands(&self) -> bool {
699 self.is_mock()
700 }
701
702 pub async fn send_clear_scene(
703 &self,
704 exec_state: &mut ExecState,
705 source_range: crate::execution::SourceRange,
706 ) -> Result<(), KclError> {
707 exec_state.mod_local.artifacts.clear();
710 exec_state.global.root_module_artifacts.clear();
711 exec_state.global.artifacts.clear();
712
713 self.engine
714 .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
715 .await
716 }
717
718 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
719 cache::bust_cache().await;
720
721 let outcome = self.run_with_caching(crate::Program::empty()).await?;
726
727 Ok(outcome)
728 }
729
730 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
731 self.eval_prelude(exec_state, SourceRange::synthetic())
732 .await
733 .map_err(KclErrorWithOutputs::no_outputs)?;
734 exec_state.mut_stack().push_new_root_env(true);
735 Ok(())
736 }
737
738 pub async fn run_mock(
739 &self,
740 program: &crate::Program,
741 mock_config: &MockConfig,
742 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
743 assert!(
744 self.is_mock(),
745 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
746 );
747
748 let use_prev_memory = mock_config.use_prev_memory;
749 let mut exec_state = ExecState::new_mock(self, mock_config);
750 if use_prev_memory {
751 match cache::read_old_memory().await {
752 Some(mem) => {
753 *exec_state.mut_stack() = mem.stack;
754 exec_state.global.module_infos = mem.module_infos;
755 #[cfg(feature = "artifact-graph")]
756 {
757 let len = mock_config
758 .sketch_block_id
759 .map(|sketch_block_id| sketch_block_id.0)
760 .unwrap_or(0);
761 if let Some(scene_objects) = mem.scene_objects.get(0..len) {
762 exec_state.global.root_module_artifacts.scene_objects = scene_objects.to_vec();
763 } else {
764 let message = format!(
765 "Cached scene objects length {} is less than expected length from cached object ID generator {}",
766 mem.scene_objects.len(),
767 len
768 );
769 debug_assert!(false, "{message}");
770 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
771 KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
772 )));
773 }
774 }
775 }
776 None => self.prepare_mem(&mut exec_state).await?,
777 }
778 } else {
779 self.prepare_mem(&mut exec_state).await?
780 };
781
782 exec_state.mut_stack().push_new_env_for_scope();
785
786 let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
787
788 let mut stack = exec_state.stack().clone();
793 let module_infos = exec_state.global.module_infos.clone();
794 #[cfg(feature = "artifact-graph")]
795 let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
796 #[cfg(not(feature = "artifact-graph"))]
797 let scene_objects = Default::default();
798 let outcome = exec_state.into_exec_outcome(result.0, self).await;
799
800 stack.squash_env(result.0);
801 let state = cache::SketchModeState {
802 stack,
803 module_infos,
804 scene_objects,
805 };
806 cache::write_old_memory(state).await;
807
808 Ok(outcome)
809 }
810
811 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
812 assert!(!self.is_mock());
813 let grid_scale = if self.settings.fixed_size_grid {
814 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
815 } else {
816 GridScaleBehavior::ScaleWithZoom
817 };
818
819 let original_program = program.clone();
820
821 let (_program, exec_state, result) = match cache::read_old_ast().await {
822 Some(mut cached_state) => {
823 let old = CacheInformation {
824 ast: &cached_state.main.ast,
825 settings: &cached_state.settings,
826 };
827 let new = CacheInformation {
828 ast: &program.ast,
829 settings: &self.settings,
830 };
831
832 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
834 CacheResult::ReExecute {
835 clear_scene,
836 reapply_settings,
837 program: changed_program,
838 } => {
839 if reapply_settings
840 && self
841 .engine
842 .reapply_settings(
843 &self.settings,
844 Default::default(),
845 &mut cached_state.main.exec_state.id_generator,
846 grid_scale,
847 )
848 .await
849 .is_err()
850 {
851 (true, program, None)
852 } else {
853 (
854 clear_scene,
855 crate::Program {
856 ast: changed_program,
857 original_file_contents: program.original_file_contents,
858 },
859 None,
860 )
861 }
862 }
863 CacheResult::CheckImportsOnly {
864 reapply_settings,
865 ast: changed_program,
866 } => {
867 let mut reapply_failed = false;
868 if reapply_settings {
869 if self
870 .engine
871 .reapply_settings(
872 &self.settings,
873 Default::default(),
874 &mut cached_state.main.exec_state.id_generator,
875 grid_scale,
876 )
877 .await
878 .is_ok()
879 {
880 cache::write_old_ast(GlobalState::with_settings(
881 cached_state.clone(),
882 self.settings.clone(),
883 ))
884 .await;
885 } else {
886 reapply_failed = true;
887 }
888 }
889
890 if reapply_failed {
891 (true, program, None)
892 } else {
893 let mut new_exec_state = ExecState::new(self);
895 let (new_universe, new_universe_map) =
896 self.get_universe(&program, &mut new_exec_state).await?;
897
898 let clear_scene = new_universe.values().any(|value| {
899 let id = value.1;
900 match (
901 cached_state.exec_state.get_source(id),
902 new_exec_state.global.get_source(id),
903 ) {
904 (Some(s0), Some(s1)) => s0.source != s1.source,
905 _ => false,
906 }
907 });
908
909 if !clear_scene {
910 return Ok(cached_state.into_exec_outcome(self).await);
912 }
913
914 (
915 true,
916 crate::Program {
917 ast: changed_program,
918 original_file_contents: program.original_file_contents,
919 },
920 Some((new_universe, new_universe_map, new_exec_state)),
921 )
922 }
923 }
924 CacheResult::NoAction(true) => {
925 if self
926 .engine
927 .reapply_settings(
928 &self.settings,
929 Default::default(),
930 &mut cached_state.main.exec_state.id_generator,
931 grid_scale,
932 )
933 .await
934 .is_ok()
935 {
936 cache::write_old_ast(GlobalState::with_settings(
938 cached_state.clone(),
939 self.settings.clone(),
940 ))
941 .await;
942
943 return Ok(cached_state.into_exec_outcome(self).await);
944 }
945 (true, program, None)
946 }
947 CacheResult::NoAction(false) => {
948 return Ok(cached_state.into_exec_outcome(self).await);
949 }
950 };
951
952 let (exec_state, result) = match import_check_info {
953 Some((new_universe, new_universe_map, mut new_exec_state)) => {
954 self.send_clear_scene(&mut new_exec_state, Default::default())
956 .await
957 .map_err(KclErrorWithOutputs::no_outputs)?;
958
959 let result = self
960 .run_concurrent(
961 &program,
962 &mut new_exec_state,
963 Some((new_universe, new_universe_map)),
964 PreserveMem::Normal,
965 )
966 .await;
967
968 (new_exec_state, result)
969 }
970 None if clear_scene => {
971 let mut exec_state = cached_state.reconstitute_exec_state();
973 exec_state.reset(self);
974
975 self.send_clear_scene(&mut exec_state, Default::default())
976 .await
977 .map_err(KclErrorWithOutputs::no_outputs)?;
978
979 let result = self
980 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
981 .await;
982
983 (exec_state, result)
984 }
985 None => {
986 let mut exec_state = cached_state.reconstitute_exec_state();
987 exec_state.mut_stack().restore_env(cached_state.main.result_env);
988
989 let result = self
990 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
991 .await;
992
993 (exec_state, result)
994 }
995 };
996
997 (program, exec_state, result)
998 }
999 None => {
1000 let mut exec_state = ExecState::new(self);
1001 self.send_clear_scene(&mut exec_state, Default::default())
1002 .await
1003 .map_err(KclErrorWithOutputs::no_outputs)?;
1004
1005 let result = self
1006 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1007 .await;
1008
1009 (program, exec_state, result)
1010 }
1011 };
1012
1013 if result.is_err() {
1014 cache::bust_cache().await;
1015 }
1016
1017 let result = result?;
1019
1020 cache::write_old_ast(GlobalState::new(
1024 exec_state.clone(),
1025 self.settings.clone(),
1026 original_program.ast,
1027 result.0,
1028 ))
1029 .await;
1030
1031 let outcome = exec_state.into_exec_outcome(result.0, self).await;
1032 Ok(outcome)
1033 }
1034
1035 pub async fn run(
1039 &self,
1040 program: &crate::Program,
1041 exec_state: &mut ExecState,
1042 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1043 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1044 .await
1045 }
1046
1047 pub async fn run_concurrent(
1052 &self,
1053 program: &crate::Program,
1054 exec_state: &mut ExecState,
1055 universe_info: Option<(Universe, UniverseMap)>,
1056 preserve_mem: PreserveMem,
1057 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1058 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1061 (universe, universe_map)
1062 } else {
1063 self.get_universe(program, exec_state).await?
1064 };
1065
1066 let default_planes = self.engine.get_default_planes().read().await.clone();
1067
1068 self.eval_prelude(exec_state, SourceRange::synthetic())
1070 .await
1071 .map_err(KclErrorWithOutputs::no_outputs)?;
1072
1073 for modules in import_graph::import_graph(&universe, self)
1074 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1075 .into_iter()
1076 {
1077 #[cfg(not(target_arch = "wasm32"))]
1078 let mut set = tokio::task::JoinSet::new();
1079
1080 #[allow(clippy::type_complexity)]
1081 let (results_tx, mut results_rx): (
1082 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1083 tokio::sync::mpsc::Receiver<_>,
1084 ) = tokio::sync::mpsc::channel(1);
1085
1086 for module in modules {
1087 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1088 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1089 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1090 )));
1091 };
1092 let module_id = *module_id;
1093 let module_path = module_path.clone();
1094 let source_range = SourceRange::from(import_stmt);
1095 let module_exec_state = exec_state.clone();
1097
1098 self.add_import_module_ops(
1099 exec_state,
1100 &program.ast,
1101 module_id,
1102 &module_path,
1103 source_range,
1104 &universe_map,
1105 );
1106
1107 let repr = repr.clone();
1108 let exec_ctxt = self.clone();
1109 let results_tx = results_tx.clone();
1110
1111 let exec_module = async |exec_ctxt: &ExecutorContext,
1112 repr: &ModuleRepr,
1113 module_id: ModuleId,
1114 module_path: &ModulePath,
1115 exec_state: &mut ExecState,
1116 source_range: SourceRange|
1117 -> Result<ModuleRepr, KclError> {
1118 match repr {
1119 ModuleRepr::Kcl(program, _) => {
1120 let result = exec_ctxt
1121 .exec_module_from_ast(
1122 program,
1123 module_id,
1124 module_path,
1125 exec_state,
1126 source_range,
1127 PreserveMem::Normal,
1128 )
1129 .await;
1130
1131 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1132 }
1133 ModuleRepr::Foreign(geom, _) => {
1134 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1135 .await
1136 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1137
1138 result.map(|val| {
1139 ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
1140 })
1141 }
1142 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1143 format!("Module {module_path} not found in universe"),
1144 vec![source_range],
1145 ))),
1146 }
1147 };
1148
1149 #[cfg(target_arch = "wasm32")]
1150 {
1151 wasm_bindgen_futures::spawn_local(async move {
1152 let mut exec_state = module_exec_state;
1153 let exec_ctxt = exec_ctxt;
1154
1155 let result = exec_module(
1156 &exec_ctxt,
1157 &repr,
1158 module_id,
1159 &module_path,
1160 &mut exec_state,
1161 source_range,
1162 )
1163 .await;
1164
1165 results_tx
1166 .send((module_id, module_path, result))
1167 .await
1168 .unwrap_or_default();
1169 });
1170 }
1171 #[cfg(not(target_arch = "wasm32"))]
1172 {
1173 set.spawn(async move {
1174 let mut exec_state = module_exec_state;
1175 let exec_ctxt = exec_ctxt;
1176
1177 let result = exec_module(
1178 &exec_ctxt,
1179 &repr,
1180 module_id,
1181 &module_path,
1182 &mut exec_state,
1183 source_range,
1184 )
1185 .await;
1186
1187 results_tx
1188 .send((module_id, module_path, result))
1189 .await
1190 .unwrap_or_default();
1191 });
1192 }
1193 }
1194
1195 drop(results_tx);
1196
1197 while let Some((module_id, _, result)) = results_rx.recv().await {
1198 match result {
1199 Ok(new_repr) => {
1200 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1201
1202 match &mut repr {
1203 ModuleRepr::Kcl(_, cache) => {
1204 let ModuleRepr::Kcl(_, session_data) = new_repr else {
1205 unreachable!();
1206 };
1207 *cache = session_data;
1208 }
1209 ModuleRepr::Foreign(_, cache) => {
1210 let ModuleRepr::Foreign(_, session_data) = new_repr else {
1211 unreachable!();
1212 };
1213 *cache = session_data;
1214 }
1215 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1216 }
1217
1218 exec_state.global.module_infos[&module_id].restore_repr(repr);
1219 }
1220 Err(e) => {
1221 return Err(exec_state.error_with_outputs(e, None, default_planes));
1222 }
1223 }
1224 }
1225 }
1226
1227 exec_state
1231 .global
1232 .root_module_artifacts
1233 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1234
1235 self.inner_run(program, exec_state, preserve_mem).await
1236 }
1237
1238 async fn get_universe(
1241 &self,
1242 program: &crate::Program,
1243 exec_state: &mut ExecState,
1244 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1245 exec_state.add_root_module_contents(program);
1246
1247 let mut universe = std::collections::HashMap::new();
1248
1249 let default_planes = self.engine.get_default_planes().read().await.clone();
1250
1251 let root_imports = import_graph::import_universe(
1252 self,
1253 &ModulePath::Main,
1254 &ModuleRepr::Kcl(program.ast.clone(), None),
1255 &mut universe,
1256 exec_state,
1257 )
1258 .await
1259 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1260
1261 Ok((universe, root_imports))
1262 }
1263
1264 #[cfg(not(feature = "artifact-graph"))]
1265 fn add_import_module_ops(
1266 &self,
1267 _exec_state: &mut ExecState,
1268 _program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1269 _module_id: ModuleId,
1270 _module_path: &ModulePath,
1271 _source_range: SourceRange,
1272 _universe_map: &UniverseMap,
1273 ) {
1274 }
1275
1276 #[cfg(feature = "artifact-graph")]
1277 fn add_import_module_ops(
1278 &self,
1279 exec_state: &mut ExecState,
1280 program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1281 module_id: ModuleId,
1282 module_path: &ModulePath,
1283 source_range: SourceRange,
1284 universe_map: &UniverseMap,
1285 ) {
1286 match module_path {
1287 ModulePath::Main => {
1288 }
1290 ModulePath::Local {
1291 value,
1292 original_import_path,
1293 } => {
1294 if universe_map.contains_key(value) {
1297 use crate::NodePath;
1298
1299 let node_path = if source_range.is_top_level_module() {
1300 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1301 NodePath::from_range(
1302 &exec_state.build_program_lookup(program.clone()),
1303 cached_body_items,
1304 source_range,
1305 )
1306 .unwrap_or_default()
1307 } else {
1308 NodePath::placeholder()
1311 };
1312
1313 let name = match original_import_path {
1314 Some(value) => value.to_string_lossy(),
1315 None => value.file_name().unwrap_or_default(),
1316 };
1317 exec_state.push_op(Operation::GroupBegin {
1318 group: Group::ModuleInstance { name, module_id },
1319 node_path,
1320 source_range,
1321 });
1322 exec_state.push_op(Operation::GroupEnd);
1326 }
1327 }
1328 ModulePath::Std { .. } => {
1329 }
1331 }
1332 }
1333
1334 async fn inner_run(
1337 &self,
1338 program: &crate::Program,
1339 exec_state: &mut ExecState,
1340 preserve_mem: PreserveMem,
1341 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1342 let _stats = crate::log::LogPerfStats::new("Interpretation");
1343
1344 let grid_scale = if self.settings.fixed_size_grid {
1346 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1347 } else {
1348 GridScaleBehavior::ScaleWithZoom
1349 };
1350 self.engine
1351 .reapply_settings(
1352 &self.settings,
1353 Default::default(),
1354 exec_state.id_generator(),
1355 grid_scale,
1356 )
1357 .await
1358 .map_err(KclErrorWithOutputs::no_outputs)?;
1359
1360 let default_planes = self.engine.get_default_planes().read().await.clone();
1361 let result = self
1362 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1363 .await;
1364
1365 crate::log::log(format!(
1366 "Post interpretation KCL memory stats: {:#?}",
1367 exec_state.stack().memory.stats
1368 ));
1369 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1370
1371 let env_ref = result.map_err(|(err, env_ref)| exec_state.error_with_outputs(err, env_ref, default_planes))?;
1372
1373 if !self.is_mock() {
1374 let mut stack = exec_state.stack().deep_clone();
1375 stack.restore_env(env_ref);
1376 let state = cache::SketchModeState {
1377 stack,
1378 module_infos: exec_state.global.module_infos.clone(),
1379 #[cfg(feature = "artifact-graph")]
1380 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1381 #[cfg(not(feature = "artifact-graph"))]
1382 scene_objects: Default::default(),
1383 };
1384 cache::write_old_memory(state).await;
1385 }
1386 let session_data = self.engine.get_session_data().await;
1387
1388 Ok((env_ref, session_data))
1389 }
1390
1391 async fn execute_and_build_graph(
1394 &self,
1395 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1396 exec_state: &mut ExecState,
1397 preserve_mem: PreserveMem,
1398 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1399 #[cfg(feature = "artifact-graph")]
1405 let start_op = exec_state.global.root_module_artifacts.operations.len();
1406
1407 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1408 .await
1409 .map_err(|e| (e, None))?;
1410
1411 let exec_result = self
1412 .exec_module_body(
1413 program,
1414 exec_state,
1415 preserve_mem,
1416 ModuleId::default(),
1417 &ModulePath::Main,
1418 )
1419 .await
1420 .map(
1421 |ModuleExecutionOutcome {
1422 environment: env_ref,
1423 artifacts: module_artifacts,
1424 ..
1425 }| {
1426 exec_state.global.root_module_artifacts.extend(module_artifacts);
1429 env_ref
1430 },
1431 )
1432 .map_err(|(err, env_ref, module_artifacts)| {
1433 if let Some(module_artifacts) = module_artifacts {
1434 exec_state.global.root_module_artifacts.extend(module_artifacts);
1437 }
1438 (err, env_ref)
1439 });
1440
1441 #[cfg(feature = "artifact-graph")]
1442 {
1443 let programs = &exec_state.build_program_lookup(program.clone());
1445 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1446 for op in exec_state
1447 .global
1448 .root_module_artifacts
1449 .operations
1450 .iter_mut()
1451 .skip(start_op)
1452 {
1453 op.fill_node_paths(programs, cached_body_items);
1454 }
1455 for module in exec_state.global.module_infos.values_mut() {
1456 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1457 for op in &mut outcome.artifacts.operations {
1458 op.fill_node_paths(programs, cached_body_items);
1459 }
1460 }
1461 }
1462 }
1463
1464 self.engine.ensure_async_commands_completed().await.map_err(|e| {
1466 match &exec_result {
1467 Ok(env_ref) => (e, Some(*env_ref)),
1468 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1470 }
1471 })?;
1472
1473 self.engine.clear_queues().await;
1476
1477 match exec_state.build_artifact_graph(&self.engine, program).await {
1478 Ok(_) => exec_result,
1479 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1480 }
1481 }
1482
1483 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1487 if exec_state.stack().memory.requires_std() {
1488 #[cfg(feature = "artifact-graph")]
1489 let initial_ops = exec_state.mod_local.artifacts.operations.len();
1490
1491 let path = vec!["std".to_owned(), "prelude".to_owned()];
1492 let resolved_path = ModulePath::from_std_import_path(&path)?;
1493 let id = self
1494 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1495 .await?;
1496 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1497
1498 exec_state.mut_stack().memory.set_std(module_memory);
1499
1500 #[cfg(feature = "artifact-graph")]
1506 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1507 }
1508
1509 Ok(())
1510 }
1511
1512 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1514 self.engine
1516 .send_modeling_cmd(
1517 uuid::Uuid::new_v4(),
1518 crate::execution::SourceRange::default(),
1519 &ModelingCmd::from(
1520 mcmd::ZoomToFit::builder()
1521 .object_ids(Default::default())
1522 .animated(false)
1523 .padding(0.1)
1524 .build(),
1525 ),
1526 )
1527 .await
1528 .map_err(KclErrorWithOutputs::no_outputs)?;
1529
1530 let resp = self
1532 .engine
1533 .send_modeling_cmd(
1534 uuid::Uuid::new_v4(),
1535 crate::execution::SourceRange::default(),
1536 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
1537 )
1538 .await
1539 .map_err(KclErrorWithOutputs::no_outputs)?;
1540
1541 let OkWebSocketResponseData::Modeling {
1542 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1543 } = resp
1544 else {
1545 return Err(ExecError::BadPng(format!(
1546 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1547 )));
1548 };
1549 Ok(contents)
1550 }
1551
1552 pub async fn export(
1554 &self,
1555 format: kittycad_modeling_cmds::format::OutputFormat3d,
1556 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1557 let resp = self
1558 .engine
1559 .send_modeling_cmd(
1560 uuid::Uuid::new_v4(),
1561 crate::SourceRange::default(),
1562 &kittycad_modeling_cmds::ModelingCmd::Export(
1563 kittycad_modeling_cmds::Export::builder()
1564 .entity_ids(vec![])
1565 .format(format)
1566 .build(),
1567 ),
1568 )
1569 .await?;
1570
1571 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1572 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1573 format!("Expected Export response, got {resp:?}",),
1574 vec![SourceRange::default()],
1575 )));
1576 };
1577
1578 Ok(files)
1579 }
1580
1581 pub async fn export_step(
1583 &self,
1584 deterministic_time: bool,
1585 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1586 let files = self
1587 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1588 kittycad_modeling_cmds::format::step::export::Options {
1589 coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1590 created: if deterministic_time {
1591 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1592 KclError::new_internal(crate::errors::KclErrorDetails::new(
1593 format!("Failed to parse date: {e}"),
1594 vec![SourceRange::default()],
1595 ))
1596 })?)
1597 } else {
1598 None
1599 },
1600 },
1601 ))
1602 .await?;
1603
1604 Ok(files)
1605 }
1606
1607 pub async fn close(&self) {
1608 self.engine.close().await;
1609 }
1610}
1611
1612#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1613pub struct ArtifactId(Uuid);
1614
1615impl ArtifactId {
1616 pub fn new(uuid: Uuid) -> Self {
1617 Self(uuid)
1618 }
1619
1620 pub fn placeholder() -> Self {
1622 Self(Uuid::nil())
1623 }
1624
1625 pub fn constraint() -> Self {
1628 Self(Uuid::nil())
1629 }
1630}
1631
1632impl From<Uuid> for ArtifactId {
1633 fn from(uuid: Uuid) -> Self {
1634 Self::new(uuid)
1635 }
1636}
1637
1638impl From<&Uuid> for ArtifactId {
1639 fn from(uuid: &Uuid) -> Self {
1640 Self::new(*uuid)
1641 }
1642}
1643
1644impl From<ArtifactId> for Uuid {
1645 fn from(id: ArtifactId) -> Self {
1646 id.0
1647 }
1648}
1649
1650impl From<&ArtifactId> for Uuid {
1651 fn from(id: &ArtifactId) -> Self {
1652 id.0
1653 }
1654}
1655
1656impl From<ModelingCmdId> for ArtifactId {
1657 fn from(id: ModelingCmdId) -> Self {
1658 Self::new(*id.as_ref())
1659 }
1660}
1661
1662impl From<&ModelingCmdId> for ArtifactId {
1663 fn from(id: &ModelingCmdId) -> Self {
1664 Self::new(*id.as_ref())
1665 }
1666}
1667
1668#[cfg(test)]
1669pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1670 parse_execute_with_project_dir(code, None).await
1671}
1672
1673#[cfg(test)]
1674pub(crate) async fn parse_execute_with_project_dir(
1675 code: &str,
1676 project_directory: Option<TypedPath>,
1677) -> Result<ExecTestResults, KclError> {
1678 let program = crate::Program::parse_no_errs(code)?;
1679
1680 let exec_ctxt = ExecutorContext {
1681 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
1682 |err| {
1683 KclError::new_internal(crate::errors::KclErrorDetails::new(
1684 format!("Failed to create mock engine connection: {err}"),
1685 vec![SourceRange::default()],
1686 ))
1687 },
1688 )?)),
1689 fs: Arc::new(crate::fs::FileManager::new()),
1690 settings: ExecutorSettings {
1691 project_directory,
1692 ..Default::default()
1693 },
1694 context_type: ContextType::Mock,
1695 };
1696 let mut exec_state = ExecState::new(&exec_ctxt);
1697 let result = exec_ctxt.run(&program, &mut exec_state).await?;
1698
1699 Ok(ExecTestResults {
1700 program,
1701 mem_env: result.0,
1702 exec_ctxt,
1703 exec_state,
1704 })
1705}
1706
1707#[cfg(test)]
1708#[derive(Debug)]
1709pub(crate) struct ExecTestResults {
1710 program: crate::Program,
1711 mem_env: EnvironmentRef,
1712 exec_ctxt: ExecutorContext,
1713 exec_state: ExecState,
1714}
1715
1716#[cfg(feature = "artifact-graph")]
1720pub struct ProgramLookup {
1721 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
1722}
1723
1724#[cfg(feature = "artifact-graph")]
1725impl ProgramLookup {
1726 pub fn new(
1729 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1730 module_infos: state::ModuleInfoMap,
1731 ) -> Self {
1732 let mut programs = IndexMap::with_capacity(module_infos.len());
1733 for (id, info) in module_infos {
1734 if let ModuleRepr::Kcl(program, _) = info.repr {
1735 programs.insert(id, program);
1736 }
1737 }
1738 programs.insert(ModuleId::default(), current);
1739 Self { programs }
1740 }
1741
1742 pub fn program_for_module(
1743 &self,
1744 module_id: ModuleId,
1745 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
1746 self.programs.get(&module_id)
1747 }
1748}
1749
1750#[cfg(test)]
1751mod tests {
1752 use pretty_assertions::assert_eq;
1753
1754 use super::*;
1755 use crate::{
1756 ModuleId,
1757 errors::{KclErrorDetails, Severity},
1758 exec::NumericType,
1759 execution::{memory::Stack, types::RuntimeType},
1760 };
1761
1762 #[track_caller]
1764 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1765 memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1766 }
1767
1768 #[tokio::test(flavor = "multi_thread")]
1769 async fn test_execute_warn() {
1770 let text = "@blah";
1771 let result = parse_execute(text).await.unwrap();
1772 let errs = result.exec_state.errors();
1773 assert_eq!(errs.len(), 1);
1774 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1775 assert!(
1776 errs[0].message.contains("Unknown annotation"),
1777 "unexpected warning message: {}",
1778 errs[0].message
1779 );
1780 }
1781
1782 #[tokio::test(flavor = "multi_thread")]
1783 async fn test_execute_fn_definitions() {
1784 let ast = r#"fn def(@x) {
1785 return x
1786}
1787fn ghi(@x) {
1788 return x
1789}
1790fn jkl(@x) {
1791 return x
1792}
1793fn hmm(@x) {
1794 return x
1795}
1796
1797yo = 5 + 6
1798
1799abc = 3
1800identifierGuy = 5
1801part001 = startSketchOn(XY)
1802|> startProfile(at = [-1.2, 4.83])
1803|> line(end = [2.8, 0])
1804|> angledLine(angle = 100 + 100, length = 3.01)
1805|> angledLine(angle = abc, length = 3.02)
1806|> angledLine(angle = def(yo), length = 3.03)
1807|> angledLine(angle = ghi(2), length = 3.04)
1808|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1809|> close()
1810yo2 = hmm([identifierGuy + 5])"#;
1811
1812 parse_execute(ast).await.unwrap();
1813 }
1814
1815 #[tokio::test(flavor = "multi_thread")]
1816 async fn test_execute_with_pipe_substitutions_unary() {
1817 let ast = r#"myVar = 3
1818part001 = startSketchOn(XY)
1819 |> startProfile(at = [0, 0])
1820 |> line(end = [3, 4], tag = $seg01)
1821 |> line(end = [
1822 min([segLen(seg01), myVar]),
1823 -legLen(hypotenuse = segLen(seg01), leg = myVar)
1824])
1825"#;
1826
1827 parse_execute(ast).await.unwrap();
1828 }
1829
1830 #[tokio::test(flavor = "multi_thread")]
1831 async fn test_execute_with_pipe_substitutions() {
1832 let ast = r#"myVar = 3
1833part001 = startSketchOn(XY)
1834 |> startProfile(at = [0, 0])
1835 |> line(end = [3, 4], tag = $seg01)
1836 |> line(end = [
1837 min([segLen(seg01), myVar]),
1838 legLen(hypotenuse = segLen(seg01), leg = myVar)
1839])
1840"#;
1841
1842 parse_execute(ast).await.unwrap();
1843 }
1844
1845 #[tokio::test(flavor = "multi_thread")]
1846 async fn test_execute_with_inline_comment() {
1847 let ast = r#"baseThick = 1
1848armAngle = 60
1849
1850baseThickHalf = baseThick / 2
1851halfArmAngle = armAngle / 2
1852
1853arrExpShouldNotBeIncluded = [1, 2, 3]
1854objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1855
1856part001 = startSketchOn(XY)
1857 |> startProfile(at = [0, 0])
1858 |> yLine(endAbsolute = 1)
1859 |> xLine(length = 3.84) // selection-range-7ish-before-this
1860
1861variableBelowShouldNotBeIncluded = 3
1862"#;
1863
1864 parse_execute(ast).await.unwrap();
1865 }
1866
1867 #[tokio::test(flavor = "multi_thread")]
1868 async fn test_execute_with_function_literal_in_pipe() {
1869 let ast = r#"w = 20
1870l = 8
1871h = 10
1872
1873fn thing() {
1874 return -8
1875}
1876
1877firstExtrude = startSketchOn(XY)
1878 |> startProfile(at = [0,0])
1879 |> line(end = [0, l])
1880 |> line(end = [w, 0])
1881 |> line(end = [0, thing()])
1882 |> close()
1883 |> extrude(length = h)"#;
1884
1885 parse_execute(ast).await.unwrap();
1886 }
1887
1888 #[tokio::test(flavor = "multi_thread")]
1889 async fn test_execute_with_function_unary_in_pipe() {
1890 let ast = r#"w = 20
1891l = 8
1892h = 10
1893
1894fn thing(@x) {
1895 return -x
1896}
1897
1898firstExtrude = startSketchOn(XY)
1899 |> startProfile(at = [0,0])
1900 |> line(end = [0, l])
1901 |> line(end = [w, 0])
1902 |> line(end = [0, thing(8)])
1903 |> close()
1904 |> extrude(length = h)"#;
1905
1906 parse_execute(ast).await.unwrap();
1907 }
1908
1909 #[tokio::test(flavor = "multi_thread")]
1910 async fn test_execute_with_function_array_in_pipe() {
1911 let ast = r#"w = 20
1912l = 8
1913h = 10
1914
1915fn thing(@x) {
1916 return [0, -x]
1917}
1918
1919firstExtrude = startSketchOn(XY)
1920 |> startProfile(at = [0,0])
1921 |> line(end = [0, l])
1922 |> line(end = [w, 0])
1923 |> line(end = thing(8))
1924 |> close()
1925 |> extrude(length = h)"#;
1926
1927 parse_execute(ast).await.unwrap();
1928 }
1929
1930 #[tokio::test(flavor = "multi_thread")]
1931 async fn test_execute_with_function_call_in_pipe() {
1932 let ast = r#"w = 20
1933l = 8
1934h = 10
1935
1936fn other_thing(@y) {
1937 return -y
1938}
1939
1940fn thing(@x) {
1941 return other_thing(x)
1942}
1943
1944firstExtrude = startSketchOn(XY)
1945 |> startProfile(at = [0,0])
1946 |> line(end = [0, l])
1947 |> line(end = [w, 0])
1948 |> line(end = [0, thing(8)])
1949 |> close()
1950 |> extrude(length = h)"#;
1951
1952 parse_execute(ast).await.unwrap();
1953 }
1954
1955 #[tokio::test(flavor = "multi_thread")]
1956 async fn test_execute_with_function_sketch() {
1957 let ast = r#"fn box(h, l, w) {
1958 myBox = startSketchOn(XY)
1959 |> startProfile(at = [0,0])
1960 |> line(end = [0, l])
1961 |> line(end = [w, 0])
1962 |> line(end = [0, -l])
1963 |> close()
1964 |> extrude(length = h)
1965
1966 return myBox
1967}
1968
1969fnBox = box(h = 3, l = 6, w = 10)"#;
1970
1971 parse_execute(ast).await.unwrap();
1972 }
1973
1974 #[tokio::test(flavor = "multi_thread")]
1975 async fn test_get_member_of_object_with_function_period() {
1976 let ast = r#"fn box(@obj) {
1977 myBox = startSketchOn(XY)
1978 |> startProfile(at = obj.start)
1979 |> line(end = [0, obj.l])
1980 |> line(end = [obj.w, 0])
1981 |> line(end = [0, -obj.l])
1982 |> close()
1983 |> extrude(length = obj.h)
1984
1985 return myBox
1986}
1987
1988thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1989"#;
1990 parse_execute(ast).await.unwrap();
1991 }
1992
1993 #[tokio::test(flavor = "multi_thread")]
1994 #[ignore] async fn test_object_member_starting_pipeline() {
1996 let ast = r#"
1997fn test2() {
1998 return {
1999 thing: startSketchOn(XY)
2000 |> startProfile(at = [0, 0])
2001 |> line(end = [0, 1])
2002 |> line(end = [1, 0])
2003 |> line(end = [0, -1])
2004 |> close()
2005 }
2006}
2007
2008x2 = test2()
2009
2010x2.thing
2011 |> extrude(length = 10)
2012"#;
2013 parse_execute(ast).await.unwrap();
2014 }
2015
2016 #[tokio::test(flavor = "multi_thread")]
2017 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
2019 let ast = r#"fn box(obj) {
2020let myBox = startSketchOn(XY)
2021 |> startProfile(at = obj.start)
2022 |> line(end = [0, obj.l])
2023 |> line(end = [obj.w, 0])
2024 |> line(end = [0, -obj.l])
2025 |> close()
2026 |> extrude(length = obj.h)
2027
2028 return myBox
2029}
2030
2031for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2032 thisBox = box(var)
2033}"#;
2034
2035 parse_execute(ast).await.unwrap();
2036 }
2037
2038 #[tokio::test(flavor = "multi_thread")]
2039 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
2041 let ast = r#"fn box(h, l, w, start) {
2042 myBox = startSketchOn(XY)
2043 |> startProfile(at = [0,0])
2044 |> line(end = [0, l])
2045 |> line(end = [w, 0])
2046 |> line(end = [0, -l])
2047 |> close()
2048 |> extrude(length = h)
2049
2050 return myBox
2051}
2052
2053
2054for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2055 const thisBox = box(var[0], var[1], var[2], var[3])
2056}"#;
2057
2058 parse_execute(ast).await.unwrap();
2059 }
2060
2061 #[tokio::test(flavor = "multi_thread")]
2062 async fn test_get_member_of_array_with_function() {
2063 let ast = r#"fn box(@arr) {
2064 myBox =startSketchOn(XY)
2065 |> startProfile(at = arr[0])
2066 |> line(end = [0, arr[1]])
2067 |> line(end = [arr[2], 0])
2068 |> line(end = [0, -arr[1]])
2069 |> close()
2070 |> extrude(length = arr[3])
2071
2072 return myBox
2073}
2074
2075thisBox = box([[0,0], 6, 10, 3])
2076
2077"#;
2078 parse_execute(ast).await.unwrap();
2079 }
2080
2081 #[tokio::test(flavor = "multi_thread")]
2082 async fn test_function_cannot_access_future_definitions() {
2083 let ast = r#"
2084fn returnX() {
2085 // x shouldn't be defined yet.
2086 return x
2087}
2088
2089x = 5
2090
2091answer = returnX()"#;
2092
2093 let result = parse_execute(ast).await;
2094 let err = result.unwrap_err();
2095 assert_eq!(err.message(), "`x` is not defined");
2096 }
2097
2098 #[tokio::test(flavor = "multi_thread")]
2099 async fn test_override_prelude() {
2100 let text = "PI = 3.0";
2101 let result = parse_execute(text).await.unwrap();
2102 let errs = result.exec_state.errors();
2103 assert!(errs.is_empty());
2104 }
2105
2106 #[tokio::test(flavor = "multi_thread")]
2107 async fn type_aliases() {
2108 let text = r#"@settings(experimentalFeatures = allow)
2109type MyTy = [number; 2]
2110fn foo(@x: MyTy) {
2111 return x[0]
2112}
2113
2114foo([0, 1])
2115
2116type Other = MyTy | Helix
2117"#;
2118 let result = parse_execute(text).await.unwrap();
2119 let errs = result.exec_state.errors();
2120 assert!(errs.is_empty());
2121 }
2122
2123 #[tokio::test(flavor = "multi_thread")]
2124 async fn test_cannot_shebang_in_fn() {
2125 let ast = r#"
2126fn foo() {
2127 #!hello
2128 return true
2129}
2130
2131foo
2132"#;
2133
2134 let result = parse_execute(ast).await;
2135 let err = result.unwrap_err();
2136 assert_eq!(
2137 err,
2138 KclError::new_syntax(KclErrorDetails::new(
2139 "Unexpected token: #".to_owned(),
2140 vec![SourceRange::new(14, 15, ModuleId::default())],
2141 )),
2142 );
2143 }
2144
2145 #[tokio::test(flavor = "multi_thread")]
2146 async fn test_pattern_transform_function_cannot_access_future_definitions() {
2147 let ast = r#"
2148fn transform(@replicaId) {
2149 // x shouldn't be defined yet.
2150 scale = x
2151 return {
2152 translate = [0, 0, replicaId * 10],
2153 scale = [scale, 1, 0],
2154 }
2155}
2156
2157fn layer() {
2158 return startSketchOn(XY)
2159 |> circle( center= [0, 0], radius= 1, tag = $tag1)
2160 |> extrude(length = 10)
2161}
2162
2163x = 5
2164
2165// The 10 layers are replicas of each other, with a transform applied to each.
2166shape = layer() |> patternTransform(instances = 10, transform = transform)
2167"#;
2168
2169 let result = parse_execute(ast).await;
2170 let err = result.unwrap_err();
2171 assert_eq!(err.message(), "`x` is not defined",);
2172 }
2173
2174 #[tokio::test(flavor = "multi_thread")]
2177 async fn test_math_execute_with_functions() {
2178 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
2179 let result = parse_execute(ast).await.unwrap();
2180 assert_eq!(
2181 5.0,
2182 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2183 .as_f64()
2184 .unwrap()
2185 );
2186 }
2187
2188 #[tokio::test(flavor = "multi_thread")]
2189 async fn test_math_execute() {
2190 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
2191 let result = parse_execute(ast).await.unwrap();
2192 assert_eq!(
2193 7.4,
2194 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2195 .as_f64()
2196 .unwrap()
2197 );
2198 }
2199
2200 #[tokio::test(flavor = "multi_thread")]
2201 async fn test_math_execute_start_negative() {
2202 let ast = r#"myVar = -5 + 6"#;
2203 let result = parse_execute(ast).await.unwrap();
2204 assert_eq!(
2205 1.0,
2206 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2207 .as_f64()
2208 .unwrap()
2209 );
2210 }
2211
2212 #[tokio::test(flavor = "multi_thread")]
2213 async fn test_math_execute_with_pi() {
2214 let ast = r#"myVar = PI * 2"#;
2215 let result = parse_execute(ast).await.unwrap();
2216 assert_eq!(
2217 std::f64::consts::TAU,
2218 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2219 .as_f64()
2220 .unwrap()
2221 );
2222 }
2223
2224 #[tokio::test(flavor = "multi_thread")]
2225 async fn test_math_define_decimal_without_leading_zero() {
2226 let ast = r#"thing = .4 + 7"#;
2227 let result = parse_execute(ast).await.unwrap();
2228 assert_eq!(
2229 7.4,
2230 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
2231 .as_f64()
2232 .unwrap()
2233 );
2234 }
2235
2236 #[tokio::test(flavor = "multi_thread")]
2237 async fn pass_std_to_std() {
2238 let ast = r#"sketch001 = startSketchOn(XY)
2239profile001 = circle(sketch001, center = [0, 0], radius = 2)
2240extrude001 = extrude(profile001, length = 5)
2241extrudes = patternLinear3d(
2242 extrude001,
2243 instances = 3,
2244 distance = 5,
2245 axis = [1, 1, 0],
2246)
2247clone001 = map(extrudes, f = clone)
2248"#;
2249 parse_execute(ast).await.unwrap();
2250 }
2251
2252 #[tokio::test(flavor = "multi_thread")]
2253 async fn test_array_reduce_nested_array() {
2254 let code = r#"
2255fn id(@el, accum) { return accum }
2256
2257answer = reduce([], initial=[[[0,0]]], f=id)
2258"#;
2259 let result = parse_execute(code).await.unwrap();
2260 assert_eq!(
2261 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2262 KclValue::HomArray {
2263 value: vec![KclValue::HomArray {
2264 value: vec![KclValue::HomArray {
2265 value: vec![
2266 KclValue::Number {
2267 value: 0.0,
2268 ty: NumericType::default(),
2269 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2270 },
2271 KclValue::Number {
2272 value: 0.0,
2273 ty: NumericType::default(),
2274 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2275 }
2276 ],
2277 ty: RuntimeType::any(),
2278 }],
2279 ty: RuntimeType::any(),
2280 }],
2281 ty: RuntimeType::any(),
2282 }
2283 );
2284 }
2285
2286 #[tokio::test(flavor = "multi_thread")]
2287 async fn test_zero_param_fn() {
2288 let ast = r#"sigmaAllow = 35000 // psi
2289leg1 = 5 // inches
2290leg2 = 8 // inches
2291fn thickness() { return 0.56 }
2292
2293bracket = startSketchOn(XY)
2294 |> startProfile(at = [0,0])
2295 |> line(end = [0, leg1])
2296 |> line(end = [leg2, 0])
2297 |> line(end = [0, -thickness()])
2298 |> line(end = [-leg2 + thickness(), 0])
2299"#;
2300 parse_execute(ast).await.unwrap();
2301 }
2302
2303 #[tokio::test(flavor = "multi_thread")]
2304 async fn test_unary_operator_not_succeeds() {
2305 let ast = r#"
2306fn returnTrue() { return !false }
2307t = true
2308f = false
2309notTrue = !t
2310notFalse = !f
2311c = !!true
2312d = !returnTrue()
2313
2314assertIs(!false, error = "expected to pass")
2315
2316fn check(x) {
2317 assertIs(!x, error = "expected argument to be false")
2318 return true
2319}
2320check(x = false)
2321"#;
2322 let result = parse_execute(ast).await.unwrap();
2323 assert_eq!(
2324 false,
2325 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2326 .as_bool()
2327 .unwrap()
2328 );
2329 assert_eq!(
2330 true,
2331 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2332 .as_bool()
2333 .unwrap()
2334 );
2335 assert_eq!(
2336 true,
2337 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2338 .as_bool()
2339 .unwrap()
2340 );
2341 assert_eq!(
2342 false,
2343 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2344 .as_bool()
2345 .unwrap()
2346 );
2347 }
2348
2349 #[tokio::test(flavor = "multi_thread")]
2350 async fn test_unary_operator_not_on_non_bool_fails() {
2351 let code1 = r#"
2352// Yup, this is null.
2353myNull = 0 / 0
2354notNull = !myNull
2355"#;
2356 assert_eq!(
2357 parse_execute(code1).await.unwrap_err().message(),
2358 "Cannot apply unary operator ! to non-boolean value: a number",
2359 );
2360
2361 let code2 = "notZero = !0";
2362 assert_eq!(
2363 parse_execute(code2).await.unwrap_err().message(),
2364 "Cannot apply unary operator ! to non-boolean value: a number",
2365 );
2366
2367 let code3 = r#"
2368notEmptyString = !""
2369"#;
2370 assert_eq!(
2371 parse_execute(code3).await.unwrap_err().message(),
2372 "Cannot apply unary operator ! to non-boolean value: a string",
2373 );
2374
2375 let code4 = r#"
2376obj = { a = 1 }
2377notMember = !obj.a
2378"#;
2379 assert_eq!(
2380 parse_execute(code4).await.unwrap_err().message(),
2381 "Cannot apply unary operator ! to non-boolean value: a number",
2382 );
2383
2384 let code5 = "
2385a = []
2386notArray = !a";
2387 assert_eq!(
2388 parse_execute(code5).await.unwrap_err().message(),
2389 "Cannot apply unary operator ! to non-boolean value: an empty array",
2390 );
2391
2392 let code6 = "
2393x = {}
2394notObject = !x";
2395 assert_eq!(
2396 parse_execute(code6).await.unwrap_err().message(),
2397 "Cannot apply unary operator ! to non-boolean value: an object",
2398 );
2399
2400 let code7 = "
2401fn x() { return 1 }
2402notFunction = !x";
2403 let fn_err = parse_execute(code7).await.unwrap_err();
2404 assert!(
2407 fn_err
2408 .message()
2409 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2410 "Actual error: {fn_err:?}"
2411 );
2412
2413 let code8 = "
2414myTagDeclarator = $myTag
2415notTagDeclarator = !myTagDeclarator";
2416 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2417 assert!(
2420 tag_declarator_err
2421 .message()
2422 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2423 "Actual error: {tag_declarator_err:?}"
2424 );
2425
2426 let code9 = "
2427myTagDeclarator = $myTag
2428notTagIdentifier = !myTag";
2429 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2430 assert!(
2433 tag_identifier_err
2434 .message()
2435 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2436 "Actual error: {tag_identifier_err:?}"
2437 );
2438
2439 let code10 = "notPipe = !(1 |> 2)";
2440 assert_eq!(
2441 parse_execute(code10).await.unwrap_err(),
2444 KclError::new_syntax(KclErrorDetails::new(
2445 "Unexpected token: !".to_owned(),
2446 vec![SourceRange::new(10, 11, ModuleId::default())],
2447 ))
2448 );
2449
2450 let code11 = "
2451fn identity(x) { return x }
2452notPipeSub = 1 |> identity(!%))";
2453 assert_eq!(
2454 parse_execute(code11).await.unwrap_err(),
2457 KclError::new_syntax(KclErrorDetails::new(
2458 "There was an unexpected `!`. Try removing it.".to_owned(),
2459 vec![SourceRange::new(56, 57, ModuleId::default())],
2460 ))
2461 );
2462
2463 }
2467
2468 #[tokio::test(flavor = "multi_thread")]
2469 async fn test_start_sketch_on_invalid_kwargs() {
2470 let current_dir = std::env::current_dir().unwrap();
2471 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2472 let mut code = std::fs::read_to_string(&path).unwrap();
2473 assert_eq!(
2474 parse_execute(&code).await.unwrap_err().message(),
2475 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2476 );
2477
2478 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2479 code = std::fs::read_to_string(&path).unwrap();
2480
2481 assert_eq!(
2482 parse_execute(&code).await.unwrap_err().message(),
2483 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2484 );
2485
2486 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2487 code = std::fs::read_to_string(&path).unwrap();
2488
2489 assert_eq!(
2490 parse_execute(&code).await.unwrap_err().message(),
2491 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2492 );
2493
2494 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2495 code = std::fs::read_to_string(&path).unwrap();
2496
2497 assert_eq!(
2498 parse_execute(&code).await.unwrap_err().message(),
2499 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2500 );
2501
2502 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2503 code = std::fs::read_to_string(&path).unwrap();
2504
2505 assert_eq!(
2506 parse_execute(&code).await.unwrap_err().message(),
2507 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2508 );
2509 }
2510
2511 #[tokio::test(flavor = "multi_thread")]
2512 async fn test_math_negative_variable_in_binary_expression() {
2513 let ast = r#"sigmaAllow = 35000 // psi
2514width = 1 // inch
2515
2516p = 150 // lbs
2517distance = 6 // inches
2518FOS = 2
2519
2520leg1 = 5 // inches
2521leg2 = 8 // inches
2522
2523thickness_squared = distance * p * FOS * 6 / sigmaAllow
2524thickness = 0.56 // inches. App does not support square root function yet
2525
2526bracket = startSketchOn(XY)
2527 |> startProfile(at = [0,0])
2528 |> line(end = [0, leg1])
2529 |> line(end = [leg2, 0])
2530 |> line(end = [0, -thickness])
2531 |> line(end = [-leg2 + thickness, 0])
2532"#;
2533 parse_execute(ast).await.unwrap();
2534 }
2535
2536 #[tokio::test(flavor = "multi_thread")]
2537 async fn test_execute_function_no_return() {
2538 let ast = r#"fn test(@origin) {
2539 origin
2540}
2541
2542test([0, 0])
2543"#;
2544 let result = parse_execute(ast).await;
2545 assert!(result.is_err());
2546 assert!(result.unwrap_err().to_string().contains("undefined"));
2547 }
2548
2549 #[tokio::test(flavor = "multi_thread")]
2550 async fn test_max_stack_size_exceeded_error() {
2551 let ast = r#"
2552fn forever(@n) {
2553 return 1 + forever(n)
2554}
2555
2556forever(1)
2557"#;
2558 let result = parse_execute(ast).await;
2559 let err = result.unwrap_err();
2560 assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
2561 }
2562
2563 #[tokio::test(flavor = "multi_thread")]
2564 async fn test_math_doubly_nested_parens() {
2565 let ast = r#"sigmaAllow = 35000 // psi
2566width = 4 // inch
2567p = 150 // Force on shelf - lbs
2568distance = 6 // inches
2569FOS = 2
2570leg1 = 5 // inches
2571leg2 = 8 // inches
2572thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2573thickness = 0.32 // inches. App does not support square root function yet
2574bracket = startSketchOn(XY)
2575 |> startProfile(at = [0,0])
2576 |> line(end = [0, leg1])
2577 |> line(end = [leg2, 0])
2578 |> line(end = [0, -thickness])
2579 |> line(end = [-1 * leg2 + thickness, 0])
2580 |> line(end = [0, -1 * leg1 + thickness])
2581 |> close()
2582 |> extrude(length = width)
2583"#;
2584 parse_execute(ast).await.unwrap();
2585 }
2586
2587 #[tokio::test(flavor = "multi_thread")]
2588 async fn test_math_nested_parens_one_less() {
2589 let ast = r#" sigmaAllow = 35000 // psi
2590width = 4 // inch
2591p = 150 // Force on shelf - lbs
2592distance = 6 // inches
2593FOS = 2
2594leg1 = 5 // inches
2595leg2 = 8 // inches
2596thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2597thickness = 0.32 // inches. App does not support square root function yet
2598bracket = startSketchOn(XY)
2599 |> startProfile(at = [0,0])
2600 |> line(end = [0, leg1])
2601 |> line(end = [leg2, 0])
2602 |> line(end = [0, -thickness])
2603 |> line(end = [-1 * leg2 + thickness, 0])
2604 |> line(end = [0, -1 * leg1 + thickness])
2605 |> close()
2606 |> extrude(length = width)
2607"#;
2608 parse_execute(ast).await.unwrap();
2609 }
2610
2611 #[tokio::test(flavor = "multi_thread")]
2612 async fn test_fn_as_operand() {
2613 let ast = r#"fn f() { return 1 }
2614x = f()
2615y = x + 1
2616z = f() + 1
2617w = f() + f()
2618"#;
2619 parse_execute(ast).await.unwrap();
2620 }
2621
2622 #[tokio::test(flavor = "multi_thread")]
2623 async fn kcl_test_ids_stable_between_executions() {
2624 let code = r#"sketch001 = startSketchOn(XZ)
2625|> startProfile(at = [61.74, 206.13])
2626|> xLine(length = 305.11, tag = $seg01)
2627|> yLine(length = -291.85)
2628|> xLine(length = -segLen(seg01))
2629|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2630|> close()
2631|> extrude(length = 40.14)
2632|> shell(
2633 thickness = 3.14,
2634 faces = [seg01]
2635)
2636"#;
2637
2638 let ctx = crate::test_server::new_context(true, None).await.unwrap();
2639 let old_program = crate::Program::parse_no_errs(code).unwrap();
2640
2641 if let Err(err) = ctx.run_with_caching(old_program).await {
2643 let report = err.into_miette_report_with_outputs(code).unwrap();
2644 let report = miette::Report::new(report);
2645 panic!("Error executing program: {report:?}");
2646 }
2647
2648 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2650
2651 let code = r#"sketch001 = startSketchOn(XZ)
2652|> startProfile(at = [62.74, 206.13])
2653|> xLine(length = 305.11, tag = $seg01)
2654|> yLine(length = -291.85)
2655|> xLine(length = -segLen(seg01))
2656|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2657|> close()
2658|> extrude(length = 40.14)
2659|> shell(
2660 faces = [seg01],
2661 thickness = 3.14,
2662)
2663"#;
2664
2665 let program = crate::Program::parse_no_errs(code).unwrap();
2667 ctx.run_with_caching(program).await.unwrap();
2669
2670 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2671
2672 assert_eq!(id_generator, new_id_generator);
2673 }
2674
2675 #[tokio::test(flavor = "multi_thread")]
2676 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2677 let code = r#"sketch001 = startSketchOn(XZ)
2678|> startProfile(at = [61.74, 206.13])
2679|> xLine(length = 305.11, tag = $seg01)
2680|> yLine(length = -291.85)
2681|> xLine(length = -segLen(seg01))
2682|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2683|> close()
2684|> extrude(length = 40.14)
2685|> shell(
2686 thickness = 3.14,
2687 faces = [seg01]
2688)
2689"#;
2690
2691 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2692 let old_program = crate::Program::parse_no_errs(code).unwrap();
2693
2694 ctx.run_with_caching(old_program.clone()).await.unwrap();
2696
2697 let settings_state = cache::read_old_ast().await.unwrap().settings;
2698
2699 assert_eq!(settings_state, ctx.settings);
2701
2702 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2704
2705 ctx.run_with_caching(old_program.clone()).await.unwrap();
2707
2708 let settings_state = cache::read_old_ast().await.unwrap().settings;
2709
2710 assert_eq!(settings_state, ctx.settings);
2712
2713 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2715
2716 ctx.run_with_caching(old_program).await.unwrap();
2718
2719 let settings_state = cache::read_old_ast().await.unwrap().settings;
2720
2721 assert_eq!(settings_state, ctx.settings);
2723
2724 ctx.close().await;
2725 }
2726
2727 #[tokio::test(flavor = "multi_thread")]
2728 async fn mock_after_not_mock() {
2729 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2730 let program = crate::Program::parse_no_errs("x = 2").unwrap();
2731 let result = ctx.run_with_caching(program).await.unwrap();
2732 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2733
2734 let ctx2 = ExecutorContext::new_mock(None).await;
2735 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2736 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
2737 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2738
2739 ctx.close().await;
2740 ctx2.close().await;
2741 }
2742
2743 #[cfg(feature = "artifact-graph")]
2744 #[tokio::test(flavor = "multi_thread")]
2745 async fn mock_has_stable_ids() {
2746 let ctx = ExecutorContext::new_mock(None).await;
2747 let mock_config = MockConfig {
2748 use_prev_memory: false,
2749 ..Default::default()
2750 };
2751 let code = "sk = startSketchOn(XY)
2752 |> startProfile(at = [0, 0])";
2753 let program = crate::Program::parse_no_errs(code).unwrap();
2754 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
2755 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2756 assert!(!ids.is_empty(), "IDs should not be empty");
2757
2758 let ctx2 = ExecutorContext::new_mock(None).await;
2759 let program2 = crate::Program::parse_no_errs(code).unwrap();
2760 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
2761 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2762
2763 assert_eq!(ids, ids2, "Generated IDs should match");
2764 ctx.close().await;
2765 ctx2.close().await;
2766 }
2767
2768 #[cfg(feature = "artifact-graph")]
2769 #[tokio::test(flavor = "multi_thread")]
2770 async fn sim_sketch_mode_real_mock_real() {
2771 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2772 let code = r#"sketch001 = startSketchOn(XY)
2773profile001 = startProfile(sketch001, at = [0, 0])
2774 |> line(end = [10, 0])
2775 |> line(end = [0, 10])
2776 |> line(end = [-10, 0])
2777 |> line(end = [0, -10])
2778 |> close()
2779"#;
2780 let program = crate::Program::parse_no_errs(code).unwrap();
2781 let result = ctx.run_with_caching(program).await.unwrap();
2782 assert_eq!(result.operations.len(), 1);
2783
2784 let mock_ctx = ExecutorContext::new_mock(None).await;
2785 let mock_program = crate::Program::parse_no_errs(code).unwrap();
2786 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
2787 assert_eq!(mock_result.operations.len(), 1);
2788
2789 let code2 = code.to_owned()
2790 + r#"
2791extrude001 = extrude(profile001, length = 10)
2792"#;
2793 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
2794 let result = ctx.run_with_caching(program2).await.unwrap();
2795 assert_eq!(result.operations.len(), 2);
2796
2797 ctx.close().await;
2798 mock_ctx.close().await;
2799 }
2800
2801 #[tokio::test(flavor = "multi_thread")]
2802 async fn read_tag_version() {
2803 let ast = r#"fn bar(@t) {
2804 return startSketchOn(XY)
2805 |> startProfile(at = [0,0])
2806 |> angledLine(
2807 angle = -60,
2808 length = segLen(t),
2809 )
2810 |> line(end = [0, 0])
2811 |> close()
2812}
2813
2814sketch = startSketchOn(XY)
2815 |> startProfile(at = [0,0])
2816 |> line(end = [0, 10])
2817 |> line(end = [10, 0], tag = $tag0)
2818 |> line(endAbsolute = [0, 0])
2819
2820fn foo() {
2821 // tag0 tags an edge
2822 return bar(tag0)
2823}
2824
2825solid = sketch |> extrude(length = 10)
2826// tag0 tags a face
2827sketch2 = startSketchOn(solid, face = tag0)
2828 |> startProfile(at = [0,0])
2829 |> line(end = [0, 1])
2830 |> line(end = [1, 0])
2831 |> line(end = [0, 0])
2832
2833foo() |> extrude(length = 1)
2834"#;
2835 parse_execute(ast).await.unwrap();
2836 }
2837
2838 #[tokio::test(flavor = "multi_thread")]
2839 async fn experimental() {
2840 let code = r#"
2841startSketchOn(XY)
2842 |> startProfile(at = [0, 0], tag = $start)
2843 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2844"#;
2845 let result = parse_execute(code).await.unwrap();
2846 let errors = result.exec_state.errors();
2847 assert_eq!(errors.len(), 1);
2848 assert_eq!(errors[0].severity, Severity::Error);
2849 let msg = &errors[0].message;
2850 assert!(msg.contains("experimental"), "found {msg}");
2851
2852 let code = r#"@settings(experimentalFeatures = allow)
2853startSketchOn(XY)
2854 |> startProfile(at = [0, 0], tag = $start)
2855 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2856"#;
2857 let result = parse_execute(code).await.unwrap();
2858 let errors = result.exec_state.errors();
2859 assert!(errors.is_empty());
2860
2861 let code = r#"@settings(experimentalFeatures = warn)
2862startSketchOn(XY)
2863 |> startProfile(at = [0, 0], tag = $start)
2864 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2865"#;
2866 let result = parse_execute(code).await.unwrap();
2867 let errors = result.exec_state.errors();
2868 assert_eq!(errors.len(), 1);
2869 assert_eq!(errors[0].severity, Severity::Warning);
2870 let msg = &errors[0].message;
2871 assert!(msg.contains("experimental"), "found {msg}");
2872
2873 let code = r#"@settings(experimentalFeatures = deny)
2874startSketchOn(XY)
2875 |> startProfile(at = [0, 0], tag = $start)
2876 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2877"#;
2878 let result = parse_execute(code).await.unwrap();
2879 let errors = result.exec_state.errors();
2880 assert_eq!(errors.len(), 1);
2881 assert_eq!(errors[0].severity, Severity::Error);
2882 let msg = &errors[0].message;
2883 assert!(msg.contains("experimental"), "found {msg}");
2884
2885 let code = r#"@settings(experimentalFeatures = foo)
2886startSketchOn(XY)
2887 |> startProfile(at = [0, 0], tag = $start)
2888 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2889"#;
2890 parse_execute(code).await.unwrap_err();
2891 }
2892}