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