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