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