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::CompilationIssue;
66use crate::ExecError;
67use crate::KclErrorWithOutputs;
68use crate::NodePath;
69use crate::SourceRange;
70#[cfg(feature = "artifact-graph")]
71use crate::collections::AhashIndexSet;
72use crate::engine::EngineManager;
73use crate::engine::GridScaleBehavior;
74use crate::errors::KclError;
75use crate::errors::KclErrorDetails;
76use crate::execution::cache::CacheInformation;
77use crate::execution::cache::CacheResult;
78use crate::execution::import_graph::Universe;
79use crate::execution::import_graph::UniverseMap;
80use crate::execution::typed_path::TypedPath;
81#[cfg(feature = "artifact-graph")]
82use crate::front::Number;
83use crate::front::Object;
84use crate::front::ObjectId;
85use crate::fs::FileManager;
86use crate::modules::ModuleExecutionOutcome;
87use crate::modules::ModuleId;
88use crate::modules::ModulePath;
89use crate::modules::ModuleRepr;
90use crate::parsing::ast::types::Expr;
91use crate::parsing::ast::types::ImportPath;
92use crate::parsing::ast::types::NodeRef;
93
94pub(crate) mod annotations;
95#[cfg(feature = "artifact-graph")]
96mod artifact;
97pub(crate) mod cache;
98mod cad_op;
99mod exec_ast;
100pub mod fn_call;
101#[cfg(test)]
102#[cfg(feature = "artifact-graph")]
103mod freedom_analysis_tests;
104mod geometry;
105mod id_generator;
106mod import;
107mod import_graph;
108pub(crate) mod kcl_value;
109mod memory;
110mod modeling;
111mod sketch_solve;
112mod sketch_transpiler;
113mod state;
114pub mod typed_path;
115pub(crate) mod types;
116
117pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
118pub(crate) const SKETCH_OBJECT_META: &str = "meta";
119pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
120
121macro_rules! control_continue {
126 ($control_flow:expr) => {{
127 let cf = $control_flow;
128 if cf.is_some_return() {
129 return Ok(cf);
130 } else {
131 cf.into_value()
132 }
133 }};
134}
135pub(crate) use control_continue;
137
138macro_rules! early_return {
143 ($control_flow:expr) => {{
144 let cf = $control_flow;
145 if cf.is_some_return() {
146 return Err(EarlyReturn::from(cf));
147 } else {
148 cf.into_value()
149 }
150 }};
151}
152pub(crate) use early_return;
154
155#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
156pub enum ControlFlowKind {
157 #[default]
158 Continue,
159 Exit,
160}
161
162impl ControlFlowKind {
163 pub fn is_some_return(&self) -> bool {
165 match self {
166 ControlFlowKind::Continue => false,
167 ControlFlowKind::Exit => true,
168 }
169 }
170}
171
172#[must_use = "You should always handle the control flow value when it is returned"]
173#[derive(Debug, Clone, PartialEq, Serialize)]
174pub struct KclValueControlFlow {
175 value: KclValue,
177 pub control: ControlFlowKind,
178}
179
180impl KclValue {
181 pub(crate) fn continue_(self) -> KclValueControlFlow {
182 KclValueControlFlow {
183 value: self,
184 control: ControlFlowKind::Continue,
185 }
186 }
187
188 pub(crate) fn exit(self) -> KclValueControlFlow {
189 KclValueControlFlow {
190 value: self,
191 control: ControlFlowKind::Exit,
192 }
193 }
194}
195
196impl KclValueControlFlow {
197 pub fn is_some_return(&self) -> bool {
199 self.control.is_some_return()
200 }
201
202 pub(crate) fn into_value(self) -> KclValue {
203 self.value
204 }
205}
206
207#[must_use = "You should always handle the control flow value when it is returned"]
214#[derive(Debug, Clone)]
215pub(crate) enum EarlyReturn {
216 Value(KclValueControlFlow),
218 Error(KclError),
220}
221
222impl From<KclValueControlFlow> for EarlyReturn {
223 fn from(cf: KclValueControlFlow) -> Self {
224 EarlyReturn::Value(cf)
225 }
226}
227
228impl From<KclError> for EarlyReturn {
229 fn from(err: KclError) -> Self {
230 EarlyReturn::Error(err)
231 }
232}
233
234pub(crate) enum StatementKind<'a> {
235 Declaration { name: &'a str },
236 Expression,
237}
238
239#[derive(Debug, Clone, Copy)]
240pub enum PreserveMem {
241 Normal,
242 Always,
243}
244
245impl PreserveMem {
246 fn normal(self) -> bool {
247 match self {
248 PreserveMem::Normal => true,
249 PreserveMem::Always => false,
250 }
251 }
252}
253
254#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
256#[ts(export)]
257#[serde(rename_all = "camelCase")]
258pub struct ExecOutcome {
259 pub variables: IndexMap<String, KclValue>,
261 #[cfg(feature = "artifact-graph")]
264 pub operations: Vec<Operation>,
265 #[cfg(feature = "artifact-graph")]
267 pub artifact_graph: ArtifactGraph,
268 #[cfg(feature = "artifact-graph")]
270 #[serde(skip)]
271 pub scene_objects: Vec<Object>,
272 #[cfg(feature = "artifact-graph")]
275 #[serde(skip)]
276 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
277 #[cfg(feature = "artifact-graph")]
278 #[serde(skip)]
279 pub var_solutions: Vec<(SourceRange, Number)>,
280 pub issues: Vec<CompilationIssue>,
282 pub filenames: IndexMap<ModuleId, ModulePath>,
284 pub default_planes: Option<DefaultPlanes>,
286}
287
288#[derive(Debug, Clone, Copy, PartialEq)]
292enum SegmentFreedom {
293 Free,
294 Fixed,
295 Conflict,
296 Error,
298}
299
300impl From<crate::front::Freedom> for SegmentFreedom {
301 fn from(f: crate::front::Freedom) -> Self {
302 match f {
303 crate::front::Freedom::Free => Self::Free,
304 crate::front::Freedom::Fixed => Self::Fixed,
305 crate::front::Freedom::Conflict => Self::Conflict,
306 }
307 }
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
312pub enum ConstraintKind {
313 FullyConstrained,
314 UnderConstrained,
315 OverConstrained,
316 Error,
320}
321
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
329pub struct SketchConstraintStatus {
330 pub name: String,
332 pub status: ConstraintKind,
334 pub free_count: usize,
336 pub conflict_count: usize,
338 pub total_count: usize,
340}
341
342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344pub struct SketchConstraintReport {
345 pub fully_constrained: Vec<SketchConstraintStatus>,
346 pub under_constrained: Vec<SketchConstraintStatus>,
347 pub over_constrained: Vec<SketchConstraintStatus>,
348 pub errors: Vec<SketchConstraintStatus>,
351}
352
353impl ExecOutcome {
354 pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
355 #[cfg(feature = "artifact-graph")]
356 {
357 debug_assert!(
358 id.0 < self.scene_objects.len(),
359 "Requested object ID {} but only have {} objects",
360 id.0,
361 self.scene_objects.len()
362 );
363 self.scene_objects.get(id.0)
364 }
365 #[cfg(not(feature = "artifact-graph"))]
366 {
367 let _ = id;
368 None
369 }
370 }
371
372 pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
374 self.issues.iter().filter(|error| error.is_err())
375 }
376
377 #[cfg(feature = "artifact-graph")]
384 pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
385 use crate::front::ObjectKind;
386 use crate::front::Segment;
387
388 let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
390 let obj = self.scene_objects.get(id.0)?;
391 if let ObjectKind::Segment {
392 segment: Segment::Point(p),
393 } = &obj.kind
394 {
395 Some(p.freedom())
396 } else {
397 None
398 }
399 };
400
401 let mut fully_constrained = Vec::new();
402 let mut under_constrained = Vec::new();
403 let mut over_constrained = Vec::new();
404 let mut errors = Vec::new();
405
406 for obj in &self.scene_objects {
407 let ObjectKind::Sketch(sketch) = &obj.kind else {
408 continue;
409 };
410
411 let mut free_count: usize = 0;
412 let mut conflict_count: usize = 0;
413 let mut error_count: usize = 0;
414 let mut total_count: usize = 0;
415
416 for &seg_id in &sketch.segments {
417 let Some(seg_obj) = self.scene_objects.get(seg_id.0) else {
418 continue;
419 };
420 let ObjectKind::Segment { segment } = &seg_obj.kind else {
421 continue;
422 };
423 if let Segment::Point(p) = segment
426 && p.owner.is_some()
427 {
428 continue;
429 }
430 let freedom = segment
431 .freedom(lookup)
432 .map(SegmentFreedom::from)
433 .unwrap_or(SegmentFreedom::Error);
434 total_count += 1;
435 match freedom {
436 SegmentFreedom::Free => free_count += 1,
437 SegmentFreedom::Conflict => conflict_count += 1,
438 SegmentFreedom::Error => error_count += 1,
439 SegmentFreedom::Fixed => {}
440 }
441 }
442
443 let status = if error_count > 0 {
449 ConstraintKind::Error
450 } else if conflict_count > 0 {
451 ConstraintKind::OverConstrained
452 } else if free_count > 0 {
453 ConstraintKind::UnderConstrained
454 } else {
455 ConstraintKind::FullyConstrained
456 };
457
458 let entry = SketchConstraintStatus {
459 name: obj.label.clone(),
460 status,
461 free_count,
462 conflict_count,
463 total_count,
464 };
465
466 match status {
467 ConstraintKind::FullyConstrained => fully_constrained.push(entry),
468 ConstraintKind::UnderConstrained => under_constrained.push(entry),
469 ConstraintKind::OverConstrained => over_constrained.push(entry),
470 ConstraintKind::Error => errors.push(entry),
471 }
472 }
473
474 SketchConstraintReport {
475 fully_constrained,
476 under_constrained,
477 over_constrained,
478 errors,
479 }
480 }
481}
482
483#[derive(Debug, Clone, PartialEq, Eq)]
485pub struct MockConfig {
486 pub use_prev_memory: bool,
487 pub sketch_block_id: Option<ObjectId>,
490 pub freedom_analysis: bool,
493 #[cfg(feature = "artifact-graph")]
495 pub segment_ids_edited: AhashIndexSet<ObjectId>,
496}
497
498impl Default for MockConfig {
499 fn default() -> Self {
500 Self {
501 use_prev_memory: true,
503 sketch_block_id: None,
504 freedom_analysis: true,
505 #[cfg(feature = "artifact-graph")]
506 segment_ids_edited: AhashIndexSet::default(),
507 }
508 }
509}
510
511impl MockConfig {
512 pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
514 Self {
515 sketch_block_id: Some(sketch_block_id),
516 ..Default::default()
517 }
518 }
519
520 #[must_use]
521 pub(crate) fn no_freedom_analysis(mut self) -> Self {
522 self.freedom_analysis = false;
523 self
524 }
525}
526
527#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
528#[ts(export)]
529#[serde(rename_all = "camelCase")]
530pub struct DefaultPlanes {
531 pub xy: uuid::Uuid,
532 pub xz: uuid::Uuid,
533 pub yz: uuid::Uuid,
534 pub neg_xy: uuid::Uuid,
535 pub neg_xz: uuid::Uuid,
536 pub neg_yz: uuid::Uuid,
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
540#[ts(export)]
541#[serde(tag = "type", rename_all = "camelCase")]
542pub struct TagIdentifier {
543 pub value: String,
544 #[serde(skip)]
547 pub info: Vec<(usize, TagEngineInfo)>,
548 #[serde(skip)]
549 pub meta: Vec<Metadata>,
550}
551
552impl TagIdentifier {
553 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
555 for (e, info) in self.info.iter().rev() {
556 if *e <= at_epoch {
557 return Some(info);
558 }
559 }
560
561 None
562 }
563
564 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
566 self.info.last().map(|i| &i.1)
567 }
568
569 pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
572 let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
573 return vec![];
574 };
575 self.info
576 .iter()
577 .rev()
578 .take_while(|(e, _)| *e == cur_epoch)
579 .map(|(_, info)| info)
580 .collect()
581 }
582
583 pub fn merge_info(&mut self, other: &TagIdentifier) {
585 assert_eq!(&self.value, &other.value);
586 for (oe, ot) in &other.info {
587 if let Some((e, t)) = self.info.last_mut() {
588 if *e > *oe {
590 continue;
591 }
592 if e == oe {
594 *t = ot.clone();
595 continue;
596 }
597 }
598 self.info.push((*oe, ot.clone()));
599 }
600 }
601
602 pub fn geometry(&self) -> Option<Geometry> {
603 self.get_cur_info().map(|info| info.geometry.clone())
604 }
605}
606
607impl Eq for TagIdentifier {}
608
609impl std::fmt::Display for TagIdentifier {
610 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611 write!(f, "{}", self.value)
612 }
613}
614
615impl std::str::FromStr for TagIdentifier {
616 type Err = KclError;
617
618 fn from_str(s: &str) -> Result<Self, Self::Err> {
619 Ok(Self {
620 value: s.to_string(),
621 info: Vec::new(),
622 meta: Default::default(),
623 })
624 }
625}
626
627impl Ord for TagIdentifier {
628 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
629 self.value.cmp(&other.value)
630 }
631}
632
633impl PartialOrd for TagIdentifier {
634 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
635 Some(self.cmp(other))
636 }
637}
638
639impl std::hash::Hash for TagIdentifier {
640 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
641 self.value.hash(state);
642 }
643}
644
645#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
647#[ts(export)]
648#[serde(tag = "type", rename_all = "camelCase")]
649pub struct TagEngineInfo {
650 pub id: uuid::Uuid,
652 pub geometry: Geometry,
654 pub path: Option<Path>,
656 pub surface: Option<ExtrudeSurface>,
658}
659
660#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
661pub enum BodyType {
662 Root,
663 Block,
664}
665
666#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
668#[ts(export)]
669#[serde(rename_all = "camelCase")]
670pub struct Metadata {
671 pub source_range: SourceRange,
673}
674
675impl From<Metadata> for Vec<SourceRange> {
676 fn from(meta: Metadata) -> Self {
677 vec![meta.source_range]
678 }
679}
680
681impl From<&Metadata> for SourceRange {
682 fn from(meta: &Metadata) -> Self {
683 meta.source_range
684 }
685}
686
687impl From<SourceRange> for Metadata {
688 fn from(source_range: SourceRange) -> Self {
689 Self { source_range }
690 }
691}
692
693impl<T> From<NodeRef<'_, T>> for Metadata {
694 fn from(node: NodeRef<'_, T>) -> Self {
695 Self {
696 source_range: SourceRange::new(node.start, node.end, node.module_id),
697 }
698 }
699}
700
701impl From<&Expr> for Metadata {
702 fn from(expr: &Expr) -> Self {
703 Self {
704 source_range: SourceRange::from(expr),
705 }
706 }
707}
708
709impl Metadata {
710 pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
711 if meta.len() == 1 {
712 let meta = &meta[0];
713 return crate::front::SourceRef::Simple {
714 range: meta.source_range,
715 node_path,
716 };
717 }
718 crate::front::SourceRef::BackTrace {
719 ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
720 }
721 }
722}
723
724#[derive(PartialEq, Debug, Default, Clone)]
726pub enum ContextType {
727 #[default]
729 Live,
730
731 Mock,
735
736 MockCustomForwarded,
738}
739
740#[derive(Debug, Clone)]
744pub struct ExecutorContext {
745 pub engine: Arc<Box<dyn EngineManager>>,
746 pub fs: Arc<FileManager>,
747 pub settings: ExecutorSettings,
748 pub context_type: ContextType,
749}
750
751#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
753#[ts(export)]
754pub struct ExecutorSettings {
755 pub highlight_edges: bool,
757 pub enable_ssao: bool,
759 pub show_grid: bool,
761 pub replay: Option<String>,
764 pub project_directory: Option<TypedPath>,
767 pub current_file: Option<TypedPath>,
770 pub fixed_size_grid: bool,
772}
773
774impl Default for ExecutorSettings {
775 fn default() -> Self {
776 Self {
777 highlight_edges: true,
778 enable_ssao: false,
779 show_grid: false,
780 replay: None,
781 project_directory: None,
782 current_file: None,
783 fixed_size_grid: true,
784 }
785 }
786}
787
788impl From<crate::settings::types::Configuration> for ExecutorSettings {
789 fn from(config: crate::settings::types::Configuration) -> Self {
790 Self::from(config.settings)
791 }
792}
793
794impl From<crate::settings::types::Settings> for ExecutorSettings {
795 fn from(settings: crate::settings::types::Settings) -> Self {
796 Self {
797 highlight_edges: settings.modeling.highlight_edges.into(),
798 enable_ssao: settings.modeling.enable_ssao.into(),
799 show_grid: settings.modeling.show_scale_grid,
800 replay: None,
801 project_directory: None,
802 current_file: None,
803 fixed_size_grid: settings.modeling.fixed_size_grid,
804 }
805 }
806}
807
808impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
809 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
810 Self::from(config.settings.modeling)
811 }
812}
813
814impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
815 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
816 Self {
817 highlight_edges: modeling.highlight_edges.into(),
818 enable_ssao: modeling.enable_ssao.into(),
819 show_grid: modeling.show_scale_grid,
820 replay: None,
821 project_directory: None,
822 current_file: None,
823 fixed_size_grid: true,
824 }
825 }
826}
827
828impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
829 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
830 Self {
831 highlight_edges: modeling.highlight_edges.into(),
832 enable_ssao: modeling.enable_ssao.into(),
833 show_grid: Default::default(),
834 replay: None,
835 project_directory: None,
836 current_file: None,
837 fixed_size_grid: true,
838 }
839 }
840}
841
842impl ExecutorSettings {
843 pub fn with_current_file(&mut self, current_file: TypedPath) {
845 if current_file.extension() == Some("kcl") {
847 self.current_file = Some(current_file.clone());
848 if let Some(parent) = current_file.parent() {
850 self.project_directory = Some(parent);
851 } else {
852 self.project_directory = Some(TypedPath::from(""));
853 }
854 } else {
855 self.project_directory = Some(current_file);
856 }
857 }
858}
859
860impl ExecutorContext {
861 pub fn new_with_engine_and_fs(
863 engine: Arc<Box<dyn EngineManager>>,
864 fs: Arc<FileManager>,
865 settings: ExecutorSettings,
866 ) -> Self {
867 ExecutorContext {
868 engine,
869 fs,
870 settings,
871 context_type: ContextType::Live,
872 }
873 }
874
875 #[cfg(not(target_arch = "wasm32"))]
877 pub fn new_with_engine(engine: Arc<Box<dyn EngineManager>>, settings: ExecutorSettings) -> Self {
878 Self::new_with_engine_and_fs(engine, Arc::new(FileManager::new()), settings)
879 }
880
881 #[cfg(not(target_arch = "wasm32"))]
883 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
884 let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
885 let (ws, _headers) = client
886 .modeling()
887 .commands_ws(kittycad::modeling::CommandsWsParams {
888 api_call_id: None,
889 fps: None,
890 order_independent_transparency: None,
891 post_effect: if settings.enable_ssao {
892 Some(kittycad::types::PostEffectType::Ssao)
893 } else {
894 None
895 },
896 replay: settings.replay.clone(),
897 show_grid: if settings.show_grid { Some(true) } else { None },
898 pool: None,
899 pr,
900 unlocked_framerate: None,
901 webrtc: Some(false),
902 video_res_width: None,
903 video_res_height: None,
904 })
905 .await?;
906
907 let engine: Arc<Box<dyn EngineManager>> =
908 Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
909
910 Ok(Self::new_with_engine(engine, settings))
911 }
912
913 #[cfg(target_arch = "wasm32")]
914 pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
915 Self::new_with_engine_and_fs(engine, fs, settings)
916 }
917
918 #[cfg(not(target_arch = "wasm32"))]
919 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
920 ExecutorContext {
921 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
922 fs: Arc::new(FileManager::new()),
923 settings: settings.unwrap_or_default(),
924 context_type: ContextType::Mock,
925 }
926 }
927
928 #[cfg(target_arch = "wasm32")]
929 pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
930 ExecutorContext {
931 engine,
932 fs,
933 settings,
934 context_type: ContextType::Mock,
935 }
936 }
937
938 #[cfg(target_arch = "wasm32")]
941 pub fn new_mock_for_lsp(
942 fs_manager: crate::fs::wasm::FileSystemManager,
943 settings: ExecutorSettings,
944 ) -> Result<Self, String> {
945 use crate::mock_engine;
946
947 let mock_engine = Arc::new(Box::new(
948 mock_engine::EngineConnection::new().map_err(|e| format!("Failed to create mock engine: {:?}", e))?,
949 ) as Box<dyn EngineManager>);
950
951 let fs = Arc::new(FileManager::new(fs_manager));
952
953 Ok(ExecutorContext {
954 engine: mock_engine,
955 fs,
956 settings,
957 context_type: ContextType::Mock,
958 })
959 }
960
961 #[cfg(not(target_arch = "wasm32"))]
962 pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
963 ExecutorContext {
964 engine,
965 fs: Arc::new(FileManager::new()),
966 settings: Default::default(),
967 context_type: ContextType::MockCustomForwarded,
968 }
969 }
970
971 #[cfg(not(target_arch = "wasm32"))]
977 pub async fn new_with_client(
978 settings: ExecutorSettings,
979 token: Option<String>,
980 engine_addr: Option<String>,
981 ) -> Result<Self> {
982 let client = crate::engine::new_zoo_client(token, engine_addr)?;
984
985 let ctx = Self::new(&client, settings).await?;
986 Ok(ctx)
987 }
988
989 #[cfg(not(target_arch = "wasm32"))]
994 pub async fn new_with_default_client() -> Result<Self> {
995 let ctx = Self::new_with_client(Default::default(), None, None).await?;
997 Ok(ctx)
998 }
999
1000 #[cfg(not(target_arch = "wasm32"))]
1002 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
1003 let ctx = ExecutorContext::new_with_client(
1004 ExecutorSettings {
1005 highlight_edges: true,
1006 enable_ssao: false,
1007 show_grid: false,
1008 replay: None,
1009 project_directory: None,
1010 current_file: None,
1011 fixed_size_grid: false,
1012 },
1013 None,
1014 engine_addr,
1015 )
1016 .await?;
1017 Ok(ctx)
1018 }
1019
1020 pub fn is_mock(&self) -> bool {
1021 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1022 }
1023
1024 pub async fn no_engine_commands(&self) -> bool {
1026 self.is_mock()
1027 }
1028
1029 pub async fn send_clear_scene(
1030 &self,
1031 exec_state: &mut ExecState,
1032 source_range: crate::execution::SourceRange,
1033 ) -> Result<(), KclError> {
1034 exec_state.mod_local.artifacts.clear();
1037 exec_state.global.root_module_artifacts.clear();
1038 exec_state.global.artifacts.clear();
1039
1040 self.engine
1041 .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
1042 .await?;
1043 if self.settings.enable_ssao {
1046 let cmd_id = exec_state.next_uuid();
1047 exec_state
1048 .batch_modeling_cmd(
1049 ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1050 ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1051 )
1052 .await?;
1053 }
1054 Ok(())
1055 }
1056
1057 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1058 cache::bust_cache().await;
1059
1060 let outcome = self.run_with_caching(crate::Program::empty()).await?;
1065
1066 Ok(outcome)
1067 }
1068
1069 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1070 self.eval_prelude(exec_state, SourceRange::synthetic())
1071 .await
1072 .map_err(KclErrorWithOutputs::no_outputs)?;
1073 exec_state.mut_stack().push_new_root_env(true);
1074 Ok(())
1075 }
1076
1077 fn restore_mock_memory(
1078 exec_state: &mut ExecState,
1079 mem: cache::SketchModeState,
1080 _mock_config: &MockConfig,
1081 ) -> Result<(), KclErrorWithOutputs> {
1082 *exec_state.mut_stack() = mem.stack;
1083 exec_state.global.module_infos = mem.module_infos;
1084 exec_state.global.path_to_source_id = mem.path_to_source_id;
1085 exec_state.global.id_to_source = mem.id_to_source;
1086 #[cfg(feature = "artifact-graph")]
1087 {
1088 let len = _mock_config
1089 .sketch_block_id
1090 .map(|sketch_block_id| sketch_block_id.0)
1091 .unwrap_or(0);
1092 if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1093 exec_state
1094 .global
1095 .root_module_artifacts
1096 .restore_scene_objects(scene_objects);
1097 } else {
1098 let message = format!(
1099 "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1100 mem.scene_objects.len(),
1101 len
1102 );
1103 debug_assert!(false, "{message}");
1104 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1105 KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1106 )));
1107 }
1108 }
1109
1110 Ok(())
1111 }
1112
1113 pub async fn run_mock(
1114 &self,
1115 program: &crate::Program,
1116 mock_config: &MockConfig,
1117 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1118 assert!(
1119 self.is_mock(),
1120 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1121 );
1122
1123 let use_prev_memory = mock_config.use_prev_memory;
1124 let mut exec_state = ExecState::new_mock(self, mock_config);
1125 if use_prev_memory {
1126 match cache::read_old_memory().await {
1127 Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
1128 None => self.prepare_mem(&mut exec_state).await?,
1129 }
1130 } else {
1131 self.prepare_mem(&mut exec_state).await?
1132 };
1133
1134 exec_state.mut_stack().push_new_env_for_scope();
1137
1138 let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1139
1140 let mut stack = exec_state.stack().clone();
1145 let module_infos = exec_state.global.module_infos.clone();
1146 let path_to_source_id = exec_state.global.path_to_source_id.clone();
1147 let id_to_source = exec_state.global.id_to_source.clone();
1148 #[cfg(feature = "artifact-graph")]
1149 let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1150 #[cfg(not(feature = "artifact-graph"))]
1151 let scene_objects = Default::default();
1152 let outcome = exec_state.into_exec_outcome(result.0, self).await;
1153
1154 stack.squash_env(result.0);
1155 let state = cache::SketchModeState {
1156 stack,
1157 module_infos,
1158 path_to_source_id,
1159 id_to_source,
1160 scene_objects,
1161 };
1162 cache::write_old_memory(state).await;
1163
1164 Ok(outcome)
1165 }
1166
1167 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1168 assert!(!self.is_mock());
1169 let grid_scale = if self.settings.fixed_size_grid {
1170 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1171 } else {
1172 GridScaleBehavior::ScaleWithZoom
1173 };
1174
1175 let original_program = program.clone();
1176
1177 let (_program, exec_state, result) = match cache::read_old_ast().await {
1178 Some(mut cached_state) => {
1179 let old = CacheInformation {
1180 ast: &cached_state.main.ast,
1181 settings: &cached_state.settings,
1182 };
1183 let new = CacheInformation {
1184 ast: &program.ast,
1185 settings: &self.settings,
1186 };
1187
1188 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1190 CacheResult::ReExecute {
1191 clear_scene,
1192 reapply_settings,
1193 program: changed_program,
1194 } => {
1195 if reapply_settings
1196 && self
1197 .engine
1198 .reapply_settings(
1199 &self.settings,
1200 Default::default(),
1201 &mut cached_state.main.exec_state.id_generator,
1202 grid_scale,
1203 )
1204 .await
1205 .is_err()
1206 {
1207 (true, program, None)
1208 } else {
1209 (
1210 clear_scene,
1211 crate::Program {
1212 ast: changed_program,
1213 original_file_contents: program.original_file_contents,
1214 },
1215 None,
1216 )
1217 }
1218 }
1219 CacheResult::CheckImportsOnly {
1220 reapply_settings,
1221 ast: changed_program,
1222 } => {
1223 let mut reapply_failed = false;
1224 if reapply_settings {
1225 if self
1226 .engine
1227 .reapply_settings(
1228 &self.settings,
1229 Default::default(),
1230 &mut cached_state.main.exec_state.id_generator,
1231 grid_scale,
1232 )
1233 .await
1234 .is_ok()
1235 {
1236 cache::write_old_ast(GlobalState::with_settings(
1237 cached_state.clone(),
1238 self.settings.clone(),
1239 ))
1240 .await;
1241 } else {
1242 reapply_failed = true;
1243 }
1244 }
1245
1246 if reapply_failed {
1247 (true, program, None)
1248 } else {
1249 let mut new_exec_state = ExecState::new(self);
1251 let (new_universe, new_universe_map) =
1252 self.get_universe(&program, &mut new_exec_state).await?;
1253
1254 let clear_scene = new_universe.values().any(|value| {
1255 let id = value.1;
1256 match (
1257 cached_state.exec_state.get_source(id),
1258 new_exec_state.global.get_source(id),
1259 ) {
1260 (Some(s0), Some(s1)) => s0.source != s1.source,
1261 _ => false,
1262 }
1263 });
1264
1265 if !clear_scene {
1266 return Ok(cached_state.into_exec_outcome(self).await);
1268 }
1269
1270 (
1271 true,
1272 crate::Program {
1273 ast: changed_program,
1274 original_file_contents: program.original_file_contents,
1275 },
1276 Some((new_universe, new_universe_map, new_exec_state)),
1277 )
1278 }
1279 }
1280 CacheResult::NoAction(true) => {
1281 if self
1282 .engine
1283 .reapply_settings(
1284 &self.settings,
1285 Default::default(),
1286 &mut cached_state.main.exec_state.id_generator,
1287 grid_scale,
1288 )
1289 .await
1290 .is_ok()
1291 {
1292 cache::write_old_ast(GlobalState::with_settings(
1294 cached_state.clone(),
1295 self.settings.clone(),
1296 ))
1297 .await;
1298
1299 return Ok(cached_state.into_exec_outcome(self).await);
1300 }
1301 (true, program, None)
1302 }
1303 CacheResult::NoAction(false) => {
1304 return Ok(cached_state.into_exec_outcome(self).await);
1305 }
1306 };
1307
1308 let (exec_state, result) = match import_check_info {
1309 Some((new_universe, new_universe_map, mut new_exec_state)) => {
1310 self.send_clear_scene(&mut new_exec_state, Default::default())
1312 .await
1313 .map_err(KclErrorWithOutputs::no_outputs)?;
1314
1315 let result = self
1316 .run_concurrent(
1317 &program,
1318 &mut new_exec_state,
1319 Some((new_universe, new_universe_map)),
1320 PreserveMem::Normal,
1321 )
1322 .await;
1323
1324 (new_exec_state, result)
1325 }
1326 None if clear_scene => {
1327 let mut exec_state = cached_state.reconstitute_exec_state();
1329 exec_state.reset(self);
1330
1331 self.send_clear_scene(&mut exec_state, Default::default())
1332 .await
1333 .map_err(KclErrorWithOutputs::no_outputs)?;
1334
1335 let result = self
1336 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1337 .await;
1338
1339 (exec_state, result)
1340 }
1341 None => {
1342 let mut exec_state = cached_state.reconstitute_exec_state();
1343 exec_state.mut_stack().restore_env(cached_state.main.result_env);
1344
1345 let result = self
1346 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1347 .await;
1348
1349 (exec_state, result)
1350 }
1351 };
1352
1353 (program, exec_state, result)
1354 }
1355 None => {
1356 let mut exec_state = ExecState::new(self);
1357 self.send_clear_scene(&mut exec_state, Default::default())
1358 .await
1359 .map_err(KclErrorWithOutputs::no_outputs)?;
1360
1361 let result = self
1362 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1363 .await;
1364
1365 (program, exec_state, result)
1366 }
1367 };
1368
1369 if result.is_err() {
1370 cache::bust_cache().await;
1371 }
1372
1373 let result = result?;
1375
1376 cache::write_old_ast(GlobalState::new(
1380 exec_state.clone(),
1381 self.settings.clone(),
1382 original_program.ast,
1383 result.0,
1384 ))
1385 .await;
1386
1387 let outcome = exec_state.into_exec_outcome(result.0, self).await;
1388 Ok(outcome)
1389 }
1390
1391 pub async fn run(
1395 &self,
1396 program: &crate::Program,
1397 exec_state: &mut ExecState,
1398 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1399 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1400 .await
1401 }
1402
1403 pub async fn run_concurrent(
1408 &self,
1409 program: &crate::Program,
1410 exec_state: &mut ExecState,
1411 universe_info: Option<(Universe, UniverseMap)>,
1412 preserve_mem: PreserveMem,
1413 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1414 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1417 (universe, universe_map)
1418 } else {
1419 self.get_universe(program, exec_state).await?
1420 };
1421
1422 let default_planes = self.engine.get_default_planes().read().await.clone();
1423
1424 self.eval_prelude(exec_state, SourceRange::synthetic())
1426 .await
1427 .map_err(KclErrorWithOutputs::no_outputs)?;
1428
1429 for modules in import_graph::import_graph(&universe, self)
1430 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1431 .into_iter()
1432 {
1433 #[cfg(not(target_arch = "wasm32"))]
1434 let mut set = tokio::task::JoinSet::new();
1435
1436 #[allow(clippy::type_complexity)]
1437 let (results_tx, mut results_rx): (
1438 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1439 tokio::sync::mpsc::Receiver<_>,
1440 ) = tokio::sync::mpsc::channel(1);
1441
1442 for module in modules {
1443 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1444 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1445 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1446 )));
1447 };
1448 let module_id = *module_id;
1449 let module_path = module_path.clone();
1450 let source_range = SourceRange::from(import_stmt);
1451 let module_exec_state = exec_state.clone();
1453
1454 self.add_import_module_ops(
1455 exec_state,
1456 &program.ast,
1457 module_id,
1458 &module_path,
1459 source_range,
1460 &universe_map,
1461 );
1462
1463 let repr = repr.clone();
1464 let exec_ctxt = self.clone();
1465 let results_tx = results_tx.clone();
1466
1467 let exec_module = async |exec_ctxt: &ExecutorContext,
1468 repr: &ModuleRepr,
1469 module_id: ModuleId,
1470 module_path: &ModulePath,
1471 exec_state: &mut ExecState,
1472 source_range: SourceRange|
1473 -> Result<ModuleRepr, KclError> {
1474 match repr {
1475 ModuleRepr::Kcl(program, _) => {
1476 let result = exec_ctxt
1477 .exec_module_from_ast(
1478 program,
1479 module_id,
1480 module_path,
1481 exec_state,
1482 source_range,
1483 PreserveMem::Normal,
1484 )
1485 .await;
1486
1487 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1488 }
1489 ModuleRepr::Foreign(geom, _) => {
1490 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1491 .await
1492 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1493
1494 result.map(|val| {
1495 ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
1496 })
1497 }
1498 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1499 format!("Module {module_path} not found in universe"),
1500 vec![source_range],
1501 ))),
1502 }
1503 };
1504
1505 #[cfg(target_arch = "wasm32")]
1506 {
1507 wasm_bindgen_futures::spawn_local(async move {
1508 let mut exec_state = module_exec_state;
1509 let exec_ctxt = exec_ctxt;
1510
1511 let result = exec_module(
1512 &exec_ctxt,
1513 &repr,
1514 module_id,
1515 &module_path,
1516 &mut exec_state,
1517 source_range,
1518 )
1519 .await;
1520
1521 results_tx
1522 .send((module_id, module_path, result))
1523 .await
1524 .unwrap_or_default();
1525 });
1526 }
1527 #[cfg(not(target_arch = "wasm32"))]
1528 {
1529 set.spawn(async move {
1530 let mut exec_state = module_exec_state;
1531 let exec_ctxt = exec_ctxt;
1532
1533 let result = exec_module(
1534 &exec_ctxt,
1535 &repr,
1536 module_id,
1537 &module_path,
1538 &mut exec_state,
1539 source_range,
1540 )
1541 .await;
1542
1543 results_tx
1544 .send((module_id, module_path, result))
1545 .await
1546 .unwrap_or_default();
1547 });
1548 }
1549 }
1550
1551 drop(results_tx);
1552
1553 while let Some((module_id, _, result)) = results_rx.recv().await {
1554 match result {
1555 Ok(new_repr) => {
1556 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1557
1558 match &mut repr {
1559 ModuleRepr::Kcl(_, cache) => {
1560 let ModuleRepr::Kcl(_, session_data) = new_repr else {
1561 unreachable!();
1562 };
1563 *cache = session_data;
1564 }
1565 ModuleRepr::Foreign(_, cache) => {
1566 let ModuleRepr::Foreign(_, session_data) = new_repr else {
1567 unreachable!();
1568 };
1569 *cache = session_data;
1570 }
1571 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1572 }
1573
1574 exec_state.global.module_infos[&module_id].restore_repr(repr);
1575 }
1576 Err(e) => {
1577 return Err(exec_state.error_with_outputs(e, None, default_planes));
1578 }
1579 }
1580 }
1581 }
1582
1583 exec_state
1587 .global
1588 .root_module_artifacts
1589 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1590
1591 self.inner_run(program, exec_state, preserve_mem).await
1592 }
1593
1594 async fn get_universe(
1597 &self,
1598 program: &crate::Program,
1599 exec_state: &mut ExecState,
1600 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1601 exec_state.add_root_module_contents(program);
1602
1603 let mut universe = std::collections::HashMap::new();
1604
1605 let default_planes = self.engine.get_default_planes().read().await.clone();
1606
1607 let root_imports = import_graph::import_universe(
1608 self,
1609 &ModulePath::Main,
1610 &ModuleRepr::Kcl(program.ast.clone(), None),
1611 &mut universe,
1612 exec_state,
1613 )
1614 .await
1615 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1616
1617 Ok((universe, root_imports))
1618 }
1619
1620 #[cfg(not(feature = "artifact-graph"))]
1621 fn add_import_module_ops(
1622 &self,
1623 _exec_state: &mut ExecState,
1624 _program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1625 _module_id: ModuleId,
1626 _module_path: &ModulePath,
1627 _source_range: SourceRange,
1628 _universe_map: &UniverseMap,
1629 ) {
1630 }
1631
1632 #[cfg(feature = "artifact-graph")]
1633 fn add_import_module_ops(
1634 &self,
1635 exec_state: &mut ExecState,
1636 program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1637 module_id: ModuleId,
1638 module_path: &ModulePath,
1639 source_range: SourceRange,
1640 universe_map: &UniverseMap,
1641 ) {
1642 match module_path {
1643 ModulePath::Main => {
1644 }
1646 ModulePath::Local {
1647 value,
1648 original_import_path,
1649 } => {
1650 if universe_map.contains_key(value) {
1653 use crate::NodePath;
1654
1655 let node_path = if source_range.is_top_level_module() {
1656 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1657 NodePath::from_range(
1658 &exec_state.build_program_lookup(program.clone()),
1659 cached_body_items,
1660 source_range,
1661 )
1662 .unwrap_or_default()
1663 } else {
1664 NodePath::placeholder()
1667 };
1668
1669 let name = match original_import_path {
1670 Some(value) => value.to_string_lossy(),
1671 None => value.file_name().unwrap_or_default(),
1672 };
1673 exec_state.push_op(Operation::GroupBegin {
1674 group: Group::ModuleInstance { name, module_id },
1675 node_path,
1676 source_range,
1677 });
1678 exec_state.push_op(Operation::GroupEnd);
1682 }
1683 }
1684 ModulePath::Std { .. } => {
1685 }
1687 }
1688 }
1689
1690 async fn inner_run(
1693 &self,
1694 program: &crate::Program,
1695 exec_state: &mut ExecState,
1696 preserve_mem: PreserveMem,
1697 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1698 let _stats = crate::log::LogPerfStats::new("Interpretation");
1699
1700 let grid_scale = if self.settings.fixed_size_grid {
1702 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1703 } else {
1704 GridScaleBehavior::ScaleWithZoom
1705 };
1706 self.engine
1707 .reapply_settings(
1708 &self.settings,
1709 Default::default(),
1710 exec_state.id_generator(),
1711 grid_scale,
1712 )
1713 .await
1714 .map_err(KclErrorWithOutputs::no_outputs)?;
1715
1716 let default_planes = self.engine.get_default_planes().read().await.clone();
1717 let result = self
1718 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1719 .await;
1720
1721 crate::log::log(format!(
1722 "Post interpretation KCL memory stats: {:#?}",
1723 exec_state.stack().memory.stats
1724 ));
1725 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1726
1727 async fn write_old_memory(ctx: &ExecutorContext, exec_state: &ExecState, env_ref: EnvironmentRef) {
1730 if ctx.is_mock() {
1731 return;
1732 }
1733 let mut stack = exec_state.stack().deep_clone();
1734 stack.restore_env(env_ref);
1735 let state = cache::SketchModeState {
1736 stack,
1737 module_infos: exec_state.global.module_infos.clone(),
1738 path_to_source_id: exec_state.global.path_to_source_id.clone(),
1739 id_to_source: exec_state.global.id_to_source.clone(),
1740 #[cfg(feature = "artifact-graph")]
1741 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1742 #[cfg(not(feature = "artifact-graph"))]
1743 scene_objects: Default::default(),
1744 };
1745 cache::write_old_memory(state).await;
1746 }
1747
1748 let env_ref = match result {
1749 Ok(env_ref) => env_ref,
1750 Err((err, env_ref)) => {
1751 if let Some(env_ref) = env_ref {
1754 write_old_memory(self, exec_state, env_ref).await;
1755 }
1756 return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
1757 }
1758 };
1759
1760 write_old_memory(self, exec_state, env_ref).await;
1761
1762 let session_data = self.engine.get_session_data().await;
1763
1764 Ok((env_ref, session_data))
1765 }
1766
1767 async fn execute_and_build_graph(
1770 &self,
1771 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1772 exec_state: &mut ExecState,
1773 preserve_mem: PreserveMem,
1774 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1775 #[cfg(feature = "artifact-graph")]
1781 let start_op = exec_state.global.root_module_artifacts.operations.len();
1782
1783 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1784 .await
1785 .map_err(|e| (e, None))?;
1786
1787 let exec_result = self
1788 .exec_module_body(
1789 program,
1790 exec_state,
1791 preserve_mem,
1792 ModuleId::default(),
1793 &ModulePath::Main,
1794 )
1795 .await
1796 .map(
1797 |ModuleExecutionOutcome {
1798 environment: env_ref,
1799 artifacts: module_artifacts,
1800 ..
1801 }| {
1802 exec_state.global.root_module_artifacts.extend(module_artifacts);
1805 env_ref
1806 },
1807 )
1808 .map_err(|(err, env_ref, module_artifacts)| {
1809 if let Some(module_artifacts) = module_artifacts {
1810 exec_state.global.root_module_artifacts.extend(module_artifacts);
1813 }
1814 (err, env_ref)
1815 });
1816
1817 #[cfg(feature = "artifact-graph")]
1818 {
1819 let programs = &exec_state.build_program_lookup(program.clone());
1821 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1822 for op in exec_state
1823 .global
1824 .root_module_artifacts
1825 .operations
1826 .iter_mut()
1827 .skip(start_op)
1828 {
1829 op.fill_node_paths(programs, cached_body_items);
1830 }
1831 for module in exec_state.global.module_infos.values_mut() {
1832 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1833 for op in &mut outcome.artifacts.operations {
1834 op.fill_node_paths(programs, cached_body_items);
1835 }
1836 }
1837 }
1838 }
1839
1840 self.engine.ensure_async_commands_completed().await.map_err(|e| {
1842 match &exec_result {
1843 Ok(env_ref) => (e, Some(*env_ref)),
1844 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1846 }
1847 })?;
1848
1849 self.engine.clear_queues().await;
1852
1853 match exec_state.build_artifact_graph(&self.engine, program).await {
1854 Ok(_) => exec_result,
1855 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1856 }
1857 }
1858
1859 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1863 if exec_state.stack().memory.requires_std() {
1864 #[cfg(feature = "artifact-graph")]
1865 let initial_ops = exec_state.mod_local.artifacts.operations.len();
1866
1867 let path = vec!["std".to_owned(), "prelude".to_owned()];
1868 let resolved_path = ModulePath::from_std_import_path(&path)?;
1869 let id = self
1870 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1871 .await?;
1872 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1873
1874 exec_state.mut_stack().memory.set_std(module_memory);
1875
1876 #[cfg(feature = "artifact-graph")]
1882 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1883 }
1884
1885 Ok(())
1886 }
1887
1888 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1890 self.engine
1892 .send_modeling_cmd(
1893 uuid::Uuid::new_v4(),
1894 crate::execution::SourceRange::default(),
1895 &ModelingCmd::from(
1896 mcmd::ZoomToFit::builder()
1897 .object_ids(Default::default())
1898 .animated(false)
1899 .padding(0.1)
1900 .build(),
1901 ),
1902 )
1903 .await
1904 .map_err(KclErrorWithOutputs::no_outputs)?;
1905
1906 let resp = self
1908 .engine
1909 .send_modeling_cmd(
1910 uuid::Uuid::new_v4(),
1911 crate::execution::SourceRange::default(),
1912 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
1913 )
1914 .await
1915 .map_err(KclErrorWithOutputs::no_outputs)?;
1916
1917 let OkWebSocketResponseData::Modeling {
1918 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1919 } = resp
1920 else {
1921 return Err(ExecError::BadPng(format!(
1922 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1923 )));
1924 };
1925 Ok(contents)
1926 }
1927
1928 pub async fn export(
1930 &self,
1931 format: kittycad_modeling_cmds::format::OutputFormat3d,
1932 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1933 let resp = self
1934 .engine
1935 .send_modeling_cmd(
1936 uuid::Uuid::new_v4(),
1937 crate::SourceRange::default(),
1938 &kittycad_modeling_cmds::ModelingCmd::Export(
1939 kittycad_modeling_cmds::Export::builder()
1940 .entity_ids(vec![])
1941 .format(format)
1942 .build(),
1943 ),
1944 )
1945 .await?;
1946
1947 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1948 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1949 format!("Expected Export response, got {resp:?}",),
1950 vec![SourceRange::default()],
1951 )));
1952 };
1953
1954 Ok(files)
1955 }
1956
1957 pub async fn export_step(
1959 &self,
1960 deterministic_time: bool,
1961 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1962 let files = self
1963 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1964 kittycad_modeling_cmds::format::step::export::Options::builder()
1965 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
1966 .maybe_created(if deterministic_time {
1967 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1968 KclError::new_internal(crate::errors::KclErrorDetails::new(
1969 format!("Failed to parse date: {e}"),
1970 vec![SourceRange::default()],
1971 ))
1972 })?)
1973 } else {
1974 None
1975 })
1976 .build(),
1977 ))
1978 .await?;
1979
1980 Ok(files)
1981 }
1982
1983 pub async fn close(&self) {
1984 self.engine.close().await;
1985 }
1986}
1987
1988#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1989pub struct ArtifactId(Uuid);
1990
1991impl ArtifactId {
1992 pub fn new(uuid: Uuid) -> Self {
1993 Self(uuid)
1994 }
1995
1996 pub fn placeholder() -> Self {
1998 Self(Uuid::nil())
1999 }
2000}
2001
2002impl From<Uuid> for ArtifactId {
2003 fn from(uuid: Uuid) -> Self {
2004 Self::new(uuid)
2005 }
2006}
2007
2008impl From<&Uuid> for ArtifactId {
2009 fn from(uuid: &Uuid) -> Self {
2010 Self::new(*uuid)
2011 }
2012}
2013
2014impl From<ArtifactId> for Uuid {
2015 fn from(id: ArtifactId) -> Self {
2016 id.0
2017 }
2018}
2019
2020impl From<&ArtifactId> for Uuid {
2021 fn from(id: &ArtifactId) -> Self {
2022 id.0
2023 }
2024}
2025
2026impl From<ModelingCmdId> for ArtifactId {
2027 fn from(id: ModelingCmdId) -> Self {
2028 Self::new(*id.as_ref())
2029 }
2030}
2031
2032impl From<&ModelingCmdId> for ArtifactId {
2033 fn from(id: &ModelingCmdId) -> Self {
2034 Self::new(*id.as_ref())
2035 }
2036}
2037
2038#[cfg(test)]
2039pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2040 parse_execute_with_project_dir(code, None).await
2041}
2042
2043#[cfg(test)]
2044pub(crate) async fn parse_execute_with_project_dir(
2045 code: &str,
2046 project_directory: Option<TypedPath>,
2047) -> Result<ExecTestResults, KclError> {
2048 let program = crate::Program::parse_no_errs(code)?;
2049
2050 let exec_ctxt = ExecutorContext {
2051 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
2052 |err| {
2053 KclError::new_internal(crate::errors::KclErrorDetails::new(
2054 format!("Failed to create mock engine connection: {err}"),
2055 vec![SourceRange::default()],
2056 ))
2057 },
2058 )?)),
2059 fs: Arc::new(crate::fs::FileManager::new()),
2060 settings: ExecutorSettings {
2061 project_directory,
2062 ..Default::default()
2063 },
2064 context_type: ContextType::Mock,
2065 };
2066 let mut exec_state = ExecState::new(&exec_ctxt);
2067 let result = exec_ctxt.run(&program, &mut exec_state).await?;
2068
2069 Ok(ExecTestResults {
2070 program,
2071 mem_env: result.0,
2072 exec_ctxt,
2073 exec_state,
2074 })
2075}
2076
2077#[cfg(test)]
2078#[derive(Debug)]
2079pub(crate) struct ExecTestResults {
2080 program: crate::Program,
2081 mem_env: EnvironmentRef,
2082 exec_ctxt: ExecutorContext,
2083 exec_state: ExecState,
2084}
2085
2086#[cfg(feature = "artifact-graph")]
2090pub struct ProgramLookup {
2091 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2092}
2093
2094#[cfg(feature = "artifact-graph")]
2095impl ProgramLookup {
2096 pub fn new(
2099 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2100 module_infos: state::ModuleInfoMap,
2101 ) -> Self {
2102 let mut programs = IndexMap::with_capacity(module_infos.len());
2103 for (id, info) in module_infos {
2104 if let ModuleRepr::Kcl(program, _) = info.repr {
2105 programs.insert(id, program);
2106 }
2107 }
2108 programs.insert(ModuleId::default(), current);
2109 Self { programs }
2110 }
2111
2112 pub fn program_for_module(
2113 &self,
2114 module_id: ModuleId,
2115 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2116 self.programs.get(&module_id)
2117 }
2118}
2119
2120#[cfg(test)]
2121mod tests {
2122 use pretty_assertions::assert_eq;
2123
2124 use super::*;
2125 use crate::ModuleId;
2126 use crate::errors::KclErrorDetails;
2127 use crate::errors::Severity;
2128 use crate::exec::NumericType;
2129 use crate::execution::memory::Stack;
2130 use crate::execution::types::RuntimeType;
2131
2132 #[track_caller]
2134 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2135 memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
2136 }
2137
2138 #[tokio::test(flavor = "multi_thread")]
2139 async fn test_execute_warn() {
2140 let text = "@blah";
2141 let result = parse_execute(text).await.unwrap();
2142 let errs = result.exec_state.issues();
2143 assert_eq!(errs.len(), 1);
2144 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2145 assert!(
2146 errs[0].message.contains("Unknown annotation"),
2147 "unexpected warning message: {}",
2148 errs[0].message
2149 );
2150 }
2151
2152 #[tokio::test(flavor = "multi_thread")]
2153 async fn test_execute_fn_definitions() {
2154 let ast = r#"fn def(@x) {
2155 return x
2156}
2157fn ghi(@x) {
2158 return x
2159}
2160fn jkl(@x) {
2161 return x
2162}
2163fn hmm(@x) {
2164 return x
2165}
2166
2167yo = 5 + 6
2168
2169abc = 3
2170identifierGuy = 5
2171part001 = startSketchOn(XY)
2172|> startProfile(at = [-1.2, 4.83])
2173|> line(end = [2.8, 0])
2174|> angledLine(angle = 100 + 100, length = 3.01)
2175|> angledLine(angle = abc, length = 3.02)
2176|> angledLine(angle = def(yo), length = 3.03)
2177|> angledLine(angle = ghi(2), length = 3.04)
2178|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2179|> close()
2180yo2 = hmm([identifierGuy + 5])"#;
2181
2182 parse_execute(ast).await.unwrap();
2183 }
2184
2185 #[tokio::test(flavor = "multi_thread")]
2186 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2187 let code = r#"
2188firstProfile = sketch(on = XY) {
2189 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2190 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2191 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2192 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2193 coincident([edge1.end, edge2.start])
2194 coincident([edge2.end, edge3.start])
2195 coincident([edge3.end, edge4.start])
2196 coincident([edge4.end, edge1.start])
2197}
2198
2199secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2200 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2201 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2202 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2203 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2204 coincident([edge5.end, edge6.start])
2205 coincident([edge6.end, edge7.start])
2206 coincident([edge7.end, edge8.start])
2207 coincident([edge8.end, edge5.start])
2208}
2209
2210firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2211secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2212"#;
2213
2214 let result = parse_execute(code).await.unwrap();
2215 assert!(result.exec_state.issues().is_empty());
2216 }
2217
2218 #[cfg(feature = "artifact-graph")]
2219 #[tokio::test(flavor = "multi_thread")]
2220 async fn sketch_block_artifact_preserves_standard_plane_name() {
2221 let code = r#"
2222sketch001 = sketch(on = -YZ) {
2223 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
2224}
2225"#;
2226
2227 let result = parse_execute(code).await.unwrap();
2228 let sketch_blocks = result
2229 .exec_state
2230 .global
2231 .artifacts
2232 .graph
2233 .values()
2234 .filter_map(|artifact| match artifact {
2235 Artifact::SketchBlock(block) => Some(block),
2236 _ => None,
2237 })
2238 .collect::<Vec<_>>();
2239
2240 assert_eq!(sketch_blocks.len(), 1);
2241 assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
2242 }
2243
2244 #[tokio::test(flavor = "multi_thread")]
2245 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2246 let code = r#"
2247sketch001 = sketch(on = YZ) {
2248 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2249 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2250 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2251 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2252 coincident([line1.end, line2.start])
2253 coincident([line2.end, line3.start])
2254 coincident([line3.end, line4.start])
2255 coincident([line4.end, line1.start])
2256}
2257
2258sketch002 = sketch(on = -XZ) {
2259 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2260 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2261 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2262 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2263 coincident([line5.end, line6.start])
2264 coincident([line6.end, line7.start])
2265 coincident([line7.end, line8.start])
2266 coincident([line8.end, line5.start])
2267}
2268
2269region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2270extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2271region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2272extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2273
2274myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2275"#;
2276
2277 let result = parse_execute(code).await.unwrap();
2278 assert!(result.exec_state.issues().is_empty());
2279 }
2280
2281 #[tokio::test(flavor = "multi_thread")]
2282 async fn issue_10741_point_circle_coincident_executes() {
2283 let code = r#"
2284sketch001 = sketch(on = YZ) {
2285 circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
2286 line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
2287 coincident([line1.start, circle1])
2288}
2289"#;
2290
2291 let result = parse_execute(code).await.unwrap();
2292 assert!(
2293 result
2294 .exec_state
2295 .issues()
2296 .iter()
2297 .all(|issue| issue.severity != Severity::Error),
2298 "unexpected execution issues: {:#?}",
2299 result.exec_state.issues()
2300 );
2301 }
2302
2303 #[tokio::test(flavor = "multi_thread")]
2304 async fn test_execute_with_pipe_substitutions_unary() {
2305 let ast = r#"myVar = 3
2306part001 = startSketchOn(XY)
2307 |> startProfile(at = [0, 0])
2308 |> line(end = [3, 4], tag = $seg01)
2309 |> line(end = [
2310 min([segLen(seg01), myVar]),
2311 -legLen(hypotenuse = segLen(seg01), leg = myVar)
2312])
2313"#;
2314
2315 parse_execute(ast).await.unwrap();
2316 }
2317
2318 #[tokio::test(flavor = "multi_thread")]
2319 async fn test_execute_with_pipe_substitutions() {
2320 let ast = r#"myVar = 3
2321part001 = startSketchOn(XY)
2322 |> startProfile(at = [0, 0])
2323 |> line(end = [3, 4], tag = $seg01)
2324 |> line(end = [
2325 min([segLen(seg01), myVar]),
2326 legLen(hypotenuse = segLen(seg01), leg = myVar)
2327])
2328"#;
2329
2330 parse_execute(ast).await.unwrap();
2331 }
2332
2333 #[tokio::test(flavor = "multi_thread")]
2334 async fn test_execute_with_inline_comment() {
2335 let ast = r#"baseThick = 1
2336armAngle = 60
2337
2338baseThickHalf = baseThick / 2
2339halfArmAngle = armAngle / 2
2340
2341arrExpShouldNotBeIncluded = [1, 2, 3]
2342objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2343
2344part001 = startSketchOn(XY)
2345 |> startProfile(at = [0, 0])
2346 |> yLine(endAbsolute = 1)
2347 |> xLine(length = 3.84) // selection-range-7ish-before-this
2348
2349variableBelowShouldNotBeIncluded = 3
2350"#;
2351
2352 parse_execute(ast).await.unwrap();
2353 }
2354
2355 #[tokio::test(flavor = "multi_thread")]
2356 async fn test_execute_with_function_literal_in_pipe() {
2357 let ast = r#"w = 20
2358l = 8
2359h = 10
2360
2361fn thing() {
2362 return -8
2363}
2364
2365firstExtrude = startSketchOn(XY)
2366 |> startProfile(at = [0,0])
2367 |> line(end = [0, l])
2368 |> line(end = [w, 0])
2369 |> line(end = [0, thing()])
2370 |> close()
2371 |> extrude(length = h)"#;
2372
2373 parse_execute(ast).await.unwrap();
2374 }
2375
2376 #[tokio::test(flavor = "multi_thread")]
2377 async fn test_execute_with_function_unary_in_pipe() {
2378 let ast = r#"w = 20
2379l = 8
2380h = 10
2381
2382fn thing(@x) {
2383 return -x
2384}
2385
2386firstExtrude = startSketchOn(XY)
2387 |> startProfile(at = [0,0])
2388 |> line(end = [0, l])
2389 |> line(end = [w, 0])
2390 |> line(end = [0, thing(8)])
2391 |> close()
2392 |> extrude(length = h)"#;
2393
2394 parse_execute(ast).await.unwrap();
2395 }
2396
2397 #[tokio::test(flavor = "multi_thread")]
2398 async fn test_execute_with_function_array_in_pipe() {
2399 let ast = r#"w = 20
2400l = 8
2401h = 10
2402
2403fn thing(@x) {
2404 return [0, -x]
2405}
2406
2407firstExtrude = startSketchOn(XY)
2408 |> startProfile(at = [0,0])
2409 |> line(end = [0, l])
2410 |> line(end = [w, 0])
2411 |> line(end = thing(8))
2412 |> close()
2413 |> extrude(length = h)"#;
2414
2415 parse_execute(ast).await.unwrap();
2416 }
2417
2418 #[tokio::test(flavor = "multi_thread")]
2419 async fn test_execute_with_function_call_in_pipe() {
2420 let ast = r#"w = 20
2421l = 8
2422h = 10
2423
2424fn other_thing(@y) {
2425 return -y
2426}
2427
2428fn thing(@x) {
2429 return other_thing(x)
2430}
2431
2432firstExtrude = startSketchOn(XY)
2433 |> startProfile(at = [0,0])
2434 |> line(end = [0, l])
2435 |> line(end = [w, 0])
2436 |> line(end = [0, thing(8)])
2437 |> close()
2438 |> extrude(length = h)"#;
2439
2440 parse_execute(ast).await.unwrap();
2441 }
2442
2443 #[tokio::test(flavor = "multi_thread")]
2444 async fn test_execute_with_function_sketch() {
2445 let ast = r#"fn box(h, l, w) {
2446 myBox = startSketchOn(XY)
2447 |> startProfile(at = [0,0])
2448 |> line(end = [0, l])
2449 |> line(end = [w, 0])
2450 |> line(end = [0, -l])
2451 |> close()
2452 |> extrude(length = h)
2453
2454 return myBox
2455}
2456
2457fnBox = box(h = 3, l = 6, w = 10)"#;
2458
2459 parse_execute(ast).await.unwrap();
2460 }
2461
2462 #[tokio::test(flavor = "multi_thread")]
2463 async fn test_get_member_of_object_with_function_period() {
2464 let ast = r#"fn box(@obj) {
2465 myBox = startSketchOn(XY)
2466 |> startProfile(at = obj.start)
2467 |> line(end = [0, obj.l])
2468 |> line(end = [obj.w, 0])
2469 |> line(end = [0, -obj.l])
2470 |> close()
2471 |> extrude(length = obj.h)
2472
2473 return myBox
2474}
2475
2476thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2477"#;
2478 parse_execute(ast).await.unwrap();
2479 }
2480
2481 #[tokio::test(flavor = "multi_thread")]
2482 #[ignore] async fn test_object_member_starting_pipeline() {
2484 let ast = r#"
2485fn test2() {
2486 return {
2487 thing: startSketchOn(XY)
2488 |> startProfile(at = [0, 0])
2489 |> line(end = [0, 1])
2490 |> line(end = [1, 0])
2491 |> line(end = [0, -1])
2492 |> close()
2493 }
2494}
2495
2496x2 = test2()
2497
2498x2.thing
2499 |> extrude(length = 10)
2500"#;
2501 parse_execute(ast).await.unwrap();
2502 }
2503
2504 #[tokio::test(flavor = "multi_thread")]
2505 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
2507 let ast = r#"fn box(obj) {
2508let myBox = startSketchOn(XY)
2509 |> startProfile(at = obj.start)
2510 |> line(end = [0, obj.l])
2511 |> line(end = [obj.w, 0])
2512 |> line(end = [0, -obj.l])
2513 |> close()
2514 |> extrude(length = obj.h)
2515
2516 return myBox
2517}
2518
2519for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2520 thisBox = box(var)
2521}"#;
2522
2523 parse_execute(ast).await.unwrap();
2524 }
2525
2526 #[tokio::test(flavor = "multi_thread")]
2527 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
2529 let ast = r#"fn box(h, l, w, start) {
2530 myBox = startSketchOn(XY)
2531 |> startProfile(at = [0,0])
2532 |> line(end = [0, l])
2533 |> line(end = [w, 0])
2534 |> line(end = [0, -l])
2535 |> close()
2536 |> extrude(length = h)
2537
2538 return myBox
2539}
2540
2541
2542for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2543 const thisBox = box(var[0], var[1], var[2], var[3])
2544}"#;
2545
2546 parse_execute(ast).await.unwrap();
2547 }
2548
2549 #[tokio::test(flavor = "multi_thread")]
2550 async fn test_get_member_of_array_with_function() {
2551 let ast = r#"fn box(@arr) {
2552 myBox =startSketchOn(XY)
2553 |> startProfile(at = arr[0])
2554 |> line(end = [0, arr[1]])
2555 |> line(end = [arr[2], 0])
2556 |> line(end = [0, -arr[1]])
2557 |> close()
2558 |> extrude(length = arr[3])
2559
2560 return myBox
2561}
2562
2563thisBox = box([[0,0], 6, 10, 3])
2564
2565"#;
2566 parse_execute(ast).await.unwrap();
2567 }
2568
2569 #[tokio::test(flavor = "multi_thread")]
2570 async fn test_function_cannot_access_future_definitions() {
2571 let ast = r#"
2572fn returnX() {
2573 // x shouldn't be defined yet.
2574 return x
2575}
2576
2577x = 5
2578
2579answer = returnX()"#;
2580
2581 let result = parse_execute(ast).await;
2582 let err = result.unwrap_err();
2583 assert_eq!(err.message(), "`x` is not defined");
2584 }
2585
2586 #[tokio::test(flavor = "multi_thread")]
2587 async fn test_override_prelude() {
2588 let text = "PI = 3.0";
2589 let result = parse_execute(text).await.unwrap();
2590 let issues = result.exec_state.issues();
2591 assert!(issues.is_empty(), "issues={issues:#?}");
2592 }
2593
2594 #[tokio::test(flavor = "multi_thread")]
2595 async fn type_aliases() {
2596 let text = r#"@settings(experimentalFeatures = allow)
2597type MyTy = [number; 2]
2598fn foo(@x: MyTy) {
2599 return x[0]
2600}
2601
2602foo([0, 1])
2603
2604type Other = MyTy | Helix
2605"#;
2606 let result = parse_execute(text).await.unwrap();
2607 let issues = result.exec_state.issues();
2608 assert!(issues.is_empty(), "issues={issues:#?}");
2609 }
2610
2611 #[tokio::test(flavor = "multi_thread")]
2612 async fn test_cannot_shebang_in_fn() {
2613 let ast = r#"
2614fn foo() {
2615 #!hello
2616 return true
2617}
2618
2619foo
2620"#;
2621
2622 let result = parse_execute(ast).await;
2623 let err = result.unwrap_err();
2624 assert_eq!(
2625 err,
2626 KclError::new_syntax(KclErrorDetails::new(
2627 "Unexpected token: #".to_owned(),
2628 vec![SourceRange::new(14, 15, ModuleId::default())],
2629 )),
2630 );
2631 }
2632
2633 #[tokio::test(flavor = "multi_thread")]
2634 async fn test_pattern_transform_function_cannot_access_future_definitions() {
2635 let ast = r#"
2636fn transform(@replicaId) {
2637 // x shouldn't be defined yet.
2638 scale = x
2639 return {
2640 translate = [0, 0, replicaId * 10],
2641 scale = [scale, 1, 0],
2642 }
2643}
2644
2645fn layer() {
2646 return startSketchOn(XY)
2647 |> circle( center= [0, 0], radius= 1, tag = $tag1)
2648 |> extrude(length = 10)
2649}
2650
2651x = 5
2652
2653// The 10 layers are replicas of each other, with a transform applied to each.
2654shape = layer() |> patternTransform(instances = 10, transform = transform)
2655"#;
2656
2657 let result = parse_execute(ast).await;
2658 let err = result.unwrap_err();
2659 assert_eq!(err.message(), "`x` is not defined",);
2660 }
2661
2662 #[tokio::test(flavor = "multi_thread")]
2665 async fn test_math_execute_with_functions() {
2666 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
2667 let result = parse_execute(ast).await.unwrap();
2668 assert_eq!(
2669 5.0,
2670 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2671 .as_f64()
2672 .unwrap()
2673 );
2674 }
2675
2676 #[tokio::test(flavor = "multi_thread")]
2677 async fn test_math_execute() {
2678 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
2679 let result = parse_execute(ast).await.unwrap();
2680 assert_eq!(
2681 7.4,
2682 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2683 .as_f64()
2684 .unwrap()
2685 );
2686 }
2687
2688 #[tokio::test(flavor = "multi_thread")]
2689 async fn test_math_execute_start_negative() {
2690 let ast = r#"myVar = -5 + 6"#;
2691 let result = parse_execute(ast).await.unwrap();
2692 assert_eq!(
2693 1.0,
2694 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2695 .as_f64()
2696 .unwrap()
2697 );
2698 }
2699
2700 #[tokio::test(flavor = "multi_thread")]
2701 async fn test_math_execute_with_pi() {
2702 let ast = r#"myVar = PI * 2"#;
2703 let result = parse_execute(ast).await.unwrap();
2704 assert_eq!(
2705 std::f64::consts::TAU,
2706 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2707 .as_f64()
2708 .unwrap()
2709 );
2710 }
2711
2712 #[tokio::test(flavor = "multi_thread")]
2713 async fn test_math_define_decimal_without_leading_zero() {
2714 let ast = r#"thing = .4 + 7"#;
2715 let result = parse_execute(ast).await.unwrap();
2716 assert_eq!(
2717 7.4,
2718 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
2719 .as_f64()
2720 .unwrap()
2721 );
2722 }
2723
2724 #[tokio::test(flavor = "multi_thread")]
2725 async fn pass_std_to_std() {
2726 let ast = r#"sketch001 = startSketchOn(XY)
2727profile001 = circle(sketch001, center = [0, 0], radius = 2)
2728extrude001 = extrude(profile001, length = 5)
2729extrudes = patternLinear3d(
2730 extrude001,
2731 instances = 3,
2732 distance = 5,
2733 axis = [1, 1, 0],
2734)
2735clone001 = map(extrudes, f = clone)
2736"#;
2737 parse_execute(ast).await.unwrap();
2738 }
2739
2740 #[tokio::test(flavor = "multi_thread")]
2741 async fn test_array_reduce_nested_array() {
2742 let code = r#"
2743fn id(@el, accum) { return accum }
2744
2745answer = reduce([], initial=[[[0,0]]], f=id)
2746"#;
2747 let result = parse_execute(code).await.unwrap();
2748 assert_eq!(
2749 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2750 KclValue::HomArray {
2751 value: vec![KclValue::HomArray {
2752 value: vec![KclValue::HomArray {
2753 value: vec![
2754 KclValue::Number {
2755 value: 0.0,
2756 ty: NumericType::default(),
2757 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2758 },
2759 KclValue::Number {
2760 value: 0.0,
2761 ty: NumericType::default(),
2762 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2763 }
2764 ],
2765 ty: RuntimeType::any(),
2766 }],
2767 ty: RuntimeType::any(),
2768 }],
2769 ty: RuntimeType::any(),
2770 }
2771 );
2772 }
2773
2774 #[tokio::test(flavor = "multi_thread")]
2775 async fn test_zero_param_fn() {
2776 let ast = r#"sigmaAllow = 35000 // psi
2777leg1 = 5 // inches
2778leg2 = 8 // inches
2779fn thickness() { return 0.56 }
2780
2781bracket = startSketchOn(XY)
2782 |> startProfile(at = [0,0])
2783 |> line(end = [0, leg1])
2784 |> line(end = [leg2, 0])
2785 |> line(end = [0, -thickness()])
2786 |> line(end = [-leg2 + thickness(), 0])
2787"#;
2788 parse_execute(ast).await.unwrap();
2789 }
2790
2791 #[tokio::test(flavor = "multi_thread")]
2792 async fn test_unary_operator_not_succeeds() {
2793 let ast = r#"
2794fn returnTrue() { return !false }
2795t = true
2796f = false
2797notTrue = !t
2798notFalse = !f
2799c = !!true
2800d = !returnTrue()
2801
2802assertIs(!false, error = "expected to pass")
2803
2804fn check(x) {
2805 assertIs(!x, error = "expected argument to be false")
2806 return true
2807}
2808check(x = false)
2809"#;
2810 let result = parse_execute(ast).await.unwrap();
2811 assert_eq!(
2812 false,
2813 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2814 .as_bool()
2815 .unwrap()
2816 );
2817 assert_eq!(
2818 true,
2819 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2820 .as_bool()
2821 .unwrap()
2822 );
2823 assert_eq!(
2824 true,
2825 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2826 .as_bool()
2827 .unwrap()
2828 );
2829 assert_eq!(
2830 false,
2831 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2832 .as_bool()
2833 .unwrap()
2834 );
2835 }
2836
2837 #[tokio::test(flavor = "multi_thread")]
2838 async fn test_unary_operator_not_on_non_bool_fails() {
2839 let code1 = r#"
2840// Yup, this is null.
2841myNull = 0 / 0
2842notNull = !myNull
2843"#;
2844 assert_eq!(
2845 parse_execute(code1).await.unwrap_err().message(),
2846 "Cannot apply unary operator ! to non-boolean value: a number",
2847 );
2848
2849 let code2 = "notZero = !0";
2850 assert_eq!(
2851 parse_execute(code2).await.unwrap_err().message(),
2852 "Cannot apply unary operator ! to non-boolean value: a number",
2853 );
2854
2855 let code3 = r#"
2856notEmptyString = !""
2857"#;
2858 assert_eq!(
2859 parse_execute(code3).await.unwrap_err().message(),
2860 "Cannot apply unary operator ! to non-boolean value: a string",
2861 );
2862
2863 let code4 = r#"
2864obj = { a = 1 }
2865notMember = !obj.a
2866"#;
2867 assert_eq!(
2868 parse_execute(code4).await.unwrap_err().message(),
2869 "Cannot apply unary operator ! to non-boolean value: a number",
2870 );
2871
2872 let code5 = "
2873a = []
2874notArray = !a";
2875 assert_eq!(
2876 parse_execute(code5).await.unwrap_err().message(),
2877 "Cannot apply unary operator ! to non-boolean value: an empty array",
2878 );
2879
2880 let code6 = "
2881x = {}
2882notObject = !x";
2883 assert_eq!(
2884 parse_execute(code6).await.unwrap_err().message(),
2885 "Cannot apply unary operator ! to non-boolean value: an object",
2886 );
2887
2888 let code7 = "
2889fn x() { return 1 }
2890notFunction = !x";
2891 let fn_err = parse_execute(code7).await.unwrap_err();
2892 assert!(
2895 fn_err
2896 .message()
2897 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2898 "Actual error: {fn_err:?}"
2899 );
2900
2901 let code8 = "
2902myTagDeclarator = $myTag
2903notTagDeclarator = !myTagDeclarator";
2904 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2905 assert!(
2908 tag_declarator_err
2909 .message()
2910 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2911 "Actual error: {tag_declarator_err:?}"
2912 );
2913
2914 let code9 = "
2915myTagDeclarator = $myTag
2916notTagIdentifier = !myTag";
2917 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2918 assert!(
2921 tag_identifier_err
2922 .message()
2923 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2924 "Actual error: {tag_identifier_err:?}"
2925 );
2926
2927 let code10 = "notPipe = !(1 |> 2)";
2928 assert_eq!(
2929 parse_execute(code10).await.unwrap_err(),
2932 KclError::new_syntax(KclErrorDetails::new(
2933 "Unexpected token: !".to_owned(),
2934 vec![SourceRange::new(10, 11, ModuleId::default())],
2935 ))
2936 );
2937
2938 let code11 = "
2939fn identity(x) { return x }
2940notPipeSub = 1 |> identity(!%))";
2941 assert_eq!(
2942 parse_execute(code11).await.unwrap_err(),
2945 KclError::new_syntax(KclErrorDetails::new(
2946 "There was an unexpected `!`. Try removing it.".to_owned(),
2947 vec![SourceRange::new(56, 57, ModuleId::default())],
2948 ))
2949 );
2950
2951 }
2955
2956 #[tokio::test(flavor = "multi_thread")]
2957 async fn test_start_sketch_on_invalid_kwargs() {
2958 let current_dir = std::env::current_dir().unwrap();
2959 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2960 let mut code = std::fs::read_to_string(&path).unwrap();
2961 assert_eq!(
2962 parse_execute(&code).await.unwrap_err().message(),
2963 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2964 );
2965
2966 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2967 code = std::fs::read_to_string(&path).unwrap();
2968
2969 assert_eq!(
2970 parse_execute(&code).await.unwrap_err().message(),
2971 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2972 );
2973
2974 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2975 code = std::fs::read_to_string(&path).unwrap();
2976
2977 assert_eq!(
2978 parse_execute(&code).await.unwrap_err().message(),
2979 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2980 );
2981
2982 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2983 code = std::fs::read_to_string(&path).unwrap();
2984
2985 assert_eq!(
2986 parse_execute(&code).await.unwrap_err().message(),
2987 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2988 );
2989
2990 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2991 code = std::fs::read_to_string(&path).unwrap();
2992
2993 assert_eq!(
2994 parse_execute(&code).await.unwrap_err().message(),
2995 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2996 );
2997 }
2998
2999 #[tokio::test(flavor = "multi_thread")]
3000 async fn test_math_negative_variable_in_binary_expression() {
3001 let ast = r#"sigmaAllow = 35000 // psi
3002width = 1 // inch
3003
3004p = 150 // lbs
3005distance = 6 // inches
3006FOS = 2
3007
3008leg1 = 5 // inches
3009leg2 = 8 // inches
3010
3011thickness_squared = distance * p * FOS * 6 / sigmaAllow
3012thickness = 0.56 // inches. App does not support square root function yet
3013
3014bracket = startSketchOn(XY)
3015 |> startProfile(at = [0,0])
3016 |> line(end = [0, leg1])
3017 |> line(end = [leg2, 0])
3018 |> line(end = [0, -thickness])
3019 |> line(end = [-leg2 + thickness, 0])
3020"#;
3021 parse_execute(ast).await.unwrap();
3022 }
3023
3024 #[tokio::test(flavor = "multi_thread")]
3025 async fn test_execute_function_no_return() {
3026 let ast = r#"fn test(@origin) {
3027 origin
3028}
3029
3030test([0, 0])
3031"#;
3032 let result = parse_execute(ast).await;
3033 assert!(result.is_err());
3034 assert!(result.unwrap_err().to_string().contains("undefined"));
3035 }
3036
3037 #[tokio::test(flavor = "multi_thread")]
3038 async fn test_max_stack_size_exceeded_error() {
3039 let ast = r#"
3040fn forever(@n) {
3041 return 1 + forever(n)
3042}
3043
3044forever(1)
3045"#;
3046 let result = parse_execute(ast).await;
3047 let err = result.unwrap_err();
3048 assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
3049 }
3050
3051 #[tokio::test(flavor = "multi_thread")]
3052 async fn test_math_doubly_nested_parens() {
3053 let ast = r#"sigmaAllow = 35000 // psi
3054width = 4 // inch
3055p = 150 // Force on shelf - lbs
3056distance = 6 // inches
3057FOS = 2
3058leg1 = 5 // inches
3059leg2 = 8 // inches
3060thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
3061thickness = 0.32 // inches. App does not support square root function yet
3062bracket = startSketchOn(XY)
3063 |> startProfile(at = [0,0])
3064 |> line(end = [0, leg1])
3065 |> line(end = [leg2, 0])
3066 |> line(end = [0, -thickness])
3067 |> line(end = [-1 * leg2 + thickness, 0])
3068 |> line(end = [0, -1 * leg1 + thickness])
3069 |> close()
3070 |> extrude(length = width)
3071"#;
3072 parse_execute(ast).await.unwrap();
3073 }
3074
3075 #[tokio::test(flavor = "multi_thread")]
3076 async fn test_math_nested_parens_one_less() {
3077 let ast = r#" sigmaAllow = 35000 // psi
3078width = 4 // inch
3079p = 150 // Force on shelf - lbs
3080distance = 6 // inches
3081FOS = 2
3082leg1 = 5 // inches
3083leg2 = 8 // inches
3084thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
3085thickness = 0.32 // inches. App does not support square root function yet
3086bracket = startSketchOn(XY)
3087 |> startProfile(at = [0,0])
3088 |> line(end = [0, leg1])
3089 |> line(end = [leg2, 0])
3090 |> line(end = [0, -thickness])
3091 |> line(end = [-1 * leg2 + thickness, 0])
3092 |> line(end = [0, -1 * leg1 + thickness])
3093 |> close()
3094 |> extrude(length = width)
3095"#;
3096 parse_execute(ast).await.unwrap();
3097 }
3098
3099 #[tokio::test(flavor = "multi_thread")]
3100 async fn test_fn_as_operand() {
3101 let ast = r#"fn f() { return 1 }
3102x = f()
3103y = x + 1
3104z = f() + 1
3105w = f() + f()
3106"#;
3107 parse_execute(ast).await.unwrap();
3108 }
3109
3110 #[tokio::test(flavor = "multi_thread")]
3111 async fn kcl_test_ids_stable_between_executions() {
3112 let code = r#"sketch001 = startSketchOn(XZ)
3113|> startProfile(at = [61.74, 206.13])
3114|> xLine(length = 305.11, tag = $seg01)
3115|> yLine(length = -291.85)
3116|> xLine(length = -segLen(seg01))
3117|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3118|> close()
3119|> extrude(length = 40.14)
3120|> shell(
3121 thickness = 3.14,
3122 faces = [seg01]
3123)
3124"#;
3125
3126 let ctx = crate::test_server::new_context(true, None).await.unwrap();
3127 let old_program = crate::Program::parse_no_errs(code).unwrap();
3128
3129 if let Err(err) = ctx.run_with_caching(old_program).await {
3131 let report = err.into_miette_report_with_outputs(code).unwrap();
3132 let report = miette::Report::new(report);
3133 panic!("Error executing program: {report:?}");
3134 }
3135
3136 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3138
3139 let code = r#"sketch001 = startSketchOn(XZ)
3140|> startProfile(at = [62.74, 206.13])
3141|> xLine(length = 305.11, tag = $seg01)
3142|> yLine(length = -291.85)
3143|> xLine(length = -segLen(seg01))
3144|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3145|> close()
3146|> extrude(length = 40.14)
3147|> shell(
3148 faces = [seg01],
3149 thickness = 3.14,
3150)
3151"#;
3152
3153 let program = crate::Program::parse_no_errs(code).unwrap();
3155 ctx.run_with_caching(program).await.unwrap();
3157
3158 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3159
3160 assert_eq!(id_generator, new_id_generator);
3161 }
3162
3163 #[tokio::test(flavor = "multi_thread")]
3164 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
3165 let code = r#"sketch001 = startSketchOn(XZ)
3166|> startProfile(at = [61.74, 206.13])
3167|> xLine(length = 305.11, tag = $seg01)
3168|> yLine(length = -291.85)
3169|> xLine(length = -segLen(seg01))
3170|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3171|> close()
3172|> extrude(length = 40.14)
3173|> shell(
3174 thickness = 3.14,
3175 faces = [seg01]
3176)
3177"#;
3178
3179 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
3180 let old_program = crate::Program::parse_no_errs(code).unwrap();
3181
3182 ctx.run_with_caching(old_program.clone()).await.unwrap();
3184
3185 let settings_state = cache::read_old_ast().await.unwrap().settings;
3186
3187 assert_eq!(settings_state, ctx.settings);
3189
3190 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3192
3193 ctx.run_with_caching(old_program.clone()).await.unwrap();
3195
3196 let settings_state = cache::read_old_ast().await.unwrap().settings;
3197
3198 assert_eq!(settings_state, ctx.settings);
3200
3201 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3203
3204 ctx.run_with_caching(old_program).await.unwrap();
3206
3207 let settings_state = cache::read_old_ast().await.unwrap().settings;
3208
3209 assert_eq!(settings_state, ctx.settings);
3211
3212 ctx.close().await;
3213 }
3214
3215 #[tokio::test(flavor = "multi_thread")]
3216 async fn mock_after_not_mock() {
3217 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3218 let program = crate::Program::parse_no_errs("x = 2").unwrap();
3219 let result = ctx.run_with_caching(program).await.unwrap();
3220 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
3221
3222 let ctx2 = ExecutorContext::new_mock(None).await;
3223 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
3224 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
3225 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
3226
3227 ctx.close().await;
3228 ctx2.close().await;
3229 }
3230
3231 #[tokio::test(flavor = "multi_thread")]
3232 async fn mock_then_add_extrude_then_mock_again() {
3233 let code = "s = sketch(on = XY) {
3234 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
3235 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
3236 coincident([line1.end, line2.start])
3237 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
3238 coincident([line2.end, line3.start])
3239 coincident([line1.start, line3.end])
3240}
3241 ";
3242 let ctx = ExecutorContext::new_mock(None).await;
3243 let program = crate::Program::parse_no_errs(code).unwrap();
3244 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3245 assert!(result.variables.contains_key("s"), "actual: {:?}", &result.variables);
3246
3247 let code2 = code.to_owned()
3248 + "
3249region001 = region(point = [1mm, 1mm], sketch = s)
3250extrude001 = extrude(region001, length = 1)
3251 ";
3252 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3253 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
3254 assert!(
3255 result.variables.contains_key("region001"),
3256 "actual: {:?}",
3257 &result.variables
3258 );
3259
3260 ctx.close().await;
3261 }
3262
3263 #[cfg(feature = "artifact-graph")]
3264 #[tokio::test(flavor = "multi_thread")]
3265 async fn mock_has_stable_ids() {
3266 let ctx = ExecutorContext::new_mock(None).await;
3267 let mock_config = MockConfig {
3268 use_prev_memory: false,
3269 ..Default::default()
3270 };
3271 let code = "sk = startSketchOn(XY)
3272 |> startProfile(at = [0, 0])";
3273 let program = crate::Program::parse_no_errs(code).unwrap();
3274 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
3275 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3276 assert!(!ids.is_empty(), "IDs should not be empty");
3277
3278 let ctx2 = ExecutorContext::new_mock(None).await;
3279 let program2 = crate::Program::parse_no_errs(code).unwrap();
3280 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
3281 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3282
3283 assert_eq!(ids, ids2, "Generated IDs should match");
3284 ctx.close().await;
3285 ctx2.close().await;
3286 }
3287
3288 #[tokio::test(flavor = "multi_thread")]
3289 async fn mock_memory_restore_preserves_module_maps() {
3290 clear_mem_cache().await;
3291
3292 let ctx = ExecutorContext::new_mock(None).await;
3293 let cold_start = MockConfig {
3294 use_prev_memory: false,
3295 ..Default::default()
3296 };
3297 ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
3298
3299 let mem = cache::read_old_memory().await.unwrap();
3300 assert!(
3301 mem.path_to_source_id.len() > 3,
3302 "expected prelude imports to populate multiple modules, got {:?}",
3303 mem.path_to_source_id
3304 );
3305
3306 let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
3307 ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
3308
3309 assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
3310 assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
3311 assert_eq!(exec_state.global.module_infos, mem.module_infos);
3312
3313 clear_mem_cache().await;
3314 ctx.close().await;
3315 }
3316
3317 #[cfg(feature = "artifact-graph")]
3318 #[tokio::test(flavor = "multi_thread")]
3319 async fn sim_sketch_mode_real_mock_real() {
3320 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3321 let code = r#"sketch001 = startSketchOn(XY)
3322profile001 = startProfile(sketch001, at = [0, 0])
3323 |> line(end = [10, 0])
3324 |> line(end = [0, 10])
3325 |> line(end = [-10, 0])
3326 |> line(end = [0, -10])
3327 |> close()
3328"#;
3329 let program = crate::Program::parse_no_errs(code).unwrap();
3330 let result = ctx.run_with_caching(program).await.unwrap();
3331 assert_eq!(result.operations.len(), 1);
3332
3333 let mock_ctx = ExecutorContext::new_mock(None).await;
3334 let mock_program = crate::Program::parse_no_errs(code).unwrap();
3335 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
3336 assert_eq!(mock_result.operations.len(), 1);
3337
3338 let code2 = code.to_owned()
3339 + r#"
3340extrude001 = extrude(profile001, length = 10)
3341"#;
3342 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3343 let result = ctx.run_with_caching(program2).await.unwrap();
3344 assert_eq!(result.operations.len(), 2);
3345
3346 ctx.close().await;
3347 mock_ctx.close().await;
3348 }
3349
3350 #[tokio::test(flavor = "multi_thread")]
3351 async fn read_tag_version() {
3352 let ast = r#"fn bar(@t) {
3353 return startSketchOn(XY)
3354 |> startProfile(at = [0,0])
3355 |> angledLine(
3356 angle = -60,
3357 length = segLen(t),
3358 )
3359 |> line(end = [0, 0])
3360 |> close()
3361}
3362
3363sketch = startSketchOn(XY)
3364 |> startProfile(at = [0,0])
3365 |> line(end = [0, 10])
3366 |> line(end = [10, 0], tag = $tag0)
3367 |> line(endAbsolute = [0, 0])
3368
3369fn foo() {
3370 // tag0 tags an edge
3371 return bar(tag0)
3372}
3373
3374solid = sketch |> extrude(length = 10)
3375// tag0 tags a face
3376sketch2 = startSketchOn(solid, face = tag0)
3377 |> startProfile(at = [0,0])
3378 |> line(end = [0, 1])
3379 |> line(end = [1, 0])
3380 |> line(end = [0, 0])
3381
3382foo() |> extrude(length = 1)
3383"#;
3384 parse_execute(ast).await.unwrap();
3385 }
3386
3387 #[tokio::test(flavor = "multi_thread")]
3388 async fn experimental() {
3389 let code = r#"
3390startSketchOn(XY)
3391 |> startProfile(at = [0, 0], tag = $start)
3392 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3393"#;
3394 let result = parse_execute(code).await.unwrap();
3395 let issues = result.exec_state.issues();
3396 assert_eq!(issues.len(), 1);
3397 assert_eq!(issues[0].severity, Severity::Error);
3398 let msg = &issues[0].message;
3399 assert!(msg.contains("experimental"), "found {msg}");
3400
3401 let code = r#"@settings(experimentalFeatures = allow)
3402startSketchOn(XY)
3403 |> startProfile(at = [0, 0], tag = $start)
3404 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3405"#;
3406 let result = parse_execute(code).await.unwrap();
3407 let issues = result.exec_state.issues();
3408 assert!(issues.is_empty(), "issues={issues:#?}");
3409
3410 let code = r#"@settings(experimentalFeatures = warn)
3411startSketchOn(XY)
3412 |> startProfile(at = [0, 0], tag = $start)
3413 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3414"#;
3415 let result = parse_execute(code).await.unwrap();
3416 let issues = result.exec_state.issues();
3417 assert_eq!(issues.len(), 1);
3418 assert_eq!(issues[0].severity, Severity::Warning);
3419 let msg = &issues[0].message;
3420 assert!(msg.contains("experimental"), "found {msg}");
3421
3422 let code = r#"@settings(experimentalFeatures = deny)
3423startSketchOn(XY)
3424 |> startProfile(at = [0, 0], tag = $start)
3425 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3426"#;
3427 let result = parse_execute(code).await.unwrap();
3428 let issues = result.exec_state.issues();
3429 assert_eq!(issues.len(), 1);
3430 assert_eq!(issues[0].severity, Severity::Error);
3431 let msg = &issues[0].message;
3432 assert!(msg.contains("experimental"), "found {msg}");
3433
3434 let code = r#"@settings(experimentalFeatures = foo)
3435startSketchOn(XY)
3436 |> startProfile(at = [0, 0], tag = $start)
3437 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3438"#;
3439 parse_execute(code).await.unwrap_err();
3440 }
3441
3442 #[tokio::test(flavor = "multi_thread")]
3443 async fn experimental_parameter() {
3444 let code = r#"
3445fn inc(@x, @(experimental = true) amount? = 1) {
3446 return x + amount
3447}
3448
3449answer = inc(5, amount = 2)
3450"#;
3451 let result = parse_execute(code).await.unwrap();
3452 let issues = result.exec_state.issues();
3453 assert_eq!(issues.len(), 1);
3454 assert_eq!(issues[0].severity, Severity::Error);
3455 let msg = &issues[0].message;
3456 assert!(msg.contains("experimental"), "found {msg}");
3457
3458 let code = r#"
3460fn inc(@x, @(experimental = true) amount? = 1) {
3461 return x + amount
3462}
3463
3464answer = inc(5)
3465"#;
3466 let result = parse_execute(code).await.unwrap();
3467 let issues = result.exec_state.issues();
3468 assert!(issues.is_empty(), "issues={issues:#?}");
3469 }
3470
3471 #[tokio::test(flavor = "multi_thread")]
3472 async fn experimental_scalar_fixed_constraint() {
3473 let code_left = r#"@settings(experimentalFeatures = warn)
3474sketch(on = XY) {
3475 point1 = point(at = [var 0mm, var 0mm])
3476 point1.at[0] == 1mm
3477}
3478"#;
3479 let code_right = r#"@settings(experimentalFeatures = warn)
3481sketch(on = XY) {
3482 point1 = point(at = [var 0mm, var 0mm])
3483 1mm == point1.at[0]
3484}
3485"#;
3486
3487 for code in [code_left, code_right] {
3488 let result = parse_execute(code).await.unwrap();
3489 let issues = result.exec_state.issues();
3490 let Some(error) = issues
3491 .iter()
3492 .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
3493 else {
3494 panic!("found {issues:#?}");
3495 };
3496 assert_eq!(error.severity, Severity::Warning);
3497 }
3498 }
3499
3500 #[tokio::test(flavor = "multi_thread")]
3504 async fn test_tangent_line_arc_executes_with_mock_engine() {
3505 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
3506 parse_execute(&code).await.unwrap();
3507 }
3508
3509 #[tokio::test(flavor = "multi_thread")]
3510 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
3511 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
3512 parse_execute(&code).await.unwrap();
3513 }
3514
3515 #[tokio::test(flavor = "multi_thread")]
3516 async fn test_tangent_line_circle_executes_with_mock_engine() {
3517 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
3518 parse_execute(&code).await.unwrap();
3519 }
3520
3521 #[tokio::test(flavor = "multi_thread")]
3522 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
3523 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
3524 parse_execute(&code).await.unwrap();
3525 }
3526
3527 #[cfg(feature = "artifact-graph")]
3532 async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
3533 let program = crate::Program::parse_no_errs(kcl).unwrap();
3534 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3535 let mut exec_state = ExecState::new(&ctx);
3536 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3537 let outcome = exec_state.into_exec_outcome(env_ref, &ctx).await;
3538 let report = outcome.sketch_constraint_report();
3539 ctx.close().await;
3540 report
3541 }
3542
3543 #[cfg(feature = "artifact-graph")]
3544 #[tokio::test(flavor = "multi_thread")]
3545 async fn test_constraint_report_fully_constrained() {
3546 let kcl = r#"
3548@settings(experimentalFeatures = allow)
3549
3550sketch(on = YZ) {
3551 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
3552 line1.start.at[0] == 2
3553 line1.start.at[1] == 8
3554 line1.end.at[0] == 5
3555 line1.end.at[1] == 7
3556}
3557"#;
3558 let report = run_constraint_report(kcl).await;
3559 assert_eq!(report.fully_constrained.len(), 1);
3560 assert_eq!(report.under_constrained.len(), 0);
3561 assert_eq!(report.over_constrained.len(), 0);
3562 assert_eq!(report.errors.len(), 0);
3563 assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
3564 }
3565
3566 #[cfg(feature = "artifact-graph")]
3567 #[tokio::test(flavor = "multi_thread")]
3568 async fn test_constraint_report_under_constrained() {
3569 let kcl = r#"
3571sketch(on = YZ) {
3572 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
3573}
3574"#;
3575 let report = run_constraint_report(kcl).await;
3576 assert_eq!(report.fully_constrained.len(), 0);
3577 assert_eq!(report.under_constrained.len(), 1);
3578 assert_eq!(report.over_constrained.len(), 0);
3579 assert_eq!(report.errors.len(), 0);
3580 assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
3581 assert!(report.under_constrained[0].free_count > 0);
3582 }
3583
3584 #[cfg(feature = "artifact-graph")]
3585 #[tokio::test(flavor = "multi_thread")]
3586 async fn test_constraint_report_over_constrained() {
3587 let kcl = r#"
3589@settings(experimentalFeatures = allow)
3590
3591sketch(on = YZ) {
3592 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
3593 line1.start.at[0] == 2
3594 line1.start.at[1] == 8
3595 line1.end.at[0] == 5
3596 line1.end.at[1] == 7
3597 distance([line1.start, line1.end]) == 100mm
3598}
3599"#;
3600 let report = run_constraint_report(kcl).await;
3601 assert_eq!(report.over_constrained.len(), 1);
3602 assert_eq!(report.errors.len(), 0);
3603 assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
3604 assert!(report.over_constrained[0].conflict_count > 0);
3605 }
3606
3607 #[cfg(feature = "artifact-graph")]
3608 #[tokio::test(flavor = "multi_thread")]
3609 async fn test_constraint_report_multiple_sketches() {
3610 let kcl = r#"
3612@settings(experimentalFeatures = allow)
3613
3614s1 = sketch(on = YZ) {
3615 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
3616 line1.start.at[0] == 2
3617 line1.start.at[1] == 8
3618 line1.end.at[0] == 5
3619 line1.end.at[1] == 7
3620}
3621
3622s2 = sketch(on = XZ) {
3623 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
3624}
3625"#;
3626 let report = run_constraint_report(kcl).await;
3627 assert_eq!(
3628 report.fully_constrained.len()
3629 + report.under_constrained.len()
3630 + report.over_constrained.len()
3631 + report.errors.len(),
3632 2,
3633 "Expected 2 sketches total"
3634 );
3635 assert_eq!(report.fully_constrained.len(), 1);
3636 assert_eq!(report.under_constrained.len(), 1);
3637 }
3638}