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