1use std::sync::Arc;
4
5use anyhow::Result;
6#[cfg(feature = "artifact-graph")]
7pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketchOnFace, StartSketchOnPlane};
8use cache::GlobalState;
9pub use cache::{bust_cache, clear_mem_cache};
10#[cfg(feature = "artifact-graph")]
11pub use cad_op::{Group, Operation};
12pub use geometry::*;
13pub use id_generator::IdGenerator;
14pub(crate) use import::PreImportedGeometry;
15use indexmap::IndexMap;
16pub use kcl_value::{KclObjectFields, KclValue};
17use kcmc::{
18 each_cmd as mcmd,
19 ok_response::{output::TakeSnapshot, OkModelingCmdResponse},
20 websocket::{ModelingSessionData, OkWebSocketResponseData},
21 ImageFormat, ModelingCmd,
22};
23use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
24pub use memory::EnvironmentRef;
25use schemars::JsonSchema;
26use serde::{Deserialize, Serialize};
27pub use state::{ExecState, MetaSettings};
28use uuid::Uuid;
29
30use crate::{
31 engine::EngineManager,
32 errors::{KclError, KclErrorDetails},
33 execution::{
34 cache::{CacheInformation, CacheResult},
35 import_graph::{Universe, UniverseMap},
36 typed_path::TypedPath,
37 types::{UnitAngle, UnitLen},
38 },
39 fs::FileManager,
40 modules::{ModuleId, ModulePath, ModuleRepr},
41 parsing::ast::types::{Expr, ImportPath, NodeRef},
42 source_range::SourceRange,
43 CompilationError, ExecError, KclErrorWithOutputs,
44};
45
46pub(crate) mod annotations;
47#[cfg(feature = "artifact-graph")]
48mod artifact;
49pub(crate) mod cache;
50mod cad_op;
51mod exec_ast;
52pub mod fn_call;
53mod geometry;
54mod id_generator;
55mod import;
56mod import_graph;
57pub(crate) mod kcl_value;
58mod memory;
59mod state;
60pub mod typed_path;
61pub(crate) mod types;
62
63enum StatementKind<'a> {
64 Declaration { name: &'a str },
65 Expression,
66}
67
68#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
70#[ts(export)]
71#[serde(rename_all = "camelCase")]
72pub struct ExecOutcome {
73 pub variables: IndexMap<String, KclValue>,
75 #[cfg(feature = "artifact-graph")]
78 pub operations: Vec<Operation>,
79 #[cfg(feature = "artifact-graph")]
81 pub artifact_commands: Vec<ArtifactCommand>,
82 #[cfg(feature = "artifact-graph")]
84 pub artifact_graph: ArtifactGraph,
85 pub errors: Vec<CompilationError>,
87 pub filenames: IndexMap<ModuleId, ModulePath>,
89 pub default_planes: Option<DefaultPlanes>,
91}
92
93#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
94#[ts(export)]
95#[serde(rename_all = "camelCase")]
96pub struct DefaultPlanes {
97 pub xy: uuid::Uuid,
98 pub xz: uuid::Uuid,
99 pub yz: uuid::Uuid,
100 pub neg_xy: uuid::Uuid,
101 pub neg_xz: uuid::Uuid,
102 pub neg_yz: uuid::Uuid,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
106#[ts(export)]
107#[serde(tag = "type", rename_all = "camelCase")]
108pub struct TagIdentifier {
109 pub value: String,
110 #[serde(skip)]
113 pub info: Vec<(usize, TagEngineInfo)>,
114 #[serde(skip)]
115 pub meta: Vec<Metadata>,
116}
117
118impl TagIdentifier {
119 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
121 for (e, info) in self.info.iter().rev() {
122 if *e <= at_epoch {
123 return Some(info);
124 }
125 }
126
127 None
128 }
129
130 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
132 self.info.last().map(|i| &i.1)
133 }
134
135 pub fn merge_info(&mut self, other: &TagIdentifier) {
137 assert_eq!(&self.value, &other.value);
138 for (oe, ot) in &other.info {
139 if let Some((e, t)) = self.info.last_mut() {
140 if *e > *oe {
142 continue;
143 }
144 if e == oe {
146 *t = ot.clone();
147 continue;
148 }
149 }
150 self.info.push((*oe, ot.clone()));
151 }
152 }
153}
154
155impl Eq for TagIdentifier {}
156
157impl std::fmt::Display for TagIdentifier {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 write!(f, "{}", self.value)
160 }
161}
162
163impl std::str::FromStr for TagIdentifier {
164 type Err = KclError;
165
166 fn from_str(s: &str) -> Result<Self, Self::Err> {
167 Ok(Self {
168 value: s.to_string(),
169 info: Vec::new(),
170 meta: Default::default(),
171 })
172 }
173}
174
175impl Ord for TagIdentifier {
176 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
177 self.value.cmp(&other.value)
178 }
179}
180
181impl PartialOrd for TagIdentifier {
182 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
183 Some(self.cmp(other))
184 }
185}
186
187impl std::hash::Hash for TagIdentifier {
188 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
189 self.value.hash(state);
190 }
191}
192
193#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
195#[ts(export)]
196#[serde(tag = "type", rename_all = "camelCase")]
197pub struct TagEngineInfo {
198 pub id: uuid::Uuid,
200 pub sketch: uuid::Uuid,
202 pub path: Option<Path>,
204 pub surface: Option<ExtrudeSurface>,
206}
207
208#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
209pub enum BodyType {
210 Root,
211 Block,
212}
213
214#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
216#[ts(export)]
217#[serde(rename_all = "camelCase")]
218pub struct Metadata {
219 pub source_range: SourceRange,
221}
222
223impl From<Metadata> for Vec<SourceRange> {
224 fn from(meta: Metadata) -> Self {
225 vec![meta.source_range]
226 }
227}
228
229impl From<SourceRange> for Metadata {
230 fn from(source_range: SourceRange) -> Self {
231 Self { source_range }
232 }
233}
234
235impl<T> From<NodeRef<'_, T>> for Metadata {
236 fn from(node: NodeRef<'_, T>) -> Self {
237 Self {
238 source_range: SourceRange::new(node.start, node.end, node.module_id),
239 }
240 }
241}
242
243impl From<&Expr> for Metadata {
244 fn from(expr: &Expr) -> Self {
245 Self {
246 source_range: SourceRange::from(expr),
247 }
248 }
249}
250
251#[derive(PartialEq, Debug, Default, Clone)]
253pub enum ContextType {
254 #[default]
256 Live,
257
258 Mock,
262
263 MockCustomForwarded,
265}
266
267#[derive(Debug, Clone)]
271pub struct ExecutorContext {
272 pub engine: Arc<Box<dyn EngineManager>>,
273 pub fs: Arc<FileManager>,
274 pub settings: ExecutorSettings,
275 pub context_type: ContextType,
276}
277
278#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
280#[ts(export)]
281pub struct ExecutorSettings {
282 pub highlight_edges: bool,
284 pub enable_ssao: bool,
286 pub show_grid: bool,
288 pub replay: Option<String>,
291 pub project_directory: Option<TypedPath>,
294 pub current_file: Option<TypedPath>,
297}
298
299impl Default for ExecutorSettings {
300 fn default() -> Self {
301 Self {
302 highlight_edges: true,
303 enable_ssao: false,
304 show_grid: false,
305 replay: None,
306 project_directory: None,
307 current_file: None,
308 }
309 }
310}
311
312impl From<crate::settings::types::Configuration> for ExecutorSettings {
313 fn from(config: crate::settings::types::Configuration) -> Self {
314 Self {
315 highlight_edges: config.settings.modeling.highlight_edges.into(),
316 enable_ssao: config.settings.modeling.enable_ssao.into(),
317 show_grid: config.settings.modeling.show_scale_grid,
318 replay: None,
319 project_directory: None,
320 current_file: None,
321 }
322 }
323}
324
325impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
326 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
327 Self {
328 highlight_edges: config.settings.modeling.highlight_edges.into(),
329 enable_ssao: config.settings.modeling.enable_ssao.into(),
330 show_grid: Default::default(),
331 replay: None,
332 project_directory: None,
333 current_file: None,
334 }
335 }
336}
337
338impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
339 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
340 Self {
341 highlight_edges: modeling.highlight_edges.into(),
342 enable_ssao: modeling.enable_ssao.into(),
343 show_grid: modeling.show_scale_grid,
344 replay: None,
345 project_directory: None,
346 current_file: None,
347 }
348 }
349}
350
351impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
352 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
353 Self {
354 highlight_edges: modeling.highlight_edges.into(),
355 enable_ssao: modeling.enable_ssao.into(),
356 show_grid: Default::default(),
357 replay: None,
358 project_directory: None,
359 current_file: None,
360 }
361 }
362}
363
364impl ExecutorSettings {
365 pub fn with_current_file(&mut self, current_file: TypedPath) {
367 if current_file.extension() == Some("kcl") {
369 self.current_file = Some(current_file.clone());
370 if let Some(parent) = current_file.parent() {
372 self.project_directory = Some(parent);
373 } else {
374 self.project_directory = Some(TypedPath::from(""));
375 }
376 } else {
377 self.project_directory = Some(current_file.clone());
378 }
379 }
380}
381
382impl ExecutorContext {
383 #[cfg(not(target_arch = "wasm32"))]
385 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
386 let (ws, _headers) = client
387 .modeling()
388 .commands_ws(
389 None,
390 None,
391 None,
392 if settings.enable_ssao {
393 Some(kittycad::types::PostEffectType::Ssao)
394 } else {
395 None
396 },
397 settings.replay.clone(),
398 if settings.show_grid { Some(true) } else { None },
399 None,
400 None,
401 None,
402 Some(false),
403 )
404 .await?;
405
406 let engine: Arc<Box<dyn EngineManager>> =
407 Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
408
409 Ok(Self {
410 engine,
411 fs: Arc::new(FileManager::new()),
412 settings,
413 context_type: ContextType::Live,
414 })
415 }
416
417 #[cfg(target_arch = "wasm32")]
418 pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
419 ExecutorContext {
420 engine,
421 fs,
422 settings,
423 context_type: ContextType::Live,
424 }
425 }
426
427 #[cfg(not(target_arch = "wasm32"))]
428 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
429 ExecutorContext {
430 engine: Arc::new(Box::new(
431 crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
432 )),
433 fs: Arc::new(FileManager::new()),
434 settings: settings.unwrap_or_default(),
435 context_type: ContextType::Mock,
436 }
437 }
438
439 #[cfg(target_arch = "wasm32")]
440 pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
441 ExecutorContext {
442 engine,
443 fs,
444 settings,
445 context_type: ContextType::Mock,
446 }
447 }
448
449 #[cfg(not(target_arch = "wasm32"))]
450 pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
451 ExecutorContext {
452 engine,
453 fs: Arc::new(FileManager::new()),
454 settings: Default::default(),
455 context_type: ContextType::MockCustomForwarded,
456 }
457 }
458
459 #[cfg(not(target_arch = "wasm32"))]
465 pub async fn new_with_client(
466 settings: ExecutorSettings,
467 token: Option<String>,
468 engine_addr: Option<String>,
469 ) -> Result<Self> {
470 let client = crate::engine::new_zoo_client(token, engine_addr)?;
472
473 let ctx = Self::new(&client, settings).await?;
474 Ok(ctx)
475 }
476
477 #[cfg(not(target_arch = "wasm32"))]
482 pub async fn new_with_default_client() -> Result<Self> {
483 let ctx = Self::new_with_client(Default::default(), None, None).await?;
485 Ok(ctx)
486 }
487
488 #[cfg(not(target_arch = "wasm32"))]
490 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
491 let ctx = ExecutorContext::new_with_client(
492 ExecutorSettings {
493 highlight_edges: true,
494 enable_ssao: false,
495 show_grid: false,
496 replay: None,
497 project_directory: None,
498 current_file: None,
499 },
500 None,
501 engine_addr,
502 )
503 .await?;
504 Ok(ctx)
505 }
506
507 pub fn is_mock(&self) -> bool {
508 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
509 }
510
511 pub async fn no_engine_commands(&self) -> bool {
513 self.is_mock()
514 }
515
516 pub async fn send_clear_scene(
517 &self,
518 exec_state: &mut ExecState,
519 source_range: crate::execution::SourceRange,
520 ) -> Result<(), KclError> {
521 self.engine
522 .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
523 .await
524 }
525
526 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
527 cache::bust_cache().await;
528
529 let outcome = self.run_with_caching(crate::Program::empty()).await?;
534
535 Ok(outcome)
536 }
537
538 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
539 self.eval_prelude(exec_state, SourceRange::synthetic())
540 .await
541 .map_err(KclErrorWithOutputs::no_outputs)?;
542 exec_state.mut_stack().push_new_root_env(true);
543 Ok(())
544 }
545
546 pub async fn run_mock(
547 &self,
548 program: crate::Program,
549 use_prev_memory: bool,
550 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
551 assert!(self.is_mock());
552
553 let mut exec_state = ExecState::new(self);
554 if use_prev_memory {
555 match cache::read_old_memory().await {
556 Some(mem) => {
557 *exec_state.mut_stack() = mem.0;
558 exec_state.global.module_infos = mem.1;
559 }
560 None => self.prepare_mem(&mut exec_state).await?,
561 }
562 } else {
563 self.prepare_mem(&mut exec_state).await?
564 };
565
566 exec_state.mut_stack().push_new_env_for_scope();
569
570 let result = self.inner_run(&program, &mut exec_state, true).await?;
571
572 let mut mem = exec_state.stack().clone();
577 let module_infos = exec_state.global.module_infos.clone();
578 let outcome = exec_state.to_mock_exec_outcome(result.0, self).await;
579
580 mem.squash_env(result.0);
581 cache::write_old_memory((mem, module_infos)).await;
582
583 Ok(outcome)
584 }
585
586 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
587 assert!(!self.is_mock());
588
589 let (program, exec_state, result) = match cache::read_old_ast().await {
590 Some(mut cached_state) => {
591 let old = CacheInformation {
592 ast: &cached_state.main.ast,
593 settings: &cached_state.settings,
594 };
595 let new = CacheInformation {
596 ast: &program.ast,
597 settings: &self.settings,
598 };
599
600 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
602 CacheResult::ReExecute {
603 clear_scene,
604 reapply_settings,
605 program: changed_program,
606 } => {
607 if reapply_settings
608 && self
609 .engine
610 .reapply_settings(
611 &self.settings,
612 Default::default(),
613 &mut cached_state.main.exec_state.id_generator,
614 )
615 .await
616 .is_err()
617 {
618 (true, program, None)
619 } else {
620 (
621 clear_scene,
622 crate::Program {
623 ast: changed_program,
624 original_file_contents: program.original_file_contents,
625 },
626 None,
627 )
628 }
629 }
630 CacheResult::CheckImportsOnly {
631 reapply_settings,
632 ast: changed_program,
633 } => {
634 if reapply_settings
635 && self
636 .engine
637 .reapply_settings(
638 &self.settings,
639 Default::default(),
640 &mut cached_state.main.exec_state.id_generator,
641 )
642 .await
643 .is_err()
644 {
645 (true, program, None)
646 } else {
647 let mut new_exec_state = ExecState::new(self);
649 let (new_universe, new_universe_map) =
650 self.get_universe(&program, &mut new_exec_state).await?;
651
652 let clear_scene = new_universe.keys().any(|key| {
653 let id = new_universe[key].1;
654 match (
655 cached_state.exec_state.get_source(id),
656 new_exec_state.global.get_source(id),
657 ) {
658 (Some(s0), Some(s1)) => s0.source != s1.source,
659 _ => false,
660 }
661 });
662
663 if !clear_scene {
664 return Ok(cached_state.into_exec_outcome(self).await);
666 }
667
668 (
669 true,
670 crate::Program {
671 ast: changed_program,
672 original_file_contents: program.original_file_contents,
673 },
674 Some((new_universe, new_universe_map, new_exec_state)),
675 )
676 }
677 }
678 CacheResult::NoAction(true) => {
679 if self
680 .engine
681 .reapply_settings(
682 &self.settings,
683 Default::default(),
684 &mut cached_state.main.exec_state.id_generator,
685 )
686 .await
687 .is_ok()
688 {
689 cache::write_old_ast(GlobalState::with_settings(
691 cached_state.clone(),
692 self.settings.clone(),
693 ))
694 .await;
695
696 return Ok(cached_state.into_exec_outcome(self).await);
697 }
698 (true, program, None)
699 }
700 CacheResult::NoAction(false) => {
701 return Ok(cached_state.into_exec_outcome(self).await);
702 }
703 };
704
705 let (exec_state, result) = match import_check_info {
706 Some((new_universe, new_universe_map, mut new_exec_state)) => {
707 self.send_clear_scene(&mut new_exec_state, Default::default())
709 .await
710 .map_err(KclErrorWithOutputs::no_outputs)?;
711
712 let result = self
713 .run_concurrent(
714 &program,
715 &mut new_exec_state,
716 Some((new_universe, new_universe_map)),
717 false,
718 )
719 .await;
720
721 (new_exec_state, result)
722 }
723 None if clear_scene => {
724 let mut exec_state = cached_state.reconstitute_exec_state();
726 exec_state.reset(self);
727
728 self.send_clear_scene(&mut exec_state, Default::default())
729 .await
730 .map_err(KclErrorWithOutputs::no_outputs)?;
731
732 let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
733
734 (exec_state, result)
735 }
736 None => {
737 let mut exec_state = cached_state.reconstitute_exec_state();
738 exec_state.mut_stack().restore_env(cached_state.main.result_env);
739
740 let result = self.run_concurrent(&program, &mut exec_state, None, true).await;
741
742 (exec_state, result)
743 }
744 };
745
746 (program, exec_state, result)
747 }
748 None => {
749 let mut exec_state = ExecState::new(self);
750 self.send_clear_scene(&mut exec_state, Default::default())
751 .await
752 .map_err(KclErrorWithOutputs::no_outputs)?;
753
754 let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
755
756 (program, exec_state, result)
757 }
758 };
759
760 if result.is_err() {
761 cache::bust_cache().await;
762 }
763
764 let result = result?;
766
767 cache::write_old_ast(GlobalState::new(
769 exec_state.clone(),
770 self.settings.clone(),
771 program.ast,
772 result.0,
773 ))
774 .await;
775
776 let outcome = exec_state.to_exec_outcome(result.0, self).await;
777 Ok(outcome)
778 }
779
780 pub async fn run(
787 &self,
788 program: &crate::Program,
789 exec_state: &mut ExecState,
790 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
791 self.run_concurrent(program, exec_state, None, false).await
792 }
793
794 pub async fn run_concurrent(
802 &self,
803 program: &crate::Program,
804 exec_state: &mut ExecState,
805 universe_info: Option<(Universe, UniverseMap)>,
806 preserve_mem: bool,
807 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
808 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
811 (universe, universe_map)
812 } else {
813 self.get_universe(program, exec_state).await?
814 };
815
816 let default_planes = self.engine.get_default_planes().read().await.clone();
817
818 self.eval_prelude(exec_state, SourceRange::synthetic())
820 .await
821 .map_err(KclErrorWithOutputs::no_outputs)?;
822
823 for modules in import_graph::import_graph(&universe, self)
824 .map_err(|err| exec_state.error_with_outputs(err, default_planes.clone()))?
825 .into_iter()
826 {
827 #[cfg(not(target_arch = "wasm32"))]
828 let mut set = tokio::task::JoinSet::new();
829
830 #[allow(clippy::type_complexity)]
831 let (results_tx, mut results_rx): (
832 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
833 tokio::sync::mpsc::Receiver<_>,
834 ) = tokio::sync::mpsc::channel(1);
835
836 for module in modules {
837 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
838 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
839 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
840 )));
841 };
842 let module_id = *module_id;
843 let module_path = module_path.clone();
844 let source_range = SourceRange::from(import_stmt);
845
846 self.add_import_module_ops(
847 exec_state,
848 program,
849 module_id,
850 &module_path,
851 source_range,
852 &universe_map,
853 );
854
855 let repr = repr.clone();
856 let exec_state = exec_state.clone();
857 let exec_ctxt = self.clone();
858 let results_tx = results_tx.clone();
859
860 let exec_module = async |exec_ctxt: &ExecutorContext,
861 repr: &ModuleRepr,
862 module_id: ModuleId,
863 module_path: &ModulePath,
864 exec_state: &mut ExecState,
865 source_range: SourceRange|
866 -> Result<ModuleRepr, KclError> {
867 match repr {
868 ModuleRepr::Kcl(program, _) => {
869 let result = exec_ctxt
870 .exec_module_from_ast(program, module_id, module_path, exec_state, source_range, false)
871 .await;
872
873 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
874 }
875 ModuleRepr::Foreign(geom, _) => {
876 let result = crate::execution::import::send_to_engine(geom.clone(), exec_ctxt)
877 .await
878 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
879
880 result.map(|val| ModuleRepr::Foreign(geom.clone(), val))
881 }
882 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
883 format!("Module {module_path} not found in universe"),
884 vec![source_range],
885 ))),
886 }
887 };
888
889 #[cfg(target_arch = "wasm32")]
890 {
891 wasm_bindgen_futures::spawn_local(async move {
892 let mut exec_state = exec_state;
893 let exec_ctxt = exec_ctxt;
894
895 let result = exec_module(
896 &exec_ctxt,
897 &repr,
898 module_id,
899 &module_path,
900 &mut exec_state,
901 source_range,
902 )
903 .await;
904
905 results_tx
906 .send((module_id, module_path, result))
907 .await
908 .unwrap_or_default();
909 });
910 }
911 #[cfg(not(target_arch = "wasm32"))]
912 {
913 set.spawn(async move {
914 let mut exec_state = exec_state;
915 let exec_ctxt = exec_ctxt;
916
917 let result = exec_module(
918 &exec_ctxt,
919 &repr,
920 module_id,
921 &module_path,
922 &mut exec_state,
923 source_range,
924 )
925 .await;
926
927 results_tx
928 .send((module_id, module_path, result))
929 .await
930 .unwrap_or_default();
931 });
932 }
933 }
934
935 drop(results_tx);
936
937 while let Some((module_id, _, result)) = results_rx.recv().await {
938 match result {
939 Ok(new_repr) => {
940 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
941
942 match &mut repr {
943 ModuleRepr::Kcl(_, cache) => {
944 let ModuleRepr::Kcl(_, session_data) = new_repr else {
945 unreachable!();
946 };
947 *cache = session_data;
948 }
949 ModuleRepr::Foreign(_, cache) => {
950 let ModuleRepr::Foreign(_, session_data) = new_repr else {
951 unreachable!();
952 };
953 *cache = session_data;
954 }
955 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
956 }
957
958 exec_state.global.module_infos[&module_id].restore_repr(repr);
959 }
960 Err(e) => {
961 return Err(exec_state.error_with_outputs(e, default_planes));
962 }
963 }
964 }
965 }
966
967 self.inner_run(program, exec_state, preserve_mem).await
968 }
969
970 async fn get_universe(
973 &self,
974 program: &crate::Program,
975 exec_state: &mut ExecState,
976 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
977 exec_state.add_root_module_contents(program);
978
979 let mut universe = std::collections::HashMap::new();
980
981 let default_planes = self.engine.get_default_planes().read().await.clone();
982
983 let root_imports = import_graph::import_universe(
984 self,
985 &ModulePath::Main,
986 &ModuleRepr::Kcl(program.ast.clone(), None),
987 &mut universe,
988 exec_state,
989 )
990 .await
991 .map_err(|err| exec_state.error_with_outputs(err, default_planes))?;
992
993 Ok((universe, root_imports))
994 }
995
996 #[cfg(feature = "artifact-graph")]
997 fn add_import_module_ops(
998 &self,
999 exec_state: &mut ExecState,
1000 program: &crate::Program,
1001 module_id: ModuleId,
1002 module_path: &ModulePath,
1003 source_range: SourceRange,
1004 universe_map: &UniverseMap,
1005 ) {
1006 match module_path {
1007 ModulePath::Main => {
1008 }
1010 ModulePath::Local { value, .. } => {
1011 if universe_map.contains_key(value) {
1014 use crate::NodePath;
1015
1016 let node_path = if source_range.is_top_level_module() {
1017 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1018 NodePath::from_range(&program.ast, cached_body_items, source_range).unwrap_or_default()
1019 } else {
1020 NodePath::placeholder()
1023 };
1024
1025 exec_state.push_op(Operation::GroupBegin {
1026 group: Group::ModuleInstance {
1027 name: value.file_name().unwrap_or_default(),
1028 module_id,
1029 },
1030 node_path,
1031 source_range,
1032 });
1033 exec_state.push_op(Operation::GroupEnd);
1037 }
1038 }
1039 ModulePath::Std { .. } => {
1040 }
1042 }
1043 }
1044
1045 #[cfg(not(feature = "artifact-graph"))]
1046 fn add_import_module_ops(
1047 &self,
1048 _exec_state: &mut ExecState,
1049 _program: &crate::Program,
1050 _module_id: ModuleId,
1051 _module_path: &ModulePath,
1052 _source_range: SourceRange,
1053 _universe_map: &UniverseMap,
1054 ) {
1055 }
1056
1057 async fn inner_run(
1060 &self,
1061 program: &crate::Program,
1062 exec_state: &mut ExecState,
1063 preserve_mem: bool,
1064 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1065 let _stats = crate::log::LogPerfStats::new("Interpretation");
1066
1067 self.engine
1069 .reapply_settings(&self.settings, Default::default(), exec_state.id_generator())
1070 .await
1071 .map_err(KclErrorWithOutputs::no_outputs)?;
1072
1073 let default_planes = self.engine.get_default_planes().read().await.clone();
1074 let result = self
1075 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1076 .await;
1077
1078 crate::log::log(format!(
1079 "Post interpretation KCL memory stats: {:#?}",
1080 exec_state.stack().memory.stats
1081 ));
1082 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1083
1084 let env_ref = result.map_err(|e| exec_state.error_with_outputs(e, default_planes))?;
1085
1086 if !self.is_mock() {
1087 let mut mem = exec_state.stack().deep_clone();
1088 mem.restore_env(env_ref);
1089 cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
1090 }
1091 let session_data = self.engine.get_session_data().await;
1092
1093 Ok((env_ref, session_data))
1094 }
1095
1096 async fn execute_and_build_graph(
1099 &self,
1100 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1101 exec_state: &mut ExecState,
1102 preserve_mem: bool,
1103 ) -> Result<EnvironmentRef, KclError> {
1104 #[cfg(feature = "artifact-graph")]
1110 let start_op = exec_state.global.artifacts.operations.len();
1111
1112 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1113 .await?;
1114
1115 let exec_result = self
1116 .exec_module_body(
1117 program,
1118 exec_state,
1119 preserve_mem,
1120 ModuleId::default(),
1121 &ModulePath::Main,
1122 )
1123 .await;
1124
1125 #[cfg(feature = "artifact-graph")]
1126 {
1127 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1129 for op in exec_state.global.artifacts.operations.iter_mut().skip(start_op) {
1130 match op {
1131 Operation::StdLibCall {
1132 node_path,
1133 source_range,
1134 ..
1135 }
1136 | Operation::GroupBegin {
1137 node_path,
1138 source_range,
1139 ..
1140 } => {
1141 node_path.fill_placeholder(program, cached_body_items, *source_range);
1142 }
1143 Operation::GroupEnd => {}
1144 }
1145 }
1146 }
1147
1148 self.engine.ensure_async_commands_completed().await?;
1150
1151 self.engine.clear_queues().await;
1154
1155 match exec_state.build_artifact_graph(&self.engine, program).await {
1156 Ok(_) => exec_result.map(|(_, env_ref, _)| env_ref),
1157 Err(err) => exec_result.and(Err(err)),
1158 }
1159 }
1160
1161 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1165 if exec_state.stack().memory.requires_std() {
1166 let path = vec!["std".to_owned(), "prelude".to_owned()];
1167 let resolved_path = ModulePath::from_std_import_path(&path)?;
1168 let id = self
1169 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1170 .await?;
1171 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1172
1173 exec_state.mut_stack().memory.set_std(module_memory);
1174 }
1175
1176 Ok(())
1177 }
1178
1179 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1181 self.engine
1183 .send_modeling_cmd(
1184 uuid::Uuid::new_v4(),
1185 crate::execution::SourceRange::default(),
1186 &ModelingCmd::from(mcmd::ZoomToFit {
1187 object_ids: Default::default(),
1188 animated: false,
1189 padding: 0.1,
1190 }),
1191 )
1192 .await
1193 .map_err(KclErrorWithOutputs::no_outputs)?;
1194
1195 let resp = self
1197 .engine
1198 .send_modeling_cmd(
1199 uuid::Uuid::new_v4(),
1200 crate::execution::SourceRange::default(),
1201 &ModelingCmd::from(mcmd::TakeSnapshot {
1202 format: ImageFormat::Png,
1203 }),
1204 )
1205 .await
1206 .map_err(KclErrorWithOutputs::no_outputs)?;
1207
1208 let OkWebSocketResponseData::Modeling {
1209 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1210 } = resp
1211 else {
1212 return Err(ExecError::BadPng(format!(
1213 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1214 )));
1215 };
1216 Ok(contents)
1217 }
1218
1219 pub async fn export(
1221 &self,
1222 format: kittycad_modeling_cmds::format::OutputFormat3d,
1223 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1224 let resp = self
1225 .engine
1226 .send_modeling_cmd(
1227 uuid::Uuid::new_v4(),
1228 crate::SourceRange::default(),
1229 &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
1230 entity_ids: vec![],
1231 format,
1232 }),
1233 )
1234 .await?;
1235
1236 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1237 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1238 format!("Expected Export response, got {resp:?}",),
1239 vec![SourceRange::default()],
1240 )));
1241 };
1242
1243 Ok(files)
1244 }
1245
1246 pub async fn export_step(
1248 &self,
1249 deterministic_time: bool,
1250 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1251 let files = self
1252 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1253 kittycad_modeling_cmds::format::step::export::Options {
1254 coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1255 created: if deterministic_time {
1256 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1257 KclError::new_internal(crate::errors::KclErrorDetails::new(
1258 format!("Failed to parse date: {}", e),
1259 vec![SourceRange::default()],
1260 ))
1261 })?)
1262 } else {
1263 None
1264 },
1265 },
1266 ))
1267 .await?;
1268
1269 Ok(files)
1270 }
1271
1272 pub async fn close(&self) {
1273 self.engine.close().await;
1274 }
1275}
1276
1277#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS, JsonSchema)]
1278pub struct ArtifactId(Uuid);
1279
1280impl ArtifactId {
1281 pub fn new(uuid: Uuid) -> Self {
1282 Self(uuid)
1283 }
1284}
1285
1286impl From<Uuid> for ArtifactId {
1287 fn from(uuid: Uuid) -> Self {
1288 Self::new(uuid)
1289 }
1290}
1291
1292impl From<&Uuid> for ArtifactId {
1293 fn from(uuid: &Uuid) -> Self {
1294 Self::new(*uuid)
1295 }
1296}
1297
1298impl From<ArtifactId> for Uuid {
1299 fn from(id: ArtifactId) -> Self {
1300 id.0
1301 }
1302}
1303
1304impl From<&ArtifactId> for Uuid {
1305 fn from(id: &ArtifactId) -> Self {
1306 id.0
1307 }
1308}
1309
1310impl From<ModelingCmdId> for ArtifactId {
1311 fn from(id: ModelingCmdId) -> Self {
1312 Self::new(*id.as_ref())
1313 }
1314}
1315
1316impl From<&ModelingCmdId> for ArtifactId {
1317 fn from(id: &ModelingCmdId) -> Self {
1318 Self::new(*id.as_ref())
1319 }
1320}
1321
1322#[cfg(test)]
1323pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1324 parse_execute_with_project_dir(code, None).await
1325}
1326
1327#[cfg(test)]
1328pub(crate) async fn parse_execute_with_project_dir(
1329 code: &str,
1330 project_directory: Option<TypedPath>,
1331) -> Result<ExecTestResults, KclError> {
1332 let program = crate::Program::parse_no_errs(code)?;
1333
1334 let exec_ctxt = ExecutorContext {
1335 engine: Arc::new(Box::new(
1336 crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
1337 KclError::new_internal(crate::errors::KclErrorDetails::new(
1338 format!("Failed to create mock engine connection: {}", err),
1339 vec![SourceRange::default()],
1340 ))
1341 })?,
1342 )),
1343 fs: Arc::new(crate::fs::FileManager::new()),
1344 settings: ExecutorSettings {
1345 project_directory,
1346 ..Default::default()
1347 },
1348 context_type: ContextType::Mock,
1349 };
1350 let mut exec_state = ExecState::new(&exec_ctxt);
1351 let result = exec_ctxt.run(&program, &mut exec_state).await?;
1352
1353 Ok(ExecTestResults {
1354 program,
1355 mem_env: result.0,
1356 exec_ctxt,
1357 exec_state,
1358 })
1359}
1360
1361#[cfg(test)]
1362#[derive(Debug)]
1363pub(crate) struct ExecTestResults {
1364 program: crate::Program,
1365 mem_env: EnvironmentRef,
1366 exec_ctxt: ExecutorContext,
1367 exec_state: ExecState,
1368}
1369
1370#[cfg(test)]
1371mod tests {
1372 use pretty_assertions::assert_eq;
1373
1374 use super::*;
1375 use crate::{errors::KclErrorDetails, execution::memory::Stack, ModuleId};
1376
1377 #[track_caller]
1379 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1380 memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1381 }
1382
1383 #[tokio::test(flavor = "multi_thread")]
1384 async fn test_execute_warn() {
1385 let text = "@blah";
1386 let result = parse_execute(text).await.unwrap();
1387 let errs = result.exec_state.errors();
1388 assert_eq!(errs.len(), 1);
1389 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1390 assert!(
1391 errs[0].message.contains("Unknown annotation"),
1392 "unexpected warning message: {}",
1393 errs[0].message
1394 );
1395 }
1396
1397 #[tokio::test(flavor = "multi_thread")]
1398 async fn test_execute_fn_definitions() {
1399 let ast = r#"fn def(@x) {
1400 return x
1401}
1402fn ghi(@x) {
1403 return x
1404}
1405fn jkl(@x) {
1406 return x
1407}
1408fn hmm(@x) {
1409 return x
1410}
1411
1412yo = 5 + 6
1413
1414abc = 3
1415identifierGuy = 5
1416part001 = startSketchOn(XY)
1417|> startProfile(at = [-1.2, 4.83])
1418|> line(end = [2.8, 0])
1419|> angledLine(angle = 100 + 100, length = 3.01)
1420|> angledLine(angle = abc, length = 3.02)
1421|> angledLine(angle = def(yo), length = 3.03)
1422|> angledLine(angle = ghi(2), length = 3.04)
1423|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1424|> close()
1425yo2 = hmm([identifierGuy + 5])"#;
1426
1427 parse_execute(ast).await.unwrap();
1428 }
1429
1430 #[tokio::test(flavor = "multi_thread")]
1431 async fn test_execute_with_pipe_substitutions_unary() {
1432 let ast = r#"myVar = 3
1433part001 = startSketchOn(XY)
1434 |> startProfile(at = [0, 0])
1435 |> line(end = [3, 4], tag = $seg01)
1436 |> line(end = [
1437 min([segLen(seg01), myVar]),
1438 -legLen(hypotenuse = segLen(seg01), leg = myVar)
1439])
1440"#;
1441
1442 parse_execute(ast).await.unwrap();
1443 }
1444
1445 #[tokio::test(flavor = "multi_thread")]
1446 async fn test_execute_with_pipe_substitutions() {
1447 let ast = r#"myVar = 3
1448part001 = startSketchOn(XY)
1449 |> startProfile(at = [0, 0])
1450 |> line(end = [3, 4], tag = $seg01)
1451 |> line(end = [
1452 min([segLen(seg01), myVar]),
1453 legLen(hypotenuse = segLen(seg01), leg = myVar)
1454])
1455"#;
1456
1457 parse_execute(ast).await.unwrap();
1458 }
1459
1460 #[tokio::test(flavor = "multi_thread")]
1461 async fn test_execute_with_inline_comment() {
1462 let ast = r#"baseThick = 1
1463armAngle = 60
1464
1465baseThickHalf = baseThick / 2
1466halfArmAngle = armAngle / 2
1467
1468arrExpShouldNotBeIncluded = [1, 2, 3]
1469objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1470
1471part001 = startSketchOn(XY)
1472 |> startProfile(at = [0, 0])
1473 |> yLine(endAbsolute = 1)
1474 |> xLine(length = 3.84) // selection-range-7ish-before-this
1475
1476variableBelowShouldNotBeIncluded = 3
1477"#;
1478
1479 parse_execute(ast).await.unwrap();
1480 }
1481
1482 #[tokio::test(flavor = "multi_thread")]
1483 async fn test_execute_with_function_literal_in_pipe() {
1484 let ast = r#"w = 20
1485l = 8
1486h = 10
1487
1488fn thing() {
1489 return -8
1490}
1491
1492firstExtrude = startSketchOn(XY)
1493 |> startProfile(at = [0,0])
1494 |> line(end = [0, l])
1495 |> line(end = [w, 0])
1496 |> line(end = [0, thing()])
1497 |> close()
1498 |> extrude(length = h)"#;
1499
1500 parse_execute(ast).await.unwrap();
1501 }
1502
1503 #[tokio::test(flavor = "multi_thread")]
1504 async fn test_execute_with_function_unary_in_pipe() {
1505 let ast = r#"w = 20
1506l = 8
1507h = 10
1508
1509fn thing(@x) {
1510 return -x
1511}
1512
1513firstExtrude = startSketchOn(XY)
1514 |> startProfile(at = [0,0])
1515 |> line(end = [0, l])
1516 |> line(end = [w, 0])
1517 |> line(end = [0, thing(8)])
1518 |> close()
1519 |> extrude(length = h)"#;
1520
1521 parse_execute(ast).await.unwrap();
1522 }
1523
1524 #[tokio::test(flavor = "multi_thread")]
1525 async fn test_execute_with_function_array_in_pipe() {
1526 let ast = r#"w = 20
1527l = 8
1528h = 10
1529
1530fn thing(@x) {
1531 return [0, -x]
1532}
1533
1534firstExtrude = startSketchOn(XY)
1535 |> startProfile(at = [0,0])
1536 |> line(end = [0, l])
1537 |> line(end = [w, 0])
1538 |> line(end = thing(8))
1539 |> close()
1540 |> extrude(length = h)"#;
1541
1542 parse_execute(ast).await.unwrap();
1543 }
1544
1545 #[tokio::test(flavor = "multi_thread")]
1546 async fn test_execute_with_function_call_in_pipe() {
1547 let ast = r#"w = 20
1548l = 8
1549h = 10
1550
1551fn other_thing(@y) {
1552 return -y
1553}
1554
1555fn thing(@x) {
1556 return other_thing(x)
1557}
1558
1559firstExtrude = startSketchOn(XY)
1560 |> startProfile(at = [0,0])
1561 |> line(end = [0, l])
1562 |> line(end = [w, 0])
1563 |> line(end = [0, thing(8)])
1564 |> close()
1565 |> extrude(length = h)"#;
1566
1567 parse_execute(ast).await.unwrap();
1568 }
1569
1570 #[tokio::test(flavor = "multi_thread")]
1571 async fn test_execute_with_function_sketch() {
1572 let ast = r#"fn box(h, l, w) {
1573 myBox = startSketchOn(XY)
1574 |> startProfile(at = [0,0])
1575 |> line(end = [0, l])
1576 |> line(end = [w, 0])
1577 |> line(end = [0, -l])
1578 |> close()
1579 |> extrude(length = h)
1580
1581 return myBox
1582}
1583
1584fnBox = box(h = 3, l = 6, w = 10)"#;
1585
1586 parse_execute(ast).await.unwrap();
1587 }
1588
1589 #[tokio::test(flavor = "multi_thread")]
1590 async fn test_get_member_of_object_with_function_period() {
1591 let ast = r#"fn box(@obj) {
1592 myBox = startSketchOn(XY)
1593 |> startProfile(at = obj.start)
1594 |> line(end = [0, obj.l])
1595 |> line(end = [obj.w, 0])
1596 |> line(end = [0, -obj.l])
1597 |> close()
1598 |> extrude(length = obj.h)
1599
1600 return myBox
1601}
1602
1603thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1604"#;
1605 parse_execute(ast).await.unwrap();
1606 }
1607
1608 #[tokio::test(flavor = "multi_thread")]
1609 #[ignore] async fn test_object_member_starting_pipeline() {
1611 let ast = r#"
1612fn test2() {
1613 return {
1614 thing: startSketchOn(XY)
1615 |> startProfile(at = [0, 0])
1616 |> line(end = [0, 1])
1617 |> line(end = [1, 0])
1618 |> line(end = [0, -1])
1619 |> close()
1620 }
1621}
1622
1623x2 = test2()
1624
1625x2.thing
1626 |> extrude(length = 10)
1627"#;
1628 parse_execute(ast).await.unwrap();
1629 }
1630
1631 #[tokio::test(flavor = "multi_thread")]
1632 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
1634 let ast = r#"fn box(obj) {
1635let myBox = startSketchOn(XY)
1636 |> startProfile(at = obj.start)
1637 |> line(end = [0, obj.l])
1638 |> line(end = [obj.w, 0])
1639 |> line(end = [0, -obj.l])
1640 |> close()
1641 |> extrude(length = obj.h)
1642
1643 return myBox
1644}
1645
1646for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1647 thisBox = box(var)
1648}"#;
1649
1650 parse_execute(ast).await.unwrap();
1651 }
1652
1653 #[tokio::test(flavor = "multi_thread")]
1654 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
1656 let ast = r#"fn box(h, l, w, start) {
1657 myBox = startSketchOn(XY)
1658 |> startProfile(at = [0,0])
1659 |> line(end = [0, l])
1660 |> line(end = [w, 0])
1661 |> line(end = [0, -l])
1662 |> close()
1663 |> extrude(length = h)
1664
1665 return myBox
1666}
1667
1668
1669for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1670 const thisBox = box(var[0], var[1], var[2], var[3])
1671}"#;
1672
1673 parse_execute(ast).await.unwrap();
1674 }
1675
1676 #[tokio::test(flavor = "multi_thread")]
1677 async fn test_get_member_of_array_with_function() {
1678 let ast = r#"fn box(@arr) {
1679 myBox =startSketchOn(XY)
1680 |> startProfile(at = arr[0])
1681 |> line(end = [0, arr[1]])
1682 |> line(end = [arr[2], 0])
1683 |> line(end = [0, -arr[1]])
1684 |> close()
1685 |> extrude(length = arr[3])
1686
1687 return myBox
1688}
1689
1690thisBox = box([[0,0], 6, 10, 3])
1691
1692"#;
1693 parse_execute(ast).await.unwrap();
1694 }
1695
1696 #[tokio::test(flavor = "multi_thread")]
1697 async fn test_function_cannot_access_future_definitions() {
1698 let ast = r#"
1699fn returnX() {
1700 // x shouldn't be defined yet.
1701 return x
1702}
1703
1704x = 5
1705
1706answer = returnX()"#;
1707
1708 let result = parse_execute(ast).await;
1709 let err = result.unwrap_err();
1710 assert_eq!(err.message(), "`x` is not defined");
1711 }
1712
1713 #[tokio::test(flavor = "multi_thread")]
1714 async fn test_override_prelude() {
1715 let text = "PI = 3.0";
1716 let result = parse_execute(text).await.unwrap();
1717 let errs = result.exec_state.errors();
1718 assert!(errs.is_empty());
1719 }
1720
1721 #[tokio::test(flavor = "multi_thread")]
1722 async fn type_aliases() {
1723 let text = r#"type MyTy = [number; 2]
1724fn foo(@x: MyTy) {
1725 return x[0]
1726}
1727
1728foo([0, 1])
1729
1730type Other = MyTy | Helix
1731"#;
1732 let result = parse_execute(text).await.unwrap();
1733 let errs = result.exec_state.errors();
1734 assert!(errs.is_empty());
1735 }
1736
1737 #[tokio::test(flavor = "multi_thread")]
1738 async fn test_cannot_shebang_in_fn() {
1739 let ast = r#"
1740fn foo() {
1741 #!hello
1742 return true
1743}
1744
1745foo
1746"#;
1747
1748 let result = parse_execute(ast).await;
1749 let err = result.unwrap_err();
1750 assert_eq!(
1751 err,
1752 KclError::new_syntax(KclErrorDetails::new(
1753 "Unexpected token: #".to_owned(),
1754 vec![SourceRange::new(14, 15, ModuleId::default())],
1755 )),
1756 );
1757 }
1758
1759 #[tokio::test(flavor = "multi_thread")]
1760 async fn test_pattern_transform_function_cannot_access_future_definitions() {
1761 let ast = r#"
1762fn transform(@replicaId) {
1763 // x shouldn't be defined yet.
1764 scale = x
1765 return {
1766 translate = [0, 0, replicaId * 10],
1767 scale = [scale, 1, 0],
1768 }
1769}
1770
1771fn layer() {
1772 return startSketchOn(XY)
1773 |> circle( center= [0, 0], radius= 1, tag = $tag1)
1774 |> extrude(length = 10)
1775}
1776
1777x = 5
1778
1779// The 10 layers are replicas of each other, with a transform applied to each.
1780shape = layer() |> patternTransform(instances = 10, transform = transform)
1781"#;
1782
1783 let result = parse_execute(ast).await;
1784 let err = result.unwrap_err();
1785 assert_eq!(err.message(), "`x` is not defined",);
1786 }
1787
1788 #[tokio::test(flavor = "multi_thread")]
1791 async fn test_math_execute_with_functions() {
1792 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
1793 let result = parse_execute(ast).await.unwrap();
1794 assert_eq!(
1795 5.0,
1796 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1797 .as_f64()
1798 .unwrap()
1799 );
1800 }
1801
1802 #[tokio::test(flavor = "multi_thread")]
1803 async fn test_math_execute() {
1804 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
1805 let result = parse_execute(ast).await.unwrap();
1806 assert_eq!(
1807 7.4,
1808 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1809 .as_f64()
1810 .unwrap()
1811 );
1812 }
1813
1814 #[tokio::test(flavor = "multi_thread")]
1815 async fn test_math_execute_start_negative() {
1816 let ast = r#"myVar = -5 + 6"#;
1817 let result = parse_execute(ast).await.unwrap();
1818 assert_eq!(
1819 1.0,
1820 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1821 .as_f64()
1822 .unwrap()
1823 );
1824 }
1825
1826 #[tokio::test(flavor = "multi_thread")]
1827 async fn test_math_execute_with_pi() {
1828 let ast = r#"myVar = PI * 2"#;
1829 let result = parse_execute(ast).await.unwrap();
1830 assert_eq!(
1831 std::f64::consts::TAU,
1832 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1833 .as_f64()
1834 .unwrap()
1835 );
1836 }
1837
1838 #[tokio::test(flavor = "multi_thread")]
1839 async fn test_math_define_decimal_without_leading_zero() {
1840 let ast = r#"thing = .4 + 7"#;
1841 let result = parse_execute(ast).await.unwrap();
1842 assert_eq!(
1843 7.4,
1844 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
1845 .as_f64()
1846 .unwrap()
1847 );
1848 }
1849
1850 #[tokio::test(flavor = "multi_thread")]
1851 async fn test_zero_param_fn() {
1852 let ast = r#"sigmaAllow = 35000 // psi
1853leg1 = 5 // inches
1854leg2 = 8 // inches
1855fn thickness() { return 0.56 }
1856
1857bracket = startSketchOn(XY)
1858 |> startProfile(at = [0,0])
1859 |> line(end = [0, leg1])
1860 |> line(end = [leg2, 0])
1861 |> line(end = [0, -thickness()])
1862 |> line(end = [-leg2 + thickness(), 0])
1863"#;
1864 parse_execute(ast).await.unwrap();
1865 }
1866
1867 #[tokio::test(flavor = "multi_thread")]
1868 async fn test_unary_operator_not_succeeds() {
1869 let ast = r#"
1870fn returnTrue() { return !false }
1871t = true
1872f = false
1873notTrue = !t
1874notFalse = !f
1875c = !!true
1876d = !returnTrue()
1877
1878assertIs(!false, error = "expected to pass")
1879
1880fn check(x) {
1881 assertIs(!x, error = "expected argument to be false")
1882 return true
1883}
1884check(x = false)
1885"#;
1886 let result = parse_execute(ast).await.unwrap();
1887 assert_eq!(
1888 false,
1889 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
1890 .as_bool()
1891 .unwrap()
1892 );
1893 assert_eq!(
1894 true,
1895 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
1896 .as_bool()
1897 .unwrap()
1898 );
1899 assert_eq!(
1900 true,
1901 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
1902 .as_bool()
1903 .unwrap()
1904 );
1905 assert_eq!(
1906 false,
1907 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
1908 .as_bool()
1909 .unwrap()
1910 );
1911 }
1912
1913 #[tokio::test(flavor = "multi_thread")]
1914 async fn test_unary_operator_not_on_non_bool_fails() {
1915 let code1 = r#"
1916// Yup, this is null.
1917myNull = 0 / 0
1918notNull = !myNull
1919"#;
1920 assert_eq!(
1921 parse_execute(code1).await.unwrap_err().message(),
1922 "Cannot apply unary operator ! to non-boolean value: number(default units)",
1923 );
1924
1925 let code2 = "notZero = !0";
1926 assert_eq!(
1927 parse_execute(code2).await.unwrap_err().message(),
1928 "Cannot apply unary operator ! to non-boolean value: number(default units)",
1929 );
1930
1931 let code3 = r#"
1932notEmptyString = !""
1933"#;
1934 assert_eq!(
1935 parse_execute(code3).await.unwrap_err().message(),
1936 "Cannot apply unary operator ! to non-boolean value: string",
1937 );
1938
1939 let code4 = r#"
1940obj = { a = 1 }
1941notMember = !obj.a
1942"#;
1943 assert_eq!(
1944 parse_execute(code4).await.unwrap_err().message(),
1945 "Cannot apply unary operator ! to non-boolean value: number(default units)",
1946 );
1947
1948 let code5 = "
1949a = []
1950notArray = !a";
1951 assert_eq!(
1952 parse_execute(code5).await.unwrap_err().message(),
1953 "Cannot apply unary operator ! to non-boolean value: [any; 0]",
1954 );
1955
1956 let code6 = "
1957x = {}
1958notObject = !x";
1959 assert_eq!(
1960 parse_execute(code6).await.unwrap_err().message(),
1961 "Cannot apply unary operator ! to non-boolean value: { }",
1962 );
1963
1964 let code7 = "
1965fn x() { return 1 }
1966notFunction = !x";
1967 let fn_err = parse_execute(code7).await.unwrap_err();
1968 assert!(
1971 fn_err
1972 .message()
1973 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
1974 "Actual error: {:?}",
1975 fn_err
1976 );
1977
1978 let code8 = "
1979myTagDeclarator = $myTag
1980notTagDeclarator = !myTagDeclarator";
1981 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
1982 assert!(
1985 tag_declarator_err
1986 .message()
1987 .starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
1988 "Actual error: {:?}",
1989 tag_declarator_err
1990 );
1991
1992 let code9 = "
1993myTagDeclarator = $myTag
1994notTagIdentifier = !myTag";
1995 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
1996 assert!(
1999 tag_identifier_err
2000 .message()
2001 .starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
2002 "Actual error: {:?}",
2003 tag_identifier_err
2004 );
2005
2006 let code10 = "notPipe = !(1 |> 2)";
2007 assert_eq!(
2008 parse_execute(code10).await.unwrap_err(),
2011 KclError::new_syntax(KclErrorDetails::new(
2012 "Unexpected token: !".to_owned(),
2013 vec![SourceRange::new(10, 11, ModuleId::default())],
2014 ))
2015 );
2016
2017 let code11 = "
2018fn identity(x) { return x }
2019notPipeSub = 1 |> identity(!%))";
2020 assert_eq!(
2021 parse_execute(code11).await.unwrap_err(),
2024 KclError::new_syntax(KclErrorDetails::new(
2025 "There was an unexpected `!`. Try removing it.".to_owned(),
2026 vec![SourceRange::new(56, 57, ModuleId::default())],
2027 ))
2028 );
2029
2030 }
2034
2035 #[tokio::test(flavor = "multi_thread")]
2036 async fn test_math_negative_variable_in_binary_expression() {
2037 let ast = r#"sigmaAllow = 35000 // psi
2038width = 1 // inch
2039
2040p = 150 // lbs
2041distance = 6 // inches
2042FOS = 2
2043
2044leg1 = 5 // inches
2045leg2 = 8 // inches
2046
2047thickness_squared = distance * p * FOS * 6 / sigmaAllow
2048thickness = 0.56 // inches. App does not support square root function yet
2049
2050bracket = startSketchOn(XY)
2051 |> startProfile(at = [0,0])
2052 |> line(end = [0, leg1])
2053 |> line(end = [leg2, 0])
2054 |> line(end = [0, -thickness])
2055 |> line(end = [-leg2 + thickness, 0])
2056"#;
2057 parse_execute(ast).await.unwrap();
2058 }
2059
2060 #[tokio::test(flavor = "multi_thread")]
2061 async fn test_execute_function_no_return() {
2062 let ast = r#"fn test(@origin) {
2063 origin
2064}
2065
2066test([0, 0])
2067"#;
2068 let result = parse_execute(ast).await;
2069 assert!(result.is_err());
2070 assert!(result.unwrap_err().to_string().contains("undefined"),);
2071 }
2072
2073 #[tokio::test(flavor = "multi_thread")]
2074 async fn test_math_doubly_nested_parens() {
2075 let ast = r#"sigmaAllow = 35000 // psi
2076width = 4 // inch
2077p = 150 // Force on shelf - lbs
2078distance = 6 // inches
2079FOS = 2
2080leg1 = 5 // inches
2081leg2 = 8 // inches
2082thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2083thickness = 0.32 // inches. App does not support square root function yet
2084bracket = startSketchOn(XY)
2085 |> startProfile(at = [0,0])
2086 |> line(end = [0, leg1])
2087 |> line(end = [leg2, 0])
2088 |> line(end = [0, -thickness])
2089 |> line(end = [-1 * leg2 + thickness, 0])
2090 |> line(end = [0, -1 * leg1 + thickness])
2091 |> close()
2092 |> extrude(length = width)
2093"#;
2094 parse_execute(ast).await.unwrap();
2095 }
2096
2097 #[tokio::test(flavor = "multi_thread")]
2098 async fn test_math_nested_parens_one_less() {
2099 let ast = r#" sigmaAllow = 35000 // psi
2100width = 4 // inch
2101p = 150 // Force on shelf - lbs
2102distance = 6 // inches
2103FOS = 2
2104leg1 = 5 // inches
2105leg2 = 8 // inches
2106thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2107thickness = 0.32 // inches. App does not support square root function yet
2108bracket = startSketchOn(XY)
2109 |> startProfile(at = [0,0])
2110 |> line(end = [0, leg1])
2111 |> line(end = [leg2, 0])
2112 |> line(end = [0, -thickness])
2113 |> line(end = [-1 * leg2 + thickness, 0])
2114 |> line(end = [0, -1 * leg1 + thickness])
2115 |> close()
2116 |> extrude(length = width)
2117"#;
2118 parse_execute(ast).await.unwrap();
2119 }
2120
2121 #[tokio::test(flavor = "multi_thread")]
2122 async fn test_fn_as_operand() {
2123 let ast = r#"fn f() { return 1 }
2124x = f()
2125y = x + 1
2126z = f() + 1
2127w = f() + f()
2128"#;
2129 parse_execute(ast).await.unwrap();
2130 }
2131
2132 #[tokio::test(flavor = "multi_thread")]
2133 async fn kcl_test_ids_stable_between_executions() {
2134 let code = r#"sketch001 = startSketchOn(XZ)
2135|> startProfile(at = [61.74, 206.13])
2136|> xLine(length = 305.11, tag = $seg01)
2137|> yLine(length = -291.85)
2138|> xLine(length = -segLen(seg01))
2139|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2140|> close()
2141|> extrude(length = 40.14)
2142|> shell(
2143 thickness = 3.14,
2144 faces = [seg01]
2145)
2146"#;
2147
2148 let ctx = crate::test_server::new_context(true, None).await.unwrap();
2149 let old_program = crate::Program::parse_no_errs(code).unwrap();
2150
2151 if let Err(err) = ctx.run_with_caching(old_program).await {
2153 let report = err.into_miette_report_with_outputs(code).unwrap();
2154 let report = miette::Report::new(report);
2155 panic!("Error executing program: {:?}", report);
2156 }
2157
2158 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2160
2161 let code = r#"sketch001 = startSketchOn(XZ)
2162|> startProfile(at = [62.74, 206.13])
2163|> xLine(length = 305.11, tag = $seg01)
2164|> yLine(length = -291.85)
2165|> xLine(length = -segLen(seg01))
2166|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2167|> close()
2168|> extrude(length = 40.14)
2169|> shell(
2170 faces = [seg01],
2171 thickness = 3.14,
2172)
2173"#;
2174
2175 let program = crate::Program::parse_no_errs(code).unwrap();
2177 ctx.run_with_caching(program).await.unwrap();
2179
2180 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2181
2182 assert_eq!(id_generator, new_id_generator);
2183 }
2184
2185 #[tokio::test(flavor = "multi_thread")]
2186 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2187 let code = r#"sketch001 = startSketchOn(XZ)
2188|> startProfile(at = [61.74, 206.13])
2189|> xLine(length = 305.11, tag = $seg01)
2190|> yLine(length = -291.85)
2191|> xLine(length = -segLen(seg01))
2192|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2193|> close()
2194|> extrude(length = 40.14)
2195|> shell(
2196 thickness = 3.14,
2197 faces = [seg01]
2198)
2199"#;
2200
2201 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2202 let old_program = crate::Program::parse_no_errs(code).unwrap();
2203
2204 ctx.run_with_caching(old_program.clone()).await.unwrap();
2206
2207 let settings_state = cache::read_old_ast().await.unwrap().settings;
2208
2209 assert_eq!(settings_state, ctx.settings);
2211
2212 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2214
2215 ctx.run_with_caching(old_program.clone()).await.unwrap();
2217
2218 let settings_state = cache::read_old_ast().await.unwrap().settings;
2219
2220 assert_eq!(settings_state, ctx.settings);
2222
2223 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2225
2226 ctx.run_with_caching(old_program).await.unwrap();
2228
2229 let settings_state = cache::read_old_ast().await.unwrap().settings;
2230
2231 assert_eq!(settings_state, ctx.settings);
2233
2234 ctx.close().await;
2235 }
2236
2237 #[tokio::test(flavor = "multi_thread")]
2238 async fn mock_after_not_mock() {
2239 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2240 let program = crate::Program::parse_no_errs("x = 2").unwrap();
2241 let result = ctx.run_with_caching(program).await.unwrap();
2242 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2243
2244 let ctx2 = ExecutorContext::new_mock(None).await;
2245 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2246 let result = ctx2.run_mock(program2, true).await.unwrap();
2247 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2248
2249 ctx.close().await;
2250 ctx2.close().await;
2251 }
2252
2253 #[tokio::test(flavor = "multi_thread")]
2254 async fn read_tag_version() {
2255 let ast = r#"fn bar(@t) {
2256 return startSketchOn(XY)
2257 |> startProfile(at = [0,0])
2258 |> angledLine(
2259 angle = -60,
2260 length = segLen(t),
2261 )
2262 |> line(end = [0, 0])
2263 |> close()
2264}
2265
2266sketch = startSketchOn(XY)
2267 |> startProfile(at = [0,0])
2268 |> line(end = [0, 10])
2269 |> line(end = [10, 0], tag = $tag0)
2270 |> line(end = [0, 0])
2271
2272fn foo() {
2273 // tag0 tags an edge
2274 return bar(tag0)
2275}
2276
2277solid = sketch |> extrude(length = 10)
2278// tag0 tags a face
2279sketch2 = startSketchOn(solid, face = tag0)
2280 |> startProfile(at = [0,0])
2281 |> line(end = [0, 1])
2282 |> line(end = [1, 0])
2283 |> line(end = [0, 0])
2284
2285foo() |> extrude(length = 1)
2286"#;
2287 parse_execute(ast).await.unwrap();
2288 }
2289}