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 let env_ref = result.map_err(|(err, env_ref)| exec_state.error_with_outputs(err, env_ref, default_planes))?;
1404
1405 if !self.is_mock() {
1406 let mut stack = exec_state.stack().deep_clone();
1407 stack.restore_env(env_ref);
1408 let state = cache::SketchModeState {
1409 stack,
1410 module_infos: exec_state.global.module_infos.clone(),
1411 #[cfg(feature = "artifact-graph")]
1412 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1413 #[cfg(not(feature = "artifact-graph"))]
1414 scene_objects: Default::default(),
1415 };
1416 cache::write_old_memory(state).await;
1417 }
1418 let session_data = self.engine.get_session_data().await;
1419
1420 Ok((env_ref, session_data))
1421 }
1422
1423 async fn execute_and_build_graph(
1426 &self,
1427 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1428 exec_state: &mut ExecState,
1429 preserve_mem: PreserveMem,
1430 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1431 #[cfg(feature = "artifact-graph")]
1437 let start_op = exec_state.global.root_module_artifacts.operations.len();
1438
1439 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1440 .await
1441 .map_err(|e| (e, None))?;
1442
1443 let exec_result = self
1444 .exec_module_body(
1445 program,
1446 exec_state,
1447 preserve_mem,
1448 ModuleId::default(),
1449 &ModulePath::Main,
1450 )
1451 .await
1452 .map(
1453 |ModuleExecutionOutcome {
1454 environment: env_ref,
1455 artifacts: module_artifacts,
1456 ..
1457 }| {
1458 exec_state.global.root_module_artifacts.extend(module_artifacts);
1461 env_ref
1462 },
1463 )
1464 .map_err(|(err, env_ref, module_artifacts)| {
1465 if let Some(module_artifacts) = module_artifacts {
1466 exec_state.global.root_module_artifacts.extend(module_artifacts);
1469 }
1470 (err, env_ref)
1471 });
1472
1473 #[cfg(feature = "artifact-graph")]
1474 {
1475 let programs = &exec_state.build_program_lookup(program.clone());
1477 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1478 for op in exec_state
1479 .global
1480 .root_module_artifacts
1481 .operations
1482 .iter_mut()
1483 .skip(start_op)
1484 {
1485 op.fill_node_paths(programs, cached_body_items);
1486 }
1487 for module in exec_state.global.module_infos.values_mut() {
1488 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1489 for op in &mut outcome.artifacts.operations {
1490 op.fill_node_paths(programs, cached_body_items);
1491 }
1492 }
1493 }
1494 }
1495
1496 self.engine.ensure_async_commands_completed().await.map_err(|e| {
1498 match &exec_result {
1499 Ok(env_ref) => (e, Some(*env_ref)),
1500 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1502 }
1503 })?;
1504
1505 self.engine.clear_queues().await;
1508
1509 match exec_state.build_artifact_graph(&self.engine, program).await {
1510 Ok(_) => exec_result,
1511 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1512 }
1513 }
1514
1515 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1519 if exec_state.stack().memory.requires_std() {
1520 #[cfg(feature = "artifact-graph")]
1521 let initial_ops = exec_state.mod_local.artifacts.operations.len();
1522
1523 let path = vec!["std".to_owned(), "prelude".to_owned()];
1524 let resolved_path = ModulePath::from_std_import_path(&path)?;
1525 let id = self
1526 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1527 .await?;
1528 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1529
1530 exec_state.mut_stack().memory.set_std(module_memory);
1531
1532 #[cfg(feature = "artifact-graph")]
1538 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1539 }
1540
1541 Ok(())
1542 }
1543
1544 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1546 self.engine
1548 .send_modeling_cmd(
1549 uuid::Uuid::new_v4(),
1550 crate::execution::SourceRange::default(),
1551 &ModelingCmd::from(
1552 mcmd::ZoomToFit::builder()
1553 .object_ids(Default::default())
1554 .animated(false)
1555 .padding(0.1)
1556 .build(),
1557 ),
1558 )
1559 .await
1560 .map_err(KclErrorWithOutputs::no_outputs)?;
1561
1562 let resp = self
1564 .engine
1565 .send_modeling_cmd(
1566 uuid::Uuid::new_v4(),
1567 crate::execution::SourceRange::default(),
1568 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
1569 )
1570 .await
1571 .map_err(KclErrorWithOutputs::no_outputs)?;
1572
1573 let OkWebSocketResponseData::Modeling {
1574 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1575 } = resp
1576 else {
1577 return Err(ExecError::BadPng(format!(
1578 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1579 )));
1580 };
1581 Ok(contents)
1582 }
1583
1584 pub async fn export(
1586 &self,
1587 format: kittycad_modeling_cmds::format::OutputFormat3d,
1588 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1589 let resp = self
1590 .engine
1591 .send_modeling_cmd(
1592 uuid::Uuid::new_v4(),
1593 crate::SourceRange::default(),
1594 &kittycad_modeling_cmds::ModelingCmd::Export(
1595 kittycad_modeling_cmds::Export::builder()
1596 .entity_ids(vec![])
1597 .format(format)
1598 .build(),
1599 ),
1600 )
1601 .await?;
1602
1603 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1604 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1605 format!("Expected Export response, got {resp:?}",),
1606 vec![SourceRange::default()],
1607 )));
1608 };
1609
1610 Ok(files)
1611 }
1612
1613 pub async fn export_step(
1615 &self,
1616 deterministic_time: bool,
1617 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1618 let files = self
1619 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1620 kittycad_modeling_cmds::format::step::export::Options::builder()
1621 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
1622 .maybe_created(if deterministic_time {
1623 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1624 KclError::new_internal(crate::errors::KclErrorDetails::new(
1625 format!("Failed to parse date: {e}"),
1626 vec![SourceRange::default()],
1627 ))
1628 })?)
1629 } else {
1630 None
1631 })
1632 .build(),
1633 ))
1634 .await?;
1635
1636 Ok(files)
1637 }
1638
1639 pub async fn close(&self) {
1640 self.engine.close().await;
1641 }
1642}
1643
1644#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1645pub struct ArtifactId(Uuid);
1646
1647impl ArtifactId {
1648 pub fn new(uuid: Uuid) -> Self {
1649 Self(uuid)
1650 }
1651
1652 pub fn placeholder() -> Self {
1654 Self(Uuid::nil())
1655 }
1656}
1657
1658impl From<Uuid> for ArtifactId {
1659 fn from(uuid: Uuid) -> Self {
1660 Self::new(uuid)
1661 }
1662}
1663
1664impl From<&Uuid> for ArtifactId {
1665 fn from(uuid: &Uuid) -> Self {
1666 Self::new(*uuid)
1667 }
1668}
1669
1670impl From<ArtifactId> for Uuid {
1671 fn from(id: ArtifactId) -> Self {
1672 id.0
1673 }
1674}
1675
1676impl From<&ArtifactId> for Uuid {
1677 fn from(id: &ArtifactId) -> Self {
1678 id.0
1679 }
1680}
1681
1682impl From<ModelingCmdId> for ArtifactId {
1683 fn from(id: ModelingCmdId) -> Self {
1684 Self::new(*id.as_ref())
1685 }
1686}
1687
1688impl From<&ModelingCmdId> for ArtifactId {
1689 fn from(id: &ModelingCmdId) -> Self {
1690 Self::new(*id.as_ref())
1691 }
1692}
1693
1694#[cfg(test)]
1695pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1696 parse_execute_with_project_dir(code, None).await
1697}
1698
1699#[cfg(test)]
1700pub(crate) async fn parse_execute_with_project_dir(
1701 code: &str,
1702 project_directory: Option<TypedPath>,
1703) -> Result<ExecTestResults, KclError> {
1704 let program = crate::Program::parse_no_errs(code)?;
1705
1706 let exec_ctxt = ExecutorContext {
1707 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
1708 |err| {
1709 KclError::new_internal(crate::errors::KclErrorDetails::new(
1710 format!("Failed to create mock engine connection: {err}"),
1711 vec![SourceRange::default()],
1712 ))
1713 },
1714 )?)),
1715 fs: Arc::new(crate::fs::FileManager::new()),
1716 settings: ExecutorSettings {
1717 project_directory,
1718 ..Default::default()
1719 },
1720 context_type: ContextType::Mock,
1721 };
1722 let mut exec_state = ExecState::new(&exec_ctxt);
1723 let result = exec_ctxt.run(&program, &mut exec_state).await?;
1724
1725 Ok(ExecTestResults {
1726 program,
1727 mem_env: result.0,
1728 exec_ctxt,
1729 exec_state,
1730 })
1731}
1732
1733#[cfg(test)]
1734#[derive(Debug)]
1735pub(crate) struct ExecTestResults {
1736 program: crate::Program,
1737 mem_env: EnvironmentRef,
1738 exec_ctxt: ExecutorContext,
1739 exec_state: ExecState,
1740}
1741
1742#[cfg(feature = "artifact-graph")]
1746pub struct ProgramLookup {
1747 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
1748}
1749
1750#[cfg(feature = "artifact-graph")]
1751impl ProgramLookup {
1752 pub fn new(
1755 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1756 module_infos: state::ModuleInfoMap,
1757 ) -> Self {
1758 let mut programs = IndexMap::with_capacity(module_infos.len());
1759 for (id, info) in module_infos {
1760 if let ModuleRepr::Kcl(program, _) = info.repr {
1761 programs.insert(id, program);
1762 }
1763 }
1764 programs.insert(ModuleId::default(), current);
1765 Self { programs }
1766 }
1767
1768 pub fn program_for_module(
1769 &self,
1770 module_id: ModuleId,
1771 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
1772 self.programs.get(&module_id)
1773 }
1774}
1775
1776#[cfg(test)]
1777mod tests {
1778 use pretty_assertions::assert_eq;
1779
1780 use super::*;
1781 use crate::{
1782 ModuleId,
1783 errors::{KclErrorDetails, Severity},
1784 exec::NumericType,
1785 execution::{memory::Stack, types::RuntimeType},
1786 };
1787
1788 #[track_caller]
1790 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1791 memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1792 }
1793
1794 #[tokio::test(flavor = "multi_thread")]
1795 async fn test_execute_warn() {
1796 let text = "@blah";
1797 let result = parse_execute(text).await.unwrap();
1798 let errs = result.exec_state.errors();
1799 assert_eq!(errs.len(), 1);
1800 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1801 assert!(
1802 errs[0].message.contains("Unknown annotation"),
1803 "unexpected warning message: {}",
1804 errs[0].message
1805 );
1806 }
1807
1808 #[tokio::test(flavor = "multi_thread")]
1809 async fn test_execute_fn_definitions() {
1810 let ast = r#"fn def(@x) {
1811 return x
1812}
1813fn ghi(@x) {
1814 return x
1815}
1816fn jkl(@x) {
1817 return x
1818}
1819fn hmm(@x) {
1820 return x
1821}
1822
1823yo = 5 + 6
1824
1825abc = 3
1826identifierGuy = 5
1827part001 = startSketchOn(XY)
1828|> startProfile(at = [-1.2, 4.83])
1829|> line(end = [2.8, 0])
1830|> angledLine(angle = 100 + 100, length = 3.01)
1831|> angledLine(angle = abc, length = 3.02)
1832|> angledLine(angle = def(yo), length = 3.03)
1833|> angledLine(angle = ghi(2), length = 3.04)
1834|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1835|> close()
1836yo2 = hmm([identifierGuy + 5])"#;
1837
1838 parse_execute(ast).await.unwrap();
1839 }
1840
1841 #[tokio::test(flavor = "multi_thread")]
1842 async fn test_execute_with_pipe_substitutions_unary() {
1843 let ast = r#"myVar = 3
1844part001 = startSketchOn(XY)
1845 |> startProfile(at = [0, 0])
1846 |> line(end = [3, 4], tag = $seg01)
1847 |> line(end = [
1848 min([segLen(seg01), myVar]),
1849 -legLen(hypotenuse = segLen(seg01), leg = myVar)
1850])
1851"#;
1852
1853 parse_execute(ast).await.unwrap();
1854 }
1855
1856 #[tokio::test(flavor = "multi_thread")]
1857 async fn test_execute_with_pipe_substitutions() {
1858 let ast = r#"myVar = 3
1859part001 = startSketchOn(XY)
1860 |> startProfile(at = [0, 0])
1861 |> line(end = [3, 4], tag = $seg01)
1862 |> line(end = [
1863 min([segLen(seg01), myVar]),
1864 legLen(hypotenuse = segLen(seg01), leg = myVar)
1865])
1866"#;
1867
1868 parse_execute(ast).await.unwrap();
1869 }
1870
1871 #[tokio::test(flavor = "multi_thread")]
1872 async fn test_execute_with_inline_comment() {
1873 let ast = r#"baseThick = 1
1874armAngle = 60
1875
1876baseThickHalf = baseThick / 2
1877halfArmAngle = armAngle / 2
1878
1879arrExpShouldNotBeIncluded = [1, 2, 3]
1880objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1881
1882part001 = startSketchOn(XY)
1883 |> startProfile(at = [0, 0])
1884 |> yLine(endAbsolute = 1)
1885 |> xLine(length = 3.84) // selection-range-7ish-before-this
1886
1887variableBelowShouldNotBeIncluded = 3
1888"#;
1889
1890 parse_execute(ast).await.unwrap();
1891 }
1892
1893 #[tokio::test(flavor = "multi_thread")]
1894 async fn test_execute_with_function_literal_in_pipe() {
1895 let ast = r#"w = 20
1896l = 8
1897h = 10
1898
1899fn thing() {
1900 return -8
1901}
1902
1903firstExtrude = startSketchOn(XY)
1904 |> startProfile(at = [0,0])
1905 |> line(end = [0, l])
1906 |> line(end = [w, 0])
1907 |> line(end = [0, thing()])
1908 |> close()
1909 |> extrude(length = h)"#;
1910
1911 parse_execute(ast).await.unwrap();
1912 }
1913
1914 #[tokio::test(flavor = "multi_thread")]
1915 async fn test_execute_with_function_unary_in_pipe() {
1916 let ast = r#"w = 20
1917l = 8
1918h = 10
1919
1920fn thing(@x) {
1921 return -x
1922}
1923
1924firstExtrude = startSketchOn(XY)
1925 |> startProfile(at = [0,0])
1926 |> line(end = [0, l])
1927 |> line(end = [w, 0])
1928 |> line(end = [0, thing(8)])
1929 |> close()
1930 |> extrude(length = h)"#;
1931
1932 parse_execute(ast).await.unwrap();
1933 }
1934
1935 #[tokio::test(flavor = "multi_thread")]
1936 async fn test_execute_with_function_array_in_pipe() {
1937 let ast = r#"w = 20
1938l = 8
1939h = 10
1940
1941fn thing(@x) {
1942 return [0, -x]
1943}
1944
1945firstExtrude = startSketchOn(XY)
1946 |> startProfile(at = [0,0])
1947 |> line(end = [0, l])
1948 |> line(end = [w, 0])
1949 |> line(end = thing(8))
1950 |> close()
1951 |> extrude(length = h)"#;
1952
1953 parse_execute(ast).await.unwrap();
1954 }
1955
1956 #[tokio::test(flavor = "multi_thread")]
1957 async fn test_execute_with_function_call_in_pipe() {
1958 let ast = r#"w = 20
1959l = 8
1960h = 10
1961
1962fn other_thing(@y) {
1963 return -y
1964}
1965
1966fn thing(@x) {
1967 return other_thing(x)
1968}
1969
1970firstExtrude = startSketchOn(XY)
1971 |> startProfile(at = [0,0])
1972 |> line(end = [0, l])
1973 |> line(end = [w, 0])
1974 |> line(end = [0, thing(8)])
1975 |> close()
1976 |> extrude(length = h)"#;
1977
1978 parse_execute(ast).await.unwrap();
1979 }
1980
1981 #[tokio::test(flavor = "multi_thread")]
1982 async fn test_execute_with_function_sketch() {
1983 let ast = r#"fn box(h, l, w) {
1984 myBox = startSketchOn(XY)
1985 |> startProfile(at = [0,0])
1986 |> line(end = [0, l])
1987 |> line(end = [w, 0])
1988 |> line(end = [0, -l])
1989 |> close()
1990 |> extrude(length = h)
1991
1992 return myBox
1993}
1994
1995fnBox = box(h = 3, l = 6, w = 10)"#;
1996
1997 parse_execute(ast).await.unwrap();
1998 }
1999
2000 #[tokio::test(flavor = "multi_thread")]
2001 async fn test_get_member_of_object_with_function_period() {
2002 let ast = r#"fn box(@obj) {
2003 myBox = startSketchOn(XY)
2004 |> startProfile(at = obj.start)
2005 |> line(end = [0, obj.l])
2006 |> line(end = [obj.w, 0])
2007 |> line(end = [0, -obj.l])
2008 |> close()
2009 |> extrude(length = obj.h)
2010
2011 return myBox
2012}
2013
2014thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2015"#;
2016 parse_execute(ast).await.unwrap();
2017 }
2018
2019 #[tokio::test(flavor = "multi_thread")]
2020 #[ignore] async fn test_object_member_starting_pipeline() {
2022 let ast = r#"
2023fn test2() {
2024 return {
2025 thing: startSketchOn(XY)
2026 |> startProfile(at = [0, 0])
2027 |> line(end = [0, 1])
2028 |> line(end = [1, 0])
2029 |> line(end = [0, -1])
2030 |> close()
2031 }
2032}
2033
2034x2 = test2()
2035
2036x2.thing
2037 |> extrude(length = 10)
2038"#;
2039 parse_execute(ast).await.unwrap();
2040 }
2041
2042 #[tokio::test(flavor = "multi_thread")]
2043 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
2045 let ast = r#"fn box(obj) {
2046let myBox = startSketchOn(XY)
2047 |> startProfile(at = obj.start)
2048 |> line(end = [0, obj.l])
2049 |> line(end = [obj.w, 0])
2050 |> line(end = [0, -obj.l])
2051 |> close()
2052 |> extrude(length = obj.h)
2053
2054 return myBox
2055}
2056
2057for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2058 thisBox = box(var)
2059}"#;
2060
2061 parse_execute(ast).await.unwrap();
2062 }
2063
2064 #[tokio::test(flavor = "multi_thread")]
2065 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
2067 let ast = r#"fn box(h, l, w, start) {
2068 myBox = startSketchOn(XY)
2069 |> startProfile(at = [0,0])
2070 |> line(end = [0, l])
2071 |> line(end = [w, 0])
2072 |> line(end = [0, -l])
2073 |> close()
2074 |> extrude(length = h)
2075
2076 return myBox
2077}
2078
2079
2080for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2081 const thisBox = box(var[0], var[1], var[2], var[3])
2082}"#;
2083
2084 parse_execute(ast).await.unwrap();
2085 }
2086
2087 #[tokio::test(flavor = "multi_thread")]
2088 async fn test_get_member_of_array_with_function() {
2089 let ast = r#"fn box(@arr) {
2090 myBox =startSketchOn(XY)
2091 |> startProfile(at = arr[0])
2092 |> line(end = [0, arr[1]])
2093 |> line(end = [arr[2], 0])
2094 |> line(end = [0, -arr[1]])
2095 |> close()
2096 |> extrude(length = arr[3])
2097
2098 return myBox
2099}
2100
2101thisBox = box([[0,0], 6, 10, 3])
2102
2103"#;
2104 parse_execute(ast).await.unwrap();
2105 }
2106
2107 #[tokio::test(flavor = "multi_thread")]
2108 async fn test_function_cannot_access_future_definitions() {
2109 let ast = r#"
2110fn returnX() {
2111 // x shouldn't be defined yet.
2112 return x
2113}
2114
2115x = 5
2116
2117answer = returnX()"#;
2118
2119 let result = parse_execute(ast).await;
2120 let err = result.unwrap_err();
2121 assert_eq!(err.message(), "`x` is not defined");
2122 }
2123
2124 #[tokio::test(flavor = "multi_thread")]
2125 async fn test_override_prelude() {
2126 let text = "PI = 3.0";
2127 let result = parse_execute(text).await.unwrap();
2128 let errs = result.exec_state.errors();
2129 assert!(errs.is_empty());
2130 }
2131
2132 #[tokio::test(flavor = "multi_thread")]
2133 async fn type_aliases() {
2134 let text = r#"@settings(experimentalFeatures = allow)
2135type MyTy = [number; 2]
2136fn foo(@x: MyTy) {
2137 return x[0]
2138}
2139
2140foo([0, 1])
2141
2142type Other = MyTy | Helix
2143"#;
2144 let result = parse_execute(text).await.unwrap();
2145 let errs = result.exec_state.errors();
2146 assert!(errs.is_empty());
2147 }
2148
2149 #[tokio::test(flavor = "multi_thread")]
2150 async fn test_cannot_shebang_in_fn() {
2151 let ast = r#"
2152fn foo() {
2153 #!hello
2154 return true
2155}
2156
2157foo
2158"#;
2159
2160 let result = parse_execute(ast).await;
2161 let err = result.unwrap_err();
2162 assert_eq!(
2163 err,
2164 KclError::new_syntax(KclErrorDetails::new(
2165 "Unexpected token: #".to_owned(),
2166 vec![SourceRange::new(14, 15, ModuleId::default())],
2167 )),
2168 );
2169 }
2170
2171 #[tokio::test(flavor = "multi_thread")]
2172 async fn test_pattern_transform_function_cannot_access_future_definitions() {
2173 let ast = r#"
2174fn transform(@replicaId) {
2175 // x shouldn't be defined yet.
2176 scale = x
2177 return {
2178 translate = [0, 0, replicaId * 10],
2179 scale = [scale, 1, 0],
2180 }
2181}
2182
2183fn layer() {
2184 return startSketchOn(XY)
2185 |> circle( center= [0, 0], radius= 1, tag = $tag1)
2186 |> extrude(length = 10)
2187}
2188
2189x = 5
2190
2191// The 10 layers are replicas of each other, with a transform applied to each.
2192shape = layer() |> patternTransform(instances = 10, transform = transform)
2193"#;
2194
2195 let result = parse_execute(ast).await;
2196 let err = result.unwrap_err();
2197 assert_eq!(err.message(), "`x` is not defined",);
2198 }
2199
2200 #[tokio::test(flavor = "multi_thread")]
2203 async fn test_math_execute_with_functions() {
2204 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
2205 let result = parse_execute(ast).await.unwrap();
2206 assert_eq!(
2207 5.0,
2208 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2209 .as_f64()
2210 .unwrap()
2211 );
2212 }
2213
2214 #[tokio::test(flavor = "multi_thread")]
2215 async fn test_math_execute() {
2216 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
2217 let result = parse_execute(ast).await.unwrap();
2218 assert_eq!(
2219 7.4,
2220 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2221 .as_f64()
2222 .unwrap()
2223 );
2224 }
2225
2226 #[tokio::test(flavor = "multi_thread")]
2227 async fn test_math_execute_start_negative() {
2228 let ast = r#"myVar = -5 + 6"#;
2229 let result = parse_execute(ast).await.unwrap();
2230 assert_eq!(
2231 1.0,
2232 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2233 .as_f64()
2234 .unwrap()
2235 );
2236 }
2237
2238 #[tokio::test(flavor = "multi_thread")]
2239 async fn test_math_execute_with_pi() {
2240 let ast = r#"myVar = PI * 2"#;
2241 let result = parse_execute(ast).await.unwrap();
2242 assert_eq!(
2243 std::f64::consts::TAU,
2244 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2245 .as_f64()
2246 .unwrap()
2247 );
2248 }
2249
2250 #[tokio::test(flavor = "multi_thread")]
2251 async fn test_math_define_decimal_without_leading_zero() {
2252 let ast = r#"thing = .4 + 7"#;
2253 let result = parse_execute(ast).await.unwrap();
2254 assert_eq!(
2255 7.4,
2256 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
2257 .as_f64()
2258 .unwrap()
2259 );
2260 }
2261
2262 #[tokio::test(flavor = "multi_thread")]
2263 async fn pass_std_to_std() {
2264 let ast = r#"sketch001 = startSketchOn(XY)
2265profile001 = circle(sketch001, center = [0, 0], radius = 2)
2266extrude001 = extrude(profile001, length = 5)
2267extrudes = patternLinear3d(
2268 extrude001,
2269 instances = 3,
2270 distance = 5,
2271 axis = [1, 1, 0],
2272)
2273clone001 = map(extrudes, f = clone)
2274"#;
2275 parse_execute(ast).await.unwrap();
2276 }
2277
2278 #[tokio::test(flavor = "multi_thread")]
2279 async fn test_array_reduce_nested_array() {
2280 let code = r#"
2281fn id(@el, accum) { return accum }
2282
2283answer = reduce([], initial=[[[0,0]]], f=id)
2284"#;
2285 let result = parse_execute(code).await.unwrap();
2286 assert_eq!(
2287 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2288 KclValue::HomArray {
2289 value: vec![KclValue::HomArray {
2290 value: vec![KclValue::HomArray {
2291 value: vec![
2292 KclValue::Number {
2293 value: 0.0,
2294 ty: NumericType::default(),
2295 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2296 },
2297 KclValue::Number {
2298 value: 0.0,
2299 ty: NumericType::default(),
2300 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2301 }
2302 ],
2303 ty: RuntimeType::any(),
2304 }],
2305 ty: RuntimeType::any(),
2306 }],
2307 ty: RuntimeType::any(),
2308 }
2309 );
2310 }
2311
2312 #[tokio::test(flavor = "multi_thread")]
2313 async fn test_zero_param_fn() {
2314 let ast = r#"sigmaAllow = 35000 // psi
2315leg1 = 5 // inches
2316leg2 = 8 // inches
2317fn thickness() { return 0.56 }
2318
2319bracket = startSketchOn(XY)
2320 |> startProfile(at = [0,0])
2321 |> line(end = [0, leg1])
2322 |> line(end = [leg2, 0])
2323 |> line(end = [0, -thickness()])
2324 |> line(end = [-leg2 + thickness(), 0])
2325"#;
2326 parse_execute(ast).await.unwrap();
2327 }
2328
2329 #[tokio::test(flavor = "multi_thread")]
2330 async fn test_unary_operator_not_succeeds() {
2331 let ast = r#"
2332fn returnTrue() { return !false }
2333t = true
2334f = false
2335notTrue = !t
2336notFalse = !f
2337c = !!true
2338d = !returnTrue()
2339
2340assertIs(!false, error = "expected to pass")
2341
2342fn check(x) {
2343 assertIs(!x, error = "expected argument to be false")
2344 return true
2345}
2346check(x = false)
2347"#;
2348 let result = parse_execute(ast).await.unwrap();
2349 assert_eq!(
2350 false,
2351 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2352 .as_bool()
2353 .unwrap()
2354 );
2355 assert_eq!(
2356 true,
2357 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2358 .as_bool()
2359 .unwrap()
2360 );
2361 assert_eq!(
2362 true,
2363 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2364 .as_bool()
2365 .unwrap()
2366 );
2367 assert_eq!(
2368 false,
2369 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2370 .as_bool()
2371 .unwrap()
2372 );
2373 }
2374
2375 #[tokio::test(flavor = "multi_thread")]
2376 async fn test_unary_operator_not_on_non_bool_fails() {
2377 let code1 = r#"
2378// Yup, this is null.
2379myNull = 0 / 0
2380notNull = !myNull
2381"#;
2382 assert_eq!(
2383 parse_execute(code1).await.unwrap_err().message(),
2384 "Cannot apply unary operator ! to non-boolean value: a number",
2385 );
2386
2387 let code2 = "notZero = !0";
2388 assert_eq!(
2389 parse_execute(code2).await.unwrap_err().message(),
2390 "Cannot apply unary operator ! to non-boolean value: a number",
2391 );
2392
2393 let code3 = r#"
2394notEmptyString = !""
2395"#;
2396 assert_eq!(
2397 parse_execute(code3).await.unwrap_err().message(),
2398 "Cannot apply unary operator ! to non-boolean value: a string",
2399 );
2400
2401 let code4 = r#"
2402obj = { a = 1 }
2403notMember = !obj.a
2404"#;
2405 assert_eq!(
2406 parse_execute(code4).await.unwrap_err().message(),
2407 "Cannot apply unary operator ! to non-boolean value: a number",
2408 );
2409
2410 let code5 = "
2411a = []
2412notArray = !a";
2413 assert_eq!(
2414 parse_execute(code5).await.unwrap_err().message(),
2415 "Cannot apply unary operator ! to non-boolean value: an empty array",
2416 );
2417
2418 let code6 = "
2419x = {}
2420notObject = !x";
2421 assert_eq!(
2422 parse_execute(code6).await.unwrap_err().message(),
2423 "Cannot apply unary operator ! to non-boolean value: an object",
2424 );
2425
2426 let code7 = "
2427fn x() { return 1 }
2428notFunction = !x";
2429 let fn_err = parse_execute(code7).await.unwrap_err();
2430 assert!(
2433 fn_err
2434 .message()
2435 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2436 "Actual error: {fn_err:?}"
2437 );
2438
2439 let code8 = "
2440myTagDeclarator = $myTag
2441notTagDeclarator = !myTagDeclarator";
2442 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2443 assert!(
2446 tag_declarator_err
2447 .message()
2448 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2449 "Actual error: {tag_declarator_err:?}"
2450 );
2451
2452 let code9 = "
2453myTagDeclarator = $myTag
2454notTagIdentifier = !myTag";
2455 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2456 assert!(
2459 tag_identifier_err
2460 .message()
2461 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2462 "Actual error: {tag_identifier_err:?}"
2463 );
2464
2465 let code10 = "notPipe = !(1 |> 2)";
2466 assert_eq!(
2467 parse_execute(code10).await.unwrap_err(),
2470 KclError::new_syntax(KclErrorDetails::new(
2471 "Unexpected token: !".to_owned(),
2472 vec![SourceRange::new(10, 11, ModuleId::default())],
2473 ))
2474 );
2475
2476 let code11 = "
2477fn identity(x) { return x }
2478notPipeSub = 1 |> identity(!%))";
2479 assert_eq!(
2480 parse_execute(code11).await.unwrap_err(),
2483 KclError::new_syntax(KclErrorDetails::new(
2484 "There was an unexpected `!`. Try removing it.".to_owned(),
2485 vec![SourceRange::new(56, 57, ModuleId::default())],
2486 ))
2487 );
2488
2489 }
2493
2494 #[tokio::test(flavor = "multi_thread")]
2495 async fn test_start_sketch_on_invalid_kwargs() {
2496 let current_dir = std::env::current_dir().unwrap();
2497 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2498 let mut code = std::fs::read_to_string(&path).unwrap();
2499 assert_eq!(
2500 parse_execute(&code).await.unwrap_err().message(),
2501 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2502 );
2503
2504 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2505 code = std::fs::read_to_string(&path).unwrap();
2506
2507 assert_eq!(
2508 parse_execute(&code).await.unwrap_err().message(),
2509 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2510 );
2511
2512 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2513 code = std::fs::read_to_string(&path).unwrap();
2514
2515 assert_eq!(
2516 parse_execute(&code).await.unwrap_err().message(),
2517 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2518 );
2519
2520 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2521 code = std::fs::read_to_string(&path).unwrap();
2522
2523 assert_eq!(
2524 parse_execute(&code).await.unwrap_err().message(),
2525 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2526 );
2527
2528 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2529 code = std::fs::read_to_string(&path).unwrap();
2530
2531 assert_eq!(
2532 parse_execute(&code).await.unwrap_err().message(),
2533 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2534 );
2535 }
2536
2537 #[tokio::test(flavor = "multi_thread")]
2538 async fn test_math_negative_variable_in_binary_expression() {
2539 let ast = r#"sigmaAllow = 35000 // psi
2540width = 1 // inch
2541
2542p = 150 // lbs
2543distance = 6 // inches
2544FOS = 2
2545
2546leg1 = 5 // inches
2547leg2 = 8 // inches
2548
2549thickness_squared = distance * p * FOS * 6 / sigmaAllow
2550thickness = 0.56 // inches. App does not support square root function yet
2551
2552bracket = startSketchOn(XY)
2553 |> startProfile(at = [0,0])
2554 |> line(end = [0, leg1])
2555 |> line(end = [leg2, 0])
2556 |> line(end = [0, -thickness])
2557 |> line(end = [-leg2 + thickness, 0])
2558"#;
2559 parse_execute(ast).await.unwrap();
2560 }
2561
2562 #[tokio::test(flavor = "multi_thread")]
2563 async fn test_execute_function_no_return() {
2564 let ast = r#"fn test(@origin) {
2565 origin
2566}
2567
2568test([0, 0])
2569"#;
2570 let result = parse_execute(ast).await;
2571 assert!(result.is_err());
2572 assert!(result.unwrap_err().to_string().contains("undefined"));
2573 }
2574
2575 #[tokio::test(flavor = "multi_thread")]
2576 async fn test_max_stack_size_exceeded_error() {
2577 let ast = r#"
2578fn forever(@n) {
2579 return 1 + forever(n)
2580}
2581
2582forever(1)
2583"#;
2584 let result = parse_execute(ast).await;
2585 let err = result.unwrap_err();
2586 assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
2587 }
2588
2589 #[tokio::test(flavor = "multi_thread")]
2590 async fn test_math_doubly_nested_parens() {
2591 let ast = r#"sigmaAllow = 35000 // psi
2592width = 4 // inch
2593p = 150 // Force on shelf - lbs
2594distance = 6 // inches
2595FOS = 2
2596leg1 = 5 // inches
2597leg2 = 8 // inches
2598thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2599thickness = 0.32 // inches. App does not support square root function yet
2600bracket = startSketchOn(XY)
2601 |> startProfile(at = [0,0])
2602 |> line(end = [0, leg1])
2603 |> line(end = [leg2, 0])
2604 |> line(end = [0, -thickness])
2605 |> line(end = [-1 * leg2 + thickness, 0])
2606 |> line(end = [0, -1 * leg1 + thickness])
2607 |> close()
2608 |> extrude(length = width)
2609"#;
2610 parse_execute(ast).await.unwrap();
2611 }
2612
2613 #[tokio::test(flavor = "multi_thread")]
2614 async fn test_math_nested_parens_one_less() {
2615 let ast = r#" sigmaAllow = 35000 // psi
2616width = 4 // inch
2617p = 150 // Force on shelf - lbs
2618distance = 6 // inches
2619FOS = 2
2620leg1 = 5 // inches
2621leg2 = 8 // inches
2622thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2623thickness = 0.32 // inches. App does not support square root function yet
2624bracket = startSketchOn(XY)
2625 |> startProfile(at = [0,0])
2626 |> line(end = [0, leg1])
2627 |> line(end = [leg2, 0])
2628 |> line(end = [0, -thickness])
2629 |> line(end = [-1 * leg2 + thickness, 0])
2630 |> line(end = [0, -1 * leg1 + thickness])
2631 |> close()
2632 |> extrude(length = width)
2633"#;
2634 parse_execute(ast).await.unwrap();
2635 }
2636
2637 #[tokio::test(flavor = "multi_thread")]
2638 async fn test_fn_as_operand() {
2639 let ast = r#"fn f() { return 1 }
2640x = f()
2641y = x + 1
2642z = f() + 1
2643w = f() + f()
2644"#;
2645 parse_execute(ast).await.unwrap();
2646 }
2647
2648 #[tokio::test(flavor = "multi_thread")]
2649 async fn kcl_test_ids_stable_between_executions() {
2650 let code = r#"sketch001 = startSketchOn(XZ)
2651|> startProfile(at = [61.74, 206.13])
2652|> xLine(length = 305.11, tag = $seg01)
2653|> yLine(length = -291.85)
2654|> xLine(length = -segLen(seg01))
2655|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2656|> close()
2657|> extrude(length = 40.14)
2658|> shell(
2659 thickness = 3.14,
2660 faces = [seg01]
2661)
2662"#;
2663
2664 let ctx = crate::test_server::new_context(true, None).await.unwrap();
2665 let old_program = crate::Program::parse_no_errs(code).unwrap();
2666
2667 if let Err(err) = ctx.run_with_caching(old_program).await {
2669 let report = err.into_miette_report_with_outputs(code).unwrap();
2670 let report = miette::Report::new(report);
2671 panic!("Error executing program: {report:?}");
2672 }
2673
2674 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2676
2677 let code = r#"sketch001 = startSketchOn(XZ)
2678|> startProfile(at = [62.74, 206.13])
2679|> xLine(length = 305.11, tag = $seg01)
2680|> yLine(length = -291.85)
2681|> xLine(length = -segLen(seg01))
2682|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2683|> close()
2684|> extrude(length = 40.14)
2685|> shell(
2686 faces = [seg01],
2687 thickness = 3.14,
2688)
2689"#;
2690
2691 let program = crate::Program::parse_no_errs(code).unwrap();
2693 ctx.run_with_caching(program).await.unwrap();
2695
2696 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2697
2698 assert_eq!(id_generator, new_id_generator);
2699 }
2700
2701 #[tokio::test(flavor = "multi_thread")]
2702 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2703 let code = r#"sketch001 = startSketchOn(XZ)
2704|> startProfile(at = [61.74, 206.13])
2705|> xLine(length = 305.11, tag = $seg01)
2706|> yLine(length = -291.85)
2707|> xLine(length = -segLen(seg01))
2708|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2709|> close()
2710|> extrude(length = 40.14)
2711|> shell(
2712 thickness = 3.14,
2713 faces = [seg01]
2714)
2715"#;
2716
2717 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2718 let old_program = crate::Program::parse_no_errs(code).unwrap();
2719
2720 ctx.run_with_caching(old_program.clone()).await.unwrap();
2722
2723 let settings_state = cache::read_old_ast().await.unwrap().settings;
2724
2725 assert_eq!(settings_state, ctx.settings);
2727
2728 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2730
2731 ctx.run_with_caching(old_program.clone()).await.unwrap();
2733
2734 let settings_state = cache::read_old_ast().await.unwrap().settings;
2735
2736 assert_eq!(settings_state, ctx.settings);
2738
2739 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2741
2742 ctx.run_with_caching(old_program).await.unwrap();
2744
2745 let settings_state = cache::read_old_ast().await.unwrap().settings;
2746
2747 assert_eq!(settings_state, ctx.settings);
2749
2750 ctx.close().await;
2751 }
2752
2753 #[tokio::test(flavor = "multi_thread")]
2754 async fn mock_after_not_mock() {
2755 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2756 let program = crate::Program::parse_no_errs("x = 2").unwrap();
2757 let result = ctx.run_with_caching(program).await.unwrap();
2758 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2759
2760 let ctx2 = ExecutorContext::new_mock(None).await;
2761 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2762 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
2763 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2764
2765 ctx.close().await;
2766 ctx2.close().await;
2767 }
2768
2769 #[cfg(feature = "artifact-graph")]
2770 #[tokio::test(flavor = "multi_thread")]
2771 async fn mock_has_stable_ids() {
2772 let ctx = ExecutorContext::new_mock(None).await;
2773 let mock_config = MockConfig {
2774 use_prev_memory: false,
2775 ..Default::default()
2776 };
2777 let code = "sk = startSketchOn(XY)
2778 |> startProfile(at = [0, 0])";
2779 let program = crate::Program::parse_no_errs(code).unwrap();
2780 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
2781 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2782 assert!(!ids.is_empty(), "IDs should not be empty");
2783
2784 let ctx2 = ExecutorContext::new_mock(None).await;
2785 let program2 = crate::Program::parse_no_errs(code).unwrap();
2786 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
2787 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2788
2789 assert_eq!(ids, ids2, "Generated IDs should match");
2790 ctx.close().await;
2791 ctx2.close().await;
2792 }
2793
2794 #[cfg(feature = "artifact-graph")]
2795 #[tokio::test(flavor = "multi_thread")]
2796 async fn sim_sketch_mode_real_mock_real() {
2797 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2798 let code = r#"sketch001 = startSketchOn(XY)
2799profile001 = startProfile(sketch001, at = [0, 0])
2800 |> line(end = [10, 0])
2801 |> line(end = [0, 10])
2802 |> line(end = [-10, 0])
2803 |> line(end = [0, -10])
2804 |> close()
2805"#;
2806 let program = crate::Program::parse_no_errs(code).unwrap();
2807 let result = ctx.run_with_caching(program).await.unwrap();
2808 assert_eq!(result.operations.len(), 1);
2809
2810 let mock_ctx = ExecutorContext::new_mock(None).await;
2811 let mock_program = crate::Program::parse_no_errs(code).unwrap();
2812 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
2813 assert_eq!(mock_result.operations.len(), 1);
2814
2815 let code2 = code.to_owned()
2816 + r#"
2817extrude001 = extrude(profile001, length = 10)
2818"#;
2819 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
2820 let result = ctx.run_with_caching(program2).await.unwrap();
2821 assert_eq!(result.operations.len(), 2);
2822
2823 ctx.close().await;
2824 mock_ctx.close().await;
2825 }
2826
2827 #[tokio::test(flavor = "multi_thread")]
2828 async fn read_tag_version() {
2829 let ast = r#"fn bar(@t) {
2830 return startSketchOn(XY)
2831 |> startProfile(at = [0,0])
2832 |> angledLine(
2833 angle = -60,
2834 length = segLen(t),
2835 )
2836 |> line(end = [0, 0])
2837 |> close()
2838}
2839
2840sketch = startSketchOn(XY)
2841 |> startProfile(at = [0,0])
2842 |> line(end = [0, 10])
2843 |> line(end = [10, 0], tag = $tag0)
2844 |> line(endAbsolute = [0, 0])
2845
2846fn foo() {
2847 // tag0 tags an edge
2848 return bar(tag0)
2849}
2850
2851solid = sketch |> extrude(length = 10)
2852// tag0 tags a face
2853sketch2 = startSketchOn(solid, face = tag0)
2854 |> startProfile(at = [0,0])
2855 |> line(end = [0, 1])
2856 |> line(end = [1, 0])
2857 |> line(end = [0, 0])
2858
2859foo() |> extrude(length = 1)
2860"#;
2861 parse_execute(ast).await.unwrap();
2862 }
2863
2864 #[tokio::test(flavor = "multi_thread")]
2865 async fn experimental() {
2866 let code = r#"
2867startSketchOn(XY)
2868 |> startProfile(at = [0, 0], tag = $start)
2869 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2870"#;
2871 let result = parse_execute(code).await.unwrap();
2872 let errors = result.exec_state.errors();
2873 assert_eq!(errors.len(), 1);
2874 assert_eq!(errors[0].severity, Severity::Error);
2875 let msg = &errors[0].message;
2876 assert!(msg.contains("experimental"), "found {msg}");
2877
2878 let code = r#"@settings(experimentalFeatures = allow)
2879startSketchOn(XY)
2880 |> startProfile(at = [0, 0], tag = $start)
2881 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2882"#;
2883 let result = parse_execute(code).await.unwrap();
2884 let errors = result.exec_state.errors();
2885 assert!(errors.is_empty());
2886
2887 let code = r#"@settings(experimentalFeatures = warn)
2888startSketchOn(XY)
2889 |> startProfile(at = [0, 0], tag = $start)
2890 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2891"#;
2892 let result = parse_execute(code).await.unwrap();
2893 let errors = result.exec_state.errors();
2894 assert_eq!(errors.len(), 1);
2895 assert_eq!(errors[0].severity, Severity::Warning);
2896 let msg = &errors[0].message;
2897 assert!(msg.contains("experimental"), "found {msg}");
2898
2899 let code = r#"@settings(experimentalFeatures = deny)
2900startSketchOn(XY)
2901 |> startProfile(at = [0, 0], tag = $start)
2902 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2903"#;
2904 let result = parse_execute(code).await.unwrap();
2905 let errors = result.exec_state.errors();
2906 assert_eq!(errors.len(), 1);
2907 assert_eq!(errors[0].severity, Severity::Error);
2908 let msg = &errors[0].message;
2909 assert!(msg.contains("experimental"), "found {msg}");
2910
2911 let code = r#"@settings(experimentalFeatures = foo)
2912startSketchOn(XY)
2913 |> startProfile(at = [0, 0], tag = $start)
2914 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2915"#;
2916 parse_execute(code).await.unwrap_err();
2917 }
2918
2919 #[tokio::test(flavor = "multi_thread")]
2920 async fn experimental_parameter() {
2921 let code = r#"
2922fn inc(@x, @(experimental = true) amount? = 1) {
2923 return x + amount
2924}
2925
2926answer = inc(5, amount = 2)
2927"#;
2928 let result = parse_execute(code).await.unwrap();
2929 let errors = result.exec_state.errors();
2930 assert_eq!(errors.len(), 1);
2931 assert_eq!(errors[0].severity, Severity::Error);
2932 let msg = &errors[0].message;
2933 assert!(msg.contains("experimental"), "found {msg}");
2934
2935 let code = r#"
2937fn inc(@x, @(experimental = true) amount? = 1) {
2938 return x + amount
2939}
2940
2941answer = inc(5)
2942"#;
2943 let result = parse_execute(code).await.unwrap();
2944 let errors = result.exec_state.errors();
2945 assert_eq!(errors.len(), 0);
2946 }
2947}