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