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