1use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use anyhow::Result;
7pub use artifact::ArtifactCommand;
8pub(crate) use artifact::EntityCloneInfo;
9pub(crate) use artifact::named_view_artifact;
10pub(crate) use artifact::sketch_block_constraint_type;
11use cache::GlobalState;
12pub use cache::bust_cache;
13pub use cache::clear_mem_cache;
14pub use geometry::*;
15pub use id_generator::IdGenerator;
16pub(crate) use import::PreImportedGeometry;
17use indexmap::IndexMap;
18pub use kcl_api::Operation;
19pub use kcl_api::artifact::Artifact;
20pub use kcl_api::artifact::ArtifactGraph;
21pub use kcl_api::artifact::CapSubType;
22pub use kcl_api::artifact::CodeRef;
23pub use kcl_api::artifact::GdtAnnotationArtifact;
24pub use kcl_api::artifact::SketchBlock;
25pub use kcl_api::artifact::SketchBlockConstraint;
26#[allow(unused_imports)]
27pub use kcl_api::artifact::SketchBlockConstraintType;
28pub use kcl_api::artifact::StartSketchOnFace;
29pub use kcl_api::artifact::StartSketchOnPlane;
30use kcl_api::ast::node_path::NodePath;
31pub use kcl_value::KclObjectFields;
32pub use kcl_value::KclObjectKind;
33pub use kcl_value::KclValue;
34pub use kcl_value_view::EdgeCutViewExt;
35pub use kcl_value_view::ExtrudeSurfaceViewExt;
36pub use kcl_value_view::KclValueView;
37pub use kcl_value_view::PathViewExt;
38pub use kcl_value_view::SolidViewExt;
39use kcmc::ImageFormat;
40use kcmc::ModelingCmd;
41use kcmc::each_cmd as mcmd;
42use kcmc::ok_response::OkModelingCmdResponse;
43use kcmc::ok_response::output::TakeSnapshot;
44use kcmc::websocket::ModelingSessionData;
45use kcmc::websocket::OkWebSocketResponseData;
46use kittycad_modeling_cmds::id::ModelingCmdId;
47use kittycad_modeling_cmds::{self as kcmc};
48pub use memory::EnvironmentRef;
49#[cfg(test)]
50pub(crate) use memory::MemoryBackendKind;
51pub(crate) use modeling::ModelingCmdMeta;
52pub use named_views::*;
53use serde::Deserialize;
54use serde::Serialize;
55pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
56pub(crate) use sketch_solve::solver_numeric_type;
57pub(crate) use solver_arc::SolverArc;
58pub(crate) use state::ConstraintKey;
59pub(crate) use state::ConstraintState;
60pub(crate) use state::ConsumedRegionInfo;
61pub(crate) use state::ConsumedRegionOperation;
62pub(crate) use state::ConsumedSolidInfo;
63pub(crate) use state::ConsumedSolidKey;
64pub(crate) use state::ConsumedSolidOperation;
65pub use state::DirectTagFilletMeta;
66pub use state::DirectTagFilletTagEntry;
67pub use state::EdgeRefactorMeta;
68pub use state::EdgeRefactorStdlibFn;
69pub use state::ExecState;
70pub use state::KclVersion;
71pub use state::LegacyAngleRefactorMeta;
72pub use state::MetaSettings;
73pub(crate) use state::ModuleArtifactState;
74pub(crate) use state::PendingEdgeRefactorMeta;
75pub(crate) use state::PendingLegacyAngleRefactorMeta;
76pub use state::RefactorMetadata;
77pub(crate) use state::TangencyMode;
78
79use crate::CompilationIssue;
80use crate::ExecError;
81use crate::KclErrorWithOutputs;
82use crate::NodePathExt;
83use crate::SourceRange;
84use crate::collections::AhashIndexSet;
85use crate::engine::EngineBatchContext;
86use crate::engine::GridScaleBehavior;
87use crate::engine::engine_manager::EngineManager;
88use crate::errors::KclError;
89use crate::errors::KclErrorDetails;
90use crate::execution::cache::CacheInformation;
91use crate::execution::cache::CacheResult;
92use crate::execution::cad_op::OperationExt;
93use crate::execution::import_graph::Universe;
94use crate::execution::import_graph::UniverseMap;
95use crate::execution::typed_path::TypedPath;
96use crate::front::Number;
97use crate::front::Object;
98use crate::front::ObjectId;
99use crate::fs::FileManager;
100use crate::fs::FileSystemHandle;
101use crate::modules::ModuleExecutionOutcome;
102use crate::modules::ModuleId;
103use crate::modules::ModulePath;
104use crate::modules::ModuleRepr;
105use crate::modules::ModuleSource;
106use crate::parsing::ast::types::Expr;
107use crate::parsing::ast::types::ImportPath;
108use crate::parsing::ast::types::NodeRef;
109
110#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
111#[ts(export)]
112pub struct OperationsByModule {
113 pub map: IndexMap<ModuleId, Vec<Operation>>,
114}
115
116#[derive(Clone, Serialize, ts_rs::TS)]
117#[ts(export)]
118#[serde(rename_all = "camelCase")]
119pub struct OperationCallbackArgs {
120 pub module_id: ModuleId,
121 pub operation: Operation,
122 pub index: usize,
123}
124
125pub trait ExecutionCallbacks: std::fmt::Debug + Send + Sync + 'static {
126 fn on_operation(&self, _args: OperationCallbackArgs) {}
127}
128
129impl OperationsByModule {
130 pub fn count(&self) -> usize {
131 self.map.values().map(Vec::len).sum()
132 }
133
134 pub fn is_empty(&self) -> bool {
135 self.map.values().all(Vec::is_empty)
136 }
137
138 pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
139 self.map.get(module_id)
140 }
141
142 pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
143 self.map.values()
144 }
145
146 pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
147 self.map.insert(module_id, operations);
148 }
149}
150
151pub(crate) mod annotations;
152mod artifact;
153#[cfg(test)]
154pub(crate) use artifact::mermaid_tests::ArtifactGraphMermaidExt;
155pub(crate) mod cache;
156mod cad_op;
157pub(crate) mod exec_ast;
158pub mod fn_call;
159#[cfg(test)]
160mod freedom_analysis_tests;
161mod geometry;
162#[cfg(test)]
163mod hide_id_contract_kcl_test_pins;
164mod id_generator;
165mod import;
166mod import_graph;
167pub(crate) mod kcl_value;
168pub(crate) mod kcl_value_view;
169pub(crate) mod machine;
170mod memory;
171mod modeling;
172mod named_views;
173mod sketch_solve;
174mod solver_arc;
175mod state;
176pub mod typed_path;
177pub(crate) mod types;
178
179pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
180pub(crate) const SKETCH_OBJECT_META: &str = "meta";
181pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
182
183macro_rules! control_continue {
188 ($control_flow:expr) => {{
189 let cf = $control_flow;
190 if cf.is_some_return() {
191 return Ok(cf);
192 } else {
193 cf.into_value()
194 }
195 }};
196}
197pub(crate) use control_continue;
199
200macro_rules! early_return {
205 ($control_flow:expr) => {{
206 let cf = $control_flow;
207 if cf.is_some_return() {
208 return Err(EarlyReturn::from(cf));
209 } else {
210 cf.into_value()
211 }
212 }};
213}
214pub(crate) use early_return;
216
217#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
218pub enum ControlFlowKind {
219 #[default]
221 Continue,
222 Return,
227 Exit,
230}
231
232impl ControlFlowKind {
233 pub fn is_some_return(&self) -> bool {
235 match self {
236 ControlFlowKind::Continue => false,
237 ControlFlowKind::Return => true,
238 ControlFlowKind::Exit => true,
239 }
240 }
241}
242
243#[must_use = "You should always handle the control flow value when it is returned"]
244#[derive(Debug, Clone, PartialEq, Serialize)]
245pub struct KclValueControlFlow {
246 value: Box<KclValue>,
248 pub control: ControlFlowKind,
249}
250
251impl KclValue {
252 pub(crate) fn continue_(self) -> KclValueControlFlow {
253 KclValueControlFlow {
254 value: Box::new(self),
255 control: ControlFlowKind::Continue,
256 }
257 }
258
259 pub(crate) fn return_(self) -> KclValueControlFlow {
260 KclValueControlFlow {
261 value: Box::new(self),
262 control: ControlFlowKind::Return,
263 }
264 }
265
266 pub(crate) fn exit(self) -> KclValueControlFlow {
267 KclValueControlFlow {
268 value: Box::new(self),
269 control: ControlFlowKind::Exit,
270 }
271 }
272}
273
274impl KclValueControlFlow {
275 pub fn is_some_return(&self) -> bool {
277 self.control.is_some_return()
278 }
279
280 pub(crate) fn is_return(&self) -> bool {
281 matches!(self.control, ControlFlowKind::Return)
282 }
283
284 pub(crate) fn is_exit(&self) -> bool {
285 matches!(self.control, ControlFlowKind::Exit)
286 }
287
288 pub(crate) fn source_ranges(&self) -> Vec<SourceRange> {
290 self.value.metadata().iter().map(|m| m.source_range).collect()
291 }
292
293 pub(crate) fn into_value(self) -> KclValue {
294 *self.value
295 }
296}
297
298#[must_use = "You should always handle the control flow value when it is returned"]
305#[allow(clippy::large_enum_variant)]
306#[derive(Debug, Clone)]
307pub(crate) enum EarlyReturn {
308 Value(KclValueControlFlow),
310 Error(KclError),
312}
313
314impl From<KclValueControlFlow> for EarlyReturn {
315 fn from(cf: KclValueControlFlow) -> Self {
316 EarlyReturn::Value(cf)
317 }
318}
319
320impl From<KclError> for EarlyReturn {
321 fn from(err: KclError) -> Self {
322 EarlyReturn::Error(err)
323 }
324}
325
326pub(crate) enum StatementKind<'a> {
327 Declaration { name: &'a str },
328 Expression,
329}
330
331#[derive(Debug, Clone, Copy)]
332pub enum PreserveMem {
333 Normal,
334 Always,
335}
336
337impl PreserveMem {
338 fn normal(self) -> bool {
339 match self {
340 PreserveMem::Normal => true,
341 PreserveMem::Always => false,
342 }
343 }
344}
345
346#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
348#[ts(export)]
349#[serde(rename_all = "camelCase")]
350pub struct ExecOutcome {
351 pub variables: IndexMap<String, KclValueView>,
353 #[cfg(test)]
355 #[serde(skip)]
356 #[ts(skip)]
357 pub(crate) test_program_memory: IndexMap<String, KclValue>,
358 pub operations: OperationsByModule,
361 pub artifact_graph: ArtifactGraph,
363 #[serde(skip)]
365 pub scene_objects: Vec<Object>,
366 #[serde(skip)]
369 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
370 #[serde(skip)]
371 pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
372 pub refactor_metadata: Vec<RefactorMetadata>,
374 pub issues: Vec<CompilationIssue>,
376 pub filenames: IndexMap<ModuleId, ModulePath>,
378 #[serde(skip)]
383 pub source_files: IndexMap<ModuleId, ModuleSource>,
384 pub default_planes: Option<DefaultPlanes>,
386}
387
388#[derive(Debug, Clone, Copy, PartialEq)]
392enum SegmentFreedom {
393 Free,
394 Fixed,
395 Conflict,
396 Error,
398}
399
400impl From<crate::front::Freedom> for SegmentFreedom {
401 fn from(f: crate::front::Freedom) -> Self {
402 match f {
403 crate::front::Freedom::Free => Self::Free,
404 crate::front::Freedom::Fixed => Self::Fixed,
405 crate::front::Freedom::Conflict => Self::Conflict,
406 }
407 }
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
412pub enum ConstraintKind {
413 FullyConstrained,
414 UnderConstrained,
415 OverConstrained,
416 Error,
420}
421
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
429pub struct SketchConstraintStatus {
430 pub name: String,
444 pub status: ConstraintKind,
446 pub free_count: usize,
448 pub conflict_count: usize,
450 pub total_count: usize,
452}
453
454#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
456pub struct SketchConstraintReport {
457 pub fully_constrained: Vec<SketchConstraintStatus>,
458 pub under_constrained: Vec<SketchConstraintStatus>,
459 pub over_constrained: Vec<SketchConstraintStatus>,
460 pub errors: Vec<SketchConstraintStatus>,
463}
464
465pub(crate) fn sketch_constraint_status_for_sketch(
474 scene_objects: &[Object],
475 sketch_obj: &Object,
476) -> Option<SketchConstraintStatus> {
477 use crate::front::ObjectKind;
478 use crate::front::Segment;
479
480 let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
481 return None;
482 };
483
484 let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
486 let obj = scene_objects.get(id.0)?;
487 if let ObjectKind::Segment {
488 segment: Segment::Point(p),
489 } = &obj.kind
490 {
491 Some(p.freedom())
492 } else {
493 None
494 }
495 };
496
497 let mut free_count: usize = 0;
498 let mut conflict_count: usize = 0;
499 let mut error_count: usize = 0;
500 let mut total_count: usize = 0;
501
502 for &seg_id in &sketch.segments {
503 let Some(seg_obj) = scene_objects.get(seg_id.0) else {
504 continue;
505 };
506 let ObjectKind::Segment { segment } = &seg_obj.kind else {
507 continue;
508 };
509 if let Segment::Point(p) = segment
512 && p.owner.is_some()
513 {
514 continue;
515 }
516 let freedom = segment
517 .freedom(lookup)
518 .map(SegmentFreedom::from)
519 .unwrap_or(SegmentFreedom::Error);
520 total_count += 1;
521 match freedom {
522 SegmentFreedom::Free => free_count += 1,
523 SegmentFreedom::Conflict => conflict_count += 1,
524 SegmentFreedom::Error => error_count += 1,
525 SegmentFreedom::Fixed => {}
526 }
527 }
528
529 let status = if error_count > 0 {
530 ConstraintKind::Error
531 } else if conflict_count > 0 {
532 ConstraintKind::OverConstrained
533 } else if free_count > 0 {
534 ConstraintKind::UnderConstrained
535 } else {
536 ConstraintKind::FullyConstrained
537 };
538
539 Some(SketchConstraintStatus {
540 name: sketch_obj.label.clone(),
541 status,
542 free_count,
543 conflict_count,
544 total_count,
545 })
546}
547
548pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
549 let mut fully_constrained = Vec::new();
550 let mut under_constrained = Vec::new();
551 let mut over_constrained = Vec::new();
552 let mut errors = Vec::new();
553 for obj in scene_objects {
554 let Some(entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
555 continue;
556 };
557 match entry.status {
558 ConstraintKind::FullyConstrained => fully_constrained.push(entry),
559 ConstraintKind::UnderConstrained => under_constrained.push(entry),
560 ConstraintKind::OverConstrained => over_constrained.push(entry),
561 ConstraintKind::Error => errors.push(entry),
562 }
563 }
564
565 SketchConstraintReport {
566 fully_constrained,
567 under_constrained,
568 over_constrained,
569 errors,
570 }
571}
572
573impl ExecOutcome {
574 pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
575 debug_assert!(
576 id.0 < self.scene_objects.len(),
577 "Requested object ID {} but only have {} objects",
578 id.0,
579 self.scene_objects.len()
580 );
581 self.scene_objects.get(id.0)
582 }
583
584 pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
586 self.issues.iter().filter(|error| error.is_err())
587 }
588
589 pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
596 sketch_constraint_report_from_scene_objects(&self.scene_objects)
597 }
598
599 pub fn render_sketch_png(
602 &self,
603 sketch_name: &str,
604 ) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
605 use crate::front::ObjectKind;
606 use crate::tooling::sketch_visualizer::SketchVisualizationError;
607
608 let sketches = self
609 .scene_objects
610 .iter()
611 .filter_map(|object| match &object.kind {
612 ObjectKind::Sketch(sketch) if object.label == sketch_name => Some(sketch),
613 _ => None,
614 })
615 .collect::<Vec<_>>();
616 let sketch = match sketches.as_slice() {
617 [] => {
618 return Err(SketchVisualizationError::SketchNotFound {
619 name: sketch_name.to_owned(),
620 });
621 }
622 [sketch] => *sketch,
623 _ => {
624 return Err(SketchVisualizationError::AmbiguousSketchName {
625 name: sketch_name.to_owned(),
626 count: sketches.len(),
627 });
628 }
629 };
630
631 crate::tooling::sketch_visualizer::render_sketch_png(&self.scene_objects, sketch)
632 }
633}
634
635#[derive(Debug, Clone, PartialEq)]
637pub struct MockConfig {
638 pub use_prev_memory: bool,
639 pub sketch_block_id: Option<ObjectId>,
642 pub freedom_analysis: bool,
645 pub segment_ids_edited: AhashIndexSet<ObjectId>,
647 pub drag_anchors: Vec<SegmentDragAnchor>,
649}
650
651#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
652#[ts(export, export_to = "FrontendApi.ts")]
653#[serde(rename_all = "camelCase")]
654pub struct SegmentDragAnchor {
655 pub segment_id: ObjectId,
656 pub target: crate::front::Point2d<Number>,
657}
658
659impl Default for MockConfig {
660 fn default() -> Self {
661 Self {
662 use_prev_memory: true,
664 sketch_block_id: None,
665 freedom_analysis: true,
666 segment_ids_edited: AhashIndexSet::default(),
667 drag_anchors: Vec::new(),
668 }
669 }
670}
671
672impl MockConfig {
673 pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
675 Self {
676 sketch_block_id: Some(sketch_block_id),
677 ..Default::default()
678 }
679 }
680
681 #[must_use]
682 pub(crate) fn no_freedom_analysis(mut self) -> Self {
683 self.freedom_analysis = false;
684 self
685 }
686}
687
688#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
689#[ts(export)]
690#[serde(rename_all = "camelCase")]
691pub struct DefaultPlanes {
692 pub xy: uuid::Uuid,
693 pub xz: uuid::Uuid,
694 pub yz: uuid::Uuid,
695 pub neg_xy: uuid::Uuid,
696 pub neg_xz: uuid::Uuid,
697 pub neg_yz: uuid::Uuid,
698}
699
700#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
701#[ts(export)]
702#[serde(tag = "type", rename_all = "camelCase")]
703pub struct TagIdentifier {
704 pub value: String,
705 #[serde(skip)]
708 pub info: Vec<(usize, TagEngineInfo)>,
709 #[serde(skip)]
710 pub meta: Vec<Metadata>,
711}
712
713impl TagIdentifier {
714 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
716 for (e, info) in self.info.iter().rev() {
717 if *e <= at_epoch {
718 return Some(info);
719 }
720 }
721
722 None
723 }
724
725 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
727 self.info.last().map(|i| &i.1)
728 }
729
730 pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
733 let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
734 return vec![];
735 };
736 self.info
737 .iter()
738 .rev()
739 .take_while(|(e, _)| *e == cur_epoch)
740 .map(|(_, info)| info)
741 .collect()
742 }
743
744 pub fn merge_info(&mut self, other: &TagIdentifier) {
746 assert_eq!(&self.value, &other.value);
747 for (oe, ot) in &other.info {
748 if let Some((e, t)) = self.info.last_mut() {
749 if *e > *oe {
751 continue;
752 }
753 if e == oe {
755 *t = ot.clone();
756 continue;
757 }
758 }
759 self.info.push((*oe, ot.clone()));
760 }
761 }
762
763 pub fn geometry(&self) -> Option<Geometry> {
764 self.get_cur_info().map(|info| info.geometry.clone())
765 }
766
767 pub(crate) fn is_body_created_tag(&self) -> bool {
768 self.get_cur_info().is_some_and(|info| {
769 matches!(&info.geometry, Geometry::Solid(_)) && info.path.is_none() && info.surface.is_some()
770 })
771 }
772}
773
774impl Eq for TagIdentifier {}
775
776impl std::fmt::Display for TagIdentifier {
777 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
778 write!(f, "{}", self.value)
779 }
780}
781
782impl std::str::FromStr for TagIdentifier {
783 type Err = KclError;
784
785 fn from_str(s: &str) -> Result<Self, Self::Err> {
786 Ok(Self {
787 value: s.to_string(),
788 info: Vec::new(),
789 meta: Default::default(),
790 })
791 }
792}
793
794impl Ord for TagIdentifier {
795 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
796 self.value.cmp(&other.value)
797 }
798}
799
800impl PartialOrd for TagIdentifier {
801 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
802 Some(self.cmp(other))
803 }
804}
805
806impl std::hash::Hash for TagIdentifier {
807 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
808 self.value.hash(state);
809 }
810}
811
812#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
814#[ts(export)]
815#[serde(tag = "type", rename_all = "camelCase")]
816pub struct TagEngineInfo {
817 pub id: uuid::Uuid,
819 pub geometry: Geometry,
821 pub path: Option<Path>,
823 pub surface: Option<ExtrudeSurface>,
825}
826
827#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
828pub enum BodyType {
829 Root,
830 Block,
831}
832
833#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
835#[ts(export)]
836#[serde(rename_all = "camelCase")]
837pub struct Metadata {
838 pub source_range: SourceRange,
840}
841
842impl From<Metadata> for Vec<SourceRange> {
843 fn from(meta: Metadata) -> Self {
844 vec![meta.source_range]
845 }
846}
847
848impl From<&Metadata> for SourceRange {
849 fn from(meta: &Metadata) -> Self {
850 meta.source_range
851 }
852}
853
854impl From<SourceRange> for Metadata {
855 fn from(source_range: SourceRange) -> Self {
856 Self { source_range }
857 }
858}
859
860impl<T> From<NodeRef<'_, T>> for Metadata {
861 fn from(node: NodeRef<'_, T>) -> Self {
862 Self {
863 source_range: SourceRange::new(node.start, node.end, node.module_id),
864 }
865 }
866}
867
868impl From<&Expr> for Metadata {
869 fn from(expr: &Expr) -> Self {
870 Self {
871 source_range: SourceRange::from(expr),
872 }
873 }
874}
875
876impl Metadata {
877 pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
878 if meta.len() == 1 {
879 let meta = &meta[0];
880 return crate::front::SourceRef::Simple {
881 range: meta.source_range,
882 node_path,
883 };
884 }
885 crate::front::SourceRef::BackTrace {
886 ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
887 }
888 }
889}
890
891#[derive(PartialEq, Debug, Default, Clone)]
893pub enum ContextType {
894 #[default]
896 Live,
897
898 Mock,
902
903 MockCustomForwarded,
905}
906
907#[derive(Clone)]
911pub struct ExecutorContext {
912 pub engine: Arc<EngineManager>,
913 pub engine_batch: EngineBatchContext,
914 pub fs: FileSystemHandle,
915 pub settings: ExecutorSettings,
916 pub context_type: ContextType,
917 pub execution_callbacks: Option<Arc<dyn ExecutionCallbacks>>,
918 pub(crate) executor_kind: machine::ExecutorKind,
922 pub(crate) machine_call_depth_limit: usize,
925}
926
927impl std::fmt::Debug for ExecutorContext {
928 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
929 f.debug_struct("ExecutorContext")
930 .field("engine", &self.engine)
931 .field("engine_batch", &self.engine_batch)
932 .field("settings", &self.settings)
933 .field("context_type", &self.context_type)
934 .field("execution_callbacks", &self.execution_callbacks)
935 .field("executor_kind", &self.executor_kind)
936 .field("machine_call_depth_limit", &self.machine_call_depth_limit)
937 .finish()
938 }
939}
940
941#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
943#[ts(export)]
944pub struct ExecutorSettings {
945 pub highlight_edges: bool,
947 pub enable_ssao: bool,
949 pub show_grid: bool,
951 pub replay: Option<String>,
954 pub project_directory: Option<TypedPath>,
957 pub current_file: Option<TypedPath>,
960 pub fixed_size_grid: bool,
962 #[serde(default, skip_serializing_if = "is_false")]
968 pub skip_artifact_graph: bool,
969 #[serde(default, skip_serializing_if = "Option::is_none")]
972 pub heartbeats: Option<u64>,
973 #[serde(default, skip_serializing_if = "Option::is_none")]
976 pub default_backface_color: Option<String>,
977}
978
979fn is_false(b: &bool) -> bool {
980 !*b
981}
982
983impl Default for ExecutorSettings {
984 fn default() -> Self {
985 Self {
986 highlight_edges: true,
987 enable_ssao: false,
988 show_grid: false,
989 replay: None,
990 project_directory: None,
991 current_file: None,
992 fixed_size_grid: true,
993 skip_artifact_graph: false,
994 heartbeats: None,
995 default_backface_color: None,
996 }
997 }
998}
999
1000impl From<crate::settings::types::Configuration> for ExecutorSettings {
1001 fn from(config: crate::settings::types::Configuration) -> Self {
1002 Self::from(config.settings)
1003 }
1004}
1005
1006impl From<crate::settings::types::Settings> for ExecutorSettings {
1007 fn from(settings: crate::settings::types::Settings) -> Self {
1008 let modeling_settings = settings.modeling.unwrap_or_default();
1009 Self {
1010 highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
1011 enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
1012 show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
1013 replay: None,
1014 project_directory: None,
1015 current_file: None,
1016 fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
1017 skip_artifact_graph: false,
1018 heartbeats: None,
1019 default_backface_color: modeling_settings.backface_color.map(|color| color.0),
1020 }
1021 }
1022}
1023
1024impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
1025 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
1026 Self::from(config.settings.modeling)
1027 }
1028}
1029
1030impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
1031 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
1032 Self {
1033 highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
1034 enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
1035 show_grid: modeling.show_scale_grid.unwrap_or_default(),
1036 replay: None,
1037 project_directory: None,
1038 current_file: None,
1039 fixed_size_grid: true,
1040 skip_artifact_graph: false,
1041 heartbeats: None,
1042 default_backface_color: modeling.backface_color.map(|color| color.0),
1043 }
1044 }
1045}
1046
1047impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
1048 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
1049 Self {
1050 highlight_edges: modeling.highlight_edges.into(),
1051 enable_ssao: modeling.enable_ssao.into(),
1052 show_grid: Default::default(),
1053 replay: None,
1054 project_directory: None,
1055 current_file: None,
1056 fixed_size_grid: true,
1057 skip_artifact_graph: false,
1058 heartbeats: None,
1059 default_backface_color: None,
1060 }
1061 }
1062}
1063
1064impl ExecutorSettings {
1065 pub fn with_current_file(&mut self, current_file: TypedPath) {
1067 if current_file.extension() == Some("kcl") {
1069 self.current_file = Some(current_file.clone());
1070 if let Some(parent) = current_file.parent() {
1072 self.project_directory = Some(parent);
1073 } else {
1074 self.project_directory = Some(TypedPath::from(""));
1075 }
1076 } else {
1077 self.project_directory = Some(current_file);
1078 }
1079 }
1080}
1081
1082impl ExecutorContext {
1083 pub fn new_with_engine_and_fs(
1085 engine: Arc<EngineManager>,
1086 fs: FileSystemHandle,
1087 settings: ExecutorSettings,
1088 ) -> Self {
1089 ExecutorContext {
1090 engine,
1091 engine_batch: EngineBatchContext::default(),
1092 fs,
1093 settings,
1094 context_type: ContextType::Live,
1095 execution_callbacks: Default::default(),
1096 executor_kind: machine::ExecutorKind::resolve(),
1097 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1098 }
1099 }
1100
1101 fn clone_with_fresh_execution_batch(&self) -> Self {
1102 Self {
1103 engine: self.engine.clone(),
1104 engine_batch: EngineBatchContext::new(),
1105 fs: self.fs.clone(),
1106 settings: self.settings.clone(),
1107 context_type: self.context_type.clone(),
1108 execution_callbacks: self.execution_callbacks.clone(),
1109 executor_kind: self.executor_kind,
1112 machine_call_depth_limit: self.machine_call_depth_limit,
1113 }
1114 }
1115
1116 #[cfg(not(target_arch = "wasm32"))]
1118 pub fn new_with_engine(engine: Arc<EngineManager>, settings: ExecutorSettings) -> Self {
1119 Self::new_with_engine_and_fs(engine, crate::fs::new_file_system_handle(FileManager::new()), settings)
1120 }
1121
1122 #[cfg(not(target_arch = "wasm32"))]
1124 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
1125 let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
1126 let (ws, _headers) = client
1127 .modeling()
1128 .commands_ws(kittycad::modeling::CommandsWsParams {
1129 api_call_id: None,
1130 fps: None,
1131 order_independent_transparency: None,
1132 post_effect: if settings.enable_ssao {
1133 Some(kittycad::types::PostEffectType::Ssao)
1134 } else {
1135 None
1136 },
1137 replay: settings.replay.clone(),
1138 show_grid: if settings.show_grid { Some(true) } else { None },
1139 pool: None,
1140 pr,
1141 unlocked_framerate: None,
1142 webrtc: Some(false),
1143 video_res_width: None,
1144 video_res_height: None,
1145 })
1146 .await?;
1147
1148 let engine_conn = EngineManager::new_websocket_transport(ws, settings.heartbeats).await;
1149 let engine = Arc::new(engine_conn);
1150
1151 Ok(Self::new_with_engine(engine, settings))
1152 }
1153
1154 #[cfg(target_arch = "wasm32")]
1155 pub fn new(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1156 Self::new_with_engine_and_fs(engine, fs, settings)
1157 }
1158
1159 #[cfg(not(target_arch = "wasm32"))]
1160 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
1161 ExecutorContext {
1162 engine: Arc::new(EngineManager::new_mock()),
1163 engine_batch: EngineBatchContext::default(),
1164 fs: crate::fs::new_file_system_handle(FileManager::new()),
1165 settings: settings.unwrap_or_default(),
1166 context_type: ContextType::Mock,
1167 execution_callbacks: Default::default(),
1168 executor_kind: machine::ExecutorKind::resolve(),
1169 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1170 }
1171 }
1172
1173 #[cfg(target_arch = "wasm32")]
1174 pub fn new_mock(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1175 ExecutorContext {
1176 engine,
1177 engine_batch: EngineBatchContext::default(),
1178 fs,
1179 settings,
1180 context_type: ContextType::Mock,
1181 execution_callbacks: Default::default(),
1182 executor_kind: machine::ExecutorKind::resolve(),
1183 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1184 }
1185 }
1186
1187 #[cfg(target_arch = "wasm32")]
1190 pub fn new_mock_for_lsp(
1191 fs_manager: crate::fs::wasm::FileSystemManager,
1192 settings: ExecutorSettings,
1193 ) -> Result<Self, String> {
1194 let fs = crate::fs::new_file_system_handle(FileManager::new(fs_manager));
1195
1196 Ok(ExecutorContext {
1197 engine: Arc::new(EngineManager::new_mock()),
1198 engine_batch: EngineBatchContext::default(),
1199 fs,
1200 settings,
1201 context_type: ContextType::Mock,
1202 execution_callbacks: Default::default(),
1203 executor_kind: machine::ExecutorKind::resolve(),
1204 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1205 })
1206 }
1207
1208 #[cfg(not(target_arch = "wasm32"))]
1209 pub fn new_forwarded_mock(engine: Arc<EngineManager>) -> Self {
1210 ExecutorContext {
1211 engine,
1212 engine_batch: EngineBatchContext::default(),
1213 fs: crate::fs::new_file_system_handle(FileManager::new()),
1214 settings: Default::default(),
1215 context_type: ContextType::MockCustomForwarded,
1216 execution_callbacks: Default::default(),
1217 executor_kind: machine::ExecutorKind::resolve(),
1218 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1219 }
1220 }
1221
1222 #[cfg(not(target_arch = "wasm32"))]
1228 pub async fn new_with_client(
1229 settings: ExecutorSettings,
1230 token: Option<String>,
1231 engine_addr: Option<String>,
1232 ) -> Result<Self> {
1233 let client = crate::engine::new_zoo_client(token, engine_addr)?;
1235
1236 let ctx = Self::new(&client, settings).await?;
1237 Ok(ctx)
1238 }
1239
1240 #[cfg(not(target_arch = "wasm32"))]
1245 pub async fn new_with_default_client() -> Result<Self> {
1246 let ctx = Self::new_with_client(Default::default(), None, None).await?;
1248 Ok(ctx)
1249 }
1250
1251 #[cfg(not(target_arch = "wasm32"))]
1253 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
1254 let ctx = ExecutorContext::new_with_client(
1255 ExecutorSettings {
1256 highlight_edges: true,
1257 enable_ssao: false,
1258 show_grid: false,
1259 replay: None,
1260 project_directory: None,
1261 current_file: None,
1262 fixed_size_grid: false,
1263 skip_artifact_graph: false,
1264 heartbeats: None,
1265 default_backface_color: None,
1266 },
1267 None,
1268 engine_addr,
1269 )
1270 .await?;
1271 Ok(ctx)
1272 }
1273
1274 pub fn is_mock(&self) -> bool {
1275 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1276 }
1277
1278 pub async fn no_engine_commands(&self) -> bool {
1280 self.is_mock()
1281 }
1282
1283 pub async fn send_clear_scene(
1284 &self,
1285 exec_state: &mut ExecState,
1286 source_range: crate::execution::SourceRange,
1287 ) -> Result<(), KclError> {
1288 exec_state.mod_local.artifacts.clear();
1291 exec_state.global.root_module_artifacts.clear();
1292 exec_state.global.artifacts.clear();
1293
1294 self.engine
1295 .clear_scene(&self.engine_batch, &mut exec_state.mod_local.id_generator, source_range)
1296 .await?;
1297 if self.settings.enable_ssao {
1300 let cmd_id = exec_state.next_uuid();
1301 exec_state
1302 .batch_modeling_cmd(
1303 ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1304 ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1305 )
1306 .await?;
1307 }
1308 Ok(())
1309 }
1310
1311 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1312 cache::bust_cache().await;
1313
1314 let outcome = self.run_with_caching(crate::Program::empty()).await?;
1319
1320 Ok(outcome)
1321 }
1322
1323 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1324 self.eval_prelude(exec_state, SourceRange::synthetic())
1325 .await
1326 .map_err(KclErrorWithOutputs::no_outputs)?;
1327 exec_state
1328 .mut_stack()
1329 .push_new_root_env(true)
1330 .map_err(KclErrorWithOutputs::no_outputs)?;
1331 Ok(())
1332 }
1333
1334 fn restore_mock_memory(
1335 exec_state: &mut ExecState,
1336 mem: cache::SketchModeState,
1337 _mock_config: &MockConfig,
1338 ) -> Result<(), KclErrorWithOutputs> {
1339 *exec_state.mut_stack() = mem.stack;
1340 exec_state.global.module_infos = mem.module_infos;
1341 exec_state.global.path_to_source_id = mem.path_to_source_id;
1342 exec_state.global.id_to_source = mem.id_to_source;
1343 exec_state.mod_local.constraint_state = mem.constraint_state;
1344 let len = _mock_config
1345 .sketch_block_id
1346 .map(|sketch_block_id| sketch_block_id.0)
1347 .unwrap_or(0);
1348 if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1349 exec_state
1350 .global
1351 .root_module_artifacts
1352 .restore_scene_objects(scene_objects);
1353 } else {
1354 let message = format!(
1355 "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1356 mem.scene_objects.len(),
1357 len
1358 );
1359 debug_assert!(false, "{message}");
1360 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1361 KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1362 )));
1363 }
1364
1365 Ok(())
1366 }
1367
1368 pub async fn run_mock(
1369 &self,
1370 program: &crate::Program,
1371 mock_config: &MockConfig,
1372 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1373 let (exec_state, main_ref) = self.run_mock_returning_state(program, mock_config).await?;
1374
1375 let mut stack = exec_state.stack().clone();
1380 let module_infos = exec_state.global.module_infos.clone();
1381 let path_to_source_id = exec_state.global.path_to_source_id.clone();
1382 let id_to_source = exec_state.global.id_to_source.clone();
1383 let constraint_state = exec_state.mod_local.constraint_state.clone();
1384 let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1385 let outcome = exec_state
1386 .into_exec_outcome(main_ref, self)
1387 .await
1388 .map_err(KclErrorWithOutputs::no_outputs)?;
1389
1390 stack.squash_env(main_ref).map_err(KclErrorWithOutputs::no_outputs)?;
1391 let state = cache::SketchModeState {
1392 stack,
1393 module_infos,
1394 path_to_source_id,
1395 id_to_source,
1396 constraint_state,
1397 scene_objects,
1398 };
1399 cache::write_old_memory(state).await;
1400
1401 Ok(outcome)
1402 }
1403
1404 async fn run_mock_returning_state(
1409 &self,
1410 program: &crate::Program,
1411 mock_config: &MockConfig,
1412 ) -> Result<(ExecState, EnvironmentRef), KclErrorWithOutputs> {
1413 assert!(
1414 self.is_mock(),
1415 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1416 );
1417
1418 let use_prev_memory = mock_config.use_prev_memory;
1419 let mut exec_state = ExecState::new_mock(self, mock_config);
1420 if use_prev_memory {
1421 match cache::read_old_memory().await {
1422 Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
1423 None => self.prepare_mem(&mut exec_state).await?,
1424 }
1425 } else {
1426 self.prepare_mem(&mut exec_state).await?
1427 };
1428
1429 exec_state
1432 .mut_stack()
1433 .push_new_env_for_scope()
1434 .map_err(KclErrorWithOutputs::no_outputs)?;
1435
1436 let (main_ref, _) = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1437
1438 Ok((exec_state, main_ref))
1439 }
1440
1441 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1442 assert!(!self.is_mock());
1443 let grid_scale = if self.settings.fixed_size_grid {
1444 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1445 } else {
1446 GridScaleBehavior::ScaleWithZoom
1447 };
1448
1449 let original_program = program.clone();
1450
1451 let (_program, exec_state, result) = match cache::read_old_ast().await {
1452 Some(mut cached_state) => {
1453 let old = CacheInformation {
1454 ast: &cached_state.main.ast,
1455 settings: &cached_state.settings,
1456 };
1457 let new = CacheInformation {
1458 ast: &program.ast,
1459 settings: &self.settings,
1460 };
1461
1462 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1464 CacheResult::ReExecute {
1465 clear_scene,
1466 reapply_settings,
1467 program: changed_program,
1468 } => {
1469 if reapply_settings
1470 && self
1471 .engine
1472 .reapply_settings(
1473 &self.engine_batch,
1474 &self.settings,
1475 Default::default(),
1476 &mut cached_state.main.exec_state.id_generator,
1477 grid_scale,
1478 )
1479 .await
1480 .is_err()
1481 {
1482 (true, program, None)
1483 } else {
1484 (
1485 clear_scene,
1486 crate::Program {
1487 ast: changed_program,
1488 original_file_contents: program.original_file_contents,
1489 },
1490 None,
1491 )
1492 }
1493 }
1494 CacheResult::CheckImportsOnly {
1495 reapply_settings,
1496 ast: changed_program,
1497 } => {
1498 let mut reapply_failed = false;
1499 if reapply_settings {
1500 if self
1501 .engine
1502 .reapply_settings(
1503 &self.engine_batch,
1504 &self.settings,
1505 Default::default(),
1506 &mut cached_state.main.exec_state.id_generator,
1507 grid_scale,
1508 )
1509 .await
1510 .is_ok()
1511 {
1512 cache::write_old_ast(GlobalState::with_settings(
1513 cached_state.clone(),
1514 self.settings.clone(),
1515 ))
1516 .await;
1517 } else {
1518 reapply_failed = true;
1519 }
1520 }
1521
1522 if reapply_failed {
1523 (true, program, None)
1524 } else {
1525 let mut new_exec_state = ExecState::new(self);
1527 let (new_universe, new_universe_map) =
1528 self.get_universe(&program, &mut new_exec_state).await?;
1529
1530 let clear_scene = new_universe.values().any(|value| {
1531 let id = value.1;
1532 match (
1533 cached_state.exec_state.get_source(id),
1534 new_exec_state.global.get_source(id),
1535 ) {
1536 (Some(s0), Some(s1)) => s0.source != s1.source,
1537 _ => false,
1538 }
1539 });
1540
1541 if !clear_scene {
1542 cache::write_old_memory(
1544 cached_state
1545 .mock_memory_state()
1546 .map_err(KclErrorWithOutputs::no_outputs)?,
1547 )
1548 .await;
1549 return cached_state
1550 .into_exec_outcome(self)
1551 .await
1552 .map_err(KclErrorWithOutputs::no_outputs);
1553 }
1554
1555 (
1556 true,
1557 crate::Program {
1558 ast: changed_program,
1559 original_file_contents: program.original_file_contents,
1560 },
1561 Some((new_universe, new_universe_map, new_exec_state)),
1562 )
1563 }
1564 }
1565 CacheResult::NoAction(true) => {
1566 if self
1567 .engine
1568 .reapply_settings(
1569 &self.engine_batch,
1570 &self.settings,
1571 Default::default(),
1572 &mut cached_state.main.exec_state.id_generator,
1573 grid_scale,
1574 )
1575 .await
1576 .is_ok()
1577 {
1578 cache::write_old_ast(GlobalState::with_settings(
1580 cached_state.clone(),
1581 self.settings.clone(),
1582 ))
1583 .await;
1584
1585 cache::write_old_memory(
1586 cached_state
1587 .mock_memory_state()
1588 .map_err(KclErrorWithOutputs::no_outputs)?,
1589 )
1590 .await;
1591 return cached_state
1592 .into_exec_outcome(self)
1593 .await
1594 .map_err(KclErrorWithOutputs::no_outputs);
1595 }
1596 (true, program, None)
1597 }
1598 CacheResult::NoAction(false) => {
1599 cache::write_old_memory(
1600 cached_state
1601 .mock_memory_state()
1602 .map_err(KclErrorWithOutputs::no_outputs)?,
1603 )
1604 .await;
1605 return cached_state
1606 .into_exec_outcome(self)
1607 .await
1608 .map_err(KclErrorWithOutputs::no_outputs);
1609 }
1610 };
1611
1612 let (exec_state, result) = match import_check_info {
1613 Some((new_universe, new_universe_map, mut new_exec_state)) => {
1614 self.send_clear_scene(&mut new_exec_state, Default::default())
1616 .await
1617 .map_err(KclErrorWithOutputs::no_outputs)?;
1618
1619 let result = self
1620 .run_concurrent(
1621 &program,
1622 &mut new_exec_state,
1623 Some((new_universe, new_universe_map)),
1624 PreserveMem::Normal,
1625 )
1626 .await;
1627
1628 (new_exec_state, result)
1629 }
1630 None if clear_scene => {
1631 let mut exec_state = cached_state.reconstitute_exec_state(self);
1633 exec_state.reset(self);
1634
1635 self.send_clear_scene(&mut exec_state, Default::default())
1636 .await
1637 .map_err(KclErrorWithOutputs::no_outputs)?;
1638
1639 let result = self
1640 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1641 .await;
1642
1643 (exec_state, result)
1644 }
1645 None => {
1646 let mut exec_state = cached_state.reconstitute_exec_state(self);
1647 exec_state
1648 .mut_stack()
1649 .restore_env(cached_state.main.result_env)
1650 .map_err(KclErrorWithOutputs::no_outputs)?;
1651
1652 let result = self
1653 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1654 .await;
1655
1656 (exec_state, result)
1657 }
1658 };
1659
1660 (program, exec_state, result)
1661 }
1662 None => {
1663 let mut exec_state = ExecState::new(self);
1664 self.send_clear_scene(&mut exec_state, Default::default())
1665 .await
1666 .map_err(KclErrorWithOutputs::no_outputs)?;
1667
1668 let result = self
1669 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1670 .await;
1671
1672 (program, exec_state, result)
1673 }
1674 };
1675
1676 if result.is_err() {
1677 cache::bust_cache().await;
1678 }
1679
1680 let result = result?;
1682
1683 cache::write_old_ast(GlobalState::new(
1687 exec_state.clone(),
1688 self.settings.clone(),
1689 original_program.ast,
1690 result.0,
1691 ))
1692 .await;
1693
1694 let outcome = exec_state
1695 .into_exec_outcome(result.0, self)
1696 .await
1697 .map_err(KclErrorWithOutputs::no_outputs)?;
1698 Ok(outcome)
1699 }
1700
1701 pub async fn run(
1705 &self,
1706 program: &crate::Program,
1707 exec_state: &mut ExecState,
1708 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1709 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1710 .await
1711 }
1712
1713 pub async fn run_concurrent(
1718 &self,
1719 program: &crate::Program,
1720 exec_state: &mut ExecState,
1721 universe_info: Option<(Universe, UniverseMap)>,
1722 preserve_mem: PreserveMem,
1723 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1724 exec_state.set_entry_point_kcl_version(program);
1728
1729 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1732 (universe, universe_map)
1733 } else {
1734 self.get_universe(program, exec_state).await?
1735 };
1736
1737 let mut sorted_imports: Vec<_> = universe_map.iter().collect();
1743 sorted_imports.sort_by_key(|(_, import_stmt)| SourceRange::from(*import_stmt));
1744 for (_path, import_stmt) in sorted_imports {
1745 let filename = match &import_stmt.path {
1749 ImportPath::Kcl { filename } => filename.to_string(),
1750 ImportPath::Foreign { path } => path.to_string(),
1751 ImportPath::Std { .. } => continue,
1752 };
1753 if let Some((_, module_id, module_path, _)) = universe.get(&filename)
1754 && let ModulePath::Local { value, .. } = module_path
1755 {
1756 let name = import_stmt
1757 .module_name()
1758 .unwrap_or_else(|| value.file_name().unwrap_or_default());
1759 let source_range = SourceRange::from(import_stmt);
1760 exec_state.push_op(crate::execution::cad_op::Operation::ModuleInstance {
1761 name,
1762 module_id: *module_id,
1763 glob: matches!(
1764 import_stmt.selector,
1765 crate::parsing::ast::types::ImportSelector::Glob(_)
1766 ),
1767 node_path: crate::NodePath::placeholder(),
1768 source_range,
1769 });
1770 }
1771 }
1772
1773 let default_planes = self.engine.get_default_planes().read().await.clone();
1774
1775 self.eval_prelude(exec_state, SourceRange::synthetic())
1777 .await
1778 .map_err(KclErrorWithOutputs::no_outputs)?;
1779
1780 for modules in import_graph::import_graph(&universe, self)
1781 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1782 .into_iter()
1783 {
1784 #[cfg(not(target_arch = "wasm32"))]
1785 let mut set = tokio::task::JoinSet::new();
1786
1787 #[allow(clippy::type_complexity)]
1788 let (results_tx, mut results_rx): (
1789 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1790 tokio::sync::mpsc::Receiver<_>,
1791 ) = tokio::sync::mpsc::channel(1);
1792
1793 for module in modules {
1794 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1795 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1796 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1797 )));
1798 };
1799 let module_id = *module_id;
1800 let module_path = module_path.clone();
1801 let source_range = SourceRange::from(import_stmt);
1802 let module_exec_state = exec_state.clone();
1804
1805 let repr = repr.clone();
1806 let exec_ctxt = self.clone_with_fresh_execution_batch();
1807 let results_tx = results_tx.clone();
1808
1809 let exec_module = async |exec_ctxt: &ExecutorContext,
1810 repr: &ModuleRepr,
1811 module_id: ModuleId,
1812 module_path: &ModulePath,
1813 exec_state: &mut ExecState,
1814 source_range: SourceRange|
1815 -> Result<ModuleRepr, KclError> {
1816 match repr {
1817 ModuleRepr::Kcl(program, _) => {
1818 let result = exec_ctxt
1819 .exec_module_from_ast(
1820 program,
1821 module_id,
1822 module_path,
1823 exec_state,
1824 source_range,
1825 PreserveMem::Normal,
1826 )
1827 .await;
1828
1829 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1830 }
1831 ModuleRepr::Foreign(geom, _) => {
1832 exec_state.mod_local.artifacts = Default::default();
1836 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1837 .await
1838 .map(|geom| Some(KclValue::ImportedGeometry(geom)))
1839 .map_err(|err| err.add_import_location(&module_path.import_name(), source_range));
1844 let module_artifacts = std::mem::take(&mut exec_state.mod_local.artifacts);
1845
1846 result.map(|val| ModuleRepr::Foreign(geom.clone(), Some((val, module_artifacts))))
1847 }
1848 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1849 format!("Module {module_path} not found in universe"),
1850 vec![source_range],
1851 ))),
1852 }
1853 };
1854
1855 #[cfg(target_arch = "wasm32")]
1856 {
1857 wasm_bindgen_futures::spawn_local(async move {
1858 let mut exec_state = module_exec_state;
1859 let exec_ctxt = exec_ctxt;
1860
1861 let result = exec_module(
1862 &exec_ctxt,
1863 &repr,
1864 module_id,
1865 &module_path,
1866 &mut exec_state,
1867 source_range,
1868 )
1869 .await;
1870
1871 results_tx
1872 .send((module_id, module_path, result))
1873 .await
1874 .unwrap_or_default();
1875 });
1876 }
1877 #[cfg(not(target_arch = "wasm32"))]
1878 {
1879 set.spawn(async move {
1880 let mut exec_state = module_exec_state;
1881 let exec_ctxt = exec_ctxt;
1882
1883 let result = exec_module(
1884 &exec_ctxt,
1885 &repr,
1886 module_id,
1887 &module_path,
1888 &mut exec_state,
1889 source_range,
1890 )
1891 .await;
1892
1893 results_tx
1894 .send((module_id, module_path, result))
1895 .await
1896 .unwrap_or_default();
1897 });
1898 }
1899 }
1900
1901 drop(results_tx);
1902
1903 while let Some((module_id, _, result)) = results_rx.recv().await {
1904 match result {
1905 Ok(new_repr) => {
1906 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1907
1908 match &mut repr {
1909 ModuleRepr::Kcl(_, cache) => {
1910 let ModuleRepr::Kcl(_, session_data) = new_repr else {
1911 unreachable!();
1912 };
1913 *cache = session_data;
1914 }
1915 ModuleRepr::Foreign(_, cache) => {
1916 let ModuleRepr::Foreign(_, session_data) = new_repr else {
1917 unreachable!();
1918 };
1919 *cache = session_data;
1920 }
1921 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1922 }
1923
1924 exec_state.global.module_infos[&module_id].restore_repr(repr);
1925 }
1926 Err(e) => {
1927 let e = import_graph::add_import_backtrace(e, module_id, &universe);
1928 return Err(exec_state.error_with_outputs(e, None, default_planes));
1929 }
1930 }
1931 }
1932 }
1933
1934 exec_state.mod_local.artifacts.operations.clear();
1939
1940 exec_state
1943 .global
1944 .root_module_artifacts
1945 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1946
1947 self.inner_run(program, exec_state, preserve_mem)
1948 .await
1949 .map_err(|mut error| {
1950 let source_ranges = error.error.source_ranges();
1956 if !source_ranges.is_empty()
1957 && !source_ranges.iter().any(|range| range.module_id().is_top_level())
1958 && let Some(outermost) = source_ranges.last()
1959 {
1960 error.error =
1961 import_graph::add_import_backtrace_from(error.error.clone(), outermost.module_id(), &universe);
1962 }
1963 error
1964 })
1965 }
1966
1967 async fn get_universe(
1970 &self,
1971 program: &crate::Program,
1972 exec_state: &mut ExecState,
1973 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1974 exec_state.add_root_module_contents(program);
1975
1976 let mut universe = std::collections::HashMap::new();
1977
1978 let default_planes = self.engine.get_default_planes().read().await.clone();
1979
1980 let root_imports = import_graph::import_universe(
1981 self,
1982 &ModulePath::Main,
1983 &ModuleRepr::Kcl(program.ast.clone(), None),
1984 &mut universe,
1985 exec_state,
1986 )
1987 .await
1988 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1989
1990 Ok((universe, root_imports))
1991 }
1992
1993 async fn inner_run(
1996 &self,
1997 program: &crate::Program,
1998 exec_state: &mut ExecState,
1999 preserve_mem: PreserveMem,
2000 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
2001 let _stats = crate::log::LogPerfStats::new("Interpretation");
2002
2003 exec_state.set_entry_point_kcl_version(program);
2007
2008 let grid_scale = if self.settings.fixed_size_grid {
2010 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
2011 } else {
2012 GridScaleBehavior::ScaleWithZoom
2013 };
2014 self.engine
2015 .reapply_settings(
2016 &self.engine_batch,
2017 &self.settings,
2018 Default::default(),
2019 exec_state.id_generator(),
2020 grid_scale,
2021 )
2022 .await
2023 .map_err(KclErrorWithOutputs::no_outputs)?;
2024
2025 let default_planes = self.engine.get_default_planes().read().await.clone();
2026 let result = self
2027 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
2028 .await;
2029
2030 crate::log::log(format!(
2031 "Post interpretation KCL memory stats: {:#?}",
2032 exec_state.stack().memory.stats()
2033 ));
2034 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
2035
2036 async fn write_old_memory(
2039 ctx: &ExecutorContext,
2040 exec_state: &ExecState,
2041 env_ref: EnvironmentRef,
2042 ) -> Result<(), KclError> {
2043 if ctx.is_mock() {
2044 return Ok(());
2045 }
2046 let mut stack = exec_state.stack().deep_clone()?;
2047 stack.restore_env(env_ref)?;
2048 let state = cache::SketchModeState {
2049 stack,
2050 module_infos: exec_state.global.module_infos.clone(),
2051 path_to_source_id: exec_state.global.path_to_source_id.clone(),
2052 id_to_source: exec_state.global.id_to_source.clone(),
2053 constraint_state: exec_state.mod_local.constraint_state.clone(),
2054 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
2055 };
2056 cache::write_old_memory(state).await;
2057 Ok(())
2058 }
2059
2060 let env_ref = match result {
2061 Ok(env_ref) => env_ref,
2062 Err((err, env_ref)) => {
2063 if let Some(env_ref) = env_ref {
2066 write_old_memory(self, exec_state, env_ref)
2067 .await
2068 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2069 }
2070 return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
2071 }
2072 };
2073
2074 write_old_memory(self, exec_state, env_ref)
2075 .await
2076 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2077
2078 let session_data = self.engine.get_session_data().await;
2079
2080 Ok((env_ref, session_data))
2081 }
2082
2083 async fn execute_and_build_graph(
2086 &self,
2087 program: NodeRef<'_, crate::parsing::ast::types::Program>,
2088 exec_state: &mut ExecState,
2089 preserve_mem: PreserveMem,
2090 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
2091 let start_op = exec_state.global.root_module_artifacts.operations.len();
2097
2098 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
2099 .await
2100 .map_err(|e| (e, None))?;
2101
2102 let exec_result = self
2103 .exec_module_body(
2104 program,
2105 exec_state,
2106 preserve_mem,
2107 ModuleId::default(),
2108 &ModulePath::Main,
2109 )
2110 .await
2111 .map(
2112 |ModuleExecutionOutcome {
2113 environment: env_ref,
2114 artifacts: module_artifacts,
2115 ..
2116 }| {
2117 exec_state.global.root_module_artifacts.extend(module_artifacts);
2120 env_ref
2121 },
2122 )
2123 .map_err(|(err, env_ref, module_artifacts)| {
2124 if let Some(module_artifacts) = module_artifacts {
2125 exec_state.global.root_module_artifacts.extend(module_artifacts);
2128 }
2129 (err, env_ref)
2130 });
2131
2132 let programs = &exec_state.build_program_lookup(program.clone());
2134 let cached_body_items = exec_state.global.artifacts.cached_body_items();
2135 for op in exec_state
2136 .global
2137 .root_module_artifacts
2138 .operations
2139 .iter_mut()
2140 .skip(start_op)
2141 {
2142 op.fill_node_paths(programs, cached_body_items);
2143 }
2144 for module in exec_state.global.module_infos.values_mut() {
2145 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
2146 for op in &mut outcome.artifacts.operations {
2147 op.fill_node_paths(programs, cached_body_items);
2148 }
2149 }
2150 }
2151
2152 self.engine
2154 .ensure_async_commands_completed(&self.engine_batch)
2155 .await
2156 .map_err(|e| {
2157 match &exec_result {
2158 Ok(env_ref) => (e, Some(*env_ref)),
2159 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
2161 }
2162 })?;
2163
2164 self.engine.clear_queues(&self.engine_batch).await;
2167
2168 match exec_state.build_artifact_graph(&self.engine, program).await {
2169 Ok(_) => exec_result,
2170 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
2171 }
2172 }
2173
2174 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
2178 if exec_state.stack().memory.requires_std() {
2179 let initial_ops = exec_state.mod_local.artifacts.operations.len();
2180
2181 let path = vec!["std".to_owned(), "prelude".to_owned()];
2182 let resolved_path = ModulePath::from_std_import_path(&path)?;
2183 let id = self
2184 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
2185 .await?;
2186 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
2187
2188 exec_state.mut_stack().memory.set_std(module_memory)?;
2189
2190 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
2196 }
2197
2198 Ok(())
2199 }
2200
2201 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
2203 self.engine
2205 .send_modeling_cmd(
2206 &self.engine_batch,
2207 uuid::Uuid::new_v4(),
2208 crate::execution::SourceRange::default(),
2209 &ModelingCmd::from(
2210 mcmd::ZoomToFit::builder()
2211 .object_ids(Default::default())
2212 .animated(false)
2213 .padding(0.1)
2214 .build(),
2215 ),
2216 )
2217 .await
2218 .map_err(KclErrorWithOutputs::no_outputs)?;
2219
2220 let resp = self
2222 .engine
2223 .send_modeling_cmd(
2224 &self.engine_batch,
2225 uuid::Uuid::new_v4(),
2226 crate::execution::SourceRange::default(),
2227 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
2228 )
2229 .await
2230 .map_err(KclErrorWithOutputs::no_outputs)?;
2231
2232 let OkWebSocketResponseData::Modeling {
2233 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
2234 } = resp
2235 else {
2236 return Err(ExecError::BadPng(format!(
2237 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
2238 )));
2239 };
2240 Ok(contents)
2241 }
2242
2243 pub async fn export(
2245 &self,
2246 format: kittycad_modeling_cmds::format::OutputFormat3d,
2247 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2248 let resp = self
2249 .engine
2250 .send_modeling_cmd(
2251 &self.engine_batch,
2252 uuid::Uuid::new_v4(),
2253 crate::SourceRange::default(),
2254 &kittycad_modeling_cmds::ModelingCmd::Export(
2255 kittycad_modeling_cmds::Export::builder()
2256 .entity_ids(vec![])
2257 .format(format)
2258 .build(),
2259 ),
2260 )
2261 .await?;
2262
2263 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
2264 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
2265 format!("Expected Export response, got {resp:?}",),
2266 vec![SourceRange::default()],
2267 )));
2268 };
2269
2270 Ok(files)
2271 }
2272
2273 pub async fn export_step(
2275 &self,
2276 deterministic_time: bool,
2277 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2278 let files = self
2279 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
2280 kittycad_modeling_cmds::format::step::export::Options::builder()
2281 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
2282 .maybe_created(if deterministic_time {
2283 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
2284 KclError::new_internal(crate::errors::KclErrorDetails::new(
2285 format!("Failed to parse date: {e}"),
2286 vec![SourceRange::default()],
2287 ))
2288 })?)
2289 } else {
2290 None
2291 })
2292 .build(),
2293 ))
2294 .await?;
2295
2296 Ok(files)
2297 }
2298
2299 pub async fn close(&self) {
2300 self.engine.close().await;
2301 }
2302}
2303
2304pub use kcl_api::ArtifactId;
2305
2306pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
2307 ArtifactId::new(*id.as_ref())
2308}
2309
2310#[cfg(test)]
2311pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2312 parse_execute_with_project_dir(code, None).await
2313}
2314
2315#[cfg(test)]
2316pub(crate) async fn parse_execute_with_project_dir(
2317 code: &str,
2318 project_directory: Option<TypedPath>,
2319) -> Result<ExecTestResults, KclError> {
2320 parse_execute_with_executor_kind(code, project_directory, machine::ExecutorKind::resolve()).await
2322}
2323
2324#[cfg(test)]
2327pub(crate) fn new_mock_executor_context(
2328 project_directory: Option<TypedPath>,
2329 executor_kind: machine::ExecutorKind,
2330) -> ExecutorContext {
2331 ExecutorContext {
2332 engine: Arc::new(EngineManager::new_mock()),
2333 engine_batch: EngineBatchContext::default(),
2334 fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2335 settings: ExecutorSettings {
2336 project_directory,
2337 ..Default::default()
2338 },
2339 context_type: ContextType::Mock,
2340 execution_callbacks: Default::default(),
2341 executor_kind,
2342 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
2343 }
2344}
2345
2346#[cfg(test)]
2347pub(crate) async fn parse_execute_with_executor_kind(
2348 code: &str,
2349 project_directory: Option<TypedPath>,
2350 executor_kind: machine::ExecutorKind,
2351) -> Result<ExecTestResults, KclError> {
2352 let program = crate::Program::parse_no_errs(code)?;
2353
2354 let exec_ctxt = new_mock_executor_context(project_directory, executor_kind);
2355 let mut exec_state = ExecState::new(&exec_ctxt);
2356 let result = exec_ctxt.run(&program, &mut exec_state).await?;
2357
2358 Ok(ExecTestResults {
2359 program,
2360 mem_env: result.0,
2361 exec_ctxt,
2362 exec_state,
2363 })
2364}
2365
2366#[cfg(test)]
2367#[derive(Debug)]
2368pub(crate) struct ExecTestResults {
2369 program: crate::Program,
2370 mem_env: EnvironmentRef,
2371 exec_ctxt: ExecutorContext,
2372 exec_state: ExecState,
2373}
2374
2375#[cfg(test)]
2376impl ExecTestResults {
2377 pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2378 &self.exec_state.global.root_module_artifacts.commands
2379 }
2380
2381 pub(crate) fn issues(&self) -> &[CompilationIssue] {
2385 self.exec_state.issues()
2386 }
2387
2388 #[track_caller]
2392 pub(crate) fn variable(&self, name: &str) -> KclValue {
2393 self.exec_state
2394 .stack()
2395 .memory
2396 .get_from_unchecked(name, self.mem_env)
2397 .unwrap()
2398 }
2399}
2400
2401pub struct ProgramLookup {
2405 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2406}
2407
2408impl ProgramLookup {
2409 pub fn new(
2412 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2413 module_infos: state::ModuleInfoMap,
2414 ) -> Self {
2415 let mut programs = IndexMap::with_capacity(module_infos.len());
2416 for (id, info) in module_infos {
2417 if let ModuleRepr::Kcl(program, _) = info.repr {
2418 programs.insert(id, program);
2419 }
2420 }
2421 programs.insert(ModuleId::default(), current);
2422 Self { programs }
2423 }
2424
2425 pub fn program_for_module(
2426 &self,
2427 module_id: ModuleId,
2428 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2429 self.programs.get(&module_id)
2430 }
2431}
2432
2433#[cfg(test)]
2434mod tests {
2435 use kcl_api::NumericType;
2436 use pretty_assertions::assert_eq;
2437
2438 use super::*;
2439 use crate::ModuleId;
2440 use crate::errors::KclErrorDetails;
2441 use crate::errors::Severity;
2442 use crate::execution::memory::Stack;
2443 use crate::execution::types::RuntimeType;
2444
2445 macro_rules! kcl_input {
2446 ($file:literal) => {
2447 include_str!(concat!("../../e2e/executor/inputs/", $file, ".kcl"))
2448 };
2449 }
2450
2451 #[test]
2452 fn clone_with_fresh_execution_batch_keeps_executor_selection() {
2453 let mut ctx = new_mock_executor_context(None, machine::ExecutorKind::Machine);
2457 ctx.machine_call_depth_limit = 123;
2458 let cloned = ctx.clone_with_fresh_execution_batch();
2459 assert_eq!(cloned.executor_kind, machine::ExecutorKind::Machine);
2460 assert_eq!(cloned.machine_call_depth_limit, 123);
2461 }
2462
2463 #[tokio::test(flavor = "multi_thread")]
2464 async fn concurrent_foreign_import_preserves_artifact_command() {
2465 let tmpdir = tempfile::TempDir::with_prefix("zma_foreign_import_artifact").unwrap();
2466 tokio::fs::write(tmpdir.path().join("cube.obj"), "o cube\n")
2467 .await
2468 .unwrap();
2469
2470 let program = crate::Program::parse_no_errs("import \"cube.obj\" as cube\n\nmodel = cube\n").unwrap();
2471 let ctx = new_mock_executor_context(
2472 Some(crate::TypedPath(tmpdir.path().into())),
2473 machine::ExecutorKind::resolve(),
2474 );
2475 let mut exec_state = ExecState::new(&ctx);
2476 let (main_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2477 let outcome = exec_state
2478 .into_exec_outcome(main_ref, &ctx)
2479 .await
2480 .expect("foreign import execution should produce an outcome");
2481 ctx.close().await;
2482
2483 let KclValueView::ImportedGeometry(imported) = &outcome.variables["model"] else {
2484 panic!("model should be imported geometry");
2485 };
2486 let artifact_id = ArtifactId::new(imported.id);
2487 let Some(Artifact::ImportedGeometry(artifact)) = outcome.artifact_graph.get(&artifact_id) else {
2488 panic!("foreign import should produce an imported geometry artifact");
2489 };
2490 assert_eq!(artifact.id, artifact_id);
2491 assert!(!artifact.code_ref.node_path.is_empty());
2492 }
2493
2494 #[tokio::test(flavor = "multi_thread")]
2495 async fn nested_import_preserves_inner_error_and_backtrace() {
2496 let project_dir = crate::TypedPath::new("/zma-kcl-import-error");
2501 let main_path = project_dir.join("main.kcl");
2502 let assembly_path = project_dir.join("assembly.kcl");
2503 let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
2504 let files = [
2507 (
2508 project_dir.join("broken.kcl").to_string(),
2509 b"export brokenValue = missingName + 1\n".to_vec(),
2510 ),
2511 (
2512 assembly_path.to_string(),
2513 b"import brokenValue from \"broken.kcl\"\n\nexport assemblyValue = brokenValue\n".to_vec(),
2514 ),
2515 ]
2516 .into_iter()
2517 .collect();
2518 let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
2519 let settings = ExecutorSettings {
2520 project_directory: Some(project_dir),
2521 current_file: Some(main_path.clone()),
2522 ..Default::default()
2523 };
2524 let program = crate::Program::parse_no_errs(main_code).unwrap();
2525
2526 let assert_error = |error: &KclErrorWithOutputs| {
2527 let KclError::UndefinedValue { details, name } = &error.error else {
2528 panic!("expected UndefinedValue, got {:#?}", error.error);
2529 };
2530 assert_eq!(name.as_deref(), Some("missingName"));
2531 assert_eq!(details.message, "`missingName` is not defined");
2532 assert_eq!(
2533 error
2534 .error
2535 .backtrace()
2536 .iter()
2537 .map(|frame| frame.fn_name.as_deref())
2538 .collect::<Vec<_>>(),
2539 [Some("import broken.kcl"), Some("import assembly.kcl"), None]
2540 );
2541 assert_eq!(
2542 error
2543 .error
2544 .backtrace()
2545 .iter()
2546 .map(|frame| frame.kind)
2547 .collect::<Vec<_>>(),
2548 [
2549 kcl_error::BacktraceItemKind::Import,
2550 kcl_error::BacktraceItemKind::Import,
2551 kcl_error::BacktraceItemKind::Call
2552 ]
2553 );
2554
2555 let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
2556 assert!(report.filename.ends_with("broken.kcl"));
2557 assert_eq!(
2558 report
2559 .related
2560 .iter()
2561 .map(|related| related.filename.as_str())
2562 .collect::<Vec<_>>(),
2563 [assembly_path.to_string(), main_path.to_string()]
2564 );
2565
2566 let rendered = format!("{:?}", miette::Report::new(report));
2567 assert!(rendered.contains("broken.kcl"));
2568 assert!(rendered.contains("assembly.kcl"));
2569 assert!(rendered.contains("main.kcl"));
2570 assert!(rendered.contains("export brokenValue = missingName + 1"));
2571 assert!(!rendered.contains("Failed to read contents"));
2572 };
2573
2574 let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
2575 mock_ctx.fs = fs.clone();
2576 let mock_error = mock_ctx
2577 .run_mock(
2578 &program,
2579 &MockConfig {
2580 use_prev_memory: false,
2581 ..Default::default()
2582 },
2583 )
2584 .await
2585 .unwrap_err();
2586 mock_ctx.close().await;
2587 assert_error(&mock_error);
2588
2589 let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
2590 concurrent_ctx.fs = fs;
2591 let mut exec_state = ExecState::new(&concurrent_ctx);
2592 let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
2593 concurrent_ctx.close().await;
2594 assert_error(&concurrent_error);
2595 }
2596
2597 #[tokio::test(flavor = "multi_thread")]
2598 async fn function_error_across_import_keeps_backtrace_innermost_first() {
2599 let project_dir = crate::TypedPath::new("/zma-kcl-import-fn-error");
2603 let main_path = project_dir.join("main.kcl");
2604 let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
2605 let files = [
2606 (
2607 project_dir.join("helper.kcl").to_string(),
2608 b"export fn inner() { return missingName }\nexport fn outer() { return inner() }\n".to_vec(),
2609 ),
2610 (
2611 project_dir.join("assembly.kcl").to_string(),
2612 b"import outer from \"helper.kcl\"\n\nexport assemblyValue = outer()\n".to_vec(),
2613 ),
2614 ]
2615 .into_iter()
2616 .collect();
2617 let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
2618 let settings = ExecutorSettings {
2619 project_directory: Some(project_dir.clone()),
2620 current_file: Some(main_path),
2621 ..Default::default()
2622 };
2623 let program = crate::Program::parse_no_errs(main_code).unwrap();
2624
2625 let assert_error = |error: &KclErrorWithOutputs| {
2626 assert!(
2627 matches!(&error.error, KclError::UndefinedValue { .. }),
2628 "expected UndefinedValue, got {:#?}",
2629 error.error
2630 );
2631 assert_eq!(
2632 error
2633 .error
2634 .backtrace()
2635 .iter()
2636 .map(|frame| frame.fn_name.as_deref())
2637 .collect::<Vec<_>>(),
2638 [Some("inner"), Some("outer"), Some("import assembly.kcl"), None]
2639 );
2640 assert_eq!(
2641 error
2642 .error
2643 .backtrace()
2644 .iter()
2645 .map(|frame| frame.kind)
2646 .collect::<Vec<_>>(),
2647 [
2648 kcl_error::BacktraceItemKind::Call,
2649 kcl_error::BacktraceItemKind::Call,
2650 kcl_error::BacktraceItemKind::Import,
2651 kcl_error::BacktraceItemKind::Call
2652 ]
2653 );
2654
2655 let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
2656 assert!(report.filename.ends_with("helper.kcl"));
2657 assert_eq!(
2658 report
2659 .related
2660 .iter()
2661 .map(|related| related.filename.as_str())
2662 .collect::<Vec<_>>(),
2663 [
2664 project_dir.join("assembly.kcl").to_string(),
2665 project_dir.join("main.kcl").to_string()
2666 ]
2667 );
2668 let rendered = format!("{:?}", miette::Report::new(report));
2669 assert!(rendered.contains("return missingName"));
2670 assert!(!rendered.contains("Failed to read contents"));
2671 };
2672
2673 let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
2674 mock_ctx.fs = fs.clone();
2675 let mock_error = mock_ctx
2676 .run_mock(
2677 &program,
2678 &MockConfig {
2679 use_prev_memory: false,
2680 ..Default::default()
2681 },
2682 )
2683 .await
2684 .unwrap_err();
2685 mock_ctx.close().await;
2686 assert_error(&mock_error);
2687
2688 let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
2689 concurrent_ctx.fs = fs;
2690 let mut exec_state = ExecState::new(&concurrent_ctx);
2691 let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
2692 concurrent_ctx.close().await;
2693 assert_error(&concurrent_error);
2694 }
2695
2696 #[track_caller]
2698 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2699 memory.memory.get_from_unchecked(name, env).unwrap()
2700 }
2701
2702 async fn execute_variables_with_backend(
2703 code: &str,
2704 backend: memory::MemoryBackendKind,
2705 ) -> IndexMap<String, KclValueView> {
2706 execute_outcome_with_backend(code, backend).await.variables
2707 }
2708
2709 async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
2710 let program = crate::Program::parse_no_errs(code).unwrap();
2711 let ctx = ExecutorContext::new_mock(None).await;
2712 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2713 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2714 let outcome = exec_state
2715 .into_exec_outcome(env_ref, &ctx)
2716 .await
2717 .expect("test execution outcome should collect variables");
2718 ctx.close().await;
2719 outcome
2720 }
2721
2722 async fn execute_error_variables_with_backend(
2723 code: &str,
2724 backend: memory::MemoryBackendKind,
2725 ) -> IndexMap<String, KclValueView> {
2726 let program = crate::Program::parse_no_errs(code).unwrap();
2727 let ctx = ExecutorContext::new_mock(None).await;
2728 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2729 let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
2730 ctx.close().await;
2731 error.variables
2732 }
2733
2734 async fn execute_project_variables_with_backend(
2735 main_code: &str,
2736 files: &[(&str, &str)],
2737 backend: memory::MemoryBackendKind,
2738 ) -> IndexMap<String, KclValueView> {
2739 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
2740 for (name, contents) in files {
2741 tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
2742 }
2743
2744 let program = crate::Program::parse_no_errs(main_code).unwrap();
2745 let ctx = ExecutorContext {
2746 engine: Arc::new(EngineManager::new_mock()),
2747 engine_batch: EngineBatchContext::default(),
2748 fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2749 settings: ExecutorSettings {
2750 project_directory: Some(crate::TypedPath(tmpdir.path().into())),
2751 ..Default::default()
2752 },
2753 context_type: ContextType::Mock,
2754 execution_callbacks: Default::default(),
2755 executor_kind: machine::ExecutorKind::resolve(),
2756 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
2757 };
2758 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2759 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2760 let outcome = exec_state
2761 .into_exec_outcome(env_ref, &ctx)
2762 .await
2763 .expect("test execution outcome should collect variables");
2764 ctx.close().await;
2765 outcome.variables
2766 }
2767
2768 async fn run_with_caching_variables_with_backend(
2769 code: &str,
2770 backend: memory::MemoryBackendKind,
2771 ) -> IndexMap<String, KclValueView> {
2772 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2773 cache::bust_cache().await;
2774 clear_mem_cache().await;
2775
2776 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
2777 let program = crate::Program::parse_no_errs(code).unwrap();
2778 ctx.run_with_caching(program.clone()).await.unwrap();
2779 let cached = ctx.run_with_caching(program).await.unwrap();
2780
2781 cache::bust_cache().await;
2782 clear_mem_cache().await;
2783 ctx.close().await;
2784 cached.variables
2785 }
2786
2787 async fn run_mock_variables_with_backend(
2788 code: &str,
2789 backend: memory::MemoryBackendKind,
2790 ) -> IndexMap<String, KclValueView> {
2791 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2792 clear_mem_cache().await;
2793
2794 let ctx = ExecutorContext::new_mock(None).await;
2795 let first = crate::Program::parse_no_errs("x = 2").unwrap();
2796 ctx.run_mock(
2797 &first,
2798 &MockConfig {
2799 use_prev_memory: false,
2800 ..Default::default()
2801 },
2802 )
2803 .await
2804 .unwrap();
2805
2806 let program = crate::Program::parse_no_errs(code).unwrap();
2807 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
2808
2809 clear_mem_cache().await;
2810 ctx.close().await;
2811 outcome.variables
2812 }
2813
2814 fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
2815 let mut keys = variables.keys().cloned().collect::<Vec<_>>();
2816 keys.sort();
2817 keys
2818 }
2819
2820 async fn collect_backend_results<T, Fut>(
2821 mut run: impl FnMut(memory::MemoryBackendKind) -> Fut,
2822 ) -> Vec<(memory::MemoryBackendKind, T)>
2823 where
2824 Fut: std::future::Future<Output = T>,
2825 {
2826 let all = memory::MemoryBackendKind::all();
2827 let mut results = Vec::with_capacity(all.len());
2828 for &kind in all {
2829 results.push((kind, run(kind).await));
2830 }
2831 results
2832 }
2833
2834 fn assert_backend_results_match<T>(results: &[(memory::MemoryBackendKind, T)])
2835 where
2836 T: std::fmt::Debug + PartialEq,
2837 {
2838 let (first, rest) = results.split_first().expect("expected at least one memory backend");
2839 let (first_kind, first_result) = first;
2840 for (kind, result) in rest {
2841 assert_eq!(
2842 result, first_result,
2843 "memory kind {kind:?} doesn't match {first_kind:?}"
2844 );
2845 }
2846 }
2847
2848 fn assert_backend_variable_results_match_expected_keys(
2849 results: &[(memory::MemoryBackendKind, IndexMap<String, KclValueView>)],
2850 expected_keys: &[&str],
2851 ) {
2852 let (first_kind, first_variables) = results.first().expect("expected at least one memory backend");
2853 let expected_keys = expected_keys.iter().map(|key| (*key).to_owned()).collect::<Vec<_>>();
2854 assert_eq!(
2855 sorted_variable_keys(first_variables),
2856 expected_keys,
2857 "memory kind {first_kind:?} doesn't match expected variables"
2858 );
2859 assert_backend_results_match(results);
2860 }
2861
2862 fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
2863 let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
2864 let KclValueView::Number { value, .. } = value else {
2865 panic!("expected `{key}` to be a number, got {value:?}");
2866 };
2867 assert_eq!(*value, expected, "{key}: {value:?}");
2868 }
2869
2870 #[tokio::test(flavor = "multi_thread")]
2871 async fn exec_outcome_variables_match_between_memory_backends() {
2872 let code = "x = 2\ny = x + 1\narr = [x, y]";
2873
2874 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2875
2876 assert_backend_variable_results_match_expected_keys(&results, &["arr", "x", "y"]);
2877 }
2878
2879 #[tokio::test(flavor = "multi_thread")]
2880 async fn error_output_variables_match_between_memory_backends() {
2881 let code = "x = 2\ny = missing + 1";
2882
2883 let results = collect_backend_results(|kind| execute_error_variables_with_backend(code, kind)).await;
2884
2885 assert_backend_variable_results_match_expected_keys(&results, &["x"]);
2886 }
2887
2888 #[tokio::test(flavor = "multi_thread")]
2889 async fn cached_execution_variables_match_between_memory_backends() {
2890 let code = "x = 2\ny = x + 1";
2891
2892 let results = collect_backend_results(|kind| run_with_caching_variables_with_backend(code, kind)).await;
2893
2894 assert_backend_variable_results_match_expected_keys(&results, &["x", "y"]);
2895 }
2896
2897 #[tokio::test(flavor = "multi_thread")]
2898 async fn mock_execution_variables_match_between_memory_backends() {
2899 let code = "y = x + 1";
2900
2901 let results = collect_backend_results(|kind| run_mock_variables_with_backend(code, kind)).await;
2902
2903 assert_backend_variable_results_match_expected_keys(&results, &["y"]);
2904 }
2905
2906 #[tokio::test(flavor = "multi_thread")]
2907 async fn module_imports_and_exported_closures_match_between_memory_backends() {
2908 let module_code = r#"
2909export base = 40
2910
2911export fn addBase(n) {
2912 return n + base
2913}
2914"#;
2915 let main_code = r#"
2916import base, addBase from 'math.kcl'
2917import 'math.kcl'
2918
2919named = addBase(n = 2)
2920qualified = math::addBase(n = 1)
2921direct = math::base
2922"#;
2923
2924 let files = [("math.kcl", module_code)];
2925 let results =
2926 collect_backend_results(|kind| execute_project_variables_with_backend(main_code, &files, kind)).await;
2927
2928 let (_, first_variables) = results.first().expect("expected at least one memory backend");
2929 assert_number_variable(first_variables, "named", 42.0);
2930 assert_number_variable(first_variables, "qualified", 41.0);
2931 assert_number_variable(first_variables, "direct", 40.0);
2932 assert_backend_results_match(&results);
2933 }
2934
2935 #[tokio::test(flavor = "multi_thread")]
2936 async fn sketch_block_variables_match_between_memory_backends() {
2937 let code = r#"
2938sketch001 = sketch(on = XY) {
2939 line1 = line(start = [0, 0], end = [1, 0])
2940 line2 = line(start = [1, 0], end = [0, 1])
2941}
2942lineCount = 2
2943"#;
2944
2945 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2946
2947 let (_, first_variables) = results.first().expect("expected at least one memory backend");
2948 assert!(first_variables.contains_key("sketch001"), "actual: {first_variables:?}");
2949 assert_number_variable(first_variables, "lineCount", 2.0);
2950 assert_backend_results_match(&results);
2951 }
2952
2953 #[tokio::test(flavor = "multi_thread")]
2954 async fn tag_call_stack_lookup_matches_between_memory_backends() {
2955 let code = r#"
2956sketch001 = startSketchOn(XY)
2957 |> startProfile(at = [0, 0])
2958 |> xLine(length = 10, tag = $seg01)
2959
2960segLength = segLen(seg01)
2961"#;
2962
2963 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2964
2965 let (_, first_variables) = results.first().expect("expected at least one memory backend");
2966 assert_number_variable(first_variables, "segLength", 10.0);
2967 assert_backend_results_match(&results);
2968 }
2969
2970 #[tokio::test(flavor = "multi_thread")]
2971 async fn test_execute_warn() {
2972 let text = "@blah";
2973 let result = parse_execute(text).await.unwrap();
2974 let errs = result.exec_state.issues();
2975 assert_eq!(errs.len(), 1);
2976 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2977 assert!(
2978 errs[0].message.contains("Unknown annotation"),
2979 "unexpected warning message: {}",
2980 errs[0].message
2981 );
2982 }
2983
2984 #[tokio::test(flavor = "multi_thread")]
2985 async fn test_execute_fn_definitions() {
2986 let ast = r#"fn def(@x) {
2987 return x
2988}
2989fn ghi(@x) {
2990 return x
2991}
2992fn jkl(@x) {
2993 return x
2994}
2995fn hmm(@x) {
2996 return x
2997}
2998
2999yo = 5 + 6
3000
3001abc = 3
3002identifierGuy = 5
3003part001 = startSketchOn(XY)
3004|> startProfile(at = [-1.2, 4.83])
3005|> line(end = [2.8, 0])
3006|> angledLine(angle = 100 + 100, length = 3.01)
3007|> angledLine(angle = abc, length = 3.02)
3008|> angledLine(angle = def(yo), length = 3.03)
3009|> angledLine(angle = ghi(2), length = 3.04)
3010|> angledLine(angle = jkl(yo) + 2, length = 3.05)
3011|> close()
3012yo2 = hmm([identifierGuy + 5])"#;
3013
3014 parse_execute(ast).await.unwrap();
3015 }
3016
3017 #[tokio::test(flavor = "multi_thread")]
3018 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
3019 let code = r#"
3020firstProfile = sketch(on = XY) {
3021 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
3022 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
3023 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
3024 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
3025 coincident([edge1.end, edge2.start])
3026 coincident([edge2.end, edge3.start])
3027 coincident([edge3.end, edge4.start])
3028 coincident([edge4.end, edge1.start])
3029}
3030
3031secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
3032 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
3033 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
3034 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
3035 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
3036 coincident([edge5.end, edge6.start])
3037 coincident([edge6.end, edge7.start])
3038 coincident([edge7.end, edge8.start])
3039 coincident([edge8.end, edge5.start])
3040}
3041
3042firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
3043secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
3044"#;
3045
3046 let result = parse_execute(code).await.unwrap();
3047 assert!(result.exec_state.issues().is_empty());
3048 }
3049
3050 #[tokio::test(flavor = "multi_thread")]
3051 async fn sketch_block_artifact_preserves_standard_plane_name() {
3052 let code = r#"
3053sketch001 = sketch(on = -YZ) {
3054 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
3055}
3056"#;
3057
3058 let result = parse_execute(code).await.unwrap();
3059 let sketch_blocks = result
3060 .exec_state
3061 .global
3062 .artifacts
3063 .graph
3064 .values()
3065 .filter_map(|artifact| match artifact {
3066 Artifact::SketchBlock(block) => Some(block),
3067 _ => None,
3068 })
3069 .collect::<Vec<_>>();
3070
3071 assert_eq!(sketch_blocks.len(), 1);
3072 assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
3073 }
3074
3075 #[tokio::test(flavor = "multi_thread")]
3076 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
3077 let code = r#"
3078sketch001 = sketch(on = YZ) {
3079 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
3080 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
3081 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
3082 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
3083 coincident([line1.end, line2.start])
3084 coincident([line2.end, line3.start])
3085 coincident([line3.end, line4.start])
3086 coincident([line4.end, line1.start])
3087}
3088
3089sketch002 = sketch(on = -XZ) {
3090 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
3091 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
3092 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
3093 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
3094 coincident([line5.end, line6.start])
3095 coincident([line6.end, line7.start])
3096 coincident([line7.end, line8.start])
3097 coincident([line8.end, line5.start])
3098}
3099
3100region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
3101extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
3102region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
3103extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
3104
3105myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
3106"#;
3107
3108 let result = parse_execute(code).await.unwrap();
3109 assert!(result.exec_state.issues().is_empty());
3110 }
3111
3112 #[tokio::test(flavor = "multi_thread")]
3113 async fn issue_10741_point_circle_coincident_executes() {
3114 let code = r#"
3115sketch001 = sketch(on = YZ) {
3116 circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
3117 line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
3118 coincident([line1.start, circle1])
3119}
3120"#;
3121
3122 let result = parse_execute(code).await.unwrap();
3123 assert!(
3124 result
3125 .exec_state
3126 .issues()
3127 .iter()
3128 .all(|issue| issue.severity != Severity::Error),
3129 "unexpected execution issues: {:#?}",
3130 result.exec_state.issues()
3131 );
3132 }
3133
3134 #[tokio::test(flavor = "multi_thread")]
3135 async fn test_execute_with_pipe_substitutions_unary() {
3136 let ast = r#"myVar = 3
3137part001 = startSketchOn(XY)
3138 |> startProfile(at = [0, 0])
3139 |> line(end = [3, 4], tag = $seg01)
3140 |> line(end = [
3141 min([segLen(seg01), myVar]),
3142 -legLen(hypotenuse = segLen(seg01), leg = myVar)
3143])
3144"#;
3145
3146 parse_execute(ast).await.unwrap();
3147 }
3148
3149 #[tokio::test(flavor = "multi_thread")]
3150 async fn test_execute_with_pipe_substitutions() {
3151 let ast = r#"myVar = 3
3152part001 = startSketchOn(XY)
3153 |> startProfile(at = [0, 0])
3154 |> line(end = [3, 4], tag = $seg01)
3155 |> line(end = [
3156 min([segLen(seg01), myVar]),
3157 legLen(hypotenuse = segLen(seg01), leg = myVar)
3158])
3159"#;
3160
3161 parse_execute(ast).await.unwrap();
3162 }
3163
3164 #[tokio::test(flavor = "multi_thread")]
3165 async fn test_execute_with_inline_comment() {
3166 let ast = r#"baseThick = 1
3167armAngle = 60
3168
3169baseThickHalf = baseThick / 2
3170halfArmAngle = armAngle / 2
3171
3172arrExpShouldNotBeIncluded = [1, 2, 3]
3173objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
3174
3175part001 = startSketchOn(XY)
3176 |> startProfile(at = [0, 0])
3177 |> yLine(endAbsolute = 1)
3178 |> xLine(length = 3.84) // selection-range-7ish-before-this
3179
3180variableBelowShouldNotBeIncluded = 3
3181"#;
3182
3183 parse_execute(ast).await.unwrap();
3184 }
3185
3186 #[tokio::test(flavor = "multi_thread")]
3187 async fn test_execute_with_function_literal_in_pipe() {
3188 let ast = r#"w = 20
3189l = 8
3190h = 10
3191
3192fn thing() {
3193 return -8
3194}
3195
3196firstExtrude = startSketchOn(XY)
3197 |> startProfile(at = [0,0])
3198 |> line(end = [0, l])
3199 |> line(end = [w, 0])
3200 |> line(end = [0, thing()])
3201 |> close()
3202 |> extrude(length = h)"#;
3203
3204 parse_execute(ast).await.unwrap();
3205 }
3206
3207 #[tokio::test(flavor = "multi_thread")]
3208 async fn test_execute_with_function_unary_in_pipe() {
3209 let ast = r#"w = 20
3210l = 8
3211h = 10
3212
3213fn thing(@x) {
3214 return -x
3215}
3216
3217firstExtrude = startSketchOn(XY)
3218 |> startProfile(at = [0,0])
3219 |> line(end = [0, l])
3220 |> line(end = [w, 0])
3221 |> line(end = [0, thing(8)])
3222 |> close()
3223 |> extrude(length = h)"#;
3224
3225 parse_execute(ast).await.unwrap();
3226 }
3227
3228 #[tokio::test(flavor = "multi_thread")]
3229 async fn test_execute_with_function_array_in_pipe() {
3230 let ast = r#"w = 20
3231l = 8
3232h = 10
3233
3234fn thing(@x) {
3235 return [0, -x]
3236}
3237
3238firstExtrude = startSketchOn(XY)
3239 |> startProfile(at = [0,0])
3240 |> line(end = [0, l])
3241 |> line(end = [w, 0])
3242 |> line(end = thing(8))
3243 |> close()
3244 |> extrude(length = h)"#;
3245
3246 parse_execute(ast).await.unwrap();
3247 }
3248
3249 #[tokio::test(flavor = "multi_thread")]
3250 async fn test_execute_with_function_call_in_pipe() {
3251 let ast = r#"w = 20
3252l = 8
3253h = 10
3254
3255fn other_thing(@y) {
3256 return -y
3257}
3258
3259fn thing(@x) {
3260 return other_thing(x)
3261}
3262
3263firstExtrude = startSketchOn(XY)
3264 |> startProfile(at = [0,0])
3265 |> line(end = [0, l])
3266 |> line(end = [w, 0])
3267 |> line(end = [0, thing(8)])
3268 |> close()
3269 |> extrude(length = h)"#;
3270
3271 parse_execute(ast).await.unwrap();
3272 }
3273
3274 #[tokio::test(flavor = "multi_thread")]
3275 async fn test_execute_with_function_sketch() {
3276 let ast = r#"fn box(h, l, w) {
3277 myBox = startSketchOn(XY)
3278 |> startProfile(at = [0,0])
3279 |> line(end = [0, l])
3280 |> line(end = [w, 0])
3281 |> line(end = [0, -l])
3282 |> close()
3283 |> extrude(length = h)
3284
3285 return myBox
3286}
3287
3288fnBox = box(h = 3, l = 6, w = 10)"#;
3289
3290 parse_execute(ast).await.unwrap();
3291 }
3292
3293 #[tokio::test(flavor = "multi_thread")]
3294 async fn test_get_member_of_object_with_function_period() {
3295 let ast = r#"fn box(@obj) {
3296 myBox = startSketchOn(XY)
3297 |> startProfile(at = obj.start)
3298 |> line(end = [0, obj.l])
3299 |> line(end = [obj.w, 0])
3300 |> line(end = [0, -obj.l])
3301 |> close()
3302 |> extrude(length = obj.h)
3303
3304 return myBox
3305}
3306
3307thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
3308"#;
3309 parse_execute(ast).await.unwrap();
3310 }
3311
3312 #[tokio::test(flavor = "multi_thread")]
3313 #[ignore] async fn test_object_member_starting_pipeline() {
3315 let ast = r#"
3316fn test2() {
3317 return {
3318 thing: startSketchOn(XY)
3319 |> startProfile(at = [0, 0])
3320 |> line(end = [0, 1])
3321 |> line(end = [1, 0])
3322 |> line(end = [0, -1])
3323 |> close()
3324 }
3325}
3326
3327x2 = test2()
3328
3329x2.thing
3330 |> extrude(length = 10)
3331"#;
3332 parse_execute(ast).await.unwrap();
3333 }
3334
3335 #[tokio::test(flavor = "multi_thread")]
3336 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
3338 let ast = r#"fn box(obj) {
3339let myBox = startSketchOn(XY)
3340 |> startProfile(at = obj.start)
3341 |> line(end = [0, obj.l])
3342 |> line(end = [obj.w, 0])
3343 |> line(end = [0, -obj.l])
3344 |> close()
3345 |> extrude(length = obj.h)
3346
3347 return myBox
3348}
3349
3350for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
3351 thisBox = box(var)
3352}"#;
3353
3354 parse_execute(ast).await.unwrap();
3355 }
3356
3357 #[tokio::test(flavor = "multi_thread")]
3358 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
3360 let ast = r#"fn box(h, l, w, start) {
3361 myBox = startSketchOn(XY)
3362 |> startProfile(at = [0,0])
3363 |> line(end = [0, l])
3364 |> line(end = [w, 0])
3365 |> line(end = [0, -l])
3366 |> close()
3367 |> extrude(length = h)
3368
3369 return myBox
3370}
3371
3372
3373for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
3374 const thisBox = box(var[0], var[1], var[2], var[3])
3375}"#;
3376
3377 parse_execute(ast).await.unwrap();
3378 }
3379
3380 #[tokio::test(flavor = "multi_thread")]
3381 async fn test_get_member_of_array_with_function() {
3382 let ast = r#"fn box(@arr) {
3383 myBox =startSketchOn(XY)
3384 |> startProfile(at = arr[0])
3385 |> line(end = [0, arr[1]])
3386 |> line(end = [arr[2], 0])
3387 |> line(end = [0, -arr[1]])
3388 |> close()
3389 |> extrude(length = arr[3])
3390
3391 return myBox
3392}
3393
3394thisBox = box([[0,0], 6, 10, 3])
3395
3396"#;
3397 parse_execute(ast).await.unwrap();
3398 }
3399
3400 #[tokio::test(flavor = "multi_thread")]
3401 async fn test_function_cannot_access_future_definitions() {
3402 let ast = r#"
3403fn returnX() {
3404 // x shouldn't be defined yet.
3405 return x
3406}
3407
3408x = 5
3409
3410answer = returnX()"#;
3411
3412 let result = parse_execute(ast).await;
3413 let err = result.unwrap_err();
3414 assert_eq!(err.message(), "`x` is not defined");
3415 }
3416
3417 #[tokio::test(flavor = "multi_thread")]
3418 async fn test_override_prelude() {
3419 let text = "PI = 3.0";
3420 let result = parse_execute(text).await.unwrap();
3421 let issues = result.exec_state.issues();
3422 assert!(issues.is_empty(), "issues={issues:#?}");
3423 }
3424
3425 #[tokio::test(flavor = "multi_thread")]
3426 async fn type_aliases() {
3427 let text = r#"@settings(experimentalFeatures = allow)
3428type MyTy = [number; 2]
3429fn foo(@x: MyTy) {
3430 return x[0]
3431}
3432
3433foo([0, 1])
3434
3435type Other = MyTy | Helix
3436"#;
3437 let result = parse_execute(text).await.unwrap();
3438 let issues = result.exec_state.issues();
3439 assert!(issues.is_empty(), "issues={issues:#?}");
3440 }
3441
3442 #[tokio::test(flavor = "multi_thread")]
3443 async fn test_cannot_shebang_in_fn() {
3444 let ast = r#"
3445fn foo() {
3446 #!hello
3447 return true
3448}
3449
3450foo
3451"#;
3452
3453 let result = parse_execute(ast).await;
3454 let err = result.unwrap_err();
3455 assert_eq!(
3456 err,
3457 KclError::new_syntax(KclErrorDetails::new(
3458 "Unexpected token: #".to_owned(),
3459 vec![SourceRange::new(14, 15, ModuleId::default())],
3460 )),
3461 );
3462 }
3463
3464 #[tokio::test(flavor = "multi_thread")]
3465 async fn test_pattern_transform_function_cannot_access_future_definitions() {
3466 let ast = r#"
3467fn transform(@replicaId) {
3468 // x shouldn't be defined yet.
3469 scale = x
3470 return {
3471 translate = [0, 0, replicaId * 10],
3472 scale = [scale, 1, 0],
3473 }
3474}
3475
3476fn layer() {
3477 return startSketchOn(XY)
3478 |> circle( center= [0, 0], radius= 1, tag = $tag1)
3479 |> extrude(length = 10)
3480}
3481
3482x = 5
3483
3484// The 10 layers are replicas of each other, with a transform applied to each.
3485shape = layer() |> patternTransform(instances = 10, transform = transform)
3486"#;
3487
3488 let result = parse_execute(ast).await;
3489 let err = result.unwrap_err();
3490 assert_eq!(err.message(), "`x` is not defined",);
3491 }
3492
3493 #[tokio::test(flavor = "multi_thread")]
3496 async fn test_math_execute_with_functions() {
3497 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
3498 let result = parse_execute(ast).await.unwrap();
3499 assert_eq!(
3500 5.0,
3501 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3502 .as_f64()
3503 .unwrap()
3504 );
3505 }
3506
3507 #[tokio::test(flavor = "multi_thread")]
3508 async fn test_math_execute() {
3509 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
3510 let result = parse_execute(ast).await.unwrap();
3511 assert_eq!(
3512 7.4,
3513 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3514 .as_f64()
3515 .unwrap()
3516 );
3517 }
3518
3519 #[tokio::test(flavor = "multi_thread")]
3520 async fn test_string_uppercase() {
3521 let composed = "\u{e9}";
3522 let uppercase_composed = "\u{c9}";
3523 let decomposed = "e\u{301}";
3524 let uppercase_decomposed = "E\u{301}";
3525 let code = format!(
3526 r#"
3527ascii = string::uppercase("Kcl")
3528unicode_expansion = string::uppercase("Straße")
3529uncased = string::uppercase("東京")
3530empty = string::uppercase("")
3531composed = string::uppercase("{composed}")
3532decomposed = string::uppercase("{decomposed}")
3533piped = "ready" |> string::uppercase()
3534"#
3535 );
3536
3537 let result = parse_execute(&code).await.unwrap();
3538 for (name, expected) in [
3539 ("ascii", "KCL"),
3540 ("unicode_expansion", "STRASSE"),
3541 ("uncased", "東京"),
3542 ("empty", ""),
3543 ("composed", uppercase_composed),
3544 ("decomposed", uppercase_decomposed),
3545 ("piped", "READY"),
3546 ] {
3547 assert_eq!(
3548 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3549 .as_str()
3550 .unwrap(),
3551 expected,
3552 "{name}"
3553 );
3554 }
3555 }
3556
3557 #[tokio::test(flavor = "multi_thread")]
3558 async fn test_string_lowercase() {
3559 let composed = "\u{c9}";
3560 let lowercase_composed = "\u{e9}";
3561 let decomposed = "E\u{301}";
3562 let lowercase_decomposed = "e\u{301}";
3563 let expanded = "i\u{307}";
3564 let code = format!(
3565 r#"
3566ascii = string::lowercase("KCL")
3567final_sigma = string::lowercase("ΟΣ")
3568medial_sigma = string::lowercase("ΟΣΑ")
3569unicode_expansion = string::lowercase("İ")
3570uncased = string::lowercase("東京")
3571empty = string::lowercase("")
3572composed = string::lowercase("{composed}")
3573decomposed = string::lowercase("{decomposed}")
3574piped = "READY" |> string::lowercase()
3575"#
3576 );
3577
3578 let result = parse_execute(&code).await.unwrap();
3579 for (name, expected) in [
3580 ("ascii", "kcl"),
3581 ("final_sigma", "ος"),
3582 ("medial_sigma", "οσα"),
3583 ("unicode_expansion", expanded),
3584 ("uncased", "東京"),
3585 ("empty", ""),
3586 ("composed", lowercase_composed),
3587 ("decomposed", lowercase_decomposed),
3588 ("piped", "ready"),
3589 ] {
3590 assert_eq!(
3591 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3592 .as_str()
3593 .unwrap(),
3594 expected,
3595 "{name}"
3596 );
3597 }
3598 }
3599
3600 #[tokio::test(flavor = "multi_thread")]
3601 async fn test_string_is_equal() {
3602 let composed = "\u{e9}";
3603 let decomposed = "e\u{301}";
3604 let code = format!(
3605 r#"
3606exact_same = string::isEqual("KCL", to = "KCL")
3607exact_different_case = string::isEqual("KCL", to = "kcl")
3608explicit_case_sensitive = string::isEqual("KCL", to = "kcl", caseInsensitive = false)
3609case_insensitive_ascii = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3610case_fold_expansion = string::isEqual("Straße", to = "STRASSE", caseInsensitive = true)
3611case_fold_expansion_reversed = string::isEqual("STRASSE", to = "Straße", caseInsensitive = true)
3612case_fold_sigma = string::isEqual("ος", to = "οσ", caseInsensitive = true)
3613case_fold_non_turkic = string::isEqual("I", to = "i", caseInsensitive = true)
3614case_fold_not_turkic = string::isEqual("I", to = "ı", caseInsensitive = true)
3615empty_same = string::isEqual("", to = "")
3616empty_different = string::isEqual("", to = "KCL")
3617exact_without_normalization = string::isEqual("{composed}", to = "{decomposed}")
3618case_fold_without_normalization = string::isEqual("{composed}", to = "{decomposed}", caseInsensitive = true)
3619piped = "ready" |> string::isEqual(to = "READY", caseInsensitive = true)
3620"#
3621 );
3622
3623 let result = parse_execute(&code).await.unwrap();
3624 for (name, expected) in [
3625 ("exact_same", true),
3626 ("exact_different_case", false),
3627 ("explicit_case_sensitive", false),
3628 ("case_insensitive_ascii", true),
3629 ("case_fold_expansion", true),
3630 ("case_fold_expansion_reversed", true),
3631 ("case_fold_sigma", true),
3632 ("case_fold_non_turkic", true),
3633 ("case_fold_not_turkic", false),
3634 ("empty_same", true),
3635 ("empty_different", false),
3636 ("exact_without_normalization", false),
3637 ("case_fold_without_normalization", false),
3638 ("piped", true),
3639 ] {
3640 assert_eq!(
3641 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3642 .as_bool()
3643 .unwrap(),
3644 expected,
3645 "{name}"
3646 );
3647 }
3648 }
3649
3650 #[tokio::test(flavor = "multi_thread")]
3651 async fn test_string_is_equal_inside_sketch_block_is_predicate() {
3652 let code = r#"
3653@settings(experimentalFeatures = allow)
3654
3655sketch(on = XY) {
3656 stringsAreEqual = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3657}
3658"#;
3659
3660 parse_execute(code).await.unwrap();
3661 }
3662
3663 #[tokio::test(flavor = "multi_thread")]
3664 async fn test_string_trim() {
3665 let ascii_whitespace = " \t\n";
3666 let tab = "\t";
3667 let non_breaking_space = "\u{a0}";
3668 let em_space = "\u{2003}";
3669 let ideographic_space = "\u{3000}";
3670 let zero_width_space = "\u{200b}";
3671 let decomposed = "e\u{301}";
3672 let code = format!(
3673 r#"
3674ascii = string::trim("{ascii_whitespace}KCL{ascii_whitespace}")
3675internal = string::trim(" KCL{tab}strings ")
3676unicode = string::trim("{non_breaking_space}{em_space}KCL{ideographic_space}")
3677all_whitespace = string::trim("{ascii_whitespace}{non_breaking_space}")
3678empty = string::trim("")
3679unchanged = string::trim("KCL")
3680without_normalization = string::trim(" {decomposed} ")
3681non_whitespace = string::trim("{zero_width_space}KCL{zero_width_space}")
3682piped = " ready " |> string::trim()
3683"#
3684 );
3685
3686 let result = parse_execute(&code).await.unwrap();
3687 let non_whitespace = format!("{zero_width_space}KCL{zero_width_space}");
3688 for (name, expected) in [
3689 ("ascii", "KCL"),
3690 ("internal", "KCL\tstrings"),
3691 ("unicode", "KCL"),
3692 ("all_whitespace", ""),
3693 ("empty", ""),
3694 ("unchanged", "KCL"),
3695 ("without_normalization", decomposed),
3696 ("non_whitespace", non_whitespace.as_str()),
3697 ("piped", "ready"),
3698 ] {
3699 assert_eq!(
3700 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3701 .as_str()
3702 .unwrap(),
3703 expected,
3704 "{name}"
3705 );
3706 }
3707 }
3708
3709 #[tokio::test(flavor = "multi_thread")]
3710 async fn test_string_trim_start() {
3711 let ascii_whitespace = " \t\n";
3712 let tab = "\t";
3713 let non_breaking_space = "\u{a0}";
3714 let em_space = "\u{2003}";
3715 let ideographic_space = "\u{3000}";
3716 let zero_width_space = "\u{200b}";
3717 let decomposed = "e\u{301}";
3718 let code = format!(
3719 r#"
3720ascii = string::trimStart("{ascii_whitespace}KCL{ascii_whitespace}")
3721internal = string::trimStart(" KCL{tab}strings")
3722unicode = string::trimStart("{non_breaking_space}{em_space}KCL{ideographic_space}")
3723all_whitespace = string::trimStart("{ascii_whitespace}{non_breaking_space}")
3724empty = string::trimStart("")
3725unchanged = string::trimStart("KCL")
3726without_normalization = string::trimStart(" {decomposed}")
3727non_whitespace_prefix = string::trimStart("{zero_width_space}{ascii_whitespace}KCL")
3728piped = " ready " |> string::trimStart()
3729"#
3730 );
3731
3732 let result = parse_execute(&code).await.unwrap();
3733 let ascii = format!("KCL{ascii_whitespace}");
3734 let unicode = format!("KCL{ideographic_space}");
3735 let non_whitespace_prefix = format!("{zero_width_space}{ascii_whitespace}KCL");
3736 for (name, expected) in [
3737 ("ascii", ascii.as_str()),
3738 ("internal", "KCL\tstrings"),
3739 ("unicode", unicode.as_str()),
3740 ("all_whitespace", ""),
3741 ("empty", ""),
3742 ("unchanged", "KCL"),
3743 ("without_normalization", decomposed),
3744 ("non_whitespace_prefix", non_whitespace_prefix.as_str()),
3745 ("piped", "ready "),
3746 ] {
3747 assert_eq!(
3748 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3749 .as_str()
3750 .unwrap(),
3751 expected,
3752 "{name}"
3753 );
3754 }
3755 }
3756
3757 #[tokio::test(flavor = "multi_thread")]
3758 async fn test_string_trim_end() {
3759 let ascii_whitespace = " \t\n";
3760 let tab = "\t";
3761 let non_breaking_space = "\u{a0}";
3762 let em_space = "\u{2003}";
3763 let ideographic_space = "\u{3000}";
3764 let zero_width_space = "\u{200b}";
3765 let decomposed = "e\u{301}";
3766 let code = format!(
3767 r#"
3768ascii = string::trimEnd("{ascii_whitespace}KCL{ascii_whitespace}")
3769internal = string::trimEnd("KCL{tab}strings ")
3770unicode = string::trimEnd("{non_breaking_space}KCL{em_space}{ideographic_space}")
3771all_whitespace = string::trimEnd("{ascii_whitespace}{non_breaking_space}")
3772empty = string::trimEnd("")
3773unchanged = string::trimEnd("KCL")
3774without_normalization = string::trimEnd("{decomposed} ")
3775non_whitespace_suffix = string::trimEnd("KCL{ascii_whitespace}{zero_width_space}")
3776piped = " ready " |> string::trimEnd()
3777"#
3778 );
3779
3780 let result = parse_execute(&code).await.unwrap();
3781 let ascii = format!("{ascii_whitespace}KCL");
3782 let unicode = format!("{non_breaking_space}KCL");
3783 let non_whitespace_suffix = format!("KCL{ascii_whitespace}{zero_width_space}");
3784 for (name, expected) in [
3785 ("ascii", ascii.as_str()),
3786 ("internal", "KCL\tstrings"),
3787 ("unicode", unicode.as_str()),
3788 ("all_whitespace", ""),
3789 ("empty", ""),
3790 ("unchanged", "KCL"),
3791 ("without_normalization", decomposed),
3792 ("non_whitespace_suffix", non_whitespace_suffix.as_str()),
3793 ("piped", " ready"),
3794 ] {
3795 assert_eq!(
3796 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3797 .as_str()
3798 .unwrap(),
3799 expected,
3800 "{name}"
3801 );
3802 }
3803 }
3804
3805 #[tokio::test(flavor = "multi_thread")]
3806 async fn test_string_to_string() {
3807 for (name, expr, expected) in [
3810 ("unitless integer", "12", "12"),
3813 ("unitless fractional", "1.5", "1.5"),
3814 ("no digits dropped", "0.1 + 0.2", "0.30000000000000004"),
3815 ("unitless negative", "-7", "-7"),
3816 ("unitless zero", "0", "0"),
3817 ("negative zero", "-0", "0"),
3818 ("count", "3_", "3_"),
3819 ("millimeters", "12mm", "12mm"),
3820 ("centimeters", "12cm", "12cm"),
3821 ("meters", "12m", "12m"),
3822 ("inches", "1.5in", "1.5in"),
3823 ("feet", "2ft", "2ft"),
3824 ("yards", "3yd", "3yd"),
3825 ("degrees", "90deg", "90deg"),
3826 ("radians", "1.5rad", "1.5rad"),
3827 ("length arithmetic", "2mm + 10mm", "12mm"),
3829 ("units the type system loses", "2mm * 10mm", "20"),
3832 ("unitless arithmetic", "1 + 2", "3"),
3833 ] {
3834 let code = format!("actual = string::toString({expr})");
3835 let result = parse_execute(&code).await.unwrap();
3836
3837 assert_eq!(
3838 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3839 .as_str()
3840 .unwrap(),
3841 expected,
3842 "case: {name}"
3843 );
3844 }
3845 }
3846
3847 #[tokio::test(flavor = "multi_thread")]
3848 async fn test_string_to_string_ignores_the_files_default_unit() {
3849 let code = "@settings(defaultLengthUnit = inch)\nactual = string::toString(12)";
3854 let result = parse_execute(code).await.unwrap();
3855
3856 assert_eq!(
3857 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3858 .as_str()
3859 .unwrap(),
3860 "12"
3861 );
3862 }
3863
3864 #[tokio::test(flavor = "multi_thread")]
3865 async fn test_string_to_string_rejects_a_non_number() {
3866 let error = parse_execute(r#"actual = string::toString("already text")"#)
3867 .await
3868 .unwrap_err();
3869
3870 assert_eq!(
3873 error.message(),
3874 "The input argument of `string::toString` requires a value with type `number`, but found a value with type `string`."
3875 );
3876 assert!(
3877 matches!(error, KclError::Argument { .. }),
3878 "expected an Argument error, found {error:?}"
3879 );
3880 }
3881
3882 #[tokio::test(flavor = "multi_thread")]
3883 async fn test_string_to_string_accepts_a_piped_argument() {
3884 let result = parse_execute("actual = 12mm |> string::toString()").await.unwrap();
3885
3886 assert_eq!(
3887 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3888 .as_str()
3889 .unwrap(),
3890 "12mm"
3891 );
3892 }
3893
3894 #[tokio::test(flavor = "multi_thread")]
3895 async fn test_string_to_string_echoes_how_the_literal_was_written() {
3896 for literal in [
3900 "12",
3901 "1.5",
3902 "0.30000000000000004",
3903 "3_",
3904 "2.5_",
3907 "-4_",
3908 "12mm",
3909 "-5mm",
3910 "1.5in",
3911 "90deg",
3912 "1.5rad",
3913 ] {
3914 let code = format!("actual = string::toString({literal})");
3915 let result = parse_execute(&code).await.unwrap();
3916
3917 assert_eq!(
3918 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3919 .as_str()
3920 .unwrap(),
3921 literal,
3922 "literal: {literal}"
3923 );
3924 }
3925 }
3926
3927 #[tokio::test(flavor = "multi_thread")]
3928 async fn test_string_to_string_spells_out_non_finite_numbers() {
3929 for (name, expr, expected) in [
3933 ("positive infinity", "1 / 0", "Infinity"),
3934 ("negative infinity", "-1 / 0", "-Infinity"),
3935 ("nan", "0 / 0", "NaN"),
3936 ("infinity from a length", "1mm / 0", "Infinity"),
3938 ("nan from a length", "0mm / 0", "NaN"),
3939 ("infinity from an angle", "1deg / 0", "Infinity"),
3940 ] {
3941 let code = format!("actual = string::toString({expr})");
3942 let result = parse_execute(&code).await.unwrap();
3943
3944 assert_eq!(
3945 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3946 .as_str()
3947 .unwrap(),
3948 expected,
3949 "case: {name}"
3950 );
3951 }
3952 }
3953
3954 #[tokio::test(flavor = "multi_thread")]
3955 async fn test_string_equality_operators() {
3956 let composed = "\u{e9}";
3957 let decomposed = "e\u{301}";
3958 let code = format!(
3959 r#"
3960equal_same_ascii = "KCL" == "KCL"
3961equal_different_case = "KCL" == "kcl"
3962not_equal_same_ascii = "KCL" != "KCL"
3963not_equal_different_case = "KCL" != "kcl"
3964equal_same_unicode = "{composed}" == "{composed}"
3965not_equal_same_unicode = "{composed}" != "{composed}"
3966equal_without_normalization = "{composed}" == "{decomposed}"
3967not_equal_without_normalization = "{composed}" != "{decomposed}"
3968"#
3969 );
3970
3971 let result = parse_execute(&code).await.unwrap();
3972 for (name, expected) in [
3973 ("equal_same_ascii", true),
3974 ("equal_different_case", false),
3975 ("not_equal_same_ascii", false),
3976 ("not_equal_different_case", true),
3977 ("equal_same_unicode", true),
3978 ("not_equal_same_unicode", false),
3979 ("equal_without_normalization", false),
3980 ("not_equal_without_normalization", true),
3981 ] {
3982 assert_eq!(
3983 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3984 .as_bool()
3985 .unwrap(),
3986 expected,
3987 "{name}"
3988 );
3989 }
3990 }
3991
3992 #[tokio::test(flavor = "multi_thread")]
3993 async fn test_string_equality_inside_sketch_block_fails_like_number_equality() {
3994 let string_code = r#"
3995@settings(experimentalFeatures = allow)
3996
3997sketch(on = XY) {
3998 stringsAreEqual = "KCL" == "KCL"
3999}
4000"#;
4001 let number_code = r#"
4002@settings(experimentalFeatures = allow)
4003
4004sketch(on = XY) {
4005 numbersAreEqual = 1 == 1
4006}
4007"#;
4008
4009 assert_eq!(
4010 parse_execute(string_code).await.unwrap_err().message(),
4011 "Cannot create an equivalence constraint between values of these types: a string and a string"
4012 );
4013 assert_eq!(
4014 parse_execute(number_code).await.unwrap_err().message(),
4015 "Cannot create an equivalence constraint between values of these types: a number and a number"
4016 );
4017 }
4018
4019 #[tokio::test(flavor = "multi_thread")]
4020 async fn test_math_execute_start_negative() {
4021 let ast = r#"myVar = -5 + 6"#;
4022 let result = parse_execute(ast).await.unwrap();
4023 assert_eq!(
4024 1.0,
4025 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4026 .as_f64()
4027 .unwrap()
4028 );
4029 }
4030
4031 #[tokio::test(flavor = "multi_thread")]
4032 async fn test_math_execute_with_pi() {
4033 let ast = r#"myVar = PI * 2"#;
4034 let result = parse_execute(ast).await.unwrap();
4035 assert_eq!(
4036 std::f64::consts::TAU,
4037 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4038 .as_f64()
4039 .unwrap()
4040 );
4041 }
4042
4043 #[tokio::test(flavor = "multi_thread")]
4044 async fn test_math_define_decimal_without_leading_zero() {
4045 let ast = r#"thing = .4 + 7"#;
4046 let result = parse_execute(ast).await.unwrap();
4047 assert_eq!(
4048 7.4,
4049 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
4050 .as_f64()
4051 .unwrap()
4052 );
4053 }
4054
4055 #[tokio::test(flavor = "multi_thread")]
4056 async fn pass_std_to_std() {
4057 let ast = r#"sketch001 = startSketchOn(XY)
4058profile001 = circle(sketch001, center = [0, 0], radius = 2)
4059extrude001 = extrude(profile001, length = 5)
4060extrudes = patternLinear3d(
4061 extrude001,
4062 instances = 3,
4063 distance = 5,
4064 axis = [1, 1, 0],
4065)
4066clone001 = map(extrudes, f = clone)
4067"#;
4068 parse_execute(ast).await.unwrap();
4069 }
4070
4071 #[tokio::test(flavor = "multi_thread")]
4072 async fn test_array_reduce_nested_array() {
4073 let code = r#"
4074fn id(@el, accum) { return accum }
4075
4076answer = reduce([], initial=[[[0,0]]], f=id)
4077"#;
4078 let result = parse_execute(code).await.unwrap();
4079 assert_eq!(
4080 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
4081 KclValue::HomArray {
4082 value: vec![KclValue::HomArray {
4083 value: vec![KclValue::HomArray {
4084 value: vec![
4085 KclValue::Number {
4086 value: 0.0,
4087 ty: NumericType::default(),
4088 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
4089 },
4090 KclValue::Number {
4091 value: 0.0,
4092 ty: NumericType::default(),
4093 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
4094 }
4095 ],
4096 ty: RuntimeType::any(),
4097 }],
4098 ty: RuntimeType::any(),
4099 }],
4100 ty: RuntimeType::any(),
4101 }
4102 );
4103 }
4104
4105 #[tokio::test(flavor = "multi_thread")]
4106 async fn test_zero_param_fn() {
4107 let ast = r#"sigmaAllow = 35000 // psi
4108leg1 = 5 // inches
4109leg2 = 8 // inches
4110fn thickness() { return 0.56 }
4111
4112bracket = startSketchOn(XY)
4113 |> startProfile(at = [0,0])
4114 |> line(end = [0, leg1])
4115 |> line(end = [leg2, 0])
4116 |> line(end = [0, -thickness()])
4117 |> line(end = [-leg2 + thickness(), 0])
4118"#;
4119 parse_execute(ast).await.unwrap();
4120 }
4121
4122 #[tokio::test(flavor = "multi_thread")]
4123 async fn test_unary_operator_not_succeeds() {
4124 let ast = r#"
4125fn returnTrue() { return !false }
4126t = true
4127f = false
4128notTrue = !t
4129notFalse = !f
4130c = !!true
4131d = !returnTrue()
4132
4133assertIs(!false, error = "expected to pass")
4134
4135fn check(x) {
4136 assertIs(!x, error = "expected argument to be false")
4137 return true
4138}
4139check(x = false)
4140"#;
4141 let result = parse_execute(ast).await.unwrap();
4142 assert_eq!(
4143 false,
4144 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
4145 .as_bool()
4146 .unwrap()
4147 );
4148 assert_eq!(
4149 true,
4150 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
4151 .as_bool()
4152 .unwrap()
4153 );
4154 assert_eq!(
4155 true,
4156 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
4157 .as_bool()
4158 .unwrap()
4159 );
4160 assert_eq!(
4161 false,
4162 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
4163 .as_bool()
4164 .unwrap()
4165 );
4166 }
4167
4168 #[tokio::test(flavor = "multi_thread")]
4169 async fn test_unary_operator_not_on_non_bool_fails() {
4170 let code1 = r#"
4171// Yup, this is null.
4172myNull = 0 / 0
4173notNull = !myNull
4174"#;
4175 assert_eq!(
4176 parse_execute(code1).await.unwrap_err().message(),
4177 "Cannot apply unary operator ! to non-boolean value: a number",
4178 );
4179
4180 let code2 = "notZero = !0";
4181 assert_eq!(
4182 parse_execute(code2).await.unwrap_err().message(),
4183 "Cannot apply unary operator ! to non-boolean value: a number",
4184 );
4185
4186 let code3 = r#"
4187notEmptyString = !""
4188"#;
4189 assert_eq!(
4190 parse_execute(code3).await.unwrap_err().message(),
4191 "Cannot apply unary operator ! to non-boolean value: a string",
4192 );
4193
4194 let code4 = r#"
4195obj = { a = 1 }
4196notMember = !obj.a
4197"#;
4198 assert_eq!(
4199 parse_execute(code4).await.unwrap_err().message(),
4200 "Cannot apply unary operator ! to non-boolean value: a number",
4201 );
4202
4203 let code5 = "
4204a = []
4205notArray = !a";
4206 assert_eq!(
4207 parse_execute(code5).await.unwrap_err().message(),
4208 "Cannot apply unary operator ! to non-boolean value: an empty array",
4209 );
4210
4211 let code6 = "
4212x = {}
4213notObject = !x";
4214 assert_eq!(
4215 parse_execute(code6).await.unwrap_err().message(),
4216 "Cannot apply unary operator ! to non-boolean value: an object",
4217 );
4218
4219 let code7 = "
4220fn x() { return 1 }
4221notFunction = !x";
4222 let fn_err = parse_execute(code7).await.unwrap_err();
4223 assert!(
4226 fn_err
4227 .message()
4228 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
4229 "Actual error: {fn_err:?}"
4230 );
4231
4232 let code8 = "
4233myTagDeclarator = $myTag
4234notTagDeclarator = !myTagDeclarator";
4235 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
4236 assert!(
4239 tag_declarator_err
4240 .message()
4241 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
4242 "Actual error: {tag_declarator_err:?}"
4243 );
4244
4245 let code9 = "
4246myTagDeclarator = $myTag
4247notTagIdentifier = !myTag";
4248 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
4249 assert!(
4252 tag_identifier_err
4253 .message()
4254 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
4255 "Actual error: {tag_identifier_err:?}"
4256 );
4257
4258 let code10 = "notPipe = !(1 |> 2)";
4259 assert_eq!(
4260 parse_execute(code10).await.unwrap_err(),
4263 KclError::new_syntax(KclErrorDetails::new(
4264 "Unexpected token: !".to_owned(),
4265 vec![SourceRange::new(10, 11, ModuleId::default())],
4266 ))
4267 );
4268
4269 let code11 = "
4270fn identity(x) { return x }
4271notPipeSub = 1 |> identity(!%))";
4272 assert_eq!(
4273 parse_execute(code11).await.unwrap_err(),
4276 KclError::new_syntax(KclErrorDetails::new(
4277 "There was an unexpected `!`. Try removing it.".to_owned(),
4278 vec![SourceRange::new(56, 57, ModuleId::default())],
4279 ))
4280 );
4281
4282 }
4286
4287 #[tokio::test(flavor = "multi_thread")]
4288 async fn test_start_sketch_on_invalid_kwargs() {
4289 let current_dir = std::env::current_dir().unwrap();
4290 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
4291 let mut code = std::fs::read_to_string(&path).unwrap();
4292 assert_eq!(
4293 parse_execute(&code).await.unwrap_err().message(),
4294 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
4295 );
4296
4297 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
4298 code = std::fs::read_to_string(&path).unwrap();
4299
4300 assert_eq!(
4301 parse_execute(&code).await.unwrap_err().message(),
4302 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
4303 );
4304
4305 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
4306 code = std::fs::read_to_string(&path).unwrap();
4307
4308 assert_eq!(
4309 parse_execute(&code).await.unwrap_err().message(),
4310 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4311 );
4312
4313 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
4314 code = std::fs::read_to_string(&path).unwrap();
4315
4316 assert_eq!(
4317 parse_execute(&code).await.unwrap_err().message(),
4318 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4319 );
4320
4321 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
4322 code = std::fs::read_to_string(&path).unwrap();
4323
4324 assert_eq!(
4325 parse_execute(&code).await.unwrap_err().message(),
4326 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
4327 );
4328 }
4329
4330 #[tokio::test(flavor = "multi_thread")]
4331 async fn test_math_negative_variable_in_binary_expression() {
4332 let ast = r#"sigmaAllow = 35000 // psi
4333width = 1 // inch
4334
4335p = 150 // lbs
4336distance = 6 // inches
4337FOS = 2
4338
4339leg1 = 5 // inches
4340leg2 = 8 // inches
4341
4342thickness_squared = distance * p * FOS * 6 / sigmaAllow
4343thickness = 0.56 // inches. App does not support square root function yet
4344
4345bracket = startSketchOn(XY)
4346 |> startProfile(at = [0,0])
4347 |> line(end = [0, leg1])
4348 |> line(end = [leg2, 0])
4349 |> line(end = [0, -thickness])
4350 |> line(end = [-leg2 + thickness, 0])
4351"#;
4352 parse_execute(ast).await.unwrap();
4353 }
4354
4355 #[tokio::test(flavor = "multi_thread")]
4356 async fn test_execute_function_no_return() {
4357 let ast = r#"fn test(@origin) {
4358 origin
4359}
4360
4361test([0, 0])
4362"#;
4363 let result = parse_execute(ast).await;
4364 assert!(result.is_err());
4365 assert!(result.unwrap_err().to_string().contains("undefined"));
4366 }
4367
4368 #[tokio::test(flavor = "multi_thread")]
4369 async fn test_max_stack_size_exceeded_error() {
4370 let ast = r#"
4371fn forever(@n) {
4372 return 1 + forever(n)
4373}
4374
4375forever(1)
4376"#;
4377 let result = parse_execute(ast).await;
4378 let err = result.unwrap_err();
4379 let msg = err.to_string();
4382 assert!(
4383 msg.contains("stack size exceeded") || msg.contains("Call depth limit"),
4384 "actual: {err:?}"
4385 );
4386 }
4387
4388 #[tokio::test(flavor = "multi_thread")]
4389 async fn test_math_doubly_nested_parens() {
4390 let ast = r#"sigmaAllow = 35000 // psi
4391width = 4 // inch
4392p = 150 // Force on shelf - lbs
4393distance = 6 // inches
4394FOS = 2
4395leg1 = 5 // inches
4396leg2 = 8 // inches
4397thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
4398thickness = 0.32 // inches. App does not support square root function yet
4399bracket = startSketchOn(XY)
4400 |> startProfile(at = [0,0])
4401 |> line(end = [0, leg1])
4402 |> line(end = [leg2, 0])
4403 |> line(end = [0, -thickness])
4404 |> line(end = [-1 * leg2 + thickness, 0])
4405 |> line(end = [0, -1 * leg1 + thickness])
4406 |> close()
4407 |> extrude(length = width)
4408"#;
4409 parse_execute(ast).await.unwrap();
4410 }
4411
4412 #[tokio::test(flavor = "multi_thread")]
4413 async fn test_math_nested_parens_one_less() {
4414 let ast = r#" sigmaAllow = 35000 // psi
4415width = 4 // inch
4416p = 150 // Force on shelf - lbs
4417distance = 6 // inches
4418FOS = 2
4419leg1 = 5 // inches
4420leg2 = 8 // inches
4421thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
4422thickness = 0.32 // inches. App does not support square root function yet
4423bracket = startSketchOn(XY)
4424 |> startProfile(at = [0,0])
4425 |> line(end = [0, leg1])
4426 |> line(end = [leg2, 0])
4427 |> line(end = [0, -thickness])
4428 |> line(end = [-1 * leg2 + thickness, 0])
4429 |> line(end = [0, -1 * leg1 + thickness])
4430 |> close()
4431 |> extrude(length = width)
4432"#;
4433 parse_execute(ast).await.unwrap();
4434 }
4435
4436 #[tokio::test(flavor = "multi_thread")]
4437 async fn test_fn_as_operand() {
4438 let ast = r#"fn f() { return 1 }
4439x = f()
4440y = x + 1
4441z = f() + 1
4442w = f() + f()
4443"#;
4444 parse_execute(ast).await.unwrap();
4445 }
4446
4447 #[tokio::test(flavor = "multi_thread")]
4448 async fn kcl_test_ids_stable_between_executions() {
4449 let code = r#"sketch001 = startSketchOn(XZ)
4450|> startProfile(at = [61.74, 206.13])
4451|> xLine(length = 305.11, tag = $seg01)
4452|> yLine(length = -291.85)
4453|> xLine(length = -segLen(seg01))
4454|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4455|> close()
4456|> extrude(length = 40.14)
4457|> shell(
4458 thickness = 3.14,
4459 faces = [seg01]
4460)
4461"#;
4462
4463 let ctx = crate::test_server::new_context(true, None).await.unwrap();
4464 let old_program = crate::Program::parse_no_errs(code).unwrap();
4465
4466 if let Err(err) = ctx.run_with_caching(old_program).await {
4468 let report = err.into_miette_report_with_outputs(code).unwrap();
4469 let report = miette::Report::new(report);
4470 panic!("Error executing program: {report:?}");
4471 }
4472
4473 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4475
4476 let code = r#"sketch001 = startSketchOn(XZ)
4477|> startProfile(at = [62.74, 206.13])
4478|> xLine(length = 305.11, tag = $seg01)
4479|> yLine(length = -291.85)
4480|> xLine(length = -segLen(seg01))
4481|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4482|> close()
4483|> extrude(length = 40.14)
4484|> shell(
4485 faces = [seg01],
4486 thickness = 3.14,
4487)
4488"#;
4489
4490 let program = crate::Program::parse_no_errs(code).unwrap();
4492 ctx.run_with_caching(program).await.unwrap();
4494
4495 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4496
4497 assert_eq!(id_generator, new_id_generator);
4498 }
4499
4500 #[tokio::test(flavor = "multi_thread")]
4501 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
4502 let code = r#"sketch001 = startSketchOn(XZ)
4503|> startProfile(at = [61.74, 206.13])
4504|> xLine(length = 305.11, tag = $seg01)
4505|> yLine(length = -291.85)
4506|> xLine(length = -segLen(seg01))
4507|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4508|> close()
4509|> extrude(length = 40.14)
4510|> shell(
4511 thickness = 3.14,
4512 faces = [seg01]
4513)
4514"#;
4515
4516 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
4517 let old_program = crate::Program::parse_no_errs(code).unwrap();
4518
4519 ctx.run_with_caching(old_program.clone()).await.unwrap();
4521
4522 let settings_state = cache::read_old_ast().await.unwrap().settings;
4523
4524 assert_eq!(settings_state, ctx.settings);
4526
4527 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4529
4530 ctx.run_with_caching(old_program.clone()).await.unwrap();
4532
4533 let settings_state = cache::read_old_ast().await.unwrap().settings;
4534
4535 assert_eq!(settings_state, ctx.settings);
4537
4538 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4540
4541 ctx.run_with_caching(old_program).await.unwrap();
4543
4544 let settings_state = cache::read_old_ast().await.unwrap().settings;
4545
4546 assert_eq!(settings_state, ctx.settings);
4548
4549 ctx.close().await;
4550 }
4551
4552 #[tokio::test(flavor = "multi_thread")]
4553 async fn mock_after_not_mock() {
4554 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4555 let program = crate::Program::parse_no_errs("x = 2").unwrap();
4556 let result = ctx.run_with_caching(program).await.unwrap();
4557 assert_number_variable(&result.variables, "x", 2.0);
4558
4559 let ctx2 = ExecutorContext::new_mock(None).await;
4560 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
4561 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
4562 assert_number_variable(&result.variables, "z", 3.0);
4563
4564 ctx.close().await;
4565 ctx2.close().await;
4566 }
4567
4568 #[tokio::test(flavor = "multi_thread")]
4570 async fn mock_execution_succeeds_after_split() {
4571 let code = kcl_input!("repro_mock_extrude");
4572 let ctx = ExecutorContext::new_mock(None).await;
4573 let program = crate::Program::parse_no_errs(code).unwrap();
4574 let _result = match ctx.run_mock(&program, &MockConfig::default()).await {
4575 Ok(res) => res,
4576 Err(e) => panic!("{}", e.error),
4577 };
4578 }
4579
4580 #[tokio::test(flavor = "multi_thread")]
4582 async fn mock_execution_rejects_oob_on_frontend_array() {
4583 let code = r#"
4584values = [10, 20]
4585third = values[2]
4586"#;
4587 let ctx = ExecutorContext::new_mock(None).await;
4588 let program = crate::Program::parse_no_errs(code).unwrap();
4589 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
4590 ctx.close().await;
4591
4592 assert!(
4593 err.error.message().contains("array doesn't have any item at index 2"),
4594 "{err:?}"
4595 );
4596 }
4597
4598 #[tokio::test(flavor = "multi_thread")]
4603 async fn mock_execution_pattern_circular_number() {
4604 let code = kcl_input!("repro_mock_pattern_circular");
4605 let ctx = ExecutorContext::new_mock(None).await;
4606 let program = crate::Program::parse_no_errs(code).unwrap();
4607 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
4608 let copies = result
4609 .variables
4610 .get("copies")
4611 .expect("no variable called 'copies' found");
4612 let value = match copies {
4613 KclValueView::Solid { .. } => {
4614 panic!("One solid?");
4615 }
4616 KclValueView::HomArray { value } => value,
4617 other => panic!("{other:#?}"),
4618 };
4619 let actual_instances = value.len();
4620 let expected_instances = 10; assert_eq!(actual_instances, expected_instances);
4622 }
4623
4624 #[tokio::test(flavor = "multi_thread")]
4629 async fn mock_execution_subtract() {
4630 let code = kcl_input!("repro_mock_subtract");
4632 let ctx = ExecutorContext::new_mock(None).await;
4633 let program = crate::Program::parse_no_errs(code).unwrap();
4634 let result = ctx.run_mock(&program, &MockConfig::default()).await;
4635 ctx.close().await;
4636 let result = match result {
4637 Ok(x) => x,
4638 Err(e) => {
4639 let error = e.error;
4640 panic!("{error}");
4641 }
4642 };
4643
4644 let subtracted_parts = result
4646 .variables
4647 .get("subtractedParts")
4648 .expect("no variable called 'subtracted_parts' found");
4649 let subtracted_parts = match subtracted_parts {
4650 KclValueView::Solid { .. } => {
4651 panic!("One solid?");
4652 }
4653 KclValueView::HomArray { value } => value,
4654 other => panic!("{other:#?}"),
4655 };
4656
4657 let expected_number_of_parts = 2;
4660 let actual_number_of_parts = subtracted_parts.len();
4661 assert_eq!(actual_number_of_parts, expected_number_of_parts);
4662 }
4663
4664 #[tokio::test(flavor = "multi_thread")]
4665 async fn mock_then_add_extrude_then_mock_again() {
4666 let code = "s = sketch(on = XY) {
4667 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
4668 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
4669 coincident([line1.end, line2.start])
4670 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
4671 coincident([line2.end, line3.start])
4672 coincident([line1.start, line3.end])
4673}
4674 ";
4675 let ctx = ExecutorContext::new_mock(None).await;
4676 let program = crate::Program::parse_no_errs(code).unwrap();
4677 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
4678 assert!(result.variables.contains_key("s"), "actual: {:?}", result.variables);
4679
4680 let code2 = code.to_owned()
4681 + "
4682region001 = region(point = [1mm, 1mm], sketch = s)
4683extrude001 = extrude(region001, length = 1)
4684 ";
4685 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4686 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
4687 assert!(
4688 result.variables.contains_key("region001"),
4689 "actual: {:?}",
4690 result.variables
4691 );
4692
4693 ctx.close().await;
4694 }
4695
4696 #[tokio::test(flavor = "multi_thread")]
4697 async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
4698 let code = format!(
4699 r#"{}
4700
4701face7 = faceOf(solid6, face = r6.tags.line1)
4702r7 = squareRegion(onSurface = face7)
4703solid7 = extrude(r7, length = width)
4704"#,
4705 include_str!("../../tests/endless_impeller/input.kcl")
4706 );
4707
4708 let result = parse_execute(&code).await.unwrap();
4709 let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
4710 assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
4711
4712 let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
4713 KclValue::Face { value } => value,
4714 value => panic!("expected face7 to be a Face, got {value:?}"),
4715 };
4716 assert!(face7.parent_solid.creator_sketch_id.is_some());
4717 }
4718
4719 #[tokio::test(flavor = "multi_thread")]
4720 async fn mock_has_stable_ids() {
4721 let ctx = ExecutorContext::new_mock(None).await;
4722 let mock_config = MockConfig {
4723 use_prev_memory: false,
4724 ..Default::default()
4725 };
4726 let code = "sk = startSketchOn(XY)
4727 |> startProfile(at = [0, 0])";
4728 let program = crate::Program::parse_no_errs(code).unwrap();
4729 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
4730 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4731 assert!(!ids.is_empty(), "IDs should not be empty");
4732
4733 let ctx2 = ExecutorContext::new_mock(None).await;
4734 let program2 = crate::Program::parse_no_errs(code).unwrap();
4735 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
4736 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4737
4738 assert_eq!(ids, ids2, "Generated IDs should match");
4739 ctx.close().await;
4740 ctx2.close().await;
4741 }
4742
4743 #[tokio::test(flavor = "multi_thread")]
4744 async fn mock_memory_restore_preserves_module_maps() {
4745 clear_mem_cache().await;
4746
4747 let ctx = ExecutorContext::new_mock(None).await;
4748 let cold_start = MockConfig {
4749 use_prev_memory: false,
4750 ..Default::default()
4751 };
4752 ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
4753
4754 let mut mem = cache::read_old_memory().await.unwrap();
4755 assert!(
4756 mem.path_to_source_id.len() > 3,
4757 "expected prelude imports to populate multiple modules, got {:?}",
4758 mem.path_to_source_id
4759 );
4760 mem.constraint_state.insert(
4761 crate::front::ObjectId(1),
4762 indexmap::indexmap! {
4763 crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
4764 crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
4765 },
4766 );
4767
4768 let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
4769 ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
4770
4771 assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
4772 assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
4773 assert_eq!(exec_state.global.module_infos, mem.module_infos);
4774 assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
4775
4776 clear_mem_cache().await;
4777 ctx.close().await;
4778 }
4779
4780 #[tokio::test(flavor = "multi_thread")]
4781 async fn run_with_caching_no_action_refreshes_mock_memory() {
4782 cache::bust_cache().await;
4783 clear_mem_cache().await;
4784
4785 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
4786 let program = crate::Program::parse_no_errs(
4787 r#"sketch001 = sketch(on = XY) {
4788 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
4789}
4790"#,
4791 )
4792 .unwrap();
4793
4794 ctx.run_with_caching(program.clone()).await.unwrap();
4795 let baseline_memory = cache::read_old_memory().await.unwrap();
4796 assert!(
4797 !baseline_memory.scene_objects.is_empty(),
4798 "expected engine execution to persist full-scene mock memory"
4799 );
4800
4801 cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
4802 assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
4803
4804 ctx.run_with_caching(program).await.unwrap();
4805 let refreshed_memory = cache::read_old_memory().await.unwrap();
4806 assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
4807 assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
4808 assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
4809
4810 cache::bust_cache().await;
4811 clear_mem_cache().await;
4812 ctx.close().await;
4813 }
4814
4815 #[tokio::test(flavor = "multi_thread")]
4816 async fn sim_sketch_mode_real_mock_real() {
4817 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4818 let code = r#"sketch001 = startSketchOn(XY)
4819profile001 = startProfile(sketch001, at = [0, 0])
4820 |> line(end = [10, 0])
4821 |> line(end = [0, 10])
4822 |> line(end = [-10, 0])
4823 |> line(end = [0, -10])
4824 |> close()
4825"#;
4826 let program = crate::Program::parse_no_errs(code).unwrap();
4827 let result = ctx.run_with_caching(program).await.unwrap();
4828 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4829
4830 let mock_ctx = ExecutorContext::new_mock(None).await;
4831 let mock_program = crate::Program::parse_no_errs(code).unwrap();
4832 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
4833 assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4834
4835 let code2 = code.to_owned()
4836 + r#"
4837extrude001 = extrude(profile001, length = 10)
4838"#;
4839 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4840 let result = ctx.run_with_caching(program2).await.unwrap();
4841 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
4842
4843 ctx.close().await;
4844 mock_ctx.close().await;
4845 }
4846
4847 #[tokio::test(flavor = "multi_thread")]
4848 async fn read_tag_version() {
4849 let ast = r#"fn bar(@t) {
4850 return startSketchOn(XY)
4851 |> startProfile(at = [0,0])
4852 |> angledLine(
4853 angle = -60,
4854 length = segLen(t),
4855 )
4856 |> line(end = [0, 0])
4857 |> close()
4858}
4859
4860sketch = startSketchOn(XY)
4861 |> startProfile(at = [0,0])
4862 |> line(end = [0, 10])
4863 |> line(end = [10, 0], tag = $tag0)
4864 |> line(endAbsolute = [0, 0])
4865
4866fn foo() {
4867 // tag0 tags an edge
4868 return bar(tag0)
4869}
4870
4871solid = sketch |> extrude(length = 10)
4872// tag0 tags a face
4873sketch2 = startSketchOn(solid, face = tag0)
4874 |> startProfile(at = [0,0])
4875 |> line(end = [0, 1])
4876 |> line(end = [1, 0])
4877 |> line(end = [0, 0])
4878
4879foo() |> extrude(length = 1)
4880"#;
4881 parse_execute(ast).await.unwrap();
4882 }
4883
4884 #[tokio::test(flavor = "multi_thread")]
4885 async fn experimental() {
4886 let code = r#"
4887startSketchOn(XY)
4888 |> startProfile(at = [0, 0], tag = $start)
4889 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4890"#;
4891 let result = parse_execute(code).await.unwrap();
4892 let issues = result.exec_state.issues();
4893 assert_eq!(issues.len(), 1);
4894 assert_eq!(issues[0].severity, Severity::Error);
4895 let msg = &issues[0].message;
4896 assert!(msg.contains("experimental"), "found {msg}");
4897
4898 let code = r#"@settings(experimentalFeatures = allow)
4899startSketchOn(XY)
4900 |> startProfile(at = [0, 0], tag = $start)
4901 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4902"#;
4903 let result = parse_execute(code).await.unwrap();
4904 let issues = result.exec_state.issues();
4905 assert!(issues.is_empty(), "issues={issues:#?}");
4906
4907 let code = r#"@settings(experimentalFeatures = warn)
4908startSketchOn(XY)
4909 |> startProfile(at = [0, 0], tag = $start)
4910 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4911"#;
4912 let result = parse_execute(code).await.unwrap();
4913 let issues = result.exec_state.issues();
4914 assert_eq!(issues.len(), 1);
4915 assert_eq!(issues[0].severity, Severity::Warning);
4916 let msg = &issues[0].message;
4917 assert!(msg.contains("experimental"), "found {msg}");
4918
4919 let code = r#"@settings(experimentalFeatures = deny)
4920startSketchOn(XY)
4921 |> startProfile(at = [0, 0], tag = $start)
4922 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4923"#;
4924 let result = parse_execute(code).await.unwrap();
4925 let issues = result.exec_state.issues();
4926 assert_eq!(issues.len(), 1);
4927 assert_eq!(issues[0].severity, Severity::Error);
4928 let msg = &issues[0].message;
4929 assert!(msg.contains("experimental"), "found {msg}");
4930
4931 let code = r#"@settings(experimentalFeatures = foo)
4932startSketchOn(XY)
4933 |> startProfile(at = [0, 0], tag = $start)
4934 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4935"#;
4936 parse_execute(code).await.unwrap_err();
4937 }
4938
4939 #[tokio::test(flavor = "multi_thread")]
4940 async fn default_angle_unit_warns_in_legacy_kcl() {
4941 for version in ["", "kclVersion = 1.0, ", "kclVersion = 2.0, "] {
4942 for unit in ["deg", "rad"] {
4943 let code = format!("@settings({version}defaultAngleUnit = {unit})\nx = 1\n");
4944 let result = parse_execute(&code).await.unwrap();
4945 let issues = result.issues();
4946 assert_eq!(issues.len(), 1, "code={code}");
4947 assert_eq!(issues[0].severity, Severity::Warning, "code={code}");
4948 assert_eq!(
4949 issues[0].message,
4950 "The `defaultAngleUnit` setting is deprecated; use explicit units for angles"
4951 );
4952 assert_eq!(variable_f64(&result, "x"), 1.0);
4953 }
4954 }
4955 }
4956
4957 #[tokio::test(flavor = "multi_thread")]
4958 async fn default_angle_unit_errors_in_kcl_v3() {
4959 for settings in [
4960 "@settings(kclVersion = \"3.0-preview\", defaultAngleUnit = deg)",
4961 "@settings(defaultAngleUnit = rad, kclVersion = \"3.0-preview\")",
4962 "@settings(defaultAngleUnit = deg)\n@settings(kclVersion = \"3.0-preview\")",
4963 "@settings(kclVersion = \"3.0-preview\")\n@settings(defaultAngleUnit = rad)",
4964 ] {
4965 let code = format!("{settings}\nx = 1\n");
4966 let Err(error) = parse_execute(&code).await else {
4967 panic!("defaultAngleUnit must fail in KCL 3.0: {code}");
4968 };
4969 assert_eq!(
4970 error.message(),
4971 "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles",
4972 "code={code}"
4973 );
4974 let ranges = error.source_ranges();
4975 assert_eq!(ranges.len(), 1);
4976 assert!(code[ranges[0].start()..ranges[0].end()].contains("defaultAngleUnit"));
4977 }
4978 }
4979
4980 #[tokio::test(flavor = "multi_thread")]
4981 async fn default_angle_unit_error_cannot_be_suppressed() {
4982 for version in ["1.0", "2.0", "\"3.0-preview\""] {
4983 let code = format!(
4984 "@warnings(allow = angleUnits)\n@settings(kclVersion = {version}, defaultAngleUnit = deg)\nx = 1\n"
4985 );
4986 let result = parse_execute(&code).await;
4987 if version == "\"3.0-preview\"" {
4988 assert_eq!(
4989 result.unwrap_err().message(),
4990 "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
4991 );
4992 } else {
4993 assert!(result.unwrap().issues().is_empty(), "code={code}");
4994 }
4995 }
4996 }
4997
4998 #[tokio::test(flavor = "multi_thread")]
4999 async fn default_angle_unit_in_import_uses_effective_kcl_version() {
5000 let dep = "@settings(kclVersion = 2.0, defaultAngleUnit = deg)\nexport x = 1\n";
5001 for version in ["1.0", "2.0", "\"3.0-preview\""] {
5002 let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5003 let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5004 if version == "\"3.0-preview\"" {
5005 assert_eq!(
5006 result.unwrap_err().message(),
5007 "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
5008 );
5009 } else {
5010 assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0);
5011 }
5012 }
5013 }
5014
5015 #[tokio::test(flavor = "multi_thread")]
5016 async fn entry_point_kcl_version_recorded_only_for_v3() {
5017 let result = parse_execute("@settings(kclVersion = \"3.0-preview\")\nx = 1\n")
5018 .await
5019 .unwrap();
5020 assert_eq!(
5021 result.exec_state.global.entry_point_kcl_version,
5022 Some(KclVersion::V3Preview)
5023 );
5024 assert!(result.exec_state.entry_point_version_is_v3_or_higher());
5025
5026 for code in [
5027 "x = 1\n",
5028 "@settings(kclVersion = 1.0)\nx = 1\n",
5029 "@settings(kclVersion = 2.0)\nx = 1\n",
5030 ] {
5031 let result = parse_execute(code).await.unwrap();
5032 assert_eq!(result.exec_state.global.entry_point_kcl_version, None, "code={code}");
5033 assert!(!result.exec_state.entry_point_version_is_v3_or_higher(), "code={code}");
5034 }
5035 }
5036
5037 #[tokio::test(flavor = "multi_thread")]
5038 async fn kcl_version_lookup_prefers_entry_point_over_module_local() {
5039 let mut exec_state = parse_execute("x = 1\n").await.unwrap().exec_state;
5040
5041 exec_state.global.entry_point_kcl_version = None;
5043 exec_state.mod_local.settings.kcl_version = KclVersion::V2;
5044 assert_eq!(exec_state.kcl_version(), KclVersion::V2);
5045 assert_eq!(exec_state.legacy_caller_kcl_version(), KclVersion::V2);
5046
5047 exec_state.global.entry_point_kcl_version = Some(KclVersion::V3Preview);
5050 assert_eq!(exec_state.kcl_version(), KclVersion::V3Preview);
5051 assert_eq!(exec_state.legacy_caller_kcl_version(), KclVersion::V2);
5052 }
5053
5054 #[tokio::test(flavor = "multi_thread")]
5059 async fn mock_execution_records_entry_point_kcl_version() {
5060 use futures::FutureExt;
5061
5062 clear_mem_cache().await;
5063
5064 let ctx = ExecutorContext::new_mock(None).await;
5065 let fresh_memory = MockConfig {
5066 use_prev_memory: false,
5067 ..Default::default()
5068 };
5069 let prev_memory = MockConfig::default();
5070
5071 let v3_program = crate::Program::parse_no_errs("@settings(kclVersion = \"3.0-preview\")\nx = 1\n").unwrap();
5072 let v2_program = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 1\n").unwrap();
5073
5074 let test_result = std::panic::AssertUnwindSafe(async {
5077 let (exec_state, _) = ctx.run_mock_returning_state(&v3_program, &fresh_memory).await.unwrap();
5078 assert_eq!(
5079 exec_state.global.entry_point_kcl_version,
5080 Some(KclVersion::V3Preview),
5081 "mock execution should record a 3.0-preview entry point"
5082 );
5083 assert!(exec_state.entry_point_version_is_v3_or_higher());
5084
5085 ctx.run_mock(&v3_program, &fresh_memory).await.unwrap();
5089 let (exec_state, _) = ctx.run_mock_returning_state(&v2_program, &prev_memory).await.unwrap();
5090 assert_eq!(exec_state.global.entry_point_kcl_version, None);
5091 assert!(!exec_state.entry_point_version_is_v3_or_higher());
5092
5093 ctx.run_mock(&v2_program, &fresh_memory).await.unwrap();
5096 let (exec_state, _) = ctx.run_mock_returning_state(&v3_program, &prev_memory).await.unwrap();
5097 assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V3Preview));
5098 })
5099 .catch_unwind()
5100 .await;
5101
5102 clear_mem_cache().await;
5103 ctx.close().await;
5104 if let Err(panic) = test_result {
5105 std::panic::resume_unwind(panic);
5106 }
5107 }
5108
5109 #[tokio::test(flavor = "multi_thread")]
5113 async fn mock_execution_applies_v3_semantics() {
5114 use futures::FutureExt;
5115
5116 clear_mem_cache().await;
5117
5118 let ctx = ExecutorContext::new_mock(None).await;
5119 let fresh_memory = MockConfig {
5120 use_prev_memory: false,
5121 ..Default::default()
5122 };
5123 let program = crate::Program::parse_no_errs(
5124 r#"@settings(kclVersion = "3.0-preview")
5125fn f() {
5126 return 1
5127 assert(1, isEqualTo = 2, error = "code after return ran")
5128}
5129x = f()
5130outer = 1
5131y = if true {
5132 outer = 2
5133 outer + 10
5134} else {
5135 0
5136}
5137"#,
5138 )
5139 .unwrap();
5140
5141 let test_result = std::panic::AssertUnwindSafe(async {
5144 let (exec_state, env) = ctx.run_mock_returning_state(&program, &fresh_memory).await.unwrap();
5145 let var = |name: &str| mem_get_json(exec_state.stack(), env, name).as_f64().unwrap();
5146 assert_eq!(var("x"), 1.0, "early return produces the function's value");
5147 assert_eq!(var("y"), 12.0, "the branch sees its own shadowing binding");
5148 assert_eq!(var("outer"), 1.0, "the outer binding is unchanged after the if");
5149 })
5150 .catch_unwind()
5151 .await;
5152
5153 clear_mem_cache().await;
5154 ctx.close().await;
5155 if let Err(panic) = test_result {
5156 std::panic::resume_unwind(panic);
5157 }
5158 }
5159
5160 fn emitted_fillet_versions_everywhere(
5165 result: &ExecTestResults,
5166 ) -> Vec<kittycad_modeling_cmds::shared::EdgeCutVersion> {
5167 let module_commands = result
5168 .exec_state
5169 .global
5170 .module_infos
5171 .values()
5172 .filter_map(|info| match &info.repr {
5173 ModuleRepr::Kcl(_, Some(outcome)) => Some(outcome.artifacts.commands.iter()),
5174 _ => None,
5175 })
5176 .flatten();
5177 result
5178 .root_module_artifact_commands()
5179 .iter()
5180 .chain(module_commands)
5181 .filter_map(|artifact_command| match &artifact_command.command {
5182 kittycad_modeling_cmds::ModelingCmd::Solid3dCutEdges(command) => Some(command.version),
5183 _ => None,
5184 })
5185 .collect()
5186 }
5187
5188 const FILLET_AT_MODULE_TOP_LEVEL: &str = r#"
5189profile = startSketchOn(XY)
5190 |> startProfile(at = [0, 0])
5191 |> line(end = [10, 0], tag = $edge)
5192 |> line(end = [0, 10])
5193 |> line(end = [-10, 0])
5194 |> close()
5195solid = extrude(profile, length = 10)
5196fillet(solid, tags = [edge], radius = 1)
5197"#;
5198
5199 const FILLET_IN_EXPORTED_FN: &str = r#"
5200export fn filletedBox() {
5201 profile = startSketchOn(XY)
5202 |> startProfile(at = [0, 0])
5203 |> line(end = [10, 0], tag = $edge)
5204 |> line(end = [0, 10])
5205 |> line(end = [-10, 0])
5206 |> close()
5207 solid = extrude(profile, length = 10)
5208 return fillet(solid, tags = [edge], radius = 1)
5209}
5210"#;
5211
5212 #[tokio::test(flavor = "multi_thread")]
5216 async fn entry_point_v3_pins_kcl_version_for_imported_modules() {
5217 use kittycad_modeling_cmds::shared::EdgeCutVersion;
5218
5219 let dep = format!("@settings(kclVersion = 2.0)\n{FILLET_AT_MODULE_TOP_LEVEL}");
5220 let main = r#"@settings(kclVersion = "3.0-preview")
5221import "dep.kcl" as dep
5222"#;
5223 let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
5224 assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
5225
5226 let dep = format!("@settings(kclVersion = 2.0)\n{FILLET_IN_EXPORTED_FN}");
5227 let main = r#"@settings(kclVersion = "3.0-preview")
5228import filletedBox from "dep.kcl"
5229box = filletedBox()
5230"#;
5231 let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
5232 assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
5233 }
5234
5235 #[tokio::test(flavor = "multi_thread")]
5240 async fn legacy_kcl_version_quirk_applies_without_v3_entry_point() {
5241 use kittycad_modeling_cmds::shared::EdgeCutVersion;
5242
5243 let dep = format!("@settings(kclVersion = \"3.0-preview\")\n{FILLET_AT_MODULE_TOP_LEVEL}");
5244 let main = r#"@settings(kclVersion = 2.0)
5245import "dep.kcl" as dep
5246"#;
5247 let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
5248 assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
5249
5250 let dep = format!("@settings(kclVersion = \"3.0-preview\")\n{FILLET_IN_EXPORTED_FN}");
5251 let main = r#"@settings(kclVersion = 2.0)
5252import filletedBox from "dep.kcl"
5253box = filletedBox()
5254"#;
5255 let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
5256 assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V1]);
5257 }
5258
5259 #[track_caller]
5260 fn variable_f64(result: &ExecTestResults, name: &str) -> f64 {
5261 mem_get_json(result.exec_state.stack(), result.mem_env, name)
5262 .as_f64()
5263 .unwrap()
5264 }
5265
5266 #[tokio::test(flavor = "multi_thread")]
5267 async fn return_terminates_function_early_in_v3() {
5268 let code = r#"@settings(kclVersion = "3.0-preview")
5269fn f() {
5270 return 1
5271 assert(1, isEqualTo = 2, error = "code after return ran")
5272}
5273x = f()
5274"#;
5275 let result = parse_execute(code).await.unwrap();
5276 assert_eq!(variable_f64(&result, "x"), 1.0);
5277 }
5278
5279 #[tokio::test(flavor = "multi_thread")]
5280 async fn second_return_is_unreachable_in_v3() {
5281 let code = r#"@settings(kclVersion = "3.0-preview")
5282fn f() {
5283 return 1
5284 return 2
5285}
5286x = f()
5287"#;
5288 let result = parse_execute(code).await.unwrap();
5289 assert_eq!(variable_f64(&result, "x"), 1.0);
5290 }
5291
5292 #[tokio::test(flavor = "multi_thread")]
5293 async fn return_inside_if_arm_returns_from_function_in_v3() {
5294 let code = r#"@settings(kclVersion = "3.0-preview")
5295fn f(@b) {
5296 dummy = if b {
5297 return 1
5298 0
5299 } else {
5300 0
5301 }
5302 return 2
5303}
5304x = f(true)
5305y = f(false)
5306"#;
5307 let result = parse_execute(code).await.unwrap();
5308 assert_eq!(variable_f64(&result, "x"), 1.0);
5309 assert_eq!(variable_f64(&result, "y"), 2.0);
5310 }
5311
5312 #[tokio::test(flavor = "multi_thread")]
5313 async fn return_inside_nested_if_returns_from_function_in_v3() {
5314 let code = r#"@settings(kclVersion = "3.0-preview")
5315fn f(@a, b) {
5316 dummy = if a {
5317 inner = if b {
5318 return 10
5319 0
5320 } else {
5321 1
5322 }
5323 inner + 1
5324 } else {
5325 2
5326 }
5327 return dummy * 100
5328}
5329x = f(true, b = true)
5330y = f(true, b = false)
5331z = f(false, b = false)
5332"#;
5333 let result = parse_execute(code).await.unwrap();
5334 assert_eq!(variable_f64(&result, "x"), 10.0);
5335 assert_eq!(variable_f64(&result, "y"), 200.0);
5336 assert_eq!(variable_f64(&result, "z"), 200.0);
5337 }
5338
5339 #[tokio::test(flavor = "multi_thread")]
5340 async fn return_inside_closure_returns_only_from_closure_in_v3() {
5341 let code = r#"@settings(kclVersion = "3.0-preview")
5342fn outer() {
5343 inner = fn() {
5344 return 5
5345 assert(1, isEqualTo = 2, error = "code after inner return ran")
5346 }
5347 v = inner()
5348 return v + 1
5349}
5350x = outer()
5351"#;
5352 let result = parse_execute(code).await.unwrap();
5353 assert_eq!(variable_f64(&result, "x"), 6.0);
5354 }
5355
5356 #[tokio::test(flavor = "multi_thread")]
5357 async fn return_type_coercion_applies_to_early_return_in_v3() {
5358 let code = r#"@settings(kclVersion = "3.0-preview")
5359fn f(): number(mm) {
5360 return 1
5361 assert(1, isEqualTo = 2, error = "code after return ran")
5362}
5363x = f()
5364"#;
5365 let result = parse_execute(code).await.unwrap();
5366 assert_eq!(variable_f64(&result, "x"), 1.0);
5367
5368 let code = r#"@settings(kclVersion = "3.0-preview")
5371fn f(): number(mm) {
5372 return "nope"
5373}
5374x = f()
5375"#;
5376 let err = parse_execute(code).await.expect_err("coercion failure should error");
5377 assert!(err.message().contains("type"), "unexpected message: {}", err.message());
5378 }
5379
5380 #[tokio::test(flavor = "multi_thread")]
5381 async fn return_at_top_level_errors() {
5382 for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
5384 let code = format!("{header}return 1\n");
5385 assert_eq!(
5386 parse_execute(&code).await.expect_err("should error").message(),
5387 "Cannot return from outside a function."
5388 );
5389 }
5390
5391 let code = r#"@settings(kclVersion = "3.0-preview")
5395x = if true {
5396 return 1
5397 0
5398} else {
5399 0
5400}
5401"#;
5402 assert_eq!(
5403 parse_execute(code).await.expect_err("should error").message(),
5404 "Cannot return from outside a function."
5405 );
5406 }
5407
5408 #[tokio::test(flavor = "multi_thread")]
5409 async fn exit_inside_function_still_exits_program_in_v3() {
5410 let code = r#"@settings(kclVersion = "3.0-preview")
5411fn f() {
5412 exit()
5413 return 1
5414}
5415x = f()
5416assert(1, isEqualTo = 2, error = "code after exit ran")
5417"#;
5418 parse_execute(code).await.unwrap();
5419 }
5420
5421 #[tokio::test(flavor = "multi_thread")]
5422 async fn return_inside_sketch_block_terminates_function_in_v3() {
5423 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
5424fn f() {
5425 sketch(on = XY) {
5426 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
5427 return 42
5428 }
5429 return 0
5430}
5431x = f()
5432"#;
5433 let result = parse_execute(code).await.unwrap();
5434 assert_eq!(variable_f64(&result, "x"), 42.0);
5435 }
5436
5437 #[tokio::test(flavor = "multi_thread")]
5438 async fn return_inside_sketch_block_ignored_without_v3() {
5439 let code = r#"@settings(experimentalFeatures = allow)
5442fn f() {
5443 sketch(on = XY) {
5444 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
5445 return 42
5446 }
5447 return 0
5448}
5449x = f()
5450"#;
5451 let result = parse_execute(code).await.unwrap();
5452 assert_eq!(variable_f64(&result, "x"), 0.0);
5453 }
5454
5455 #[tokio::test(flavor = "multi_thread")]
5456 async fn code_after_return_still_runs_without_v3() {
5457 let code = r#"fn f() {
5458 return 1
5459 assert(1, isEqualTo = 2, error = "ran past return")
5460}
5461x = f()
5462"#;
5463 let err = parse_execute(code).await.expect_err("should error");
5464 assert!(
5465 err.message().contains("ran past return"),
5466 "unexpected message: {}",
5467 err.message()
5468 );
5469 }
5470
5471 #[tokio::test(flavor = "multi_thread")]
5472 async fn multiple_returns_error_without_v3() {
5473 let code = r#"fn f() {
5474 return 1
5475 return 2
5476}
5477x = f()
5478"#;
5479 assert_eq!(
5480 parse_execute(code).await.expect_err("should error").message(),
5481 "Multiple returns from a single function."
5482 );
5483 }
5484
5485 #[tokio::test(flavor = "multi_thread")]
5486 async fn if_arm_return_plus_function_return_errors_without_v3() {
5487 let code = r#"fn f() {
5491 dummy = if true {
5492 return 1
5493 0
5494 } else {
5495 0
5496 }
5497 return 2
5498}
5499x = f()
5500"#;
5501 assert_eq!(
5502 parse_execute(code).await.expect_err("should error").message(),
5503 "Multiple returns from a single function."
5504 );
5505 }
5506
5507 #[tokio::test(flavor = "multi_thread")]
5508 async fn top_level_if_arm_return_ignored_without_v3() {
5509 let code = r#"x = if true {
5513 return 1
5514 0
5515} else {
5516 0
5517}
5518"#;
5519 let result = parse_execute(code).await.unwrap();
5520 assert_eq!(variable_f64(&result, "x"), 0.0);
5521 assert_eq!(variable_f64(&result, memory::RETURN_NAME), 1.0);
5522 }
5523
5524 #[tokio::test(flavor = "multi_thread")]
5527 async fn return_semantics_gated_on_entry_point_not_module() {
5528 let dep = r#"@settings(kclVersion = "3.0-preview")
5533ignored = if true {
5534 return 1
5535 0
5536} else {
5537 0
5538}
5539
5540export fn f() {
5541 return 1
5542 assert(1, isEqualTo = 2, error = "ran past return")
5543}
5544"#;
5545 let main = r#"@settings(kclVersion = 2.0)
5546import f from "dep.kcl"
5547x = f()
5548"#;
5549 let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
5550 assert!(
5551 err.message().contains("ran past return"),
5552 "unexpected message: {}",
5553 err.message()
5554 );
5555
5556 let dep = r#"@settings(kclVersion = 2.0)
5559export fn f() {
5560 return 1
5561 assert(1, isEqualTo = 2, error = "ran past return")
5562}
5563"#;
5564 let main = r#"@settings(kclVersion = "3.0-preview")
5565import f from "dep.kcl"
5566x = f()
5567"#;
5568 let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
5569 assert_eq!(variable_f64(&result, "x"), 1.0);
5570 }
5571
5572 #[tokio::test(flavor = "multi_thread")]
5578 async fn return_inside_map_and_reduce_callbacks_in_v3() {
5579 let code = r#"@settings(kclVersion = "3.0-preview")
5580doubled = map([1, 2, 3], f = fn(@i) {
5581 return i * 2
5582 assert(1, isEqualTo = 2, error = "code after return ran in the map callback")
5583})
5584assert(doubled[0], isEqualTo = 2, error = "map result 0")
5585assert(doubled[1], isEqualTo = 4, error = "map result 1")
5586assert(doubled[2], isEqualTo = 6, error = "map result 2")
5587
5588total = reduce([1, 2, 3], initial = 0, f = fn(@i, accum) {
5589 return accum + i
5590 assert(1, isEqualTo = 2, error = "code after return ran in the reduce callback")
5591})
5592assert(total, isEqualTo = 6, error = "reduce total")
5593"#;
5594 let result = parse_execute(code).await.unwrap();
5595 assert_eq!(variable_f64(&result, "total"), 6.0);
5596 }
5597
5598 #[tokio::test(flavor = "multi_thread")]
5604 async fn early_returns_do_not_leak_machine_call_depth() {
5605 let code = r#"@settings(kclVersion = "3.0-preview")
5606fn one() {
5607 return 1
5608 assert(1, isEqualTo = 2, error = "code after return ran")
5609}
5610total = reduce([1..100], initial = 0, f = fn(@i, accum) {
5611 return accum + one()
5612})
5613assert(total, isEqualTo = 100, error = "each call returns 1")
5614"#;
5615 let result = parse_execute(code).await.unwrap();
5616 let high_water = result.exec_state.global.machine_depth_high_water;
5620 assert!(high_water < 10, "high water: {high_water}");
5621 }
5622
5623 #[tokio::test(flavor = "multi_thread")]
5629 async fn top_level_if_arm_return_in_imported_module_errors_in_v3() {
5630 let dep = r#"@settings(kclVersion = 2.0)
5631x = if true {
5632 return 1
5633 0
5634} else {
5635 0
5636}
5637export y = x
5638"#;
5639 let main = r#"@settings(kclVersion = "3.0-preview")
5640import y from "dep.kcl"
5641z = y
5642"#;
5643 let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
5644 assert!(
5645 err.message().contains("Cannot return from outside a function."),
5646 "unexpected message: {}",
5647 err.message()
5648 );
5649 }
5650
5651 #[tokio::test(flavor = "multi_thread")]
5657 async fn return_of_exit_still_exits_program_in_v3() {
5658 let code = r#"@settings(kclVersion = "3.0-preview")
5659fn f() {
5660 return exit()
5661}
5662x = f()
5663assert(1, isEqualTo = 2, error = "code after exit ran")
5664"#;
5665 parse_execute(code).await.unwrap();
5666 }
5667
5668 #[tokio::test(flavor = "multi_thread")]
5669 async fn if_arm_bindings_do_not_leak_in_v3() {
5670 let code = r#"@settings(kclVersion = "3.0-preview")
5671x = if true {
5672 y = 1
5673 y
5674} else {
5675 0
5676}
5677z = y
5678"#;
5679 let err = parse_execute(code).await.expect_err("should error");
5680 assert!(
5681 err.message().contains("`y` is not defined"),
5682 "unexpected message: {}",
5683 err.message()
5684 );
5685 }
5686
5687 #[tokio::test(flavor = "multi_thread")]
5688 async fn if_arm_bindings_leak_without_v3() {
5689 for header in ["", "@settings(kclVersion = 2.0)\n"] {
5692 let code = format!(
5693 r#"{header}x = if true {{
5694 y = 1
5695 y
5696}} else {{
5697 0
5698}}
5699z = y
5700"#
5701 );
5702 let result = parse_execute(&code).await.unwrap();
5703 assert_eq!(variable_f64(&result, "z"), 1.0);
5704 }
5705 }
5706
5707 #[tokio::test(flavor = "multi_thread")]
5708 async fn if_arm_shadowing_allowed_in_v3() {
5709 let code = r#"@settings(kclVersion = "3.0-preview")
5710y = 1
5711x = if true {
5712 y = 2
5713 y + 10
5714} else {
5715 0
5716}
5717"#;
5718 let result = parse_execute(code).await.unwrap();
5719 assert_eq!(variable_f64(&result, "x"), 12.0);
5720 assert_eq!(variable_f64(&result, "y"), 1.0);
5721 }
5722
5723 #[tokio::test(flavor = "multi_thread")]
5724 async fn if_arm_shadowing_still_errors_without_v3() {
5725 for header in ["", "@settings(kclVersion = 2.0)\n"] {
5728 let code = format!(
5729 r#"{header}y = 1
5730x = if true {{
5731 y = 2
5732 y
5733}} else {{
5734 0
5735}}
5736"#
5737 );
5738 let err = parse_execute(&code).await.expect_err("should error");
5739 assert!(
5740 err.message().contains("Cannot redefine `y`"),
5741 "unexpected message: {}",
5742 err.message()
5743 );
5744 }
5745 }
5746
5747 #[tokio::test(flavor = "multi_thread")]
5748 async fn if_arm_closure_escape_in_v3() {
5749 let code = r#"@settings(kclVersion = "3.0-preview")
5752n = 1
5753f = if true {
5754 m = 41
5755 g = fn() {
5756 return m + n
5757 }
5758 g
5759} else {
5760 g = fn() {
5761 return 0
5762 }
5763 g
5764}
5765x = f()
5766"#;
5767 let result = parse_execute(code).await.unwrap();
5768 assert_eq!(variable_f64(&result, "x"), 42.0);
5769 }
5770
5771 #[tokio::test(flavor = "multi_thread")]
5772 async fn recursive_if_arm_closure_keeps_enclosing_function_frame_alive_in_v3() {
5773 let code = r#"@settings(kclVersion = "3.0-preview")
5777fn makeCounter() {
5778 outer = 40
5779 selected = if true {
5780 inner = 2
5781 fn count(@n) {
5782 return if n == 0 {
5783 outer + inner
5784 } else {
5785 count(n - 1) + 1
5786 }
5787 }
5788 count
5789 } else {
5790 fn fallback(@n) {
5791 return n
5792 }
5793 fallback
5794 }
5795 return selected
5796}
5797counter = makeCounter()
5798x = counter(3)
5799"#;
5800 let result = parse_execute(code).await.unwrap();
5801 assert_eq!(variable_f64(&result, "x"), 45.0);
5802 }
5803
5804 #[tokio::test(flavor = "multi_thread")]
5805 async fn return_inside_scoped_if_arm_in_v3() {
5806 let code = r#"@settings(kclVersion = "3.0-preview")
5809fn f(@b) {
5810 local = if b {
5811 w = 1
5812 return w + 9
5813 0
5814 } else {
5815 0
5816 }
5817 return local
5818}
5819x = f(true)
5820y = f(false)
5821"#;
5822 let result = parse_execute(code).await.unwrap();
5823 assert_eq!(variable_f64(&result, "x"), 10.0);
5824 assert_eq!(variable_f64(&result, "y"), 0.0);
5825 }
5826
5827 #[tokio::test(flavor = "multi_thread")]
5828 async fn else_if_and_nested_if_scoping_in_v3() {
5829 let code = r#"@settings(kclVersion = "3.0-preview")
5830x = if false {
5831 0
5832} else if true {
5833 a = 1
5834 b = if true {
5835 c = 2
5836 a + c
5837 } else {
5838 0
5839 }
5840 a + b
5841} else {
5842 0
5843}
5844"#;
5845 let result = parse_execute(code).await.unwrap();
5846 assert_eq!(variable_f64(&result, "x"), 4.0);
5847
5848 let code = r#"@settings(kclVersion = "3.0-preview")
5850x = if true {
5851 b = if true {
5852 c = 2
5853 c
5854 } else {
5855 0
5856 }
5857 b + c
5858} else {
5859 0
5860}
5861"#;
5862 let err = parse_execute(code).await.expect_err("should error");
5863 assert!(
5864 err.message().contains("`c` is not defined"),
5865 "unexpected message: {}",
5866 err.message()
5867 );
5868 }
5869
5870 #[tokio::test(flavor = "multi_thread")]
5875 async fn else_if_and_final_else_arms_are_isolated_in_v3() {
5876 let code = r#"@settings(kclVersion = "3.0-preview")
5878x = if false {
5879 0
5880} else if true {
5881 y = 1
5882 y
5883} else {
5884 0
5885}
5886z = y
5887"#;
5888 let err = parse_execute(code).await.expect_err("should error");
5889 assert!(
5890 err.message().contains("`y` is not defined"),
5891 "unexpected message: {}",
5892 err.message()
5893 );
5894
5895 let code = r#"@settings(kclVersion = "3.0-preview")
5897x = if false {
5898 0
5899} else if false {
5900 0
5901} else {
5902 y = 1
5903 y
5904}
5905z = y
5906"#;
5907 let err = parse_execute(code).await.expect_err("should error");
5908 assert!(
5909 err.message().contains("`y` is not defined"),
5910 "unexpected message: {}",
5911 err.message()
5912 );
5913
5914 let code = r#"@settings(kclVersion = "3.0-preview")
5916outer = 1
5917x = if false {
5918 0
5919} else if true {
5920 outer = 2
5921 outer + 10
5922} else {
5923 0
5924}
5925"#;
5926 let result = parse_execute(code).await.unwrap();
5927 assert_eq!(variable_f64(&result, "x"), 12.0);
5928 assert_eq!(variable_f64(&result, "outer"), 1.0);
5929
5930 let code = r#"@settings(kclVersion = "3.0-preview")
5932outer = 1
5933x = if false {
5934 0
5935} else if false {
5936 0
5937} else {
5938 outer = 2
5939 outer + 10
5940}
5941"#;
5942 let result = parse_execute(code).await.unwrap();
5943 assert_eq!(variable_f64(&result, "x"), 12.0);
5944 assert_eq!(variable_f64(&result, "outer"), 1.0);
5945 }
5946
5947 #[tokio::test(flavor = "multi_thread")]
5951 async fn else_if_and_final_else_arm_bindings_leak_without_v3() {
5952 for header in ["", "@settings(kclVersion = 2.0)\n"] {
5953 let code = format!(
5954 r#"{header}x = if false {{
5955 0
5956}} else if true {{
5957 y = 1
5958 y
5959}} else {{
5960 0
5961}}
5962z = y
5963"#
5964 );
5965 let result = parse_execute(&code).await.unwrap();
5966 assert_eq!(variable_f64(&result, "z"), 1.0, "code={code}");
5967
5968 let code = format!(
5969 r#"{header}x = if false {{
5970 0
5971}} else if false {{
5972 0
5973}} else {{
5974 y = 1
5975 y
5976}}
5977z = y
5978"#
5979 );
5980 let result = parse_execute(&code).await.unwrap();
5981 assert_eq!(variable_f64(&result, "z"), 1.0, "code={code}");
5982
5983 let code = format!(
5984 r#"{header}outer = 1
5985x = if false {{
5986 0
5987}} else if true {{
5988 outer = 2
5989 outer
5990}} else {{
5991 0
5992}}
5993"#
5994 );
5995 let err = parse_execute(&code).await.expect_err("should error");
5996 assert!(
5997 err.message().contains("Cannot redefine `outer`"),
5998 "unexpected message: {}",
5999 err.message()
6000 );
6001 }
6002 }
6003
6004 #[tokio::test(flavor = "multi_thread")]
6005 async fn error_inside_if_arm_unwinds_balanced_in_v3() {
6006 let code = r#"@settings(kclVersion = "3.0-preview")
6009fn f() {
6010 dummy = if true {
6011 assert(1, isEqualTo = 2, error = "boom")
6012 0
6013 } else {
6014 0
6015 }
6016 return dummy
6017}
6018x = f()
6019"#;
6020 let err = parse_execute(code).await.expect_err("should error");
6021 assert!(err.message().contains("boom"), "unexpected message: {}", err.message());
6022 }
6023
6024 #[tokio::test(flavor = "multi_thread")]
6025 async fn exit_inside_scoped_if_arm_in_v3() {
6026 let code = r#"@settings(kclVersion = "3.0-preview")
6027fn f() {
6028 dummy = if true {
6029 exit()
6030 0
6031 } else {
6032 0
6033 }
6034 return dummy
6035}
6036x = f()
6037assert(1, isEqualTo = 2, error = "code after exit ran")
6038"#;
6039 parse_execute(code).await.unwrap();
6040 }
6041
6042 #[tokio::test(flavor = "multi_thread")]
6045 async fn if_arm_scoping_gated_on_entry_point_not_module() {
6046 let dep = r#"@settings(kclVersion = "3.0-preview")
6049ignored = if true {
6050 leaked = 1
6051 leaked
6052} else {
6053 0
6054}
6055export leakCheck = leaked
6056"#;
6057 let main = r#"@settings(kclVersion = 2.0)
6058import leakCheck from "dep.kcl"
6059x = leakCheck
6060"#;
6061 let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
6062 assert_eq!(variable_f64(&result, "x"), 1.0);
6063
6064 let dep = r#"@settings(kclVersion = 2.0)
6067ignored = if true {
6068 arm = 1
6069 arm
6070} else {
6071 0
6072}
6073export fn leakCheck() {
6074 return arm
6075}
6076"#;
6077 let main = r#"@settings(kclVersion = "3.0-preview")
6078import leakCheck from "dep.kcl"
6079x = leakCheck()
6080"#;
6081 let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
6082 assert!(
6083 err.message().contains("`arm` is not defined"),
6084 "unexpected message: {}",
6085 err.message()
6086 );
6087 }
6088
6089 #[tokio::test(flavor = "multi_thread")]
6094 async fn unwind_through_sketch_block_inside_scoped_if_arm_in_v3() {
6095 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
6098fn f() {
6099 dummy = if true {
6100 s = sketch(on = XY) {
6101 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
6102 q = notDefinedAnywhere
6103 }
6104 0
6105 } else {
6106 0
6107 }
6108 return dummy
6109}
6110x = f()
6111"#;
6112 let err = parse_execute(code).await.unwrap_err();
6113 assert!(
6114 err.message().contains("`notDefinedAnywhere` is not defined"),
6115 "unexpected message: {}",
6116 err.message()
6117 );
6118
6119 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
6121fn f() {
6122 dummy = if true {
6123 s = sketch(on = XY) {
6124 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
6125 e = exit()
6126 }
6127 0
6128 } else {
6129 0
6130 }
6131 return dummy
6132}
6133x = f()
6134assert(1, isEqualTo = 2, error = "code after exit ran")
6135"#;
6136 parse_execute(code).await.unwrap();
6137
6138 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
6140fn g() {
6141 dummy = if true {
6142 s = sketch(on = XY) {
6143 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
6144 return 42
6145 }
6146 0
6147 } else {
6148 0
6149 }
6150 return 0
6151}
6152y = g()
6153"#;
6154 let result = parse_execute(code).await.unwrap();
6155 assert_eq!(variable_f64(&result, "y"), 42.0);
6156 }
6157
6158 #[tokio::test(flavor = "multi_thread")]
6162 async fn tag_declared_inside_if_arm_is_arm_local_in_v3() {
6163 let arm_body = r#"p = if true {
6164 profile = startSketchOn(XY)
6165 |> startProfile(at = [0, 0])
6166 |> line(end = [10, 0], tag = $edge)
6167 |> line(end = [0, 10])
6168 |> line(end = [-10, 0])
6169 |> close()
6170 inArmLen = segLen(edge)
6171 assert(inArmLen, isEqualTo = 10, error = "tag is usable within its arm")
6172 profile
6173} else {
6174 startSketchOn(XY)
6175 |> startProfile(at = [0, 0])
6176 |> line(end = [5, 0])
6177 |> line(end = [0, 5])
6178 |> line(end = [-5, 0])
6179 |> close()
6180}
6181len = segLen(edge)
6182"#;
6183
6184 let code = format!("@settings(kclVersion = \"3.0-preview\")\n{arm_body}");
6185 let err = parse_execute(&code).await.unwrap_err();
6186 assert!(
6187 err.message().contains("`edge` is not defined"),
6188 "unexpected message: {}",
6189 err.message()
6190 );
6191
6192 let result = parse_execute(arm_body).await.unwrap();
6194 assert_eq!(variable_f64(&result, "len"), 10.0);
6195 }
6196
6197 #[tokio::test(flavor = "multi_thread")]
6203 async fn if_arm_scopes_do_not_retain_function_frames_in_v3() {
6204 let code = r#"@settings(kclVersion = "3.0-preview")
6205fn pick(@i) {
6206 r = if i > 50 {
6207 a = i * 2
6208 a
6209 } else {
6210 b = i + 1
6211 b
6212 }
6213 return r
6214}
6215results = map([1..100], f = fn(@i) { return pick(i) })
6216assert(results[0], isEqualTo = 2, error = "pick(1) = 2")
6217assert(results[99], isEqualTo = 200, error = "pick(100) = 200")
6218"#;
6219 let result = parse_execute(code).await.unwrap();
6220 let retained = result.exec_state.stack().memory.envs_with_bindings();
6223 assert!(retained < 20, "retained environments: {retained}");
6224 }
6225
6226 #[tokio::test(flavor = "multi_thread")]
6231 async fn if_arm_scoping_inside_pipe_in_v3() {
6232 let code = r#"@settings(kclVersion = "3.0-preview")
6233cond = true
6234result = 5
6235 |> if cond {
6236 a = 20
6237 a
6238 } else {
6239 0
6240 }
6241 |> max([%, 1])
6242"#;
6243 let result = parse_execute(code).await.unwrap();
6244 assert_eq!(variable_f64(&result, "result"), 20.0);
6248
6249 let code = r#"@settings(kclVersion = "3.0-preview")
6251cond = true
6252result = 5
6253 |> if cond {
6254 a = 20
6255 a
6256 } else {
6257 0
6258 }
6259leaked = a
6260"#;
6261 let err = parse_execute(code).await.unwrap_err();
6262 assert!(
6263 err.message().contains("`a` is not defined"),
6264 "unexpected message: {}",
6265 err.message()
6266 );
6267 }
6268
6269 #[tokio::test(flavor = "multi_thread")]
6270 async fn member_expression_evaluates_object_before_property_in_v3() {
6271 let code = r#"@settings(kclVersion = "3.0-preview")
6274x = a[b]
6275"#;
6276 let err = parse_execute(code).await.expect_err("should error");
6277 assert_eq!(err.message(), "`a` is not defined");
6278 }
6279
6280 #[tokio::test(flavor = "multi_thread")]
6281 async fn member_expression_evaluates_property_before_object_without_v3() {
6282 let code = r#"@settings(kclVersion = 2.0)
6285x = a[b]
6286"#;
6287 let err = parse_execute(code).await.expect_err("should error");
6288 assert_eq!(err.message(), "`b` is not defined");
6289 }
6290
6291 #[tokio::test(flavor = "multi_thread")]
6292 async fn member_expression_undefined_object_with_static_property_in_v3() {
6293 let code = r#"@settings(kclVersion = "3.0-preview")
6294x = a.b
6295"#;
6296 let err = parse_execute(code).await.expect_err("should error");
6297 assert_eq!(err.message(), "`a` is not defined");
6298 }
6299
6300 #[tokio::test(flavor = "multi_thread")]
6301 async fn member_expression_values_in_v3() {
6302 let code = r#"@settings(kclVersion = "3.0-preview")
6305fn xs() {
6306 return [10, 20, 30]
6307}
6308fn one() {
6309 return 1
6310}
6311obj = { inner = { xs = xs() } }
6312objs = [obj, obj]
6313a = obj.inner.xs[one()]
6314b = xs()[one() + 1]
6315c = objs[0].inner.xs[0]
6316"#;
6317 let result = parse_execute(code).await.unwrap();
6318 assert_eq!(variable_f64(&result, "a"), 20.0);
6319 assert_eq!(variable_f64(&result, "b"), 30.0);
6320 assert_eq!(variable_f64(&result, "c"), 10.0);
6321 }
6322
6323 #[tokio::test(flavor = "multi_thread")]
6324 async fn exit_inside_member_expression_in_v3() {
6325 for code in [
6328 r#"@settings(kclVersion = "3.0-preview")
6329x = exit()[0]
6330assert(1, isEqualTo = 2, error = "code after exit ran")
6331"#,
6332 r#"@settings(kclVersion = "3.0-preview")
6333arr = [1]
6334x = arr[exit()]
6335assert(1, isEqualTo = 2, error = "code after exit ran")
6336"#,
6337 ] {
6338 parse_execute(code).await.unwrap();
6339 }
6340 }
6341
6342 #[tokio::test(flavor = "multi_thread")]
6343 async fn experimental_parameter() {
6344 let code = r#"
6345fn inc(@x, @(experimental = true) amount? = 1) {
6346 return x + amount
6347}
6348
6349answer = inc(5, amount = 2)
6350"#;
6351 let result = parse_execute(code).await.unwrap();
6352 let issues = result.exec_state.issues();
6353 assert_eq!(issues.len(), 1);
6354 assert_eq!(issues[0].severity, Severity::Error);
6355 let msg = &issues[0].message;
6356 assert!(msg.contains("experimental"), "found {msg}");
6357
6358 let code = r#"
6360fn inc(@x, @(experimental = true) amount? = 1) {
6361 return x + amount
6362}
6363
6364answer = inc(5)
6365"#;
6366 let result = parse_execute(code).await.unwrap();
6367 let issues = result.exec_state.issues();
6368 assert!(issues.is_empty(), "issues={issues:#?}");
6369 }
6370
6371 #[tokio::test(flavor = "multi_thread")]
6372 async fn experimental_scalar_fixed_constraint() {
6373 let code_left = r#"@settings(experimentalFeatures = warn)
6374sketch(on = XY) {
6375 point1 = point(at = [var 0mm, var 0mm])
6376 point1.at[0] == 1mm
6377}
6378"#;
6379 let code_right = r#"@settings(experimentalFeatures = warn)
6381sketch(on = XY) {
6382 point1 = point(at = [var 0mm, var 0mm])
6383 1mm == point1.at[0]
6384}
6385"#;
6386
6387 for code in [code_left, code_right] {
6388 let result = parse_execute(code).await.unwrap();
6389 let issues = result.exec_state.issues();
6390 let Some(error) = issues
6391 .iter()
6392 .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
6393 else {
6394 panic!("found {issues:#?}");
6395 };
6396 assert_eq!(error.severity, Severity::Warning);
6397 }
6398 }
6399
6400 #[tokio::test(flavor = "multi_thread")]
6404 async fn test_tangent_line_arc_executes_with_mock_engine() {
6405 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
6406 parse_execute(&code).await.unwrap();
6407 }
6408
6409 #[tokio::test(flavor = "multi_thread")]
6410 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
6411 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
6412 parse_execute(&code).await.unwrap();
6413 }
6414
6415 #[tokio::test(flavor = "multi_thread")]
6416 async fn test_tangent_line_circle_executes_with_mock_engine() {
6417 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
6418 parse_execute(&code).await.unwrap();
6419 }
6420
6421 #[tokio::test(flavor = "multi_thread")]
6422 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
6423 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
6424 parse_execute(&code).await.unwrap();
6425 }
6426
6427 #[tokio::test(flavor = "multi_thread")]
6428 async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
6429 let code = r#"startX = 2
6430
6431baseSketch = sketch(on = XY) {
6432 yoyo = line(start = [startX, 0], end = [7, 6])
6433 line2 = line(start = [7, 6], end = [7, 12])
6434 hi = line(start = [7, 12], end = [startX, 0])
6435}
6436
6437baseRegion = region(point = [5.5, 6], sketch = baseSketch)
6438myExtrude = extrude(
6439 baseRegion,
6440 length = 5,
6441 tagEnd = $endCap,
6442 tagStart = $startCap,
6443)
6444yodawg = getCommonEdge(faces = [
6445 baseRegion.tags.hi,
6446 baseRegion.tags.yoyo
6447])
6448
6449cutSketch = sketch(on = YZ) {
6450 myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
6451 myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
6452 line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
6453}
6454
6455cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
6456extrude001 = extrude(cutRegion, length = 5)
6457solid001 = subtract(myExtrude, tools = extrude001)
6458
6459yoyo = getOppositeEdge(baseRegion.tags.hi)
6460fillet(solid001, radius = 0.1, tags = yoyo)
6461"#;
6462
6463 parse_execute(code).await.unwrap();
6464 }
6465
6466 async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
6471 let program = crate::Program::parse_no_errs(kcl).unwrap();
6472 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
6473 let mut exec_state = ExecState::new(&ctx);
6474 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
6475 let outcome = exec_state
6476 .into_exec_outcome(env_ref, &ctx)
6477 .await
6478 .expect("constraint report test outcome should collect variables");
6479 let report = outcome.sketch_constraint_report();
6480 ctx.close().await;
6481 report
6482 }
6483
6484 #[tokio::test(flavor = "multi_thread")]
6485 async fn warn_when_sketch_is_over_constrained() {
6486 let code = r#"
6487sketch001 = sketch(on = XY) {
6488 line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
6489 fixed([line1.start, ORIGIN])
6490 fixed([line1.start, [20, 20]])
6491}
6492"#;
6493 let result = parse_execute(code).await.unwrap();
6494 let issues = result.exec_state.issues();
6495 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
6496 panic!("expected over-constrained warning; found {issues:#?}");
6497 };
6498 assert_eq!(warning.severity, Severity::Warning);
6499 }
6500
6501 #[tokio::test(flavor = "multi_thread")]
6502 async fn over_constrained_warning_identifies_signed_vertical_distance_direction() {
6503 let code = r#"
6504sketch001 = sketch(on = XY) {
6505 line1 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
6506 fixed([line1.start, [0mm, 10mm]])
6507 fixed([line1.end, ORIGIN])
6508 verticalDistance([line1.start, line1.end]) == 10mm
6509}
6510"#;
6511 let result = parse_execute(code).await.unwrap();
6512 let issues = result.exec_state.issues();
6513 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
6514 panic!("expected over-constrained warning; found {issues:#?}");
6515 };
6516 assert!(
6517 warning.message.contains(
6518 "Unsatisfied signed verticalDistance constraint: a positive right-hand side requires the second point to be above the first"
6519 ),
6520 "expected signed-direction diagnostic; found {warning:#?}"
6521 );
6522 }
6523
6524 #[tokio::test(flavor = "multi_thread")]
6525 async fn no_warning_when_sketch_is_not_over_constrained() {
6526 let code = r#"
6528sketch001 = sketch(on = XY) {
6529 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
6530}
6531"#;
6532 let result = parse_execute(code).await.unwrap();
6533 let issues = result.exec_state.issues();
6534 assert!(
6535 !issues.iter().any(|issue| issue.message.contains("over-constrained")),
6536 "did not expect over-constrained warning; found {issues:#?}"
6537 );
6538 }
6539
6540 #[tokio::test(flavor = "multi_thread")]
6541 async fn test_constraint_report_fully_constrained() {
6542 let kcl = r#"
6544@settings(experimentalFeatures = allow)
6545
6546sketch(on = YZ) {
6547 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
6548 line1.start.at[0] == 2
6549 line1.start.at[1] == 8
6550 line1.end.at[0] == 5
6551 line1.end.at[1] == 7
6552}
6553"#;
6554 let report = run_constraint_report(kcl).await;
6555 assert_eq!(report.fully_constrained.len(), 1);
6556 assert_eq!(report.under_constrained.len(), 0);
6557 assert_eq!(report.over_constrained.len(), 0);
6558 assert_eq!(report.errors.len(), 0);
6559 assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
6560 }
6561
6562 #[tokio::test(flavor = "multi_thread")]
6563 async fn test_constraint_report_under_constrained() {
6564 let kcl = r#"
6566sketch(on = YZ) {
6567 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
6568}
6569"#;
6570 let report = run_constraint_report(kcl).await;
6571 assert_eq!(report.fully_constrained.len(), 0);
6572 assert_eq!(report.under_constrained.len(), 1);
6573 assert_eq!(report.over_constrained.len(), 0);
6574 assert_eq!(report.errors.len(), 0);
6575 assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
6576 assert!(report.under_constrained[0].free_count > 0);
6577 }
6578
6579 #[tokio::test(flavor = "multi_thread")]
6580 async fn test_constraint_report_over_constrained() {
6581 let kcl = r#"
6583@settings(experimentalFeatures = allow)
6584
6585sketch(on = YZ) {
6586 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
6587 line1.start.at[0] == 2
6588 line1.start.at[1] == 8
6589 line1.end.at[0] == 5
6590 line1.end.at[1] == 7
6591 distance([line1.start, line1.end]) == 100mm
6592}
6593"#;
6594 let report = run_constraint_report(kcl).await;
6595 assert_eq!(report.over_constrained.len(), 1);
6596 assert_eq!(report.errors.len(), 0);
6597 assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
6598 assert!(report.over_constrained[0].conflict_count > 0);
6599 }
6600
6601 #[tokio::test(flavor = "multi_thread")]
6602 async fn test_constraint_report_multiple_sketches() {
6603 let kcl = r#"
6605@settings(experimentalFeatures = allow)
6606
6607s1 = sketch(on = YZ) {
6608 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
6609 line1.start.at[0] == 2
6610 line1.start.at[1] == 8
6611 line1.end.at[0] == 5
6612 line1.end.at[1] == 7
6613}
6614
6615s2 = sketch(on = XZ) {
6616 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
6617}
6618"#;
6619 let report = run_constraint_report(kcl).await;
6620 assert_eq!(
6621 report.fully_constrained.len()
6622 + report.under_constrained.len()
6623 + report.over_constrained.len()
6624 + report.errors.len(),
6625 2,
6626 "Expected 2 sketches total"
6627 );
6628 assert_eq!(report.fully_constrained.len(), 1);
6629 assert_eq!(report.under_constrained.len(), 1);
6630 }
6631
6632 #[tokio::test(flavor = "multi_thread")]
6633 async fn test_constraint_report_reports_sketch_names() {
6634 let kcl = r#"
6639@settings(experimentalFeatures = allow)
6640
6641fixedSketch = sketch(on = YZ) {
6642 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
6643 line1.start.at[0] == 2
6644 line1.start.at[1] == 8
6645 line1.end.at[0] == 5
6646 line1.end.at[1] == 7
6647}
6648
6649looseSketch = sketch(on = XZ) {
6650 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
6651}
6652
6653conflictSketch = sketch(on = XY) {
6654 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
6655 line1.start.at[0] == 2
6656 line1.start.at[1] == 8
6657 line1.end.at[0] == 5
6658 line1.end.at[1] == 7
6659 distance([line1.start, line1.end]) == 100mm
6660}
6661"#;
6662 let report = run_constraint_report(kcl).await;
6663 assert_eq!(report.errors.len(), 0);
6664 assert_eq!(report.fully_constrained.len(), 1);
6665 assert_eq!(report.under_constrained.len(), 1);
6666 assert_eq!(report.over_constrained.len(), 1);
6667 assert_eq!(report.fully_constrained[0].name, "fixedSketch");
6668 assert_eq!(report.under_constrained[0].name, "looseSketch");
6669 assert_eq!(report.over_constrained[0].name, "conflictSketch");
6670 }
6671
6672 #[tokio::test(flavor = "multi_thread")]
6673 async fn test_constraint_report_name_empty_without_declaration() {
6674 let kcl = r#"
6678sketch(on = YZ) {
6679 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
6680}
6681"#;
6682 let report = run_constraint_report(kcl).await;
6683 assert_eq!(report.under_constrained.len(), 1);
6684 assert_eq!(report.under_constrained[0].name, "");
6685 }
6686
6687 #[tokio::test(flavor = "multi_thread")]
6688 async fn test_constraint_report_names_repeat_across_calls() {
6689 let kcl = r#"
6694fn makeSketch() {
6695 inner = sketch(on = XY) {
6696 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
6697 }
6698 return inner
6699}
6700
6701first = makeSketch()
6702second = makeSketch()
6703"#;
6704 let report = run_constraint_report(kcl).await;
6705 assert_eq!(report.under_constrained.len(), 2);
6706 assert_eq!(report.under_constrained[0].name, "inner");
6707 assert_eq!(report.under_constrained[1].name, "inner");
6708 }
6709
6710 #[tokio::test(flavor = "multi_thread")]
6711 async fn test_enum_declaration_is_experimental() {
6712 let code = "type Color { | Red }";
6715 assert_eq!(
6716 parse_execute(code).await.unwrap_err().message(),
6717 "Use of enum declarations is experimental and may change or be removed."
6718 );
6719 }
6720
6721 #[tokio::test(flavor = "multi_thread")]
6722 async fn enum_declaration_registers_type() {
6723 let code = r#"@settings(experimentalFeatures = allow)
6727type Color { | Red | Green }
6728"#;
6729 parse_execute(code).await.unwrap();
6730
6731 let code = r#"@settings(experimentalFeatures = allow)
6732export type Color { | Red | Green }
6733"#;
6734 parse_execute(code).await.unwrap();
6735
6736 let code = r#"@settings(experimentalFeatures = allow)
6738type Empty { | }
6739"#;
6740 parse_execute(code).await.unwrap();
6741 }
6742
6743 #[tokio::test(flavor = "multi_thread")]
6744 async fn enum_declaration_rejects_nested_scope() {
6745 let allow = "@settings(experimentalFeatures = allow)\n";
6752 for (case, code) in [
6753 (
6754 "function body",
6755 format!("{allow}fn palette() {{\n type Color {{ | Red }}\n return 0\n}}\npalette()\n"),
6756 ),
6757 (
6758 "sketch block",
6759 format!(
6760 "{allow}sketch(on = XY) {{\n type Color {{ | Red }}\n l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
6761 ),
6762 ),
6763 (
6764 "if arm",
6765 format!("{allow}x = if true {{\n type Color {{ | Red }}\n 0\n}} else {{\n 0\n}}\n"),
6766 ),
6767 ] {
6768 assert_eq!(
6769 parse_execute(&code).await.unwrap_err().message(),
6770 "Enum declarations are only supported at the top-level of a file. Move `type Color` to the top-level.",
6771 "case: {case}"
6772 );
6773 }
6774 }
6775
6776 #[tokio::test(flavor = "multi_thread")]
6777 async fn enum_alone_is_restricted_to_top_level() {
6778 let allow = "@settings(experimentalFeatures = allow)\n";
6785 for (case, code) in [
6786 (
6787 "function body",
6788 format!("{allow}fn f() {{\n type Temperature = number(_)\n return 0\n}}\nx = f()\n"),
6789 ),
6790 (
6791 "sketch block",
6792 format!(
6793 "{allow}sketch(on = XY) {{\n type Temperature = number(_)\n l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
6794 ),
6795 ),
6796 ] {
6797 parse_execute(&code)
6798 .await
6799 .unwrap_or_else(|err| panic!("a type alias should be allowed in a {case}: {}", err.message()));
6800 }
6801 }
6802
6803 #[tokio::test(flavor = "multi_thread")]
6804 async fn enum_declaration_rejects_duplicate() {
6805 let code = r#"@settings(experimentalFeatures = allow)
6806type Color { | Red | Green | Red }
6807"#;
6808 assert_eq!(
6809 parse_execute(code).await.unwrap_err().message(),
6810 "Duplicate variant `Red` in enum `Color`."
6811 );
6812 }
6813
6814 async fn execute_with_modules(main: &str, modules: &[(&str, &str)]) -> Result<ExecTestResults, KclError> {
6816 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_enum_clash").unwrap();
6817 for (name, source) in modules {
6818 tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
6819 }
6820
6821 parse_execute_with_project_dir(main, Some(crate::TypedPath(tmpdir.path().into()))).await
6822 }
6823
6824 async fn issues_with_empty_module(main: &str) -> Vec<crate::errors::CompilationIssue> {
6834 use futures::FutureExt;
6835
6836 let project_dir = crate::TypedPath::new("/zma-kcl-member-ranges");
6837 let files = [(project_dir.join("m.kcl").to_string(), Vec::new())]
6840 .into_iter()
6841 .collect();
6842
6843 let program = crate::Program::parse_no_errs(main).unwrap();
6844 let ctx = ExecutorContext {
6845 engine: Arc::new(EngineManager::new_mock()),
6846 engine_batch: EngineBatchContext::default(),
6847 fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
6848 settings: ExecutorSettings {
6849 project_directory: Some(project_dir),
6850 ..Default::default()
6851 },
6852 context_type: ContextType::Mock,
6853 execution_callbacks: Default::default(),
6854 executor_kind: machine::ExecutorKind::resolve(),
6855 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
6856 };
6857 let mut exec_state = ExecState::new(&ctx);
6858 let run_result = std::panic::AssertUnwindSafe(ctx.run(&program, &mut exec_state))
6862 .catch_unwind()
6863 .await;
6864 ctx.close().await;
6865 if let Err(panic) = run_result {
6866 std::panic::resume_unwind(panic);
6867 }
6868 exec_state.issues().to_vec()
6869 }
6870
6871 #[tokio::test(flavor = "multi_thread")]
6872 async fn member_object_diagnostics_use_object_range() {
6873 for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
6879 let main = format!("{header}import \"m.kcl\" as m\nx = m.field\n");
6880 let issues = issues_with_empty_module(&main).await;
6881 let warning = issues
6882 .iter()
6883 .find(|issue| issue.message.contains("no return value"))
6884 .expect("missing-return warning should be recorded");
6885 let object_start = main.rfind("m.field").unwrap();
6886 assert_eq!(
6887 (warning.source_range.start(), warning.source_range.end()),
6888 (object_start, object_start + 1),
6889 "warning should point at the object's span (header={header:?})"
6890 );
6891 }
6892 }
6893
6894 #[tokio::test(flavor = "multi_thread")]
6895 async fn member_property_diagnostics_use_property_range() {
6896 for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
6899 let main = format!("{header}import \"m.kcl\" as m\narr = [1]\nx = arr[m]\n");
6900 let issues = issues_with_empty_module(&main).await;
6901 let warning = issues
6902 .iter()
6903 .find(|issue| issue.message.contains("no return value"))
6904 .expect("missing-return warning should be recorded");
6905 let prop_start = main.rfind("[m]").unwrap() + 1;
6906 assert_eq!(
6907 (warning.source_range.start(), warning.source_range.end()),
6908 (prop_start, prop_start + 1),
6909 "warning should point at the property's span (header={header:?})"
6910 );
6911 }
6912 }
6913
6914 #[tokio::test(flavor = "multi_thread")]
6915 async fn backtrace_reports_fully_qualified_fn_names() {
6916 let main = "import \"m.kcl\" as m\nx = m::f()\n";
6920 let modules = [("m.kcl", "export fn f() {\n return undefinedVariable\n}\n")];
6921 let err = execute_with_modules(main, &modules).await.unwrap_err();
6922 let fn_names: Vec<_> = err.backtrace().into_iter().filter_map(|item| item.fn_name).collect();
6923 assert_eq!(fn_names, vec!["m::f".to_owned()]);
6924 }
6925
6926 #[tokio::test(flavor = "multi_thread")]
6927 async fn whole_module_name_executes_as_operand() {
6928 let main = r#"import "m.kcl" as m
6932sum = m + m
6933neg = -m
6934"#;
6935 let result = execute_with_modules(main, &[("m.kcl", "42\n")]).await.unwrap();
6936 assert_eq!(
6937 mem_get_json(result.exec_state.stack(), result.mem_env, "sum").as_f64(),
6938 Some(84.0)
6939 );
6940 assert_eq!(
6941 mem_get_json(result.exec_state.stack(), result.mem_env, "neg").as_f64(),
6942 Some(-42.0)
6943 );
6944 }
6945
6946 #[tokio::test(flavor = "multi_thread")]
6947 async fn whole_module_without_return_as_operand_errors() {
6948 let main = "import \"m.kcl\" as m
6953x = m + 1
6954";
6955 let err = execute_with_modules(main, &[("m.kcl", "")]).await.unwrap_err();
6956 assert!(
6957 err.message().contains("Expected a number, but found none"),
6958 "expected the operand to be the module's missing-return KclNone, got: {}",
6959 err.message()
6960 );
6961 }
6962
6963 #[tokio::test(flavor = "multi_thread")]
6964 async fn enum_rejects_name_clash_with_module() {
6965 let plain_module = ("Color.kcl", "export x = 1\n");
6970 let enum_module = (
6971 "enums.kcl",
6972 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
6973 );
6974
6975 for (case, main, modules) in [
6976 (
6977 "module then enum",
6978 "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\ntype Color { | Red }\n",
6979 vec![plain_module],
6980 ),
6981 (
6982 "enum then module",
6983 "@settings(experimentalFeatures = allow)\ntype Color { | Red }\nimport \"Color.kcl\"\n",
6984 vec![plain_module],
6985 ),
6986 (
6987 "named import of an enum",
6988 "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport Color from 'enums.kcl'\n",
6989 vec![plain_module, enum_module],
6990 ),
6991 (
6992 "glob import of an enum",
6993 "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport * from 'enums.kcl'\n",
6994 vec![plain_module, enum_module],
6995 ),
6996 ] {
6997 let err = execute_with_modules(main, &modules).await.unwrap_err();
6998 assert_eq!(
6999 err.message(),
7000 "An enum and a module cannot share the name `Color` in the same scope, because `Color::x` would be ambiguous. Rename one of them.",
7001 "case: {case}"
7002 );
7003 }
7004 }
7005
7006 #[tokio::test(flavor = "multi_thread")]
7007 async fn enum_constructs_variant() {
7008 let allow = "@settings(experimentalFeatures = allow)\n";
7009 let colors = (
7010 "colors.kcl",
7011 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
7012 );
7013
7014 for (case, main, modules) in [
7015 (
7016 "declared locally",
7017 format!("{allow}type Color {{ | Red | Green }}\nx = Color::Red\n"),
7018 vec![],
7019 ),
7020 (
7021 "reached through a module path",
7024 format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
7025 vec![colors],
7026 ),
7027 (
7028 "imported by name",
7029 format!("{allow}import Color from 'colors.kcl'\nx = Color::Red\n"),
7030 vec![colors],
7031 ),
7032 (
7033 "imported under an alias",
7036 format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade::Red\n"),
7037 vec![colors],
7038 ),
7039 ] {
7040 let result = execute_with_modules(&main, &modules)
7041 .await
7042 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
7043 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
7044 panic!("case: {case}: `x` should hold an enum value");
7045 };
7046 assert_eq!(value.qualified_name(), "Color::Red", "case: {case}");
7047 }
7048 }
7049
7050 #[tokio::test(flavor = "multi_thread")]
7058 async fn signature_types_resolve_in_declaring_module() {
7059 let colors = (
7060 "colors.kcl",
7061 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n return c\n}\n",
7062 );
7063 let main =
7066 "@settings(experimentalFeatures = allow)\nimport \"colors.kcl\"\nr = colors::paint(colors::Color::Red)\n";
7067
7068 let result = execute_with_modules(main, &[colors]).await.unwrap();
7069 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
7070 panic!("`r` should hold an enum value");
7071 };
7072 assert_eq!(value.qualified_name(), "Color::Red");
7073 }
7074
7075 #[tokio::test(flavor = "multi_thread")]
7076 async fn signature_types_resolve_under_import_alias() {
7077 let colors = (
7081 "colors.kcl",
7082 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n return c\n}\n",
7083 );
7084 let main = "@settings(experimentalFeatures = allow)\nimport \"colors.kcl\" as painter\nr = painter::paint(painter::Color::Red)\n";
7085
7086 let result = execute_with_modules(main, &[colors]).await.unwrap();
7087 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
7088 panic!("`r` should hold an enum value");
7089 };
7090 assert_eq!(value.qualified_name(), "Color::Red");
7091 }
7092
7093 #[tokio::test(flavor = "multi_thread")]
7094 async fn signature_types_ignore_caller_scope() {
7095 let broken = (
7100 "broken.kcl",
7101 "@settings(experimentalFeatures = allow)\nexport fn f(@x: Missing) {\n return x\n}\n",
7102 );
7103 let main = "@settings(experimentalFeatures = allow)\ntype Missing = string\nimport \"broken.kcl\"\nr = broken::f(\"hi\")\n";
7104
7105 let err = execute_with_modules(main, &[broken]).await.unwrap_err();
7106 assert!(
7107 err.message().contains("Unknown type: Missing"),
7108 "message: {}",
7109 err.message()
7110 );
7111 }
7112
7113 #[tokio::test(flavor = "multi_thread")]
7114 async fn signature_types_reject_forward_reference() {
7115 let main = "@settings(experimentalFeatures = allow)\nfn f(@x: Later) {\n return x\n}\ntype Later = string\n";
7119
7120 let err = parse_execute(main).await.unwrap_err();
7121 assert!(
7122 err.message().contains("Unknown type: Later"),
7123 "message: {}",
7124 err.message()
7125 );
7126 }
7127
7128 #[tokio::test(flavor = "multi_thread")]
7129 async fn signature_types_resolve_in_enclosing_scope() {
7130 let main = "@settings(experimentalFeatures = allow)\ntype Width = string\nfn makeMeasure() {\n type Width = number(mm)\n return fn(@w: Width) { return w }\n}\nmeasure = makeMeasure()\nr = measure(42)\n";
7135
7136 let result = parse_execute(main).await.unwrap();
7137 let KclValue::Number { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
7138 panic!("`r` should hold a number");
7139 };
7140 assert_eq!(value, 42.0);
7141 }
7142
7143 #[tokio::test(flavor = "multi_thread")]
7153 async fn signature_number_types_ignore_module_default_units() {
7154 let units_in = (
7155 "units_in.kcl",
7156 "@settings(defaultLengthUnit = in)\nexport fn passThrough(@x: number(Length)) {\n return x\n}\n",
7157 );
7158 let main = "import \"units_in.kcl\"\na = units_in::passThrough(42)\nb = units_in::passThrough(42mm)\nc = units_in::passThrough(42in)\n";
7161
7162 let result = execute_with_modules(main, &[units_in]).await.unwrap();
7163 for (name, expected_ty) in [
7164 (
7174 "a",
7175 kcl_api::NumericType::Default {
7176 len: kcl_api::UnitLength::Millimeters,
7177 angle: kcl_api::UnitAngle::Degrees,
7178 },
7179 ),
7180 (
7181 "b",
7182 kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Millimeters)),
7183 ),
7184 (
7185 "c",
7186 kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Inches)),
7187 ),
7188 ] {
7189 let KclValue::Number { value, ty, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name)
7190 else {
7191 panic!("`{name}` should hold a number");
7192 };
7193 assert_eq!(value, 42.0, "`{name}` should keep its magnitude");
7194 assert_eq!(ty, expected_ty, "`{name}` should keep the caller-side unit context");
7195 }
7196 }
7197
7198 #[tokio::test(flavor = "multi_thread")]
7206 async fn signature_types_use_declaring_scope_when_both_scopes_define_the_name() {
7207 let m1 = (
7208 "m1.kcl",
7209 "@settings(experimentalFeatures = allow)\ntype A = string\n\nexport fn test(@a: A) {\n return a\n}\n",
7210 );
7211 let main =
7212 "@settings(experimentalFeatures = allow)\nimport * from \"m1.kcl\"\ntype A = number(mm)\nx = test(2mm)\n";
7213
7214 let err = execute_with_modules(main, &[m1]).await.unwrap_err();
7215 assert_eq!(
7216 err.message(),
7217 "The input argument of `test` requires a value with type `A`, but found a number (mm) (with type `number(mm)`)."
7218 );
7219 }
7220
7221 #[tokio::test(flavor = "multi_thread")]
7222 async fn enum_rejects_bad_variant_paths() {
7223 let allow = "@settings(experimentalFeatures = allow)\n";
7224
7225 for (case, main, modules, message) in [
7226 (
7227 "unknown variant",
7228 format!("{allow}type Color {{ | Red | Green }}\nx = Color::Blue\n"),
7229 vec![],
7230 "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
7231 ),
7232 (
7233 "enum with no variants",
7234 format!("{allow}type Empty {{ | }}\nx = Empty::Red\n"),
7235 vec![],
7236 "`Red` is not a variant of enum `Empty`. Enum `Empty` has no variants.",
7237 ),
7238 (
7239 "path continues past the enum",
7240 format!("{allow}type Color {{ | Red }}\nx = Color::Red::more\n"),
7241 vec![],
7242 "`Color` is an enum, so only a variant name can follow it. There is nothing to reach through `Color::Red`.",
7243 ),
7244 (
7245 "variant name is case sensitive",
7246 format!("{allow}type Color {{ | Red }}\nx = Color::red\n"),
7247 vec![],
7248 "`red` is not a variant of enum `Color`. Its variants are: Red.",
7249 ),
7250 (
7251 "enum not exported from its module",
7252 format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
7253 vec![(
7254 "colors.kcl",
7255 "@settings(experimentalFeatures = allow)\ntype Color { | Red }\n",
7256 )],
7257 "Item Color not found in module's exported items",
7258 ),
7259 (
7260 "a type alias cannot head a path",
7263 format!("{allow}type T = number(_)\nx = T::foo\n"),
7264 vec![],
7265 "`T` is not defined",
7266 ),
7267 (
7268 "a value cannot head a path",
7271 "Color = 5\nx = Color::Red\n".to_owned(),
7272 vec![],
7273 "`Color` is not defined",
7274 ),
7275 ] {
7276 let err = execute_with_modules(&main, &modules).await.unwrap_err();
7277 assert_eq!(err.message(), message, "case: {case}");
7278 }
7279 }
7280
7281 #[tokio::test(flavor = "multi_thread")]
7282 async fn enum_compares_by_variant() {
7283 let code = r#"@settings(experimentalFeatures = allow)
7284type Color { | Red | Green }
7285sameEq = Color::Red == Color::Red
7286sameNeq = Color::Red != Color::Red
7287otherEq = Color::Red == Color::Green
7288otherNeq = Color::Red != Color::Green
7289"#;
7290 let result = parse_execute(code).await.unwrap();
7291
7292 for (name, expected) in [
7293 ("sameEq", true),
7294 ("sameNeq", false),
7295 ("otherEq", false),
7296 ("otherNeq", true),
7297 ] {
7298 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
7299 panic!("`{name}` should hold a bool");
7300 };
7301 assert_eq!(value, expected, "variable: {name}");
7302 }
7303 }
7304
7305 #[tokio::test(flavor = "multi_thread")]
7306 async fn enum_usable_inside_sketch_block() {
7307 let code = r#"@settings(experimentalFeatures = allow)
7316type Color { | Red | Green }
7317sketch(on = XY) {
7318 c = Color::Red
7319 assertIs(Color::Red != Color::Green)
7320 assertIs(!(Color::Red != Color::Red))
7321 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7322}
7323"#;
7324 parse_execute(code)
7325 .await
7326 .unwrap_or_else(|err| panic!("enum use inside a sketch block should work: {}", err.message()));
7327 }
7328
7329 #[tokio::test(flavor = "multi_thread")]
7330 async fn enum_eq_reserved_inside_sketch_block() {
7331 let allow = "@settings(experimentalFeatures = allow)\n";
7341 let tail = " l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}\n";
7342 for (case, declaration, comparison, types) in [
7343 (
7344 "enums",
7345 "type Color { | Red | Green }\n",
7346 "Color::Red == Color::Green",
7347 "a value of enum `Color` and a value of enum `Color`",
7348 ),
7349 ("strings", "", "\"a\" == \"b\"", "a string and a string"),
7350 ("numbers", "", "1 == 2", "a number and a number"),
7351 ] {
7352 let code = format!("{allow}{declaration}sketch(on = XY) {{\n x = {comparison}\n{tail}");
7353 assert_eq!(
7354 parse_execute(&code).await.unwrap_err().message(),
7355 format!("Cannot create an equivalence constraint between values of these types: {types}"),
7356 "case: {case}"
7357 );
7358 }
7359 }
7360
7361 #[tokio::test(flavor = "multi_thread")]
7362 async fn enum_same_file_imported_twice_is_one_type() {
7363 let main = r#"@settings(experimentalFeatures = allow)
7367import Color as A from 'colors.kcl'
7368import Color as B from 'colors.kcl'
7369x = A::Red == B::Red
7370y = A::Red == B::Green
7371"#;
7372 let result = execute_with_modules(
7373 main,
7374 &[(
7375 "colors.kcl",
7376 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
7377 )],
7378 )
7379 .await
7380 .unwrap();
7381
7382 for (name, expected) in [("x", true), ("y", false)] {
7383 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
7384 panic!("`{name}` should hold a bool");
7385 };
7386 assert_eq!(value, expected, "variable: {name}");
7387 }
7388 }
7389
7390 #[tokio::test(flavor = "multi_thread")]
7391 async fn enum_rejects_comparison_across_types() {
7392 let allow = "@settings(experimentalFeatures = allow)\n";
7393 let color = (
7394 "a.kcl",
7395 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
7396 );
7397 let other_color = (
7398 "b.kcl",
7399 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
7400 );
7401
7402 for (case, main, modules, message) in [
7403 (
7404 "two enums declared separately",
7405 format!("{allow}type Color {{ | Red }}\ntype Shade {{ | Red }}\nx = Color::Red == Shade::Red\n"),
7406 vec![],
7407 "Cannot compare enum `Color` with enum `Shade`. They are different types.",
7408 ),
7409 (
7410 "two enums sharing a name",
7414 format!(
7415 "{allow}import Color as A from 'a.kcl'\nimport Color as B from 'b.kcl'\nx = A::Red == B::Red\n"
7416 ),
7417 vec![color, other_color],
7418 "Cannot compare two different enums that are both named `Color`. They come from separate declarations.",
7419 ),
7420 (
7421 "an enum and a number",
7422 format!("{allow}type Color {{ | Red }}\nx = Color::Red == 5\n"),
7423 vec![],
7424 "Cannot compare enum `Color::Red` with a number.",
7425 ),
7426 (
7427 "a number and an enum, in that order",
7428 format!("{allow}type Color {{ | Red }}\nx = 5 == Color::Red\n"),
7429 vec![],
7430 "Cannot compare enum `Color::Red` with a number.",
7431 ),
7432 (
7433 "an enum and a string",
7434 format!("{allow}type Color {{ | Red }}\nx = Color::Red == \"Red\"\n"),
7435 vec![],
7436 "Cannot compare enum `Color::Red` with a string.",
7437 ),
7438 ] {
7439 let err = execute_with_modules(&main, &modules).await.unwrap_err();
7440 assert_eq!(err.message(), message, "case: {case}");
7441 }
7442 }
7443
7444 #[tokio::test(flavor = "multi_thread")]
7445 async fn enum_rejects_bare_type_name_as_value() {
7446 let allow = "@settings(experimentalFeatures = allow)\n";
7447 let colors = (
7448 "colors.kcl",
7449 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
7450 );
7451
7452 for (case, main, modules, message) in [
7453 (
7454 "enum suggests a variant",
7455 format!("{allow}type Color {{ | Red | Green }}\nx = Color\n"),
7456 vec![],
7457 "`Color` is a type, not a value. Use one of its variants, such as `Color::Red`.",
7458 ),
7459 (
7460 "suggestion uses the import alias",
7463 format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade\n"),
7464 vec![colors],
7465 "`Shade` is a type, not a value. Use one of its variants, such as `Shade::Red`.",
7466 ),
7467 (
7468 "enum with no variants suggests nothing",
7469 format!("{allow}type Empty {{ | }}\nx = Empty\n"),
7470 vec![],
7471 "`Empty` is a type, not a value.",
7472 ),
7473 (
7474 "a type alias reports the same way",
7475 format!("{allow}type T = number(_)\nx = T\n"),
7476 vec![],
7477 "`T` is a type, not a value.",
7478 ),
7479 (
7480 "an unknown name is still undefined",
7483 "x = Nope\n".to_owned(),
7484 vec![],
7485 "`Nope` is not defined",
7486 ),
7487 ] {
7488 let err = execute_with_modules(&main, &modules).await.unwrap_err();
7489 assert_eq!(err.message(), message, "case: {case}");
7490 }
7491 }
7492
7493 #[tokio::test(flavor = "multi_thread")]
7494 async fn enum_use_gated_by_consuming_module() {
7495 let main = r#"import "colors.kcl"
7503x = colors::Color::Red
7504"#;
7505 let result = execute_with_modules(
7506 main,
7507 &[(
7508 "colors.kcl",
7509 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
7510 )],
7511 )
7512 .await
7513 .unwrap();
7514
7515 let issues = &result.exec_state.global.issues;
7516 assert_eq!(issues.len(), 1, "issues: {issues:?}");
7517 assert_eq!(
7518 issues[0].message,
7519 "Use of the enum `Color` is experimental and may change or be removed."
7520 );
7521 assert_eq!(issues[0].severity, Severity::Error);
7522 }
7523
7524 #[tokio::test(flavor = "multi_thread")]
7525 async fn enum_use_not_gated_when_consumer_allows_it() {
7526 let code = r#"@settings(experimentalFeatures = allow)
7529type Color { | Red }
7530x = Color::Red
7531"#;
7532 let result = parse_execute(code).await.unwrap();
7533 assert!(
7534 result.exec_state.global.issues.is_empty(),
7535 "issues: {:?}",
7536 result.exec_state.global.issues
7537 );
7538 }
7539
7540 #[tokio::test(flavor = "multi_thread")]
7541 async fn enum_allows_name_sharing_outside_modules() {
7542 for (case, main, modules) in [
7549 (
7550 "an alias may share a name with a module",
7553 "@settings(experimentalFeatures = allow)\ntype Temperature = number(_)\nimport \"Temperature.kcl\"\n",
7554 vec![("Temperature.kcl", "export x = 1\n")],
7555 ),
7556 (
7557 "a value may share a name with an enum",
7558 "@settings(experimentalFeatures = allow)\ntype Color { | Red }\nColor = 5\n",
7559 vec![],
7560 ),
7561 ] {
7562 if let Err(err) = execute_with_modules(main, &modules).await {
7563 panic!("case: {case}: {}", err.message());
7564 }
7565 }
7566 }
7567
7568 #[tokio::test(flavor = "multi_thread")]
7569 async fn enum_declaration_rejects_redefinition() {
7570 let code = r#"@settings(experimentalFeatures = allow)
7571type Color { | Red }
7572type Color { | Green }
7573"#;
7574 assert_eq!(
7575 parse_execute(code).await.unwrap_err().message(),
7576 "Redefinition of type Color."
7577 );
7578 }
7579
7580 #[tokio::test(flavor = "multi_thread")]
7586 async fn enum_projects_to_string() {
7587 let header = r#"
7588 @settings(experimentalFeatures = allow)
7589 type Color { | Red | Green }
7590 type Label = string
7591 "#;
7592
7593 for (case, body, expected) in [
7594 ("a variant", "x = Color::Red: string", "Red"),
7595 ("another variant of the same enum", "x = Color::Green: string", "Green"),
7596 ("an alias of the target type", "x = Color::Red: Label", "Red"),
7597 (
7598 "an element of a projected array",
7599 r#"
7600 pair = [Color::Red, Color::Green]: [string]
7601 x = pair[1]
7602 "#,
7603 "Green",
7604 ),
7605 (
7606 "an element of a nested projected array",
7607 r#"
7608 grid = [[Color::Green]]: [[string]]
7609 x = grid[0][0]
7610 "#,
7611 "Green",
7612 ),
7613 (
7614 "a one-element array against a bare string",
7615 "x = [Color::Red]: string",
7616 "Red",
7617 ),
7618 ] {
7619 let result = parse_execute(&format!("{header}{body}\n"))
7620 .await
7621 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
7622 let KclValue::String { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
7623 panic!("case: {case}: `x` should hold a string");
7624 };
7625 assert_eq!(value, expected, "case: {case}");
7626 }
7627 }
7628
7629 #[tokio::test(flavor = "multi_thread")]
7633 async fn enum_ascription_keeps_the_enum() {
7634 let header = r#"
7635 @settings(experimentalFeatures = allow)
7636 type Color { | Red | Green }
7637 type Paint = Color
7638 "#;
7639
7640 for (case, expression, expected) in [
7641 ("its own type", "(Color::Red: Color) == Color::Red", true),
7642 ("an alias of its own type", "(Color::Red: Paint) == Color::Red", true),
7643 (
7644 "the ascription does not change which variant it is",
7645 "(Color::Red: Color) == Color::Green",
7646 false,
7647 ),
7648 ] {
7649 let result = parse_execute(&format!("{header}x = {expression}\n"))
7650 .await
7651 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
7652 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
7653 panic!("case: {case}: `x` should hold a bool");
7654 };
7655 assert_eq!(value, expected, "case: {case}");
7656 }
7657 }
7658
7659 #[tokio::test(flavor = "multi_thread")]
7663 async fn enum_projection_is_not_implicit() {
7664 let header = r#"
7665 @settings(experimentalFeatures = allow)
7666 type Color { | Red | Green }
7667 "#;
7668 let found = "but found a value of enum `Color` (with type `Color`).";
7669
7670 for (case, body, expected) in [
7671 (
7672 "unlabeled argument",
7673 r#"
7674 fn label(@text: string) { return text }
7675 x = label(Color::Red)
7676 "#,
7677 format!("The input argument of `label` requires a value with type `string`, {found}"),
7678 ),
7679 (
7680 "labeled argument",
7681 r#"
7682 fn label(text: string) { return text }
7683 x = label(text = Color::Red)
7684 "#,
7685 format!("text requires a value with type `string`, {found}"),
7686 ),
7687 (
7688 "return",
7689 r#"
7690 fn label(): string { return Color::Red }
7691 x = label()
7692 "#,
7693 format!("This function requires its result to be a value with type `string`, {found}"),
7694 ),
7695 (
7696 "inside an array at an argument boundary",
7701 r#"
7702 fn labels(@text: [string]) { return text }
7703 x = labels([Color::Red])
7704 "#,
7705 "The input argument of `labels` requires an array of strings (`[string]`), but found an array of `Color` with 1 value (with type `[any; 1]`).".to_owned(),
7706 ),
7707 ] {
7708 assert_eq!(
7709 parse_execute(&format!("{header}{body}\n")).await.unwrap_err().message(),
7710 expected,
7711 "case: {case}"
7712 );
7713 }
7714 }
7715
7716 #[tokio::test(flavor = "multi_thread")]
7719 async fn enum_ascription_rejections() {
7720 let header = r#"
7721 @settings(experimentalFeatures = allow)
7722 type Color { | Red }
7723 type Shade { | Red }
7724 "#;
7725 let no_number = "Cannot project enum `Color` to a number. An enum projects to `string`; projecting to a number is not supported yet.";
7726
7727 for (case, expression, expected) in [
7728 ("a number target", "Color::Red: number(_)", no_number.to_owned()),
7729 (
7730 "a number target reached through an array, so the reason survives the walk",
7731 "[Color::Red]: [number(_)]",
7732 no_number.to_owned(),
7733 ),
7734 (
7735 "a boolean target, which is not a projection at all",
7736 "Color::Red: bool",
7737 "could not coerce a value of enum `Color` (with type `Color`) to type `bool`".to_owned(),
7738 ),
7739 (
7740 "another enum whose variants happen to match",
7741 "Color::Red: Shade",
7742 "could not coerce a value of enum `Color` (with type `Color`) to type `Shade`".to_owned(),
7743 ),
7744 ] {
7745 assert_eq!(
7746 parse_execute(&format!("{header}x = {expression}\n"))
7747 .await
7748 .unwrap_err()
7749 .message(),
7750 expected,
7751 "case: {case}"
7752 );
7753 }
7754 }
7755
7756 #[tokio::test(flavor = "multi_thread")]
7763 async fn enum_flows_through_declared_types() {
7764 let header = r#"
7765 @settings(experimentalFeatures = allow)
7766 type Color { | Red | Green }
7767 type Shade { | Red }
7768 "#;
7769
7770 for (case, body, expected) in [
7771 (
7772 "an unlabeled parameter",
7773 r#"
7774 fn paint(@c: Color) { return c }
7775 x = paint(Color::Red) == Color::Red
7776 "#,
7777 None,
7778 ),
7779 (
7780 "a labeled parameter",
7781 r#"
7782 fn paint(c: Color) { return c }
7783 x = paint(c = Color::Green) == Color::Green
7784 "#,
7785 None,
7786 ),
7787 (
7788 "a declared return type",
7789 r#"
7790 fn pick(): Color { return Color::Red }
7791 x = pick() == Color::Red
7792 "#,
7793 None,
7794 ),
7795 (
7796 "an array parameter",
7797 r#"
7798 fn firstOf(@cs: [Color]) { return cs[0] }
7799 x = firstOf([Color::Red, Color::Green]) == Color::Red
7800 "#,
7801 None,
7802 ),
7803 (
7804 "an object field",
7809 r#"
7810 fn take(@o: { c: Color }) { return o.c }
7811 x = take({ c = Color::Green }) == Color::Green
7812 "#,
7813 None,
7814 ),
7815 (
7816 "a union that names the enum",
7817 r#"
7818 fn either(@v: Color | string) { return v }
7819 x = either(Color::Red) == Color::Red
7820 "#,
7821 None,
7822 ),
7823 (
7824 "the same union given the other member",
7825 r#"
7826 fn either(@v: Color | string) { return v }
7827 x = either("plain") == "plain"
7828 "#,
7829 None,
7830 ),
7831 (
7832 "another declaration at the same boundary",
7833 r#"
7834 fn paint(@c: Color) { return c }
7835 x = paint(Shade::Red) == Shade::Red
7836 "#,
7837 Some(
7838 "The input argument of `paint` requires a value with type `Color`, but found a value of enum `Shade` (with type `Shade`).",
7839 ),
7840 ),
7841 ] {
7842 let code = format!("{header}{body}\n");
7843 match expected {
7844 None => {
7845 let result = parse_execute(&code)
7846 .await
7847 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
7848 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x")
7849 else {
7850 panic!("case: {case}: `x` should hold a bool");
7851 };
7852 assert!(value, "case: {case}: the value did not survive the boundary");
7853 }
7854 Some(message) => assert_eq!(
7855 parse_execute(&code).await.unwrap_err().message(),
7856 message,
7857 "case: {case}"
7858 ),
7859 }
7860 }
7861 }
7862}