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