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