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