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