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