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