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