1use std::collections::BTreeMap;
4use std::future::Future;
5use std::sync::Arc;
6
7use anyhow::Result;
8pub use artifact::ArtifactCommand;
9pub(crate) use artifact::EntityCloneInfo;
10pub(crate) use artifact::named_view_artifact;
11pub(crate) use artifact::sketch_block_constraint_type;
12use cache::GlobalState;
13pub use cache::bust_cache;
14pub use cache::clear_mem_cache;
15use futures::future::BoxFuture;
16pub use geometry::*;
17pub use id_generator::IdGenerator;
18pub(crate) use import::PreImportedGeometry;
19use indexmap::IndexMap;
20pub use kcl_api::DefaultPlanes;
21pub use kcl_api::Operation;
22pub use kcl_api::artifact::Artifact;
23pub use kcl_api::artifact::ArtifactGraph;
24pub use kcl_api::artifact::CapSubType;
25pub use kcl_api::artifact::CodeRef;
26pub use kcl_api::artifact::GdtAnnotationArtifact;
27pub use kcl_api::artifact::SketchBlock;
28pub use kcl_api::artifact::SketchBlockConstraint;
29#[allow(unused_imports)]
30pub use kcl_api::artifact::SketchBlockConstraintType;
31pub use kcl_api::artifact::StartSketchOnFace;
32pub use kcl_api::artifact::StartSketchOnPlane;
33use kcl_api::ast::node_path::NodePath;
34pub use kcl_value::KclObjectFields;
35pub use kcl_value::KclObjectKind;
36pub use kcl_value::KclValue;
37pub use kcl_value_view::EdgeCutViewExt;
38pub use kcl_value_view::ExtrudeSurfaceViewExt;
39pub use kcl_value_view::KclValueView;
40pub use kcl_value_view::PathViewExt;
41pub use kcl_value_view::SolidViewExt;
42use kcmc::ImageFormat;
43use kcmc::ModelingCmd;
44use kcmc::each_cmd as mcmd;
45use kcmc::ok_response::OkModelingCmdResponse;
46use kcmc::ok_response::output::TakeSnapshot;
47use kcmc::websocket::ModelingSessionData;
48use kcmc::websocket::OkWebSocketResponseData;
49use kittycad_modeling_cmds::id::ModelingCmdId;
50use kittycad_modeling_cmds::{self as kcmc};
51pub use memory::EnvironmentRef;
52#[cfg(test)]
53pub(crate) use memory::MemoryBackendKind;
54pub(crate) use modeling::ModelingCmdMeta;
55pub use named_views::*;
56use serde::Deserialize;
57use serde::Serialize;
58pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
59pub(crate) use sketch_solve::solver_numeric_type;
60pub(crate) use solver_arc::SolverArc;
61pub(crate) use state::ConstraintKey;
62pub(crate) use state::ConstraintState;
63pub(crate) use state::ConsumedRegionInfo;
64pub(crate) use state::ConsumedRegionOperation;
65pub(crate) use state::ConsumedSolidInfo;
66pub(crate) use state::ConsumedSolidKey;
67pub(crate) use state::ConsumedSolidOperation;
68pub use state::DirectTagFilletMeta;
69pub use state::DirectTagFilletTagEntry;
70pub use state::EdgeRefactorMeta;
71pub use state::EdgeRefactorStdlibFn;
72pub use state::ExecState;
73pub use state::KclVersion;
74pub use state::LegacyAngleRefactorMeta;
75pub use state::MetaSettings;
76pub(crate) use state::ModuleArtifactState;
77pub use state::NotYetAdded;
78pub(crate) use state::PendingEdgeRefactorMeta;
79pub(crate) use state::PendingLegacyAngleRefactorMeta;
80pub use state::RefactorMetadata;
81pub(crate) use state::TangencyMode;
82pub(crate) use state::declared_kcl_version;
83
84use crate::CompilationIssue;
85use crate::ExecError;
86use crate::KclErrorWithOutputs;
87use crate::NodePathExt;
88use crate::SourceRange;
89use crate::collections::AhashIndexSet;
90use crate::engine::EngineBatchContext;
91use crate::engine::GridScaleBehavior;
92use crate::engine::engine_manager::EngineManager;
93use crate::errors::KclError;
94use crate::errors::KclErrorDetails;
95use crate::execution::cache::CacheInformation;
96use crate::execution::cache::CacheResult;
97use crate::execution::cad_op::OperationExt;
98use crate::execution::import_graph::Universe;
99use crate::execution::import_graph::UniverseMap;
100use crate::execution::typed_path::TypedPath;
101use crate::front::Number;
102use crate::front::Object;
103use crate::front::ObjectId;
104use crate::fs::FileManager;
105use crate::fs::FileSystemHandle;
106use crate::modules::ModuleExecutionOutcome;
107use crate::modules::ModuleId;
108use crate::modules::ModulePath;
109use crate::modules::ModuleRepr;
110use crate::modules::ModuleSource;
111use crate::parsing::ast::types::Expr;
112use crate::parsing::ast::types::ImportPath;
113use crate::parsing::ast::types::NodeRef;
114
115#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
116#[ts(export)]
117pub struct OperationsByModule {
118 pub map: IndexMap<ModuleId, Vec<Operation>>,
119}
120
121#[derive(Clone, Serialize, ts_rs::TS)]
122#[ts(export)]
123#[serde(rename_all = "camelCase")]
124pub struct OperationCallbackArgs {
125 pub module_id: ModuleId,
126 pub operation: Operation,
127 pub index: usize,
128}
129
130pub trait ExecutionCallbacks: std::fmt::Debug + Send + Sync + 'static {
131 fn on_operation(&self, _args: OperationCallbackArgs) {}
132}
133
134impl OperationsByModule {
135 pub fn count(&self) -> usize {
136 self.map.values().map(Vec::len).sum()
137 }
138
139 pub fn is_empty(&self) -> bool {
140 self.map.values().all(Vec::is_empty)
141 }
142
143 pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
144 self.map.get(module_id)
145 }
146
147 pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
148 self.map.values()
149 }
150
151 pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
152 self.map.insert(module_id, operations);
153 }
154}
155
156pub(crate) mod annotations;
157mod artifact;
158#[cfg(test)]
159pub(crate) use artifact::mermaid_tests::ArtifactGraphMermaidExt;
160pub(crate) mod cache;
161mod cad_op;
162pub(crate) mod exec_ast;
163pub mod fn_call;
164#[cfg(test)]
165mod freedom_analysis_tests;
166mod geometry;
167#[cfg(test)]
168mod hide_id_contract_kcl_test_pins;
169mod id_generator;
170mod import;
171mod import_graph;
172pub(crate) mod kcl_value;
173pub(crate) mod kcl_value_view;
174pub(crate) mod machine;
175mod memory;
176mod modeling;
177mod named_views;
178mod sketch_solve;
179mod solver_arc;
180mod state;
181pub mod typed_path;
182pub(crate) mod types;
183
184pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
185pub(crate) const SKETCH_OBJECT_META: &str = "meta";
186pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
187
188macro_rules! control_continue {
193 ($control_flow:expr) => {{
194 let cf = $control_flow;
195 if cf.is_some_return() {
196 return Ok(cf);
197 } else {
198 cf.into_value()
199 }
200 }};
201}
202pub(crate) use control_continue;
204
205macro_rules! early_return {
210 ($control_flow:expr) => {{
211 let cf = $control_flow;
212 if cf.is_some_return() {
213 return Err(EarlyReturn::from(cf));
214 } else {
215 cf.into_value()
216 }
217 }};
218}
219pub(crate) use early_return;
221
222#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
223pub enum ControlFlowKind {
224 #[default]
226 Continue,
227 Return,
232 Exit,
235}
236
237impl ControlFlowKind {
238 pub fn is_some_return(&self) -> bool {
240 match self {
241 ControlFlowKind::Continue => false,
242 ControlFlowKind::Return => true,
243 ControlFlowKind::Exit => true,
244 }
245 }
246}
247
248#[must_use = "You should always handle the control flow value when it is returned"]
249#[derive(Debug, Clone, PartialEq, Serialize)]
250pub struct KclValueControlFlow {
251 value: Box<KclValue>,
253 pub control: ControlFlowKind,
254}
255
256impl KclValue {
257 pub(crate) fn continue_(self) -> KclValueControlFlow {
258 KclValueControlFlow {
259 value: Box::new(self),
260 control: ControlFlowKind::Continue,
261 }
262 }
263
264 pub(crate) fn return_(self) -> KclValueControlFlow {
265 KclValueControlFlow {
266 value: Box::new(self),
267 control: ControlFlowKind::Return,
268 }
269 }
270
271 pub(crate) fn exit(self) -> KclValueControlFlow {
272 KclValueControlFlow {
273 value: Box::new(self),
274 control: ControlFlowKind::Exit,
275 }
276 }
277}
278
279impl KclValueControlFlow {
280 pub fn is_some_return(&self) -> bool {
282 self.control.is_some_return()
283 }
284
285 pub(crate) fn is_return(&self) -> bool {
286 matches!(self.control, ControlFlowKind::Return)
287 }
288
289 pub(crate) fn is_exit(&self) -> bool {
290 matches!(self.control, ControlFlowKind::Exit)
291 }
292
293 pub(crate) fn source_ranges(&self) -> Vec<SourceRange> {
295 let deduplicated = self
296 .value
297 .metadata()
298 .into_iter()
299 .map(|m| m.source_range)
300 .collect::<AhashIndexSet<_>>();
301 deduplicated.into_iter().collect()
302 }
303
304 pub(crate) fn into_value(self) -> KclValue {
305 *self.value
306 }
307}
308
309#[must_use = "You should always handle the control flow value when it is returned"]
316#[allow(clippy::large_enum_variant)]
317#[derive(Debug, Clone)]
318pub(crate) enum EarlyReturn {
319 Value(KclValueControlFlow),
321 Error(KclError),
323}
324
325impl From<KclValueControlFlow> for EarlyReturn {
326 fn from(cf: KclValueControlFlow) -> Self {
327 EarlyReturn::Value(cf)
328 }
329}
330
331impl From<KclError> for EarlyReturn {
332 fn from(err: KclError) -> Self {
333 EarlyReturn::Error(err)
334 }
335}
336
337pub(crate) enum StatementKind<'a> {
338 Declaration { name: &'a str },
339 Expression,
340}
341
342#[derive(Debug, Clone, Copy)]
343pub enum PreserveMem {
344 Normal,
345 Always,
346}
347
348impl PreserveMem {
349 fn normal(self) -> bool {
350 match self {
351 PreserveMem::Normal => true,
352 PreserveMem::Always => false,
353 }
354 }
355}
356
357#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
359#[ts(export)]
360#[serde(rename_all = "camelCase")]
361pub struct ExecOutcome {
362 pub variables: IndexMap<String, KclValueView>,
364 #[cfg(test)]
366 #[serde(skip)]
367 #[ts(skip)]
368 pub(crate) test_program_memory: IndexMap<String, KclValue>,
369 pub operations: OperationsByModule,
372 pub artifact_graph: ArtifactGraph,
374 #[serde(skip)]
376 pub scene_objects: Vec<Object>,
377 #[serde(skip)]
380 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
381 #[serde(skip)]
382 pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
383 pub refactor_metadata: Vec<RefactorMetadata>,
385 pub issues: Vec<CompilationIssue>,
387 pub filenames: IndexMap<ModuleId, ModulePath>,
389 #[serde(skip)]
394 pub source_files: IndexMap<ModuleId, ModuleSource>,
395 pub default_planes: Option<DefaultPlanes>,
397}
398
399#[derive(Debug, Clone, Copy, PartialEq)]
403enum SegmentFreedom {
404 Free,
405 Fixed,
406 Conflict,
407 Error,
409}
410
411impl From<crate::front::Freedom> for SegmentFreedom {
412 fn from(f: crate::front::Freedom) -> Self {
413 match f {
414 crate::front::Freedom::Free => Self::Free,
415 crate::front::Freedom::Fixed => Self::Fixed,
416 crate::front::Freedom::Conflict => Self::Conflict,
417 }
418 }
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
423pub enum ConstraintKind {
424 FullyConstrained,
425 UnderConstrained,
426 OverConstrained,
427 Error,
431}
432
433#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
440pub struct SketchConstraintStatus {
441 pub name: String,
453 pub instance_index: usize,
456 pub status: ConstraintKind,
458 pub free_count: usize,
460 pub conflict_count: usize,
462 pub total_count: usize,
464}
465
466#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
468pub struct SketchConstraintReport {
469 pub fully_constrained: Vec<SketchConstraintStatus>,
470 pub under_constrained: Vec<SketchConstraintStatus>,
471 pub over_constrained: Vec<SketchConstraintStatus>,
472 pub errors: Vec<SketchConstraintStatus>,
475}
476
477pub(crate) fn sketch_constraint_status_for_sketch(
486 scene_objects: &[Object],
487 sketch_obj: &Object,
488) -> Option<SketchConstraintStatus> {
489 use crate::front::ObjectKind;
490 use crate::front::Segment;
491
492 let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
493 return None;
494 };
495
496 let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
498 let obj = scene_objects.get(id.0)?;
499 if let ObjectKind::Segment {
500 segment: Segment::Point(p),
501 } = &obj.kind
502 {
503 Some(p.freedom())
504 } else {
505 None
506 }
507 };
508
509 let mut free_count: usize = 0;
510 let mut conflict_count: usize = 0;
511 let mut error_count: usize = 0;
512 let mut total_count: usize = 0;
513
514 for &seg_id in &sketch.segments {
515 let Some(seg_obj) = scene_objects.get(seg_id.0) else {
516 continue;
517 };
518 let ObjectKind::Segment { segment } = &seg_obj.kind else {
519 continue;
520 };
521 if let Segment::Point(p) = segment
524 && p.owner.is_some()
525 {
526 continue;
527 }
528 let freedom = segment
529 .freedom(lookup)
530 .map(SegmentFreedom::from)
531 .unwrap_or(SegmentFreedom::Error);
532 total_count += 1;
533 match freedom {
534 SegmentFreedom::Free => free_count += 1,
535 SegmentFreedom::Conflict => conflict_count += 1,
536 SegmentFreedom::Error => error_count += 1,
537 SegmentFreedom::Fixed => {}
538 }
539 }
540
541 let status = if error_count > 0 {
542 ConstraintKind::Error
543 } else if conflict_count > 0 {
544 ConstraintKind::OverConstrained
545 } else if free_count > 0 {
546 ConstraintKind::UnderConstrained
547 } else {
548 ConstraintKind::FullyConstrained
549 };
550
551 Some(SketchConstraintStatus {
552 name: sketch_obj.label.clone(),
553 instance_index: 0,
554 status,
555 free_count,
556 conflict_count,
557 total_count,
558 })
559}
560
561pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
562 let mut fully_constrained = Vec::new();
563 let mut under_constrained = Vec::new();
564 let mut over_constrained = Vec::new();
565 let mut errors = Vec::new();
566 let mut instance_counts = std::collections::HashMap::new();
567 for obj in scene_objects {
568 let Some(mut entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
569 continue;
570 };
571 let count = instance_counts.entry(entry.name.clone()).or_insert(0);
572 entry.instance_index = *count;
573 *count += 1;
574 match entry.status {
575 ConstraintKind::FullyConstrained => fully_constrained.push(entry),
576 ConstraintKind::UnderConstrained => under_constrained.push(entry),
577 ConstraintKind::OverConstrained => over_constrained.push(entry),
578 ConstraintKind::Error => errors.push(entry),
579 }
580 }
581
582 SketchConstraintReport {
583 fully_constrained,
584 under_constrained,
585 over_constrained,
586 errors,
587 }
588}
589
590impl ExecOutcome {
591 pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
592 debug_assert!(
593 id.0 < self.scene_objects.len(),
594 "Requested object ID {} but only have {} objects",
595 id.0,
596 self.scene_objects.len()
597 );
598 self.scene_objects.get(id.0)
599 }
600
601 pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
603 self.issues.iter().filter(|error| error.is_err())
604 }
605
606 pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
613 sketch_constraint_report_from_scene_objects(&self.scene_objects)
614 }
615
616 pub fn render_sketch_png(
619 &self,
620 sketch_name: &str,
621 ) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
622 self.render_sketch_png_instance(sketch_name, None)
623 }
624
625 pub fn render_sketch_png_instance(
628 &self,
629 sketch_name: &str,
630 instance_index: Option<usize>,
631 ) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
632 render_sketch_png_from_scene_objects(&self.scene_objects, sketch_name, instance_index)
633 }
634}
635
636pub(crate) fn render_sketch_png_from_scene_objects(
637 scene_objects: &[crate::front::Object],
638 sketch_name: &str,
639 instance_index: Option<usize>,
640) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
641 use crate::front::ObjectKind;
642 use crate::tooling::sketch_visualizer::SketchVisualizationError;
643
644 let sketches_matching_name = scene_objects
645 .iter()
646 .filter_map(|object| match &object.kind {
647 ObjectKind::Sketch(sketch) if object.label == sketch_name => Some(sketch),
648 _ => None,
649 })
650 .collect::<Vec<_>>();
651
652 let sketch = match (sketches_matching_name.as_slice(), instance_index) {
654 ([], _) => {
656 return Err(SketchVisualizationError::SketchNotFound {
657 name: sketch_name.to_owned(),
658 });
659 }
660 (_nonempty, Some(index)) => {
662 *sketches_matching_name
663 .get(index)
664 .ok_or_else(|| SketchVisualizationError::InstanceNotFound {
665 name: sketch_name.to_owned(),
666 index,
667 count: sketches_matching_name.len(),
668 })?
669 }
670 ([sketch], None) => *sketch,
673
674 (_nonempty, None) => {
677 return Err(SketchVisualizationError::AmbiguousSketchName {
678 name: sketch_name.to_owned(),
679 count: sketches_matching_name.len(),
680 });
681 }
682 };
683
684 crate::tooling::sketch_visualizer::render_sketch_png(scene_objects, sketch)
686}
687
688#[derive(Debug, Clone, PartialEq)]
690pub struct MockConfig {
691 pub use_prev_memory: bool,
692 pub sketch_block_id: Option<ObjectId>,
695 pub freedom_analysis: bool,
698 pub segment_ids_edited: AhashIndexSet<ObjectId>,
700 pub drag_anchors: Vec<SegmentDragAnchor>,
702}
703
704#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
705#[ts(export, export_to = "FrontendApi.ts")]
706#[serde(rename_all = "camelCase")]
707pub struct SegmentDragAnchor {
708 pub segment_id: ObjectId,
709 pub target: crate::front::Point2d<Number>,
710}
711
712impl Default for MockConfig {
713 fn default() -> Self {
714 Self {
715 use_prev_memory: true,
717 sketch_block_id: None,
718 freedom_analysis: true,
719 segment_ids_edited: AhashIndexSet::default(),
720 drag_anchors: Vec::new(),
721 }
722 }
723}
724
725impl MockConfig {
726 pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
728 Self {
729 sketch_block_id: Some(sketch_block_id),
730 ..Default::default()
731 }
732 }
733
734 #[must_use]
735 pub(crate) fn no_freedom_analysis(mut self) -> Self {
736 self.freedom_analysis = false;
737 self
738 }
739}
740
741#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
742#[ts(export)]
743#[serde(tag = "type", rename_all = "camelCase")]
744pub struct TagIdentifier {
745 pub value: String,
746 #[serde(skip)]
749 pub info: Vec<(usize, TagEngineInfo)>,
750 #[serde(skip)]
751 pub meta: Vec<Metadata>,
752}
753
754impl TagIdentifier {
755 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
757 for (e, info) in self.info.iter().rev() {
758 if *e <= at_epoch {
759 return Some(info);
760 }
761 }
762
763 None
764 }
765
766 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
768 self.info.last().map(|i| &i.1)
769 }
770
771 pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
774 let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
775 return vec![];
776 };
777 self.info
778 .iter()
779 .rev()
780 .take_while(|(e, _)| *e == cur_epoch)
781 .map(|(_, info)| info)
782 .collect()
783 }
784
785 pub fn merge_info(&mut self, other: &TagIdentifier) {
787 assert_eq!(&self.value, &other.value);
788 for (oe, ot) in &other.info {
789 if let Some((e, t)) = self.info.last_mut() {
790 if *e > *oe {
792 continue;
793 }
794 if e == oe {
796 *t = ot.clone();
797 continue;
798 }
799 }
800 self.info.push((*oe, ot.clone()));
801 }
802 }
803
804 pub fn geometry(&self) -> Option<Geometry> {
805 self.get_cur_info().map(|info| info.geometry.clone())
806 }
807
808 pub(crate) fn is_body_created_tag(&self) -> bool {
809 self.get_cur_info().is_some_and(|info| {
810 matches!(&info.geometry, Geometry::Solid(_)) && info.path.is_none() && info.surface.is_some()
811 })
812 }
813}
814
815impl Eq for TagIdentifier {}
816
817impl std::fmt::Display for TagIdentifier {
818 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
819 write!(f, "{}", self.value)
820 }
821}
822
823impl std::str::FromStr for TagIdentifier {
824 type Err = KclError;
825
826 fn from_str(s: &str) -> Result<Self, Self::Err> {
827 Ok(Self {
828 value: s.to_string(),
829 info: Vec::new(),
830 meta: Default::default(),
831 })
832 }
833}
834
835impl Ord for TagIdentifier {
836 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
837 self.value.cmp(&other.value)
838 }
839}
840
841impl PartialOrd for TagIdentifier {
842 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
843 Some(self.cmp(other))
844 }
845}
846
847impl std::hash::Hash for TagIdentifier {
848 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
849 self.value.hash(state);
850 }
851}
852
853#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
855#[ts(export)]
856#[serde(tag = "type", rename_all = "camelCase")]
857pub struct TagEngineInfo {
858 pub id: uuid::Uuid,
860 pub geometry: Geometry,
862 pub path: Option<Path>,
864 pub surface: Option<ExtrudeSurface>,
866}
867
868#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
869pub enum BodyType {
870 Root,
871 Block,
872}
873
874#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
876#[ts(export)]
877#[serde(rename_all = "camelCase")]
878pub struct Metadata {
879 pub source_range: SourceRange,
881}
882
883impl From<Metadata> for Vec<SourceRange> {
884 fn from(meta: Metadata) -> Self {
885 vec![meta.source_range]
886 }
887}
888
889impl From<&Metadata> for SourceRange {
890 fn from(meta: &Metadata) -> Self {
891 meta.source_range
892 }
893}
894
895impl From<SourceRange> for Metadata {
896 fn from(source_range: SourceRange) -> Self {
897 Self { source_range }
898 }
899}
900
901impl<T> From<NodeRef<'_, T>> for Metadata {
902 fn from(node: NodeRef<'_, T>) -> Self {
903 Self {
904 source_range: SourceRange::new(node.start, node.end, node.module_id),
905 }
906 }
907}
908
909impl From<&Expr> for Metadata {
910 fn from(expr: &Expr) -> Self {
911 Self {
912 source_range: SourceRange::from(expr),
913 }
914 }
915}
916
917impl Metadata {
918 pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
919 if meta.len() == 1 {
920 let meta = &meta[0];
921 return crate::front::SourceRef::Simple {
922 range: meta.source_range,
923 node_path,
924 };
925 }
926 crate::front::SourceRef::BackTrace {
927 ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
928 }
929 }
930}
931
932#[derive(PartialEq, Debug, Default, Clone)]
934pub enum ContextType {
935 #[default]
937 Live,
938
939 Mock,
943
944 MockCustomForwarded,
946}
947
948#[derive(Clone)]
952pub struct ExecutorContext {
953 pub engine: Arc<EngineManager>,
954 pub engine_batch: EngineBatchContext,
955 pub fs: FileSystemHandle,
956 pub settings: ExecutorSettings,
957 pub context_type: ContextType,
958 pub execution_callbacks: Option<Arc<dyn ExecutionCallbacks>>,
959 pub(crate) executor_kind: machine::ExecutorKind,
963 pub(crate) machine_call_depth_limit: usize,
966}
967
968impl std::fmt::Debug for ExecutorContext {
969 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
970 f.debug_struct("ExecutorContext")
971 .field("engine", &self.engine)
972 .field("engine_batch", &self.engine_batch)
973 .field("settings", &self.settings)
974 .field("context_type", &self.context_type)
975 .field("execution_callbacks", &self.execution_callbacks)
976 .field("executor_kind", &self.executor_kind)
977 .field("machine_call_depth_limit", &self.machine_call_depth_limit)
978 .finish()
979 }
980}
981
982#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
984#[ts(export)]
985pub struct ExecutorSettings {
986 pub highlight_edges: bool,
988 pub enable_ssao: bool,
990 pub show_grid: bool,
992 pub replay: Option<String>,
995 pub project_directory: Option<TypedPath>,
998 pub current_file: Option<TypedPath>,
1001 pub fixed_size_grid: bool,
1003 #[serde(default, skip_serializing_if = "is_false")]
1009 pub skip_artifact_graph: bool,
1010 #[serde(default, skip_serializing_if = "Option::is_none")]
1013 pub heartbeats: Option<u64>,
1014 #[serde(default, skip_serializing_if = "Option::is_none")]
1017 pub default_backface_color: Option<String>,
1018 #[serde(default, skip_serializing_if = "Option::is_none")]
1020 pub pool: Option<String>,
1021 #[serde(default, skip_serializing_if = "Option::is_none")]
1023 pub video_res_width: Option<u32>,
1024 #[serde(default, skip_serializing_if = "Option::is_none")]
1026 pub video_res_height: Option<u32>,
1027 pub geometry_only: bool,
1029}
1030
1031fn is_false(b: &bool) -> bool {
1032 !*b
1033}
1034
1035impl Default for ExecutorSettings {
1036 fn default() -> Self {
1037 Self {
1038 highlight_edges: true,
1039 enable_ssao: false,
1040 show_grid: false,
1041 replay: None,
1042 project_directory: None,
1043 current_file: None,
1044 fixed_size_grid: true,
1045 skip_artifact_graph: false,
1046 heartbeats: None,
1047 default_backface_color: None,
1048 pool: None,
1049 video_res_width: None,
1050 video_res_height: None,
1051 geometry_only: false,
1052 }
1053 }
1054}
1055
1056impl From<crate::settings::types::Configuration> for ExecutorSettings {
1057 fn from(config: crate::settings::types::Configuration) -> Self {
1058 Self::from(config.settings)
1059 }
1060}
1061
1062impl From<crate::settings::types::Settings> for ExecutorSettings {
1063 fn from(settings: crate::settings::types::Settings) -> Self {
1064 let modeling_settings = settings.modeling.unwrap_or_default();
1065 Self {
1066 highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
1067 enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
1068 show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
1069 replay: None,
1070 project_directory: None,
1071 current_file: None,
1072 fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
1073 skip_artifact_graph: false,
1074 heartbeats: None,
1075 default_backface_color: modeling_settings.backface_color.map(|color| color.0),
1076 pool: None,
1077 video_res_width: None,
1078 video_res_height: None,
1079 geometry_only: false,
1080 }
1081 }
1082}
1083
1084impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
1085 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
1086 Self::from(config.settings.modeling)
1087 }
1088}
1089
1090impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
1091 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
1092 Self {
1093 highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
1094 enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
1095 show_grid: modeling.show_scale_grid.unwrap_or_default(),
1096 replay: None,
1097 project_directory: None,
1098 current_file: None,
1099 fixed_size_grid: true,
1100 skip_artifact_graph: false,
1101 heartbeats: None,
1102 default_backface_color: modeling.backface_color.map(|color| color.0),
1103 pool: None,
1104 video_res_width: None,
1105 video_res_height: None,
1106 geometry_only: false,
1107 }
1108 }
1109}
1110
1111impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
1112 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
1113 Self {
1114 highlight_edges: modeling.highlight_edges.into(),
1115 enable_ssao: modeling.enable_ssao.into(),
1116 show_grid: Default::default(),
1117 replay: None,
1118 project_directory: None,
1119 current_file: None,
1120 fixed_size_grid: true,
1121 skip_artifact_graph: false,
1122 heartbeats: None,
1123 default_backface_color: None,
1124 pool: None,
1125 video_res_width: None,
1126 video_res_height: None,
1127 geometry_only: false,
1128 }
1129 }
1130}
1131
1132impl ExecutorSettings {
1133 pub fn with_current_file(&mut self, current_file: TypedPath) {
1135 if current_file.extension() == Some("kcl") {
1137 self.current_file = Some(current_file.clone());
1138 if let Some(parent) = current_file.parent() {
1140 self.project_directory = Some(parent);
1141 } else {
1142 self.project_directory = Some(TypedPath::from(""));
1143 }
1144 } else {
1145 self.project_directory = Some(current_file);
1146 }
1147 }
1148}
1149
1150enum PreparedCachedExecution {
1153 Cached(Box<ExecOutcome>),
1154 Execute(Box<CachedExecution>),
1155}
1156
1157struct CachedExecution {
1158 program: crate::Program,
1159 exec_state: Box<ExecState>,
1160 universe_info: Option<(Universe, UniverseMap)>,
1161 preserve_mem: PreserveMem,
1162}
1163
1164impl ExecutorContext {
1165 pub fn new_with_engine_and_fs(
1167 engine: Arc<EngineManager>,
1168 fs: FileSystemHandle,
1169 settings: ExecutorSettings,
1170 ) -> Self {
1171 ExecutorContext {
1172 engine,
1173 engine_batch: EngineBatchContext::default(),
1174 fs,
1175 settings,
1176 context_type: ContextType::Live,
1177 execution_callbacks: Default::default(),
1178 executor_kind: machine::ExecutorKind::resolve(),
1179 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1180 }
1181 }
1182
1183 fn clone_with_fresh_execution_batch(&self) -> Self {
1184 Self {
1185 engine: self.engine.clone(),
1186 engine_batch: EngineBatchContext::new(),
1187 fs: self.fs.clone(),
1188 settings: self.settings.clone(),
1189 context_type: self.context_type.clone(),
1190 execution_callbacks: self.execution_callbacks.clone(),
1191 executor_kind: self.executor_kind,
1194 machine_call_depth_limit: self.machine_call_depth_limit,
1195 }
1196 }
1197
1198 #[cfg(not(target_arch = "wasm32"))]
1200 pub fn new_with_engine(engine: Arc<EngineManager>, settings: ExecutorSettings) -> Self {
1201 Self::new_with_engine_and_fs(engine, crate::fs::new_file_system_handle(FileManager::new()), settings)
1202 }
1203
1204 #[cfg(not(target_arch = "wasm32"))]
1207 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings, kcl_version: KclVersion) -> Result<Self> {
1208 let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
1209 let (ws, headers) = client
1210 .modeling()
1211 .commands_ws(kittycad::modeling::CommandsWsParams {
1212 api_call_id: None,
1213 fps: None,
1214 order_independent_transparency: None,
1215 post_effect: if settings.enable_ssao {
1216 Some(kittycad::types::PostEffectType::Ssao)
1217 } else {
1218 None
1219 },
1220 replay: settings.replay.clone(),
1221 show_grid: if settings.show_grid { Some(true) } else { None },
1222 pool: if settings.geometry_only {
1223 Some("cpu".to_string())
1224 } else {
1225 settings.pool.clone()
1226 },
1227 geometry_only: Some(settings.geometry_only),
1228 pr,
1229 unlocked_framerate: None,
1230 webrtc: Some(false),
1231 video_res_width: settings.video_res_width,
1232 video_res_height: settings.video_res_height,
1233 kcl_version: Some(match kcl_version {
1234 KclVersion::V1 => kittycad::types::KclVersion::One0,
1235 KclVersion::V2 => kittycad::types::KclVersion::Two0,
1236 KclVersion::V3Preview => kittycad::types::KclVersion::Three0Preview,
1237 }),
1238 })
1239 .await?;
1240
1241 let request_id = headers
1242 .get("x-request-id")
1243 .and_then(|id| id.to_str().ok())
1244 .map(str::to_owned);
1245 let engine_conn =
1246 EngineManager::new_websocket_transport_with_request_id(ws, settings.heartbeats, request_id).await;
1247 let engine = Arc::new(engine_conn);
1248
1249 Ok(Self::new_with_engine(engine, settings))
1250 }
1251
1252 #[cfg(target_arch = "wasm32")]
1253 pub fn new(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1254 Self::new_with_engine_and_fs(engine, fs, settings)
1255 }
1256
1257 #[cfg(not(target_arch = "wasm32"))]
1258 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
1259 ExecutorContext {
1260 engine: Arc::new(EngineManager::new_mock()),
1261 engine_batch: EngineBatchContext::default(),
1262 fs: crate::fs::new_file_system_handle(FileManager::new()),
1263 settings: settings.unwrap_or_default(),
1264 context_type: ContextType::Mock,
1265 execution_callbacks: Default::default(),
1266 executor_kind: machine::ExecutorKind::resolve(),
1267 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1268 }
1269 }
1270
1271 #[cfg(target_arch = "wasm32")]
1272 pub fn new_mock(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1273 ExecutorContext {
1274 engine,
1275 engine_batch: EngineBatchContext::default(),
1276 fs,
1277 settings,
1278 context_type: ContextType::Mock,
1279 execution_callbacks: Default::default(),
1280 executor_kind: machine::ExecutorKind::resolve(),
1281 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1282 }
1283 }
1284
1285 #[cfg(target_arch = "wasm32")]
1288 pub fn new_mock_for_lsp(
1289 fs_manager: crate::fs::wasm::FileSystemManager,
1290 settings: ExecutorSettings,
1291 ) -> Result<Self, String> {
1292 let fs = crate::fs::new_file_system_handle(FileManager::new(fs_manager));
1293
1294 Ok(ExecutorContext {
1295 engine: Arc::new(EngineManager::new_mock()),
1296 engine_batch: EngineBatchContext::default(),
1297 fs,
1298 settings,
1299 context_type: ContextType::Mock,
1300 execution_callbacks: Default::default(),
1301 executor_kind: machine::ExecutorKind::resolve(),
1302 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1303 })
1304 }
1305
1306 #[cfg(not(target_arch = "wasm32"))]
1307 pub fn new_forwarded_mock(engine: Arc<EngineManager>) -> Self {
1308 ExecutorContext {
1309 engine,
1310 engine_batch: EngineBatchContext::default(),
1311 fs: crate::fs::new_file_system_handle(FileManager::new()),
1312 settings: Default::default(),
1313 context_type: ContextType::MockCustomForwarded,
1314 execution_callbacks: Default::default(),
1315 executor_kind: machine::ExecutorKind::resolve(),
1316 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1317 }
1318 }
1319
1320 #[cfg(not(target_arch = "wasm32"))]
1326 pub async fn new_with_client(
1327 settings: ExecutorSettings,
1328 token: Option<String>,
1329 engine_addr: Option<String>,
1330 kcl_version: KclVersion,
1331 ) -> Result<Self> {
1332 let client = crate::engine::new_zoo_client(token, engine_addr)?;
1334
1335 let ctx = Self::new(&client, settings, kcl_version).await?;
1336 Ok(ctx)
1337 }
1338
1339 #[cfg(not(target_arch = "wasm32"))]
1345 pub async fn new_with_version(kcl_version: KclVersion) -> Result<Self> {
1346 Self::new_with_client(Default::default(), None, None, kcl_version).await
1347 }
1348
1349 #[cfg_attr(not(test), deprecated(note = "use fn new_with_version() instead"))]
1356 #[cfg(not(target_arch = "wasm32"))]
1357 pub async fn new_with_default_client() -> Result<Self> {
1358 Self::new_with_client(Default::default(), None, None, KclVersion::default()).await
1359 }
1360
1361 #[cfg(not(target_arch = "wasm32"))]
1363 pub async fn new_geometry_only_with_default_client() -> Result<Self> {
1364 Self::new_geometry_only_with_version(KclVersion::default()).await
1365 }
1366
1367 #[cfg(not(target_arch = "wasm32"))]
1369 pub async fn new_geometry_only_with_version(kcl_version: KclVersion) -> Result<Self> {
1370 Self::new_with_client(
1371 ExecutorSettings {
1372 geometry_only: true,
1373 ..Default::default()
1374 },
1375 None,
1376 None,
1377 kcl_version,
1378 )
1379 .await
1380 }
1381
1382 #[cfg(not(target_arch = "wasm32"))]
1384 pub async fn new_for_unit_test(engine_addr: Option<String>, kcl_version: KclVersion) -> Result<Self> {
1385 let ctx = ExecutorContext::new_with_client(
1386 ExecutorSettings {
1387 highlight_edges: true,
1388 enable_ssao: false,
1389 show_grid: false,
1390 replay: None,
1391 project_directory: None,
1392 current_file: None,
1393 fixed_size_grid: false,
1394 skip_artifact_graph: false,
1395 heartbeats: None,
1396 default_backface_color: None,
1397 pool: None,
1398 video_res_width: None,
1399 video_res_height: None,
1400 geometry_only: false,
1401 },
1402 None,
1403 engine_addr,
1404 kcl_version,
1405 )
1406 .await?;
1407 Ok(ctx)
1408 }
1409
1410 pub fn is_mock(&self) -> bool {
1411 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1412 }
1413
1414 pub async fn no_engine_commands(&self) -> bool {
1416 self.is_mock()
1417 }
1418
1419 pub async fn send_clear_scene(
1420 &self,
1421 exec_state: &mut ExecState,
1422 source_range: crate::execution::SourceRange,
1423 ) -> Result<(), KclError> {
1424 exec_state.mod_local.artifacts.clear();
1427 exec_state.global.root_module_artifacts.clear();
1428 exec_state.global.artifacts.clear();
1429
1430 self.engine
1431 .clear_scene(
1432 &self.engine_batch,
1433 &mut exec_state.mod_local.id_generator,
1434 source_range,
1435 self.settings.geometry_only,
1436 )
1437 .await?;
1438 if !self.settings.geometry_only && self.settings.enable_ssao {
1440 let cmd_id = exec_state.next_uuid();
1441 exec_state
1442 .batch_modeling_cmd(
1443 ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1444 ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1445 )
1446 .await?;
1447 }
1448 Ok(())
1449 }
1450
1451 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1452 cache::bust_cache().await;
1453
1454 let outcome = self.run_with_caching(crate::Program::empty()).await?;
1459
1460 Ok(outcome)
1461 }
1462
1463 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1464 self.eval_prelude(exec_state, SourceRange::synthetic())
1465 .await
1466 .map_err(KclErrorWithOutputs::no_outputs)?;
1467 exec_state
1468 .mut_stack()
1469 .push_new_root_env(true)
1470 .map_err(KclErrorWithOutputs::no_outputs)?;
1471 Ok(())
1472 }
1473
1474 fn restore_mock_memory(
1475 exec_state: &mut ExecState,
1476 mem: cache::SketchModeState,
1477 _mock_config: &MockConfig,
1478 ) -> Result<(), KclErrorWithOutputs> {
1479 *exec_state.mut_stack() = mem.stack;
1480 exec_state.global.module_infos = mem.module_infos;
1481 exec_state.global.path_to_source_id = mem.path_to_source_id;
1482 exec_state.global.id_to_source = mem.id_to_source;
1483 exec_state.global.never_type_ranges = mem.never_type_ranges;
1484 exec_state.global.std_not_yet_added = mem.std_not_yet_added;
1485 exec_state.mod_local.constraint_state = mem.constraint_state;
1486 let len = _mock_config
1487 .sketch_block_id
1488 .map(|sketch_block_id| sketch_block_id.0)
1489 .unwrap_or(0);
1490 if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1491 exec_state
1492 .global
1493 .root_module_artifacts
1494 .restore_scene_objects(scene_objects);
1495 } else {
1496 let message = format!(
1497 "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1498 mem.scene_objects.len(),
1499 len
1500 );
1501 debug_assert!(false, "{message}");
1502 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1503 KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1504 )));
1505 }
1506
1507 Ok(())
1508 }
1509
1510 pub async fn run_mock(
1511 &self,
1512 program: &crate::Program,
1513 mock_config: &MockConfig,
1514 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1515 let (exec_state, main_ref) = self.run_mock_returning_state(program, mock_config).await?;
1516
1517 let mut stack = exec_state.stack().clone();
1522 let module_infos = exec_state.global.module_infos.clone();
1523 let path_to_source_id = exec_state.global.path_to_source_id.clone();
1524 let id_to_source = exec_state.global.id_to_source.clone();
1525 let never_type_ranges = exec_state.global.never_type_ranges.clone();
1526 let constraint_state = exec_state.mod_local.constraint_state.clone();
1527 let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1528 let std_not_yet_added = exec_state.global.std_not_yet_added.clone();
1529 let kcl_version = exec_state.entry_point_kcl_version();
1530 let outcome = exec_state
1531 .into_exec_outcome(main_ref, self)
1532 .await
1533 .map_err(KclErrorWithOutputs::no_outputs)?;
1534
1535 stack.squash_env(main_ref).map_err(KclErrorWithOutputs::no_outputs)?;
1536 let state = cache::SketchModeState {
1537 stack,
1538 module_infos,
1539 path_to_source_id,
1540 id_to_source,
1541 never_type_ranges,
1542 constraint_state,
1543 scene_objects,
1544 std_not_yet_added,
1545 kcl_version,
1546 };
1547 cache::write_old_memory(state).await;
1548
1549 Ok(outcome)
1550 }
1551
1552 async fn run_mock_returning_state(
1557 &self,
1558 program: &crate::Program,
1559 mock_config: &MockConfig,
1560 ) -> Result<(ExecState, EnvironmentRef), KclErrorWithOutputs> {
1561 assert!(
1562 self.is_mock(),
1563 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1564 );
1565
1566 let use_prev_memory = mock_config.use_prev_memory;
1567 let mut exec_state = ExecState::new_mock(self, mock_config);
1568 exec_state
1571 .set_entry_point_kcl_version(program)
1572 .map_err(KclErrorWithOutputs::no_outputs)?;
1573 if use_prev_memory {
1574 match cache::read_old_memory().await {
1575 Some(mem) if mem.reusable_for(exec_state.entry_point_kcl_version()) => {
1576 Self::restore_mock_memory(&mut exec_state, mem, mock_config)?
1577 }
1578 _ => self.prepare_mem(&mut exec_state).await?,
1582 }
1583 } else {
1584 self.prepare_mem(&mut exec_state).await?
1585 };
1586
1587 exec_state
1590 .mut_stack()
1591 .push_new_env_for_scope()
1592 .map_err(KclErrorWithOutputs::no_outputs)?;
1593
1594 let (main_ref, _) = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1595
1596 Ok((exec_state, main_ref))
1597 }
1598
1599 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1602 assert!(!self.is_mock());
1603 let result = self
1604 .with_engine_execution(Box::pin(self.run_with_caching_inner(program)))
1605 .await;
1606 if result.is_err() {
1607 cache::bust_cache().await;
1608 }
1609 result
1610 }
1611
1612 async fn run_with_caching_inner(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1613 let original_program = program.clone();
1614 let CachedExecution {
1615 program,
1616 mut exec_state,
1617 universe_info,
1618 preserve_mem,
1619 } = *match self.prepare_cached_execution(program).await? {
1620 PreparedCachedExecution::Cached(outcome) => return Ok(*outcome),
1621 PreparedCachedExecution::Execute(execution) => execution,
1622 };
1623 let result = self
1624 .run_concurrent_inner(&program, &mut exec_state, universe_info, preserve_mem)
1625 .await;
1626 self.finish_cached_execution(original_program, exec_state, result).await
1627 }
1628
1629 #[inline(never)]
1632 fn prepare_cached_execution(
1633 &self,
1634 program: crate::Program,
1635 ) -> BoxFuture<'_, Result<PreparedCachedExecution, KclErrorWithOutputs>> {
1636 Box::pin(async move {
1637 let grid_scale = if self.settings.fixed_size_grid {
1638 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1639 } else {
1640 GridScaleBehavior::ScaleWithZoom
1641 };
1642
1643 let (program, exec_state, universe_info, preserve_mem) = match cache::read_old_ast().await {
1644 Some(mut cached_state) => {
1645 let old = CacheInformation {
1646 ast: &cached_state.main.ast,
1647 settings: &cached_state.settings,
1648 };
1649 let new = CacheInformation {
1650 ast: &program.ast,
1651 settings: &self.settings,
1652 };
1653
1654 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1656 CacheResult::ReExecute {
1657 clear_scene,
1658 reapply_settings,
1659 program: changed_program,
1660 } => {
1661 if reapply_settings
1662 && self
1663 .engine
1664 .reapply_settings(
1665 &self.engine_batch,
1666 &self.settings,
1667 Default::default(),
1668 &mut cached_state.main.exec_state.id_generator,
1669 grid_scale,
1670 )
1671 .await
1672 .is_err()
1673 {
1674 (true, program, None)
1675 } else {
1676 (
1677 clear_scene,
1678 crate::Program {
1679 ast: changed_program,
1680 original_file_contents: program.original_file_contents,
1681 },
1682 None,
1683 )
1684 }
1685 }
1686 CacheResult::CheckImportsOnly {
1687 reapply_settings,
1688 ast: changed_program,
1689 } => {
1690 let mut reapply_failed = false;
1691 if reapply_settings {
1692 if self
1693 .engine
1694 .reapply_settings(
1695 &self.engine_batch,
1696 &self.settings,
1697 Default::default(),
1698 &mut cached_state.main.exec_state.id_generator,
1699 grid_scale,
1700 )
1701 .await
1702 .is_ok()
1703 {
1704 cache::write_old_ast(GlobalState::with_settings(
1705 cached_state.clone(),
1706 self.settings.clone(),
1707 ))
1708 .await;
1709 } else {
1710 reapply_failed = true;
1711 }
1712 }
1713
1714 if reapply_failed {
1715 (true, program, None)
1716 } else {
1717 let mut new_exec_state = ExecState::new(self);
1719 let (new_universe, new_universe_map) =
1720 self.get_universe(&program, &mut new_exec_state).await?;
1721
1722 let clear_scene = new_universe.values().any(|value| {
1723 let id = value.1;
1724 match (
1725 cached_state.exec_state.get_source(id),
1726 new_exec_state.global.get_source(id),
1727 ) {
1728 (Some(s0), Some(s1)) => s0.source != s1.source,
1729 _ => false,
1730 }
1731 });
1732
1733 if !clear_scene {
1734 cache::write_old_memory(
1736 cached_state
1737 .mock_memory_state()
1738 .map_err(KclErrorWithOutputs::no_outputs)?,
1739 )
1740 .await;
1741 return cached_state
1742 .into_exec_outcome(self)
1743 .await
1744 .map(|outcome| PreparedCachedExecution::Cached(Box::new(outcome)))
1745 .map_err(KclErrorWithOutputs::no_outputs);
1746 }
1747
1748 (
1749 true,
1750 crate::Program {
1751 ast: changed_program,
1752 original_file_contents: program.original_file_contents,
1753 },
1754 Some((new_universe, new_universe_map, new_exec_state)),
1755 )
1756 }
1757 }
1758 CacheResult::NoAction(true) => {
1759 if self
1760 .engine
1761 .reapply_settings(
1762 &self.engine_batch,
1763 &self.settings,
1764 Default::default(),
1765 &mut cached_state.main.exec_state.id_generator,
1766 grid_scale,
1767 )
1768 .await
1769 .is_ok()
1770 {
1771 cache::write_old_ast(GlobalState::with_settings(
1773 cached_state.clone(),
1774 self.settings.clone(),
1775 ))
1776 .await;
1777
1778 cache::write_old_memory(
1779 cached_state
1780 .mock_memory_state()
1781 .map_err(KclErrorWithOutputs::no_outputs)?,
1782 )
1783 .await;
1784 return cached_state
1785 .into_exec_outcome(self)
1786 .await
1787 .map(|outcome| PreparedCachedExecution::Cached(Box::new(outcome)))
1788 .map_err(KclErrorWithOutputs::no_outputs);
1789 }
1790 (true, program, None)
1791 }
1792 CacheResult::NoAction(false) => {
1793 cache::write_old_memory(
1794 cached_state
1795 .mock_memory_state()
1796 .map_err(KclErrorWithOutputs::no_outputs)?,
1797 )
1798 .await;
1799 return cached_state
1800 .into_exec_outcome(self)
1801 .await
1802 .map(|outcome| PreparedCachedExecution::Cached(Box::new(outcome)))
1803 .map_err(KclErrorWithOutputs::no_outputs);
1804 }
1805 };
1806
1807 let (exec_state, universe_info, preserve_mem) = match import_check_info {
1808 Some((new_universe, new_universe_map, mut new_exec_state)) => {
1809 self.send_clear_scene(&mut new_exec_state, Default::default())
1811 .await
1812 .map_err(KclErrorWithOutputs::no_outputs)?;
1813
1814 (
1815 new_exec_state,
1816 Some((new_universe, new_universe_map)),
1817 PreserveMem::Normal,
1818 )
1819 }
1820 None if clear_scene => {
1821 let mut exec_state = cached_state.reconstitute_exec_state(self);
1823 exec_state.reset(self);
1824
1825 self.send_clear_scene(&mut exec_state, Default::default())
1826 .await
1827 .map_err(KclErrorWithOutputs::no_outputs)?;
1828
1829 (exec_state, None, PreserveMem::Normal)
1830 }
1831 None => {
1832 let mut exec_state = cached_state.reconstitute_exec_state(self);
1833 exec_state
1834 .mut_stack()
1835 .restore_env(cached_state.main.result_env)
1836 .map_err(KclErrorWithOutputs::no_outputs)?;
1837
1838 (exec_state, None, PreserveMem::Always)
1839 }
1840 };
1841
1842 (program, exec_state, universe_info, preserve_mem)
1843 }
1844 None => {
1845 let mut exec_state = ExecState::new(self);
1846 self.send_clear_scene(&mut exec_state, Default::default())
1847 .await
1848 .map_err(KclErrorWithOutputs::no_outputs)?;
1849
1850 (program, exec_state, None, PreserveMem::Normal)
1851 }
1852 };
1853
1854 Ok(PreparedCachedExecution::Execute(Box::new(CachedExecution {
1855 program,
1856 exec_state: Box::new(exec_state),
1857 universe_info,
1858 preserve_mem,
1859 })))
1860 })
1861 }
1862
1863 #[inline(never)]
1864 fn finish_cached_execution(
1865 &self,
1866 original_program: crate::Program,
1867 exec_state: Box<ExecState>,
1868 result: Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs>,
1869 ) -> BoxFuture<'_, Result<ExecOutcome, KclErrorWithOutputs>> {
1870 Box::pin(async move {
1871 let result = result?;
1873
1874 cache::write_old_ast(GlobalState::new(
1878 (*exec_state).clone(),
1879 self.settings.clone(),
1880 original_program.ast,
1881 result.0,
1882 ))
1883 .await;
1884
1885 let outcome = exec_state
1886 .into_exec_outcome(result.0, self)
1887 .await
1888 .map_err(KclErrorWithOutputs::no_outputs)?;
1889 Ok(outcome)
1890 })
1891 }
1892
1893 pub async fn run(
1897 &self,
1898 program: &crate::Program,
1899 exec_state: &mut ExecState,
1900 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1901 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1902 .await
1903 }
1904
1905 pub async fn run_concurrent(
1910 &self,
1911 program: &crate::Program,
1912 exec_state: &mut ExecState,
1913 universe_info: Option<(Universe, UniverseMap)>,
1914 preserve_mem: PreserveMem,
1915 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1916 self.with_engine_execution(Box::pin(self.run_concurrent_inner(
1917 program,
1918 exec_state,
1919 universe_info,
1920 preserve_mem,
1921 )))
1922 .await
1923 }
1924
1925 async fn with_engine_execution<T>(
1927 &self,
1928 execution: impl Future<Output = Result<T, KclErrorWithOutputs>>,
1929 ) -> Result<T, KclErrorWithOutputs> {
1930 self.send_execution_boundary(ModelingCmd::from(
1931 mcmd::BeginExecution::builder().enable_render(true).build(),
1932 ))
1933 .await
1934 .map_err(KclErrorWithOutputs::no_outputs)?;
1935
1936 let exec_res = execution.await;
1937 let flush_res = self.engine.ensure_async_commands_completed(&self.engine_batch).await;
1938 if flush_res.is_err() {
1939 self.engine.clear_queues(&self.engine_batch).await;
1941 }
1942 let end_res = self
1944 .send_execution_boundary(ModelingCmd::from(mcmd::EndExecution::default()))
1945 .await;
1946 match (exec_res, flush_res.and(end_res)) {
1947 (Ok(res), Ok(())) => Ok(res),
1948 (Err(e), _) => Err(e),
1949 (Ok(_), Err(e)) => Err(KclErrorWithOutputs::no_outputs(e)),
1950 }
1951 }
1952
1953 async fn send_execution_boundary(&self, cmd: ModelingCmd) -> Result<(), KclError> {
1954 self.engine
1957 .send_modeling_cmd(
1958 &EngineBatchContext::new(),
1959 uuid::Uuid::new_v4(),
1960 SourceRange::synthetic(),
1961 &cmd,
1962 )
1963 .await
1964 .map(|_| ())
1965 }
1966
1967 async fn run_concurrent_inner(
1968 &self,
1969 program: &crate::Program,
1970 exec_state: &mut ExecState,
1971 universe_info: Option<(Universe, UniverseMap)>,
1972 preserve_mem: PreserveMem,
1973 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1974 exec_state
1978 .set_entry_point_kcl_version(program)
1979 .map_err(KclErrorWithOutputs::no_outputs)?;
1980
1981 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1984 (universe, universe_map)
1985 } else {
1986 self.get_universe(program, exec_state).await?
1987 };
1988
1989 let mut sorted_imports: Vec<_> = universe_map.iter().collect();
1995 sorted_imports.sort_by_key(|(_, import_stmt)| SourceRange::from(*import_stmt));
1996 for (_path, import_stmt) in sorted_imports {
1997 let filename = match &import_stmt.path {
2001 ImportPath::Kcl { filename } => filename.to_string(),
2002 ImportPath::Foreign { path } => path.to_string(),
2003 ImportPath::Std { .. } => continue,
2004 };
2005 if let Some((_, module_id, module_path, _)) = universe.get(&filename)
2006 && let ModulePath::Local { value, .. } = module_path
2007 {
2008 let name = import_stmt
2009 .module_name()
2010 .unwrap_or_else(|| value.file_name().unwrap_or_default());
2011 let source_range = SourceRange::from(import_stmt);
2012 exec_state.push_op(crate::execution::cad_op::Operation::ModuleInstance {
2013 name,
2014 module_id: *module_id,
2015 glob: matches!(
2016 import_stmt.selector,
2017 crate::parsing::ast::types::ImportSelector::Glob(_)
2018 ),
2019 node_path: crate::NodePath::placeholder(),
2020 source_range,
2021 });
2022 }
2023 }
2024
2025 let default_planes = self.engine.get_default_planes().read().await.clone();
2026
2027 self.eval_prelude(exec_state, SourceRange::synthetic())
2029 .await
2030 .map_err(KclErrorWithOutputs::no_outputs)?;
2031
2032 for modules in import_graph::import_graph(&universe, self)
2033 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
2034 .into_iter()
2035 {
2036 #[cfg(not(target_arch = "wasm32"))]
2037 let mut set = tokio::task::JoinSet::new();
2038
2039 #[allow(clippy::type_complexity)]
2040 let (results_tx, mut results_rx): (
2041 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
2042 tokio::sync::mpsc::Receiver<_>,
2043 ) = tokio::sync::mpsc::channel(1);
2044
2045 for module in modules {
2046 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
2047 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
2048 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
2049 )));
2050 };
2051 let module_id = *module_id;
2052 let module_path = module_path.clone();
2053 let source_range = SourceRange::from(import_stmt);
2054 let module_exec_state = exec_state.clone();
2056
2057 let repr = repr.clone();
2058 let exec_ctxt = self.clone_with_fresh_execution_batch();
2059 let results_tx = results_tx.clone();
2060
2061 let exec_module = async |exec_ctxt: &ExecutorContext,
2062 repr: &ModuleRepr,
2063 module_id: ModuleId,
2064 module_path: &ModulePath,
2065 exec_state: &mut ExecState,
2066 source_range: SourceRange|
2067 -> Result<ModuleRepr, KclError> {
2068 match repr {
2069 ModuleRepr::Kcl(program, _) => {
2070 let result = exec_ctxt
2071 .exec_module_from_ast(
2072 program,
2073 module_id,
2074 module_path,
2075 exec_state,
2076 source_range,
2077 PreserveMem::Normal,
2078 )
2079 .await;
2080
2081 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
2082 }
2083 ModuleRepr::Foreign(geom, _) => {
2084 exec_state.mod_local.artifacts = Default::default();
2088 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
2089 .await
2090 .map(|geom| Some(KclValue::ImportedGeometry(geom)))
2091 .map_err(|err| err.add_import_location(&module_path.import_name(), source_range));
2096 let module_artifacts = std::mem::take(&mut exec_state.mod_local.artifacts);
2097
2098 result.map(|val| ModuleRepr::Foreign(geom.clone(), Some((val, module_artifacts))))
2099 }
2100 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
2101 format!("Module {module_path} not found in universe"),
2102 vec![source_range],
2103 ))),
2104 }
2105 };
2106
2107 #[cfg(target_arch = "wasm32")]
2108 {
2109 wasm_bindgen_futures::spawn_local(async move {
2110 let mut exec_state = module_exec_state;
2111 let exec_ctxt = exec_ctxt;
2112
2113 let result = exec_module(
2114 &exec_ctxt,
2115 &repr,
2116 module_id,
2117 &module_path,
2118 &mut exec_state,
2119 source_range,
2120 )
2121 .await;
2122
2123 results_tx
2124 .send((module_id, module_path, result))
2125 .await
2126 .unwrap_or_default();
2127 });
2128 }
2129 #[cfg(not(target_arch = "wasm32"))]
2130 {
2131 set.spawn(async move {
2132 let mut exec_state = module_exec_state;
2133 let exec_ctxt = exec_ctxt;
2134
2135 let result = exec_module(
2136 &exec_ctxt,
2137 &repr,
2138 module_id,
2139 &module_path,
2140 &mut exec_state,
2141 source_range,
2142 )
2143 .await;
2144
2145 results_tx
2146 .send((module_id, module_path, result))
2147 .await
2148 .unwrap_or_default();
2149 });
2150 }
2151 }
2152
2153 drop(results_tx);
2154
2155 while let Some((module_id, _, result)) = results_rx.recv().await {
2156 match result {
2157 Ok(new_repr) => {
2158 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
2159
2160 match &mut repr {
2161 ModuleRepr::Kcl(_, cache) => {
2162 let ModuleRepr::Kcl(_, session_data) = new_repr else {
2163 unreachable!();
2164 };
2165 *cache = session_data;
2166 }
2167 ModuleRepr::Foreign(_, cache) => {
2168 let ModuleRepr::Foreign(_, session_data) = new_repr else {
2169 unreachable!();
2170 };
2171 *cache = session_data;
2172 }
2173 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
2174 }
2175
2176 exec_state.global.module_infos[&module_id].restore_repr(repr);
2177 }
2178 Err(e) => {
2179 let e = import_graph::add_import_backtrace(e, module_id, &universe);
2180 return Err(exec_state.error_with_outputs(e, None, default_planes));
2181 }
2182 }
2183 }
2184 }
2185
2186 exec_state.mod_local.artifacts.operations.clear();
2191
2192 exec_state
2195 .global
2196 .root_module_artifacts
2197 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
2198
2199 self.inner_run(program, exec_state, preserve_mem)
2200 .await
2201 .map_err(|mut error| {
2202 let source_ranges = error.error.source_ranges();
2208 if !source_ranges.is_empty()
2209 && !source_ranges.iter().any(|range| range.module_id().is_top_level())
2210 && let Some(outermost) = source_ranges.last()
2211 {
2212 error.error =
2213 import_graph::add_import_backtrace_from(error.error.clone(), outermost.module_id(), &universe);
2214 }
2215 error
2216 })
2217 }
2218
2219 async fn get_universe(
2222 &self,
2223 program: &crate::Program,
2224 exec_state: &mut ExecState,
2225 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
2226 exec_state
2229 .set_entry_point_kcl_version(program)
2230 .map_err(KclErrorWithOutputs::no_outputs)?;
2231 exec_state.add_root_module_contents(program);
2232
2233 let mut universe = std::collections::HashMap::new();
2234
2235 let default_planes = self.engine.get_default_planes().read().await.clone();
2236
2237 let root_imports = import_graph::import_universe(
2238 self,
2239 &ModulePath::Main,
2240 &ModuleRepr::Kcl(program.ast.clone(), None),
2241 &mut universe,
2242 exec_state,
2243 )
2244 .await
2245 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
2246
2247 Ok((universe, root_imports))
2248 }
2249
2250 async fn inner_run(
2253 &self,
2254 program: &crate::Program,
2255 exec_state: &mut ExecState,
2256 preserve_mem: PreserveMem,
2257 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
2258 let _stats = crate::log::LogPerfStats::new("Interpretation");
2259
2260 exec_state
2264 .set_entry_point_kcl_version(program)
2265 .map_err(KclErrorWithOutputs::no_outputs)?;
2266
2267 let grid_scale = if self.settings.fixed_size_grid {
2269 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
2270 } else {
2271 GridScaleBehavior::ScaleWithZoom
2272 };
2273 self.engine
2274 .reapply_settings(
2275 &self.engine_batch,
2276 &self.settings,
2277 Default::default(),
2278 exec_state.id_generator(),
2279 grid_scale,
2280 )
2281 .await
2282 .map_err(KclErrorWithOutputs::no_outputs)?;
2283
2284 let default_planes = self.engine.get_default_planes().read().await.clone();
2285 let result = self
2286 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
2287 .await;
2288
2289 crate::log::log(format!(
2290 "Post interpretation KCL memory stats: {:#?}",
2291 exec_state.stack().memory.stats()
2292 ));
2293 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
2294
2295 async fn write_old_memory(
2298 ctx: &ExecutorContext,
2299 exec_state: &ExecState,
2300 env_ref: EnvironmentRef,
2301 ) -> Result<(), KclError> {
2302 if ctx.is_mock() {
2303 return Ok(());
2304 }
2305 let mut stack = exec_state.stack().deep_clone()?;
2306 stack.restore_env(env_ref)?;
2307 let state = cache::SketchModeState {
2308 stack,
2309 module_infos: exec_state.global.module_infos.clone(),
2310 path_to_source_id: exec_state.global.path_to_source_id.clone(),
2311 id_to_source: exec_state.global.id_to_source.clone(),
2312 never_type_ranges: exec_state.global.never_type_ranges.clone(),
2313 constraint_state: exec_state.mod_local.constraint_state.clone(),
2314 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
2315 std_not_yet_added: exec_state.global.std_not_yet_added.clone(),
2316 kcl_version: exec_state.entry_point_kcl_version(),
2317 };
2318 cache::write_old_memory(state).await;
2319 Ok(())
2320 }
2321
2322 let env_ref = match result {
2323 Ok(env_ref) => env_ref,
2324 Err((err, env_ref)) => {
2325 if let Some(env_ref) = env_ref {
2328 write_old_memory(self, exec_state, env_ref)
2329 .await
2330 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2331 }
2332 return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
2333 }
2334 };
2335
2336 write_old_memory(self, exec_state, env_ref)
2337 .await
2338 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2339
2340 let session_data = self.engine.get_session_data().await;
2341
2342 Ok((env_ref, session_data))
2343 }
2344
2345 async fn execute_and_build_graph(
2348 &self,
2349 program: NodeRef<'_, crate::parsing::ast::types::Program>,
2350 exec_state: &mut ExecState,
2351 preserve_mem: PreserveMem,
2352 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
2353 let start_op = exec_state.global.root_module_artifacts.operations.len();
2359
2360 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
2361 .await
2362 .map_err(|e| (e, None))?;
2363
2364 let exec_result = self
2365 .exec_module_body(
2366 program,
2367 exec_state,
2368 preserve_mem,
2369 ModuleId::default(),
2370 &ModulePath::Main,
2371 )
2372 .await
2373 .map(
2374 |ModuleExecutionOutcome {
2375 environment: env_ref,
2376 artifacts: module_artifacts,
2377 ..
2378 }| {
2379 exec_state.global.root_module_artifacts.extend(module_artifacts);
2382 env_ref
2383 },
2384 )
2385 .map_err(|(err, env_ref, module_artifacts)| {
2386 if let Some(module_artifacts) = module_artifacts {
2387 exec_state.global.root_module_artifacts.extend(module_artifacts);
2390 }
2391 (err, env_ref)
2392 });
2393
2394 let programs = &exec_state.build_program_lookup(program.clone());
2396 let cached_body_items = exec_state.global.artifacts.cached_body_items();
2397 for op in exec_state
2398 .global
2399 .root_module_artifacts
2400 .operations
2401 .iter_mut()
2402 .skip(start_op)
2403 {
2404 op.fill_node_paths(programs, cached_body_items);
2405 }
2406 for module in exec_state.global.module_infos.values_mut() {
2407 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
2408 for op in &mut outcome.artifacts.operations {
2409 op.fill_node_paths(programs, cached_body_items);
2410 }
2411 }
2412 }
2413
2414 self.engine
2416 .ensure_async_commands_completed(&self.engine_batch)
2417 .await
2418 .map_err(|e| {
2419 match &exec_result {
2420 Ok(env_ref) => (e, Some(*env_ref)),
2421 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
2423 }
2424 })?;
2425
2426 self.engine.clear_queues(&self.engine_batch).await;
2429
2430 match exec_state.build_artifact_graph(&self.engine, program).await {
2431 Ok(_) => exec_result,
2432 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
2433 }
2434 }
2435
2436 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
2440 if exec_state.stack().memory.requires_std() {
2441 let initial_ops = exec_state.mod_local.artifacts.operations.len();
2442
2443 let path = vec!["std".to_owned(), "prelude".to_owned()];
2444 let resolved_path = ModulePath::from_std_import_path(&path)?;
2445 let id = self
2446 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
2447 .await?;
2448 let prelude = self.exec_module_for_items(id, exec_state, source_range).await?;
2449
2450 exec_state.mut_stack().memory.set_std(prelude.environment)?;
2451 exec_state.global.std_not_yet_added = prelude.not_yet_added;
2453
2454 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
2460 }
2461
2462 Ok(())
2463 }
2464
2465 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
2467 self.engine
2469 .send_modeling_cmd(
2470 &self.engine_batch,
2471 uuid::Uuid::new_v4(),
2472 crate::execution::SourceRange::default(),
2473 &ModelingCmd::from(
2474 mcmd::ZoomToFit::builder()
2475 .object_ids(Default::default())
2476 .animated(false)
2477 .padding(0.1)
2478 .build(),
2479 ),
2480 )
2481 .await
2482 .map_err(KclErrorWithOutputs::no_outputs)?;
2483
2484 let resp = self
2486 .engine
2487 .send_modeling_cmd(
2488 &self.engine_batch,
2489 uuid::Uuid::new_v4(),
2490 crate::execution::SourceRange::default(),
2491 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
2492 )
2493 .await
2494 .map_err(KclErrorWithOutputs::no_outputs)?;
2495
2496 let OkWebSocketResponseData::Modeling {
2497 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
2498 } = resp
2499 else {
2500 return Err(ExecError::BadPng(format!(
2501 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
2502 )));
2503 };
2504 Ok(contents)
2505 }
2506
2507 pub async fn export(
2509 &self,
2510 format: kittycad_modeling_cmds::format::OutputFormat3d,
2511 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2512 let resp = self
2513 .engine
2514 .send_modeling_cmd(
2515 &self.engine_batch,
2516 uuid::Uuid::new_v4(),
2517 crate::SourceRange::default(),
2518 &kittycad_modeling_cmds::ModelingCmd::Export(
2519 kittycad_modeling_cmds::Export::builder()
2520 .entity_ids(vec![])
2521 .format(format)
2522 .build(),
2523 ),
2524 )
2525 .await?;
2526
2527 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
2528 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
2529 format!("Expected Export response, got {resp:?}",),
2530 vec![SourceRange::default()],
2531 )));
2532 };
2533
2534 Ok(files)
2535 }
2536
2537 pub async fn export_step(
2539 &self,
2540 deterministic_time: bool,
2541 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2542 let files = self
2543 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
2544 kittycad_modeling_cmds::format::step::export::Options::builder()
2545 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
2546 .maybe_created(if deterministic_time {
2547 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
2548 KclError::new_internal(crate::errors::KclErrorDetails::new(
2549 format!("Failed to parse date: {e}"),
2550 vec![SourceRange::default()],
2551 ))
2552 })?)
2553 } else {
2554 None
2555 })
2556 .build(),
2557 ))
2558 .await?;
2559
2560 Ok(files)
2561 }
2562
2563 pub async fn close(&self) {
2564 self.engine.close().await;
2565 }
2566}
2567
2568pub use kcl_api::ArtifactId;
2569
2570pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
2571 ArtifactId::new(*id.as_ref())
2572}
2573
2574#[cfg(test)]
2575pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2576 parse_execute_with_project_dir(code, None).await
2577}
2578
2579#[cfg(test)]
2580pub(crate) async fn parse_execute_with_project_dir(
2581 code: &str,
2582 project_directory: Option<TypedPath>,
2583) -> Result<ExecTestResults, KclError> {
2584 parse_execute_with_executor_kind(code, project_directory, machine::ExecutorKind::resolve()).await
2586}
2587
2588#[cfg(test)]
2591pub(crate) fn new_mock_executor_context(
2592 project_directory: Option<TypedPath>,
2593 executor_kind: machine::ExecutorKind,
2594) -> ExecutorContext {
2595 ExecutorContext {
2596 engine: Arc::new(EngineManager::new_mock()),
2597 engine_batch: EngineBatchContext::default(),
2598 fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2599 settings: ExecutorSettings {
2600 project_directory,
2601 ..Default::default()
2602 },
2603 context_type: ContextType::Mock,
2604 execution_callbacks: Default::default(),
2605 executor_kind,
2606 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
2607 }
2608}
2609
2610#[cfg(test)]
2611pub(crate) async fn parse_execute_with_executor_kind(
2612 code: &str,
2613 project_directory: Option<TypedPath>,
2614 executor_kind: machine::ExecutorKind,
2615) -> Result<ExecTestResults, KclError> {
2616 parse_execute_inner(code, project_directory, executor_kind, false).await
2617}
2618
2619#[cfg(test)]
2623pub(crate) async fn parse_execute_hinting_all(
2624 code: &str,
2625 project_directory: Option<TypedPath>,
2626) -> Result<ExecTestResults, KclError> {
2627 parse_execute_inner(code, project_directory, machine::ExecutorKind::resolve(), true).await
2628}
2629
2630#[cfg(test)]
2631async fn parse_execute_inner(
2632 code: &str,
2633 project_directory: Option<TypedPath>,
2634 executor_kind: machine::ExecutorKind,
2635 hint_all_not_yet_added: bool,
2636) -> Result<ExecTestResults, KclError> {
2637 let program = crate::Program::parse_no_errs(code)?;
2638
2639 let exec_ctxt = new_mock_executor_context(project_directory, executor_kind);
2640 let mut exec_state = ExecState::new(&exec_ctxt);
2641 exec_state.global.hint_all_not_yet_added = hint_all_not_yet_added;
2642 let result = exec_ctxt.run(&program, &mut exec_state).await?;
2643
2644 Ok(ExecTestResults {
2645 program,
2646 mem_env: result.0,
2647 exec_ctxt,
2648 exec_state,
2649 })
2650}
2651
2652#[cfg(test)]
2653#[derive(Debug)]
2654pub(crate) struct ExecTestResults {
2655 program: crate::Program,
2656 mem_env: EnvironmentRef,
2657 exec_ctxt: ExecutorContext,
2658 exec_state: ExecState,
2659}
2660
2661#[cfg(test)]
2662impl ExecTestResults {
2663 pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2664 &self.exec_state.global.root_module_artifacts.commands
2665 }
2666
2667 pub(crate) fn issues(&self) -> &[CompilationIssue] {
2671 self.exec_state.issues()
2672 }
2673
2674 #[track_caller]
2678 pub(crate) fn variable(&self, name: &str) -> KclValue {
2679 self.exec_state
2680 .stack()
2681 .memory
2682 .get_from_unchecked(name, self.mem_env)
2683 .unwrap()
2684 }
2685}
2686
2687pub struct ProgramLookup {
2691 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2692}
2693
2694impl ProgramLookup {
2695 pub fn new(
2698 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2699 module_infos: state::ModuleInfoMap,
2700 ) -> Self {
2701 let mut programs = IndexMap::with_capacity(module_infos.len());
2702 for (id, info) in module_infos {
2703 if let ModuleRepr::Kcl(program, _) = info.repr {
2704 programs.insert(id, program);
2705 }
2706 }
2707 programs.insert(ModuleId::default(), current);
2708 Self { programs }
2709 }
2710
2711 pub fn program_for_module(
2712 &self,
2713 module_id: ModuleId,
2714 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2715 self.programs.get(&module_id)
2716 }
2717}
2718
2719#[cfg(test)]
2720mod tests {
2721 use kcl_api::NumericType;
2722 use pretty_assertions::assert_eq;
2723
2724 use super::*;
2725 use crate::ModuleId;
2726 use crate::errors::KclErrorDetails;
2727 use crate::errors::Severity;
2728 use crate::execution::kcl_value::TypeDef;
2729 use crate::execution::memory::Stack;
2730 use crate::execution::types::RuntimeType;
2731
2732 macro_rules! kcl_input {
2733 ($file:literal) => {
2734 include_str!(concat!("../../e2e/executor/inputs/", $file, ".kcl"))
2735 };
2736 }
2737
2738 #[cfg(not(target_arch = "wasm32"))]
2739 #[tokio::test]
2740 async fn native_websocket_sends_entrypoint_kcl_version() {
2741 use tokio::io::AsyncReadExt;
2742 use tokio::io::AsyncWriteExt;
2743
2744 for (source, expected) in [
2745 ("", "1.0"),
2746 ("@settings(defaultLengthUnit = mm)", "1.0"),
2747 ("@settings(kclVersion = 2.0)", "2.0"),
2748 ("@settings(kclVersion = \"3.0-preview\")", "3.0-preview"),
2749 ] {
2750 let program = crate::Program::parse_no_errs(source).unwrap();
2751 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2752 let address = listener.local_addr().unwrap();
2753 let request = tokio::spawn(async move {
2754 let (mut socket, _) = listener.accept().await.unwrap();
2755 let mut request = Vec::new();
2756 let mut buffer = [0; 1024];
2757 while !request.windows(4).any(|bytes| bytes == b"\r\n\r\n") {
2758 let count = socket.read(&mut buffer).await.unwrap();
2759 assert_ne!(count, 0, "connection closed before sending HTTP headers");
2760 request.extend_from_slice(&buffer[..count]);
2761 }
2762 socket
2764 .write_all(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
2765 .await
2766 .unwrap();
2767 String::from_utf8(request).unwrap()
2768 });
2769 let mut client = kittycad::Client::new("test-token");
2770 client.set_base_url(format!("http://{address}"));
2771 let result = tokio::time::timeout(
2772 std::time::Duration::from_secs(5),
2773 ExecutorContext::new(&client, Default::default(), program.language_version().unwrap()),
2774 )
2775 .await
2776 .unwrap();
2777 assert!(result.is_err(), "the test server deliberately rejects the upgrade");
2778 let request = tokio::time::timeout(std::time::Duration::from_secs(5), request)
2779 .await
2780 .unwrap()
2781 .unwrap();
2782 let target = request.lines().next().unwrap().split_whitespace().nth(1).unwrap();
2783 let url = url::Url::parse(&format!("http://localhost{target}")).unwrap();
2784 let versions: Vec<_> = url
2785 .query_pairs()
2786 .filter(|(key, _)| key == "kcl_version")
2787 .map(|(_, value)| value.into_owned())
2788 .collect();
2789 assert_eq!(versions, vec![expected]);
2790 }
2791 }
2792
2793 #[test]
2794 fn clone_with_fresh_execution_batch_keeps_executor_selection() {
2795 let mut ctx = new_mock_executor_context(None, machine::ExecutorKind::Machine);
2799 ctx.machine_call_depth_limit = 123;
2800 let cloned = ctx.clone_with_fresh_execution_batch();
2801 assert_eq!(cloned.executor_kind, machine::ExecutorKind::Machine);
2802 assert_eq!(cloned.machine_call_depth_limit, 123);
2803 }
2804
2805 #[tokio::test(flavor = "multi_thread")]
2806 async fn concurrent_foreign_import_preserves_artifact_command() {
2807 let tmpdir = tempfile::TempDir::with_prefix("zma_foreign_import_artifact").unwrap();
2808 tokio::fs::write(tmpdir.path().join("cube.obj"), "o cube\n")
2809 .await
2810 .unwrap();
2811
2812 let program = crate::Program::parse_no_errs("import \"cube.obj\" as cube\n\nmodel = cube\n").unwrap();
2813 let ctx = new_mock_executor_context(
2814 Some(crate::TypedPath(tmpdir.path().into())),
2815 machine::ExecutorKind::resolve(),
2816 );
2817 let mut exec_state = ExecState::new(&ctx);
2818 let (main_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2819 let outcome = exec_state
2820 .into_exec_outcome(main_ref, &ctx)
2821 .await
2822 .expect("foreign import execution should produce an outcome");
2823 ctx.close().await;
2824
2825 let KclValueView::ImportedGeometry(imported) = &outcome.variables["model"] else {
2826 panic!("model should be imported geometry");
2827 };
2828 let artifact_id = ArtifactId::new(imported.id);
2829 let Some(Artifact::ImportedGeometry(artifact)) = outcome.artifact_graph.get(&artifact_id) else {
2830 panic!("foreign import should produce an imported geometry artifact");
2831 };
2832 assert_eq!(artifact.id, artifact_id);
2833 assert!(!artifact.code_ref.node_path.is_empty());
2834 }
2835
2836 #[tokio::test(flavor = "multi_thread")]
2837 async fn nested_import_preserves_inner_error_and_backtrace() {
2838 let project_dir = crate::TypedPath::new("/zma-kcl-import-error");
2843 let main_path = project_dir.join("main.kcl");
2844 let assembly_path = project_dir.join("assembly.kcl");
2845 let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
2846 let files = [
2849 (
2850 project_dir.join("broken.kcl").to_string(),
2851 b"export brokenValue = missingName + 1\n".to_vec(),
2852 ),
2853 (
2854 assembly_path.to_string(),
2855 b"import brokenValue from \"broken.kcl\"\n\nexport assemblyValue = brokenValue\n".to_vec(),
2856 ),
2857 ]
2858 .into_iter()
2859 .collect();
2860 let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
2861 let settings = ExecutorSettings {
2862 project_directory: Some(project_dir),
2863 current_file: Some(main_path.clone()),
2864 ..Default::default()
2865 };
2866 let program = crate::Program::parse_no_errs(main_code).unwrap();
2867
2868 let assert_error = |error: &KclErrorWithOutputs| {
2869 let KclError::UndefinedValue { details, name } = &error.error else {
2870 panic!("expected UndefinedValue, got {:#?}", error.error);
2871 };
2872 assert_eq!(name.as_deref(), Some("missingName"));
2873 assert_eq!(details.message, "`missingName` is not defined");
2874 assert_eq!(
2875 error
2876 .error
2877 .backtrace()
2878 .iter()
2879 .map(|frame| frame.fn_name.as_deref())
2880 .collect::<Vec<_>>(),
2881 [Some("import broken.kcl"), Some("import assembly.kcl"), None]
2882 );
2883 assert_eq!(
2884 error
2885 .error
2886 .backtrace()
2887 .iter()
2888 .map(|frame| frame.kind)
2889 .collect::<Vec<_>>(),
2890 [
2891 kcl_error::BacktraceItemKind::Import,
2892 kcl_error::BacktraceItemKind::Import,
2893 kcl_error::BacktraceItemKind::Call
2894 ]
2895 );
2896
2897 let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
2898 assert!(report.filename.ends_with("broken.kcl"));
2899 assert_eq!(
2900 report
2901 .related
2902 .iter()
2903 .map(|related| related.filename.as_str())
2904 .collect::<Vec<_>>(),
2905 [assembly_path.to_string(), main_path.to_string()]
2906 );
2907
2908 let rendered = format!("{:?}", miette::Report::new(report));
2909 assert!(rendered.contains("broken.kcl"));
2910 assert!(rendered.contains("assembly.kcl"));
2911 assert!(rendered.contains("main.kcl"));
2912 assert!(rendered.contains("export brokenValue = missingName + 1"));
2913 assert!(!rendered.contains("Failed to read contents"));
2914 };
2915
2916 let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
2917 mock_ctx.fs = fs.clone();
2918 let mock_error = mock_ctx
2919 .run_mock(
2920 &program,
2921 &MockConfig {
2922 use_prev_memory: false,
2923 ..Default::default()
2924 },
2925 )
2926 .await
2927 .unwrap_err();
2928 mock_ctx.close().await;
2929 assert_error(&mock_error);
2930
2931 let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
2932 concurrent_ctx.fs = fs;
2933 let mut exec_state = ExecState::new(&concurrent_ctx);
2934 let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
2935 concurrent_ctx.close().await;
2936 assert_error(&concurrent_error);
2937 }
2938
2939 #[tokio::test(flavor = "multi_thread")]
2940 async fn function_error_across_import_keeps_backtrace_innermost_first() {
2941 let project_dir = crate::TypedPath::new("/zma-kcl-import-fn-error");
2945 let main_path = project_dir.join("main.kcl");
2946 let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
2947 let files = [
2948 (
2949 project_dir.join("helper.kcl").to_string(),
2950 b"export fn inner() { return missingName }\nexport fn outer() { return inner() }\n".to_vec(),
2951 ),
2952 (
2953 project_dir.join("assembly.kcl").to_string(),
2954 b"import outer from \"helper.kcl\"\n\nexport assemblyValue = outer()\n".to_vec(),
2955 ),
2956 ]
2957 .into_iter()
2958 .collect();
2959 let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
2960 let settings = ExecutorSettings {
2961 project_directory: Some(project_dir.clone()),
2962 current_file: Some(main_path),
2963 ..Default::default()
2964 };
2965 let program = crate::Program::parse_no_errs(main_code).unwrap();
2966
2967 let assert_error = |error: &KclErrorWithOutputs| {
2968 assert!(
2969 matches!(&error.error, KclError::UndefinedValue { .. }),
2970 "expected UndefinedValue, got {:#?}",
2971 error.error
2972 );
2973 assert_eq!(
2974 error
2975 .error
2976 .backtrace()
2977 .iter()
2978 .map(|frame| frame.fn_name.as_deref())
2979 .collect::<Vec<_>>(),
2980 [Some("inner"), Some("outer"), Some("import assembly.kcl"), None]
2981 );
2982 assert_eq!(
2983 error
2984 .error
2985 .backtrace()
2986 .iter()
2987 .map(|frame| frame.kind)
2988 .collect::<Vec<_>>(),
2989 [
2990 kcl_error::BacktraceItemKind::Call,
2991 kcl_error::BacktraceItemKind::Call,
2992 kcl_error::BacktraceItemKind::Import,
2993 kcl_error::BacktraceItemKind::Call
2994 ]
2995 );
2996
2997 let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
2998 assert!(report.filename.ends_with("helper.kcl"));
2999 assert_eq!(
3000 report
3001 .related
3002 .iter()
3003 .map(|related| related.filename.as_str())
3004 .collect::<Vec<_>>(),
3005 [
3006 project_dir.join("assembly.kcl").to_string(),
3007 project_dir.join("main.kcl").to_string()
3008 ]
3009 );
3010 let rendered = format!("{:?}", miette::Report::new(report));
3011 assert!(rendered.contains("return missingName"));
3012 assert!(!rendered.contains("Failed to read contents"));
3013 };
3014
3015 let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
3016 mock_ctx.fs = fs.clone();
3017 let mock_error = mock_ctx
3018 .run_mock(
3019 &program,
3020 &MockConfig {
3021 use_prev_memory: false,
3022 ..Default::default()
3023 },
3024 )
3025 .await
3026 .unwrap_err();
3027 mock_ctx.close().await;
3028 assert_error(&mock_error);
3029
3030 let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
3031 concurrent_ctx.fs = fs;
3032 let mut exec_state = ExecState::new(&concurrent_ctx);
3033 let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
3034 concurrent_ctx.close().await;
3035 assert_error(&concurrent_error);
3036 }
3037
3038 #[track_caller]
3040 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
3041 memory.memory.get_from_unchecked(name, env).unwrap()
3042 }
3043
3044 async fn execute_variables_with_backend(
3045 code: &str,
3046 backend: memory::MemoryBackendKind,
3047 ) -> IndexMap<String, KclValueView> {
3048 execute_outcome_with_backend(code, backend).await.variables
3049 }
3050
3051 async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
3052 let program = crate::Program::parse_no_errs(code).unwrap();
3053 let ctx = ExecutorContext::new_mock(None).await;
3054 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
3055 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3056 let outcome = exec_state
3057 .into_exec_outcome(env_ref, &ctx)
3058 .await
3059 .expect("test execution outcome should collect variables");
3060 ctx.close().await;
3061 outcome
3062 }
3063
3064 async fn execute_error_variables_with_backend(
3065 code: &str,
3066 backend: memory::MemoryBackendKind,
3067 ) -> IndexMap<String, KclValueView> {
3068 let program = crate::Program::parse_no_errs(code).unwrap();
3069 let ctx = ExecutorContext::new_mock(None).await;
3070 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
3071 let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
3072 ctx.close().await;
3073 error.variables
3074 }
3075
3076 async fn execute_project_variables_with_backend(
3077 main_code: &str,
3078 files: &[(&str, &str)],
3079 backend: memory::MemoryBackendKind,
3080 ) -> IndexMap<String, KclValueView> {
3081 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
3082 for (name, contents) in files {
3083 tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
3084 }
3085
3086 let program = crate::Program::parse_no_errs(main_code).unwrap();
3087 let ctx = ExecutorContext {
3088 engine: Arc::new(EngineManager::new_mock()),
3089 engine_batch: EngineBatchContext::default(),
3090 fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
3091 settings: ExecutorSettings {
3092 project_directory: Some(crate::TypedPath(tmpdir.path().into())),
3093 ..Default::default()
3094 },
3095 context_type: ContextType::Mock,
3096 execution_callbacks: Default::default(),
3097 executor_kind: machine::ExecutorKind::resolve(),
3098 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
3099 };
3100 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
3101 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3102 let outcome = exec_state
3103 .into_exec_outcome(env_ref, &ctx)
3104 .await
3105 .expect("test execution outcome should collect variables");
3106 ctx.close().await;
3107 outcome.variables
3108 }
3109
3110 async fn run_with_caching_variables_with_backend(
3111 code: &str,
3112 backend: memory::MemoryBackendKind,
3113 ) -> IndexMap<String, KclValueView> {
3114 let _backend = memory::MemoryBackendKind::override_for_test(backend);
3115 cache::bust_cache().await;
3116 clear_mem_cache().await;
3117
3118 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
3119 let program = crate::Program::parse_no_errs(code).unwrap();
3120 ctx.run_with_caching(program.clone()).await.unwrap();
3121 let cached = ctx.run_with_caching(program).await.unwrap();
3122
3123 cache::bust_cache().await;
3124 clear_mem_cache().await;
3125 ctx.close().await;
3126 cached.variables
3127 }
3128
3129 async fn run_mock_variables_with_backend(
3130 code: &str,
3131 backend: memory::MemoryBackendKind,
3132 ) -> IndexMap<String, KclValueView> {
3133 let _backend = memory::MemoryBackendKind::override_for_test(backend);
3134 clear_mem_cache().await;
3135
3136 let ctx = ExecutorContext::new_mock(None).await;
3137 let first = crate::Program::parse_no_errs("x = 2").unwrap();
3138 ctx.run_mock(
3139 &first,
3140 &MockConfig {
3141 use_prev_memory: false,
3142 ..Default::default()
3143 },
3144 )
3145 .await
3146 .unwrap();
3147
3148 let program = crate::Program::parse_no_errs(code).unwrap();
3149 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3150
3151 clear_mem_cache().await;
3152 ctx.close().await;
3153 outcome.variables
3154 }
3155
3156 fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
3157 let mut keys = variables.keys().cloned().collect::<Vec<_>>();
3158 keys.sort();
3159 keys
3160 }
3161
3162 async fn collect_backend_results<T, Fut>(
3163 mut run: impl FnMut(memory::MemoryBackendKind) -> Fut,
3164 ) -> Vec<(memory::MemoryBackendKind, T)>
3165 where
3166 Fut: std::future::Future<Output = T>,
3167 {
3168 let all = memory::MemoryBackendKind::all();
3169 let mut results = Vec::with_capacity(all.len());
3170 for &kind in all {
3171 results.push((kind, run(kind).await));
3172 }
3173 results
3174 }
3175
3176 fn assert_backend_results_match<T>(results: &[(memory::MemoryBackendKind, T)])
3177 where
3178 T: std::fmt::Debug + PartialEq,
3179 {
3180 let (first, rest) = results.split_first().expect("expected at least one memory backend");
3181 let (first_kind, first_result) = first;
3182 for (kind, result) in rest {
3183 assert_eq!(
3184 result, first_result,
3185 "memory kind {kind:?} doesn't match {first_kind:?}"
3186 );
3187 }
3188 }
3189
3190 fn assert_backend_variable_results_match_expected_keys(
3191 results: &[(memory::MemoryBackendKind, IndexMap<String, KclValueView>)],
3192 expected_keys: &[&str],
3193 ) {
3194 let (first_kind, first_variables) = results.first().expect("expected at least one memory backend");
3195 let expected_keys = expected_keys.iter().map(|key| (*key).to_owned()).collect::<Vec<_>>();
3196 assert_eq!(
3197 sorted_variable_keys(first_variables),
3198 expected_keys,
3199 "memory kind {first_kind:?} doesn't match expected variables"
3200 );
3201 assert_backend_results_match(results);
3202 }
3203
3204 fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
3205 let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
3206 let KclValueView::Number { value, .. } = value else {
3207 panic!("expected `{key}` to be a number, got {value:?}");
3208 };
3209 assert_eq!(*value, expected, "{key}: {value:?}");
3210 }
3211
3212 #[tokio::test(flavor = "multi_thread")]
3213 async fn exec_outcome_variables_match_between_memory_backends() {
3214 let code = "x = 2\ny = x + 1\narr = [x, y]";
3215
3216 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
3217
3218 assert_backend_variable_results_match_expected_keys(&results, &["arr", "x", "y"]);
3219 }
3220
3221 #[tokio::test(flavor = "multi_thread")]
3222 async fn error_output_variables_match_between_memory_backends() {
3223 let code = "x = 2\ny = missing + 1";
3224
3225 let results = collect_backend_results(|kind| execute_error_variables_with_backend(code, kind)).await;
3226
3227 assert_backend_variable_results_match_expected_keys(&results, &["x"]);
3228 }
3229
3230 #[tokio::test(flavor = "multi_thread")]
3231 async fn cached_execution_variables_match_between_memory_backends() {
3232 let code = "x = 2\ny = x + 1";
3233
3234 let results = collect_backend_results(|kind| run_with_caching_variables_with_backend(code, kind)).await;
3235
3236 assert_backend_variable_results_match_expected_keys(&results, &["x", "y"]);
3237 }
3238
3239 #[tokio::test(flavor = "multi_thread")]
3240 async fn mock_execution_variables_match_between_memory_backends() {
3241 let code = "y = x + 1";
3242
3243 let results = collect_backend_results(|kind| run_mock_variables_with_backend(code, kind)).await;
3244
3245 assert_backend_variable_results_match_expected_keys(&results, &["y"]);
3246 }
3247
3248 #[tokio::test(flavor = "multi_thread")]
3249 async fn module_imports_and_exported_closures_match_between_memory_backends() {
3250 let module_code = r#"
3251export base = 40
3252
3253export fn addBase(n) {
3254 return n + base
3255}
3256"#;
3257 let main_code = r#"
3258import base, addBase from 'math.kcl'
3259import 'math.kcl'
3260
3261named = addBase(n = 2)
3262qualified = math::addBase(n = 1)
3263direct = math::base
3264"#;
3265
3266 let files = [("math.kcl", module_code)];
3267 let results =
3268 collect_backend_results(|kind| execute_project_variables_with_backend(main_code, &files, kind)).await;
3269
3270 let (_, first_variables) = results.first().expect("expected at least one memory backend");
3271 assert_number_variable(first_variables, "named", 42.0);
3272 assert_number_variable(first_variables, "qualified", 41.0);
3273 assert_number_variable(first_variables, "direct", 40.0);
3274 assert_backend_results_match(&results);
3275 }
3276
3277 #[tokio::test(flavor = "multi_thread")]
3278 async fn sketch_block_variables_match_between_memory_backends() {
3279 let code = r#"
3280sketch001 = sketch(on = XY) {
3281 line1 = line(start = [0, 0], end = [1, 0])
3282 line2 = line(start = [1, 0], end = [0, 1])
3283}
3284lineCount = 2
3285"#;
3286
3287 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
3288
3289 let (_, first_variables) = results.first().expect("expected at least one memory backend");
3290 assert!(first_variables.contains_key("sketch001"), "actual: {first_variables:?}");
3291 assert_number_variable(first_variables, "lineCount", 2.0);
3292 assert_backend_results_match(&results);
3293 }
3294
3295 #[tokio::test(flavor = "multi_thread")]
3296 async fn tag_call_stack_lookup_matches_between_memory_backends() {
3297 let code = r#"
3298sketch001 = startSketchOn(XY)
3299 |> startProfile(at = [0, 0])
3300 |> xLine(length = 10, tag = $seg01)
3301
3302segLength = segLen(seg01)
3303"#;
3304
3305 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
3306
3307 let (_, first_variables) = results.first().expect("expected at least one memory backend");
3308 assert_number_variable(first_variables, "segLength", 10.0);
3309 assert_backend_results_match(&results);
3310 }
3311
3312 #[tokio::test(flavor = "multi_thread")]
3313 async fn test_execute_warn() {
3314 let text = "@blah";
3315 let result = parse_execute(text).await.unwrap();
3316 let errs = result.exec_state.issues();
3317 assert_eq!(errs.len(), 1);
3318 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
3319 assert!(
3320 errs[0].message.contains("Unknown annotation"),
3321 "unexpected warning message: {}",
3322 errs[0].message
3323 );
3324 }
3325
3326 #[tokio::test(flavor = "multi_thread")]
3327 async fn test_execute_fn_definitions() {
3328 let ast = r#"fn def(@x) {
3329 return x
3330}
3331fn ghi(@x) {
3332 return x
3333}
3334fn jkl(@x) {
3335 return x
3336}
3337fn hmm(@x) {
3338 return x
3339}
3340
3341yo = 5 + 6
3342
3343abc = 3
3344identifierGuy = 5
3345part001 = startSketchOn(XY)
3346|> startProfile(at = [-1.2, 4.83])
3347|> line(end = [2.8, 0])
3348|> angledLine(angle = 100 + 100, length = 3.01)
3349|> angledLine(angle = abc, length = 3.02)
3350|> angledLine(angle = def(yo), length = 3.03)
3351|> angledLine(angle = ghi(2), length = 3.04)
3352|> angledLine(angle = jkl(yo) + 2, length = 3.05)
3353|> close()
3354yo2 = hmm([identifierGuy + 5])"#;
3355
3356 parse_execute(ast).await.unwrap();
3357 }
3358
3359 #[tokio::test(flavor = "multi_thread")]
3360 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
3361 let code = r#"
3362firstProfile = sketch(on = XY) {
3363 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
3364 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
3365 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
3366 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
3367 coincident([edge1.end, edge2.start])
3368 coincident([edge2.end, edge3.start])
3369 coincident([edge3.end, edge4.start])
3370 coincident([edge4.end, edge1.start])
3371}
3372
3373secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
3374 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
3375 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
3376 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
3377 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
3378 coincident([edge5.end, edge6.start])
3379 coincident([edge6.end, edge7.start])
3380 coincident([edge7.end, edge8.start])
3381 coincident([edge8.end, edge5.start])
3382}
3383
3384firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
3385secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
3386"#;
3387
3388 let result = parse_execute(code).await.unwrap();
3389 assert!(result.exec_state.issues().is_empty());
3390 }
3391
3392 #[tokio::test(flavor = "multi_thread")]
3393 async fn sketch_block_artifact_preserves_standard_plane_name() {
3394 let code = r#"
3395sketch001 = sketch(on = -YZ) {
3396 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
3397}
3398"#;
3399
3400 let result = parse_execute(code).await.unwrap();
3401 let sketch_blocks = result
3402 .exec_state
3403 .global
3404 .artifacts
3405 .graph
3406 .values()
3407 .filter_map(|artifact| match artifact {
3408 Artifact::SketchBlock(block) => Some(block),
3409 _ => None,
3410 })
3411 .collect::<Vec<_>>();
3412
3413 assert_eq!(sketch_blocks.len(), 1);
3414 assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
3415 }
3416
3417 #[tokio::test(flavor = "multi_thread")]
3418 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
3419 let code = r#"
3420sketch001 = sketch(on = YZ) {
3421 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
3422 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
3423 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
3424 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
3425 coincident([line1.end, line2.start])
3426 coincident([line2.end, line3.start])
3427 coincident([line3.end, line4.start])
3428 coincident([line4.end, line1.start])
3429}
3430
3431sketch002 = sketch(on = -XZ) {
3432 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
3433 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
3434 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
3435 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
3436 coincident([line5.end, line6.start])
3437 coincident([line6.end, line7.start])
3438 coincident([line7.end, line8.start])
3439 coincident([line8.end, line5.start])
3440}
3441
3442region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
3443extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
3444region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
3445extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
3446
3447myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
3448"#;
3449
3450 let result = parse_execute(code).await.unwrap();
3451 assert!(result.exec_state.issues().is_empty());
3452 }
3453
3454 #[tokio::test(flavor = "multi_thread")]
3455 async fn issue_10741_point_circle_coincident_executes() {
3456 let code = r#"
3457sketch001 = sketch(on = YZ) {
3458 circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
3459 line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
3460 coincident([line1.start, circle1])
3461}
3462"#;
3463
3464 let result = parse_execute(code).await.unwrap();
3465 assert!(
3466 result
3467 .exec_state
3468 .issues()
3469 .iter()
3470 .all(|issue| issue.severity != Severity::Error),
3471 "unexpected execution issues: {:#?}",
3472 result.exec_state.issues()
3473 );
3474 }
3475
3476 #[tokio::test(flavor = "multi_thread")]
3477 async fn test_execute_with_pipe_substitutions_unary() {
3478 let ast = r#"myVar = 3
3479part001 = startSketchOn(XY)
3480 |> startProfile(at = [0, 0])
3481 |> line(end = [3, 4], tag = $seg01)
3482 |> line(end = [
3483 min([segLen(seg01), myVar]),
3484 -legLen(hypotenuse = segLen(seg01), leg = myVar)
3485])
3486"#;
3487
3488 parse_execute(ast).await.unwrap();
3489 }
3490
3491 #[tokio::test(flavor = "multi_thread")]
3492 async fn test_execute_with_pipe_substitutions() {
3493 let ast = r#"myVar = 3
3494part001 = startSketchOn(XY)
3495 |> startProfile(at = [0, 0])
3496 |> line(end = [3, 4], tag = $seg01)
3497 |> line(end = [
3498 min([segLen(seg01), myVar]),
3499 legLen(hypotenuse = segLen(seg01), leg = myVar)
3500])
3501"#;
3502
3503 parse_execute(ast).await.unwrap();
3504 }
3505
3506 #[tokio::test(flavor = "multi_thread")]
3507 async fn test_execute_with_inline_comment() {
3508 let ast = r#"baseThick = 1
3509armAngle = 60
3510
3511baseThickHalf = baseThick / 2
3512halfArmAngle = armAngle / 2
3513
3514arrExpShouldNotBeIncluded = [1, 2, 3]
3515objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
3516
3517part001 = startSketchOn(XY)
3518 |> startProfile(at = [0, 0])
3519 |> yLine(endAbsolute = 1)
3520 |> xLine(length = 3.84) // selection-range-7ish-before-this
3521
3522variableBelowShouldNotBeIncluded = 3
3523"#;
3524
3525 parse_execute(ast).await.unwrap();
3526 }
3527
3528 #[tokio::test(flavor = "multi_thread")]
3529 async fn test_execute_with_function_literal_in_pipe() {
3530 let ast = r#"w = 20
3531l = 8
3532h = 10
3533
3534fn thing() {
3535 return -8
3536}
3537
3538firstExtrude = startSketchOn(XY)
3539 |> startProfile(at = [0,0])
3540 |> line(end = [0, l])
3541 |> line(end = [w, 0])
3542 |> line(end = [0, thing()])
3543 |> close()
3544 |> extrude(length = h)"#;
3545
3546 parse_execute(ast).await.unwrap();
3547 }
3548
3549 #[tokio::test(flavor = "multi_thread")]
3550 async fn test_execute_with_function_unary_in_pipe() {
3551 let ast = r#"w = 20
3552l = 8
3553h = 10
3554
3555fn thing(@x) {
3556 return -x
3557}
3558
3559firstExtrude = startSketchOn(XY)
3560 |> startProfile(at = [0,0])
3561 |> line(end = [0, l])
3562 |> line(end = [w, 0])
3563 |> line(end = [0, thing(8)])
3564 |> close()
3565 |> extrude(length = h)"#;
3566
3567 parse_execute(ast).await.unwrap();
3568 }
3569
3570 #[tokio::test(flavor = "multi_thread")]
3571 async fn test_execute_with_function_array_in_pipe() {
3572 let ast = r#"w = 20
3573l = 8
3574h = 10
3575
3576fn thing(@x) {
3577 return [0, -x]
3578}
3579
3580firstExtrude = startSketchOn(XY)
3581 |> startProfile(at = [0,0])
3582 |> line(end = [0, l])
3583 |> line(end = [w, 0])
3584 |> line(end = thing(8))
3585 |> close()
3586 |> extrude(length = h)"#;
3587
3588 parse_execute(ast).await.unwrap();
3589 }
3590
3591 #[tokio::test(flavor = "multi_thread")]
3592 async fn test_execute_with_function_call_in_pipe() {
3593 let ast = r#"w = 20
3594l = 8
3595h = 10
3596
3597fn other_thing(@y) {
3598 return -y
3599}
3600
3601fn thing(@x) {
3602 return other_thing(x)
3603}
3604
3605firstExtrude = startSketchOn(XY)
3606 |> startProfile(at = [0,0])
3607 |> line(end = [0, l])
3608 |> line(end = [w, 0])
3609 |> line(end = [0, thing(8)])
3610 |> close()
3611 |> extrude(length = h)"#;
3612
3613 parse_execute(ast).await.unwrap();
3614 }
3615
3616 #[tokio::test(flavor = "multi_thread")]
3617 async fn test_execute_with_function_sketch() {
3618 let ast = r#"fn box(h, l, w) {
3619 myBox = startSketchOn(XY)
3620 |> startProfile(at = [0,0])
3621 |> line(end = [0, l])
3622 |> line(end = [w, 0])
3623 |> line(end = [0, -l])
3624 |> close()
3625 |> extrude(length = h)
3626
3627 return myBox
3628}
3629
3630fnBox = box(h = 3, l = 6, w = 10)"#;
3631
3632 parse_execute(ast).await.unwrap();
3633 }
3634
3635 #[tokio::test(flavor = "multi_thread")]
3636 async fn test_get_member_of_object_with_function_period() {
3637 let ast = r#"fn box(@obj) {
3638 myBox = startSketchOn(XY)
3639 |> startProfile(at = obj.start)
3640 |> line(end = [0, obj.l])
3641 |> line(end = [obj.w, 0])
3642 |> line(end = [0, -obj.l])
3643 |> close()
3644 |> extrude(length = obj.h)
3645
3646 return myBox
3647}
3648
3649thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
3650"#;
3651 parse_execute(ast).await.unwrap();
3652 }
3653
3654 #[tokio::test(flavor = "multi_thread")]
3655 #[ignore] async fn test_object_member_starting_pipeline() {
3657 let ast = r#"
3658fn test2() {
3659 return {
3660 thing: startSketchOn(XY)
3661 |> startProfile(at = [0, 0])
3662 |> line(end = [0, 1])
3663 |> line(end = [1, 0])
3664 |> line(end = [0, -1])
3665 |> close()
3666 }
3667}
3668
3669x2 = test2()
3670
3671x2.thing
3672 |> extrude(length = 10)
3673"#;
3674 parse_execute(ast).await.unwrap();
3675 }
3676
3677 #[tokio::test(flavor = "multi_thread")]
3678 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
3680 let ast = r#"fn box(obj) {
3681let myBox = startSketchOn(XY)
3682 |> startProfile(at = obj.start)
3683 |> line(end = [0, obj.l])
3684 |> line(end = [obj.w, 0])
3685 |> line(end = [0, -obj.l])
3686 |> close()
3687 |> extrude(length = obj.h)
3688
3689 return myBox
3690}
3691
3692for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
3693 thisBox = box(var)
3694}"#;
3695
3696 parse_execute(ast).await.unwrap();
3697 }
3698
3699 #[tokio::test(flavor = "multi_thread")]
3700 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
3702 let ast = r#"fn box(h, l, w, start) {
3703 myBox = startSketchOn(XY)
3704 |> startProfile(at = [0,0])
3705 |> line(end = [0, l])
3706 |> line(end = [w, 0])
3707 |> line(end = [0, -l])
3708 |> close()
3709 |> extrude(length = h)
3710
3711 return myBox
3712}
3713
3714
3715for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
3716 const thisBox = box(var[0], var[1], var[2], var[3])
3717}"#;
3718
3719 parse_execute(ast).await.unwrap();
3720 }
3721
3722 #[tokio::test(flavor = "multi_thread")]
3723 async fn test_get_member_of_array_with_function() {
3724 let ast = r#"fn box(@arr) {
3725 myBox =startSketchOn(XY)
3726 |> startProfile(at = arr[0])
3727 |> line(end = [0, arr[1]])
3728 |> line(end = [arr[2], 0])
3729 |> line(end = [0, -arr[1]])
3730 |> close()
3731 |> extrude(length = arr[3])
3732
3733 return myBox
3734}
3735
3736thisBox = box([[0,0], 6, 10, 3])
3737
3738"#;
3739 parse_execute(ast).await.unwrap();
3740 }
3741
3742 #[tokio::test(flavor = "multi_thread")]
3743 async fn test_function_cannot_access_future_definitions() {
3744 let ast = r#"
3745fn returnX() {
3746 // x shouldn't be defined yet.
3747 return x
3748}
3749
3750x = 5
3751
3752answer = returnX()"#;
3753
3754 let result = parse_execute(ast).await;
3755 let err = result.unwrap_err();
3756 assert_eq!(err.message(), "`x` is not defined");
3757 }
3758
3759 #[tokio::test(flavor = "multi_thread")]
3760 async fn test_override_prelude() {
3761 let text = "PI = 3.0";
3762 let result = parse_execute(text).await.unwrap();
3763 let issues = result.exec_state.issues();
3764 assert!(issues.is_empty(), "issues={issues:#?}");
3765 }
3766
3767 #[tokio::test(flavor = "multi_thread")]
3768 async fn type_aliases() {
3769 let text = r#"@settings(kclVersion = "3.0-preview")
3770type MyTy = [number; 2]
3771fn foo(@x: MyTy) {
3772 return x[0]
3773}
3774
3775foo([0, 1])
3776
3777type Other = MyTy | Helix
3778"#;
3779 let result = parse_execute(text).await.unwrap();
3780 let issues = result.exec_state.issues();
3781 assert!(issues.is_empty(), "issues={issues:#?}");
3782 }
3783
3784 #[tokio::test(flavor = "multi_thread")]
3785 async fn test_cannot_shebang_in_fn() {
3786 let ast = r#"
3787fn foo() {
3788 #!hello
3789 return true
3790}
3791
3792foo
3793"#;
3794
3795 let result = parse_execute(ast).await;
3796 let err = result.unwrap_err();
3797 assert_eq!(
3798 err,
3799 KclError::new_syntax(KclErrorDetails::new(
3800 "Unexpected token: #".to_owned(),
3801 vec![SourceRange::new(14, 15, ModuleId::default())],
3802 )),
3803 );
3804 }
3805
3806 #[tokio::test(flavor = "multi_thread")]
3807 async fn test_pattern_transform_function_cannot_access_future_definitions() {
3808 let ast = r#"
3809fn transform(@replicaId) {
3810 // x shouldn't be defined yet.
3811 scale = x
3812 return {
3813 translate = [0, 0, replicaId * 10],
3814 scale = [scale, 1, 0],
3815 }
3816}
3817
3818fn layer() {
3819 return startSketchOn(XY)
3820 |> circle( center= [0, 0], radius= 1, tag = $tag1)
3821 |> extrude(length = 10)
3822}
3823
3824x = 5
3825
3826// The 10 layers are replicas of each other, with a transform applied to each.
3827shape = layer() |> patternTransform(instances = 10, transform = transform)
3828"#;
3829
3830 let result = parse_execute(ast).await;
3831 let err = result.unwrap_err();
3832 assert_eq!(err.message(), "`x` is not defined",);
3833 }
3834
3835 #[tokio::test(flavor = "multi_thread")]
3838 async fn test_math_execute_with_functions() {
3839 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
3840 let result = parse_execute(ast).await.unwrap();
3841 assert_eq!(
3842 5.0,
3843 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3844 .as_f64()
3845 .unwrap()
3846 );
3847 }
3848
3849 #[tokio::test(flavor = "multi_thread")]
3850 async fn test_math_execute() {
3851 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
3852 let result = parse_execute(ast).await.unwrap();
3853 assert_eq!(
3854 7.4,
3855 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3856 .as_f64()
3857 .unwrap()
3858 );
3859 }
3860
3861 #[tokio::test(flavor = "multi_thread")]
3862 async fn test_string_uppercase() {
3863 let composed = "\u{e9}";
3864 let uppercase_composed = "\u{c9}";
3865 let decomposed = "e\u{301}";
3866 let uppercase_decomposed = "E\u{301}";
3867 let code = format!(
3868 r#"
3869ascii = string::uppercase("Kcl")
3870unicode_expansion = string::uppercase("Straße")
3871uncased = string::uppercase("東京")
3872empty = string::uppercase("")
3873composed = string::uppercase("{composed}")
3874decomposed = string::uppercase("{decomposed}")
3875piped = "ready" |> string::uppercase()
3876"#
3877 );
3878
3879 let result = parse_execute(&code).await.unwrap();
3880 for (name, expected) in [
3881 ("ascii", "KCL"),
3882 ("unicode_expansion", "STRASSE"),
3883 ("uncased", "東京"),
3884 ("empty", ""),
3885 ("composed", uppercase_composed),
3886 ("decomposed", uppercase_decomposed),
3887 ("piped", "READY"),
3888 ] {
3889 assert_eq!(
3890 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3891 .as_str()
3892 .unwrap(),
3893 expected,
3894 "{name}"
3895 );
3896 }
3897 }
3898
3899 #[tokio::test(flavor = "multi_thread")]
3900 async fn test_string_lowercase() {
3901 let composed = "\u{c9}";
3902 let lowercase_composed = "\u{e9}";
3903 let decomposed = "E\u{301}";
3904 let lowercase_decomposed = "e\u{301}";
3905 let expanded = "i\u{307}";
3906 let code = format!(
3907 r#"
3908ascii = string::lowercase("KCL")
3909final_sigma = string::lowercase("ΟΣ")
3910medial_sigma = string::lowercase("ΟΣΑ")
3911unicode_expansion = string::lowercase("İ")
3912uncased = string::lowercase("東京")
3913empty = string::lowercase("")
3914composed = string::lowercase("{composed}")
3915decomposed = string::lowercase("{decomposed}")
3916piped = "READY" |> string::lowercase()
3917"#
3918 );
3919
3920 let result = parse_execute(&code).await.unwrap();
3921 for (name, expected) in [
3922 ("ascii", "kcl"),
3923 ("final_sigma", "ος"),
3924 ("medial_sigma", "οσα"),
3925 ("unicode_expansion", expanded),
3926 ("uncased", "東京"),
3927 ("empty", ""),
3928 ("composed", lowercase_composed),
3929 ("decomposed", lowercase_decomposed),
3930 ("piped", "ready"),
3931 ] {
3932 assert_eq!(
3933 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3934 .as_str()
3935 .unwrap(),
3936 expected,
3937 "{name}"
3938 );
3939 }
3940 }
3941
3942 #[tokio::test(flavor = "multi_thread")]
3943 async fn test_string_is_equal() {
3944 let composed = "\u{e9}";
3945 let decomposed = "e\u{301}";
3946 let code = format!(
3947 r#"
3948exact_same = string::isEqual("KCL", to = "KCL")
3949exact_different_case = string::isEqual("KCL", to = "kcl")
3950explicit_case_sensitive = string::isEqual("KCL", to = "kcl", caseInsensitive = false)
3951case_insensitive_ascii = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3952case_fold_expansion = string::isEqual("Straße", to = "STRASSE", caseInsensitive = true)
3953case_fold_expansion_reversed = string::isEqual("STRASSE", to = "Straße", caseInsensitive = true)
3954case_fold_sigma = string::isEqual("ος", to = "οσ", caseInsensitive = true)
3955case_fold_non_turkic = string::isEqual("I", to = "i", caseInsensitive = true)
3956case_fold_not_turkic = string::isEqual("I", to = "ı", caseInsensitive = true)
3957empty_same = string::isEqual("", to = "")
3958empty_different = string::isEqual("", to = "KCL")
3959exact_without_normalization = string::isEqual("{composed}", to = "{decomposed}")
3960case_fold_without_normalization = string::isEqual("{composed}", to = "{decomposed}", caseInsensitive = true)
3961piped = "ready" |> string::isEqual(to = "READY", caseInsensitive = true)
3962"#
3963 );
3964
3965 let result = parse_execute(&code).await.unwrap();
3966 for (name, expected) in [
3967 ("exact_same", true),
3968 ("exact_different_case", false),
3969 ("explicit_case_sensitive", false),
3970 ("case_insensitive_ascii", true),
3971 ("case_fold_expansion", true),
3972 ("case_fold_expansion_reversed", true),
3973 ("case_fold_sigma", true),
3974 ("case_fold_non_turkic", true),
3975 ("case_fold_not_turkic", false),
3976 ("empty_same", true),
3977 ("empty_different", false),
3978 ("exact_without_normalization", false),
3979 ("case_fold_without_normalization", false),
3980 ("piped", true),
3981 ] {
3982 assert_eq!(
3983 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3984 .as_bool()
3985 .unwrap(),
3986 expected,
3987 "{name}"
3988 );
3989 }
3990 }
3991
3992 #[tokio::test(flavor = "multi_thread")]
3993 async fn test_string_is_equal_inside_sketch_block_is_predicate() {
3994 let code = r#"
3995@settings(experimentalFeatures = allow)
3996
3997sketch(on = XY) {
3998 stringsAreEqual = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3999}
4000"#;
4001
4002 parse_execute(code).await.unwrap();
4003 }
4004
4005 #[tokio::test(flavor = "multi_thread")]
4006 async fn test_string_trim() {
4007 let ascii_whitespace = " \t\n";
4008 let tab = "\t";
4009 let non_breaking_space = "\u{a0}";
4010 let em_space = "\u{2003}";
4011 let ideographic_space = "\u{3000}";
4012 let zero_width_space = "\u{200b}";
4013 let decomposed = "e\u{301}";
4014 let code = format!(
4015 r#"
4016ascii = string::trim("{ascii_whitespace}KCL{ascii_whitespace}")
4017internal = string::trim(" KCL{tab}strings ")
4018unicode = string::trim("{non_breaking_space}{em_space}KCL{ideographic_space}")
4019all_whitespace = string::trim("{ascii_whitespace}{non_breaking_space}")
4020empty = string::trim("")
4021unchanged = string::trim("KCL")
4022without_normalization = string::trim(" {decomposed} ")
4023non_whitespace = string::trim("{zero_width_space}KCL{zero_width_space}")
4024piped = " ready " |> string::trim()
4025"#
4026 );
4027
4028 let result = parse_execute(&code).await.unwrap();
4029 let non_whitespace = format!("{zero_width_space}KCL{zero_width_space}");
4030 for (name, expected) in [
4031 ("ascii", "KCL"),
4032 ("internal", "KCL\tstrings"),
4033 ("unicode", "KCL"),
4034 ("all_whitespace", ""),
4035 ("empty", ""),
4036 ("unchanged", "KCL"),
4037 ("without_normalization", decomposed),
4038 ("non_whitespace", non_whitespace.as_str()),
4039 ("piped", "ready"),
4040 ] {
4041 assert_eq!(
4042 mem_get_json(result.exec_state.stack(), result.mem_env, name)
4043 .as_str()
4044 .unwrap(),
4045 expected,
4046 "{name}"
4047 );
4048 }
4049 }
4050
4051 #[tokio::test(flavor = "multi_thread")]
4052 async fn test_string_trim_start() {
4053 let ascii_whitespace = " \t\n";
4054 let tab = "\t";
4055 let non_breaking_space = "\u{a0}";
4056 let em_space = "\u{2003}";
4057 let ideographic_space = "\u{3000}";
4058 let zero_width_space = "\u{200b}";
4059 let decomposed = "e\u{301}";
4060 let code = format!(
4061 r#"
4062ascii = string::trimStart("{ascii_whitespace}KCL{ascii_whitespace}")
4063internal = string::trimStart(" KCL{tab}strings")
4064unicode = string::trimStart("{non_breaking_space}{em_space}KCL{ideographic_space}")
4065all_whitespace = string::trimStart("{ascii_whitespace}{non_breaking_space}")
4066empty = string::trimStart("")
4067unchanged = string::trimStart("KCL")
4068without_normalization = string::trimStart(" {decomposed}")
4069non_whitespace_prefix = string::trimStart("{zero_width_space}{ascii_whitespace}KCL")
4070piped = " ready " |> string::trimStart()
4071"#
4072 );
4073
4074 let result = parse_execute(&code).await.unwrap();
4075 let ascii = format!("KCL{ascii_whitespace}");
4076 let unicode = format!("KCL{ideographic_space}");
4077 let non_whitespace_prefix = format!("{zero_width_space}{ascii_whitespace}KCL");
4078 for (name, expected) in [
4079 ("ascii", ascii.as_str()),
4080 ("internal", "KCL\tstrings"),
4081 ("unicode", unicode.as_str()),
4082 ("all_whitespace", ""),
4083 ("empty", ""),
4084 ("unchanged", "KCL"),
4085 ("without_normalization", decomposed),
4086 ("non_whitespace_prefix", non_whitespace_prefix.as_str()),
4087 ("piped", "ready "),
4088 ] {
4089 assert_eq!(
4090 mem_get_json(result.exec_state.stack(), result.mem_env, name)
4091 .as_str()
4092 .unwrap(),
4093 expected,
4094 "{name}"
4095 );
4096 }
4097 }
4098
4099 #[tokio::test(flavor = "multi_thread")]
4100 async fn test_string_trim_end() {
4101 let ascii_whitespace = " \t\n";
4102 let tab = "\t";
4103 let non_breaking_space = "\u{a0}";
4104 let em_space = "\u{2003}";
4105 let ideographic_space = "\u{3000}";
4106 let zero_width_space = "\u{200b}";
4107 let decomposed = "e\u{301}";
4108 let code = format!(
4109 r#"
4110ascii = string::trimEnd("{ascii_whitespace}KCL{ascii_whitespace}")
4111internal = string::trimEnd("KCL{tab}strings ")
4112unicode = string::trimEnd("{non_breaking_space}KCL{em_space}{ideographic_space}")
4113all_whitespace = string::trimEnd("{ascii_whitespace}{non_breaking_space}")
4114empty = string::trimEnd("")
4115unchanged = string::trimEnd("KCL")
4116without_normalization = string::trimEnd("{decomposed} ")
4117non_whitespace_suffix = string::trimEnd("KCL{ascii_whitespace}{zero_width_space}")
4118piped = " ready " |> string::trimEnd()
4119"#
4120 );
4121
4122 let result = parse_execute(&code).await.unwrap();
4123 let ascii = format!("{ascii_whitespace}KCL");
4124 let unicode = format!("{non_breaking_space}KCL");
4125 let non_whitespace_suffix = format!("KCL{ascii_whitespace}{zero_width_space}");
4126 for (name, expected) in [
4127 ("ascii", ascii.as_str()),
4128 ("internal", "KCL\tstrings"),
4129 ("unicode", unicode.as_str()),
4130 ("all_whitespace", ""),
4131 ("empty", ""),
4132 ("unchanged", "KCL"),
4133 ("without_normalization", decomposed),
4134 ("non_whitespace_suffix", non_whitespace_suffix.as_str()),
4135 ("piped", " ready"),
4136 ] {
4137 assert_eq!(
4138 mem_get_json(result.exec_state.stack(), result.mem_env, name)
4139 .as_str()
4140 .unwrap(),
4141 expected,
4142 "{name}"
4143 );
4144 }
4145 }
4146
4147 #[tokio::test(flavor = "multi_thread")]
4148 async fn test_string_to_string() {
4149 for (name, expr, expected) in [
4152 ("unitless integer", "12", "12"),
4155 ("unitless fractional", "1.5", "1.5"),
4156 ("no digits dropped", "0.1 + 0.2", "0.30000000000000004"),
4157 ("unitless negative", "-7", "-7"),
4158 ("unitless zero", "0", "0"),
4159 ("negative zero", "-0", "0"),
4160 ("count", "3_", "3_"),
4161 ("millimeters", "12mm", "12mm"),
4162 ("centimeters", "12cm", "12cm"),
4163 ("meters", "12m", "12m"),
4164 ("inches", "1.5in", "1.5in"),
4165 ("feet", "2ft", "2ft"),
4166 ("yards", "3yd", "3yd"),
4167 ("degrees", "90deg", "90deg"),
4168 ("radians", "1.5rad", "1.5rad"),
4169 ("length arithmetic", "2mm + 10mm", "12mm"),
4171 ("units the type system loses", "2mm * 10mm", "20"),
4174 ("unitless arithmetic", "1 + 2", "3"),
4175 ] {
4176 let code = format!("actual = string::toString({expr})");
4177 let result = parse_execute(&code).await.unwrap();
4178
4179 assert_eq!(
4180 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4181 .as_str()
4182 .unwrap(),
4183 expected,
4184 "case: {name}"
4185 );
4186 }
4187 }
4188
4189 #[tokio::test(flavor = "multi_thread")]
4190 async fn test_string_to_string_ignores_the_files_default_unit() {
4191 let code = "@settings(defaultLengthUnit = inch)\nactual = string::toString(12)";
4196 let result = parse_execute(code).await.unwrap();
4197
4198 assert_eq!(
4199 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4200 .as_str()
4201 .unwrap(),
4202 "12"
4203 );
4204 }
4205
4206 #[tokio::test(flavor = "multi_thread")]
4207 async fn test_string_to_string_rejects_a_non_number() {
4208 let error = parse_execute(r#"actual = string::toString("already text")"#)
4209 .await
4210 .unwrap_err();
4211
4212 assert_eq!(
4215 error.message(),
4216 "The input argument of `string::toString` requires a value with type `number`, but found a value with type `string`."
4217 );
4218 assert!(
4219 matches!(error, KclError::Argument { .. }),
4220 "expected an Argument error, found {error:?}"
4221 );
4222 }
4223
4224 #[tokio::test(flavor = "multi_thread")]
4225 async fn test_string_to_string_accepts_a_piped_argument() {
4226 let result = parse_execute("actual = 12mm |> string::toString()").await.unwrap();
4227
4228 assert_eq!(
4229 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4230 .as_str()
4231 .unwrap(),
4232 "12mm"
4233 );
4234 }
4235
4236 #[tokio::test(flavor = "multi_thread")]
4237 async fn test_string_to_string_echoes_how_the_literal_was_written() {
4238 for literal in [
4242 "12",
4243 "1.5",
4244 "0.30000000000000004",
4245 "3_",
4246 "2.5_",
4249 "-4_",
4250 "12mm",
4251 "-5mm",
4252 "1.5in",
4253 "90deg",
4254 "1.5rad",
4255 ] {
4256 let code = format!("actual = string::toString({literal})");
4257 let result = parse_execute(&code).await.unwrap();
4258
4259 assert_eq!(
4260 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4261 .as_str()
4262 .unwrap(),
4263 literal,
4264 "literal: {literal}"
4265 );
4266 }
4267 }
4268
4269 #[tokio::test(flavor = "multi_thread")]
4270 async fn test_string_to_string_spells_out_non_finite_numbers() {
4271 for (name, expr, expected) in [
4275 ("positive infinity", "1 / 0", "Infinity"),
4276 ("negative infinity", "-1 / 0", "-Infinity"),
4277 ("nan", "0 / 0", "NaN"),
4278 ("infinity from a length", "1mm / 0", "Infinity"),
4280 ("nan from a length", "0mm / 0", "NaN"),
4281 ("infinity from an angle", "1deg / 0", "Infinity"),
4282 ] {
4283 let code = format!("actual = string::toString({expr})");
4284 let result = parse_execute(&code).await.unwrap();
4285
4286 assert_eq!(
4287 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
4288 .as_str()
4289 .unwrap(),
4290 expected,
4291 "case: {name}"
4292 );
4293 }
4294 }
4295
4296 #[tokio::test(flavor = "multi_thread")]
4297 async fn test_string_equality_operators() {
4298 let composed = "\u{e9}";
4299 let decomposed = "e\u{301}";
4300 let code = format!(
4301 r#"
4302equal_same_ascii = "KCL" == "KCL"
4303equal_different_case = "KCL" == "kcl"
4304not_equal_same_ascii = "KCL" != "KCL"
4305not_equal_different_case = "KCL" != "kcl"
4306equal_same_unicode = "{composed}" == "{composed}"
4307not_equal_same_unicode = "{composed}" != "{composed}"
4308equal_without_normalization = "{composed}" == "{decomposed}"
4309not_equal_without_normalization = "{composed}" != "{decomposed}"
4310"#
4311 );
4312
4313 let result = parse_execute(&code).await.unwrap();
4314 for (name, expected) in [
4315 ("equal_same_ascii", true),
4316 ("equal_different_case", false),
4317 ("not_equal_same_ascii", false),
4318 ("not_equal_different_case", true),
4319 ("equal_same_unicode", true),
4320 ("not_equal_same_unicode", false),
4321 ("equal_without_normalization", false),
4322 ("not_equal_without_normalization", true),
4323 ] {
4324 assert_eq!(
4325 mem_get_json(result.exec_state.stack(), result.mem_env, name)
4326 .as_bool()
4327 .unwrap(),
4328 expected,
4329 "{name}"
4330 );
4331 }
4332 }
4333
4334 #[tokio::test(flavor = "multi_thread")]
4335 async fn test_string_equality_inside_sketch_block_fails_like_number_equality() {
4336 let string_code = r#"
4337@settings(experimentalFeatures = allow)
4338
4339sketch(on = XY) {
4340 stringsAreEqual = "KCL" == "KCL"
4341}
4342"#;
4343 let number_code = r#"
4344@settings(experimentalFeatures = allow)
4345
4346sketch(on = XY) {
4347 numbersAreEqual = 1 == 1
4348}
4349"#;
4350
4351 assert_eq!(
4352 parse_execute(string_code).await.unwrap_err().message(),
4353 "Cannot create an equivalence constraint between values of these types: a string and a string"
4354 );
4355 assert_eq!(
4356 parse_execute(number_code).await.unwrap_err().message(),
4357 "Cannot create an equivalence constraint between values of these types: a number and a number"
4358 );
4359 }
4360
4361 #[tokio::test(flavor = "multi_thread")]
4362 async fn test_math_execute_start_negative() {
4363 let ast = r#"myVar = -5 + 6"#;
4364 let result = parse_execute(ast).await.unwrap();
4365 assert_eq!(
4366 1.0,
4367 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4368 .as_f64()
4369 .unwrap()
4370 );
4371 }
4372
4373 #[tokio::test(flavor = "multi_thread")]
4374 async fn test_math_execute_with_pi() {
4375 let ast = r#"myVar = PI * 2"#;
4376 let result = parse_execute(ast).await.unwrap();
4377 assert_eq!(
4378 std::f64::consts::TAU,
4379 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4380 .as_f64()
4381 .unwrap()
4382 );
4383 }
4384
4385 #[tokio::test(flavor = "multi_thread")]
4386 async fn test_math_define_decimal_without_leading_zero() {
4387 let ast = r#"thing = .4 + 7"#;
4388 let result = parse_execute(ast).await.unwrap();
4389 assert_eq!(
4390 7.4,
4391 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
4392 .as_f64()
4393 .unwrap()
4394 );
4395 }
4396
4397 #[tokio::test(flavor = "multi_thread")]
4398 async fn pass_std_to_std() {
4399 let ast = r#"sketch001 = startSketchOn(XY)
4400profile001 = circle(sketch001, center = [0, 0], radius = 2)
4401extrude001 = extrude(profile001, length = 5)
4402extrudes = patternLinear3d(
4403 extrude001,
4404 instances = 3,
4405 distance = 5,
4406 axis = [1, 1, 0],
4407)
4408clone001 = map(extrudes, f = clone)
4409"#;
4410 parse_execute(ast).await.unwrap();
4411 }
4412
4413 #[tokio::test(flavor = "multi_thread")]
4414 async fn test_array_reduce_nested_array() {
4415 let code = r#"
4416fn id(@el, accum) { return accum }
4417
4418answer = reduce([], initial=[[[0,0]]], f=id)
4419"#;
4420 let result = parse_execute(code).await.unwrap();
4421 assert_eq!(
4422 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
4423 KclValue::HomArray {
4424 value: vec![KclValue::HomArray {
4425 value: vec![KclValue::HomArray {
4426 value: vec![
4427 KclValue::Number {
4428 value: 0.0,
4429 ty: NumericType::default(),
4430 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
4431 },
4432 KclValue::Number {
4433 value: 0.0,
4434 ty: NumericType::default(),
4435 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
4436 }
4437 ],
4438 ty: RuntimeType::any(),
4439 }],
4440 ty: RuntimeType::any(),
4441 }],
4442 ty: RuntimeType::any(),
4443 }
4444 );
4445 }
4446
4447 #[tokio::test(flavor = "multi_thread")]
4448 async fn test_zero_param_fn() {
4449 let ast = r#"sigmaAllow = 35000 // psi
4450leg1 = 5 // inches
4451leg2 = 8 // inches
4452fn thickness() { return 0.56 }
4453
4454bracket = startSketchOn(XY)
4455 |> startProfile(at = [0,0])
4456 |> line(end = [0, leg1])
4457 |> line(end = [leg2, 0])
4458 |> line(end = [0, -thickness()])
4459 |> line(end = [-leg2 + thickness(), 0])
4460"#;
4461 parse_execute(ast).await.unwrap();
4462 }
4463
4464 #[tokio::test(flavor = "multi_thread")]
4465 async fn test_unary_operator_not_succeeds() {
4466 let ast = r#"
4467fn returnTrue() { return !false }
4468t = true
4469f = false
4470notTrue = !t
4471notFalse = !f
4472c = !!true
4473d = !returnTrue()
4474
4475assertIs(!false, error = "expected to pass")
4476
4477fn check(x) {
4478 assertIs(!x, error = "expected argument to be false")
4479 return true
4480}
4481check(x = false)
4482"#;
4483 let result = parse_execute(ast).await.unwrap();
4484 assert_eq!(
4485 false,
4486 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
4487 .as_bool()
4488 .unwrap()
4489 );
4490 assert_eq!(
4491 true,
4492 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
4493 .as_bool()
4494 .unwrap()
4495 );
4496 assert_eq!(
4497 true,
4498 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
4499 .as_bool()
4500 .unwrap()
4501 );
4502 assert_eq!(
4503 false,
4504 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
4505 .as_bool()
4506 .unwrap()
4507 );
4508 }
4509
4510 #[tokio::test(flavor = "multi_thread")]
4511 async fn test_unary_operator_not_on_non_bool_fails() {
4512 let code1 = r#"
4513// Yup, this is null.
4514myNull = 0 / 0
4515notNull = !myNull
4516"#;
4517 assert_eq!(
4518 parse_execute(code1).await.unwrap_err().message(),
4519 "Cannot apply unary operator ! to non-boolean value: a number",
4520 );
4521
4522 let code2 = "notZero = !0";
4523 assert_eq!(
4524 parse_execute(code2).await.unwrap_err().message(),
4525 "Cannot apply unary operator ! to non-boolean value: a number",
4526 );
4527
4528 let code3 = r#"
4529notEmptyString = !""
4530"#;
4531 assert_eq!(
4532 parse_execute(code3).await.unwrap_err().message(),
4533 "Cannot apply unary operator ! to non-boolean value: a string",
4534 );
4535
4536 let code4 = r#"
4537obj = { a = 1 }
4538notMember = !obj.a
4539"#;
4540 assert_eq!(
4541 parse_execute(code4).await.unwrap_err().message(),
4542 "Cannot apply unary operator ! to non-boolean value: a number",
4543 );
4544
4545 let code5 = "
4546a = []
4547notArray = !a";
4548 assert_eq!(
4549 parse_execute(code5).await.unwrap_err().message(),
4550 "Cannot apply unary operator ! to non-boolean value: an empty array",
4551 );
4552
4553 let code6 = "
4554x = {}
4555notObject = !x";
4556 assert_eq!(
4557 parse_execute(code6).await.unwrap_err().message(),
4558 "Cannot apply unary operator ! to non-boolean value: an object",
4559 );
4560
4561 let code7 = "
4562fn x() { return 1 }
4563notFunction = !x";
4564 let fn_err = parse_execute(code7).await.unwrap_err();
4565 assert!(
4568 fn_err
4569 .message()
4570 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
4571 "Actual error: {fn_err:?}"
4572 );
4573
4574 let code8 = "
4575myTagDeclarator = $myTag
4576notTagDeclarator = !myTagDeclarator";
4577 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
4578 assert!(
4581 tag_declarator_err
4582 .message()
4583 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
4584 "Actual error: {tag_declarator_err:?}"
4585 );
4586
4587 let code9 = "
4588myTagDeclarator = $myTag
4589notTagIdentifier = !myTag";
4590 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
4591 assert!(
4594 tag_identifier_err
4595 .message()
4596 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
4597 "Actual error: {tag_identifier_err:?}"
4598 );
4599
4600 let code10 = "notPipe = !(1 |> 2)";
4601 assert_eq!(
4602 parse_execute(code10).await.unwrap_err(),
4605 KclError::new_syntax(KclErrorDetails::new(
4606 "Unexpected token: !".to_owned(),
4607 vec![SourceRange::new(10, 11, ModuleId::default())],
4608 ))
4609 );
4610
4611 let code11 = "
4612fn identity(x) { return x }
4613notPipeSub = 1 |> identity(!%))";
4614 assert_eq!(
4615 parse_execute(code11).await.unwrap_err(),
4618 KclError::new_syntax(KclErrorDetails::new(
4619 "There was an unexpected `!`. Try removing it.".to_owned(),
4620 vec![SourceRange::new(56, 57, ModuleId::default())],
4621 ))
4622 );
4623
4624 }
4628
4629 #[tokio::test(flavor = "multi_thread")]
4630 async fn test_start_sketch_on_invalid_kwargs() {
4631 let current_dir = std::env::current_dir().unwrap();
4632 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
4633 let mut code = std::fs::read_to_string(&path).unwrap();
4634 assert_eq!(
4635 parse_execute(&code).await.unwrap_err().message(),
4636 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
4637 );
4638
4639 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
4640 code = std::fs::read_to_string(&path).unwrap();
4641
4642 assert_eq!(
4643 parse_execute(&code).await.unwrap_err().message(),
4644 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
4645 );
4646
4647 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
4648 code = std::fs::read_to_string(&path).unwrap();
4649
4650 assert_eq!(
4651 parse_execute(&code).await.unwrap_err().message(),
4652 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4653 );
4654
4655 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
4656 code = std::fs::read_to_string(&path).unwrap();
4657
4658 assert_eq!(
4659 parse_execute(&code).await.unwrap_err().message(),
4660 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4661 );
4662
4663 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
4664 code = std::fs::read_to_string(&path).unwrap();
4665
4666 assert_eq!(
4667 parse_execute(&code).await.unwrap_err().message(),
4668 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
4669 );
4670 }
4671
4672 #[tokio::test(flavor = "multi_thread")]
4673 async fn test_math_negative_variable_in_binary_expression() {
4674 let ast = r#"sigmaAllow = 35000 // psi
4675width = 1 // inch
4676
4677p = 150 // lbs
4678distance = 6 // inches
4679FOS = 2
4680
4681leg1 = 5 // inches
4682leg2 = 8 // inches
4683
4684thickness_squared = distance * p * FOS * 6 / sigmaAllow
4685thickness = 0.56 // inches. App does not support square root function yet
4686
4687bracket = startSketchOn(XY)
4688 |> startProfile(at = [0,0])
4689 |> line(end = [0, leg1])
4690 |> line(end = [leg2, 0])
4691 |> line(end = [0, -thickness])
4692 |> line(end = [-leg2 + thickness, 0])
4693"#;
4694 parse_execute(ast).await.unwrap();
4695 }
4696
4697 #[tokio::test(flavor = "multi_thread")]
4698 async fn test_execute_function_no_return() {
4699 let ast = r#"fn test(@origin) {
4700 origin
4701}
4702
4703test([0, 0])
4704"#;
4705 let result = parse_execute(ast).await;
4706 assert!(result.is_err());
4707 assert!(result.unwrap_err().to_string().contains("undefined"));
4708 }
4709
4710 #[tokio::test(flavor = "multi_thread")]
4711 async fn test_max_stack_size_exceeded_error() {
4712 let ast = r#"
4713fn forever(@n) {
4714 return 1 + forever(n)
4715}
4716
4717forever(1)
4718"#;
4719 let result = parse_execute(ast).await;
4720 let err = result.unwrap_err();
4721 let msg = err.to_string();
4724 assert!(
4725 msg.contains("stack size exceeded") || msg.contains("Call depth limit"),
4726 "actual: {err:?}"
4727 );
4728 }
4729
4730 #[tokio::test(flavor = "multi_thread")]
4731 async fn test_math_doubly_nested_parens() {
4732 let ast = r#"sigmaAllow = 35000 // psi
4733width = 4 // inch
4734p = 150 // Force on shelf - lbs
4735distance = 6 // inches
4736FOS = 2
4737leg1 = 5 // inches
4738leg2 = 8 // inches
4739thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
4740thickness = 0.32 // inches. App does not support square root function yet
4741bracket = startSketchOn(XY)
4742 |> startProfile(at = [0,0])
4743 |> line(end = [0, leg1])
4744 |> line(end = [leg2, 0])
4745 |> line(end = [0, -thickness])
4746 |> line(end = [-1 * leg2 + thickness, 0])
4747 |> line(end = [0, -1 * leg1 + thickness])
4748 |> close()
4749 |> extrude(length = width)
4750"#;
4751 parse_execute(ast).await.unwrap();
4752 }
4753
4754 #[tokio::test(flavor = "multi_thread")]
4755 async fn test_math_nested_parens_one_less() {
4756 let ast = r#" sigmaAllow = 35000 // psi
4757width = 4 // inch
4758p = 150 // Force on shelf - lbs
4759distance = 6 // inches
4760FOS = 2
4761leg1 = 5 // inches
4762leg2 = 8 // inches
4763thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
4764thickness = 0.32 // inches. App does not support square root function yet
4765bracket = startSketchOn(XY)
4766 |> startProfile(at = [0,0])
4767 |> line(end = [0, leg1])
4768 |> line(end = [leg2, 0])
4769 |> line(end = [0, -thickness])
4770 |> line(end = [-1 * leg2 + thickness, 0])
4771 |> line(end = [0, -1 * leg1 + thickness])
4772 |> close()
4773 |> extrude(length = width)
4774"#;
4775 parse_execute(ast).await.unwrap();
4776 }
4777
4778 #[tokio::test(flavor = "multi_thread")]
4779 async fn test_fn_as_operand() {
4780 let ast = r#"fn f() { return 1 }
4781x = f()
4782y = x + 1
4783z = f() + 1
4784w = f() + f()
4785"#;
4786 parse_execute(ast).await.unwrap();
4787 }
4788
4789 #[tokio::test(flavor = "multi_thread")]
4790 async fn kcl_test_ids_stable_between_executions() {
4791 let code = r#"sketch001 = startSketchOn(XZ)
4792|> startProfile(at = [61.74, 206.13])
4793|> xLine(length = 305.11, tag = $seg01)
4794|> yLine(length = -291.85)
4795|> xLine(length = -segLen(seg01))
4796|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4797|> close()
4798|> extrude(length = 40.14)
4799|> shell(
4800 thickness = 3.14,
4801 faces = [seg01]
4802)
4803"#;
4804
4805 let old_program = crate::Program::parse_no_errs(code).unwrap();
4806 let ctx = crate::test_server::new_context(true, None, true, old_program.language_version().unwrap())
4807 .await
4808 .unwrap();
4809
4810 if let Err(err) = ctx.run_with_caching(old_program).await {
4812 let report = err.into_miette_report_with_outputs(code).unwrap();
4813 let report = miette::Report::new(report);
4814 panic!("Error executing program: {report:?}");
4815 }
4816
4817 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4819
4820 let code = r#"sketch001 = startSketchOn(XZ)
4821|> startProfile(at = [62.74, 206.13])
4822|> xLine(length = 305.11, tag = $seg01)
4823|> yLine(length = -291.85)
4824|> xLine(length = -segLen(seg01))
4825|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4826|> close()
4827|> extrude(length = 40.14)
4828|> shell(
4829 faces = [seg01],
4830 thickness = 3.14,
4831)
4832"#;
4833
4834 let program = crate::Program::parse_no_errs(code).unwrap();
4836 ctx.run_with_caching(program).await.unwrap();
4838
4839 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4840
4841 assert_eq!(id_generator, new_id_generator);
4842 }
4843
4844 #[tokio::test(flavor = "multi_thread")]
4845 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
4846 let code = r#"sketch001 = startSketchOn(XZ)
4847|> startProfile(at = [61.74, 206.13])
4848|> xLine(length = 305.11, tag = $seg01)
4849|> yLine(length = -291.85)
4850|> xLine(length = -segLen(seg01))
4851|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4852|> close()
4853|> extrude(length = 40.14)
4854|> shell(
4855 thickness = 3.14,
4856 faces = [seg01]
4857)
4858"#;
4859
4860 let old_program = crate::Program::parse_no_errs(code).unwrap();
4861 let mut ctx = crate::test_server::new_context(true, None, true, old_program.language_version().unwrap())
4862 .await
4863 .unwrap();
4864
4865 ctx.run_with_caching(old_program.clone()).await.unwrap();
4867
4868 let settings_state = cache::read_old_ast().await.unwrap().settings;
4869
4870 assert_eq!(settings_state, ctx.settings);
4872
4873 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4875
4876 ctx.run_with_caching(old_program.clone()).await.unwrap();
4878
4879 let settings_state = cache::read_old_ast().await.unwrap().settings;
4880
4881 assert_eq!(settings_state, ctx.settings);
4883
4884 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4886
4887 ctx.run_with_caching(old_program).await.unwrap();
4889
4890 let settings_state = cache::read_old_ast().await.unwrap().settings;
4891
4892 assert_eq!(settings_state, ctx.settings);
4894
4895 ctx.close().await;
4896 }
4897
4898 #[tokio::test(flavor = "multi_thread")]
4899 async fn mock_after_not_mock() {
4900 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
4901 let program = crate::Program::parse_no_errs("x = 2").unwrap();
4902 let result = ctx.run_with_caching(program).await.unwrap();
4903 assert_number_variable(&result.variables, "x", 2.0);
4904
4905 let ctx2 = ExecutorContext::new_mock(None).await;
4906 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
4907 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
4908 assert_number_variable(&result.variables, "z", 3.0);
4909
4910 ctx.close().await;
4911 ctx2.close().await;
4912 }
4913
4914 #[tokio::test(flavor = "multi_thread")]
4916 async fn mock_execution_succeeds_after_split() {
4917 let code = kcl_input!("repro_mock_extrude");
4918 let ctx = ExecutorContext::new_mock(None).await;
4919 let program = crate::Program::parse_no_errs(code).unwrap();
4920 let _result = match ctx.run_mock(&program, &MockConfig::default()).await {
4921 Ok(res) => res,
4922 Err(e) => panic!("{}", e.error),
4923 };
4924 }
4925
4926 #[tokio::test(flavor = "multi_thread")]
4928 async fn mock_execution_rejects_oob_on_frontend_array() {
4929 let code = r#"
4930values = [10, 20]
4931third = values[2]
4932"#;
4933 let ctx = ExecutorContext::new_mock(None).await;
4934 let program = crate::Program::parse_no_errs(code).unwrap();
4935 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
4936 ctx.close().await;
4937
4938 assert!(
4939 err.error.message().contains("array doesn't have any item at index 2"),
4940 "{err:?}"
4941 );
4942 }
4943
4944 #[tokio::test(flavor = "multi_thread")]
4949 async fn mock_execution_pattern_circular_number() {
4950 let code = kcl_input!("repro_mock_pattern_circular");
4951 let ctx = ExecutorContext::new_mock(None).await;
4952 let program = crate::Program::parse_no_errs(code).unwrap();
4953 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
4954 let copies = result
4955 .variables
4956 .get("copies")
4957 .expect("no variable called 'copies' found");
4958 let value = match copies {
4959 KclValueView::Solid { .. } => {
4960 panic!("One solid?");
4961 }
4962 KclValueView::HomArray { value } => value,
4963 other => panic!("{other:#?}"),
4964 };
4965 let actual_instances = value.len();
4966 let expected_instances = 10; assert_eq!(actual_instances, expected_instances);
4968 }
4969
4970 #[tokio::test(flavor = "multi_thread")]
4975 async fn mock_execution_subtract() {
4976 let code = kcl_input!("repro_mock_subtract");
4978 let ctx = ExecutorContext::new_mock(None).await;
4979 let program = crate::Program::parse_no_errs(code).unwrap();
4980 let result = ctx.run_mock(&program, &MockConfig::default()).await;
4981 ctx.close().await;
4982 let result = match result {
4983 Ok(x) => x,
4984 Err(e) => {
4985 let error = e.error;
4986 panic!("{error}");
4987 }
4988 };
4989
4990 let subtracted_parts = result
4992 .variables
4993 .get("subtractedParts")
4994 .expect("no variable called 'subtracted_parts' found");
4995 let subtracted_parts = match subtracted_parts {
4996 KclValueView::Solid { .. } => {
4997 panic!("One solid?");
4998 }
4999 KclValueView::HomArray { value } => value,
5000 other => panic!("{other:#?}"),
5001 };
5002
5003 let expected_number_of_parts = 2;
5006 let actual_number_of_parts = subtracted_parts.len();
5007 assert_eq!(actual_number_of_parts, expected_number_of_parts);
5008 }
5009
5010 #[tokio::test(flavor = "multi_thread")]
5011 async fn mock_then_add_extrude_then_mock_again() {
5012 let code = "s = sketch(on = XY) {
5013 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
5014 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
5015 coincident([line1.end, line2.start])
5016 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
5017 coincident([line2.end, line3.start])
5018 coincident([line1.start, line3.end])
5019}
5020 ";
5021 let ctx = ExecutorContext::new_mock(None).await;
5022 let program = crate::Program::parse_no_errs(code).unwrap();
5023 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
5024 assert!(result.variables.contains_key("s"), "actual: {:?}", result.variables);
5025
5026 let code2 = code.to_owned()
5027 + "
5028region001 = region(point = [1mm, 1mm], sketch = s)
5029extrude001 = extrude(region001, length = 1)
5030 ";
5031 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
5032 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
5033 assert!(
5034 result.variables.contains_key("region001"),
5035 "actual: {:?}",
5036 result.variables
5037 );
5038
5039 ctx.close().await;
5040 }
5041
5042 #[tokio::test(flavor = "multi_thread")]
5043 async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
5044 let code = format!(
5045 r#"{}
5046
5047face7 = faceOf(solid6, face = r6.tags.line1)
5048r7 = squareRegion(onSurface = face7)
5049solid7 = extrude(r7, length = width)
5050"#,
5051 include_str!("../../tests/endless_impeller/input.kcl")
5052 );
5053
5054 let result = parse_execute(&code).await.unwrap();
5055 let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
5056 assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
5057
5058 let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
5059 KclValue::Face { value } => value,
5060 value => panic!("expected face7 to be a Face, got {value:?}"),
5061 };
5062 assert!(face7.parent_solid.creator_sketch_id.is_some());
5063 }
5064
5065 #[tokio::test(flavor = "multi_thread")]
5066 async fn mock_has_stable_ids() {
5067 let ctx = ExecutorContext::new_mock(None).await;
5068 let mock_config = MockConfig {
5069 use_prev_memory: false,
5070 ..Default::default()
5071 };
5072 let code = "sk = startSketchOn(XY)
5073 |> startProfile(at = [0, 0])";
5074 let program = crate::Program::parse_no_errs(code).unwrap();
5075 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
5076 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
5077 assert!(!ids.is_empty(), "IDs should not be empty");
5078
5079 let ctx2 = ExecutorContext::new_mock(None).await;
5080 let program2 = crate::Program::parse_no_errs(code).unwrap();
5081 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
5082 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
5083
5084 assert_eq!(ids, ids2, "Generated IDs should match");
5085 ctx.close().await;
5086 ctx2.close().await;
5087 }
5088
5089 #[tokio::test(flavor = "multi_thread")]
5090 async fn mock_memory_restore_preserves_module_maps() {
5091 clear_mem_cache().await;
5092
5093 let ctx = ExecutorContext::new_mock(None).await;
5094 let cold_start = MockConfig {
5095 use_prev_memory: false,
5096 ..Default::default()
5097 };
5098 ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
5099
5100 let mut mem = cache::read_old_memory().await.unwrap();
5101 assert!(
5102 mem.path_to_source_id.len() > 3,
5103 "expected prelude imports to populate multiple modules, got {:?}",
5104 mem.path_to_source_id
5105 );
5106 mem.constraint_state.insert(
5107 crate::front::ObjectId(1),
5108 indexmap::indexmap! {
5109 crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
5110 crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
5111 },
5112 );
5113 let imported_id = ModuleId::from_usize(42);
5114 mem.never_type_ranges
5115 .insert(imported_id, vec![SourceRange::new(0, 5, imported_id)]);
5116
5117 let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
5118 ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
5119
5120 assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
5121 assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
5122 assert_eq!(exec_state.global.never_type_ranges, mem.never_type_ranges);
5123 assert_eq!(exec_state.global.module_infos, mem.module_infos);
5124 assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
5125
5126 clear_mem_cache().await;
5127 ctx.close().await;
5128 }
5129
5130 #[tokio::test(flavor = "multi_thread")]
5134 async fn cached_execution_phases_preserve_edits_settings_and_error_recovery() {
5135 cache::bust_cache().await;
5136 clear_mem_cache().await;
5137 let mut ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5138 let original = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 2").unwrap();
5139 for _ in 0..2 {
5140 let outcome = ctx.run_with_caching(original.clone()).await.unwrap();
5141 assert_number_variable(&outcome.variables, "x", 2.0);
5142 }
5143
5144 let extended = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 2\ny = x + 1").unwrap();
5147 for _ in 0..2 {
5148 let outcome = ctx.run_with_caching(extended.clone()).await.unwrap();
5149 assert_number_variable(&outcome.variables, "x", 2.0);
5150 assert_number_variable(&outcome.variables, "y", 3.0);
5151 assert_eq!(cache::read_old_ast().await.unwrap().main.ast.body.len(), 2);
5152 }
5153
5154 let changed = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 4\ny = x + 1").unwrap();
5156 let outcome = ctx.run_with_caching(changed.clone()).await.unwrap();
5157 assert_number_variable(&outcome.variables, "y", 5.0);
5158 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
5159 let outcome = ctx.run_with_caching(changed).await.unwrap();
5160 assert_number_variable(&outcome.variables, "y", 5.0);
5161 assert_eq!(cache::read_old_ast().await.unwrap().settings, ctx.settings);
5162
5163 let invalid = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = missing").unwrap();
5164 ctx.run_with_caching(invalid).await.unwrap_err();
5165 assert!(
5166 cache::read_old_ast().await.is_none(),
5167 "execution errors must invalidate the cache"
5168 );
5169 let outcome = ctx.run_with_caching(original).await.unwrap();
5170 assert_number_variable(&outcome.variables, "x", 2.0);
5171 cache::bust_cache().await;
5172 clear_mem_cache().await;
5173 ctx.close().await;
5174 }
5175
5176 #[tokio::test(flavor = "multi_thread")]
5177 async fn cached_execution_phases_recheck_changed_imports() {
5178 cache::bust_cache().await;
5179 clear_mem_cache().await;
5180 let project = tempfile::TempDir::with_prefix("kcl_cache_phases").unwrap();
5181 let imported = project.path().join("values.kcl");
5182 tokio::fs::write(&imported, "export x = 2").await.unwrap();
5183 let mut ctx = ExecutorContext::new_with_engine(
5184 Arc::new(EngineManager::new_mock()),
5185 ExecutorSettings {
5186 project_directory: Some(crate::TypedPath(project.path().into())),
5187 ..Default::default()
5188 },
5189 );
5190 let program =
5191 crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nimport x from 'values.kcl'\ny = x + 1")
5192 .unwrap();
5193 for _ in 0..2 {
5194 let outcome = ctx.run_with_caching(program.clone()).await.unwrap();
5195 assert_number_variable(&outcome.variables, "y", 3.0);
5196 }
5197 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
5199 let outcome = ctx.run_with_caching(program.clone()).await.unwrap();
5200 assert_number_variable(&outcome.variables, "y", 3.0);
5201 assert_eq!(cache::read_old_ast().await.unwrap().settings, ctx.settings);
5202
5203 tokio::fs::write(&imported, "export x = 7").await.unwrap();
5204 for _ in 0..2 {
5205 let outcome = ctx.run_with_caching(program.clone()).await.unwrap();
5206 assert_number_variable(&outcome.variables, "y", 8.0);
5207 }
5208 cache::bust_cache().await;
5209 clear_mem_cache().await;
5210 ctx.close().await;
5211 }
5212
5213 #[tokio::test(flavor = "multi_thread")]
5214 async fn run_with_caching_no_action_refreshes_mock_memory() {
5215 cache::bust_cache().await;
5216 clear_mem_cache().await;
5217
5218 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
5219 let program = crate::Program::parse_no_errs(
5220 r#"sketch001 = sketch(on = XY) {
5221 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
5222}
5223"#,
5224 )
5225 .unwrap();
5226
5227 ctx.run_with_caching(program.clone()).await.unwrap();
5228 let baseline_memory = cache::read_old_memory().await.unwrap();
5229 assert!(
5230 !baseline_memory.scene_objects.is_empty(),
5231 "expected engine execution to persist full-scene mock memory"
5232 );
5233
5234 cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
5235 assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
5236
5237 ctx.run_with_caching(program).await.unwrap();
5238 let refreshed_memory = cache::read_old_memory().await.unwrap();
5239 assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
5240 assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
5241 assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
5242
5243 cache::bust_cache().await;
5244 clear_mem_cache().await;
5245 ctx.close().await;
5246 }
5247
5248 #[tokio::test(flavor = "multi_thread")]
5249 async fn sim_sketch_mode_real_mock_real() {
5250 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
5251 let code = r#"sketch001 = startSketchOn(XY)
5252profile001 = startProfile(sketch001, at = [0, 0])
5253 |> line(end = [10, 0])
5254 |> line(end = [0, 10])
5255 |> line(end = [-10, 0])
5256 |> line(end = [0, -10])
5257 |> close()
5258"#;
5259 let program = crate::Program::parse_no_errs(code).unwrap();
5260 let result = ctx.run_with_caching(program).await.unwrap();
5261 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
5262
5263 let mock_ctx = ExecutorContext::new_mock(None).await;
5264 let mock_program = crate::Program::parse_no_errs(code).unwrap();
5265 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
5266 assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
5267
5268 let code2 = code.to_owned()
5269 + r#"
5270extrude001 = extrude(profile001, length = 10)
5271"#;
5272 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
5273 let result = ctx.run_with_caching(program2).await.unwrap();
5274 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
5275
5276 ctx.close().await;
5277 mock_ctx.close().await;
5278 }
5279
5280 #[tokio::test(flavor = "multi_thread")]
5281 async fn read_tag_version() {
5282 let ast = r#"fn bar(@t) {
5283 return startSketchOn(XY)
5284 |> startProfile(at = [0,0])
5285 |> angledLine(
5286 angle = -60,
5287 length = segLen(t),
5288 )
5289 |> line(end = [0, 0])
5290 |> close()
5291}
5292
5293sketch = startSketchOn(XY)
5294 |> startProfile(at = [0,0])
5295 |> line(end = [0, 10])
5296 |> line(end = [10, 0], tag = $tag0)
5297 |> line(endAbsolute = [0, 0])
5298
5299fn foo() {
5300 // tag0 tags an edge
5301 return bar(tag0)
5302}
5303
5304solid = sketch |> extrude(length = 10)
5305// tag0 tags a face
5306sketch2 = startSketchOn(solid, face = tag0)
5307 |> startProfile(at = [0,0])
5308 |> line(end = [0, 1])
5309 |> line(end = [1, 0])
5310 |> line(end = [0, 0])
5311
5312foo() |> extrude(length = 1)
5313"#;
5314 parse_execute(ast).await.unwrap();
5315 }
5316
5317 #[tokio::test(flavor = "multi_thread")]
5318 async fn experimental() {
5319 let code = r#"
5320startSketchOn(XY)
5321 |> startProfile(at = [0, 0], tag = $start)
5322 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5323"#;
5324 let result = parse_execute(code).await.unwrap();
5325 let issues = result.exec_state.issues();
5326 assert_eq!(issues.len(), 1);
5327 assert_eq!(issues[0].severity, Severity::Error);
5328 let msg = &issues[0].message;
5329 assert!(msg.contains("experimental"), "found {msg}");
5330
5331 let code = r#"@settings(experimentalFeatures = allow)
5332startSketchOn(XY)
5333 |> startProfile(at = [0, 0], tag = $start)
5334 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5335"#;
5336 let result = parse_execute(code).await.unwrap();
5337 let issues = result.exec_state.issues();
5338 assert!(issues.is_empty(), "issues={issues:#?}");
5339
5340 let code = r#"@settings(experimentalFeatures = warn)
5341startSketchOn(XY)
5342 |> startProfile(at = [0, 0], tag = $start)
5343 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5344"#;
5345 let result = parse_execute(code).await.unwrap();
5346 let issues = result.exec_state.issues();
5347 assert_eq!(issues.len(), 1);
5348 assert_eq!(issues[0].severity, Severity::Warning);
5349 let msg = &issues[0].message;
5350 assert!(msg.contains("experimental"), "found {msg}");
5351
5352 let code = r#"@settings(experimentalFeatures = deny)
5353startSketchOn(XY)
5354 |> startProfile(at = [0, 0], tag = $start)
5355 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5356"#;
5357 let result = parse_execute(code).await.unwrap();
5358 let issues = result.exec_state.issues();
5359 assert_eq!(issues.len(), 1);
5360 assert_eq!(issues[0].severity, Severity::Error);
5361 let msg = &issues[0].message;
5362 assert!(msg.contains("experimental"), "found {msg}");
5363
5364 let code = r#"@settings(experimentalFeatures = foo)
5365startSketchOn(XY)
5366 |> startProfile(at = [0, 0], tag = $start)
5367 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
5368"#;
5369 parse_execute(code).await.unwrap_err();
5370 }
5371
5372 #[tokio::test(flavor = "multi_thread")]
5373 async fn default_angle_unit_warns_in_legacy_kcl() {
5374 for version in ["", "kclVersion = 1.0, ", "kclVersion = 2.0, "] {
5375 for unit in ["deg", "rad"] {
5376 let code = format!("@settings({version}defaultAngleUnit = {unit})\nx = 1\n");
5377 let result = parse_execute(&code).await.unwrap();
5378 let issues = result.issues();
5379 assert_eq!(issues.len(), 1, "code={code}");
5380 assert_eq!(issues[0].severity, Severity::Warning, "code={code}");
5381 assert_eq!(
5382 issues[0].message,
5383 "The `defaultAngleUnit` setting is deprecated; use explicit units for angles"
5384 );
5385 assert_eq!(variable_f64(&result, "x"), 1.0);
5386 }
5387 }
5388 }
5389
5390 #[tokio::test(flavor = "multi_thread")]
5391 async fn default_angle_unit_errors_in_kcl_v3() {
5392 for settings in [
5393 "@settings(kclVersion = \"3.0-preview\", defaultAngleUnit = deg)",
5394 "@settings(defaultAngleUnit = rad, kclVersion = \"3.0-preview\")",
5395 "@settings(defaultAngleUnit = deg)\n@settings(kclVersion = \"3.0-preview\")",
5396 "@settings(kclVersion = \"3.0-preview\")\n@settings(defaultAngleUnit = rad)",
5397 ] {
5398 let code = format!("{settings}\nx = 1\n");
5399 let Err(error) = parse_execute(&code).await else {
5400 panic!("defaultAngleUnit must fail in KCL 3.0: {code}");
5401 };
5402 assert_eq!(
5403 error.message(),
5404 "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles",
5405 "code={code}"
5406 );
5407 let ranges = error.source_ranges();
5408 assert_eq!(ranges.len(), 1);
5409 assert!(code[ranges[0].start()..ranges[0].end()].contains("defaultAngleUnit"));
5410 }
5411 }
5412
5413 #[tokio::test(flavor = "multi_thread")]
5414 async fn default_angle_unit_error_cannot_be_suppressed() {
5415 for version in ["1.0", "2.0", "\"3.0-preview\""] {
5416 let code = format!(
5417 "@warnings(allow = angleUnits)\n@settings(kclVersion = {version}, defaultAngleUnit = deg)\nx = 1\n"
5418 );
5419 let result = parse_execute(&code).await;
5420 if version == "\"3.0-preview\"" {
5421 assert_eq!(
5422 result.unwrap_err().message(),
5423 "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
5424 );
5425 } else {
5426 assert!(result.unwrap().issues().is_empty(), "code={code}");
5427 }
5428 }
5429 }
5430
5431 #[tokio::test(flavor = "multi_thread")]
5437 async fn default_angle_unit_in_import_uses_effective_kcl_version() {
5438 let dep = "@settings(defaultAngleUnit = deg)\nexport x = 1\n";
5439 for version in ["1.0", "2.0", "\"3.0-preview\""] {
5440 let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5441 let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5442 if version == "\"3.0-preview\"" {
5443 assert_eq!(
5444 result.unwrap_err().message(),
5445 "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
5446 );
5447 } else {
5448 assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0);
5449 }
5450 }
5451 }
5452
5453 #[tokio::test(flavor = "multi_thread")]
5464 async fn diagnostics_attribute_in_import_uses_effective_kcl_version() {
5465 let dep = "@diagnostics(allow = bogus)\nexport x = 1\n";
5466 for version in ["1.0", "2.0", "\"3.0-preview\""] {
5467 let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5468 let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5469 if version == "\"3.0-preview\"" {
5470 let error = result.unwrap_err();
5471 let message = error.message();
5472 assert!(
5473 message.starts_with("Unexpected diagnostic value: `bogus`; accepted values: "),
5474 "main={main}, message={message}"
5475 );
5476 } else {
5477 assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0, "main={main}");
5478 }
5479 }
5480
5481 let dep = "@warnings(allow = bogus)\nexport x = 1\n";
5485 for version in ["1.0", "2.0", "\"3.0-preview\""] {
5486 let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
5487 let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
5488 if version == "\"3.0-preview\"" {
5489 assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0, "main={main}");
5490 } else {
5491 let error = result.unwrap_err();
5492 let message = error.message();
5493 assert!(
5494 message.starts_with("Unexpected warning value: `bogus`; accepted values: "),
5495 "main={main}, message={message}"
5496 );
5497 }
5498 }
5499 }
5500
5501 #[tokio::test(flavor = "multi_thread")]
5505 async fn entry_point_kcl_version_records_declared_version() {
5506 for (code, expected) in [
5507 ("x = 1\n", None),
5508 ("@settings(defaultLengthUnit = in)\nx = 1\n", None),
5509 ("@settings(kclVersion = 1.0)\nx = 1\n", Some(KclVersion::V1)),
5510 ("@settings(kclVersion = 2.0)\nx = 1\n", Some(KclVersion::V2)),
5511 (
5512 "@settings(kclVersion = \"3.0-preview\")\nx = 1\n",
5513 Some(KclVersion::V3Preview),
5514 ),
5515 ] {
5516 let result = parse_execute(code).await.unwrap();
5517 assert_eq!(
5518 result.exec_state.global.entry_point_kcl_version, expected,
5519 "code={code}"
5520 );
5521 assert_eq!(
5522 result.exec_state.entry_point_version_is_v3_or_higher(),
5523 expected == Some(KclVersion::V3Preview),
5524 "code={code}"
5525 );
5526 }
5527 }
5528
5529 #[tokio::test(flavor = "multi_thread")]
5530 async fn kcl_version_lookup_prefers_entry_point_over_module_local() {
5531 let mut exec_state = parse_execute("x = 1\n").await.unwrap().exec_state;
5532
5533 exec_state.global.entry_point_kcl_version = None;
5535 exec_state.mod_local.settings.kcl_version = KclVersion::V2;
5536 assert_eq!(exec_state.kcl_version(), KclVersion::V2);
5537 assert_eq!(exec_state.legacy_caller_kcl_version(), KclVersion::V2);
5538
5539 exec_state.global.entry_point_kcl_version = Some(KclVersion::V1);
5542 assert_eq!(exec_state.kcl_version(), KclVersion::V2);
5543
5544 exec_state.global.entry_point_kcl_version = Some(KclVersion::V3Preview);
5547 assert_eq!(exec_state.kcl_version(), KclVersion::V3Preview);
5548 assert_eq!(exec_state.legacy_caller_kcl_version(), KclVersion::V2);
5549 }
5550
5551 #[tokio::test(flavor = "multi_thread")]
5556 async fn mock_execution_records_entry_point_kcl_version() {
5557 use futures::FutureExt;
5558
5559 clear_mem_cache().await;
5560
5561 let ctx = ExecutorContext::new_mock(None).await;
5562 let fresh_memory = MockConfig {
5563 use_prev_memory: false,
5564 ..Default::default()
5565 };
5566 let prev_memory = MockConfig::default();
5567
5568 let v3_program = crate::Program::parse_no_errs("@settings(kclVersion = \"3.0-preview\")\nx = 1\n").unwrap();
5569 let v2_program = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 1\n").unwrap();
5570
5571 let test_result = std::panic::AssertUnwindSafe(async {
5574 let (exec_state, _) = ctx.run_mock_returning_state(&v3_program, &fresh_memory).await.unwrap();
5575 assert_eq!(
5576 exec_state.global.entry_point_kcl_version,
5577 Some(KclVersion::V3Preview),
5578 "mock execution should record a 3.0-preview entry point"
5579 );
5580 assert!(exec_state.entry_point_version_is_v3_or_higher());
5581
5582 ctx.run_mock(&v3_program, &fresh_memory).await.unwrap();
5586 let (exec_state, _) = ctx.run_mock_returning_state(&v2_program, &prev_memory).await.unwrap();
5587 assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V2));
5588 assert!(!exec_state.entry_point_version_is_v3_or_higher());
5589
5590 ctx.run_mock(&v2_program, &fresh_memory).await.unwrap();
5593 let (exec_state, _) = ctx.run_mock_returning_state(&v3_program, &prev_memory).await.unwrap();
5594 assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V3Preview));
5595 })
5596 .catch_unwind()
5597 .await;
5598
5599 clear_mem_cache().await;
5600 ctx.close().await;
5601 if let Err(panic) = test_result {
5602 std::panic::resume_unwind(panic);
5603 }
5604 }
5605
5606 #[tokio::test(flavor = "multi_thread")]
5610 async fn mock_execution_applies_v3_semantics() {
5611 use futures::FutureExt;
5612
5613 clear_mem_cache().await;
5614
5615 let ctx = ExecutorContext::new_mock(None).await;
5616 let fresh_memory = MockConfig {
5617 use_prev_memory: false,
5618 ..Default::default()
5619 };
5620 let program = crate::Program::parse_no_errs(
5621 r#"@settings(kclVersion = "3.0-preview")
5622fn f() {
5623 return 1
5624 assert(1, isEqualTo = 2, error = "code after return ran")
5625}
5626x = f()
5627outer = 1
5628y = if true {
5629 outer = 2
5630 outer + 10
5631} else {
5632 0
5633}
5634"#,
5635 )
5636 .unwrap();
5637
5638 let test_result = std::panic::AssertUnwindSafe(async {
5641 let (exec_state, env) = ctx.run_mock_returning_state(&program, &fresh_memory).await.unwrap();
5642 let var = |name: &str| mem_get_json(exec_state.stack(), env, name).as_f64().unwrap();
5643 assert_eq!(var("x"), 1.0, "early return produces the function's value");
5644 assert_eq!(var("y"), 12.0, "the branch sees its own shadowing binding");
5645 assert_eq!(var("outer"), 1.0, "the outer binding is unchanged after the if");
5646 })
5647 .catch_unwind()
5648 .await;
5649
5650 clear_mem_cache().await;
5651 ctx.close().await;
5652 if let Err(panic) = test_result {
5653 std::panic::resume_unwind(panic);
5654 }
5655 }
5656
5657 fn commands_everywhere(result: &ExecTestResults) -> impl Iterator<Item = &kittycad_modeling_cmds::ModelingCmd> {
5660 let module_commands = result
5661 .exec_state
5662 .global
5663 .module_infos
5664 .values()
5665 .filter_map(|info| match &info.repr {
5666 ModuleRepr::Kcl(_, Some(outcome)) => Some(outcome.artifacts.commands.iter()),
5667 _ => None,
5668 })
5669 .flatten();
5670 result
5671 .root_module_artifact_commands()
5672 .iter()
5673 .chain(module_commands)
5674 .map(|artifact_command| &artifact_command.command)
5675 }
5676
5677 fn emitted_fillet_versions_everywhere(
5681 result: &ExecTestResults,
5682 ) -> Vec<kittycad_modeling_cmds::shared::EdgeCutVersion> {
5683 commands_everywhere(result)
5684 .filter_map(|command| match command {
5685 kittycad_modeling_cmds::ModelingCmd::Solid3dCutEdges(command) => Some(command.version),
5686 _ => None,
5687 })
5688 .collect()
5689 }
5690
5691 fn emitted_region_versions_everywhere(
5696 result: &ExecTestResults,
5697 ) -> Vec<kittycad_modeling_cmds::shared::RegionVersion> {
5698 commands_everywhere(result)
5699 .filter_map(|command| match command {
5700 kittycad_modeling_cmds::ModelingCmd::CreateRegion(command) => Some(command.version.clone()),
5701 _ => None,
5702 })
5703 .collect()
5704 }
5705
5706 const FILLET_AT_MODULE_TOP_LEVEL: &str = r#"
5707profile = startSketchOn(XY)
5708 |> startProfile(at = [0, 0])
5709 |> line(end = [10, 0], tag = $edge)
5710 |> line(end = [0, 10])
5711 |> line(end = [-10, 0])
5712 |> close()
5713solid = extrude(profile, length = 10)
5714fillet(solid, tags = [edge], radius = 1)
5715"#;
5716
5717 const FILLET_IN_EXPORTED_FN: &str = r#"
5718export fn filletedBox() {
5719 profile = startSketchOn(XY)
5720 |> startProfile(at = [0, 0])
5721 |> line(end = [10, 0], tag = $edge)
5722 |> line(end = [0, 10])
5723 |> line(end = [-10, 0])
5724 |> close()
5725 solid = extrude(profile, length = 10)
5726 return fillet(solid, tags = [edge], radius = 1)
5727}
5728"#;
5729
5730 #[tokio::test(flavor = "multi_thread")]
5737 async fn entry_point_v3_pins_kcl_version_for_imported_modules() {
5738 use kittycad_modeling_cmds::shared::EdgeCutVersion;
5739
5740 let dep = FILLET_AT_MODULE_TOP_LEVEL;
5741 let main = r#"@settings(kclVersion = "3.0-preview")
5742import "dep.kcl" as dep
5743"#;
5744 let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
5745 assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
5746
5747 let dep = FILLET_IN_EXPORTED_FN;
5748 let main = r#"@settings(kclVersion = "3.0-preview")
5749import filletedBox from "dep.kcl"
5750box = filletedBox()
5751"#;
5752 let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
5753 assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
5754 }
5755
5756 const REGION_AT_MODULE_TOP_LEVEL: &str = r#"
5757profile = sketch(on = XY) {
5758 outline = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
5759}
5760disc = region(segments = [profile.outline])
5761"#;
5762
5763 const REGION_IN_EXPORTED_FN: &str = r#"
5764export fn disc() {
5765 profile = sketch(on = XY) {
5766 outline = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
5767 }
5768 return region(segments = [profile.outline])
5769}
5770"#;
5771
5772 #[tokio::test(flavor = "multi_thread")]
5778 async fn legacy_kcl_version_quirk_applies_without_v3_entry_point() {
5779 use kittycad_modeling_cmds::shared::RegionVersion;
5780
5781 let dep = format!("@settings(kclVersion = 1.0)\n{REGION_AT_MODULE_TOP_LEVEL}");
5782 let main = r#"@settings(kclVersion = 2.0)
5783import "dep.kcl" as dep
5784"#;
5785 let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
5786 assert_eq!(emitted_region_versions_everywhere(&result), vec![RegionVersion::V0]);
5787
5788 let dep = format!("@settings(kclVersion = 1.0)\n{REGION_IN_EXPORTED_FN}");
5789 let main = r#"@settings(kclVersion = 2.0)
5790import disc from "dep.kcl"
5791face = disc()
5792"#;
5793 let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
5794 assert_eq!(emitted_region_versions_everywhere(&result), vec![RegionVersion::V1]);
5795 }
5796
5797 fn versioned_modules_context(modules: &[(&str, &str)]) -> ExecutorContext {
5802 let project_dir = crate::TypedPath::new("/zma-kcl-version-mismatch");
5803 let files = modules
5806 .iter()
5807 .map(|(name, source)| (project_dir.join(name).to_string(), source.as_bytes().to_vec()))
5808 .collect();
5809 ExecutorContext {
5810 engine: Arc::new(EngineManager::new_mock()),
5811 engine_batch: EngineBatchContext::default(),
5812 fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
5813 settings: ExecutorSettings {
5814 current_file: Some(project_dir.join("main.kcl")),
5815 project_directory: Some(project_dir),
5816 ..Default::default()
5817 },
5818 context_type: ContextType::Mock,
5819 execution_callbacks: Default::default(),
5820 executor_kind: machine::ExecutorKind::resolve(),
5821 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
5822 }
5823 }
5824
5825 async fn run_versioned_modules(main: &str, modules: &[(&str, &str)]) -> Result<(), KclError> {
5828 let ctx = versioned_modules_context(modules);
5829 let program = crate::Program::parse_no_errs(main).unwrap();
5830 let mut exec_state = ExecState::new(&ctx);
5831 let result = ctx.run(&program, &mut exec_state).await;
5832 ctx.close().await;
5833 result.map(|_| ()).map_err(|err| err.error)
5834 }
5835
5836 async fn run_versioned_modules_mock(main: &str, modules: &[(&str, &str)]) -> Result<(), KclError> {
5839 let ctx = versioned_modules_context(modules);
5840 let program = crate::Program::parse_no_errs(main).unwrap();
5841 let mock_config = MockConfig {
5842 use_prev_memory: false,
5843 ..Default::default()
5844 };
5845 let result = ctx.run_mock_returning_state(&program, &mock_config).await;
5846 ctx.close().await;
5847 result.map(|_| ()).map_err(|err| err.error)
5848 }
5849
5850 const V3_MAIN_IMPORTING_DEP: &str =
5851 "@settings(kclVersion = \"3.0-preview\")\nimport width from \"dep.kcl\"\nx = width\n";
5852
5853 fn dep_declaring(version: &str) -> String {
5854 format!("@settings(kclVersion = {version})\nexport width = 10\n")
5855 }
5856
5857 #[track_caller]
5860 fn assert_kcl_version_mismatch(error: &KclError, expected_dep_version: &str) {
5861 assert!(matches!(error, KclError::Semantic { .. }), "{error:#?}");
5862 assert_eq!(
5863 error.message(),
5864 format!(
5865 "Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` declares kclVersion 3.0-preview, but the imported file `/zma-kcl-version-mismatch/dep.kcl` declares kclVersion {expected_dep_version}. Update the kclVersion setting in one of these files to match the other."
5866 )
5867 );
5868 }
5869
5870 #[tokio::test(flavor = "multi_thread")]
5875 async fn imported_module_kcl_version_must_match_v3_entry_point() {
5876 for dep_version in ["2.0", "1.0"] {
5877 let dep = dep_declaring(dep_version);
5878 let error = run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
5879 .await
5880 .expect_err("mismatched kclVersion should be rejected");
5881 assert_kcl_version_mismatch(&error, dep_version);
5882
5883 let ranges = error.source_ranges();
5884 assert_eq!(ranges.len(), 2, "{ranges:#?}");
5885 assert!(!ranges[0].module_id().is_top_level());
5887 let declaration = format!("kclVersion = {dep_version}");
5888 let start = dep.find(&declaration).unwrap();
5889 assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + declaration.len()));
5890 assert!(ranges[1].module_id().is_top_level());
5892 let import_stmt = "import width from \"dep.kcl\"";
5893 let start = V3_MAIN_IMPORTING_DEP.find(import_stmt).unwrap();
5894 assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
5895 assert_eq!(
5896 error
5897 .backtrace()
5898 .iter()
5899 .map(|frame| frame.fn_name.as_deref())
5900 .collect::<Vec<_>>(),
5901 [Some("import dep.kcl"), None]
5902 );
5903 }
5904 }
5905
5906 #[tokio::test(flavor = "multi_thread")]
5909 async fn imported_module_without_kcl_version_is_allowed_under_v3_entry_point() {
5910 for dep in [
5911 "export width = 10\n",
5912 "@settings(defaultLengthUnit = in)\nexport width = 10\n",
5913 ] {
5914 run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep)])
5915 .await
5916 .unwrap_or_else(|err| panic!("dep={dep:?}: {err:#?}"));
5917 }
5918 }
5919
5920 #[tokio::test(flavor = "multi_thread")]
5923 async fn imported_module_matching_v3_kcl_version_is_allowed() {
5924 for dep_version in ["\"3.0-preview\"", "\"3-preview\"", "\"3.0.0-preview\""] {
5925 let dep = dep_declaring(dep_version);
5926 run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
5927 .await
5928 .unwrap_or_else(|err| panic!("dep={dep_version}: {err:#?}"));
5929 }
5930 }
5931
5932 #[tokio::test(flavor = "multi_thread")]
5935 async fn pre_v3_kcl_versions_may_be_mixed_without_v3_entry_point() {
5936 for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
5937 for dep_version in ["1.0", "2.0"] {
5938 let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
5939 let dep = dep_declaring(dep_version);
5940 run_versioned_modules(&main, &[("dep.kcl", &dep)])
5941 .await
5942 .unwrap_or_else(|err| panic!("main={main_header:?} dep={dep_version}: {err:#?}"));
5943 }
5944 }
5945 }
5946
5947 const V3_DEP_UNDER_UNDECLARED_ENTRY_POINT: &str = "Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` does not declare a kclVersion, but the imported file `/zma-kcl-version-mismatch/dep.kcl` declares kclVersion 3.0-preview. Declare the same kclVersion in the entry point, or update the setting in the imported file.";
5950
5951 #[tokio::test(flavor = "multi_thread")]
5958 async fn v3_import_requires_v3_entry_point() {
5959 for (main_header, entry_point_declares, fix) in [
5960 (
5961 "",
5962 "does not declare a kclVersion",
5963 "Declare the same kclVersion in the entry point, or update the setting in the imported file.",
5964 ),
5965 (
5966 "@settings(kclVersion = 1.0)\n",
5967 "declares kclVersion 1.0",
5968 "Update the kclVersion setting in one of these files to match the other.",
5969 ),
5970 (
5971 "@settings(kclVersion = 2.0)\n",
5972 "declares kclVersion 2.0",
5973 "Update the kclVersion setting in one of these files to match the other.",
5974 ),
5975 ] {
5976 let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
5977 let dep = dep_declaring("\"3.0-preview\"");
5978 let error = run_versioned_modules(&main, &[("dep.kcl", &dep)])
5979 .await
5980 .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
5981 assert!(matches!(error, KclError::Semantic { .. }), "{error:#?}");
5982 assert_eq!(
5983 error.message(),
5984 format!(
5985 "Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` {entry_point_declares}, but the imported file `/zma-kcl-version-mismatch/dep.kcl` declares kclVersion 3.0-preview. {fix}"
5986 ),
5987 "main={main_header:?}"
5988 );
5989
5990 let ranges = error.source_ranges();
5991 assert_eq!(ranges.len(), 2, "{ranges:#?}");
5992 assert!(!ranges[0].module_id().is_top_level());
5994 let declaration = "kclVersion = \"3.0-preview\"";
5995 let start = dep.find(declaration).unwrap();
5996 assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + declaration.len()));
5997 assert!(ranges[1].module_id().is_top_level());
5999 let import_stmt = "import width from \"dep.kcl\"";
6000 let start = main.find(import_stmt).unwrap();
6001 assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
6002 assert_eq!(
6003 error
6004 .backtrace()
6005 .iter()
6006 .map(|frame| frame.fn_name.as_deref())
6007 .collect::<Vec<_>>(),
6008 [Some("import dep.kcl"), None]
6009 );
6010 }
6011 }
6012
6013 #[tokio::test(flavor = "multi_thread")]
6016 async fn v3_import_spellings_are_all_rejected_without_v3_entry_point() {
6017 let main = "import width from \"dep.kcl\"\nx = width\n";
6018 for dep_version in ["\"3-preview\"", "\"3.0.0-preview\""] {
6019 let dep = dep_declaring(dep_version);
6020 let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
6021 .await
6022 .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
6023 assert_eq!(
6024 error.message(),
6025 V3_DEP_UNDER_UNDECLARED_ENTRY_POINT,
6026 "dep={dep_version}"
6027 );
6028 }
6029 }
6030
6031 #[tokio::test(flavor = "multi_thread")]
6034 async fn unreferenced_v3_whole_module_import_is_checked_in_mock_execution() {
6035 let main = "import \"dep.kcl\" as dep\nx = 1\n";
6036 let dep = dep_declaring("\"3.0-preview\"");
6037 let error = run_versioned_modules_mock(main, &[("dep.kcl", &dep)])
6038 .await
6039 .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected in mock execution");
6040 assert_eq!(error.message(), V3_DEP_UNDER_UNDECLARED_ENTRY_POINT);
6041 let ranges = error.source_ranges();
6042 assert_eq!(ranges.len(), 2, "{ranges:#?}");
6043 assert!(!ranges[0].module_id().is_top_level());
6044 assert!(ranges[1].module_id().is_top_level());
6045 let import_stmt = "import \"dep.kcl\" as dep";
6046 let start = main.find(import_stmt).unwrap();
6047 assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
6048
6049 let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
6051 .await
6052 .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected in engine execution");
6053 assert_eq!(error.message(), V3_DEP_UNDER_UNDECLARED_ENTRY_POINT);
6054 }
6055
6056 #[tokio::test(flavor = "multi_thread")]
6060 async fn transitive_v3_import_requires_v3_entry_point() {
6061 let main = "import doubled from \"a.kcl\"\nx = doubled\n";
6062 let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
6063 let b = dep_declaring("\"3.0-preview\"");
6064 let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", &b)])
6065 .await
6066 .expect_err("a transitive KCL 3.0 import without a KCL 3.0 entry point should be rejected");
6067 assert_eq!(
6068 error.message(),
6069 "Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` does not declare a kclVersion, but the imported file `/zma-kcl-version-mismatch/b.kcl` declares kclVersion 3.0-preview. Declare the same kclVersion in the entry point, or update the setting in the imported file."
6070 );
6071 assert_eq!(
6072 error
6073 .backtrace()
6074 .iter()
6075 .map(|frame| frame.fn_name.as_deref())
6076 .collect::<Vec<_>>(),
6077 [Some("import b.kcl"), Some("import a.kcl"), None]
6078 );
6079 }
6080
6081 #[tokio::test(flavor = "multi_thread")]
6084 async fn v3_import_without_v3_entry_point_or_entry_point_path() {
6085 let main = "import width from \"dep.kcl\"\nx = width\n";
6086 let dep = dep_declaring("\"3.0-preview\"");
6087 let error = execute_with_modules(main, &[("dep.kcl", &dep)])
6088 .await
6089 .expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
6090 let message = error.message();
6091 assert!(
6092 message.starts_with(
6093 "Mixing KCL versions in a single program is not allowed. The entry point does not declare a kclVersion, but the imported file `"
6094 ),
6095 "{message}"
6096 );
6097 assert!(
6098 message.ends_with(
6099 "dep.kcl` declares kclVersion 3.0-preview. Declare the same kclVersion in the entry point, or update the setting in the imported file."
6100 ),
6101 "{message}"
6102 );
6103 }
6104
6105 #[tokio::test(flavor = "multi_thread")]
6109 async fn transitive_import_kcl_version_mismatch_names_entry_point_and_mismatched_file() {
6110 let main = "@settings(kclVersion = \"3.0-preview\")\nimport doubled from \"a.kcl\"\nx = doubled\n";
6111 let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
6112 let b = dep_declaring("2.0");
6113 let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", &b)])
6114 .await
6115 .expect_err("mismatched kclVersion in a transitive import should be rejected");
6116 assert_eq!(
6117 error.message(),
6118 "Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` declares kclVersion 3.0-preview, but the imported file `/zma-kcl-version-mismatch/b.kcl` declares kclVersion 2.0. Update the kclVersion setting in one of these files to match the other."
6119 );
6120 assert_eq!(
6121 error
6122 .backtrace()
6123 .iter()
6124 .map(|frame| frame.fn_name.as_deref())
6125 .collect::<Vec<_>>(),
6126 [Some("import b.kcl"), Some("import a.kcl"), None]
6127 );
6128 let ranges = error.source_ranges();
6129 assert_eq!(ranges.len(), 3, "{ranges:#?}");
6130 assert!(!ranges[0].module_id().is_top_level());
6131 assert!(!ranges[1].module_id().is_top_level());
6132 assert!(ranges[2].module_id().is_top_level());
6133 }
6134
6135 #[tokio::test(flavor = "multi_thread")]
6138 async fn unreferenced_whole_module_import_kcl_version_is_checked_in_mock_execution() {
6139 let main = "@settings(kclVersion = \"3.0-preview\")\nimport \"dep.kcl\" as dep\nx = 1\n";
6140 let dep = dep_declaring("2.0");
6141 let error = run_versioned_modules_mock(main, &[("dep.kcl", &dep)])
6142 .await
6143 .expect_err("mismatched kclVersion should be rejected in mock execution");
6144 assert_kcl_version_mismatch(&error, "2.0");
6145 let ranges = error.source_ranges();
6146 assert_eq!(ranges.len(), 2, "{ranges:#?}");
6147 assert!(!ranges[0].module_id().is_top_level());
6148 assert!(ranges[1].module_id().is_top_level());
6149 let import_stmt = "import \"dep.kcl\" as dep";
6150 let start = main.find(import_stmt).unwrap();
6151 assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
6152
6153 let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
6155 .await
6156 .expect_err("mismatched kclVersion should be rejected in engine execution");
6157 assert_kcl_version_mismatch(&error, "2.0");
6158
6159 run_versioned_modules_mock(main, &[("dep.kcl", "export width = 10\n")])
6161 .await
6162 .unwrap();
6163 }
6164
6165 #[tokio::test(flavor = "multi_thread")]
6166 async fn imported_use_keyword_follows_entry_point_version() {
6167 let dep = "use = 10\nexport width = use\n";
6168 for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
6169 let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6170 run_versioned_modules(&main, &[("dep.kcl", dep)])
6171 .await
6172 .unwrap_or_else(|error| panic!("main={main_header:?}: {error:#?}"));
6173 }
6174
6175 let main_v1 = "@settings(kclVersion = 1.0)\nimport width from \"dep.kcl\"\nx = width\n";
6176 let dep_v2 = format!("@settings(kclVersion = 2.0)\n{dep}");
6177 run_versioned_modules(main_v1, &[("dep.kcl", &dep_v2)]).await.unwrap();
6178
6179 for dep in [
6180 dep.to_owned(),
6181 format!("@settings(kclVersion = \"3.0-preview\")\n{dep}"),
6182 ] {
6183 let error = run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
6184 .await
6185 .expect_err("V3 imports must reject a use identifier");
6186 assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6187 assert_eq!(error.message(), crate::parsing::RESERVED_USE_MESSAGE);
6188 let ranges = error.source_ranges();
6189 assert_eq!(ranges.len(), 2, "{ranges:#?}");
6190 let start = dep.find("use =").unwrap();
6191 assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + 3));
6192 assert!(!ranges[0].module_id().is_top_level());
6193 assert!(ranges[1].module_id().is_top_level());
6194 }
6195 }
6196
6197 #[tokio::test(flavor = "multi_thread")]
6198 async fn imported_never_type_follows_entry_point_version() {
6199 let dep = "export fn stop(): never {}\n";
6200 for version in ["1.0", "2.0"] {
6201 let main = format!("@settings(kclVersion = {version})\nimport stop from \"dep.kcl\"\nx = 1\n");
6202 for run_mock in [false, true] {
6203 let error = if run_mock {
6204 run_versioned_modules_mock(&main, &[("dep.kcl", dep)]).await
6205 } else {
6206 run_versioned_modules(&main, &[("dep.kcl", dep)]).await
6207 }
6208 .expect_err("older KCL versions must reject imported `never` types");
6209 assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6210 assert_eq!(
6211 error.message(),
6212 format!("The `never` type requires KCL 3.0-preview, but this program uses KCL {version}.")
6213 );
6214 let ranges = error.source_ranges();
6215 assert_eq!(ranges.len(), 2, "{ranges:#?}");
6216 let start = dep.find("never").unwrap();
6217 assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + "never".len()));
6218 assert!(!ranges[0].module_id().is_top_level());
6219 assert!(ranges[1].module_id().is_top_level());
6220 }
6221 }
6222
6223 let main_v3 = "@settings(kclVersion = \"3.0-preview\")\nimport stop from \"dep.kcl\"\nx = 1\n";
6224 run_versioned_modules(main_v3, &[("dep.kcl", dep)]).await.unwrap();
6225
6226 let dep_v2 = format!("@settings(kclVersion = 2.0)\n{dep}");
6227 let error = run_versioned_modules(main_v3, &[("dep.kcl", &dep_v2)])
6228 .await
6229 .expect_err("version mismatch must precede `never` validation");
6230 assert_kcl_version_mismatch(&error, "2.0");
6231 assert_eq!(
6232 error
6233 .backtrace()
6234 .iter()
6235 .map(|frame| frame.fn_name.as_deref())
6236 .collect::<Vec<_>>(),
6237 [Some("import dep.kcl"), None]
6238 );
6239
6240 let main_v2 = "@settings(kclVersion = 2.0)\nimport stop from \"dep.kcl\"\nx = 1\n";
6241 let dep_v3 = format!("@settings(kclVersion = \"3.0-preview\")\n{dep}");
6242 let error = run_versioned_modules(main_v2, &[("dep.kcl", &dep_v3)])
6243 .await
6244 .expect_err("version mismatch must precede `never` validation");
6245 assert!(
6246 error
6247 .message()
6248 .starts_with("Mixing KCL versions in a single program is not allowed.")
6249 );
6250
6251 let unused_import = "@settings(kclVersion = 2.0)\nimport \"dep.kcl\" as dep\nx = 1\n";
6253 let error = run_versioned_modules_mock(unused_import, &[("dep.kcl", dep)])
6254 .await
6255 .expect_err("an unused imported module must not bypass `never` validation");
6256 assert_eq!(
6257 error.message(),
6258 "The `never` type requires KCL 3.0-preview, but this program uses KCL 2.0."
6259 );
6260 }
6261
6262 #[tokio::test(flavor = "multi_thread")]
6263 async fn never_type_resolution_rejects_an_unvalidated_v2_ast() {
6264 let source = "@settings(kclVersion = 2.0)\nfn stop(): never {}\n";
6265 let (ast, _) = crate::parsing::parse_str_syntax(source, ModuleId::default()).unwrap();
6266 let program = crate::Program {
6267 ast,
6268 original_file_contents: source.to_owned(),
6269 };
6270 let ctx = versioned_modules_context(&[]);
6271 let mut exec_state = ExecState::new(&ctx);
6272 let error = ctx.run(&program, &mut exec_state).await.unwrap_err().error;
6273 ctx.close().await;
6274
6275 assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6276 assert_eq!(
6277 error.message(),
6278 "The `never` type requires KCL 3.0-preview, but this program uses KCL 2.0."
6279 );
6280 }
6281
6282 #[tokio::test(flavor = "multi_thread")]
6283 async fn import_universe_uses_entry_point_version_in_a_fresh_state() {
6284 let main = "@settings(kclVersion = \"3.0-preview\")\nimport stop from \"dep.kcl\"\nx = 1\n";
6285 let ctx = versioned_modules_context(&[("dep.kcl", "export fn stop(): never {}\n")]);
6286 let program = crate::Program::parse_no_errs(main).unwrap();
6287 let mut exec_state = ExecState::new(&ctx);
6288
6289 ctx.get_universe(&program, &mut exec_state).await.unwrap();
6290 assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V3Preview));
6291 ctx.close().await;
6292 }
6293
6294 #[tokio::test(flavor = "multi_thread")]
6295 async fn imported_enum_identifier_follows_entry_point_version() {
6296 let dep = "enum = 10\nexport width = enum\n";
6297 for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
6298 let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6299 run_versioned_modules(&main, &[("dep.kcl", dep)])
6300 .await
6301 .unwrap_or_else(|error| panic!("main={main_header:?}: {error:#?}"));
6302 }
6303
6304 for run_mock in [false, true] {
6305 let error = if run_mock {
6306 run_versioned_modules_mock(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep)]).await
6307 } else {
6308 run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep)]).await
6309 }
6310 .expect_err("V3 imports must reject an enum identifier");
6311 assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6312 assert_eq!(error.message(), crate::parsing::RESERVED_ENUM_MESSAGE);
6313 let ranges = error.source_ranges();
6314 assert_eq!(ranges.len(), 2, "{ranges:#?}");
6315 assert_eq!((ranges[0].start(), ranges[0].end()), (0, "enum".len()));
6316 assert!(!ranges[0].module_id().is_top_level());
6317 assert!(ranges[1].module_id().is_top_level());
6318 }
6319
6320 let dep_v2 = format!("@settings(kclVersion = 2.0)\n{dep}");
6321 let error = run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep_v2)])
6322 .await
6323 .expect_err("version mismatch must precede enum validation");
6324 assert_kcl_version_mismatch(&error, "2.0");
6325 }
6326
6327 #[tokio::test(flavor = "multi_thread")]
6328 async fn imported_import_modifiers_follow_entry_point_version() {
6329 for word in ["template", "lazy", "component"] {
6330 let dep = format!("import {word} from \"nested.kcl\"\nexport width = 10\n");
6331 let nested = format!("export {word} = 1\n");
6332 for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
6333 let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
6334 run_versioned_modules(&main, &[("dep.kcl", &dep), ("nested.kcl", &nested)])
6335 .await
6336 .unwrap_or_else(|error| panic!("main={main_header:?}, word={word}: {error:#?}"));
6337 }
6338
6339 for run_mock in [false, true] {
6340 let error = if run_mock {
6341 run_versioned_modules_mock(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep), ("nested.kcl", &nested)])
6342 .await
6343 } else {
6344 run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep), ("nested.kcl", &nested)]).await
6345 }
6346 .expect_err("V3 imports must reject a reserved import modifier");
6347 assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6348 assert_eq!(
6349 error.message(),
6350 format!(
6351 "`{word}` is reserved as an import modifier in KCL 3.0 and cannot be the first imported item"
6352 )
6353 );
6354 let ranges = error.source_ranges();
6355 assert_eq!(ranges.len(), 2, "{ranges:#?}");
6356 let start = dep.find(word).unwrap();
6357 assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + word.len()));
6358 assert!(!ranges[0].module_id().is_top_level());
6359 assert!(ranges[1].module_id().is_top_level());
6360 }
6361 }
6362 }
6363
6364 #[tokio::test(flavor = "multi_thread")]
6365 async fn import_version_mismatch_precedes_import_modifier_error() {
6366 let dep = "@settings(kclVersion = 2.0)\nimport lazy from \"nested.kcl\"\nexport width = 10\n";
6367 let nested = "export lazy = 1\n";
6368 for run_mock in [false, true] {
6369 let error = if run_mock {
6370 run_versioned_modules_mock(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep), ("nested.kcl", nested)]).await
6371 } else {
6372 run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep), ("nested.kcl", nested)]).await
6373 }
6374 .expect_err("version mismatch must precede import modifier validation");
6375 assert_kcl_version_mismatch(&error, "2.0");
6376 }
6377 }
6378
6379 #[tokio::test(flavor = "multi_thread")]
6380 async fn imported_use_function_name_is_allowed_under_v3() {
6381 let dep = "fn use() { return 10 }\nexport width = use()\n";
6382 run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep)])
6383 .await
6384 .unwrap();
6385 }
6386
6387 #[tokio::test(flavor = "multi_thread")]
6388 async fn import_version_mismatch_precedes_use_keyword_error() {
6389 let dep_v2 = "@settings(kclVersion = 2.0)\nuse = 10\nexport width = use\n";
6390 let error = run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep_v2)])
6391 .await
6392 .expect_err("version mismatch must precede the use keyword error");
6393 assert_kcl_version_mismatch(&error, "2.0");
6394
6395 let main_v2 = "@settings(kclVersion = 2.0)\nimport width from \"dep.kcl\"\nx = width\n";
6396 let dep_v3 = "@settings(kclVersion = \"3.0-preview\")\nuse = 10\nexport width = use\n";
6397 let error = run_versioned_modules(main_v2, &[("dep.kcl", dep_v3)])
6398 .await
6399 .expect_err("version mismatch must precede the use keyword error");
6400 assert!(
6401 error
6402 .message()
6403 .starts_with("Mixing KCL versions in a single program is not allowed.")
6404 );
6405 }
6406
6407 #[tokio::test(flavor = "multi_thread")]
6408 async fn unreferenced_whole_module_import_checks_use_keyword_in_mock_execution() {
6409 let main = "@settings(kclVersion = \"3.0-preview\")\nimport \"dep.kcl\" as dep\nx = 1\n";
6410 let dep = "use = 10\n";
6411 let error = run_versioned_modules_mock(main, &[("dep.kcl", dep)])
6412 .await
6413 .expect_err("mock import must reject a use identifier");
6414 assert_eq!(error.message(), crate::parsing::RESERVED_USE_MESSAGE);
6415 let ranges = error.source_ranges();
6416 assert_eq!(ranges.len(), 2, "{ranges:#?}");
6417 assert!(!ranges[0].module_id().is_top_level());
6418 assert!(ranges[1].module_id().is_top_level());
6419
6420 let dep_v2 = "@settings(kclVersion = 2.0)\nuse = 10\n";
6421 let error = run_versioned_modules_mock(main, &[("dep.kcl", dep_v2)])
6422 .await
6423 .expect_err("version mismatch must precede the use keyword error in mock execution");
6424 assert_kcl_version_mismatch(&error, "2.0");
6425 }
6426
6427 #[tokio::test(flavor = "multi_thread")]
6428 async fn transitive_import_checks_use_keyword_under_v3_entry_point() {
6429 let main = "@settings(kclVersion = \"3.0-preview\")\nimport doubled from \"a.kcl\"\nx = doubled\n";
6430 let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
6431 let b = "use = 10\nexport width = use\n";
6432 let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", b)])
6433 .await
6434 .expect_err("transitive import must reject a use identifier");
6435 assert!(matches!(error, KclError::Syntax { .. }), "{error:#?}");
6436 assert_eq!(error.message(), crate::parsing::RESERVED_USE_MESSAGE);
6437 let ranges = error.source_ranges();
6438 assert_eq!(ranges.len(), 3, "{ranges:#?}");
6439 assert!(!ranges[0].module_id().is_top_level());
6440 assert!(!ranges[1].module_id().is_top_level());
6441 assert!(ranges[2].module_id().is_top_level());
6442 assert_eq!(
6443 error
6444 .backtrace()
6445 .iter()
6446 .map(|frame| frame.fn_name.as_deref())
6447 .collect::<Vec<_>>(),
6448 [Some("import b.kcl"), Some("import a.kcl"), None]
6449 );
6450 }
6451
6452 #[tokio::test(flavor = "multi_thread")]
6457 async fn std_modules_are_exempt_from_kcl_version_matching() {
6458 let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport QUARTER_TURN from \"std::turns\"\nx = QUARTER_TURN\n";
6459 run_versioned_modules(main, &[]).await.unwrap();
6460 run_versioned_modules_mock(main, &[]).await.unwrap();
6461 }
6462
6463 #[tokio::test(flavor = "multi_thread")]
6466 async fn kcl_version_mismatch_without_entry_point_path() {
6467 let dep = dep_declaring("2.0");
6468 let error = execute_with_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
6469 .await
6470 .expect_err("mismatched kclVersion should be rejected");
6471 let message = error.message();
6472 assert!(
6473 message.starts_with(
6474 "Mixing KCL versions in a single program is not allowed. The entry point declares kclVersion 3.0-preview, but the imported file `"
6475 ),
6476 "{message}"
6477 );
6478 assert!(
6479 message.ends_with(
6480 "dep.kcl` declares kclVersion 2.0. Update the kclVersion setting in one of these files to match the other."
6481 ),
6482 "{message}"
6483 );
6484 }
6485
6486 #[track_caller]
6487 fn variable_f64(result: &ExecTestResults, name: &str) -> f64 {
6488 mem_get_json(result.exec_state.stack(), result.mem_env, name)
6489 .as_f64()
6490 .unwrap()
6491 }
6492
6493 #[tokio::test(flavor = "multi_thread")]
6494 async fn return_terminates_function_early_in_v3() {
6495 let code = r#"@settings(kclVersion = "3.0-preview")
6496fn f() {
6497 return 1
6498 assert(1, isEqualTo = 2, error = "code after return ran")
6499}
6500x = f()
6501"#;
6502 let result = parse_execute(code).await.unwrap();
6503 assert_eq!(variable_f64(&result, "x"), 1.0);
6504 }
6505
6506 #[tokio::test(flavor = "multi_thread")]
6507 async fn second_return_is_unreachable_in_v3() {
6508 let code = r#"@settings(kclVersion = "3.0-preview")
6509fn f() {
6510 return 1
6511 return 2
6512}
6513x = f()
6514"#;
6515 let result = parse_execute(code).await.unwrap();
6516 assert_eq!(variable_f64(&result, "x"), 1.0);
6517 }
6518
6519 #[tokio::test(flavor = "multi_thread")]
6520 async fn return_inside_if_arm_returns_from_function_in_v3() {
6521 let code = r#"@settings(kclVersion = "3.0-preview")
6522fn f(@b) {
6523 dummy = if b {
6524 return 1
6525 0
6526 } else {
6527 0
6528 }
6529 return 2
6530}
6531x = f(true)
6532y = f(false)
6533"#;
6534 let result = parse_execute(code).await.unwrap();
6535 assert_eq!(variable_f64(&result, "x"), 1.0);
6536 assert_eq!(variable_f64(&result, "y"), 2.0);
6537 }
6538
6539 #[tokio::test(flavor = "multi_thread")]
6540 async fn return_inside_nested_if_returns_from_function_in_v3() {
6541 let code = r#"@settings(kclVersion = "3.0-preview")
6542fn f(@a, b) {
6543 dummy = if a {
6544 inner = if b {
6545 return 10
6546 0
6547 } else {
6548 1
6549 }
6550 inner + 1
6551 } else {
6552 2
6553 }
6554 return dummy * 100
6555}
6556x = f(true, b = true)
6557y = f(true, b = false)
6558z = f(false, b = false)
6559"#;
6560 let result = parse_execute(code).await.unwrap();
6561 assert_eq!(variable_f64(&result, "x"), 10.0);
6562 assert_eq!(variable_f64(&result, "y"), 200.0);
6563 assert_eq!(variable_f64(&result, "z"), 200.0);
6564 }
6565
6566 #[tokio::test(flavor = "multi_thread")]
6567 async fn return_inside_closure_returns_only_from_closure_in_v3() {
6568 let code = r#"@settings(kclVersion = "3.0-preview")
6569fn outer() {
6570 inner = fn() {
6571 return 5
6572 assert(1, isEqualTo = 2, error = "code after inner return ran")
6573 }
6574 v = inner()
6575 return v + 1
6576}
6577x = outer()
6578"#;
6579 let result = parse_execute(code).await.unwrap();
6580 assert_eq!(variable_f64(&result, "x"), 6.0);
6581 }
6582
6583 #[tokio::test(flavor = "multi_thread")]
6584 async fn return_type_coercion_applies_to_early_return_in_v3() {
6585 let code = r#"@settings(kclVersion = "3.0-preview")
6586fn f(): number(mm) {
6587 return 1
6588 assert(1, isEqualTo = 2, error = "code after return ran")
6589}
6590x = f()
6591"#;
6592 let result = parse_execute(code).await.unwrap();
6593 assert_eq!(variable_f64(&result, "x"), 1.0);
6594
6595 let code = r#"@settings(kclVersion = "3.0-preview")
6598fn f(): number(mm) {
6599 return "nope"
6600}
6601x = f()
6602"#;
6603 let err = parse_execute(code).await.expect_err("coercion failure should error");
6604 assert!(err.message().contains("type"), "unexpected message: {}", err.message());
6605 }
6606
6607 #[tokio::test(flavor = "multi_thread")]
6608 async fn return_at_top_level_errors() {
6609 for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
6611 let code = format!("{header}return 1\n");
6612 assert_eq!(
6613 parse_execute(&code).await.expect_err("should error").message(),
6614 "Cannot return from outside a function."
6615 );
6616 }
6617
6618 let code = r#"@settings(kclVersion = "3.0-preview")
6622x = if true {
6623 return 1
6624 0
6625} else {
6626 0
6627}
6628"#;
6629 assert_eq!(
6630 parse_execute(code).await.expect_err("should error").message(),
6631 "Cannot return from outside a function."
6632 );
6633 }
6634
6635 #[tokio::test(flavor = "multi_thread")]
6636 async fn exit_inside_function_still_exits_program_in_v3() {
6637 let code = r#"@settings(kclVersion = "3.0-preview")
6638fn f() {
6639 exit()
6640 return 1
6641}
6642x = f()
6643assert(1, isEqualTo = 2, error = "code after exit ran")
6644"#;
6645 parse_execute(code).await.unwrap();
6646 }
6647
6648 #[tokio::test(flavor = "multi_thread")]
6649 async fn return_inside_sketch_block_terminates_function_in_v3() {
6650 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
6651fn f() {
6652 sketch(on = XY) {
6653 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
6654 return 42
6655 }
6656 return 0
6657}
6658x = f()
6659"#;
6660 let result = parse_execute(code).await.unwrap();
6661 assert_eq!(variable_f64(&result, "x"), 42.0);
6662 }
6663
6664 #[tokio::test(flavor = "multi_thread")]
6665 async fn return_inside_sketch_block_ignored_without_v3() {
6666 let code = r#"@settings(experimentalFeatures = allow)
6669fn f() {
6670 sketch(on = XY) {
6671 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
6672 return 42
6673 }
6674 return 0
6675}
6676x = f()
6677"#;
6678 let result = parse_execute(code).await.unwrap();
6679 assert_eq!(variable_f64(&result, "x"), 0.0);
6680 }
6681
6682 #[tokio::test(flavor = "multi_thread")]
6683 async fn code_after_return_still_runs_without_v3() {
6684 let code = r#"fn f() {
6685 return 1
6686 assert(1, isEqualTo = 2, error = "ran past return")
6687}
6688x = f()
6689"#;
6690 let err = parse_execute(code).await.expect_err("should error");
6691 assert!(
6692 err.message().contains("ran past return"),
6693 "unexpected message: {}",
6694 err.message()
6695 );
6696 }
6697
6698 #[tokio::test(flavor = "multi_thread")]
6699 async fn multiple_returns_error_without_v3() {
6700 let code = r#"fn f() {
6701 return 1
6702 return 2
6703}
6704x = f()
6705"#;
6706 assert_eq!(
6707 parse_execute(code).await.expect_err("should error").message(),
6708 "Multiple returns from a single function."
6709 );
6710 }
6711
6712 #[tokio::test(flavor = "multi_thread")]
6713 async fn if_arm_return_plus_function_return_errors_without_v3() {
6714 let code = r#"fn f() {
6718 dummy = if true {
6719 return 1
6720 0
6721 } else {
6722 0
6723 }
6724 return 2
6725}
6726x = f()
6727"#;
6728 assert_eq!(
6729 parse_execute(code).await.expect_err("should error").message(),
6730 "Multiple returns from a single function."
6731 );
6732 }
6733
6734 #[tokio::test(flavor = "multi_thread")]
6735 async fn top_level_if_arm_return_ignored_without_v3() {
6736 let code = r#"x = if true {
6740 return 1
6741 0
6742} else {
6743 0
6744}
6745"#;
6746 let result = parse_execute(code).await.unwrap();
6747 assert_eq!(variable_f64(&result, "x"), 0.0);
6748 assert_eq!(variable_f64(&result, memory::RETURN_NAME), 1.0);
6749 }
6750
6751 #[tokio::test(flavor = "multi_thread")]
6754 async fn return_semantics_gated_on_entry_point_not_module() {
6755 let dep = r#"@settings(kclVersion = "3.0-preview")
6759export fn f() {
6760 return 1
6761 assert(1, isEqualTo = 2, error = "ran past return")
6762}
6763"#;
6764 let main = r#"@settings(kclVersion = 2.0)
6765import f from "dep.kcl"
6766x = f()
6767"#;
6768 let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
6769 assert!(
6770 err.message()
6771 .starts_with("Mixing KCL versions in a single program is not allowed."),
6772 "unexpected message: {}",
6773 err.message()
6774 );
6775
6776 let dep = r#"export fn f() {
6781 return 1
6782 assert(1, isEqualTo = 2, error = "ran past return")
6783}
6784"#;
6785 let main = r#"@settings(kclVersion = "3.0-preview")
6786import f from "dep.kcl"
6787x = f()
6788"#;
6789 let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
6790 assert_eq!(variable_f64(&result, "x"), 1.0);
6791 }
6792
6793 #[tokio::test(flavor = "multi_thread")]
6799 async fn return_inside_map_and_reduce_callbacks_in_v3() {
6800 let code = r#"@settings(kclVersion = "3.0-preview")
6801doubled = map([1, 2, 3], f = fn(@i) {
6802 return i * 2
6803 assert(1, isEqualTo = 2, error = "code after return ran in the map callback")
6804})
6805assert(doubled[0], isEqualTo = 2, error = "map result 0")
6806assert(doubled[1], isEqualTo = 4, error = "map result 1")
6807assert(doubled[2], isEqualTo = 6, error = "map result 2")
6808
6809total = reduce([1, 2, 3], initial = 0, f = fn(@i, accum) {
6810 return accum + i
6811 assert(1, isEqualTo = 2, error = "code after return ran in the reduce callback")
6812})
6813assert(total, isEqualTo = 6, error = "reduce total")
6814"#;
6815 let result = parse_execute(code).await.unwrap();
6816 assert_eq!(variable_f64(&result, "total"), 6.0);
6817 }
6818
6819 #[tokio::test(flavor = "multi_thread")]
6825 async fn early_returns_do_not_leak_machine_call_depth() {
6826 let code = r#"@settings(kclVersion = "3.0-preview")
6827fn one() {
6828 return 1
6829 assert(1, isEqualTo = 2, error = "code after return ran")
6830}
6831total = reduce([1..100], initial = 0, f = fn(@i, accum) {
6832 return accum + one()
6833})
6834assert(total, isEqualTo = 100, error = "each call returns 1")
6835"#;
6836 let result = parse_execute(code).await.unwrap();
6837 let high_water = result.exec_state.global.machine_depth_high_water;
6841 assert!(high_water < 10, "high water: {high_water}");
6842 }
6843
6844 #[tokio::test(flavor = "multi_thread")]
6852 async fn top_level_if_arm_return_in_imported_module_errors_in_v3() {
6853 let dep = r#"x = if true {
6854 return 1
6855 0
6856} else {
6857 0
6858}
6859export y = x
6860"#;
6861 let main = r#"@settings(kclVersion = "3.0-preview")
6862import y from "dep.kcl"
6863z = y
6864"#;
6865 let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
6866 assert!(
6867 err.message().contains("Cannot return from outside a function."),
6868 "unexpected message: {}",
6869 err.message()
6870 );
6871 }
6872
6873 #[tokio::test(flavor = "multi_thread")]
6879 async fn return_of_exit_still_exits_program_in_v3() {
6880 let code = r#"@settings(kclVersion = "3.0-preview")
6881fn f() {
6882 return exit()
6883}
6884x = f()
6885assert(1, isEqualTo = 2, error = "code after exit ran")
6886"#;
6887 parse_execute(code).await.unwrap();
6888 }
6889
6890 #[tokio::test(flavor = "multi_thread")]
6891 async fn if_arm_bindings_do_not_leak_in_v3() {
6892 let code = r#"@settings(kclVersion = "3.0-preview")
6893x = if true {
6894 y = 1
6895 y
6896} else {
6897 0
6898}
6899z = y
6900"#;
6901 let err = parse_execute(code).await.expect_err("should error");
6902 assert!(
6903 err.message().contains("`y` is not defined"),
6904 "unexpected message: {}",
6905 err.message()
6906 );
6907 }
6908
6909 #[tokio::test(flavor = "multi_thread")]
6910 async fn if_arm_bindings_leak_without_v3() {
6911 for header in ["", "@settings(kclVersion = 2.0)\n"] {
6914 let code = format!(
6915 r#"{header}x = if true {{
6916 y = 1
6917 y
6918}} else {{
6919 0
6920}}
6921z = y
6922"#
6923 );
6924 let result = parse_execute(&code).await.unwrap();
6925 assert_eq!(variable_f64(&result, "z"), 1.0);
6926 }
6927 }
6928
6929 #[tokio::test(flavor = "multi_thread")]
6930 async fn if_arm_shadowing_allowed_in_v3() {
6931 let code = r#"@settings(kclVersion = "3.0-preview")
6932y = 1
6933x = if true {
6934 y = 2
6935 y + 10
6936} else {
6937 0
6938}
6939"#;
6940 let result = parse_execute(code).await.unwrap();
6941 assert_eq!(variable_f64(&result, "x"), 12.0);
6942 assert_eq!(variable_f64(&result, "y"), 1.0);
6943 }
6944
6945 #[tokio::test(flavor = "multi_thread")]
6946 async fn if_arm_shadowing_still_errors_without_v3() {
6947 for header in ["", "@settings(kclVersion = 2.0)\n"] {
6950 let code = format!(
6951 r#"{header}y = 1
6952x = if true {{
6953 y = 2
6954 y
6955}} else {{
6956 0
6957}}
6958"#
6959 );
6960 let err = parse_execute(&code).await.expect_err("should error");
6961 assert!(
6962 err.message().contains("Cannot redefine `y`"),
6963 "unexpected message: {}",
6964 err.message()
6965 );
6966 }
6967 }
6968
6969 #[tokio::test(flavor = "multi_thread")]
6970 async fn if_arm_closure_escape_in_v3() {
6971 let code = r#"@settings(kclVersion = "3.0-preview")
6974n = 1
6975f = if true {
6976 m = 41
6977 g = fn() {
6978 return m + n
6979 }
6980 g
6981} else {
6982 g = fn() {
6983 return 0
6984 }
6985 g
6986}
6987x = f()
6988"#;
6989 let result = parse_execute(code).await.unwrap();
6990 assert_eq!(variable_f64(&result, "x"), 42.0);
6991 }
6992
6993 #[tokio::test(flavor = "multi_thread")]
6994 async fn recursive_if_arm_closure_keeps_enclosing_function_frame_alive_in_v3() {
6995 let code = r#"@settings(kclVersion = "3.0-preview")
6999fn makeCounter() {
7000 outer = 40
7001 selected = if true {
7002 inner = 2
7003 fn count(@n) {
7004 return if n == 0 {
7005 outer + inner
7006 } else {
7007 count(n - 1) + 1
7008 }
7009 }
7010 count
7011 } else {
7012 fn fallback(@n) {
7013 return n
7014 }
7015 fallback
7016 }
7017 return selected
7018}
7019counter = makeCounter()
7020x = counter(3)
7021"#;
7022 let result = parse_execute(code).await.unwrap();
7023 assert_eq!(variable_f64(&result, "x"), 45.0);
7024 }
7025
7026 #[tokio::test(flavor = "multi_thread")]
7027 async fn return_inside_scoped_if_arm_in_v3() {
7028 let code = r#"@settings(kclVersion = "3.0-preview")
7031fn f(@b) {
7032 local = if b {
7033 w = 1
7034 return w + 9
7035 0
7036 } else {
7037 0
7038 }
7039 return local
7040}
7041x = f(true)
7042y = f(false)
7043"#;
7044 let result = parse_execute(code).await.unwrap();
7045 assert_eq!(variable_f64(&result, "x"), 10.0);
7046 assert_eq!(variable_f64(&result, "y"), 0.0);
7047 }
7048
7049 #[tokio::test(flavor = "multi_thread")]
7050 async fn else_if_and_nested_if_scoping_in_v3() {
7051 let code = r#"@settings(kclVersion = "3.0-preview")
7052x = if false {
7053 0
7054} else if true {
7055 a = 1
7056 b = if true {
7057 c = 2
7058 a + c
7059 } else {
7060 0
7061 }
7062 a + b
7063} else {
7064 0
7065}
7066"#;
7067 let result = parse_execute(code).await.unwrap();
7068 assert_eq!(variable_f64(&result, "x"), 4.0);
7069
7070 let code = r#"@settings(kclVersion = "3.0-preview")
7072x = if true {
7073 b = if true {
7074 c = 2
7075 c
7076 } else {
7077 0
7078 }
7079 b + c
7080} else {
7081 0
7082}
7083"#;
7084 let err = parse_execute(code).await.expect_err("should error");
7085 assert!(
7086 err.message().contains("`c` is not defined"),
7087 "unexpected message: {}",
7088 err.message()
7089 );
7090 }
7091
7092 #[tokio::test(flavor = "multi_thread")]
7097 async fn else_if_and_final_else_arms_are_isolated_in_v3() {
7098 let code = r#"@settings(kclVersion = "3.0-preview")
7100x = if false {
7101 0
7102} else if true {
7103 y = 1
7104 y
7105} else {
7106 0
7107}
7108z = y
7109"#;
7110 let err = parse_execute(code).await.expect_err("should error");
7111 assert!(
7112 err.message().contains("`y` is not defined"),
7113 "unexpected message: {}",
7114 err.message()
7115 );
7116
7117 let code = r#"@settings(kclVersion = "3.0-preview")
7119x = if false {
7120 0
7121} else if false {
7122 0
7123} else {
7124 y = 1
7125 y
7126}
7127z = y
7128"#;
7129 let err = parse_execute(code).await.expect_err("should error");
7130 assert!(
7131 err.message().contains("`y` is not defined"),
7132 "unexpected message: {}",
7133 err.message()
7134 );
7135
7136 let code = r#"@settings(kclVersion = "3.0-preview")
7138outer = 1
7139x = if false {
7140 0
7141} else if true {
7142 outer = 2
7143 outer + 10
7144} else {
7145 0
7146}
7147"#;
7148 let result = parse_execute(code).await.unwrap();
7149 assert_eq!(variable_f64(&result, "x"), 12.0);
7150 assert_eq!(variable_f64(&result, "outer"), 1.0);
7151
7152 let code = r#"@settings(kclVersion = "3.0-preview")
7154outer = 1
7155x = if false {
7156 0
7157} else if false {
7158 0
7159} else {
7160 outer = 2
7161 outer + 10
7162}
7163"#;
7164 let result = parse_execute(code).await.unwrap();
7165 assert_eq!(variable_f64(&result, "x"), 12.0);
7166 assert_eq!(variable_f64(&result, "outer"), 1.0);
7167 }
7168
7169 #[tokio::test(flavor = "multi_thread")]
7173 async fn else_if_and_final_else_arm_bindings_leak_without_v3() {
7174 for header in ["", "@settings(kclVersion = 2.0)\n"] {
7175 let code = format!(
7176 r#"{header}x = if false {{
7177 0
7178}} else if true {{
7179 y = 1
7180 y
7181}} else {{
7182 0
7183}}
7184z = y
7185"#
7186 );
7187 let result = parse_execute(&code).await.unwrap();
7188 assert_eq!(variable_f64(&result, "z"), 1.0, "code={code}");
7189
7190 let code = format!(
7191 r#"{header}x = if false {{
7192 0
7193}} else if false {{
7194 0
7195}} else {{
7196 y = 1
7197 y
7198}}
7199z = y
7200"#
7201 );
7202 let result = parse_execute(&code).await.unwrap();
7203 assert_eq!(variable_f64(&result, "z"), 1.0, "code={code}");
7204
7205 let code = format!(
7206 r#"{header}outer = 1
7207x = if false {{
7208 0
7209}} else if true {{
7210 outer = 2
7211 outer
7212}} else {{
7213 0
7214}}
7215"#
7216 );
7217 let err = parse_execute(&code).await.expect_err("should error");
7218 assert!(
7219 err.message().contains("Cannot redefine `outer`"),
7220 "unexpected message: {}",
7221 err.message()
7222 );
7223 }
7224 }
7225
7226 #[tokio::test(flavor = "multi_thread")]
7227 async fn error_inside_if_arm_unwinds_balanced_in_v3() {
7228 let code = r#"@settings(kclVersion = "3.0-preview")
7231fn f() {
7232 dummy = if true {
7233 assert(1, isEqualTo = 2, error = "boom")
7234 0
7235 } else {
7236 0
7237 }
7238 return dummy
7239}
7240x = f()
7241"#;
7242 let err = parse_execute(code).await.expect_err("should error");
7243 assert!(err.message().contains("boom"), "unexpected message: {}", err.message());
7244 }
7245
7246 #[tokio::test(flavor = "multi_thread")]
7247 async fn exit_inside_scoped_if_arm_in_v3() {
7248 let code = r#"@settings(kclVersion = "3.0-preview")
7249fn f() {
7250 dummy = if true {
7251 exit()
7252 0
7253 } else {
7254 0
7255 }
7256 return dummy
7257}
7258x = f()
7259assert(1, isEqualTo = 2, error = "code after exit ran")
7260"#;
7261 parse_execute(code).await.unwrap();
7262 }
7263
7264 #[tokio::test(flavor = "multi_thread")]
7267 async fn if_arm_scoping_gated_on_entry_point_not_module() {
7268 let dep = r#"@settings(kclVersion = "3.0-preview")
7272ignored = if true {
7273 leaked = 1
7274 leaked
7275} else {
7276 0
7277}
7278export leakCheck = leaked
7279"#;
7280 let main = r#"@settings(kclVersion = 2.0)
7281import leakCheck from "dep.kcl"
7282x = leakCheck
7283"#;
7284 let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
7285 assert!(
7286 err.message()
7287 .starts_with("Mixing KCL versions in a single program is not allowed."),
7288 "unexpected message: {}",
7289 err.message()
7290 );
7291
7292 let dep = r#"ignored = if true {
7297 arm = 1
7298 arm
7299} else {
7300 0
7301}
7302export fn leakCheck() {
7303 return arm
7304}
7305"#;
7306 let main = r#"@settings(kclVersion = "3.0-preview")
7307import leakCheck from "dep.kcl"
7308x = leakCheck()
7309"#;
7310 let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
7311 assert!(
7312 err.message().contains("`arm` is not defined"),
7313 "unexpected message: {}",
7314 err.message()
7315 );
7316 }
7317
7318 #[tokio::test(flavor = "multi_thread")]
7323 async fn unwind_through_sketch_block_inside_scoped_if_arm_in_v3() {
7324 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7327fn f() {
7328 dummy = if true {
7329 s = sketch(on = XY) {
7330 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7331 q = notDefinedAnywhere
7332 }
7333 0
7334 } else {
7335 0
7336 }
7337 return dummy
7338}
7339x = f()
7340"#;
7341 let err = parse_execute(code).await.unwrap_err();
7342 assert!(
7343 err.message().contains("`notDefinedAnywhere` is not defined"),
7344 "unexpected message: {}",
7345 err.message()
7346 );
7347
7348 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7350fn f() {
7351 dummy = if true {
7352 s = sketch(on = XY) {
7353 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7354 e = exit()
7355 }
7356 0
7357 } else {
7358 0
7359 }
7360 return dummy
7361}
7362x = f()
7363assert(1, isEqualTo = 2, error = "code after exit ran")
7364"#;
7365 parse_execute(code).await.unwrap();
7366
7367 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7369fn g() {
7370 dummy = if true {
7371 s = sketch(on = XY) {
7372 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
7373 return 42
7374 }
7375 0
7376 } else {
7377 0
7378 }
7379 return 0
7380}
7381y = g()
7382"#;
7383 let result = parse_execute(code).await.unwrap();
7384 assert_eq!(variable_f64(&result, "y"), 42.0);
7385 }
7386
7387 #[tokio::test(flavor = "multi_thread")]
7391 async fn tag_declared_inside_if_arm_is_arm_local_in_v3() {
7392 let arm_body = r#"p = if true {
7393 profile = startSketchOn(XY)
7394 |> startProfile(at = [0, 0])
7395 |> line(end = [10, 0], tag = $edge)
7396 |> line(end = [0, 10])
7397 |> line(end = [-10, 0])
7398 |> close()
7399 inArmLen = segLen(edge)
7400 assert(inArmLen, isEqualTo = 10, error = "tag is usable within its arm")
7401 profile
7402} else {
7403 startSketchOn(XY)
7404 |> startProfile(at = [0, 0])
7405 |> line(end = [5, 0])
7406 |> line(end = [0, 5])
7407 |> line(end = [-5, 0])
7408 |> close()
7409}
7410len = segLen(edge)
7411"#;
7412
7413 let code = format!("@settings(kclVersion = \"3.0-preview\")\n{arm_body}");
7414 let err = parse_execute(&code).await.unwrap_err();
7415 assert!(
7416 err.message().contains("`edge` is not defined"),
7417 "unexpected message: {}",
7418 err.message()
7419 );
7420
7421 let result = parse_execute(arm_body).await.unwrap();
7423 assert_eq!(variable_f64(&result, "len"), 10.0);
7424 }
7425
7426 #[tokio::test(flavor = "multi_thread")]
7432 async fn if_arm_scopes_do_not_retain_function_frames_in_v3() {
7433 let code = r#"@settings(kclVersion = "3.0-preview")
7434fn pick(@i) {
7435 r = if i > 50 {
7436 a = i * 2
7437 a
7438 } else {
7439 b = i + 1
7440 b
7441 }
7442 return r
7443}
7444results = map([1..100], f = fn(@i) { return pick(i) })
7445assert(results[0], isEqualTo = 2, error = "pick(1) = 2")
7446assert(results[99], isEqualTo = 200, error = "pick(100) = 200")
7447"#;
7448 let result = parse_execute(code).await.unwrap();
7449 let retained = result.exec_state.stack().memory.envs_with_bindings();
7452 assert!(retained < 20, "retained environments: {retained}");
7453 }
7454
7455 #[tokio::test(flavor = "multi_thread")]
7460 async fn if_arm_scoping_inside_pipe_in_v3() {
7461 let code = r#"@settings(kclVersion = "3.0-preview")
7462cond = true
7463result = 5
7464 |> if cond {
7465 a = 20
7466 a
7467 } else {
7468 0
7469 }
7470 |> max([%, 1])
7471"#;
7472 let result = parse_execute(code).await.unwrap();
7473 assert_eq!(variable_f64(&result, "result"), 20.0);
7477
7478 let code = r#"@settings(kclVersion = "3.0-preview")
7480cond = true
7481result = 5
7482 |> if cond {
7483 a = 20
7484 a
7485 } else {
7486 0
7487 }
7488leaked = a
7489"#;
7490 let err = parse_execute(code).await.unwrap_err();
7491 assert!(
7492 err.message().contains("`a` is not defined"),
7493 "unexpected message: {}",
7494 err.message()
7495 );
7496 }
7497
7498 #[tokio::test(flavor = "multi_thread")]
7499 async fn member_expression_evaluates_object_before_property_in_v3() {
7500 let code = r#"@settings(kclVersion = "3.0-preview")
7503x = a[b]
7504"#;
7505 let err = parse_execute(code).await.expect_err("should error");
7506 assert_eq!(err.message(), "`a` is not defined");
7507 }
7508
7509 #[tokio::test(flavor = "multi_thread")]
7510 async fn member_expression_evaluates_property_before_object_without_v3() {
7511 let code = r#"@settings(kclVersion = 2.0)
7514x = a[b]
7515"#;
7516 let err = parse_execute(code).await.expect_err("should error");
7517 assert_eq!(err.message(), "`b` is not defined");
7518 }
7519
7520 #[tokio::test(flavor = "multi_thread")]
7521 async fn member_expression_undefined_object_with_static_property_in_v3() {
7522 let code = r#"@settings(kclVersion = "3.0-preview")
7523x = a.b
7524"#;
7525 let err = parse_execute(code).await.expect_err("should error");
7526 assert_eq!(err.message(), "`a` is not defined");
7527 }
7528
7529 #[tokio::test(flavor = "multi_thread")]
7530 async fn member_expression_values_in_v3() {
7531 let code = r#"@settings(kclVersion = "3.0-preview")
7534fn xs() {
7535 return [10, 20, 30]
7536}
7537fn one() {
7538 return 1
7539}
7540obj = { inner = { xs = xs() } }
7541objs = [obj, obj]
7542a = obj.inner.xs[one()]
7543b = xs()[one() + 1]
7544c = objs[0].inner.xs[0]
7545"#;
7546 let result = parse_execute(code).await.unwrap();
7547 assert_eq!(variable_f64(&result, "a"), 20.0);
7548 assert_eq!(variable_f64(&result, "b"), 30.0);
7549 assert_eq!(variable_f64(&result, "c"), 10.0);
7550 }
7551
7552 #[tokio::test(flavor = "multi_thread")]
7553 async fn exit_inside_member_expression_in_v3() {
7554 for code in [
7557 r#"@settings(kclVersion = "3.0-preview")
7558x = exit()[0]
7559assert(1, isEqualTo = 2, error = "code after exit ran")
7560"#,
7561 r#"@settings(kclVersion = "3.0-preview")
7562arr = [1]
7563x = arr[exit()]
7564assert(1, isEqualTo = 2, error = "code after exit ran")
7565"#,
7566 ] {
7567 parse_execute(code).await.unwrap();
7568 }
7569 }
7570
7571 #[tokio::test(flavor = "multi_thread")]
7572 async fn experimental_parameter() {
7573 let code = r#"
7574fn inc(@x, @(experimental = true) amount? = 1) {
7575 return x + amount
7576}
7577
7578answer = inc(5, amount = 2)
7579"#;
7580 let result = parse_execute(code).await.unwrap();
7581 let issues = result.exec_state.issues();
7582 assert_eq!(issues.len(), 1);
7583 assert_eq!(issues[0].severity, Severity::Error);
7584 let msg = &issues[0].message;
7585 assert!(msg.contains("experimental"), "found {msg}");
7586
7587 let code = r#"
7589fn inc(@x, @(experimental = true) amount? = 1) {
7590 return x + amount
7591}
7592
7593answer = inc(5)
7594"#;
7595 let result = parse_execute(code).await.unwrap();
7596 let issues = result.exec_state.issues();
7597 assert!(issues.is_empty(), "issues={issues:#?}");
7598 }
7599
7600 #[tokio::test(flavor = "multi_thread")]
7601 async fn experimental_scalar_fixed_constraint() {
7602 let code_left = r#"@settings(experimentalFeatures = warn)
7603sketch(on = XY) {
7604 point1 = point(at = [var 0mm, var 0mm])
7605 point1.at[0] == 1mm
7606}
7607"#;
7608 let code_right = r#"@settings(experimentalFeatures = warn)
7610sketch(on = XY) {
7611 point1 = point(at = [var 0mm, var 0mm])
7612 1mm == point1.at[0]
7613}
7614"#;
7615
7616 for code in [code_left, code_right] {
7617 let result = parse_execute(code).await.unwrap();
7618 let issues = result.exec_state.issues();
7619 let Some(error) = issues
7620 .iter()
7621 .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
7622 else {
7623 panic!("found {issues:#?}");
7624 };
7625 assert_eq!(error.severity, Severity::Warning);
7626 }
7627 }
7628
7629 #[tokio::test(flavor = "multi_thread")]
7633 async fn test_tangent_line_arc_executes_with_mock_engine() {
7634 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
7635 parse_execute(&code).await.unwrap();
7636 }
7637
7638 #[tokio::test(flavor = "multi_thread")]
7639 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
7640 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
7641 parse_execute(&code).await.unwrap();
7642 }
7643
7644 #[tokio::test(flavor = "multi_thread")]
7645 async fn test_tangent_line_circle_executes_with_mock_engine() {
7646 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
7647 parse_execute(&code).await.unwrap();
7648 }
7649
7650 #[tokio::test(flavor = "multi_thread")]
7651 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
7652 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
7653 parse_execute(&code).await.unwrap();
7654 }
7655
7656 #[tokio::test(flavor = "multi_thread")]
7657 async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
7658 let code = r#"startX = 2
7659
7660baseSketch = sketch(on = XY) {
7661 yoyo = line(start = [startX, 0], end = [7, 6])
7662 line2 = line(start = [7, 6], end = [7, 12])
7663 hi = line(start = [7, 12], end = [startX, 0])
7664}
7665
7666baseRegion = region(point = [5.5, 6], sketch = baseSketch)
7667myExtrude = extrude(
7668 baseRegion,
7669 length = 5,
7670 tagEnd = $endCap,
7671 tagStart = $startCap,
7672)
7673yodawg = getCommonEdge(faces = [
7674 baseRegion.tags.hi,
7675 baseRegion.tags.yoyo
7676])
7677
7678cutSketch = sketch(on = YZ) {
7679 myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
7680 myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
7681 line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
7682}
7683
7684cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
7685extrude001 = extrude(cutRegion, length = 5)
7686solid001 = subtract(myExtrude, tools = extrude001)
7687
7688yoyo = getOppositeEdge(baseRegion.tags.hi)
7689fillet(solid001, radius = 0.1, tags = yoyo)
7690"#;
7691
7692 parse_execute(code).await.unwrap();
7693 }
7694
7695 async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
7700 let program = crate::Program::parse_no_errs(kcl).unwrap();
7701 let ctx = ExecutorContext::new_geometry_only_with_version(program.language_version().unwrap())
7702 .await
7703 .unwrap();
7704 let mut exec_state = ExecState::new(&ctx);
7705 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
7706 let outcome = exec_state
7707 .into_exec_outcome(env_ref, &ctx)
7708 .await
7709 .expect("constraint report test outcome should collect variables");
7710 let report = outcome.sketch_constraint_report();
7711 ctx.close().await;
7712 report
7713 }
7714
7715 #[tokio::test(flavor = "multi_thread")]
7716 async fn warn_when_sketch_is_over_constrained() {
7717 let code = r#"
7718sketch001 = sketch(on = XY) {
7719 line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
7720 fixed([line1.start, ORIGIN])
7721 fixed([line1.start, [20, 20]])
7722}
7723"#;
7724 let result = parse_execute(code).await.unwrap();
7725 let issues = result.exec_state.issues();
7726 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
7727 panic!("expected over-constrained warning; found {issues:#?}");
7728 };
7729 assert_eq!(warning.severity, Severity::Warning);
7730 }
7731
7732 #[tokio::test(flavor = "multi_thread")]
7733 async fn over_constrained_warning_identifies_signed_vertical_distance_direction() {
7734 let code = r#"
7735sketch001 = sketch(on = XY) {
7736 line1 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
7737 fixed([line1.start, [0mm, 10mm]])
7738 fixed([line1.end, ORIGIN])
7739 verticalDistance([line1.start, line1.end]) == 10mm
7740}
7741"#;
7742 let result = parse_execute(code).await.unwrap();
7743 let issues = result.exec_state.issues();
7744 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
7745 panic!("expected over-constrained warning; found {issues:#?}");
7746 };
7747 assert!(
7748 warning.message.contains(
7749 "Unsatisfied signed verticalDistance constraint: a positive right-hand side requires the second point to be above the first"
7750 ),
7751 "expected signed-direction diagnostic; found {warning:#?}"
7752 );
7753 }
7754
7755 #[tokio::test(flavor = "multi_thread")]
7756 async fn no_warning_when_sketch_is_not_over_constrained() {
7757 let code = r#"
7759sketch001 = sketch(on = XY) {
7760 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7761}
7762"#;
7763 let result = parse_execute(code).await.unwrap();
7764 let issues = result.exec_state.issues();
7765 assert!(
7766 !issues.iter().any(|issue| issue.message.contains("over-constrained")),
7767 "did not expect over-constrained warning; found {issues:#?}"
7768 );
7769 }
7770
7771 #[tokio::test(flavor = "multi_thread")]
7772 async fn test_constraint_report_fully_constrained() {
7773 let kcl = r#"
7775@settings(experimentalFeatures = allow)
7776
7777sketch(on = YZ) {
7778 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
7779 line1.start.at[0] == 2
7780 line1.start.at[1] == 8
7781 line1.end.at[0] == 5
7782 line1.end.at[1] == 7
7783}
7784"#;
7785 let report = run_constraint_report(kcl).await;
7786 assert_eq!(report.fully_constrained.len(), 1);
7787 assert_eq!(report.under_constrained.len(), 0);
7788 assert_eq!(report.over_constrained.len(), 0);
7789 assert_eq!(report.errors.len(), 0);
7790 assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
7791 }
7792
7793 #[tokio::test(flavor = "multi_thread")]
7794 async fn test_constraint_report_under_constrained() {
7795 let kcl = r#"
7797sketch(on = YZ) {
7798 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
7799}
7800"#;
7801 let report = run_constraint_report(kcl).await;
7802 assert_eq!(report.fully_constrained.len(), 0);
7803 assert_eq!(report.under_constrained.len(), 1);
7804 assert_eq!(report.over_constrained.len(), 0);
7805 assert_eq!(report.errors.len(), 0);
7806 assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
7807 assert!(report.under_constrained[0].free_count > 0);
7808 }
7809
7810 #[tokio::test(flavor = "multi_thread")]
7811 async fn test_constraint_report_over_constrained() {
7812 let kcl = r#"
7814@settings(experimentalFeatures = allow)
7815
7816sketch(on = YZ) {
7817 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
7818 line1.start.at[0] == 2
7819 line1.start.at[1] == 8
7820 line1.end.at[0] == 5
7821 line1.end.at[1] == 7
7822 distance([line1.start, line1.end]) == 100mm
7823}
7824"#;
7825 let report = run_constraint_report(kcl).await;
7826 assert_eq!(report.over_constrained.len(), 1);
7827 assert_eq!(report.errors.len(), 0);
7828 assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
7829 assert!(report.over_constrained[0].conflict_count > 0);
7830 }
7831
7832 #[tokio::test(flavor = "multi_thread")]
7833 async fn test_constraint_report_multiple_sketches() {
7834 let kcl = r#"
7836@settings(experimentalFeatures = allow)
7837
7838s1 = sketch(on = YZ) {
7839 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
7840 line1.start.at[0] == 2
7841 line1.start.at[1] == 8
7842 line1.end.at[0] == 5
7843 line1.end.at[1] == 7
7844}
7845
7846s2 = sketch(on = XZ) {
7847 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7848}
7849"#;
7850 let report = run_constraint_report(kcl).await;
7851 assert_eq!(
7852 report.fully_constrained.len()
7853 + report.under_constrained.len()
7854 + report.over_constrained.len()
7855 + report.errors.len(),
7856 2,
7857 "Expected 2 sketches total"
7858 );
7859 assert_eq!(report.fully_constrained.len(), 1);
7860 assert_eq!(report.under_constrained.len(), 1);
7861 }
7862
7863 #[tokio::test(flavor = "multi_thread")]
7864 async fn test_constraint_report_reports_sketch_names() {
7865 let kcl = r#"
7870@settings(experimentalFeatures = allow)
7871
7872fixedSketch = sketch(on = YZ) {
7873 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
7874 line1.start.at[0] == 2
7875 line1.start.at[1] == 8
7876 line1.end.at[0] == 5
7877 line1.end.at[1] == 7
7878}
7879
7880looseSketch = sketch(on = XZ) {
7881 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7882}
7883
7884conflictSketch = sketch(on = XY) {
7885 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
7886 line1.start.at[0] == 2
7887 line1.start.at[1] == 8
7888 line1.end.at[0] == 5
7889 line1.end.at[1] == 7
7890 distance([line1.start, line1.end]) == 100mm
7891}
7892"#;
7893 let report = run_constraint_report(kcl).await;
7894 assert_eq!(report.errors.len(), 0);
7895 assert_eq!(report.fully_constrained.len(), 1);
7896 assert_eq!(report.under_constrained.len(), 1);
7897 assert_eq!(report.over_constrained.len(), 1);
7898 assert_eq!(report.fully_constrained[0].name, "fixedSketch");
7899 assert_eq!(report.under_constrained[0].name, "looseSketch");
7900 assert_eq!(report.over_constrained[0].name, "conflictSketch");
7901 }
7902
7903 #[tokio::test(flavor = "multi_thread")]
7904 async fn test_constraint_report_name_empty_without_declaration() {
7905 let kcl = r#"
7909sketch(on = YZ) {
7910 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
7911}
7912"#;
7913 let report = run_constraint_report(kcl).await;
7914 assert_eq!(report.under_constrained.len(), 1);
7915 assert_eq!(report.under_constrained[0].name, "");
7916 }
7917
7918 #[tokio::test(flavor = "multi_thread")]
7919 async fn test_constraint_report_names_repeat_across_calls() {
7920 let kcl = r#"
7925fn makeSketch() {
7926 inner = sketch(on = XY) {
7927 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
7928 }
7929 return inner
7930}
7931
7932first = makeSketch()
7933second = makeSketch()
7934"#;
7935 let report = run_constraint_report(kcl).await;
7936 assert_eq!(report.under_constrained.len(), 2);
7937 assert_eq!(report.under_constrained[0].name, "inner");
7938 assert_eq!(report.under_constrained[1].name, "inner");
7939 }
7940
7941 #[tokio::test(flavor = "multi_thread")]
7942 async fn user_aliases_and_enums_require_v3_even_with_experimental_opt_in() {
7943 for version in ["1.0", "2.0"] {
7944 for opt_in in ["", ", experimentalFeatures = allow"] {
7945 for (feature, declaration) in [
7946 ("Type aliases", "type Distance = number(mm)"),
7947 ("Enum declarations", "type Color { | Red }"),
7948 ] {
7949 let code = format!("@settings(kclVersion = \"{version}\"{opt_in})\n{declaration}\n");
7950 assert_eq!(
7951 parse_execute(&code).await.unwrap_err().message(),
7952 format!("{feature} require KCL 3.0-preview, but this program uses KCL {version}."),
7953 "code: {code}"
7954 );
7955 }
7956 }
7957 }
7958 }
7959
7960 #[tokio::test(flavor = "multi_thread")]
7961 async fn user_aliases_and_enums_execute_in_v3_without_experimental_opt_in() {
7962 for code in [
7963 "@settings(kclVersion = \"3.0-preview\")\ntype Distance = number(mm)\nx = 1mm: Distance\n",
7964 "@settings(kclVersion = \"3.0-preview\")\ntype Color { | Red }\nx = Color::Red\n",
7965 ] {
7966 let result = parse_execute(code).await.unwrap();
7967 assert!(result.exec_state.issues().is_empty(), "code: {code}");
7968 }
7969 }
7970
7971 #[tokio::test(flavor = "multi_thread")]
7972 async fn standard_library_type_aliases_remain_available_before_v3() {
7973 for version in ["1.0", "2.0"] {
7974 let code = format!("@settings(kclVersion = \"{version}\")\nx = 1mm: mm\n");
7975 let result = parse_execute(&code).await.unwrap();
7976 assert!(result.exec_state.issues().is_empty(), "code: {code}");
7977 }
7978 }
7979
7980 #[tokio::test(flavor = "multi_thread")]
7981 async fn enum_declaration_registers_type() {
7982 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7986type Color { | Red | Green }
7987"#;
7988 parse_execute(code).await.unwrap();
7989
7990 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7991export type Color { | Red | Green }
7992"#;
7993 parse_execute(code).await.unwrap();
7994
7995 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
7997type Empty { | }
7998"#;
7999 parse_execute(code).await.unwrap();
8000 }
8001
8002 #[tokio::test(flavor = "multi_thread")]
8003 async fn enum_declaration_rejects_nested_scope() {
8004 let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8011 for (case, code) in [
8012 (
8013 "function body",
8014 format!("{allow}fn palette() {{\n type Color {{ | Red }}\n return 0\n}}\npalette()\n"),
8015 ),
8016 (
8017 "sketch block",
8018 format!(
8019 "{allow}sketch(on = XY) {{\n type Color {{ | Red }}\n l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
8020 ),
8021 ),
8022 (
8023 "if arm",
8024 format!("{allow}x = if true {{\n type Color {{ | Red }}\n 0\n}} else {{\n 0\n}}\n"),
8025 ),
8026 ] {
8027 assert_eq!(
8028 parse_execute(&code).await.unwrap_err().message(),
8029 "Enum declarations are only supported at the top-level of a file. Move `type Color` to the top-level.",
8030 "case: {case}"
8031 );
8032 }
8033 }
8034
8035 #[tokio::test(flavor = "multi_thread")]
8036 async fn enum_alone_is_restricted_to_top_level() {
8037 let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8044 for (case, code) in [
8045 (
8046 "function body",
8047 format!("{allow}fn f() {{\n type Temperature = number(_)\n return 0\n}}\nx = f()\n"),
8048 ),
8049 (
8050 "sketch block",
8051 format!(
8052 "{allow}sketch(on = XY) {{\n type Temperature = number(_)\n l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
8053 ),
8054 ),
8055 ] {
8056 parse_execute(&code)
8057 .await
8058 .unwrap_or_else(|err| panic!("a type alias should be allowed in a {case}: {}", err.message()));
8059 }
8060 }
8061
8062 #[tokio::test(flavor = "multi_thread")]
8063 async fn enum_declaration_rejects_duplicate() {
8064 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8065type Color { | Red | Green | Red }
8066"#;
8067 assert_eq!(
8068 parse_execute(code).await.unwrap_err().message(),
8069 "Duplicate variant `Red` in enum `Color`."
8070 );
8071 }
8072
8073 async fn execute_with_modules(main: &str, modules: &[(&str, &str)]) -> Result<ExecTestResults, KclError> {
8075 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_enum_clash").unwrap();
8076 for (name, source) in modules {
8077 tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
8078 }
8079
8080 parse_execute_with_project_dir(main, Some(crate::TypedPath(tmpdir.path().into()))).await
8081 }
8082
8083 async fn execute_with_modules_hinting_all(
8086 main: &str,
8087 modules: &[(&str, &str)],
8088 ) -> Result<ExecTestResults, KclError> {
8089 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_added_in").unwrap();
8090 for (name, source) in modules {
8091 tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
8092 }
8093
8094 parse_execute_hinting_all(main, Some(crate::TypedPath(tmpdir.path().into()))).await
8095 }
8096
8097 async fn issues_with_empty_module(main: &str) -> Vec<crate::errors::CompilationIssue> {
8107 use futures::FutureExt;
8108
8109 let project_dir = crate::TypedPath::new("/zma-kcl-member-ranges");
8110 let files = [(project_dir.join("m.kcl").to_string(), Vec::new())]
8113 .into_iter()
8114 .collect();
8115
8116 let program = crate::Program::parse_no_errs(main).unwrap();
8117 let ctx = ExecutorContext {
8118 engine: Arc::new(EngineManager::new_mock()),
8119 engine_batch: EngineBatchContext::default(),
8120 fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
8121 settings: ExecutorSettings {
8122 project_directory: Some(project_dir),
8123 ..Default::default()
8124 },
8125 context_type: ContextType::Mock,
8126 execution_callbacks: Default::default(),
8127 executor_kind: machine::ExecutorKind::resolve(),
8128 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
8129 };
8130 let mut exec_state = ExecState::new(&ctx);
8131 let run_result = std::panic::AssertUnwindSafe(ctx.run(&program, &mut exec_state))
8135 .catch_unwind()
8136 .await;
8137 ctx.close().await;
8138 if let Err(panic) = run_result {
8139 std::panic::resume_unwind(panic);
8140 }
8141 exec_state.issues().to_vec()
8142 }
8143
8144 #[tokio::test(flavor = "multi_thread")]
8145 async fn member_object_diagnostics_use_object_range() {
8146 for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
8152 let main = format!("{header}import \"m.kcl\" as m\nx = m.field\n");
8153 let issues = issues_with_empty_module(&main).await;
8154 let warning = issues
8155 .iter()
8156 .find(|issue| issue.message.contains("no return value"))
8157 .expect("missing-return warning should be recorded");
8158 let object_start = main.rfind("m.field").unwrap();
8159 assert_eq!(
8160 (warning.source_range.start(), warning.source_range.end()),
8161 (object_start, object_start + 1),
8162 "warning should point at the object's span (header={header:?})"
8163 );
8164 }
8165 }
8166
8167 #[tokio::test(flavor = "multi_thread")]
8168 async fn member_property_diagnostics_use_property_range() {
8169 for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
8172 let main = format!("{header}import \"m.kcl\" as m\narr = [1]\nx = arr[m]\n");
8173 let issues = issues_with_empty_module(&main).await;
8174 let warning = issues
8175 .iter()
8176 .find(|issue| issue.message.contains("no return value"))
8177 .expect("missing-return warning should be recorded");
8178 let prop_start = main.rfind("[m]").unwrap() + 1;
8179 assert_eq!(
8180 (warning.source_range.start(), warning.source_range.end()),
8181 (prop_start, prop_start + 1),
8182 "warning should point at the property's span (header={header:?})"
8183 );
8184 }
8185 }
8186
8187 #[tokio::test(flavor = "multi_thread")]
8188 async fn backtrace_reports_fully_qualified_fn_names() {
8189 let main = "import \"m.kcl\" as m\nx = m::f()\n";
8193 let modules = [("m.kcl", "export fn f() {\n return undefinedVariable\n}\n")];
8194 let err = execute_with_modules(main, &modules).await.unwrap_err();
8195 let fn_names: Vec<_> = err.backtrace().into_iter().filter_map(|item| item.fn_name).collect();
8196 assert_eq!(fn_names, vec!["m::f".to_owned()]);
8197 }
8198
8199 #[tokio::test(flavor = "multi_thread")]
8200 async fn whole_module_name_executes_as_operand() {
8201 let main = r#"import "m.kcl" as m
8205sum = m + m
8206neg = -m
8207"#;
8208 let result = execute_with_modules(main, &[("m.kcl", "42\n")]).await.unwrap();
8209 assert_eq!(
8210 mem_get_json(result.exec_state.stack(), result.mem_env, "sum").as_f64(),
8211 Some(84.0)
8212 );
8213 assert_eq!(
8214 mem_get_json(result.exec_state.stack(), result.mem_env, "neg").as_f64(),
8215 Some(-42.0)
8216 );
8217 }
8218
8219 #[tokio::test(flavor = "multi_thread")]
8220 async fn whole_module_without_return_as_operand_errors() {
8221 let main = "import \"m.kcl\" as m
8226x = m + 1
8227";
8228 let err = execute_with_modules(main, &[("m.kcl", "")]).await.unwrap_err();
8229 assert!(
8230 err.message().contains("Expected a number, but found none"),
8231 "expected the operand to be the module's missing-return KclNone, got: {}",
8232 err.message()
8233 );
8234 }
8235
8236 #[tokio::test(flavor = "multi_thread")]
8237 async fn enum_rejects_name_clash_with_module() {
8238 let plain_module = ("Color.kcl", "export x = 1\n");
8244 let enum_module = (
8245 "enums.kcl",
8246 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red }\n",
8247 );
8248
8249 for (case, main, modules) in [
8250 (
8251 "module then enum",
8252 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"Color.kcl\"\ntype Color { | Red }\n",
8253 vec![plain_module],
8254 ),
8255 (
8256 "enum then module",
8257 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Color { | Red }\nimport \"Color.kcl\"\n",
8258 vec![plain_module],
8259 ),
8260 (
8261 "named import of an enum",
8262 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport Color from 'enums.kcl'\n",
8263 vec![plain_module, enum_module],
8264 ),
8265 (
8266 "glob import of an enum",
8267 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport * from 'enums.kcl'\n",
8268 vec![plain_module, enum_module],
8269 ),
8270 (
8271 "module then enum alias",
8272 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Base { | Red }\nimport \"Color.kcl\"\ntype Color = Base\n",
8273 vec![plain_module],
8274 ),
8275 (
8276 "enum alias then module",
8277 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Base { | Red }\ntype Color = Base\nimport \"Color.kcl\"\n",
8278 vec![plain_module],
8279 ),
8280 ] {
8281 let err = execute_with_modules(main, &modules).await.unwrap_err();
8282 assert_eq!(
8283 err.message(),
8284 "An enum and a module cannot share the name `Color` in the same scope, because `Color::x` would be ambiguous. Rename one of them.",
8285 "case: {case}"
8286 );
8287 }
8288 }
8289
8290 #[tokio::test(flavor = "multi_thread")]
8291 async fn enum_alias_can_shadow_module_from_outer_scope() {
8292 let main = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8293type Color { | Red }
8294import "Shade.kcl"
8295
8296fn pick(): Color {
8297 type Shade = Color
8298 return Shade::Red
8299}
8300
8301result = pick()
8302"#;
8303 let result = execute_with_modules(main, &[("Shade.kcl", "export value = 1\n")])
8304 .await
8305 .unwrap();
8306 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "result") else {
8307 panic!("`result` should hold an enum value");
8308 };
8309 assert_eq!(value.qualified_name(), "Color::Red");
8310 }
8311
8312 #[tokio::test(flavor = "multi_thread")]
8313 async fn enum_constructs_variant() {
8314 let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8315 let colors = (
8316 "colors.kcl",
8317 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
8318 );
8319
8320 for (case, main, modules) in [
8321 (
8322 "declared locally",
8323 format!("{allow}type Color {{ | Red | Green }}\nx = Color::Red\n"),
8324 vec![],
8325 ),
8326 (
8327 "reached through a module path",
8330 format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
8331 vec![colors],
8332 ),
8333 (
8334 "imported by name",
8335 format!("{allow}import Color from 'colors.kcl'\nx = Color::Red\n"),
8336 vec![colors],
8337 ),
8338 (
8339 "imported under an alias",
8342 format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade::Red\n"),
8343 vec![colors],
8344 ),
8345 ] {
8346 let result = execute_with_modules(&main, &modules)
8347 .await
8348 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
8349 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
8350 panic!("case: {case}: `x` should hold an enum value");
8351 };
8352 assert_eq!(value.qualified_name(), "Color::Red", "case: {case}");
8353 }
8354 }
8355
8356 #[tokio::test(flavor = "multi_thread")]
8357 async fn enum_aliases_preserve_the_original_declaration() {
8358 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8359type Color { | Red | Green }
8360type C = Color
8361type D = C
8362
8363original = Color::Red
8364directAlias = C::Red
8365chainedAlias = D::Red
8366directEqualsOriginal = directAlias == original
8367chainEqualsOriginal = chainedAlias == original
8368
8369fn passThroughAlias(@color: C): Color {
8370 return color
8371}
8372
8373passed = passThroughAlias(D::Green)
8374"#;
8375
8376 let result = parse_execute(code).await.unwrap();
8377 let memory = result.exec_state.stack();
8378
8379 let KclValue::Type {
8380 value: TypeDef::Enum(original_def),
8381 ..
8382 } = mem_get_json(memory, result.mem_env, &format!("{}Color", memory::TYPE_PREFIX))
8383 else {
8384 panic!("`Color` should hold an enum definition");
8385 };
8386 for alias in ["C", "D"] {
8387 let KclValue::Type {
8388 value: TypeDef::Enum(alias_def),
8389 ..
8390 } = mem_get_json(memory, result.mem_env, &format!("{}{alias}", memory::TYPE_PREFIX))
8391 else {
8392 panic!("`{alias}` should hold an enum definition");
8393 };
8394 assert!(Arc::ptr_eq(&original_def, &alias_def), "alias: {alias}");
8395 }
8396
8397 for name in ["directEqualsOriginal", "chainEqualsOriginal"] {
8398 let KclValue::Bool { value, .. } = mem_get_json(memory, result.mem_env, name) else {
8399 panic!("`{name}` should hold a boolean");
8400 };
8401 assert!(value, "comparison: {name}");
8402 }
8403
8404 let KclValue::Enum { value: passed } = mem_get_json(memory, result.mem_env, "passed") else {
8405 panic!("`passed` should hold an enum value");
8406 };
8407 assert_eq!(passed.enum_id(), original_def.id());
8408 assert_eq!(passed.variant(), "Green");
8409 }
8410
8411 #[tokio::test(flavor = "multi_thread")]
8412 async fn enum_aliases_survive_qualified_imports_and_reexports() {
8413 let main = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8414import "colors.kcl"
8415import "aliases.kcl"
8416import "tones.kcl"
8417import Paint as Finish from "aliases.kcl"
8418import * from "aliases.kcl"
8419
8420original = colors::Color::Red
8421qualifiedAlias = aliases::Paint::Red
8422namedImportAlias = Finish::Green
8423globImportAlias = Paint::Red
8424namedTargetAlias = tones::Tint::Green
8425aliasesEqualOriginal = qualifiedAlias == original
8426
8427fn throughAlias(@color: Finish): colors::Color {
8428 return color
8429}
8430
8431fn throughOriginal(@color: colors::Color): Finish {
8432 return color
8433}
8434
8435fromAlias = throughAlias(Finish::Green)
8436originalIntoAlias = throughAlias(colors::Color::Green)
8437fromOriginal = throughOriginal(colors::Color::Red)
8438"#;
8439 let modules = [
8440 (
8441 "colors.kcl",
8442 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
8443 ),
8444 (
8445 "palette.kcl",
8446 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"colors.kcl\" as swatches\nexport type Shade = swatches::Color\n",
8447 ),
8448 (
8449 "aliases.kcl",
8450 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"palette.kcl\"\nexport type Paint = palette::Shade\n",
8451 ),
8452 (
8453 "tones.kcl",
8454 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport Color from \"colors.kcl\"\nexport type Tint = Color\n",
8455 ),
8456 ];
8457
8458 let result = execute_with_modules(main, &modules).await.unwrap();
8459 let memory = result.exec_state.stack();
8460 let KclValue::Enum { value: original } = mem_get_json(memory, result.mem_env, "original") else {
8461 panic!("`original` should hold an enum value");
8462 };
8463 let original_id = original.enum_id();
8464
8465 for (name, variant) in [
8466 ("qualifiedAlias", "Red"),
8467 ("namedImportAlias", "Green"),
8468 ("globImportAlias", "Red"),
8469 ("namedTargetAlias", "Green"),
8470 ("fromAlias", "Green"),
8471 ("originalIntoAlias", "Green"),
8472 ("fromOriginal", "Red"),
8473 ] {
8474 let KclValue::Enum { value } = mem_get_json(memory, result.mem_env, name) else {
8475 panic!("`{name}` should hold an enum value");
8476 };
8477 assert_eq!(value.enum_id(), original_id, "value: {name}");
8478 assert_eq!(value.variant(), variant, "value: {name}");
8479 }
8480
8481 let KclValue::Bool { value, .. } = mem_get_json(memory, result.mem_env, "aliasesEqualOriginal") else {
8482 panic!("`aliasesEqualOriginal` should hold a boolean");
8483 };
8484 assert!(value);
8485
8486 let KclValue::Type {
8487 value: TypeDef::Enum(finish_def),
8488 ..
8489 } = mem_get_json(memory, result.mem_env, &format!("{}Finish", memory::TYPE_PREFIX))
8490 else {
8491 panic!("`Finish` should hold an enum definition");
8492 };
8493 assert_eq!(finish_def.id(), original_id);
8494 }
8495
8496 #[tokio::test(flavor = "multi_thread")]
8504 async fn signature_types_resolve_in_declaring_module() {
8505 let colors = (
8506 "colors.kcl",
8507 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n return c\n}\n",
8508 );
8509 let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"colors.kcl\"\nr = colors::paint(colors::Color::Red)\n";
8512
8513 let result = execute_with_modules(main, &[colors]).await.unwrap();
8514 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
8515 panic!("`r` should hold an enum value");
8516 };
8517 assert_eq!(value.qualified_name(), "Color::Red");
8518 }
8519
8520 #[tokio::test(flavor = "multi_thread")]
8521 async fn qualified_type_paths_resolve_in_aliases_and_ascriptions() {
8522 let main = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8523type ViewOrientation = view::Orientation
8524front = view::Orientation::Front: view::Orientation
8525"#;
8526
8527 parse_execute(main).await.unwrap();
8528 }
8529
8530 #[tokio::test(flavor = "multi_thread")]
8531 async fn unknown_qualified_type_reports_the_written_name() {
8532 let main = "fn f(@value: missing::Orientation) {}\n";
8533
8534 let err = parse_execute(main).await.unwrap_err();
8535 assert_eq!(err.message(), "Unknown type: missing::Orientation");
8536 }
8537
8538 #[tokio::test(flavor = "multi_thread")]
8539 async fn signature_types_resolve_under_import_alias() {
8540 let colors = (
8544 "colors.kcl",
8545 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n return c\n}\n",
8546 );
8547 let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport \"colors.kcl\" as painter\nr = painter::paint(painter::Color::Red)\n";
8548
8549 let result = execute_with_modules(main, &[colors]).await.unwrap();
8550 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
8551 panic!("`r` should hold an enum value");
8552 };
8553 assert_eq!(value.qualified_name(), "Color::Red");
8554 }
8555
8556 #[tokio::test(flavor = "multi_thread")]
8557 async fn signature_types_ignore_caller_scope() {
8558 let broken = (
8563 "broken.kcl",
8564 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport fn f(@x: Missing) {\n return x\n}\n",
8565 );
8566 let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Missing = string\nimport \"broken.kcl\"\nr = broken::f(\"hi\")\n";
8567
8568 let err = execute_with_modules(main, &[broken]).await.unwrap_err();
8569 assert!(
8570 err.message().contains("Unknown type: Missing"),
8571 "message: {}",
8572 err.message()
8573 );
8574 }
8575
8576 #[tokio::test(flavor = "multi_thread")]
8577 async fn signature_types_reject_forward_reference() {
8578 let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nfn f(@x: Later) {\n return x\n}\ntype Later = string\n";
8582
8583 let err = parse_execute(main).await.unwrap_err();
8584 assert!(
8585 err.message().contains("Unknown type: Later"),
8586 "message: {}",
8587 err.message()
8588 );
8589 }
8590
8591 #[tokio::test(flavor = "multi_thread")]
8592 async fn signature_types_resolve_in_enclosing_scope() {
8593 let main = "@settings(kclVersion = \"3.0-preview\", 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";
8598
8599 let result = parse_execute(main).await.unwrap();
8600 let KclValue::Number { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
8601 panic!("`r` should hold a number");
8602 };
8603 assert_eq!(value, 42.0);
8604 }
8605
8606 #[tokio::test(flavor = "multi_thread")]
8616 async fn signature_number_types_ignore_module_default_units() {
8617 let units_in = (
8618 "units_in.kcl",
8619 "@settings(defaultLengthUnit = in)\nexport fn passThrough(@x: number(Length)) {\n return x\n}\n",
8620 );
8621 let main = "import \"units_in.kcl\"\na = units_in::passThrough(42)\nb = units_in::passThrough(42mm)\nc = units_in::passThrough(42in)\n";
8624
8625 let result = execute_with_modules(main, &[units_in]).await.unwrap();
8626 for (name, expected_ty) in [
8627 (
8637 "a",
8638 kcl_api::NumericType::Default {
8639 len: kcl_api::UnitLength::Millimeters,
8640 angle: kcl_api::UnitAngle::Degrees,
8641 },
8642 ),
8643 (
8644 "b",
8645 kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Millimeters)),
8646 ),
8647 (
8648 "c",
8649 kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Inches)),
8650 ),
8651 ] {
8652 let KclValue::Number { value, ty, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name)
8653 else {
8654 panic!("`{name}` should hold a number");
8655 };
8656 assert_eq!(value, 42.0, "`{name}` should keep its magnitude");
8657 assert_eq!(ty, expected_ty, "`{name}` should keep the caller-side unit context");
8658 }
8659 }
8660
8661 #[tokio::test(flavor = "multi_thread")]
8669 async fn signature_types_use_declaring_scope_when_both_scopes_define_the_name() {
8670 let m1 = (
8671 "m1.kcl",
8672 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype A = string\n\nexport fn test(@a: A) {\n return a\n}\n",
8673 );
8674 let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport * from \"m1.kcl\"\ntype A = number(mm)\nx = test(2mm)\n";
8675
8676 let err = execute_with_modules(main, &[m1]).await.unwrap_err();
8677 assert_eq!(
8678 err.message(),
8679 "The input argument of `test` requires a value with type `A`, but found a number (mm) (with type `number(mm)`)."
8680 );
8681 }
8682
8683 #[tokio::test(flavor = "multi_thread")]
8684 async fn enum_rejects_bad_variant_paths() {
8685 let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8686
8687 for (case, main, modules, message) in [
8688 (
8689 "unknown variant",
8690 format!("{allow}type Color {{ | Red | Green }}\nx = Color::Blue\n"),
8691 vec![],
8692 "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
8693 ),
8694 (
8695 "unknown variant through an alias",
8696 format!("{allow}type Color {{ | Red | Green }}\ntype Paint = Color\nx = Paint::Blue\n"),
8697 vec![],
8698 "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
8699 ),
8700 (
8701 "enum with no variants",
8702 format!("{allow}type Empty {{ | }}\nx = Empty::Red\n"),
8703 vec![],
8704 "`Red` is not a variant of enum `Empty`. Enum `Empty` has no variants.",
8705 ),
8706 (
8707 "path continues past the enum",
8708 format!("{allow}type Color {{ | Red }}\nx = Color::Red::more\n"),
8709 vec![],
8710 "`Color` is an enum, so only a variant name can follow it. There is nothing to reach through `Color::Red`.",
8711 ),
8712 (
8713 "variant name is case sensitive",
8714 format!("{allow}type Color {{ | Red }}\nx = Color::red\n"),
8715 vec![],
8716 "`red` is not a variant of enum `Color`. Its variants are: Red.",
8717 ),
8718 (
8719 "enum not exported from its module",
8720 format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
8721 vec![(
8722 "colors.kcl",
8723 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Color { | Red }\n",
8724 )],
8725 "Item Color not found in module's exported items",
8726 ),
8727 (
8728 "a non-enum type alias cannot head a path",
8729 format!("{allow}type T = number(_)\nx = T::foo\n"),
8730 vec![],
8731 "`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
8732 ),
8733 (
8734 "a chained non-enum type alias cannot head a path",
8735 format!("{allow}type T = number(_)\ntype U = T\nx = U::foo\n"),
8736 vec![],
8737 "`U` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
8738 ),
8739 (
8740 "a qualified non-enum type alias cannot head a path",
8741 format!("{allow}import \"types.kcl\"\nx = types::T::foo\n"),
8742 vec![(
8743 "types.kcl",
8744 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type T = number(_)\n",
8745 )],
8746 "`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
8747 ),
8748 (
8749 "an alias containing an enum is not an enum alias",
8750 format!("{allow}type Color {{ | Red }}\ntype T = Color | string\nx = T::Red\n"),
8751 vec![],
8752 "`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
8753 ),
8754 (
8755 "a value cannot head a path",
8758 "Color = 5\nx = Color::Red\n".to_owned(),
8759 vec![],
8760 "`Color` is not defined",
8761 ),
8762 ] {
8763 let err = execute_with_modules(&main, &modules).await.unwrap_err();
8764 assert_eq!(err.message(), message, "case: {case}");
8765 }
8766 }
8767
8768 #[tokio::test(flavor = "multi_thread")]
8769 async fn enum_compares_by_variant() {
8770 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8771type Color { | Red | Green }
8772sameEq = Color::Red == Color::Red
8773sameNeq = Color::Red != Color::Red
8774otherEq = Color::Red == Color::Green
8775otherNeq = Color::Red != Color::Green
8776"#;
8777 let result = parse_execute(code).await.unwrap();
8778
8779 for (name, expected) in [
8780 ("sameEq", true),
8781 ("sameNeq", false),
8782 ("otherEq", false),
8783 ("otherNeq", true),
8784 ] {
8785 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
8786 panic!("`{name}` should hold a bool");
8787 };
8788 assert_eq!(value, expected, "variable: {name}");
8789 }
8790 }
8791
8792 #[tokio::test(flavor = "multi_thread")]
8793 async fn enum_usable_inside_sketch_block() {
8794 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8803type Color { | Red | Green }
8804sketch(on = XY) {
8805 c = Color::Red
8806 assertIs(Color::Red != Color::Green)
8807 assertIs(!(Color::Red != Color::Red))
8808 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
8809}
8810"#;
8811 parse_execute(code)
8812 .await
8813 .unwrap_or_else(|err| panic!("enum use inside a sketch block should work: {}", err.message()));
8814 }
8815
8816 #[tokio::test(flavor = "multi_thread")]
8817 async fn enum_eq_reserved_inside_sketch_block() {
8818 let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8828 let tail = " l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}\n";
8829 for (case, declaration, comparison, types) in [
8830 (
8831 "enums",
8832 "type Color { | Red | Green }\n",
8833 "Color::Red == Color::Green",
8834 "a value of enum `Color` and a value of enum `Color`",
8835 ),
8836 ("strings", "", "\"a\" == \"b\"", "a string and a string"),
8837 ("numbers", "", "1 == 2", "a number and a number"),
8838 ] {
8839 let code = format!("{allow}{declaration}sketch(on = XY) {{\n x = {comparison}\n{tail}");
8840 assert_eq!(
8841 parse_execute(&code).await.unwrap_err().message(),
8842 format!("Cannot create an equivalence constraint between values of these types: {types}"),
8843 "case: {case}"
8844 );
8845 }
8846 }
8847
8848 #[tokio::test(flavor = "multi_thread")]
8849 async fn enum_same_file_imported_twice_is_one_type() {
8850 let main = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
8854import Color as A from 'colors.kcl'
8855import Color as B from 'colors.kcl'
8856x = A::Red == B::Red
8857y = A::Red == B::Green
8858"#;
8859 let result = execute_with_modules(
8860 main,
8861 &[(
8862 "colors.kcl",
8863 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
8864 )],
8865 )
8866 .await
8867 .unwrap();
8868
8869 for (name, expected) in [("x", true), ("y", false)] {
8870 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
8871 panic!("`{name}` should hold a bool");
8872 };
8873 assert_eq!(value, expected, "variable: {name}");
8874 }
8875 }
8876
8877 #[tokio::test(flavor = "multi_thread")]
8878 async fn enum_rejects_comparison_across_types() {
8879 let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8880 let color = (
8881 "a.kcl",
8882 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red }\n",
8883 );
8884 let other_color = (
8885 "b.kcl",
8886 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red }\n",
8887 );
8888
8889 for (case, main, modules, message) in [
8890 (
8891 "two enums declared separately",
8892 format!("{allow}type Color {{ | Red }}\ntype Shade {{ | Red }}\nx = Color::Red == Shade::Red\n"),
8893 vec![],
8894 "Cannot compare enum `Color` with enum `Shade`. They are different types.",
8895 ),
8896 (
8897 "two enums sharing a name",
8901 format!(
8902 "{allow}import Color as A from 'a.kcl'\nimport Color as B from 'b.kcl'\nx = A::Red == B::Red\n"
8903 ),
8904 vec![color, other_color],
8905 "Cannot compare two different enums that are both named `Color`. They come from separate declarations.",
8906 ),
8907 (
8908 "an enum and a number",
8909 format!("{allow}type Color {{ | Red }}\nx = Color::Red == 5\n"),
8910 vec![],
8911 "Cannot compare enum `Color::Red` with a number.",
8912 ),
8913 (
8914 "a number and an enum, in that order",
8915 format!("{allow}type Color {{ | Red }}\nx = 5 == Color::Red\n"),
8916 vec![],
8917 "Cannot compare enum `Color::Red` with a number.",
8918 ),
8919 (
8920 "an enum and a string",
8921 format!("{allow}type Color {{ | Red }}\nx = Color::Red == \"Red\"\n"),
8922 vec![],
8923 "Cannot compare enum `Color::Red` with a string.",
8924 ),
8925 ] {
8926 let err = execute_with_modules(&main, &modules).await.unwrap_err();
8927 assert_eq!(err.message(), message, "case: {case}");
8928 }
8929 }
8930
8931 #[tokio::test(flavor = "multi_thread")]
8932 async fn enum_rejects_bare_type_name_as_value() {
8933 let allow = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\n";
8934 let colors = (
8935 "colors.kcl",
8936 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
8937 );
8938
8939 for (case, main, modules, message) in [
8940 (
8941 "enum suggests a variant",
8942 format!("{allow}type Color {{ | Red | Green }}\nx = Color\n"),
8943 vec![],
8944 "`Color` is a type, not a value. Use one of its variants, such as `Color::Red`.",
8945 ),
8946 (
8947 "suggestion uses the import alias",
8950 format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade\n"),
8951 vec![colors],
8952 "`Shade` is a type, not a value. Use one of its variants, such as `Shade::Red`.",
8953 ),
8954 (
8955 "suggestion uses the type alias",
8956 format!("{allow}type Color {{ | Red | Green }}\ntype Paint = Color\nx = Paint\n"),
8957 vec![],
8958 "`Paint` is a type, not a value. Use one of its variants, such as `Paint::Red`.",
8959 ),
8960 (
8961 "enum with no variants suggests nothing",
8962 format!("{allow}type Empty {{ | }}\nx = Empty\n"),
8963 vec![],
8964 "`Empty` is a type, not a value.",
8965 ),
8966 (
8967 "a type alias reports the same way",
8968 format!("{allow}type T = number(_)\nx = T\n"),
8969 vec![],
8970 "`T` is a type, not a value.",
8971 ),
8972 (
8973 "an unknown name is still undefined",
8976 "x = Nope\n".to_owned(),
8977 vec![],
8978 "`Nope` is not defined",
8979 ),
8980 ] {
8981 let err = execute_with_modules(&main, &modules).await.unwrap_err();
8982 assert_eq!(err.message(), message, "case: {case}");
8983 }
8984 }
8985
8986 #[tokio::test(flavor = "multi_thread")]
8987 async fn imported_user_type_declarations_require_v3_even_with_opt_in() {
8988 for version in ["1.0", "2.0"] {
8989 for module_settings in [format!("@settings(kclVersion = \"{version}\")\n"), String::new()] {
8990 for (feature, declaration, use_type) in [
8991 (
8992 "Type aliases",
8993 "export type Distance = number(mm)",
8994 "x = 1mm: types::Distance",
8995 ),
8996 (
8997 "Enum declarations",
8998 "export type Color { | Red }",
8999 "x = types::Color::Red",
9000 ),
9001 ] {
9002 let main = format!(
9003 "@settings(kclVersion = \"{version}\", experimentalFeatures = allow)\nimport \"types.kcl\"\n{use_type}\n"
9004 );
9005 let module = format!("{module_settings}{declaration}\n");
9006 let err = execute_with_modules(&main, &[("types.kcl", &module)])
9007 .await
9008 .unwrap_err();
9009 assert_eq!(
9010 err.message(),
9011 format!("{feature} require KCL 3.0-preview, but this program uses KCL {version}."),
9012 "main: {main}; module: {module}"
9013 );
9014 }
9015 }
9016 }
9017 }
9018
9019 #[tokio::test(flavor = "multi_thread")]
9020 async fn named_view_access_in_unversioned_import_uses_entry_point_version() {
9021 let dep = "export camera = view::directed([0, 1, -2])\n";
9022 for version in ["1.0", "2.0"] {
9023 let main = format!("@settings(kclVersion = \"{version}\")\nimport \"dep.kcl\"\nx = dep::camera\n");
9024 let err = execute_with_modules(&main, &[("dep.kcl", dep)]).await.unwrap_err();
9025 assert!(
9026 err.message()
9027 .contains(&format!("added in KCL 3.0, but this program uses KCL {version}")),
9028 "error: {}",
9029 err.message()
9030 );
9031 }
9032
9033 let main = "@settings(kclVersion = \"3.0-preview\")\nimport \"dep.kcl\"\nx = dep::camera\n";
9034 let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
9035 assert!(result.exec_state.issues().is_empty());
9036 }
9037
9038 #[tokio::test(flavor = "multi_thread")]
9039 async fn enum_use_is_not_experimental_in_v3() {
9040 let code = r#"@settings(kclVersion = "3.0-preview")
9041type Color { | Red }
9042x = Color::Red
9043"#;
9044 let result = parse_execute(code).await.unwrap();
9045 assert!(
9046 result.exec_state.global.issues.is_empty(),
9047 "issues: {:?}",
9048 result.exec_state.global.issues
9049 );
9050 }
9051
9052 #[tokio::test(flavor = "multi_thread")]
9053 async fn v3_enum_variant_warning_follows_explicit_annotations() {
9054 for (case, declarations, binding, expected_warnings) in [
9055 ("unannotated enum", "type Color { | Red }", "Color", 0),
9056 (
9057 "annotated enum",
9058 "@(experimental = true)\ntype Color { | Red }",
9059 "Color",
9060 1,
9061 ),
9062 (
9063 "alias of annotated enum",
9064 "@(experimental = true)\ntype Color { | Red }\ntype Shade = Color",
9065 "Shade",
9066 1,
9067 ),
9068 (
9069 "annotated alias",
9070 "type Color { | Red }\n@(experimental = true)\ntype Shade = Color",
9071 "Shade",
9072 1,
9073 ),
9074 ] {
9075 let code = format!(
9076 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = warn)\n{declarations}\nx = {binding}::Red\n"
9077 );
9078 let result = parse_execute(&code)
9079 .await
9080 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
9081 let variant_warnings: Vec<_> = result
9082 .exec_state
9083 .issues()
9084 .iter()
9085 .filter(|issue| {
9086 issue.message == "Use of the enum `Color` is experimental and may change or be removed."
9087 })
9088 .collect();
9089 assert_eq!(variant_warnings.len(), expected_warnings, "case: {case}");
9090 assert!(
9091 variant_warnings.iter().all(|issue| issue.severity == Severity::Warning),
9092 "case: {case}"
9093 );
9094 }
9095 }
9096
9097 #[tokio::test(flavor = "multi_thread")]
9098 async fn enum_allows_name_sharing_outside_modules() {
9099 for (case, main, modules) in [
9107 (
9108 "an alias may share a name with a module",
9111 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Temperature = number(_)\nimport \"Temperature.kcl\"\nx = Temperature::x\n",
9112 vec![("Temperature.kcl", "export x = 1\n")],
9113 ),
9114 (
9115 "a value may share a name with an enum",
9116 "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\ntype Color { | Red }\nColor = 5\n",
9117 vec![],
9118 ),
9119 ] {
9120 if let Err(err) = execute_with_modules(main, &modules).await {
9121 panic!("case: {case}: {}", err.message());
9122 }
9123 }
9124 }
9125
9126 #[tokio::test(flavor = "multi_thread")]
9127 async fn enum_declaration_rejects_redefinition() {
9128 let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9129type Color { | Red }
9130type Color { | Green }
9131"#;
9132 assert_eq!(
9133 parse_execute(code).await.unwrap_err().message(),
9134 "Redefinition of type Color."
9135 );
9136 }
9137
9138 #[tokio::test(flavor = "multi_thread")]
9144 async fn enum_projects_to_string() {
9145 let header = r#"
9146 @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9147 type Color { | Red | Green }
9148 type Label = string
9149 "#;
9150
9151 for (case, body, expected) in [
9152 ("a variant", "x = Color::Red: string", "Red"),
9153 ("another variant of the same enum", "x = Color::Green: string", "Green"),
9154 ("an alias of the target type", "x = Color::Red: Label", "Red"),
9155 (
9156 "an element of a projected array",
9157 r#"
9158 pair = [Color::Red, Color::Green]: [string]
9159 x = pair[1]
9160 "#,
9161 "Green",
9162 ),
9163 (
9164 "an element of a nested projected array",
9165 r#"
9166 grid = [[Color::Green]]: [[string]]
9167 x = grid[0][0]
9168 "#,
9169 "Green",
9170 ),
9171 (
9172 "a one-element array against a bare string",
9173 "x = [Color::Red]: string",
9174 "Red",
9175 ),
9176 ] {
9177 let result = parse_execute(&format!("{header}{body}\n"))
9178 .await
9179 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
9180 let KclValue::String { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
9181 panic!("case: {case}: `x` should hold a string");
9182 };
9183 assert_eq!(value, expected, "case: {case}");
9184 }
9185 }
9186
9187 #[tokio::test(flavor = "multi_thread")]
9191 async fn enum_ascription_keeps_the_enum() {
9192 let header = r#"
9193 @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9194 type Color { | Red | Green }
9195 type Paint = Color
9196 "#;
9197
9198 for (case, expression, expected) in [
9199 ("its own type", "(Color::Red: Color) == Color::Red", true),
9200 ("an alias of its own type", "(Color::Red: Paint) == Color::Red", true),
9201 (
9202 "the ascription does not change which variant it is",
9203 "(Color::Red: Color) == Color::Green",
9204 false,
9205 ),
9206 ] {
9207 let result = parse_execute(&format!("{header}x = {expression}\n"))
9208 .await
9209 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
9210 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
9211 panic!("case: {case}: `x` should hold a bool");
9212 };
9213 assert_eq!(value, expected, "case: {case}");
9214 }
9215 }
9216
9217 #[tokio::test(flavor = "multi_thread")]
9221 async fn enum_projection_is_not_implicit() {
9222 let header = r#"
9223 @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9224 type Color { | Red | Green }
9225 "#;
9226 let found = "but found a value of enum `Color` (with type `Color`).";
9227
9228 for (case, body, expected) in [
9229 (
9230 "unlabeled argument",
9231 r#"
9232 fn label(@text: string) { return text }
9233 x = label(Color::Red)
9234 "#,
9235 format!("The input argument of `label` requires a value with type `string`, {found}"),
9236 ),
9237 (
9238 "labeled argument",
9239 r#"
9240 fn label(text: string) { return text }
9241 x = label(text = Color::Red)
9242 "#,
9243 format!("text requires a value with type `string`, {found}"),
9244 ),
9245 (
9246 "return",
9247 r#"
9248 fn label(): string { return Color::Red }
9249 x = label()
9250 "#,
9251 format!("This function requires its result to be a value with type `string`, {found}"),
9252 ),
9253 (
9254 "inside an array at an argument boundary",
9259 r#"
9260 fn labels(@text: [string]) { return text }
9261 x = labels([Color::Red])
9262 "#,
9263 "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(),
9264 ),
9265 ] {
9266 assert_eq!(
9267 parse_execute(&format!("{header}{body}\n")).await.unwrap_err().message(),
9268 expected,
9269 "case: {case}"
9270 );
9271 }
9272 }
9273
9274 #[tokio::test(flavor = "multi_thread")]
9277 async fn enum_ascription_rejections() {
9278 let header = r#"
9279 @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9280 type Color { | Red }
9281 type Shade { | Red }
9282 "#;
9283 let no_number = "Cannot project enum `Color` to a number. An enum projects to `string`; projecting to a number is not supported yet.";
9284
9285 for (case, expression, expected) in [
9286 ("a number target", "Color::Red: number(_)", no_number.to_owned()),
9287 (
9288 "a number target reached through an array, so the reason survives the walk",
9289 "[Color::Red]: [number(_)]",
9290 no_number.to_owned(),
9291 ),
9292 (
9293 "a boolean target, which is not a projection at all",
9294 "Color::Red: bool",
9295 "could not coerce a value of enum `Color` (with type `Color`) to type `bool`".to_owned(),
9296 ),
9297 (
9298 "another enum whose variants happen to match",
9299 "Color::Red: Shade",
9300 "could not coerce a value of enum `Color` (with type `Color`) to type `Shade`".to_owned(),
9301 ),
9302 ] {
9303 assert_eq!(
9304 parse_execute(&format!("{header}x = {expression}\n"))
9305 .await
9306 .unwrap_err()
9307 .message(),
9308 expected,
9309 "case: {case}"
9310 );
9311 }
9312 }
9313
9314 #[tokio::test(flavor = "multi_thread")]
9321 async fn enum_flows_through_declared_types() {
9322 let header = r#"
9323 @settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
9324 type Color { | Red | Green }
9325 type Paint = Color
9326 type Shade { | Red }
9327 "#;
9328
9329 for (case, body, expected) in [
9330 (
9331 "an alias parameter accepts the original enum",
9332 r#"
9333 fn paint(@c: Paint) { return c }
9334 x = paint(Color::Red) == Paint::Red
9335 "#,
9336 None,
9337 ),
9338 (
9339 "an unlabeled parameter",
9340 r#"
9341 fn paint(@c: Color) { return c }
9342 x = paint(Color::Red) == Color::Red
9343 "#,
9344 None,
9345 ),
9346 (
9347 "a labeled parameter",
9348 r#"
9349 fn paint(c: Color) { return c }
9350 x = paint(c = Color::Green) == Color::Green
9351 "#,
9352 None,
9353 ),
9354 (
9355 "a declared return type",
9356 r#"
9357 fn pick(): Color { return Color::Red }
9358 x = pick() == Color::Red
9359 "#,
9360 None,
9361 ),
9362 (
9363 "an array parameter",
9364 r#"
9365 fn firstOf(@cs: [Color]) { return cs[0] }
9366 x = firstOf([Color::Red, Color::Green]) == Color::Red
9367 "#,
9368 None,
9369 ),
9370 (
9371 "an object field",
9376 r#"
9377 fn take(@o: { c: Color }) { return o.c }
9378 x = take({ c = Color::Green }) == Color::Green
9379 "#,
9380 None,
9381 ),
9382 (
9383 "a union that names the enum",
9384 r#"
9385 fn either(@v: Color | string) { return v }
9386 x = either(Color::Red) == Color::Red
9387 "#,
9388 None,
9389 ),
9390 (
9391 "the same union given the other member",
9392 r#"
9393 fn either(@v: Color | string) { return v }
9394 x = either("plain") == "plain"
9395 "#,
9396 None,
9397 ),
9398 (
9399 "an alias parameter rejects a different enum",
9400 r#"
9401 fn paint(@c: Paint) { return c }
9402 x = paint(Shade::Red) == Shade::Red
9403 "#,
9404 Some(
9405 "The input argument of `paint` requires a value with type `Paint`, but found a value of enum `Shade` (with type `Shade`).",
9406 ),
9407 ),
9408 (
9409 "another declaration at the same boundary",
9410 r#"
9411 fn paint(@c: Color) { return c }
9412 x = paint(Shade::Red) == Shade::Red
9413 "#,
9414 Some(
9415 "The input argument of `paint` requires a value with type `Color`, but found a value of enum `Shade` (with type `Shade`).",
9416 ),
9417 ),
9418 ] {
9419 let code = format!("{header}{body}\n");
9420 match expected {
9421 None => {
9422 let result = parse_execute(&code)
9423 .await
9424 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
9425 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x")
9426 else {
9427 panic!("case: {case}: `x` should hold a bool");
9428 };
9429 assert!(value, "case: {case}: the value did not survive the boundary");
9430 }
9431 Some(message) => assert_eq!(
9432 parse_execute(&code).await.unwrap_err().message(),
9433 message,
9434 "case: {case}"
9435 ),
9436 }
9437 }
9438 }
9439
9440 async fn added_in_error(kcl_version: &str, body: &str) -> Option<String> {
9447 let program = format!("@settings(kclVersion = {kcl_version}, experimentalFeatures = allow)\n{body}");
9448 match parse_execute_hinting_all(&program, None).await {
9449 Ok(result) => {
9450 assert!(
9451 result.issues().is_empty(),
9452 "kclVersion {kcl_version}: unexpected issues: {:#?}",
9453 result.issues()
9454 );
9455 None
9456 }
9457 Err(err) => Some(err.message().to_owned()),
9458 }
9459 }
9460
9461 #[tokio::test(flavor = "multi_thread")]
9462 async fn not_yet_added_function_is_undefined() {
9463 let body = r#"@(added_in = "3.0")
9464fn newFn() { return 1 }
9465x = newFn()
9466"#;
9467 assert_eq!(
9468 added_in_error("2.0", body).await.as_deref(),
9469 Some("`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0")
9470 );
9471 assert_eq!(added_in_error("\"3.0-preview\"", body).await, None);
9472 }
9473
9474 #[tokio::test(flavor = "multi_thread")]
9475 async fn not_yet_added_function_cannot_be_referenced_as_a_value() {
9476 let body = r#"@(added_in = "3.0")
9478fn newFn() { return 1 }
9479f = newFn
9480"#;
9481 assert_eq!(
9482 added_in_error("2.0", body).await.as_deref(),
9483 Some("`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0")
9484 );
9485 }
9486
9487 #[tokio::test(flavor = "multi_thread")]
9488 async fn added_in_boundary_is_inclusive_and_prerelease_counts_as_release() {
9489 for (kcl_version, added_in) in [
9490 ("1.0", "1.0"),
9491 ("2.0", "1.0"),
9492 ("2.0", "2.0"),
9493 ("\"3.0-preview\"", "3.0"),
9494 ] {
9495 let body = format!("@(added_in = \"{added_in}\")\nfn newFn() {{ return 1 }}\nx = newFn()\n");
9496 assert_eq!(
9497 added_in_error(kcl_version, &body).await,
9498 None,
9499 "kclVersion {kcl_version}, added_in {added_in}"
9500 );
9501 }
9502 }
9503
9504 #[tokio::test(flavor = "multi_thread")]
9505 async fn not_yet_added_error_reports_default_kcl_version() {
9506 let program = r#"@(added_in = "2.0")
9508fn newFn() { return 1 }
9509x = newFn()
9510"#;
9511 assert_eq!(
9512 parse_execute_hinting_all(program, None).await.unwrap_err().message(),
9513 "`newFn` is not defined; it was added in KCL 2.0, but this program uses KCL 1.0"
9514 );
9515 }
9516
9517 #[tokio::test(flavor = "multi_thread")]
9518 async fn not_yet_added_constant_is_undefined() {
9519 let body = r#"@(added_in = "3.0")
9520newConst = 5
9521x = newConst
9522"#;
9523 assert_eq!(
9524 added_in_error("2.0", body).await.as_deref(),
9525 Some("`newConst` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0")
9526 );
9527 assert_eq!(added_in_error("\"3.0-preview\"", body).await, None);
9528 }
9529
9530 #[tokio::test(flavor = "multi_thread")]
9531 async fn not_yet_added_type_is_unknown() {
9532 let body = r#"@(added_in = "3.0")
9533type NewT = [number; 2]
9534x = [1, 2]: NewT
9535"#;
9536 assert_eq!(
9537 added_in_error("2.0", body).await.as_deref(),
9538 Some("Unknown type: NewT; it was added in KCL 3.0, but this program uses KCL 2.0")
9539 );
9540 assert_eq!(added_in_error("\"3.0-preview\"", body).await, None);
9541 }
9542
9543 #[tokio::test(flavor = "multi_thread")]
9546 async fn function_using_not_yet_added_type_fails_at_its_declaration() {
9547 let ty = "@(added_in = \"3.0\")\ntype NewT = [number; 2]\n";
9548 let type_error = Some("Unknown type: NewT; it was added in KCL 3.0, but this program uses KCL 2.0");
9549
9550 let body = format!("{ty}fn usesNew(@p: NewT) {{ return p }}\n");
9552 assert_eq!(added_in_error("2.0", &body).await.as_deref(), type_error);
9553 let body = format!("{ty}@(added_in = \"2.0\")\nfn usesNew(@p: NewT) {{ return p }}\n");
9555 assert_eq!(added_in_error("2.0", &body).await.as_deref(), type_error);
9556 let body = format!("{ty}fn makesNew(): NewT {{ return [1, 2] }}\n");
9558 assert_eq!(added_in_error("2.0", &body).await.as_deref(), type_error);
9559
9560 let body = format!("{ty}@(added_in = \"3.0\")\nfn usesNew(@p: NewT) {{ return p }}\n");
9562 assert_eq!(added_in_error("2.0", &body).await, None);
9563 let body = format!("{body}x = usesNew([1, 2])\n");
9564 assert_eq!(
9565 added_in_error("2.0", &body).await.as_deref(),
9566 Some("`usesNew` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0")
9567 );
9568
9569 let body = format!("{ty}fn usesNew(@p: NewT) {{ return p }}\nx = usesNew([1, 2])\n");
9571 assert_eq!(added_in_error("\"3.0-preview\"", &body).await, None);
9572 }
9573
9574 #[tokio::test(flavor = "multi_thread")]
9575 async fn not_yet_added_declaration_is_absent_from_imports() {
9576 let dep = "@(added_in = \"3.0\")\nexport fn newFn() { return 1 }\nexport fn oldFn() { return 2 }\n";
9579
9580 let main = "@settings(kclVersion = 2.0)\nimport newFn from \"dep.kcl\"\nx = newFn()\n";
9582 assert_eq!(
9583 execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
9584 .await
9585 .unwrap_err()
9586 .message(),
9587 "newFn is not defined in module; it was added in KCL 3.0, but this program uses KCL 2.0"
9588 );
9589
9590 let main = "@settings(kclVersion = 2.0)\nimport \"dep.kcl\"\nx = dep::newFn()\n";
9592 assert_eq!(
9593 execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
9594 .await
9595 .unwrap_err()
9596 .message(),
9597 "`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0"
9598 );
9599
9600 let main = "@settings(kclVersion = 2.0)\nimport * from \"dep.kcl\"\nx = oldFn()\ny = newFn()\n";
9602 assert_eq!(
9603 execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
9604 .await
9605 .unwrap_err()
9606 .message(),
9607 "`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0"
9608 );
9609
9610 let main = "@settings(kclVersion = \"3.0-preview\")\nimport newFn from \"dep.kcl\"\nx = newFn()\n";
9612 let result = execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
9613 .await
9614 .unwrap();
9615 assert_eq!(variable_f64(&result, "x"), 1.0);
9616 }
9617
9618 #[tokio::test(flavor = "multi_thread")]
9621 async fn not_yet_added_user_declarations_get_no_hint() {
9622 let program = "@settings(kclVersion = 2.0)\n@(added_in = \"3.0\")\nfn newFn() { return 1 }\nx = newFn()\n";
9623 assert_eq!(
9624 parse_execute(program).await.unwrap_err().message(),
9625 "`newFn` is not defined"
9626 );
9627
9628 let dep = "@(added_in = \"3.0\")\nexport fn newFn() { return 1 }\n";
9629 let main = "@settings(kclVersion = 2.0)\nimport newFn from \"dep.kcl\"\n";
9630 assert_eq!(
9631 execute_with_modules(main, &[("dep.kcl", dep)])
9632 .await
9633 .unwrap_err()
9634 .message(),
9635 "newFn is not defined in module"
9636 );
9637 }
9638
9639 #[tokio::test(flavor = "multi_thread")]
9640 async fn not_yet_added_records_whether_the_declaration_is_std() {
9641 let ctx = new_mock_executor_context(None, machine::ExecutorKind::resolve());
9642 let mut exec_state = ExecState::new(&ctx);
9643 let version = |s: &str| annotations::VersionConstraint::parse(s).unwrap();
9644
9645 exec_state.record_not_yet_added("userFn".to_owned(), version("3.0"), false);
9646 exec_state.mod_local.path = ModulePath::Std {
9647 value: "solid".to_owned(),
9648 };
9649 exec_state.record_not_yet_added("stdFn".to_owned(), version("3.0"), true);
9650
9651 let records = &exec_state.mod_local.not_yet_added;
9652 assert!(!records["userFn"].item.is_std);
9653 assert!(!records["userFn"].exported);
9654 assert!(records["stdFn"].item.is_std);
9655 assert!(records["stdFn"].exported);
9656 ctx.close().await;
9657 }
9658
9659 #[tokio::test(flavor = "multi_thread")]
9661 async fn not_yet_added_records_follow_glob_re_exports() {
9662 let dep = "@(added_in = \"3.0\")\nexport fn newFn() { return 1 }\n";
9663 let mid = "export import * from \"dep.kcl\"\n";
9664 let modules = [("dep.kcl", dep), ("mid.kcl", mid)];
9665
9666 let main = "@settings(kclVersion = 2.0)\nimport * from \"mid.kcl\"\nx = newFn()\n";
9667 assert_eq!(
9668 execute_with_modules_hinting_all(main, &modules)
9669 .await
9670 .unwrap_err()
9671 .message(),
9672 "`newFn` is not defined; it was added in KCL 3.0, but this program uses KCL 2.0"
9673 );
9674
9675 let main = "@settings(kclVersion = 2.0)\nimport newFn from \"mid.kcl\"\nx = newFn()\n";
9676 assert_eq!(
9677 execute_with_modules_hinting_all(main, &modules)
9678 .await
9679 .unwrap_err()
9680 .message(),
9681 "newFn is not defined in module; it was added in KCL 3.0, but this program uses KCL 2.0"
9682 );
9683 }
9684
9685 #[tokio::test(flavor = "multi_thread")]
9687 async fn private_not_yet_added_records_stay_private() {
9688 let dep = "@(added_in = \"3.0\")\nfn privateFn() { return 1 }\nexport fn oldFn() { return 2 }\n";
9689
9690 let main = "@settings(kclVersion = 2.0)\nimport * from \"dep.kcl\"\nx = privateFn()\n";
9691 assert_eq!(
9692 execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
9693 .await
9694 .unwrap_err()
9695 .message(),
9696 "`privateFn` is not defined"
9697 );
9698
9699 let main = "@settings(kclVersion = 2.0)\nimport privateFn from \"dep.kcl\"\n";
9700 assert_eq!(
9701 execute_with_modules_hinting_all(main, &[("dep.kcl", dep)])
9702 .await
9703 .unwrap_err()
9704 .message(),
9705 "privateFn is not defined in module"
9706 );
9707
9708 let dep = "@(added_in = \"3.0\")\nexport fn newFn() { return 1 }\n";
9710 let mid = "import * from \"dep.kcl\"\nexport fn other() { return 2 }\n";
9711 let main = "@settings(kclVersion = 2.0)\nimport * from \"mid.kcl\"\nx = newFn()\n";
9712 assert_eq!(
9713 execute_with_modules_hinting_all(main, &[("dep.kcl", dep), ("mid.kcl", mid)])
9714 .await
9715 .unwrap_err()
9716 .message(),
9717 "`newFn` is not defined"
9718 );
9719 }
9720
9721 #[tokio::test(flavor = "multi_thread")]
9724 async fn std_not_yet_added_records_survive_the_mock_memory_cache() {
9725 let ctx = new_mock_executor_context(None, machine::ExecutorKind::resolve());
9726 let mut exec_state = ExecState::new(&ctx);
9727 let version = |s: &str| annotations::VersionConstraint::parse(s).unwrap();
9728 let program = crate::Program::parse_no_errs("x = 1").unwrap();
9729 let (main_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
9730 exec_state.global.std_not_yet_added.insert(
9731 "cube".to_owned(),
9732 NotYetAdded {
9733 added_in: version("3.0"),
9734 is_std: true,
9735 },
9736 );
9737
9738 let cached = cache::GlobalState::new(exec_state, ctx.settings.clone(), program.ast.clone(), main_ref);
9739 let mem = cached.mock_memory_state().unwrap();
9740 assert_eq!(mem.std_not_yet_added["cube"].added_in, version("3.0"));
9741
9742 let mock_config = MockConfig::default();
9743 let mut restored = ExecState::new_mock(&ctx, &mock_config);
9744 ExecutorContext::restore_mock_memory(&mut restored, mem, &mock_config).unwrap();
9745 assert!(
9746 !restored.stack().memory.requires_std(),
9747 "cached memory already has the prelude"
9748 );
9749 assert_eq!(
9750 restored.not_yet_added_in_scope("cube").map(|r| &r.added_in),
9751 Some(&version("3.0"))
9752 );
9753 let err = KclError::new_undefined_value(
9755 KclErrorDetails::new("`cube` is not defined".to_owned(), Vec::new()),
9756 None,
9757 );
9758 assert_eq!(
9759 restored.with_not_yet_added_hint(&["cube"], err).message(),
9760 "`cube` is not defined; it was added in KCL 3.0, but this program uses KCL 1.0"
9761 );
9762 ctx.close().await;
9763 }
9764
9765 async fn run_mock_reusing_memory(ctx: &ExecutorContext, code: &str) -> Result<ExecOutcome, KclErrorWithOutputs> {
9770 let program = crate::Program::parse_no_errs(code).unwrap();
9771 ctx.run_mock(
9772 &program,
9773 &MockConfig {
9774 use_prev_memory: true,
9775 ..Default::default()
9776 },
9777 )
9778 .await
9779 }
9780
9781 #[track_caller]
9782 fn outcome_f64(outcome: &ExecOutcome, name: &str) -> f64 {
9783 let Some(KclValueView::Number { value, .. }) = outcome.variables.get(name) else {
9784 panic!(
9785 "expected `{name}` to be a number, got {:?}",
9786 outcome.variables.get(name)
9787 );
9788 };
9789 *value
9790 }
9791
9792 #[tokio::test(flavor = "multi_thread")]
9796 async fn mock_memory_is_rebuilt_on_a_kcl_version_downgrade() {
9797 clear_mem_cache().await;
9798 let ctx = ExecutorContext::new_mock(None).await;
9799 let body = "@(added_in = \"3.0\")\nfn newFn() {\n return 42\n}\n\nanswer = newFn()\n";
9800
9801 let outcome = run_mock_reusing_memory(&ctx, &format!("@settings(kclVersion = \"3.0-preview\")\n{body}"))
9802 .await
9803 .unwrap();
9804 assert_eq!(outcome_f64(&outcome, "answer"), 42.0);
9805
9806 let err = run_mock_reusing_memory(&ctx, &format!("@settings(kclVersion = 2.0)\n{body}"))
9808 .await
9809 .unwrap_err();
9810 assert_eq!(err.error.message(), "`newFn` is not defined");
9811
9812 clear_mem_cache().await;
9813 ctx.close().await;
9814 }
9815
9816 async fn mock_context_with_modules(modules: &[(&str, &str)]) -> (ExecutorContext, tempfile::TempDir) {
9818 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_mock_memory").unwrap();
9819 for (name, source) in modules {
9820 tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
9821 }
9822 let ctx = ExecutorContext::new_mock(Some(ExecutorSettings {
9823 project_directory: Some(crate::TypedPath(tmpdir.path().into())),
9824 ..Default::default()
9825 }))
9826 .await;
9827 (ctx, tmpdir)
9828 }
9829
9830 const GATED_DEP: &str =
9831 "@(added_in = \"3.0\")\nexport fn newFn() {\n return 42\n}\nexport fn oldFn() {\n return 1\n}\n";
9832
9833 #[tokio::test(flavor = "multi_thread")]
9836 async fn mock_memory_is_rebuilt_on_a_kcl_version_upgrade_with_imports() {
9837 clear_mem_cache().await;
9838 let (ctx, _tmpdir) = mock_context_with_modules(&[("dep.kcl", GATED_DEP)]).await;
9839
9840 let outcome = run_mock_reusing_memory(
9841 &ctx,
9842 "@settings(kclVersion = 2.0)\nimport * from \"dep.kcl\"\nx = oldFn()\n",
9843 )
9844 .await
9845 .unwrap();
9846 assert_eq!(outcome_f64(&outcome, "x"), 1.0);
9847
9848 let outcome = run_mock_reusing_memory(
9849 &ctx,
9850 "@settings(kclVersion = \"3.0-preview\")\nimport * from \"dep.kcl\"\nanswer = newFn()\n",
9851 )
9852 .await
9853 .unwrap();
9854 assert_eq!(outcome_f64(&outcome, "answer"), 42.0);
9855
9856 clear_mem_cache().await;
9857 ctx.close().await;
9858 }
9859
9860 #[tokio::test(flavor = "multi_thread")]
9861 async fn mock_memory_is_rebuilt_on_a_kcl_version_downgrade_with_imports() {
9862 clear_mem_cache().await;
9863 let (ctx, _tmpdir) = mock_context_with_modules(&[("dep.kcl", GATED_DEP)]).await;
9864
9865 let outcome = run_mock_reusing_memory(
9866 &ctx,
9867 "@settings(kclVersion = \"3.0-preview\")\nimport newFn from \"dep.kcl\"\nanswer = newFn()\n",
9868 )
9869 .await
9870 .unwrap();
9871 assert_eq!(outcome_f64(&outcome, "answer"), 42.0);
9872
9873 let err = run_mock_reusing_memory(
9874 &ctx,
9875 "@settings(kclVersion = 2.0)\nimport newFn from \"dep.kcl\"\nanswer = newFn()\n",
9876 )
9877 .await
9878 .unwrap_err();
9879 assert_eq!(err.error.message(), "newFn is not defined in module");
9880
9881 clear_mem_cache().await;
9882 ctx.close().await;
9883 }
9884
9885 #[tokio::test(flavor = "multi_thread")]
9889 async fn mock_memory_is_reused_when_only_other_settings_change() {
9890 clear_mem_cache().await;
9891 let ctx = ExecutorContext::new_mock(None).await;
9892
9893 run_mock_reusing_memory(&ctx, "@settings(kclVersion = 2.0)\nx = 2\n")
9894 .await
9895 .unwrap();
9896 let outcome = run_mock_reusing_memory(
9897 &ctx,
9898 "@settings(kclVersion = 2.0, experimentalFeatures = allow)\ny = x\n",
9899 )
9900 .await
9901 .unwrap();
9902 assert_eq!(outcome_f64(&outcome, "y"), 2.0);
9903
9904 clear_mem_cache().await;
9905 ctx.close().await;
9906 }
9907
9908 #[tokio::test(flavor = "multi_thread")]
9911 async fn mock_memory_treats_an_undeclared_kcl_version_as_the_default() {
9912 clear_mem_cache().await;
9913 let ctx = ExecutorContext::new_mock(None).await;
9914
9915 run_mock_reusing_memory(&ctx, "x = 2\n").await.unwrap();
9916 let outcome = run_mock_reusing_memory(&ctx, "@settings(kclVersion = 1.0)\ny = x\n")
9917 .await
9918 .unwrap();
9919 assert_eq!(outcome_f64(&outcome, "y"), 2.0);
9920 let err = run_mock_reusing_memory(&ctx, "@settings(kclVersion = 2.0)\nz = x\n")
9921 .await
9922 .unwrap_err();
9923 assert_eq!(err.error.message(), "`x` is not defined");
9924
9925 clear_mem_cache().await;
9926 ctx.close().await;
9927 }
9928}