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,
442 pub instance_index: usize,
445 pub status: ConstraintKind,
447 pub free_count: usize,
449 pub conflict_count: usize,
451 pub total_count: usize,
453}
454
455#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
457pub struct SketchConstraintReport {
458 pub fully_constrained: Vec<SketchConstraintStatus>,
459 pub under_constrained: Vec<SketchConstraintStatus>,
460 pub over_constrained: Vec<SketchConstraintStatus>,
461 pub errors: Vec<SketchConstraintStatus>,
464}
465
466pub(crate) fn sketch_constraint_status_for_sketch(
475 scene_objects: &[Object],
476 sketch_obj: &Object,
477) -> Option<SketchConstraintStatus> {
478 use crate::front::ObjectKind;
479 use crate::front::Segment;
480
481 let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
482 return None;
483 };
484
485 let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
487 let obj = scene_objects.get(id.0)?;
488 if let ObjectKind::Segment {
489 segment: Segment::Point(p),
490 } = &obj.kind
491 {
492 Some(p.freedom())
493 } else {
494 None
495 }
496 };
497
498 let mut free_count: usize = 0;
499 let mut conflict_count: usize = 0;
500 let mut error_count: usize = 0;
501 let mut total_count: usize = 0;
502
503 for &seg_id in &sketch.segments {
504 let Some(seg_obj) = scene_objects.get(seg_id.0) else {
505 continue;
506 };
507 let ObjectKind::Segment { segment } = &seg_obj.kind else {
508 continue;
509 };
510 if let Segment::Point(p) = segment
513 && p.owner.is_some()
514 {
515 continue;
516 }
517 let freedom = segment
518 .freedom(lookup)
519 .map(SegmentFreedom::from)
520 .unwrap_or(SegmentFreedom::Error);
521 total_count += 1;
522 match freedom {
523 SegmentFreedom::Free => free_count += 1,
524 SegmentFreedom::Conflict => conflict_count += 1,
525 SegmentFreedom::Error => error_count += 1,
526 SegmentFreedom::Fixed => {}
527 }
528 }
529
530 let status = if error_count > 0 {
531 ConstraintKind::Error
532 } else if conflict_count > 0 {
533 ConstraintKind::OverConstrained
534 } else if free_count > 0 {
535 ConstraintKind::UnderConstrained
536 } else {
537 ConstraintKind::FullyConstrained
538 };
539
540 Some(SketchConstraintStatus {
541 name: sketch_obj.label.clone(),
542 instance_index: 0,
543 status,
544 free_count,
545 conflict_count,
546 total_count,
547 })
548}
549
550pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
551 let mut fully_constrained = Vec::new();
552 let mut under_constrained = Vec::new();
553 let mut over_constrained = Vec::new();
554 let mut errors = Vec::new();
555 let mut instance_counts = std::collections::HashMap::new();
556 for obj in scene_objects {
557 let Some(mut entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
558 continue;
559 };
560 let count = instance_counts.entry(entry.name.clone()).or_insert(0);
561 entry.instance_index = *count;
562 *count += 1;
563 match entry.status {
564 ConstraintKind::FullyConstrained => fully_constrained.push(entry),
565 ConstraintKind::UnderConstrained => under_constrained.push(entry),
566 ConstraintKind::OverConstrained => over_constrained.push(entry),
567 ConstraintKind::Error => errors.push(entry),
568 }
569 }
570
571 SketchConstraintReport {
572 fully_constrained,
573 under_constrained,
574 over_constrained,
575 errors,
576 }
577}
578
579impl ExecOutcome {
580 pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
581 debug_assert!(
582 id.0 < self.scene_objects.len(),
583 "Requested object ID {} but only have {} objects",
584 id.0,
585 self.scene_objects.len()
586 );
587 self.scene_objects.get(id.0)
588 }
589
590 pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
592 self.issues.iter().filter(|error| error.is_err())
593 }
594
595 pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
602 sketch_constraint_report_from_scene_objects(&self.scene_objects)
603 }
604
605 pub fn render_sketch_png(
608 &self,
609 sketch_name: &str,
610 ) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
611 self.render_sketch_png_instance(sketch_name, None)
612 }
613
614 pub fn render_sketch_png_instance(
617 &self,
618 sketch_name: &str,
619 instance_index: Option<usize>,
620 ) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
621 use crate::front::ObjectKind;
622 use crate::tooling::sketch_visualizer::SketchVisualizationError;
623
624 let sketches = self
625 .scene_objects
626 .iter()
627 .filter_map(|object| match &object.kind {
628 ObjectKind::Sketch(sketch) if object.label == sketch_name => Some(sketch),
629 _ => None,
630 })
631 .collect::<Vec<_>>();
632 let sketch = match (sketches.as_slice(), instance_index) {
633 ([], _) => {
634 return Err(SketchVisualizationError::SketchNotFound {
635 name: sketch_name.to_owned(),
636 });
637 }
638 (_, Some(index)) => *sketches
639 .get(index)
640 .ok_or_else(|| SketchVisualizationError::InstanceNotFound {
641 name: sketch_name.to_owned(),
642 index,
643 count: sketches.len(),
644 })?,
645 ([sketch], None) => *sketch,
646 (_, None) => {
647 return Err(SketchVisualizationError::AmbiguousSketchName {
648 name: sketch_name.to_owned(),
649 count: sketches.len(),
650 });
651 }
652 };
653
654 crate::tooling::sketch_visualizer::render_sketch_png(&self.scene_objects, sketch)
655 }
656}
657
658#[derive(Debug, Clone, PartialEq)]
660pub struct MockConfig {
661 pub use_prev_memory: bool,
662 pub sketch_block_id: Option<ObjectId>,
665 pub freedom_analysis: bool,
668 pub segment_ids_edited: AhashIndexSet<ObjectId>,
670 pub drag_anchors: Vec<SegmentDragAnchor>,
672}
673
674#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
675#[ts(export, export_to = "FrontendApi.ts")]
676#[serde(rename_all = "camelCase")]
677pub struct SegmentDragAnchor {
678 pub segment_id: ObjectId,
679 pub target: crate::front::Point2d<Number>,
680}
681
682impl Default for MockConfig {
683 fn default() -> Self {
684 Self {
685 use_prev_memory: true,
687 sketch_block_id: None,
688 freedom_analysis: true,
689 segment_ids_edited: AhashIndexSet::default(),
690 drag_anchors: Vec::new(),
691 }
692 }
693}
694
695impl MockConfig {
696 pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
698 Self {
699 sketch_block_id: Some(sketch_block_id),
700 ..Default::default()
701 }
702 }
703
704 #[must_use]
705 pub(crate) fn no_freedom_analysis(mut self) -> Self {
706 self.freedom_analysis = false;
707 self
708 }
709}
710
711#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
712#[ts(export)]
713#[serde(rename_all = "camelCase")]
714pub struct DefaultPlanes {
715 pub xy: uuid::Uuid,
716 pub xz: uuid::Uuid,
717 pub yz: uuid::Uuid,
718 pub neg_xy: uuid::Uuid,
719 pub neg_xz: uuid::Uuid,
720 pub neg_yz: uuid::Uuid,
721}
722
723#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
724#[ts(export)]
725#[serde(tag = "type", rename_all = "camelCase")]
726pub struct TagIdentifier {
727 pub value: String,
728 #[serde(skip)]
731 pub info: Vec<(usize, TagEngineInfo)>,
732 #[serde(skip)]
733 pub meta: Vec<Metadata>,
734}
735
736impl TagIdentifier {
737 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
739 for (e, info) in self.info.iter().rev() {
740 if *e <= at_epoch {
741 return Some(info);
742 }
743 }
744
745 None
746 }
747
748 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
750 self.info.last().map(|i| &i.1)
751 }
752
753 pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
756 let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
757 return vec![];
758 };
759 self.info
760 .iter()
761 .rev()
762 .take_while(|(e, _)| *e == cur_epoch)
763 .map(|(_, info)| info)
764 .collect()
765 }
766
767 pub fn merge_info(&mut self, other: &TagIdentifier) {
769 assert_eq!(&self.value, &other.value);
770 for (oe, ot) in &other.info {
771 if let Some((e, t)) = self.info.last_mut() {
772 if *e > *oe {
774 continue;
775 }
776 if e == oe {
778 *t = ot.clone();
779 continue;
780 }
781 }
782 self.info.push((*oe, ot.clone()));
783 }
784 }
785
786 pub fn geometry(&self) -> Option<Geometry> {
787 self.get_cur_info().map(|info| info.geometry.clone())
788 }
789
790 pub(crate) fn is_body_created_tag(&self) -> bool {
791 self.get_cur_info().is_some_and(|info| {
792 matches!(&info.geometry, Geometry::Solid(_)) && info.path.is_none() && info.surface.is_some()
793 })
794 }
795}
796
797impl Eq for TagIdentifier {}
798
799impl std::fmt::Display for TagIdentifier {
800 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
801 write!(f, "{}", self.value)
802 }
803}
804
805impl std::str::FromStr for TagIdentifier {
806 type Err = KclError;
807
808 fn from_str(s: &str) -> Result<Self, Self::Err> {
809 Ok(Self {
810 value: s.to_string(),
811 info: Vec::new(),
812 meta: Default::default(),
813 })
814 }
815}
816
817impl Ord for TagIdentifier {
818 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
819 self.value.cmp(&other.value)
820 }
821}
822
823impl PartialOrd for TagIdentifier {
824 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
825 Some(self.cmp(other))
826 }
827}
828
829impl std::hash::Hash for TagIdentifier {
830 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
831 self.value.hash(state);
832 }
833}
834
835#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
837#[ts(export)]
838#[serde(tag = "type", rename_all = "camelCase")]
839pub struct TagEngineInfo {
840 pub id: uuid::Uuid,
842 pub geometry: Geometry,
844 pub path: Option<Path>,
846 pub surface: Option<ExtrudeSurface>,
848}
849
850#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
851pub enum BodyType {
852 Root,
853 Block,
854}
855
856#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
858#[ts(export)]
859#[serde(rename_all = "camelCase")]
860pub struct Metadata {
861 pub source_range: SourceRange,
863}
864
865impl From<Metadata> for Vec<SourceRange> {
866 fn from(meta: Metadata) -> Self {
867 vec![meta.source_range]
868 }
869}
870
871impl From<&Metadata> for SourceRange {
872 fn from(meta: &Metadata) -> Self {
873 meta.source_range
874 }
875}
876
877impl From<SourceRange> for Metadata {
878 fn from(source_range: SourceRange) -> Self {
879 Self { source_range }
880 }
881}
882
883impl<T> From<NodeRef<'_, T>> for Metadata {
884 fn from(node: NodeRef<'_, T>) -> Self {
885 Self {
886 source_range: SourceRange::new(node.start, node.end, node.module_id),
887 }
888 }
889}
890
891impl From<&Expr> for Metadata {
892 fn from(expr: &Expr) -> Self {
893 Self {
894 source_range: SourceRange::from(expr),
895 }
896 }
897}
898
899impl Metadata {
900 pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
901 if meta.len() == 1 {
902 let meta = &meta[0];
903 return crate::front::SourceRef::Simple {
904 range: meta.source_range,
905 node_path,
906 };
907 }
908 crate::front::SourceRef::BackTrace {
909 ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
910 }
911 }
912}
913
914#[derive(PartialEq, Debug, Default, Clone)]
916pub enum ContextType {
917 #[default]
919 Live,
920
921 Mock,
925
926 MockCustomForwarded,
928}
929
930#[derive(Clone)]
934pub struct ExecutorContext {
935 pub engine: Arc<EngineManager>,
936 pub engine_batch: EngineBatchContext,
937 pub fs: FileSystemHandle,
938 pub settings: ExecutorSettings,
939 pub context_type: ContextType,
940 pub execution_callbacks: Option<Arc<dyn ExecutionCallbacks>>,
941 pub(crate) executor_kind: machine::ExecutorKind,
945 pub(crate) machine_call_depth_limit: usize,
948}
949
950impl std::fmt::Debug for ExecutorContext {
951 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
952 f.debug_struct("ExecutorContext")
953 .field("engine", &self.engine)
954 .field("engine_batch", &self.engine_batch)
955 .field("settings", &self.settings)
956 .field("context_type", &self.context_type)
957 .field("execution_callbacks", &self.execution_callbacks)
958 .field("executor_kind", &self.executor_kind)
959 .field("machine_call_depth_limit", &self.machine_call_depth_limit)
960 .finish()
961 }
962}
963
964#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
966#[ts(export)]
967pub struct ExecutorSettings {
968 pub highlight_edges: bool,
970 pub enable_ssao: bool,
972 pub show_grid: bool,
974 pub replay: Option<String>,
977 pub project_directory: Option<TypedPath>,
980 pub current_file: Option<TypedPath>,
983 pub fixed_size_grid: bool,
985 #[serde(default, skip_serializing_if = "is_false")]
991 pub skip_artifact_graph: bool,
992 #[serde(default, skip_serializing_if = "Option::is_none")]
995 pub heartbeats: Option<u64>,
996 #[serde(default, skip_serializing_if = "Option::is_none")]
999 pub default_backface_color: Option<String>,
1000 #[serde(default, skip_serializing_if = "Option::is_none")]
1002 pub pool: Option<String>,
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1005 pub video_res_width: Option<u32>,
1006 #[serde(default, skip_serializing_if = "Option::is_none")]
1008 pub video_res_height: Option<u32>,
1009 pub geometry_only: bool,
1011}
1012
1013fn is_false(b: &bool) -> bool {
1014 !*b
1015}
1016
1017impl Default for ExecutorSettings {
1018 fn default() -> Self {
1019 Self {
1020 highlight_edges: true,
1021 enable_ssao: false,
1022 show_grid: false,
1023 replay: None,
1024 project_directory: None,
1025 current_file: None,
1026 fixed_size_grid: true,
1027 skip_artifact_graph: false,
1028 heartbeats: None,
1029 default_backface_color: None,
1030 pool: None,
1031 video_res_width: None,
1032 video_res_height: None,
1033 geometry_only: false,
1034 }
1035 }
1036}
1037
1038impl From<crate::settings::types::Configuration> for ExecutorSettings {
1039 fn from(config: crate::settings::types::Configuration) -> Self {
1040 Self::from(config.settings)
1041 }
1042}
1043
1044impl From<crate::settings::types::Settings> for ExecutorSettings {
1045 fn from(settings: crate::settings::types::Settings) -> Self {
1046 let modeling_settings = settings.modeling.unwrap_or_default();
1047 Self {
1048 highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
1049 enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
1050 show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
1051 replay: None,
1052 project_directory: None,
1053 current_file: None,
1054 fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
1055 skip_artifact_graph: false,
1056 heartbeats: None,
1057 default_backface_color: modeling_settings.backface_color.map(|color| color.0),
1058 pool: None,
1059 video_res_width: None,
1060 video_res_height: None,
1061 geometry_only: false,
1062 }
1063 }
1064}
1065
1066impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
1067 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
1068 Self::from(config.settings.modeling)
1069 }
1070}
1071
1072impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
1073 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
1074 Self {
1075 highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
1076 enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
1077 show_grid: modeling.show_scale_grid.unwrap_or_default(),
1078 replay: None,
1079 project_directory: None,
1080 current_file: None,
1081 fixed_size_grid: true,
1082 skip_artifact_graph: false,
1083 heartbeats: None,
1084 default_backface_color: modeling.backface_color.map(|color| color.0),
1085 pool: None,
1086 video_res_width: None,
1087 video_res_height: None,
1088 geometry_only: false,
1089 }
1090 }
1091}
1092
1093impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
1094 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
1095 Self {
1096 highlight_edges: modeling.highlight_edges.into(),
1097 enable_ssao: modeling.enable_ssao.into(),
1098 show_grid: Default::default(),
1099 replay: None,
1100 project_directory: None,
1101 current_file: None,
1102 fixed_size_grid: true,
1103 skip_artifact_graph: false,
1104 heartbeats: None,
1105 default_backface_color: None,
1106 pool: None,
1107 video_res_width: None,
1108 video_res_height: None,
1109 geometry_only: false,
1110 }
1111 }
1112}
1113
1114impl ExecutorSettings {
1115 pub fn with_current_file(&mut self, current_file: TypedPath) {
1117 if current_file.extension() == Some("kcl") {
1119 self.current_file = Some(current_file.clone());
1120 if let Some(parent) = current_file.parent() {
1122 self.project_directory = Some(parent);
1123 } else {
1124 self.project_directory = Some(TypedPath::from(""));
1125 }
1126 } else {
1127 self.project_directory = Some(current_file);
1128 }
1129 }
1130}
1131
1132impl ExecutorContext {
1133 pub fn new_with_engine_and_fs(
1135 engine: Arc<EngineManager>,
1136 fs: FileSystemHandle,
1137 settings: ExecutorSettings,
1138 ) -> Self {
1139 ExecutorContext {
1140 engine,
1141 engine_batch: EngineBatchContext::default(),
1142 fs,
1143 settings,
1144 context_type: ContextType::Live,
1145 execution_callbacks: Default::default(),
1146 executor_kind: machine::ExecutorKind::resolve(),
1147 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1148 }
1149 }
1150
1151 fn clone_with_fresh_execution_batch(&self) -> Self {
1152 Self {
1153 engine: self.engine.clone(),
1154 engine_batch: EngineBatchContext::new(),
1155 fs: self.fs.clone(),
1156 settings: self.settings.clone(),
1157 context_type: self.context_type.clone(),
1158 execution_callbacks: self.execution_callbacks.clone(),
1159 executor_kind: self.executor_kind,
1162 machine_call_depth_limit: self.machine_call_depth_limit,
1163 }
1164 }
1165
1166 #[cfg(not(target_arch = "wasm32"))]
1168 pub fn new_with_engine(engine: Arc<EngineManager>, settings: ExecutorSettings) -> Self {
1169 Self::new_with_engine_and_fs(engine, crate::fs::new_file_system_handle(FileManager::new()), settings)
1170 }
1171
1172 #[cfg(not(target_arch = "wasm32"))]
1174 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
1175 let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
1176 let (ws, headers) = client
1177 .modeling()
1178 .commands_ws(kittycad::modeling::CommandsWsParams {
1179 api_call_id: None,
1180 fps: None,
1181 order_independent_transparency: None,
1182 post_effect: if settings.enable_ssao {
1183 Some(kittycad::types::PostEffectType::Ssao)
1184 } else {
1185 None
1186 },
1187 replay: settings.replay.clone(),
1188 show_grid: if settings.show_grid { Some(true) } else { None },
1189 pool: if settings.geometry_only {
1190 Some("cpu".to_string())
1191 } else {
1192 settings.pool.clone()
1193 },
1194 geometry_only: Some(settings.geometry_only),
1195 kcl_version: None,
1196 pr,
1197 unlocked_framerate: None,
1198 webrtc: Some(false),
1199 video_res_width: settings.video_res_width,
1200 video_res_height: settings.video_res_height,
1201 })
1202 .await?;
1203
1204 let request_id = headers
1205 .get("x-request-id")
1206 .and_then(|id| id.to_str().ok())
1207 .map(str::to_owned);
1208 let engine_conn =
1209 EngineManager::new_websocket_transport_with_request_id(ws, settings.heartbeats, request_id).await;
1210 let engine = Arc::new(engine_conn);
1211
1212 Ok(Self::new_with_engine(engine, settings))
1213 }
1214
1215 #[cfg(target_arch = "wasm32")]
1216 pub fn new(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1217 Self::new_with_engine_and_fs(engine, fs, settings)
1218 }
1219
1220 #[cfg(not(target_arch = "wasm32"))]
1221 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
1222 ExecutorContext {
1223 engine: Arc::new(EngineManager::new_mock()),
1224 engine_batch: EngineBatchContext::default(),
1225 fs: crate::fs::new_file_system_handle(FileManager::new()),
1226 settings: settings.unwrap_or_default(),
1227 context_type: ContextType::Mock,
1228 execution_callbacks: Default::default(),
1229 executor_kind: machine::ExecutorKind::resolve(),
1230 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1231 }
1232 }
1233
1234 #[cfg(target_arch = "wasm32")]
1235 pub fn new_mock(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1236 ExecutorContext {
1237 engine,
1238 engine_batch: EngineBatchContext::default(),
1239 fs,
1240 settings,
1241 context_type: ContextType::Mock,
1242 execution_callbacks: Default::default(),
1243 executor_kind: machine::ExecutorKind::resolve(),
1244 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1245 }
1246 }
1247
1248 #[cfg(target_arch = "wasm32")]
1251 pub fn new_mock_for_lsp(
1252 fs_manager: crate::fs::wasm::FileSystemManager,
1253 settings: ExecutorSettings,
1254 ) -> Result<Self, String> {
1255 let fs = crate::fs::new_file_system_handle(FileManager::new(fs_manager));
1256
1257 Ok(ExecutorContext {
1258 engine: Arc::new(EngineManager::new_mock()),
1259 engine_batch: EngineBatchContext::default(),
1260 fs,
1261 settings,
1262 context_type: ContextType::Mock,
1263 execution_callbacks: Default::default(),
1264 executor_kind: machine::ExecutorKind::resolve(),
1265 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1266 })
1267 }
1268
1269 #[cfg(not(target_arch = "wasm32"))]
1270 pub fn new_forwarded_mock(engine: Arc<EngineManager>) -> Self {
1271 ExecutorContext {
1272 engine,
1273 engine_batch: EngineBatchContext::default(),
1274 fs: crate::fs::new_file_system_handle(FileManager::new()),
1275 settings: Default::default(),
1276 context_type: ContextType::MockCustomForwarded,
1277 execution_callbacks: Default::default(),
1278 executor_kind: machine::ExecutorKind::resolve(),
1279 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1280 }
1281 }
1282
1283 #[cfg(not(target_arch = "wasm32"))]
1289 pub async fn new_with_client(
1290 settings: ExecutorSettings,
1291 token: Option<String>,
1292 engine_addr: Option<String>,
1293 ) -> Result<Self> {
1294 let client = crate::engine::new_zoo_client(token, engine_addr)?;
1296
1297 let ctx = Self::new(&client, settings).await?;
1298 Ok(ctx)
1299 }
1300
1301 #[cfg(not(target_arch = "wasm32"))]
1306 pub async fn new_with_default_client() -> Result<Self> {
1307 let ctx = Self::new_with_client(Default::default(), None, None).await?;
1309 Ok(ctx)
1310 }
1311
1312 #[cfg(not(target_arch = "wasm32"))]
1314 pub async fn new_geometry_only_with_default_client() -> Result<Self> {
1315 Self::new_with_client(
1316 ExecutorSettings {
1317 geometry_only: true,
1318 ..Default::default()
1319 },
1320 None,
1321 None,
1322 )
1323 .await
1324 }
1325
1326 #[cfg(not(target_arch = "wasm32"))]
1328 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
1329 let ctx = ExecutorContext::new_with_client(
1330 ExecutorSettings {
1331 highlight_edges: true,
1332 enable_ssao: false,
1333 show_grid: false,
1334 replay: None,
1335 project_directory: None,
1336 current_file: None,
1337 fixed_size_grid: false,
1338 skip_artifact_graph: false,
1339 heartbeats: None,
1340 default_backface_color: None,
1341 pool: None,
1342 video_res_width: None,
1343 video_res_height: None,
1344 geometry_only: false,
1345 },
1346 None,
1347 engine_addr,
1348 )
1349 .await?;
1350 Ok(ctx)
1351 }
1352
1353 pub fn is_mock(&self) -> bool {
1354 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1355 }
1356
1357 pub async fn no_engine_commands(&self) -> bool {
1359 self.is_mock()
1360 }
1361
1362 pub async fn send_clear_scene(
1363 &self,
1364 exec_state: &mut ExecState,
1365 source_range: crate::execution::SourceRange,
1366 ) -> Result<(), KclError> {
1367 exec_state.mod_local.artifacts.clear();
1370 exec_state.global.root_module_artifacts.clear();
1371 exec_state.global.artifacts.clear();
1372
1373 self.engine
1374 .clear_scene(
1375 &self.engine_batch,
1376 &mut exec_state.mod_local.id_generator,
1377 source_range,
1378 self.settings.geometry_only,
1379 )
1380 .await?;
1381 if !self.settings.geometry_only && self.settings.enable_ssao {
1383 let cmd_id = exec_state.next_uuid();
1384 exec_state
1385 .batch_modeling_cmd(
1386 ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1387 ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1388 )
1389 .await?;
1390 }
1391 Ok(())
1392 }
1393
1394 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1395 cache::bust_cache().await;
1396
1397 let outcome = self.run_with_caching(crate::Program::empty()).await?;
1402
1403 Ok(outcome)
1404 }
1405
1406 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1407 self.eval_prelude(exec_state, SourceRange::synthetic())
1408 .await
1409 .map_err(KclErrorWithOutputs::no_outputs)?;
1410 exec_state
1411 .mut_stack()
1412 .push_new_root_env(true)
1413 .map_err(KclErrorWithOutputs::no_outputs)?;
1414 Ok(())
1415 }
1416
1417 fn restore_mock_memory(
1418 exec_state: &mut ExecState,
1419 mem: cache::SketchModeState,
1420 _mock_config: &MockConfig,
1421 ) -> Result<(), KclErrorWithOutputs> {
1422 *exec_state.mut_stack() = mem.stack;
1423 exec_state.global.module_infos = mem.module_infos;
1424 exec_state.global.path_to_source_id = mem.path_to_source_id;
1425 exec_state.global.id_to_source = mem.id_to_source;
1426 exec_state.mod_local.constraint_state = mem.constraint_state;
1427 let len = _mock_config
1428 .sketch_block_id
1429 .map(|sketch_block_id| sketch_block_id.0)
1430 .unwrap_or(0);
1431 if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1432 exec_state
1433 .global
1434 .root_module_artifacts
1435 .restore_scene_objects(scene_objects);
1436 } else {
1437 let message = format!(
1438 "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1439 mem.scene_objects.len(),
1440 len
1441 );
1442 debug_assert!(false, "{message}");
1443 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1444 KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1445 )));
1446 }
1447
1448 Ok(())
1449 }
1450
1451 pub async fn run_mock(
1452 &self,
1453 program: &crate::Program,
1454 mock_config: &MockConfig,
1455 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1456 let (exec_state, main_ref) = self.run_mock_returning_state(program, mock_config).await?;
1457
1458 let mut stack = exec_state.stack().clone();
1463 let module_infos = exec_state.global.module_infos.clone();
1464 let path_to_source_id = exec_state.global.path_to_source_id.clone();
1465 let id_to_source = exec_state.global.id_to_source.clone();
1466 let constraint_state = exec_state.mod_local.constraint_state.clone();
1467 let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1468 let outcome = exec_state
1469 .into_exec_outcome(main_ref, self)
1470 .await
1471 .map_err(KclErrorWithOutputs::no_outputs)?;
1472
1473 stack.squash_env(main_ref).map_err(KclErrorWithOutputs::no_outputs)?;
1474 let state = cache::SketchModeState {
1475 stack,
1476 module_infos,
1477 path_to_source_id,
1478 id_to_source,
1479 constraint_state,
1480 scene_objects,
1481 };
1482 cache::write_old_memory(state).await;
1483
1484 Ok(outcome)
1485 }
1486
1487 async fn run_mock_returning_state(
1492 &self,
1493 program: &crate::Program,
1494 mock_config: &MockConfig,
1495 ) -> Result<(ExecState, EnvironmentRef), KclErrorWithOutputs> {
1496 assert!(
1497 self.is_mock(),
1498 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1499 );
1500
1501 let use_prev_memory = mock_config.use_prev_memory;
1502 let mut exec_state = ExecState::new_mock(self, mock_config);
1503 if use_prev_memory {
1504 match cache::read_old_memory().await {
1505 Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
1506 None => self.prepare_mem(&mut exec_state).await?,
1507 }
1508 } else {
1509 self.prepare_mem(&mut exec_state).await?
1510 };
1511
1512 exec_state
1515 .mut_stack()
1516 .push_new_env_for_scope()
1517 .map_err(KclErrorWithOutputs::no_outputs)?;
1518
1519 let (main_ref, _) = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1520
1521 Ok((exec_state, main_ref))
1522 }
1523
1524 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1525 assert!(!self.is_mock());
1526 let grid_scale = if self.settings.fixed_size_grid {
1527 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1528 } else {
1529 GridScaleBehavior::ScaleWithZoom
1530 };
1531
1532 let original_program = program.clone();
1533
1534 let (_program, exec_state, result) = match cache::read_old_ast().await {
1535 Some(mut cached_state) => {
1536 let old = CacheInformation {
1537 ast: &cached_state.main.ast,
1538 settings: &cached_state.settings,
1539 };
1540 let new = CacheInformation {
1541 ast: &program.ast,
1542 settings: &self.settings,
1543 };
1544
1545 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1547 CacheResult::ReExecute {
1548 clear_scene,
1549 reapply_settings,
1550 program: changed_program,
1551 } => {
1552 if reapply_settings
1553 && self
1554 .engine
1555 .reapply_settings(
1556 &self.engine_batch,
1557 &self.settings,
1558 Default::default(),
1559 &mut cached_state.main.exec_state.id_generator,
1560 grid_scale,
1561 )
1562 .await
1563 .is_err()
1564 {
1565 (true, program, None)
1566 } else {
1567 (
1568 clear_scene,
1569 crate::Program {
1570 ast: changed_program,
1571 original_file_contents: program.original_file_contents,
1572 },
1573 None,
1574 )
1575 }
1576 }
1577 CacheResult::CheckImportsOnly {
1578 reapply_settings,
1579 ast: changed_program,
1580 } => {
1581 let mut reapply_failed = false;
1582 if reapply_settings {
1583 if self
1584 .engine
1585 .reapply_settings(
1586 &self.engine_batch,
1587 &self.settings,
1588 Default::default(),
1589 &mut cached_state.main.exec_state.id_generator,
1590 grid_scale,
1591 )
1592 .await
1593 .is_ok()
1594 {
1595 cache::write_old_ast(GlobalState::with_settings(
1596 cached_state.clone(),
1597 self.settings.clone(),
1598 ))
1599 .await;
1600 } else {
1601 reapply_failed = true;
1602 }
1603 }
1604
1605 if reapply_failed {
1606 (true, program, None)
1607 } else {
1608 let mut new_exec_state = ExecState::new(self);
1610 let (new_universe, new_universe_map) =
1611 self.get_universe(&program, &mut new_exec_state).await?;
1612
1613 let clear_scene = new_universe.values().any(|value| {
1614 let id = value.1;
1615 match (
1616 cached_state.exec_state.get_source(id),
1617 new_exec_state.global.get_source(id),
1618 ) {
1619 (Some(s0), Some(s1)) => s0.source != s1.source,
1620 _ => false,
1621 }
1622 });
1623
1624 if !clear_scene {
1625 cache::write_old_memory(
1627 cached_state
1628 .mock_memory_state()
1629 .map_err(KclErrorWithOutputs::no_outputs)?,
1630 )
1631 .await;
1632 return cached_state
1633 .into_exec_outcome(self)
1634 .await
1635 .map_err(KclErrorWithOutputs::no_outputs);
1636 }
1637
1638 (
1639 true,
1640 crate::Program {
1641 ast: changed_program,
1642 original_file_contents: program.original_file_contents,
1643 },
1644 Some((new_universe, new_universe_map, new_exec_state)),
1645 )
1646 }
1647 }
1648 CacheResult::NoAction(true) => {
1649 if self
1650 .engine
1651 .reapply_settings(
1652 &self.engine_batch,
1653 &self.settings,
1654 Default::default(),
1655 &mut cached_state.main.exec_state.id_generator,
1656 grid_scale,
1657 )
1658 .await
1659 .is_ok()
1660 {
1661 cache::write_old_ast(GlobalState::with_settings(
1663 cached_state.clone(),
1664 self.settings.clone(),
1665 ))
1666 .await;
1667
1668 cache::write_old_memory(
1669 cached_state
1670 .mock_memory_state()
1671 .map_err(KclErrorWithOutputs::no_outputs)?,
1672 )
1673 .await;
1674 return cached_state
1675 .into_exec_outcome(self)
1676 .await
1677 .map_err(KclErrorWithOutputs::no_outputs);
1678 }
1679 (true, program, None)
1680 }
1681 CacheResult::NoAction(false) => {
1682 cache::write_old_memory(
1683 cached_state
1684 .mock_memory_state()
1685 .map_err(KclErrorWithOutputs::no_outputs)?,
1686 )
1687 .await;
1688 return cached_state
1689 .into_exec_outcome(self)
1690 .await
1691 .map_err(KclErrorWithOutputs::no_outputs);
1692 }
1693 };
1694
1695 let (exec_state, result) = match import_check_info {
1696 Some((new_universe, new_universe_map, mut new_exec_state)) => {
1697 self.send_clear_scene(&mut new_exec_state, Default::default())
1699 .await
1700 .map_err(KclErrorWithOutputs::no_outputs)?;
1701
1702 let result = self
1703 .run_concurrent(
1704 &program,
1705 &mut new_exec_state,
1706 Some((new_universe, new_universe_map)),
1707 PreserveMem::Normal,
1708 )
1709 .await;
1710
1711 (new_exec_state, result)
1712 }
1713 None if clear_scene => {
1714 let mut exec_state = cached_state.reconstitute_exec_state(self);
1716 exec_state.reset(self);
1717
1718 self.send_clear_scene(&mut exec_state, Default::default())
1719 .await
1720 .map_err(KclErrorWithOutputs::no_outputs)?;
1721
1722 let result = self
1723 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1724 .await;
1725
1726 (exec_state, result)
1727 }
1728 None => {
1729 let mut exec_state = cached_state.reconstitute_exec_state(self);
1730 exec_state
1731 .mut_stack()
1732 .restore_env(cached_state.main.result_env)
1733 .map_err(KclErrorWithOutputs::no_outputs)?;
1734
1735 let result = self
1736 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1737 .await;
1738
1739 (exec_state, result)
1740 }
1741 };
1742
1743 (program, exec_state, result)
1744 }
1745 None => {
1746 let mut exec_state = ExecState::new(self);
1747 self.send_clear_scene(&mut exec_state, Default::default())
1748 .await
1749 .map_err(KclErrorWithOutputs::no_outputs)?;
1750
1751 let result = self
1752 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1753 .await;
1754
1755 (program, exec_state, result)
1756 }
1757 };
1758
1759 if result.is_err() {
1760 cache::bust_cache().await;
1761 }
1762
1763 let result = result?;
1765
1766 cache::write_old_ast(GlobalState::new(
1770 exec_state.clone(),
1771 self.settings.clone(),
1772 original_program.ast,
1773 result.0,
1774 ))
1775 .await;
1776
1777 let outcome = exec_state
1778 .into_exec_outcome(result.0, self)
1779 .await
1780 .map_err(KclErrorWithOutputs::no_outputs)?;
1781 Ok(outcome)
1782 }
1783
1784 pub async fn run(
1788 &self,
1789 program: &crate::Program,
1790 exec_state: &mut ExecState,
1791 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1792 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1793 .await
1794 }
1795
1796 pub async fn run_concurrent(
1801 &self,
1802 program: &crate::Program,
1803 exec_state: &mut ExecState,
1804 universe_info: Option<(Universe, UniverseMap)>,
1805 preserve_mem: PreserveMem,
1806 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1807 exec_state.set_entry_point_kcl_version(program);
1811
1812 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1815 (universe, universe_map)
1816 } else {
1817 self.get_universe(program, exec_state).await?
1818 };
1819
1820 let mut sorted_imports: Vec<_> = universe_map.iter().collect();
1826 sorted_imports.sort_by_key(|(_, import_stmt)| SourceRange::from(*import_stmt));
1827 for (_path, import_stmt) in sorted_imports {
1828 let filename = match &import_stmt.path {
1832 ImportPath::Kcl { filename } => filename.to_string(),
1833 ImportPath::Foreign { path } => path.to_string(),
1834 ImportPath::Std { .. } => continue,
1835 };
1836 if let Some((_, module_id, module_path, _)) = universe.get(&filename)
1837 && let ModulePath::Local { value, .. } = module_path
1838 {
1839 let name = import_stmt
1840 .module_name()
1841 .unwrap_or_else(|| value.file_name().unwrap_or_default());
1842 let source_range = SourceRange::from(import_stmt);
1843 exec_state.push_op(crate::execution::cad_op::Operation::ModuleInstance {
1844 name,
1845 module_id: *module_id,
1846 glob: matches!(
1847 import_stmt.selector,
1848 crate::parsing::ast::types::ImportSelector::Glob(_)
1849 ),
1850 node_path: crate::NodePath::placeholder(),
1851 source_range,
1852 });
1853 }
1854 }
1855
1856 let default_planes = self.engine.get_default_planes().read().await.clone();
1857
1858 self.eval_prelude(exec_state, SourceRange::synthetic())
1860 .await
1861 .map_err(KclErrorWithOutputs::no_outputs)?;
1862
1863 for modules in import_graph::import_graph(&universe, self)
1864 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1865 .into_iter()
1866 {
1867 #[cfg(not(target_arch = "wasm32"))]
1868 let mut set = tokio::task::JoinSet::new();
1869
1870 #[allow(clippy::type_complexity)]
1871 let (results_tx, mut results_rx): (
1872 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1873 tokio::sync::mpsc::Receiver<_>,
1874 ) = tokio::sync::mpsc::channel(1);
1875
1876 for module in modules {
1877 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1878 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1879 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1880 )));
1881 };
1882 let module_id = *module_id;
1883 let module_path = module_path.clone();
1884 let source_range = SourceRange::from(import_stmt);
1885 let module_exec_state = exec_state.clone();
1887
1888 let repr = repr.clone();
1889 let exec_ctxt = self.clone_with_fresh_execution_batch();
1890 let results_tx = results_tx.clone();
1891
1892 let exec_module = async |exec_ctxt: &ExecutorContext,
1893 repr: &ModuleRepr,
1894 module_id: ModuleId,
1895 module_path: &ModulePath,
1896 exec_state: &mut ExecState,
1897 source_range: SourceRange|
1898 -> Result<ModuleRepr, KclError> {
1899 match repr {
1900 ModuleRepr::Kcl(program, _) => {
1901 let result = exec_ctxt
1902 .exec_module_from_ast(
1903 program,
1904 module_id,
1905 module_path,
1906 exec_state,
1907 source_range,
1908 PreserveMem::Normal,
1909 )
1910 .await;
1911
1912 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1913 }
1914 ModuleRepr::Foreign(geom, _) => {
1915 exec_state.mod_local.artifacts = Default::default();
1919 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1920 .await
1921 .map(|geom| Some(KclValue::ImportedGeometry(geom)))
1922 .map_err(|err| err.add_import_location(&module_path.import_name(), source_range));
1927 let module_artifacts = std::mem::take(&mut exec_state.mod_local.artifacts);
1928
1929 result.map(|val| ModuleRepr::Foreign(geom.clone(), Some((val, module_artifacts))))
1930 }
1931 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1932 format!("Module {module_path} not found in universe"),
1933 vec![source_range],
1934 ))),
1935 }
1936 };
1937
1938 #[cfg(target_arch = "wasm32")]
1939 {
1940 wasm_bindgen_futures::spawn_local(async move {
1941 let mut exec_state = module_exec_state;
1942 let exec_ctxt = exec_ctxt;
1943
1944 let result = exec_module(
1945 &exec_ctxt,
1946 &repr,
1947 module_id,
1948 &module_path,
1949 &mut exec_state,
1950 source_range,
1951 )
1952 .await;
1953
1954 results_tx
1955 .send((module_id, module_path, result))
1956 .await
1957 .unwrap_or_default();
1958 });
1959 }
1960 #[cfg(not(target_arch = "wasm32"))]
1961 {
1962 set.spawn(async move {
1963 let mut exec_state = module_exec_state;
1964 let exec_ctxt = exec_ctxt;
1965
1966 let result = exec_module(
1967 &exec_ctxt,
1968 &repr,
1969 module_id,
1970 &module_path,
1971 &mut exec_state,
1972 source_range,
1973 )
1974 .await;
1975
1976 results_tx
1977 .send((module_id, module_path, result))
1978 .await
1979 .unwrap_or_default();
1980 });
1981 }
1982 }
1983
1984 drop(results_tx);
1985
1986 while let Some((module_id, _, result)) = results_rx.recv().await {
1987 match result {
1988 Ok(new_repr) => {
1989 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1990
1991 match &mut repr {
1992 ModuleRepr::Kcl(_, cache) => {
1993 let ModuleRepr::Kcl(_, session_data) = new_repr else {
1994 unreachable!();
1995 };
1996 *cache = session_data;
1997 }
1998 ModuleRepr::Foreign(_, cache) => {
1999 let ModuleRepr::Foreign(_, session_data) = new_repr else {
2000 unreachable!();
2001 };
2002 *cache = session_data;
2003 }
2004 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
2005 }
2006
2007 exec_state.global.module_infos[&module_id].restore_repr(repr);
2008 }
2009 Err(e) => {
2010 let e = import_graph::add_import_backtrace(e, module_id, &universe);
2011 return Err(exec_state.error_with_outputs(e, None, default_planes));
2012 }
2013 }
2014 }
2015 }
2016
2017 exec_state.mod_local.artifacts.operations.clear();
2022
2023 exec_state
2026 .global
2027 .root_module_artifacts
2028 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
2029
2030 self.inner_run(program, exec_state, preserve_mem)
2031 .await
2032 .map_err(|mut error| {
2033 let source_ranges = error.error.source_ranges();
2039 if !source_ranges.is_empty()
2040 && !source_ranges.iter().any(|range| range.module_id().is_top_level())
2041 && let Some(outermost) = source_ranges.last()
2042 {
2043 error.error =
2044 import_graph::add_import_backtrace_from(error.error.clone(), outermost.module_id(), &universe);
2045 }
2046 error
2047 })
2048 }
2049
2050 async fn get_universe(
2053 &self,
2054 program: &crate::Program,
2055 exec_state: &mut ExecState,
2056 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
2057 exec_state.add_root_module_contents(program);
2058
2059 let mut universe = std::collections::HashMap::new();
2060
2061 let default_planes = self.engine.get_default_planes().read().await.clone();
2062
2063 let root_imports = import_graph::import_universe(
2064 self,
2065 &ModulePath::Main,
2066 &ModuleRepr::Kcl(program.ast.clone(), None),
2067 &mut universe,
2068 exec_state,
2069 )
2070 .await
2071 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
2072
2073 Ok((universe, root_imports))
2074 }
2075
2076 async fn inner_run(
2079 &self,
2080 program: &crate::Program,
2081 exec_state: &mut ExecState,
2082 preserve_mem: PreserveMem,
2083 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
2084 let _stats = crate::log::LogPerfStats::new("Interpretation");
2085
2086 exec_state.set_entry_point_kcl_version(program);
2090
2091 let grid_scale = if self.settings.fixed_size_grid {
2093 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
2094 } else {
2095 GridScaleBehavior::ScaleWithZoom
2096 };
2097 self.engine
2098 .reapply_settings(
2099 &self.engine_batch,
2100 &self.settings,
2101 Default::default(),
2102 exec_state.id_generator(),
2103 grid_scale,
2104 )
2105 .await
2106 .map_err(KclErrorWithOutputs::no_outputs)?;
2107
2108 let default_planes = self.engine.get_default_planes().read().await.clone();
2109 let result = self
2110 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
2111 .await;
2112
2113 crate::log::log(format!(
2114 "Post interpretation KCL memory stats: {:#?}",
2115 exec_state.stack().memory.stats()
2116 ));
2117 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
2118
2119 async fn write_old_memory(
2122 ctx: &ExecutorContext,
2123 exec_state: &ExecState,
2124 env_ref: EnvironmentRef,
2125 ) -> Result<(), KclError> {
2126 if ctx.is_mock() {
2127 return Ok(());
2128 }
2129 let mut stack = exec_state.stack().deep_clone()?;
2130 stack.restore_env(env_ref)?;
2131 let state = cache::SketchModeState {
2132 stack,
2133 module_infos: exec_state.global.module_infos.clone(),
2134 path_to_source_id: exec_state.global.path_to_source_id.clone(),
2135 id_to_source: exec_state.global.id_to_source.clone(),
2136 constraint_state: exec_state.mod_local.constraint_state.clone(),
2137 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
2138 };
2139 cache::write_old_memory(state).await;
2140 Ok(())
2141 }
2142
2143 let env_ref = match result {
2144 Ok(env_ref) => env_ref,
2145 Err((err, env_ref)) => {
2146 if let Some(env_ref) = env_ref {
2149 write_old_memory(self, exec_state, env_ref)
2150 .await
2151 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2152 }
2153 return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
2154 }
2155 };
2156
2157 write_old_memory(self, exec_state, env_ref)
2158 .await
2159 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2160
2161 let session_data = self.engine.get_session_data().await;
2162
2163 Ok((env_ref, session_data))
2164 }
2165
2166 async fn execute_and_build_graph(
2169 &self,
2170 program: NodeRef<'_, crate::parsing::ast::types::Program>,
2171 exec_state: &mut ExecState,
2172 preserve_mem: PreserveMem,
2173 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
2174 let start_op = exec_state.global.root_module_artifacts.operations.len();
2180
2181 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
2182 .await
2183 .map_err(|e| (e, None))?;
2184
2185 let exec_result = self
2186 .exec_module_body(
2187 program,
2188 exec_state,
2189 preserve_mem,
2190 ModuleId::default(),
2191 &ModulePath::Main,
2192 )
2193 .await
2194 .map(
2195 |ModuleExecutionOutcome {
2196 environment: env_ref,
2197 artifacts: module_artifacts,
2198 ..
2199 }| {
2200 exec_state.global.root_module_artifacts.extend(module_artifacts);
2203 env_ref
2204 },
2205 )
2206 .map_err(|(err, env_ref, module_artifacts)| {
2207 if let Some(module_artifacts) = module_artifacts {
2208 exec_state.global.root_module_artifacts.extend(module_artifacts);
2211 }
2212 (err, env_ref)
2213 });
2214
2215 let programs = &exec_state.build_program_lookup(program.clone());
2217 let cached_body_items = exec_state.global.artifacts.cached_body_items();
2218 for op in exec_state
2219 .global
2220 .root_module_artifacts
2221 .operations
2222 .iter_mut()
2223 .skip(start_op)
2224 {
2225 op.fill_node_paths(programs, cached_body_items);
2226 }
2227 for module in exec_state.global.module_infos.values_mut() {
2228 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
2229 for op in &mut outcome.artifacts.operations {
2230 op.fill_node_paths(programs, cached_body_items);
2231 }
2232 }
2233 }
2234
2235 self.engine
2237 .ensure_async_commands_completed(&self.engine_batch)
2238 .await
2239 .map_err(|e| {
2240 match &exec_result {
2241 Ok(env_ref) => (e, Some(*env_ref)),
2242 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
2244 }
2245 })?;
2246
2247 self.engine.clear_queues(&self.engine_batch).await;
2250
2251 match exec_state.build_artifact_graph(&self.engine, program).await {
2252 Ok(_) => exec_result,
2253 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
2254 }
2255 }
2256
2257 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
2261 if exec_state.stack().memory.requires_std() {
2262 let initial_ops = exec_state.mod_local.artifacts.operations.len();
2263
2264 let path = vec!["std".to_owned(), "prelude".to_owned()];
2265 let resolved_path = ModulePath::from_std_import_path(&path)?;
2266 let id = self
2267 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
2268 .await?;
2269 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
2270
2271 exec_state.mut_stack().memory.set_std(module_memory)?;
2272
2273 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
2279 }
2280
2281 Ok(())
2282 }
2283
2284 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
2286 self.engine
2288 .send_modeling_cmd(
2289 &self.engine_batch,
2290 uuid::Uuid::new_v4(),
2291 crate::execution::SourceRange::default(),
2292 &ModelingCmd::from(
2293 mcmd::ZoomToFit::builder()
2294 .object_ids(Default::default())
2295 .animated(false)
2296 .padding(0.1)
2297 .build(),
2298 ),
2299 )
2300 .await
2301 .map_err(KclErrorWithOutputs::no_outputs)?;
2302
2303 let resp = self
2305 .engine
2306 .send_modeling_cmd(
2307 &self.engine_batch,
2308 uuid::Uuid::new_v4(),
2309 crate::execution::SourceRange::default(),
2310 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
2311 )
2312 .await
2313 .map_err(KclErrorWithOutputs::no_outputs)?;
2314
2315 let OkWebSocketResponseData::Modeling {
2316 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
2317 } = resp
2318 else {
2319 return Err(ExecError::BadPng(format!(
2320 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
2321 )));
2322 };
2323 Ok(contents)
2324 }
2325
2326 pub async fn export(
2328 &self,
2329 format: kittycad_modeling_cmds::format::OutputFormat3d,
2330 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2331 let resp = self
2332 .engine
2333 .send_modeling_cmd(
2334 &self.engine_batch,
2335 uuid::Uuid::new_v4(),
2336 crate::SourceRange::default(),
2337 &kittycad_modeling_cmds::ModelingCmd::Export(
2338 kittycad_modeling_cmds::Export::builder()
2339 .entity_ids(vec![])
2340 .format(format)
2341 .build(),
2342 ),
2343 )
2344 .await?;
2345
2346 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
2347 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
2348 format!("Expected Export response, got {resp:?}",),
2349 vec![SourceRange::default()],
2350 )));
2351 };
2352
2353 Ok(files)
2354 }
2355
2356 pub async fn export_step(
2358 &self,
2359 deterministic_time: bool,
2360 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2361 let files = self
2362 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
2363 kittycad_modeling_cmds::format::step::export::Options::builder()
2364 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
2365 .maybe_created(if deterministic_time {
2366 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
2367 KclError::new_internal(crate::errors::KclErrorDetails::new(
2368 format!("Failed to parse date: {e}"),
2369 vec![SourceRange::default()],
2370 ))
2371 })?)
2372 } else {
2373 None
2374 })
2375 .build(),
2376 ))
2377 .await?;
2378
2379 Ok(files)
2380 }
2381
2382 pub async fn close(&self) {
2383 self.engine.close().await;
2384 }
2385}
2386
2387pub use kcl_api::ArtifactId;
2388
2389pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
2390 ArtifactId::new(*id.as_ref())
2391}
2392
2393#[cfg(test)]
2394pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2395 parse_execute_with_project_dir(code, None).await
2396}
2397
2398#[cfg(test)]
2399pub(crate) async fn parse_execute_with_project_dir(
2400 code: &str,
2401 project_directory: Option<TypedPath>,
2402) -> Result<ExecTestResults, KclError> {
2403 parse_execute_with_executor_kind(code, project_directory, machine::ExecutorKind::resolve()).await
2405}
2406
2407#[cfg(test)]
2410pub(crate) fn new_mock_executor_context(
2411 project_directory: Option<TypedPath>,
2412 executor_kind: machine::ExecutorKind,
2413) -> ExecutorContext {
2414 ExecutorContext {
2415 engine: Arc::new(EngineManager::new_mock()),
2416 engine_batch: EngineBatchContext::default(),
2417 fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2418 settings: ExecutorSettings {
2419 project_directory,
2420 ..Default::default()
2421 },
2422 context_type: ContextType::Mock,
2423 execution_callbacks: Default::default(),
2424 executor_kind,
2425 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
2426 }
2427}
2428
2429#[cfg(test)]
2430pub(crate) async fn parse_execute_with_executor_kind(
2431 code: &str,
2432 project_directory: Option<TypedPath>,
2433 executor_kind: machine::ExecutorKind,
2434) -> Result<ExecTestResults, KclError> {
2435 let program = crate::Program::parse_no_errs(code)?;
2436
2437 let exec_ctxt = new_mock_executor_context(project_directory, executor_kind);
2438 let mut exec_state = ExecState::new(&exec_ctxt);
2439 let result = exec_ctxt.run(&program, &mut exec_state).await?;
2440
2441 Ok(ExecTestResults {
2442 program,
2443 mem_env: result.0,
2444 exec_ctxt,
2445 exec_state,
2446 })
2447}
2448
2449#[cfg(test)]
2450#[derive(Debug)]
2451pub(crate) struct ExecTestResults {
2452 program: crate::Program,
2453 mem_env: EnvironmentRef,
2454 exec_ctxt: ExecutorContext,
2455 exec_state: ExecState,
2456}
2457
2458#[cfg(test)]
2459impl ExecTestResults {
2460 pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2461 &self.exec_state.global.root_module_artifacts.commands
2462 }
2463
2464 pub(crate) fn issues(&self) -> &[CompilationIssue] {
2468 self.exec_state.issues()
2469 }
2470
2471 #[track_caller]
2475 pub(crate) fn variable(&self, name: &str) -> KclValue {
2476 self.exec_state
2477 .stack()
2478 .memory
2479 .get_from_unchecked(name, self.mem_env)
2480 .unwrap()
2481 }
2482}
2483
2484pub struct ProgramLookup {
2488 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2489}
2490
2491impl ProgramLookup {
2492 pub fn new(
2495 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2496 module_infos: state::ModuleInfoMap,
2497 ) -> Self {
2498 let mut programs = IndexMap::with_capacity(module_infos.len());
2499 for (id, info) in module_infos {
2500 if let ModuleRepr::Kcl(program, _) = info.repr {
2501 programs.insert(id, program);
2502 }
2503 }
2504 programs.insert(ModuleId::default(), current);
2505 Self { programs }
2506 }
2507
2508 pub fn program_for_module(
2509 &self,
2510 module_id: ModuleId,
2511 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2512 self.programs.get(&module_id)
2513 }
2514}
2515
2516#[cfg(test)]
2517mod tests {
2518 use kcl_api::NumericType;
2519 use pretty_assertions::assert_eq;
2520
2521 use super::*;
2522 use crate::ModuleId;
2523 use crate::errors::KclErrorDetails;
2524 use crate::errors::Severity;
2525 use crate::execution::kcl_value::TypeDef;
2526 use crate::execution::memory::Stack;
2527 use crate::execution::types::RuntimeType;
2528
2529 macro_rules! kcl_input {
2530 ($file:literal) => {
2531 include_str!(concat!("../../e2e/executor/inputs/", $file, ".kcl"))
2532 };
2533 }
2534
2535 #[test]
2536 fn clone_with_fresh_execution_batch_keeps_executor_selection() {
2537 let mut ctx = new_mock_executor_context(None, machine::ExecutorKind::Machine);
2541 ctx.machine_call_depth_limit = 123;
2542 let cloned = ctx.clone_with_fresh_execution_batch();
2543 assert_eq!(cloned.executor_kind, machine::ExecutorKind::Machine);
2544 assert_eq!(cloned.machine_call_depth_limit, 123);
2545 }
2546
2547 #[tokio::test(flavor = "multi_thread")]
2548 async fn concurrent_foreign_import_preserves_artifact_command() {
2549 let tmpdir = tempfile::TempDir::with_prefix("zma_foreign_import_artifact").unwrap();
2550 tokio::fs::write(tmpdir.path().join("cube.obj"), "o cube\n")
2551 .await
2552 .unwrap();
2553
2554 let program = crate::Program::parse_no_errs("import \"cube.obj\" as cube\n\nmodel = cube\n").unwrap();
2555 let ctx = new_mock_executor_context(
2556 Some(crate::TypedPath(tmpdir.path().into())),
2557 machine::ExecutorKind::resolve(),
2558 );
2559 let mut exec_state = ExecState::new(&ctx);
2560 let (main_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2561 let outcome = exec_state
2562 .into_exec_outcome(main_ref, &ctx)
2563 .await
2564 .expect("foreign import execution should produce an outcome");
2565 ctx.close().await;
2566
2567 let KclValueView::ImportedGeometry(imported) = &outcome.variables["model"] else {
2568 panic!("model should be imported geometry");
2569 };
2570 let artifact_id = ArtifactId::new(imported.id);
2571 let Some(Artifact::ImportedGeometry(artifact)) = outcome.artifact_graph.get(&artifact_id) else {
2572 panic!("foreign import should produce an imported geometry artifact");
2573 };
2574 assert_eq!(artifact.id, artifact_id);
2575 assert!(!artifact.code_ref.node_path.is_empty());
2576 }
2577
2578 #[tokio::test(flavor = "multi_thread")]
2579 async fn nested_import_preserves_inner_error_and_backtrace() {
2580 let project_dir = crate::TypedPath::new("/zma-kcl-import-error");
2585 let main_path = project_dir.join("main.kcl");
2586 let assembly_path = project_dir.join("assembly.kcl");
2587 let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
2588 let files = [
2591 (
2592 project_dir.join("broken.kcl").to_string(),
2593 b"export brokenValue = missingName + 1\n".to_vec(),
2594 ),
2595 (
2596 assembly_path.to_string(),
2597 b"import brokenValue from \"broken.kcl\"\n\nexport assemblyValue = brokenValue\n".to_vec(),
2598 ),
2599 ]
2600 .into_iter()
2601 .collect();
2602 let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
2603 let settings = ExecutorSettings {
2604 project_directory: Some(project_dir),
2605 current_file: Some(main_path.clone()),
2606 ..Default::default()
2607 };
2608 let program = crate::Program::parse_no_errs(main_code).unwrap();
2609
2610 let assert_error = |error: &KclErrorWithOutputs| {
2611 let KclError::UndefinedValue { details, name } = &error.error else {
2612 panic!("expected UndefinedValue, got {:#?}", error.error);
2613 };
2614 assert_eq!(name.as_deref(), Some("missingName"));
2615 assert_eq!(details.message, "`missingName` is not defined");
2616 assert_eq!(
2617 error
2618 .error
2619 .backtrace()
2620 .iter()
2621 .map(|frame| frame.fn_name.as_deref())
2622 .collect::<Vec<_>>(),
2623 [Some("import broken.kcl"), Some("import assembly.kcl"), None]
2624 );
2625 assert_eq!(
2626 error
2627 .error
2628 .backtrace()
2629 .iter()
2630 .map(|frame| frame.kind)
2631 .collect::<Vec<_>>(),
2632 [
2633 kcl_error::BacktraceItemKind::Import,
2634 kcl_error::BacktraceItemKind::Import,
2635 kcl_error::BacktraceItemKind::Call
2636 ]
2637 );
2638
2639 let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
2640 assert!(report.filename.ends_with("broken.kcl"));
2641 assert_eq!(
2642 report
2643 .related
2644 .iter()
2645 .map(|related| related.filename.as_str())
2646 .collect::<Vec<_>>(),
2647 [assembly_path.to_string(), main_path.to_string()]
2648 );
2649
2650 let rendered = format!("{:?}", miette::Report::new(report));
2651 assert!(rendered.contains("broken.kcl"));
2652 assert!(rendered.contains("assembly.kcl"));
2653 assert!(rendered.contains("main.kcl"));
2654 assert!(rendered.contains("export brokenValue = missingName + 1"));
2655 assert!(!rendered.contains("Failed to read contents"));
2656 };
2657
2658 let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
2659 mock_ctx.fs = fs.clone();
2660 let mock_error = mock_ctx
2661 .run_mock(
2662 &program,
2663 &MockConfig {
2664 use_prev_memory: false,
2665 ..Default::default()
2666 },
2667 )
2668 .await
2669 .unwrap_err();
2670 mock_ctx.close().await;
2671 assert_error(&mock_error);
2672
2673 let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
2674 concurrent_ctx.fs = fs;
2675 let mut exec_state = ExecState::new(&concurrent_ctx);
2676 let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
2677 concurrent_ctx.close().await;
2678 assert_error(&concurrent_error);
2679 }
2680
2681 #[tokio::test(flavor = "multi_thread")]
2682 async fn function_error_across_import_keeps_backtrace_innermost_first() {
2683 let project_dir = crate::TypedPath::new("/zma-kcl-import-fn-error");
2687 let main_path = project_dir.join("main.kcl");
2688 let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
2689 let files = [
2690 (
2691 project_dir.join("helper.kcl").to_string(),
2692 b"export fn inner() { return missingName }\nexport fn outer() { return inner() }\n".to_vec(),
2693 ),
2694 (
2695 project_dir.join("assembly.kcl").to_string(),
2696 b"import outer from \"helper.kcl\"\n\nexport assemblyValue = outer()\n".to_vec(),
2697 ),
2698 ]
2699 .into_iter()
2700 .collect();
2701 let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
2702 let settings = ExecutorSettings {
2703 project_directory: Some(project_dir.clone()),
2704 current_file: Some(main_path),
2705 ..Default::default()
2706 };
2707 let program = crate::Program::parse_no_errs(main_code).unwrap();
2708
2709 let assert_error = |error: &KclErrorWithOutputs| {
2710 assert!(
2711 matches!(&error.error, KclError::UndefinedValue { .. }),
2712 "expected UndefinedValue, got {:#?}",
2713 error.error
2714 );
2715 assert_eq!(
2716 error
2717 .error
2718 .backtrace()
2719 .iter()
2720 .map(|frame| frame.fn_name.as_deref())
2721 .collect::<Vec<_>>(),
2722 [Some("inner"), Some("outer"), Some("import assembly.kcl"), None]
2723 );
2724 assert_eq!(
2725 error
2726 .error
2727 .backtrace()
2728 .iter()
2729 .map(|frame| frame.kind)
2730 .collect::<Vec<_>>(),
2731 [
2732 kcl_error::BacktraceItemKind::Call,
2733 kcl_error::BacktraceItemKind::Call,
2734 kcl_error::BacktraceItemKind::Import,
2735 kcl_error::BacktraceItemKind::Call
2736 ]
2737 );
2738
2739 let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
2740 assert!(report.filename.ends_with("helper.kcl"));
2741 assert_eq!(
2742 report
2743 .related
2744 .iter()
2745 .map(|related| related.filename.as_str())
2746 .collect::<Vec<_>>(),
2747 [
2748 project_dir.join("assembly.kcl").to_string(),
2749 project_dir.join("main.kcl").to_string()
2750 ]
2751 );
2752 let rendered = format!("{:?}", miette::Report::new(report));
2753 assert!(rendered.contains("return missingName"));
2754 assert!(!rendered.contains("Failed to read contents"));
2755 };
2756
2757 let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
2758 mock_ctx.fs = fs.clone();
2759 let mock_error = mock_ctx
2760 .run_mock(
2761 &program,
2762 &MockConfig {
2763 use_prev_memory: false,
2764 ..Default::default()
2765 },
2766 )
2767 .await
2768 .unwrap_err();
2769 mock_ctx.close().await;
2770 assert_error(&mock_error);
2771
2772 let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
2773 concurrent_ctx.fs = fs;
2774 let mut exec_state = ExecState::new(&concurrent_ctx);
2775 let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
2776 concurrent_ctx.close().await;
2777 assert_error(&concurrent_error);
2778 }
2779
2780 #[track_caller]
2782 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2783 memory.memory.get_from_unchecked(name, env).unwrap()
2784 }
2785
2786 async fn execute_variables_with_backend(
2787 code: &str,
2788 backend: memory::MemoryBackendKind,
2789 ) -> IndexMap<String, KclValueView> {
2790 execute_outcome_with_backend(code, backend).await.variables
2791 }
2792
2793 async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
2794 let program = crate::Program::parse_no_errs(code).unwrap();
2795 let ctx = ExecutorContext::new_mock(None).await;
2796 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2797 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2798 let outcome = exec_state
2799 .into_exec_outcome(env_ref, &ctx)
2800 .await
2801 .expect("test execution outcome should collect variables");
2802 ctx.close().await;
2803 outcome
2804 }
2805
2806 async fn execute_error_variables_with_backend(
2807 code: &str,
2808 backend: memory::MemoryBackendKind,
2809 ) -> IndexMap<String, KclValueView> {
2810 let program = crate::Program::parse_no_errs(code).unwrap();
2811 let ctx = ExecutorContext::new_mock(None).await;
2812 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2813 let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
2814 ctx.close().await;
2815 error.variables
2816 }
2817
2818 async fn execute_project_variables_with_backend(
2819 main_code: &str,
2820 files: &[(&str, &str)],
2821 backend: memory::MemoryBackendKind,
2822 ) -> IndexMap<String, KclValueView> {
2823 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
2824 for (name, contents) in files {
2825 tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
2826 }
2827
2828 let program = crate::Program::parse_no_errs(main_code).unwrap();
2829 let ctx = ExecutorContext {
2830 engine: Arc::new(EngineManager::new_mock()),
2831 engine_batch: EngineBatchContext::default(),
2832 fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2833 settings: ExecutorSettings {
2834 project_directory: Some(crate::TypedPath(tmpdir.path().into())),
2835 ..Default::default()
2836 },
2837 context_type: ContextType::Mock,
2838 execution_callbacks: Default::default(),
2839 executor_kind: machine::ExecutorKind::resolve(),
2840 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
2841 };
2842 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2843 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2844 let outcome = exec_state
2845 .into_exec_outcome(env_ref, &ctx)
2846 .await
2847 .expect("test execution outcome should collect variables");
2848 ctx.close().await;
2849 outcome.variables
2850 }
2851
2852 async fn run_with_caching_variables_with_backend(
2853 code: &str,
2854 backend: memory::MemoryBackendKind,
2855 ) -> IndexMap<String, KclValueView> {
2856 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2857 cache::bust_cache().await;
2858 clear_mem_cache().await;
2859
2860 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
2861 let program = crate::Program::parse_no_errs(code).unwrap();
2862 ctx.run_with_caching(program.clone()).await.unwrap();
2863 let cached = ctx.run_with_caching(program).await.unwrap();
2864
2865 cache::bust_cache().await;
2866 clear_mem_cache().await;
2867 ctx.close().await;
2868 cached.variables
2869 }
2870
2871 async fn run_mock_variables_with_backend(
2872 code: &str,
2873 backend: memory::MemoryBackendKind,
2874 ) -> IndexMap<String, KclValueView> {
2875 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2876 clear_mem_cache().await;
2877
2878 let ctx = ExecutorContext::new_mock(None).await;
2879 let first = crate::Program::parse_no_errs("x = 2").unwrap();
2880 ctx.run_mock(
2881 &first,
2882 &MockConfig {
2883 use_prev_memory: false,
2884 ..Default::default()
2885 },
2886 )
2887 .await
2888 .unwrap();
2889
2890 let program = crate::Program::parse_no_errs(code).unwrap();
2891 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
2892
2893 clear_mem_cache().await;
2894 ctx.close().await;
2895 outcome.variables
2896 }
2897
2898 fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
2899 let mut keys = variables.keys().cloned().collect::<Vec<_>>();
2900 keys.sort();
2901 keys
2902 }
2903
2904 async fn collect_backend_results<T, Fut>(
2905 mut run: impl FnMut(memory::MemoryBackendKind) -> Fut,
2906 ) -> Vec<(memory::MemoryBackendKind, T)>
2907 where
2908 Fut: std::future::Future<Output = T>,
2909 {
2910 let all = memory::MemoryBackendKind::all();
2911 let mut results = Vec::with_capacity(all.len());
2912 for &kind in all {
2913 results.push((kind, run(kind).await));
2914 }
2915 results
2916 }
2917
2918 fn assert_backend_results_match<T>(results: &[(memory::MemoryBackendKind, T)])
2919 where
2920 T: std::fmt::Debug + PartialEq,
2921 {
2922 let (first, rest) = results.split_first().expect("expected at least one memory backend");
2923 let (first_kind, first_result) = first;
2924 for (kind, result) in rest {
2925 assert_eq!(
2926 result, first_result,
2927 "memory kind {kind:?} doesn't match {first_kind:?}"
2928 );
2929 }
2930 }
2931
2932 fn assert_backend_variable_results_match_expected_keys(
2933 results: &[(memory::MemoryBackendKind, IndexMap<String, KclValueView>)],
2934 expected_keys: &[&str],
2935 ) {
2936 let (first_kind, first_variables) = results.first().expect("expected at least one memory backend");
2937 let expected_keys = expected_keys.iter().map(|key| (*key).to_owned()).collect::<Vec<_>>();
2938 assert_eq!(
2939 sorted_variable_keys(first_variables),
2940 expected_keys,
2941 "memory kind {first_kind:?} doesn't match expected variables"
2942 );
2943 assert_backend_results_match(results);
2944 }
2945
2946 fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
2947 let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
2948 let KclValueView::Number { value, .. } = value else {
2949 panic!("expected `{key}` to be a number, got {value:?}");
2950 };
2951 assert_eq!(*value, expected, "{key}: {value:?}");
2952 }
2953
2954 #[tokio::test(flavor = "multi_thread")]
2955 async fn exec_outcome_variables_match_between_memory_backends() {
2956 let code = "x = 2\ny = x + 1\narr = [x, y]";
2957
2958 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2959
2960 assert_backend_variable_results_match_expected_keys(&results, &["arr", "x", "y"]);
2961 }
2962
2963 #[tokio::test(flavor = "multi_thread")]
2964 async fn error_output_variables_match_between_memory_backends() {
2965 let code = "x = 2\ny = missing + 1";
2966
2967 let results = collect_backend_results(|kind| execute_error_variables_with_backend(code, kind)).await;
2968
2969 assert_backend_variable_results_match_expected_keys(&results, &["x"]);
2970 }
2971
2972 #[tokio::test(flavor = "multi_thread")]
2973 async fn cached_execution_variables_match_between_memory_backends() {
2974 let code = "x = 2\ny = x + 1";
2975
2976 let results = collect_backend_results(|kind| run_with_caching_variables_with_backend(code, kind)).await;
2977
2978 assert_backend_variable_results_match_expected_keys(&results, &["x", "y"]);
2979 }
2980
2981 #[tokio::test(flavor = "multi_thread")]
2982 async fn mock_execution_variables_match_between_memory_backends() {
2983 let code = "y = x + 1";
2984
2985 let results = collect_backend_results(|kind| run_mock_variables_with_backend(code, kind)).await;
2986
2987 assert_backend_variable_results_match_expected_keys(&results, &["y"]);
2988 }
2989
2990 #[tokio::test(flavor = "multi_thread")]
2991 async fn module_imports_and_exported_closures_match_between_memory_backends() {
2992 let module_code = r#"
2993export base = 40
2994
2995export fn addBase(n) {
2996 return n + base
2997}
2998"#;
2999 let main_code = r#"
3000import base, addBase from 'math.kcl'
3001import 'math.kcl'
3002
3003named = addBase(n = 2)
3004qualified = math::addBase(n = 1)
3005direct = math::base
3006"#;
3007
3008 let files = [("math.kcl", module_code)];
3009 let results =
3010 collect_backend_results(|kind| execute_project_variables_with_backend(main_code, &files, kind)).await;
3011
3012 let (_, first_variables) = results.first().expect("expected at least one memory backend");
3013 assert_number_variable(first_variables, "named", 42.0);
3014 assert_number_variable(first_variables, "qualified", 41.0);
3015 assert_number_variable(first_variables, "direct", 40.0);
3016 assert_backend_results_match(&results);
3017 }
3018
3019 #[tokio::test(flavor = "multi_thread")]
3020 async fn sketch_block_variables_match_between_memory_backends() {
3021 let code = r#"
3022sketch001 = sketch(on = XY) {
3023 line1 = line(start = [0, 0], end = [1, 0])
3024 line2 = line(start = [1, 0], end = [0, 1])
3025}
3026lineCount = 2
3027"#;
3028
3029 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
3030
3031 let (_, first_variables) = results.first().expect("expected at least one memory backend");
3032 assert!(first_variables.contains_key("sketch001"), "actual: {first_variables:?}");
3033 assert_number_variable(first_variables, "lineCount", 2.0);
3034 assert_backend_results_match(&results);
3035 }
3036
3037 #[tokio::test(flavor = "multi_thread")]
3038 async fn tag_call_stack_lookup_matches_between_memory_backends() {
3039 let code = r#"
3040sketch001 = startSketchOn(XY)
3041 |> startProfile(at = [0, 0])
3042 |> xLine(length = 10, tag = $seg01)
3043
3044segLength = segLen(seg01)
3045"#;
3046
3047 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
3048
3049 let (_, first_variables) = results.first().expect("expected at least one memory backend");
3050 assert_number_variable(first_variables, "segLength", 10.0);
3051 assert_backend_results_match(&results);
3052 }
3053
3054 #[tokio::test(flavor = "multi_thread")]
3055 async fn test_execute_warn() {
3056 let text = "@blah";
3057 let result = parse_execute(text).await.unwrap();
3058 let errs = result.exec_state.issues();
3059 assert_eq!(errs.len(), 1);
3060 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
3061 assert!(
3062 errs[0].message.contains("Unknown annotation"),
3063 "unexpected warning message: {}",
3064 errs[0].message
3065 );
3066 }
3067
3068 #[tokio::test(flavor = "multi_thread")]
3069 async fn test_execute_fn_definitions() {
3070 let ast = r#"fn def(@x) {
3071 return x
3072}
3073fn ghi(@x) {
3074 return x
3075}
3076fn jkl(@x) {
3077 return x
3078}
3079fn hmm(@x) {
3080 return x
3081}
3082
3083yo = 5 + 6
3084
3085abc = 3
3086identifierGuy = 5
3087part001 = startSketchOn(XY)
3088|> startProfile(at = [-1.2, 4.83])
3089|> line(end = [2.8, 0])
3090|> angledLine(angle = 100 + 100, length = 3.01)
3091|> angledLine(angle = abc, length = 3.02)
3092|> angledLine(angle = def(yo), length = 3.03)
3093|> angledLine(angle = ghi(2), length = 3.04)
3094|> angledLine(angle = jkl(yo) + 2, length = 3.05)
3095|> close()
3096yo2 = hmm([identifierGuy + 5])"#;
3097
3098 parse_execute(ast).await.unwrap();
3099 }
3100
3101 #[tokio::test(flavor = "multi_thread")]
3102 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
3103 let code = r#"
3104firstProfile = sketch(on = XY) {
3105 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
3106 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
3107 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
3108 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
3109 coincident([edge1.end, edge2.start])
3110 coincident([edge2.end, edge3.start])
3111 coincident([edge3.end, edge4.start])
3112 coincident([edge4.end, edge1.start])
3113}
3114
3115secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
3116 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
3117 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
3118 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
3119 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
3120 coincident([edge5.end, edge6.start])
3121 coincident([edge6.end, edge7.start])
3122 coincident([edge7.end, edge8.start])
3123 coincident([edge8.end, edge5.start])
3124}
3125
3126firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
3127secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
3128"#;
3129
3130 let result = parse_execute(code).await.unwrap();
3131 assert!(result.exec_state.issues().is_empty());
3132 }
3133
3134 #[tokio::test(flavor = "multi_thread")]
3135 async fn sketch_block_artifact_preserves_standard_plane_name() {
3136 let code = r#"
3137sketch001 = sketch(on = -YZ) {
3138 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
3139}
3140"#;
3141
3142 let result = parse_execute(code).await.unwrap();
3143 let sketch_blocks = result
3144 .exec_state
3145 .global
3146 .artifacts
3147 .graph
3148 .values()
3149 .filter_map(|artifact| match artifact {
3150 Artifact::SketchBlock(block) => Some(block),
3151 _ => None,
3152 })
3153 .collect::<Vec<_>>();
3154
3155 assert_eq!(sketch_blocks.len(), 1);
3156 assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
3157 }
3158
3159 #[tokio::test(flavor = "multi_thread")]
3160 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
3161 let code = r#"
3162sketch001 = sketch(on = YZ) {
3163 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
3164 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
3165 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
3166 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
3167 coincident([line1.end, line2.start])
3168 coincident([line2.end, line3.start])
3169 coincident([line3.end, line4.start])
3170 coincident([line4.end, line1.start])
3171}
3172
3173sketch002 = sketch(on = -XZ) {
3174 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
3175 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
3176 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
3177 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
3178 coincident([line5.end, line6.start])
3179 coincident([line6.end, line7.start])
3180 coincident([line7.end, line8.start])
3181 coincident([line8.end, line5.start])
3182}
3183
3184region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
3185extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
3186region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
3187extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
3188
3189myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
3190"#;
3191
3192 let result = parse_execute(code).await.unwrap();
3193 assert!(result.exec_state.issues().is_empty());
3194 }
3195
3196 #[tokio::test(flavor = "multi_thread")]
3197 async fn issue_10741_point_circle_coincident_executes() {
3198 let code = r#"
3199sketch001 = sketch(on = YZ) {
3200 circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
3201 line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
3202 coincident([line1.start, circle1])
3203}
3204"#;
3205
3206 let result = parse_execute(code).await.unwrap();
3207 assert!(
3208 result
3209 .exec_state
3210 .issues()
3211 .iter()
3212 .all(|issue| issue.severity != Severity::Error),
3213 "unexpected execution issues: {:#?}",
3214 result.exec_state.issues()
3215 );
3216 }
3217
3218 #[tokio::test(flavor = "multi_thread")]
3219 async fn test_execute_with_pipe_substitutions_unary() {
3220 let ast = r#"myVar = 3
3221part001 = startSketchOn(XY)
3222 |> startProfile(at = [0, 0])
3223 |> line(end = [3, 4], tag = $seg01)
3224 |> line(end = [
3225 min([segLen(seg01), myVar]),
3226 -legLen(hypotenuse = segLen(seg01), leg = myVar)
3227])
3228"#;
3229
3230 parse_execute(ast).await.unwrap();
3231 }
3232
3233 #[tokio::test(flavor = "multi_thread")]
3234 async fn test_execute_with_pipe_substitutions() {
3235 let ast = r#"myVar = 3
3236part001 = startSketchOn(XY)
3237 |> startProfile(at = [0, 0])
3238 |> line(end = [3, 4], tag = $seg01)
3239 |> line(end = [
3240 min([segLen(seg01), myVar]),
3241 legLen(hypotenuse = segLen(seg01), leg = myVar)
3242])
3243"#;
3244
3245 parse_execute(ast).await.unwrap();
3246 }
3247
3248 #[tokio::test(flavor = "multi_thread")]
3249 async fn test_execute_with_inline_comment() {
3250 let ast = r#"baseThick = 1
3251armAngle = 60
3252
3253baseThickHalf = baseThick / 2
3254halfArmAngle = armAngle / 2
3255
3256arrExpShouldNotBeIncluded = [1, 2, 3]
3257objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
3258
3259part001 = startSketchOn(XY)
3260 |> startProfile(at = [0, 0])
3261 |> yLine(endAbsolute = 1)
3262 |> xLine(length = 3.84) // selection-range-7ish-before-this
3263
3264variableBelowShouldNotBeIncluded = 3
3265"#;
3266
3267 parse_execute(ast).await.unwrap();
3268 }
3269
3270 #[tokio::test(flavor = "multi_thread")]
3271 async fn test_execute_with_function_literal_in_pipe() {
3272 let ast = r#"w = 20
3273l = 8
3274h = 10
3275
3276fn thing() {
3277 return -8
3278}
3279
3280firstExtrude = startSketchOn(XY)
3281 |> startProfile(at = [0,0])
3282 |> line(end = [0, l])
3283 |> line(end = [w, 0])
3284 |> line(end = [0, thing()])
3285 |> close()
3286 |> extrude(length = h)"#;
3287
3288 parse_execute(ast).await.unwrap();
3289 }
3290
3291 #[tokio::test(flavor = "multi_thread")]
3292 async fn test_execute_with_function_unary_in_pipe() {
3293 let ast = r#"w = 20
3294l = 8
3295h = 10
3296
3297fn thing(@x) {
3298 return -x
3299}
3300
3301firstExtrude = startSketchOn(XY)
3302 |> startProfile(at = [0,0])
3303 |> line(end = [0, l])
3304 |> line(end = [w, 0])
3305 |> line(end = [0, thing(8)])
3306 |> close()
3307 |> extrude(length = h)"#;
3308
3309 parse_execute(ast).await.unwrap();
3310 }
3311
3312 #[tokio::test(flavor = "multi_thread")]
3313 async fn test_execute_with_function_array_in_pipe() {
3314 let ast = r#"w = 20
3315l = 8
3316h = 10
3317
3318fn thing(@x) {
3319 return [0, -x]
3320}
3321
3322firstExtrude = startSketchOn(XY)
3323 |> startProfile(at = [0,0])
3324 |> line(end = [0, l])
3325 |> line(end = [w, 0])
3326 |> line(end = thing(8))
3327 |> close()
3328 |> extrude(length = h)"#;
3329
3330 parse_execute(ast).await.unwrap();
3331 }
3332
3333 #[tokio::test(flavor = "multi_thread")]
3334 async fn test_execute_with_function_call_in_pipe() {
3335 let ast = r#"w = 20
3336l = 8
3337h = 10
3338
3339fn other_thing(@y) {
3340 return -y
3341}
3342
3343fn thing(@x) {
3344 return other_thing(x)
3345}
3346
3347firstExtrude = startSketchOn(XY)
3348 |> startProfile(at = [0,0])
3349 |> line(end = [0, l])
3350 |> line(end = [w, 0])
3351 |> line(end = [0, thing(8)])
3352 |> close()
3353 |> extrude(length = h)"#;
3354
3355 parse_execute(ast).await.unwrap();
3356 }
3357
3358 #[tokio::test(flavor = "multi_thread")]
3359 async fn test_execute_with_function_sketch() {
3360 let ast = r#"fn box(h, l, w) {
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
3372fnBox = box(h = 3, l = 6, w = 10)"#;
3373
3374 parse_execute(ast).await.unwrap();
3375 }
3376
3377 #[tokio::test(flavor = "multi_thread")]
3378 async fn test_get_member_of_object_with_function_period() {
3379 let ast = r#"fn box(@obj) {
3380 myBox = startSketchOn(XY)
3381 |> startProfile(at = obj.start)
3382 |> line(end = [0, obj.l])
3383 |> line(end = [obj.w, 0])
3384 |> line(end = [0, -obj.l])
3385 |> close()
3386 |> extrude(length = obj.h)
3387
3388 return myBox
3389}
3390
3391thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
3392"#;
3393 parse_execute(ast).await.unwrap();
3394 }
3395
3396 #[tokio::test(flavor = "multi_thread")]
3397 #[ignore] async fn test_object_member_starting_pipeline() {
3399 let ast = r#"
3400fn test2() {
3401 return {
3402 thing: startSketchOn(XY)
3403 |> startProfile(at = [0, 0])
3404 |> line(end = [0, 1])
3405 |> line(end = [1, 0])
3406 |> line(end = [0, -1])
3407 |> close()
3408 }
3409}
3410
3411x2 = test2()
3412
3413x2.thing
3414 |> extrude(length = 10)
3415"#;
3416 parse_execute(ast).await.unwrap();
3417 }
3418
3419 #[tokio::test(flavor = "multi_thread")]
3420 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
3422 let ast = r#"fn box(obj) {
3423let myBox = startSketchOn(XY)
3424 |> startProfile(at = obj.start)
3425 |> line(end = [0, obj.l])
3426 |> line(end = [obj.w, 0])
3427 |> line(end = [0, -obj.l])
3428 |> close()
3429 |> extrude(length = obj.h)
3430
3431 return myBox
3432}
3433
3434for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
3435 thisBox = box(var)
3436}"#;
3437
3438 parse_execute(ast).await.unwrap();
3439 }
3440
3441 #[tokio::test(flavor = "multi_thread")]
3442 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
3444 let ast = r#"fn box(h, l, w, start) {
3445 myBox = startSketchOn(XY)
3446 |> startProfile(at = [0,0])
3447 |> line(end = [0, l])
3448 |> line(end = [w, 0])
3449 |> line(end = [0, -l])
3450 |> close()
3451 |> extrude(length = h)
3452
3453 return myBox
3454}
3455
3456
3457for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
3458 const thisBox = box(var[0], var[1], var[2], var[3])
3459}"#;
3460
3461 parse_execute(ast).await.unwrap();
3462 }
3463
3464 #[tokio::test(flavor = "multi_thread")]
3465 async fn test_get_member_of_array_with_function() {
3466 let ast = r#"fn box(@arr) {
3467 myBox =startSketchOn(XY)
3468 |> startProfile(at = arr[0])
3469 |> line(end = [0, arr[1]])
3470 |> line(end = [arr[2], 0])
3471 |> line(end = [0, -arr[1]])
3472 |> close()
3473 |> extrude(length = arr[3])
3474
3475 return myBox
3476}
3477
3478thisBox = box([[0,0], 6, 10, 3])
3479
3480"#;
3481 parse_execute(ast).await.unwrap();
3482 }
3483
3484 #[tokio::test(flavor = "multi_thread")]
3485 async fn test_function_cannot_access_future_definitions() {
3486 let ast = r#"
3487fn returnX() {
3488 // x shouldn't be defined yet.
3489 return x
3490}
3491
3492x = 5
3493
3494answer = returnX()"#;
3495
3496 let result = parse_execute(ast).await;
3497 let err = result.unwrap_err();
3498 assert_eq!(err.message(), "`x` is not defined");
3499 }
3500
3501 #[tokio::test(flavor = "multi_thread")]
3502 async fn test_override_prelude() {
3503 let text = "PI = 3.0";
3504 let result = parse_execute(text).await.unwrap();
3505 let issues = result.exec_state.issues();
3506 assert!(issues.is_empty(), "issues={issues:#?}");
3507 }
3508
3509 #[tokio::test(flavor = "multi_thread")]
3510 async fn type_aliases() {
3511 let text = r#"@settings(experimentalFeatures = allow)
3512type MyTy = [number; 2]
3513fn foo(@x: MyTy) {
3514 return x[0]
3515}
3516
3517foo([0, 1])
3518
3519type Other = MyTy | Helix
3520"#;
3521 let result = parse_execute(text).await.unwrap();
3522 let issues = result.exec_state.issues();
3523 assert!(issues.is_empty(), "issues={issues:#?}");
3524 }
3525
3526 #[tokio::test(flavor = "multi_thread")]
3527 async fn test_cannot_shebang_in_fn() {
3528 let ast = r#"
3529fn foo() {
3530 #!hello
3531 return true
3532}
3533
3534foo
3535"#;
3536
3537 let result = parse_execute(ast).await;
3538 let err = result.unwrap_err();
3539 assert_eq!(
3540 err,
3541 KclError::new_syntax(KclErrorDetails::new(
3542 "Unexpected token: #".to_owned(),
3543 vec![SourceRange::new(14, 15, ModuleId::default())],
3544 )),
3545 );
3546 }
3547
3548 #[tokio::test(flavor = "multi_thread")]
3549 async fn test_pattern_transform_function_cannot_access_future_definitions() {
3550 let ast = r#"
3551fn transform(@replicaId) {
3552 // x shouldn't be defined yet.
3553 scale = x
3554 return {
3555 translate = [0, 0, replicaId * 10],
3556 scale = [scale, 1, 0],
3557 }
3558}
3559
3560fn layer() {
3561 return startSketchOn(XY)
3562 |> circle( center= [0, 0], radius= 1, tag = $tag1)
3563 |> extrude(length = 10)
3564}
3565
3566x = 5
3567
3568// The 10 layers are replicas of each other, with a transform applied to each.
3569shape = layer() |> patternTransform(instances = 10, transform = transform)
3570"#;
3571
3572 let result = parse_execute(ast).await;
3573 let err = result.unwrap_err();
3574 assert_eq!(err.message(), "`x` is not defined",);
3575 }
3576
3577 #[tokio::test(flavor = "multi_thread")]
3580 async fn test_math_execute_with_functions() {
3581 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
3582 let result = parse_execute(ast).await.unwrap();
3583 assert_eq!(
3584 5.0,
3585 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3586 .as_f64()
3587 .unwrap()
3588 );
3589 }
3590
3591 #[tokio::test(flavor = "multi_thread")]
3592 async fn test_math_execute() {
3593 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
3594 let result = parse_execute(ast).await.unwrap();
3595 assert_eq!(
3596 7.4,
3597 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3598 .as_f64()
3599 .unwrap()
3600 );
3601 }
3602
3603 #[tokio::test(flavor = "multi_thread")]
3604 async fn test_string_uppercase() {
3605 let composed = "\u{e9}";
3606 let uppercase_composed = "\u{c9}";
3607 let decomposed = "e\u{301}";
3608 let uppercase_decomposed = "E\u{301}";
3609 let code = format!(
3610 r#"
3611ascii = string::uppercase("Kcl")
3612unicode_expansion = string::uppercase("Straße")
3613uncased = string::uppercase("東京")
3614empty = string::uppercase("")
3615composed = string::uppercase("{composed}")
3616decomposed = string::uppercase("{decomposed}")
3617piped = "ready" |> string::uppercase()
3618"#
3619 );
3620
3621 let result = parse_execute(&code).await.unwrap();
3622 for (name, expected) in [
3623 ("ascii", "KCL"),
3624 ("unicode_expansion", "STRASSE"),
3625 ("uncased", "東京"),
3626 ("empty", ""),
3627 ("composed", uppercase_composed),
3628 ("decomposed", uppercase_decomposed),
3629 ("piped", "READY"),
3630 ] {
3631 assert_eq!(
3632 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3633 .as_str()
3634 .unwrap(),
3635 expected,
3636 "{name}"
3637 );
3638 }
3639 }
3640
3641 #[tokio::test(flavor = "multi_thread")]
3642 async fn test_string_lowercase() {
3643 let composed = "\u{c9}";
3644 let lowercase_composed = "\u{e9}";
3645 let decomposed = "E\u{301}";
3646 let lowercase_decomposed = "e\u{301}";
3647 let expanded = "i\u{307}";
3648 let code = format!(
3649 r#"
3650ascii = string::lowercase("KCL")
3651final_sigma = string::lowercase("ΟΣ")
3652medial_sigma = string::lowercase("ΟΣΑ")
3653unicode_expansion = string::lowercase("İ")
3654uncased = string::lowercase("東京")
3655empty = string::lowercase("")
3656composed = string::lowercase("{composed}")
3657decomposed = string::lowercase("{decomposed}")
3658piped = "READY" |> string::lowercase()
3659"#
3660 );
3661
3662 let result = parse_execute(&code).await.unwrap();
3663 for (name, expected) in [
3664 ("ascii", "kcl"),
3665 ("final_sigma", "ος"),
3666 ("medial_sigma", "οσα"),
3667 ("unicode_expansion", expanded),
3668 ("uncased", "東京"),
3669 ("empty", ""),
3670 ("composed", lowercase_composed),
3671 ("decomposed", lowercase_decomposed),
3672 ("piped", "ready"),
3673 ] {
3674 assert_eq!(
3675 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3676 .as_str()
3677 .unwrap(),
3678 expected,
3679 "{name}"
3680 );
3681 }
3682 }
3683
3684 #[tokio::test(flavor = "multi_thread")]
3685 async fn test_string_is_equal() {
3686 let composed = "\u{e9}";
3687 let decomposed = "e\u{301}";
3688 let code = format!(
3689 r#"
3690exact_same = string::isEqual("KCL", to = "KCL")
3691exact_different_case = string::isEqual("KCL", to = "kcl")
3692explicit_case_sensitive = string::isEqual("KCL", to = "kcl", caseInsensitive = false)
3693case_insensitive_ascii = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3694case_fold_expansion = string::isEqual("Straße", to = "STRASSE", caseInsensitive = true)
3695case_fold_expansion_reversed = string::isEqual("STRASSE", to = "Straße", caseInsensitive = true)
3696case_fold_sigma = string::isEqual("ος", to = "οσ", caseInsensitive = true)
3697case_fold_non_turkic = string::isEqual("I", to = "i", caseInsensitive = true)
3698case_fold_not_turkic = string::isEqual("I", to = "ı", caseInsensitive = true)
3699empty_same = string::isEqual("", to = "")
3700empty_different = string::isEqual("", to = "KCL")
3701exact_without_normalization = string::isEqual("{composed}", to = "{decomposed}")
3702case_fold_without_normalization = string::isEqual("{composed}", to = "{decomposed}", caseInsensitive = true)
3703piped = "ready" |> string::isEqual(to = "READY", caseInsensitive = true)
3704"#
3705 );
3706
3707 let result = parse_execute(&code).await.unwrap();
3708 for (name, expected) in [
3709 ("exact_same", true),
3710 ("exact_different_case", false),
3711 ("explicit_case_sensitive", false),
3712 ("case_insensitive_ascii", true),
3713 ("case_fold_expansion", true),
3714 ("case_fold_expansion_reversed", true),
3715 ("case_fold_sigma", true),
3716 ("case_fold_non_turkic", true),
3717 ("case_fold_not_turkic", false),
3718 ("empty_same", true),
3719 ("empty_different", false),
3720 ("exact_without_normalization", false),
3721 ("case_fold_without_normalization", false),
3722 ("piped", true),
3723 ] {
3724 assert_eq!(
3725 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3726 .as_bool()
3727 .unwrap(),
3728 expected,
3729 "{name}"
3730 );
3731 }
3732 }
3733
3734 #[tokio::test(flavor = "multi_thread")]
3735 async fn test_string_is_equal_inside_sketch_block_is_predicate() {
3736 let code = r#"
3737@settings(experimentalFeatures = allow)
3738
3739sketch(on = XY) {
3740 stringsAreEqual = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3741}
3742"#;
3743
3744 parse_execute(code).await.unwrap();
3745 }
3746
3747 #[tokio::test(flavor = "multi_thread")]
3748 async fn test_string_trim() {
3749 let ascii_whitespace = " \t\n";
3750 let tab = "\t";
3751 let non_breaking_space = "\u{a0}";
3752 let em_space = "\u{2003}";
3753 let ideographic_space = "\u{3000}";
3754 let zero_width_space = "\u{200b}";
3755 let decomposed = "e\u{301}";
3756 let code = format!(
3757 r#"
3758ascii = string::trim("{ascii_whitespace}KCL{ascii_whitespace}")
3759internal = string::trim(" KCL{tab}strings ")
3760unicode = string::trim("{non_breaking_space}{em_space}KCL{ideographic_space}")
3761all_whitespace = string::trim("{ascii_whitespace}{non_breaking_space}")
3762empty = string::trim("")
3763unchanged = string::trim("KCL")
3764without_normalization = string::trim(" {decomposed} ")
3765non_whitespace = string::trim("{zero_width_space}KCL{zero_width_space}")
3766piped = " ready " |> string::trim()
3767"#
3768 );
3769
3770 let result = parse_execute(&code).await.unwrap();
3771 let non_whitespace = format!("{zero_width_space}KCL{zero_width_space}");
3772 for (name, expected) in [
3773 ("ascii", "KCL"),
3774 ("internal", "KCL\tstrings"),
3775 ("unicode", "KCL"),
3776 ("all_whitespace", ""),
3777 ("empty", ""),
3778 ("unchanged", "KCL"),
3779 ("without_normalization", decomposed),
3780 ("non_whitespace", non_whitespace.as_str()),
3781 ("piped", "ready"),
3782 ] {
3783 assert_eq!(
3784 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3785 .as_str()
3786 .unwrap(),
3787 expected,
3788 "{name}"
3789 );
3790 }
3791 }
3792
3793 #[tokio::test(flavor = "multi_thread")]
3794 async fn test_string_trim_start() {
3795 let ascii_whitespace = " \t\n";
3796 let tab = "\t";
3797 let non_breaking_space = "\u{a0}";
3798 let em_space = "\u{2003}";
3799 let ideographic_space = "\u{3000}";
3800 let zero_width_space = "\u{200b}";
3801 let decomposed = "e\u{301}";
3802 let code = format!(
3803 r#"
3804ascii = string::trimStart("{ascii_whitespace}KCL{ascii_whitespace}")
3805internal = string::trimStart(" KCL{tab}strings")
3806unicode = string::trimStart("{non_breaking_space}{em_space}KCL{ideographic_space}")
3807all_whitespace = string::trimStart("{ascii_whitespace}{non_breaking_space}")
3808empty = string::trimStart("")
3809unchanged = string::trimStart("KCL")
3810without_normalization = string::trimStart(" {decomposed}")
3811non_whitespace_prefix = string::trimStart("{zero_width_space}{ascii_whitespace}KCL")
3812piped = " ready " |> string::trimStart()
3813"#
3814 );
3815
3816 let result = parse_execute(&code).await.unwrap();
3817 let ascii = format!("KCL{ascii_whitespace}");
3818 let unicode = format!("KCL{ideographic_space}");
3819 let non_whitespace_prefix = format!("{zero_width_space}{ascii_whitespace}KCL");
3820 for (name, expected) in [
3821 ("ascii", ascii.as_str()),
3822 ("internal", "KCL\tstrings"),
3823 ("unicode", unicode.as_str()),
3824 ("all_whitespace", ""),
3825 ("empty", ""),
3826 ("unchanged", "KCL"),
3827 ("without_normalization", decomposed),
3828 ("non_whitespace_prefix", non_whitespace_prefix.as_str()),
3829 ("piped", "ready "),
3830 ] {
3831 assert_eq!(
3832 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3833 .as_str()
3834 .unwrap(),
3835 expected,
3836 "{name}"
3837 );
3838 }
3839 }
3840
3841 #[tokio::test(flavor = "multi_thread")]
3842 async fn test_string_trim_end() {
3843 let ascii_whitespace = " \t\n";
3844 let tab = "\t";
3845 let non_breaking_space = "\u{a0}";
3846 let em_space = "\u{2003}";
3847 let ideographic_space = "\u{3000}";
3848 let zero_width_space = "\u{200b}";
3849 let decomposed = "e\u{301}";
3850 let code = format!(
3851 r#"
3852ascii = string::trimEnd("{ascii_whitespace}KCL{ascii_whitespace}")
3853internal = string::trimEnd("KCL{tab}strings ")
3854unicode = string::trimEnd("{non_breaking_space}KCL{em_space}{ideographic_space}")
3855all_whitespace = string::trimEnd("{ascii_whitespace}{non_breaking_space}")
3856empty = string::trimEnd("")
3857unchanged = string::trimEnd("KCL")
3858without_normalization = string::trimEnd("{decomposed} ")
3859non_whitespace_suffix = string::trimEnd("KCL{ascii_whitespace}{zero_width_space}")
3860piped = " ready " |> string::trimEnd()
3861"#
3862 );
3863
3864 let result = parse_execute(&code).await.unwrap();
3865 let ascii = format!("{ascii_whitespace}KCL");
3866 let unicode = format!("{non_breaking_space}KCL");
3867 let non_whitespace_suffix = format!("KCL{ascii_whitespace}{zero_width_space}");
3868 for (name, expected) in [
3869 ("ascii", ascii.as_str()),
3870 ("internal", "KCL\tstrings"),
3871 ("unicode", unicode.as_str()),
3872 ("all_whitespace", ""),
3873 ("empty", ""),
3874 ("unchanged", "KCL"),
3875 ("without_normalization", decomposed),
3876 ("non_whitespace_suffix", non_whitespace_suffix.as_str()),
3877 ("piped", " ready"),
3878 ] {
3879 assert_eq!(
3880 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3881 .as_str()
3882 .unwrap(),
3883 expected,
3884 "{name}"
3885 );
3886 }
3887 }
3888
3889 #[tokio::test(flavor = "multi_thread")]
3890 async fn test_string_to_string() {
3891 for (name, expr, expected) in [
3894 ("unitless integer", "12", "12"),
3897 ("unitless fractional", "1.5", "1.5"),
3898 ("no digits dropped", "0.1 + 0.2", "0.30000000000000004"),
3899 ("unitless negative", "-7", "-7"),
3900 ("unitless zero", "0", "0"),
3901 ("negative zero", "-0", "0"),
3902 ("count", "3_", "3_"),
3903 ("millimeters", "12mm", "12mm"),
3904 ("centimeters", "12cm", "12cm"),
3905 ("meters", "12m", "12m"),
3906 ("inches", "1.5in", "1.5in"),
3907 ("feet", "2ft", "2ft"),
3908 ("yards", "3yd", "3yd"),
3909 ("degrees", "90deg", "90deg"),
3910 ("radians", "1.5rad", "1.5rad"),
3911 ("length arithmetic", "2mm + 10mm", "12mm"),
3913 ("units the type system loses", "2mm * 10mm", "20"),
3916 ("unitless arithmetic", "1 + 2", "3"),
3917 ] {
3918 let code = format!("actual = string::toString({expr})");
3919 let result = parse_execute(&code).await.unwrap();
3920
3921 assert_eq!(
3922 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3923 .as_str()
3924 .unwrap(),
3925 expected,
3926 "case: {name}"
3927 );
3928 }
3929 }
3930
3931 #[tokio::test(flavor = "multi_thread")]
3932 async fn test_string_to_string_ignores_the_files_default_unit() {
3933 let code = "@settings(defaultLengthUnit = inch)\nactual = string::toString(12)";
3938 let result = parse_execute(code).await.unwrap();
3939
3940 assert_eq!(
3941 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3942 .as_str()
3943 .unwrap(),
3944 "12"
3945 );
3946 }
3947
3948 #[tokio::test(flavor = "multi_thread")]
3949 async fn test_string_to_string_rejects_a_non_number() {
3950 let error = parse_execute(r#"actual = string::toString("already text")"#)
3951 .await
3952 .unwrap_err();
3953
3954 assert_eq!(
3957 error.message(),
3958 "The input argument of `string::toString` requires a value with type `number`, but found a value with type `string`."
3959 );
3960 assert!(
3961 matches!(error, KclError::Argument { .. }),
3962 "expected an Argument error, found {error:?}"
3963 );
3964 }
3965
3966 #[tokio::test(flavor = "multi_thread")]
3967 async fn test_string_to_string_accepts_a_piped_argument() {
3968 let result = parse_execute("actual = 12mm |> string::toString()").await.unwrap();
3969
3970 assert_eq!(
3971 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3972 .as_str()
3973 .unwrap(),
3974 "12mm"
3975 );
3976 }
3977
3978 #[tokio::test(flavor = "multi_thread")]
3979 async fn test_string_to_string_echoes_how_the_literal_was_written() {
3980 for literal in [
3984 "12",
3985 "1.5",
3986 "0.30000000000000004",
3987 "3_",
3988 "2.5_",
3991 "-4_",
3992 "12mm",
3993 "-5mm",
3994 "1.5in",
3995 "90deg",
3996 "1.5rad",
3997 ] {
3998 let code = format!("actual = string::toString({literal})");
3999 let result = parse_execute(&code).await.unwrap();
4000
4001 assert_eq!(
4002 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4003 .as_str()
4004 .unwrap(),
4005 literal,
4006 "literal: {literal}"
4007 );
4008 }
4009 }
4010
4011 #[tokio::test(flavor = "multi_thread")]
4012 async fn test_string_to_string_spells_out_non_finite_numbers() {
4013 for (name, expr, expected) in [
4017 ("positive infinity", "1 / 0", "Infinity"),
4018 ("negative infinity", "-1 / 0", "-Infinity"),
4019 ("nan", "0 / 0", "NaN"),
4020 ("infinity from a length", "1mm / 0", "Infinity"),
4022 ("nan from a length", "0mm / 0", "NaN"),
4023 ("infinity from an angle", "1deg / 0", "Infinity"),
4024 ] {
4025 let code = format!("actual = string::toString({expr})");
4026 let result = parse_execute(&code).await.unwrap();
4027
4028 assert_eq!(
4029 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4030 .as_str()
4031 .unwrap(),
4032 expected,
4033 "case: {name}"
4034 );
4035 }
4036 }
4037
4038 #[tokio::test(flavor = "multi_thread")]
4039 async fn test_string_equality_operators() {
4040 let composed = "\u{e9}";
4041 let decomposed = "e\u{301}";
4042 let code = format!(
4043 r#"
4044equal_same_ascii = "KCL" == "KCL"
4045equal_different_case = "KCL" == "kcl"
4046not_equal_same_ascii = "KCL" != "KCL"
4047not_equal_different_case = "KCL" != "kcl"
4048equal_same_unicode = "{composed}" == "{composed}"
4049not_equal_same_unicode = "{composed}" != "{composed}"
4050equal_without_normalization = "{composed}" == "{decomposed}"
4051not_equal_without_normalization = "{composed}" != "{decomposed}"
4052"#
4053 );
4054
4055 let result = parse_execute(&code).await.unwrap();
4056 for (name, expected) in [
4057 ("equal_same_ascii", true),
4058 ("equal_different_case", false),
4059 ("not_equal_same_ascii", false),
4060 ("not_equal_different_case", true),
4061 ("equal_same_unicode", true),
4062 ("not_equal_same_unicode", false),
4063 ("equal_without_normalization", false),
4064 ("not_equal_without_normalization", true),
4065 ] {
4066 assert_eq!(
4067 mem_get_json(result.exec_state.stack(), result.mem_env, name)
4068 .as_bool()
4069 .unwrap(),
4070 expected,
4071 "{name}"
4072 );
4073 }
4074 }
4075
4076 #[tokio::test(flavor = "multi_thread")]
4077 async fn test_string_equality_inside_sketch_block_fails_like_number_equality() {
4078 let string_code = r#"
4079@settings(experimentalFeatures = allow)
4080
4081sketch(on = XY) {
4082 stringsAreEqual = "KCL" == "KCL"
4083}
4084"#;
4085 let number_code = r#"
4086@settings(experimentalFeatures = allow)
4087
4088sketch(on = XY) {
4089 numbersAreEqual = 1 == 1
4090}
4091"#;
4092
4093 assert_eq!(
4094 parse_execute(string_code).await.unwrap_err().message(),
4095 "Cannot create an equivalence constraint between values of these types: a string and a string"
4096 );
4097 assert_eq!(
4098 parse_execute(number_code).await.unwrap_err().message(),
4099 "Cannot create an equivalence constraint between values of these types: a number and a number"
4100 );
4101 }
4102
4103 #[tokio::test(flavor = "multi_thread")]
4104 async fn test_math_execute_start_negative() {
4105 let ast = r#"myVar = -5 + 6"#;
4106 let result = parse_execute(ast).await.unwrap();
4107 assert_eq!(
4108 1.0,
4109 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4110 .as_f64()
4111 .unwrap()
4112 );
4113 }
4114
4115 #[tokio::test(flavor = "multi_thread")]
4116 async fn test_math_execute_with_pi() {
4117 let ast = r#"myVar = PI * 2"#;
4118 let result = parse_execute(ast).await.unwrap();
4119 assert_eq!(
4120 std::f64::consts::TAU,
4121 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4122 .as_f64()
4123 .unwrap()
4124 );
4125 }
4126
4127 #[tokio::test(flavor = "multi_thread")]
4128 async fn test_math_define_decimal_without_leading_zero() {
4129 let ast = r#"thing = .4 + 7"#;
4130 let result = parse_execute(ast).await.unwrap();
4131 assert_eq!(
4132 7.4,
4133 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
4134 .as_f64()
4135 .unwrap()
4136 );
4137 }
4138
4139 #[tokio::test(flavor = "multi_thread")]
4140 async fn pass_std_to_std() {
4141 let ast = r#"sketch001 = startSketchOn(XY)
4142profile001 = circle(sketch001, center = [0, 0], radius = 2)
4143extrude001 = extrude(profile001, length = 5)
4144extrudes = patternLinear3d(
4145 extrude001,
4146 instances = 3,
4147 distance = 5,
4148 axis = [1, 1, 0],
4149)
4150clone001 = map(extrudes, f = clone)
4151"#;
4152 parse_execute(ast).await.unwrap();
4153 }
4154
4155 #[tokio::test(flavor = "multi_thread")]
4156 async fn test_array_reduce_nested_array() {
4157 let code = r#"
4158fn id(@el, accum) { return accum }
4159
4160answer = reduce([], initial=[[[0,0]]], f=id)
4161"#;
4162 let result = parse_execute(code).await.unwrap();
4163 assert_eq!(
4164 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
4165 KclValue::HomArray {
4166 value: vec![KclValue::HomArray {
4167 value: vec![KclValue::HomArray {
4168 value: vec![
4169 KclValue::Number {
4170 value: 0.0,
4171 ty: NumericType::default(),
4172 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
4173 },
4174 KclValue::Number {
4175 value: 0.0,
4176 ty: NumericType::default(),
4177 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
4178 }
4179 ],
4180 ty: RuntimeType::any(),
4181 }],
4182 ty: RuntimeType::any(),
4183 }],
4184 ty: RuntimeType::any(),
4185 }
4186 );
4187 }
4188
4189 #[tokio::test(flavor = "multi_thread")]
4190 async fn test_zero_param_fn() {
4191 let ast = r#"sigmaAllow = 35000 // psi
4192leg1 = 5 // inches
4193leg2 = 8 // inches
4194fn thickness() { return 0.56 }
4195
4196bracket = startSketchOn(XY)
4197 |> startProfile(at = [0,0])
4198 |> line(end = [0, leg1])
4199 |> line(end = [leg2, 0])
4200 |> line(end = [0, -thickness()])
4201 |> line(end = [-leg2 + thickness(), 0])
4202"#;
4203 parse_execute(ast).await.unwrap();
4204 }
4205
4206 #[tokio::test(flavor = "multi_thread")]
4207 async fn test_unary_operator_not_succeeds() {
4208 let ast = r#"
4209fn returnTrue() { return !false }
4210t = true
4211f = false
4212notTrue = !t
4213notFalse = !f
4214c = !!true
4215d = !returnTrue()
4216
4217assertIs(!false, error = "expected to pass")
4218
4219fn check(x) {
4220 assertIs(!x, error = "expected argument to be false")
4221 return true
4222}
4223check(x = false)
4224"#;
4225 let result = parse_execute(ast).await.unwrap();
4226 assert_eq!(
4227 false,
4228 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
4229 .as_bool()
4230 .unwrap()
4231 );
4232 assert_eq!(
4233 true,
4234 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
4235 .as_bool()
4236 .unwrap()
4237 );
4238 assert_eq!(
4239 true,
4240 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
4241 .as_bool()
4242 .unwrap()
4243 );
4244 assert_eq!(
4245 false,
4246 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
4247 .as_bool()
4248 .unwrap()
4249 );
4250 }
4251
4252 #[tokio::test(flavor = "multi_thread")]
4253 async fn test_unary_operator_not_on_non_bool_fails() {
4254 let code1 = r#"
4255// Yup, this is null.
4256myNull = 0 / 0
4257notNull = !myNull
4258"#;
4259 assert_eq!(
4260 parse_execute(code1).await.unwrap_err().message(),
4261 "Cannot apply unary operator ! to non-boolean value: a number",
4262 );
4263
4264 let code2 = "notZero = !0";
4265 assert_eq!(
4266 parse_execute(code2).await.unwrap_err().message(),
4267 "Cannot apply unary operator ! to non-boolean value: a number",
4268 );
4269
4270 let code3 = r#"
4271notEmptyString = !""
4272"#;
4273 assert_eq!(
4274 parse_execute(code3).await.unwrap_err().message(),
4275 "Cannot apply unary operator ! to non-boolean value: a string",
4276 );
4277
4278 let code4 = r#"
4279obj = { a = 1 }
4280notMember = !obj.a
4281"#;
4282 assert_eq!(
4283 parse_execute(code4).await.unwrap_err().message(),
4284 "Cannot apply unary operator ! to non-boolean value: a number",
4285 );
4286
4287 let code5 = "
4288a = []
4289notArray = !a";
4290 assert_eq!(
4291 parse_execute(code5).await.unwrap_err().message(),
4292 "Cannot apply unary operator ! to non-boolean value: an empty array",
4293 );
4294
4295 let code6 = "
4296x = {}
4297notObject = !x";
4298 assert_eq!(
4299 parse_execute(code6).await.unwrap_err().message(),
4300 "Cannot apply unary operator ! to non-boolean value: an object",
4301 );
4302
4303 let code7 = "
4304fn x() { return 1 }
4305notFunction = !x";
4306 let fn_err = parse_execute(code7).await.unwrap_err();
4307 assert!(
4310 fn_err
4311 .message()
4312 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
4313 "Actual error: {fn_err:?}"
4314 );
4315
4316 let code8 = "
4317myTagDeclarator = $myTag
4318notTagDeclarator = !myTagDeclarator";
4319 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
4320 assert!(
4323 tag_declarator_err
4324 .message()
4325 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
4326 "Actual error: {tag_declarator_err:?}"
4327 );
4328
4329 let code9 = "
4330myTagDeclarator = $myTag
4331notTagIdentifier = !myTag";
4332 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
4333 assert!(
4336 tag_identifier_err
4337 .message()
4338 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
4339 "Actual error: {tag_identifier_err:?}"
4340 );
4341
4342 let code10 = "notPipe = !(1 |> 2)";
4343 assert_eq!(
4344 parse_execute(code10).await.unwrap_err(),
4347 KclError::new_syntax(KclErrorDetails::new(
4348 "Unexpected token: !".to_owned(),
4349 vec![SourceRange::new(10, 11, ModuleId::default())],
4350 ))
4351 );
4352
4353 let code11 = "
4354fn identity(x) { return x }
4355notPipeSub = 1 |> identity(!%))";
4356 assert_eq!(
4357 parse_execute(code11).await.unwrap_err(),
4360 KclError::new_syntax(KclErrorDetails::new(
4361 "There was an unexpected `!`. Try removing it.".to_owned(),
4362 vec![SourceRange::new(56, 57, ModuleId::default())],
4363 ))
4364 );
4365
4366 }
4370
4371 #[tokio::test(flavor = "multi_thread")]
4372 async fn test_start_sketch_on_invalid_kwargs() {
4373 let current_dir = std::env::current_dir().unwrap();
4374 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
4375 let mut code = std::fs::read_to_string(&path).unwrap();
4376 assert_eq!(
4377 parse_execute(&code).await.unwrap_err().message(),
4378 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
4379 );
4380
4381 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
4382 code = std::fs::read_to_string(&path).unwrap();
4383
4384 assert_eq!(
4385 parse_execute(&code).await.unwrap_err().message(),
4386 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
4387 );
4388
4389 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
4390 code = std::fs::read_to_string(&path).unwrap();
4391
4392 assert_eq!(
4393 parse_execute(&code).await.unwrap_err().message(),
4394 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4395 );
4396
4397 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
4398 code = std::fs::read_to_string(&path).unwrap();
4399
4400 assert_eq!(
4401 parse_execute(&code).await.unwrap_err().message(),
4402 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4403 );
4404
4405 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
4406 code = std::fs::read_to_string(&path).unwrap();
4407
4408 assert_eq!(
4409 parse_execute(&code).await.unwrap_err().message(),
4410 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
4411 );
4412 }
4413
4414 #[tokio::test(flavor = "multi_thread")]
4415 async fn test_math_negative_variable_in_binary_expression() {
4416 let ast = r#"sigmaAllow = 35000 // psi
4417width = 1 // inch
4418
4419p = 150 // lbs
4420distance = 6 // inches
4421FOS = 2
4422
4423leg1 = 5 // inches
4424leg2 = 8 // inches
4425
4426thickness_squared = distance * p * FOS * 6 / sigmaAllow
4427thickness = 0.56 // inches. App does not support square root function yet
4428
4429bracket = startSketchOn(XY)
4430 |> startProfile(at = [0,0])
4431 |> line(end = [0, leg1])
4432 |> line(end = [leg2, 0])
4433 |> line(end = [0, -thickness])
4434 |> line(end = [-leg2 + thickness, 0])
4435"#;
4436 parse_execute(ast).await.unwrap();
4437 }
4438
4439 #[tokio::test(flavor = "multi_thread")]
4440 async fn test_execute_function_no_return() {
4441 let ast = r#"fn test(@origin) {
4442 origin
4443}
4444
4445test([0, 0])
4446"#;
4447 let result = parse_execute(ast).await;
4448 assert!(result.is_err());
4449 assert!(result.unwrap_err().to_string().contains("undefined"));
4450 }
4451
4452 #[tokio::test(flavor = "multi_thread")]
4453 async fn test_max_stack_size_exceeded_error() {
4454 let ast = r#"
4455fn forever(@n) {
4456 return 1 + forever(n)
4457}
4458
4459forever(1)
4460"#;
4461 let result = parse_execute(ast).await;
4462 let err = result.unwrap_err();
4463 let msg = err.to_string();
4466 assert!(
4467 msg.contains("stack size exceeded") || msg.contains("Call depth limit"),
4468 "actual: {err:?}"
4469 );
4470 }
4471
4472 #[tokio::test(flavor = "multi_thread")]
4473 async fn test_math_doubly_nested_parens() {
4474 let ast = r#"sigmaAllow = 35000 // psi
4475width = 4 // inch
4476p = 150 // Force on shelf - lbs
4477distance = 6 // inches
4478FOS = 2
4479leg1 = 5 // inches
4480leg2 = 8 // inches
4481thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
4482thickness = 0.32 // inches. App does not support square root function yet
4483bracket = startSketchOn(XY)
4484 |> startProfile(at = [0,0])
4485 |> line(end = [0, leg1])
4486 |> line(end = [leg2, 0])
4487 |> line(end = [0, -thickness])
4488 |> line(end = [-1 * leg2 + thickness, 0])
4489 |> line(end = [0, -1 * leg1 + thickness])
4490 |> close()
4491 |> extrude(length = width)
4492"#;
4493 parse_execute(ast).await.unwrap();
4494 }
4495
4496 #[tokio::test(flavor = "multi_thread")]
4497 async fn test_math_nested_parens_one_less() {
4498 let ast = r#" sigmaAllow = 35000 // psi
4499width = 4 // inch
4500p = 150 // Force on shelf - lbs
4501distance = 6 // inches
4502FOS = 2
4503leg1 = 5 // inches
4504leg2 = 8 // inches
4505thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
4506thickness = 0.32 // inches. App does not support square root function yet
4507bracket = startSketchOn(XY)
4508 |> startProfile(at = [0,0])
4509 |> line(end = [0, leg1])
4510 |> line(end = [leg2, 0])
4511 |> line(end = [0, -thickness])
4512 |> line(end = [-1 * leg2 + thickness, 0])
4513 |> line(end = [0, -1 * leg1 + thickness])
4514 |> close()
4515 |> extrude(length = width)
4516"#;
4517 parse_execute(ast).await.unwrap();
4518 }
4519
4520 #[tokio::test(flavor = "multi_thread")]
4521 async fn test_fn_as_operand() {
4522 let ast = r#"fn f() { return 1 }
4523x = f()
4524y = x + 1
4525z = f() + 1
4526w = f() + f()
4527"#;
4528 parse_execute(ast).await.unwrap();
4529 }
4530
4531 #[tokio::test(flavor = "multi_thread")]
4532 async fn kcl_test_ids_stable_between_executions() {
4533 let code = r#"sketch001 = startSketchOn(XZ)
4534|> startProfile(at = [61.74, 206.13])
4535|> xLine(length = 305.11, tag = $seg01)
4536|> yLine(length = -291.85)
4537|> xLine(length = -segLen(seg01))
4538|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4539|> close()
4540|> extrude(length = 40.14)
4541|> shell(
4542 thickness = 3.14,
4543 faces = [seg01]
4544)
4545"#;
4546
4547 let ctx = crate::test_server::new_context(true, None, true).await.unwrap();
4548 let old_program = crate::Program::parse_no_errs(code).unwrap();
4549
4550 if let Err(err) = ctx.run_with_caching(old_program).await {
4552 let report = err.into_miette_report_with_outputs(code).unwrap();
4553 let report = miette::Report::new(report);
4554 panic!("Error executing program: {report:?}");
4555 }
4556
4557 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4559
4560 let code = r#"sketch001 = startSketchOn(XZ)
4561|> startProfile(at = [62.74, 206.13])
4562|> xLine(length = 305.11, tag = $seg01)
4563|> yLine(length = -291.85)
4564|> xLine(length = -segLen(seg01))
4565|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4566|> close()
4567|> extrude(length = 40.14)
4568|> shell(
4569 faces = [seg01],
4570 thickness = 3.14,
4571)
4572"#;
4573
4574 let program = crate::Program::parse_no_errs(code).unwrap();
4576 ctx.run_with_caching(program).await.unwrap();
4578
4579 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4580
4581 assert_eq!(id_generator, new_id_generator);
4582 }
4583
4584 #[tokio::test(flavor = "multi_thread")]
4585 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
4586 let code = r#"sketch001 = startSketchOn(XZ)
4587|> startProfile(at = [61.74, 206.13])
4588|> xLine(length = 305.11, tag = $seg01)
4589|> yLine(length = -291.85)
4590|> xLine(length = -segLen(seg01))
4591|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4592|> close()
4593|> extrude(length = 40.14)
4594|> shell(
4595 thickness = 3.14,
4596 faces = [seg01]
4597)
4598"#;
4599
4600 let mut ctx = crate::test_server::new_context(true, None, true).await.unwrap();
4601 let old_program = crate::Program::parse_no_errs(code).unwrap();
4602
4603 ctx.run_with_caching(old_program.clone()).await.unwrap();
4605
4606 let settings_state = cache::read_old_ast().await.unwrap().settings;
4607
4608 assert_eq!(settings_state, ctx.settings);
4610
4611 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4613
4614 ctx.run_with_caching(old_program.clone()).await.unwrap();
4616
4617 let settings_state = cache::read_old_ast().await.unwrap().settings;
4618
4619 assert_eq!(settings_state, ctx.settings);
4621
4622 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4624
4625 ctx.run_with_caching(old_program).await.unwrap();
4627
4628 let settings_state = cache::read_old_ast().await.unwrap().settings;
4629
4630 assert_eq!(settings_state, ctx.settings);
4632
4633 ctx.close().await;
4634 }
4635
4636 #[tokio::test(flavor = "multi_thread")]
4637 async fn mock_after_not_mock() {
4638 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
4639 let program = crate::Program::parse_no_errs("x = 2").unwrap();
4640 let result = ctx.run_with_caching(program).await.unwrap();
4641 assert_number_variable(&result.variables, "x", 2.0);
4642
4643 let ctx2 = ExecutorContext::new_mock(None).await;
4644 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
4645 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
4646 assert_number_variable(&result.variables, "z", 3.0);
4647
4648 ctx.close().await;
4649 ctx2.close().await;
4650 }
4651
4652 #[tokio::test(flavor = "multi_thread")]
4654 async fn mock_execution_succeeds_after_split() {
4655 let code = kcl_input!("repro_mock_extrude");
4656 let ctx = ExecutorContext::new_mock(None).await;
4657 let program = crate::Program::parse_no_errs(code).unwrap();
4658 let _result = match ctx.run_mock(&program, &MockConfig::default()).await {
4659 Ok(res) => res,
4660 Err(e) => panic!("{}", e.error),
4661 };
4662 }
4663
4664 #[tokio::test(flavor = "multi_thread")]
4666 async fn mock_execution_rejects_oob_on_frontend_array() {
4667 let code = r#"
4668values = [10, 20]
4669third = values[2]
4670"#;
4671 let ctx = ExecutorContext::new_mock(None).await;
4672 let program = crate::Program::parse_no_errs(code).unwrap();
4673 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
4674 ctx.close().await;
4675
4676 assert!(
4677 err.error.message().contains("array doesn't have any item at index 2"),
4678 "{err:?}"
4679 );
4680 }
4681
4682 #[tokio::test(flavor = "multi_thread")]
4687 async fn mock_execution_pattern_circular_number() {
4688 let code = kcl_input!("repro_mock_pattern_circular");
4689 let ctx = ExecutorContext::new_mock(None).await;
4690 let program = crate::Program::parse_no_errs(code).unwrap();
4691 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
4692 let copies = result
4693 .variables
4694 .get("copies")
4695 .expect("no variable called 'copies' found");
4696 let value = match copies {
4697 KclValueView::Solid { .. } => {
4698 panic!("One solid?");
4699 }
4700 KclValueView::HomArray { value } => value,
4701 other => panic!("{other:#?}"),
4702 };
4703 let actual_instances = value.len();
4704 let expected_instances = 10; assert_eq!(actual_instances, expected_instances);
4706 }
4707
4708 #[tokio::test(flavor = "multi_thread")]
4713 async fn mock_execution_subtract() {
4714 let code = kcl_input!("repro_mock_subtract");
4716 let ctx = ExecutorContext::new_mock(None).await;
4717 let program = crate::Program::parse_no_errs(code).unwrap();
4718 let result = ctx.run_mock(&program, &MockConfig::default()).await;
4719 ctx.close().await;
4720 let result = match result {
4721 Ok(x) => x,
4722 Err(e) => {
4723 let error = e.error;
4724 panic!("{error}");
4725 }
4726 };
4727
4728 let subtracted_parts = result
4730 .variables
4731 .get("subtractedParts")
4732 .expect("no variable called 'subtracted_parts' found");
4733 let subtracted_parts = match subtracted_parts {
4734 KclValueView::Solid { .. } => {
4735 panic!("One solid?");
4736 }
4737 KclValueView::HomArray { value } => value,
4738 other => panic!("{other:#?}"),
4739 };
4740
4741 let expected_number_of_parts = 2;
4744 let actual_number_of_parts = subtracted_parts.len();
4745 assert_eq!(actual_number_of_parts, expected_number_of_parts);
4746 }
4747
4748 #[tokio::test(flavor = "multi_thread")]
4749 async fn mock_then_add_extrude_then_mock_again() {
4750 let code = "s = sketch(on = XY) {
4751 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
4752 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
4753 coincident([line1.end, line2.start])
4754 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
4755 coincident([line2.end, line3.start])
4756 coincident([line1.start, line3.end])
4757}
4758 ";
4759 let ctx = ExecutorContext::new_mock(None).await;
4760 let program = crate::Program::parse_no_errs(code).unwrap();
4761 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
4762 assert!(result.variables.contains_key("s"), "actual: {:?}", result.variables);
4763
4764 let code2 = code.to_owned()
4765 + "
4766region001 = region(point = [1mm, 1mm], sketch = s)
4767extrude001 = extrude(region001, length = 1)
4768 ";
4769 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4770 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
4771 assert!(
4772 result.variables.contains_key("region001"),
4773 "actual: {:?}",
4774 result.variables
4775 );
4776
4777 ctx.close().await;
4778 }
4779
4780 #[tokio::test(flavor = "multi_thread")]
4781 async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
4782 let code = format!(
4783 r#"{}
4784
4785face7 = faceOf(solid6, face = r6.tags.line1)
4786r7 = squareRegion(onSurface = face7)
4787solid7 = extrude(r7, length = width)
4788"#,
4789 include_str!("../../tests/endless_impeller/input.kcl")
4790 );
4791
4792 let result = parse_execute(&code).await.unwrap();
4793 let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
4794 assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
4795
4796 let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
4797 KclValue::Face { value } => value,
4798 value => panic!("expected face7 to be a Face, got {value:?}"),
4799 };
4800 assert!(face7.parent_solid.creator_sketch_id.is_some());
4801 }
4802
4803 #[tokio::test(flavor = "multi_thread")]
4804 async fn mock_has_stable_ids() {
4805 let ctx = ExecutorContext::new_mock(None).await;
4806 let mock_config = MockConfig {
4807 use_prev_memory: false,
4808 ..Default::default()
4809 };
4810 let code = "sk = startSketchOn(XY)
4811 |> startProfile(at = [0, 0])";
4812 let program = crate::Program::parse_no_errs(code).unwrap();
4813 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
4814 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4815 assert!(!ids.is_empty(), "IDs should not be empty");
4816
4817 let ctx2 = ExecutorContext::new_mock(None).await;
4818 let program2 = crate::Program::parse_no_errs(code).unwrap();
4819 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
4820 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4821
4822 assert_eq!(ids, ids2, "Generated IDs should match");
4823 ctx.close().await;
4824 ctx2.close().await;
4825 }
4826
4827 #[tokio::test(flavor = "multi_thread")]
4828 async fn mock_memory_restore_preserves_module_maps() {
4829 clear_mem_cache().await;
4830
4831 let ctx = ExecutorContext::new_mock(None).await;
4832 let cold_start = MockConfig {
4833 use_prev_memory: false,
4834 ..Default::default()
4835 };
4836 ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
4837
4838 let mut mem = cache::read_old_memory().await.unwrap();
4839 assert!(
4840 mem.path_to_source_id.len() > 3,
4841 "expected prelude imports to populate multiple modules, got {:?}",
4842 mem.path_to_source_id
4843 );
4844 mem.constraint_state.insert(
4845 crate::front::ObjectId(1),
4846 indexmap::indexmap! {
4847 crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
4848 crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
4849 },
4850 );
4851
4852 let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
4853 ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
4854
4855 assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
4856 assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
4857 assert_eq!(exec_state.global.module_infos, mem.module_infos);
4858 assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
4859
4860 clear_mem_cache().await;
4861 ctx.close().await;
4862 }
4863
4864 #[tokio::test(flavor = "multi_thread")]
4865 async fn run_with_caching_no_action_refreshes_mock_memory() {
4866 cache::bust_cache().await;
4867 clear_mem_cache().await;
4868
4869 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
4870 let program = crate::Program::parse_no_errs(
4871 r#"sketch001 = sketch(on = XY) {
4872 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
4873}
4874"#,
4875 )
4876 .unwrap();
4877
4878 ctx.run_with_caching(program.clone()).await.unwrap();
4879 let baseline_memory = cache::read_old_memory().await.unwrap();
4880 assert!(
4881 !baseline_memory.scene_objects.is_empty(),
4882 "expected engine execution to persist full-scene mock memory"
4883 );
4884
4885 cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
4886 assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
4887
4888 ctx.run_with_caching(program).await.unwrap();
4889 let refreshed_memory = cache::read_old_memory().await.unwrap();
4890 assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
4891 assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
4892 assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
4893
4894 cache::bust_cache().await;
4895 clear_mem_cache().await;
4896 ctx.close().await;
4897 }
4898
4899 #[tokio::test(flavor = "multi_thread")]
4900 async fn sim_sketch_mode_real_mock_real() {
4901 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
4902 let code = r#"sketch001 = startSketchOn(XY)
4903profile001 = startProfile(sketch001, at = [0, 0])
4904 |> line(end = [10, 0])
4905 |> line(end = [0, 10])
4906 |> line(end = [-10, 0])
4907 |> line(end = [0, -10])
4908 |> close()
4909"#;
4910 let program = crate::Program::parse_no_errs(code).unwrap();
4911 let result = ctx.run_with_caching(program).await.unwrap();
4912 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4913
4914 let mock_ctx = ExecutorContext::new_mock(None).await;
4915 let mock_program = crate::Program::parse_no_errs(code).unwrap();
4916 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
4917 assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4918
4919 let code2 = code.to_owned()
4920 + r#"
4921extrude001 = extrude(profile001, length = 10)
4922"#;
4923 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4924 let result = ctx.run_with_caching(program2).await.unwrap();
4925 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
4926
4927 ctx.close().await;
4928 mock_ctx.close().await;
4929 }
4930
4931 #[tokio::test(flavor = "multi_thread")]
4932 async fn read_tag_version() {
4933 let ast = r#"fn bar(@t) {
4934 return startSketchOn(XY)
4935 |> startProfile(at = [0,0])
4936 |> angledLine(
4937 angle = -60,
4938 length = segLen(t),
4939 )
4940 |> line(end = [0, 0])
4941 |> close()
4942}
4943
4944sketch = startSketchOn(XY)
4945 |> startProfile(at = [0,0])
4946 |> line(end = [0, 10])
4947 |> line(end = [10, 0], tag = $tag0)
4948 |> line(endAbsolute = [0, 0])
4949
4950fn foo() {
4951 // tag0 tags an edge
4952 return bar(tag0)
4953}
4954
4955solid = sketch |> extrude(length = 10)
4956// tag0 tags a face
4957sketch2 = startSketchOn(solid, face = tag0)
4958 |> startProfile(at = [0,0])
4959 |> line(end = [0, 1])
4960 |> line(end = [1, 0])
4961 |> line(end = [0, 0])
4962
4963foo() |> extrude(length = 1)
4964"#;
4965 parse_execute(ast).await.unwrap();
4966 }
4967
4968 #[tokio::test(flavor = "multi_thread")]
4969 async fn experimental() {
4970 let code = r#"
4971startSketchOn(XY)
4972 |> startProfile(at = [0, 0], tag = $start)
4973 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4974"#;
4975 let result = parse_execute(code).await.unwrap();
4976 let issues = result.exec_state.issues();
4977 assert_eq!(issues.len(), 1);
4978 assert_eq!(issues[0].severity, Severity::Error);
4979 let msg = &issues[0].message;
4980 assert!(msg.contains("experimental"), "found {msg}");
4981
4982 let code = r#"@settings(experimentalFeatures = allow)
4983startSketchOn(XY)
4984 |> startProfile(at = [0, 0], tag = $start)
4985 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4986"#;
4987 let result = parse_execute(code).await.unwrap();
4988 let issues = result.exec_state.issues();
4989 assert!(issues.is_empty(), "issues={issues:#?}");
4990
4991 let code = r#"@settings(experimentalFeatures = warn)
4992startSketchOn(XY)
4993 |> startProfile(at = [0, 0], tag = $start)
4994 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4995"#;
4996 let result = parse_execute(code).await.unwrap();
4997 let issues = result.exec_state.issues();
4998 assert_eq!(issues.len(), 1);
4999 assert_eq!(issues[0].severity, Severity::Warning);
5000 let msg = &issues[0].message;
5001 assert!(msg.contains("experimental"), "found {msg}");
5002
5003 let code = r#"@settings(experimentalFeatures = deny)
5004startSketchOn(XY)
5005 |> startProfile(at = [0, 0], tag = $start)
5006 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5007"#;
5008 let result = parse_execute(code).await.unwrap();
5009 let issues = result.exec_state.issues();
5010 assert_eq!(issues.len(), 1);
5011 assert_eq!(issues[0].severity, Severity::Error);
5012 let msg = &issues[0].message;
5013 assert!(msg.contains("experimental"), "found {msg}");
5014
5015 let code = r#"@settings(experimentalFeatures = foo)
5016startSketchOn(XY)
5017 |> startProfile(at = [0, 0], tag = $start)
5018 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5019"#;
5020 parse_execute(code).await.unwrap_err();
5021 }
5022
5023 #[tokio::test(flavor = "multi_thread")]
5024 async fn default_angle_unit_warns_in_legacy_kcl() {
5025 for version in ["", "kclVersion = 1.0, ", "kclVersion = 2.0, "] {
5026 for unit in ["deg", "rad"] {
5027 let code = format!("@settings({version}defaultAngleUnit = {unit})\nx = 1\n");
5028 let result = parse_execute(&code).await.unwrap();
5029 let issues = result.issues();
5030 assert_eq!(issues.len(), 1, "code={code}");
5031 assert_eq!(issues[0].severity, Severity::Warning, "code={code}");
5032 assert_eq!(
5033 issues[0].message,
5034 "The `defaultAngleUnit` setting is deprecated; use explicit units for angles"
5035 );
5036 assert_eq!(variable_f64(&result, "x"), 1.0);
5037 }
5038 }
5039 }
5040
5041 #[tokio::test(flavor = "multi_thread")]
5042 async fn default_angle_unit_errors_in_kcl_v3() {
5043 for settings in [
5044 "@settings(kclVersion = \"3.0-preview\", defaultAngleUnit = deg)",
5045 "@settings(defaultAngleUnit = rad, kclVersion = \"3.0-preview\")",
5046 "@settings(defaultAngleUnit = deg)\n@settings(kclVersion = \"3.0-preview\")",
5047 "@settings(kclVersion = \"3.0-preview\")\n@settings(defaultAngleUnit = rad)",
5048 ] {
5049 let code = format!("{settings}\nx = 1\n");
5050 let Err(error) = parse_execute(&code).await else {
5051 panic!("defaultAngleUnit must fail in KCL 3.0: {code}");
5052 };
5053 assert_eq!(
5054 error.message(),
5055 "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles",
5056 "code={code}"
5057 );
5058 let ranges = error.source_ranges();
5059 assert_eq!(ranges.len(), 1);
5060 assert!(code[ranges[0].start()..ranges[0].end()].contains("defaultAngleUnit"));
5061 }
5062 }
5063
5064 #[tokio::test(flavor = "multi_thread")]
5065 async fn default_angle_unit_error_cannot_be_suppressed() {
5066 for version in ["1.0", "2.0", "\"3.0-preview\""] {
5067 let code = format!(
5068 "@warnings(allow = angleUnits)\n@settings(kclVersion = {version}, defaultAngleUnit = deg)\nx = 1\n"
5069 );
5070 let result = parse_execute(&code).await;
5071 if version == "\"3.0-preview\"" {
5072 assert_eq!(
5073 result.unwrap_err().message(),
5074 "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
5075 );
5076 } else {
5077 assert!(result.unwrap().issues().is_empty(), "code={code}");
5078 }
5079 }
5080 }
5081
5082 #[tokio::test(flavor = "multi_thread")]
5088 async fn default_angle_unit_in_import_uses_effective_kcl_version() {
5089 let dep = "@settings(defaultAngleUnit = deg)\nexport x = 1\n";
5090 for version in ["1.0", "2.0", "\"3.0-preview\""] {
5091 let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5092 let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5093 if version == "\"3.0-preview\"" {
5094 assert_eq!(
5095 result.unwrap_err().message(),
5096 "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
5097 );
5098 } else {
5099 assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0);
5100 }
5101 }
5102 }
5103
5104 #[tokio::test(flavor = "multi_thread")]
5115 async fn diagnostics_attribute_in_import_uses_effective_kcl_version() {
5116 let dep = "@diagnostics(allow = bogus)\nexport x = 1\n";
5117 for version in ["1.0", "2.0", "\"3.0-preview\""] {
5118 let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5119 let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5120 if version == "\"3.0-preview\"" {
5121 let error = result.unwrap_err();
5122 let message = error.message();
5123 assert!(
5124 message.starts_with("Unexpected diagnostic value: `bogus`; accepted values: "),
5125 "main={main}, message={message}"
5126 );
5127 } else {
5128 assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0, "main={main}");
5129 }
5130 }
5131
5132 let dep = "@warnings(allow = bogus)\nexport x = 1\n";
5136 for version in ["1.0", "2.0", "\"3.0-preview\""] {
5137 let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5138 let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5139 if version == "\"3.0-preview\"" {
5140 assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0, "main={main}");
5141 } else {
5142 let error = result.unwrap_err();
5143 let message = error.message();
5144 assert!(
5145 message.starts_with("Unexpected warning value: `bogus`; accepted values: "),
5146 "main={main}, message={message}"
5147 );
5148 }
5149 }
5150 }
5151
5152 #[tokio::test(flavor = "multi_thread")]
5156 async fn entry_point_kcl_version_records_declared_version() {
5157 for (code, expected) in [
5158 ("x = 1\n", None),
5159 ("@settings(defaultLengthUnit = in)\nx = 1\n", None),
5160 ("@settings(kclVersion = 1.0)\nx = 1\n", Some(KclVersion::V1)),
5161 ("@settings(kclVersion = 2.0)\nx = 1\n", Some(KclVersion::V2)),
5162 (
5163 "@settings(kclVersion = \"3.0-preview\")\nx = 1\n",
5164 Some(KclVersion::V3Preview),
5165 ),
5166 ] {
5167 let result = parse_execute(code).await.unwrap();
5168 assert_eq!(
5169 result.exec_state.global.entry_point_kcl_version, expected,
5170 "code={code}"
5171 );
5172 assert_eq!(
5173 result.exec_state.entry_point_version_is_v3_or_higher(),
5174 expected == Some(KclVersion::V3Preview),
5175 "code={code}"
5176 );
5177 }
5178 }
5179
5180 #[tokio::test(flavor = "multi_thread")]
5181 async fn kcl_version_lookup_prefers_entry_point_over_module_local() {
5182 let mut exec_state = parse_execute("x = 1\n").await.unwrap().exec_state;
5183
5184 exec_state.global.entry_point_kcl_version = None;
5186 exec_state.mod_local.settings.kcl_version = KclVersion::V2;
5187 assert_eq!(exec_state.kcl_version(), KclVersion::V2);
5188 assert_eq!(exec_state.legacy_caller_kcl_version(), KclVersion::V2);
5189
5190 exec_state.global.entry_point_kcl_version = Some(KclVersion::V1);
5193 assert_eq!(exec_state.kcl_version(), KclVersion::V2);
5194
5195 exec_state.global.entry_point_kcl_version = Some(KclVersion::V3Preview);
5198 assert_eq!(exec_state.kcl_version(), KclVersion::V3Preview);
5199 assert_eq!(exec_state.legacy_caller_kcl_version(), KclVersion::V2);
5200 }
5201
5202 #[tokio::test(flavor = "multi_thread")]
5207 async fn mock_execution_records_entry_point_kcl_version() {
5208 use futures::FutureExt;
5209
5210 clear_mem_cache().await;
5211
5212 let ctx = ExecutorContext::new_mock(None).await;
5213 let fresh_memory = MockConfig {
5214 use_prev_memory: false,
5215 ..Default::default()
5216 };
5217 let prev_memory = MockConfig::default();
5218
5219 let v3_program = crate::Program::parse_no_errs("@settings(kclVersion = \"3.0-preview\")\nx = 1\n").unwrap();
5220 let v2_program = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 1\n").unwrap();
5221
5222 let test_result = std::panic::AssertUnwindSafe(async {
5225 let (exec_state, _) = ctx.run_mock_returning_state(&v3_program, &fresh_memory).await.unwrap();
5226 assert_eq!(
5227 exec_state.global.entry_point_kcl_version,
5228 Some(KclVersion::V3Preview),
5229 "mock execution should record a 3.0-preview entry point"
5230 );
5231 assert!(exec_state.entry_point_version_is_v3_or_higher());
5232
5233 ctx.run_mock(&v3_program, &fresh_memory).await.unwrap();
5237 let (exec_state, _) = ctx.run_mock_returning_state(&v2_program, &prev_memory).await.unwrap();
5238 assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V2));
5239 assert!(!exec_state.entry_point_version_is_v3_or_higher());
5240
5241 ctx.run_mock(&v2_program, &fresh_memory).await.unwrap();
5244 let (exec_state, _) = ctx.run_mock_returning_state(&v3_program, &prev_memory).await.unwrap();
5245 assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V3Preview));
5246 })
5247 .catch_unwind()
5248 .await;
5249
5250 clear_mem_cache().await;
5251 ctx.close().await;
5252 if let Err(panic) = test_result {
5253 std::panic::resume_unwind(panic);
5254 }
5255 }
5256
5257 #[tokio::test(flavor = "multi_thread")]
5261 async fn mock_execution_applies_v3_semantics() {
5262 use futures::FutureExt;
5263
5264 clear_mem_cache().await;
5265
5266 let ctx = ExecutorContext::new_mock(None).await;
5267 let fresh_memory = MockConfig {
5268 use_prev_memory: false,
5269 ..Default::default()
5270 };
5271 let program = crate::Program::parse_no_errs(
5272 r#"@settings(kclVersion = "3.0-preview")
5273fn f() {
5274 return 1
5275 assert(1, isEqualTo = 2, error = "code after return ran")
5276}
5277x = f()
5278outer = 1
5279y = if true {
5280 outer = 2
5281 outer + 10
5282} else {
5283 0
5284}
5285"#,
5286 )
5287 .unwrap();
5288
5289 let test_result = std::panic::AssertUnwindSafe(async {
5292 let (exec_state, env) = ctx.run_mock_returning_state(&program, &fresh_memory).await.unwrap();
5293 let var = |name: &str| mem_get_json(exec_state.stack(), env, name).as_f64().unwrap();
5294 assert_eq!(var("x"), 1.0, "early return produces the function's value");
5295 assert_eq!(var("y"), 12.0, "the branch sees its own shadowing binding");
5296 assert_eq!(var("outer"), 1.0, "the outer binding is unchanged after the if");
5297 })
5298 .catch_unwind()
5299 .await;
5300
5301 clear_mem_cache().await;
5302 ctx.close().await;
5303 if let Err(panic) = test_result {
5304 std::panic::resume_unwind(panic);
5305 }
5306 }
5307
5308 fn commands_everywhere(result: &ExecTestResults) -> impl Iterator<Item = &kittycad_modeling_cmds::ModelingCmd> {
5311 let module_commands = result
5312 .exec_state
5313 .global
5314 .module_infos
5315 .values()
5316 .filter_map(|info| match &info.repr {
5317 ModuleRepr::Kcl(_, Some(outcome)) => Some(outcome.artifacts.commands.iter()),
5318 _ => None,
5319 })
5320 .flatten();
5321 result
5322 .root_module_artifact_commands()
5323 .iter()
5324 .chain(module_commands)
5325 .map(|artifact_command| &artifact_command.command)
5326 }
5327
5328 fn emitted_fillet_versions_everywhere(
5332 result: &ExecTestResults,
5333 ) -> Vec<kittycad_modeling_cmds::shared::EdgeCutVersion> {
5334 commands_everywhere(result)
5335 .filter_map(|command| match command {
5336 kittycad_modeling_cmds::ModelingCmd::Solid3dCutEdges(command) => Some(command.version),
5337 _ => None,
5338 })
5339 .collect()
5340 }
5341
5342 fn emitted_region_versions_everywhere(
5347 result: &ExecTestResults,
5348 ) -> Vec<kittycad_modeling_cmds::shared::RegionVersion> {
5349 commands_everywhere(result)
5350 .filter_map(|command| match command {
5351 kittycad_modeling_cmds::ModelingCmd::CreateRegion(command) => Some(command.version.clone()),
5352 _ => None,
5353 })
5354 .collect()
5355 }
5356
5357 const FILLET_AT_MODULE_TOP_LEVEL: &str = r#"
5358profile = startSketchOn(XY)
5359 |> startProfile(at = [0, 0])
5360 |> line(end = [10, 0], tag = $edge)
5361 |> line(end = [0, 10])
5362 |> line(end = [-10, 0])
5363 |> close()
5364solid = extrude(profile, length = 10)
5365fillet(solid, tags = [edge], radius = 1)
5366"#;
5367
5368 const FILLET_IN_EXPORTED_FN: &str = r#"
5369export fn filletedBox() {
5370 profile = startSketchOn(XY)
5371 |> startProfile(at = [0, 0])
5372 |> line(end = [10, 0], tag = $edge)
5373 |> line(end = [0, 10])
5374 |> line(end = [-10, 0])
5375 |> close()
5376 solid = extrude(profile, length = 10)
5377 return fillet(solid, tags = [edge], radius = 1)
5378}
5379"#;
5380
5381 #[tokio::test(flavor = "multi_thread")]
5388 async fn entry_point_v3_pins_kcl_version_for_imported_modules() {
5389 use kittycad_modeling_cmds::shared::EdgeCutVersion;
5390
5391 let dep = FILLET_AT_MODULE_TOP_LEVEL;
5392 let main = r#"@settings(kclVersion = "3.0-preview")
5393import "dep.kcl" as dep
5394"#;
5395 let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
5396 assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
5397
5398 let dep = FILLET_IN_EXPORTED_FN;
5399 let main = r#"@settings(kclVersion = "3.0-preview")
5400import filletedBox from "dep.kcl"
5401box = filletedBox()
5402"#;
5403 let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
5404 assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
5405 }
5406
5407 const REGION_AT_MODULE_TOP_LEVEL: &str = r#"
5408profile = sketch(on = XY) {
5409 outline = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
5410}
5411disc = region(segments = [profile.outline])
5412"#;
5413
5414 const REGION_IN_EXPORTED_FN: &str = r#"
5415export fn disc() {
5416 profile = sketch(on = XY) {
5417 outline = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
5418 }
5419 return region(segments = [profile.outline])
5420}
5421"#;
5422
5423 #[tokio::test(flavor = "multi_thread")]
5429 async fn legacy_kcl_version_quirk_applies_without_v3_entry_point() {
5430 use kittycad_modeling_cmds::shared::RegionVersion;
5431
5432 let dep = format!("@settings(kclVersion = 1.0)\n{REGION_AT_MODULE_TOP_LEVEL}");
5433 let main = r#"@settings(kclVersion = 2.0)
5434import "dep.kcl" as dep
5435"#;
5436 let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
5437 assert_eq!(emitted_region_versions_everywhere(&result), vec![RegionVersion::V0]);
5438
5439 let dep = format!("@settings(kclVersion = 1.0)\n{REGION_IN_EXPORTED_FN}");
5440 let main = r#"@settings(kclVersion = 2.0)
5441import disc from "dep.kcl"
5442face = disc()
5443"#;
5444 let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
5445 assert_eq!(emitted_region_versions_everywhere(&result), vec![RegionVersion::V1]);
5446 }
5447
5448 fn versioned_modules_context(modules: &[(&str, &str)]) -> ExecutorContext {
5453 let project_dir = crate::TypedPath::new("/zma-kcl-version-mismatch");
5454 let files = modules
5457 .iter()
5458 .map(|(name, source)| (project_dir.join(name).to_string(), source.as_bytes().to_vec()))
5459 .collect();
5460 ExecutorContext {
5461 engine: Arc::new(EngineManager::new_mock()),
5462 engine_batch: EngineBatchContext::default(),
5463 fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
5464 settings: ExecutorSettings {
5465 current_file: Some(project_dir.join("main.kcl")),
5466 project_directory: Some(project_dir),
5467 ..Default::default()
5468 },
5469 context_type: ContextType::Mock,
5470 execution_callbacks: Default::default(),
5471 executor_kind: machine::ExecutorKind::resolve(),
5472 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
5473 }
5474 }
5475
5476 async fn run_versioned_modules(main: &str, modules: &[(&str, &str)]) -> Result<(), KclError> {
5479 let ctx = versioned_modules_context(modules);
5480 let program = crate::Program::parse_no_errs(main).unwrap();
5481 let mut exec_state = ExecState::new(&ctx);
5482 let result = ctx.run(&program, &mut exec_state).await;
5483 ctx.close().await;
5484 result.map(|_| ()).map_err(|err| err.error)
5485 }
5486
5487 async fn run_versioned_modules_mock(main: &str, modules: &[(&str, &str)]) -> Result<(), KclError> {
5490 let ctx = versioned_modules_context(modules);
5491 let program = crate::Program::parse_no_errs(main).unwrap();
5492 let mock_config = MockConfig {
5493 use_prev_memory: false,
5494 ..Default::default()
5495 };
5496 let result = ctx.run_mock_returning_state(&program, &mock_config).await;
5497 ctx.close().await;
5498 result.map(|_| ()).map_err(|err| err.error)
5499 }
5500
5501 const V3_MAIN_IMPORTING_DEP: &str =
5502 "@settings(kclVersion = \"3.0-preview\")\nimport width from \"dep.kcl\"\nx = width\n";
5503
5504 fn dep_declaring(version: &str) -> String {
5505 format!("@settings(kclVersion = {version})\nexport width = 10\n")
5506 }
5507
5508 #[track_caller]
5511 fn assert_kcl_version_mismatch(error: &KclError, expected_dep_version: &str) {
5512 assert!(matches!(error, KclError::Semantic { .. }), "{error:#?}");
5513 assert_eq!(
5514 error.message(),
5515 format!(
5516 "Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` declares kclVersion 3.0-preview, but the imported file `/zma-kcl-version-mismatch/dep.kcl` declares kclVersion {expected_dep_version}. Update the kclVersion setting in one of these files to match the other."
5517 )
5518 );
5519 }
5520
5521 #[tokio::test(flavor = "multi_thread")]
5526 async fn imported_module_kcl_version_must_match_v3_entry_point() {
5527 for dep_version in ["2.0", "1.0"] {
5528 let dep = dep_declaring(dep_version);
5529 let error = run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
5530 .await
5531 .expect_err("mismatched kclVersion should be rejected");
5532 assert_kcl_version_mismatch(&error, dep_version);
5533
5534 let ranges = error.source_ranges();
5535 assert_eq!(ranges.len(), 2, "{ranges:#?}");
5536 assert!(!ranges[0].module_id().is_top_level());
5538 let declaration = format!("kclVersion = {dep_version}");
5539 let start = dep.find(&declaration).unwrap();
5540 assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + declaration.len()));
5541 assert!(ranges[1].module_id().is_top_level());
5543 let import_stmt = "import width from \"dep.kcl\"";
5544 let start = V3_MAIN_IMPORTING_DEP.find(import_stmt).unwrap();
5545 assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
5546 assert_eq!(
5547 error
5548 .backtrace()
5549 .iter()
5550 .map(|frame| frame.fn_name.as_deref())
5551 .collect::<Vec<_>>(),
5552 [Some("import dep.kcl"), None]
5553 );
5554 }
5555 }
5556
5557 #[tokio::test(flavor = "multi_thread")]
5560 async fn imported_module_without_kcl_version_is_allowed_under_v3_entry_point() {
5561 for dep in [
5562 "export width = 10\n",
5563 "@settings(defaultLengthUnit = in)\nexport width = 10\n",
5564 ] {
5565 run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep)])
5566 .await
5567 .unwrap_or_else(|err| panic!("dep={dep:?}: {err:#?}"));
5568 }
5569 }
5570
5571 #[tokio::test(flavor = "multi_thread")]
5574 async fn imported_module_matching_v3_kcl_version_is_allowed() {
5575 for dep_version in ["\"3.0-preview\"", "\"3-preview\"", "\"3.0.0-preview\""] {
5576 let dep = dep_declaring(dep_version);
5577 run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
5578 .await
5579 .unwrap_or_else(|err| panic!("dep={dep_version}: {err:#?}"));
5580 }
5581 }
5582
5583 #[tokio::test(flavor = "multi_thread")]
5586 async fn pre_v3_kcl_versions_may_be_mixed_without_v3_entry_point() {
5587 for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
5588 for dep_version in ["1.0", "2.0"] {
5589 let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
5590 let dep = dep_declaring(dep_version);
5591 run_versioned_modules(&main, &[("dep.kcl", &dep)])
5592 .await
5593 .unwrap_or_else(|err| panic!("main={main_header:?} dep={dep_version}: {err:#?}"));
5594 }
5595 }
5596 }
5597
5598 const V3_DEP_UNDER_UNDECLARED_ENTRY_POINT: &str = "Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` does not declare a kclVersion, but the imported file `/zma-kcl-version-mismatch/dep.kcl` declares kclVersion 3.0-preview. Declare the same kclVersion in the entry point, or update the setting in the imported file.";
5601
5602 #[tokio::test(flavor = "multi_thread")]
5609 async fn v3_import_requires_v3_entry_point() {
5610 for (main_header, entry_point_declares, fix) in [
5611 (
5612 "",
5613 "does not declare a kclVersion",
5614 "Declare the same kclVersion in the entry point, or update the setting in the imported file.",
5615 ),
5616 (
5617 "@settings(kclVersion = 1.0)\n",
5618 "declares kclVersion 1.0",
5619 "Update the kclVersion setting in one of these files to match the other.",
5620 ),
5621 (
5622 "@settings(kclVersion = 2.0)\n",
5623 "declares kclVersion 2.0",
5624 "Update the kclVersion setting in one of these files to match the other.",
5625 ),
5626 ] {
5627 let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
5628 let dep = dep_declaring("\"3.0-preview\"");
5629 let error = run_versioned_modules(&main, &[("dep.kcl", &dep)])
5630 .await
5631 .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
5632 assert!(matches!(error, KclError::Semantic { .. }), "{error:#?}");
5633 assert_eq!(
5634 error.message(),
5635 format!(
5636 "Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` {entry_point_declares}, but the imported file `/zma-kcl-version-mismatch/dep.kcl` declares kclVersion 3.0-preview. {fix}"
5637 ),
5638 "main={main_header:?}"
5639 );
5640
5641 let ranges = error.source_ranges();
5642 assert_eq!(ranges.len(), 2, "{ranges:#?}");
5643 assert!(!ranges[0].module_id().is_top_level());
5645 let declaration = "kclVersion = \"3.0-preview\"";
5646 let start = dep.find(declaration).unwrap();
5647 assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + declaration.len()));
5648 assert!(ranges[1].module_id().is_top_level());
5650 let import_stmt = "import width from \"dep.kcl\"";
5651 let start = main.find(import_stmt).unwrap();
5652 assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
5653 assert_eq!(
5654 error
5655 .backtrace()
5656 .iter()
5657 .map(|frame| frame.fn_name.as_deref())
5658 .collect::<Vec<_>>(),
5659 [Some("import dep.kcl"), None]
5660 );
5661 }
5662 }
5663
5664 #[tokio::test(flavor = "multi_thread")]
5667 async fn v3_import_spellings_are_all_rejected_without_v3_entry_point() {
5668 let main = "import width from \"dep.kcl\"\nx = width\n";
5669 for dep_version in ["\"3-preview\"", "\"3.0.0-preview\""] {
5670 let dep = dep_declaring(dep_version);
5671 let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
5672 .await
5673 .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
5674 assert_eq!(
5675 error.message(),
5676 V3_DEP_UNDER_UNDECLARED_ENTRY_POINT,
5677 "dep={dep_version}"
5678 );
5679 }
5680 }
5681
5682 #[tokio::test(flavor = "multi_thread")]
5685 async fn unreferenced_v3_whole_module_import_is_checked_in_mock_execution() {
5686 let main = "import \"dep.kcl\" as dep\nx = 1\n";
5687 let dep = dep_declaring("\"3.0-preview\"");
5688 let error = run_versioned_modules_mock(main, &[("dep.kcl", &dep)])
5689 .await
5690 .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected in mock execution");
5691 assert_eq!(error.message(), V3_DEP_UNDER_UNDECLARED_ENTRY_POINT);
5692 let ranges = error.source_ranges();
5693 assert_eq!(ranges.len(), 2, "{ranges:#?}");
5694 assert!(!ranges[0].module_id().is_top_level());
5695 assert!(ranges[1].module_id().is_top_level());
5696 let import_stmt = "import \"dep.kcl\" as dep";
5697 let start = main.find(import_stmt).unwrap();
5698 assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
5699
5700 let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
5702 .await
5703 .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected in engine execution");
5704 assert_eq!(error.message(), V3_DEP_UNDER_UNDECLARED_ENTRY_POINT);
5705 }
5706
5707 #[tokio::test(flavor = "multi_thread")]
5711 async fn transitive_v3_import_requires_v3_entry_point() {
5712 let main = "import doubled from \"a.kcl\"\nx = doubled\n";
5713 let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
5714 let b = dep_declaring("\"3.0-preview\"");
5715 let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", &b)])
5716 .await
5717 .expect_err("a transitive KCL 3.0 import without a KCL 3.0 entry point should be rejected");
5718 assert_eq!(
5719 error.message(),
5720 "Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` does not declare a kclVersion, but the imported file `/zma-kcl-version-mismatch/b.kcl` declares kclVersion 3.0-preview. Declare the same kclVersion in the entry point, or update the setting in the imported file."
5721 );
5722 assert_eq!(
5723 error
5724 .backtrace()
5725 .iter()
5726 .map(|frame| frame.fn_name.as_deref())
5727 .collect::<Vec<_>>(),
5728 [Some("import b.kcl"), Some("import a.kcl"), None]
5729 );
5730 }
5731
5732 #[tokio::test(flavor = "multi_thread")]
5735 async fn v3_import_without_v3_entry_point_or_entry_point_path() {
5736 let main = "import width from \"dep.kcl\"\nx = width\n";
5737 let dep = dep_declaring("\"3.0-preview\"");
5738 let error = execute_with_modules(main, &[("dep.kcl", &dep)])
5739 .await
5740 .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
5741 let message = error.message();
5742 assert!(
5743 message.starts_with(
5744 "Mixing KCL versions in a single program is not allowed. The entry point does not declare a kclVersion, but the imported file `"
5745 ),
5746 "{message}"
5747 );
5748 assert!(
5749 message.ends_with(
5750 "dep.kcl` declares kclVersion 3.0-preview. Declare the same kclVersion in the entry point, or update the setting in the imported file."
5751 ),
5752 "{message}"
5753 );
5754 }
5755
5756 #[tokio::test(flavor = "multi_thread")]
5760 async fn transitive_import_kcl_version_mismatch_names_entry_point_and_mismatched_file() {
5761 let main = "@settings(kclVersion = \"3.0-preview\")\nimport doubled from \"a.kcl\"\nx = doubled\n";
5762 let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
5763 let b = dep_declaring("2.0");
5764 let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", &b)])
5765 .await
5766 .expect_err("mismatched kclVersion in a transitive import should be rejected");
5767 assert_eq!(
5768 error.message(),
5769 "Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` declares kclVersion 3.0-preview, but the imported file `/zma-kcl-version-mismatch/b.kcl` declares kclVersion 2.0. Update the kclVersion setting in one of these files to match the other."
5770 );
5771 assert_eq!(
5772 error
5773 .backtrace()
5774 .iter()
5775 .map(|frame| frame.fn_name.as_deref())
5776 .collect::<Vec<_>>(),
5777 [Some("import b.kcl"), Some("import a.kcl"), None]
5778 );
5779 let ranges = error.source_ranges();
5780 assert_eq!(ranges.len(), 3, "{ranges:#?}");
5781 assert!(!ranges[0].module_id().is_top_level());
5782 assert!(!ranges[1].module_id().is_top_level());
5783 assert!(ranges[2].module_id().is_top_level());
5784 }
5785
5786 #[tokio::test(flavor = "multi_thread")]
5789 async fn unreferenced_whole_module_import_kcl_version_is_checked_in_mock_execution() {
5790 let main = "@settings(kclVersion = \"3.0-preview\")\nimport \"dep.kcl\" as dep\nx = 1\n";
5791 let dep = dep_declaring("2.0");
5792 let error = run_versioned_modules_mock(main, &[("dep.kcl", &dep)])
5793 .await
5794 .expect_err("mismatched kclVersion should be rejected in mock execution");
5795 assert_kcl_version_mismatch(&error, "2.0");
5796 let ranges = error.source_ranges();
5797 assert_eq!(ranges.len(), 2, "{ranges:#?}");
5798 assert!(!ranges[0].module_id().is_top_level());
5799 assert!(ranges[1].module_id().is_top_level());
5800 let import_stmt = "import \"dep.kcl\" as dep";
5801 let start = main.find(import_stmt).unwrap();
5802 assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
5803
5804 let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
5806 .await
5807 .expect_err("mismatched kclVersion should be rejected in engine execution");
5808 assert_kcl_version_mismatch(&error, "2.0");
5809
5810 run_versioned_modules_mock(main, &[("dep.kcl", "export width = 10\n")])
5812 .await
5813 .unwrap();
5814 }
5815
5816 #[tokio::test(flavor = "multi_thread")]
5821 async fn std_modules_are_exempt_from_kcl_version_matching() {
5822 let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport QUARTER_TURN from \"std::turns\"\nx = QUARTER_TURN\n";
5823 run_versioned_modules(main, &[]).await.unwrap();
5824 run_versioned_modules_mock(main, &[]).await.unwrap();
5825 }
5826
5827 #[tokio::test(flavor = "multi_thread")]
5830 async fn kcl_version_mismatch_without_entry_point_path() {
5831 let dep = dep_declaring("2.0");
5832 let error = execute_with_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
5833 .await
5834 .expect_err("mismatched kclVersion should be rejected");
5835 let message = error.message();
5836 assert!(
5837 message.starts_with(
5838 "Mixing KCL versions in a single program is not allowed. The entry point declares kclVersion 3.0-preview, but the imported file `"
5839 ),
5840 "{message}"
5841 );
5842 assert!(
5843 message.ends_with(
5844 "dep.kcl` declares kclVersion 2.0. Update the kclVersion setting in one of these files to match the other."
5845 ),
5846 "{message}"
5847 );
5848 }
5849
5850 #[track_caller]
5851 fn variable_f64(result: &ExecTestResults, name: &str) -> f64 {
5852 mem_get_json(result.exec_state.stack(), result.mem_env, name)
5853 .as_f64()
5854 .unwrap()
5855 }
5856
5857 #[tokio::test(flavor = "multi_thread")]
5858 async fn return_terminates_function_early_in_v3() {
5859 let code = r#"@settings(kclVersion = "3.0-preview")
5860fn f() {
5861 return 1
5862 assert(1, isEqualTo = 2, error = "code after return ran")
5863}
5864x = f()
5865"#;
5866 let result = parse_execute(code).await.unwrap();
5867 assert_eq!(variable_f64(&result, "x"), 1.0);
5868 }
5869
5870 #[tokio::test(flavor = "multi_thread")]
5871 async fn second_return_is_unreachable_in_v3() {
5872 let code = r#"@settings(kclVersion = "3.0-preview")
5873fn f() {
5874 return 1
5875 return 2
5876}
5877x = f()
5878"#;
5879 let result = parse_execute(code).await.unwrap();
5880 assert_eq!(variable_f64(&result, "x"), 1.0);
5881 }
5882
5883 #[tokio::test(flavor = "multi_thread")]
5884 async fn return_inside_if_arm_returns_from_function_in_v3() {
5885 let code = r#"@settings(kclVersion = "3.0-preview")
5886fn f(@b) {
5887 dummy = if b {
5888 return 1
5889 0
5890 } else {
5891 0
5892 }
5893 return 2
5894}
5895x = f(true)
5896y = f(false)
5897"#;
5898 let result = parse_execute(code).await.unwrap();
5899 assert_eq!(variable_f64(&result, "x"), 1.0);
5900 assert_eq!(variable_f64(&result, "y"), 2.0);
5901 }
5902
5903 #[tokio::test(flavor = "multi_thread")]
5904 async fn return_inside_nested_if_returns_from_function_in_v3() {
5905 let code = r#"@settings(kclVersion = "3.0-preview")
5906fn f(@a, b) {
5907 dummy = if a {
5908 inner = if b {
5909 return 10
5910 0
5911 } else {
5912 1
5913 }
5914 inner + 1
5915 } else {
5916 2
5917 }
5918 return dummy * 100
5919}
5920x = f(true, b = true)
5921y = f(true, b = false)
5922z = f(false, b = false)
5923"#;
5924 let result = parse_execute(code).await.unwrap();
5925 assert_eq!(variable_f64(&result, "x"), 10.0);
5926 assert_eq!(variable_f64(&result, "y"), 200.0);
5927 assert_eq!(variable_f64(&result, "z"), 200.0);
5928 }
5929
5930 #[tokio::test(flavor = "multi_thread")]
5931 async fn return_inside_closure_returns_only_from_closure_in_v3() {
5932 let code = r#"@settings(kclVersion = "3.0-preview")
5933fn outer() {
5934 inner = fn() {
5935 return 5
5936 assert(1, isEqualTo = 2, error = "code after inner return ran")
5937 }
5938 v = inner()
5939 return v + 1
5940}
5941x = outer()
5942"#;
5943 let result = parse_execute(code).await.unwrap();
5944 assert_eq!(variable_f64(&result, "x"), 6.0);
5945 }
5946
5947 #[tokio::test(flavor = "multi_thread")]
5948 async fn return_type_coercion_applies_to_early_return_in_v3() {
5949 let code = r#"@settings(kclVersion = "3.0-preview")
5950fn f(): number(mm) {
5951 return 1
5952 assert(1, isEqualTo = 2, error = "code after return ran")
5953}
5954x = f()
5955"#;
5956 let result = parse_execute(code).await.unwrap();
5957 assert_eq!(variable_f64(&result, "x"), 1.0);
5958
5959 let code = r#"@settings(kclVersion = "3.0-preview")
5962fn f(): number(mm) {
5963 return "nope"
5964}
5965x = f()
5966"#;
5967 let err = parse_execute(code).await.expect_err("coercion failure should error");
5968 assert!(err.message().contains("type"), "unexpected message: {}", err.message());
5969 }
5970
5971 #[tokio::test(flavor = "multi_thread")]
5972 async fn return_at_top_level_errors() {
5973 for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
5975 let code = format!("{header}return 1\n");
5976 assert_eq!(
5977 parse_execute(&code).await.expect_err("should error").message(),
5978 "Cannot return from outside a function."
5979 );
5980 }
5981
5982 let code = r#"@settings(kclVersion = "3.0-preview")
5986x = if true {
5987 return 1
5988 0
5989} else {
5990 0
5991}
5992"#;
5993 assert_eq!(
5994 parse_execute(code).await.expect_err("should error").message(),
5995 "Cannot return from outside a function."
5996 );
5997 }
5998
5999 #[tokio::test(flavor = "multi_thread")]
6000 async fn exit_inside_function_still_exits_program_in_v3() {
6001 let code = r#"@settings(kclVersion = "3.0-preview")
6002fn f() {
6003 exit()
6004 return 1
6005}
6006x = f()
6007assert(1, isEqualTo = 2, error = "code after exit ran")
6008"#;
6009 parse_execute(code).await.unwrap();
6010 }
6011
6012 #[tokio::test(flavor = "multi_thread")]
6013 async fn return_inside_sketch_block_terminates_function_in_v3() {
6014 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
6015fn f() {
6016 sketch(on = XY) {
6017 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
6018 return 42
6019 }
6020 return 0
6021}
6022x = f()
6023"#;
6024 let result = parse_execute(code).await.unwrap();
6025 assert_eq!(variable_f64(&result, "x"), 42.0);
6026 }
6027
6028 #[tokio::test(flavor = "multi_thread")]
6029 async fn return_inside_sketch_block_ignored_without_v3() {
6030 let code = r#"@settings(experimentalFeatures = allow)
6033fn f() {
6034 sketch(on = XY) {
6035 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
6036 return 42
6037 }
6038 return 0
6039}
6040x = f()
6041"#;
6042 let result = parse_execute(code).await.unwrap();
6043 assert_eq!(variable_f64(&result, "x"), 0.0);
6044 }
6045
6046 #[tokio::test(flavor = "multi_thread")]
6047 async fn code_after_return_still_runs_without_v3() {
6048 let code = r#"fn f() {
6049 return 1
6050 assert(1, isEqualTo = 2, error = "ran past return")
6051}
6052x = f()
6053"#;
6054 let err = parse_execute(code).await.expect_err("should error");
6055 assert!(
6056 err.message().contains("ran past return"),
6057 "unexpected message: {}",
6058 err.message()
6059 );
6060 }
6061
6062 #[tokio::test(flavor = "multi_thread")]
6063 async fn multiple_returns_error_without_v3() {
6064 let code = r#"fn f() {
6065 return 1
6066 return 2
6067}
6068x = f()
6069"#;
6070 assert_eq!(
6071 parse_execute(code).await.expect_err("should error").message(),
6072 "Multiple returns from a single function."
6073 );
6074 }
6075
6076 #[tokio::test(flavor = "multi_thread")]
6077 async fn if_arm_return_plus_function_return_errors_without_v3() {
6078 let code = r#"fn f() {
6082 dummy = if true {
6083 return 1
6084 0
6085 } else {
6086 0
6087 }
6088 return 2
6089}
6090x = f()
6091"#;
6092 assert_eq!(
6093 parse_execute(code).await.expect_err("should error").message(),
6094 "Multiple returns from a single function."
6095 );
6096 }
6097
6098 #[tokio::test(flavor = "multi_thread")]
6099 async fn top_level_if_arm_return_ignored_without_v3() {
6100 let code = r#"x = if true {
6104 return 1
6105 0
6106} else {
6107 0
6108}
6109"#;
6110 let result = parse_execute(code).await.unwrap();
6111 assert_eq!(variable_f64(&result, "x"), 0.0);
6112 assert_eq!(variable_f64(&result, memory::RETURN_NAME), 1.0);
6113 }
6114
6115 #[tokio::test(flavor = "multi_thread")]
6118 async fn return_semantics_gated_on_entry_point_not_module() {
6119 let dep = r#"@settings(kclVersion = "3.0-preview")
6123export fn f() {
6124 return 1
6125 assert(1, isEqualTo = 2, error = "ran past return")
6126}
6127"#;
6128 let main = r#"@settings(kclVersion = 2.0)
6129import f from "dep.kcl"
6130x = f()
6131"#;
6132 let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
6133 assert!(
6134 err.message()
6135 .starts_with("Mixing KCL versions in a single program is not allowed."),
6136 "unexpected message: {}",
6137 err.message()
6138 );
6139
6140 let dep = r#"export fn f() {
6145 return 1
6146 assert(1, isEqualTo = 2, error = "ran past return")
6147}
6148"#;
6149 let main = r#"@settings(kclVersion = "3.0-preview")
6150import f from "dep.kcl"
6151x = f()
6152"#;
6153 let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
6154 assert_eq!(variable_f64(&result, "x"), 1.0);
6155 }
6156
6157 #[tokio::test(flavor = "multi_thread")]
6163 async fn return_inside_map_and_reduce_callbacks_in_v3() {
6164 let code = r#"@settings(kclVersion = "3.0-preview")
6165doubled = map([1, 2, 3], f = fn(@i) {
6166 return i * 2
6167 assert(1, isEqualTo = 2, error = "code after return ran in the map callback")
6168})
6169assert(doubled[0], isEqualTo = 2, error = "map result 0")
6170assert(doubled[1], isEqualTo = 4, error = "map result 1")
6171assert(doubled[2], isEqualTo = 6, error = "map result 2")
6172
6173total = reduce([1, 2, 3], initial = 0, f = fn(@i, accum) {
6174 return accum + i
6175 assert(1, isEqualTo = 2, error = "code after return ran in the reduce callback")
6176})
6177assert(total, isEqualTo = 6, error = "reduce total")
6178"#;
6179 let result = parse_execute(code).await.unwrap();
6180 assert_eq!(variable_f64(&result, "total"), 6.0);
6181 }
6182
6183 #[tokio::test(flavor = "multi_thread")]
6189 async fn early_returns_do_not_leak_machine_call_depth() {
6190 let code = r#"@settings(kclVersion = "3.0-preview")
6191fn one() {
6192 return 1
6193 assert(1, isEqualTo = 2, error = "code after return ran")
6194}
6195total = reduce([1..100], initial = 0, f = fn(@i, accum) {
6196 return accum + one()
6197})
6198assert(total, isEqualTo = 100, error = "each call returns 1")
6199"#;
6200 let result = parse_execute(code).await.unwrap();
6201 let high_water = result.exec_state.global.machine_depth_high_water;
6205 assert!(high_water < 10, "high water: {high_water}");
6206 }
6207
6208 #[tokio::test(flavor = "multi_thread")]
6216 async fn top_level_if_arm_return_in_imported_module_errors_in_v3() {
6217 let dep = r#"x = if true {
6218 return 1
6219 0
6220} else {
6221 0
6222}
6223export y = x
6224"#;
6225 let main = r#"@settings(kclVersion = "3.0-preview")
6226import y from "dep.kcl"
6227z = y
6228"#;
6229 let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
6230 assert!(
6231 err.message().contains("Cannot return from outside a function."),
6232 "unexpected message: {}",
6233 err.message()
6234 );
6235 }
6236
6237 #[tokio::test(flavor = "multi_thread")]
6243 async fn return_of_exit_still_exits_program_in_v3() {
6244 let code = r#"@settings(kclVersion = "3.0-preview")
6245fn f() {
6246 return exit()
6247}
6248x = f()
6249assert(1, isEqualTo = 2, error = "code after exit ran")
6250"#;
6251 parse_execute(code).await.unwrap();
6252 }
6253
6254 #[tokio::test(flavor = "multi_thread")]
6255 async fn if_arm_bindings_do_not_leak_in_v3() {
6256 let code = r#"@settings(kclVersion = "3.0-preview")
6257x = if true {
6258 y = 1
6259 y
6260} else {
6261 0
6262}
6263z = y
6264"#;
6265 let err = parse_execute(code).await.expect_err("should error");
6266 assert!(
6267 err.message().contains("`y` is not defined"),
6268 "unexpected message: {}",
6269 err.message()
6270 );
6271 }
6272
6273 #[tokio::test(flavor = "multi_thread")]
6274 async fn if_arm_bindings_leak_without_v3() {
6275 for header in ["", "@settings(kclVersion = 2.0)\n"] {
6278 let code = format!(
6279 r#"{header}x = if true {{
6280 y = 1
6281 y
6282}} else {{
6283 0
6284}}
6285z = y
6286"#
6287 );
6288 let result = parse_execute(&code).await.unwrap();
6289 assert_eq!(variable_f64(&result, "z"), 1.0);
6290 }
6291 }
6292
6293 #[tokio::test(flavor = "multi_thread")]
6294 async fn if_arm_shadowing_allowed_in_v3() {
6295 let code = r#"@settings(kclVersion = "3.0-preview")
6296y = 1
6297x = if true {
6298 y = 2
6299 y + 10
6300} else {
6301 0
6302}
6303"#;
6304 let result = parse_execute(code).await.unwrap();
6305 assert_eq!(variable_f64(&result, "x"), 12.0);
6306 assert_eq!(variable_f64(&result, "y"), 1.0);
6307 }
6308
6309 #[tokio::test(flavor = "multi_thread")]
6310 async fn if_arm_shadowing_still_errors_without_v3() {
6311 for header in ["", "@settings(kclVersion = 2.0)\n"] {
6314 let code = format!(
6315 r#"{header}y = 1
6316x = if true {{
6317 y = 2
6318 y
6319}} else {{
6320 0
6321}}
6322"#
6323 );
6324 let err = parse_execute(&code).await.expect_err("should error");
6325 assert!(
6326 err.message().contains("Cannot redefine `y`"),
6327 "unexpected message: {}",
6328 err.message()
6329 );
6330 }
6331 }
6332
6333 #[tokio::test(flavor = "multi_thread")]
6334 async fn if_arm_closure_escape_in_v3() {
6335 let code = r#"@settings(kclVersion = "3.0-preview")
6338n = 1
6339f = if true {
6340 m = 41
6341 g = fn() {
6342 return m + n
6343 }
6344 g
6345} else {
6346 g = fn() {
6347 return 0
6348 }
6349 g
6350}
6351x = f()
6352"#;
6353 let result = parse_execute(code).await.unwrap();
6354 assert_eq!(variable_f64(&result, "x"), 42.0);
6355 }
6356
6357 #[tokio::test(flavor = "multi_thread")]
6358 async fn recursive_if_arm_closure_keeps_enclosing_function_frame_alive_in_v3() {
6359 let code = r#"@settings(kclVersion = "3.0-preview")
6363fn makeCounter() {
6364 outer = 40
6365 selected = if true {
6366 inner = 2
6367 fn count(@n) {
6368 return if n == 0 {
6369 outer + inner
6370 } else {
6371 count(n - 1) + 1
6372 }
6373 }
6374 count
6375 } else {
6376 fn fallback(@n) {
6377 return n
6378 }
6379 fallback
6380 }
6381 return selected
6382}
6383counter = makeCounter()
6384x = counter(3)
6385"#;
6386 let result = parse_execute(code).await.unwrap();
6387 assert_eq!(variable_f64(&result, "x"), 45.0);
6388 }
6389
6390 #[tokio::test(flavor = "multi_thread")]
6391 async fn return_inside_scoped_if_arm_in_v3() {
6392 let code = r#"@settings(kclVersion = "3.0-preview")
6395fn f(@b) {
6396 local = if b {
6397 w = 1
6398 return w + 9
6399 0
6400 } else {
6401 0
6402 }
6403 return local
6404}
6405x = f(true)
6406y = f(false)
6407"#;
6408 let result = parse_execute(code).await.unwrap();
6409 assert_eq!(variable_f64(&result, "x"), 10.0);
6410 assert_eq!(variable_f64(&result, "y"), 0.0);
6411 }
6412
6413 #[tokio::test(flavor = "multi_thread")]
6414 async fn else_if_and_nested_if_scoping_in_v3() {
6415 let code = r#"@settings(kclVersion = "3.0-preview")
6416x = if false {
6417 0
6418} else if true {
6419 a = 1
6420 b = if true {
6421 c = 2
6422 a + c
6423 } else {
6424 0
6425 }
6426 a + b
6427} else {
6428 0
6429}
6430"#;
6431 let result = parse_execute(code).await.unwrap();
6432 assert_eq!(variable_f64(&result, "x"), 4.0);
6433
6434 let code = r#"@settings(kclVersion = "3.0-preview")
6436x = if true {
6437 b = if true {
6438 c = 2
6439 c
6440 } else {
6441 0
6442 }
6443 b + c
6444} else {
6445 0
6446}
6447"#;
6448 let err = parse_execute(code).await.expect_err("should error");
6449 assert!(
6450 err.message().contains("`c` is not defined"),
6451 "unexpected message: {}",
6452 err.message()
6453 );
6454 }
6455
6456 #[tokio::test(flavor = "multi_thread")]
6461 async fn else_if_and_final_else_arms_are_isolated_in_v3() {
6462 let code = r#"@settings(kclVersion = "3.0-preview")
6464x = if false {
6465 0
6466} else if true {
6467 y = 1
6468 y
6469} else {
6470 0
6471}
6472z = y
6473"#;
6474 let err = parse_execute(code).await.expect_err("should error");
6475 assert!(
6476 err.message().contains("`y` is not defined"),
6477 "unexpected message: {}",
6478 err.message()
6479 );
6480
6481 let code = r#"@settings(kclVersion = "3.0-preview")
6483x = if false {
6484 0
6485} else if false {
6486 0
6487} else {
6488 y = 1
6489 y
6490}
6491z = y
6492"#;
6493 let err = parse_execute(code).await.expect_err("should error");
6494 assert!(
6495 err.message().contains("`y` is not defined"),
6496 "unexpected message: {}",
6497 err.message()
6498 );
6499
6500 let code = r#"@settings(kclVersion = "3.0-preview")
6502outer = 1
6503x = if false {
6504 0
6505} else if true {
6506 outer = 2
6507 outer + 10
6508} else {
6509 0
6510}
6511"#;
6512 let result = parse_execute(code).await.unwrap();
6513 assert_eq!(variable_f64(&result, "x"), 12.0);
6514 assert_eq!(variable_f64(&result, "outer"), 1.0);
6515
6516 let code = r#"@settings(kclVersion = "3.0-preview")
6518outer = 1
6519x = if false {
6520 0
6521} else if false {
6522 0
6523} else {
6524 outer = 2
6525 outer + 10
6526}
6527"#;
6528 let result = parse_execute(code).await.unwrap();
6529 assert_eq!(variable_f64(&result, "x"), 12.0);
6530 assert_eq!(variable_f64(&result, "outer"), 1.0);
6531 }
6532
6533 #[tokio::test(flavor = "multi_thread")]
6537 async fn else_if_and_final_else_arm_bindings_leak_without_v3() {
6538 for header in ["", "@settings(kclVersion = 2.0)\n"] {
6539 let code = format!(
6540 r#"{header}x = if false {{
6541 0
6542}} else if true {{
6543 y = 1
6544 y
6545}} else {{
6546 0
6547}}
6548z = y
6549"#
6550 );
6551 let result = parse_execute(&code).await.unwrap();
6552 assert_eq!(variable_f64(&result, "z"), 1.0, "code={code}");
6553
6554 let code = format!(
6555 r#"{header}x = if false {{
6556 0
6557}} else if false {{
6558 0
6559}} else {{
6560 y = 1
6561 y
6562}}
6563z = y
6564"#
6565 );
6566 let result = parse_execute(&code).await.unwrap();
6567 assert_eq!(variable_f64(&result, "z"), 1.0, "code={code}");
6568
6569 let code = format!(
6570 r#"{header}outer = 1
6571x = if false {{
6572 0
6573}} else if true {{
6574 outer = 2
6575 outer
6576}} else {{
6577 0
6578}}
6579"#
6580 );
6581 let err = parse_execute(&code).await.expect_err("should error");
6582 assert!(
6583 err.message().contains("Cannot redefine `outer`"),
6584 "unexpected message: {}",
6585 err.message()
6586 );
6587 }
6588 }
6589
6590 #[tokio::test(flavor = "multi_thread")]
6591 async fn error_inside_if_arm_unwinds_balanced_in_v3() {
6592 let code = r#"@settings(kclVersion = "3.0-preview")
6595fn f() {
6596 dummy = if true {
6597 assert(1, isEqualTo = 2, error = "boom")
6598 0
6599 } else {
6600 0
6601 }
6602 return dummy
6603}
6604x = f()
6605"#;
6606 let err = parse_execute(code).await.expect_err("should error");
6607 assert!(err.message().contains("boom"), "unexpected message: {}", err.message());
6608 }
6609
6610 #[tokio::test(flavor = "multi_thread")]
6611 async fn exit_inside_scoped_if_arm_in_v3() {
6612 let code = r#"@settings(kclVersion = "3.0-preview")
6613fn f() {
6614 dummy = if true {
6615 exit()
6616 0
6617 } else {
6618 0
6619 }
6620 return dummy
6621}
6622x = f()
6623assert(1, isEqualTo = 2, error = "code after exit ran")
6624"#;
6625 parse_execute(code).await.unwrap();
6626 }
6627
6628 #[tokio::test(flavor = "multi_thread")]
6631 async fn if_arm_scoping_gated_on_entry_point_not_module() {
6632 let dep = r#"@settings(kclVersion = "3.0-preview")
6636ignored = if true {
6637 leaked = 1
6638 leaked
6639} else {
6640 0
6641}
6642export leakCheck = leaked
6643"#;
6644 let main = r#"@settings(kclVersion = 2.0)
6645import leakCheck from "dep.kcl"
6646x = leakCheck
6647"#;
6648 let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
6649 assert!(
6650 err.message()
6651 .starts_with("Mixing KCL versions in a single program is not allowed."),
6652 "unexpected message: {}",
6653 err.message()
6654 );
6655
6656 let dep = r#"ignored = if true {
6661 arm = 1
6662 arm
6663} else {
6664 0
6665}
6666export fn leakCheck() {
6667 return arm
6668}
6669"#;
6670 let main = r#"@settings(kclVersion = "3.0-preview")
6671import leakCheck from "dep.kcl"
6672x = leakCheck()
6673"#;
6674 let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
6675 assert!(
6676 err.message().contains("`arm` is not defined"),
6677 "unexpected message: {}",
6678 err.message()
6679 );
6680 }
6681
6682 #[tokio::test(flavor = "multi_thread")]
6687 async fn unwind_through_sketch_block_inside_scoped_if_arm_in_v3() {
6688 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
6691fn f() {
6692 dummy = if true {
6693 s = sketch(on = XY) {
6694 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
6695 q = notDefinedAnywhere
6696 }
6697 0
6698 } else {
6699 0
6700 }
6701 return dummy
6702}
6703x = f()
6704"#;
6705 let err = parse_execute(code).await.unwrap_err();
6706 assert!(
6707 err.message().contains("`notDefinedAnywhere` is not defined"),
6708 "unexpected message: {}",
6709 err.message()
6710 );
6711
6712 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
6714fn f() {
6715 dummy = if true {
6716 s = sketch(on = XY) {
6717 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
6718 e = exit()
6719 }
6720 0
6721 } else {
6722 0
6723 }
6724 return dummy
6725}
6726x = f()
6727assert(1, isEqualTo = 2, error = "code after exit ran")
6728"#;
6729 parse_execute(code).await.unwrap();
6730
6731 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
6733fn g() {
6734 dummy = if true {
6735 s = sketch(on = XY) {
6736 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
6737 return 42
6738 }
6739 0
6740 } else {
6741 0
6742 }
6743 return 0
6744}
6745y = g()
6746"#;
6747 let result = parse_execute(code).await.unwrap();
6748 assert_eq!(variable_f64(&result, "y"), 42.0);
6749 }
6750
6751 #[tokio::test(flavor = "multi_thread")]
6755 async fn tag_declared_inside_if_arm_is_arm_local_in_v3() {
6756 let arm_body = r#"p = if true {
6757 profile = startSketchOn(XY)
6758 |> startProfile(at = [0, 0])
6759 |> line(end = [10, 0], tag = $edge)
6760 |> line(end = [0, 10])
6761 |> line(end = [-10, 0])
6762 |> close()
6763 inArmLen = segLen(edge)
6764 assert(inArmLen, isEqualTo = 10, error = "tag is usable within its arm")
6765 profile
6766} else {
6767 startSketchOn(XY)
6768 |> startProfile(at = [0, 0])
6769 |> line(end = [5, 0])
6770 |> line(end = [0, 5])
6771 |> line(end = [-5, 0])
6772 |> close()
6773}
6774len = segLen(edge)
6775"#;
6776
6777 let code = format!("@settings(kclVersion = \"3.0-preview\")\n{arm_body}");
6778 let err = parse_execute(&code).await.unwrap_err();
6779 assert!(
6780 err.message().contains("`edge` is not defined"),
6781 "unexpected message: {}",
6782 err.message()
6783 );
6784
6785 let result = parse_execute(arm_body).await.unwrap();
6787 assert_eq!(variable_f64(&result, "len"), 10.0);
6788 }
6789
6790 #[tokio::test(flavor = "multi_thread")]
6796 async fn if_arm_scopes_do_not_retain_function_frames_in_v3() {
6797 let code = r#"@settings(kclVersion = "3.0-preview")
6798fn pick(@i) {
6799 r = if i > 50 {
6800 a = i * 2
6801 a
6802 } else {
6803 b = i + 1
6804 b
6805 }
6806 return r
6807}
6808results = map([1..100], f = fn(@i) { return pick(i) })
6809assert(results[0], isEqualTo = 2, error = "pick(1) = 2")
6810assert(results[99], isEqualTo = 200, error = "pick(100) = 200")
6811"#;
6812 let result = parse_execute(code).await.unwrap();
6813 let retained = result.exec_state.stack().memory.envs_with_bindings();
6816 assert!(retained < 20, "retained environments: {retained}");
6817 }
6818
6819 #[tokio::test(flavor = "multi_thread")]
6824 async fn if_arm_scoping_inside_pipe_in_v3() {
6825 let code = r#"@settings(kclVersion = "3.0-preview")
6826cond = true
6827result = 5
6828 |> if cond {
6829 a = 20
6830 a
6831 } else {
6832 0
6833 }
6834 |> max([%, 1])
6835"#;
6836 let result = parse_execute(code).await.unwrap();
6837 assert_eq!(variable_f64(&result, "result"), 20.0);
6841
6842 let code = r#"@settings(kclVersion = "3.0-preview")
6844cond = true
6845result = 5
6846 |> if cond {
6847 a = 20
6848 a
6849 } else {
6850 0
6851 }
6852leaked = a
6853"#;
6854 let err = parse_execute(code).await.unwrap_err();
6855 assert!(
6856 err.message().contains("`a` is not defined"),
6857 "unexpected message: {}",
6858 err.message()
6859 );
6860 }
6861
6862 #[tokio::test(flavor = "multi_thread")]
6863 async fn member_expression_evaluates_object_before_property_in_v3() {
6864 let code = r#"@settings(kclVersion = "3.0-preview")
6867x = a[b]
6868"#;
6869 let err = parse_execute(code).await.expect_err("should error");
6870 assert_eq!(err.message(), "`a` is not defined");
6871 }
6872
6873 #[tokio::test(flavor = "multi_thread")]
6874 async fn member_expression_evaluates_property_before_object_without_v3() {
6875 let code = r#"@settings(kclVersion = 2.0)
6878x = a[b]
6879"#;
6880 let err = parse_execute(code).await.expect_err("should error");
6881 assert_eq!(err.message(), "`b` is not defined");
6882 }
6883
6884 #[tokio::test(flavor = "multi_thread")]
6885 async fn member_expression_undefined_object_with_static_property_in_v3() {
6886 let code = r#"@settings(kclVersion = "3.0-preview")
6887x = a.b
6888"#;
6889 let err = parse_execute(code).await.expect_err("should error");
6890 assert_eq!(err.message(), "`a` is not defined");
6891 }
6892
6893 #[tokio::test(flavor = "multi_thread")]
6894 async fn member_expression_values_in_v3() {
6895 let code = r#"@settings(kclVersion = "3.0-preview")
6898fn xs() {
6899 return [10, 20, 30]
6900}
6901fn one() {
6902 return 1
6903}
6904obj = { inner = { xs = xs() } }
6905objs = [obj, obj]
6906a = obj.inner.xs[one()]
6907b = xs()[one() + 1]
6908c = objs[0].inner.xs[0]
6909"#;
6910 let result = parse_execute(code).await.unwrap();
6911 assert_eq!(variable_f64(&result, "a"), 20.0);
6912 assert_eq!(variable_f64(&result, "b"), 30.0);
6913 assert_eq!(variable_f64(&result, "c"), 10.0);
6914 }
6915
6916 #[tokio::test(flavor = "multi_thread")]
6917 async fn exit_inside_member_expression_in_v3() {
6918 for code in [
6921 r#"@settings(kclVersion = "3.0-preview")
6922x = exit()[0]
6923assert(1, isEqualTo = 2, error = "code after exit ran")
6924"#,
6925 r#"@settings(kclVersion = "3.0-preview")
6926arr = [1]
6927x = arr[exit()]
6928assert(1, isEqualTo = 2, error = "code after exit ran")
6929"#,
6930 ] {
6931 parse_execute(code).await.unwrap();
6932 }
6933 }
6934
6935 #[tokio::test(flavor = "multi_thread")]
6936 async fn experimental_parameter() {
6937 let code = r#"
6938fn inc(@x, @(experimental = true) amount? = 1) {
6939 return x + amount
6940}
6941
6942answer = inc(5, amount = 2)
6943"#;
6944 let result = parse_execute(code).await.unwrap();
6945 let issues = result.exec_state.issues();
6946 assert_eq!(issues.len(), 1);
6947 assert_eq!(issues[0].severity, Severity::Error);
6948 let msg = &issues[0].message;
6949 assert!(msg.contains("experimental"), "found {msg}");
6950
6951 let code = r#"
6953fn inc(@x, @(experimental = true) amount? = 1) {
6954 return x + amount
6955}
6956
6957answer = inc(5)
6958"#;
6959 let result = parse_execute(code).await.unwrap();
6960 let issues = result.exec_state.issues();
6961 assert!(issues.is_empty(), "issues={issues:#?}");
6962 }
6963
6964 #[tokio::test(flavor = "multi_thread")]
6965 async fn experimental_scalar_fixed_constraint() {
6966 let code_left = r#"@settings(experimentalFeatures = warn)
6967sketch(on = XY) {
6968 point1 = point(at = [var 0mm, var 0mm])
6969 point1.at[0] == 1mm
6970}
6971"#;
6972 let code_right = r#"@settings(experimentalFeatures = warn)
6974sketch(on = XY) {
6975 point1 = point(at = [var 0mm, var 0mm])
6976 1mm == point1.at[0]
6977}
6978"#;
6979
6980 for code in [code_left, code_right] {
6981 let result = parse_execute(code).await.unwrap();
6982 let issues = result.exec_state.issues();
6983 let Some(error) = issues
6984 .iter()
6985 .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
6986 else {
6987 panic!("found {issues:#?}");
6988 };
6989 assert_eq!(error.severity, Severity::Warning);
6990 }
6991 }
6992
6993 #[tokio::test(flavor = "multi_thread")]
6997 async fn test_tangent_line_arc_executes_with_mock_engine() {
6998 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
6999 parse_execute(&code).await.unwrap();
7000 }
7001
7002 #[tokio::test(flavor = "multi_thread")]
7003 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
7004 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
7005 parse_execute(&code).await.unwrap();
7006 }
7007
7008 #[tokio::test(flavor = "multi_thread")]
7009 async fn test_tangent_line_circle_executes_with_mock_engine() {
7010 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
7011 parse_execute(&code).await.unwrap();
7012 }
7013
7014 #[tokio::test(flavor = "multi_thread")]
7015 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
7016 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
7017 parse_execute(&code).await.unwrap();
7018 }
7019
7020 #[tokio::test(flavor = "multi_thread")]
7021 async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
7022 let code = r#"startX = 2
7023
7024baseSketch = sketch(on = XY) {
7025 yoyo = line(start = [startX, 0], end = [7, 6])
7026 line2 = line(start = [7, 6], end = [7, 12])
7027 hi = line(start = [7, 12], end = [startX, 0])
7028}
7029
7030baseRegion = region(point = [5.5, 6], sketch = baseSketch)
7031myExtrude = extrude(
7032 baseRegion,
7033 length = 5,
7034 tagEnd = $endCap,
7035 tagStart = $startCap,
7036)
7037yodawg = getCommonEdge(faces = [
7038 baseRegion.tags.hi,
7039 baseRegion.tags.yoyo
7040])
7041
7042cutSketch = sketch(on = YZ) {
7043 myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
7044 myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
7045 line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
7046}
7047
7048cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
7049extrude001 = extrude(cutRegion, length = 5)
7050solid001 = subtract(myExtrude, tools = extrude001)
7051
7052yoyo = getOppositeEdge(baseRegion.tags.hi)
7053fillet(solid001, radius = 0.1, tags = yoyo)
7054"#;
7055
7056 parse_execute(code).await.unwrap();
7057 }
7058
7059 async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
7064 let program = crate::Program::parse_no_errs(kcl).unwrap();
7065 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7066 let mut exec_state = ExecState::new(&ctx);
7067 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
7068 let outcome = exec_state
7069 .into_exec_outcome(env_ref, &ctx)
7070 .await
7071 .expect("constraint report test outcome should collect variables");
7072 let report = outcome.sketch_constraint_report();
7073 ctx.close().await;
7074 report
7075 }
7076
7077 #[tokio::test(flavor = "multi_thread")]
7078 async fn warn_when_sketch_is_over_constrained() {
7079 let code = r#"
7080sketch001 = sketch(on = XY) {
7081 line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
7082 fixed([line1.start, ORIGIN])
7083 fixed([line1.start, [20, 20]])
7084}
7085"#;
7086 let result = parse_execute(code).await.unwrap();
7087 let issues = result.exec_state.issues();
7088 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
7089 panic!("expected over-constrained warning; found {issues:#?}");
7090 };
7091 assert_eq!(warning.severity, Severity::Warning);
7092 }
7093
7094 #[tokio::test(flavor = "multi_thread")]
7095 async fn over_constrained_warning_identifies_signed_vertical_distance_direction() {
7096 let code = r#"
7097sketch001 = sketch(on = XY) {
7098 line1 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
7099 fixed([line1.start, [0mm, 10mm]])
7100 fixed([line1.end, ORIGIN])
7101 verticalDistance([line1.start, line1.end]) == 10mm
7102}
7103"#;
7104 let result = parse_execute(code).await.unwrap();
7105 let issues = result.exec_state.issues();
7106 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
7107 panic!("expected over-constrained warning; found {issues:#?}");
7108 };
7109 assert!(
7110 warning.message.contains(
7111 "Unsatisfied signed verticalDistance constraint: a positive right-hand side requires the second point to be above the first"
7112 ),
7113 "expected signed-direction diagnostic; found {warning:#?}"
7114 );
7115 }
7116
7117 #[tokio::test(flavor = "multi_thread")]
7118 async fn no_warning_when_sketch_is_not_over_constrained() {
7119 let code = r#"
7121sketch001 = sketch(on = XY) {
7122 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7123}
7124"#;
7125 let result = parse_execute(code).await.unwrap();
7126 let issues = result.exec_state.issues();
7127 assert!(
7128 !issues.iter().any(|issue| issue.message.contains("over-constrained")),
7129 "did not expect over-constrained warning; found {issues:#?}"
7130 );
7131 }
7132
7133 #[tokio::test(flavor = "multi_thread")]
7134 async fn test_constraint_report_fully_constrained() {
7135 let kcl = r#"
7137@settings(experimentalFeatures = allow)
7138
7139sketch(on = YZ) {
7140 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
7141 line1.start.at[0] == 2
7142 line1.start.at[1] == 8
7143 line1.end.at[0] == 5
7144 line1.end.at[1] == 7
7145}
7146"#;
7147 let report = run_constraint_report(kcl).await;
7148 assert_eq!(report.fully_constrained.len(), 1);
7149 assert_eq!(report.under_constrained.len(), 0);
7150 assert_eq!(report.over_constrained.len(), 0);
7151 assert_eq!(report.errors.len(), 0);
7152 assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
7153 }
7154
7155 #[tokio::test(flavor = "multi_thread")]
7156 async fn test_constraint_report_under_constrained() {
7157 let kcl = r#"
7159sketch(on = YZ) {
7160 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
7161}
7162"#;
7163 let report = run_constraint_report(kcl).await;
7164 assert_eq!(report.fully_constrained.len(), 0);
7165 assert_eq!(report.under_constrained.len(), 1);
7166 assert_eq!(report.over_constrained.len(), 0);
7167 assert_eq!(report.errors.len(), 0);
7168 assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
7169 assert!(report.under_constrained[0].free_count > 0);
7170 }
7171
7172 #[tokio::test(flavor = "multi_thread")]
7173 async fn test_constraint_report_over_constrained() {
7174 let kcl = r#"
7176@settings(experimentalFeatures = allow)
7177
7178sketch(on = YZ) {
7179 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
7180 line1.start.at[0] == 2
7181 line1.start.at[1] == 8
7182 line1.end.at[0] == 5
7183 line1.end.at[1] == 7
7184 distance([line1.start, line1.end]) == 100mm
7185}
7186"#;
7187 let report = run_constraint_report(kcl).await;
7188 assert_eq!(report.over_constrained.len(), 1);
7189 assert_eq!(report.errors.len(), 0);
7190 assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
7191 assert!(report.over_constrained[0].conflict_count > 0);
7192 }
7193
7194 #[tokio::test(flavor = "multi_thread")]
7195 async fn test_constraint_report_multiple_sketches() {
7196 let kcl = r#"
7198@settings(experimentalFeatures = allow)
7199
7200s1 = sketch(on = YZ) {
7201 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
7202 line1.start.at[0] == 2
7203 line1.start.at[1] == 8
7204 line1.end.at[0] == 5
7205 line1.end.at[1] == 7
7206}
7207
7208s2 = sketch(on = XZ) {
7209 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7210}
7211"#;
7212 let report = run_constraint_report(kcl).await;
7213 assert_eq!(
7214 report.fully_constrained.len()
7215 + report.under_constrained.len()
7216 + report.over_constrained.len()
7217 + report.errors.len(),
7218 2,
7219 "Expected 2 sketches total"
7220 );
7221 assert_eq!(report.fully_constrained.len(), 1);
7222 assert_eq!(report.under_constrained.len(), 1);
7223 }
7224
7225 #[tokio::test(flavor = "multi_thread")]
7226 async fn test_constraint_report_reports_sketch_names() {
7227 let kcl = r#"
7232@settings(experimentalFeatures = allow)
7233
7234fixedSketch = sketch(on = YZ) {
7235 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
7236 line1.start.at[0] == 2
7237 line1.start.at[1] == 8
7238 line1.end.at[0] == 5
7239 line1.end.at[1] == 7
7240}
7241
7242looseSketch = sketch(on = XZ) {
7243 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7244}
7245
7246conflictSketch = sketch(on = XY) {
7247 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
7248 line1.start.at[0] == 2
7249 line1.start.at[1] == 8
7250 line1.end.at[0] == 5
7251 line1.end.at[1] == 7
7252 distance([line1.start, line1.end]) == 100mm
7253}
7254"#;
7255 let report = run_constraint_report(kcl).await;
7256 assert_eq!(report.errors.len(), 0);
7257 assert_eq!(report.fully_constrained.len(), 1);
7258 assert_eq!(report.under_constrained.len(), 1);
7259 assert_eq!(report.over_constrained.len(), 1);
7260 assert_eq!(report.fully_constrained[0].name, "fixedSketch");
7261 assert_eq!(report.under_constrained[0].name, "looseSketch");
7262 assert_eq!(report.over_constrained[0].name, "conflictSketch");
7263 }
7264
7265 #[tokio::test(flavor = "multi_thread")]
7266 async fn test_constraint_report_name_empty_without_declaration() {
7267 let kcl = r#"
7271sketch(on = YZ) {
7272 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
7273}
7274"#;
7275 let report = run_constraint_report(kcl).await;
7276 assert_eq!(report.under_constrained.len(), 1);
7277 assert_eq!(report.under_constrained[0].name, "");
7278 }
7279
7280 #[tokio::test(flavor = "multi_thread")]
7281 async fn test_constraint_report_names_repeat_across_calls() {
7282 let kcl = r#"
7287fn makeSketch() {
7288 inner = sketch(on = XY) {
7289 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7290 }
7291 return inner
7292}
7293
7294first = makeSketch()
7295second = makeSketch()
7296"#;
7297 let report = run_constraint_report(kcl).await;
7298 assert_eq!(report.under_constrained.len(), 2);
7299 assert_eq!(report.under_constrained[0].name, "inner");
7300 assert_eq!(report.under_constrained[1].name, "inner");
7301 }
7302
7303 #[tokio::test(flavor = "multi_thread")]
7304 async fn test_enum_declaration_is_experimental() {
7305 let code = "type Color { | Red }";
7308 assert_eq!(
7309 parse_execute(code).await.unwrap_err().message(),
7310 "Use of enum declarations is experimental and may change or be removed."
7311 );
7312 }
7313
7314 #[tokio::test(flavor = "multi_thread")]
7315 async fn enum_declaration_registers_type() {
7316 let code = r#"@settings(experimentalFeatures = allow)
7320type Color { | Red | Green }
7321"#;
7322 parse_execute(code).await.unwrap();
7323
7324 let code = r#"@settings(experimentalFeatures = allow)
7325export type Color { | Red | Green }
7326"#;
7327 parse_execute(code).await.unwrap();
7328
7329 let code = r#"@settings(experimentalFeatures = allow)
7331type Empty { | }
7332"#;
7333 parse_execute(code).await.unwrap();
7334 }
7335
7336 #[tokio::test(flavor = "multi_thread")]
7337 async fn enum_declaration_rejects_nested_scope() {
7338 let allow = "@settings(experimentalFeatures = allow)\n";
7345 for (case, code) in [
7346 (
7347 "function body",
7348 format!("{allow}fn palette() {{\n type Color {{ | Red }}\n return 0\n}}\npalette()\n"),
7349 ),
7350 (
7351 "sketch block",
7352 format!(
7353 "{allow}sketch(on = XY) {{\n type Color {{ | Red }}\n l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
7354 ),
7355 ),
7356 (
7357 "if arm",
7358 format!("{allow}x = if true {{\n type Color {{ | Red }}\n 0\n}} else {{\n 0\n}}\n"),
7359 ),
7360 ] {
7361 assert_eq!(
7362 parse_execute(&code).await.unwrap_err().message(),
7363 "Enum declarations are only supported at the top-level of a file. Move `type Color` to the top-level.",
7364 "case: {case}"
7365 );
7366 }
7367 }
7368
7369 #[tokio::test(flavor = "multi_thread")]
7370 async fn enum_alone_is_restricted_to_top_level() {
7371 let allow = "@settings(experimentalFeatures = allow)\n";
7378 for (case, code) in [
7379 (
7380 "function body",
7381 format!("{allow}fn f() {{\n type Temperature = number(_)\n return 0\n}}\nx = f()\n"),
7382 ),
7383 (
7384 "sketch block",
7385 format!(
7386 "{allow}sketch(on = XY) {{\n type Temperature = number(_)\n l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
7387 ),
7388 ),
7389 ] {
7390 parse_execute(&code)
7391 .await
7392 .unwrap_or_else(|err| panic!("a type alias should be allowed in a {case}: {}", err.message()));
7393 }
7394 }
7395
7396 #[tokio::test(flavor = "multi_thread")]
7397 async fn enum_declaration_rejects_duplicate() {
7398 let code = r#"@settings(experimentalFeatures = allow)
7399type Color { | Red | Green | Red }
7400"#;
7401 assert_eq!(
7402 parse_execute(code).await.unwrap_err().message(),
7403 "Duplicate variant `Red` in enum `Color`."
7404 );
7405 }
7406
7407 async fn execute_with_modules(main: &str, modules: &[(&str, &str)]) -> Result<ExecTestResults, KclError> {
7409 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_enum_clash").unwrap();
7410 for (name, source) in modules {
7411 tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
7412 }
7413
7414 parse_execute_with_project_dir(main, Some(crate::TypedPath(tmpdir.path().into()))).await
7415 }
7416
7417 async fn issues_with_empty_module(main: &str) -> Vec<crate::errors::CompilationIssue> {
7427 use futures::FutureExt;
7428
7429 let project_dir = crate::TypedPath::new("/zma-kcl-member-ranges");
7430 let files = [(project_dir.join("m.kcl").to_string(), Vec::new())]
7433 .into_iter()
7434 .collect();
7435
7436 let program = crate::Program::parse_no_errs(main).unwrap();
7437 let ctx = ExecutorContext {
7438 engine: Arc::new(EngineManager::new_mock()),
7439 engine_batch: EngineBatchContext::default(),
7440 fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
7441 settings: ExecutorSettings {
7442 project_directory: Some(project_dir),
7443 ..Default::default()
7444 },
7445 context_type: ContextType::Mock,
7446 execution_callbacks: Default::default(),
7447 executor_kind: machine::ExecutorKind::resolve(),
7448 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
7449 };
7450 let mut exec_state = ExecState::new(&ctx);
7451 let run_result = std::panic::AssertUnwindSafe(ctx.run(&program, &mut exec_state))
7455 .catch_unwind()
7456 .await;
7457 ctx.close().await;
7458 if let Err(panic) = run_result {
7459 std::panic::resume_unwind(panic);
7460 }
7461 exec_state.issues().to_vec()
7462 }
7463
7464 #[tokio::test(flavor = "multi_thread")]
7465 async fn member_object_diagnostics_use_object_range() {
7466 for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
7472 let main = format!("{header}import \"m.kcl\" as m\nx = m.field\n");
7473 let issues = issues_with_empty_module(&main).await;
7474 let warning = issues
7475 .iter()
7476 .find(|issue| issue.message.contains("no return value"))
7477 .expect("missing-return warning should be recorded");
7478 let object_start = main.rfind("m.field").unwrap();
7479 assert_eq!(
7480 (warning.source_range.start(), warning.source_range.end()),
7481 (object_start, object_start + 1),
7482 "warning should point at the object's span (header={header:?})"
7483 );
7484 }
7485 }
7486
7487 #[tokio::test(flavor = "multi_thread")]
7488 async fn member_property_diagnostics_use_property_range() {
7489 for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
7492 let main = format!("{header}import \"m.kcl\" as m\narr = [1]\nx = arr[m]\n");
7493 let issues = issues_with_empty_module(&main).await;
7494 let warning = issues
7495 .iter()
7496 .find(|issue| issue.message.contains("no return value"))
7497 .expect("missing-return warning should be recorded");
7498 let prop_start = main.rfind("[m]").unwrap() + 1;
7499 assert_eq!(
7500 (warning.source_range.start(), warning.source_range.end()),
7501 (prop_start, prop_start + 1),
7502 "warning should point at the property's span (header={header:?})"
7503 );
7504 }
7505 }
7506
7507 #[tokio::test(flavor = "multi_thread")]
7508 async fn backtrace_reports_fully_qualified_fn_names() {
7509 let main = "import \"m.kcl\" as m\nx = m::f()\n";
7513 let modules = [("m.kcl", "export fn f() {\n return undefinedVariable\n}\n")];
7514 let err = execute_with_modules(main, &modules).await.unwrap_err();
7515 let fn_names: Vec<_> = err.backtrace().into_iter().filter_map(|item| item.fn_name).collect();
7516 assert_eq!(fn_names, vec!["m::f".to_owned()]);
7517 }
7518
7519 #[tokio::test(flavor = "multi_thread")]
7520 async fn whole_module_name_executes_as_operand() {
7521 let main = r#"import "m.kcl" as m
7525sum = m + m
7526neg = -m
7527"#;
7528 let result = execute_with_modules(main, &[("m.kcl", "42\n")]).await.unwrap();
7529 assert_eq!(
7530 mem_get_json(result.exec_state.stack(), result.mem_env, "sum").as_f64(),
7531 Some(84.0)
7532 );
7533 assert_eq!(
7534 mem_get_json(result.exec_state.stack(), result.mem_env, "neg").as_f64(),
7535 Some(-42.0)
7536 );
7537 }
7538
7539 #[tokio::test(flavor = "multi_thread")]
7540 async fn whole_module_without_return_as_operand_errors() {
7541 let main = "import \"m.kcl\" as m
7546x = m + 1
7547";
7548 let err = execute_with_modules(main, &[("m.kcl", "")]).await.unwrap_err();
7549 assert!(
7550 err.message().contains("Expected a number, but found none"),
7551 "expected the operand to be the module's missing-return KclNone, got: {}",
7552 err.message()
7553 );
7554 }
7555
7556 #[tokio::test(flavor = "multi_thread")]
7557 async fn enum_rejects_name_clash_with_module() {
7558 let plain_module = ("Color.kcl", "export x = 1\n");
7564 let enum_module = (
7565 "enums.kcl",
7566 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
7567 );
7568
7569 for (case, main, modules) in [
7570 (
7571 "module then enum",
7572 "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\ntype Color { | Red }\n",
7573 vec![plain_module],
7574 ),
7575 (
7576 "enum then module",
7577 "@settings(experimentalFeatures = allow)\ntype Color { | Red }\nimport \"Color.kcl\"\n",
7578 vec![plain_module],
7579 ),
7580 (
7581 "named import of an enum",
7582 "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport Color from 'enums.kcl'\n",
7583 vec![plain_module, enum_module],
7584 ),
7585 (
7586 "glob import of an enum",
7587 "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport * from 'enums.kcl'\n",
7588 vec![plain_module, enum_module],
7589 ),
7590 (
7591 "module then enum alias",
7592 "@settings(experimentalFeatures = allow)\ntype Base { | Red }\nimport \"Color.kcl\"\ntype Color = Base\n",
7593 vec![plain_module],
7594 ),
7595 (
7596 "enum alias then module",
7597 "@settings(experimentalFeatures = allow)\ntype Base { | Red }\ntype Color = Base\nimport \"Color.kcl\"\n",
7598 vec![plain_module],
7599 ),
7600 ] {
7601 let err = execute_with_modules(main, &modules).await.unwrap_err();
7602 assert_eq!(
7603 err.message(),
7604 "An enum and a module cannot share the name `Color` in the same scope, because `Color::x` would be ambiguous. Rename one of them.",
7605 "case: {case}"
7606 );
7607 }
7608 }
7609
7610 #[tokio::test(flavor = "multi_thread")]
7611 async fn enum_alias_can_shadow_module_from_outer_scope() {
7612 let main = r#"@settings(experimentalFeatures = allow)
7613type Color { | Red }
7614import "Shade.kcl"
7615
7616fn pick(): Color {
7617 type Shade = Color
7618 return Shade::Red
7619}
7620
7621result = pick()
7622"#;
7623 let result = execute_with_modules(main, &[("Shade.kcl", "export value = 1\n")])
7624 .await
7625 .unwrap();
7626 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "result") else {
7627 panic!("`result` should hold an enum value");
7628 };
7629 assert_eq!(value.qualified_name(), "Color::Red");
7630 }
7631
7632 #[tokio::test(flavor = "multi_thread")]
7633 async fn enum_constructs_variant() {
7634 let allow = "@settings(experimentalFeatures = allow)\n";
7635 let colors = (
7636 "colors.kcl",
7637 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
7638 );
7639
7640 for (case, main, modules) in [
7641 (
7642 "declared locally",
7643 format!("{allow}type Color {{ | Red | Green }}\nx = Color::Red\n"),
7644 vec![],
7645 ),
7646 (
7647 "reached through a module path",
7650 format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
7651 vec![colors],
7652 ),
7653 (
7654 "imported by name",
7655 format!("{allow}import Color from 'colors.kcl'\nx = Color::Red\n"),
7656 vec![colors],
7657 ),
7658 (
7659 "imported under an alias",
7662 format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade::Red\n"),
7663 vec![colors],
7664 ),
7665 ] {
7666 let result = execute_with_modules(&main, &modules)
7667 .await
7668 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
7669 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
7670 panic!("case: {case}: `x` should hold an enum value");
7671 };
7672 assert_eq!(value.qualified_name(), "Color::Red", "case: {case}");
7673 }
7674 }
7675
7676 #[tokio::test(flavor = "multi_thread")]
7677 async fn enum_aliases_preserve_the_original_declaration() {
7678 let code = r#"@settings(experimentalFeatures = allow)
7679type Color { | Red | Green }
7680type C = Color
7681type D = C
7682
7683original = Color::Red
7684directAlias = C::Red
7685chainedAlias = D::Red
7686directEqualsOriginal = directAlias == original
7687chainEqualsOriginal = chainedAlias == original
7688
7689fn passThroughAlias(@color: C): Color {
7690 return color
7691}
7692
7693passed = passThroughAlias(D::Green)
7694"#;
7695
7696 let result = parse_execute(code).await.unwrap();
7697 let memory = result.exec_state.stack();
7698
7699 let KclValue::Type {
7700 value: TypeDef::Enum(original_def),
7701 ..
7702 } = mem_get_json(memory, result.mem_env, &format!("{}Color", memory::TYPE_PREFIX))
7703 else {
7704 panic!("`Color` should hold an enum definition");
7705 };
7706 for alias in ["C", "D"] {
7707 let KclValue::Type {
7708 value: TypeDef::Enum(alias_def),
7709 ..
7710 } = mem_get_json(memory, result.mem_env, &format!("{}{alias}", memory::TYPE_PREFIX))
7711 else {
7712 panic!("`{alias}` should hold an enum definition");
7713 };
7714 assert!(Arc::ptr_eq(&original_def, &alias_def), "alias: {alias}");
7715 }
7716
7717 for name in ["directEqualsOriginal", "chainEqualsOriginal"] {
7718 let KclValue::Bool { value, .. } = mem_get_json(memory, result.mem_env, name) else {
7719 panic!("`{name}` should hold a boolean");
7720 };
7721 assert!(value, "comparison: {name}");
7722 }
7723
7724 let KclValue::Enum { value: passed } = mem_get_json(memory, result.mem_env, "passed") else {
7725 panic!("`passed` should hold an enum value");
7726 };
7727 assert_eq!(passed.enum_id(), original_def.id());
7728 assert_eq!(passed.variant(), "Green");
7729 }
7730
7731 #[tokio::test(flavor = "multi_thread")]
7732 async fn enum_aliases_survive_qualified_imports_and_reexports() {
7733 let main = r#"@settings(experimentalFeatures = allow)
7734import "colors.kcl"
7735import "aliases.kcl"
7736import "tones.kcl"
7737import Paint as Finish from "aliases.kcl"
7738import * from "aliases.kcl"
7739
7740original = colors::Color::Red
7741qualifiedAlias = aliases::Paint::Red
7742namedImportAlias = Finish::Green
7743globImportAlias = Paint::Red
7744namedTargetAlias = tones::Tint::Green
7745aliasesEqualOriginal = qualifiedAlias == original
7746
7747fn throughAlias(@color: Finish): colors::Color {
7748 return color
7749}
7750
7751fn throughOriginal(@color: colors::Color): Finish {
7752 return color
7753}
7754
7755fromAlias = throughAlias(Finish::Green)
7756originalIntoAlias = throughAlias(colors::Color::Green)
7757fromOriginal = throughOriginal(colors::Color::Red)
7758"#;
7759 let modules = [
7760 (
7761 "colors.kcl",
7762 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
7763 ),
7764 (
7765 "palette.kcl",
7766 "@settings(experimentalFeatures = allow)\nimport \"colors.kcl\" as swatches\nexport type Shade = swatches::Color\n",
7767 ),
7768 (
7769 "aliases.kcl",
7770 "@settings(experimentalFeatures = allow)\nimport \"palette.kcl\"\nexport type Paint = palette::Shade\n",
7771 ),
7772 (
7773 "tones.kcl",
7774 "@settings(experimentalFeatures = allow)\nimport Color from \"colors.kcl\"\nexport type Tint = Color\n",
7775 ),
7776 ];
7777
7778 let result = execute_with_modules(main, &modules).await.unwrap();
7779 let memory = result.exec_state.stack();
7780 let KclValue::Enum { value: original } = mem_get_json(memory, result.mem_env, "original") else {
7781 panic!("`original` should hold an enum value");
7782 };
7783 let original_id = original.enum_id();
7784
7785 for (name, variant) in [
7786 ("qualifiedAlias", "Red"),
7787 ("namedImportAlias", "Green"),
7788 ("globImportAlias", "Red"),
7789 ("namedTargetAlias", "Green"),
7790 ("fromAlias", "Green"),
7791 ("originalIntoAlias", "Green"),
7792 ("fromOriginal", "Red"),
7793 ] {
7794 let KclValue::Enum { value } = mem_get_json(memory, result.mem_env, name) else {
7795 panic!("`{name}` should hold an enum value");
7796 };
7797 assert_eq!(value.enum_id(), original_id, "value: {name}");
7798 assert_eq!(value.variant(), variant, "value: {name}");
7799 }
7800
7801 let KclValue::Bool { value, .. } = mem_get_json(memory, result.mem_env, "aliasesEqualOriginal") else {
7802 panic!("`aliasesEqualOriginal` should hold a boolean");
7803 };
7804 assert!(value);
7805
7806 let KclValue::Type {
7807 value: TypeDef::Enum(finish_def),
7808 ..
7809 } = mem_get_json(memory, result.mem_env, &format!("{}Finish", memory::TYPE_PREFIX))
7810 else {
7811 panic!("`Finish` should hold an enum definition");
7812 };
7813 assert_eq!(finish_def.id(), original_id);
7814 }
7815
7816 #[tokio::test(flavor = "multi_thread")]
7824 async fn signature_types_resolve_in_declaring_module() {
7825 let colors = (
7826 "colors.kcl",
7827 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n return c\n}\n",
7828 );
7829 let main =
7832 "@settings(experimentalFeatures = allow)\nimport \"colors.kcl\"\nr = colors::paint(colors::Color::Red)\n";
7833
7834 let result = execute_with_modules(main, &[colors]).await.unwrap();
7835 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
7836 panic!("`r` should hold an enum value");
7837 };
7838 assert_eq!(value.qualified_name(), "Color::Red");
7839 }
7840
7841 #[tokio::test(flavor = "multi_thread")]
7842 async fn qualified_type_paths_resolve_in_aliases_and_ascriptions() {
7843 let main = r#"@settings(experimentalFeatures = allow)
7844type ViewOrientation = view::Orientation
7845front = view::Orientation::Front: view::Orientation
7846"#;
7847
7848 parse_execute(main).await.unwrap();
7849 }
7850
7851 #[tokio::test(flavor = "multi_thread")]
7852 async fn unknown_qualified_type_reports_the_written_name() {
7853 let main = "fn f(@value: missing::Orientation) {}\n";
7854
7855 let err = parse_execute(main).await.unwrap_err();
7856 assert_eq!(err.message(), "Unknown type: missing::Orientation");
7857 }
7858
7859 #[tokio::test(flavor = "multi_thread")]
7860 async fn signature_types_resolve_under_import_alias() {
7861 let colors = (
7865 "colors.kcl",
7866 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n return c\n}\n",
7867 );
7868 let main = "@settings(experimentalFeatures = allow)\nimport \"colors.kcl\" as painter\nr = painter::paint(painter::Color::Red)\n";
7869
7870 let result = execute_with_modules(main, &[colors]).await.unwrap();
7871 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
7872 panic!("`r` should hold an enum value");
7873 };
7874 assert_eq!(value.qualified_name(), "Color::Red");
7875 }
7876
7877 #[tokio::test(flavor = "multi_thread")]
7878 async fn signature_types_ignore_caller_scope() {
7879 let broken = (
7884 "broken.kcl",
7885 "@settings(experimentalFeatures = allow)\nexport fn f(@x: Missing) {\n return x\n}\n",
7886 );
7887 let main = "@settings(experimentalFeatures = allow)\ntype Missing = string\nimport \"broken.kcl\"\nr = broken::f(\"hi\")\n";
7888
7889 let err = execute_with_modules(main, &[broken]).await.unwrap_err();
7890 assert!(
7891 err.message().contains("Unknown type: Missing"),
7892 "message: {}",
7893 err.message()
7894 );
7895 }
7896
7897 #[tokio::test(flavor = "multi_thread")]
7898 async fn signature_types_reject_forward_reference() {
7899 let main = "@settings(experimentalFeatures = allow)\nfn f(@x: Later) {\n return x\n}\ntype Later = string\n";
7903
7904 let err = parse_execute(main).await.unwrap_err();
7905 assert!(
7906 err.message().contains("Unknown type: Later"),
7907 "message: {}",
7908 err.message()
7909 );
7910 }
7911
7912 #[tokio::test(flavor = "multi_thread")]
7913 async fn signature_types_resolve_in_enclosing_scope() {
7914 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";
7919
7920 let result = parse_execute(main).await.unwrap();
7921 let KclValue::Number { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
7922 panic!("`r` should hold a number");
7923 };
7924 assert_eq!(value, 42.0);
7925 }
7926
7927 #[tokio::test(flavor = "multi_thread")]
7937 async fn signature_number_types_ignore_module_default_units() {
7938 let units_in = (
7939 "units_in.kcl",
7940 "@settings(defaultLengthUnit = in)\nexport fn passThrough(@x: number(Length)) {\n return x\n}\n",
7941 );
7942 let main = "import \"units_in.kcl\"\na = units_in::passThrough(42)\nb = units_in::passThrough(42mm)\nc = units_in::passThrough(42in)\n";
7945
7946 let result = execute_with_modules(main, &[units_in]).await.unwrap();
7947 for (name, expected_ty) in [
7948 (
7958 "a",
7959 kcl_api::NumericType::Default {
7960 len: kcl_api::UnitLength::Millimeters,
7961 angle: kcl_api::UnitAngle::Degrees,
7962 },
7963 ),
7964 (
7965 "b",
7966 kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Millimeters)),
7967 ),
7968 (
7969 "c",
7970 kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Inches)),
7971 ),
7972 ] {
7973 let KclValue::Number { value, ty, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name)
7974 else {
7975 panic!("`{name}` should hold a number");
7976 };
7977 assert_eq!(value, 42.0, "`{name}` should keep its magnitude");
7978 assert_eq!(ty, expected_ty, "`{name}` should keep the caller-side unit context");
7979 }
7980 }
7981
7982 #[tokio::test(flavor = "multi_thread")]
7990 async fn signature_types_use_declaring_scope_when_both_scopes_define_the_name() {
7991 let m1 = (
7992 "m1.kcl",
7993 "@settings(experimentalFeatures = allow)\ntype A = string\n\nexport fn test(@a: A) {\n return a\n}\n",
7994 );
7995 let main =
7996 "@settings(experimentalFeatures = allow)\nimport * from \"m1.kcl\"\ntype A = number(mm)\nx = test(2mm)\n";
7997
7998 let err = execute_with_modules(main, &[m1]).await.unwrap_err();
7999 assert_eq!(
8000 err.message(),
8001 "The input argument of `test` requires a value with type `A`, but found a number (mm) (with type `number(mm)`)."
8002 );
8003 }
8004
8005 #[tokio::test(flavor = "multi_thread")]
8006 async fn enum_rejects_bad_variant_paths() {
8007 let allow = "@settings(experimentalFeatures = allow)\n";
8008
8009 for (case, main, modules, message) in [
8010 (
8011 "unknown variant",
8012 format!("{allow}type Color {{ | Red | Green }}\nx = Color::Blue\n"),
8013 vec![],
8014 "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
8015 ),
8016 (
8017 "unknown variant through an alias",
8018 format!("{allow}type Color {{ | Red | Green }}\ntype Paint = Color\nx = Paint::Blue\n"),
8019 vec![],
8020 "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
8021 ),
8022 (
8023 "enum with no variants",
8024 format!("{allow}type Empty {{ | }}\nx = Empty::Red\n"),
8025 vec![],
8026 "`Red` is not a variant of enum `Empty`. Enum `Empty` has no variants.",
8027 ),
8028 (
8029 "path continues past the enum",
8030 format!("{allow}type Color {{ | Red }}\nx = Color::Red::more\n"),
8031 vec![],
8032 "`Color` is an enum, so only a variant name can follow it. There is nothing to reach through `Color::Red`.",
8033 ),
8034 (
8035 "variant name is case sensitive",
8036 format!("{allow}type Color {{ | Red }}\nx = Color::red\n"),
8037 vec![],
8038 "`red` is not a variant of enum `Color`. Its variants are: Red.",
8039 ),
8040 (
8041 "enum not exported from its module",
8042 format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
8043 vec![(
8044 "colors.kcl",
8045 "@settings(experimentalFeatures = allow)\ntype Color { | Red }\n",
8046 )],
8047 "Item Color not found in module's exported items",
8048 ),
8049 (
8050 "a non-enum type alias cannot head a path",
8051 format!("{allow}type T = number(_)\nx = T::foo\n"),
8052 vec![],
8053 "`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
8054 ),
8055 (
8056 "a chained non-enum type alias cannot head a path",
8057 format!("{allow}type T = number(_)\ntype U = T\nx = U::foo\n"),
8058 vec![],
8059 "`U` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
8060 ),
8061 (
8062 "a qualified non-enum type alias cannot head a path",
8063 format!("{allow}import \"types.kcl\"\nx = types::T::foo\n"),
8064 vec![(
8065 "types.kcl",
8066 "@settings(experimentalFeatures = allow)\nexport type T = number(_)\n",
8067 )],
8068 "`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
8069 ),
8070 (
8071 "an alias containing an enum is not an enum alias",
8072 format!("{allow}type Color {{ | Red }}\ntype T = Color | string\nx = T::Red\n"),
8073 vec![],
8074 "`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
8075 ),
8076 (
8077 "a value cannot head a path",
8080 "Color = 5\nx = Color::Red\n".to_owned(),
8081 vec![],
8082 "`Color` is not defined",
8083 ),
8084 ] {
8085 let err = execute_with_modules(&main, &modules).await.unwrap_err();
8086 assert_eq!(err.message(), message, "case: {case}");
8087 }
8088 }
8089
8090 #[tokio::test(flavor = "multi_thread")]
8091 async fn enum_compares_by_variant() {
8092 let code = r#"@settings(experimentalFeatures = allow)
8093type Color { | Red | Green }
8094sameEq = Color::Red == Color::Red
8095sameNeq = Color::Red != Color::Red
8096otherEq = Color::Red == Color::Green
8097otherNeq = Color::Red != Color::Green
8098"#;
8099 let result = parse_execute(code).await.unwrap();
8100
8101 for (name, expected) in [
8102 ("sameEq", true),
8103 ("sameNeq", false),
8104 ("otherEq", false),
8105 ("otherNeq", true),
8106 ] {
8107 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
8108 panic!("`{name}` should hold a bool");
8109 };
8110 assert_eq!(value, expected, "variable: {name}");
8111 }
8112 }
8113
8114 #[tokio::test(flavor = "multi_thread")]
8115 async fn enum_usable_inside_sketch_block() {
8116 let code = r#"@settings(experimentalFeatures = allow)
8125type Color { | Red | Green }
8126sketch(on = XY) {
8127 c = Color::Red
8128 assertIs(Color::Red != Color::Green)
8129 assertIs(!(Color::Red != Color::Red))
8130 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
8131}
8132"#;
8133 parse_execute(code)
8134 .await
8135 .unwrap_or_else(|err| panic!("enum use inside a sketch block should work: {}", err.message()));
8136 }
8137
8138 #[tokio::test(flavor = "multi_thread")]
8139 async fn enum_eq_reserved_inside_sketch_block() {
8140 let allow = "@settings(experimentalFeatures = allow)\n";
8150 let tail = " l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}\n";
8151 for (case, declaration, comparison, types) in [
8152 (
8153 "enums",
8154 "type Color { | Red | Green }\n",
8155 "Color::Red == Color::Green",
8156 "a value of enum `Color` and a value of enum `Color`",
8157 ),
8158 ("strings", "", "\"a\" == \"b\"", "a string and a string"),
8159 ("numbers", "", "1 == 2", "a number and a number"),
8160 ] {
8161 let code = format!("{allow}{declaration}sketch(on = XY) {{\n x = {comparison}\n{tail}");
8162 assert_eq!(
8163 parse_execute(&code).await.unwrap_err().message(),
8164 format!("Cannot create an equivalence constraint between values of these types: {types}"),
8165 "case: {case}"
8166 );
8167 }
8168 }
8169
8170 #[tokio::test(flavor = "multi_thread")]
8171 async fn enum_same_file_imported_twice_is_one_type() {
8172 let main = r#"@settings(experimentalFeatures = allow)
8176import Color as A from 'colors.kcl'
8177import Color as B from 'colors.kcl'
8178x = A::Red == B::Red
8179y = A::Red == B::Green
8180"#;
8181 let result = execute_with_modules(
8182 main,
8183 &[(
8184 "colors.kcl",
8185 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
8186 )],
8187 )
8188 .await
8189 .unwrap();
8190
8191 for (name, expected) in [("x", true), ("y", false)] {
8192 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
8193 panic!("`{name}` should hold a bool");
8194 };
8195 assert_eq!(value, expected, "variable: {name}");
8196 }
8197 }
8198
8199 #[tokio::test(flavor = "multi_thread")]
8200 async fn enum_rejects_comparison_across_types() {
8201 let allow = "@settings(experimentalFeatures = allow)\n";
8202 let color = (
8203 "a.kcl",
8204 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
8205 );
8206 let other_color = (
8207 "b.kcl",
8208 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
8209 );
8210
8211 for (case, main, modules, message) in [
8212 (
8213 "two enums declared separately",
8214 format!("{allow}type Color {{ | Red }}\ntype Shade {{ | Red }}\nx = Color::Red == Shade::Red\n"),
8215 vec![],
8216 "Cannot compare enum `Color` with enum `Shade`. They are different types.",
8217 ),
8218 (
8219 "two enums sharing a name",
8223 format!(
8224 "{allow}import Color as A from 'a.kcl'\nimport Color as B from 'b.kcl'\nx = A::Red == B::Red\n"
8225 ),
8226 vec![color, other_color],
8227 "Cannot compare two different enums that are both named `Color`. They come from separate declarations.",
8228 ),
8229 (
8230 "an enum and a number",
8231 format!("{allow}type Color {{ | Red }}\nx = Color::Red == 5\n"),
8232 vec![],
8233 "Cannot compare enum `Color::Red` with a number.",
8234 ),
8235 (
8236 "a number and an enum, in that order",
8237 format!("{allow}type Color {{ | Red }}\nx = 5 == Color::Red\n"),
8238 vec![],
8239 "Cannot compare enum `Color::Red` with a number.",
8240 ),
8241 (
8242 "an enum and a string",
8243 format!("{allow}type Color {{ | Red }}\nx = Color::Red == \"Red\"\n"),
8244 vec![],
8245 "Cannot compare enum `Color::Red` with a string.",
8246 ),
8247 ] {
8248 let err = execute_with_modules(&main, &modules).await.unwrap_err();
8249 assert_eq!(err.message(), message, "case: {case}");
8250 }
8251 }
8252
8253 #[tokio::test(flavor = "multi_thread")]
8254 async fn enum_rejects_bare_type_name_as_value() {
8255 let allow = "@settings(experimentalFeatures = allow)\n";
8256 let colors = (
8257 "colors.kcl",
8258 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
8259 );
8260
8261 for (case, main, modules, message) in [
8262 (
8263 "enum suggests a variant",
8264 format!("{allow}type Color {{ | Red | Green }}\nx = Color\n"),
8265 vec![],
8266 "`Color` is a type, not a value. Use one of its variants, such as `Color::Red`.",
8267 ),
8268 (
8269 "suggestion uses the import alias",
8272 format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade\n"),
8273 vec![colors],
8274 "`Shade` is a type, not a value. Use one of its variants, such as `Shade::Red`.",
8275 ),
8276 (
8277 "suggestion uses the type alias",
8278 format!("{allow}type Color {{ | Red | Green }}\ntype Paint = Color\nx = Paint\n"),
8279 vec![],
8280 "`Paint` is a type, not a value. Use one of its variants, such as `Paint::Red`.",
8281 ),
8282 (
8283 "enum with no variants suggests nothing",
8284 format!("{allow}type Empty {{ | }}\nx = Empty\n"),
8285 vec![],
8286 "`Empty` is a type, not a value.",
8287 ),
8288 (
8289 "a type alias reports the same way",
8290 format!("{allow}type T = number(_)\nx = T\n"),
8291 vec![],
8292 "`T` is a type, not a value.",
8293 ),
8294 (
8295 "an unknown name is still undefined",
8298 "x = Nope\n".to_owned(),
8299 vec![],
8300 "`Nope` is not defined",
8301 ),
8302 ] {
8303 let err = execute_with_modules(&main, &modules).await.unwrap_err();
8304 assert_eq!(err.message(), message, "case: {case}");
8305 }
8306 }
8307
8308 #[tokio::test(flavor = "multi_thread")]
8309 async fn enum_use_gated_by_consuming_module() {
8310 let colors = (
8319 "colors.kcl",
8320 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
8321 );
8322 let aliases = (
8323 "aliases.kcl",
8324 "@settings(experimentalFeatures = allow)\nimport \"colors.kcl\"\nexport type Shade = colors::Color\n",
8325 );
8326
8327 for (case, main, modules) in [
8328 (
8329 "original binding",
8330 "import \"colors.kcl\"\nx = colors::Color::Red\n",
8331 vec![colors],
8332 ),
8333 (
8334 "re-exported alias",
8335 "import \"aliases.kcl\"\nx = aliases::Shade::Red\n",
8336 vec![colors, aliases],
8337 ),
8338 ] {
8339 let result = execute_with_modules(main, &modules)
8340 .await
8341 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
8342
8343 let issues = &result.exec_state.global.issues;
8344 assert_eq!(issues.len(), 1, "case: {case}: issues: {issues:?}");
8345 assert_eq!(
8346 issues[0].message, "Use of the enum `Color` is experimental and may change or be removed.",
8347 "case: {case}"
8348 );
8349 assert_eq!(issues[0].severity, Severity::Error, "case: {case}");
8350 }
8351 }
8352
8353 #[tokio::test(flavor = "multi_thread")]
8354 async fn enum_use_not_gated_when_consumer_allows_it() {
8355 let code = r#"@settings(experimentalFeatures = allow)
8358type Color { | Red }
8359x = Color::Red
8360"#;
8361 let result = parse_execute(code).await.unwrap();
8362 assert!(
8363 result.exec_state.global.issues.is_empty(),
8364 "issues: {:?}",
8365 result.exec_state.global.issues
8366 );
8367 }
8368
8369 #[tokio::test(flavor = "multi_thread")]
8370 async fn enum_allows_name_sharing_outside_modules() {
8371 for (case, main, modules) in [
8379 (
8380 "an alias may share a name with a module",
8383 "@settings(experimentalFeatures = allow)\ntype Temperature = number(_)\nimport \"Temperature.kcl\"\nx = Temperature::x\n",
8384 vec![("Temperature.kcl", "export x = 1\n")],
8385 ),
8386 (
8387 "a value may share a name with an enum",
8388 "@settings(experimentalFeatures = allow)\ntype Color { | Red }\nColor = 5\n",
8389 vec![],
8390 ),
8391 ] {
8392 if let Err(err) = execute_with_modules(main, &modules).await {
8393 panic!("case: {case}: {}", err.message());
8394 }
8395 }
8396 }
8397
8398 #[tokio::test(flavor = "multi_thread")]
8399 async fn enum_declaration_rejects_redefinition() {
8400 let code = r#"@settings(experimentalFeatures = allow)
8401type Color { | Red }
8402type Color { | Green }
8403"#;
8404 assert_eq!(
8405 parse_execute(code).await.unwrap_err().message(),
8406 "Redefinition of type Color."
8407 );
8408 }
8409
8410 #[tokio::test(flavor = "multi_thread")]
8416 async fn enum_projects_to_string() {
8417 let header = r#"
8418 @settings(experimentalFeatures = allow)
8419 type Color { | Red | Green }
8420 type Label = string
8421 "#;
8422
8423 for (case, body, expected) in [
8424 ("a variant", "x = Color::Red: string", "Red"),
8425 ("another variant of the same enum", "x = Color::Green: string", "Green"),
8426 ("an alias of the target type", "x = Color::Red: Label", "Red"),
8427 (
8428 "an element of a projected array",
8429 r#"
8430 pair = [Color::Red, Color::Green]: [string]
8431 x = pair[1]
8432 "#,
8433 "Green",
8434 ),
8435 (
8436 "an element of a nested projected array",
8437 r#"
8438 grid = [[Color::Green]]: [[string]]
8439 x = grid[0][0]
8440 "#,
8441 "Green",
8442 ),
8443 (
8444 "a one-element array against a bare string",
8445 "x = [Color::Red]: string",
8446 "Red",
8447 ),
8448 ] {
8449 let result = parse_execute(&format!("{header}{body}\n"))
8450 .await
8451 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
8452 let KclValue::String { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
8453 panic!("case: {case}: `x` should hold a string");
8454 };
8455 assert_eq!(value, expected, "case: {case}");
8456 }
8457 }
8458
8459 #[tokio::test(flavor = "multi_thread")]
8463 async fn enum_ascription_keeps_the_enum() {
8464 let header = r#"
8465 @settings(experimentalFeatures = allow)
8466 type Color { | Red | Green }
8467 type Paint = Color
8468 "#;
8469
8470 for (case, expression, expected) in [
8471 ("its own type", "(Color::Red: Color) == Color::Red", true),
8472 ("an alias of its own type", "(Color::Red: Paint) == Color::Red", true),
8473 (
8474 "the ascription does not change which variant it is",
8475 "(Color::Red: Color) == Color::Green",
8476 false,
8477 ),
8478 ] {
8479 let result = parse_execute(&format!("{header}x = {expression}\n"))
8480 .await
8481 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
8482 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
8483 panic!("case: {case}: `x` should hold a bool");
8484 };
8485 assert_eq!(value, expected, "case: {case}");
8486 }
8487 }
8488
8489 #[tokio::test(flavor = "multi_thread")]
8493 async fn enum_projection_is_not_implicit() {
8494 let header = r#"
8495 @settings(experimentalFeatures = allow)
8496 type Color { | Red | Green }
8497 "#;
8498 let found = "but found a value of enum `Color` (with type `Color`).";
8499
8500 for (case, body, expected) in [
8501 (
8502 "unlabeled argument",
8503 r#"
8504 fn label(@text: string) { return text }
8505 x = label(Color::Red)
8506 "#,
8507 format!("The input argument of `label` requires a value with type `string`, {found}"),
8508 ),
8509 (
8510 "labeled argument",
8511 r#"
8512 fn label(text: string) { return text }
8513 x = label(text = Color::Red)
8514 "#,
8515 format!("text requires a value with type `string`, {found}"),
8516 ),
8517 (
8518 "return",
8519 r#"
8520 fn label(): string { return Color::Red }
8521 x = label()
8522 "#,
8523 format!("This function requires its result to be a value with type `string`, {found}"),
8524 ),
8525 (
8526 "inside an array at an argument boundary",
8531 r#"
8532 fn labels(@text: [string]) { return text }
8533 x = labels([Color::Red])
8534 "#,
8535 "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(),
8536 ),
8537 ] {
8538 assert_eq!(
8539 parse_execute(&format!("{header}{body}\n")).await.unwrap_err().message(),
8540 expected,
8541 "case: {case}"
8542 );
8543 }
8544 }
8545
8546 #[tokio::test(flavor = "multi_thread")]
8549 async fn enum_ascription_rejections() {
8550 let header = r#"
8551 @settings(experimentalFeatures = allow)
8552 type Color { | Red }
8553 type Shade { | Red }
8554 "#;
8555 let no_number = "Cannot project enum `Color` to a number. An enum projects to `string`; projecting to a number is not supported yet.";
8556
8557 for (case, expression, expected) in [
8558 ("a number target", "Color::Red: number(_)", no_number.to_owned()),
8559 (
8560 "a number target reached through an array, so the reason survives the walk",
8561 "[Color::Red]: [number(_)]",
8562 no_number.to_owned(),
8563 ),
8564 (
8565 "a boolean target, which is not a projection at all",
8566 "Color::Red: bool",
8567 "could not coerce a value of enum `Color` (with type `Color`) to type `bool`".to_owned(),
8568 ),
8569 (
8570 "another enum whose variants happen to match",
8571 "Color::Red: Shade",
8572 "could not coerce a value of enum `Color` (with type `Color`) to type `Shade`".to_owned(),
8573 ),
8574 ] {
8575 assert_eq!(
8576 parse_execute(&format!("{header}x = {expression}\n"))
8577 .await
8578 .unwrap_err()
8579 .message(),
8580 expected,
8581 "case: {case}"
8582 );
8583 }
8584 }
8585
8586 #[tokio::test(flavor = "multi_thread")]
8593 async fn enum_flows_through_declared_types() {
8594 let header = r#"
8595 @settings(experimentalFeatures = allow)
8596 type Color { | Red | Green }
8597 type Paint = Color
8598 type Shade { | Red }
8599 "#;
8600
8601 for (case, body, expected) in [
8602 (
8603 "an alias parameter accepts the original enum",
8604 r#"
8605 fn paint(@c: Paint) { return c }
8606 x = paint(Color::Red) == Paint::Red
8607 "#,
8608 None,
8609 ),
8610 (
8611 "an unlabeled parameter",
8612 r#"
8613 fn paint(@c: Color) { return c }
8614 x = paint(Color::Red) == Color::Red
8615 "#,
8616 None,
8617 ),
8618 (
8619 "a labeled parameter",
8620 r#"
8621 fn paint(c: Color) { return c }
8622 x = paint(c = Color::Green) == Color::Green
8623 "#,
8624 None,
8625 ),
8626 (
8627 "a declared return type",
8628 r#"
8629 fn pick(): Color { return Color::Red }
8630 x = pick() == Color::Red
8631 "#,
8632 None,
8633 ),
8634 (
8635 "an array parameter",
8636 r#"
8637 fn firstOf(@cs: [Color]) { return cs[0] }
8638 x = firstOf([Color::Red, Color::Green]) == Color::Red
8639 "#,
8640 None,
8641 ),
8642 (
8643 "an object field",
8648 r#"
8649 fn take(@o: { c: Color }) { return o.c }
8650 x = take({ c = Color::Green }) == Color::Green
8651 "#,
8652 None,
8653 ),
8654 (
8655 "a union that names the enum",
8656 r#"
8657 fn either(@v: Color | string) { return v }
8658 x = either(Color::Red) == Color::Red
8659 "#,
8660 None,
8661 ),
8662 (
8663 "the same union given the other member",
8664 r#"
8665 fn either(@v: Color | string) { return v }
8666 x = either("plain") == "plain"
8667 "#,
8668 None,
8669 ),
8670 (
8671 "an alias parameter rejects a different enum",
8672 r#"
8673 fn paint(@c: Paint) { return c }
8674 x = paint(Shade::Red) == Shade::Red
8675 "#,
8676 Some(
8677 "The input argument of `paint` requires a value with type `Paint`, but found a value of enum `Shade` (with type `Shade`).",
8678 ),
8679 ),
8680 (
8681 "another declaration at the same boundary",
8682 r#"
8683 fn paint(@c: Color) { return c }
8684 x = paint(Shade::Red) == Shade::Red
8685 "#,
8686 Some(
8687 "The input argument of `paint` requires a value with type `Color`, but found a value of enum `Shade` (with type `Shade`).",
8688 ),
8689 ),
8690 ] {
8691 let code = format!("{header}{body}\n");
8692 match expected {
8693 None => {
8694 let result = parse_execute(&code)
8695 .await
8696 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
8697 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x")
8698 else {
8699 panic!("case: {case}: `x` should hold a bool");
8700 };
8701 assert!(value, "case: {case}: the value did not survive the boundary");
8702 }
8703 Some(message) => assert_eq!(
8704 parse_execute(&code).await.unwrap_err().message(),
8705 message,
8706 "case: {case}"
8707 ),
8708 }
8709 }
8710 }
8711}