1use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use anyhow::Result;
7pub use artifact::Artifact;
8pub use artifact::ArtifactCommand;
9pub use artifact::ArtifactGraph;
10pub use artifact::CapSubType;
11pub use artifact::CodeRef;
12pub use artifact::GdtAnnotationArtifact;
13pub use artifact::SketchBlock;
14pub use artifact::SketchBlockConstraint;
15pub use artifact::SketchBlockConstraintType;
16pub use artifact::StartSketchOnFace;
17pub use artifact::StartSketchOnPlane;
18use cache::GlobalState;
19pub use cache::bust_cache;
20pub use cache::clear_mem_cache;
21pub use cad_op::Group;
22pub use cad_op::Operation;
23pub use geometry::*;
24pub use id_generator::IdGenerator;
25pub(crate) use import::PreImportedGeometry;
26use indexmap::IndexMap;
27pub use kcl_value::KclObjectFields;
28pub use kcl_value::KclValue;
29use kcmc::ImageFormat;
30use kcmc::ModelingCmd;
31use kcmc::each_cmd as mcmd;
32use kcmc::ok_response::OkModelingCmdResponse;
33use kcmc::ok_response::output::TakeSnapshot;
34use kcmc::websocket::ModelingSessionData;
35use kcmc::websocket::OkWebSocketResponseData;
36use kittycad_modeling_cmds::id::ModelingCmdId;
37use kittycad_modeling_cmds::{self as kcmc};
38pub use memory::EnvironmentRef;
39#[cfg(test)]
40pub(crate) use memory::MemoryBackendKind;
41pub(crate) use modeling::ModelingCmdMeta;
42use serde::Deserialize;
43use serde::Serialize;
44pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
45pub(crate) use sketch_solve::solver_numeric_type;
46pub use sketch_transpiler::pre_execute_transpile;
47pub use sketch_transpiler::transpile_all_old_sketches_to_new;
48pub use sketch_transpiler::transpile_old_sketch_to_new;
49pub use sketch_transpiler::transpile_old_sketch_to_new_ast;
50pub use sketch_transpiler::transpile_old_sketch_to_new_with_execution;
51pub(crate) use state::ConstraintKey;
52pub(crate) use state::ConstraintState;
53pub(crate) use state::ConsumedSolidInfo;
54pub(crate) use state::ConsumedSolidKey;
55pub(crate) use state::ConsumedSolidOperation;
56pub use state::ExecState;
57pub(crate) use state::KclVersion;
58pub use state::MetaSettings;
59pub(crate) use state::ModuleArtifactState;
60pub(crate) use state::TangencyMode;
61use uuid::Uuid;
62
63use crate::CompilationIssue;
64use crate::ExecError;
65use crate::KclErrorWithOutputs;
66use crate::NodePath;
67use crate::SourceRange;
68use crate::collections::AhashIndexSet;
69use crate::engine::EngineBatchContext;
70use crate::engine::EngineManager;
71use crate::engine::GridScaleBehavior;
72use crate::errors::KclError;
73use crate::errors::KclErrorDetails;
74use crate::execution::cache::CacheInformation;
75use crate::execution::cache::CacheResult;
76use crate::execution::import_graph::Universe;
77use crate::execution::import_graph::UniverseMap;
78use crate::execution::typed_path::TypedPath;
79use crate::front::Number;
80use crate::front::Object;
81use crate::front::ObjectId;
82use crate::fs::FileManager;
83use crate::modules::ModuleExecutionOutcome;
84use crate::modules::ModuleId;
85use crate::modules::ModulePath;
86use crate::modules::ModuleRepr;
87use crate::parsing::ast::types::Expr;
88use crate::parsing::ast::types::ImportPath;
89use crate::parsing::ast::types::NodeRef;
90
91#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
92#[ts(export)]
93pub struct OperationsByModule {
94 pub map: IndexMap<ModuleId, Vec<Operation>>,
95}
96
97#[derive(Clone, Serialize, ts_rs::TS)]
98#[ts(export)]
99#[serde(rename_all = "camelCase")]
100pub struct OperationCallbackArgs {
101 pub module_id: ModuleId,
102 pub operation: Operation,
103 pub index: usize,
104}
105
106pub trait ExecutionCallbacks: std::fmt::Debug + Send + Sync + 'static {
107 fn on_operation(&self, _args: OperationCallbackArgs) {}
108}
109
110impl OperationsByModule {
111 pub fn count(&self) -> usize {
112 self.map.values().map(Vec::len).sum()
113 }
114
115 pub fn is_empty(&self) -> bool {
116 self.map.values().all(Vec::is_empty)
117 }
118
119 pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
120 self.map.get(module_id)
121 }
122
123 pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
124 self.map.values()
125 }
126
127 pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
128 self.map.insert(module_id, operations);
129 }
130}
131
132pub(crate) mod annotations;
133mod artifact;
134pub(crate) mod cache;
135mod cad_op;
136mod exec_ast;
137pub mod fn_call;
138#[cfg(test)]
139mod freedom_analysis_tests;
140mod geometry;
141mod id_generator;
142mod import;
143mod import_graph;
144pub(crate) mod kcl_value;
145mod memory;
146mod modeling;
147mod sketch_solve;
148mod sketch_transpiler;
149mod state;
150pub mod typed_path;
151pub(crate) mod types;
152
153pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
154pub(crate) const SKETCH_OBJECT_META: &str = "meta";
155pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
156
157macro_rules! control_continue {
162 ($control_flow:expr) => {{
163 let cf = $control_flow;
164 if cf.is_some_return() {
165 return Ok(cf);
166 } else {
167 cf.into_value()
168 }
169 }};
170}
171pub(crate) use control_continue;
173
174macro_rules! early_return {
179 ($control_flow:expr) => {{
180 let cf = $control_flow;
181 if cf.is_some_return() {
182 return Err(EarlyReturn::from(cf));
183 } else {
184 cf.into_value()
185 }
186 }};
187}
188pub(crate) use early_return;
190
191#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
192pub enum ControlFlowKind {
193 #[default]
194 Continue,
195 Exit,
196}
197
198impl ControlFlowKind {
199 pub fn is_some_return(&self) -> bool {
201 match self {
202 ControlFlowKind::Continue => false,
203 ControlFlowKind::Exit => true,
204 }
205 }
206}
207
208#[must_use = "You should always handle the control flow value when it is returned"]
209#[derive(Debug, Clone, PartialEq, Serialize)]
210pub struct KclValueControlFlow {
211 value: KclValue,
213 pub control: ControlFlowKind,
214}
215
216impl KclValue {
217 pub(crate) fn continue_(self) -> KclValueControlFlow {
218 KclValueControlFlow {
219 value: self,
220 control: ControlFlowKind::Continue,
221 }
222 }
223
224 pub(crate) fn exit(self) -> KclValueControlFlow {
225 KclValueControlFlow {
226 value: self,
227 control: ControlFlowKind::Exit,
228 }
229 }
230}
231
232impl KclValueControlFlow {
233 pub fn is_some_return(&self) -> bool {
235 self.control.is_some_return()
236 }
237
238 pub(crate) fn into_value(self) -> KclValue {
239 self.value
240 }
241}
242
243#[must_use = "You should always handle the control flow value when it is returned"]
250#[derive(Debug, Clone)]
251pub(crate) enum EarlyReturn {
252 Value(KclValueControlFlow),
254 Error(KclError),
256}
257
258impl From<KclValueControlFlow> for EarlyReturn {
259 fn from(cf: KclValueControlFlow) -> Self {
260 EarlyReturn::Value(cf)
261 }
262}
263
264impl From<KclError> for EarlyReturn {
265 fn from(err: KclError) -> Self {
266 EarlyReturn::Error(err)
267 }
268}
269
270pub(crate) enum StatementKind<'a> {
271 Declaration { name: &'a str },
272 Expression,
273}
274
275#[derive(Debug, Clone, Copy)]
276pub enum PreserveMem {
277 Normal,
278 Always,
279}
280
281impl PreserveMem {
282 fn normal(self) -> bool {
283 match self {
284 PreserveMem::Normal => true,
285 PreserveMem::Always => false,
286 }
287 }
288}
289
290#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
292#[ts(export)]
293#[serde(rename_all = "camelCase")]
294pub struct ExecOutcome {
295 pub variables: IndexMap<String, KclValue>,
297 pub operations: OperationsByModule,
300 pub artifact_graph: ArtifactGraph,
302 #[serde(skip)]
304 pub scene_objects: Vec<Object>,
305 #[serde(skip)]
308 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
309 #[serde(skip)]
310 pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
311 pub issues: Vec<CompilationIssue>,
313 pub filenames: IndexMap<ModuleId, ModulePath>,
315 pub default_planes: Option<DefaultPlanes>,
317}
318
319#[derive(Debug, Clone, Copy, PartialEq)]
323enum SegmentFreedom {
324 Free,
325 Fixed,
326 Conflict,
327 Error,
329}
330
331impl From<crate::front::Freedom> for SegmentFreedom {
332 fn from(f: crate::front::Freedom) -> Self {
333 match f {
334 crate::front::Freedom::Free => Self::Free,
335 crate::front::Freedom::Fixed => Self::Fixed,
336 crate::front::Freedom::Conflict => Self::Conflict,
337 }
338 }
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
343pub enum ConstraintKind {
344 FullyConstrained,
345 UnderConstrained,
346 OverConstrained,
347 Error,
351}
352
353#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
360pub struct SketchConstraintStatus {
361 pub name: String,
363 pub status: ConstraintKind,
365 pub free_count: usize,
367 pub conflict_count: usize,
369 pub total_count: usize,
371}
372
373#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
375pub struct SketchConstraintReport {
376 pub fully_constrained: Vec<SketchConstraintStatus>,
377 pub under_constrained: Vec<SketchConstraintStatus>,
378 pub over_constrained: Vec<SketchConstraintStatus>,
379 pub errors: Vec<SketchConstraintStatus>,
382}
383
384pub(crate) fn sketch_constraint_status_for_sketch(
393 scene_objects: &[Object],
394 sketch_obj: &Object,
395) -> Option<SketchConstraintStatus> {
396 use crate::front::ObjectKind;
397 use crate::front::Segment;
398
399 let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
400 return None;
401 };
402
403 let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
405 let obj = scene_objects.get(id.0)?;
406 if let ObjectKind::Segment {
407 segment: Segment::Point(p),
408 } = &obj.kind
409 {
410 Some(p.freedom())
411 } else {
412 None
413 }
414 };
415
416 let mut free_count: usize = 0;
417 let mut conflict_count: usize = 0;
418 let mut error_count: usize = 0;
419 let mut total_count: usize = 0;
420
421 for &seg_id in &sketch.segments {
422 let Some(seg_obj) = scene_objects.get(seg_id.0) else {
423 continue;
424 };
425 let ObjectKind::Segment { segment } = &seg_obj.kind else {
426 continue;
427 };
428 if let Segment::Point(p) = segment
431 && p.owner.is_some()
432 {
433 continue;
434 }
435 let freedom = segment
436 .freedom(lookup)
437 .map(SegmentFreedom::from)
438 .unwrap_or(SegmentFreedom::Error);
439 total_count += 1;
440 match freedom {
441 SegmentFreedom::Free => free_count += 1,
442 SegmentFreedom::Conflict => conflict_count += 1,
443 SegmentFreedom::Error => error_count += 1,
444 SegmentFreedom::Fixed => {}
445 }
446 }
447
448 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 Some(SketchConstraintStatus {
459 name: sketch_obj.label.clone(),
460 status,
461 free_count,
462 conflict_count,
463 total_count,
464 })
465}
466
467pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
468 let mut fully_constrained = Vec::new();
469 let mut under_constrained = Vec::new();
470 let mut over_constrained = Vec::new();
471 let mut errors = Vec::new();
472
473 for obj in scene_objects {
474 let Some(entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
475 continue;
476 };
477 match entry.status {
478 ConstraintKind::FullyConstrained => fully_constrained.push(entry),
479 ConstraintKind::UnderConstrained => under_constrained.push(entry),
480 ConstraintKind::OverConstrained => over_constrained.push(entry),
481 ConstraintKind::Error => errors.push(entry),
482 }
483 }
484
485 SketchConstraintReport {
486 fully_constrained,
487 under_constrained,
488 over_constrained,
489 errors,
490 }
491}
492
493impl ExecOutcome {
494 pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
495 debug_assert!(
496 id.0 < self.scene_objects.len(),
497 "Requested object ID {} but only have {} objects",
498 id.0,
499 self.scene_objects.len()
500 );
501 self.scene_objects.get(id.0)
502 }
503
504 pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
506 self.issues.iter().filter(|error| error.is_err())
507 }
508
509 pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
516 sketch_constraint_report_from_scene_objects(&self.scene_objects)
517 }
518}
519
520#[derive(Debug, Clone, PartialEq)]
522pub struct MockConfig {
523 pub use_prev_memory: bool,
524 pub sketch_block_id: Option<ObjectId>,
527 pub freedom_analysis: bool,
530 pub segment_ids_edited: AhashIndexSet<ObjectId>,
532 pub drag_anchors: Vec<SegmentDragAnchor>,
534}
535
536#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
537#[ts(export, export_to = "FrontendApi.ts")]
538#[serde(rename_all = "camelCase")]
539pub struct SegmentDragAnchor {
540 pub segment_id: ObjectId,
541 pub target: crate::front::Point2d<Number>,
542}
543
544impl Default for MockConfig {
545 fn default() -> Self {
546 Self {
547 use_prev_memory: true,
549 sketch_block_id: None,
550 freedom_analysis: true,
551 segment_ids_edited: AhashIndexSet::default(),
552 drag_anchors: Vec::new(),
553 }
554 }
555}
556
557impl MockConfig {
558 pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
560 Self {
561 sketch_block_id: Some(sketch_block_id),
562 ..Default::default()
563 }
564 }
565
566 #[must_use]
567 pub(crate) fn no_freedom_analysis(mut self) -> Self {
568 self.freedom_analysis = false;
569 self
570 }
571}
572
573#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
574#[ts(export)]
575#[serde(rename_all = "camelCase")]
576pub struct DefaultPlanes {
577 pub xy: uuid::Uuid,
578 pub xz: uuid::Uuid,
579 pub yz: uuid::Uuid,
580 pub neg_xy: uuid::Uuid,
581 pub neg_xz: uuid::Uuid,
582 pub neg_yz: uuid::Uuid,
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
586#[ts(export)]
587#[serde(tag = "type", rename_all = "camelCase")]
588pub struct TagIdentifier {
589 pub value: String,
590 #[serde(skip)]
593 pub info: Vec<(usize, TagEngineInfo)>,
594 #[serde(skip)]
595 pub meta: Vec<Metadata>,
596}
597
598impl TagIdentifier {
599 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
601 for (e, info) in self.info.iter().rev() {
602 if *e <= at_epoch {
603 return Some(info);
604 }
605 }
606
607 None
608 }
609
610 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
612 self.info.last().map(|i| &i.1)
613 }
614
615 pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
618 let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
619 return vec![];
620 };
621 self.info
622 .iter()
623 .rev()
624 .take_while(|(e, _)| *e == cur_epoch)
625 .map(|(_, info)| info)
626 .collect()
627 }
628
629 pub fn merge_info(&mut self, other: &TagIdentifier) {
631 assert_eq!(&self.value, &other.value);
632 for (oe, ot) in &other.info {
633 if let Some((e, t)) = self.info.last_mut() {
634 if *e > *oe {
636 continue;
637 }
638 if e == oe {
640 *t = ot.clone();
641 continue;
642 }
643 }
644 self.info.push((*oe, ot.clone()));
645 }
646 }
647
648 pub fn geometry(&self) -> Option<Geometry> {
649 self.get_cur_info().map(|info| info.geometry.clone())
650 }
651}
652
653impl Eq for TagIdentifier {}
654
655impl std::fmt::Display for TagIdentifier {
656 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657 write!(f, "{}", self.value)
658 }
659}
660
661impl std::str::FromStr for TagIdentifier {
662 type Err = KclError;
663
664 fn from_str(s: &str) -> Result<Self, Self::Err> {
665 Ok(Self {
666 value: s.to_string(),
667 info: Vec::new(),
668 meta: Default::default(),
669 })
670 }
671}
672
673impl Ord for TagIdentifier {
674 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
675 self.value.cmp(&other.value)
676 }
677}
678
679impl PartialOrd for TagIdentifier {
680 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
681 Some(self.cmp(other))
682 }
683}
684
685impl std::hash::Hash for TagIdentifier {
686 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
687 self.value.hash(state);
688 }
689}
690
691#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
693#[ts(export)]
694#[serde(tag = "type", rename_all = "camelCase")]
695pub struct TagEngineInfo {
696 pub id: uuid::Uuid,
698 pub geometry: Geometry,
700 pub path: Option<Path>,
702 pub surface: Option<ExtrudeSurface>,
704}
705
706#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
707pub enum BodyType {
708 Root,
709 Block,
710}
711
712#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
714#[ts(export)]
715#[serde(rename_all = "camelCase")]
716pub struct Metadata {
717 pub source_range: SourceRange,
719}
720
721impl From<Metadata> for Vec<SourceRange> {
722 fn from(meta: Metadata) -> Self {
723 vec![meta.source_range]
724 }
725}
726
727impl From<&Metadata> for SourceRange {
728 fn from(meta: &Metadata) -> Self {
729 meta.source_range
730 }
731}
732
733impl From<SourceRange> for Metadata {
734 fn from(source_range: SourceRange) -> Self {
735 Self { source_range }
736 }
737}
738
739impl<T> From<NodeRef<'_, T>> for Metadata {
740 fn from(node: NodeRef<'_, T>) -> Self {
741 Self {
742 source_range: SourceRange::new(node.start, node.end, node.module_id),
743 }
744 }
745}
746
747impl From<&Expr> for Metadata {
748 fn from(expr: &Expr) -> Self {
749 Self {
750 source_range: SourceRange::from(expr),
751 }
752 }
753}
754
755impl Metadata {
756 pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
757 if meta.len() == 1 {
758 let meta = &meta[0];
759 return crate::front::SourceRef::Simple {
760 range: meta.source_range,
761 node_path,
762 };
763 }
764 crate::front::SourceRef::BackTrace {
765 ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
766 }
767 }
768}
769
770#[derive(PartialEq, Debug, Default, Clone)]
772pub enum ContextType {
773 #[default]
775 Live,
776
777 Mock,
781
782 MockCustomForwarded,
784}
785
786#[derive(Debug, Clone)]
790pub struct ExecutorContext {
791 pub engine: Arc<Box<dyn EngineManager>>,
792 pub engine_batch: EngineBatchContext,
793 pub fs: Arc<FileManager>,
794 pub settings: ExecutorSettings,
795 pub context_type: ContextType,
796 pub execution_callbacks: Option<Arc<dyn ExecutionCallbacks>>,
797}
798
799#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
801#[ts(export)]
802pub struct ExecutorSettings {
803 pub highlight_edges: bool,
805 pub enable_ssao: bool,
807 pub show_grid: bool,
809 pub replay: Option<String>,
812 pub project_directory: Option<TypedPath>,
815 pub current_file: Option<TypedPath>,
818 pub fixed_size_grid: bool,
820 #[serde(default, skip_serializing_if = "is_false")]
826 pub skip_artifact_graph: bool,
827 #[serde(default, skip_serializing_if = "Option::is_none")]
830 pub heartbeats: Option<u64>,
831 #[serde(default, skip_serializing_if = "Option::is_none")]
834 pub default_backface_color: Option<String>,
835}
836
837fn is_false(b: &bool) -> bool {
838 !*b
839}
840
841impl Default for ExecutorSettings {
842 fn default() -> Self {
843 Self {
844 highlight_edges: true,
845 enable_ssao: false,
846 show_grid: false,
847 replay: None,
848 project_directory: None,
849 current_file: None,
850 fixed_size_grid: true,
851 skip_artifact_graph: false,
852 heartbeats: None,
853 default_backface_color: None,
854 }
855 }
856}
857
858impl From<crate::settings::types::Configuration> for ExecutorSettings {
859 fn from(config: crate::settings::types::Configuration) -> Self {
860 Self::from(config.settings)
861 }
862}
863
864impl From<crate::settings::types::Settings> for ExecutorSettings {
865 fn from(settings: crate::settings::types::Settings) -> Self {
866 let modeling_settings = settings.modeling.unwrap_or_default();
867 Self {
868 highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
869 enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
870 show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
871 replay: None,
872 project_directory: None,
873 current_file: None,
874 fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
875 skip_artifact_graph: false,
876 heartbeats: None,
877 default_backface_color: modeling_settings.backface_color.map(|color| color.0),
878 }
879 }
880}
881
882impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
883 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
884 Self::from(config.settings.modeling)
885 }
886}
887
888impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
889 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
890 Self {
891 highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
892 enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
893 show_grid: modeling.show_scale_grid.unwrap_or_default(),
894 replay: None,
895 project_directory: None,
896 current_file: None,
897 fixed_size_grid: true,
898 skip_artifact_graph: false,
899 heartbeats: None,
900 default_backface_color: modeling.backface_color.map(|color| color.0),
901 }
902 }
903}
904
905impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
906 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
907 Self {
908 highlight_edges: modeling.highlight_edges.into(),
909 enable_ssao: modeling.enable_ssao.into(),
910 show_grid: Default::default(),
911 replay: None,
912 project_directory: None,
913 current_file: None,
914 fixed_size_grid: true,
915 skip_artifact_graph: false,
916 heartbeats: None,
917 default_backface_color: None,
918 }
919 }
920}
921
922impl ExecutorSettings {
923 pub fn with_current_file(&mut self, current_file: TypedPath) {
925 if current_file.extension() == Some("kcl") {
927 self.current_file = Some(current_file.clone());
928 if let Some(parent) = current_file.parent() {
930 self.project_directory = Some(parent);
931 } else {
932 self.project_directory = Some(TypedPath::from(""));
933 }
934 } else {
935 self.project_directory = Some(current_file);
936 }
937 }
938}
939
940impl ExecutorContext {
941 pub fn new_with_engine_and_fs(
943 engine: Arc<Box<dyn EngineManager>>,
944 fs: Arc<FileManager>,
945 settings: ExecutorSettings,
946 ) -> Self {
947 ExecutorContext {
948 engine,
949 engine_batch: EngineBatchContext::default(),
950 fs,
951 settings,
952 context_type: ContextType::Live,
953 execution_callbacks: Default::default(),
954 }
955 }
956
957 fn clone_with_fresh_execution_batch(&self) -> Self {
958 Self {
959 engine: self.engine.clone(),
960 engine_batch: EngineBatchContext::new(),
961 fs: self.fs.clone(),
962 settings: self.settings.clone(),
963 context_type: self.context_type.clone(),
964 execution_callbacks: self.execution_callbacks.clone(),
965 }
966 }
967
968 #[cfg(not(target_arch = "wasm32"))]
970 pub fn new_with_engine(engine: Arc<Box<dyn EngineManager>>, settings: ExecutorSettings) -> Self {
971 Self::new_with_engine_and_fs(engine, Arc::new(FileManager::new()), settings)
972 }
973
974 #[cfg(not(target_arch = "wasm32"))]
976 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
977 let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
978 let (ws, _headers) = client
979 .modeling()
980 .commands_ws(kittycad::modeling::CommandsWsParams {
981 api_call_id: None,
982 fps: None,
983 order_independent_transparency: None,
984 post_effect: if settings.enable_ssao {
985 Some(kittycad::types::PostEffectType::Ssao)
986 } else {
987 None
988 },
989 replay: settings.replay.clone(),
990 show_grid: if settings.show_grid { Some(true) } else { None },
991 pool: None,
992 pr,
993 unlocked_framerate: None,
994 webrtc: Some(false),
995 video_res_width: None,
996 video_res_height: None,
997 })
998 .await?;
999
1000 let engine: Arc<Box<dyn EngineManager>> = Arc::new(Box::new(
1001 crate::engine::conn::EngineConnection::new(ws, settings.heartbeats).await?,
1002 ));
1003
1004 Ok(Self::new_with_engine(engine, settings))
1005 }
1006
1007 #[cfg(target_arch = "wasm32")]
1008 pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
1009 Self::new_with_engine_and_fs(engine, fs, settings)
1010 }
1011
1012 #[cfg(not(target_arch = "wasm32"))]
1013 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
1014 ExecutorContext {
1015 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
1016 engine_batch: EngineBatchContext::default(),
1017 fs: Arc::new(FileManager::new()),
1018 settings: settings.unwrap_or_default(),
1019 context_type: ContextType::Mock,
1020 execution_callbacks: Default::default(),
1021 }
1022 }
1023
1024 #[cfg(target_arch = "wasm32")]
1025 pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
1026 ExecutorContext {
1027 engine,
1028 engine_batch: EngineBatchContext::default(),
1029 fs,
1030 settings,
1031 context_type: ContextType::Mock,
1032 execution_callbacks: Default::default(),
1033 }
1034 }
1035
1036 #[cfg(target_arch = "wasm32")]
1039 pub fn new_mock_for_lsp(
1040 fs_manager: crate::fs::wasm::FileSystemManager,
1041 settings: ExecutorSettings,
1042 ) -> Result<Self, String> {
1043 use crate::mock_engine;
1044
1045 let mock_engine = Arc::new(Box::new(
1046 mock_engine::EngineConnection::new().map_err(|e| format!("Failed to create mock engine: {:?}", e))?,
1047 ) as Box<dyn EngineManager>);
1048
1049 let fs = Arc::new(FileManager::new(fs_manager));
1050
1051 Ok(ExecutorContext {
1052 engine: mock_engine,
1053 engine_batch: EngineBatchContext::default(),
1054 fs,
1055 settings,
1056 context_type: ContextType::Mock,
1057 execution_callbacks: Default::default(),
1058 })
1059 }
1060
1061 #[cfg(not(target_arch = "wasm32"))]
1062 pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
1063 ExecutorContext {
1064 engine,
1065 engine_batch: EngineBatchContext::default(),
1066 fs: Arc::new(FileManager::new()),
1067 settings: Default::default(),
1068 context_type: ContextType::MockCustomForwarded,
1069 execution_callbacks: Default::default(),
1070 }
1071 }
1072
1073 #[cfg(not(target_arch = "wasm32"))]
1079 pub async fn new_with_client(
1080 settings: ExecutorSettings,
1081 token: Option<String>,
1082 engine_addr: Option<String>,
1083 ) -> Result<Self> {
1084 let client = crate::engine::new_zoo_client(token, engine_addr)?;
1086
1087 let ctx = Self::new(&client, settings).await?;
1088 Ok(ctx)
1089 }
1090
1091 #[cfg(not(target_arch = "wasm32"))]
1096 pub async fn new_with_default_client() -> Result<Self> {
1097 let ctx = Self::new_with_client(Default::default(), None, None).await?;
1099 Ok(ctx)
1100 }
1101
1102 #[cfg(not(target_arch = "wasm32"))]
1104 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
1105 let ctx = ExecutorContext::new_with_client(
1106 ExecutorSettings {
1107 highlight_edges: true,
1108 enable_ssao: false,
1109 show_grid: false,
1110 replay: None,
1111 project_directory: None,
1112 current_file: None,
1113 fixed_size_grid: false,
1114 skip_artifact_graph: false,
1115 heartbeats: None,
1116 default_backface_color: None,
1117 },
1118 None,
1119 engine_addr,
1120 )
1121 .await?;
1122 Ok(ctx)
1123 }
1124
1125 pub fn is_mock(&self) -> bool {
1126 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1127 }
1128
1129 pub async fn no_engine_commands(&self) -> bool {
1131 self.is_mock()
1132 }
1133
1134 pub async fn send_clear_scene(
1135 &self,
1136 exec_state: &mut ExecState,
1137 source_range: crate::execution::SourceRange,
1138 ) -> Result<(), KclError> {
1139 exec_state.mod_local.artifacts.clear();
1142 exec_state.global.root_module_artifacts.clear();
1143 exec_state.global.artifacts.clear();
1144
1145 self.engine
1146 .clear_scene(&self.engine_batch, &mut exec_state.mod_local.id_generator, source_range)
1147 .await?;
1148 if self.settings.enable_ssao {
1151 let cmd_id = exec_state.next_uuid();
1152 exec_state
1153 .batch_modeling_cmd(
1154 ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1155 ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1156 )
1157 .await?;
1158 }
1159 Ok(())
1160 }
1161
1162 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1163 cache::bust_cache().await;
1164
1165 let outcome = self.run_with_caching(crate::Program::empty()).await?;
1170
1171 Ok(outcome)
1172 }
1173
1174 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1175 self.eval_prelude(exec_state, SourceRange::synthetic())
1176 .await
1177 .map_err(KclErrorWithOutputs::no_outputs)?;
1178 exec_state
1179 .mut_stack()
1180 .push_new_root_env(true)
1181 .map_err(KclErrorWithOutputs::no_outputs)?;
1182 Ok(())
1183 }
1184
1185 fn restore_mock_memory(
1186 exec_state: &mut ExecState,
1187 mem: cache::SketchModeState,
1188 _mock_config: &MockConfig,
1189 ) -> Result<(), KclErrorWithOutputs> {
1190 *exec_state.mut_stack() = mem.stack;
1191 exec_state.global.module_infos = mem.module_infos;
1192 exec_state.global.path_to_source_id = mem.path_to_source_id;
1193 exec_state.global.id_to_source = mem.id_to_source;
1194 exec_state.mod_local.constraint_state = mem.constraint_state;
1195 let len = _mock_config
1196 .sketch_block_id
1197 .map(|sketch_block_id| sketch_block_id.0)
1198 .unwrap_or(0);
1199 if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1200 exec_state
1201 .global
1202 .root_module_artifacts
1203 .restore_scene_objects(scene_objects);
1204 } else {
1205 let message = format!(
1206 "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1207 mem.scene_objects.len(),
1208 len
1209 );
1210 debug_assert!(false, "{message}");
1211 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1212 KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1213 )));
1214 }
1215
1216 Ok(())
1217 }
1218
1219 pub async fn run_mock(
1220 &self,
1221 program: &crate::Program,
1222 mock_config: &MockConfig,
1223 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1224 assert!(
1225 self.is_mock(),
1226 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1227 );
1228
1229 let use_prev_memory = mock_config.use_prev_memory;
1230 let mut exec_state = ExecState::new_mock(self, mock_config);
1231 if use_prev_memory {
1232 match cache::read_old_memory().await {
1233 Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
1234 None => self.prepare_mem(&mut exec_state).await?,
1235 }
1236 } else {
1237 self.prepare_mem(&mut exec_state).await?
1238 };
1239
1240 exec_state
1243 .mut_stack()
1244 .push_new_env_for_scope()
1245 .map_err(KclErrorWithOutputs::no_outputs)?;
1246
1247 let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1248
1249 let mut stack = exec_state.stack().clone();
1254 let module_infos = exec_state.global.module_infos.clone();
1255 let path_to_source_id = exec_state.global.path_to_source_id.clone();
1256 let id_to_source = exec_state.global.id_to_source.clone();
1257 let constraint_state = exec_state.mod_local.constraint_state.clone();
1258 let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1259 let outcome = exec_state
1260 .into_exec_outcome(result.0, self)
1261 .await
1262 .map_err(KclErrorWithOutputs::no_outputs)?;
1263
1264 stack.squash_env(result.0).map_err(KclErrorWithOutputs::no_outputs)?;
1265 let state = cache::SketchModeState {
1266 stack,
1267 module_infos,
1268 path_to_source_id,
1269 id_to_source,
1270 constraint_state,
1271 scene_objects,
1272 };
1273 cache::write_old_memory(state).await;
1274
1275 Ok(outcome)
1276 }
1277
1278 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1279 assert!(!self.is_mock());
1280 let grid_scale = if self.settings.fixed_size_grid {
1281 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1282 } else {
1283 GridScaleBehavior::ScaleWithZoom
1284 };
1285
1286 let original_program = program.clone();
1287
1288 let (_program, exec_state, result) = match cache::read_old_ast().await {
1289 Some(mut cached_state) => {
1290 let old = CacheInformation {
1291 ast: &cached_state.main.ast,
1292 settings: &cached_state.settings,
1293 };
1294 let new = CacheInformation {
1295 ast: &program.ast,
1296 settings: &self.settings,
1297 };
1298
1299 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1301 CacheResult::ReExecute {
1302 clear_scene,
1303 reapply_settings,
1304 program: changed_program,
1305 } => {
1306 if reapply_settings
1307 && self
1308 .engine
1309 .reapply_settings(
1310 &self.engine_batch,
1311 &self.settings,
1312 Default::default(),
1313 &mut cached_state.main.exec_state.id_generator,
1314 grid_scale,
1315 )
1316 .await
1317 .is_err()
1318 {
1319 (true, program, None)
1320 } else {
1321 (
1322 clear_scene,
1323 crate::Program {
1324 ast: changed_program,
1325 original_file_contents: program.original_file_contents,
1326 },
1327 None,
1328 )
1329 }
1330 }
1331 CacheResult::CheckImportsOnly {
1332 reapply_settings,
1333 ast: changed_program,
1334 } => {
1335 let mut reapply_failed = false;
1336 if reapply_settings {
1337 if self
1338 .engine
1339 .reapply_settings(
1340 &self.engine_batch,
1341 &self.settings,
1342 Default::default(),
1343 &mut cached_state.main.exec_state.id_generator,
1344 grid_scale,
1345 )
1346 .await
1347 .is_ok()
1348 {
1349 cache::write_old_ast(GlobalState::with_settings(
1350 cached_state.clone(),
1351 self.settings.clone(),
1352 ))
1353 .await;
1354 } else {
1355 reapply_failed = true;
1356 }
1357 }
1358
1359 if reapply_failed {
1360 (true, program, None)
1361 } else {
1362 let mut new_exec_state = ExecState::new(self);
1364 let (new_universe, new_universe_map) =
1365 self.get_universe(&program, &mut new_exec_state).await?;
1366
1367 let clear_scene = new_universe.values().any(|value| {
1368 let id = value.1;
1369 match (
1370 cached_state.exec_state.get_source(id),
1371 new_exec_state.global.get_source(id),
1372 ) {
1373 (Some(s0), Some(s1)) => s0.source != s1.source,
1374 _ => false,
1375 }
1376 });
1377
1378 if !clear_scene {
1379 cache::write_old_memory(
1381 cached_state
1382 .mock_memory_state()
1383 .map_err(KclErrorWithOutputs::no_outputs)?,
1384 )
1385 .await;
1386 return cached_state
1387 .into_exec_outcome(self)
1388 .await
1389 .map_err(KclErrorWithOutputs::no_outputs);
1390 }
1391
1392 (
1393 true,
1394 crate::Program {
1395 ast: changed_program,
1396 original_file_contents: program.original_file_contents,
1397 },
1398 Some((new_universe, new_universe_map, new_exec_state)),
1399 )
1400 }
1401 }
1402 CacheResult::NoAction(true) => {
1403 if self
1404 .engine
1405 .reapply_settings(
1406 &self.engine_batch,
1407 &self.settings,
1408 Default::default(),
1409 &mut cached_state.main.exec_state.id_generator,
1410 grid_scale,
1411 )
1412 .await
1413 .is_ok()
1414 {
1415 cache::write_old_ast(GlobalState::with_settings(
1417 cached_state.clone(),
1418 self.settings.clone(),
1419 ))
1420 .await;
1421
1422 cache::write_old_memory(
1423 cached_state
1424 .mock_memory_state()
1425 .map_err(KclErrorWithOutputs::no_outputs)?,
1426 )
1427 .await;
1428 return cached_state
1429 .into_exec_outcome(self)
1430 .await
1431 .map_err(KclErrorWithOutputs::no_outputs);
1432 }
1433 (true, program, None)
1434 }
1435 CacheResult::NoAction(false) => {
1436 cache::write_old_memory(
1437 cached_state
1438 .mock_memory_state()
1439 .map_err(KclErrorWithOutputs::no_outputs)?,
1440 )
1441 .await;
1442 return cached_state
1443 .into_exec_outcome(self)
1444 .await
1445 .map_err(KclErrorWithOutputs::no_outputs);
1446 }
1447 };
1448
1449 let (exec_state, result) = match import_check_info {
1450 Some((new_universe, new_universe_map, mut new_exec_state)) => {
1451 self.send_clear_scene(&mut new_exec_state, Default::default())
1453 .await
1454 .map_err(KclErrorWithOutputs::no_outputs)?;
1455
1456 let result = self
1457 .run_concurrent(
1458 &program,
1459 &mut new_exec_state,
1460 Some((new_universe, new_universe_map)),
1461 PreserveMem::Normal,
1462 )
1463 .await;
1464
1465 (new_exec_state, result)
1466 }
1467 None if clear_scene => {
1468 let mut exec_state = cached_state.reconstitute_exec_state(self);
1470 exec_state.reset(self);
1471
1472 self.send_clear_scene(&mut exec_state, Default::default())
1473 .await
1474 .map_err(KclErrorWithOutputs::no_outputs)?;
1475
1476 let result = self
1477 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1478 .await;
1479
1480 (exec_state, result)
1481 }
1482 None => {
1483 let mut exec_state = cached_state.reconstitute_exec_state(self);
1484 exec_state
1485 .mut_stack()
1486 .restore_env(cached_state.main.result_env)
1487 .map_err(KclErrorWithOutputs::no_outputs)?;
1488
1489 let result = self
1490 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1491 .await;
1492
1493 (exec_state, result)
1494 }
1495 };
1496
1497 (program, exec_state, result)
1498 }
1499 None => {
1500 let mut exec_state = ExecState::new(self);
1501 self.send_clear_scene(&mut exec_state, Default::default())
1502 .await
1503 .map_err(KclErrorWithOutputs::no_outputs)?;
1504
1505 let result = self
1506 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1507 .await;
1508
1509 (program, exec_state, result)
1510 }
1511 };
1512
1513 if result.is_err() {
1514 cache::bust_cache().await;
1515 }
1516
1517 let result = result?;
1519
1520 cache::write_old_ast(GlobalState::new(
1524 exec_state.clone(),
1525 self.settings.clone(),
1526 original_program.ast,
1527 result.0,
1528 ))
1529 .await;
1530
1531 let outcome = exec_state
1532 .into_exec_outcome(result.0, self)
1533 .await
1534 .map_err(KclErrorWithOutputs::no_outputs)?;
1535 Ok(outcome)
1536 }
1537
1538 pub async fn run(
1542 &self,
1543 program: &crate::Program,
1544 exec_state: &mut ExecState,
1545 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1546 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1547 .await
1548 }
1549
1550 pub async fn run_concurrent(
1555 &self,
1556 program: &crate::Program,
1557 exec_state: &mut ExecState,
1558 universe_info: Option<(Universe, UniverseMap)>,
1559 preserve_mem: PreserveMem,
1560 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1561 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1564 (universe, universe_map)
1565 } else {
1566 self.get_universe(program, exec_state).await?
1567 };
1568
1569 let mut sorted_imports: Vec<_> = universe_map.iter().collect();
1575 sorted_imports.sort_by_key(|(_, import_stmt)| SourceRange::from(*import_stmt));
1576 for (_path, import_stmt) in sorted_imports {
1577 let filename = match &import_stmt.path {
1581 ImportPath::Kcl { filename } => filename.to_string(),
1582 ImportPath::Foreign { path } => path.to_string(),
1583 ImportPath::Std { .. } => continue,
1584 };
1585 if let Some((_, module_id, module_path, _)) = universe.get(&filename)
1586 && let ModulePath::Local { value, .. } = module_path
1587 {
1588 let name = import_stmt
1589 .module_name()
1590 .unwrap_or_else(|| value.file_name().unwrap_or_default());
1591 let source_range = SourceRange::from(import_stmt);
1592 exec_state.push_op(crate::execution::cad_op::Operation::ModuleInstance {
1593 name,
1594 module_id: *module_id,
1595 glob: matches!(
1596 import_stmt.selector,
1597 crate::parsing::ast::types::ImportSelector::Glob(_)
1598 ),
1599 node_path: crate::NodePath::placeholder(),
1600 source_range,
1601 });
1602 }
1603 }
1604
1605 let default_planes = self.engine.get_default_planes().read().await.clone();
1606
1607 self.eval_prelude(exec_state, SourceRange::synthetic())
1609 .await
1610 .map_err(KclErrorWithOutputs::no_outputs)?;
1611
1612 for modules in import_graph::import_graph(&universe, self)
1613 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1614 .into_iter()
1615 {
1616 #[cfg(not(target_arch = "wasm32"))]
1617 let mut set = tokio::task::JoinSet::new();
1618
1619 #[allow(clippy::type_complexity)]
1620 let (results_tx, mut results_rx): (
1621 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1622 tokio::sync::mpsc::Receiver<_>,
1623 ) = tokio::sync::mpsc::channel(1);
1624
1625 for module in modules {
1626 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1627 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1628 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1629 )));
1630 };
1631 let module_id = *module_id;
1632 let module_path = module_path.clone();
1633 let source_range = SourceRange::from(import_stmt);
1634 let module_exec_state = exec_state.clone();
1636
1637 let repr = repr.clone();
1638 let exec_ctxt = self.clone_with_fresh_execution_batch();
1639 let results_tx = results_tx.clone();
1640
1641 let exec_module = async |exec_ctxt: &ExecutorContext,
1642 repr: &ModuleRepr,
1643 module_id: ModuleId,
1644 module_path: &ModulePath,
1645 exec_state: &mut ExecState,
1646 source_range: SourceRange|
1647 -> Result<ModuleRepr, KclError> {
1648 match repr {
1649 ModuleRepr::Kcl(program, _) => {
1650 let result = exec_ctxt
1651 .exec_module_from_ast(
1652 program,
1653 module_id,
1654 module_path,
1655 exec_state,
1656 source_range,
1657 PreserveMem::Normal,
1658 )
1659 .await;
1660
1661 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1662 }
1663 ModuleRepr::Foreign(geom, _) => {
1664 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1665 .await
1666 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1667
1668 result.map(|val| ModuleRepr::Foreign(geom.clone(), Some((val, Default::default()))))
1673 }
1674 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1675 format!("Module {module_path} not found in universe"),
1676 vec![source_range],
1677 ))),
1678 }
1679 };
1680
1681 #[cfg(target_arch = "wasm32")]
1682 {
1683 wasm_bindgen_futures::spawn_local(async move {
1684 let mut exec_state = module_exec_state;
1685 let exec_ctxt = exec_ctxt;
1686
1687 let result = exec_module(
1688 &exec_ctxt,
1689 &repr,
1690 module_id,
1691 &module_path,
1692 &mut exec_state,
1693 source_range,
1694 )
1695 .await;
1696
1697 results_tx
1698 .send((module_id, module_path, result))
1699 .await
1700 .unwrap_or_default();
1701 });
1702 }
1703 #[cfg(not(target_arch = "wasm32"))]
1704 {
1705 set.spawn(async move {
1706 let mut exec_state = module_exec_state;
1707 let exec_ctxt = exec_ctxt;
1708
1709 let result = exec_module(
1710 &exec_ctxt,
1711 &repr,
1712 module_id,
1713 &module_path,
1714 &mut exec_state,
1715 source_range,
1716 )
1717 .await;
1718
1719 results_tx
1720 .send((module_id, module_path, result))
1721 .await
1722 .unwrap_or_default();
1723 });
1724 }
1725 }
1726
1727 drop(results_tx);
1728
1729 while let Some((module_id, _, result)) = results_rx.recv().await {
1730 match result {
1731 Ok(new_repr) => {
1732 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1733
1734 match &mut repr {
1735 ModuleRepr::Kcl(_, cache) => {
1736 let ModuleRepr::Kcl(_, session_data) = new_repr else {
1737 unreachable!();
1738 };
1739 *cache = session_data;
1740 }
1741 ModuleRepr::Foreign(_, cache) => {
1742 let ModuleRepr::Foreign(_, session_data) = new_repr else {
1743 unreachable!();
1744 };
1745 *cache = session_data;
1746 }
1747 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1748 }
1749
1750 exec_state.global.module_infos[&module_id].restore_repr(repr);
1751 }
1752 Err(e) => {
1753 return Err(exec_state.error_with_outputs(e, None, default_planes));
1754 }
1755 }
1756 }
1757 }
1758
1759 exec_state.mod_local.artifacts.operations.clear();
1764
1765 exec_state
1768 .global
1769 .root_module_artifacts
1770 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1771
1772 self.inner_run(program, exec_state, preserve_mem).await
1773 }
1774
1775 async fn get_universe(
1778 &self,
1779 program: &crate::Program,
1780 exec_state: &mut ExecState,
1781 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1782 exec_state.add_root_module_contents(program);
1783
1784 let mut universe = std::collections::HashMap::new();
1785
1786 let default_planes = self.engine.get_default_planes().read().await.clone();
1787
1788 let root_imports = import_graph::import_universe(
1789 self,
1790 &ModulePath::Main,
1791 &ModuleRepr::Kcl(program.ast.clone(), None),
1792 &mut universe,
1793 exec_state,
1794 )
1795 .await
1796 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1797
1798 Ok((universe, root_imports))
1799 }
1800
1801 async fn inner_run(
1804 &self,
1805 program: &crate::Program,
1806 exec_state: &mut ExecState,
1807 preserve_mem: PreserveMem,
1808 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1809 let _stats = crate::log::LogPerfStats::new("Interpretation");
1810
1811 let grid_scale = if self.settings.fixed_size_grid {
1813 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1814 } else {
1815 GridScaleBehavior::ScaleWithZoom
1816 };
1817 self.engine
1818 .reapply_settings(
1819 &self.engine_batch,
1820 &self.settings,
1821 Default::default(),
1822 exec_state.id_generator(),
1823 grid_scale,
1824 )
1825 .await
1826 .map_err(KclErrorWithOutputs::no_outputs)?;
1827
1828 let default_planes = self.engine.get_default_planes().read().await.clone();
1829 let result = self
1830 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1831 .await;
1832
1833 crate::log::log(format!(
1834 "Post interpretation KCL memory stats: {:#?}",
1835 exec_state.stack().memory.stats()
1836 ));
1837 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1838
1839 async fn write_old_memory(
1842 ctx: &ExecutorContext,
1843 exec_state: &ExecState,
1844 env_ref: EnvironmentRef,
1845 ) -> Result<(), KclError> {
1846 if ctx.is_mock() {
1847 return Ok(());
1848 }
1849 let mut stack = exec_state.stack().deep_clone()?;
1850 stack.restore_env(env_ref)?;
1851 let state = cache::SketchModeState {
1852 stack,
1853 module_infos: exec_state.global.module_infos.clone(),
1854 path_to_source_id: exec_state.global.path_to_source_id.clone(),
1855 id_to_source: exec_state.global.id_to_source.clone(),
1856 constraint_state: exec_state.mod_local.constraint_state.clone(),
1857 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1858 };
1859 cache::write_old_memory(state).await;
1860 Ok(())
1861 }
1862
1863 let env_ref = match result {
1864 Ok(env_ref) => env_ref,
1865 Err((err, env_ref)) => {
1866 if let Some(env_ref) = env_ref {
1869 write_old_memory(self, exec_state, env_ref)
1870 .await
1871 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
1872 }
1873 return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
1874 }
1875 };
1876
1877 write_old_memory(self, exec_state, env_ref)
1878 .await
1879 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
1880
1881 let session_data = self.engine.get_session_data().await;
1882
1883 Ok((env_ref, session_data))
1884 }
1885
1886 async fn execute_and_build_graph(
1889 &self,
1890 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1891 exec_state: &mut ExecState,
1892 preserve_mem: PreserveMem,
1893 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1894 let start_op = exec_state.global.root_module_artifacts.operations.len();
1900
1901 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1902 .await
1903 .map_err(|e| (e, None))?;
1904
1905 let exec_result = self
1906 .exec_module_body(
1907 program,
1908 exec_state,
1909 preserve_mem,
1910 ModuleId::default(),
1911 &ModulePath::Main,
1912 )
1913 .await
1914 .map(
1915 |ModuleExecutionOutcome {
1916 environment: env_ref,
1917 artifacts: module_artifacts,
1918 ..
1919 }| {
1920 exec_state.global.root_module_artifacts.extend(module_artifacts);
1923 env_ref
1924 },
1925 )
1926 .map_err(|(err, env_ref, module_artifacts)| {
1927 if let Some(module_artifacts) = module_artifacts {
1928 exec_state.global.root_module_artifacts.extend(module_artifacts);
1931 }
1932 (err, env_ref)
1933 });
1934
1935 let programs = &exec_state.build_program_lookup(program.clone());
1937 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1938 for op in exec_state
1939 .global
1940 .root_module_artifacts
1941 .operations
1942 .iter_mut()
1943 .skip(start_op)
1944 {
1945 op.fill_node_paths(programs, cached_body_items);
1946 }
1947 for module in exec_state.global.module_infos.values_mut() {
1948 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1949 for op in &mut outcome.artifacts.operations {
1950 op.fill_node_paths(programs, cached_body_items);
1951 }
1952 }
1953 }
1954
1955 self.engine
1957 .ensure_async_commands_completed(&self.engine_batch)
1958 .await
1959 .map_err(|e| {
1960 match &exec_result {
1961 Ok(env_ref) => (e, Some(*env_ref)),
1962 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1964 }
1965 })?;
1966
1967 self.engine.clear_queues(&self.engine_batch).await;
1970
1971 match exec_state.build_artifact_graph(&self.engine, program).await {
1972 Ok(_) => exec_result,
1973 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1974 }
1975 }
1976
1977 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1981 if exec_state.stack().memory.requires_std() {
1982 let initial_ops = exec_state.mod_local.artifacts.operations.len();
1983
1984 let path = vec!["std".to_owned(), "prelude".to_owned()];
1985 let resolved_path = ModulePath::from_std_import_path(&path)?;
1986 let id = self
1987 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1988 .await?;
1989 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1990
1991 exec_state.mut_stack().memory.set_std(module_memory)?;
1992
1993 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1999 }
2000
2001 Ok(())
2002 }
2003
2004 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
2006 self.engine
2008 .send_modeling_cmd(
2009 &self.engine_batch,
2010 uuid::Uuid::new_v4(),
2011 crate::execution::SourceRange::default(),
2012 &ModelingCmd::from(
2013 mcmd::ZoomToFit::builder()
2014 .object_ids(Default::default())
2015 .animated(false)
2016 .padding(0.1)
2017 .build(),
2018 ),
2019 )
2020 .await
2021 .map_err(KclErrorWithOutputs::no_outputs)?;
2022
2023 let resp = self
2025 .engine
2026 .send_modeling_cmd(
2027 &self.engine_batch,
2028 uuid::Uuid::new_v4(),
2029 crate::execution::SourceRange::default(),
2030 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
2031 )
2032 .await
2033 .map_err(KclErrorWithOutputs::no_outputs)?;
2034
2035 let OkWebSocketResponseData::Modeling {
2036 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
2037 } = resp
2038 else {
2039 return Err(ExecError::BadPng(format!(
2040 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
2041 )));
2042 };
2043 Ok(contents)
2044 }
2045
2046 pub async fn export(
2048 &self,
2049 format: kittycad_modeling_cmds::format::OutputFormat3d,
2050 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2051 let resp = self
2052 .engine
2053 .send_modeling_cmd(
2054 &self.engine_batch,
2055 uuid::Uuid::new_v4(),
2056 crate::SourceRange::default(),
2057 &kittycad_modeling_cmds::ModelingCmd::Export(
2058 kittycad_modeling_cmds::Export::builder()
2059 .entity_ids(vec![])
2060 .format(format)
2061 .build(),
2062 ),
2063 )
2064 .await?;
2065
2066 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
2067 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
2068 format!("Expected Export response, got {resp:?}",),
2069 vec![SourceRange::default()],
2070 )));
2071 };
2072
2073 Ok(files)
2074 }
2075
2076 pub async fn export_step(
2078 &self,
2079 deterministic_time: bool,
2080 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2081 let files = self
2082 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
2083 kittycad_modeling_cmds::format::step::export::Options::builder()
2084 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
2085 .maybe_created(if deterministic_time {
2086 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
2087 KclError::new_internal(crate::errors::KclErrorDetails::new(
2088 format!("Failed to parse date: {e}"),
2089 vec![SourceRange::default()],
2090 ))
2091 })?)
2092 } else {
2093 None
2094 })
2095 .build(),
2096 ))
2097 .await?;
2098
2099 Ok(files)
2100 }
2101
2102 pub async fn close(&self) {
2103 self.engine.close().await;
2104 }
2105}
2106
2107#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
2108pub struct ArtifactId(Uuid);
2109
2110impl ArtifactId {
2111 pub fn new(uuid: Uuid) -> Self {
2112 Self(uuid)
2113 }
2114
2115 pub fn placeholder() -> Self {
2117 Self(Uuid::nil())
2118 }
2119}
2120
2121impl From<Uuid> for ArtifactId {
2122 fn from(uuid: Uuid) -> Self {
2123 Self::new(uuid)
2124 }
2125}
2126
2127impl From<&Uuid> for ArtifactId {
2128 fn from(uuid: &Uuid) -> Self {
2129 Self::new(*uuid)
2130 }
2131}
2132
2133impl From<ArtifactId> for Uuid {
2134 fn from(id: ArtifactId) -> Self {
2135 id.0
2136 }
2137}
2138
2139impl From<&ArtifactId> for Uuid {
2140 fn from(id: &ArtifactId) -> Self {
2141 id.0
2142 }
2143}
2144
2145impl From<ModelingCmdId> for ArtifactId {
2146 fn from(id: ModelingCmdId) -> Self {
2147 Self::new(*id.as_ref())
2148 }
2149}
2150
2151impl From<&ModelingCmdId> for ArtifactId {
2152 fn from(id: &ModelingCmdId) -> Self {
2153 Self::new(*id.as_ref())
2154 }
2155}
2156
2157#[cfg(test)]
2158pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2159 parse_execute_with_project_dir(code, None).await
2160}
2161
2162#[cfg(test)]
2163pub(crate) async fn parse_execute_with_project_dir(
2164 code: &str,
2165 project_directory: Option<TypedPath>,
2166) -> Result<ExecTestResults, KclError> {
2167 let program = crate::Program::parse_no_errs(code)?;
2168
2169 let exec_ctxt = ExecutorContext {
2170 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
2171 |err| {
2172 KclError::new_internal(crate::errors::KclErrorDetails::new(
2173 format!("Failed to create mock engine connection: {err}"),
2174 vec![SourceRange::default()],
2175 ))
2176 },
2177 )?)),
2178 engine_batch: EngineBatchContext::default(),
2179 fs: Arc::new(crate::fs::FileManager::new()),
2180 settings: ExecutorSettings {
2181 project_directory,
2182 ..Default::default()
2183 },
2184 context_type: ContextType::Mock,
2185 execution_callbacks: Default::default(),
2186 };
2187 let mut exec_state = ExecState::new(&exec_ctxt);
2188 let result = exec_ctxt.run(&program, &mut exec_state).await?;
2189
2190 Ok(ExecTestResults {
2191 program,
2192 mem_env: result.0,
2193 exec_ctxt,
2194 exec_state,
2195 })
2196}
2197
2198#[cfg(test)]
2199#[derive(Debug)]
2200pub(crate) struct ExecTestResults {
2201 program: crate::Program,
2202 mem_env: EnvironmentRef,
2203 exec_ctxt: ExecutorContext,
2204 exec_state: ExecState,
2205}
2206
2207#[cfg(test)]
2208impl ExecTestResults {
2209 pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2210 &self.exec_state.global.root_module_artifacts.commands
2211 }
2212}
2213
2214pub struct ProgramLookup {
2218 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2219}
2220
2221impl ProgramLookup {
2222 pub fn new(
2225 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2226 module_infos: state::ModuleInfoMap,
2227 ) -> Self {
2228 let mut programs = IndexMap::with_capacity(module_infos.len());
2229 for (id, info) in module_infos {
2230 if let ModuleRepr::Kcl(program, _) = info.repr {
2231 programs.insert(id, program);
2232 }
2233 }
2234 programs.insert(ModuleId::default(), current);
2235 Self { programs }
2236 }
2237
2238 pub fn program_for_module(
2239 &self,
2240 module_id: ModuleId,
2241 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2242 self.programs.get(&module_id)
2243 }
2244}
2245
2246#[cfg(test)]
2247mod tests {
2248 use pretty_assertions::assert_eq;
2249
2250 use super::*;
2251 use crate::ModuleId;
2252 use crate::errors::KclErrorDetails;
2253 use crate::errors::Severity;
2254 use crate::exec::NumericType;
2255 use crate::execution::memory::Stack;
2256 use crate::execution::types::RuntimeType;
2257
2258 #[track_caller]
2260 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2261 memory.memory.get_from_unchecked(name, env).unwrap()
2262 }
2263
2264 async fn execute_variables_with_backend(
2265 code: &str,
2266 backend: memory::MemoryBackendKind,
2267 ) -> IndexMap<String, KclValue> {
2268 execute_outcome_with_backend(code, backend).await.variables
2269 }
2270
2271 async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
2272 let program = crate::Program::parse_no_errs(code).unwrap();
2273 let ctx = ExecutorContext::new_mock(None).await;
2274 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2275 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2276 let outcome = exec_state
2277 .into_exec_outcome(env_ref, &ctx)
2278 .await
2279 .expect("test execution outcome should collect variables");
2280 ctx.close().await;
2281 outcome
2282 }
2283
2284 async fn execute_error_variables_with_backend(
2285 code: &str,
2286 backend: memory::MemoryBackendKind,
2287 ) -> IndexMap<String, KclValue> {
2288 let program = crate::Program::parse_no_errs(code).unwrap();
2289 let ctx = ExecutorContext::new_mock(None).await;
2290 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2291 let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
2292 ctx.close().await;
2293 error.variables
2294 }
2295
2296 async fn execute_project_variables_with_backend(
2297 main_code: &str,
2298 files: &[(&str, &str)],
2299 backend: memory::MemoryBackendKind,
2300 ) -> IndexMap<String, KclValue> {
2301 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
2302 for (name, contents) in files {
2303 tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
2304 }
2305
2306 let program = crate::Program::parse_no_errs(main_code).unwrap();
2307 let ctx = ExecutorContext {
2308 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
2309 engine_batch: EngineBatchContext::default(),
2310 fs: Arc::new(crate::fs::FileManager::new()),
2311 settings: ExecutorSettings {
2312 project_directory: Some(crate::TypedPath(tmpdir.path().into())),
2313 ..Default::default()
2314 },
2315 context_type: ContextType::Mock,
2316 execution_callbacks: Default::default(),
2317 };
2318 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2319 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2320 let outcome = exec_state
2321 .into_exec_outcome(env_ref, &ctx)
2322 .await
2323 .expect("test execution outcome should collect variables");
2324 ctx.close().await;
2325 outcome.variables
2326 }
2327
2328 async fn run_with_caching_variables_with_backend(
2329 code: &str,
2330 backend: memory::MemoryBackendKind,
2331 ) -> IndexMap<String, KclValue> {
2332 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2333 cache::bust_cache().await;
2334 clear_mem_cache().await;
2335
2336 let ctx = ExecutorContext::new_with_engine(
2337 Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
2338 Default::default(),
2339 );
2340 let program = crate::Program::parse_no_errs(code).unwrap();
2341 ctx.run_with_caching(program.clone()).await.unwrap();
2342 let cached = ctx.run_with_caching(program).await.unwrap();
2343
2344 cache::bust_cache().await;
2345 clear_mem_cache().await;
2346 ctx.close().await;
2347 cached.variables
2348 }
2349
2350 async fn run_mock_variables_with_backend(
2351 code: &str,
2352 backend: memory::MemoryBackendKind,
2353 ) -> IndexMap<String, KclValue> {
2354 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2355 clear_mem_cache().await;
2356
2357 let ctx = ExecutorContext::new_mock(None).await;
2358 let first = crate::Program::parse_no_errs("x = 2").unwrap();
2359 ctx.run_mock(
2360 &first,
2361 &MockConfig {
2362 use_prev_memory: false,
2363 ..Default::default()
2364 },
2365 )
2366 .await
2367 .unwrap();
2368
2369 let program = crate::Program::parse_no_errs(code).unwrap();
2370 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
2371
2372 clear_mem_cache().await;
2373 ctx.close().await;
2374 outcome.variables
2375 }
2376
2377 fn sorted_variable_keys(variables: &IndexMap<String, KclValue>) -> Vec<String> {
2378 let mut keys = variables.keys().cloned().collect::<Vec<_>>();
2379 keys.sort();
2380 keys
2381 }
2382
2383 fn assert_number_variable(variables: &IndexMap<String, KclValue>, key: &str, expected: f64) {
2384 let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
2385 assert_eq!(value.as_f64().unwrap(), expected, "{key}: {value:?}");
2386 }
2387
2388 #[tokio::test(flavor = "multi_thread")]
2389 async fn exec_outcome_variables_match_between_memory_backends() {
2390 let code = "x = 2\ny = x + 1\narr = [x, y]";
2391
2392 let legacy = execute_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2393 let arena = execute_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2394
2395 assert_eq!(sorted_variable_keys(&legacy), vec!["arr", "x", "y"]);
2396 assert_eq!(arena, legacy);
2397 }
2398
2399 #[tokio::test(flavor = "multi_thread")]
2400 async fn error_output_variables_match_between_memory_backends() {
2401 let code = "x = 2\ny = missing + 1";
2402
2403 let legacy = execute_error_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2404 let arena = execute_error_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2405
2406 assert_eq!(sorted_variable_keys(&legacy), vec!["x"]);
2407 assert_eq!(arena, legacy);
2408 }
2409
2410 #[tokio::test(flavor = "multi_thread")]
2411 async fn cached_execution_variables_match_between_memory_backends() {
2412 let code = "x = 2\ny = x + 1";
2413
2414 let legacy = run_with_caching_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2415 let arena = run_with_caching_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2416
2417 assert_eq!(sorted_variable_keys(&legacy), vec!["x", "y"]);
2418 assert_eq!(arena, legacy);
2419 }
2420
2421 #[tokio::test(flavor = "multi_thread")]
2422 async fn mock_execution_variables_match_between_memory_backends() {
2423 let code = "y = x + 1";
2424
2425 let legacy = run_mock_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2426 let arena = run_mock_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2427
2428 assert_eq!(sorted_variable_keys(&legacy), vec!["y"]);
2429 assert_eq!(arena, legacy);
2430 }
2431
2432 #[tokio::test(flavor = "multi_thread")]
2433 async fn module_imports_and_exported_closures_match_between_memory_backends() {
2434 let module_code = r#"
2435export base = 40
2436
2437export fn addBase(n) {
2438 return n + base
2439}
2440"#;
2441 let main_code = r#"
2442import base, addBase from 'math.kcl'
2443import 'math.kcl'
2444
2445named = addBase(n = 2)
2446qualified = math::addBase(n = 1)
2447direct = math::base
2448"#;
2449
2450 let legacy = execute_project_variables_with_backend(
2451 main_code,
2452 &[("math.kcl", module_code)],
2453 memory::MemoryBackendKind::Legacy,
2454 )
2455 .await;
2456 let arena = execute_project_variables_with_backend(
2457 main_code,
2458 &[("math.kcl", module_code)],
2459 memory::MemoryBackendKind::Arena,
2460 )
2461 .await;
2462
2463 assert_number_variable(&legacy, "named", 42.0);
2464 assert_number_variable(&legacy, "qualified", 41.0);
2465 assert_number_variable(&legacy, "direct", 40.0);
2466 assert_eq!(arena, legacy);
2467 }
2468
2469 #[tokio::test(flavor = "multi_thread")]
2470 async fn sketch_block_variables_match_between_memory_backends() {
2471 let code = r#"
2472sketch001 = sketch(on = XY) {
2473 line1 = line(start = [0, 0], end = [1, 0])
2474 line2 = line(start = [1, 0], end = [0, 1])
2475}
2476lineCount = 2
2477"#;
2478
2479 let legacy = execute_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2480 let arena = execute_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2481
2482 assert!(legacy.contains_key("sketch001"), "actual: {legacy:?}");
2483 assert_number_variable(&legacy, "lineCount", 2.0);
2484 assert_eq!(arena, legacy);
2485 }
2486
2487 #[tokio::test(flavor = "multi_thread")]
2488 async fn tag_call_stack_lookup_matches_between_memory_backends() {
2489 let code = r#"
2490sketch001 = startSketchOn(XY)
2491 |> startProfile(at = [0, 0])
2492 |> xLine(length = 10, tag = $seg01)
2493
2494segLength = segLen(seg01)
2495"#;
2496
2497 let legacy = execute_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2498 let arena = execute_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2499
2500 assert_number_variable(&legacy, "segLength", 10.0);
2501 assert_eq!(arena, legacy);
2502 }
2503
2504 #[tokio::test(flavor = "multi_thread")]
2505 async fn sketch_transpiler_exec_outcome_variables_match_between_memory_backends() {
2506 let code = r#"
2507sketch001 = startSketchOn(XY)
2508 |> startProfile(at = [0, 0])
2509 |> line(end = [1, 0])
2510"#;
2511 let program = crate::Program::parse_no_errs(code).unwrap();
2512
2513 let legacy = execute_outcome_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2514 let arena = execute_outcome_with_backend(code, memory::MemoryBackendKind::Arena).await;
2515
2516 let legacy_transpiled = transpile_old_sketch_to_new(&legacy, &program, "sketch001").unwrap();
2517 let arena_transpiled = transpile_old_sketch_to_new(&arena, &program, "sketch001").unwrap();
2518 assert_eq!(arena_transpiled, legacy_transpiled);
2519 }
2520
2521 #[tokio::test(flavor = "multi_thread")]
2522 async fn test_execute_warn() {
2523 let text = "@blah";
2524 let result = parse_execute(text).await.unwrap();
2525 let errs = result.exec_state.issues();
2526 assert_eq!(errs.len(), 1);
2527 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2528 assert!(
2529 errs[0].message.contains("Unknown annotation"),
2530 "unexpected warning message: {}",
2531 errs[0].message
2532 );
2533 }
2534
2535 #[tokio::test(flavor = "multi_thread")]
2536 async fn test_execute_fn_definitions() {
2537 let ast = r#"fn def(@x) {
2538 return x
2539}
2540fn ghi(@x) {
2541 return x
2542}
2543fn jkl(@x) {
2544 return x
2545}
2546fn hmm(@x) {
2547 return x
2548}
2549
2550yo = 5 + 6
2551
2552abc = 3
2553identifierGuy = 5
2554part001 = startSketchOn(XY)
2555|> startProfile(at = [-1.2, 4.83])
2556|> line(end = [2.8, 0])
2557|> angledLine(angle = 100 + 100, length = 3.01)
2558|> angledLine(angle = abc, length = 3.02)
2559|> angledLine(angle = def(yo), length = 3.03)
2560|> angledLine(angle = ghi(2), length = 3.04)
2561|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2562|> close()
2563yo2 = hmm([identifierGuy + 5])"#;
2564
2565 parse_execute(ast).await.unwrap();
2566 }
2567
2568 #[tokio::test(flavor = "multi_thread")]
2569 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2570 let code = r#"
2571firstProfile = sketch(on = XY) {
2572 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2573 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2574 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2575 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2576 coincident([edge1.end, edge2.start])
2577 coincident([edge2.end, edge3.start])
2578 coincident([edge3.end, edge4.start])
2579 coincident([edge4.end, edge1.start])
2580}
2581
2582secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2583 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2584 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2585 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2586 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2587 coincident([edge5.end, edge6.start])
2588 coincident([edge6.end, edge7.start])
2589 coincident([edge7.end, edge8.start])
2590 coincident([edge8.end, edge5.start])
2591}
2592
2593firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2594secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2595"#;
2596
2597 let result = parse_execute(code).await.unwrap();
2598 assert!(result.exec_state.issues().is_empty());
2599 }
2600
2601 #[tokio::test(flavor = "multi_thread")]
2602 async fn sketch_block_artifact_preserves_standard_plane_name() {
2603 let code = r#"
2604sketch001 = sketch(on = -YZ) {
2605 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
2606}
2607"#;
2608
2609 let result = parse_execute(code).await.unwrap();
2610 let sketch_blocks = result
2611 .exec_state
2612 .global
2613 .artifacts
2614 .graph
2615 .values()
2616 .filter_map(|artifact| match artifact {
2617 Artifact::SketchBlock(block) => Some(block),
2618 _ => None,
2619 })
2620 .collect::<Vec<_>>();
2621
2622 assert_eq!(sketch_blocks.len(), 1);
2623 assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
2624 }
2625
2626 #[tokio::test(flavor = "multi_thread")]
2627 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2628 let code = r#"
2629sketch001 = sketch(on = YZ) {
2630 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2631 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2632 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2633 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2634 coincident([line1.end, line2.start])
2635 coincident([line2.end, line3.start])
2636 coincident([line3.end, line4.start])
2637 coincident([line4.end, line1.start])
2638}
2639
2640sketch002 = sketch(on = -XZ) {
2641 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2642 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2643 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2644 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2645 coincident([line5.end, line6.start])
2646 coincident([line6.end, line7.start])
2647 coincident([line7.end, line8.start])
2648 coincident([line8.end, line5.start])
2649}
2650
2651region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2652extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2653region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2654extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2655
2656myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2657"#;
2658
2659 let result = parse_execute(code).await.unwrap();
2660 assert!(result.exec_state.issues().is_empty());
2661 }
2662
2663 #[tokio::test(flavor = "multi_thread")]
2664 async fn issue_10741_point_circle_coincident_executes() {
2665 let code = r#"
2666sketch001 = sketch(on = YZ) {
2667 circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
2668 line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
2669 coincident([line1.start, circle1])
2670}
2671"#;
2672
2673 let result = parse_execute(code).await.unwrap();
2674 assert!(
2675 result
2676 .exec_state
2677 .issues()
2678 .iter()
2679 .all(|issue| issue.severity != Severity::Error),
2680 "unexpected execution issues: {:#?}",
2681 result.exec_state.issues()
2682 );
2683 }
2684
2685 #[tokio::test(flavor = "multi_thread")]
2686 async fn test_execute_with_pipe_substitutions_unary() {
2687 let ast = r#"myVar = 3
2688part001 = startSketchOn(XY)
2689 |> startProfile(at = [0, 0])
2690 |> line(end = [3, 4], tag = $seg01)
2691 |> line(end = [
2692 min([segLen(seg01), myVar]),
2693 -legLen(hypotenuse = segLen(seg01), leg = myVar)
2694])
2695"#;
2696
2697 parse_execute(ast).await.unwrap();
2698 }
2699
2700 #[tokio::test(flavor = "multi_thread")]
2701 async fn test_execute_with_pipe_substitutions() {
2702 let ast = r#"myVar = 3
2703part001 = startSketchOn(XY)
2704 |> startProfile(at = [0, 0])
2705 |> line(end = [3, 4], tag = $seg01)
2706 |> line(end = [
2707 min([segLen(seg01), myVar]),
2708 legLen(hypotenuse = segLen(seg01), leg = myVar)
2709])
2710"#;
2711
2712 parse_execute(ast).await.unwrap();
2713 }
2714
2715 #[tokio::test(flavor = "multi_thread")]
2716 async fn test_execute_with_inline_comment() {
2717 let ast = r#"baseThick = 1
2718armAngle = 60
2719
2720baseThickHalf = baseThick / 2
2721halfArmAngle = armAngle / 2
2722
2723arrExpShouldNotBeIncluded = [1, 2, 3]
2724objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2725
2726part001 = startSketchOn(XY)
2727 |> startProfile(at = [0, 0])
2728 |> yLine(endAbsolute = 1)
2729 |> xLine(length = 3.84) // selection-range-7ish-before-this
2730
2731variableBelowShouldNotBeIncluded = 3
2732"#;
2733
2734 parse_execute(ast).await.unwrap();
2735 }
2736
2737 #[tokio::test(flavor = "multi_thread")]
2738 async fn test_execute_with_function_literal_in_pipe() {
2739 let ast = r#"w = 20
2740l = 8
2741h = 10
2742
2743fn thing() {
2744 return -8
2745}
2746
2747firstExtrude = startSketchOn(XY)
2748 |> startProfile(at = [0,0])
2749 |> line(end = [0, l])
2750 |> line(end = [w, 0])
2751 |> line(end = [0, thing()])
2752 |> close()
2753 |> extrude(length = h)"#;
2754
2755 parse_execute(ast).await.unwrap();
2756 }
2757
2758 #[tokio::test(flavor = "multi_thread")]
2759 async fn test_execute_with_function_unary_in_pipe() {
2760 let ast = r#"w = 20
2761l = 8
2762h = 10
2763
2764fn thing(@x) {
2765 return -x
2766}
2767
2768firstExtrude = startSketchOn(XY)
2769 |> startProfile(at = [0,0])
2770 |> line(end = [0, l])
2771 |> line(end = [w, 0])
2772 |> line(end = [0, thing(8)])
2773 |> close()
2774 |> extrude(length = h)"#;
2775
2776 parse_execute(ast).await.unwrap();
2777 }
2778
2779 #[tokio::test(flavor = "multi_thread")]
2780 async fn test_execute_with_function_array_in_pipe() {
2781 let ast = r#"w = 20
2782l = 8
2783h = 10
2784
2785fn thing(@x) {
2786 return [0, -x]
2787}
2788
2789firstExtrude = startSketchOn(XY)
2790 |> startProfile(at = [0,0])
2791 |> line(end = [0, l])
2792 |> line(end = [w, 0])
2793 |> line(end = thing(8))
2794 |> close()
2795 |> extrude(length = h)"#;
2796
2797 parse_execute(ast).await.unwrap();
2798 }
2799
2800 #[tokio::test(flavor = "multi_thread")]
2801 async fn test_execute_with_function_call_in_pipe() {
2802 let ast = r#"w = 20
2803l = 8
2804h = 10
2805
2806fn other_thing(@y) {
2807 return -y
2808}
2809
2810fn thing(@x) {
2811 return other_thing(x)
2812}
2813
2814firstExtrude = startSketchOn(XY)
2815 |> startProfile(at = [0,0])
2816 |> line(end = [0, l])
2817 |> line(end = [w, 0])
2818 |> line(end = [0, thing(8)])
2819 |> close()
2820 |> extrude(length = h)"#;
2821
2822 parse_execute(ast).await.unwrap();
2823 }
2824
2825 #[tokio::test(flavor = "multi_thread")]
2826 async fn test_execute_with_function_sketch() {
2827 let ast = r#"fn box(h, l, w) {
2828 myBox = startSketchOn(XY)
2829 |> startProfile(at = [0,0])
2830 |> line(end = [0, l])
2831 |> line(end = [w, 0])
2832 |> line(end = [0, -l])
2833 |> close()
2834 |> extrude(length = h)
2835
2836 return myBox
2837}
2838
2839fnBox = box(h = 3, l = 6, w = 10)"#;
2840
2841 parse_execute(ast).await.unwrap();
2842 }
2843
2844 #[tokio::test(flavor = "multi_thread")]
2845 async fn test_get_member_of_object_with_function_period() {
2846 let ast = r#"fn box(@obj) {
2847 myBox = startSketchOn(XY)
2848 |> startProfile(at = obj.start)
2849 |> line(end = [0, obj.l])
2850 |> line(end = [obj.w, 0])
2851 |> line(end = [0, -obj.l])
2852 |> close()
2853 |> extrude(length = obj.h)
2854
2855 return myBox
2856}
2857
2858thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2859"#;
2860 parse_execute(ast).await.unwrap();
2861 }
2862
2863 #[tokio::test(flavor = "multi_thread")]
2864 #[ignore] async fn test_object_member_starting_pipeline() {
2866 let ast = r#"
2867fn test2() {
2868 return {
2869 thing: startSketchOn(XY)
2870 |> startProfile(at = [0, 0])
2871 |> line(end = [0, 1])
2872 |> line(end = [1, 0])
2873 |> line(end = [0, -1])
2874 |> close()
2875 }
2876}
2877
2878x2 = test2()
2879
2880x2.thing
2881 |> extrude(length = 10)
2882"#;
2883 parse_execute(ast).await.unwrap();
2884 }
2885
2886 #[tokio::test(flavor = "multi_thread")]
2887 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
2889 let ast = r#"fn box(obj) {
2890let myBox = startSketchOn(XY)
2891 |> startProfile(at = obj.start)
2892 |> line(end = [0, obj.l])
2893 |> line(end = [obj.w, 0])
2894 |> line(end = [0, -obj.l])
2895 |> close()
2896 |> extrude(length = obj.h)
2897
2898 return myBox
2899}
2900
2901for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2902 thisBox = box(var)
2903}"#;
2904
2905 parse_execute(ast).await.unwrap();
2906 }
2907
2908 #[tokio::test(flavor = "multi_thread")]
2909 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
2911 let ast = r#"fn box(h, l, w, start) {
2912 myBox = startSketchOn(XY)
2913 |> startProfile(at = [0,0])
2914 |> line(end = [0, l])
2915 |> line(end = [w, 0])
2916 |> line(end = [0, -l])
2917 |> close()
2918 |> extrude(length = h)
2919
2920 return myBox
2921}
2922
2923
2924for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2925 const thisBox = box(var[0], var[1], var[2], var[3])
2926}"#;
2927
2928 parse_execute(ast).await.unwrap();
2929 }
2930
2931 #[tokio::test(flavor = "multi_thread")]
2932 async fn test_get_member_of_array_with_function() {
2933 let ast = r#"fn box(@arr) {
2934 myBox =startSketchOn(XY)
2935 |> startProfile(at = arr[0])
2936 |> line(end = [0, arr[1]])
2937 |> line(end = [arr[2], 0])
2938 |> line(end = [0, -arr[1]])
2939 |> close()
2940 |> extrude(length = arr[3])
2941
2942 return myBox
2943}
2944
2945thisBox = box([[0,0], 6, 10, 3])
2946
2947"#;
2948 parse_execute(ast).await.unwrap();
2949 }
2950
2951 #[tokio::test(flavor = "multi_thread")]
2952 async fn test_function_cannot_access_future_definitions() {
2953 let ast = r#"
2954fn returnX() {
2955 // x shouldn't be defined yet.
2956 return x
2957}
2958
2959x = 5
2960
2961answer = returnX()"#;
2962
2963 let result = parse_execute(ast).await;
2964 let err = result.unwrap_err();
2965 assert_eq!(err.message(), "`x` is not defined");
2966 }
2967
2968 #[tokio::test(flavor = "multi_thread")]
2969 async fn test_override_prelude() {
2970 let text = "PI = 3.0";
2971 let result = parse_execute(text).await.unwrap();
2972 let issues = result.exec_state.issues();
2973 assert!(issues.is_empty(), "issues={issues:#?}");
2974 }
2975
2976 #[tokio::test(flavor = "multi_thread")]
2977 async fn type_aliases() {
2978 let text = r#"@settings(experimentalFeatures = allow)
2979type MyTy = [number; 2]
2980fn foo(@x: MyTy) {
2981 return x[0]
2982}
2983
2984foo([0, 1])
2985
2986type Other = MyTy | Helix
2987"#;
2988 let result = parse_execute(text).await.unwrap();
2989 let issues = result.exec_state.issues();
2990 assert!(issues.is_empty(), "issues={issues:#?}");
2991 }
2992
2993 #[tokio::test(flavor = "multi_thread")]
2994 async fn test_cannot_shebang_in_fn() {
2995 let ast = r#"
2996fn foo() {
2997 #!hello
2998 return true
2999}
3000
3001foo
3002"#;
3003
3004 let result = parse_execute(ast).await;
3005 let err = result.unwrap_err();
3006 assert_eq!(
3007 err,
3008 KclError::new_syntax(KclErrorDetails::new(
3009 "Unexpected token: #".to_owned(),
3010 vec![SourceRange::new(14, 15, ModuleId::default())],
3011 )),
3012 );
3013 }
3014
3015 #[tokio::test(flavor = "multi_thread")]
3016 async fn test_pattern_transform_function_cannot_access_future_definitions() {
3017 let ast = r#"
3018fn transform(@replicaId) {
3019 // x shouldn't be defined yet.
3020 scale = x
3021 return {
3022 translate = [0, 0, replicaId * 10],
3023 scale = [scale, 1, 0],
3024 }
3025}
3026
3027fn layer() {
3028 return startSketchOn(XY)
3029 |> circle( center= [0, 0], radius= 1, tag = $tag1)
3030 |> extrude(length = 10)
3031}
3032
3033x = 5
3034
3035// The 10 layers are replicas of each other, with a transform applied to each.
3036shape = layer() |> patternTransform(instances = 10, transform = transform)
3037"#;
3038
3039 let result = parse_execute(ast).await;
3040 let err = result.unwrap_err();
3041 assert_eq!(err.message(), "`x` is not defined",);
3042 }
3043
3044 #[tokio::test(flavor = "multi_thread")]
3047 async fn test_math_execute_with_functions() {
3048 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
3049 let result = parse_execute(ast).await.unwrap();
3050 assert_eq!(
3051 5.0,
3052 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3053 .as_f64()
3054 .unwrap()
3055 );
3056 }
3057
3058 #[tokio::test(flavor = "multi_thread")]
3059 async fn test_math_execute() {
3060 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
3061 let result = parse_execute(ast).await.unwrap();
3062 assert_eq!(
3063 7.4,
3064 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3065 .as_f64()
3066 .unwrap()
3067 );
3068 }
3069
3070 #[tokio::test(flavor = "multi_thread")]
3071 async fn test_math_execute_start_negative() {
3072 let ast = r#"myVar = -5 + 6"#;
3073 let result = parse_execute(ast).await.unwrap();
3074 assert_eq!(
3075 1.0,
3076 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3077 .as_f64()
3078 .unwrap()
3079 );
3080 }
3081
3082 #[tokio::test(flavor = "multi_thread")]
3083 async fn test_math_execute_with_pi() {
3084 let ast = r#"myVar = PI * 2"#;
3085 let result = parse_execute(ast).await.unwrap();
3086 assert_eq!(
3087 std::f64::consts::TAU,
3088 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3089 .as_f64()
3090 .unwrap()
3091 );
3092 }
3093
3094 #[tokio::test(flavor = "multi_thread")]
3095 async fn test_math_define_decimal_without_leading_zero() {
3096 let ast = r#"thing = .4 + 7"#;
3097 let result = parse_execute(ast).await.unwrap();
3098 assert_eq!(
3099 7.4,
3100 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
3101 .as_f64()
3102 .unwrap()
3103 );
3104 }
3105
3106 #[tokio::test(flavor = "multi_thread")]
3107 async fn pass_std_to_std() {
3108 let ast = r#"sketch001 = startSketchOn(XY)
3109profile001 = circle(sketch001, center = [0, 0], radius = 2)
3110extrude001 = extrude(profile001, length = 5)
3111extrudes = patternLinear3d(
3112 extrude001,
3113 instances = 3,
3114 distance = 5,
3115 axis = [1, 1, 0],
3116)
3117clone001 = map(extrudes, f = clone)
3118"#;
3119 parse_execute(ast).await.unwrap();
3120 }
3121
3122 #[tokio::test(flavor = "multi_thread")]
3123 async fn test_array_reduce_nested_array() {
3124 let code = r#"
3125fn id(@el, accum) { return accum }
3126
3127answer = reduce([], initial=[[[0,0]]], f=id)
3128"#;
3129 let result = parse_execute(code).await.unwrap();
3130 assert_eq!(
3131 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
3132 KclValue::HomArray {
3133 value: vec![KclValue::HomArray {
3134 value: vec![KclValue::HomArray {
3135 value: vec![
3136 KclValue::Number {
3137 value: 0.0,
3138 ty: NumericType::default(),
3139 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
3140 },
3141 KclValue::Number {
3142 value: 0.0,
3143 ty: NumericType::default(),
3144 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
3145 }
3146 ],
3147 ty: RuntimeType::any(),
3148 }],
3149 ty: RuntimeType::any(),
3150 }],
3151 ty: RuntimeType::any(),
3152 }
3153 );
3154 }
3155
3156 #[tokio::test(flavor = "multi_thread")]
3157 async fn test_zero_param_fn() {
3158 let ast = r#"sigmaAllow = 35000 // psi
3159leg1 = 5 // inches
3160leg2 = 8 // inches
3161fn thickness() { return 0.56 }
3162
3163bracket = startSketchOn(XY)
3164 |> startProfile(at = [0,0])
3165 |> line(end = [0, leg1])
3166 |> line(end = [leg2, 0])
3167 |> line(end = [0, -thickness()])
3168 |> line(end = [-leg2 + thickness(), 0])
3169"#;
3170 parse_execute(ast).await.unwrap();
3171 }
3172
3173 #[tokio::test(flavor = "multi_thread")]
3174 async fn test_unary_operator_not_succeeds() {
3175 let ast = r#"
3176fn returnTrue() { return !false }
3177t = true
3178f = false
3179notTrue = !t
3180notFalse = !f
3181c = !!true
3182d = !returnTrue()
3183
3184assertIs(!false, error = "expected to pass")
3185
3186fn check(x) {
3187 assertIs(!x, error = "expected argument to be false")
3188 return true
3189}
3190check(x = false)
3191"#;
3192 let result = parse_execute(ast).await.unwrap();
3193 assert_eq!(
3194 false,
3195 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
3196 .as_bool()
3197 .unwrap()
3198 );
3199 assert_eq!(
3200 true,
3201 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
3202 .as_bool()
3203 .unwrap()
3204 );
3205 assert_eq!(
3206 true,
3207 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
3208 .as_bool()
3209 .unwrap()
3210 );
3211 assert_eq!(
3212 false,
3213 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
3214 .as_bool()
3215 .unwrap()
3216 );
3217 }
3218
3219 #[tokio::test(flavor = "multi_thread")]
3220 async fn test_unary_operator_not_on_non_bool_fails() {
3221 let code1 = r#"
3222// Yup, this is null.
3223myNull = 0 / 0
3224notNull = !myNull
3225"#;
3226 assert_eq!(
3227 parse_execute(code1).await.unwrap_err().message(),
3228 "Cannot apply unary operator ! to non-boolean value: a number",
3229 );
3230
3231 let code2 = "notZero = !0";
3232 assert_eq!(
3233 parse_execute(code2).await.unwrap_err().message(),
3234 "Cannot apply unary operator ! to non-boolean value: a number",
3235 );
3236
3237 let code3 = r#"
3238notEmptyString = !""
3239"#;
3240 assert_eq!(
3241 parse_execute(code3).await.unwrap_err().message(),
3242 "Cannot apply unary operator ! to non-boolean value: a string",
3243 );
3244
3245 let code4 = r#"
3246obj = { a = 1 }
3247notMember = !obj.a
3248"#;
3249 assert_eq!(
3250 parse_execute(code4).await.unwrap_err().message(),
3251 "Cannot apply unary operator ! to non-boolean value: a number",
3252 );
3253
3254 let code5 = "
3255a = []
3256notArray = !a";
3257 assert_eq!(
3258 parse_execute(code5).await.unwrap_err().message(),
3259 "Cannot apply unary operator ! to non-boolean value: an empty array",
3260 );
3261
3262 let code6 = "
3263x = {}
3264notObject = !x";
3265 assert_eq!(
3266 parse_execute(code6).await.unwrap_err().message(),
3267 "Cannot apply unary operator ! to non-boolean value: an object",
3268 );
3269
3270 let code7 = "
3271fn x() { return 1 }
3272notFunction = !x";
3273 let fn_err = parse_execute(code7).await.unwrap_err();
3274 assert!(
3277 fn_err
3278 .message()
3279 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
3280 "Actual error: {fn_err:?}"
3281 );
3282
3283 let code8 = "
3284myTagDeclarator = $myTag
3285notTagDeclarator = !myTagDeclarator";
3286 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
3287 assert!(
3290 tag_declarator_err
3291 .message()
3292 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
3293 "Actual error: {tag_declarator_err:?}"
3294 );
3295
3296 let code9 = "
3297myTagDeclarator = $myTag
3298notTagIdentifier = !myTag";
3299 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
3300 assert!(
3303 tag_identifier_err
3304 .message()
3305 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
3306 "Actual error: {tag_identifier_err:?}"
3307 );
3308
3309 let code10 = "notPipe = !(1 |> 2)";
3310 assert_eq!(
3311 parse_execute(code10).await.unwrap_err(),
3314 KclError::new_syntax(KclErrorDetails::new(
3315 "Unexpected token: !".to_owned(),
3316 vec![SourceRange::new(10, 11, ModuleId::default())],
3317 ))
3318 );
3319
3320 let code11 = "
3321fn identity(x) { return x }
3322notPipeSub = 1 |> identity(!%))";
3323 assert_eq!(
3324 parse_execute(code11).await.unwrap_err(),
3327 KclError::new_syntax(KclErrorDetails::new(
3328 "There was an unexpected `!`. Try removing it.".to_owned(),
3329 vec![SourceRange::new(56, 57, ModuleId::default())],
3330 ))
3331 );
3332
3333 }
3337
3338 #[tokio::test(flavor = "multi_thread")]
3339 async fn test_start_sketch_on_invalid_kwargs() {
3340 let current_dir = std::env::current_dir().unwrap();
3341 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
3342 let mut code = std::fs::read_to_string(&path).unwrap();
3343 assert_eq!(
3344 parse_execute(&code).await.unwrap_err().message(),
3345 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
3346 );
3347
3348 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
3349 code = std::fs::read_to_string(&path).unwrap();
3350
3351 assert_eq!(
3352 parse_execute(&code).await.unwrap_err().message(),
3353 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
3354 );
3355
3356 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
3357 code = std::fs::read_to_string(&path).unwrap();
3358
3359 assert_eq!(
3360 parse_execute(&code).await.unwrap_err().message(),
3361 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
3362 );
3363
3364 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
3365 code = std::fs::read_to_string(&path).unwrap();
3366
3367 assert_eq!(
3368 parse_execute(&code).await.unwrap_err().message(),
3369 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
3370 );
3371
3372 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
3373 code = std::fs::read_to_string(&path).unwrap();
3374
3375 assert_eq!(
3376 parse_execute(&code).await.unwrap_err().message(),
3377 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
3378 );
3379 }
3380
3381 #[tokio::test(flavor = "multi_thread")]
3382 async fn test_math_negative_variable_in_binary_expression() {
3383 let ast = r#"sigmaAllow = 35000 // psi
3384width = 1 // inch
3385
3386p = 150 // lbs
3387distance = 6 // inches
3388FOS = 2
3389
3390leg1 = 5 // inches
3391leg2 = 8 // inches
3392
3393thickness_squared = distance * p * FOS * 6 / sigmaAllow
3394thickness = 0.56 // inches. App does not support square root function yet
3395
3396bracket = startSketchOn(XY)
3397 |> startProfile(at = [0,0])
3398 |> line(end = [0, leg1])
3399 |> line(end = [leg2, 0])
3400 |> line(end = [0, -thickness])
3401 |> line(end = [-leg2 + thickness, 0])
3402"#;
3403 parse_execute(ast).await.unwrap();
3404 }
3405
3406 #[tokio::test(flavor = "multi_thread")]
3407 async fn test_execute_function_no_return() {
3408 let ast = r#"fn test(@origin) {
3409 origin
3410}
3411
3412test([0, 0])
3413"#;
3414 let result = parse_execute(ast).await;
3415 assert!(result.is_err());
3416 assert!(result.unwrap_err().to_string().contains("undefined"));
3417 }
3418
3419 #[tokio::test(flavor = "multi_thread")]
3420 async fn test_max_stack_size_exceeded_error() {
3421 let ast = r#"
3422fn forever(@n) {
3423 return 1 + forever(n)
3424}
3425
3426forever(1)
3427"#;
3428 let result = parse_execute(ast).await;
3429 let err = result.unwrap_err();
3430 assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
3431 }
3432
3433 #[tokio::test(flavor = "multi_thread")]
3434 async fn test_math_doubly_nested_parens() {
3435 let ast = r#"sigmaAllow = 35000 // psi
3436width = 4 // inch
3437p = 150 // Force on shelf - lbs
3438distance = 6 // inches
3439FOS = 2
3440leg1 = 5 // inches
3441leg2 = 8 // inches
3442thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
3443thickness = 0.32 // inches. App does not support square root function yet
3444bracket = startSketchOn(XY)
3445 |> startProfile(at = [0,0])
3446 |> line(end = [0, leg1])
3447 |> line(end = [leg2, 0])
3448 |> line(end = [0, -thickness])
3449 |> line(end = [-1 * leg2 + thickness, 0])
3450 |> line(end = [0, -1 * leg1 + thickness])
3451 |> close()
3452 |> extrude(length = width)
3453"#;
3454 parse_execute(ast).await.unwrap();
3455 }
3456
3457 #[tokio::test(flavor = "multi_thread")]
3458 async fn test_math_nested_parens_one_less() {
3459 let ast = r#" sigmaAllow = 35000 // psi
3460width = 4 // inch
3461p = 150 // Force on shelf - lbs
3462distance = 6 // inches
3463FOS = 2
3464leg1 = 5 // inches
3465leg2 = 8 // inches
3466thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
3467thickness = 0.32 // inches. App does not support square root function yet
3468bracket = startSketchOn(XY)
3469 |> startProfile(at = [0,0])
3470 |> line(end = [0, leg1])
3471 |> line(end = [leg2, 0])
3472 |> line(end = [0, -thickness])
3473 |> line(end = [-1 * leg2 + thickness, 0])
3474 |> line(end = [0, -1 * leg1 + thickness])
3475 |> close()
3476 |> extrude(length = width)
3477"#;
3478 parse_execute(ast).await.unwrap();
3479 }
3480
3481 #[tokio::test(flavor = "multi_thread")]
3482 async fn test_fn_as_operand() {
3483 let ast = r#"fn f() { return 1 }
3484x = f()
3485y = x + 1
3486z = f() + 1
3487w = f() + f()
3488"#;
3489 parse_execute(ast).await.unwrap();
3490 }
3491
3492 #[tokio::test(flavor = "multi_thread")]
3493 async fn kcl_test_ids_stable_between_executions() {
3494 let code = r#"sketch001 = startSketchOn(XZ)
3495|> startProfile(at = [61.74, 206.13])
3496|> xLine(length = 305.11, tag = $seg01)
3497|> yLine(length = -291.85)
3498|> xLine(length = -segLen(seg01))
3499|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3500|> close()
3501|> extrude(length = 40.14)
3502|> shell(
3503 thickness = 3.14,
3504 faces = [seg01]
3505)
3506"#;
3507
3508 let ctx = crate::test_server::new_context(true, None).await.unwrap();
3509 let old_program = crate::Program::parse_no_errs(code).unwrap();
3510
3511 if let Err(err) = ctx.run_with_caching(old_program).await {
3513 let report = err.into_miette_report_with_outputs(code).unwrap();
3514 let report = miette::Report::new(report);
3515 panic!("Error executing program: {report:?}");
3516 }
3517
3518 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3520
3521 let code = r#"sketch001 = startSketchOn(XZ)
3522|> startProfile(at = [62.74, 206.13])
3523|> xLine(length = 305.11, tag = $seg01)
3524|> yLine(length = -291.85)
3525|> xLine(length = -segLen(seg01))
3526|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3527|> close()
3528|> extrude(length = 40.14)
3529|> shell(
3530 faces = [seg01],
3531 thickness = 3.14,
3532)
3533"#;
3534
3535 let program = crate::Program::parse_no_errs(code).unwrap();
3537 ctx.run_with_caching(program).await.unwrap();
3539
3540 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3541
3542 assert_eq!(id_generator, new_id_generator);
3543 }
3544
3545 #[tokio::test(flavor = "multi_thread")]
3546 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
3547 let code = r#"sketch001 = startSketchOn(XZ)
3548|> startProfile(at = [61.74, 206.13])
3549|> xLine(length = 305.11, tag = $seg01)
3550|> yLine(length = -291.85)
3551|> xLine(length = -segLen(seg01))
3552|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3553|> close()
3554|> extrude(length = 40.14)
3555|> shell(
3556 thickness = 3.14,
3557 faces = [seg01]
3558)
3559"#;
3560
3561 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
3562 let old_program = crate::Program::parse_no_errs(code).unwrap();
3563
3564 ctx.run_with_caching(old_program.clone()).await.unwrap();
3566
3567 let settings_state = cache::read_old_ast().await.unwrap().settings;
3568
3569 assert_eq!(settings_state, ctx.settings);
3571
3572 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3574
3575 ctx.run_with_caching(old_program.clone()).await.unwrap();
3577
3578 let settings_state = cache::read_old_ast().await.unwrap().settings;
3579
3580 assert_eq!(settings_state, ctx.settings);
3582
3583 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3585
3586 ctx.run_with_caching(old_program).await.unwrap();
3588
3589 let settings_state = cache::read_old_ast().await.unwrap().settings;
3590
3591 assert_eq!(settings_state, ctx.settings);
3593
3594 ctx.close().await;
3595 }
3596
3597 #[tokio::test(flavor = "multi_thread")]
3598 async fn mock_after_not_mock() {
3599 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3600 let program = crate::Program::parse_no_errs("x = 2").unwrap();
3601 let result = ctx.run_with_caching(program).await.unwrap();
3602 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
3603
3604 let ctx2 = ExecutorContext::new_mock(None).await;
3605 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
3606 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
3607 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
3608
3609 ctx.close().await;
3610 ctx2.close().await;
3611 }
3612
3613 #[tokio::test(flavor = "multi_thread")]
3614 async fn mock_then_add_extrude_then_mock_again() {
3615 let code = "s = sketch(on = XY) {
3616 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
3617 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
3618 coincident([line1.end, line2.start])
3619 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
3620 coincident([line2.end, line3.start])
3621 coincident([line1.start, line3.end])
3622}
3623 ";
3624 let ctx = ExecutorContext::new_mock(None).await;
3625 let program = crate::Program::parse_no_errs(code).unwrap();
3626 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3627 assert!(result.variables.contains_key("s"), "actual: {:?}", &result.variables);
3628
3629 let code2 = code.to_owned()
3630 + "
3631region001 = region(point = [1mm, 1mm], sketch = s)
3632extrude001 = extrude(region001, length = 1)
3633 ";
3634 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3635 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
3636 assert!(
3637 result.variables.contains_key("region001"),
3638 "actual: {:?}",
3639 &result.variables
3640 );
3641
3642 ctx.close().await;
3643 }
3644
3645 #[tokio::test(flavor = "multi_thread")]
3646 async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
3647 let code = format!(
3648 r#"{}
3649
3650face7 = faceOf(solid6, face = r6.tags.line1)
3651r7 = squareRegion(onSurface = face7)
3652solid7 = extrude(r7, length = width)
3653"#,
3654 include_str!("../../tests/endless_impeller/input.kcl")
3655 );
3656
3657 let result = parse_execute(&code).await.unwrap();
3658 let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
3659 assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
3660
3661 let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
3662 KclValue::Face { value } => value,
3663 value => panic!("expected face7 to be a Face, got {value:?}"),
3664 };
3665 assert!(face7.parent_solid.creator_sketch_id.is_some());
3666 }
3667
3668 #[tokio::test(flavor = "multi_thread")]
3669 async fn mock_has_stable_ids() {
3670 let ctx = ExecutorContext::new_mock(None).await;
3671 let mock_config = MockConfig {
3672 use_prev_memory: false,
3673 ..Default::default()
3674 };
3675 let code = "sk = startSketchOn(XY)
3676 |> startProfile(at = [0, 0])";
3677 let program = crate::Program::parse_no_errs(code).unwrap();
3678 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
3679 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3680 assert!(!ids.is_empty(), "IDs should not be empty");
3681
3682 let ctx2 = ExecutorContext::new_mock(None).await;
3683 let program2 = crate::Program::parse_no_errs(code).unwrap();
3684 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
3685 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3686
3687 assert_eq!(ids, ids2, "Generated IDs should match");
3688 ctx.close().await;
3689 ctx2.close().await;
3690 }
3691
3692 #[tokio::test(flavor = "multi_thread")]
3693 async fn mock_memory_restore_preserves_module_maps() {
3694 clear_mem_cache().await;
3695
3696 let ctx = ExecutorContext::new_mock(None).await;
3697 let cold_start = MockConfig {
3698 use_prev_memory: false,
3699 ..Default::default()
3700 };
3701 ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
3702
3703 let mut mem = cache::read_old_memory().await.unwrap();
3704 assert!(
3705 mem.path_to_source_id.len() > 3,
3706 "expected prelude imports to populate multiple modules, got {:?}",
3707 mem.path_to_source_id
3708 );
3709 mem.constraint_state.insert(
3710 crate::front::ObjectId(1),
3711 indexmap::indexmap! {
3712 crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
3713 crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
3714 },
3715 );
3716
3717 let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
3718 ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
3719
3720 assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
3721 assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
3722 assert_eq!(exec_state.global.module_infos, mem.module_infos);
3723 assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
3724
3725 clear_mem_cache().await;
3726 ctx.close().await;
3727 }
3728
3729 #[tokio::test(flavor = "multi_thread")]
3730 async fn run_with_caching_no_action_refreshes_mock_memory() {
3731 cache::bust_cache().await;
3732 clear_mem_cache().await;
3733
3734 let ctx = ExecutorContext::new_with_engine(
3735 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
3736 Default::default(),
3737 );
3738 let program = crate::Program::parse_no_errs(
3739 r#"sketch001 = sketch(on = XY) {
3740 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
3741}
3742"#,
3743 )
3744 .unwrap();
3745
3746 ctx.run_with_caching(program.clone()).await.unwrap();
3747 let baseline_memory = cache::read_old_memory().await.unwrap();
3748 assert!(
3749 !baseline_memory.scene_objects.is_empty(),
3750 "expected engine execution to persist full-scene mock memory"
3751 );
3752
3753 cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
3754 assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
3755
3756 ctx.run_with_caching(program).await.unwrap();
3757 let refreshed_memory = cache::read_old_memory().await.unwrap();
3758 assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
3759 assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
3760 assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
3761
3762 cache::bust_cache().await;
3763 clear_mem_cache().await;
3764 ctx.close().await;
3765 }
3766
3767 #[tokio::test(flavor = "multi_thread")]
3768 async fn sim_sketch_mode_real_mock_real() {
3769 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3770 let code = r#"sketch001 = startSketchOn(XY)
3771profile001 = startProfile(sketch001, at = [0, 0])
3772 |> line(end = [10, 0])
3773 |> line(end = [0, 10])
3774 |> line(end = [-10, 0])
3775 |> line(end = [0, -10])
3776 |> close()
3777"#;
3778 let program = crate::Program::parse_no_errs(code).unwrap();
3779 let result = ctx.run_with_caching(program).await.unwrap();
3780 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
3781
3782 let mock_ctx = ExecutorContext::new_mock(None).await;
3783 let mock_program = crate::Program::parse_no_errs(code).unwrap();
3784 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
3785 assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
3786
3787 let code2 = code.to_owned()
3788 + r#"
3789extrude001 = extrude(profile001, length = 10)
3790"#;
3791 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3792 let result = ctx.run_with_caching(program2).await.unwrap();
3793 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
3794
3795 ctx.close().await;
3796 mock_ctx.close().await;
3797 }
3798
3799 #[tokio::test(flavor = "multi_thread")]
3800 async fn read_tag_version() {
3801 let ast = r#"fn bar(@t) {
3802 return startSketchOn(XY)
3803 |> startProfile(at = [0,0])
3804 |> angledLine(
3805 angle = -60,
3806 length = segLen(t),
3807 )
3808 |> line(end = [0, 0])
3809 |> close()
3810}
3811
3812sketch = startSketchOn(XY)
3813 |> startProfile(at = [0,0])
3814 |> line(end = [0, 10])
3815 |> line(end = [10, 0], tag = $tag0)
3816 |> line(endAbsolute = [0, 0])
3817
3818fn foo() {
3819 // tag0 tags an edge
3820 return bar(tag0)
3821}
3822
3823solid = sketch |> extrude(length = 10)
3824// tag0 tags a face
3825sketch2 = startSketchOn(solid, face = tag0)
3826 |> startProfile(at = [0,0])
3827 |> line(end = [0, 1])
3828 |> line(end = [1, 0])
3829 |> line(end = [0, 0])
3830
3831foo() |> extrude(length = 1)
3832"#;
3833 parse_execute(ast).await.unwrap();
3834 }
3835
3836 #[tokio::test(flavor = "multi_thread")]
3837 async fn experimental() {
3838 let code = r#"
3839startSketchOn(XY)
3840 |> startProfile(at = [0, 0], tag = $start)
3841 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3842"#;
3843 let result = parse_execute(code).await.unwrap();
3844 let issues = result.exec_state.issues();
3845 assert_eq!(issues.len(), 1);
3846 assert_eq!(issues[0].severity, Severity::Error);
3847 let msg = &issues[0].message;
3848 assert!(msg.contains("experimental"), "found {msg}");
3849
3850 let code = r#"@settings(experimentalFeatures = allow)
3851startSketchOn(XY)
3852 |> startProfile(at = [0, 0], tag = $start)
3853 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3854"#;
3855 let result = parse_execute(code).await.unwrap();
3856 let issues = result.exec_state.issues();
3857 assert!(issues.is_empty(), "issues={issues:#?}");
3858
3859 let code = r#"@settings(experimentalFeatures = warn)
3860startSketchOn(XY)
3861 |> startProfile(at = [0, 0], tag = $start)
3862 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3863"#;
3864 let result = parse_execute(code).await.unwrap();
3865 let issues = result.exec_state.issues();
3866 assert_eq!(issues.len(), 1);
3867 assert_eq!(issues[0].severity, Severity::Warning);
3868 let msg = &issues[0].message;
3869 assert!(msg.contains("experimental"), "found {msg}");
3870
3871 let code = r#"@settings(experimentalFeatures = deny)
3872startSketchOn(XY)
3873 |> startProfile(at = [0, 0], tag = $start)
3874 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3875"#;
3876 let result = parse_execute(code).await.unwrap();
3877 let issues = result.exec_state.issues();
3878 assert_eq!(issues.len(), 1);
3879 assert_eq!(issues[0].severity, Severity::Error);
3880 let msg = &issues[0].message;
3881 assert!(msg.contains("experimental"), "found {msg}");
3882
3883 let code = r#"@settings(experimentalFeatures = foo)
3884startSketchOn(XY)
3885 |> startProfile(at = [0, 0], tag = $start)
3886 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3887"#;
3888 parse_execute(code).await.unwrap_err();
3889 }
3890
3891 #[tokio::test(flavor = "multi_thread")]
3892 async fn experimental_parameter() {
3893 let code = r#"
3894fn inc(@x, @(experimental = true) amount? = 1) {
3895 return x + amount
3896}
3897
3898answer = inc(5, amount = 2)
3899"#;
3900 let result = parse_execute(code).await.unwrap();
3901 let issues = result.exec_state.issues();
3902 assert_eq!(issues.len(), 1);
3903 assert_eq!(issues[0].severity, Severity::Error);
3904 let msg = &issues[0].message;
3905 assert!(msg.contains("experimental"), "found {msg}");
3906
3907 let code = r#"
3909fn inc(@x, @(experimental = true) amount? = 1) {
3910 return x + amount
3911}
3912
3913answer = inc(5)
3914"#;
3915 let result = parse_execute(code).await.unwrap();
3916 let issues = result.exec_state.issues();
3917 assert!(issues.is_empty(), "issues={issues:#?}");
3918 }
3919
3920 #[tokio::test(flavor = "multi_thread")]
3921 async fn experimental_scalar_fixed_constraint() {
3922 let code_left = r#"@settings(experimentalFeatures = warn)
3923sketch(on = XY) {
3924 point1 = point(at = [var 0mm, var 0mm])
3925 point1.at[0] == 1mm
3926}
3927"#;
3928 let code_right = r#"@settings(experimentalFeatures = warn)
3930sketch(on = XY) {
3931 point1 = point(at = [var 0mm, var 0mm])
3932 1mm == point1.at[0]
3933}
3934"#;
3935
3936 for code in [code_left, code_right] {
3937 let result = parse_execute(code).await.unwrap();
3938 let issues = result.exec_state.issues();
3939 let Some(error) = issues
3940 .iter()
3941 .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
3942 else {
3943 panic!("found {issues:#?}");
3944 };
3945 assert_eq!(error.severity, Severity::Warning);
3946 }
3947 }
3948
3949 #[tokio::test(flavor = "multi_thread")]
3953 async fn test_tangent_line_arc_executes_with_mock_engine() {
3954 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
3955 parse_execute(&code).await.unwrap();
3956 }
3957
3958 #[tokio::test(flavor = "multi_thread")]
3959 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
3960 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
3961 parse_execute(&code).await.unwrap();
3962 }
3963
3964 #[tokio::test(flavor = "multi_thread")]
3965 async fn test_tangent_line_circle_executes_with_mock_engine() {
3966 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
3967 parse_execute(&code).await.unwrap();
3968 }
3969
3970 #[tokio::test(flavor = "multi_thread")]
3971 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
3972 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
3973 parse_execute(&code).await.unwrap();
3974 }
3975
3976 #[tokio::test(flavor = "multi_thread")]
3977 async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
3978 let code = r#"startX = 2
3979
3980baseSketch = sketch(on = XY) {
3981 yoyo = line(start = [startX, 0], end = [7, 6])
3982 line2 = line(start = [7, 6], end = [7, 12])
3983 hi = line(start = [7, 12], end = [startX, 0])
3984}
3985
3986baseRegion = region(point = [5.5, 6], sketch = baseSketch)
3987myExtrude = extrude(
3988 baseRegion,
3989 length = 5,
3990 tagEnd = $endCap,
3991 tagStart = $startCap,
3992)
3993yodawg = getCommonEdge(faces = [
3994 baseRegion.tags.hi,
3995 baseRegion.tags.yoyo
3996])
3997
3998cutSketch = sketch(on = YZ) {
3999 myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
4000 myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
4001 line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
4002}
4003
4004cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
4005extrude001 = extrude(cutRegion, length = 5)
4006solid001 = subtract(myExtrude, tools = extrude001)
4007
4008yoyo = getOppositeEdge(baseRegion.tags.hi)
4009fillet(solid001, radius = 0.1, tags = yoyo)
4010"#;
4011
4012 parse_execute(code).await.unwrap();
4013 }
4014
4015 async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
4020 let program = crate::Program::parse_no_errs(kcl).unwrap();
4021 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4022 let mut exec_state = ExecState::new(&ctx);
4023 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
4024 let outcome = exec_state
4025 .into_exec_outcome(env_ref, &ctx)
4026 .await
4027 .expect("constraint report test outcome should collect variables");
4028 let report = outcome.sketch_constraint_report();
4029 ctx.close().await;
4030 report
4031 }
4032
4033 #[tokio::test(flavor = "multi_thread")]
4034 async fn warn_when_sketch_is_over_constrained() {
4035 let code = r#"
4036sketch001 = sketch(on = XY) {
4037 line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
4038 fixed([line1.start, ORIGIN])
4039 fixed([line1.start, [20, 20]])
4040}
4041"#;
4042 let result = parse_execute(code).await.unwrap();
4043 let issues = result.exec_state.issues();
4044 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
4045 panic!("expected over-constrained warning; found {issues:#?}");
4046 };
4047 assert_eq!(warning.severity, Severity::Warning);
4048 }
4049
4050 #[tokio::test(flavor = "multi_thread")]
4051 async fn no_warning_when_sketch_is_not_over_constrained() {
4052 let code = r#"
4054sketch001 = sketch(on = XY) {
4055 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4056}
4057"#;
4058 let result = parse_execute(code).await.unwrap();
4059 let issues = result.exec_state.issues();
4060 assert!(
4061 !issues.iter().any(|issue| issue.message.contains("over-constrained")),
4062 "did not expect over-constrained warning; found {issues:#?}"
4063 );
4064 }
4065
4066 #[tokio::test(flavor = "multi_thread")]
4067 async fn test_constraint_report_fully_constrained() {
4068 let kcl = r#"
4070@settings(experimentalFeatures = allow)
4071
4072sketch(on = YZ) {
4073 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4074 line1.start.at[0] == 2
4075 line1.start.at[1] == 8
4076 line1.end.at[0] == 5
4077 line1.end.at[1] == 7
4078}
4079"#;
4080 let report = run_constraint_report(kcl).await;
4081 assert_eq!(report.fully_constrained.len(), 1);
4082 assert_eq!(report.under_constrained.len(), 0);
4083 assert_eq!(report.over_constrained.len(), 0);
4084 assert_eq!(report.errors.len(), 0);
4085 assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
4086 }
4087
4088 #[tokio::test(flavor = "multi_thread")]
4089 async fn test_constraint_report_under_constrained() {
4090 let kcl = r#"
4092sketch(on = YZ) {
4093 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
4094}
4095"#;
4096 let report = run_constraint_report(kcl).await;
4097 assert_eq!(report.fully_constrained.len(), 0);
4098 assert_eq!(report.under_constrained.len(), 1);
4099 assert_eq!(report.over_constrained.len(), 0);
4100 assert_eq!(report.errors.len(), 0);
4101 assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
4102 assert!(report.under_constrained[0].free_count > 0);
4103 }
4104
4105 #[tokio::test(flavor = "multi_thread")]
4106 async fn test_constraint_report_over_constrained() {
4107 let kcl = r#"
4109@settings(experimentalFeatures = allow)
4110
4111sketch(on = YZ) {
4112 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4113 line1.start.at[0] == 2
4114 line1.start.at[1] == 8
4115 line1.end.at[0] == 5
4116 line1.end.at[1] == 7
4117 distance([line1.start, line1.end]) == 100mm
4118}
4119"#;
4120 let report = run_constraint_report(kcl).await;
4121 assert_eq!(report.over_constrained.len(), 1);
4122 assert_eq!(report.errors.len(), 0);
4123 assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
4124 assert!(report.over_constrained[0].conflict_count > 0);
4125 }
4126
4127 #[tokio::test(flavor = "multi_thread")]
4128 async fn test_constraint_report_multiple_sketches() {
4129 let kcl = r#"
4131@settings(experimentalFeatures = allow)
4132
4133s1 = sketch(on = YZ) {
4134 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4135 line1.start.at[0] == 2
4136 line1.start.at[1] == 8
4137 line1.end.at[0] == 5
4138 line1.end.at[1] == 7
4139}
4140
4141s2 = sketch(on = XZ) {
4142 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4143}
4144"#;
4145 let report = run_constraint_report(kcl).await;
4146 assert_eq!(
4147 report.fully_constrained.len()
4148 + report.under_constrained.len()
4149 + report.over_constrained.len()
4150 + report.errors.len(),
4151 2,
4152 "Expected 2 sketches total"
4153 );
4154 assert_eq!(report.fully_constrained.len(), 1);
4155 assert_eq!(report.under_constrained.len(), 1);
4156 }
4157}