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