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