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