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