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