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