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