1#[cfg(feature = "artifact-graph")]
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7use anyhow::Result;
8#[cfg(feature = "artifact-graph")]
9pub use artifact::Artifact;
10#[cfg(feature = "artifact-graph")]
11pub use artifact::ArtifactCommand;
12#[cfg(feature = "artifact-graph")]
13pub use artifact::ArtifactGraph;
14#[cfg(feature = "artifact-graph")]
15pub use artifact::CapSubType;
16#[cfg(feature = "artifact-graph")]
17pub use artifact::CodeRef;
18#[cfg(feature = "artifact-graph")]
19pub use artifact::SketchBlock;
20#[cfg(feature = "artifact-graph")]
21pub use artifact::SketchBlockConstraint;
22#[cfg(feature = "artifact-graph")]
23pub use artifact::SketchBlockConstraintType;
24#[cfg(feature = "artifact-graph")]
25pub use artifact::StartSketchOnFace;
26#[cfg(feature = "artifact-graph")]
27pub use artifact::StartSketchOnPlane;
28use cache::GlobalState;
29pub use cache::bust_cache;
30pub use cache::clear_mem_cache;
31#[cfg(feature = "artifact-graph")]
32pub use cad_op::Group;
33pub use cad_op::Operation;
34pub use geometry::*;
35pub use id_generator::IdGenerator;
36pub(crate) use import::PreImportedGeometry;
37use indexmap::IndexMap;
38pub use kcl_value::KclObjectFields;
39pub use kcl_value::KclValue;
40use kcmc::ImageFormat;
41use kcmc::ModelingCmd;
42use kcmc::each_cmd as mcmd;
43use kcmc::ok_response::OkModelingCmdResponse;
44use kcmc::ok_response::output::TakeSnapshot;
45use kcmc::websocket::ModelingSessionData;
46use kcmc::websocket::OkWebSocketResponseData;
47use kittycad_modeling_cmds::id::ModelingCmdId;
48use kittycad_modeling_cmds::{self as kcmc};
49pub use memory::EnvironmentRef;
50pub(crate) use modeling::ModelingCmdMeta;
51use serde::Deserialize;
52use serde::Serialize;
53pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
54pub(crate) use sketch_solve::solver_numeric_type;
55pub use sketch_transpiler::pre_execute_transpile;
56pub use sketch_transpiler::transpile_all_old_sketches_to_new;
57pub use sketch_transpiler::transpile_old_sketch_to_new;
58pub use sketch_transpiler::transpile_old_sketch_to_new_ast;
59pub use sketch_transpiler::transpile_old_sketch_to_new_with_execution;
60pub use state::ExecState;
61pub use state::MetaSettings;
62pub(crate) use state::ModuleArtifactState;
63use uuid::Uuid;
64
65use crate::CompilationError;
66use crate::ExecError;
67use crate::KclErrorWithOutputs;
68use crate::SourceRange;
69#[cfg(feature = "artifact-graph")]
70use crate::collections::AhashIndexSet;
71use crate::engine::EngineManager;
72use crate::engine::GridScaleBehavior;
73use crate::errors::KclError;
74use crate::errors::KclErrorDetails;
75use crate::execution::cache::CacheInformation;
76use crate::execution::cache::CacheResult;
77use crate::execution::import_graph::Universe;
78use crate::execution::import_graph::UniverseMap;
79use crate::execution::typed_path::TypedPath;
80#[cfg(feature = "artifact-graph")]
81use crate::front::Number;
82use crate::front::Object;
83use crate::front::ObjectId;
84use crate::fs::FileManager;
85use crate::modules::ModuleExecutionOutcome;
86use crate::modules::ModuleId;
87use crate::modules::ModulePath;
88use crate::modules::ModuleRepr;
89use crate::parsing::ast::types::Expr;
90use crate::parsing::ast::types::ImportPath;
91use crate::parsing::ast::types::NodeRef;
92
93pub(crate) mod annotations;
94#[cfg(feature = "artifact-graph")]
95mod artifact;
96pub(crate) mod cache;
97mod cad_op;
98mod exec_ast;
99pub mod fn_call;
100#[cfg(test)]
101#[cfg(feature = "artifact-graph")]
102mod freedom_analysis_tests;
103mod geometry;
104mod id_generator;
105mod import;
106mod import_graph;
107pub(crate) mod kcl_value;
108mod memory;
109mod modeling;
110mod sketch_solve;
111mod sketch_transpiler;
112mod state;
113pub mod typed_path;
114pub(crate) mod types;
115
116pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
117pub(crate) const SKETCH_OBJECT_META: &str = "meta";
118pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
119
120macro_rules! control_continue {
125 ($control_flow:expr) => {{
126 let cf = $control_flow;
127 if cf.is_some_return() {
128 return Ok(cf);
129 } else {
130 cf.into_value()
131 }
132 }};
133}
134pub(crate) use control_continue;
136
137macro_rules! early_return {
142 ($control_flow:expr) => {{
143 let cf = $control_flow;
144 if cf.is_some_return() {
145 return Err(EarlyReturn::from(cf));
146 } else {
147 cf.into_value()
148 }
149 }};
150}
151pub(crate) use early_return;
153
154#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
155pub enum ControlFlowKind {
156 #[default]
157 Continue,
158 Exit,
159}
160
161impl ControlFlowKind {
162 pub fn is_some_return(&self) -> bool {
164 match self {
165 ControlFlowKind::Continue => false,
166 ControlFlowKind::Exit => true,
167 }
168 }
169}
170
171#[must_use = "You should always handle the control flow value when it is returned"]
172#[derive(Debug, Clone, PartialEq, Serialize)]
173pub struct KclValueControlFlow {
174 value: KclValue,
176 pub control: ControlFlowKind,
177}
178
179impl KclValue {
180 pub(crate) fn continue_(self) -> KclValueControlFlow {
181 KclValueControlFlow {
182 value: self,
183 control: ControlFlowKind::Continue,
184 }
185 }
186
187 pub(crate) fn exit(self) -> KclValueControlFlow {
188 KclValueControlFlow {
189 value: self,
190 control: ControlFlowKind::Exit,
191 }
192 }
193}
194
195impl KclValueControlFlow {
196 pub fn is_some_return(&self) -> bool {
198 self.control.is_some_return()
199 }
200
201 pub(crate) fn into_value(self) -> KclValue {
202 self.value
203 }
204}
205
206#[must_use = "You should always handle the control flow value when it is returned"]
213#[derive(Debug, Clone)]
214pub(crate) enum EarlyReturn {
215 Value(KclValueControlFlow),
217 Error(KclError),
219}
220
221impl From<KclValueControlFlow> for EarlyReturn {
222 fn from(cf: KclValueControlFlow) -> Self {
223 EarlyReturn::Value(cf)
224 }
225}
226
227impl From<KclError> for EarlyReturn {
228 fn from(err: KclError) -> Self {
229 EarlyReturn::Error(err)
230 }
231}
232
233pub(crate) enum StatementKind<'a> {
234 Declaration { name: &'a str },
235 Expression,
236}
237
238#[derive(Debug, Clone, Copy)]
239pub enum PreserveMem {
240 Normal,
241 Always,
242}
243
244impl PreserveMem {
245 fn normal(self) -> bool {
246 match self {
247 PreserveMem::Normal => true,
248 PreserveMem::Always => false,
249 }
250 }
251}
252
253#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
255#[ts(export)]
256#[serde(rename_all = "camelCase")]
257pub struct ExecOutcome {
258 pub variables: IndexMap<String, KclValue>,
260 #[cfg(feature = "artifact-graph")]
263 pub operations: Vec<Operation>,
264 #[cfg(feature = "artifact-graph")]
266 pub artifact_graph: ArtifactGraph,
267 #[cfg(feature = "artifact-graph")]
269 #[serde(skip)]
270 pub scene_objects: Vec<Object>,
271 #[cfg(feature = "artifact-graph")]
274 #[serde(skip)]
275 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
276 #[cfg(feature = "artifact-graph")]
277 #[serde(skip)]
278 pub var_solutions: Vec<(SourceRange, Number)>,
279 pub errors: Vec<CompilationError>,
282 pub filenames: IndexMap<ModuleId, ModulePath>,
284 pub default_planes: Option<DefaultPlanes>,
286}
287
288impl ExecOutcome {
289 pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
290 #[cfg(feature = "artifact-graph")]
291 {
292 debug_assert!(
293 id.0 < self.scene_objects.len(),
294 "Requested object ID {} but only have {} objects",
295 id.0,
296 self.scene_objects.len()
297 );
298 self.scene_objects.get(id.0)
299 }
300 #[cfg(not(feature = "artifact-graph"))]
301 {
302 let _ = id;
303 None
304 }
305 }
306
307 pub fn actual_errors(&self) -> impl Iterator<Item = &CompilationError> {
309 self.errors.iter().filter(|error| error.is_err())
310 }
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct MockConfig {
316 pub use_prev_memory: bool,
317 pub sketch_block_id: Option<ObjectId>,
320 pub freedom_analysis: bool,
323 #[cfg(feature = "artifact-graph")]
325 pub segment_ids_edited: AhashIndexSet<ObjectId>,
326}
327
328impl Default for MockConfig {
329 fn default() -> Self {
330 Self {
331 use_prev_memory: true,
333 sketch_block_id: None,
334 freedom_analysis: true,
335 #[cfg(feature = "artifact-graph")]
336 segment_ids_edited: AhashIndexSet::default(),
337 }
338 }
339}
340
341impl MockConfig {
342 pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
344 Self {
345 sketch_block_id: Some(sketch_block_id),
346 ..Default::default()
347 }
348 }
349
350 #[must_use]
351 pub(crate) fn no_freedom_analysis(mut self) -> Self {
352 self.freedom_analysis = false;
353 self
354 }
355}
356
357#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
358#[ts(export)]
359#[serde(rename_all = "camelCase")]
360pub struct DefaultPlanes {
361 pub xy: uuid::Uuid,
362 pub xz: uuid::Uuid,
363 pub yz: uuid::Uuid,
364 pub neg_xy: uuid::Uuid,
365 pub neg_xz: uuid::Uuid,
366 pub neg_yz: uuid::Uuid,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
370#[ts(export)]
371#[serde(tag = "type", rename_all = "camelCase")]
372pub struct TagIdentifier {
373 pub value: String,
374 #[serde(skip)]
377 pub info: Vec<(usize, TagEngineInfo)>,
378 #[serde(skip)]
379 pub meta: Vec<Metadata>,
380}
381
382impl TagIdentifier {
383 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
385 for (e, info) in self.info.iter().rev() {
386 if *e <= at_epoch {
387 return Some(info);
388 }
389 }
390
391 None
392 }
393
394 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
396 self.info.last().map(|i| &i.1)
397 }
398
399 pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
402 let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
403 return vec![];
404 };
405 self.info
406 .iter()
407 .rev()
408 .take_while(|(e, _)| *e == cur_epoch)
409 .map(|(_, info)| info)
410 .collect()
411 }
412
413 pub fn merge_info(&mut self, other: &TagIdentifier) {
415 assert_eq!(&self.value, &other.value);
416 for (oe, ot) in &other.info {
417 if let Some((e, t)) = self.info.last_mut() {
418 if *e > *oe {
420 continue;
421 }
422 if e == oe {
424 *t = ot.clone();
425 continue;
426 }
427 }
428 self.info.push((*oe, ot.clone()));
429 }
430 }
431
432 pub fn geometry(&self) -> Option<Geometry> {
433 self.get_cur_info().map(|info| info.geometry.clone())
434 }
435}
436
437impl Eq for TagIdentifier {}
438
439impl std::fmt::Display for TagIdentifier {
440 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441 write!(f, "{}", self.value)
442 }
443}
444
445impl std::str::FromStr for TagIdentifier {
446 type Err = KclError;
447
448 fn from_str(s: &str) -> Result<Self, Self::Err> {
449 Ok(Self {
450 value: s.to_string(),
451 info: Vec::new(),
452 meta: Default::default(),
453 })
454 }
455}
456
457impl Ord for TagIdentifier {
458 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
459 self.value.cmp(&other.value)
460 }
461}
462
463impl PartialOrd for TagIdentifier {
464 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
465 Some(self.cmp(other))
466 }
467}
468
469impl std::hash::Hash for TagIdentifier {
470 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
471 self.value.hash(state);
472 }
473}
474
475#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
477#[ts(export)]
478#[serde(tag = "type", rename_all = "camelCase")]
479pub struct TagEngineInfo {
480 pub id: uuid::Uuid,
482 pub geometry: Geometry,
484 pub path: Option<Path>,
486 pub surface: Option<ExtrudeSurface>,
488}
489
490#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
491pub enum BodyType {
492 Root,
493 Block,
494}
495
496#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
498#[ts(export)]
499#[serde(rename_all = "camelCase")]
500pub struct Metadata {
501 pub source_range: SourceRange,
503}
504
505impl From<Metadata> for Vec<SourceRange> {
506 fn from(meta: Metadata) -> Self {
507 vec![meta.source_range]
508 }
509}
510
511impl From<&Metadata> for SourceRange {
512 fn from(meta: &Metadata) -> Self {
513 meta.source_range
514 }
515}
516
517impl From<SourceRange> for Metadata {
518 fn from(source_range: SourceRange) -> Self {
519 Self { source_range }
520 }
521}
522
523impl<T> From<NodeRef<'_, T>> for Metadata {
524 fn from(node: NodeRef<'_, T>) -> Self {
525 Self {
526 source_range: SourceRange::new(node.start, node.end, node.module_id),
527 }
528 }
529}
530
531impl From<&Expr> for Metadata {
532 fn from(expr: &Expr) -> Self {
533 Self {
534 source_range: SourceRange::from(expr),
535 }
536 }
537}
538
539impl Metadata {
540 pub fn to_source_ref(meta: &[Metadata]) -> crate::front::SourceRef {
541 if meta.len() == 1 {
542 let meta = &meta[0];
543 return crate::front::SourceRef::Simple {
544 range: meta.source_range,
545 };
546 }
547 crate::front::SourceRef::BackTrace {
548 ranges: meta.iter().map(|m| m.source_range).collect(),
549 }
550 }
551}
552
553#[derive(PartialEq, Debug, Default, Clone)]
555pub enum ContextType {
556 #[default]
558 Live,
559
560 Mock,
564
565 MockCustomForwarded,
567}
568
569#[derive(Debug, Clone)]
573pub struct ExecutorContext {
574 pub engine: Arc<Box<dyn EngineManager>>,
575 pub fs: Arc<FileManager>,
576 pub settings: ExecutorSettings,
577 pub context_type: ContextType,
578}
579
580#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
582#[ts(export)]
583pub struct ExecutorSettings {
584 pub highlight_edges: bool,
586 pub enable_ssao: bool,
588 pub show_grid: bool,
590 pub replay: Option<String>,
593 pub project_directory: Option<TypedPath>,
596 pub current_file: Option<TypedPath>,
599 pub fixed_size_grid: bool,
601}
602
603impl Default for ExecutorSettings {
604 fn default() -> Self {
605 Self {
606 highlight_edges: true,
607 enable_ssao: false,
608 show_grid: false,
609 replay: None,
610 project_directory: None,
611 current_file: None,
612 fixed_size_grid: true,
613 }
614 }
615}
616
617impl From<crate::settings::types::Configuration> for ExecutorSettings {
618 fn from(config: crate::settings::types::Configuration) -> Self {
619 Self::from(config.settings)
620 }
621}
622
623impl From<crate::settings::types::Settings> for ExecutorSettings {
624 fn from(settings: crate::settings::types::Settings) -> Self {
625 Self {
626 highlight_edges: settings.modeling.highlight_edges.into(),
627 enable_ssao: settings.modeling.enable_ssao.into(),
628 show_grid: settings.modeling.show_scale_grid,
629 replay: None,
630 project_directory: None,
631 current_file: None,
632 fixed_size_grid: settings.modeling.fixed_size_grid,
633 }
634 }
635}
636
637impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
638 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
639 Self::from(config.settings.modeling)
640 }
641}
642
643impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
644 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
645 Self {
646 highlight_edges: modeling.highlight_edges.into(),
647 enable_ssao: modeling.enable_ssao.into(),
648 show_grid: modeling.show_scale_grid,
649 replay: None,
650 project_directory: None,
651 current_file: None,
652 fixed_size_grid: true,
653 }
654 }
655}
656
657impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
658 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
659 Self {
660 highlight_edges: modeling.highlight_edges.into(),
661 enable_ssao: modeling.enable_ssao.into(),
662 show_grid: Default::default(),
663 replay: None,
664 project_directory: None,
665 current_file: None,
666 fixed_size_grid: true,
667 }
668 }
669}
670
671impl ExecutorSettings {
672 pub fn with_current_file(&mut self, current_file: TypedPath) {
674 if current_file.extension() == Some("kcl") {
676 self.current_file = Some(current_file.clone());
677 if let Some(parent) = current_file.parent() {
679 self.project_directory = Some(parent);
680 } else {
681 self.project_directory = Some(TypedPath::from(""));
682 }
683 } else {
684 self.project_directory = Some(current_file);
685 }
686 }
687}
688
689impl ExecutorContext {
690 #[cfg(not(target_arch = "wasm32"))]
692 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
693 let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
694 let (ws, _headers) = client
695 .modeling()
696 .commands_ws(kittycad::modeling::CommandsWsParams {
697 api_call_id: None,
698 fps: None,
699 order_independent_transparency: None,
700 post_effect: if settings.enable_ssao {
701 Some(kittycad::types::PostEffectType::Ssao)
702 } else {
703 None
704 },
705 replay: settings.replay.clone(),
706 show_grid: if settings.show_grid { Some(true) } else { None },
707 pool: None,
708 pr,
709 unlocked_framerate: None,
710 webrtc: Some(false),
711 video_res_width: None,
712 video_res_height: None,
713 })
714 .await?;
715
716 let engine: Arc<Box<dyn EngineManager>> =
717 Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
718
719 Ok(Self {
720 engine,
721 fs: Arc::new(FileManager::new()),
722 settings,
723 context_type: ContextType::Live,
724 })
725 }
726
727 #[cfg(target_arch = "wasm32")]
728 pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
729 ExecutorContext {
730 engine,
731 fs,
732 settings,
733 context_type: ContextType::Live,
734 }
735 }
736
737 #[cfg(not(target_arch = "wasm32"))]
738 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
739 ExecutorContext {
740 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
741 fs: Arc::new(FileManager::new()),
742 settings: settings.unwrap_or_default(),
743 context_type: ContextType::Mock,
744 }
745 }
746
747 #[cfg(target_arch = "wasm32")]
748 pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
749 ExecutorContext {
750 engine,
751 fs,
752 settings,
753 context_type: ContextType::Mock,
754 }
755 }
756
757 #[cfg(target_arch = "wasm32")]
760 pub fn new_mock_for_lsp(
761 fs_manager: crate::fs::wasm::FileSystemManager,
762 settings: ExecutorSettings,
763 ) -> Result<Self, String> {
764 use crate::mock_engine;
765
766 let mock_engine = Arc::new(Box::new(
767 mock_engine::EngineConnection::new().map_err(|e| format!("Failed to create mock engine: {:?}", e))?,
768 ) as Box<dyn EngineManager>);
769
770 let fs = Arc::new(FileManager::new(fs_manager));
771
772 Ok(ExecutorContext {
773 engine: mock_engine,
774 fs,
775 settings,
776 context_type: ContextType::Mock,
777 })
778 }
779
780 #[cfg(not(target_arch = "wasm32"))]
781 pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
782 ExecutorContext {
783 engine,
784 fs: Arc::new(FileManager::new()),
785 settings: Default::default(),
786 context_type: ContextType::MockCustomForwarded,
787 }
788 }
789
790 #[cfg(not(target_arch = "wasm32"))]
796 pub async fn new_with_client(
797 settings: ExecutorSettings,
798 token: Option<String>,
799 engine_addr: Option<String>,
800 ) -> Result<Self> {
801 let client = crate::engine::new_zoo_client(token, engine_addr)?;
803
804 let ctx = Self::new(&client, settings).await?;
805 Ok(ctx)
806 }
807
808 #[cfg(not(target_arch = "wasm32"))]
813 pub async fn new_with_default_client() -> Result<Self> {
814 let ctx = Self::new_with_client(Default::default(), None, None).await?;
816 Ok(ctx)
817 }
818
819 #[cfg(not(target_arch = "wasm32"))]
821 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
822 let ctx = ExecutorContext::new_with_client(
823 ExecutorSettings {
824 highlight_edges: true,
825 enable_ssao: false,
826 show_grid: false,
827 replay: None,
828 project_directory: None,
829 current_file: None,
830 fixed_size_grid: false,
831 },
832 None,
833 engine_addr,
834 )
835 .await?;
836 Ok(ctx)
837 }
838
839 pub fn is_mock(&self) -> bool {
840 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
841 }
842
843 pub async fn no_engine_commands(&self) -> bool {
845 self.is_mock()
846 }
847
848 pub async fn send_clear_scene(
849 &self,
850 exec_state: &mut ExecState,
851 source_range: crate::execution::SourceRange,
852 ) -> Result<(), KclError> {
853 exec_state.mod_local.artifacts.clear();
856 exec_state.global.root_module_artifacts.clear();
857 exec_state.global.artifacts.clear();
858
859 self.engine
860 .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
861 .await?;
862 if self.settings.enable_ssao {
865 let cmd_id = exec_state.next_uuid();
866 exec_state
867 .batch_modeling_cmd(
868 ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
869 ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
870 )
871 .await?;
872 }
873 Ok(())
874 }
875
876 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
877 cache::bust_cache().await;
878
879 let outcome = self.run_with_caching(crate::Program::empty()).await?;
884
885 Ok(outcome)
886 }
887
888 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
889 self.eval_prelude(exec_state, SourceRange::synthetic())
890 .await
891 .map_err(KclErrorWithOutputs::no_outputs)?;
892 exec_state.mut_stack().push_new_root_env(true);
893 Ok(())
894 }
895
896 pub async fn run_mock(
897 &self,
898 program: &crate::Program,
899 mock_config: &MockConfig,
900 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
901 assert!(
902 self.is_mock(),
903 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
904 );
905
906 let use_prev_memory = mock_config.use_prev_memory;
907 let mut exec_state = ExecState::new_mock(self, mock_config);
908 if use_prev_memory {
909 match cache::read_old_memory().await {
910 Some(mem) => {
911 *exec_state.mut_stack() = mem.stack;
912 exec_state.global.module_infos = mem.module_infos;
913 #[cfg(feature = "artifact-graph")]
914 {
915 let len = mock_config
916 .sketch_block_id
917 .map(|sketch_block_id| sketch_block_id.0)
918 .unwrap_or(0);
919 if let Some(scene_objects) = mem.scene_objects.get(0..len) {
920 exec_state
921 .global
922 .root_module_artifacts
923 .restore_scene_objects(scene_objects);
924 } else {
925 let message = format!(
926 "Cached scene objects length {} is less than expected length from cached object ID generator {}",
927 mem.scene_objects.len(),
928 len
929 );
930 debug_assert!(false, "{message}");
931 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
932 KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
933 )));
934 }
935 }
936 }
937 None => self.prepare_mem(&mut exec_state).await?,
938 }
939 } else {
940 self.prepare_mem(&mut exec_state).await?
941 };
942
943 exec_state.mut_stack().push_new_env_for_scope();
946
947 let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
948
949 let mut stack = exec_state.stack().clone();
954 let module_infos = exec_state.global.module_infos.clone();
955 #[cfg(feature = "artifact-graph")]
956 let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
957 #[cfg(not(feature = "artifact-graph"))]
958 let scene_objects = Default::default();
959 let outcome = exec_state.into_exec_outcome(result.0, self).await;
960
961 stack.squash_env(result.0);
962 let state = cache::SketchModeState {
963 stack,
964 module_infos,
965 scene_objects,
966 };
967 cache::write_old_memory(state).await;
968
969 Ok(outcome)
970 }
971
972 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
973 assert!(!self.is_mock());
974 let grid_scale = if self.settings.fixed_size_grid {
975 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
976 } else {
977 GridScaleBehavior::ScaleWithZoom
978 };
979
980 let original_program = program.clone();
981
982 let (_program, exec_state, result) = match cache::read_old_ast().await {
983 Some(mut cached_state) => {
984 let old = CacheInformation {
985 ast: &cached_state.main.ast,
986 settings: &cached_state.settings,
987 };
988 let new = CacheInformation {
989 ast: &program.ast,
990 settings: &self.settings,
991 };
992
993 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
995 CacheResult::ReExecute {
996 clear_scene,
997 reapply_settings,
998 program: changed_program,
999 } => {
1000 if reapply_settings
1001 && self
1002 .engine
1003 .reapply_settings(
1004 &self.settings,
1005 Default::default(),
1006 &mut cached_state.main.exec_state.id_generator,
1007 grid_scale,
1008 )
1009 .await
1010 .is_err()
1011 {
1012 (true, program, None)
1013 } else {
1014 (
1015 clear_scene,
1016 crate::Program {
1017 ast: changed_program,
1018 original_file_contents: program.original_file_contents,
1019 },
1020 None,
1021 )
1022 }
1023 }
1024 CacheResult::CheckImportsOnly {
1025 reapply_settings,
1026 ast: changed_program,
1027 } => {
1028 let mut reapply_failed = false;
1029 if reapply_settings {
1030 if self
1031 .engine
1032 .reapply_settings(
1033 &self.settings,
1034 Default::default(),
1035 &mut cached_state.main.exec_state.id_generator,
1036 grid_scale,
1037 )
1038 .await
1039 .is_ok()
1040 {
1041 cache::write_old_ast(GlobalState::with_settings(
1042 cached_state.clone(),
1043 self.settings.clone(),
1044 ))
1045 .await;
1046 } else {
1047 reapply_failed = true;
1048 }
1049 }
1050
1051 if reapply_failed {
1052 (true, program, None)
1053 } else {
1054 let mut new_exec_state = ExecState::new(self);
1056 let (new_universe, new_universe_map) =
1057 self.get_universe(&program, &mut new_exec_state).await?;
1058
1059 let clear_scene = new_universe.values().any(|value| {
1060 let id = value.1;
1061 match (
1062 cached_state.exec_state.get_source(id),
1063 new_exec_state.global.get_source(id),
1064 ) {
1065 (Some(s0), Some(s1)) => s0.source != s1.source,
1066 _ => false,
1067 }
1068 });
1069
1070 if !clear_scene {
1071 return Ok(cached_state.into_exec_outcome(self).await);
1073 }
1074
1075 (
1076 true,
1077 crate::Program {
1078 ast: changed_program,
1079 original_file_contents: program.original_file_contents,
1080 },
1081 Some((new_universe, new_universe_map, new_exec_state)),
1082 )
1083 }
1084 }
1085 CacheResult::NoAction(true) => {
1086 if self
1087 .engine
1088 .reapply_settings(
1089 &self.settings,
1090 Default::default(),
1091 &mut cached_state.main.exec_state.id_generator,
1092 grid_scale,
1093 )
1094 .await
1095 .is_ok()
1096 {
1097 cache::write_old_ast(GlobalState::with_settings(
1099 cached_state.clone(),
1100 self.settings.clone(),
1101 ))
1102 .await;
1103
1104 return Ok(cached_state.into_exec_outcome(self).await);
1105 }
1106 (true, program, None)
1107 }
1108 CacheResult::NoAction(false) => {
1109 return Ok(cached_state.into_exec_outcome(self).await);
1110 }
1111 };
1112
1113 let (exec_state, result) = match import_check_info {
1114 Some((new_universe, new_universe_map, mut new_exec_state)) => {
1115 self.send_clear_scene(&mut new_exec_state, Default::default())
1117 .await
1118 .map_err(KclErrorWithOutputs::no_outputs)?;
1119
1120 let result = self
1121 .run_concurrent(
1122 &program,
1123 &mut new_exec_state,
1124 Some((new_universe, new_universe_map)),
1125 PreserveMem::Normal,
1126 )
1127 .await;
1128
1129 (new_exec_state, result)
1130 }
1131 None if clear_scene => {
1132 let mut exec_state = cached_state.reconstitute_exec_state();
1134 exec_state.reset(self);
1135
1136 self.send_clear_scene(&mut exec_state, Default::default())
1137 .await
1138 .map_err(KclErrorWithOutputs::no_outputs)?;
1139
1140 let result = self
1141 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1142 .await;
1143
1144 (exec_state, result)
1145 }
1146 None => {
1147 let mut exec_state = cached_state.reconstitute_exec_state();
1148 exec_state.mut_stack().restore_env(cached_state.main.result_env);
1149
1150 let result = self
1151 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1152 .await;
1153
1154 (exec_state, result)
1155 }
1156 };
1157
1158 (program, exec_state, result)
1159 }
1160 None => {
1161 let mut exec_state = ExecState::new(self);
1162 self.send_clear_scene(&mut exec_state, Default::default())
1163 .await
1164 .map_err(KclErrorWithOutputs::no_outputs)?;
1165
1166 let result = self
1167 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1168 .await;
1169
1170 (program, exec_state, result)
1171 }
1172 };
1173
1174 if result.is_err() {
1175 cache::bust_cache().await;
1176 }
1177
1178 let result = result?;
1180
1181 cache::write_old_ast(GlobalState::new(
1185 exec_state.clone(),
1186 self.settings.clone(),
1187 original_program.ast,
1188 result.0,
1189 ))
1190 .await;
1191
1192 let outcome = exec_state.into_exec_outcome(result.0, self).await;
1193 Ok(outcome)
1194 }
1195
1196 pub async fn run(
1200 &self,
1201 program: &crate::Program,
1202 exec_state: &mut ExecState,
1203 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1204 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1205 .await
1206 }
1207
1208 pub async fn run_concurrent(
1213 &self,
1214 program: &crate::Program,
1215 exec_state: &mut ExecState,
1216 universe_info: Option<(Universe, UniverseMap)>,
1217 preserve_mem: PreserveMem,
1218 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1219 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1222 (universe, universe_map)
1223 } else {
1224 self.get_universe(program, exec_state).await?
1225 };
1226
1227 let default_planes = self.engine.get_default_planes().read().await.clone();
1228
1229 self.eval_prelude(exec_state, SourceRange::synthetic())
1231 .await
1232 .map_err(KclErrorWithOutputs::no_outputs)?;
1233
1234 for modules in import_graph::import_graph(&universe, self)
1235 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1236 .into_iter()
1237 {
1238 #[cfg(not(target_arch = "wasm32"))]
1239 let mut set = tokio::task::JoinSet::new();
1240
1241 #[allow(clippy::type_complexity)]
1242 let (results_tx, mut results_rx): (
1243 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1244 tokio::sync::mpsc::Receiver<_>,
1245 ) = tokio::sync::mpsc::channel(1);
1246
1247 for module in modules {
1248 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1249 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1250 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1251 )));
1252 };
1253 let module_id = *module_id;
1254 let module_path = module_path.clone();
1255 let source_range = SourceRange::from(import_stmt);
1256 let module_exec_state = exec_state.clone();
1258
1259 self.add_import_module_ops(
1260 exec_state,
1261 &program.ast,
1262 module_id,
1263 &module_path,
1264 source_range,
1265 &universe_map,
1266 );
1267
1268 let repr = repr.clone();
1269 let exec_ctxt = self.clone();
1270 let results_tx = results_tx.clone();
1271
1272 let exec_module = async |exec_ctxt: &ExecutorContext,
1273 repr: &ModuleRepr,
1274 module_id: ModuleId,
1275 module_path: &ModulePath,
1276 exec_state: &mut ExecState,
1277 source_range: SourceRange|
1278 -> Result<ModuleRepr, KclError> {
1279 match repr {
1280 ModuleRepr::Kcl(program, _) => {
1281 let result = exec_ctxt
1282 .exec_module_from_ast(
1283 program,
1284 module_id,
1285 module_path,
1286 exec_state,
1287 source_range,
1288 PreserveMem::Normal,
1289 )
1290 .await;
1291
1292 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1293 }
1294 ModuleRepr::Foreign(geom, _) => {
1295 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1296 .await
1297 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1298
1299 result.map(|val| {
1300 ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
1301 })
1302 }
1303 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1304 format!("Module {module_path} not found in universe"),
1305 vec![source_range],
1306 ))),
1307 }
1308 };
1309
1310 #[cfg(target_arch = "wasm32")]
1311 {
1312 wasm_bindgen_futures::spawn_local(async move {
1313 let mut exec_state = module_exec_state;
1314 let exec_ctxt = exec_ctxt;
1315
1316 let result = exec_module(
1317 &exec_ctxt,
1318 &repr,
1319 module_id,
1320 &module_path,
1321 &mut exec_state,
1322 source_range,
1323 )
1324 .await;
1325
1326 results_tx
1327 .send((module_id, module_path, result))
1328 .await
1329 .unwrap_or_default();
1330 });
1331 }
1332 #[cfg(not(target_arch = "wasm32"))]
1333 {
1334 set.spawn(async move {
1335 let mut exec_state = module_exec_state;
1336 let exec_ctxt = exec_ctxt;
1337
1338 let result = exec_module(
1339 &exec_ctxt,
1340 &repr,
1341 module_id,
1342 &module_path,
1343 &mut exec_state,
1344 source_range,
1345 )
1346 .await;
1347
1348 results_tx
1349 .send((module_id, module_path, result))
1350 .await
1351 .unwrap_or_default();
1352 });
1353 }
1354 }
1355
1356 drop(results_tx);
1357
1358 while let Some((module_id, _, result)) = results_rx.recv().await {
1359 match result {
1360 Ok(new_repr) => {
1361 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1362
1363 match &mut repr {
1364 ModuleRepr::Kcl(_, cache) => {
1365 let ModuleRepr::Kcl(_, session_data) = new_repr else {
1366 unreachable!();
1367 };
1368 *cache = session_data;
1369 }
1370 ModuleRepr::Foreign(_, cache) => {
1371 let ModuleRepr::Foreign(_, session_data) = new_repr else {
1372 unreachable!();
1373 };
1374 *cache = session_data;
1375 }
1376 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1377 }
1378
1379 exec_state.global.module_infos[&module_id].restore_repr(repr);
1380 }
1381 Err(e) => {
1382 return Err(exec_state.error_with_outputs(e, None, default_planes));
1383 }
1384 }
1385 }
1386 }
1387
1388 exec_state
1392 .global
1393 .root_module_artifacts
1394 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1395
1396 self.inner_run(program, exec_state, preserve_mem).await
1397 }
1398
1399 async fn get_universe(
1402 &self,
1403 program: &crate::Program,
1404 exec_state: &mut ExecState,
1405 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1406 exec_state.add_root_module_contents(program);
1407
1408 let mut universe = std::collections::HashMap::new();
1409
1410 let default_planes = self.engine.get_default_planes().read().await.clone();
1411
1412 let root_imports = import_graph::import_universe(
1413 self,
1414 &ModulePath::Main,
1415 &ModuleRepr::Kcl(program.ast.clone(), None),
1416 &mut universe,
1417 exec_state,
1418 )
1419 .await
1420 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1421
1422 Ok((universe, root_imports))
1423 }
1424
1425 #[cfg(not(feature = "artifact-graph"))]
1426 fn add_import_module_ops(
1427 &self,
1428 _exec_state: &mut ExecState,
1429 _program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1430 _module_id: ModuleId,
1431 _module_path: &ModulePath,
1432 _source_range: SourceRange,
1433 _universe_map: &UniverseMap,
1434 ) {
1435 }
1436
1437 #[cfg(feature = "artifact-graph")]
1438 fn add_import_module_ops(
1439 &self,
1440 exec_state: &mut ExecState,
1441 program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1442 module_id: ModuleId,
1443 module_path: &ModulePath,
1444 source_range: SourceRange,
1445 universe_map: &UniverseMap,
1446 ) {
1447 match module_path {
1448 ModulePath::Main => {
1449 }
1451 ModulePath::Local {
1452 value,
1453 original_import_path,
1454 } => {
1455 if universe_map.contains_key(value) {
1458 use crate::NodePath;
1459
1460 let node_path = if source_range.is_top_level_module() {
1461 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1462 NodePath::from_range(
1463 &exec_state.build_program_lookup(program.clone()),
1464 cached_body_items,
1465 source_range,
1466 )
1467 .unwrap_or_default()
1468 } else {
1469 NodePath::placeholder()
1472 };
1473
1474 let name = match original_import_path {
1475 Some(value) => value.to_string_lossy(),
1476 None => value.file_name().unwrap_or_default(),
1477 };
1478 exec_state.push_op(Operation::GroupBegin {
1479 group: Group::ModuleInstance { name, module_id },
1480 node_path,
1481 source_range,
1482 });
1483 exec_state.push_op(Operation::GroupEnd);
1487 }
1488 }
1489 ModulePath::Std { .. } => {
1490 }
1492 }
1493 }
1494
1495 async fn inner_run(
1498 &self,
1499 program: &crate::Program,
1500 exec_state: &mut ExecState,
1501 preserve_mem: PreserveMem,
1502 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1503 let _stats = crate::log::LogPerfStats::new("Interpretation");
1504
1505 let grid_scale = if self.settings.fixed_size_grid {
1507 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1508 } else {
1509 GridScaleBehavior::ScaleWithZoom
1510 };
1511 self.engine
1512 .reapply_settings(
1513 &self.settings,
1514 Default::default(),
1515 exec_state.id_generator(),
1516 grid_scale,
1517 )
1518 .await
1519 .map_err(KclErrorWithOutputs::no_outputs)?;
1520
1521 let default_planes = self.engine.get_default_planes().read().await.clone();
1522 let result = self
1523 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1524 .await;
1525
1526 crate::log::log(format!(
1527 "Post interpretation KCL memory stats: {:#?}",
1528 exec_state.stack().memory.stats
1529 ));
1530 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1531
1532 async fn write_old_memory(ctx: &ExecutorContext, exec_state: &ExecState, env_ref: EnvironmentRef) {
1535 if ctx.is_mock() {
1536 return;
1537 }
1538 let mut stack = exec_state.stack().deep_clone();
1539 stack.restore_env(env_ref);
1540 let state = cache::SketchModeState {
1541 stack,
1542 module_infos: exec_state.global.module_infos.clone(),
1543 #[cfg(feature = "artifact-graph")]
1544 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1545 #[cfg(not(feature = "artifact-graph"))]
1546 scene_objects: Default::default(),
1547 };
1548 cache::write_old_memory(state).await;
1549 }
1550
1551 let env_ref = match result {
1552 Ok(env_ref) => env_ref,
1553 Err((err, env_ref)) => {
1554 if let Some(env_ref) = env_ref {
1557 write_old_memory(self, exec_state, env_ref).await;
1558 }
1559 return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
1560 }
1561 };
1562
1563 write_old_memory(self, exec_state, env_ref).await;
1564
1565 let session_data = self.engine.get_session_data().await;
1566
1567 Ok((env_ref, session_data))
1568 }
1569
1570 async fn execute_and_build_graph(
1573 &self,
1574 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1575 exec_state: &mut ExecState,
1576 preserve_mem: PreserveMem,
1577 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1578 #[cfg(feature = "artifact-graph")]
1584 let start_op = exec_state.global.root_module_artifacts.operations.len();
1585
1586 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1587 .await
1588 .map_err(|e| (e, None))?;
1589
1590 let exec_result = self
1591 .exec_module_body(
1592 program,
1593 exec_state,
1594 preserve_mem,
1595 ModuleId::default(),
1596 &ModulePath::Main,
1597 )
1598 .await
1599 .map(
1600 |ModuleExecutionOutcome {
1601 environment: env_ref,
1602 artifacts: module_artifacts,
1603 ..
1604 }| {
1605 exec_state.global.root_module_artifacts.extend(module_artifacts);
1608 env_ref
1609 },
1610 )
1611 .map_err(|(err, env_ref, module_artifacts)| {
1612 if let Some(module_artifacts) = module_artifacts {
1613 exec_state.global.root_module_artifacts.extend(module_artifacts);
1616 }
1617 (err, env_ref)
1618 });
1619
1620 #[cfg(feature = "artifact-graph")]
1621 {
1622 let programs = &exec_state.build_program_lookup(program.clone());
1624 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1625 for op in exec_state
1626 .global
1627 .root_module_artifacts
1628 .operations
1629 .iter_mut()
1630 .skip(start_op)
1631 {
1632 op.fill_node_paths(programs, cached_body_items);
1633 }
1634 for module in exec_state.global.module_infos.values_mut() {
1635 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1636 for op in &mut outcome.artifacts.operations {
1637 op.fill_node_paths(programs, cached_body_items);
1638 }
1639 }
1640 }
1641 }
1642
1643 self.engine.ensure_async_commands_completed().await.map_err(|e| {
1645 match &exec_result {
1646 Ok(env_ref) => (e, Some(*env_ref)),
1647 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1649 }
1650 })?;
1651
1652 self.engine.clear_queues().await;
1655
1656 match exec_state.build_artifact_graph(&self.engine, program).await {
1657 Ok(_) => exec_result,
1658 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1659 }
1660 }
1661
1662 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1666 if exec_state.stack().memory.requires_std() {
1667 #[cfg(feature = "artifact-graph")]
1668 let initial_ops = exec_state.mod_local.artifacts.operations.len();
1669
1670 let path = vec!["std".to_owned(), "prelude".to_owned()];
1671 let resolved_path = ModulePath::from_std_import_path(&path)?;
1672 let id = self
1673 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1674 .await?;
1675 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1676
1677 exec_state.mut_stack().memory.set_std(module_memory);
1678
1679 #[cfg(feature = "artifact-graph")]
1685 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1686 }
1687
1688 Ok(())
1689 }
1690
1691 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1693 self.engine
1695 .send_modeling_cmd(
1696 uuid::Uuid::new_v4(),
1697 crate::execution::SourceRange::default(),
1698 &ModelingCmd::from(
1699 mcmd::ZoomToFit::builder()
1700 .object_ids(Default::default())
1701 .animated(false)
1702 .padding(0.1)
1703 .build(),
1704 ),
1705 )
1706 .await
1707 .map_err(KclErrorWithOutputs::no_outputs)?;
1708
1709 let resp = self
1711 .engine
1712 .send_modeling_cmd(
1713 uuid::Uuid::new_v4(),
1714 crate::execution::SourceRange::default(),
1715 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
1716 )
1717 .await
1718 .map_err(KclErrorWithOutputs::no_outputs)?;
1719
1720 let OkWebSocketResponseData::Modeling {
1721 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1722 } = resp
1723 else {
1724 return Err(ExecError::BadPng(format!(
1725 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1726 )));
1727 };
1728 Ok(contents)
1729 }
1730
1731 pub async fn export(
1733 &self,
1734 format: kittycad_modeling_cmds::format::OutputFormat3d,
1735 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1736 let resp = self
1737 .engine
1738 .send_modeling_cmd(
1739 uuid::Uuid::new_v4(),
1740 crate::SourceRange::default(),
1741 &kittycad_modeling_cmds::ModelingCmd::Export(
1742 kittycad_modeling_cmds::Export::builder()
1743 .entity_ids(vec![])
1744 .format(format)
1745 .build(),
1746 ),
1747 )
1748 .await?;
1749
1750 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1751 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1752 format!("Expected Export response, got {resp:?}",),
1753 vec![SourceRange::default()],
1754 )));
1755 };
1756
1757 Ok(files)
1758 }
1759
1760 pub async fn export_step(
1762 &self,
1763 deterministic_time: bool,
1764 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1765 let files = self
1766 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1767 kittycad_modeling_cmds::format::step::export::Options::builder()
1768 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
1769 .maybe_created(if deterministic_time {
1770 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1771 KclError::new_internal(crate::errors::KclErrorDetails::new(
1772 format!("Failed to parse date: {e}"),
1773 vec![SourceRange::default()],
1774 ))
1775 })?)
1776 } else {
1777 None
1778 })
1779 .build(),
1780 ))
1781 .await?;
1782
1783 Ok(files)
1784 }
1785
1786 pub async fn close(&self) {
1787 self.engine.close().await;
1788 }
1789}
1790
1791#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1792pub struct ArtifactId(Uuid);
1793
1794impl ArtifactId {
1795 pub fn new(uuid: Uuid) -> Self {
1796 Self(uuid)
1797 }
1798
1799 pub fn placeholder() -> Self {
1801 Self(Uuid::nil())
1802 }
1803}
1804
1805impl From<Uuid> for ArtifactId {
1806 fn from(uuid: Uuid) -> Self {
1807 Self::new(uuid)
1808 }
1809}
1810
1811impl From<&Uuid> for ArtifactId {
1812 fn from(uuid: &Uuid) -> Self {
1813 Self::new(*uuid)
1814 }
1815}
1816
1817impl From<ArtifactId> for Uuid {
1818 fn from(id: ArtifactId) -> Self {
1819 id.0
1820 }
1821}
1822
1823impl From<&ArtifactId> for Uuid {
1824 fn from(id: &ArtifactId) -> Self {
1825 id.0
1826 }
1827}
1828
1829impl From<ModelingCmdId> for ArtifactId {
1830 fn from(id: ModelingCmdId) -> Self {
1831 Self::new(*id.as_ref())
1832 }
1833}
1834
1835impl From<&ModelingCmdId> for ArtifactId {
1836 fn from(id: &ModelingCmdId) -> Self {
1837 Self::new(*id.as_ref())
1838 }
1839}
1840
1841#[cfg(test)]
1842pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1843 parse_execute_with_project_dir(code, None).await
1844}
1845
1846#[cfg(test)]
1847pub(crate) async fn parse_execute_with_project_dir(
1848 code: &str,
1849 project_directory: Option<TypedPath>,
1850) -> Result<ExecTestResults, KclError> {
1851 let program = crate::Program::parse_no_errs(code)?;
1852
1853 let exec_ctxt = ExecutorContext {
1854 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
1855 |err| {
1856 KclError::new_internal(crate::errors::KclErrorDetails::new(
1857 format!("Failed to create mock engine connection: {err}"),
1858 vec![SourceRange::default()],
1859 ))
1860 },
1861 )?)),
1862 fs: Arc::new(crate::fs::FileManager::new()),
1863 settings: ExecutorSettings {
1864 project_directory,
1865 ..Default::default()
1866 },
1867 context_type: ContextType::Mock,
1868 };
1869 let mut exec_state = ExecState::new(&exec_ctxt);
1870 let result = exec_ctxt.run(&program, &mut exec_state).await?;
1871
1872 Ok(ExecTestResults {
1873 program,
1874 mem_env: result.0,
1875 exec_ctxt,
1876 exec_state,
1877 })
1878}
1879
1880#[cfg(test)]
1881#[derive(Debug)]
1882pub(crate) struct ExecTestResults {
1883 program: crate::Program,
1884 mem_env: EnvironmentRef,
1885 exec_ctxt: ExecutorContext,
1886 exec_state: ExecState,
1887}
1888
1889#[cfg(feature = "artifact-graph")]
1893pub struct ProgramLookup {
1894 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
1895}
1896
1897#[cfg(feature = "artifact-graph")]
1898impl ProgramLookup {
1899 pub fn new(
1902 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1903 module_infos: state::ModuleInfoMap,
1904 ) -> Self {
1905 let mut programs = IndexMap::with_capacity(module_infos.len());
1906 for (id, info) in module_infos {
1907 if let ModuleRepr::Kcl(program, _) = info.repr {
1908 programs.insert(id, program);
1909 }
1910 }
1911 programs.insert(ModuleId::default(), current);
1912 Self { programs }
1913 }
1914
1915 pub fn program_for_module(
1916 &self,
1917 module_id: ModuleId,
1918 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
1919 self.programs.get(&module_id)
1920 }
1921}
1922
1923#[cfg(test)]
1924mod tests {
1925 use pretty_assertions::assert_eq;
1926
1927 use super::*;
1928 use crate::ModuleId;
1929 use crate::errors::KclErrorDetails;
1930 use crate::errors::Severity;
1931 use crate::exec::NumericType;
1932 use crate::execution::memory::Stack;
1933 use crate::execution::types::RuntimeType;
1934
1935 #[track_caller]
1937 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1938 memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1939 }
1940
1941 #[tokio::test(flavor = "multi_thread")]
1942 async fn test_execute_warn() {
1943 let text = "@blah";
1944 let result = parse_execute(text).await.unwrap();
1945 let errs = result.exec_state.errors();
1946 assert_eq!(errs.len(), 1);
1947 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1948 assert!(
1949 errs[0].message.contains("Unknown annotation"),
1950 "unexpected warning message: {}",
1951 errs[0].message
1952 );
1953 }
1954
1955 #[tokio::test(flavor = "multi_thread")]
1956 async fn test_execute_fn_definitions() {
1957 let ast = r#"fn def(@x) {
1958 return x
1959}
1960fn ghi(@x) {
1961 return x
1962}
1963fn jkl(@x) {
1964 return x
1965}
1966fn hmm(@x) {
1967 return x
1968}
1969
1970yo = 5 + 6
1971
1972abc = 3
1973identifierGuy = 5
1974part001 = startSketchOn(XY)
1975|> startProfile(at = [-1.2, 4.83])
1976|> line(end = [2.8, 0])
1977|> angledLine(angle = 100 + 100, length = 3.01)
1978|> angledLine(angle = abc, length = 3.02)
1979|> angledLine(angle = def(yo), length = 3.03)
1980|> angledLine(angle = ghi(2), length = 3.04)
1981|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1982|> close()
1983yo2 = hmm([identifierGuy + 5])"#;
1984
1985 parse_execute(ast).await.unwrap();
1986 }
1987
1988 #[tokio::test(flavor = "multi_thread")]
1989 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
1990 let code = r#"@settings(experimentalFeatures = allow)
1991firstProfile = sketch(on = XY) {
1992 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
1993 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
1994 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
1995 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
1996 coincident([edge1.end, edge2.start])
1997 coincident([edge2.end, edge3.start])
1998 coincident([edge3.end, edge4.start])
1999 coincident([edge4.end, edge1.start])
2000}
2001
2002secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2003 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2004 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2005 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2006 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2007 coincident([edge5.end, edge6.start])
2008 coincident([edge6.end, edge7.start])
2009 coincident([edge7.end, edge8.start])
2010 coincident([edge8.end, edge5.start])
2011}
2012
2013firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2014secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2015"#;
2016
2017 let result = parse_execute(code).await.unwrap();
2018 assert!(result.exec_state.errors().is_empty());
2019 }
2020
2021 #[tokio::test(flavor = "multi_thread")]
2022 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2023 let code = r#"@settings(experimentalFeatures = allow)
2024sketch001 = sketch(on = YZ) {
2025 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2026 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2027 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2028 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2029 coincident([line1.end, line2.start])
2030 coincident([line2.end, line3.start])
2031 coincident([line3.end, line4.start])
2032 coincident([line4.end, line1.start])
2033}
2034
2035sketch002 = sketch(on = -XZ) {
2036 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2037 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2038 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2039 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2040 coincident([line5.end, line6.start])
2041 coincident([line6.end, line7.start])
2042 coincident([line7.end, line8.start])
2043 coincident([line8.end, line5.start])
2044}
2045
2046region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2047extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2048region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2049extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2050
2051myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2052"#;
2053
2054 let result = parse_execute(code).await.unwrap();
2055 assert!(result.exec_state.errors().is_empty());
2056 }
2057
2058 #[tokio::test(flavor = "multi_thread")]
2059 async fn test_execute_with_pipe_substitutions_unary() {
2060 let ast = r#"myVar = 3
2061part001 = startSketchOn(XY)
2062 |> startProfile(at = [0, 0])
2063 |> line(end = [3, 4], tag = $seg01)
2064 |> line(end = [
2065 min([segLen(seg01), myVar]),
2066 -legLen(hypotenuse = segLen(seg01), leg = myVar)
2067])
2068"#;
2069
2070 parse_execute(ast).await.unwrap();
2071 }
2072
2073 #[tokio::test(flavor = "multi_thread")]
2074 async fn test_execute_with_pipe_substitutions() {
2075 let ast = r#"myVar = 3
2076part001 = startSketchOn(XY)
2077 |> startProfile(at = [0, 0])
2078 |> line(end = [3, 4], tag = $seg01)
2079 |> line(end = [
2080 min([segLen(seg01), myVar]),
2081 legLen(hypotenuse = segLen(seg01), leg = myVar)
2082])
2083"#;
2084
2085 parse_execute(ast).await.unwrap();
2086 }
2087
2088 #[tokio::test(flavor = "multi_thread")]
2089 async fn test_execute_with_inline_comment() {
2090 let ast = r#"baseThick = 1
2091armAngle = 60
2092
2093baseThickHalf = baseThick / 2
2094halfArmAngle = armAngle / 2
2095
2096arrExpShouldNotBeIncluded = [1, 2, 3]
2097objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2098
2099part001 = startSketchOn(XY)
2100 |> startProfile(at = [0, 0])
2101 |> yLine(endAbsolute = 1)
2102 |> xLine(length = 3.84) // selection-range-7ish-before-this
2103
2104variableBelowShouldNotBeIncluded = 3
2105"#;
2106
2107 parse_execute(ast).await.unwrap();
2108 }
2109
2110 #[tokio::test(flavor = "multi_thread")]
2111 async fn test_execute_with_function_literal_in_pipe() {
2112 let ast = r#"w = 20
2113l = 8
2114h = 10
2115
2116fn thing() {
2117 return -8
2118}
2119
2120firstExtrude = startSketchOn(XY)
2121 |> startProfile(at = [0,0])
2122 |> line(end = [0, l])
2123 |> line(end = [w, 0])
2124 |> line(end = [0, thing()])
2125 |> close()
2126 |> extrude(length = h)"#;
2127
2128 parse_execute(ast).await.unwrap();
2129 }
2130
2131 #[tokio::test(flavor = "multi_thread")]
2132 async fn test_execute_with_function_unary_in_pipe() {
2133 let ast = r#"w = 20
2134l = 8
2135h = 10
2136
2137fn thing(@x) {
2138 return -x
2139}
2140
2141firstExtrude = startSketchOn(XY)
2142 |> startProfile(at = [0,0])
2143 |> line(end = [0, l])
2144 |> line(end = [w, 0])
2145 |> line(end = [0, thing(8)])
2146 |> close()
2147 |> extrude(length = h)"#;
2148
2149 parse_execute(ast).await.unwrap();
2150 }
2151
2152 #[tokio::test(flavor = "multi_thread")]
2153 async fn test_execute_with_function_array_in_pipe() {
2154 let ast = r#"w = 20
2155l = 8
2156h = 10
2157
2158fn thing(@x) {
2159 return [0, -x]
2160}
2161
2162firstExtrude = startSketchOn(XY)
2163 |> startProfile(at = [0,0])
2164 |> line(end = [0, l])
2165 |> line(end = [w, 0])
2166 |> line(end = thing(8))
2167 |> close()
2168 |> extrude(length = h)"#;
2169
2170 parse_execute(ast).await.unwrap();
2171 }
2172
2173 #[tokio::test(flavor = "multi_thread")]
2174 async fn test_execute_with_function_call_in_pipe() {
2175 let ast = r#"w = 20
2176l = 8
2177h = 10
2178
2179fn other_thing(@y) {
2180 return -y
2181}
2182
2183fn thing(@x) {
2184 return other_thing(x)
2185}
2186
2187firstExtrude = startSketchOn(XY)
2188 |> startProfile(at = [0,0])
2189 |> line(end = [0, l])
2190 |> line(end = [w, 0])
2191 |> line(end = [0, thing(8)])
2192 |> close()
2193 |> extrude(length = h)"#;
2194
2195 parse_execute(ast).await.unwrap();
2196 }
2197
2198 #[tokio::test(flavor = "multi_thread")]
2199 async fn test_execute_with_function_sketch() {
2200 let ast = r#"fn box(h, l, w) {
2201 myBox = startSketchOn(XY)
2202 |> startProfile(at = [0,0])
2203 |> line(end = [0, l])
2204 |> line(end = [w, 0])
2205 |> line(end = [0, -l])
2206 |> close()
2207 |> extrude(length = h)
2208
2209 return myBox
2210}
2211
2212fnBox = box(h = 3, l = 6, w = 10)"#;
2213
2214 parse_execute(ast).await.unwrap();
2215 }
2216
2217 #[tokio::test(flavor = "multi_thread")]
2218 async fn test_get_member_of_object_with_function_period() {
2219 let ast = r#"fn box(@obj) {
2220 myBox = startSketchOn(XY)
2221 |> startProfile(at = obj.start)
2222 |> line(end = [0, obj.l])
2223 |> line(end = [obj.w, 0])
2224 |> line(end = [0, -obj.l])
2225 |> close()
2226 |> extrude(length = obj.h)
2227
2228 return myBox
2229}
2230
2231thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2232"#;
2233 parse_execute(ast).await.unwrap();
2234 }
2235
2236 #[tokio::test(flavor = "multi_thread")]
2237 #[ignore] async fn test_object_member_starting_pipeline() {
2239 let ast = r#"
2240fn test2() {
2241 return {
2242 thing: startSketchOn(XY)
2243 |> startProfile(at = [0, 0])
2244 |> line(end = [0, 1])
2245 |> line(end = [1, 0])
2246 |> line(end = [0, -1])
2247 |> close()
2248 }
2249}
2250
2251x2 = test2()
2252
2253x2.thing
2254 |> extrude(length = 10)
2255"#;
2256 parse_execute(ast).await.unwrap();
2257 }
2258
2259 #[tokio::test(flavor = "multi_thread")]
2260 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
2262 let ast = r#"fn box(obj) {
2263let myBox = startSketchOn(XY)
2264 |> startProfile(at = obj.start)
2265 |> line(end = [0, obj.l])
2266 |> line(end = [obj.w, 0])
2267 |> line(end = [0, -obj.l])
2268 |> close()
2269 |> extrude(length = obj.h)
2270
2271 return myBox
2272}
2273
2274for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2275 thisBox = box(var)
2276}"#;
2277
2278 parse_execute(ast).await.unwrap();
2279 }
2280
2281 #[tokio::test(flavor = "multi_thread")]
2282 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
2284 let ast = r#"fn box(h, l, w, start) {
2285 myBox = startSketchOn(XY)
2286 |> startProfile(at = [0,0])
2287 |> line(end = [0, l])
2288 |> line(end = [w, 0])
2289 |> line(end = [0, -l])
2290 |> close()
2291 |> extrude(length = h)
2292
2293 return myBox
2294}
2295
2296
2297for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2298 const thisBox = box(var[0], var[1], var[2], var[3])
2299}"#;
2300
2301 parse_execute(ast).await.unwrap();
2302 }
2303
2304 #[tokio::test(flavor = "multi_thread")]
2305 async fn test_get_member_of_array_with_function() {
2306 let ast = r#"fn box(@arr) {
2307 myBox =startSketchOn(XY)
2308 |> startProfile(at = arr[0])
2309 |> line(end = [0, arr[1]])
2310 |> line(end = [arr[2], 0])
2311 |> line(end = [0, -arr[1]])
2312 |> close()
2313 |> extrude(length = arr[3])
2314
2315 return myBox
2316}
2317
2318thisBox = box([[0,0], 6, 10, 3])
2319
2320"#;
2321 parse_execute(ast).await.unwrap();
2322 }
2323
2324 #[tokio::test(flavor = "multi_thread")]
2325 async fn test_function_cannot_access_future_definitions() {
2326 let ast = r#"
2327fn returnX() {
2328 // x shouldn't be defined yet.
2329 return x
2330}
2331
2332x = 5
2333
2334answer = returnX()"#;
2335
2336 let result = parse_execute(ast).await;
2337 let err = result.unwrap_err();
2338 assert_eq!(err.message(), "`x` is not defined");
2339 }
2340
2341 #[tokio::test(flavor = "multi_thread")]
2342 async fn test_override_prelude() {
2343 let text = "PI = 3.0";
2344 let result = parse_execute(text).await.unwrap();
2345 let errs = result.exec_state.errors();
2346 assert!(errs.is_empty());
2347 }
2348
2349 #[tokio::test(flavor = "multi_thread")]
2350 async fn type_aliases() {
2351 let text = r#"@settings(experimentalFeatures = allow)
2352type MyTy = [number; 2]
2353fn foo(@x: MyTy) {
2354 return x[0]
2355}
2356
2357foo([0, 1])
2358
2359type Other = MyTy | Helix
2360"#;
2361 let result = parse_execute(text).await.unwrap();
2362 let errs = result.exec_state.errors();
2363 assert!(errs.is_empty());
2364 }
2365
2366 #[tokio::test(flavor = "multi_thread")]
2367 async fn test_cannot_shebang_in_fn() {
2368 let ast = r#"
2369fn foo() {
2370 #!hello
2371 return true
2372}
2373
2374foo
2375"#;
2376
2377 let result = parse_execute(ast).await;
2378 let err = result.unwrap_err();
2379 assert_eq!(
2380 err,
2381 KclError::new_syntax(KclErrorDetails::new(
2382 "Unexpected token: #".to_owned(),
2383 vec![SourceRange::new(14, 15, ModuleId::default())],
2384 )),
2385 );
2386 }
2387
2388 #[tokio::test(flavor = "multi_thread")]
2389 async fn test_pattern_transform_function_cannot_access_future_definitions() {
2390 let ast = r#"
2391fn transform(@replicaId) {
2392 // x shouldn't be defined yet.
2393 scale = x
2394 return {
2395 translate = [0, 0, replicaId * 10],
2396 scale = [scale, 1, 0],
2397 }
2398}
2399
2400fn layer() {
2401 return startSketchOn(XY)
2402 |> circle( center= [0, 0], radius= 1, tag = $tag1)
2403 |> extrude(length = 10)
2404}
2405
2406x = 5
2407
2408// The 10 layers are replicas of each other, with a transform applied to each.
2409shape = layer() |> patternTransform(instances = 10, transform = transform)
2410"#;
2411
2412 let result = parse_execute(ast).await;
2413 let err = result.unwrap_err();
2414 assert_eq!(err.message(), "`x` is not defined",);
2415 }
2416
2417 #[tokio::test(flavor = "multi_thread")]
2420 async fn test_math_execute_with_functions() {
2421 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
2422 let result = parse_execute(ast).await.unwrap();
2423 assert_eq!(
2424 5.0,
2425 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2426 .as_f64()
2427 .unwrap()
2428 );
2429 }
2430
2431 #[tokio::test(flavor = "multi_thread")]
2432 async fn test_math_execute() {
2433 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
2434 let result = parse_execute(ast).await.unwrap();
2435 assert_eq!(
2436 7.4,
2437 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2438 .as_f64()
2439 .unwrap()
2440 );
2441 }
2442
2443 #[tokio::test(flavor = "multi_thread")]
2444 async fn test_math_execute_start_negative() {
2445 let ast = r#"myVar = -5 + 6"#;
2446 let result = parse_execute(ast).await.unwrap();
2447 assert_eq!(
2448 1.0,
2449 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2450 .as_f64()
2451 .unwrap()
2452 );
2453 }
2454
2455 #[tokio::test(flavor = "multi_thread")]
2456 async fn test_math_execute_with_pi() {
2457 let ast = r#"myVar = PI * 2"#;
2458 let result = parse_execute(ast).await.unwrap();
2459 assert_eq!(
2460 std::f64::consts::TAU,
2461 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2462 .as_f64()
2463 .unwrap()
2464 );
2465 }
2466
2467 #[tokio::test(flavor = "multi_thread")]
2468 async fn test_math_define_decimal_without_leading_zero() {
2469 let ast = r#"thing = .4 + 7"#;
2470 let result = parse_execute(ast).await.unwrap();
2471 assert_eq!(
2472 7.4,
2473 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
2474 .as_f64()
2475 .unwrap()
2476 );
2477 }
2478
2479 #[tokio::test(flavor = "multi_thread")]
2480 async fn pass_std_to_std() {
2481 let ast = r#"sketch001 = startSketchOn(XY)
2482profile001 = circle(sketch001, center = [0, 0], radius = 2)
2483extrude001 = extrude(profile001, length = 5)
2484extrudes = patternLinear3d(
2485 extrude001,
2486 instances = 3,
2487 distance = 5,
2488 axis = [1, 1, 0],
2489)
2490clone001 = map(extrudes, f = clone)
2491"#;
2492 parse_execute(ast).await.unwrap();
2493 }
2494
2495 #[tokio::test(flavor = "multi_thread")]
2496 async fn test_array_reduce_nested_array() {
2497 let code = r#"
2498fn id(@el, accum) { return accum }
2499
2500answer = reduce([], initial=[[[0,0]]], f=id)
2501"#;
2502 let result = parse_execute(code).await.unwrap();
2503 assert_eq!(
2504 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2505 KclValue::HomArray {
2506 value: vec![KclValue::HomArray {
2507 value: vec![KclValue::HomArray {
2508 value: vec![
2509 KclValue::Number {
2510 value: 0.0,
2511 ty: NumericType::default(),
2512 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2513 },
2514 KclValue::Number {
2515 value: 0.0,
2516 ty: NumericType::default(),
2517 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2518 }
2519 ],
2520 ty: RuntimeType::any(),
2521 }],
2522 ty: RuntimeType::any(),
2523 }],
2524 ty: RuntimeType::any(),
2525 }
2526 );
2527 }
2528
2529 #[tokio::test(flavor = "multi_thread")]
2530 async fn test_zero_param_fn() {
2531 let ast = r#"sigmaAllow = 35000 // psi
2532leg1 = 5 // inches
2533leg2 = 8 // inches
2534fn thickness() { return 0.56 }
2535
2536bracket = startSketchOn(XY)
2537 |> startProfile(at = [0,0])
2538 |> line(end = [0, leg1])
2539 |> line(end = [leg2, 0])
2540 |> line(end = [0, -thickness()])
2541 |> line(end = [-leg2 + thickness(), 0])
2542"#;
2543 parse_execute(ast).await.unwrap();
2544 }
2545
2546 #[tokio::test(flavor = "multi_thread")]
2547 async fn test_unary_operator_not_succeeds() {
2548 let ast = r#"
2549fn returnTrue() { return !false }
2550t = true
2551f = false
2552notTrue = !t
2553notFalse = !f
2554c = !!true
2555d = !returnTrue()
2556
2557assertIs(!false, error = "expected to pass")
2558
2559fn check(x) {
2560 assertIs(!x, error = "expected argument to be false")
2561 return true
2562}
2563check(x = false)
2564"#;
2565 let result = parse_execute(ast).await.unwrap();
2566 assert_eq!(
2567 false,
2568 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2569 .as_bool()
2570 .unwrap()
2571 );
2572 assert_eq!(
2573 true,
2574 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2575 .as_bool()
2576 .unwrap()
2577 );
2578 assert_eq!(
2579 true,
2580 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2581 .as_bool()
2582 .unwrap()
2583 );
2584 assert_eq!(
2585 false,
2586 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2587 .as_bool()
2588 .unwrap()
2589 );
2590 }
2591
2592 #[tokio::test(flavor = "multi_thread")]
2593 async fn test_unary_operator_not_on_non_bool_fails() {
2594 let code1 = r#"
2595// Yup, this is null.
2596myNull = 0 / 0
2597notNull = !myNull
2598"#;
2599 assert_eq!(
2600 parse_execute(code1).await.unwrap_err().message(),
2601 "Cannot apply unary operator ! to non-boolean value: a number",
2602 );
2603
2604 let code2 = "notZero = !0";
2605 assert_eq!(
2606 parse_execute(code2).await.unwrap_err().message(),
2607 "Cannot apply unary operator ! to non-boolean value: a number",
2608 );
2609
2610 let code3 = r#"
2611notEmptyString = !""
2612"#;
2613 assert_eq!(
2614 parse_execute(code3).await.unwrap_err().message(),
2615 "Cannot apply unary operator ! to non-boolean value: a string",
2616 );
2617
2618 let code4 = r#"
2619obj = { a = 1 }
2620notMember = !obj.a
2621"#;
2622 assert_eq!(
2623 parse_execute(code4).await.unwrap_err().message(),
2624 "Cannot apply unary operator ! to non-boolean value: a number",
2625 );
2626
2627 let code5 = "
2628a = []
2629notArray = !a";
2630 assert_eq!(
2631 parse_execute(code5).await.unwrap_err().message(),
2632 "Cannot apply unary operator ! to non-boolean value: an empty array",
2633 );
2634
2635 let code6 = "
2636x = {}
2637notObject = !x";
2638 assert_eq!(
2639 parse_execute(code6).await.unwrap_err().message(),
2640 "Cannot apply unary operator ! to non-boolean value: an object",
2641 );
2642
2643 let code7 = "
2644fn x() { return 1 }
2645notFunction = !x";
2646 let fn_err = parse_execute(code7).await.unwrap_err();
2647 assert!(
2650 fn_err
2651 .message()
2652 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2653 "Actual error: {fn_err:?}"
2654 );
2655
2656 let code8 = "
2657myTagDeclarator = $myTag
2658notTagDeclarator = !myTagDeclarator";
2659 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2660 assert!(
2663 tag_declarator_err
2664 .message()
2665 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2666 "Actual error: {tag_declarator_err:?}"
2667 );
2668
2669 let code9 = "
2670myTagDeclarator = $myTag
2671notTagIdentifier = !myTag";
2672 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2673 assert!(
2676 tag_identifier_err
2677 .message()
2678 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2679 "Actual error: {tag_identifier_err:?}"
2680 );
2681
2682 let code10 = "notPipe = !(1 |> 2)";
2683 assert_eq!(
2684 parse_execute(code10).await.unwrap_err(),
2687 KclError::new_syntax(KclErrorDetails::new(
2688 "Unexpected token: !".to_owned(),
2689 vec![SourceRange::new(10, 11, ModuleId::default())],
2690 ))
2691 );
2692
2693 let code11 = "
2694fn identity(x) { return x }
2695notPipeSub = 1 |> identity(!%))";
2696 assert_eq!(
2697 parse_execute(code11).await.unwrap_err(),
2700 KclError::new_syntax(KclErrorDetails::new(
2701 "There was an unexpected `!`. Try removing it.".to_owned(),
2702 vec![SourceRange::new(56, 57, ModuleId::default())],
2703 ))
2704 );
2705
2706 }
2710
2711 #[tokio::test(flavor = "multi_thread")]
2712 async fn test_start_sketch_on_invalid_kwargs() {
2713 let current_dir = std::env::current_dir().unwrap();
2714 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2715 let mut code = std::fs::read_to_string(&path).unwrap();
2716 assert_eq!(
2717 parse_execute(&code).await.unwrap_err().message(),
2718 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2719 );
2720
2721 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2722 code = std::fs::read_to_string(&path).unwrap();
2723
2724 assert_eq!(
2725 parse_execute(&code).await.unwrap_err().message(),
2726 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2727 );
2728
2729 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2730 code = std::fs::read_to_string(&path).unwrap();
2731
2732 assert_eq!(
2733 parse_execute(&code).await.unwrap_err().message(),
2734 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2735 );
2736
2737 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2738 code = std::fs::read_to_string(&path).unwrap();
2739
2740 assert_eq!(
2741 parse_execute(&code).await.unwrap_err().message(),
2742 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2743 );
2744
2745 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2746 code = std::fs::read_to_string(&path).unwrap();
2747
2748 assert_eq!(
2749 parse_execute(&code).await.unwrap_err().message(),
2750 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2751 );
2752 }
2753
2754 #[tokio::test(flavor = "multi_thread")]
2755 async fn test_math_negative_variable_in_binary_expression() {
2756 let ast = r#"sigmaAllow = 35000 // psi
2757width = 1 // inch
2758
2759p = 150 // lbs
2760distance = 6 // inches
2761FOS = 2
2762
2763leg1 = 5 // inches
2764leg2 = 8 // inches
2765
2766thickness_squared = distance * p * FOS * 6 / sigmaAllow
2767thickness = 0.56 // inches. App does not support square root function yet
2768
2769bracket = startSketchOn(XY)
2770 |> startProfile(at = [0,0])
2771 |> line(end = [0, leg1])
2772 |> line(end = [leg2, 0])
2773 |> line(end = [0, -thickness])
2774 |> line(end = [-leg2 + thickness, 0])
2775"#;
2776 parse_execute(ast).await.unwrap();
2777 }
2778
2779 #[tokio::test(flavor = "multi_thread")]
2780 async fn test_execute_function_no_return() {
2781 let ast = r#"fn test(@origin) {
2782 origin
2783}
2784
2785test([0, 0])
2786"#;
2787 let result = parse_execute(ast).await;
2788 assert!(result.is_err());
2789 assert!(result.unwrap_err().to_string().contains("undefined"));
2790 }
2791
2792 #[tokio::test(flavor = "multi_thread")]
2793 async fn test_max_stack_size_exceeded_error() {
2794 let ast = r#"
2795fn forever(@n) {
2796 return 1 + forever(n)
2797}
2798
2799forever(1)
2800"#;
2801 let result = parse_execute(ast).await;
2802 let err = result.unwrap_err();
2803 assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
2804 }
2805
2806 #[tokio::test(flavor = "multi_thread")]
2807 async fn test_math_doubly_nested_parens() {
2808 let ast = r#"sigmaAllow = 35000 // psi
2809width = 4 // inch
2810p = 150 // Force on shelf - lbs
2811distance = 6 // inches
2812FOS = 2
2813leg1 = 5 // inches
2814leg2 = 8 // inches
2815thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2816thickness = 0.32 // inches. App does not support square root function yet
2817bracket = startSketchOn(XY)
2818 |> startProfile(at = [0,0])
2819 |> line(end = [0, leg1])
2820 |> line(end = [leg2, 0])
2821 |> line(end = [0, -thickness])
2822 |> line(end = [-1 * leg2 + thickness, 0])
2823 |> line(end = [0, -1 * leg1 + thickness])
2824 |> close()
2825 |> extrude(length = width)
2826"#;
2827 parse_execute(ast).await.unwrap();
2828 }
2829
2830 #[tokio::test(flavor = "multi_thread")]
2831 async fn test_math_nested_parens_one_less() {
2832 let ast = r#" sigmaAllow = 35000 // psi
2833width = 4 // inch
2834p = 150 // Force on shelf - lbs
2835distance = 6 // inches
2836FOS = 2
2837leg1 = 5 // inches
2838leg2 = 8 // inches
2839thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2840thickness = 0.32 // inches. App does not support square root function yet
2841bracket = startSketchOn(XY)
2842 |> startProfile(at = [0,0])
2843 |> line(end = [0, leg1])
2844 |> line(end = [leg2, 0])
2845 |> line(end = [0, -thickness])
2846 |> line(end = [-1 * leg2 + thickness, 0])
2847 |> line(end = [0, -1 * leg1 + thickness])
2848 |> close()
2849 |> extrude(length = width)
2850"#;
2851 parse_execute(ast).await.unwrap();
2852 }
2853
2854 #[tokio::test(flavor = "multi_thread")]
2855 async fn test_fn_as_operand() {
2856 let ast = r#"fn f() { return 1 }
2857x = f()
2858y = x + 1
2859z = f() + 1
2860w = f() + f()
2861"#;
2862 parse_execute(ast).await.unwrap();
2863 }
2864
2865 #[tokio::test(flavor = "multi_thread")]
2866 async fn kcl_test_ids_stable_between_executions() {
2867 let code = r#"sketch001 = startSketchOn(XZ)
2868|> startProfile(at = [61.74, 206.13])
2869|> xLine(length = 305.11, tag = $seg01)
2870|> yLine(length = -291.85)
2871|> xLine(length = -segLen(seg01))
2872|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2873|> close()
2874|> extrude(length = 40.14)
2875|> shell(
2876 thickness = 3.14,
2877 faces = [seg01]
2878)
2879"#;
2880
2881 let ctx = crate::test_server::new_context(true, None).await.unwrap();
2882 let old_program = crate::Program::parse_no_errs(code).unwrap();
2883
2884 if let Err(err) = ctx.run_with_caching(old_program).await {
2886 let report = err.into_miette_report_with_outputs(code).unwrap();
2887 let report = miette::Report::new(report);
2888 panic!("Error executing program: {report:?}");
2889 }
2890
2891 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2893
2894 let code = r#"sketch001 = startSketchOn(XZ)
2895|> startProfile(at = [62.74, 206.13])
2896|> xLine(length = 305.11, tag = $seg01)
2897|> yLine(length = -291.85)
2898|> xLine(length = -segLen(seg01))
2899|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2900|> close()
2901|> extrude(length = 40.14)
2902|> shell(
2903 faces = [seg01],
2904 thickness = 3.14,
2905)
2906"#;
2907
2908 let program = crate::Program::parse_no_errs(code).unwrap();
2910 ctx.run_with_caching(program).await.unwrap();
2912
2913 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2914
2915 assert_eq!(id_generator, new_id_generator);
2916 }
2917
2918 #[tokio::test(flavor = "multi_thread")]
2919 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2920 let code = r#"sketch001 = startSketchOn(XZ)
2921|> startProfile(at = [61.74, 206.13])
2922|> xLine(length = 305.11, tag = $seg01)
2923|> yLine(length = -291.85)
2924|> xLine(length = -segLen(seg01))
2925|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2926|> close()
2927|> extrude(length = 40.14)
2928|> shell(
2929 thickness = 3.14,
2930 faces = [seg01]
2931)
2932"#;
2933
2934 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2935 let old_program = crate::Program::parse_no_errs(code).unwrap();
2936
2937 ctx.run_with_caching(old_program.clone()).await.unwrap();
2939
2940 let settings_state = cache::read_old_ast().await.unwrap().settings;
2941
2942 assert_eq!(settings_state, ctx.settings);
2944
2945 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2947
2948 ctx.run_with_caching(old_program.clone()).await.unwrap();
2950
2951 let settings_state = cache::read_old_ast().await.unwrap().settings;
2952
2953 assert_eq!(settings_state, ctx.settings);
2955
2956 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2958
2959 ctx.run_with_caching(old_program).await.unwrap();
2961
2962 let settings_state = cache::read_old_ast().await.unwrap().settings;
2963
2964 assert_eq!(settings_state, ctx.settings);
2966
2967 ctx.close().await;
2968 }
2969
2970 #[tokio::test(flavor = "multi_thread")]
2971 async fn mock_after_not_mock() {
2972 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2973 let program = crate::Program::parse_no_errs("x = 2").unwrap();
2974 let result = ctx.run_with_caching(program).await.unwrap();
2975 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2976
2977 let ctx2 = ExecutorContext::new_mock(None).await;
2978 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2979 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
2980 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2981
2982 ctx.close().await;
2983 ctx2.close().await;
2984 }
2985
2986 #[tokio::test(flavor = "multi_thread")]
2987 async fn mock_then_add_extrude_then_mock_again() {
2988 let code = "@settings(experimentalFeatures = allow)
2989
2990s = sketch(on = XY) {
2991 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
2992 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
2993 coincident([line1.end, line2.start])
2994 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
2995 coincident([line2.end, line3.start])
2996 coincident([line1.start, line3.end])
2997}
2998 ";
2999 let ctx = ExecutorContext::new_mock(None).await;
3000 let program = crate::Program::parse_no_errs(code).unwrap();
3001 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3002 assert!(result.variables.contains_key("s"), "actual: {:?}", &result.variables);
3003
3004 let code2 = code.to_owned()
3005 + "
3006region001 = region(point = [1mm, 1mm], sketch = s)
3007extrude001 = extrude(region001, length = 1)
3008 ";
3009 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3010 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
3011 assert!(
3012 result.variables.contains_key("region001"),
3013 "actual: {:?}",
3014 &result.variables
3015 );
3016
3017 ctx.close().await;
3018 }
3019
3020 #[cfg(feature = "artifact-graph")]
3021 #[tokio::test(flavor = "multi_thread")]
3022 async fn mock_has_stable_ids() {
3023 let ctx = ExecutorContext::new_mock(None).await;
3024 let mock_config = MockConfig {
3025 use_prev_memory: false,
3026 ..Default::default()
3027 };
3028 let code = "sk = startSketchOn(XY)
3029 |> startProfile(at = [0, 0])";
3030 let program = crate::Program::parse_no_errs(code).unwrap();
3031 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
3032 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3033 assert!(!ids.is_empty(), "IDs should not be empty");
3034
3035 let ctx2 = ExecutorContext::new_mock(None).await;
3036 let program2 = crate::Program::parse_no_errs(code).unwrap();
3037 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
3038 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3039
3040 assert_eq!(ids, ids2, "Generated IDs should match");
3041 ctx.close().await;
3042 ctx2.close().await;
3043 }
3044
3045 #[cfg(feature = "artifact-graph")]
3046 #[tokio::test(flavor = "multi_thread")]
3047 async fn sim_sketch_mode_real_mock_real() {
3048 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3049 let code = r#"sketch001 = startSketchOn(XY)
3050profile001 = startProfile(sketch001, at = [0, 0])
3051 |> line(end = [10, 0])
3052 |> line(end = [0, 10])
3053 |> line(end = [-10, 0])
3054 |> line(end = [0, -10])
3055 |> close()
3056"#;
3057 let program = crate::Program::parse_no_errs(code).unwrap();
3058 let result = ctx.run_with_caching(program).await.unwrap();
3059 assert_eq!(result.operations.len(), 1);
3060
3061 let mock_ctx = ExecutorContext::new_mock(None).await;
3062 let mock_program = crate::Program::parse_no_errs(code).unwrap();
3063 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
3064 assert_eq!(mock_result.operations.len(), 1);
3065
3066 let code2 = code.to_owned()
3067 + r#"
3068extrude001 = extrude(profile001, length = 10)
3069"#;
3070 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3071 let result = ctx.run_with_caching(program2).await.unwrap();
3072 assert_eq!(result.operations.len(), 2);
3073
3074 ctx.close().await;
3075 mock_ctx.close().await;
3076 }
3077
3078 #[tokio::test(flavor = "multi_thread")]
3079 async fn read_tag_version() {
3080 let ast = r#"fn bar(@t) {
3081 return startSketchOn(XY)
3082 |> startProfile(at = [0,0])
3083 |> angledLine(
3084 angle = -60,
3085 length = segLen(t),
3086 )
3087 |> line(end = [0, 0])
3088 |> close()
3089}
3090
3091sketch = startSketchOn(XY)
3092 |> startProfile(at = [0,0])
3093 |> line(end = [0, 10])
3094 |> line(end = [10, 0], tag = $tag0)
3095 |> line(endAbsolute = [0, 0])
3096
3097fn foo() {
3098 // tag0 tags an edge
3099 return bar(tag0)
3100}
3101
3102solid = sketch |> extrude(length = 10)
3103// tag0 tags a face
3104sketch2 = startSketchOn(solid, face = tag0)
3105 |> startProfile(at = [0,0])
3106 |> line(end = [0, 1])
3107 |> line(end = [1, 0])
3108 |> line(end = [0, 0])
3109
3110foo() |> extrude(length = 1)
3111"#;
3112 parse_execute(ast).await.unwrap();
3113 }
3114
3115 #[tokio::test(flavor = "multi_thread")]
3116 async fn experimental() {
3117 let code = r#"
3118startSketchOn(XY)
3119 |> startProfile(at = [0, 0], tag = $start)
3120 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3121"#;
3122 let result = parse_execute(code).await.unwrap();
3123 let errors = result.exec_state.errors();
3124 assert_eq!(errors.len(), 1);
3125 assert_eq!(errors[0].severity, Severity::Error);
3126 let msg = &errors[0].message;
3127 assert!(msg.contains("experimental"), "found {msg}");
3128
3129 let code = r#"@settings(experimentalFeatures = allow)
3130startSketchOn(XY)
3131 |> startProfile(at = [0, 0], tag = $start)
3132 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3133"#;
3134 let result = parse_execute(code).await.unwrap();
3135 let errors = result.exec_state.errors();
3136 assert!(errors.is_empty());
3137
3138 let code = r#"@settings(experimentalFeatures = warn)
3139startSketchOn(XY)
3140 |> startProfile(at = [0, 0], tag = $start)
3141 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3142"#;
3143 let result = parse_execute(code).await.unwrap();
3144 let errors = result.exec_state.errors();
3145 assert_eq!(errors.len(), 1);
3146 assert_eq!(errors[0].severity, Severity::Warning);
3147 let msg = &errors[0].message;
3148 assert!(msg.contains("experimental"), "found {msg}");
3149
3150 let code = r#"@settings(experimentalFeatures = deny)
3151startSketchOn(XY)
3152 |> startProfile(at = [0, 0], tag = $start)
3153 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3154"#;
3155 let result = parse_execute(code).await.unwrap();
3156 let errors = result.exec_state.errors();
3157 assert_eq!(errors.len(), 1);
3158 assert_eq!(errors[0].severity, Severity::Error);
3159 let msg = &errors[0].message;
3160 assert!(msg.contains("experimental"), "found {msg}");
3161
3162 let code = r#"@settings(experimentalFeatures = foo)
3163startSketchOn(XY)
3164 |> startProfile(at = [0, 0], tag = $start)
3165 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3166"#;
3167 parse_execute(code).await.unwrap_err();
3168 }
3169
3170 #[tokio::test(flavor = "multi_thread")]
3171 async fn experimental_parameter() {
3172 let code = r#"
3173fn inc(@x, @(experimental = true) amount? = 1) {
3174 return x + amount
3175}
3176
3177answer = inc(5, amount = 2)
3178"#;
3179 let result = parse_execute(code).await.unwrap();
3180 let errors = result.exec_state.errors();
3181 assert_eq!(errors.len(), 1);
3182 assert_eq!(errors[0].severity, Severity::Error);
3183 let msg = &errors[0].message;
3184 assert!(msg.contains("experimental"), "found {msg}");
3185
3186 let code = r#"
3188fn inc(@x, @(experimental = true) amount? = 1) {
3189 return x + amount
3190}
3191
3192answer = inc(5)
3193"#;
3194 let result = parse_execute(code).await.unwrap();
3195 let errors = result.exec_state.errors();
3196 assert_eq!(errors.len(), 0);
3197 }
3198
3199 #[tokio::test(flavor = "multi_thread")]
3203 async fn test_tangent_line_arc_executes_with_mock_engine() {
3204 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
3205 parse_execute(&code).await.unwrap();
3206 }
3207
3208 #[tokio::test(flavor = "multi_thread")]
3209 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
3210 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
3211 parse_execute(&code).await.unwrap();
3212 }
3213
3214 #[tokio::test(flavor = "multi_thread")]
3215 async fn test_tangent_line_circle_executes_with_mock_engine() {
3216 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
3217 parse_execute(&code).await.unwrap();
3218 }
3219
3220 #[tokio::test(flavor = "multi_thread")]
3221 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
3222 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
3223 parse_execute(&code).await.unwrap();
3224 }
3225
3226 }