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