1use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use anyhow::Result;
7pub use artifact::ArtifactCommand;
8pub(crate) use artifact::EntityCloneInfo;
9pub(crate) use artifact::named_view_artifact;
10pub(crate) use artifact::sketch_block_constraint_type;
11use cache::GlobalState;
12pub use cache::bust_cache;
13pub use cache::clear_mem_cache;
14pub use geometry::*;
15pub use id_generator::IdGenerator;
16pub(crate) use import::PreImportedGeometry;
17use indexmap::IndexMap;
18pub use kcl_api::Operation;
19pub use kcl_api::artifact::Artifact;
20pub use kcl_api::artifact::ArtifactGraph;
21pub use kcl_api::artifact::CapSubType;
22pub use kcl_api::artifact::CodeRef;
23pub use kcl_api::artifact::GdtAnnotationArtifact;
24pub use kcl_api::artifact::SketchBlock;
25pub use kcl_api::artifact::SketchBlockConstraint;
26#[allow(unused_imports)]
27pub use kcl_api::artifact::SketchBlockConstraintType;
28pub use kcl_api::artifact::StartSketchOnFace;
29pub use kcl_api::artifact::StartSketchOnPlane;
30use kcl_api::ast::node_path::NodePath;
31pub use kcl_value::KclObjectFields;
32pub use kcl_value::KclObjectKind;
33pub use kcl_value::KclValue;
34pub use kcl_value_view::KclValueView;
35use kcmc::ImageFormat;
36use kcmc::ModelingCmd;
37use kcmc::each_cmd as mcmd;
38use kcmc::ok_response::OkModelingCmdResponse;
39use kcmc::ok_response::output::TakeSnapshot;
40use kcmc::websocket::ModelingSessionData;
41use kcmc::websocket::OkWebSocketResponseData;
42use kittycad_modeling_cmds::id::ModelingCmdId;
43use kittycad_modeling_cmds::{self as kcmc};
44pub use memory::EnvironmentRef;
45#[cfg(test)]
46pub(crate) use memory::MemoryBackendKind;
47pub(crate) use modeling::ModelingCmdMeta;
48pub use named_views::*;
49use serde::Deserialize;
50use serde::Serialize;
51pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
52pub(crate) use sketch_solve::solver_numeric_type;
53pub use sketch_transpiler::pre_execute_transpile;
54pub use sketch_transpiler::transpile_all_old_sketches_to_new;
55pub use sketch_transpiler::transpile_old_sketch_to_new;
56pub use sketch_transpiler::transpile_old_sketch_to_new_ast;
57pub use sketch_transpiler::transpile_old_sketch_to_new_with_execution;
58pub(crate) use solver_arc::SolverArc;
59pub(crate) use state::ConstraintKey;
60pub(crate) use state::ConstraintState;
61pub(crate) use state::ConsumedRegionInfo;
62pub(crate) use state::ConsumedRegionOperation;
63pub(crate) use state::ConsumedSolidInfo;
64pub(crate) use state::ConsumedSolidKey;
65pub(crate) use state::ConsumedSolidOperation;
66pub use state::DirectTagFilletMeta;
67pub use state::DirectTagFilletTagEntry;
68pub use state::EdgeRefactorMeta;
69pub use state::EdgeRefactorStdlibFn;
70pub use state::ExecState;
71pub use state::KclVersion;
72pub use state::LegacyAngleRefactorMeta;
73pub use state::MetaSettings;
74pub(crate) use state::ModuleArtifactState;
75pub(crate) use state::PendingEdgeRefactorMeta;
76pub(crate) use state::PendingLegacyAngleRefactorMeta;
77pub use state::RefactorMetadata;
78pub(crate) use state::TangencyMode;
79
80use crate::CompilationIssue;
81use crate::ExecError;
82use crate::KclErrorWithOutputs;
83use crate::NodePathExt;
84use crate::SourceRange;
85use crate::collections::AhashIndexSet;
86use crate::engine::EngineBatchContext;
87use crate::engine::GridScaleBehavior;
88use crate::engine::engine_manager::EngineManager;
89use crate::errors::KclError;
90use crate::errors::KclErrorDetails;
91use crate::execution::cache::CacheInformation;
92use crate::execution::cache::CacheResult;
93use crate::execution::cad_op::OperationExt;
94use crate::execution::import_graph::Universe;
95use crate::execution::import_graph::UniverseMap;
96use crate::execution::typed_path::TypedPath;
97use crate::front::Number;
98use crate::front::Object;
99use crate::front::ObjectId;
100use crate::fs::FileManager;
101use crate::fs::FileSystemHandle;
102use crate::modules::ModuleExecutionOutcome;
103use crate::modules::ModuleId;
104use crate::modules::ModulePath;
105use crate::modules::ModuleRepr;
106use crate::modules::ModuleSource;
107use crate::parsing::ast::types::Expr;
108use crate::parsing::ast::types::ImportPath;
109use crate::parsing::ast::types::NodeRef;
110
111#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
112#[ts(export)]
113pub struct OperationsByModule {
114 pub map: IndexMap<ModuleId, Vec<Operation>>,
115}
116
117#[derive(Clone, Serialize, ts_rs::TS)]
118#[ts(export)]
119#[serde(rename_all = "camelCase")]
120pub struct OperationCallbackArgs {
121 pub module_id: ModuleId,
122 pub operation: Operation,
123 pub index: usize,
124}
125
126pub trait ExecutionCallbacks: std::fmt::Debug + Send + Sync + 'static {
127 fn on_operation(&self, _args: OperationCallbackArgs) {}
128}
129
130impl OperationsByModule {
131 pub fn count(&self) -> usize {
132 self.map.values().map(Vec::len).sum()
133 }
134
135 pub fn is_empty(&self) -> bool {
136 self.map.values().all(Vec::is_empty)
137 }
138
139 pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
140 self.map.get(module_id)
141 }
142
143 pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
144 self.map.values()
145 }
146
147 pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
148 self.map.insert(module_id, operations);
149 }
150}
151
152pub(crate) mod annotations;
153mod artifact;
154#[cfg(test)]
155pub(crate) use artifact::mermaid_tests::ArtifactGraphMermaidExt;
156pub(crate) mod cache;
157mod cad_op;
158pub(crate) mod exec_ast;
159pub mod fn_call;
160#[cfg(test)]
161mod freedom_analysis_tests;
162mod geometry;
163#[cfg(test)]
164mod hide_id_contract_kcl_test_pins;
165mod id_generator;
166mod import;
167mod import_graph;
168pub(crate) mod kcl_value;
169pub(crate) mod kcl_value_view;
170pub(crate) mod machine;
171mod memory;
172mod modeling;
173mod named_views;
174mod sketch_solve;
175mod sketch_transpiler;
176mod solver_arc;
177mod state;
178pub mod typed_path;
179pub(crate) mod types;
180
181pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
182pub(crate) const SKETCH_OBJECT_META: &str = "meta";
183pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
184
185macro_rules! control_continue {
190 ($control_flow:expr) => {{
191 let cf = $control_flow;
192 if cf.is_some_return() {
193 return Ok(cf);
194 } else {
195 cf.into_value()
196 }
197 }};
198}
199pub(crate) use control_continue;
201
202macro_rules! early_return {
207 ($control_flow:expr) => {{
208 let cf = $control_flow;
209 if cf.is_some_return() {
210 return Err(EarlyReturn::from(cf));
211 } else {
212 cf.into_value()
213 }
214 }};
215}
216pub(crate) use early_return;
218
219#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
220pub enum ControlFlowKind {
221 #[default]
222 Continue,
223 Exit,
224}
225
226impl ControlFlowKind {
227 pub fn is_some_return(&self) -> bool {
229 match self {
230 ControlFlowKind::Continue => false,
231 ControlFlowKind::Exit => true,
232 }
233 }
234}
235
236#[must_use = "You should always handle the control flow value when it is returned"]
237#[derive(Debug, Clone, PartialEq, Serialize)]
238pub struct KclValueControlFlow {
239 value: Box<KclValue>,
241 pub control: ControlFlowKind,
242}
243
244impl KclValue {
245 pub(crate) fn continue_(self) -> KclValueControlFlow {
246 KclValueControlFlow {
247 value: Box::new(self),
248 control: ControlFlowKind::Continue,
249 }
250 }
251
252 pub(crate) fn exit(self) -> KclValueControlFlow {
253 KclValueControlFlow {
254 value: Box::new(self),
255 control: ControlFlowKind::Exit,
256 }
257 }
258}
259
260impl KclValueControlFlow {
261 pub fn is_some_return(&self) -> bool {
263 self.control.is_some_return()
264 }
265
266 pub(crate) fn into_value(self) -> KclValue {
267 *self.value
268 }
269}
270
271#[must_use = "You should always handle the control flow value when it is returned"]
278#[allow(clippy::large_enum_variant)]
279#[derive(Debug, Clone)]
280pub(crate) enum EarlyReturn {
281 Value(KclValueControlFlow),
283 Error(KclError),
285}
286
287impl From<KclValueControlFlow> for EarlyReturn {
288 fn from(cf: KclValueControlFlow) -> Self {
289 EarlyReturn::Value(cf)
290 }
291}
292
293impl From<KclError> for EarlyReturn {
294 fn from(err: KclError) -> Self {
295 EarlyReturn::Error(err)
296 }
297}
298
299pub(crate) enum StatementKind<'a> {
300 Declaration { name: &'a str },
301 Expression,
302}
303
304#[derive(Debug, Clone, Copy)]
305pub enum PreserveMem {
306 Normal,
307 Always,
308}
309
310impl PreserveMem {
311 fn normal(self) -> bool {
312 match self {
313 PreserveMem::Normal => true,
314 PreserveMem::Always => false,
315 }
316 }
317}
318
319#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
321#[ts(export)]
322#[serde(rename_all = "camelCase")]
323pub struct ExecOutcome {
324 pub variables: IndexMap<String, KclValueView>,
326 pub operations: OperationsByModule,
329 pub artifact_graph: ArtifactGraph,
331 #[serde(skip)]
333 pub scene_objects: Vec<Object>,
334 #[serde(skip)]
337 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
338 #[serde(skip)]
339 pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
340 pub refactor_metadata: Vec<RefactorMetadata>,
342 pub issues: Vec<CompilationIssue>,
344 pub filenames: IndexMap<ModuleId, ModulePath>,
346 #[serde(skip)]
351 pub source_files: IndexMap<ModuleId, ModuleSource>,
352 pub default_planes: Option<DefaultPlanes>,
354}
355
356#[derive(Debug, Clone, Copy, PartialEq)]
360enum SegmentFreedom {
361 Free,
362 Fixed,
363 Conflict,
364 Error,
366}
367
368impl From<crate::front::Freedom> for SegmentFreedom {
369 fn from(f: crate::front::Freedom) -> Self {
370 match f {
371 crate::front::Freedom::Free => Self::Free,
372 crate::front::Freedom::Fixed => Self::Fixed,
373 crate::front::Freedom::Conflict => Self::Conflict,
374 }
375 }
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
380pub enum ConstraintKind {
381 FullyConstrained,
382 UnderConstrained,
383 OverConstrained,
384 Error,
388}
389
390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
397pub struct SketchConstraintStatus {
398 pub name: String,
412 pub status: ConstraintKind,
414 pub free_count: usize,
416 pub conflict_count: usize,
418 pub total_count: usize,
420}
421
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
424pub struct SketchConstraintReport {
425 pub fully_constrained: Vec<SketchConstraintStatus>,
426 pub under_constrained: Vec<SketchConstraintStatus>,
427 pub over_constrained: Vec<SketchConstraintStatus>,
428 pub errors: Vec<SketchConstraintStatus>,
431}
432
433pub(crate) fn sketch_constraint_status_for_sketch(
442 scene_objects: &[Object],
443 sketch_obj: &Object,
444) -> Option<SketchConstraintStatus> {
445 use crate::front::ObjectKind;
446 use crate::front::Segment;
447
448 let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
449 return None;
450 };
451
452 let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
454 let obj = scene_objects.get(id.0)?;
455 if let ObjectKind::Segment {
456 segment: Segment::Point(p),
457 } = &obj.kind
458 {
459 Some(p.freedom())
460 } else {
461 None
462 }
463 };
464
465 let mut free_count: usize = 0;
466 let mut conflict_count: usize = 0;
467 let mut error_count: usize = 0;
468 let mut total_count: usize = 0;
469
470 for &seg_id in &sketch.segments {
471 let Some(seg_obj) = scene_objects.get(seg_id.0) else {
472 continue;
473 };
474 let ObjectKind::Segment { segment } = &seg_obj.kind else {
475 continue;
476 };
477 if let Segment::Point(p) = segment
480 && p.owner.is_some()
481 {
482 continue;
483 }
484 let freedom = segment
485 .freedom(lookup)
486 .map(SegmentFreedom::from)
487 .unwrap_or(SegmentFreedom::Error);
488 total_count += 1;
489 match freedom {
490 SegmentFreedom::Free => free_count += 1,
491 SegmentFreedom::Conflict => conflict_count += 1,
492 SegmentFreedom::Error => error_count += 1,
493 SegmentFreedom::Fixed => {}
494 }
495 }
496
497 let status = if error_count > 0 {
498 ConstraintKind::Error
499 } else if conflict_count > 0 {
500 ConstraintKind::OverConstrained
501 } else if free_count > 0 {
502 ConstraintKind::UnderConstrained
503 } else {
504 ConstraintKind::FullyConstrained
505 };
506
507 Some(SketchConstraintStatus {
508 name: sketch_obj.label.clone(),
509 status,
510 free_count,
511 conflict_count,
512 total_count,
513 })
514}
515
516pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
517 let mut fully_constrained = Vec::new();
518 let mut under_constrained = Vec::new();
519 let mut over_constrained = Vec::new();
520 let mut errors = Vec::new();
521 for obj in scene_objects {
522 let Some(entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
523 continue;
524 };
525 match entry.status {
526 ConstraintKind::FullyConstrained => fully_constrained.push(entry),
527 ConstraintKind::UnderConstrained => under_constrained.push(entry),
528 ConstraintKind::OverConstrained => over_constrained.push(entry),
529 ConstraintKind::Error => errors.push(entry),
530 }
531 }
532
533 SketchConstraintReport {
534 fully_constrained,
535 under_constrained,
536 over_constrained,
537 errors,
538 }
539}
540
541impl ExecOutcome {
542 pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
543 debug_assert!(
544 id.0 < self.scene_objects.len(),
545 "Requested object ID {} but only have {} objects",
546 id.0,
547 self.scene_objects.len()
548 );
549 self.scene_objects.get(id.0)
550 }
551
552 pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
554 self.issues.iter().filter(|error| error.is_err())
555 }
556
557 pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
564 sketch_constraint_report_from_scene_objects(&self.scene_objects)
565 }
566
567 pub fn render_sketch_png(
570 &self,
571 sketch_name: &str,
572 ) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
573 use crate::front::ObjectKind;
574 use crate::tooling::sketch_visualizer::SketchVisualizationError;
575
576 let sketches = self
577 .scene_objects
578 .iter()
579 .filter_map(|object| match &object.kind {
580 ObjectKind::Sketch(sketch) if object.label == sketch_name => Some(sketch),
581 _ => None,
582 })
583 .collect::<Vec<_>>();
584 let sketch = match sketches.as_slice() {
585 [] => {
586 return Err(SketchVisualizationError::SketchNotFound {
587 name: sketch_name.to_owned(),
588 });
589 }
590 [sketch] => *sketch,
591 _ => {
592 return Err(SketchVisualizationError::AmbiguousSketchName {
593 name: sketch_name.to_owned(),
594 count: sketches.len(),
595 });
596 }
597 };
598
599 crate::tooling::sketch_visualizer::render_sketch_png(&self.scene_objects, sketch)
600 }
601}
602
603#[derive(Debug, Clone, PartialEq)]
605pub struct MockConfig {
606 pub use_prev_memory: bool,
607 pub sketch_block_id: Option<ObjectId>,
610 pub freedom_analysis: bool,
613 pub segment_ids_edited: AhashIndexSet<ObjectId>,
615 pub drag_anchors: Vec<SegmentDragAnchor>,
617}
618
619#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
620#[ts(export, export_to = "FrontendApi.ts")]
621#[serde(rename_all = "camelCase")]
622pub struct SegmentDragAnchor {
623 pub segment_id: ObjectId,
624 pub target: crate::front::Point2d<Number>,
625}
626
627impl Default for MockConfig {
628 fn default() -> Self {
629 Self {
630 use_prev_memory: true,
632 sketch_block_id: None,
633 freedom_analysis: true,
634 segment_ids_edited: AhashIndexSet::default(),
635 drag_anchors: Vec::new(),
636 }
637 }
638}
639
640impl MockConfig {
641 pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
643 Self {
644 sketch_block_id: Some(sketch_block_id),
645 ..Default::default()
646 }
647 }
648
649 #[must_use]
650 pub(crate) fn no_freedom_analysis(mut self) -> Self {
651 self.freedom_analysis = false;
652 self
653 }
654}
655
656#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
657#[ts(export)]
658#[serde(rename_all = "camelCase")]
659pub struct DefaultPlanes {
660 pub xy: uuid::Uuid,
661 pub xz: uuid::Uuid,
662 pub yz: uuid::Uuid,
663 pub neg_xy: uuid::Uuid,
664 pub neg_xz: uuid::Uuid,
665 pub neg_yz: uuid::Uuid,
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
669#[ts(export)]
670#[serde(tag = "type", rename_all = "camelCase")]
671pub struct TagIdentifier {
672 pub value: String,
673 #[serde(skip)]
676 pub info: Vec<(usize, TagEngineInfo)>,
677 #[serde(skip)]
678 pub meta: Vec<Metadata>,
679}
680
681impl TagIdentifier {
682 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
684 for (e, info) in self.info.iter().rev() {
685 if *e <= at_epoch {
686 return Some(info);
687 }
688 }
689
690 None
691 }
692
693 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
695 self.info.last().map(|i| &i.1)
696 }
697
698 pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
701 let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
702 return vec![];
703 };
704 self.info
705 .iter()
706 .rev()
707 .take_while(|(e, _)| *e == cur_epoch)
708 .map(|(_, info)| info)
709 .collect()
710 }
711
712 pub fn merge_info(&mut self, other: &TagIdentifier) {
714 assert_eq!(&self.value, &other.value);
715 for (oe, ot) in &other.info {
716 if let Some((e, t)) = self.info.last_mut() {
717 if *e > *oe {
719 continue;
720 }
721 if e == oe {
723 *t = ot.clone();
724 continue;
725 }
726 }
727 self.info.push((*oe, ot.clone()));
728 }
729 }
730
731 pub fn geometry(&self) -> Option<Geometry> {
732 self.get_cur_info().map(|info| info.geometry.clone())
733 }
734
735 pub(crate) fn is_body_created_tag(&self) -> bool {
736 self.get_cur_info().is_some_and(|info| {
737 matches!(&info.geometry, Geometry::Solid(_)) && info.path.is_none() && info.surface.is_some()
738 })
739 }
740}
741
742impl Eq for TagIdentifier {}
743
744impl std::fmt::Display for TagIdentifier {
745 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
746 write!(f, "{}", self.value)
747 }
748}
749
750impl std::str::FromStr for TagIdentifier {
751 type Err = KclError;
752
753 fn from_str(s: &str) -> Result<Self, Self::Err> {
754 Ok(Self {
755 value: s.to_string(),
756 info: Vec::new(),
757 meta: Default::default(),
758 })
759 }
760}
761
762impl Ord for TagIdentifier {
763 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
764 self.value.cmp(&other.value)
765 }
766}
767
768impl PartialOrd for TagIdentifier {
769 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
770 Some(self.cmp(other))
771 }
772}
773
774impl std::hash::Hash for TagIdentifier {
775 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
776 self.value.hash(state);
777 }
778}
779
780#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
782#[ts(export)]
783#[serde(tag = "type", rename_all = "camelCase")]
784pub struct TagEngineInfo {
785 pub id: uuid::Uuid,
787 pub geometry: Geometry,
789 pub path: Option<Path>,
791 pub surface: Option<ExtrudeSurface>,
793}
794
795#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
796pub enum BodyType {
797 Root,
798 Block,
799}
800
801#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
803#[ts(export)]
804#[serde(rename_all = "camelCase")]
805pub struct Metadata {
806 pub source_range: SourceRange,
808}
809
810impl From<Metadata> for Vec<SourceRange> {
811 fn from(meta: Metadata) -> Self {
812 vec![meta.source_range]
813 }
814}
815
816impl From<&Metadata> for SourceRange {
817 fn from(meta: &Metadata) -> Self {
818 meta.source_range
819 }
820}
821
822impl From<SourceRange> for Metadata {
823 fn from(source_range: SourceRange) -> Self {
824 Self { source_range }
825 }
826}
827
828impl<T> From<NodeRef<'_, T>> for Metadata {
829 fn from(node: NodeRef<'_, T>) -> Self {
830 Self {
831 source_range: SourceRange::new(node.start, node.end, node.module_id),
832 }
833 }
834}
835
836impl From<&Expr> for Metadata {
837 fn from(expr: &Expr) -> Self {
838 Self {
839 source_range: SourceRange::from(expr),
840 }
841 }
842}
843
844impl Metadata {
845 pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
846 if meta.len() == 1 {
847 let meta = &meta[0];
848 return crate::front::SourceRef::Simple {
849 range: meta.source_range,
850 node_path,
851 };
852 }
853 crate::front::SourceRef::BackTrace {
854 ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
855 }
856 }
857}
858
859#[derive(PartialEq, Debug, Default, Clone)]
861pub enum ContextType {
862 #[default]
864 Live,
865
866 Mock,
870
871 MockCustomForwarded,
873}
874
875#[derive(Clone)]
879pub struct ExecutorContext {
880 pub engine: Arc<EngineManager>,
881 pub engine_batch: EngineBatchContext,
882 pub fs: FileSystemHandle,
883 pub settings: ExecutorSettings,
884 pub context_type: ContextType,
885 pub execution_callbacks: Option<Arc<dyn ExecutionCallbacks>>,
886 pub(crate) executor_kind: machine::ExecutorKind,
890 pub(crate) machine_call_depth_limit: usize,
893}
894
895impl std::fmt::Debug for ExecutorContext {
896 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
897 f.debug_struct("ExecutorContext")
898 .field("engine", &self.engine)
899 .field("engine_batch", &self.engine_batch)
900 .field("settings", &self.settings)
901 .field("context_type", &self.context_type)
902 .field("execution_callbacks", &self.execution_callbacks)
903 .field("executor_kind", &self.executor_kind)
904 .field("machine_call_depth_limit", &self.machine_call_depth_limit)
905 .finish()
906 }
907}
908
909#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
911#[ts(export)]
912pub struct ExecutorSettings {
913 pub highlight_edges: bool,
915 pub enable_ssao: bool,
917 pub show_grid: bool,
919 pub replay: Option<String>,
922 pub project_directory: Option<TypedPath>,
925 pub current_file: Option<TypedPath>,
928 pub fixed_size_grid: bool,
930 #[serde(default, skip_serializing_if = "is_false")]
936 pub skip_artifact_graph: bool,
937 #[serde(default, skip_serializing_if = "Option::is_none")]
940 pub heartbeats: Option<u64>,
941 #[serde(default, skip_serializing_if = "Option::is_none")]
944 pub default_backface_color: Option<String>,
945}
946
947fn is_false(b: &bool) -> bool {
948 !*b
949}
950
951impl Default for ExecutorSettings {
952 fn default() -> Self {
953 Self {
954 highlight_edges: true,
955 enable_ssao: false,
956 show_grid: false,
957 replay: None,
958 project_directory: None,
959 current_file: None,
960 fixed_size_grid: true,
961 skip_artifact_graph: false,
962 heartbeats: None,
963 default_backface_color: None,
964 }
965 }
966}
967
968impl From<crate::settings::types::Configuration> for ExecutorSettings {
969 fn from(config: crate::settings::types::Configuration) -> Self {
970 Self::from(config.settings)
971 }
972}
973
974impl From<crate::settings::types::Settings> for ExecutorSettings {
975 fn from(settings: crate::settings::types::Settings) -> Self {
976 let modeling_settings = settings.modeling.unwrap_or_default();
977 Self {
978 highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
979 enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
980 show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
981 replay: None,
982 project_directory: None,
983 current_file: None,
984 fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
985 skip_artifact_graph: false,
986 heartbeats: None,
987 default_backface_color: modeling_settings.backface_color.map(|color| color.0),
988 }
989 }
990}
991
992impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
993 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
994 Self::from(config.settings.modeling)
995 }
996}
997
998impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
999 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
1000 Self {
1001 highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
1002 enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
1003 show_grid: modeling.show_scale_grid.unwrap_or_default(),
1004 replay: None,
1005 project_directory: None,
1006 current_file: None,
1007 fixed_size_grid: true,
1008 skip_artifact_graph: false,
1009 heartbeats: None,
1010 default_backface_color: modeling.backface_color.map(|color| color.0),
1011 }
1012 }
1013}
1014
1015impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
1016 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
1017 Self {
1018 highlight_edges: modeling.highlight_edges.into(),
1019 enable_ssao: modeling.enable_ssao.into(),
1020 show_grid: Default::default(),
1021 replay: None,
1022 project_directory: None,
1023 current_file: None,
1024 fixed_size_grid: true,
1025 skip_artifact_graph: false,
1026 heartbeats: None,
1027 default_backface_color: None,
1028 }
1029 }
1030}
1031
1032impl ExecutorSettings {
1033 pub fn with_current_file(&mut self, current_file: TypedPath) {
1035 if current_file.extension() == Some("kcl") {
1037 self.current_file = Some(current_file.clone());
1038 if let Some(parent) = current_file.parent() {
1040 self.project_directory = Some(parent);
1041 } else {
1042 self.project_directory = Some(TypedPath::from(""));
1043 }
1044 } else {
1045 self.project_directory = Some(current_file);
1046 }
1047 }
1048}
1049
1050impl ExecutorContext {
1051 pub fn new_with_engine_and_fs(
1053 engine: Arc<EngineManager>,
1054 fs: FileSystemHandle,
1055 settings: ExecutorSettings,
1056 ) -> Self {
1057 ExecutorContext {
1058 engine,
1059 engine_batch: EngineBatchContext::default(),
1060 fs,
1061 settings,
1062 context_type: ContextType::Live,
1063 execution_callbacks: Default::default(),
1064 executor_kind: machine::ExecutorKind::resolve(),
1065 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1066 }
1067 }
1068
1069 fn clone_with_fresh_execution_batch(&self) -> Self {
1070 Self {
1071 engine: self.engine.clone(),
1072 engine_batch: EngineBatchContext::new(),
1073 fs: self.fs.clone(),
1074 settings: self.settings.clone(),
1075 context_type: self.context_type.clone(),
1076 execution_callbacks: self.execution_callbacks.clone(),
1077 executor_kind: self.executor_kind,
1080 machine_call_depth_limit: self.machine_call_depth_limit,
1081 }
1082 }
1083
1084 #[cfg(not(target_arch = "wasm32"))]
1086 pub fn new_with_engine(engine: Arc<EngineManager>, settings: ExecutorSettings) -> Self {
1087 Self::new_with_engine_and_fs(engine, crate::fs::new_file_system_handle(FileManager::new()), settings)
1088 }
1089
1090 #[cfg(not(target_arch = "wasm32"))]
1092 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
1093 let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
1094 let (ws, _headers) = client
1095 .modeling()
1096 .commands_ws(kittycad::modeling::CommandsWsParams {
1097 api_call_id: None,
1098 fps: None,
1099 order_independent_transparency: None,
1100 post_effect: if settings.enable_ssao {
1101 Some(kittycad::types::PostEffectType::Ssao)
1102 } else {
1103 None
1104 },
1105 replay: settings.replay.clone(),
1106 show_grid: if settings.show_grid { Some(true) } else { None },
1107 pool: None,
1108 pr,
1109 unlocked_framerate: None,
1110 webrtc: Some(false),
1111 video_res_width: None,
1112 video_res_height: None,
1113 })
1114 .await?;
1115
1116 let engine_conn = EngineManager::new_websocket_transport(ws, settings.heartbeats).await;
1117 let engine = Arc::new(engine_conn);
1118
1119 Ok(Self::new_with_engine(engine, settings))
1120 }
1121
1122 #[cfg(target_arch = "wasm32")]
1123 pub fn new(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1124 Self::new_with_engine_and_fs(engine, fs, settings)
1125 }
1126
1127 #[cfg(not(target_arch = "wasm32"))]
1128 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
1129 ExecutorContext {
1130 engine: Arc::new(EngineManager::new_mock()),
1131 engine_batch: EngineBatchContext::default(),
1132 fs: crate::fs::new_file_system_handle(FileManager::new()),
1133 settings: settings.unwrap_or_default(),
1134 context_type: ContextType::Mock,
1135 execution_callbacks: Default::default(),
1136 executor_kind: machine::ExecutorKind::resolve(),
1137 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1138 }
1139 }
1140
1141 #[cfg(target_arch = "wasm32")]
1142 pub fn new_mock(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1143 ExecutorContext {
1144 engine,
1145 engine_batch: EngineBatchContext::default(),
1146 fs,
1147 settings,
1148 context_type: ContextType::Mock,
1149 execution_callbacks: Default::default(),
1150 executor_kind: machine::ExecutorKind::resolve(),
1151 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1152 }
1153 }
1154
1155 #[cfg(target_arch = "wasm32")]
1158 pub fn new_mock_for_lsp(
1159 fs_manager: crate::fs::wasm::FileSystemManager,
1160 settings: ExecutorSettings,
1161 ) -> Result<Self, String> {
1162 let fs = crate::fs::new_file_system_handle(FileManager::new(fs_manager));
1163
1164 Ok(ExecutorContext {
1165 engine: Arc::new(EngineManager::new_mock()),
1166 engine_batch: EngineBatchContext::default(),
1167 fs,
1168 settings,
1169 context_type: ContextType::Mock,
1170 execution_callbacks: Default::default(),
1171 executor_kind: machine::ExecutorKind::resolve(),
1172 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1173 })
1174 }
1175
1176 #[cfg(not(target_arch = "wasm32"))]
1177 pub fn new_forwarded_mock(engine: Arc<EngineManager>) -> Self {
1178 ExecutorContext {
1179 engine,
1180 engine_batch: EngineBatchContext::default(),
1181 fs: crate::fs::new_file_system_handle(FileManager::new()),
1182 settings: Default::default(),
1183 context_type: ContextType::MockCustomForwarded,
1184 execution_callbacks: Default::default(),
1185 executor_kind: machine::ExecutorKind::resolve(),
1186 machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1187 }
1188 }
1189
1190 #[cfg(not(target_arch = "wasm32"))]
1196 pub async fn new_with_client(
1197 settings: ExecutorSettings,
1198 token: Option<String>,
1199 engine_addr: Option<String>,
1200 ) -> Result<Self> {
1201 let client = crate::engine::new_zoo_client(token, engine_addr)?;
1203
1204 let ctx = Self::new(&client, settings).await?;
1205 Ok(ctx)
1206 }
1207
1208 #[cfg(not(target_arch = "wasm32"))]
1213 pub async fn new_with_default_client() -> Result<Self> {
1214 let ctx = Self::new_with_client(Default::default(), None, None).await?;
1216 Ok(ctx)
1217 }
1218
1219 #[cfg(not(target_arch = "wasm32"))]
1221 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
1222 let ctx = ExecutorContext::new_with_client(
1223 ExecutorSettings {
1224 highlight_edges: true,
1225 enable_ssao: false,
1226 show_grid: false,
1227 replay: None,
1228 project_directory: None,
1229 current_file: None,
1230 fixed_size_grid: false,
1231 skip_artifact_graph: false,
1232 heartbeats: None,
1233 default_backface_color: None,
1234 },
1235 None,
1236 engine_addr,
1237 )
1238 .await?;
1239 Ok(ctx)
1240 }
1241
1242 pub fn is_mock(&self) -> bool {
1243 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1244 }
1245
1246 pub async fn no_engine_commands(&self) -> bool {
1248 self.is_mock()
1249 }
1250
1251 pub async fn send_clear_scene(
1252 &self,
1253 exec_state: &mut ExecState,
1254 source_range: crate::execution::SourceRange,
1255 ) -> Result<(), KclError> {
1256 exec_state.mod_local.artifacts.clear();
1259 exec_state.global.root_module_artifacts.clear();
1260 exec_state.global.artifacts.clear();
1261
1262 self.engine
1263 .clear_scene(&self.engine_batch, &mut exec_state.mod_local.id_generator, source_range)
1264 .await?;
1265 if self.settings.enable_ssao {
1268 let cmd_id = exec_state.next_uuid();
1269 exec_state
1270 .batch_modeling_cmd(
1271 ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1272 ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1273 )
1274 .await?;
1275 }
1276 Ok(())
1277 }
1278
1279 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1280 cache::bust_cache().await;
1281
1282 let outcome = self.run_with_caching(crate::Program::empty()).await?;
1287
1288 Ok(outcome)
1289 }
1290
1291 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1292 self.eval_prelude(exec_state, SourceRange::synthetic())
1293 .await
1294 .map_err(KclErrorWithOutputs::no_outputs)?;
1295 exec_state
1296 .mut_stack()
1297 .push_new_root_env(true)
1298 .map_err(KclErrorWithOutputs::no_outputs)?;
1299 Ok(())
1300 }
1301
1302 fn restore_mock_memory(
1303 exec_state: &mut ExecState,
1304 mem: cache::SketchModeState,
1305 _mock_config: &MockConfig,
1306 ) -> Result<(), KclErrorWithOutputs> {
1307 *exec_state.mut_stack() = mem.stack;
1308 exec_state.global.module_infos = mem.module_infos;
1309 exec_state.global.path_to_source_id = mem.path_to_source_id;
1310 exec_state.global.id_to_source = mem.id_to_source;
1311 exec_state.mod_local.constraint_state = mem.constraint_state;
1312 let len = _mock_config
1313 .sketch_block_id
1314 .map(|sketch_block_id| sketch_block_id.0)
1315 .unwrap_or(0);
1316 if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1317 exec_state
1318 .global
1319 .root_module_artifacts
1320 .restore_scene_objects(scene_objects);
1321 } else {
1322 let message = format!(
1323 "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1324 mem.scene_objects.len(),
1325 len
1326 );
1327 debug_assert!(false, "{message}");
1328 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1329 KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1330 )));
1331 }
1332
1333 Ok(())
1334 }
1335
1336 pub async fn run_mock(
1337 &self,
1338 program: &crate::Program,
1339 mock_config: &MockConfig,
1340 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1341 assert!(
1342 self.is_mock(),
1343 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1344 );
1345
1346 let use_prev_memory = mock_config.use_prev_memory;
1347 let mut exec_state = ExecState::new_mock(self, mock_config);
1348 if use_prev_memory {
1349 match cache::read_old_memory().await {
1350 Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
1351 None => self.prepare_mem(&mut exec_state).await?,
1352 }
1353 } else {
1354 self.prepare_mem(&mut exec_state).await?
1355 };
1356
1357 exec_state
1360 .mut_stack()
1361 .push_new_env_for_scope()
1362 .map_err(KclErrorWithOutputs::no_outputs)?;
1363
1364 let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1365
1366 let mut stack = exec_state.stack().clone();
1371 let module_infos = exec_state.global.module_infos.clone();
1372 let path_to_source_id = exec_state.global.path_to_source_id.clone();
1373 let id_to_source = exec_state.global.id_to_source.clone();
1374 let constraint_state = exec_state.mod_local.constraint_state.clone();
1375 let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1376 let outcome = exec_state
1377 .into_exec_outcome(result.0, self)
1378 .await
1379 .map_err(KclErrorWithOutputs::no_outputs)?;
1380
1381 stack.squash_env(result.0).map_err(KclErrorWithOutputs::no_outputs)?;
1382 let state = cache::SketchModeState {
1383 stack,
1384 module_infos,
1385 path_to_source_id,
1386 id_to_source,
1387 constraint_state,
1388 scene_objects,
1389 };
1390 cache::write_old_memory(state).await;
1391
1392 Ok(outcome)
1393 }
1394
1395 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1396 assert!(!self.is_mock());
1397 let grid_scale = if self.settings.fixed_size_grid {
1398 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1399 } else {
1400 GridScaleBehavior::ScaleWithZoom
1401 };
1402
1403 let original_program = program.clone();
1404
1405 let (_program, exec_state, result) = match cache::read_old_ast().await {
1406 Some(mut cached_state) => {
1407 let old = CacheInformation {
1408 ast: &cached_state.main.ast,
1409 settings: &cached_state.settings,
1410 };
1411 let new = CacheInformation {
1412 ast: &program.ast,
1413 settings: &self.settings,
1414 };
1415
1416 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1418 CacheResult::ReExecute {
1419 clear_scene,
1420 reapply_settings,
1421 program: changed_program,
1422 } => {
1423 if reapply_settings
1424 && self
1425 .engine
1426 .reapply_settings(
1427 &self.engine_batch,
1428 &self.settings,
1429 Default::default(),
1430 &mut cached_state.main.exec_state.id_generator,
1431 grid_scale,
1432 )
1433 .await
1434 .is_err()
1435 {
1436 (true, program, None)
1437 } else {
1438 (
1439 clear_scene,
1440 crate::Program {
1441 ast: changed_program,
1442 original_file_contents: program.original_file_contents,
1443 },
1444 None,
1445 )
1446 }
1447 }
1448 CacheResult::CheckImportsOnly {
1449 reapply_settings,
1450 ast: changed_program,
1451 } => {
1452 let mut reapply_failed = false;
1453 if reapply_settings {
1454 if self
1455 .engine
1456 .reapply_settings(
1457 &self.engine_batch,
1458 &self.settings,
1459 Default::default(),
1460 &mut cached_state.main.exec_state.id_generator,
1461 grid_scale,
1462 )
1463 .await
1464 .is_ok()
1465 {
1466 cache::write_old_ast(GlobalState::with_settings(
1467 cached_state.clone(),
1468 self.settings.clone(),
1469 ))
1470 .await;
1471 } else {
1472 reapply_failed = true;
1473 }
1474 }
1475
1476 if reapply_failed {
1477 (true, program, None)
1478 } else {
1479 let mut new_exec_state = ExecState::new(self);
1481 let (new_universe, new_universe_map) =
1482 self.get_universe(&program, &mut new_exec_state).await?;
1483
1484 let clear_scene = new_universe.values().any(|value| {
1485 let id = value.1;
1486 match (
1487 cached_state.exec_state.get_source(id),
1488 new_exec_state.global.get_source(id),
1489 ) {
1490 (Some(s0), Some(s1)) => s0.source != s1.source,
1491 _ => false,
1492 }
1493 });
1494
1495 if !clear_scene {
1496 cache::write_old_memory(
1498 cached_state
1499 .mock_memory_state()
1500 .map_err(KclErrorWithOutputs::no_outputs)?,
1501 )
1502 .await;
1503 return cached_state
1504 .into_exec_outcome(self)
1505 .await
1506 .map_err(KclErrorWithOutputs::no_outputs);
1507 }
1508
1509 (
1510 true,
1511 crate::Program {
1512 ast: changed_program,
1513 original_file_contents: program.original_file_contents,
1514 },
1515 Some((new_universe, new_universe_map, new_exec_state)),
1516 )
1517 }
1518 }
1519 CacheResult::NoAction(true) => {
1520 if self
1521 .engine
1522 .reapply_settings(
1523 &self.engine_batch,
1524 &self.settings,
1525 Default::default(),
1526 &mut cached_state.main.exec_state.id_generator,
1527 grid_scale,
1528 )
1529 .await
1530 .is_ok()
1531 {
1532 cache::write_old_ast(GlobalState::with_settings(
1534 cached_state.clone(),
1535 self.settings.clone(),
1536 ))
1537 .await;
1538
1539 cache::write_old_memory(
1540 cached_state
1541 .mock_memory_state()
1542 .map_err(KclErrorWithOutputs::no_outputs)?,
1543 )
1544 .await;
1545 return cached_state
1546 .into_exec_outcome(self)
1547 .await
1548 .map_err(KclErrorWithOutputs::no_outputs);
1549 }
1550 (true, program, None)
1551 }
1552 CacheResult::NoAction(false) => {
1553 cache::write_old_memory(
1554 cached_state
1555 .mock_memory_state()
1556 .map_err(KclErrorWithOutputs::no_outputs)?,
1557 )
1558 .await;
1559 return cached_state
1560 .into_exec_outcome(self)
1561 .await
1562 .map_err(KclErrorWithOutputs::no_outputs);
1563 }
1564 };
1565
1566 let (exec_state, result) = match import_check_info {
1567 Some((new_universe, new_universe_map, mut new_exec_state)) => {
1568 self.send_clear_scene(&mut new_exec_state, Default::default())
1570 .await
1571 .map_err(KclErrorWithOutputs::no_outputs)?;
1572
1573 let result = self
1574 .run_concurrent(
1575 &program,
1576 &mut new_exec_state,
1577 Some((new_universe, new_universe_map)),
1578 PreserveMem::Normal,
1579 )
1580 .await;
1581
1582 (new_exec_state, result)
1583 }
1584 None if clear_scene => {
1585 let mut exec_state = cached_state.reconstitute_exec_state(self);
1587 exec_state.reset(self);
1588
1589 self.send_clear_scene(&mut exec_state, Default::default())
1590 .await
1591 .map_err(KclErrorWithOutputs::no_outputs)?;
1592
1593 let result = self
1594 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1595 .await;
1596
1597 (exec_state, result)
1598 }
1599 None => {
1600 let mut exec_state = cached_state.reconstitute_exec_state(self);
1601 exec_state
1602 .mut_stack()
1603 .restore_env(cached_state.main.result_env)
1604 .map_err(KclErrorWithOutputs::no_outputs)?;
1605
1606 let result = self
1607 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1608 .await;
1609
1610 (exec_state, result)
1611 }
1612 };
1613
1614 (program, exec_state, result)
1615 }
1616 None => {
1617 let mut exec_state = ExecState::new(self);
1618 self.send_clear_scene(&mut exec_state, Default::default())
1619 .await
1620 .map_err(KclErrorWithOutputs::no_outputs)?;
1621
1622 let result = self
1623 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1624 .await;
1625
1626 (program, exec_state, result)
1627 }
1628 };
1629
1630 if result.is_err() {
1631 cache::bust_cache().await;
1632 }
1633
1634 let result = result?;
1636
1637 cache::write_old_ast(GlobalState::new(
1641 exec_state.clone(),
1642 self.settings.clone(),
1643 original_program.ast,
1644 result.0,
1645 ))
1646 .await;
1647
1648 let outcome = exec_state
1649 .into_exec_outcome(result.0, self)
1650 .await
1651 .map_err(KclErrorWithOutputs::no_outputs)?;
1652 Ok(outcome)
1653 }
1654
1655 pub async fn run(
1659 &self,
1660 program: &crate::Program,
1661 exec_state: &mut ExecState,
1662 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1663 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1664 .await
1665 }
1666
1667 pub async fn run_concurrent(
1672 &self,
1673 program: &crate::Program,
1674 exec_state: &mut ExecState,
1675 universe_info: Option<(Universe, UniverseMap)>,
1676 preserve_mem: PreserveMem,
1677 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1678 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1681 (universe, universe_map)
1682 } else {
1683 self.get_universe(program, exec_state).await?
1684 };
1685
1686 let mut sorted_imports: Vec<_> = universe_map.iter().collect();
1692 sorted_imports.sort_by_key(|(_, import_stmt)| SourceRange::from(*import_stmt));
1693 for (_path, import_stmt) in sorted_imports {
1694 let filename = match &import_stmt.path {
1698 ImportPath::Kcl { filename } => filename.to_string(),
1699 ImportPath::Foreign { path } => path.to_string(),
1700 ImportPath::Std { .. } => continue,
1701 };
1702 if let Some((_, module_id, module_path, _)) = universe.get(&filename)
1703 && let ModulePath::Local { value, .. } = module_path
1704 {
1705 let name = import_stmt
1706 .module_name()
1707 .unwrap_or_else(|| value.file_name().unwrap_or_default());
1708 let source_range = SourceRange::from(import_stmt);
1709 exec_state.push_op(crate::execution::cad_op::Operation::ModuleInstance {
1710 name,
1711 module_id: *module_id,
1712 glob: matches!(
1713 import_stmt.selector,
1714 crate::parsing::ast::types::ImportSelector::Glob(_)
1715 ),
1716 node_path: crate::NodePath::placeholder(),
1717 source_range,
1718 });
1719 }
1720 }
1721
1722 let default_planes = self.engine.get_default_planes().read().await.clone();
1723
1724 self.eval_prelude(exec_state, SourceRange::synthetic())
1726 .await
1727 .map_err(KclErrorWithOutputs::no_outputs)?;
1728
1729 for modules in import_graph::import_graph(&universe, self)
1730 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1731 .into_iter()
1732 {
1733 #[cfg(not(target_arch = "wasm32"))]
1734 let mut set = tokio::task::JoinSet::new();
1735
1736 #[allow(clippy::type_complexity)]
1737 let (results_tx, mut results_rx): (
1738 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1739 tokio::sync::mpsc::Receiver<_>,
1740 ) = tokio::sync::mpsc::channel(1);
1741
1742 for module in modules {
1743 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1744 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1745 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1746 )));
1747 };
1748 let module_id = *module_id;
1749 let module_path = module_path.clone();
1750 let source_range = SourceRange::from(import_stmt);
1751 let module_exec_state = exec_state.clone();
1753
1754 let repr = repr.clone();
1755 let exec_ctxt = self.clone_with_fresh_execution_batch();
1756 let results_tx = results_tx.clone();
1757
1758 let exec_module = async |exec_ctxt: &ExecutorContext,
1759 repr: &ModuleRepr,
1760 module_id: ModuleId,
1761 module_path: &ModulePath,
1762 exec_state: &mut ExecState,
1763 source_range: SourceRange|
1764 -> Result<ModuleRepr, KclError> {
1765 match repr {
1766 ModuleRepr::Kcl(program, _) => {
1767 let result = exec_ctxt
1768 .exec_module_from_ast(
1769 program,
1770 module_id,
1771 module_path,
1772 exec_state,
1773 source_range,
1774 PreserveMem::Normal,
1775 )
1776 .await;
1777
1778 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1779 }
1780 ModuleRepr::Foreign(geom, _) => {
1781 exec_state.mod_local.artifacts = Default::default();
1785 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1786 .await
1787 .map(|geom| Some(KclValue::ImportedGeometry(geom)))
1788 .map_err(|err| err.add_import_location(&module_path.import_name(), source_range));
1793 let module_artifacts = std::mem::take(&mut exec_state.mod_local.artifacts);
1794
1795 result.map(|val| ModuleRepr::Foreign(geom.clone(), Some((val, module_artifacts))))
1796 }
1797 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1798 format!("Module {module_path} not found in universe"),
1799 vec![source_range],
1800 ))),
1801 }
1802 };
1803
1804 #[cfg(target_arch = "wasm32")]
1805 {
1806 wasm_bindgen_futures::spawn_local(async move {
1807 let mut exec_state = module_exec_state;
1808 let exec_ctxt = exec_ctxt;
1809
1810 let result = exec_module(
1811 &exec_ctxt,
1812 &repr,
1813 module_id,
1814 &module_path,
1815 &mut exec_state,
1816 source_range,
1817 )
1818 .await;
1819
1820 results_tx
1821 .send((module_id, module_path, result))
1822 .await
1823 .unwrap_or_default();
1824 });
1825 }
1826 #[cfg(not(target_arch = "wasm32"))]
1827 {
1828 set.spawn(async move {
1829 let mut exec_state = module_exec_state;
1830 let exec_ctxt = exec_ctxt;
1831
1832 let result = exec_module(
1833 &exec_ctxt,
1834 &repr,
1835 module_id,
1836 &module_path,
1837 &mut exec_state,
1838 source_range,
1839 )
1840 .await;
1841
1842 results_tx
1843 .send((module_id, module_path, result))
1844 .await
1845 .unwrap_or_default();
1846 });
1847 }
1848 }
1849
1850 drop(results_tx);
1851
1852 while let Some((module_id, _, result)) = results_rx.recv().await {
1853 match result {
1854 Ok(new_repr) => {
1855 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1856
1857 match &mut repr {
1858 ModuleRepr::Kcl(_, cache) => {
1859 let ModuleRepr::Kcl(_, session_data) = new_repr else {
1860 unreachable!();
1861 };
1862 *cache = session_data;
1863 }
1864 ModuleRepr::Foreign(_, cache) => {
1865 let ModuleRepr::Foreign(_, session_data) = new_repr else {
1866 unreachable!();
1867 };
1868 *cache = session_data;
1869 }
1870 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1871 }
1872
1873 exec_state.global.module_infos[&module_id].restore_repr(repr);
1874 }
1875 Err(e) => {
1876 let e = import_graph::add_import_backtrace(e, module_id, &universe);
1877 return Err(exec_state.error_with_outputs(e, None, default_planes));
1878 }
1879 }
1880 }
1881 }
1882
1883 exec_state.mod_local.artifacts.operations.clear();
1888
1889 exec_state
1892 .global
1893 .root_module_artifacts
1894 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1895
1896 self.inner_run(program, exec_state, preserve_mem)
1897 .await
1898 .map_err(|mut error| {
1899 let source_ranges = error.error.source_ranges();
1905 if !source_ranges.is_empty()
1906 && !source_ranges.iter().any(|range| range.module_id().is_top_level())
1907 && let Some(outermost) = source_ranges.last()
1908 {
1909 error.error =
1910 import_graph::add_import_backtrace_from(error.error.clone(), outermost.module_id(), &universe);
1911 }
1912 error
1913 })
1914 }
1915
1916 async fn get_universe(
1919 &self,
1920 program: &crate::Program,
1921 exec_state: &mut ExecState,
1922 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1923 exec_state.add_root_module_contents(program);
1924
1925 let mut universe = std::collections::HashMap::new();
1926
1927 let default_planes = self.engine.get_default_planes().read().await.clone();
1928
1929 let root_imports = import_graph::import_universe(
1930 self,
1931 &ModulePath::Main,
1932 &ModuleRepr::Kcl(program.ast.clone(), None),
1933 &mut universe,
1934 exec_state,
1935 )
1936 .await
1937 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1938
1939 Ok((universe, root_imports))
1940 }
1941
1942 async fn inner_run(
1945 &self,
1946 program: &crate::Program,
1947 exec_state: &mut ExecState,
1948 preserve_mem: PreserveMem,
1949 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1950 let _stats = crate::log::LogPerfStats::new("Interpretation");
1951
1952 let grid_scale = if self.settings.fixed_size_grid {
1954 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1955 } else {
1956 GridScaleBehavior::ScaleWithZoom
1957 };
1958 self.engine
1959 .reapply_settings(
1960 &self.engine_batch,
1961 &self.settings,
1962 Default::default(),
1963 exec_state.id_generator(),
1964 grid_scale,
1965 )
1966 .await
1967 .map_err(KclErrorWithOutputs::no_outputs)?;
1968
1969 let default_planes = self.engine.get_default_planes().read().await.clone();
1970 let result = self
1971 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1972 .await;
1973
1974 crate::log::log(format!(
1975 "Post interpretation KCL memory stats: {:#?}",
1976 exec_state.stack().memory.stats()
1977 ));
1978 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1979
1980 async fn write_old_memory(
1983 ctx: &ExecutorContext,
1984 exec_state: &ExecState,
1985 env_ref: EnvironmentRef,
1986 ) -> Result<(), KclError> {
1987 if ctx.is_mock() {
1988 return Ok(());
1989 }
1990 let mut stack = exec_state.stack().deep_clone()?;
1991 stack.restore_env(env_ref)?;
1992 let state = cache::SketchModeState {
1993 stack,
1994 module_infos: exec_state.global.module_infos.clone(),
1995 path_to_source_id: exec_state.global.path_to_source_id.clone(),
1996 id_to_source: exec_state.global.id_to_source.clone(),
1997 constraint_state: exec_state.mod_local.constraint_state.clone(),
1998 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1999 };
2000 cache::write_old_memory(state).await;
2001 Ok(())
2002 }
2003
2004 let env_ref = match result {
2005 Ok(env_ref) => env_ref,
2006 Err((err, env_ref)) => {
2007 if let Some(env_ref) = env_ref {
2010 write_old_memory(self, exec_state, env_ref)
2011 .await
2012 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2013 }
2014 return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
2015 }
2016 };
2017
2018 write_old_memory(self, exec_state, env_ref)
2019 .await
2020 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
2021
2022 let session_data = self.engine.get_session_data().await;
2023
2024 Ok((env_ref, session_data))
2025 }
2026
2027 async fn execute_and_build_graph(
2030 &self,
2031 program: NodeRef<'_, crate::parsing::ast::types::Program>,
2032 exec_state: &mut ExecState,
2033 preserve_mem: PreserveMem,
2034 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
2035 let start_op = exec_state.global.root_module_artifacts.operations.len();
2041
2042 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
2043 .await
2044 .map_err(|e| (e, None))?;
2045
2046 let exec_result = self
2047 .exec_module_body(
2048 program,
2049 exec_state,
2050 preserve_mem,
2051 ModuleId::default(),
2052 &ModulePath::Main,
2053 )
2054 .await
2055 .map(
2056 |ModuleExecutionOutcome {
2057 environment: env_ref,
2058 artifacts: module_artifacts,
2059 ..
2060 }| {
2061 exec_state.global.root_module_artifacts.extend(module_artifacts);
2064 env_ref
2065 },
2066 )
2067 .map_err(|(err, env_ref, module_artifacts)| {
2068 if let Some(module_artifacts) = module_artifacts {
2069 exec_state.global.root_module_artifacts.extend(module_artifacts);
2072 }
2073 (err, env_ref)
2074 });
2075
2076 let programs = &exec_state.build_program_lookup(program.clone());
2078 let cached_body_items = exec_state.global.artifacts.cached_body_items();
2079 for op in exec_state
2080 .global
2081 .root_module_artifacts
2082 .operations
2083 .iter_mut()
2084 .skip(start_op)
2085 {
2086 op.fill_node_paths(programs, cached_body_items);
2087 }
2088 for module in exec_state.global.module_infos.values_mut() {
2089 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
2090 for op in &mut outcome.artifacts.operations {
2091 op.fill_node_paths(programs, cached_body_items);
2092 }
2093 }
2094 }
2095
2096 self.engine
2098 .ensure_async_commands_completed(&self.engine_batch)
2099 .await
2100 .map_err(|e| {
2101 match &exec_result {
2102 Ok(env_ref) => (e, Some(*env_ref)),
2103 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
2105 }
2106 })?;
2107
2108 self.engine.clear_queues(&self.engine_batch).await;
2111
2112 match exec_state.build_artifact_graph(&self.engine, program).await {
2113 Ok(_) => exec_result,
2114 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
2115 }
2116 }
2117
2118 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
2122 if exec_state.stack().memory.requires_std() {
2123 let initial_ops = exec_state.mod_local.artifacts.operations.len();
2124
2125 let path = vec!["std".to_owned(), "prelude".to_owned()];
2126 let resolved_path = ModulePath::from_std_import_path(&path)?;
2127 let id = self
2128 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
2129 .await?;
2130 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
2131
2132 exec_state.mut_stack().memory.set_std(module_memory)?;
2133
2134 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
2140 }
2141
2142 Ok(())
2143 }
2144
2145 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
2147 self.engine
2149 .send_modeling_cmd(
2150 &self.engine_batch,
2151 uuid::Uuid::new_v4(),
2152 crate::execution::SourceRange::default(),
2153 &ModelingCmd::from(
2154 mcmd::ZoomToFit::builder()
2155 .object_ids(Default::default())
2156 .animated(false)
2157 .padding(0.1)
2158 .build(),
2159 ),
2160 )
2161 .await
2162 .map_err(KclErrorWithOutputs::no_outputs)?;
2163
2164 let resp = self
2166 .engine
2167 .send_modeling_cmd(
2168 &self.engine_batch,
2169 uuid::Uuid::new_v4(),
2170 crate::execution::SourceRange::default(),
2171 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
2172 )
2173 .await
2174 .map_err(KclErrorWithOutputs::no_outputs)?;
2175
2176 let OkWebSocketResponseData::Modeling {
2177 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
2178 } = resp
2179 else {
2180 return Err(ExecError::BadPng(format!(
2181 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
2182 )));
2183 };
2184 Ok(contents)
2185 }
2186
2187 pub async fn export(
2189 &self,
2190 format: kittycad_modeling_cmds::format::OutputFormat3d,
2191 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2192 let resp = self
2193 .engine
2194 .send_modeling_cmd(
2195 &self.engine_batch,
2196 uuid::Uuid::new_v4(),
2197 crate::SourceRange::default(),
2198 &kittycad_modeling_cmds::ModelingCmd::Export(
2199 kittycad_modeling_cmds::Export::builder()
2200 .entity_ids(vec![])
2201 .format(format)
2202 .build(),
2203 ),
2204 )
2205 .await?;
2206
2207 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
2208 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
2209 format!("Expected Export response, got {resp:?}",),
2210 vec![SourceRange::default()],
2211 )));
2212 };
2213
2214 Ok(files)
2215 }
2216
2217 pub async fn export_step(
2219 &self,
2220 deterministic_time: bool,
2221 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2222 let files = self
2223 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
2224 kittycad_modeling_cmds::format::step::export::Options::builder()
2225 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
2226 .maybe_created(if deterministic_time {
2227 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
2228 KclError::new_internal(crate::errors::KclErrorDetails::new(
2229 format!("Failed to parse date: {e}"),
2230 vec![SourceRange::default()],
2231 ))
2232 })?)
2233 } else {
2234 None
2235 })
2236 .build(),
2237 ))
2238 .await?;
2239
2240 Ok(files)
2241 }
2242
2243 pub async fn close(&self) {
2244 self.engine.close().await;
2245 }
2246}
2247
2248pub use kcl_api::ArtifactId;
2249
2250pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
2251 ArtifactId::new(*id.as_ref())
2252}
2253
2254#[cfg(test)]
2255pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2256 parse_execute_with_project_dir(code, None).await
2257}
2258
2259#[cfg(test)]
2260pub(crate) async fn parse_execute_with_project_dir(
2261 code: &str,
2262 project_directory: Option<TypedPath>,
2263) -> Result<ExecTestResults, KclError> {
2264 parse_execute_with_executor_kind(code, project_directory, machine::ExecutorKind::resolve()).await
2266}
2267
2268#[cfg(test)]
2271pub(crate) fn new_mock_executor_context(
2272 project_directory: Option<TypedPath>,
2273 executor_kind: machine::ExecutorKind,
2274) -> ExecutorContext {
2275 ExecutorContext {
2276 engine: Arc::new(EngineManager::new_mock()),
2277 engine_batch: EngineBatchContext::default(),
2278 fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2279 settings: ExecutorSettings {
2280 project_directory,
2281 ..Default::default()
2282 },
2283 context_type: ContextType::Mock,
2284 execution_callbacks: Default::default(),
2285 executor_kind,
2286 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
2287 }
2288}
2289
2290#[cfg(test)]
2291pub(crate) async fn parse_execute_with_executor_kind(
2292 code: &str,
2293 project_directory: Option<TypedPath>,
2294 executor_kind: machine::ExecutorKind,
2295) -> Result<ExecTestResults, KclError> {
2296 let program = crate::Program::parse_no_errs(code)?;
2297
2298 let exec_ctxt = new_mock_executor_context(project_directory, executor_kind);
2299 let mut exec_state = ExecState::new(&exec_ctxt);
2300 let result = exec_ctxt.run(&program, &mut exec_state).await?;
2301
2302 Ok(ExecTestResults {
2303 program,
2304 mem_env: result.0,
2305 exec_ctxt,
2306 exec_state,
2307 })
2308}
2309
2310#[cfg(test)]
2311#[derive(Debug)]
2312pub(crate) struct ExecTestResults {
2313 program: crate::Program,
2314 mem_env: EnvironmentRef,
2315 exec_ctxt: ExecutorContext,
2316 exec_state: ExecState,
2317}
2318
2319#[cfg(test)]
2320impl ExecTestResults {
2321 pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2322 &self.exec_state.global.root_module_artifacts.commands
2323 }
2324
2325 pub(crate) fn issues(&self) -> &[CompilationIssue] {
2329 self.exec_state.issues()
2330 }
2331
2332 #[track_caller]
2336 pub(crate) fn variable(&self, name: &str) -> KclValue {
2337 self.exec_state
2338 .stack()
2339 .memory
2340 .get_from_unchecked(name, self.mem_env)
2341 .unwrap()
2342 }
2343}
2344
2345pub struct ProgramLookup {
2349 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2350}
2351
2352impl ProgramLookup {
2353 pub fn new(
2356 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2357 module_infos: state::ModuleInfoMap,
2358 ) -> Self {
2359 let mut programs = IndexMap::with_capacity(module_infos.len());
2360 for (id, info) in module_infos {
2361 if let ModuleRepr::Kcl(program, _) = info.repr {
2362 programs.insert(id, program);
2363 }
2364 }
2365 programs.insert(ModuleId::default(), current);
2366 Self { programs }
2367 }
2368
2369 pub fn program_for_module(
2370 &self,
2371 module_id: ModuleId,
2372 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2373 self.programs.get(&module_id)
2374 }
2375}
2376
2377#[cfg(test)]
2378mod tests {
2379 use kcl_api::NumericType;
2380 use pretty_assertions::assert_eq;
2381
2382 use super::*;
2383 use crate::ModuleId;
2384 use crate::errors::KclErrorDetails;
2385 use crate::errors::Severity;
2386 use crate::execution::memory::Stack;
2387 use crate::execution::types::RuntimeType;
2388
2389 macro_rules! kcl_input {
2390 ($file:literal) => {
2391 include_str!(concat!("../../e2e/executor/inputs/", $file, ".kcl"))
2392 };
2393 }
2394
2395 #[test]
2396 fn clone_with_fresh_execution_batch_keeps_executor_selection() {
2397 let mut ctx = new_mock_executor_context(None, machine::ExecutorKind::Machine);
2401 ctx.machine_call_depth_limit = 123;
2402 let cloned = ctx.clone_with_fresh_execution_batch();
2403 assert_eq!(cloned.executor_kind, machine::ExecutorKind::Machine);
2404 assert_eq!(cloned.machine_call_depth_limit, 123);
2405 }
2406
2407 #[tokio::test(flavor = "multi_thread")]
2408 async fn concurrent_foreign_import_preserves_artifact_command() {
2409 let tmpdir = tempfile::TempDir::with_prefix("zma_foreign_import_artifact").unwrap();
2410 tokio::fs::write(tmpdir.path().join("cube.obj"), "o cube\n")
2411 .await
2412 .unwrap();
2413
2414 let program = crate::Program::parse_no_errs("import \"cube.obj\" as cube\n\nmodel = cube\n").unwrap();
2415 let ctx = new_mock_executor_context(
2416 Some(crate::TypedPath(tmpdir.path().into())),
2417 machine::ExecutorKind::resolve(),
2418 );
2419 let mut exec_state = ExecState::new(&ctx);
2420 let (main_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2421 let outcome = exec_state
2422 .into_exec_outcome(main_ref, &ctx)
2423 .await
2424 .expect("foreign import execution should produce an outcome");
2425 ctx.close().await;
2426
2427 let KclValueView::ImportedGeometry(imported) = &outcome.variables["model"] else {
2428 panic!("model should be imported geometry");
2429 };
2430 let artifact_id = ArtifactId::new(imported.id);
2431 let Some(Artifact::ImportedGeometry(artifact)) = outcome.artifact_graph.get(&artifact_id) else {
2432 panic!("foreign import should produce an imported geometry artifact");
2433 };
2434 assert_eq!(artifact.id, artifact_id);
2435 assert!(!artifact.code_ref.node_path.is_empty());
2436 }
2437
2438 #[tokio::test(flavor = "multi_thread")]
2439 async fn nested_import_preserves_inner_error_and_backtrace() {
2440 let project_dir = crate::TypedPath::new("/zma-kcl-import-error");
2445 let main_path = project_dir.join("main.kcl");
2446 let assembly_path = project_dir.join("assembly.kcl");
2447 let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
2448 let files = [
2451 (
2452 project_dir.join("broken.kcl").to_string(),
2453 b"export brokenValue = missingName + 1\n".to_vec(),
2454 ),
2455 (
2456 assembly_path.to_string(),
2457 b"import brokenValue from \"broken.kcl\"\n\nexport assemblyValue = brokenValue\n".to_vec(),
2458 ),
2459 ]
2460 .into_iter()
2461 .collect();
2462 let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
2463 let settings = ExecutorSettings {
2464 project_directory: Some(project_dir),
2465 current_file: Some(main_path.clone()),
2466 ..Default::default()
2467 };
2468 let program = crate::Program::parse_no_errs(main_code).unwrap();
2469
2470 let assert_error = |error: &KclErrorWithOutputs| {
2471 let KclError::UndefinedValue { details, name } = &error.error else {
2472 panic!("expected UndefinedValue, got {:#?}", error.error);
2473 };
2474 assert_eq!(name.as_deref(), Some("missingName"));
2475 assert_eq!(details.message, "`missingName` is not defined");
2476 assert_eq!(
2477 error
2478 .error
2479 .backtrace()
2480 .iter()
2481 .map(|frame| frame.fn_name.as_deref())
2482 .collect::<Vec<_>>(),
2483 [Some("import broken.kcl"), Some("import assembly.kcl"), None]
2484 );
2485 assert_eq!(
2486 error
2487 .error
2488 .backtrace()
2489 .iter()
2490 .map(|frame| frame.kind)
2491 .collect::<Vec<_>>(),
2492 [
2493 kcl_error::BacktraceItemKind::Import,
2494 kcl_error::BacktraceItemKind::Import,
2495 kcl_error::BacktraceItemKind::Call
2496 ]
2497 );
2498
2499 let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
2500 assert!(report.filename.ends_with("broken.kcl"));
2501 assert_eq!(
2502 report
2503 .related
2504 .iter()
2505 .map(|related| related.filename.as_str())
2506 .collect::<Vec<_>>(),
2507 [assembly_path.to_string(), main_path.to_string()]
2508 );
2509
2510 let rendered = format!("{:?}", miette::Report::new(report));
2511 assert!(rendered.contains("broken.kcl"));
2512 assert!(rendered.contains("assembly.kcl"));
2513 assert!(rendered.contains("main.kcl"));
2514 assert!(rendered.contains("export brokenValue = missingName + 1"));
2515 assert!(!rendered.contains("Failed to read contents"));
2516 };
2517
2518 let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
2519 mock_ctx.fs = fs.clone();
2520 let mock_error = mock_ctx
2521 .run_mock(
2522 &program,
2523 &MockConfig {
2524 use_prev_memory: false,
2525 ..Default::default()
2526 },
2527 )
2528 .await
2529 .unwrap_err();
2530 mock_ctx.close().await;
2531 assert_error(&mock_error);
2532
2533 let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
2534 concurrent_ctx.fs = fs;
2535 let mut exec_state = ExecState::new(&concurrent_ctx);
2536 let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
2537 concurrent_ctx.close().await;
2538 assert_error(&concurrent_error);
2539 }
2540
2541 #[tokio::test(flavor = "multi_thread")]
2542 async fn function_error_across_import_keeps_backtrace_innermost_first() {
2543 let project_dir = crate::TypedPath::new("/zma-kcl-import-fn-error");
2547 let main_path = project_dir.join("main.kcl");
2548 let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
2549 let files = [
2550 (
2551 project_dir.join("helper.kcl").to_string(),
2552 b"export fn inner() { return missingName }\nexport fn outer() { return inner() }\n".to_vec(),
2553 ),
2554 (
2555 project_dir.join("assembly.kcl").to_string(),
2556 b"import outer from \"helper.kcl\"\n\nexport assemblyValue = outer()\n".to_vec(),
2557 ),
2558 ]
2559 .into_iter()
2560 .collect();
2561 let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
2562 let settings = ExecutorSettings {
2563 project_directory: Some(project_dir.clone()),
2564 current_file: Some(main_path),
2565 ..Default::default()
2566 };
2567 let program = crate::Program::parse_no_errs(main_code).unwrap();
2568
2569 let assert_error = |error: &KclErrorWithOutputs| {
2570 assert!(
2571 matches!(&error.error, KclError::UndefinedValue { .. }),
2572 "expected UndefinedValue, got {:#?}",
2573 error.error
2574 );
2575 assert_eq!(
2576 error
2577 .error
2578 .backtrace()
2579 .iter()
2580 .map(|frame| frame.fn_name.as_deref())
2581 .collect::<Vec<_>>(),
2582 [Some("inner"), Some("outer"), Some("import assembly.kcl"), None]
2583 );
2584 assert_eq!(
2585 error
2586 .error
2587 .backtrace()
2588 .iter()
2589 .map(|frame| frame.kind)
2590 .collect::<Vec<_>>(),
2591 [
2592 kcl_error::BacktraceItemKind::Call,
2593 kcl_error::BacktraceItemKind::Call,
2594 kcl_error::BacktraceItemKind::Import,
2595 kcl_error::BacktraceItemKind::Call
2596 ]
2597 );
2598
2599 let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
2600 assert!(report.filename.ends_with("helper.kcl"));
2601 assert_eq!(
2602 report
2603 .related
2604 .iter()
2605 .map(|related| related.filename.as_str())
2606 .collect::<Vec<_>>(),
2607 [
2608 project_dir.join("assembly.kcl").to_string(),
2609 project_dir.join("main.kcl").to_string()
2610 ]
2611 );
2612 let rendered = format!("{:?}", miette::Report::new(report));
2613 assert!(rendered.contains("return missingName"));
2614 assert!(!rendered.contains("Failed to read contents"));
2615 };
2616
2617 let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
2618 mock_ctx.fs = fs.clone();
2619 let mock_error = mock_ctx
2620 .run_mock(
2621 &program,
2622 &MockConfig {
2623 use_prev_memory: false,
2624 ..Default::default()
2625 },
2626 )
2627 .await
2628 .unwrap_err();
2629 mock_ctx.close().await;
2630 assert_error(&mock_error);
2631
2632 let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
2633 concurrent_ctx.fs = fs;
2634 let mut exec_state = ExecState::new(&concurrent_ctx);
2635 let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
2636 concurrent_ctx.close().await;
2637 assert_error(&concurrent_error);
2638 }
2639
2640 #[track_caller]
2642 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2643 memory.memory.get_from_unchecked(name, env).unwrap()
2644 }
2645
2646 async fn execute_variables_with_backend(
2647 code: &str,
2648 backend: memory::MemoryBackendKind,
2649 ) -> IndexMap<String, KclValueView> {
2650 execute_outcome_with_backend(code, backend).await.variables
2651 }
2652
2653 async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
2654 let program = crate::Program::parse_no_errs(code).unwrap();
2655 let ctx = ExecutorContext::new_mock(None).await;
2656 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2657 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2658 let outcome = exec_state
2659 .into_exec_outcome(env_ref, &ctx)
2660 .await
2661 .expect("test execution outcome should collect variables");
2662 ctx.close().await;
2663 outcome
2664 }
2665
2666 async fn execute_error_variables_with_backend(
2667 code: &str,
2668 backend: memory::MemoryBackendKind,
2669 ) -> IndexMap<String, KclValueView> {
2670 let program = crate::Program::parse_no_errs(code).unwrap();
2671 let ctx = ExecutorContext::new_mock(None).await;
2672 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2673 let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
2674 ctx.close().await;
2675 error.variables
2676 }
2677
2678 async fn execute_project_variables_with_backend(
2679 main_code: &str,
2680 files: &[(&str, &str)],
2681 backend: memory::MemoryBackendKind,
2682 ) -> IndexMap<String, KclValueView> {
2683 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
2684 for (name, contents) in files {
2685 tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
2686 }
2687
2688 let program = crate::Program::parse_no_errs(main_code).unwrap();
2689 let ctx = ExecutorContext {
2690 engine: Arc::new(EngineManager::new_mock()),
2691 engine_batch: EngineBatchContext::default(),
2692 fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2693 settings: ExecutorSettings {
2694 project_directory: Some(crate::TypedPath(tmpdir.path().into())),
2695 ..Default::default()
2696 },
2697 context_type: ContextType::Mock,
2698 execution_callbacks: Default::default(),
2699 executor_kind: machine::ExecutorKind::resolve(),
2700 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
2701 };
2702 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2703 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2704 let outcome = exec_state
2705 .into_exec_outcome(env_ref, &ctx)
2706 .await
2707 .expect("test execution outcome should collect variables");
2708 ctx.close().await;
2709 outcome.variables
2710 }
2711
2712 async fn run_with_caching_variables_with_backend(
2713 code: &str,
2714 backend: memory::MemoryBackendKind,
2715 ) -> IndexMap<String, KclValueView> {
2716 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2717 cache::bust_cache().await;
2718 clear_mem_cache().await;
2719
2720 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
2721 let program = crate::Program::parse_no_errs(code).unwrap();
2722 ctx.run_with_caching(program.clone()).await.unwrap();
2723 let cached = ctx.run_with_caching(program).await.unwrap();
2724
2725 cache::bust_cache().await;
2726 clear_mem_cache().await;
2727 ctx.close().await;
2728 cached.variables
2729 }
2730
2731 async fn run_mock_variables_with_backend(
2732 code: &str,
2733 backend: memory::MemoryBackendKind,
2734 ) -> IndexMap<String, KclValueView> {
2735 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2736 clear_mem_cache().await;
2737
2738 let ctx = ExecutorContext::new_mock(None).await;
2739 let first = crate::Program::parse_no_errs("x = 2").unwrap();
2740 ctx.run_mock(
2741 &first,
2742 &MockConfig {
2743 use_prev_memory: false,
2744 ..Default::default()
2745 },
2746 )
2747 .await
2748 .unwrap();
2749
2750 let program = crate::Program::parse_no_errs(code).unwrap();
2751 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
2752
2753 clear_mem_cache().await;
2754 ctx.close().await;
2755 outcome.variables
2756 }
2757
2758 fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
2759 let mut keys = variables.keys().cloned().collect::<Vec<_>>();
2760 keys.sort();
2761 keys
2762 }
2763
2764 async fn collect_backend_results<T, Fut>(
2765 mut run: impl FnMut(memory::MemoryBackendKind) -> Fut,
2766 ) -> Vec<(memory::MemoryBackendKind, T)>
2767 where
2768 Fut: std::future::Future<Output = T>,
2769 {
2770 let all = memory::MemoryBackendKind::all();
2771 let mut results = Vec::with_capacity(all.len());
2772 for &kind in all {
2773 results.push((kind, run(kind).await));
2774 }
2775 results
2776 }
2777
2778 fn assert_backend_results_match<T>(results: &[(memory::MemoryBackendKind, T)])
2779 where
2780 T: std::fmt::Debug + PartialEq,
2781 {
2782 let (first, rest) = results.split_first().expect("expected at least one memory backend");
2783 let (first_kind, first_result) = first;
2784 for (kind, result) in rest {
2785 assert_eq!(
2786 result, first_result,
2787 "memory kind {kind:?} doesn't match {first_kind:?}"
2788 );
2789 }
2790 }
2791
2792 fn assert_backend_variable_results_match_expected_keys(
2793 results: &[(memory::MemoryBackendKind, IndexMap<String, KclValueView>)],
2794 expected_keys: &[&str],
2795 ) {
2796 let (first_kind, first_variables) = results.first().expect("expected at least one memory backend");
2797 let expected_keys = expected_keys.iter().map(|key| (*key).to_owned()).collect::<Vec<_>>();
2798 assert_eq!(
2799 sorted_variable_keys(first_variables),
2800 expected_keys,
2801 "memory kind {first_kind:?} doesn't match expected variables"
2802 );
2803 assert_backend_results_match(results);
2804 }
2805
2806 fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
2807 let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
2808 let KclValueView::Number { value, .. } = value else {
2809 panic!("expected `{key}` to be a number, got {value:?}");
2810 };
2811 assert_eq!(*value, expected, "{key}: {value:?}");
2812 }
2813
2814 #[tokio::test(flavor = "multi_thread")]
2815 async fn exec_outcome_variables_match_between_memory_backends() {
2816 let code = "x = 2\ny = x + 1\narr = [x, y]";
2817
2818 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2819
2820 assert_backend_variable_results_match_expected_keys(&results, &["arr", "x", "y"]);
2821 }
2822
2823 #[tokio::test(flavor = "multi_thread")]
2824 async fn error_output_variables_match_between_memory_backends() {
2825 let code = "x = 2\ny = missing + 1";
2826
2827 let results = collect_backend_results(|kind| execute_error_variables_with_backend(code, kind)).await;
2828
2829 assert_backend_variable_results_match_expected_keys(&results, &["x"]);
2830 }
2831
2832 #[tokio::test(flavor = "multi_thread")]
2833 async fn cached_execution_variables_match_between_memory_backends() {
2834 let code = "x = 2\ny = x + 1";
2835
2836 let results = collect_backend_results(|kind| run_with_caching_variables_with_backend(code, kind)).await;
2837
2838 assert_backend_variable_results_match_expected_keys(&results, &["x", "y"]);
2839 }
2840
2841 #[tokio::test(flavor = "multi_thread")]
2842 async fn mock_execution_variables_match_between_memory_backends() {
2843 let code = "y = x + 1";
2844
2845 let results = collect_backend_results(|kind| run_mock_variables_with_backend(code, kind)).await;
2846
2847 assert_backend_variable_results_match_expected_keys(&results, &["y"]);
2848 }
2849
2850 #[tokio::test(flavor = "multi_thread")]
2851 async fn module_imports_and_exported_closures_match_between_memory_backends() {
2852 let module_code = r#"
2853export base = 40
2854
2855export fn addBase(n) {
2856 return n + base
2857}
2858"#;
2859 let main_code = r#"
2860import base, addBase from 'math.kcl'
2861import 'math.kcl'
2862
2863named = addBase(n = 2)
2864qualified = math::addBase(n = 1)
2865direct = math::base
2866"#;
2867
2868 let files = [("math.kcl", module_code)];
2869 let results =
2870 collect_backend_results(|kind| execute_project_variables_with_backend(main_code, &files, kind)).await;
2871
2872 let (_, first_variables) = results.first().expect("expected at least one memory backend");
2873 assert_number_variable(first_variables, "named", 42.0);
2874 assert_number_variable(first_variables, "qualified", 41.0);
2875 assert_number_variable(first_variables, "direct", 40.0);
2876 assert_backend_results_match(&results);
2877 }
2878
2879 #[tokio::test(flavor = "multi_thread")]
2880 async fn sketch_block_variables_match_between_memory_backends() {
2881 let code = r#"
2882sketch001 = sketch(on = XY) {
2883 line1 = line(start = [0, 0], end = [1, 0])
2884 line2 = line(start = [1, 0], end = [0, 1])
2885}
2886lineCount = 2
2887"#;
2888
2889 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2890
2891 let (_, first_variables) = results.first().expect("expected at least one memory backend");
2892 assert!(first_variables.contains_key("sketch001"), "actual: {first_variables:?}");
2893 assert_number_variable(first_variables, "lineCount", 2.0);
2894 assert_backend_results_match(&results);
2895 }
2896
2897 #[tokio::test(flavor = "multi_thread")]
2898 async fn tag_call_stack_lookup_matches_between_memory_backends() {
2899 let code = r#"
2900sketch001 = startSketchOn(XY)
2901 |> startProfile(at = [0, 0])
2902 |> xLine(length = 10, tag = $seg01)
2903
2904segLength = segLen(seg01)
2905"#;
2906
2907 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2908
2909 let (_, first_variables) = results.first().expect("expected at least one memory backend");
2910 assert_number_variable(first_variables, "segLength", 10.0);
2911 assert_backend_results_match(&results);
2912 }
2913
2914 #[tokio::test(flavor = "multi_thread")]
2915 async fn sketch_transpiler_exec_outcome_variables_match_between_memory_backends() {
2916 let code = r#"
2917sketch001 = startSketchOn(XY)
2918 |> startProfile(at = [0, 0])
2919 |> line(end = [1, 0])
2920"#;
2921 let program = crate::Program::parse_no_errs(code).unwrap();
2922
2923 let outcomes = collect_backend_results(|kind| execute_outcome_with_backend(code, kind)).await;
2924 let mut transpiled = Vec::with_capacity(outcomes.len());
2925 for (kind, outcome) in &outcomes {
2926 let sketch = transpile_old_sketch_to_new(outcome, &program, "sketch001").unwrap();
2927 transpiled.push((*kind, sketch));
2928 }
2929
2930 assert_backend_results_match(&transpiled);
2931 }
2932
2933 #[tokio::test(flavor = "multi_thread")]
2934 async fn test_execute_warn() {
2935 let text = "@blah";
2936 let result = parse_execute(text).await.unwrap();
2937 let errs = result.exec_state.issues();
2938 assert_eq!(errs.len(), 1);
2939 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2940 assert!(
2941 errs[0].message.contains("Unknown annotation"),
2942 "unexpected warning message: {}",
2943 errs[0].message
2944 );
2945 }
2946
2947 #[tokio::test(flavor = "multi_thread")]
2948 async fn test_execute_fn_definitions() {
2949 let ast = r#"fn def(@x) {
2950 return x
2951}
2952fn ghi(@x) {
2953 return x
2954}
2955fn jkl(@x) {
2956 return x
2957}
2958fn hmm(@x) {
2959 return x
2960}
2961
2962yo = 5 + 6
2963
2964abc = 3
2965identifierGuy = 5
2966part001 = startSketchOn(XY)
2967|> startProfile(at = [-1.2, 4.83])
2968|> line(end = [2.8, 0])
2969|> angledLine(angle = 100 + 100, length = 3.01)
2970|> angledLine(angle = abc, length = 3.02)
2971|> angledLine(angle = def(yo), length = 3.03)
2972|> angledLine(angle = ghi(2), length = 3.04)
2973|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2974|> close()
2975yo2 = hmm([identifierGuy + 5])"#;
2976
2977 parse_execute(ast).await.unwrap();
2978 }
2979
2980 #[tokio::test(flavor = "multi_thread")]
2981 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2982 let code = r#"
2983firstProfile = sketch(on = XY) {
2984 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2985 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2986 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2987 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2988 coincident([edge1.end, edge2.start])
2989 coincident([edge2.end, edge3.start])
2990 coincident([edge3.end, edge4.start])
2991 coincident([edge4.end, edge1.start])
2992}
2993
2994secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2995 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2996 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2997 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2998 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2999 coincident([edge5.end, edge6.start])
3000 coincident([edge6.end, edge7.start])
3001 coincident([edge7.end, edge8.start])
3002 coincident([edge8.end, edge5.start])
3003}
3004
3005firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
3006secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
3007"#;
3008
3009 let result = parse_execute(code).await.unwrap();
3010 assert!(result.exec_state.issues().is_empty());
3011 }
3012
3013 #[tokio::test(flavor = "multi_thread")]
3014 async fn sketch_block_artifact_preserves_standard_plane_name() {
3015 let code = r#"
3016sketch001 = sketch(on = -YZ) {
3017 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
3018}
3019"#;
3020
3021 let result = parse_execute(code).await.unwrap();
3022 let sketch_blocks = result
3023 .exec_state
3024 .global
3025 .artifacts
3026 .graph
3027 .values()
3028 .filter_map(|artifact| match artifact {
3029 Artifact::SketchBlock(block) => Some(block),
3030 _ => None,
3031 })
3032 .collect::<Vec<_>>();
3033
3034 assert_eq!(sketch_blocks.len(), 1);
3035 assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
3036 }
3037
3038 #[tokio::test(flavor = "multi_thread")]
3039 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
3040 let code = r#"
3041sketch001 = sketch(on = YZ) {
3042 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
3043 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
3044 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
3045 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
3046 coincident([line1.end, line2.start])
3047 coincident([line2.end, line3.start])
3048 coincident([line3.end, line4.start])
3049 coincident([line4.end, line1.start])
3050}
3051
3052sketch002 = sketch(on = -XZ) {
3053 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
3054 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
3055 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
3056 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
3057 coincident([line5.end, line6.start])
3058 coincident([line6.end, line7.start])
3059 coincident([line7.end, line8.start])
3060 coincident([line8.end, line5.start])
3061}
3062
3063region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
3064extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
3065region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
3066extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
3067
3068myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
3069"#;
3070
3071 let result = parse_execute(code).await.unwrap();
3072 assert!(result.exec_state.issues().is_empty());
3073 }
3074
3075 #[tokio::test(flavor = "multi_thread")]
3076 async fn issue_10741_point_circle_coincident_executes() {
3077 let code = r#"
3078sketch001 = sketch(on = YZ) {
3079 circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
3080 line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
3081 coincident([line1.start, circle1])
3082}
3083"#;
3084
3085 let result = parse_execute(code).await.unwrap();
3086 assert!(
3087 result
3088 .exec_state
3089 .issues()
3090 .iter()
3091 .all(|issue| issue.severity != Severity::Error),
3092 "unexpected execution issues: {:#?}",
3093 result.exec_state.issues()
3094 );
3095 }
3096
3097 #[tokio::test(flavor = "multi_thread")]
3098 async fn test_execute_with_pipe_substitutions_unary() {
3099 let ast = r#"myVar = 3
3100part001 = startSketchOn(XY)
3101 |> startProfile(at = [0, 0])
3102 |> line(end = [3, 4], tag = $seg01)
3103 |> line(end = [
3104 min([segLen(seg01), myVar]),
3105 -legLen(hypotenuse = segLen(seg01), leg = myVar)
3106])
3107"#;
3108
3109 parse_execute(ast).await.unwrap();
3110 }
3111
3112 #[tokio::test(flavor = "multi_thread")]
3113 async fn test_execute_with_pipe_substitutions() {
3114 let ast = r#"myVar = 3
3115part001 = startSketchOn(XY)
3116 |> startProfile(at = [0, 0])
3117 |> line(end = [3, 4], tag = $seg01)
3118 |> line(end = [
3119 min([segLen(seg01), myVar]),
3120 legLen(hypotenuse = segLen(seg01), leg = myVar)
3121])
3122"#;
3123
3124 parse_execute(ast).await.unwrap();
3125 }
3126
3127 #[tokio::test(flavor = "multi_thread")]
3128 async fn test_execute_with_inline_comment() {
3129 let ast = r#"baseThick = 1
3130armAngle = 60
3131
3132baseThickHalf = baseThick / 2
3133halfArmAngle = armAngle / 2
3134
3135arrExpShouldNotBeIncluded = [1, 2, 3]
3136objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
3137
3138part001 = startSketchOn(XY)
3139 |> startProfile(at = [0, 0])
3140 |> yLine(endAbsolute = 1)
3141 |> xLine(length = 3.84) // selection-range-7ish-before-this
3142
3143variableBelowShouldNotBeIncluded = 3
3144"#;
3145
3146 parse_execute(ast).await.unwrap();
3147 }
3148
3149 #[tokio::test(flavor = "multi_thread")]
3150 async fn test_execute_with_function_literal_in_pipe() {
3151 let ast = r#"w = 20
3152l = 8
3153h = 10
3154
3155fn thing() {
3156 return -8
3157}
3158
3159firstExtrude = startSketchOn(XY)
3160 |> startProfile(at = [0,0])
3161 |> line(end = [0, l])
3162 |> line(end = [w, 0])
3163 |> line(end = [0, thing()])
3164 |> close()
3165 |> extrude(length = h)"#;
3166
3167 parse_execute(ast).await.unwrap();
3168 }
3169
3170 #[tokio::test(flavor = "multi_thread")]
3171 async fn test_execute_with_function_unary_in_pipe() {
3172 let ast = r#"w = 20
3173l = 8
3174h = 10
3175
3176fn thing(@x) {
3177 return -x
3178}
3179
3180firstExtrude = startSketchOn(XY)
3181 |> startProfile(at = [0,0])
3182 |> line(end = [0, l])
3183 |> line(end = [w, 0])
3184 |> line(end = [0, thing(8)])
3185 |> close()
3186 |> extrude(length = h)"#;
3187
3188 parse_execute(ast).await.unwrap();
3189 }
3190
3191 #[tokio::test(flavor = "multi_thread")]
3192 async fn test_execute_with_function_array_in_pipe() {
3193 let ast = r#"w = 20
3194l = 8
3195h = 10
3196
3197fn thing(@x) {
3198 return [0, -x]
3199}
3200
3201firstExtrude = startSketchOn(XY)
3202 |> startProfile(at = [0,0])
3203 |> line(end = [0, l])
3204 |> line(end = [w, 0])
3205 |> line(end = thing(8))
3206 |> close()
3207 |> extrude(length = h)"#;
3208
3209 parse_execute(ast).await.unwrap();
3210 }
3211
3212 #[tokio::test(flavor = "multi_thread")]
3213 async fn test_execute_with_function_call_in_pipe() {
3214 let ast = r#"w = 20
3215l = 8
3216h = 10
3217
3218fn other_thing(@y) {
3219 return -y
3220}
3221
3222fn thing(@x) {
3223 return other_thing(x)
3224}
3225
3226firstExtrude = startSketchOn(XY)
3227 |> startProfile(at = [0,0])
3228 |> line(end = [0, l])
3229 |> line(end = [w, 0])
3230 |> line(end = [0, thing(8)])
3231 |> close()
3232 |> extrude(length = h)"#;
3233
3234 parse_execute(ast).await.unwrap();
3235 }
3236
3237 #[tokio::test(flavor = "multi_thread")]
3238 async fn test_execute_with_function_sketch() {
3239 let ast = r#"fn box(h, l, w) {
3240 myBox = startSketchOn(XY)
3241 |> startProfile(at = [0,0])
3242 |> line(end = [0, l])
3243 |> line(end = [w, 0])
3244 |> line(end = [0, -l])
3245 |> close()
3246 |> extrude(length = h)
3247
3248 return myBox
3249}
3250
3251fnBox = box(h = 3, l = 6, w = 10)"#;
3252
3253 parse_execute(ast).await.unwrap();
3254 }
3255
3256 #[tokio::test(flavor = "multi_thread")]
3257 async fn test_get_member_of_object_with_function_period() {
3258 let ast = r#"fn box(@obj) {
3259 myBox = startSketchOn(XY)
3260 |> startProfile(at = obj.start)
3261 |> line(end = [0, obj.l])
3262 |> line(end = [obj.w, 0])
3263 |> line(end = [0, -obj.l])
3264 |> close()
3265 |> extrude(length = obj.h)
3266
3267 return myBox
3268}
3269
3270thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
3271"#;
3272 parse_execute(ast).await.unwrap();
3273 }
3274
3275 #[tokio::test(flavor = "multi_thread")]
3276 #[ignore] async fn test_object_member_starting_pipeline() {
3278 let ast = r#"
3279fn test2() {
3280 return {
3281 thing: startSketchOn(XY)
3282 |> startProfile(at = [0, 0])
3283 |> line(end = [0, 1])
3284 |> line(end = [1, 0])
3285 |> line(end = [0, -1])
3286 |> close()
3287 }
3288}
3289
3290x2 = test2()
3291
3292x2.thing
3293 |> extrude(length = 10)
3294"#;
3295 parse_execute(ast).await.unwrap();
3296 }
3297
3298 #[tokio::test(flavor = "multi_thread")]
3299 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
3301 let ast = r#"fn box(obj) {
3302let myBox = startSketchOn(XY)
3303 |> startProfile(at = obj.start)
3304 |> line(end = [0, obj.l])
3305 |> line(end = [obj.w, 0])
3306 |> line(end = [0, -obj.l])
3307 |> close()
3308 |> extrude(length = obj.h)
3309
3310 return myBox
3311}
3312
3313for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
3314 thisBox = box(var)
3315}"#;
3316
3317 parse_execute(ast).await.unwrap();
3318 }
3319
3320 #[tokio::test(flavor = "multi_thread")]
3321 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
3323 let ast = r#"fn box(h, l, w, start) {
3324 myBox = startSketchOn(XY)
3325 |> startProfile(at = [0,0])
3326 |> line(end = [0, l])
3327 |> line(end = [w, 0])
3328 |> line(end = [0, -l])
3329 |> close()
3330 |> extrude(length = h)
3331
3332 return myBox
3333}
3334
3335
3336for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
3337 const thisBox = box(var[0], var[1], var[2], var[3])
3338}"#;
3339
3340 parse_execute(ast).await.unwrap();
3341 }
3342
3343 #[tokio::test(flavor = "multi_thread")]
3344 async fn test_get_member_of_array_with_function() {
3345 let ast = r#"fn box(@arr) {
3346 myBox =startSketchOn(XY)
3347 |> startProfile(at = arr[0])
3348 |> line(end = [0, arr[1]])
3349 |> line(end = [arr[2], 0])
3350 |> line(end = [0, -arr[1]])
3351 |> close()
3352 |> extrude(length = arr[3])
3353
3354 return myBox
3355}
3356
3357thisBox = box([[0,0], 6, 10, 3])
3358
3359"#;
3360 parse_execute(ast).await.unwrap();
3361 }
3362
3363 #[tokio::test(flavor = "multi_thread")]
3364 async fn test_function_cannot_access_future_definitions() {
3365 let ast = r#"
3366fn returnX() {
3367 // x shouldn't be defined yet.
3368 return x
3369}
3370
3371x = 5
3372
3373answer = returnX()"#;
3374
3375 let result = parse_execute(ast).await;
3376 let err = result.unwrap_err();
3377 assert_eq!(err.message(), "`x` is not defined");
3378 }
3379
3380 #[tokio::test(flavor = "multi_thread")]
3381 async fn test_override_prelude() {
3382 let text = "PI = 3.0";
3383 let result = parse_execute(text).await.unwrap();
3384 let issues = result.exec_state.issues();
3385 assert!(issues.is_empty(), "issues={issues:#?}");
3386 }
3387
3388 #[tokio::test(flavor = "multi_thread")]
3389 async fn type_aliases() {
3390 let text = r#"@settings(experimentalFeatures = allow)
3391type MyTy = [number; 2]
3392fn foo(@x: MyTy) {
3393 return x[0]
3394}
3395
3396foo([0, 1])
3397
3398type Other = MyTy | Helix
3399"#;
3400 let result = parse_execute(text).await.unwrap();
3401 let issues = result.exec_state.issues();
3402 assert!(issues.is_empty(), "issues={issues:#?}");
3403 }
3404
3405 #[tokio::test(flavor = "multi_thread")]
3406 async fn test_cannot_shebang_in_fn() {
3407 let ast = r#"
3408fn foo() {
3409 #!hello
3410 return true
3411}
3412
3413foo
3414"#;
3415
3416 let result = parse_execute(ast).await;
3417 let err = result.unwrap_err();
3418 assert_eq!(
3419 err,
3420 KclError::new_syntax(KclErrorDetails::new(
3421 "Unexpected token: #".to_owned(),
3422 vec![SourceRange::new(14, 15, ModuleId::default())],
3423 )),
3424 );
3425 }
3426
3427 #[tokio::test(flavor = "multi_thread")]
3428 async fn test_pattern_transform_function_cannot_access_future_definitions() {
3429 let ast = r#"
3430fn transform(@replicaId) {
3431 // x shouldn't be defined yet.
3432 scale = x
3433 return {
3434 translate = [0, 0, replicaId * 10],
3435 scale = [scale, 1, 0],
3436 }
3437}
3438
3439fn layer() {
3440 return startSketchOn(XY)
3441 |> circle( center= [0, 0], radius= 1, tag = $tag1)
3442 |> extrude(length = 10)
3443}
3444
3445x = 5
3446
3447// The 10 layers are replicas of each other, with a transform applied to each.
3448shape = layer() |> patternTransform(instances = 10, transform = transform)
3449"#;
3450
3451 let result = parse_execute(ast).await;
3452 let err = result.unwrap_err();
3453 assert_eq!(err.message(), "`x` is not defined",);
3454 }
3455
3456 #[tokio::test(flavor = "multi_thread")]
3459 async fn test_math_execute_with_functions() {
3460 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
3461 let result = parse_execute(ast).await.unwrap();
3462 assert_eq!(
3463 5.0,
3464 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3465 .as_f64()
3466 .unwrap()
3467 );
3468 }
3469
3470 #[tokio::test(flavor = "multi_thread")]
3471 async fn test_math_execute() {
3472 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
3473 let result = parse_execute(ast).await.unwrap();
3474 assert_eq!(
3475 7.4,
3476 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3477 .as_f64()
3478 .unwrap()
3479 );
3480 }
3481
3482 #[tokio::test(flavor = "multi_thread")]
3483 async fn test_string_uppercase() {
3484 let composed = "\u{e9}";
3485 let uppercase_composed = "\u{c9}";
3486 let decomposed = "e\u{301}";
3487 let uppercase_decomposed = "E\u{301}";
3488 let code = format!(
3489 r#"
3490ascii = string::uppercase("Kcl")
3491unicode_expansion = string::uppercase("Straße")
3492uncased = string::uppercase("東京")
3493empty = string::uppercase("")
3494composed = string::uppercase("{composed}")
3495decomposed = string::uppercase("{decomposed}")
3496piped = "ready" |> string::uppercase()
3497"#
3498 );
3499
3500 let result = parse_execute(&code).await.unwrap();
3501 for (name, expected) in [
3502 ("ascii", "KCL"),
3503 ("unicode_expansion", "STRASSE"),
3504 ("uncased", "東京"),
3505 ("empty", ""),
3506 ("composed", uppercase_composed),
3507 ("decomposed", uppercase_decomposed),
3508 ("piped", "READY"),
3509 ] {
3510 assert_eq!(
3511 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3512 .as_str()
3513 .unwrap(),
3514 expected,
3515 "{name}"
3516 );
3517 }
3518 }
3519
3520 #[tokio::test(flavor = "multi_thread")]
3521 async fn test_string_lowercase() {
3522 let composed = "\u{c9}";
3523 let lowercase_composed = "\u{e9}";
3524 let decomposed = "E\u{301}";
3525 let lowercase_decomposed = "e\u{301}";
3526 let expanded = "i\u{307}";
3527 let code = format!(
3528 r#"
3529ascii = string::lowercase("KCL")
3530final_sigma = string::lowercase("ΟΣ")
3531medial_sigma = string::lowercase("ΟΣΑ")
3532unicode_expansion = string::lowercase("İ")
3533uncased = string::lowercase("東京")
3534empty = string::lowercase("")
3535composed = string::lowercase("{composed}")
3536decomposed = string::lowercase("{decomposed}")
3537piped = "READY" |> string::lowercase()
3538"#
3539 );
3540
3541 let result = parse_execute(&code).await.unwrap();
3542 for (name, expected) in [
3543 ("ascii", "kcl"),
3544 ("final_sigma", "ος"),
3545 ("medial_sigma", "οσα"),
3546 ("unicode_expansion", expanded),
3547 ("uncased", "東京"),
3548 ("empty", ""),
3549 ("composed", lowercase_composed),
3550 ("decomposed", lowercase_decomposed),
3551 ("piped", "ready"),
3552 ] {
3553 assert_eq!(
3554 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3555 .as_str()
3556 .unwrap(),
3557 expected,
3558 "{name}"
3559 );
3560 }
3561 }
3562
3563 #[tokio::test(flavor = "multi_thread")]
3564 async fn test_string_is_equal() {
3565 let composed = "\u{e9}";
3566 let decomposed = "e\u{301}";
3567 let code = format!(
3568 r#"
3569exact_same = string::isEqual("KCL", to = "KCL")
3570exact_different_case = string::isEqual("KCL", to = "kcl")
3571explicit_case_sensitive = string::isEqual("KCL", to = "kcl", caseInsensitive = false)
3572case_insensitive_ascii = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3573case_fold_expansion = string::isEqual("Straße", to = "STRASSE", caseInsensitive = true)
3574case_fold_expansion_reversed = string::isEqual("STRASSE", to = "Straße", caseInsensitive = true)
3575case_fold_sigma = string::isEqual("ος", to = "οσ", caseInsensitive = true)
3576case_fold_non_turkic = string::isEqual("I", to = "i", caseInsensitive = true)
3577case_fold_not_turkic = string::isEqual("I", to = "ı", caseInsensitive = true)
3578empty_same = string::isEqual("", to = "")
3579empty_different = string::isEqual("", to = "KCL")
3580exact_without_normalization = string::isEqual("{composed}", to = "{decomposed}")
3581case_fold_without_normalization = string::isEqual("{composed}", to = "{decomposed}", caseInsensitive = true)
3582piped = "ready" |> string::isEqual(to = "READY", caseInsensitive = true)
3583"#
3584 );
3585
3586 let result = parse_execute(&code).await.unwrap();
3587 for (name, expected) in [
3588 ("exact_same", true),
3589 ("exact_different_case", false),
3590 ("explicit_case_sensitive", false),
3591 ("case_insensitive_ascii", true),
3592 ("case_fold_expansion", true),
3593 ("case_fold_expansion_reversed", true),
3594 ("case_fold_sigma", true),
3595 ("case_fold_non_turkic", true),
3596 ("case_fold_not_turkic", false),
3597 ("empty_same", true),
3598 ("empty_different", false),
3599 ("exact_without_normalization", false),
3600 ("case_fold_without_normalization", false),
3601 ("piped", true),
3602 ] {
3603 assert_eq!(
3604 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3605 .as_bool()
3606 .unwrap(),
3607 expected,
3608 "{name}"
3609 );
3610 }
3611 }
3612
3613 #[tokio::test(flavor = "multi_thread")]
3614 async fn test_string_is_equal_inside_sketch_block_is_predicate() {
3615 let code = r#"
3616@settings(experimentalFeatures = allow)
3617
3618sketch(on = XY) {
3619 stringsAreEqual = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3620}
3621"#;
3622
3623 parse_execute(code).await.unwrap();
3624 }
3625
3626 #[tokio::test(flavor = "multi_thread")]
3627 async fn test_string_trim() {
3628 let ascii_whitespace = " \t\n";
3629 let tab = "\t";
3630 let non_breaking_space = "\u{a0}";
3631 let em_space = "\u{2003}";
3632 let ideographic_space = "\u{3000}";
3633 let zero_width_space = "\u{200b}";
3634 let decomposed = "e\u{301}";
3635 let code = format!(
3636 r#"
3637ascii = string::trim("{ascii_whitespace}KCL{ascii_whitespace}")
3638internal = string::trim(" KCL{tab}strings ")
3639unicode = string::trim("{non_breaking_space}{em_space}KCL{ideographic_space}")
3640all_whitespace = string::trim("{ascii_whitespace}{non_breaking_space}")
3641empty = string::trim("")
3642unchanged = string::trim("KCL")
3643without_normalization = string::trim(" {decomposed} ")
3644non_whitespace = string::trim("{zero_width_space}KCL{zero_width_space}")
3645piped = " ready " |> string::trim()
3646"#
3647 );
3648
3649 let result = parse_execute(&code).await.unwrap();
3650 let non_whitespace = format!("{zero_width_space}KCL{zero_width_space}");
3651 for (name, expected) in [
3652 ("ascii", "KCL"),
3653 ("internal", "KCL\tstrings"),
3654 ("unicode", "KCL"),
3655 ("all_whitespace", ""),
3656 ("empty", ""),
3657 ("unchanged", "KCL"),
3658 ("without_normalization", decomposed),
3659 ("non_whitespace", non_whitespace.as_str()),
3660 ("piped", "ready"),
3661 ] {
3662 assert_eq!(
3663 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3664 .as_str()
3665 .unwrap(),
3666 expected,
3667 "{name}"
3668 );
3669 }
3670 }
3671
3672 #[tokio::test(flavor = "multi_thread")]
3673 async fn test_string_trim_start() {
3674 let ascii_whitespace = " \t\n";
3675 let tab = "\t";
3676 let non_breaking_space = "\u{a0}";
3677 let em_space = "\u{2003}";
3678 let ideographic_space = "\u{3000}";
3679 let zero_width_space = "\u{200b}";
3680 let decomposed = "e\u{301}";
3681 let code = format!(
3682 r#"
3683ascii = string::trimStart("{ascii_whitespace}KCL{ascii_whitespace}")
3684internal = string::trimStart(" KCL{tab}strings")
3685unicode = string::trimStart("{non_breaking_space}{em_space}KCL{ideographic_space}")
3686all_whitespace = string::trimStart("{ascii_whitespace}{non_breaking_space}")
3687empty = string::trimStart("")
3688unchanged = string::trimStart("KCL")
3689without_normalization = string::trimStart(" {decomposed}")
3690non_whitespace_prefix = string::trimStart("{zero_width_space}{ascii_whitespace}KCL")
3691piped = " ready " |> string::trimStart()
3692"#
3693 );
3694
3695 let result = parse_execute(&code).await.unwrap();
3696 let ascii = format!("KCL{ascii_whitespace}");
3697 let unicode = format!("KCL{ideographic_space}");
3698 let non_whitespace_prefix = format!("{zero_width_space}{ascii_whitespace}KCL");
3699 for (name, expected) in [
3700 ("ascii", ascii.as_str()),
3701 ("internal", "KCL\tstrings"),
3702 ("unicode", unicode.as_str()),
3703 ("all_whitespace", ""),
3704 ("empty", ""),
3705 ("unchanged", "KCL"),
3706 ("without_normalization", decomposed),
3707 ("non_whitespace_prefix", non_whitespace_prefix.as_str()),
3708 ("piped", "ready "),
3709 ] {
3710 assert_eq!(
3711 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3712 .as_str()
3713 .unwrap(),
3714 expected,
3715 "{name}"
3716 );
3717 }
3718 }
3719
3720 #[tokio::test(flavor = "multi_thread")]
3721 async fn test_string_trim_end() {
3722 let ascii_whitespace = " \t\n";
3723 let tab = "\t";
3724 let non_breaking_space = "\u{a0}";
3725 let em_space = "\u{2003}";
3726 let ideographic_space = "\u{3000}";
3727 let zero_width_space = "\u{200b}";
3728 let decomposed = "e\u{301}";
3729 let code = format!(
3730 r#"
3731ascii = string::trimEnd("{ascii_whitespace}KCL{ascii_whitespace}")
3732internal = string::trimEnd("KCL{tab}strings ")
3733unicode = string::trimEnd("{non_breaking_space}KCL{em_space}{ideographic_space}")
3734all_whitespace = string::trimEnd("{ascii_whitespace}{non_breaking_space}")
3735empty = string::trimEnd("")
3736unchanged = string::trimEnd("KCL")
3737without_normalization = string::trimEnd("{decomposed} ")
3738non_whitespace_suffix = string::trimEnd("KCL{ascii_whitespace}{zero_width_space}")
3739piped = " ready " |> string::trimEnd()
3740"#
3741 );
3742
3743 let result = parse_execute(&code).await.unwrap();
3744 let ascii = format!("{ascii_whitespace}KCL");
3745 let unicode = format!("{non_breaking_space}KCL");
3746 let non_whitespace_suffix = format!("KCL{ascii_whitespace}{zero_width_space}");
3747 for (name, expected) in [
3748 ("ascii", ascii.as_str()),
3749 ("internal", "KCL\tstrings"),
3750 ("unicode", unicode.as_str()),
3751 ("all_whitespace", ""),
3752 ("empty", ""),
3753 ("unchanged", "KCL"),
3754 ("without_normalization", decomposed),
3755 ("non_whitespace_suffix", non_whitespace_suffix.as_str()),
3756 ("piped", " ready"),
3757 ] {
3758 assert_eq!(
3759 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3760 .as_str()
3761 .unwrap(),
3762 expected,
3763 "{name}"
3764 );
3765 }
3766 }
3767
3768 #[tokio::test(flavor = "multi_thread")]
3769 async fn test_string_to_string() {
3770 for (name, expr, expected) in [
3773 ("unitless integer", "12", "12"),
3776 ("unitless fractional", "1.5", "1.5"),
3777 ("no digits dropped", "0.1 + 0.2", "0.30000000000000004"),
3778 ("unitless negative", "-7", "-7"),
3779 ("unitless zero", "0", "0"),
3780 ("negative zero", "-0", "0"),
3781 ("count", "3_", "3_"),
3782 ("millimeters", "12mm", "12mm"),
3783 ("centimeters", "12cm", "12cm"),
3784 ("meters", "12m", "12m"),
3785 ("inches", "1.5in", "1.5in"),
3786 ("feet", "2ft", "2ft"),
3787 ("yards", "3yd", "3yd"),
3788 ("degrees", "90deg", "90deg"),
3789 ("radians", "1.5rad", "1.5rad"),
3790 ("length arithmetic", "2mm + 10mm", "12mm"),
3792 ("units the type system loses", "2mm * 10mm", "20"),
3795 ("unitless arithmetic", "1 + 2", "3"),
3796 ] {
3797 let code = format!("actual = string::toString({expr})");
3798 let result = parse_execute(&code).await.unwrap();
3799
3800 assert_eq!(
3801 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3802 .as_str()
3803 .unwrap(),
3804 expected,
3805 "case: {name}"
3806 );
3807 }
3808 }
3809
3810 #[tokio::test(flavor = "multi_thread")]
3811 async fn test_string_to_string_ignores_the_files_default_unit() {
3812 let code = "@settings(defaultLengthUnit = inch)\nactual = string::toString(12)";
3817 let result = parse_execute(code).await.unwrap();
3818
3819 assert_eq!(
3820 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3821 .as_str()
3822 .unwrap(),
3823 "12"
3824 );
3825 }
3826
3827 #[tokio::test(flavor = "multi_thread")]
3828 async fn test_string_to_string_rejects_a_non_number() {
3829 let error = parse_execute(r#"actual = string::toString("already text")"#)
3830 .await
3831 .unwrap_err();
3832
3833 assert_eq!(
3836 error.message(),
3837 "The input argument of `string::toString` requires a value with type `number`, but found a value with type `string`."
3838 );
3839 assert!(
3840 matches!(error, KclError::Argument { .. }),
3841 "expected an Argument error, found {error:?}"
3842 );
3843 }
3844
3845 #[tokio::test(flavor = "multi_thread")]
3846 async fn test_string_to_string_accepts_a_piped_argument() {
3847 let result = parse_execute("actual = 12mm |> string::toString()").await.unwrap();
3848
3849 assert_eq!(
3850 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3851 .as_str()
3852 .unwrap(),
3853 "12mm"
3854 );
3855 }
3856
3857 #[tokio::test(flavor = "multi_thread")]
3858 async fn test_string_to_string_echoes_how_the_literal_was_written() {
3859 for literal in [
3863 "12",
3864 "1.5",
3865 "0.30000000000000004",
3866 "3_",
3867 "2.5_",
3870 "-4_",
3871 "12mm",
3872 "-5mm",
3873 "1.5in",
3874 "90deg",
3875 "1.5rad",
3876 ] {
3877 let code = format!("actual = string::toString({literal})");
3878 let result = parse_execute(&code).await.unwrap();
3879
3880 assert_eq!(
3881 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3882 .as_str()
3883 .unwrap(),
3884 literal,
3885 "literal: {literal}"
3886 );
3887 }
3888 }
3889
3890 #[tokio::test(flavor = "multi_thread")]
3891 async fn test_string_to_string_spells_out_non_finite_numbers() {
3892 for (name, expr, expected) in [
3896 ("positive infinity", "1 / 0", "Infinity"),
3897 ("negative infinity", "-1 / 0", "-Infinity"),
3898 ("nan", "0 / 0", "NaN"),
3899 ("infinity from a length", "1mm / 0", "Infinity"),
3901 ("nan from a length", "0mm / 0", "NaN"),
3902 ("infinity from an angle", "1deg / 0", "Infinity"),
3903 ] {
3904 let code = format!("actual = string::toString({expr})");
3905 let result = parse_execute(&code).await.unwrap();
3906
3907 assert_eq!(
3908 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3909 .as_str()
3910 .unwrap(),
3911 expected,
3912 "case: {name}"
3913 );
3914 }
3915 }
3916
3917 #[tokio::test(flavor = "multi_thread")]
3918 async fn test_string_equality_operators() {
3919 let composed = "\u{e9}";
3920 let decomposed = "e\u{301}";
3921 let code = format!(
3922 r#"
3923equal_same_ascii = "KCL" == "KCL"
3924equal_different_case = "KCL" == "kcl"
3925not_equal_same_ascii = "KCL" != "KCL"
3926not_equal_different_case = "KCL" != "kcl"
3927equal_same_unicode = "{composed}" == "{composed}"
3928not_equal_same_unicode = "{composed}" != "{composed}"
3929equal_without_normalization = "{composed}" == "{decomposed}"
3930not_equal_without_normalization = "{composed}" != "{decomposed}"
3931"#
3932 );
3933
3934 let result = parse_execute(&code).await.unwrap();
3935 for (name, expected) in [
3936 ("equal_same_ascii", true),
3937 ("equal_different_case", false),
3938 ("not_equal_same_ascii", false),
3939 ("not_equal_different_case", true),
3940 ("equal_same_unicode", true),
3941 ("not_equal_same_unicode", false),
3942 ("equal_without_normalization", false),
3943 ("not_equal_without_normalization", true),
3944 ] {
3945 assert_eq!(
3946 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3947 .as_bool()
3948 .unwrap(),
3949 expected,
3950 "{name}"
3951 );
3952 }
3953 }
3954
3955 #[tokio::test(flavor = "multi_thread")]
3956 async fn test_string_equality_inside_sketch_block_fails_like_number_equality() {
3957 let string_code = r#"
3958@settings(experimentalFeatures = allow)
3959
3960sketch(on = XY) {
3961 stringsAreEqual = "KCL" == "KCL"
3962}
3963"#;
3964 let number_code = r#"
3965@settings(experimentalFeatures = allow)
3966
3967sketch(on = XY) {
3968 numbersAreEqual = 1 == 1
3969}
3970"#;
3971
3972 assert_eq!(
3973 parse_execute(string_code).await.unwrap_err().message(),
3974 "Cannot create an equivalence constraint between values of these types: a string and a string"
3975 );
3976 assert_eq!(
3977 parse_execute(number_code).await.unwrap_err().message(),
3978 "Cannot create an equivalence constraint between values of these types: a number and a number"
3979 );
3980 }
3981
3982 #[tokio::test(flavor = "multi_thread")]
3983 async fn test_math_execute_start_negative() {
3984 let ast = r#"myVar = -5 + 6"#;
3985 let result = parse_execute(ast).await.unwrap();
3986 assert_eq!(
3987 1.0,
3988 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3989 .as_f64()
3990 .unwrap()
3991 );
3992 }
3993
3994 #[tokio::test(flavor = "multi_thread")]
3995 async fn test_math_execute_with_pi() {
3996 let ast = r#"myVar = PI * 2"#;
3997 let result = parse_execute(ast).await.unwrap();
3998 assert_eq!(
3999 std::f64::consts::TAU,
4000 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
4001 .as_f64()
4002 .unwrap()
4003 );
4004 }
4005
4006 #[tokio::test(flavor = "multi_thread")]
4007 async fn test_math_define_decimal_without_leading_zero() {
4008 let ast = r#"thing = .4 + 7"#;
4009 let result = parse_execute(ast).await.unwrap();
4010 assert_eq!(
4011 7.4,
4012 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
4013 .as_f64()
4014 .unwrap()
4015 );
4016 }
4017
4018 #[tokio::test(flavor = "multi_thread")]
4019 async fn pass_std_to_std() {
4020 let ast = r#"sketch001 = startSketchOn(XY)
4021profile001 = circle(sketch001, center = [0, 0], radius = 2)
4022extrude001 = extrude(profile001, length = 5)
4023extrudes = patternLinear3d(
4024 extrude001,
4025 instances = 3,
4026 distance = 5,
4027 axis = [1, 1, 0],
4028)
4029clone001 = map(extrudes, f = clone)
4030"#;
4031 parse_execute(ast).await.unwrap();
4032 }
4033
4034 #[tokio::test(flavor = "multi_thread")]
4035 async fn test_array_reduce_nested_array() {
4036 let code = r#"
4037fn id(@el, accum) { return accum }
4038
4039answer = reduce([], initial=[[[0,0]]], f=id)
4040"#;
4041 let result = parse_execute(code).await.unwrap();
4042 assert_eq!(
4043 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
4044 KclValue::HomArray {
4045 value: vec![KclValue::HomArray {
4046 value: vec![KclValue::HomArray {
4047 value: vec![
4048 KclValue::Number {
4049 value: 0.0,
4050 ty: NumericType::default(),
4051 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
4052 },
4053 KclValue::Number {
4054 value: 0.0,
4055 ty: NumericType::default(),
4056 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
4057 }
4058 ],
4059 ty: RuntimeType::any(),
4060 }],
4061 ty: RuntimeType::any(),
4062 }],
4063 ty: RuntimeType::any(),
4064 }
4065 );
4066 }
4067
4068 #[tokio::test(flavor = "multi_thread")]
4069 async fn test_zero_param_fn() {
4070 let ast = r#"sigmaAllow = 35000 // psi
4071leg1 = 5 // inches
4072leg2 = 8 // inches
4073fn thickness() { return 0.56 }
4074
4075bracket = startSketchOn(XY)
4076 |> startProfile(at = [0,0])
4077 |> line(end = [0, leg1])
4078 |> line(end = [leg2, 0])
4079 |> line(end = [0, -thickness()])
4080 |> line(end = [-leg2 + thickness(), 0])
4081"#;
4082 parse_execute(ast).await.unwrap();
4083 }
4084
4085 #[tokio::test(flavor = "multi_thread")]
4086 async fn test_unary_operator_not_succeeds() {
4087 let ast = r#"
4088fn returnTrue() { return !false }
4089t = true
4090f = false
4091notTrue = !t
4092notFalse = !f
4093c = !!true
4094d = !returnTrue()
4095
4096assertIs(!false, error = "expected to pass")
4097
4098fn check(x) {
4099 assertIs(!x, error = "expected argument to be false")
4100 return true
4101}
4102check(x = false)
4103"#;
4104 let result = parse_execute(ast).await.unwrap();
4105 assert_eq!(
4106 false,
4107 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
4108 .as_bool()
4109 .unwrap()
4110 );
4111 assert_eq!(
4112 true,
4113 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
4114 .as_bool()
4115 .unwrap()
4116 );
4117 assert_eq!(
4118 true,
4119 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
4120 .as_bool()
4121 .unwrap()
4122 );
4123 assert_eq!(
4124 false,
4125 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
4126 .as_bool()
4127 .unwrap()
4128 );
4129 }
4130
4131 #[tokio::test(flavor = "multi_thread")]
4132 async fn test_unary_operator_not_on_non_bool_fails() {
4133 let code1 = r#"
4134// Yup, this is null.
4135myNull = 0 / 0
4136notNull = !myNull
4137"#;
4138 assert_eq!(
4139 parse_execute(code1).await.unwrap_err().message(),
4140 "Cannot apply unary operator ! to non-boolean value: a number",
4141 );
4142
4143 let code2 = "notZero = !0";
4144 assert_eq!(
4145 parse_execute(code2).await.unwrap_err().message(),
4146 "Cannot apply unary operator ! to non-boolean value: a number",
4147 );
4148
4149 let code3 = r#"
4150notEmptyString = !""
4151"#;
4152 assert_eq!(
4153 parse_execute(code3).await.unwrap_err().message(),
4154 "Cannot apply unary operator ! to non-boolean value: a string",
4155 );
4156
4157 let code4 = r#"
4158obj = { a = 1 }
4159notMember = !obj.a
4160"#;
4161 assert_eq!(
4162 parse_execute(code4).await.unwrap_err().message(),
4163 "Cannot apply unary operator ! to non-boolean value: a number",
4164 );
4165
4166 let code5 = "
4167a = []
4168notArray = !a";
4169 assert_eq!(
4170 parse_execute(code5).await.unwrap_err().message(),
4171 "Cannot apply unary operator ! to non-boolean value: an empty array",
4172 );
4173
4174 let code6 = "
4175x = {}
4176notObject = !x";
4177 assert_eq!(
4178 parse_execute(code6).await.unwrap_err().message(),
4179 "Cannot apply unary operator ! to non-boolean value: an object",
4180 );
4181
4182 let code7 = "
4183fn x() { return 1 }
4184notFunction = !x";
4185 let fn_err = parse_execute(code7).await.unwrap_err();
4186 assert!(
4189 fn_err
4190 .message()
4191 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
4192 "Actual error: {fn_err:?}"
4193 );
4194
4195 let code8 = "
4196myTagDeclarator = $myTag
4197notTagDeclarator = !myTagDeclarator";
4198 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
4199 assert!(
4202 tag_declarator_err
4203 .message()
4204 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
4205 "Actual error: {tag_declarator_err:?}"
4206 );
4207
4208 let code9 = "
4209myTagDeclarator = $myTag
4210notTagIdentifier = !myTag";
4211 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
4212 assert!(
4215 tag_identifier_err
4216 .message()
4217 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
4218 "Actual error: {tag_identifier_err:?}"
4219 );
4220
4221 let code10 = "notPipe = !(1 |> 2)";
4222 assert_eq!(
4223 parse_execute(code10).await.unwrap_err(),
4226 KclError::new_syntax(KclErrorDetails::new(
4227 "Unexpected token: !".to_owned(),
4228 vec![SourceRange::new(10, 11, ModuleId::default())],
4229 ))
4230 );
4231
4232 let code11 = "
4233fn identity(x) { return x }
4234notPipeSub = 1 |> identity(!%))";
4235 assert_eq!(
4236 parse_execute(code11).await.unwrap_err(),
4239 KclError::new_syntax(KclErrorDetails::new(
4240 "There was an unexpected `!`. Try removing it.".to_owned(),
4241 vec![SourceRange::new(56, 57, ModuleId::default())],
4242 ))
4243 );
4244
4245 }
4249
4250 #[tokio::test(flavor = "multi_thread")]
4251 async fn test_start_sketch_on_invalid_kwargs() {
4252 let current_dir = std::env::current_dir().unwrap();
4253 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
4254 let mut code = std::fs::read_to_string(&path).unwrap();
4255 assert_eq!(
4256 parse_execute(&code).await.unwrap_err().message(),
4257 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
4258 );
4259
4260 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
4261 code = std::fs::read_to_string(&path).unwrap();
4262
4263 assert_eq!(
4264 parse_execute(&code).await.unwrap_err().message(),
4265 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
4266 );
4267
4268 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
4269 code = std::fs::read_to_string(&path).unwrap();
4270
4271 assert_eq!(
4272 parse_execute(&code).await.unwrap_err().message(),
4273 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4274 );
4275
4276 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
4277 code = std::fs::read_to_string(&path).unwrap();
4278
4279 assert_eq!(
4280 parse_execute(&code).await.unwrap_err().message(),
4281 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
4282 );
4283
4284 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
4285 code = std::fs::read_to_string(&path).unwrap();
4286
4287 assert_eq!(
4288 parse_execute(&code).await.unwrap_err().message(),
4289 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
4290 );
4291 }
4292
4293 #[tokio::test(flavor = "multi_thread")]
4294 async fn test_math_negative_variable_in_binary_expression() {
4295 let ast = r#"sigmaAllow = 35000 // psi
4296width = 1 // inch
4297
4298p = 150 // lbs
4299distance = 6 // inches
4300FOS = 2
4301
4302leg1 = 5 // inches
4303leg2 = 8 // inches
4304
4305thickness_squared = distance * p * FOS * 6 / sigmaAllow
4306thickness = 0.56 // inches. App does not support square root function yet
4307
4308bracket = startSketchOn(XY)
4309 |> startProfile(at = [0,0])
4310 |> line(end = [0, leg1])
4311 |> line(end = [leg2, 0])
4312 |> line(end = [0, -thickness])
4313 |> line(end = [-leg2 + thickness, 0])
4314"#;
4315 parse_execute(ast).await.unwrap();
4316 }
4317
4318 #[tokio::test(flavor = "multi_thread")]
4319 async fn test_execute_function_no_return() {
4320 let ast = r#"fn test(@origin) {
4321 origin
4322}
4323
4324test([0, 0])
4325"#;
4326 let result = parse_execute(ast).await;
4327 assert!(result.is_err());
4328 assert!(result.unwrap_err().to_string().contains("undefined"));
4329 }
4330
4331 #[tokio::test(flavor = "multi_thread")]
4332 async fn test_max_stack_size_exceeded_error() {
4333 let ast = r#"
4334fn forever(@n) {
4335 return 1 + forever(n)
4336}
4337
4338forever(1)
4339"#;
4340 let result = parse_execute(ast).await;
4341 let err = result.unwrap_err();
4342 let msg = err.to_string();
4345 assert!(
4346 msg.contains("stack size exceeded") || msg.contains("Call depth limit"),
4347 "actual: {err:?}"
4348 );
4349 }
4350
4351 #[tokio::test(flavor = "multi_thread")]
4352 async fn test_math_doubly_nested_parens() {
4353 let ast = r#"sigmaAllow = 35000 // psi
4354width = 4 // inch
4355p = 150 // Force on shelf - lbs
4356distance = 6 // inches
4357FOS = 2
4358leg1 = 5 // inches
4359leg2 = 8 // inches
4360thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
4361thickness = 0.32 // inches. App does not support square root function yet
4362bracket = startSketchOn(XY)
4363 |> startProfile(at = [0,0])
4364 |> line(end = [0, leg1])
4365 |> line(end = [leg2, 0])
4366 |> line(end = [0, -thickness])
4367 |> line(end = [-1 * leg2 + thickness, 0])
4368 |> line(end = [0, -1 * leg1 + thickness])
4369 |> close()
4370 |> extrude(length = width)
4371"#;
4372 parse_execute(ast).await.unwrap();
4373 }
4374
4375 #[tokio::test(flavor = "multi_thread")]
4376 async fn test_math_nested_parens_one_less() {
4377 let ast = r#" sigmaAllow = 35000 // psi
4378width = 4 // inch
4379p = 150 // Force on shelf - lbs
4380distance = 6 // inches
4381FOS = 2
4382leg1 = 5 // inches
4383leg2 = 8 // inches
4384thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
4385thickness = 0.32 // inches. App does not support square root function yet
4386bracket = startSketchOn(XY)
4387 |> startProfile(at = [0,0])
4388 |> line(end = [0, leg1])
4389 |> line(end = [leg2, 0])
4390 |> line(end = [0, -thickness])
4391 |> line(end = [-1 * leg2 + thickness, 0])
4392 |> line(end = [0, -1 * leg1 + thickness])
4393 |> close()
4394 |> extrude(length = width)
4395"#;
4396 parse_execute(ast).await.unwrap();
4397 }
4398
4399 #[tokio::test(flavor = "multi_thread")]
4400 async fn test_fn_as_operand() {
4401 let ast = r#"fn f() { return 1 }
4402x = f()
4403y = x + 1
4404z = f() + 1
4405w = f() + f()
4406"#;
4407 parse_execute(ast).await.unwrap();
4408 }
4409
4410 #[tokio::test(flavor = "multi_thread")]
4411 async fn kcl_test_ids_stable_between_executions() {
4412 let code = r#"sketch001 = startSketchOn(XZ)
4413|> startProfile(at = [61.74, 206.13])
4414|> xLine(length = 305.11, tag = $seg01)
4415|> yLine(length = -291.85)
4416|> xLine(length = -segLen(seg01))
4417|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4418|> close()
4419|> extrude(length = 40.14)
4420|> shell(
4421 thickness = 3.14,
4422 faces = [seg01]
4423)
4424"#;
4425
4426 let ctx = crate::test_server::new_context(true, None).await.unwrap();
4427 let old_program = crate::Program::parse_no_errs(code).unwrap();
4428
4429 if let Err(err) = ctx.run_with_caching(old_program).await {
4431 let report = err.into_miette_report_with_outputs(code).unwrap();
4432 let report = miette::Report::new(report);
4433 panic!("Error executing program: {report:?}");
4434 }
4435
4436 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4438
4439 let code = r#"sketch001 = startSketchOn(XZ)
4440|> startProfile(at = [62.74, 206.13])
4441|> xLine(length = 305.11, tag = $seg01)
4442|> yLine(length = -291.85)
4443|> xLine(length = -segLen(seg01))
4444|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4445|> close()
4446|> extrude(length = 40.14)
4447|> shell(
4448 faces = [seg01],
4449 thickness = 3.14,
4450)
4451"#;
4452
4453 let program = crate::Program::parse_no_errs(code).unwrap();
4455 ctx.run_with_caching(program).await.unwrap();
4457
4458 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4459
4460 assert_eq!(id_generator, new_id_generator);
4461 }
4462
4463 #[tokio::test(flavor = "multi_thread")]
4464 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
4465 let code = r#"sketch001 = startSketchOn(XZ)
4466|> startProfile(at = [61.74, 206.13])
4467|> xLine(length = 305.11, tag = $seg01)
4468|> yLine(length = -291.85)
4469|> xLine(length = -segLen(seg01))
4470|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4471|> close()
4472|> extrude(length = 40.14)
4473|> shell(
4474 thickness = 3.14,
4475 faces = [seg01]
4476)
4477"#;
4478
4479 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
4480 let old_program = crate::Program::parse_no_errs(code).unwrap();
4481
4482 ctx.run_with_caching(old_program.clone()).await.unwrap();
4484
4485 let settings_state = cache::read_old_ast().await.unwrap().settings;
4486
4487 assert_eq!(settings_state, ctx.settings);
4489
4490 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4492
4493 ctx.run_with_caching(old_program.clone()).await.unwrap();
4495
4496 let settings_state = cache::read_old_ast().await.unwrap().settings;
4497
4498 assert_eq!(settings_state, ctx.settings);
4500
4501 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4503
4504 ctx.run_with_caching(old_program).await.unwrap();
4506
4507 let settings_state = cache::read_old_ast().await.unwrap().settings;
4508
4509 assert_eq!(settings_state, ctx.settings);
4511
4512 ctx.close().await;
4513 }
4514
4515 #[tokio::test(flavor = "multi_thread")]
4516 async fn mock_after_not_mock() {
4517 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4518 let program = crate::Program::parse_no_errs("x = 2").unwrap();
4519 let result = ctx.run_with_caching(program).await.unwrap();
4520 assert_number_variable(&result.variables, "x", 2.0);
4521
4522 let ctx2 = ExecutorContext::new_mock(None).await;
4523 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
4524 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
4525 assert_number_variable(&result.variables, "z", 3.0);
4526
4527 ctx.close().await;
4528 ctx2.close().await;
4529 }
4530
4531 #[tokio::test(flavor = "multi_thread")]
4533 async fn mock_execution_succeeds_after_split() {
4534 let code = kcl_input!("repro_mock_extrude");
4535 let ctx = ExecutorContext::new_mock(None).await;
4536 let program = crate::Program::parse_no_errs(code).unwrap();
4537 let _result = match ctx.run_mock(&program, &MockConfig::default()).await {
4538 Ok(res) => res,
4539 Err(e) => panic!("{}", e.error),
4540 };
4541 }
4542
4543 #[tokio::test(flavor = "multi_thread")]
4545 async fn mock_execution_rejects_oob_on_frontend_array() {
4546 let code = r#"
4547values = [10, 20]
4548third = values[2]
4549"#;
4550 let ctx = ExecutorContext::new_mock(None).await;
4551 let program = crate::Program::parse_no_errs(code).unwrap();
4552 let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
4553 ctx.close().await;
4554
4555 assert!(
4556 err.error.message().contains("array doesn't have any item at index 2"),
4557 "{err:?}"
4558 );
4559 }
4560
4561 #[tokio::test(flavor = "multi_thread")]
4566 async fn mock_execution_pattern_circular_number() {
4567 let code = kcl_input!("repro_mock_pattern_circular");
4568 let ctx = ExecutorContext::new_mock(None).await;
4569 let program = crate::Program::parse_no_errs(code).unwrap();
4570 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
4571 let copies = result
4572 .variables
4573 .get("copies")
4574 .expect("no variable called 'copies' found");
4575 let value = match copies {
4576 KclValueView::Solid { .. } => {
4577 panic!("One solid?");
4578 }
4579 KclValueView::HomArray { value } => value,
4580 other => panic!("{other:#?}"),
4581 };
4582 let actual_instances = value.len();
4583 let expected_instances = 10; assert_eq!(actual_instances, expected_instances);
4585 }
4586
4587 #[tokio::test(flavor = "multi_thread")]
4592 async fn mock_execution_subtract() {
4593 let code = kcl_input!("repro_mock_subtract");
4595 let ctx = ExecutorContext::new_mock(None).await;
4596 let program = crate::Program::parse_no_errs(code).unwrap();
4597 let result = ctx.run_mock(&program, &MockConfig::default()).await;
4598 ctx.close().await;
4599 let result = match result {
4600 Ok(x) => x,
4601 Err(e) => {
4602 let error = e.error;
4603 panic!("{error}");
4604 }
4605 };
4606
4607 let subtracted_parts = result
4609 .variables
4610 .get("subtractedParts")
4611 .expect("no variable called 'subtracted_parts' found");
4612 let subtracted_parts = match subtracted_parts {
4613 KclValueView::Solid { .. } => {
4614 panic!("One solid?");
4615 }
4616 KclValueView::HomArray { value } => value,
4617 other => panic!("{other:#?}"),
4618 };
4619
4620 let expected_number_of_parts = 2;
4623 let actual_number_of_parts = subtracted_parts.len();
4624 assert_eq!(actual_number_of_parts, expected_number_of_parts);
4625 }
4626
4627 #[tokio::test(flavor = "multi_thread")]
4628 async fn mock_then_add_extrude_then_mock_again() {
4629 let code = "s = sketch(on = XY) {
4630 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
4631 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
4632 coincident([line1.end, line2.start])
4633 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
4634 coincident([line2.end, line3.start])
4635 coincident([line1.start, line3.end])
4636}
4637 ";
4638 let ctx = ExecutorContext::new_mock(None).await;
4639 let program = crate::Program::parse_no_errs(code).unwrap();
4640 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
4641 assert!(result.variables.contains_key("s"), "actual: {:?}", result.variables);
4642
4643 let code2 = code.to_owned()
4644 + "
4645region001 = region(point = [1mm, 1mm], sketch = s)
4646extrude001 = extrude(region001, length = 1)
4647 ";
4648 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4649 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
4650 assert!(
4651 result.variables.contains_key("region001"),
4652 "actual: {:?}",
4653 result.variables
4654 );
4655
4656 ctx.close().await;
4657 }
4658
4659 #[tokio::test(flavor = "multi_thread")]
4660 async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
4661 let code = format!(
4662 r#"{}
4663
4664face7 = faceOf(solid6, face = r6.tags.line1)
4665r7 = squareRegion(onSurface = face7)
4666solid7 = extrude(r7, length = width)
4667"#,
4668 include_str!("../../tests/endless_impeller/input.kcl")
4669 );
4670
4671 let result = parse_execute(&code).await.unwrap();
4672 let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
4673 assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
4674
4675 let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
4676 KclValue::Face { value } => value,
4677 value => panic!("expected face7 to be a Face, got {value:?}"),
4678 };
4679 assert!(face7.parent_solid.creator_sketch_id.is_some());
4680 }
4681
4682 #[tokio::test(flavor = "multi_thread")]
4683 async fn mock_has_stable_ids() {
4684 let ctx = ExecutorContext::new_mock(None).await;
4685 let mock_config = MockConfig {
4686 use_prev_memory: false,
4687 ..Default::default()
4688 };
4689 let code = "sk = startSketchOn(XY)
4690 |> startProfile(at = [0, 0])";
4691 let program = crate::Program::parse_no_errs(code).unwrap();
4692 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
4693 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4694 assert!(!ids.is_empty(), "IDs should not be empty");
4695
4696 let ctx2 = ExecutorContext::new_mock(None).await;
4697 let program2 = crate::Program::parse_no_errs(code).unwrap();
4698 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
4699 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4700
4701 assert_eq!(ids, ids2, "Generated IDs should match");
4702 ctx.close().await;
4703 ctx2.close().await;
4704 }
4705
4706 #[tokio::test(flavor = "multi_thread")]
4707 async fn mock_memory_restore_preserves_module_maps() {
4708 clear_mem_cache().await;
4709
4710 let ctx = ExecutorContext::new_mock(None).await;
4711 let cold_start = MockConfig {
4712 use_prev_memory: false,
4713 ..Default::default()
4714 };
4715 ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
4716
4717 let mut mem = cache::read_old_memory().await.unwrap();
4718 assert!(
4719 mem.path_to_source_id.len() > 3,
4720 "expected prelude imports to populate multiple modules, got {:?}",
4721 mem.path_to_source_id
4722 );
4723 mem.constraint_state.insert(
4724 crate::front::ObjectId(1),
4725 indexmap::indexmap! {
4726 crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
4727 crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
4728 },
4729 );
4730
4731 let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
4732 ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
4733
4734 assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
4735 assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
4736 assert_eq!(exec_state.global.module_infos, mem.module_infos);
4737 assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
4738
4739 clear_mem_cache().await;
4740 ctx.close().await;
4741 }
4742
4743 #[tokio::test(flavor = "multi_thread")]
4744 async fn run_with_caching_no_action_refreshes_mock_memory() {
4745 cache::bust_cache().await;
4746 clear_mem_cache().await;
4747
4748 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
4749 let program = crate::Program::parse_no_errs(
4750 r#"sketch001 = sketch(on = XY) {
4751 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
4752}
4753"#,
4754 )
4755 .unwrap();
4756
4757 ctx.run_with_caching(program.clone()).await.unwrap();
4758 let baseline_memory = cache::read_old_memory().await.unwrap();
4759 assert!(
4760 !baseline_memory.scene_objects.is_empty(),
4761 "expected engine execution to persist full-scene mock memory"
4762 );
4763
4764 cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
4765 assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
4766
4767 ctx.run_with_caching(program).await.unwrap();
4768 let refreshed_memory = cache::read_old_memory().await.unwrap();
4769 assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
4770 assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
4771 assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
4772
4773 cache::bust_cache().await;
4774 clear_mem_cache().await;
4775 ctx.close().await;
4776 }
4777
4778 #[tokio::test(flavor = "multi_thread")]
4779 async fn sim_sketch_mode_real_mock_real() {
4780 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4781 let code = r#"sketch001 = startSketchOn(XY)
4782profile001 = startProfile(sketch001, at = [0, 0])
4783 |> line(end = [10, 0])
4784 |> line(end = [0, 10])
4785 |> line(end = [-10, 0])
4786 |> line(end = [0, -10])
4787 |> close()
4788"#;
4789 let program = crate::Program::parse_no_errs(code).unwrap();
4790 let result = ctx.run_with_caching(program).await.unwrap();
4791 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4792
4793 let mock_ctx = ExecutorContext::new_mock(None).await;
4794 let mock_program = crate::Program::parse_no_errs(code).unwrap();
4795 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
4796 assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4797
4798 let code2 = code.to_owned()
4799 + r#"
4800extrude001 = extrude(profile001, length = 10)
4801"#;
4802 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4803 let result = ctx.run_with_caching(program2).await.unwrap();
4804 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
4805
4806 ctx.close().await;
4807 mock_ctx.close().await;
4808 }
4809
4810 #[tokio::test(flavor = "multi_thread")]
4811 async fn read_tag_version() {
4812 let ast = r#"fn bar(@t) {
4813 return startSketchOn(XY)
4814 |> startProfile(at = [0,0])
4815 |> angledLine(
4816 angle = -60,
4817 length = segLen(t),
4818 )
4819 |> line(end = [0, 0])
4820 |> close()
4821}
4822
4823sketch = startSketchOn(XY)
4824 |> startProfile(at = [0,0])
4825 |> line(end = [0, 10])
4826 |> line(end = [10, 0], tag = $tag0)
4827 |> line(endAbsolute = [0, 0])
4828
4829fn foo() {
4830 // tag0 tags an edge
4831 return bar(tag0)
4832}
4833
4834solid = sketch |> extrude(length = 10)
4835// tag0 tags a face
4836sketch2 = startSketchOn(solid, face = tag0)
4837 |> startProfile(at = [0,0])
4838 |> line(end = [0, 1])
4839 |> line(end = [1, 0])
4840 |> line(end = [0, 0])
4841
4842foo() |> extrude(length = 1)
4843"#;
4844 parse_execute(ast).await.unwrap();
4845 }
4846
4847 #[tokio::test(flavor = "multi_thread")]
4848 async fn experimental() {
4849 let code = r#"
4850startSketchOn(XY)
4851 |> startProfile(at = [0, 0], tag = $start)
4852 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4853"#;
4854 let result = parse_execute(code).await.unwrap();
4855 let issues = result.exec_state.issues();
4856 assert_eq!(issues.len(), 1);
4857 assert_eq!(issues[0].severity, Severity::Error);
4858 let msg = &issues[0].message;
4859 assert!(msg.contains("experimental"), "found {msg}");
4860
4861 let code = r#"@settings(experimentalFeatures = allow)
4862startSketchOn(XY)
4863 |> startProfile(at = [0, 0], tag = $start)
4864 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4865"#;
4866 let result = parse_execute(code).await.unwrap();
4867 let issues = result.exec_state.issues();
4868 assert!(issues.is_empty(), "issues={issues:#?}");
4869
4870 let code = r#"@settings(experimentalFeatures = warn)
4871startSketchOn(XY)
4872 |> startProfile(at = [0, 0], tag = $start)
4873 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4874"#;
4875 let result = parse_execute(code).await.unwrap();
4876 let issues = result.exec_state.issues();
4877 assert_eq!(issues.len(), 1);
4878 assert_eq!(issues[0].severity, Severity::Warning);
4879 let msg = &issues[0].message;
4880 assert!(msg.contains("experimental"), "found {msg}");
4881
4882 let code = r#"@settings(experimentalFeatures = deny)
4883startSketchOn(XY)
4884 |> startProfile(at = [0, 0], tag = $start)
4885 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4886"#;
4887 let result = parse_execute(code).await.unwrap();
4888 let issues = result.exec_state.issues();
4889 assert_eq!(issues.len(), 1);
4890 assert_eq!(issues[0].severity, Severity::Error);
4891 let msg = &issues[0].message;
4892 assert!(msg.contains("experimental"), "found {msg}");
4893
4894 let code = r#"@settings(experimentalFeatures = foo)
4895startSketchOn(XY)
4896 |> startProfile(at = [0, 0], tag = $start)
4897 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4898"#;
4899 parse_execute(code).await.unwrap_err();
4900 }
4901
4902 #[tokio::test(flavor = "multi_thread")]
4903 async fn experimental_parameter() {
4904 let code = r#"
4905fn inc(@x, @(experimental = true) amount? = 1) {
4906 return x + amount
4907}
4908
4909answer = inc(5, amount = 2)
4910"#;
4911 let result = parse_execute(code).await.unwrap();
4912 let issues = result.exec_state.issues();
4913 assert_eq!(issues.len(), 1);
4914 assert_eq!(issues[0].severity, Severity::Error);
4915 let msg = &issues[0].message;
4916 assert!(msg.contains("experimental"), "found {msg}");
4917
4918 let code = r#"
4920fn inc(@x, @(experimental = true) amount? = 1) {
4921 return x + amount
4922}
4923
4924answer = inc(5)
4925"#;
4926 let result = parse_execute(code).await.unwrap();
4927 let issues = result.exec_state.issues();
4928 assert!(issues.is_empty(), "issues={issues:#?}");
4929 }
4930
4931 #[tokio::test(flavor = "multi_thread")]
4932 async fn experimental_scalar_fixed_constraint() {
4933 let code_left = r#"@settings(experimentalFeatures = warn)
4934sketch(on = XY) {
4935 point1 = point(at = [var 0mm, var 0mm])
4936 point1.at[0] == 1mm
4937}
4938"#;
4939 let code_right = r#"@settings(experimentalFeatures = warn)
4941sketch(on = XY) {
4942 point1 = point(at = [var 0mm, var 0mm])
4943 1mm == point1.at[0]
4944}
4945"#;
4946
4947 for code in [code_left, code_right] {
4948 let result = parse_execute(code).await.unwrap();
4949 let issues = result.exec_state.issues();
4950 let Some(error) = issues
4951 .iter()
4952 .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
4953 else {
4954 panic!("found {issues:#?}");
4955 };
4956 assert_eq!(error.severity, Severity::Warning);
4957 }
4958 }
4959
4960 #[tokio::test(flavor = "multi_thread")]
4964 async fn test_tangent_line_arc_executes_with_mock_engine() {
4965 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
4966 parse_execute(&code).await.unwrap();
4967 }
4968
4969 #[tokio::test(flavor = "multi_thread")]
4970 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
4971 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
4972 parse_execute(&code).await.unwrap();
4973 }
4974
4975 #[tokio::test(flavor = "multi_thread")]
4976 async fn test_tangent_line_circle_executes_with_mock_engine() {
4977 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
4978 parse_execute(&code).await.unwrap();
4979 }
4980
4981 #[tokio::test(flavor = "multi_thread")]
4982 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
4983 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
4984 parse_execute(&code).await.unwrap();
4985 }
4986
4987 #[tokio::test(flavor = "multi_thread")]
4988 async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
4989 let code = r#"startX = 2
4990
4991baseSketch = sketch(on = XY) {
4992 yoyo = line(start = [startX, 0], end = [7, 6])
4993 line2 = line(start = [7, 6], end = [7, 12])
4994 hi = line(start = [7, 12], end = [startX, 0])
4995}
4996
4997baseRegion = region(point = [5.5, 6], sketch = baseSketch)
4998myExtrude = extrude(
4999 baseRegion,
5000 length = 5,
5001 tagEnd = $endCap,
5002 tagStart = $startCap,
5003)
5004yodawg = getCommonEdge(faces = [
5005 baseRegion.tags.hi,
5006 baseRegion.tags.yoyo
5007])
5008
5009cutSketch = sketch(on = YZ) {
5010 myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
5011 myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
5012 line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
5013}
5014
5015cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
5016extrude001 = extrude(cutRegion, length = 5)
5017solid001 = subtract(myExtrude, tools = extrude001)
5018
5019yoyo = getOppositeEdge(baseRegion.tags.hi)
5020fillet(solid001, radius = 0.1, tags = yoyo)
5021"#;
5022
5023 parse_execute(code).await.unwrap();
5024 }
5025
5026 async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
5031 let program = crate::Program::parse_no_errs(kcl).unwrap();
5032 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5033 let mut exec_state = ExecState::new(&ctx);
5034 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
5035 let outcome = exec_state
5036 .into_exec_outcome(env_ref, &ctx)
5037 .await
5038 .expect("constraint report test outcome should collect variables");
5039 let report = outcome.sketch_constraint_report();
5040 ctx.close().await;
5041 report
5042 }
5043
5044 #[tokio::test(flavor = "multi_thread")]
5045 async fn warn_when_sketch_is_over_constrained() {
5046 let code = r#"
5047sketch001 = sketch(on = XY) {
5048 line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
5049 fixed([line1.start, ORIGIN])
5050 fixed([line1.start, [20, 20]])
5051}
5052"#;
5053 let result = parse_execute(code).await.unwrap();
5054 let issues = result.exec_state.issues();
5055 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
5056 panic!("expected over-constrained warning; found {issues:#?}");
5057 };
5058 assert_eq!(warning.severity, Severity::Warning);
5059 }
5060
5061 #[tokio::test(flavor = "multi_thread")]
5062 async fn over_constrained_warning_identifies_signed_vertical_distance_direction() {
5063 let code = r#"
5064sketch001 = sketch(on = XY) {
5065 line1 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
5066 fixed([line1.start, [0mm, 10mm]])
5067 fixed([line1.end, ORIGIN])
5068 verticalDistance([line1.start, line1.end]) == 10mm
5069}
5070"#;
5071 let result = parse_execute(code).await.unwrap();
5072 let issues = result.exec_state.issues();
5073 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
5074 panic!("expected over-constrained warning; found {issues:#?}");
5075 };
5076 assert!(
5077 warning.message.contains(
5078 "Unsatisfied signed verticalDistance constraint: a positive right-hand side requires the second point to be above the first"
5079 ),
5080 "expected signed-direction diagnostic; found {warning:#?}"
5081 );
5082 }
5083
5084 #[tokio::test(flavor = "multi_thread")]
5085 async fn no_warning_when_sketch_is_not_over_constrained() {
5086 let code = r#"
5088sketch001 = sketch(on = XY) {
5089 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5090}
5091"#;
5092 let result = parse_execute(code).await.unwrap();
5093 let issues = result.exec_state.issues();
5094 assert!(
5095 !issues.iter().any(|issue| issue.message.contains("over-constrained")),
5096 "did not expect over-constrained warning; found {issues:#?}"
5097 );
5098 }
5099
5100 #[tokio::test(flavor = "multi_thread")]
5101 async fn test_constraint_report_fully_constrained() {
5102 let kcl = r#"
5104@settings(experimentalFeatures = allow)
5105
5106sketch(on = YZ) {
5107 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
5108 line1.start.at[0] == 2
5109 line1.start.at[1] == 8
5110 line1.end.at[0] == 5
5111 line1.end.at[1] == 7
5112}
5113"#;
5114 let report = run_constraint_report(kcl).await;
5115 assert_eq!(report.fully_constrained.len(), 1);
5116 assert_eq!(report.under_constrained.len(), 0);
5117 assert_eq!(report.over_constrained.len(), 0);
5118 assert_eq!(report.errors.len(), 0);
5119 assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
5120 }
5121
5122 #[tokio::test(flavor = "multi_thread")]
5123 async fn test_constraint_report_under_constrained() {
5124 let kcl = r#"
5126sketch(on = YZ) {
5127 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
5128}
5129"#;
5130 let report = run_constraint_report(kcl).await;
5131 assert_eq!(report.fully_constrained.len(), 0);
5132 assert_eq!(report.under_constrained.len(), 1);
5133 assert_eq!(report.over_constrained.len(), 0);
5134 assert_eq!(report.errors.len(), 0);
5135 assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
5136 assert!(report.under_constrained[0].free_count > 0);
5137 }
5138
5139 #[tokio::test(flavor = "multi_thread")]
5140 async fn test_constraint_report_over_constrained() {
5141 let kcl = r#"
5143@settings(experimentalFeatures = allow)
5144
5145sketch(on = YZ) {
5146 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
5147 line1.start.at[0] == 2
5148 line1.start.at[1] == 8
5149 line1.end.at[0] == 5
5150 line1.end.at[1] == 7
5151 distance([line1.start, line1.end]) == 100mm
5152}
5153"#;
5154 let report = run_constraint_report(kcl).await;
5155 assert_eq!(report.over_constrained.len(), 1);
5156 assert_eq!(report.errors.len(), 0);
5157 assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
5158 assert!(report.over_constrained[0].conflict_count > 0);
5159 }
5160
5161 #[tokio::test(flavor = "multi_thread")]
5162 async fn test_constraint_report_multiple_sketches() {
5163 let kcl = r#"
5165@settings(experimentalFeatures = allow)
5166
5167s1 = sketch(on = YZ) {
5168 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
5169 line1.start.at[0] == 2
5170 line1.start.at[1] == 8
5171 line1.end.at[0] == 5
5172 line1.end.at[1] == 7
5173}
5174
5175s2 = sketch(on = XZ) {
5176 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5177}
5178"#;
5179 let report = run_constraint_report(kcl).await;
5180 assert_eq!(
5181 report.fully_constrained.len()
5182 + report.under_constrained.len()
5183 + report.over_constrained.len()
5184 + report.errors.len(),
5185 2,
5186 "Expected 2 sketches total"
5187 );
5188 assert_eq!(report.fully_constrained.len(), 1);
5189 assert_eq!(report.under_constrained.len(), 1);
5190 }
5191
5192 #[tokio::test(flavor = "multi_thread")]
5193 async fn test_constraint_report_reports_sketch_names() {
5194 let kcl = r#"
5199@settings(experimentalFeatures = allow)
5200
5201fixedSketch = sketch(on = YZ) {
5202 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
5203 line1.start.at[0] == 2
5204 line1.start.at[1] == 8
5205 line1.end.at[0] == 5
5206 line1.end.at[1] == 7
5207}
5208
5209looseSketch = sketch(on = XZ) {
5210 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5211}
5212
5213conflictSketch = sketch(on = XY) {
5214 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
5215 line1.start.at[0] == 2
5216 line1.start.at[1] == 8
5217 line1.end.at[0] == 5
5218 line1.end.at[1] == 7
5219 distance([line1.start, line1.end]) == 100mm
5220}
5221"#;
5222 let report = run_constraint_report(kcl).await;
5223 assert_eq!(report.errors.len(), 0);
5224 assert_eq!(report.fully_constrained.len(), 1);
5225 assert_eq!(report.under_constrained.len(), 1);
5226 assert_eq!(report.over_constrained.len(), 1);
5227 assert_eq!(report.fully_constrained[0].name, "fixedSketch");
5228 assert_eq!(report.under_constrained[0].name, "looseSketch");
5229 assert_eq!(report.over_constrained[0].name, "conflictSketch");
5230 }
5231
5232 #[tokio::test(flavor = "multi_thread")]
5233 async fn test_constraint_report_name_empty_without_declaration() {
5234 let kcl = r#"
5238sketch(on = YZ) {
5239 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
5240}
5241"#;
5242 let report = run_constraint_report(kcl).await;
5243 assert_eq!(report.under_constrained.len(), 1);
5244 assert_eq!(report.under_constrained[0].name, "");
5245 }
5246
5247 #[tokio::test(flavor = "multi_thread")]
5248 async fn test_constraint_report_names_repeat_across_calls() {
5249 let kcl = r#"
5254fn makeSketch() {
5255 inner = sketch(on = XY) {
5256 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
5257 }
5258 return inner
5259}
5260
5261first = makeSketch()
5262second = makeSketch()
5263"#;
5264 let report = run_constraint_report(kcl).await;
5265 assert_eq!(report.under_constrained.len(), 2);
5266 assert_eq!(report.under_constrained[0].name, "inner");
5267 assert_eq!(report.under_constrained[1].name, "inner");
5268 }
5269
5270 #[tokio::test(flavor = "multi_thread")]
5271 async fn test_enum_declaration_is_experimental() {
5272 let code = "type Color { | Red }";
5275 assert_eq!(
5276 parse_execute(code).await.unwrap_err().message(),
5277 "Use of enum declarations is experimental and may change or be removed."
5278 );
5279 }
5280
5281 #[tokio::test(flavor = "multi_thread")]
5282 async fn enum_declaration_registers_type() {
5283 let code = r#"@settings(experimentalFeatures = allow)
5287type Color { | Red | Green }
5288"#;
5289 parse_execute(code).await.unwrap();
5290
5291 let code = r#"@settings(experimentalFeatures = allow)
5292export type Color { | Red | Green }
5293"#;
5294 parse_execute(code).await.unwrap();
5295
5296 let code = r#"@settings(experimentalFeatures = allow)
5298type Empty { | }
5299"#;
5300 parse_execute(code).await.unwrap();
5301 }
5302
5303 #[tokio::test(flavor = "multi_thread")]
5304 async fn enum_declaration_rejects_nested_scope() {
5305 let allow = "@settings(experimentalFeatures = allow)\n";
5312 for (case, code) in [
5313 (
5314 "function body",
5315 format!("{allow}fn palette() {{\n type Color {{ | Red }}\n return 0\n}}\npalette()\n"),
5316 ),
5317 (
5318 "sketch block",
5319 format!(
5320 "{allow}sketch(on = XY) {{\n type Color {{ | Red }}\n l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
5321 ),
5322 ),
5323 ] {
5324 assert_eq!(
5325 parse_execute(&code).await.unwrap_err().message(),
5326 "Enum declarations are only supported at the top-level of a file. Move `type Color` to the top-level.",
5327 "case: {case}"
5328 );
5329 }
5330 }
5331
5332 #[tokio::test(flavor = "multi_thread")]
5333 async fn enum_alone_is_restricted_to_top_level() {
5334 let allow = "@settings(experimentalFeatures = allow)\n";
5341 for (case, code) in [
5342 (
5343 "function body",
5344 format!("{allow}fn f() {{\n type Temperature = number(_)\n return 0\n}}\nx = f()\n"),
5345 ),
5346 (
5347 "sketch block",
5348 format!(
5349 "{allow}sketch(on = XY) {{\n type Temperature = number(_)\n l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
5350 ),
5351 ),
5352 ] {
5353 parse_execute(&code)
5354 .await
5355 .unwrap_or_else(|err| panic!("a type alias should be allowed in a {case}: {}", err.message()));
5356 }
5357 }
5358
5359 #[tokio::test(flavor = "multi_thread")]
5360 async fn enum_declaration_rejects_duplicate() {
5361 let code = r#"@settings(experimentalFeatures = allow)
5362type Color { | Red | Green | Red }
5363"#;
5364 assert_eq!(
5365 parse_execute(code).await.unwrap_err().message(),
5366 "Duplicate variant `Red` in enum `Color`."
5367 );
5368 }
5369
5370 async fn execute_with_modules(main: &str, modules: &[(&str, &str)]) -> Result<ExecTestResults, KclError> {
5372 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_enum_clash").unwrap();
5373 for (name, source) in modules {
5374 tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
5375 }
5376
5377 parse_execute_with_project_dir(main, Some(crate::TypedPath(tmpdir.path().into()))).await
5378 }
5379
5380 async fn issues_with_empty_module(main: &str) -> Vec<crate::errors::CompilationIssue> {
5390 use futures::FutureExt;
5391
5392 let project_dir = crate::TypedPath::new("/zma-kcl-member-ranges");
5393 let files = [(project_dir.join("m.kcl").to_string(), Vec::new())]
5396 .into_iter()
5397 .collect();
5398
5399 let program = crate::Program::parse_no_errs(main).unwrap();
5400 let ctx = ExecutorContext {
5401 engine: Arc::new(EngineManager::new_mock()),
5402 engine_batch: EngineBatchContext::default(),
5403 fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
5404 settings: ExecutorSettings {
5405 project_directory: Some(project_dir),
5406 ..Default::default()
5407 },
5408 context_type: ContextType::Mock,
5409 execution_callbacks: Default::default(),
5410 executor_kind: machine::ExecutorKind::resolve(),
5411 machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
5412 };
5413 let mut exec_state = ExecState::new(&ctx);
5414 let run_result = std::panic::AssertUnwindSafe(ctx.run(&program, &mut exec_state))
5418 .catch_unwind()
5419 .await;
5420 ctx.close().await;
5421 if let Err(panic) = run_result {
5422 std::panic::resume_unwind(panic);
5423 }
5424 exec_state.issues().to_vec()
5425 }
5426
5427 #[tokio::test(flavor = "multi_thread")]
5428 async fn member_object_diagnostics_use_object_range() {
5429 let main = "import \"m.kcl\" as m
5433x = m.field
5434";
5435 let issues = issues_with_empty_module(main).await;
5436 let warning = issues
5437 .iter()
5438 .find(|issue| issue.message.contains("no return value"))
5439 .expect("missing-return warning should be recorded");
5440 let object_start = main.rfind("m.field").unwrap();
5441 assert_eq!(
5442 (warning.source_range.start(), warning.source_range.end()),
5443 (object_start, object_start + 1),
5444 "warning should point at the object's span"
5445 );
5446 }
5447
5448 #[tokio::test(flavor = "multi_thread")]
5449 async fn member_property_diagnostics_use_property_range() {
5450 let main = "import \"m.kcl\" as m
5453arr = [1]
5454x = arr[m]
5455";
5456 let issues = issues_with_empty_module(main).await;
5457 let warning = issues
5458 .iter()
5459 .find(|issue| issue.message.contains("no return value"))
5460 .expect("missing-return warning should be recorded");
5461 let prop_start = main.rfind("[m]").unwrap() + 1;
5462 assert_eq!(
5463 (warning.source_range.start(), warning.source_range.end()),
5464 (prop_start, prop_start + 1),
5465 "warning should point at the property's span"
5466 );
5467 }
5468
5469 #[tokio::test(flavor = "multi_thread")]
5470 async fn backtrace_reports_fully_qualified_fn_names() {
5471 let main = "import \"m.kcl\" as m\nx = m::f()\n";
5475 let modules = [("m.kcl", "export fn f() {\n return undefinedVariable\n}\n")];
5476 let err = execute_with_modules(main, &modules).await.unwrap_err();
5477 let fn_names: Vec<_> = err.backtrace().into_iter().filter_map(|item| item.fn_name).collect();
5478 assert_eq!(fn_names, vec!["m::f".to_owned()]);
5479 }
5480
5481 #[tokio::test(flavor = "multi_thread")]
5482 async fn whole_module_name_executes_as_operand() {
5483 let main = r#"import "m.kcl" as m
5487sum = m + m
5488neg = -m
5489"#;
5490 let result = execute_with_modules(main, &[("m.kcl", "42\n")]).await.unwrap();
5491 assert_eq!(
5492 mem_get_json(result.exec_state.stack(), result.mem_env, "sum").as_f64(),
5493 Some(84.0)
5494 );
5495 assert_eq!(
5496 mem_get_json(result.exec_state.stack(), result.mem_env, "neg").as_f64(),
5497 Some(-42.0)
5498 );
5499 }
5500
5501 #[tokio::test(flavor = "multi_thread")]
5502 async fn whole_module_without_return_as_operand_errors() {
5503 let main = "import \"m.kcl\" as m
5508x = m + 1
5509";
5510 let err = execute_with_modules(main, &[("m.kcl", "")]).await.unwrap_err();
5511 assert!(
5512 err.message().contains("Expected a number, but found none"),
5513 "expected the operand to be the module's missing-return KclNone, got: {}",
5514 err.message()
5515 );
5516 }
5517
5518 #[tokio::test(flavor = "multi_thread")]
5519 async fn enum_rejects_name_clash_with_module() {
5520 let plain_module = ("Color.kcl", "export x = 1\n");
5525 let enum_module = (
5526 "enums.kcl",
5527 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
5528 );
5529
5530 for (case, main, modules) in [
5531 (
5532 "module then enum",
5533 "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\ntype Color { | Red }\n",
5534 vec![plain_module],
5535 ),
5536 (
5537 "enum then module",
5538 "@settings(experimentalFeatures = allow)\ntype Color { | Red }\nimport \"Color.kcl\"\n",
5539 vec![plain_module],
5540 ),
5541 (
5542 "named import of an enum",
5543 "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport Color from 'enums.kcl'\n",
5544 vec![plain_module, enum_module],
5545 ),
5546 (
5547 "glob import of an enum",
5548 "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport * from 'enums.kcl'\n",
5549 vec![plain_module, enum_module],
5550 ),
5551 ] {
5552 let err = execute_with_modules(main, &modules).await.unwrap_err();
5553 assert_eq!(
5554 err.message(),
5555 "An enum and a module cannot share the name `Color` in the same scope, because `Color::x` would be ambiguous. Rename one of them.",
5556 "case: {case}"
5557 );
5558 }
5559 }
5560
5561 #[tokio::test(flavor = "multi_thread")]
5562 async fn enum_constructs_variant() {
5563 let allow = "@settings(experimentalFeatures = allow)\n";
5564 let colors = (
5565 "colors.kcl",
5566 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
5567 );
5568
5569 for (case, main, modules) in [
5570 (
5571 "declared locally",
5572 format!("{allow}type Color {{ | Red | Green }}\nx = Color::Red\n"),
5573 vec![],
5574 ),
5575 (
5576 "reached through a module path",
5579 format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
5580 vec![colors],
5581 ),
5582 (
5583 "imported by name",
5584 format!("{allow}import Color from 'colors.kcl'\nx = Color::Red\n"),
5585 vec![colors],
5586 ),
5587 (
5588 "imported under an alias",
5591 format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade::Red\n"),
5592 vec![colors],
5593 ),
5594 ] {
5595 let result = execute_with_modules(&main, &modules)
5596 .await
5597 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
5598 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
5599 panic!("case: {case}: `x` should hold an enum value");
5600 };
5601 assert_eq!(value.qualified_name(), "Color::Red", "case: {case}");
5602 }
5603 }
5604
5605 #[tokio::test(flavor = "multi_thread")]
5613 async fn signature_types_resolve_in_declaring_module() {
5614 let colors = (
5615 "colors.kcl",
5616 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n return c\n}\n",
5617 );
5618 let main =
5621 "@settings(experimentalFeatures = allow)\nimport \"colors.kcl\"\nr = colors::paint(colors::Color::Red)\n";
5622
5623 let result = execute_with_modules(main, &[colors]).await.unwrap();
5624 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
5625 panic!("`r` should hold an enum value");
5626 };
5627 assert_eq!(value.qualified_name(), "Color::Red");
5628 }
5629
5630 #[tokio::test(flavor = "multi_thread")]
5631 async fn signature_types_resolve_under_import_alias() {
5632 let colors = (
5636 "colors.kcl",
5637 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n return c\n}\n",
5638 );
5639 let main = "@settings(experimentalFeatures = allow)\nimport \"colors.kcl\" as painter\nr = painter::paint(painter::Color::Red)\n";
5640
5641 let result = execute_with_modules(main, &[colors]).await.unwrap();
5642 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
5643 panic!("`r` should hold an enum value");
5644 };
5645 assert_eq!(value.qualified_name(), "Color::Red");
5646 }
5647
5648 #[tokio::test(flavor = "multi_thread")]
5649 async fn signature_types_ignore_caller_scope() {
5650 let broken = (
5655 "broken.kcl",
5656 "@settings(experimentalFeatures = allow)\nexport fn f(@x: Missing) {\n return x\n}\n",
5657 );
5658 let main = "@settings(experimentalFeatures = allow)\ntype Missing = string\nimport \"broken.kcl\"\nr = broken::f(\"hi\")\n";
5659
5660 let err = execute_with_modules(main, &[broken]).await.unwrap_err();
5661 assert!(
5662 err.message().contains("Unknown type: Missing"),
5663 "message: {}",
5664 err.message()
5665 );
5666 }
5667
5668 #[tokio::test(flavor = "multi_thread")]
5669 async fn signature_types_reject_forward_reference() {
5670 let main = "@settings(experimentalFeatures = allow)\nfn f(@x: Later) {\n return x\n}\ntype Later = string\n";
5674
5675 let err = parse_execute(main).await.unwrap_err();
5676 assert!(
5677 err.message().contains("Unknown type: Later"),
5678 "message: {}",
5679 err.message()
5680 );
5681 }
5682
5683 #[tokio::test(flavor = "multi_thread")]
5684 async fn signature_types_resolve_in_enclosing_scope() {
5685 let main = "@settings(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";
5690
5691 let result = parse_execute(main).await.unwrap();
5692 let KclValue::Number { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
5693 panic!("`r` should hold a number");
5694 };
5695 assert_eq!(value, 42.0);
5696 }
5697
5698 #[tokio::test(flavor = "multi_thread")]
5708 async fn signature_number_types_ignore_module_default_units() {
5709 let units_in = (
5710 "units_in.kcl",
5711 "@settings(defaultLengthUnit = in)\nexport fn passThrough(@x: number(Length)) {\n return x\n}\n",
5712 );
5713 let main = "import \"units_in.kcl\"\na = units_in::passThrough(42)\nb = units_in::passThrough(42mm)\nc = units_in::passThrough(42in)\n";
5716
5717 let result = execute_with_modules(main, &[units_in]).await.unwrap();
5718 for (name, expected_ty) in [
5719 (
5729 "a",
5730 kcl_api::NumericType::Default {
5731 len: kcl_api::UnitLength::Millimeters,
5732 angle: kcl_api::UnitAngle::Degrees,
5733 },
5734 ),
5735 (
5736 "b",
5737 kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Millimeters)),
5738 ),
5739 (
5740 "c",
5741 kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Inches)),
5742 ),
5743 ] {
5744 let KclValue::Number { value, ty, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name)
5745 else {
5746 panic!("`{name}` should hold a number");
5747 };
5748 assert_eq!(value, 42.0, "`{name}` should keep its magnitude");
5749 assert_eq!(ty, expected_ty, "`{name}` should keep the caller-side unit context");
5750 }
5751 }
5752
5753 #[tokio::test(flavor = "multi_thread")]
5761 async fn signature_types_use_declaring_scope_when_both_scopes_define_the_name() {
5762 let m1 = (
5763 "m1.kcl",
5764 "@settings(experimentalFeatures = allow)\ntype A = string\n\nexport fn test(@a: A) {\n return a\n}\n",
5765 );
5766 let main =
5767 "@settings(experimentalFeatures = allow)\nimport * from \"m1.kcl\"\ntype A = number(mm)\nx = test(2mm)\n";
5768
5769 let err = execute_with_modules(main, &[m1]).await.unwrap_err();
5770 assert_eq!(
5771 err.message(),
5772 "The input argument of `test` requires a value with type `A`, but found a number (mm) (with type `number(mm)`)."
5773 );
5774 }
5775
5776 #[tokio::test(flavor = "multi_thread")]
5777 async fn enum_rejects_bad_variant_paths() {
5778 let allow = "@settings(experimentalFeatures = allow)\n";
5779
5780 for (case, main, modules, message) in [
5781 (
5782 "unknown variant",
5783 format!("{allow}type Color {{ | Red | Green }}\nx = Color::Blue\n"),
5784 vec![],
5785 "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
5786 ),
5787 (
5788 "enum with no variants",
5789 format!("{allow}type Empty {{ | }}\nx = Empty::Red\n"),
5790 vec![],
5791 "`Red` is not a variant of enum `Empty`. Enum `Empty` has no variants.",
5792 ),
5793 (
5794 "path continues past the enum",
5795 format!("{allow}type Color {{ | Red }}\nx = Color::Red::more\n"),
5796 vec![],
5797 "`Color` is an enum, so only a variant name can follow it. There is nothing to reach through `Color::Red`.",
5798 ),
5799 (
5800 "variant name is case sensitive",
5801 format!("{allow}type Color {{ | Red }}\nx = Color::red\n"),
5802 vec![],
5803 "`red` is not a variant of enum `Color`. Its variants are: Red.",
5804 ),
5805 (
5806 "enum not exported from its module",
5807 format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
5808 vec![(
5809 "colors.kcl",
5810 "@settings(experimentalFeatures = allow)\ntype Color { | Red }\n",
5811 )],
5812 "Item Color not found in module's exported items",
5813 ),
5814 (
5815 "a type alias cannot head a path",
5818 format!("{allow}type T = number(_)\nx = T::foo\n"),
5819 vec![],
5820 "`T` is not defined",
5821 ),
5822 (
5823 "a value cannot head a path",
5826 "Color = 5\nx = Color::Red\n".to_owned(),
5827 vec![],
5828 "`Color` is not defined",
5829 ),
5830 ] {
5831 let err = execute_with_modules(&main, &modules).await.unwrap_err();
5832 assert_eq!(err.message(), message, "case: {case}");
5833 }
5834 }
5835
5836 #[tokio::test(flavor = "multi_thread")]
5837 async fn enum_compares_by_variant() {
5838 let code = r#"@settings(experimentalFeatures = allow)
5839type Color { | Red | Green }
5840sameEq = Color::Red == Color::Red
5841sameNeq = Color::Red != Color::Red
5842otherEq = Color::Red == Color::Green
5843otherNeq = Color::Red != Color::Green
5844"#;
5845 let result = parse_execute(code).await.unwrap();
5846
5847 for (name, expected) in [
5848 ("sameEq", true),
5849 ("sameNeq", false),
5850 ("otherEq", false),
5851 ("otherNeq", true),
5852 ] {
5853 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
5854 panic!("`{name}` should hold a bool");
5855 };
5856 assert_eq!(value, expected, "variable: {name}");
5857 }
5858 }
5859
5860 #[tokio::test(flavor = "multi_thread")]
5861 async fn enum_usable_inside_sketch_block() {
5862 let code = r#"@settings(experimentalFeatures = allow)
5871type Color { | Red | Green }
5872sketch(on = XY) {
5873 c = Color::Red
5874 assertIs(Color::Red != Color::Green)
5875 assertIs(!(Color::Red != Color::Red))
5876 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
5877}
5878"#;
5879 parse_execute(code)
5880 .await
5881 .unwrap_or_else(|err| panic!("enum use inside a sketch block should work: {}", err.message()));
5882 }
5883
5884 #[tokio::test(flavor = "multi_thread")]
5885 async fn enum_eq_reserved_inside_sketch_block() {
5886 let allow = "@settings(experimentalFeatures = allow)\n";
5896 let tail = " l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}\n";
5897 for (case, declaration, comparison, types) in [
5898 (
5899 "enums",
5900 "type Color { | Red | Green }\n",
5901 "Color::Red == Color::Green",
5902 "a value of enum `Color` and a value of enum `Color`",
5903 ),
5904 ("strings", "", "\"a\" == \"b\"", "a string and a string"),
5905 ("numbers", "", "1 == 2", "a number and a number"),
5906 ] {
5907 let code = format!("{allow}{declaration}sketch(on = XY) {{\n x = {comparison}\n{tail}");
5908 assert_eq!(
5909 parse_execute(&code).await.unwrap_err().message(),
5910 format!("Cannot create an equivalence constraint between values of these types: {types}"),
5911 "case: {case}"
5912 );
5913 }
5914 }
5915
5916 #[tokio::test(flavor = "multi_thread")]
5917 async fn enum_same_file_imported_twice_is_one_type() {
5918 let main = r#"@settings(experimentalFeatures = allow)
5922import Color as A from 'colors.kcl'
5923import Color as B from 'colors.kcl'
5924x = A::Red == B::Red
5925y = A::Red == B::Green
5926"#;
5927 let result = execute_with_modules(
5928 main,
5929 &[(
5930 "colors.kcl",
5931 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
5932 )],
5933 )
5934 .await
5935 .unwrap();
5936
5937 for (name, expected) in [("x", true), ("y", false)] {
5938 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
5939 panic!("`{name}` should hold a bool");
5940 };
5941 assert_eq!(value, expected, "variable: {name}");
5942 }
5943 }
5944
5945 #[tokio::test(flavor = "multi_thread")]
5946 async fn enum_rejects_comparison_across_types() {
5947 let allow = "@settings(experimentalFeatures = allow)\n";
5948 let color = (
5949 "a.kcl",
5950 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
5951 );
5952 let other_color = (
5953 "b.kcl",
5954 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
5955 );
5956
5957 for (case, main, modules, message) in [
5958 (
5959 "two enums declared separately",
5960 format!("{allow}type Color {{ | Red }}\ntype Shade {{ | Red }}\nx = Color::Red == Shade::Red\n"),
5961 vec![],
5962 "Cannot compare enum `Color` with enum `Shade`. They are different types.",
5963 ),
5964 (
5965 "two enums sharing a name",
5969 format!(
5970 "{allow}import Color as A from 'a.kcl'\nimport Color as B from 'b.kcl'\nx = A::Red == B::Red\n"
5971 ),
5972 vec![color, other_color],
5973 "Cannot compare two different enums that are both named `Color`. They come from separate declarations.",
5974 ),
5975 (
5976 "an enum and a number",
5977 format!("{allow}type Color {{ | Red }}\nx = Color::Red == 5\n"),
5978 vec![],
5979 "Cannot compare enum `Color::Red` with a number.",
5980 ),
5981 (
5982 "a number and an enum, in that order",
5983 format!("{allow}type Color {{ | Red }}\nx = 5 == Color::Red\n"),
5984 vec![],
5985 "Cannot compare enum `Color::Red` with a number.",
5986 ),
5987 (
5988 "an enum and a string",
5989 format!("{allow}type Color {{ | Red }}\nx = Color::Red == \"Red\"\n"),
5990 vec![],
5991 "Cannot compare enum `Color::Red` with a string.",
5992 ),
5993 ] {
5994 let err = execute_with_modules(&main, &modules).await.unwrap_err();
5995 assert_eq!(err.message(), message, "case: {case}");
5996 }
5997 }
5998
5999 #[tokio::test(flavor = "multi_thread")]
6000 async fn enum_rejects_bare_type_name_as_value() {
6001 let allow = "@settings(experimentalFeatures = allow)\n";
6002 let colors = (
6003 "colors.kcl",
6004 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
6005 );
6006
6007 for (case, main, modules, message) in [
6008 (
6009 "enum suggests a variant",
6010 format!("{allow}type Color {{ | Red | Green }}\nx = Color\n"),
6011 vec![],
6012 "`Color` is a type, not a value. Use one of its variants, such as `Color::Red`.",
6013 ),
6014 (
6015 "suggestion uses the import alias",
6018 format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade\n"),
6019 vec![colors],
6020 "`Shade` is a type, not a value. Use one of its variants, such as `Shade::Red`.",
6021 ),
6022 (
6023 "enum with no variants suggests nothing",
6024 format!("{allow}type Empty {{ | }}\nx = Empty\n"),
6025 vec![],
6026 "`Empty` is a type, not a value.",
6027 ),
6028 (
6029 "a type alias reports the same way",
6030 format!("{allow}type T = number(_)\nx = T\n"),
6031 vec![],
6032 "`T` is a type, not a value.",
6033 ),
6034 (
6035 "an unknown name is still undefined",
6038 "x = Nope\n".to_owned(),
6039 vec![],
6040 "`Nope` is not defined",
6041 ),
6042 ] {
6043 let err = execute_with_modules(&main, &modules).await.unwrap_err();
6044 assert_eq!(err.message(), message, "case: {case}");
6045 }
6046 }
6047
6048 #[tokio::test(flavor = "multi_thread")]
6049 async fn enum_use_gated_by_consuming_module() {
6050 let main = r#"import "colors.kcl"
6058x = colors::Color::Red
6059"#;
6060 let result = execute_with_modules(
6061 main,
6062 &[(
6063 "colors.kcl",
6064 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
6065 )],
6066 )
6067 .await
6068 .unwrap();
6069
6070 let issues = &result.exec_state.global.issues;
6071 assert_eq!(issues.len(), 1, "issues: {issues:?}");
6072 assert_eq!(
6073 issues[0].message,
6074 "Use of the enum `Color` is experimental and may change or be removed."
6075 );
6076 assert_eq!(issues[0].severity, Severity::Error);
6077 }
6078
6079 #[tokio::test(flavor = "multi_thread")]
6080 async fn enum_use_not_gated_when_consumer_allows_it() {
6081 let code = r#"@settings(experimentalFeatures = allow)
6084type Color { | Red }
6085x = Color::Red
6086"#;
6087 let result = parse_execute(code).await.unwrap();
6088 assert!(
6089 result.exec_state.global.issues.is_empty(),
6090 "issues: {:?}",
6091 result.exec_state.global.issues
6092 );
6093 }
6094
6095 #[tokio::test(flavor = "multi_thread")]
6096 async fn enum_allows_name_sharing_outside_modules() {
6097 for (case, main, modules) in [
6104 (
6105 "an alias may share a name with a module",
6108 "@settings(experimentalFeatures = allow)\ntype Temperature = number(_)\nimport \"Temperature.kcl\"\n",
6109 vec![("Temperature.kcl", "export x = 1\n")],
6110 ),
6111 (
6112 "a value may share a name with an enum",
6113 "@settings(experimentalFeatures = allow)\ntype Color { | Red }\nColor = 5\n",
6114 vec![],
6115 ),
6116 ] {
6117 if let Err(err) = execute_with_modules(main, &modules).await {
6118 panic!("case: {case}: {}", err.message());
6119 }
6120 }
6121 }
6122
6123 #[tokio::test(flavor = "multi_thread")]
6124 async fn enum_declaration_rejects_redefinition() {
6125 let code = r#"@settings(experimentalFeatures = allow)
6126type Color { | Red }
6127type Color { | Green }
6128"#;
6129 assert_eq!(
6130 parse_execute(code).await.unwrap_err().message(),
6131 "Redefinition of type Color."
6132 );
6133 }
6134
6135 #[tokio::test(flavor = "multi_thread")]
6141 async fn enum_projects_to_string() {
6142 let header = r#"
6143 @settings(experimentalFeatures = allow)
6144 type Color { | Red | Green }
6145 type Label = string
6146 "#;
6147
6148 for (case, body, expected) in [
6149 ("a variant", "x = Color::Red: string", "Red"),
6150 ("another variant of the same enum", "x = Color::Green: string", "Green"),
6151 ("an alias of the target type", "x = Color::Red: Label", "Red"),
6152 (
6153 "an element of a projected array",
6154 r#"
6155 pair = [Color::Red, Color::Green]: [string]
6156 x = pair[1]
6157 "#,
6158 "Green",
6159 ),
6160 (
6161 "an element of a nested projected array",
6162 r#"
6163 grid = [[Color::Green]]: [[string]]
6164 x = grid[0][0]
6165 "#,
6166 "Green",
6167 ),
6168 (
6169 "a one-element array against a bare string",
6170 "x = [Color::Red]: string",
6171 "Red",
6172 ),
6173 ] {
6174 let result = parse_execute(&format!("{header}{body}\n"))
6175 .await
6176 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
6177 let KclValue::String { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
6178 panic!("case: {case}: `x` should hold a string");
6179 };
6180 assert_eq!(value, expected, "case: {case}");
6181 }
6182 }
6183
6184 #[tokio::test(flavor = "multi_thread")]
6188 async fn enum_ascription_keeps_the_enum() {
6189 let header = r#"
6190 @settings(experimentalFeatures = allow)
6191 type Color { | Red | Green }
6192 type Paint = Color
6193 "#;
6194
6195 for (case, expression, expected) in [
6196 ("its own type", "(Color::Red: Color) == Color::Red", true),
6197 ("an alias of its own type", "(Color::Red: Paint) == Color::Red", true),
6198 (
6199 "the ascription does not change which variant it is",
6200 "(Color::Red: Color) == Color::Green",
6201 false,
6202 ),
6203 ] {
6204 let result = parse_execute(&format!("{header}x = {expression}\n"))
6205 .await
6206 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
6207 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
6208 panic!("case: {case}: `x` should hold a bool");
6209 };
6210 assert_eq!(value, expected, "case: {case}");
6211 }
6212 }
6213
6214 #[tokio::test(flavor = "multi_thread")]
6218 async fn enum_projection_is_not_implicit() {
6219 let header = r#"
6220 @settings(experimentalFeatures = allow)
6221 type Color { | Red | Green }
6222 "#;
6223 let found = "but found a value of enum `Color` (with type `Color`).";
6224
6225 for (case, body, expected) in [
6226 (
6227 "unlabeled argument",
6228 r#"
6229 fn label(@text: string) { return text }
6230 x = label(Color::Red)
6231 "#,
6232 format!("The input argument of `label` requires a value with type `string`, {found}"),
6233 ),
6234 (
6235 "labeled argument",
6236 r#"
6237 fn label(text: string) { return text }
6238 x = label(text = Color::Red)
6239 "#,
6240 format!("text requires a value with type `string`, {found}"),
6241 ),
6242 (
6243 "return",
6244 r#"
6245 fn label(): string { return Color::Red }
6246 x = label()
6247 "#,
6248 format!("This function requires its result to be a value with type `string`, {found}"),
6249 ),
6250 (
6251 "inside an array at an argument boundary",
6256 r#"
6257 fn labels(@text: [string]) { return text }
6258 x = labels([Color::Red])
6259 "#,
6260 "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(),
6261 ),
6262 ] {
6263 assert_eq!(
6264 parse_execute(&format!("{header}{body}\n")).await.unwrap_err().message(),
6265 expected,
6266 "case: {case}"
6267 );
6268 }
6269 }
6270
6271 #[tokio::test(flavor = "multi_thread")]
6274 async fn enum_ascription_rejections() {
6275 let header = r#"
6276 @settings(experimentalFeatures = allow)
6277 type Color { | Red }
6278 type Shade { | Red }
6279 "#;
6280 let no_number = "Cannot project enum `Color` to a number. An enum projects to `string`; projecting to a number is not supported yet.";
6281
6282 for (case, expression, expected) in [
6283 ("a number target", "Color::Red: number(_)", no_number.to_owned()),
6284 (
6285 "a number target reached through an array, so the reason survives the walk",
6286 "[Color::Red]: [number(_)]",
6287 no_number.to_owned(),
6288 ),
6289 (
6290 "a boolean target, which is not a projection at all",
6291 "Color::Red: bool",
6292 "could not coerce a value of enum `Color` (with type `Color`) to type `bool`".to_owned(),
6293 ),
6294 (
6295 "another enum whose variants happen to match",
6296 "Color::Red: Shade",
6297 "could not coerce a value of enum `Color` (with type `Color`) to type `Shade`".to_owned(),
6298 ),
6299 ] {
6300 assert_eq!(
6301 parse_execute(&format!("{header}x = {expression}\n"))
6302 .await
6303 .unwrap_err()
6304 .message(),
6305 expected,
6306 "case: {case}"
6307 );
6308 }
6309 }
6310
6311 #[tokio::test(flavor = "multi_thread")]
6318 async fn enum_flows_through_declared_types() {
6319 let header = r#"
6320 @settings(experimentalFeatures = allow)
6321 type Color { | Red | Green }
6322 type Shade { | Red }
6323 "#;
6324
6325 for (case, body, expected) in [
6326 (
6327 "an unlabeled parameter",
6328 r#"
6329 fn paint(@c: Color) { return c }
6330 x = paint(Color::Red) == Color::Red
6331 "#,
6332 None,
6333 ),
6334 (
6335 "a labeled parameter",
6336 r#"
6337 fn paint(c: Color) { return c }
6338 x = paint(c = Color::Green) == Color::Green
6339 "#,
6340 None,
6341 ),
6342 (
6343 "a declared return type",
6344 r#"
6345 fn pick(): Color { return Color::Red }
6346 x = pick() == Color::Red
6347 "#,
6348 None,
6349 ),
6350 (
6351 "an array parameter",
6352 r#"
6353 fn firstOf(@cs: [Color]) { return cs[0] }
6354 x = firstOf([Color::Red, Color::Green]) == Color::Red
6355 "#,
6356 None,
6357 ),
6358 (
6359 "an object field",
6364 r#"
6365 fn take(@o: { c: Color }) { return o.c }
6366 x = take({ c = Color::Green }) == Color::Green
6367 "#,
6368 None,
6369 ),
6370 (
6371 "a union that names the enum",
6372 r#"
6373 fn either(@v: Color | string) { return v }
6374 x = either(Color::Red) == Color::Red
6375 "#,
6376 None,
6377 ),
6378 (
6379 "the same union given the other member",
6380 r#"
6381 fn either(@v: Color | string) { return v }
6382 x = either("plain") == "plain"
6383 "#,
6384 None,
6385 ),
6386 (
6387 "another declaration at the same boundary",
6388 r#"
6389 fn paint(@c: Color) { return c }
6390 x = paint(Shade::Red) == Shade::Red
6391 "#,
6392 Some(
6393 "The input argument of `paint` requires a value with type `Color`, but found a value of enum `Shade` (with type `Shade`).",
6394 ),
6395 ),
6396 ] {
6397 let code = format!("{header}{body}\n");
6398 match expected {
6399 None => {
6400 let result = parse_execute(&code)
6401 .await
6402 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
6403 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x")
6404 else {
6405 panic!("case: {case}: `x` should hold a bool");
6406 };
6407 assert!(value, "case: {case}: the value did not survive the boundary");
6408 }
6409 Some(message) => assert_eq!(
6410 parse_execute(&code).await.unwrap_err().message(),
6411 message,
6412 "case: {case}"
6413 ),
6414 }
6415 }
6416 }
6417}