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