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::sketch_block_constraint_type;
10use cache::GlobalState;
11pub use cache::bust_cache;
12pub use cache::clear_mem_cache;
13pub use geometry::*;
14pub use id_generator::IdGenerator;
15pub(crate) use import::PreImportedGeometry;
16use indexmap::IndexMap;
17pub use kcl_api::Operation;
18pub use kcl_api::artifact::Artifact;
19pub use kcl_api::artifact::ArtifactGraph;
20pub use kcl_api::artifact::CapSubType;
21pub use kcl_api::artifact::CodeRef;
22pub use kcl_api::artifact::GdtAnnotationArtifact;
23pub use kcl_api::artifact::SketchBlock;
24pub use kcl_api::artifact::SketchBlockConstraint;
25#[allow(unused_imports)]
26pub use kcl_api::artifact::SketchBlockConstraintType;
27pub use kcl_api::artifact::StartSketchOnFace;
28pub use kcl_api::artifact::StartSketchOnPlane;
29use kcl_api::ast::node_path::NodePath;
30pub use kcl_value::KclObjectFields;
31pub use kcl_value::KclObjectKind;
32pub use kcl_value::KclValue;
33pub use kcl_value_view::KclValueView;
34use kcmc::ImageFormat;
35use kcmc::ModelingCmd;
36use kcmc::each_cmd as mcmd;
37use kcmc::ok_response::OkModelingCmdResponse;
38use kcmc::ok_response::output::TakeSnapshot;
39use kcmc::websocket::ModelingSessionData;
40use kcmc::websocket::OkWebSocketResponseData;
41use kittycad_modeling_cmds::id::ModelingCmdId;
42use kittycad_modeling_cmds::{self as kcmc};
43pub use memory::EnvironmentRef;
44#[cfg(test)]
45pub(crate) use memory::MemoryBackendKind;
46pub(crate) use modeling::ModelingCmdMeta;
47use serde::Deserialize;
48use serde::Serialize;
49pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
50pub(crate) use sketch_solve::solver_numeric_type;
51pub use sketch_transpiler::pre_execute_transpile;
52pub use sketch_transpiler::transpile_all_old_sketches_to_new;
53pub use sketch_transpiler::transpile_old_sketch_to_new;
54pub use sketch_transpiler::transpile_old_sketch_to_new_ast;
55pub use sketch_transpiler::transpile_old_sketch_to_new_with_execution;
56pub(crate) use solver_arc::SolverArc;
57pub(crate) use state::ConstraintKey;
58pub(crate) use state::ConstraintState;
59pub(crate) use state::ConsumedRegionInfo;
60pub(crate) use state::ConsumedRegionOperation;
61pub(crate) use state::ConsumedSolidInfo;
62pub(crate) use state::ConsumedSolidKey;
63pub(crate) use state::ConsumedSolidOperation;
64pub use state::DirectTagFilletMeta;
65pub use state::DirectTagFilletTagEntry;
66pub use state::EdgeRefactorMeta;
67pub use state::EdgeRefactorStdlibFn;
68pub use state::ExecState;
69pub(crate) use state::KclVersion;
70pub use state::MetaSettings;
71pub(crate) use state::ModuleArtifactState;
72pub(crate) use state::PendingEdgeRefactorMeta;
73pub use state::RefactorMetadata;
74pub(crate) use state::TangencyMode;
75
76use crate::CompilationIssue;
77use crate::ExecError;
78use crate::KclErrorWithOutputs;
79use crate::NodePathExt;
80use crate::SourceRange;
81use crate::collections::AhashIndexSet;
82use crate::engine::EngineBatchContext;
83use crate::engine::GridScaleBehavior;
84use crate::engine::engine_manager::EngineManager;
85use crate::errors::KclError;
86use crate::errors::KclErrorDetails;
87use crate::execution::cache::CacheInformation;
88use crate::execution::cache::CacheResult;
89use crate::execution::cad_op::OperationExt;
90use crate::execution::import_graph::Universe;
91use crate::execution::import_graph::UniverseMap;
92use crate::execution::typed_path::TypedPath;
93use crate::front::Number;
94use crate::front::Object;
95use crate::front::ObjectId;
96use crate::fs::FileManager;
97use crate::fs::FileSystemHandle;
98use crate::modules::ModuleExecutionOutcome;
99use crate::modules::ModuleId;
100use crate::modules::ModulePath;
101use crate::modules::ModuleRepr;
102use crate::parsing::ast::types::Expr;
103use crate::parsing::ast::types::ImportPath;
104use crate::parsing::ast::types::NodeRef;
105
106#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
107#[ts(export)]
108pub struct OperationsByModule {
109 pub map: IndexMap<ModuleId, Vec<Operation>>,
110}
111
112#[derive(Clone, Serialize, ts_rs::TS)]
113#[ts(export)]
114#[serde(rename_all = "camelCase")]
115pub struct OperationCallbackArgs {
116 pub module_id: ModuleId,
117 pub operation: Operation,
118 pub index: usize,
119}
120
121pub trait ExecutionCallbacks: std::fmt::Debug + Send + Sync + 'static {
122 fn on_operation(&self, _args: OperationCallbackArgs) {}
123}
124
125impl OperationsByModule {
126 pub fn count(&self) -> usize {
127 self.map.values().map(Vec::len).sum()
128 }
129
130 pub fn is_empty(&self) -> bool {
131 self.map.values().all(Vec::is_empty)
132 }
133
134 pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
135 self.map.get(module_id)
136 }
137
138 pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
139 self.map.values()
140 }
141
142 pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
143 self.map.insert(module_id, operations);
144 }
145}
146
147pub(crate) mod annotations;
148mod artifact;
149#[cfg(test)]
150pub(crate) use artifact::mermaid_tests::ArtifactGraphMermaidExt;
151pub(crate) mod cache;
152mod cad_op;
153mod exec_ast;
154pub mod fn_call;
155#[cfg(test)]
156mod freedom_analysis_tests;
157mod geometry;
158mod id_generator;
159mod import;
160mod import_graph;
161pub(crate) mod kcl_value;
162pub(crate) mod kcl_value_view;
163mod memory;
164mod modeling;
165mod sketch_solve;
166mod sketch_transpiler;
167mod solver_arc;
168mod state;
169pub mod typed_path;
170pub(crate) mod types;
171
172pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
173pub(crate) const SKETCH_OBJECT_META: &str = "meta";
174pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
175
176macro_rules! control_continue {
181 ($control_flow:expr) => {{
182 let cf = $control_flow;
183 if cf.is_some_return() {
184 return Ok(cf);
185 } else {
186 cf.into_value()
187 }
188 }};
189}
190pub(crate) use control_continue;
192
193macro_rules! early_return {
198 ($control_flow:expr) => {{
199 let cf = $control_flow;
200 if cf.is_some_return() {
201 return Err(EarlyReturn::from(cf));
202 } else {
203 cf.into_value()
204 }
205 }};
206}
207pub(crate) use early_return;
209
210#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
211pub enum ControlFlowKind {
212 #[default]
213 Continue,
214 Exit,
215}
216
217impl ControlFlowKind {
218 pub fn is_some_return(&self) -> bool {
220 match self {
221 ControlFlowKind::Continue => false,
222 ControlFlowKind::Exit => true,
223 }
224 }
225}
226
227#[must_use = "You should always handle the control flow value when it is returned"]
228#[derive(Debug, Clone, PartialEq, Serialize)]
229pub struct KclValueControlFlow {
230 value: Box<KclValue>,
232 pub control: ControlFlowKind,
233}
234
235impl KclValue {
236 pub(crate) fn continue_(self) -> KclValueControlFlow {
237 KclValueControlFlow {
238 value: Box::new(self),
239 control: ControlFlowKind::Continue,
240 }
241 }
242
243 pub(crate) fn exit(self) -> KclValueControlFlow {
244 KclValueControlFlow {
245 value: Box::new(self),
246 control: ControlFlowKind::Exit,
247 }
248 }
249}
250
251impl KclValueControlFlow {
252 pub fn is_some_return(&self) -> bool {
254 self.control.is_some_return()
255 }
256
257 pub(crate) fn into_value(self) -> KclValue {
258 *self.value
259 }
260}
261
262#[must_use = "You should always handle the control flow value when it is returned"]
269#[allow(clippy::large_enum_variant)]
270#[derive(Debug, Clone)]
271pub(crate) enum EarlyReturn {
272 Value(KclValueControlFlow),
274 Error(KclError),
276}
277
278impl From<KclValueControlFlow> for EarlyReturn {
279 fn from(cf: KclValueControlFlow) -> Self {
280 EarlyReturn::Value(cf)
281 }
282}
283
284impl From<KclError> for EarlyReturn {
285 fn from(err: KclError) -> Self {
286 EarlyReturn::Error(err)
287 }
288}
289
290pub(crate) enum StatementKind<'a> {
291 Declaration { name: &'a str },
292 Expression,
293}
294
295#[derive(Debug, Clone, Copy)]
296pub enum PreserveMem {
297 Normal,
298 Always,
299}
300
301impl PreserveMem {
302 fn normal(self) -> bool {
303 match self {
304 PreserveMem::Normal => true,
305 PreserveMem::Always => false,
306 }
307 }
308}
309
310#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
312#[ts(export)]
313#[serde(rename_all = "camelCase")]
314pub struct ExecOutcome {
315 pub variables: IndexMap<String, KclValueView>,
317 pub operations: OperationsByModule,
320 pub artifact_graph: ArtifactGraph,
322 #[serde(skip)]
324 pub scene_objects: Vec<Object>,
325 #[serde(skip)]
328 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
329 #[serde(skip)]
330 pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
331 pub refactor_metadata: Vec<RefactorMetadata>,
333 pub issues: Vec<CompilationIssue>,
335 pub filenames: IndexMap<ModuleId, ModulePath>,
337 pub default_planes: Option<DefaultPlanes>,
339}
340
341#[derive(Debug, Clone, Copy, PartialEq)]
345enum SegmentFreedom {
346 Free,
347 Fixed,
348 Conflict,
349 Error,
351}
352
353impl From<crate::front::Freedom> for SegmentFreedom {
354 fn from(f: crate::front::Freedom) -> Self {
355 match f {
356 crate::front::Freedom::Free => Self::Free,
357 crate::front::Freedom::Fixed => Self::Fixed,
358 crate::front::Freedom::Conflict => Self::Conflict,
359 }
360 }
361}
362
363#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
365pub enum ConstraintKind {
366 FullyConstrained,
367 UnderConstrained,
368 OverConstrained,
369 Error,
373}
374
375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
382pub struct SketchConstraintStatus {
383 pub name: String,
385 pub status: ConstraintKind,
387 pub free_count: usize,
389 pub conflict_count: usize,
391 pub total_count: usize,
393}
394
395#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
397pub struct SketchConstraintReport {
398 pub fully_constrained: Vec<SketchConstraintStatus>,
399 pub under_constrained: Vec<SketchConstraintStatus>,
400 pub over_constrained: Vec<SketchConstraintStatus>,
401 pub errors: Vec<SketchConstraintStatus>,
404}
405
406pub(crate) fn sketch_constraint_status_for_sketch(
415 scene_objects: &[Object],
416 sketch_obj: &Object,
417) -> Option<SketchConstraintStatus> {
418 use crate::front::ObjectKind;
419 use crate::front::Segment;
420
421 let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
422 return None;
423 };
424
425 let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
427 let obj = scene_objects.get(id.0)?;
428 if let ObjectKind::Segment {
429 segment: Segment::Point(p),
430 } = &obj.kind
431 {
432 Some(p.freedom())
433 } else {
434 None
435 }
436 };
437
438 let mut free_count: usize = 0;
439 let mut conflict_count: usize = 0;
440 let mut error_count: usize = 0;
441 let mut total_count: usize = 0;
442
443 for &seg_id in &sketch.segments {
444 let Some(seg_obj) = scene_objects.get(seg_id.0) else {
445 continue;
446 };
447 let ObjectKind::Segment { segment } = &seg_obj.kind else {
448 continue;
449 };
450 if let Segment::Point(p) = segment
453 && p.owner.is_some()
454 {
455 continue;
456 }
457 let freedom = segment
458 .freedom(lookup)
459 .map(SegmentFreedom::from)
460 .unwrap_or(SegmentFreedom::Error);
461 total_count += 1;
462 match freedom {
463 SegmentFreedom::Free => free_count += 1,
464 SegmentFreedom::Conflict => conflict_count += 1,
465 SegmentFreedom::Error => error_count += 1,
466 SegmentFreedom::Fixed => {}
467 }
468 }
469
470 let status = if error_count > 0 {
471 ConstraintKind::Error
472 } else if conflict_count > 0 {
473 ConstraintKind::OverConstrained
474 } else if free_count > 0 {
475 ConstraintKind::UnderConstrained
476 } else {
477 ConstraintKind::FullyConstrained
478 };
479
480 Some(SketchConstraintStatus {
481 name: sketch_obj.label.clone(),
482 status,
483 free_count,
484 conflict_count,
485 total_count,
486 })
487}
488
489pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
490 let mut fully_constrained = Vec::new();
491 let mut under_constrained = Vec::new();
492 let mut over_constrained = Vec::new();
493 let mut errors = Vec::new();
494
495 for obj in scene_objects {
496 let Some(entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
497 continue;
498 };
499 match entry.status {
500 ConstraintKind::FullyConstrained => fully_constrained.push(entry),
501 ConstraintKind::UnderConstrained => under_constrained.push(entry),
502 ConstraintKind::OverConstrained => over_constrained.push(entry),
503 ConstraintKind::Error => errors.push(entry),
504 }
505 }
506
507 SketchConstraintReport {
508 fully_constrained,
509 under_constrained,
510 over_constrained,
511 errors,
512 }
513}
514
515impl ExecOutcome {
516 pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
517 debug_assert!(
518 id.0 < self.scene_objects.len(),
519 "Requested object ID {} but only have {} objects",
520 id.0,
521 self.scene_objects.len()
522 );
523 self.scene_objects.get(id.0)
524 }
525
526 pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
528 self.issues.iter().filter(|error| error.is_err())
529 }
530
531 pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
538 sketch_constraint_report_from_scene_objects(&self.scene_objects)
539 }
540}
541
542#[derive(Debug, Clone, PartialEq)]
544pub struct MockConfig {
545 pub use_prev_memory: bool,
546 pub sketch_block_id: Option<ObjectId>,
549 pub freedom_analysis: bool,
552 pub segment_ids_edited: AhashIndexSet<ObjectId>,
554 pub drag_anchors: Vec<SegmentDragAnchor>,
556}
557
558#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
559#[ts(export, export_to = "FrontendApi.ts")]
560#[serde(rename_all = "camelCase")]
561pub struct SegmentDragAnchor {
562 pub segment_id: ObjectId,
563 pub target: crate::front::Point2d<Number>,
564}
565
566impl Default for MockConfig {
567 fn default() -> Self {
568 Self {
569 use_prev_memory: true,
571 sketch_block_id: None,
572 freedom_analysis: true,
573 segment_ids_edited: AhashIndexSet::default(),
574 drag_anchors: Vec::new(),
575 }
576 }
577}
578
579impl MockConfig {
580 pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
582 Self {
583 sketch_block_id: Some(sketch_block_id),
584 ..Default::default()
585 }
586 }
587
588 #[must_use]
589 pub(crate) fn no_freedom_analysis(mut self) -> Self {
590 self.freedom_analysis = false;
591 self
592 }
593}
594
595#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
596#[ts(export)]
597#[serde(rename_all = "camelCase")]
598pub struct DefaultPlanes {
599 pub xy: uuid::Uuid,
600 pub xz: uuid::Uuid,
601 pub yz: uuid::Uuid,
602 pub neg_xy: uuid::Uuid,
603 pub neg_xz: uuid::Uuid,
604 pub neg_yz: uuid::Uuid,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
608#[ts(export)]
609#[serde(tag = "type", rename_all = "camelCase")]
610pub struct TagIdentifier {
611 pub value: String,
612 #[serde(skip)]
615 pub info: Vec<(usize, TagEngineInfo)>,
616 #[serde(skip)]
617 pub meta: Vec<Metadata>,
618}
619
620impl TagIdentifier {
621 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
623 for (e, info) in self.info.iter().rev() {
624 if *e <= at_epoch {
625 return Some(info);
626 }
627 }
628
629 None
630 }
631
632 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
634 self.info.last().map(|i| &i.1)
635 }
636
637 pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
640 let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
641 return vec![];
642 };
643 self.info
644 .iter()
645 .rev()
646 .take_while(|(e, _)| *e == cur_epoch)
647 .map(|(_, info)| info)
648 .collect()
649 }
650
651 pub fn merge_info(&mut self, other: &TagIdentifier) {
653 assert_eq!(&self.value, &other.value);
654 for (oe, ot) in &other.info {
655 if let Some((e, t)) = self.info.last_mut() {
656 if *e > *oe {
658 continue;
659 }
660 if e == oe {
662 *t = ot.clone();
663 continue;
664 }
665 }
666 self.info.push((*oe, ot.clone()));
667 }
668 }
669
670 pub fn geometry(&self) -> Option<Geometry> {
671 self.get_cur_info().map(|info| info.geometry.clone())
672 }
673
674 pub(crate) fn is_body_created_tag(&self) -> bool {
675 self.get_cur_info().is_some_and(|info| {
676 matches!(&info.geometry, Geometry::Solid(_)) && info.path.is_none() && info.surface.is_some()
677 })
678 }
679}
680
681impl Eq for TagIdentifier {}
682
683impl std::fmt::Display for TagIdentifier {
684 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685 write!(f, "{}", self.value)
686 }
687}
688
689impl std::str::FromStr for TagIdentifier {
690 type Err = KclError;
691
692 fn from_str(s: &str) -> Result<Self, Self::Err> {
693 Ok(Self {
694 value: s.to_string(),
695 info: Vec::new(),
696 meta: Default::default(),
697 })
698 }
699}
700
701impl Ord for TagIdentifier {
702 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
703 self.value.cmp(&other.value)
704 }
705}
706
707impl PartialOrd for TagIdentifier {
708 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
709 Some(self.cmp(other))
710 }
711}
712
713impl std::hash::Hash for TagIdentifier {
714 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
715 self.value.hash(state);
716 }
717}
718
719#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
721#[ts(export)]
722#[serde(tag = "type", rename_all = "camelCase")]
723pub struct TagEngineInfo {
724 pub id: uuid::Uuid,
726 pub geometry: Geometry,
728 pub path: Option<Path>,
730 pub surface: Option<ExtrudeSurface>,
732}
733
734#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
735pub enum BodyType {
736 Root,
737 Block,
738}
739
740#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
742#[ts(export)]
743#[serde(rename_all = "camelCase")]
744pub struct Metadata {
745 pub source_range: SourceRange,
747}
748
749impl From<Metadata> for Vec<SourceRange> {
750 fn from(meta: Metadata) -> Self {
751 vec![meta.source_range]
752 }
753}
754
755impl From<&Metadata> for SourceRange {
756 fn from(meta: &Metadata) -> Self {
757 meta.source_range
758 }
759}
760
761impl From<SourceRange> for Metadata {
762 fn from(source_range: SourceRange) -> Self {
763 Self { source_range }
764 }
765}
766
767impl<T> From<NodeRef<'_, T>> for Metadata {
768 fn from(node: NodeRef<'_, T>) -> Self {
769 Self {
770 source_range: SourceRange::new(node.start, node.end, node.module_id),
771 }
772 }
773}
774
775impl From<&Expr> for Metadata {
776 fn from(expr: &Expr) -> Self {
777 Self {
778 source_range: SourceRange::from(expr),
779 }
780 }
781}
782
783impl Metadata {
784 pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
785 if meta.len() == 1 {
786 let meta = &meta[0];
787 return crate::front::SourceRef::Simple {
788 range: meta.source_range,
789 node_path,
790 };
791 }
792 crate::front::SourceRef::BackTrace {
793 ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
794 }
795 }
796}
797
798#[derive(PartialEq, Debug, Default, Clone)]
800pub enum ContextType {
801 #[default]
803 Live,
804
805 Mock,
809
810 MockCustomForwarded,
812}
813
814#[derive(Clone)]
818pub struct ExecutorContext {
819 pub engine: Arc<EngineManager>,
820 pub engine_batch: EngineBatchContext,
821 pub fs: FileSystemHandle,
822 pub settings: ExecutorSettings,
823 pub context_type: ContextType,
824 pub execution_callbacks: Option<Arc<dyn ExecutionCallbacks>>,
825}
826
827impl std::fmt::Debug for ExecutorContext {
828 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
829 f.debug_struct("ExecutorContext")
830 .field("engine", &self.engine)
831 .field("engine_batch", &self.engine_batch)
832 .field("settings", &self.settings)
833 .field("context_type", &self.context_type)
834 .field("execution_callbacks", &self.execution_callbacks)
835 .finish()
836 }
837}
838
839#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
841#[ts(export)]
842pub struct ExecutorSettings {
843 pub highlight_edges: bool,
845 pub enable_ssao: bool,
847 pub show_grid: bool,
849 pub replay: Option<String>,
852 pub project_directory: Option<TypedPath>,
855 pub current_file: Option<TypedPath>,
858 pub fixed_size_grid: bool,
860 #[serde(default, skip_serializing_if = "is_false")]
866 pub skip_artifact_graph: bool,
867 #[serde(default, skip_serializing_if = "Option::is_none")]
870 pub heartbeats: Option<u64>,
871 #[serde(default, skip_serializing_if = "Option::is_none")]
874 pub default_backface_color: Option<String>,
875}
876
877fn is_false(b: &bool) -> bool {
878 !*b
879}
880
881impl Default for ExecutorSettings {
882 fn default() -> Self {
883 Self {
884 highlight_edges: true,
885 enable_ssao: false,
886 show_grid: false,
887 replay: None,
888 project_directory: None,
889 current_file: None,
890 fixed_size_grid: true,
891 skip_artifact_graph: false,
892 heartbeats: None,
893 default_backface_color: None,
894 }
895 }
896}
897
898impl From<crate::settings::types::Configuration> for ExecutorSettings {
899 fn from(config: crate::settings::types::Configuration) -> Self {
900 Self::from(config.settings)
901 }
902}
903
904impl From<crate::settings::types::Settings> for ExecutorSettings {
905 fn from(settings: crate::settings::types::Settings) -> Self {
906 let modeling_settings = settings.modeling.unwrap_or_default();
907 Self {
908 highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
909 enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
910 show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
911 replay: None,
912 project_directory: None,
913 current_file: None,
914 fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
915 skip_artifact_graph: false,
916 heartbeats: None,
917 default_backface_color: modeling_settings.backface_color.map(|color| color.0),
918 }
919 }
920}
921
922impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
923 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
924 Self::from(config.settings.modeling)
925 }
926}
927
928impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
929 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
930 Self {
931 highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
932 enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
933 show_grid: modeling.show_scale_grid.unwrap_or_default(),
934 replay: None,
935 project_directory: None,
936 current_file: None,
937 fixed_size_grid: true,
938 skip_artifact_graph: false,
939 heartbeats: None,
940 default_backface_color: modeling.backface_color.map(|color| color.0),
941 }
942 }
943}
944
945impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
946 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
947 Self {
948 highlight_edges: modeling.highlight_edges.into(),
949 enable_ssao: modeling.enable_ssao.into(),
950 show_grid: Default::default(),
951 replay: None,
952 project_directory: None,
953 current_file: None,
954 fixed_size_grid: true,
955 skip_artifact_graph: false,
956 heartbeats: None,
957 default_backface_color: None,
958 }
959 }
960}
961
962impl ExecutorSettings {
963 pub fn with_current_file(&mut self, current_file: TypedPath) {
965 if current_file.extension() == Some("kcl") {
967 self.current_file = Some(current_file.clone());
968 if let Some(parent) = current_file.parent() {
970 self.project_directory = Some(parent);
971 } else {
972 self.project_directory = Some(TypedPath::from(""));
973 }
974 } else {
975 self.project_directory = Some(current_file);
976 }
977 }
978}
979
980impl ExecutorContext {
981 pub fn new_with_engine_and_fs(
983 engine: Arc<EngineManager>,
984 fs: FileSystemHandle,
985 settings: ExecutorSettings,
986 ) -> Self {
987 ExecutorContext {
988 engine,
989 engine_batch: EngineBatchContext::default(),
990 fs,
991 settings,
992 context_type: ContextType::Live,
993 execution_callbacks: Default::default(),
994 }
995 }
996
997 fn clone_with_fresh_execution_batch(&self) -> Self {
998 Self {
999 engine: self.engine.clone(),
1000 engine_batch: EngineBatchContext::new(),
1001 fs: self.fs.clone(),
1002 settings: self.settings.clone(),
1003 context_type: self.context_type.clone(),
1004 execution_callbacks: self.execution_callbacks.clone(),
1005 }
1006 }
1007
1008 #[cfg(not(target_arch = "wasm32"))]
1010 pub fn new_with_engine(engine: Arc<EngineManager>, settings: ExecutorSettings) -> Self {
1011 Self::new_with_engine_and_fs(engine, crate::fs::new_file_system_handle(FileManager::new()), settings)
1012 }
1013
1014 #[cfg(not(target_arch = "wasm32"))]
1016 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
1017 let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
1018 let (ws, _headers) = client
1019 .modeling()
1020 .commands_ws(kittycad::modeling::CommandsWsParams {
1021 api_call_id: None,
1022 fps: None,
1023 order_independent_transparency: None,
1024 post_effect: if settings.enable_ssao {
1025 Some(kittycad::types::PostEffectType::Ssao)
1026 } else {
1027 None
1028 },
1029 replay: settings.replay.clone(),
1030 show_grid: if settings.show_grid { Some(true) } else { None },
1031 pool: None,
1032 pr,
1033 unlocked_framerate: None,
1034 webrtc: Some(false),
1035 video_res_width: None,
1036 video_res_height: None,
1037 })
1038 .await?;
1039
1040 let engine_conn = EngineManager::new_websocket_transport(ws, settings.heartbeats).await;
1041 let engine = Arc::new(engine_conn);
1042
1043 Ok(Self::new_with_engine(engine, settings))
1044 }
1045
1046 #[cfg(target_arch = "wasm32")]
1047 pub fn new(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1048 Self::new_with_engine_and_fs(engine, fs, settings)
1049 }
1050
1051 #[cfg(not(target_arch = "wasm32"))]
1052 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
1053 ExecutorContext {
1054 engine: Arc::new(EngineManager::new_mock()),
1055 engine_batch: EngineBatchContext::default(),
1056 fs: crate::fs::new_file_system_handle(FileManager::new()),
1057 settings: settings.unwrap_or_default(),
1058 context_type: ContextType::Mock,
1059 execution_callbacks: Default::default(),
1060 }
1061 }
1062
1063 #[cfg(target_arch = "wasm32")]
1064 pub fn new_mock(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1065 ExecutorContext {
1066 engine,
1067 engine_batch: EngineBatchContext::default(),
1068 fs,
1069 settings,
1070 context_type: ContextType::Mock,
1071 execution_callbacks: Default::default(),
1072 }
1073 }
1074
1075 #[cfg(target_arch = "wasm32")]
1078 pub fn new_mock_for_lsp(
1079 fs_manager: crate::fs::wasm::FileSystemManager,
1080 settings: ExecutorSettings,
1081 ) -> Result<Self, String> {
1082 let fs = crate::fs::new_file_system_handle(FileManager::new(fs_manager));
1083
1084 Ok(ExecutorContext {
1085 engine: Arc::new(EngineManager::new_mock()),
1086 engine_batch: EngineBatchContext::default(),
1087 fs,
1088 settings,
1089 context_type: ContextType::Mock,
1090 execution_callbacks: Default::default(),
1091 })
1092 }
1093
1094 #[cfg(not(target_arch = "wasm32"))]
1095 pub fn new_forwarded_mock(engine: Arc<EngineManager>) -> Self {
1096 ExecutorContext {
1097 engine,
1098 engine_batch: EngineBatchContext::default(),
1099 fs: crate::fs::new_file_system_handle(FileManager::new()),
1100 settings: Default::default(),
1101 context_type: ContextType::MockCustomForwarded,
1102 execution_callbacks: Default::default(),
1103 }
1104 }
1105
1106 #[cfg(not(target_arch = "wasm32"))]
1112 pub async fn new_with_client(
1113 settings: ExecutorSettings,
1114 token: Option<String>,
1115 engine_addr: Option<String>,
1116 ) -> Result<Self> {
1117 let client = crate::engine::new_zoo_client(token, engine_addr)?;
1119
1120 let ctx = Self::new(&client, settings).await?;
1121 Ok(ctx)
1122 }
1123
1124 #[cfg(not(target_arch = "wasm32"))]
1129 pub async fn new_with_default_client() -> Result<Self> {
1130 let ctx = Self::new_with_client(Default::default(), None, None).await?;
1132 Ok(ctx)
1133 }
1134
1135 #[cfg(not(target_arch = "wasm32"))]
1137 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
1138 let ctx = ExecutorContext::new_with_client(
1139 ExecutorSettings {
1140 highlight_edges: true,
1141 enable_ssao: false,
1142 show_grid: false,
1143 replay: None,
1144 project_directory: None,
1145 current_file: None,
1146 fixed_size_grid: false,
1147 skip_artifact_graph: false,
1148 heartbeats: None,
1149 default_backface_color: None,
1150 },
1151 None,
1152 engine_addr,
1153 )
1154 .await?;
1155 Ok(ctx)
1156 }
1157
1158 pub fn is_mock(&self) -> bool {
1159 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1160 }
1161
1162 pub async fn no_engine_commands(&self) -> bool {
1164 self.is_mock()
1165 }
1166
1167 pub async fn send_clear_scene(
1168 &self,
1169 exec_state: &mut ExecState,
1170 source_range: crate::execution::SourceRange,
1171 ) -> Result<(), KclError> {
1172 exec_state.mod_local.artifacts.clear();
1175 exec_state.global.root_module_artifacts.clear();
1176 exec_state.global.artifacts.clear();
1177
1178 self.engine
1179 .clear_scene(&self.engine_batch, &mut exec_state.mod_local.id_generator, source_range)
1180 .await?;
1181 if self.settings.enable_ssao {
1184 let cmd_id = exec_state.next_uuid();
1185 exec_state
1186 .batch_modeling_cmd(
1187 ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1188 ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1189 )
1190 .await?;
1191 }
1192 Ok(())
1193 }
1194
1195 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1196 cache::bust_cache().await;
1197
1198 let outcome = self.run_with_caching(crate::Program::empty()).await?;
1203
1204 Ok(outcome)
1205 }
1206
1207 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1208 self.eval_prelude(exec_state, SourceRange::synthetic())
1209 .await
1210 .map_err(KclErrorWithOutputs::no_outputs)?;
1211 exec_state
1212 .mut_stack()
1213 .push_new_root_env(true)
1214 .map_err(KclErrorWithOutputs::no_outputs)?;
1215 Ok(())
1216 }
1217
1218 fn restore_mock_memory(
1219 exec_state: &mut ExecState,
1220 mem: cache::SketchModeState,
1221 _mock_config: &MockConfig,
1222 ) -> Result<(), KclErrorWithOutputs> {
1223 *exec_state.mut_stack() = mem.stack;
1224 exec_state.global.module_infos = mem.module_infos;
1225 exec_state.global.path_to_source_id = mem.path_to_source_id;
1226 exec_state.global.id_to_source = mem.id_to_source;
1227 exec_state.mod_local.constraint_state = mem.constraint_state;
1228 let len = _mock_config
1229 .sketch_block_id
1230 .map(|sketch_block_id| sketch_block_id.0)
1231 .unwrap_or(0);
1232 if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1233 exec_state
1234 .global
1235 .root_module_artifacts
1236 .restore_scene_objects(scene_objects);
1237 } else {
1238 let message = format!(
1239 "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1240 mem.scene_objects.len(),
1241 len
1242 );
1243 debug_assert!(false, "{message}");
1244 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1245 KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1246 )));
1247 }
1248
1249 Ok(())
1250 }
1251
1252 pub async fn run_mock(
1253 &self,
1254 program: &crate::Program,
1255 mock_config: &MockConfig,
1256 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1257 assert!(
1258 self.is_mock(),
1259 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1260 );
1261
1262 let use_prev_memory = mock_config.use_prev_memory;
1263 let mut exec_state = ExecState::new_mock(self, mock_config);
1264 if use_prev_memory {
1265 match cache::read_old_memory().await {
1266 Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
1267 None => self.prepare_mem(&mut exec_state).await?,
1268 }
1269 } else {
1270 self.prepare_mem(&mut exec_state).await?
1271 };
1272
1273 exec_state
1276 .mut_stack()
1277 .push_new_env_for_scope()
1278 .map_err(KclErrorWithOutputs::no_outputs)?;
1279
1280 let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1281
1282 let mut stack = exec_state.stack().clone();
1287 let module_infos = exec_state.global.module_infos.clone();
1288 let path_to_source_id = exec_state.global.path_to_source_id.clone();
1289 let id_to_source = exec_state.global.id_to_source.clone();
1290 let constraint_state = exec_state.mod_local.constraint_state.clone();
1291 let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1292 let outcome = exec_state
1293 .into_exec_outcome(result.0, self)
1294 .await
1295 .map_err(KclErrorWithOutputs::no_outputs)?;
1296
1297 stack.squash_env(result.0).map_err(KclErrorWithOutputs::no_outputs)?;
1298 let state = cache::SketchModeState {
1299 stack,
1300 module_infos,
1301 path_to_source_id,
1302 id_to_source,
1303 constraint_state,
1304 scene_objects,
1305 };
1306 cache::write_old_memory(state).await;
1307
1308 Ok(outcome)
1309 }
1310
1311 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1312 assert!(!self.is_mock());
1313 let grid_scale = if self.settings.fixed_size_grid {
1314 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1315 } else {
1316 GridScaleBehavior::ScaleWithZoom
1317 };
1318
1319 let original_program = program.clone();
1320
1321 let (_program, exec_state, result) = match cache::read_old_ast().await {
1322 Some(mut cached_state) => {
1323 let old = CacheInformation {
1324 ast: &cached_state.main.ast,
1325 settings: &cached_state.settings,
1326 };
1327 let new = CacheInformation {
1328 ast: &program.ast,
1329 settings: &self.settings,
1330 };
1331
1332 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1334 CacheResult::ReExecute {
1335 clear_scene,
1336 reapply_settings,
1337 program: changed_program,
1338 } => {
1339 if reapply_settings
1340 && self
1341 .engine
1342 .reapply_settings(
1343 &self.engine_batch,
1344 &self.settings,
1345 Default::default(),
1346 &mut cached_state.main.exec_state.id_generator,
1347 grid_scale,
1348 )
1349 .await
1350 .is_err()
1351 {
1352 (true, program, None)
1353 } else {
1354 (
1355 clear_scene,
1356 crate::Program {
1357 ast: changed_program,
1358 original_file_contents: program.original_file_contents,
1359 },
1360 None,
1361 )
1362 }
1363 }
1364 CacheResult::CheckImportsOnly {
1365 reapply_settings,
1366 ast: changed_program,
1367 } => {
1368 let mut reapply_failed = false;
1369 if reapply_settings {
1370 if self
1371 .engine
1372 .reapply_settings(
1373 &self.engine_batch,
1374 &self.settings,
1375 Default::default(),
1376 &mut cached_state.main.exec_state.id_generator,
1377 grid_scale,
1378 )
1379 .await
1380 .is_ok()
1381 {
1382 cache::write_old_ast(GlobalState::with_settings(
1383 cached_state.clone(),
1384 self.settings.clone(),
1385 ))
1386 .await;
1387 } else {
1388 reapply_failed = true;
1389 }
1390 }
1391
1392 if reapply_failed {
1393 (true, program, None)
1394 } else {
1395 let mut new_exec_state = ExecState::new(self);
1397 let (new_universe, new_universe_map) =
1398 self.get_universe(&program, &mut new_exec_state).await?;
1399
1400 let clear_scene = new_universe.values().any(|value| {
1401 let id = value.1;
1402 match (
1403 cached_state.exec_state.get_source(id),
1404 new_exec_state.global.get_source(id),
1405 ) {
1406 (Some(s0), Some(s1)) => s0.source != s1.source,
1407 _ => false,
1408 }
1409 });
1410
1411 if !clear_scene {
1412 cache::write_old_memory(
1414 cached_state
1415 .mock_memory_state()
1416 .map_err(KclErrorWithOutputs::no_outputs)?,
1417 )
1418 .await;
1419 return cached_state
1420 .into_exec_outcome(self)
1421 .await
1422 .map_err(KclErrorWithOutputs::no_outputs);
1423 }
1424
1425 (
1426 true,
1427 crate::Program {
1428 ast: changed_program,
1429 original_file_contents: program.original_file_contents,
1430 },
1431 Some((new_universe, new_universe_map, new_exec_state)),
1432 )
1433 }
1434 }
1435 CacheResult::NoAction(true) => {
1436 if self
1437 .engine
1438 .reapply_settings(
1439 &self.engine_batch,
1440 &self.settings,
1441 Default::default(),
1442 &mut cached_state.main.exec_state.id_generator,
1443 grid_scale,
1444 )
1445 .await
1446 .is_ok()
1447 {
1448 cache::write_old_ast(GlobalState::with_settings(
1450 cached_state.clone(),
1451 self.settings.clone(),
1452 ))
1453 .await;
1454
1455 cache::write_old_memory(
1456 cached_state
1457 .mock_memory_state()
1458 .map_err(KclErrorWithOutputs::no_outputs)?,
1459 )
1460 .await;
1461 return cached_state
1462 .into_exec_outcome(self)
1463 .await
1464 .map_err(KclErrorWithOutputs::no_outputs);
1465 }
1466 (true, program, None)
1467 }
1468 CacheResult::NoAction(false) => {
1469 cache::write_old_memory(
1470 cached_state
1471 .mock_memory_state()
1472 .map_err(KclErrorWithOutputs::no_outputs)?,
1473 )
1474 .await;
1475 return cached_state
1476 .into_exec_outcome(self)
1477 .await
1478 .map_err(KclErrorWithOutputs::no_outputs);
1479 }
1480 };
1481
1482 let (exec_state, result) = match import_check_info {
1483 Some((new_universe, new_universe_map, mut new_exec_state)) => {
1484 self.send_clear_scene(&mut new_exec_state, Default::default())
1486 .await
1487 .map_err(KclErrorWithOutputs::no_outputs)?;
1488
1489 let result = self
1490 .run_concurrent(
1491 &program,
1492 &mut new_exec_state,
1493 Some((new_universe, new_universe_map)),
1494 PreserveMem::Normal,
1495 )
1496 .await;
1497
1498 (new_exec_state, result)
1499 }
1500 None if clear_scene => {
1501 let mut exec_state = cached_state.reconstitute_exec_state(self);
1503 exec_state.reset(self);
1504
1505 self.send_clear_scene(&mut exec_state, Default::default())
1506 .await
1507 .map_err(KclErrorWithOutputs::no_outputs)?;
1508
1509 let result = self
1510 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1511 .await;
1512
1513 (exec_state, result)
1514 }
1515 None => {
1516 let mut exec_state = cached_state.reconstitute_exec_state(self);
1517 exec_state
1518 .mut_stack()
1519 .restore_env(cached_state.main.result_env)
1520 .map_err(KclErrorWithOutputs::no_outputs)?;
1521
1522 let result = self
1523 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1524 .await;
1525
1526 (exec_state, result)
1527 }
1528 };
1529
1530 (program, exec_state, result)
1531 }
1532 None => {
1533 let mut exec_state = ExecState::new(self);
1534 self.send_clear_scene(&mut exec_state, Default::default())
1535 .await
1536 .map_err(KclErrorWithOutputs::no_outputs)?;
1537
1538 let result = self
1539 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1540 .await;
1541
1542 (program, exec_state, result)
1543 }
1544 };
1545
1546 if result.is_err() {
1547 cache::bust_cache().await;
1548 }
1549
1550 let result = result?;
1552
1553 cache::write_old_ast(GlobalState::new(
1557 exec_state.clone(),
1558 self.settings.clone(),
1559 original_program.ast,
1560 result.0,
1561 ))
1562 .await;
1563
1564 let outcome = exec_state
1565 .into_exec_outcome(result.0, self)
1566 .await
1567 .map_err(KclErrorWithOutputs::no_outputs)?;
1568 Ok(outcome)
1569 }
1570
1571 pub async fn run(
1575 &self,
1576 program: &crate::Program,
1577 exec_state: &mut ExecState,
1578 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1579 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1580 .await
1581 }
1582
1583 pub async fn run_concurrent(
1588 &self,
1589 program: &crate::Program,
1590 exec_state: &mut ExecState,
1591 universe_info: Option<(Universe, UniverseMap)>,
1592 preserve_mem: PreserveMem,
1593 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1594 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1597 (universe, universe_map)
1598 } else {
1599 self.get_universe(program, exec_state).await?
1600 };
1601
1602 let mut sorted_imports: Vec<_> = universe_map.iter().collect();
1608 sorted_imports.sort_by_key(|(_, import_stmt)| SourceRange::from(*import_stmt));
1609 for (_path, import_stmt) in sorted_imports {
1610 let filename = match &import_stmt.path {
1614 ImportPath::Kcl { filename } => filename.to_string(),
1615 ImportPath::Foreign { path } => path.to_string(),
1616 ImportPath::Std { .. } => continue,
1617 };
1618 if let Some((_, module_id, module_path, _)) = universe.get(&filename)
1619 && let ModulePath::Local { value, .. } = module_path
1620 {
1621 let name = import_stmt
1622 .module_name()
1623 .unwrap_or_else(|| value.file_name().unwrap_or_default());
1624 let source_range = SourceRange::from(import_stmt);
1625 exec_state.push_op(crate::execution::cad_op::Operation::ModuleInstance {
1626 name,
1627 module_id: *module_id,
1628 glob: matches!(
1629 import_stmt.selector,
1630 crate::parsing::ast::types::ImportSelector::Glob(_)
1631 ),
1632 node_path: crate::NodePath::placeholder(),
1633 source_range,
1634 });
1635 }
1636 }
1637
1638 let default_planes = self.engine.get_default_planes().read().await.clone();
1639
1640 self.eval_prelude(exec_state, SourceRange::synthetic())
1642 .await
1643 .map_err(KclErrorWithOutputs::no_outputs)?;
1644
1645 for modules in import_graph::import_graph(&universe, self)
1646 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1647 .into_iter()
1648 {
1649 #[cfg(not(target_arch = "wasm32"))]
1650 let mut set = tokio::task::JoinSet::new();
1651
1652 #[allow(clippy::type_complexity)]
1653 let (results_tx, mut results_rx): (
1654 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1655 tokio::sync::mpsc::Receiver<_>,
1656 ) = tokio::sync::mpsc::channel(1);
1657
1658 for module in modules {
1659 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1660 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1661 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1662 )));
1663 };
1664 let module_id = *module_id;
1665 let module_path = module_path.clone();
1666 let source_range = SourceRange::from(import_stmt);
1667 let module_exec_state = exec_state.clone();
1669
1670 let repr = repr.clone();
1671 let exec_ctxt = self.clone_with_fresh_execution_batch();
1672 let results_tx = results_tx.clone();
1673
1674 let exec_module = async |exec_ctxt: &ExecutorContext,
1675 repr: &ModuleRepr,
1676 module_id: ModuleId,
1677 module_path: &ModulePath,
1678 exec_state: &mut ExecState,
1679 source_range: SourceRange|
1680 -> Result<ModuleRepr, KclError> {
1681 match repr {
1682 ModuleRepr::Kcl(program, _) => {
1683 let result = exec_ctxt
1684 .exec_module_from_ast(
1685 program,
1686 module_id,
1687 module_path,
1688 exec_state,
1689 source_range,
1690 PreserveMem::Normal,
1691 )
1692 .await;
1693
1694 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1695 }
1696 ModuleRepr::Foreign(geom, _) => {
1697 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1698 .await
1699 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1700
1701 result.map(|val| ModuleRepr::Foreign(geom.clone(), Some((val, Default::default()))))
1706 }
1707 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1708 format!("Module {module_path} not found in universe"),
1709 vec![source_range],
1710 ))),
1711 }
1712 };
1713
1714 #[cfg(target_arch = "wasm32")]
1715 {
1716 wasm_bindgen_futures::spawn_local(async move {
1717 let mut exec_state = module_exec_state;
1718 let exec_ctxt = exec_ctxt;
1719
1720 let result = exec_module(
1721 &exec_ctxt,
1722 &repr,
1723 module_id,
1724 &module_path,
1725 &mut exec_state,
1726 source_range,
1727 )
1728 .await;
1729
1730 results_tx
1731 .send((module_id, module_path, result))
1732 .await
1733 .unwrap_or_default();
1734 });
1735 }
1736 #[cfg(not(target_arch = "wasm32"))]
1737 {
1738 set.spawn(async move {
1739 let mut exec_state = module_exec_state;
1740 let exec_ctxt = exec_ctxt;
1741
1742 let result = exec_module(
1743 &exec_ctxt,
1744 &repr,
1745 module_id,
1746 &module_path,
1747 &mut exec_state,
1748 source_range,
1749 )
1750 .await;
1751
1752 results_tx
1753 .send((module_id, module_path, result))
1754 .await
1755 .unwrap_or_default();
1756 });
1757 }
1758 }
1759
1760 drop(results_tx);
1761
1762 while let Some((module_id, _, result)) = results_rx.recv().await {
1763 match result {
1764 Ok(new_repr) => {
1765 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1766
1767 match &mut repr {
1768 ModuleRepr::Kcl(_, cache) => {
1769 let ModuleRepr::Kcl(_, session_data) = new_repr else {
1770 unreachable!();
1771 };
1772 *cache = session_data;
1773 }
1774 ModuleRepr::Foreign(_, cache) => {
1775 let ModuleRepr::Foreign(_, session_data) = new_repr else {
1776 unreachable!();
1777 };
1778 *cache = session_data;
1779 }
1780 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1781 }
1782
1783 exec_state.global.module_infos[&module_id].restore_repr(repr);
1784 }
1785 Err(e) => {
1786 return Err(exec_state.error_with_outputs(e, None, default_planes));
1787 }
1788 }
1789 }
1790 }
1791
1792 exec_state.mod_local.artifacts.operations.clear();
1797
1798 exec_state
1801 .global
1802 .root_module_artifacts
1803 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1804
1805 self.inner_run(program, exec_state, preserve_mem).await
1806 }
1807
1808 async fn get_universe(
1811 &self,
1812 program: &crate::Program,
1813 exec_state: &mut ExecState,
1814 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1815 exec_state.add_root_module_contents(program);
1816
1817 let mut universe = std::collections::HashMap::new();
1818
1819 let default_planes = self.engine.get_default_planes().read().await.clone();
1820
1821 let root_imports = import_graph::import_universe(
1822 self,
1823 &ModulePath::Main,
1824 &ModuleRepr::Kcl(program.ast.clone(), None),
1825 &mut universe,
1826 exec_state,
1827 )
1828 .await
1829 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1830
1831 Ok((universe, root_imports))
1832 }
1833
1834 async fn inner_run(
1837 &self,
1838 program: &crate::Program,
1839 exec_state: &mut ExecState,
1840 preserve_mem: PreserveMem,
1841 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1842 let _stats = crate::log::LogPerfStats::new("Interpretation");
1843
1844 let grid_scale = if self.settings.fixed_size_grid {
1846 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1847 } else {
1848 GridScaleBehavior::ScaleWithZoom
1849 };
1850 self.engine
1851 .reapply_settings(
1852 &self.engine_batch,
1853 &self.settings,
1854 Default::default(),
1855 exec_state.id_generator(),
1856 grid_scale,
1857 )
1858 .await
1859 .map_err(KclErrorWithOutputs::no_outputs)?;
1860
1861 let default_planes = self.engine.get_default_planes().read().await.clone();
1862 let result = self
1863 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1864 .await;
1865
1866 crate::log::log(format!(
1867 "Post interpretation KCL memory stats: {:#?}",
1868 exec_state.stack().memory.stats()
1869 ));
1870 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1871
1872 async fn write_old_memory(
1875 ctx: &ExecutorContext,
1876 exec_state: &ExecState,
1877 env_ref: EnvironmentRef,
1878 ) -> Result<(), KclError> {
1879 if ctx.is_mock() {
1880 return Ok(());
1881 }
1882 let mut stack = exec_state.stack().deep_clone()?;
1883 stack.restore_env(env_ref)?;
1884 let state = cache::SketchModeState {
1885 stack,
1886 module_infos: exec_state.global.module_infos.clone(),
1887 path_to_source_id: exec_state.global.path_to_source_id.clone(),
1888 id_to_source: exec_state.global.id_to_source.clone(),
1889 constraint_state: exec_state.mod_local.constraint_state.clone(),
1890 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1891 };
1892 cache::write_old_memory(state).await;
1893 Ok(())
1894 }
1895
1896 let env_ref = match result {
1897 Ok(env_ref) => env_ref,
1898 Err((err, env_ref)) => {
1899 if let Some(env_ref) = env_ref {
1902 write_old_memory(self, exec_state, env_ref)
1903 .await
1904 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
1905 }
1906 return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
1907 }
1908 };
1909
1910 write_old_memory(self, exec_state, env_ref)
1911 .await
1912 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
1913
1914 let session_data = self.engine.get_session_data().await;
1915
1916 Ok((env_ref, session_data))
1917 }
1918
1919 async fn execute_and_build_graph(
1922 &self,
1923 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1924 exec_state: &mut ExecState,
1925 preserve_mem: PreserveMem,
1926 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1927 let start_op = exec_state.global.root_module_artifacts.operations.len();
1933
1934 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1935 .await
1936 .map_err(|e| (e, None))?;
1937
1938 let exec_result = self
1939 .exec_module_body(
1940 program,
1941 exec_state,
1942 preserve_mem,
1943 ModuleId::default(),
1944 &ModulePath::Main,
1945 )
1946 .await
1947 .map(
1948 |ModuleExecutionOutcome {
1949 environment: env_ref,
1950 artifacts: module_artifacts,
1951 ..
1952 }| {
1953 exec_state.global.root_module_artifacts.extend(module_artifacts);
1956 env_ref
1957 },
1958 )
1959 .map_err(|(err, env_ref, module_artifacts)| {
1960 if let Some(module_artifacts) = module_artifacts {
1961 exec_state.global.root_module_artifacts.extend(module_artifacts);
1964 }
1965 (err, env_ref)
1966 });
1967
1968 let programs = &exec_state.build_program_lookup(program.clone());
1970 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1971 for op in exec_state
1972 .global
1973 .root_module_artifacts
1974 .operations
1975 .iter_mut()
1976 .skip(start_op)
1977 {
1978 op.fill_node_paths(programs, cached_body_items);
1979 }
1980 for module in exec_state.global.module_infos.values_mut() {
1981 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1982 for op in &mut outcome.artifacts.operations {
1983 op.fill_node_paths(programs, cached_body_items);
1984 }
1985 }
1986 }
1987
1988 self.engine
1990 .ensure_async_commands_completed(&self.engine_batch)
1991 .await
1992 .map_err(|e| {
1993 match &exec_result {
1994 Ok(env_ref) => (e, Some(*env_ref)),
1995 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1997 }
1998 })?;
1999
2000 self.engine.clear_queues(&self.engine_batch).await;
2003
2004 match exec_state.build_artifact_graph(&self.engine, program).await {
2005 Ok(_) => exec_result,
2006 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
2007 }
2008 }
2009
2010 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
2014 if exec_state.stack().memory.requires_std() {
2015 let initial_ops = exec_state.mod_local.artifacts.operations.len();
2016
2017 let path = vec!["std".to_owned(), "prelude".to_owned()];
2018 let resolved_path = ModulePath::from_std_import_path(&path)?;
2019 let id = self
2020 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
2021 .await?;
2022 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
2023
2024 exec_state.mut_stack().memory.set_std(module_memory)?;
2025
2026 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
2032 }
2033
2034 Ok(())
2035 }
2036
2037 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
2039 self.engine
2041 .send_modeling_cmd(
2042 &self.engine_batch,
2043 uuid::Uuid::new_v4(),
2044 crate::execution::SourceRange::default(),
2045 &ModelingCmd::from(
2046 mcmd::ZoomToFit::builder()
2047 .object_ids(Default::default())
2048 .animated(false)
2049 .padding(0.1)
2050 .build(),
2051 ),
2052 )
2053 .await
2054 .map_err(KclErrorWithOutputs::no_outputs)?;
2055
2056 let resp = self
2058 .engine
2059 .send_modeling_cmd(
2060 &self.engine_batch,
2061 uuid::Uuid::new_v4(),
2062 crate::execution::SourceRange::default(),
2063 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
2064 )
2065 .await
2066 .map_err(KclErrorWithOutputs::no_outputs)?;
2067
2068 let OkWebSocketResponseData::Modeling {
2069 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
2070 } = resp
2071 else {
2072 return Err(ExecError::BadPng(format!(
2073 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
2074 )));
2075 };
2076 Ok(contents)
2077 }
2078
2079 pub async fn export(
2081 &self,
2082 format: kittycad_modeling_cmds::format::OutputFormat3d,
2083 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2084 let resp = self
2085 .engine
2086 .send_modeling_cmd(
2087 &self.engine_batch,
2088 uuid::Uuid::new_v4(),
2089 crate::SourceRange::default(),
2090 &kittycad_modeling_cmds::ModelingCmd::Export(
2091 kittycad_modeling_cmds::Export::builder()
2092 .entity_ids(vec![])
2093 .format(format)
2094 .build(),
2095 ),
2096 )
2097 .await?;
2098
2099 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
2100 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
2101 format!("Expected Export response, got {resp:?}",),
2102 vec![SourceRange::default()],
2103 )));
2104 };
2105
2106 Ok(files)
2107 }
2108
2109 pub async fn export_step(
2111 &self,
2112 deterministic_time: bool,
2113 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2114 let files = self
2115 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
2116 kittycad_modeling_cmds::format::step::export::Options::builder()
2117 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
2118 .maybe_created(if deterministic_time {
2119 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
2120 KclError::new_internal(crate::errors::KclErrorDetails::new(
2121 format!("Failed to parse date: {e}"),
2122 vec![SourceRange::default()],
2123 ))
2124 })?)
2125 } else {
2126 None
2127 })
2128 .build(),
2129 ))
2130 .await?;
2131
2132 Ok(files)
2133 }
2134
2135 pub async fn close(&self) {
2136 self.engine.close().await;
2137 }
2138}
2139
2140pub use kcl_api::ArtifactId;
2141
2142pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
2143 ArtifactId::new(*id.as_ref())
2144}
2145
2146#[cfg(test)]
2147pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2148 parse_execute_with_project_dir(code, None).await
2149}
2150
2151#[cfg(test)]
2152pub(crate) async fn parse_execute_with_project_dir(
2153 code: &str,
2154 project_directory: Option<TypedPath>,
2155) -> Result<ExecTestResults, KclError> {
2156 let program = crate::Program::parse_no_errs(code)?;
2157
2158 let exec_ctxt = ExecutorContext {
2159 engine: Arc::new(EngineManager::new_mock()),
2160 engine_batch: EngineBatchContext::default(),
2161 fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2162 settings: ExecutorSettings {
2163 project_directory,
2164 ..Default::default()
2165 },
2166 context_type: ContextType::Mock,
2167 execution_callbacks: Default::default(),
2168 };
2169 let mut exec_state = ExecState::new(&exec_ctxt);
2170 let result = exec_ctxt.run(&program, &mut exec_state).await?;
2171
2172 Ok(ExecTestResults {
2173 program,
2174 mem_env: result.0,
2175 exec_ctxt,
2176 exec_state,
2177 })
2178}
2179
2180#[cfg(test)]
2181#[derive(Debug)]
2182pub(crate) struct ExecTestResults {
2183 program: crate::Program,
2184 mem_env: EnvironmentRef,
2185 exec_ctxt: ExecutorContext,
2186 exec_state: ExecState,
2187}
2188
2189#[cfg(test)]
2190impl ExecTestResults {
2191 pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2192 &self.exec_state.global.root_module_artifacts.commands
2193 }
2194}
2195
2196pub struct ProgramLookup {
2200 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2201}
2202
2203impl ProgramLookup {
2204 pub fn new(
2207 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2208 module_infos: state::ModuleInfoMap,
2209 ) -> Self {
2210 let mut programs = IndexMap::with_capacity(module_infos.len());
2211 for (id, info) in module_infos {
2212 if let ModuleRepr::Kcl(program, _) = info.repr {
2213 programs.insert(id, program);
2214 }
2215 }
2216 programs.insert(ModuleId::default(), current);
2217 Self { programs }
2218 }
2219
2220 pub fn program_for_module(
2221 &self,
2222 module_id: ModuleId,
2223 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2224 self.programs.get(&module_id)
2225 }
2226}
2227
2228#[cfg(test)]
2229mod tests {
2230 use kcl_api::NumericType;
2231 use pretty_assertions::assert_eq;
2232
2233 use super::*;
2234 use crate::ModuleId;
2235 use crate::errors::KclErrorDetails;
2236 use crate::errors::Severity;
2237 use crate::execution::memory::Stack;
2238 use crate::execution::types::RuntimeType;
2239
2240 macro_rules! kcl_input {
2241 ($file:literal) => {
2242 include_str!(concat!("../../e2e/executor/inputs/", $file, ".kcl"))
2243 };
2244 }
2245
2246 #[track_caller]
2248 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2249 memory.memory.get_from_unchecked(name, env).unwrap()
2250 }
2251
2252 async fn execute_variables_with_backend(
2253 code: &str,
2254 backend: memory::MemoryBackendKind,
2255 ) -> IndexMap<String, KclValueView> {
2256 execute_outcome_with_backend(code, backend).await.variables
2257 }
2258
2259 async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
2260 let program = crate::Program::parse_no_errs(code).unwrap();
2261 let ctx = ExecutorContext::new_mock(None).await;
2262 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2263 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2264 let outcome = exec_state
2265 .into_exec_outcome(env_ref, &ctx)
2266 .await
2267 .expect("test execution outcome should collect variables");
2268 ctx.close().await;
2269 outcome
2270 }
2271
2272 async fn execute_error_variables_with_backend(
2273 code: &str,
2274 backend: memory::MemoryBackendKind,
2275 ) -> IndexMap<String, KclValueView> {
2276 let program = crate::Program::parse_no_errs(code).unwrap();
2277 let ctx = ExecutorContext::new_mock(None).await;
2278 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2279 let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
2280 ctx.close().await;
2281 error.variables
2282 }
2283
2284 async fn execute_project_variables_with_backend(
2285 main_code: &str,
2286 files: &[(&str, &str)],
2287 backend: memory::MemoryBackendKind,
2288 ) -> IndexMap<String, KclValueView> {
2289 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
2290 for (name, contents) in files {
2291 tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
2292 }
2293
2294 let program = crate::Program::parse_no_errs(main_code).unwrap();
2295 let ctx = ExecutorContext {
2296 engine: Arc::new(EngineManager::new_mock()),
2297 engine_batch: EngineBatchContext::default(),
2298 fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2299 settings: ExecutorSettings {
2300 project_directory: Some(crate::TypedPath(tmpdir.path().into())),
2301 ..Default::default()
2302 },
2303 context_type: ContextType::Mock,
2304 execution_callbacks: Default::default(),
2305 };
2306 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2307 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2308 let outcome = exec_state
2309 .into_exec_outcome(env_ref, &ctx)
2310 .await
2311 .expect("test execution outcome should collect variables");
2312 ctx.close().await;
2313 outcome.variables
2314 }
2315
2316 async fn run_with_caching_variables_with_backend(
2317 code: &str,
2318 backend: memory::MemoryBackendKind,
2319 ) -> IndexMap<String, KclValueView> {
2320 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2321 cache::bust_cache().await;
2322 clear_mem_cache().await;
2323
2324 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
2325 let program = crate::Program::parse_no_errs(code).unwrap();
2326 ctx.run_with_caching(program.clone()).await.unwrap();
2327 let cached = ctx.run_with_caching(program).await.unwrap();
2328
2329 cache::bust_cache().await;
2330 clear_mem_cache().await;
2331 ctx.close().await;
2332 cached.variables
2333 }
2334
2335 async fn run_mock_variables_with_backend(
2336 code: &str,
2337 backend: memory::MemoryBackendKind,
2338 ) -> IndexMap<String, KclValueView> {
2339 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2340 clear_mem_cache().await;
2341
2342 let ctx = ExecutorContext::new_mock(None).await;
2343 let first = crate::Program::parse_no_errs("x = 2").unwrap();
2344 ctx.run_mock(
2345 &first,
2346 &MockConfig {
2347 use_prev_memory: false,
2348 ..Default::default()
2349 },
2350 )
2351 .await
2352 .unwrap();
2353
2354 let program = crate::Program::parse_no_errs(code).unwrap();
2355 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
2356
2357 clear_mem_cache().await;
2358 ctx.close().await;
2359 outcome.variables
2360 }
2361
2362 fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
2363 let mut keys = variables.keys().cloned().collect::<Vec<_>>();
2364 keys.sort();
2365 keys
2366 }
2367
2368 async fn collect_backend_results<T, Fut>(
2369 mut run: impl FnMut(memory::MemoryBackendKind) -> Fut,
2370 ) -> Vec<(memory::MemoryBackendKind, T)>
2371 where
2372 Fut: std::future::Future<Output = T>,
2373 {
2374 let all = memory::MemoryBackendKind::all();
2375 let mut results = Vec::with_capacity(all.len());
2376 for &kind in all {
2377 results.push((kind, run(kind).await));
2378 }
2379 results
2380 }
2381
2382 fn assert_backend_results_match<T>(results: &[(memory::MemoryBackendKind, T)])
2383 where
2384 T: std::fmt::Debug + PartialEq,
2385 {
2386 let (first, rest) = results.split_first().expect("expected at least one memory backend");
2387 let (first_kind, first_result) = first;
2388 for (kind, result) in rest {
2389 assert_eq!(
2390 result, first_result,
2391 "memory kind {kind:?} doesn't match {first_kind:?}"
2392 );
2393 }
2394 }
2395
2396 fn assert_backend_variable_results_match_expected_keys(
2397 results: &[(memory::MemoryBackendKind, IndexMap<String, KclValueView>)],
2398 expected_keys: &[&str],
2399 ) {
2400 let (first_kind, first_variables) = results.first().expect("expected at least one memory backend");
2401 let expected_keys = expected_keys.iter().map(|key| (*key).to_owned()).collect::<Vec<_>>();
2402 assert_eq!(
2403 sorted_variable_keys(first_variables),
2404 expected_keys,
2405 "memory kind {first_kind:?} doesn't match expected variables"
2406 );
2407 assert_backend_results_match(results);
2408 }
2409
2410 fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
2411 let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
2412 let KclValueView::Number { value, .. } = value else {
2413 panic!("expected `{key}` to be a number, got {value:?}");
2414 };
2415 assert_eq!(*value, expected, "{key}: {value:?}");
2416 }
2417
2418 #[tokio::test(flavor = "multi_thread")]
2419 async fn exec_outcome_variables_match_between_memory_backends() {
2420 let code = "x = 2\ny = x + 1\narr = [x, y]";
2421
2422 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2423
2424 assert_backend_variable_results_match_expected_keys(&results, &["arr", "x", "y"]);
2425 }
2426
2427 #[tokio::test(flavor = "multi_thread")]
2428 async fn error_output_variables_match_between_memory_backends() {
2429 let code = "x = 2\ny = missing + 1";
2430
2431 let results = collect_backend_results(|kind| execute_error_variables_with_backend(code, kind)).await;
2432
2433 assert_backend_variable_results_match_expected_keys(&results, &["x"]);
2434 }
2435
2436 #[tokio::test(flavor = "multi_thread")]
2437 async fn cached_execution_variables_match_between_memory_backends() {
2438 let code = "x = 2\ny = x + 1";
2439
2440 let results = collect_backend_results(|kind| run_with_caching_variables_with_backend(code, kind)).await;
2441
2442 assert_backend_variable_results_match_expected_keys(&results, &["x", "y"]);
2443 }
2444
2445 #[tokio::test(flavor = "multi_thread")]
2446 async fn mock_execution_variables_match_between_memory_backends() {
2447 let code = "y = x + 1";
2448
2449 let results = collect_backend_results(|kind| run_mock_variables_with_backend(code, kind)).await;
2450
2451 assert_backend_variable_results_match_expected_keys(&results, &["y"]);
2452 }
2453
2454 #[tokio::test(flavor = "multi_thread")]
2455 async fn module_imports_and_exported_closures_match_between_memory_backends() {
2456 let module_code = r#"
2457export base = 40
2458
2459export fn addBase(n) {
2460 return n + base
2461}
2462"#;
2463 let main_code = r#"
2464import base, addBase from 'math.kcl'
2465import 'math.kcl'
2466
2467named = addBase(n = 2)
2468qualified = math::addBase(n = 1)
2469direct = math::base
2470"#;
2471
2472 let files = [("math.kcl", module_code)];
2473 let results =
2474 collect_backend_results(|kind| execute_project_variables_with_backend(main_code, &files, kind)).await;
2475
2476 let (_, first_variables) = results.first().expect("expected at least one memory backend");
2477 assert_number_variable(first_variables, "named", 42.0);
2478 assert_number_variable(first_variables, "qualified", 41.0);
2479 assert_number_variable(first_variables, "direct", 40.0);
2480 assert_backend_results_match(&results);
2481 }
2482
2483 #[tokio::test(flavor = "multi_thread")]
2484 async fn sketch_block_variables_match_between_memory_backends() {
2485 let code = r#"
2486sketch001 = sketch(on = XY) {
2487 line1 = line(start = [0, 0], end = [1, 0])
2488 line2 = line(start = [1, 0], end = [0, 1])
2489}
2490lineCount = 2
2491"#;
2492
2493 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2494
2495 let (_, first_variables) = results.first().expect("expected at least one memory backend");
2496 assert!(first_variables.contains_key("sketch001"), "actual: {first_variables:?}");
2497 assert_number_variable(first_variables, "lineCount", 2.0);
2498 assert_backend_results_match(&results);
2499 }
2500
2501 #[tokio::test(flavor = "multi_thread")]
2502 async fn tag_call_stack_lookup_matches_between_memory_backends() {
2503 let code = r#"
2504sketch001 = startSketchOn(XY)
2505 |> startProfile(at = [0, 0])
2506 |> xLine(length = 10, tag = $seg01)
2507
2508segLength = segLen(seg01)
2509"#;
2510
2511 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2512
2513 let (_, first_variables) = results.first().expect("expected at least one memory backend");
2514 assert_number_variable(first_variables, "segLength", 10.0);
2515 assert_backend_results_match(&results);
2516 }
2517
2518 #[tokio::test(flavor = "multi_thread")]
2519 async fn sketch_transpiler_exec_outcome_variables_match_between_memory_backends() {
2520 let code = r#"
2521sketch001 = startSketchOn(XY)
2522 |> startProfile(at = [0, 0])
2523 |> line(end = [1, 0])
2524"#;
2525 let program = crate::Program::parse_no_errs(code).unwrap();
2526
2527 let outcomes = collect_backend_results(|kind| execute_outcome_with_backend(code, kind)).await;
2528 let mut transpiled = Vec::with_capacity(outcomes.len());
2529 for (kind, outcome) in &outcomes {
2530 let sketch = transpile_old_sketch_to_new(outcome, &program, "sketch001").unwrap();
2531 transpiled.push((*kind, sketch));
2532 }
2533
2534 assert_backend_results_match(&transpiled);
2535 }
2536
2537 #[tokio::test(flavor = "multi_thread")]
2538 async fn test_execute_warn() {
2539 let text = "@blah";
2540 let result = parse_execute(text).await.unwrap();
2541 let errs = result.exec_state.issues();
2542 assert_eq!(errs.len(), 1);
2543 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2544 assert!(
2545 errs[0].message.contains("Unknown annotation"),
2546 "unexpected warning message: {}",
2547 errs[0].message
2548 );
2549 }
2550
2551 #[tokio::test(flavor = "multi_thread")]
2552 async fn test_execute_fn_definitions() {
2553 let ast = r#"fn def(@x) {
2554 return x
2555}
2556fn ghi(@x) {
2557 return x
2558}
2559fn jkl(@x) {
2560 return x
2561}
2562fn hmm(@x) {
2563 return x
2564}
2565
2566yo = 5 + 6
2567
2568abc = 3
2569identifierGuy = 5
2570part001 = startSketchOn(XY)
2571|> startProfile(at = [-1.2, 4.83])
2572|> line(end = [2.8, 0])
2573|> angledLine(angle = 100 + 100, length = 3.01)
2574|> angledLine(angle = abc, length = 3.02)
2575|> angledLine(angle = def(yo), length = 3.03)
2576|> angledLine(angle = ghi(2), length = 3.04)
2577|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2578|> close()
2579yo2 = hmm([identifierGuy + 5])"#;
2580
2581 parse_execute(ast).await.unwrap();
2582 }
2583
2584 #[tokio::test(flavor = "multi_thread")]
2585 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2586 let code = r#"
2587firstProfile = sketch(on = XY) {
2588 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2589 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2590 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2591 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2592 coincident([edge1.end, edge2.start])
2593 coincident([edge2.end, edge3.start])
2594 coincident([edge3.end, edge4.start])
2595 coincident([edge4.end, edge1.start])
2596}
2597
2598secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2599 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2600 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2601 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2602 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2603 coincident([edge5.end, edge6.start])
2604 coincident([edge6.end, edge7.start])
2605 coincident([edge7.end, edge8.start])
2606 coincident([edge8.end, edge5.start])
2607}
2608
2609firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2610secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2611"#;
2612
2613 let result = parse_execute(code).await.unwrap();
2614 assert!(result.exec_state.issues().is_empty());
2615 }
2616
2617 #[tokio::test(flavor = "multi_thread")]
2618 async fn sketch_block_artifact_preserves_standard_plane_name() {
2619 let code = r#"
2620sketch001 = sketch(on = -YZ) {
2621 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
2622}
2623"#;
2624
2625 let result = parse_execute(code).await.unwrap();
2626 let sketch_blocks = result
2627 .exec_state
2628 .global
2629 .artifacts
2630 .graph
2631 .values()
2632 .filter_map(|artifact| match artifact {
2633 Artifact::SketchBlock(block) => Some(block),
2634 _ => None,
2635 })
2636 .collect::<Vec<_>>();
2637
2638 assert_eq!(sketch_blocks.len(), 1);
2639 assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
2640 }
2641
2642 #[tokio::test(flavor = "multi_thread")]
2643 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2644 let code = r#"
2645sketch001 = sketch(on = YZ) {
2646 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2647 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2648 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2649 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2650 coincident([line1.end, line2.start])
2651 coincident([line2.end, line3.start])
2652 coincident([line3.end, line4.start])
2653 coincident([line4.end, line1.start])
2654}
2655
2656sketch002 = sketch(on = -XZ) {
2657 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2658 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2659 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2660 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2661 coincident([line5.end, line6.start])
2662 coincident([line6.end, line7.start])
2663 coincident([line7.end, line8.start])
2664 coincident([line8.end, line5.start])
2665}
2666
2667region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2668extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2669region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2670extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2671
2672myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2673"#;
2674
2675 let result = parse_execute(code).await.unwrap();
2676 assert!(result.exec_state.issues().is_empty());
2677 }
2678
2679 #[tokio::test(flavor = "multi_thread")]
2680 async fn issue_10741_point_circle_coincident_executes() {
2681 let code = r#"
2682sketch001 = sketch(on = YZ) {
2683 circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
2684 line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
2685 coincident([line1.start, circle1])
2686}
2687"#;
2688
2689 let result = parse_execute(code).await.unwrap();
2690 assert!(
2691 result
2692 .exec_state
2693 .issues()
2694 .iter()
2695 .all(|issue| issue.severity != Severity::Error),
2696 "unexpected execution issues: {:#?}",
2697 result.exec_state.issues()
2698 );
2699 }
2700
2701 #[tokio::test(flavor = "multi_thread")]
2702 async fn test_execute_with_pipe_substitutions_unary() {
2703 let ast = r#"myVar = 3
2704part001 = startSketchOn(XY)
2705 |> startProfile(at = [0, 0])
2706 |> line(end = [3, 4], tag = $seg01)
2707 |> line(end = [
2708 min([segLen(seg01), myVar]),
2709 -legLen(hypotenuse = segLen(seg01), leg = myVar)
2710])
2711"#;
2712
2713 parse_execute(ast).await.unwrap();
2714 }
2715
2716 #[tokio::test(flavor = "multi_thread")]
2717 async fn test_execute_with_pipe_substitutions() {
2718 let ast = r#"myVar = 3
2719part001 = startSketchOn(XY)
2720 |> startProfile(at = [0, 0])
2721 |> line(end = [3, 4], tag = $seg01)
2722 |> line(end = [
2723 min([segLen(seg01), myVar]),
2724 legLen(hypotenuse = segLen(seg01), leg = myVar)
2725])
2726"#;
2727
2728 parse_execute(ast).await.unwrap();
2729 }
2730
2731 #[tokio::test(flavor = "multi_thread")]
2732 async fn test_execute_with_inline_comment() {
2733 let ast = r#"baseThick = 1
2734armAngle = 60
2735
2736baseThickHalf = baseThick / 2
2737halfArmAngle = armAngle / 2
2738
2739arrExpShouldNotBeIncluded = [1, 2, 3]
2740objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2741
2742part001 = startSketchOn(XY)
2743 |> startProfile(at = [0, 0])
2744 |> yLine(endAbsolute = 1)
2745 |> xLine(length = 3.84) // selection-range-7ish-before-this
2746
2747variableBelowShouldNotBeIncluded = 3
2748"#;
2749
2750 parse_execute(ast).await.unwrap();
2751 }
2752
2753 #[tokio::test(flavor = "multi_thread")]
2754 async fn test_execute_with_function_literal_in_pipe() {
2755 let ast = r#"w = 20
2756l = 8
2757h = 10
2758
2759fn thing() {
2760 return -8
2761}
2762
2763firstExtrude = startSketchOn(XY)
2764 |> startProfile(at = [0,0])
2765 |> line(end = [0, l])
2766 |> line(end = [w, 0])
2767 |> line(end = [0, thing()])
2768 |> close()
2769 |> extrude(length = h)"#;
2770
2771 parse_execute(ast).await.unwrap();
2772 }
2773
2774 #[tokio::test(flavor = "multi_thread")]
2775 async fn test_execute_with_function_unary_in_pipe() {
2776 let ast = r#"w = 20
2777l = 8
2778h = 10
2779
2780fn thing(@x) {
2781 return -x
2782}
2783
2784firstExtrude = startSketchOn(XY)
2785 |> startProfile(at = [0,0])
2786 |> line(end = [0, l])
2787 |> line(end = [w, 0])
2788 |> line(end = [0, thing(8)])
2789 |> close()
2790 |> extrude(length = h)"#;
2791
2792 parse_execute(ast).await.unwrap();
2793 }
2794
2795 #[tokio::test(flavor = "multi_thread")]
2796 async fn test_execute_with_function_array_in_pipe() {
2797 let ast = r#"w = 20
2798l = 8
2799h = 10
2800
2801fn thing(@x) {
2802 return [0, -x]
2803}
2804
2805firstExtrude = startSketchOn(XY)
2806 |> startProfile(at = [0,0])
2807 |> line(end = [0, l])
2808 |> line(end = [w, 0])
2809 |> line(end = thing(8))
2810 |> close()
2811 |> extrude(length = h)"#;
2812
2813 parse_execute(ast).await.unwrap();
2814 }
2815
2816 #[tokio::test(flavor = "multi_thread")]
2817 async fn test_execute_with_function_call_in_pipe() {
2818 let ast = r#"w = 20
2819l = 8
2820h = 10
2821
2822fn other_thing(@y) {
2823 return -y
2824}
2825
2826fn thing(@x) {
2827 return other_thing(x)
2828}
2829
2830firstExtrude = startSketchOn(XY)
2831 |> startProfile(at = [0,0])
2832 |> line(end = [0, l])
2833 |> line(end = [w, 0])
2834 |> line(end = [0, thing(8)])
2835 |> close()
2836 |> extrude(length = h)"#;
2837
2838 parse_execute(ast).await.unwrap();
2839 }
2840
2841 #[tokio::test(flavor = "multi_thread")]
2842 async fn test_execute_with_function_sketch() {
2843 let ast = r#"fn box(h, l, w) {
2844 myBox = startSketchOn(XY)
2845 |> startProfile(at = [0,0])
2846 |> line(end = [0, l])
2847 |> line(end = [w, 0])
2848 |> line(end = [0, -l])
2849 |> close()
2850 |> extrude(length = h)
2851
2852 return myBox
2853}
2854
2855fnBox = box(h = 3, l = 6, w = 10)"#;
2856
2857 parse_execute(ast).await.unwrap();
2858 }
2859
2860 #[tokio::test(flavor = "multi_thread")]
2861 async fn test_get_member_of_object_with_function_period() {
2862 let ast = r#"fn box(@obj) {
2863 myBox = startSketchOn(XY)
2864 |> startProfile(at = obj.start)
2865 |> line(end = [0, obj.l])
2866 |> line(end = [obj.w, 0])
2867 |> line(end = [0, -obj.l])
2868 |> close()
2869 |> extrude(length = obj.h)
2870
2871 return myBox
2872}
2873
2874thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2875"#;
2876 parse_execute(ast).await.unwrap();
2877 }
2878
2879 #[tokio::test(flavor = "multi_thread")]
2880 #[ignore] async fn test_object_member_starting_pipeline() {
2882 let ast = r#"
2883fn test2() {
2884 return {
2885 thing: startSketchOn(XY)
2886 |> startProfile(at = [0, 0])
2887 |> line(end = [0, 1])
2888 |> line(end = [1, 0])
2889 |> line(end = [0, -1])
2890 |> close()
2891 }
2892}
2893
2894x2 = test2()
2895
2896x2.thing
2897 |> extrude(length = 10)
2898"#;
2899 parse_execute(ast).await.unwrap();
2900 }
2901
2902 #[tokio::test(flavor = "multi_thread")]
2903 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
2905 let ast = r#"fn box(obj) {
2906let myBox = startSketchOn(XY)
2907 |> startProfile(at = obj.start)
2908 |> line(end = [0, obj.l])
2909 |> line(end = [obj.w, 0])
2910 |> line(end = [0, -obj.l])
2911 |> close()
2912 |> extrude(length = obj.h)
2913
2914 return myBox
2915}
2916
2917for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2918 thisBox = box(var)
2919}"#;
2920
2921 parse_execute(ast).await.unwrap();
2922 }
2923
2924 #[tokio::test(flavor = "multi_thread")]
2925 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
2927 let ast = r#"fn box(h, l, w, start) {
2928 myBox = startSketchOn(XY)
2929 |> startProfile(at = [0,0])
2930 |> line(end = [0, l])
2931 |> line(end = [w, 0])
2932 |> line(end = [0, -l])
2933 |> close()
2934 |> extrude(length = h)
2935
2936 return myBox
2937}
2938
2939
2940for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2941 const thisBox = box(var[0], var[1], var[2], var[3])
2942}"#;
2943
2944 parse_execute(ast).await.unwrap();
2945 }
2946
2947 #[tokio::test(flavor = "multi_thread")]
2948 async fn test_get_member_of_array_with_function() {
2949 let ast = r#"fn box(@arr) {
2950 myBox =startSketchOn(XY)
2951 |> startProfile(at = arr[0])
2952 |> line(end = [0, arr[1]])
2953 |> line(end = [arr[2], 0])
2954 |> line(end = [0, -arr[1]])
2955 |> close()
2956 |> extrude(length = arr[3])
2957
2958 return myBox
2959}
2960
2961thisBox = box([[0,0], 6, 10, 3])
2962
2963"#;
2964 parse_execute(ast).await.unwrap();
2965 }
2966
2967 #[tokio::test(flavor = "multi_thread")]
2968 async fn test_function_cannot_access_future_definitions() {
2969 let ast = r#"
2970fn returnX() {
2971 // x shouldn't be defined yet.
2972 return x
2973}
2974
2975x = 5
2976
2977answer = returnX()"#;
2978
2979 let result = parse_execute(ast).await;
2980 let err = result.unwrap_err();
2981 assert_eq!(err.message(), "`x` is not defined");
2982 }
2983
2984 #[tokio::test(flavor = "multi_thread")]
2985 async fn test_override_prelude() {
2986 let text = "PI = 3.0";
2987 let result = parse_execute(text).await.unwrap();
2988 let issues = result.exec_state.issues();
2989 assert!(issues.is_empty(), "issues={issues:#?}");
2990 }
2991
2992 #[tokio::test(flavor = "multi_thread")]
2993 async fn type_aliases() {
2994 let text = r#"@settings(experimentalFeatures = allow)
2995type MyTy = [number; 2]
2996fn foo(@x: MyTy) {
2997 return x[0]
2998}
2999
3000foo([0, 1])
3001
3002type Other = MyTy | Helix
3003"#;
3004 let result = parse_execute(text).await.unwrap();
3005 let issues = result.exec_state.issues();
3006 assert!(issues.is_empty(), "issues={issues:#?}");
3007 }
3008
3009 #[tokio::test(flavor = "multi_thread")]
3010 async fn test_cannot_shebang_in_fn() {
3011 let ast = r#"
3012fn foo() {
3013 #!hello
3014 return true
3015}
3016
3017foo
3018"#;
3019
3020 let result = parse_execute(ast).await;
3021 let err = result.unwrap_err();
3022 assert_eq!(
3023 err,
3024 KclError::new_syntax(KclErrorDetails::new(
3025 "Unexpected token: #".to_owned(),
3026 vec![SourceRange::new(14, 15, ModuleId::default())],
3027 )),
3028 );
3029 }
3030
3031 #[tokio::test(flavor = "multi_thread")]
3032 async fn test_pattern_transform_function_cannot_access_future_definitions() {
3033 let ast = r#"
3034fn transform(@replicaId) {
3035 // x shouldn't be defined yet.
3036 scale = x
3037 return {
3038 translate = [0, 0, replicaId * 10],
3039 scale = [scale, 1, 0],
3040 }
3041}
3042
3043fn layer() {
3044 return startSketchOn(XY)
3045 |> circle( center= [0, 0], radius= 1, tag = $tag1)
3046 |> extrude(length = 10)
3047}
3048
3049x = 5
3050
3051// The 10 layers are replicas of each other, with a transform applied to each.
3052shape = layer() |> patternTransform(instances = 10, transform = transform)
3053"#;
3054
3055 let result = parse_execute(ast).await;
3056 let err = result.unwrap_err();
3057 assert_eq!(err.message(), "`x` is not defined",);
3058 }
3059
3060 #[tokio::test(flavor = "multi_thread")]
3063 async fn test_math_execute_with_functions() {
3064 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
3065 let result = parse_execute(ast).await.unwrap();
3066 assert_eq!(
3067 5.0,
3068 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3069 .as_f64()
3070 .unwrap()
3071 );
3072 }
3073
3074 #[tokio::test(flavor = "multi_thread")]
3075 async fn test_math_execute() {
3076 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
3077 let result = parse_execute(ast).await.unwrap();
3078 assert_eq!(
3079 7.4,
3080 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3081 .as_f64()
3082 .unwrap()
3083 );
3084 }
3085
3086 #[tokio::test(flavor = "multi_thread")]
3087 async fn test_string_uppercase() {
3088 let composed = "\u{e9}";
3089 let uppercase_composed = "\u{c9}";
3090 let decomposed = "e\u{301}";
3091 let uppercase_decomposed = "E\u{301}";
3092 let code = format!(
3093 r#"
3094ascii = string::uppercase("Kcl")
3095unicode_expansion = string::uppercase("Straße")
3096uncased = string::uppercase("東京")
3097empty = string::uppercase("")
3098composed = string::uppercase("{composed}")
3099decomposed = string::uppercase("{decomposed}")
3100piped = "ready" |> string::uppercase()
3101"#
3102 );
3103
3104 let result = parse_execute(&code).await.unwrap();
3105 for (name, expected) in [
3106 ("ascii", "KCL"),
3107 ("unicode_expansion", "STRASSE"),
3108 ("uncased", "東京"),
3109 ("empty", ""),
3110 ("composed", uppercase_composed),
3111 ("decomposed", uppercase_decomposed),
3112 ("piped", "READY"),
3113 ] {
3114 assert_eq!(
3115 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3116 .as_str()
3117 .unwrap(),
3118 expected,
3119 "{name}"
3120 );
3121 }
3122 }
3123
3124 #[tokio::test(flavor = "multi_thread")]
3125 async fn test_string_lowercase() {
3126 let composed = "\u{c9}";
3127 let lowercase_composed = "\u{e9}";
3128 let decomposed = "E\u{301}";
3129 let lowercase_decomposed = "e\u{301}";
3130 let expanded = "i\u{307}";
3131 let code = format!(
3132 r#"
3133ascii = string::lowercase("KCL")
3134final_sigma = string::lowercase("ΟΣ")
3135medial_sigma = string::lowercase("ΟΣΑ")
3136unicode_expansion = string::lowercase("İ")
3137uncased = string::lowercase("東京")
3138empty = string::lowercase("")
3139composed = string::lowercase("{composed}")
3140decomposed = string::lowercase("{decomposed}")
3141piped = "READY" |> string::lowercase()
3142"#
3143 );
3144
3145 let result = parse_execute(&code).await.unwrap();
3146 for (name, expected) in [
3147 ("ascii", "kcl"),
3148 ("final_sigma", "ος"),
3149 ("medial_sigma", "οσα"),
3150 ("unicode_expansion", expanded),
3151 ("uncased", "東京"),
3152 ("empty", ""),
3153 ("composed", lowercase_composed),
3154 ("decomposed", lowercase_decomposed),
3155 ("piped", "ready"),
3156 ] {
3157 assert_eq!(
3158 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3159 .as_str()
3160 .unwrap(),
3161 expected,
3162 "{name}"
3163 );
3164 }
3165 }
3166
3167 #[tokio::test(flavor = "multi_thread")]
3168 async fn test_string_is_equal() {
3169 let composed = "\u{e9}";
3170 let decomposed = "e\u{301}";
3171 let code = format!(
3172 r#"
3173exact_same = string::isEqual("KCL", to = "KCL")
3174exact_different_case = string::isEqual("KCL", to = "kcl")
3175explicit_case_sensitive = string::isEqual("KCL", to = "kcl", caseInsensitive = false)
3176case_insensitive_ascii = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3177case_fold_expansion = string::isEqual("Straße", to = "STRASSE", caseInsensitive = true)
3178case_fold_expansion_reversed = string::isEqual("STRASSE", to = "Straße", caseInsensitive = true)
3179case_fold_sigma = string::isEqual("ος", to = "οσ", caseInsensitive = true)
3180case_fold_non_turkic = string::isEqual("I", to = "i", caseInsensitive = true)
3181case_fold_not_turkic = string::isEqual("I", to = "ı", caseInsensitive = true)
3182empty_same = string::isEqual("", to = "")
3183empty_different = string::isEqual("", to = "KCL")
3184exact_without_normalization = string::isEqual("{composed}", to = "{decomposed}")
3185case_fold_without_normalization = string::isEqual("{composed}", to = "{decomposed}", caseInsensitive = true)
3186piped = "ready" |> string::isEqual(to = "READY", caseInsensitive = true)
3187"#
3188 );
3189
3190 let result = parse_execute(&code).await.unwrap();
3191 for (name, expected) in [
3192 ("exact_same", true),
3193 ("exact_different_case", false),
3194 ("explicit_case_sensitive", false),
3195 ("case_insensitive_ascii", true),
3196 ("case_fold_expansion", true),
3197 ("case_fold_expansion_reversed", true),
3198 ("case_fold_sigma", true),
3199 ("case_fold_non_turkic", true),
3200 ("case_fold_not_turkic", false),
3201 ("empty_same", true),
3202 ("empty_different", false),
3203 ("exact_without_normalization", false),
3204 ("case_fold_without_normalization", false),
3205 ("piped", true),
3206 ] {
3207 assert_eq!(
3208 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3209 .as_bool()
3210 .unwrap(),
3211 expected,
3212 "{name}"
3213 );
3214 }
3215 }
3216
3217 #[tokio::test(flavor = "multi_thread")]
3218 async fn test_string_is_equal_inside_sketch_block_is_predicate() {
3219 let code = r#"
3220@settings(experimentalFeatures = allow)
3221
3222sketch(on = XY) {
3223 stringsAreEqual = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3224}
3225"#;
3226
3227 parse_execute(code).await.unwrap();
3228 }
3229
3230 #[tokio::test(flavor = "multi_thread")]
3231 async fn test_string_trim() {
3232 let ascii_whitespace = " \t\n";
3233 let tab = "\t";
3234 let non_breaking_space = "\u{a0}";
3235 let em_space = "\u{2003}";
3236 let ideographic_space = "\u{3000}";
3237 let zero_width_space = "\u{200b}";
3238 let decomposed = "e\u{301}";
3239 let code = format!(
3240 r#"
3241ascii = string::trim("{ascii_whitespace}KCL{ascii_whitespace}")
3242internal = string::trim(" KCL{tab}strings ")
3243unicode = string::trim("{non_breaking_space}{em_space}KCL{ideographic_space}")
3244all_whitespace = string::trim("{ascii_whitespace}{non_breaking_space}")
3245empty = string::trim("")
3246unchanged = string::trim("KCL")
3247without_normalization = string::trim(" {decomposed} ")
3248non_whitespace = string::trim("{zero_width_space}KCL{zero_width_space}")
3249piped = " ready " |> string::trim()
3250"#
3251 );
3252
3253 let result = parse_execute(&code).await.unwrap();
3254 let non_whitespace = format!("{zero_width_space}KCL{zero_width_space}");
3255 for (name, expected) in [
3256 ("ascii", "KCL"),
3257 ("internal", "KCL\tstrings"),
3258 ("unicode", "KCL"),
3259 ("all_whitespace", ""),
3260 ("empty", ""),
3261 ("unchanged", "KCL"),
3262 ("without_normalization", decomposed),
3263 ("non_whitespace", non_whitespace.as_str()),
3264 ("piped", "ready"),
3265 ] {
3266 assert_eq!(
3267 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3268 .as_str()
3269 .unwrap(),
3270 expected,
3271 "{name}"
3272 );
3273 }
3274 }
3275
3276 #[tokio::test(flavor = "multi_thread")]
3277 async fn test_string_trim_start() {
3278 let ascii_whitespace = " \t\n";
3279 let tab = "\t";
3280 let non_breaking_space = "\u{a0}";
3281 let em_space = "\u{2003}";
3282 let ideographic_space = "\u{3000}";
3283 let zero_width_space = "\u{200b}";
3284 let decomposed = "e\u{301}";
3285 let code = format!(
3286 r#"
3287ascii = string::trimStart("{ascii_whitespace}KCL{ascii_whitespace}")
3288internal = string::trimStart(" KCL{tab}strings")
3289unicode = string::trimStart("{non_breaking_space}{em_space}KCL{ideographic_space}")
3290all_whitespace = string::trimStart("{ascii_whitespace}{non_breaking_space}")
3291empty = string::trimStart("")
3292unchanged = string::trimStart("KCL")
3293without_normalization = string::trimStart(" {decomposed}")
3294non_whitespace_prefix = string::trimStart("{zero_width_space}{ascii_whitespace}KCL")
3295piped = " ready " |> string::trimStart()
3296"#
3297 );
3298
3299 let result = parse_execute(&code).await.unwrap();
3300 let ascii = format!("KCL{ascii_whitespace}");
3301 let unicode = format!("KCL{ideographic_space}");
3302 let non_whitespace_prefix = format!("{zero_width_space}{ascii_whitespace}KCL");
3303 for (name, expected) in [
3304 ("ascii", ascii.as_str()),
3305 ("internal", "KCL\tstrings"),
3306 ("unicode", unicode.as_str()),
3307 ("all_whitespace", ""),
3308 ("empty", ""),
3309 ("unchanged", "KCL"),
3310 ("without_normalization", decomposed),
3311 ("non_whitespace_prefix", non_whitespace_prefix.as_str()),
3312 ("piped", "ready "),
3313 ] {
3314 assert_eq!(
3315 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3316 .as_str()
3317 .unwrap(),
3318 expected,
3319 "{name}"
3320 );
3321 }
3322 }
3323
3324 #[tokio::test(flavor = "multi_thread")]
3325 async fn test_string_trim_end() {
3326 let ascii_whitespace = " \t\n";
3327 let tab = "\t";
3328 let non_breaking_space = "\u{a0}";
3329 let em_space = "\u{2003}";
3330 let ideographic_space = "\u{3000}";
3331 let zero_width_space = "\u{200b}";
3332 let decomposed = "e\u{301}";
3333 let code = format!(
3334 r#"
3335ascii = string::trimEnd("{ascii_whitespace}KCL{ascii_whitespace}")
3336internal = string::trimEnd("KCL{tab}strings ")
3337unicode = string::trimEnd("{non_breaking_space}KCL{em_space}{ideographic_space}")
3338all_whitespace = string::trimEnd("{ascii_whitespace}{non_breaking_space}")
3339empty = string::trimEnd("")
3340unchanged = string::trimEnd("KCL")
3341without_normalization = string::trimEnd("{decomposed} ")
3342non_whitespace_suffix = string::trimEnd("KCL{ascii_whitespace}{zero_width_space}")
3343piped = " ready " |> string::trimEnd()
3344"#
3345 );
3346
3347 let result = parse_execute(&code).await.unwrap();
3348 let ascii = format!("{ascii_whitespace}KCL");
3349 let unicode = format!("{non_breaking_space}KCL");
3350 let non_whitespace_suffix = format!("KCL{ascii_whitespace}{zero_width_space}");
3351 for (name, expected) in [
3352 ("ascii", ascii.as_str()),
3353 ("internal", "KCL\tstrings"),
3354 ("unicode", unicode.as_str()),
3355 ("all_whitespace", ""),
3356 ("empty", ""),
3357 ("unchanged", "KCL"),
3358 ("without_normalization", decomposed),
3359 ("non_whitespace_suffix", non_whitespace_suffix.as_str()),
3360 ("piped", " ready"),
3361 ] {
3362 assert_eq!(
3363 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3364 .as_str()
3365 .unwrap(),
3366 expected,
3367 "{name}"
3368 );
3369 }
3370 }
3371
3372 #[tokio::test(flavor = "multi_thread")]
3373 async fn test_string_to_string() {
3374 for (name, expr, expected) in [
3377 ("unitless integer", "12", "12"),
3380 ("unitless fractional", "1.5", "1.5"),
3381 ("no digits dropped", "0.1 + 0.2", "0.30000000000000004"),
3382 ("unitless negative", "-7", "-7"),
3383 ("unitless zero", "0", "0"),
3384 ("negative zero", "-0", "0"),
3385 ("count", "3_", "3_"),
3386 ("millimeters", "12mm", "12mm"),
3387 ("centimeters", "12cm", "12cm"),
3388 ("meters", "12m", "12m"),
3389 ("inches", "1.5in", "1.5in"),
3390 ("feet", "2ft", "2ft"),
3391 ("yards", "3yd", "3yd"),
3392 ("degrees", "90deg", "90deg"),
3393 ("radians", "1.5rad", "1.5rad"),
3394 ("length arithmetic", "2mm + 10mm", "12mm"),
3396 ("units the type system loses", "2mm * 10mm", "20"),
3399 ("unitless arithmetic", "1 + 2", "3"),
3400 ] {
3401 let code = format!("actual = string::toString({expr})");
3402 let result = parse_execute(&code).await.unwrap();
3403
3404 assert_eq!(
3405 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3406 .as_str()
3407 .unwrap(),
3408 expected,
3409 "case: {name}"
3410 );
3411 }
3412 }
3413
3414 #[tokio::test(flavor = "multi_thread")]
3415 async fn test_string_to_string_ignores_the_files_default_unit() {
3416 let code = "@settings(defaultLengthUnit = inch)\nactual = string::toString(12)";
3421 let result = parse_execute(code).await.unwrap();
3422
3423 assert_eq!(
3424 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3425 .as_str()
3426 .unwrap(),
3427 "12"
3428 );
3429 }
3430
3431 #[tokio::test(flavor = "multi_thread")]
3432 async fn test_string_to_string_rejects_a_non_number() {
3433 let error = parse_execute(r#"actual = string::toString("already text")"#)
3434 .await
3435 .unwrap_err();
3436
3437 assert_eq!(
3440 error.message(),
3441 "The input argument of `string::toString` requires a value with type `number`, but found a value with type `string`."
3442 );
3443 assert!(
3444 matches!(error, KclError::Argument { .. }),
3445 "expected an Argument error, found {error:?}"
3446 );
3447 }
3448
3449 #[tokio::test(flavor = "multi_thread")]
3450 async fn test_string_to_string_accepts_a_piped_argument() {
3451 let result = parse_execute("actual = 12mm |> string::toString()").await.unwrap();
3452
3453 assert_eq!(
3454 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3455 .as_str()
3456 .unwrap(),
3457 "12mm"
3458 );
3459 }
3460
3461 #[tokio::test(flavor = "multi_thread")]
3462 async fn test_string_to_string_echoes_how_the_literal_was_written() {
3463 for literal in [
3467 "12",
3468 "1.5",
3469 "0.30000000000000004",
3470 "3_",
3471 "2.5_",
3474 "-4_",
3475 "12mm",
3476 "-5mm",
3477 "1.5in",
3478 "90deg",
3479 "1.5rad",
3480 ] {
3481 let code = format!("actual = string::toString({literal})");
3482 let result = parse_execute(&code).await.unwrap();
3483
3484 assert_eq!(
3485 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3486 .as_str()
3487 .unwrap(),
3488 literal,
3489 "literal: {literal}"
3490 );
3491 }
3492 }
3493
3494 #[tokio::test(flavor = "multi_thread")]
3495 async fn test_string_to_string_spells_out_non_finite_numbers() {
3496 for (name, expr, expected) in [
3500 ("positive infinity", "1 / 0", "Infinity"),
3501 ("negative infinity", "-1 / 0", "-Infinity"),
3502 ("nan", "0 / 0", "NaN"),
3503 ("infinity from a length", "1mm / 0", "Infinity"),
3505 ("nan from a length", "0mm / 0", "NaN"),
3506 ("infinity from an angle", "1deg / 0", "Infinity"),
3507 ] {
3508 let code = format!("actual = string::toString({expr})");
3509 let result = parse_execute(&code).await.unwrap();
3510
3511 assert_eq!(
3512 mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
3513 .as_str()
3514 .unwrap(),
3515 expected,
3516 "case: {name}"
3517 );
3518 }
3519 }
3520
3521 #[tokio::test(flavor = "multi_thread")]
3522 async fn test_string_equality_operators() {
3523 let composed = "\u{e9}";
3524 let decomposed = "e\u{301}";
3525 let code = format!(
3526 r#"
3527equal_same_ascii = "KCL" == "KCL"
3528equal_different_case = "KCL" == "kcl"
3529not_equal_same_ascii = "KCL" != "KCL"
3530not_equal_different_case = "KCL" != "kcl"
3531equal_same_unicode = "{composed}" == "{composed}"
3532not_equal_same_unicode = "{composed}" != "{composed}"
3533equal_without_normalization = "{composed}" == "{decomposed}"
3534not_equal_without_normalization = "{composed}" != "{decomposed}"
3535"#
3536 );
3537
3538 let result = parse_execute(&code).await.unwrap();
3539 for (name, expected) in [
3540 ("equal_same_ascii", true),
3541 ("equal_different_case", false),
3542 ("not_equal_same_ascii", false),
3543 ("not_equal_different_case", true),
3544 ("equal_same_unicode", true),
3545 ("not_equal_same_unicode", false),
3546 ("equal_without_normalization", false),
3547 ("not_equal_without_normalization", true),
3548 ] {
3549 assert_eq!(
3550 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3551 .as_bool()
3552 .unwrap(),
3553 expected,
3554 "{name}"
3555 );
3556 }
3557 }
3558
3559 #[tokio::test(flavor = "multi_thread")]
3560 async fn test_string_equality_inside_sketch_block_fails_like_number_equality() {
3561 let string_code = r#"
3562@settings(experimentalFeatures = allow)
3563
3564sketch(on = XY) {
3565 stringsAreEqual = "KCL" == "KCL"
3566}
3567"#;
3568 let number_code = r#"
3569@settings(experimentalFeatures = allow)
3570
3571sketch(on = XY) {
3572 numbersAreEqual = 1 == 1
3573}
3574"#;
3575
3576 assert_eq!(
3577 parse_execute(string_code).await.unwrap_err().message(),
3578 "Cannot create an equivalence constraint between values of these types: a string and a string"
3579 );
3580 assert_eq!(
3581 parse_execute(number_code).await.unwrap_err().message(),
3582 "Cannot create an equivalence constraint between values of these types: a number and a number"
3583 );
3584 }
3585
3586 #[tokio::test(flavor = "multi_thread")]
3587 async fn test_math_execute_start_negative() {
3588 let ast = r#"myVar = -5 + 6"#;
3589 let result = parse_execute(ast).await.unwrap();
3590 assert_eq!(
3591 1.0,
3592 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3593 .as_f64()
3594 .unwrap()
3595 );
3596 }
3597
3598 #[tokio::test(flavor = "multi_thread")]
3599 async fn test_math_execute_with_pi() {
3600 let ast = r#"myVar = PI * 2"#;
3601 let result = parse_execute(ast).await.unwrap();
3602 assert_eq!(
3603 std::f64::consts::TAU,
3604 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3605 .as_f64()
3606 .unwrap()
3607 );
3608 }
3609
3610 #[tokio::test(flavor = "multi_thread")]
3611 async fn test_math_define_decimal_without_leading_zero() {
3612 let ast = r#"thing = .4 + 7"#;
3613 let result = parse_execute(ast).await.unwrap();
3614 assert_eq!(
3615 7.4,
3616 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
3617 .as_f64()
3618 .unwrap()
3619 );
3620 }
3621
3622 #[tokio::test(flavor = "multi_thread")]
3623 async fn pass_std_to_std() {
3624 let ast = r#"sketch001 = startSketchOn(XY)
3625profile001 = circle(sketch001, center = [0, 0], radius = 2)
3626extrude001 = extrude(profile001, length = 5)
3627extrudes = patternLinear3d(
3628 extrude001,
3629 instances = 3,
3630 distance = 5,
3631 axis = [1, 1, 0],
3632)
3633clone001 = map(extrudes, f = clone)
3634"#;
3635 parse_execute(ast).await.unwrap();
3636 }
3637
3638 #[tokio::test(flavor = "multi_thread")]
3639 async fn test_array_reduce_nested_array() {
3640 let code = r#"
3641fn id(@el, accum) { return accum }
3642
3643answer = reduce([], initial=[[[0,0]]], f=id)
3644"#;
3645 let result = parse_execute(code).await.unwrap();
3646 assert_eq!(
3647 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
3648 KclValue::HomArray {
3649 value: vec![KclValue::HomArray {
3650 value: vec![KclValue::HomArray {
3651 value: vec![
3652 KclValue::Number {
3653 value: 0.0,
3654 ty: NumericType::default(),
3655 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
3656 },
3657 KclValue::Number {
3658 value: 0.0,
3659 ty: NumericType::default(),
3660 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
3661 }
3662 ],
3663 ty: RuntimeType::any(),
3664 }],
3665 ty: RuntimeType::any(),
3666 }],
3667 ty: RuntimeType::any(),
3668 }
3669 );
3670 }
3671
3672 #[tokio::test(flavor = "multi_thread")]
3673 async fn test_zero_param_fn() {
3674 let ast = r#"sigmaAllow = 35000 // psi
3675leg1 = 5 // inches
3676leg2 = 8 // inches
3677fn thickness() { return 0.56 }
3678
3679bracket = startSketchOn(XY)
3680 |> startProfile(at = [0,0])
3681 |> line(end = [0, leg1])
3682 |> line(end = [leg2, 0])
3683 |> line(end = [0, -thickness()])
3684 |> line(end = [-leg2 + thickness(), 0])
3685"#;
3686 parse_execute(ast).await.unwrap();
3687 }
3688
3689 #[tokio::test(flavor = "multi_thread")]
3690 async fn test_unary_operator_not_succeeds() {
3691 let ast = r#"
3692fn returnTrue() { return !false }
3693t = true
3694f = false
3695notTrue = !t
3696notFalse = !f
3697c = !!true
3698d = !returnTrue()
3699
3700assertIs(!false, error = "expected to pass")
3701
3702fn check(x) {
3703 assertIs(!x, error = "expected argument to be false")
3704 return true
3705}
3706check(x = false)
3707"#;
3708 let result = parse_execute(ast).await.unwrap();
3709 assert_eq!(
3710 false,
3711 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
3712 .as_bool()
3713 .unwrap()
3714 );
3715 assert_eq!(
3716 true,
3717 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
3718 .as_bool()
3719 .unwrap()
3720 );
3721 assert_eq!(
3722 true,
3723 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
3724 .as_bool()
3725 .unwrap()
3726 );
3727 assert_eq!(
3728 false,
3729 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
3730 .as_bool()
3731 .unwrap()
3732 );
3733 }
3734
3735 #[tokio::test(flavor = "multi_thread")]
3736 async fn test_unary_operator_not_on_non_bool_fails() {
3737 let code1 = r#"
3738// Yup, this is null.
3739myNull = 0 / 0
3740notNull = !myNull
3741"#;
3742 assert_eq!(
3743 parse_execute(code1).await.unwrap_err().message(),
3744 "Cannot apply unary operator ! to non-boolean value: a number",
3745 );
3746
3747 let code2 = "notZero = !0";
3748 assert_eq!(
3749 parse_execute(code2).await.unwrap_err().message(),
3750 "Cannot apply unary operator ! to non-boolean value: a number",
3751 );
3752
3753 let code3 = r#"
3754notEmptyString = !""
3755"#;
3756 assert_eq!(
3757 parse_execute(code3).await.unwrap_err().message(),
3758 "Cannot apply unary operator ! to non-boolean value: a string",
3759 );
3760
3761 let code4 = r#"
3762obj = { a = 1 }
3763notMember = !obj.a
3764"#;
3765 assert_eq!(
3766 parse_execute(code4).await.unwrap_err().message(),
3767 "Cannot apply unary operator ! to non-boolean value: a number",
3768 );
3769
3770 let code5 = "
3771a = []
3772notArray = !a";
3773 assert_eq!(
3774 parse_execute(code5).await.unwrap_err().message(),
3775 "Cannot apply unary operator ! to non-boolean value: an empty array",
3776 );
3777
3778 let code6 = "
3779x = {}
3780notObject = !x";
3781 assert_eq!(
3782 parse_execute(code6).await.unwrap_err().message(),
3783 "Cannot apply unary operator ! to non-boolean value: an object",
3784 );
3785
3786 let code7 = "
3787fn x() { return 1 }
3788notFunction = !x";
3789 let fn_err = parse_execute(code7).await.unwrap_err();
3790 assert!(
3793 fn_err
3794 .message()
3795 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
3796 "Actual error: {fn_err:?}"
3797 );
3798
3799 let code8 = "
3800myTagDeclarator = $myTag
3801notTagDeclarator = !myTagDeclarator";
3802 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
3803 assert!(
3806 tag_declarator_err
3807 .message()
3808 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
3809 "Actual error: {tag_declarator_err:?}"
3810 );
3811
3812 let code9 = "
3813myTagDeclarator = $myTag
3814notTagIdentifier = !myTag";
3815 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
3816 assert!(
3819 tag_identifier_err
3820 .message()
3821 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
3822 "Actual error: {tag_identifier_err:?}"
3823 );
3824
3825 let code10 = "notPipe = !(1 |> 2)";
3826 assert_eq!(
3827 parse_execute(code10).await.unwrap_err(),
3830 KclError::new_syntax(KclErrorDetails::new(
3831 "Unexpected token: !".to_owned(),
3832 vec![SourceRange::new(10, 11, ModuleId::default())],
3833 ))
3834 );
3835
3836 let code11 = "
3837fn identity(x) { return x }
3838notPipeSub = 1 |> identity(!%))";
3839 assert_eq!(
3840 parse_execute(code11).await.unwrap_err(),
3843 KclError::new_syntax(KclErrorDetails::new(
3844 "There was an unexpected `!`. Try removing it.".to_owned(),
3845 vec![SourceRange::new(56, 57, ModuleId::default())],
3846 ))
3847 );
3848
3849 }
3853
3854 #[tokio::test(flavor = "multi_thread")]
3855 async fn test_start_sketch_on_invalid_kwargs() {
3856 let current_dir = std::env::current_dir().unwrap();
3857 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
3858 let mut code = std::fs::read_to_string(&path).unwrap();
3859 assert_eq!(
3860 parse_execute(&code).await.unwrap_err().message(),
3861 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
3862 );
3863
3864 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
3865 code = std::fs::read_to_string(&path).unwrap();
3866
3867 assert_eq!(
3868 parse_execute(&code).await.unwrap_err().message(),
3869 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
3870 );
3871
3872 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
3873 code = std::fs::read_to_string(&path).unwrap();
3874
3875 assert_eq!(
3876 parse_execute(&code).await.unwrap_err().message(),
3877 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
3878 );
3879
3880 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
3881 code = std::fs::read_to_string(&path).unwrap();
3882
3883 assert_eq!(
3884 parse_execute(&code).await.unwrap_err().message(),
3885 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
3886 );
3887
3888 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
3889 code = std::fs::read_to_string(&path).unwrap();
3890
3891 assert_eq!(
3892 parse_execute(&code).await.unwrap_err().message(),
3893 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
3894 );
3895 }
3896
3897 #[tokio::test(flavor = "multi_thread")]
3898 async fn test_math_negative_variable_in_binary_expression() {
3899 let ast = r#"sigmaAllow = 35000 // psi
3900width = 1 // inch
3901
3902p = 150 // lbs
3903distance = 6 // inches
3904FOS = 2
3905
3906leg1 = 5 // inches
3907leg2 = 8 // inches
3908
3909thickness_squared = distance * p * FOS * 6 / sigmaAllow
3910thickness = 0.56 // inches. App does not support square root function yet
3911
3912bracket = startSketchOn(XY)
3913 |> startProfile(at = [0,0])
3914 |> line(end = [0, leg1])
3915 |> line(end = [leg2, 0])
3916 |> line(end = [0, -thickness])
3917 |> line(end = [-leg2 + thickness, 0])
3918"#;
3919 parse_execute(ast).await.unwrap();
3920 }
3921
3922 #[tokio::test(flavor = "multi_thread")]
3923 async fn test_execute_function_no_return() {
3924 let ast = r#"fn test(@origin) {
3925 origin
3926}
3927
3928test([0, 0])
3929"#;
3930 let result = parse_execute(ast).await;
3931 assert!(result.is_err());
3932 assert!(result.unwrap_err().to_string().contains("undefined"));
3933 }
3934
3935 #[tokio::test(flavor = "multi_thread")]
3936 async fn test_max_stack_size_exceeded_error() {
3937 let ast = r#"
3938fn forever(@n) {
3939 return 1 + forever(n)
3940}
3941
3942forever(1)
3943"#;
3944 let result = parse_execute(ast).await;
3945 let err = result.unwrap_err();
3946 assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
3947 }
3948
3949 #[tokio::test(flavor = "multi_thread")]
3950 async fn test_math_doubly_nested_parens() {
3951 let ast = r#"sigmaAllow = 35000 // psi
3952width = 4 // inch
3953p = 150 // Force on shelf - lbs
3954distance = 6 // inches
3955FOS = 2
3956leg1 = 5 // inches
3957leg2 = 8 // inches
3958thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
3959thickness = 0.32 // inches. App does not support square root function yet
3960bracket = startSketchOn(XY)
3961 |> startProfile(at = [0,0])
3962 |> line(end = [0, leg1])
3963 |> line(end = [leg2, 0])
3964 |> line(end = [0, -thickness])
3965 |> line(end = [-1 * leg2 + thickness, 0])
3966 |> line(end = [0, -1 * leg1 + thickness])
3967 |> close()
3968 |> extrude(length = width)
3969"#;
3970 parse_execute(ast).await.unwrap();
3971 }
3972
3973 #[tokio::test(flavor = "multi_thread")]
3974 async fn test_math_nested_parens_one_less() {
3975 let ast = r#" sigmaAllow = 35000 // psi
3976width = 4 // inch
3977p = 150 // Force on shelf - lbs
3978distance = 6 // inches
3979FOS = 2
3980leg1 = 5 // inches
3981leg2 = 8 // inches
3982thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
3983thickness = 0.32 // inches. App does not support square root function yet
3984bracket = startSketchOn(XY)
3985 |> startProfile(at = [0,0])
3986 |> line(end = [0, leg1])
3987 |> line(end = [leg2, 0])
3988 |> line(end = [0, -thickness])
3989 |> line(end = [-1 * leg2 + thickness, 0])
3990 |> line(end = [0, -1 * leg1 + thickness])
3991 |> close()
3992 |> extrude(length = width)
3993"#;
3994 parse_execute(ast).await.unwrap();
3995 }
3996
3997 #[tokio::test(flavor = "multi_thread")]
3998 async fn test_fn_as_operand() {
3999 let ast = r#"fn f() { return 1 }
4000x = f()
4001y = x + 1
4002z = f() + 1
4003w = f() + f()
4004"#;
4005 parse_execute(ast).await.unwrap();
4006 }
4007
4008 #[tokio::test(flavor = "multi_thread")]
4009 async fn kcl_test_ids_stable_between_executions() {
4010 let code = r#"sketch001 = startSketchOn(XZ)
4011|> startProfile(at = [61.74, 206.13])
4012|> xLine(length = 305.11, tag = $seg01)
4013|> yLine(length = -291.85)
4014|> xLine(length = -segLen(seg01))
4015|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4016|> close()
4017|> extrude(length = 40.14)
4018|> shell(
4019 thickness = 3.14,
4020 faces = [seg01]
4021)
4022"#;
4023
4024 let ctx = crate::test_server::new_context(true, None).await.unwrap();
4025 let old_program = crate::Program::parse_no_errs(code).unwrap();
4026
4027 if let Err(err) = ctx.run_with_caching(old_program).await {
4029 let report = err.into_miette_report_with_outputs(code).unwrap();
4030 let report = miette::Report::new(report);
4031 panic!("Error executing program: {report:?}");
4032 }
4033
4034 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4036
4037 let code = r#"sketch001 = startSketchOn(XZ)
4038|> startProfile(at = [62.74, 206.13])
4039|> xLine(length = 305.11, tag = $seg01)
4040|> yLine(length = -291.85)
4041|> xLine(length = -segLen(seg01))
4042|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4043|> close()
4044|> extrude(length = 40.14)
4045|> shell(
4046 faces = [seg01],
4047 thickness = 3.14,
4048)
4049"#;
4050
4051 let program = crate::Program::parse_no_errs(code).unwrap();
4053 ctx.run_with_caching(program).await.unwrap();
4055
4056 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
4057
4058 assert_eq!(id_generator, new_id_generator);
4059 }
4060
4061 #[tokio::test(flavor = "multi_thread")]
4062 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
4063 let code = r#"sketch001 = startSketchOn(XZ)
4064|> startProfile(at = [61.74, 206.13])
4065|> xLine(length = 305.11, tag = $seg01)
4066|> yLine(length = -291.85)
4067|> xLine(length = -segLen(seg01))
4068|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
4069|> close()
4070|> extrude(length = 40.14)
4071|> shell(
4072 thickness = 3.14,
4073 faces = [seg01]
4074)
4075"#;
4076
4077 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
4078 let old_program = crate::Program::parse_no_errs(code).unwrap();
4079
4080 ctx.run_with_caching(old_program.clone()).await.unwrap();
4082
4083 let settings_state = cache::read_old_ast().await.unwrap().settings;
4084
4085 assert_eq!(settings_state, ctx.settings);
4087
4088 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4090
4091 ctx.run_with_caching(old_program.clone()).await.unwrap();
4093
4094 let settings_state = cache::read_old_ast().await.unwrap().settings;
4095
4096 assert_eq!(settings_state, ctx.settings);
4098
4099 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
4101
4102 ctx.run_with_caching(old_program).await.unwrap();
4104
4105 let settings_state = cache::read_old_ast().await.unwrap().settings;
4106
4107 assert_eq!(settings_state, ctx.settings);
4109
4110 ctx.close().await;
4111 }
4112
4113 #[tokio::test(flavor = "multi_thread")]
4114 async fn mock_after_not_mock() {
4115 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4116 let program = crate::Program::parse_no_errs("x = 2").unwrap();
4117 let result = ctx.run_with_caching(program).await.unwrap();
4118 assert_number_variable(&result.variables, "x", 2.0);
4119
4120 let ctx2 = ExecutorContext::new_mock(None).await;
4121 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
4122 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
4123 assert_number_variable(&result.variables, "z", 3.0);
4124
4125 ctx.close().await;
4126 ctx2.close().await;
4127 }
4128
4129 #[tokio::test(flavor = "multi_thread")]
4131 async fn mock_execution_succeeds_after_split() {
4132 let code = kcl_input!("repro_mock_extrude");
4133 let ctx = ExecutorContext::new_mock(None).await;
4134 let program = crate::Program::parse_no_errs(code).unwrap();
4135 let _result = match ctx.run_mock(&program, &MockConfig::default()).await {
4136 Ok(res) => res,
4137 Err(e) => panic!("{}", e.error),
4138 };
4139 }
4140
4141 #[tokio::test(flavor = "multi_thread")]
4142 async fn mock_then_add_extrude_then_mock_again() {
4143 let code = "s = sketch(on = XY) {
4144 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
4145 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
4146 coincident([line1.end, line2.start])
4147 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
4148 coincident([line2.end, line3.start])
4149 coincident([line1.start, line3.end])
4150}
4151 ";
4152 let ctx = ExecutorContext::new_mock(None).await;
4153 let program = crate::Program::parse_no_errs(code).unwrap();
4154 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
4155 assert!(result.variables.contains_key("s"), "actual: {:?}", result.variables);
4156
4157 let code2 = code.to_owned()
4158 + "
4159region001 = region(point = [1mm, 1mm], sketch = s)
4160extrude001 = extrude(region001, length = 1)
4161 ";
4162 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4163 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
4164 assert!(
4165 result.variables.contains_key("region001"),
4166 "actual: {:?}",
4167 result.variables
4168 );
4169
4170 ctx.close().await;
4171 }
4172
4173 #[tokio::test(flavor = "multi_thread")]
4174 async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
4175 let code = format!(
4176 r#"{}
4177
4178face7 = faceOf(solid6, face = r6.tags.line1)
4179r7 = squareRegion(onSurface = face7)
4180solid7 = extrude(r7, length = width)
4181"#,
4182 include_str!("../../tests/endless_impeller/input.kcl")
4183 );
4184
4185 let result = parse_execute(&code).await.unwrap();
4186 let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
4187 assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
4188
4189 let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
4190 KclValue::Face { value } => value,
4191 value => panic!("expected face7 to be a Face, got {value:?}"),
4192 };
4193 assert!(face7.parent_solid.creator_sketch_id.is_some());
4194 }
4195
4196 #[tokio::test(flavor = "multi_thread")]
4197 async fn mock_has_stable_ids() {
4198 let ctx = ExecutorContext::new_mock(None).await;
4199 let mock_config = MockConfig {
4200 use_prev_memory: false,
4201 ..Default::default()
4202 };
4203 let code = "sk = startSketchOn(XY)
4204 |> startProfile(at = [0, 0])";
4205 let program = crate::Program::parse_no_errs(code).unwrap();
4206 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
4207 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4208 assert!(!ids.is_empty(), "IDs should not be empty");
4209
4210 let ctx2 = ExecutorContext::new_mock(None).await;
4211 let program2 = crate::Program::parse_no_errs(code).unwrap();
4212 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
4213 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4214
4215 assert_eq!(ids, ids2, "Generated IDs should match");
4216 ctx.close().await;
4217 ctx2.close().await;
4218 }
4219
4220 #[tokio::test(flavor = "multi_thread")]
4221 async fn mock_memory_restore_preserves_module_maps() {
4222 clear_mem_cache().await;
4223
4224 let ctx = ExecutorContext::new_mock(None).await;
4225 let cold_start = MockConfig {
4226 use_prev_memory: false,
4227 ..Default::default()
4228 };
4229 ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
4230
4231 let mut mem = cache::read_old_memory().await.unwrap();
4232 assert!(
4233 mem.path_to_source_id.len() > 3,
4234 "expected prelude imports to populate multiple modules, got {:?}",
4235 mem.path_to_source_id
4236 );
4237 mem.constraint_state.insert(
4238 crate::front::ObjectId(1),
4239 indexmap::indexmap! {
4240 crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
4241 crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
4242 },
4243 );
4244
4245 let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
4246 ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
4247
4248 assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
4249 assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
4250 assert_eq!(exec_state.global.module_infos, mem.module_infos);
4251 assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
4252
4253 clear_mem_cache().await;
4254 ctx.close().await;
4255 }
4256
4257 #[tokio::test(flavor = "multi_thread")]
4258 async fn run_with_caching_no_action_refreshes_mock_memory() {
4259 cache::bust_cache().await;
4260 clear_mem_cache().await;
4261
4262 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
4263 let program = crate::Program::parse_no_errs(
4264 r#"sketch001 = sketch(on = XY) {
4265 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
4266}
4267"#,
4268 )
4269 .unwrap();
4270
4271 ctx.run_with_caching(program.clone()).await.unwrap();
4272 let baseline_memory = cache::read_old_memory().await.unwrap();
4273 assert!(
4274 !baseline_memory.scene_objects.is_empty(),
4275 "expected engine execution to persist full-scene mock memory"
4276 );
4277
4278 cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
4279 assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
4280
4281 ctx.run_with_caching(program).await.unwrap();
4282 let refreshed_memory = cache::read_old_memory().await.unwrap();
4283 assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
4284 assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
4285 assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
4286
4287 cache::bust_cache().await;
4288 clear_mem_cache().await;
4289 ctx.close().await;
4290 }
4291
4292 #[tokio::test(flavor = "multi_thread")]
4293 async fn sim_sketch_mode_real_mock_real() {
4294 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4295 let code = r#"sketch001 = startSketchOn(XY)
4296profile001 = startProfile(sketch001, at = [0, 0])
4297 |> line(end = [10, 0])
4298 |> line(end = [0, 10])
4299 |> line(end = [-10, 0])
4300 |> line(end = [0, -10])
4301 |> close()
4302"#;
4303 let program = crate::Program::parse_no_errs(code).unwrap();
4304 let result = ctx.run_with_caching(program).await.unwrap();
4305 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4306
4307 let mock_ctx = ExecutorContext::new_mock(None).await;
4308 let mock_program = crate::Program::parse_no_errs(code).unwrap();
4309 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
4310 assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4311
4312 let code2 = code.to_owned()
4313 + r#"
4314extrude001 = extrude(profile001, length = 10)
4315"#;
4316 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4317 let result = ctx.run_with_caching(program2).await.unwrap();
4318 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
4319
4320 ctx.close().await;
4321 mock_ctx.close().await;
4322 }
4323
4324 #[tokio::test(flavor = "multi_thread")]
4325 async fn read_tag_version() {
4326 let ast = r#"fn bar(@t) {
4327 return startSketchOn(XY)
4328 |> startProfile(at = [0,0])
4329 |> angledLine(
4330 angle = -60,
4331 length = segLen(t),
4332 )
4333 |> line(end = [0, 0])
4334 |> close()
4335}
4336
4337sketch = startSketchOn(XY)
4338 |> startProfile(at = [0,0])
4339 |> line(end = [0, 10])
4340 |> line(end = [10, 0], tag = $tag0)
4341 |> line(endAbsolute = [0, 0])
4342
4343fn foo() {
4344 // tag0 tags an edge
4345 return bar(tag0)
4346}
4347
4348solid = sketch |> extrude(length = 10)
4349// tag0 tags a face
4350sketch2 = startSketchOn(solid, face = tag0)
4351 |> startProfile(at = [0,0])
4352 |> line(end = [0, 1])
4353 |> line(end = [1, 0])
4354 |> line(end = [0, 0])
4355
4356foo() |> extrude(length = 1)
4357"#;
4358 parse_execute(ast).await.unwrap();
4359 }
4360
4361 #[tokio::test(flavor = "multi_thread")]
4362 async fn experimental() {
4363 let code = r#"
4364startSketchOn(XY)
4365 |> startProfile(at = [0, 0], tag = $start)
4366 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4367"#;
4368 let result = parse_execute(code).await.unwrap();
4369 let issues = result.exec_state.issues();
4370 assert_eq!(issues.len(), 1);
4371 assert_eq!(issues[0].severity, Severity::Error);
4372 let msg = &issues[0].message;
4373 assert!(msg.contains("experimental"), "found {msg}");
4374
4375 let code = r#"@settings(experimentalFeatures = allow)
4376startSketchOn(XY)
4377 |> startProfile(at = [0, 0], tag = $start)
4378 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4379"#;
4380 let result = parse_execute(code).await.unwrap();
4381 let issues = result.exec_state.issues();
4382 assert!(issues.is_empty(), "issues={issues:#?}");
4383
4384 let code = r#"@settings(experimentalFeatures = warn)
4385startSketchOn(XY)
4386 |> startProfile(at = [0, 0], tag = $start)
4387 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4388"#;
4389 let result = parse_execute(code).await.unwrap();
4390 let issues = result.exec_state.issues();
4391 assert_eq!(issues.len(), 1);
4392 assert_eq!(issues[0].severity, Severity::Warning);
4393 let msg = &issues[0].message;
4394 assert!(msg.contains("experimental"), "found {msg}");
4395
4396 let code = r#"@settings(experimentalFeatures = deny)
4397startSketchOn(XY)
4398 |> startProfile(at = [0, 0], tag = $start)
4399 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4400"#;
4401 let result = parse_execute(code).await.unwrap();
4402 let issues = result.exec_state.issues();
4403 assert_eq!(issues.len(), 1);
4404 assert_eq!(issues[0].severity, Severity::Error);
4405 let msg = &issues[0].message;
4406 assert!(msg.contains("experimental"), "found {msg}");
4407
4408 let code = r#"@settings(experimentalFeatures = foo)
4409startSketchOn(XY)
4410 |> startProfile(at = [0, 0], tag = $start)
4411 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4412"#;
4413 parse_execute(code).await.unwrap_err();
4414 }
4415
4416 #[tokio::test(flavor = "multi_thread")]
4417 async fn experimental_parameter() {
4418 let code = r#"
4419fn inc(@x, @(experimental = true) amount? = 1) {
4420 return x + amount
4421}
4422
4423answer = inc(5, amount = 2)
4424"#;
4425 let result = parse_execute(code).await.unwrap();
4426 let issues = result.exec_state.issues();
4427 assert_eq!(issues.len(), 1);
4428 assert_eq!(issues[0].severity, Severity::Error);
4429 let msg = &issues[0].message;
4430 assert!(msg.contains("experimental"), "found {msg}");
4431
4432 let code = r#"
4434fn inc(@x, @(experimental = true) amount? = 1) {
4435 return x + amount
4436}
4437
4438answer = inc(5)
4439"#;
4440 let result = parse_execute(code).await.unwrap();
4441 let issues = result.exec_state.issues();
4442 assert!(issues.is_empty(), "issues={issues:#?}");
4443 }
4444
4445 #[tokio::test(flavor = "multi_thread")]
4446 async fn experimental_scalar_fixed_constraint() {
4447 let code_left = r#"@settings(experimentalFeatures = warn)
4448sketch(on = XY) {
4449 point1 = point(at = [var 0mm, var 0mm])
4450 point1.at[0] == 1mm
4451}
4452"#;
4453 let code_right = r#"@settings(experimentalFeatures = warn)
4455sketch(on = XY) {
4456 point1 = point(at = [var 0mm, var 0mm])
4457 1mm == point1.at[0]
4458}
4459"#;
4460
4461 for code in [code_left, code_right] {
4462 let result = parse_execute(code).await.unwrap();
4463 let issues = result.exec_state.issues();
4464 let Some(error) = issues
4465 .iter()
4466 .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
4467 else {
4468 panic!("found {issues:#?}");
4469 };
4470 assert_eq!(error.severity, Severity::Warning);
4471 }
4472 }
4473
4474 #[tokio::test(flavor = "multi_thread")]
4478 async fn test_tangent_line_arc_executes_with_mock_engine() {
4479 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
4480 parse_execute(&code).await.unwrap();
4481 }
4482
4483 #[tokio::test(flavor = "multi_thread")]
4484 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
4485 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
4486 parse_execute(&code).await.unwrap();
4487 }
4488
4489 #[tokio::test(flavor = "multi_thread")]
4490 async fn test_tangent_line_circle_executes_with_mock_engine() {
4491 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
4492 parse_execute(&code).await.unwrap();
4493 }
4494
4495 #[tokio::test(flavor = "multi_thread")]
4496 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
4497 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
4498 parse_execute(&code).await.unwrap();
4499 }
4500
4501 #[tokio::test(flavor = "multi_thread")]
4502 async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
4503 let code = r#"startX = 2
4504
4505baseSketch = sketch(on = XY) {
4506 yoyo = line(start = [startX, 0], end = [7, 6])
4507 line2 = line(start = [7, 6], end = [7, 12])
4508 hi = line(start = [7, 12], end = [startX, 0])
4509}
4510
4511baseRegion = region(point = [5.5, 6], sketch = baseSketch)
4512myExtrude = extrude(
4513 baseRegion,
4514 length = 5,
4515 tagEnd = $endCap,
4516 tagStart = $startCap,
4517)
4518yodawg = getCommonEdge(faces = [
4519 baseRegion.tags.hi,
4520 baseRegion.tags.yoyo
4521])
4522
4523cutSketch = sketch(on = YZ) {
4524 myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
4525 myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
4526 line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
4527}
4528
4529cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
4530extrude001 = extrude(cutRegion, length = 5)
4531solid001 = subtract(myExtrude, tools = extrude001)
4532
4533yoyo = getOppositeEdge(baseRegion.tags.hi)
4534fillet(solid001, radius = 0.1, tags = yoyo)
4535"#;
4536
4537 parse_execute(code).await.unwrap();
4538 }
4539
4540 async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
4545 let program = crate::Program::parse_no_errs(kcl).unwrap();
4546 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4547 let mut exec_state = ExecState::new(&ctx);
4548 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
4549 let outcome = exec_state
4550 .into_exec_outcome(env_ref, &ctx)
4551 .await
4552 .expect("constraint report test outcome should collect variables");
4553 let report = outcome.sketch_constraint_report();
4554 ctx.close().await;
4555 report
4556 }
4557
4558 #[tokio::test(flavor = "multi_thread")]
4559 async fn warn_when_sketch_is_over_constrained() {
4560 let code = r#"
4561sketch001 = sketch(on = XY) {
4562 line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
4563 fixed([line1.start, ORIGIN])
4564 fixed([line1.start, [20, 20]])
4565}
4566"#;
4567 let result = parse_execute(code).await.unwrap();
4568 let issues = result.exec_state.issues();
4569 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
4570 panic!("expected over-constrained warning; found {issues:#?}");
4571 };
4572 assert_eq!(warning.severity, Severity::Warning);
4573 }
4574
4575 #[tokio::test(flavor = "multi_thread")]
4576 async fn no_warning_when_sketch_is_not_over_constrained() {
4577 let code = r#"
4579sketch001 = sketch(on = XY) {
4580 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4581}
4582"#;
4583 let result = parse_execute(code).await.unwrap();
4584 let issues = result.exec_state.issues();
4585 assert!(
4586 !issues.iter().any(|issue| issue.message.contains("over-constrained")),
4587 "did not expect over-constrained warning; found {issues:#?}"
4588 );
4589 }
4590
4591 #[tokio::test(flavor = "multi_thread")]
4592 async fn test_constraint_report_fully_constrained() {
4593 let kcl = r#"
4595@settings(experimentalFeatures = allow)
4596
4597sketch(on = YZ) {
4598 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4599 line1.start.at[0] == 2
4600 line1.start.at[1] == 8
4601 line1.end.at[0] == 5
4602 line1.end.at[1] == 7
4603}
4604"#;
4605 let report = run_constraint_report(kcl).await;
4606 assert_eq!(report.fully_constrained.len(), 1);
4607 assert_eq!(report.under_constrained.len(), 0);
4608 assert_eq!(report.over_constrained.len(), 0);
4609 assert_eq!(report.errors.len(), 0);
4610 assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
4611 }
4612
4613 #[tokio::test(flavor = "multi_thread")]
4614 async fn test_constraint_report_under_constrained() {
4615 let kcl = r#"
4617sketch(on = YZ) {
4618 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
4619}
4620"#;
4621 let report = run_constraint_report(kcl).await;
4622 assert_eq!(report.fully_constrained.len(), 0);
4623 assert_eq!(report.under_constrained.len(), 1);
4624 assert_eq!(report.over_constrained.len(), 0);
4625 assert_eq!(report.errors.len(), 0);
4626 assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
4627 assert!(report.under_constrained[0].free_count > 0);
4628 }
4629
4630 #[tokio::test(flavor = "multi_thread")]
4631 async fn test_constraint_report_over_constrained() {
4632 let kcl = r#"
4634@settings(experimentalFeatures = allow)
4635
4636sketch(on = YZ) {
4637 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4638 line1.start.at[0] == 2
4639 line1.start.at[1] == 8
4640 line1.end.at[0] == 5
4641 line1.end.at[1] == 7
4642 distance([line1.start, line1.end]) == 100mm
4643}
4644"#;
4645 let report = run_constraint_report(kcl).await;
4646 assert_eq!(report.over_constrained.len(), 1);
4647 assert_eq!(report.errors.len(), 0);
4648 assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
4649 assert!(report.over_constrained[0].conflict_count > 0);
4650 }
4651
4652 #[tokio::test(flavor = "multi_thread")]
4653 async fn test_constraint_report_multiple_sketches() {
4654 let kcl = r#"
4656@settings(experimentalFeatures = allow)
4657
4658s1 = sketch(on = YZ) {
4659 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4660 line1.start.at[0] == 2
4661 line1.start.at[1] == 8
4662 line1.end.at[0] == 5
4663 line1.end.at[1] == 7
4664}
4665
4666s2 = sketch(on = XZ) {
4667 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4668}
4669"#;
4670 let report = run_constraint_report(kcl).await;
4671 assert_eq!(
4672 report.fully_constrained.len()
4673 + report.under_constrained.len()
4674 + report.over_constrained.len()
4675 + report.errors.len(),
4676 2,
4677 "Expected 2 sketches total"
4678 );
4679 assert_eq!(report.fully_constrained.len(), 1);
4680 assert_eq!(report.under_constrained.len(), 1);
4681 }
4682
4683 #[tokio::test(flavor = "multi_thread")]
4684 async fn test_enum_declaration_is_experimental() {
4685 let code = "type Color { | Red }";
4688 assert_eq!(
4689 parse_execute(code).await.unwrap_err().message(),
4690 "Use of enum declarations is experimental and may change or be removed."
4691 );
4692 }
4693
4694 #[tokio::test(flavor = "multi_thread")]
4695 async fn enum_declaration_registers_type() {
4696 let code = r#"@settings(experimentalFeatures = allow)
4700type Color { | Red | Green }
4701"#;
4702 parse_execute(code).await.unwrap();
4703
4704 let code = r#"@settings(experimentalFeatures = allow)
4705export type Color { | Red | Green }
4706"#;
4707 parse_execute(code).await.unwrap();
4708
4709 let code = r#"@settings(experimentalFeatures = allow)
4711type Empty { | }
4712"#;
4713 parse_execute(code).await.unwrap();
4714 }
4715
4716 #[tokio::test(flavor = "multi_thread")]
4717 async fn enum_declaration_rejects_nested_scope() {
4718 let allow = "@settings(experimentalFeatures = allow)\n";
4725 for (case, code) in [
4726 (
4727 "function body",
4728 format!("{allow}fn palette() {{\n type Color {{ | Red }}\n return 0\n}}\npalette()\n"),
4729 ),
4730 (
4731 "sketch block",
4732 format!(
4733 "{allow}sketch(on = XY) {{\n type Color {{ | Red }}\n l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
4734 ),
4735 ),
4736 ] {
4737 assert_eq!(
4738 parse_execute(&code).await.unwrap_err().message(),
4739 "Enum declarations are only supported at the top-level of a file. Move `type Color` to the top-level.",
4740 "case: {case}"
4741 );
4742 }
4743 }
4744
4745 #[tokio::test(flavor = "multi_thread")]
4746 async fn enum_alone_is_restricted_to_top_level() {
4747 let allow = "@settings(experimentalFeatures = allow)\n";
4754 for (case, code) in [
4755 (
4756 "function body",
4757 format!("{allow}fn f() {{\n type Temperature = number(_)\n return 0\n}}\nx = f()\n"),
4758 ),
4759 (
4760 "sketch block",
4761 format!(
4762 "{allow}sketch(on = XY) {{\n type Temperature = number(_)\n l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
4763 ),
4764 ),
4765 ] {
4766 parse_execute(&code)
4767 .await
4768 .unwrap_or_else(|err| panic!("a type alias should be allowed in a {case}: {}", err.message()));
4769 }
4770 }
4771
4772 #[tokio::test(flavor = "multi_thread")]
4773 async fn enum_declaration_rejects_duplicate() {
4774 let code = r#"@settings(experimentalFeatures = allow)
4775type Color { | Red | Green | Red }
4776"#;
4777 assert_eq!(
4778 parse_execute(code).await.unwrap_err().message(),
4779 "Duplicate variant `Red` in enum `Color`."
4780 );
4781 }
4782
4783 async fn execute_with_modules(main: &str, modules: &[(&str, &str)]) -> Result<ExecTestResults, KclError> {
4785 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_enum_clash").unwrap();
4786 for (name, source) in modules {
4787 tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
4788 }
4789
4790 parse_execute_with_project_dir(main, Some(crate::TypedPath(tmpdir.path().into()))).await
4791 }
4792
4793 #[tokio::test(flavor = "multi_thread")]
4794 async fn enum_rejects_name_clash_with_module() {
4795 let plain_module = ("Color.kcl", "export x = 1\n");
4800 let enum_module = (
4801 "enums.kcl",
4802 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
4803 );
4804
4805 for (case, main, modules) in [
4806 (
4807 "module then enum",
4808 "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\ntype Color { | Red }\n",
4809 vec![plain_module],
4810 ),
4811 (
4812 "enum then module",
4813 "@settings(experimentalFeatures = allow)\ntype Color { | Red }\nimport \"Color.kcl\"\n",
4814 vec![plain_module],
4815 ),
4816 (
4817 "named import of an enum",
4818 "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport Color from 'enums.kcl'\n",
4819 vec![plain_module, enum_module],
4820 ),
4821 (
4822 "glob import of an enum",
4823 "@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport * from 'enums.kcl'\n",
4824 vec![plain_module, enum_module],
4825 ),
4826 ] {
4827 let err = execute_with_modules(main, &modules).await.unwrap_err();
4828 assert_eq!(
4829 err.message(),
4830 "An enum and a module cannot share the name `Color` in the same scope, because `Color::x` would be ambiguous. Rename one of them.",
4831 "case: {case}"
4832 );
4833 }
4834 }
4835
4836 #[tokio::test(flavor = "multi_thread")]
4837 async fn enum_constructs_variant() {
4838 let allow = "@settings(experimentalFeatures = allow)\n";
4839 let colors = (
4840 "colors.kcl",
4841 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
4842 );
4843
4844 for (case, main, modules) in [
4845 (
4846 "declared locally",
4847 format!("{allow}type Color {{ | Red | Green }}\nx = Color::Red\n"),
4848 vec![],
4849 ),
4850 (
4851 "reached through a module path",
4854 format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
4855 vec![colors],
4856 ),
4857 (
4858 "imported by name",
4859 format!("{allow}import Color from 'colors.kcl'\nx = Color::Red\n"),
4860 vec![colors],
4861 ),
4862 (
4863 "imported under an alias",
4866 format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade::Red\n"),
4867 vec![colors],
4868 ),
4869 ] {
4870 let result = execute_with_modules(&main, &modules)
4871 .await
4872 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
4873 let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
4874 panic!("case: {case}: `x` should hold an enum value");
4875 };
4876 assert_eq!(value.qualified_name(), "Color::Red", "case: {case}");
4877 }
4878 }
4879
4880 #[tokio::test(flavor = "multi_thread")]
4881 async fn enum_rejects_bad_variant_paths() {
4882 let allow = "@settings(experimentalFeatures = allow)\n";
4883
4884 for (case, main, modules, message) in [
4885 (
4886 "unknown variant",
4887 format!("{allow}type Color {{ | Red | Green }}\nx = Color::Blue\n"),
4888 vec![],
4889 "`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
4890 ),
4891 (
4892 "enum with no variants",
4893 format!("{allow}type Empty {{ | }}\nx = Empty::Red\n"),
4894 vec![],
4895 "`Red` is not a variant of enum `Empty`. Enum `Empty` has no variants.",
4896 ),
4897 (
4898 "path continues past the enum",
4899 format!("{allow}type Color {{ | Red }}\nx = Color::Red::more\n"),
4900 vec![],
4901 "`Color` is an enum, so only a variant name can follow it. There is nothing to reach through `Color::Red`.",
4902 ),
4903 (
4904 "variant name is case sensitive",
4905 format!("{allow}type Color {{ | Red }}\nx = Color::red\n"),
4906 vec![],
4907 "`red` is not a variant of enum `Color`. Its variants are: Red.",
4908 ),
4909 (
4910 "enum not exported from its module",
4911 format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
4912 vec![(
4913 "colors.kcl",
4914 "@settings(experimentalFeatures = allow)\ntype Color { | Red }\n",
4915 )],
4916 "Item Color not found in module's exported items",
4917 ),
4918 (
4919 "a type alias cannot head a path",
4922 format!("{allow}type T = number(_)\nx = T::foo\n"),
4923 vec![],
4924 "`T` is not defined",
4925 ),
4926 (
4927 "a value cannot head a path",
4930 "Color = 5\nx = Color::Red\n".to_owned(),
4931 vec![],
4932 "`Color` is not defined",
4933 ),
4934 ] {
4935 let err = execute_with_modules(&main, &modules).await.unwrap_err();
4936 assert_eq!(err.message(), message, "case: {case}");
4937 }
4938 }
4939
4940 #[tokio::test(flavor = "multi_thread")]
4941 async fn enum_compares_by_variant() {
4942 let code = r#"@settings(experimentalFeatures = allow)
4943type Color { | Red | Green }
4944sameEq = Color::Red == Color::Red
4945sameNeq = Color::Red != Color::Red
4946otherEq = Color::Red == Color::Green
4947otherNeq = Color::Red != Color::Green
4948"#;
4949 let result = parse_execute(code).await.unwrap();
4950
4951 for (name, expected) in [
4952 ("sameEq", true),
4953 ("sameNeq", false),
4954 ("otherEq", false),
4955 ("otherNeq", true),
4956 ] {
4957 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
4958 panic!("`{name}` should hold a bool");
4959 };
4960 assert_eq!(value, expected, "variable: {name}");
4961 }
4962 }
4963
4964 #[tokio::test(flavor = "multi_thread")]
4965 async fn enum_usable_inside_sketch_block() {
4966 let code = r#"@settings(experimentalFeatures = allow)
4975type Color { | Red | Green }
4976sketch(on = XY) {
4977 c = Color::Red
4978 assertIs(Color::Red != Color::Green)
4979 assertIs(!(Color::Red != Color::Red))
4980 l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
4981}
4982"#;
4983 parse_execute(code)
4984 .await
4985 .unwrap_or_else(|err| panic!("enum use inside a sketch block should work: {}", err.message()));
4986 }
4987
4988 #[tokio::test(flavor = "multi_thread")]
4989 async fn enum_eq_reserved_inside_sketch_block() {
4990 let allow = "@settings(experimentalFeatures = allow)\n";
5000 let tail = " l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}\n";
5001 for (case, declaration, comparison, types) in [
5002 (
5003 "enums",
5004 "type Color { | Red | Green }\n",
5005 "Color::Red == Color::Green",
5006 "a value of enum `Color` and a value of enum `Color`",
5007 ),
5008 ("strings", "", "\"a\" == \"b\"", "a string and a string"),
5009 ("numbers", "", "1 == 2", "a number and a number"),
5010 ] {
5011 let code = format!("{allow}{declaration}sketch(on = XY) {{\n x = {comparison}\n{tail}");
5012 assert_eq!(
5013 parse_execute(&code).await.unwrap_err().message(),
5014 format!("Cannot create an equivalence constraint between values of these types: {types}"),
5015 "case: {case}"
5016 );
5017 }
5018 }
5019
5020 #[tokio::test(flavor = "multi_thread")]
5021 async fn enum_same_file_imported_twice_is_one_type() {
5022 let main = r#"@settings(experimentalFeatures = allow)
5026import Color as A from 'colors.kcl'
5027import Color as B from 'colors.kcl'
5028x = A::Red == B::Red
5029y = A::Red == B::Green
5030"#;
5031 let result = execute_with_modules(
5032 main,
5033 &[(
5034 "colors.kcl",
5035 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
5036 )],
5037 )
5038 .await
5039 .unwrap();
5040
5041 for (name, expected) in [("x", true), ("y", false)] {
5042 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
5043 panic!("`{name}` should hold a bool");
5044 };
5045 assert_eq!(value, expected, "variable: {name}");
5046 }
5047 }
5048
5049 #[tokio::test(flavor = "multi_thread")]
5050 async fn enum_rejects_comparison_across_types() {
5051 let allow = "@settings(experimentalFeatures = allow)\n";
5052 let color = (
5053 "a.kcl",
5054 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
5055 );
5056 let other_color = (
5057 "b.kcl",
5058 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
5059 );
5060
5061 for (case, main, modules, message) in [
5062 (
5063 "two enums declared separately",
5064 format!("{allow}type Color {{ | Red }}\ntype Shade {{ | Red }}\nx = Color::Red == Shade::Red\n"),
5065 vec![],
5066 "Cannot compare enum `Color` with enum `Shade`. They are different types.",
5067 ),
5068 (
5069 "two enums sharing a name",
5073 format!(
5074 "{allow}import Color as A from 'a.kcl'\nimport Color as B from 'b.kcl'\nx = A::Red == B::Red\n"
5075 ),
5076 vec![color, other_color],
5077 "Cannot compare two different enums that are both named `Color`. They come from separate declarations.",
5078 ),
5079 (
5080 "an enum and a number",
5081 format!("{allow}type Color {{ | Red }}\nx = Color::Red == 5\n"),
5082 vec![],
5083 "Cannot compare enum `Color::Red` with a number.",
5084 ),
5085 (
5086 "a number and an enum, in that order",
5087 format!("{allow}type Color {{ | Red }}\nx = 5 == Color::Red\n"),
5088 vec![],
5089 "Cannot compare enum `Color::Red` with a number.",
5090 ),
5091 (
5092 "an enum and a string",
5093 format!("{allow}type Color {{ | Red }}\nx = Color::Red == \"Red\"\n"),
5094 vec![],
5095 "Cannot compare enum `Color::Red` with a string.",
5096 ),
5097 ] {
5098 let err = execute_with_modules(&main, &modules).await.unwrap_err();
5099 assert_eq!(err.message(), message, "case: {case}");
5100 }
5101 }
5102
5103 #[tokio::test(flavor = "multi_thread")]
5104 async fn enum_rejects_bare_type_name_as_value() {
5105 let allow = "@settings(experimentalFeatures = allow)\n";
5106 let colors = (
5107 "colors.kcl",
5108 "@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
5109 );
5110
5111 for (case, main, modules, message) in [
5112 (
5113 "enum suggests a variant",
5114 format!("{allow}type Color {{ | Red | Green }}\nx = Color\n"),
5115 vec![],
5116 "`Color` is a type, not a value. Use one of its variants, such as `Color::Red`.",
5117 ),
5118 (
5119 "suggestion uses the import alias",
5122 format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade\n"),
5123 vec![colors],
5124 "`Shade` is a type, not a value. Use one of its variants, such as `Shade::Red`.",
5125 ),
5126 (
5127 "enum with no variants suggests nothing",
5128 format!("{allow}type Empty {{ | }}\nx = Empty\n"),
5129 vec![],
5130 "`Empty` is a type, not a value.",
5131 ),
5132 (
5133 "a type alias reports the same way",
5134 format!("{allow}type T = number(_)\nx = T\n"),
5135 vec![],
5136 "`T` is a type, not a value.",
5137 ),
5138 (
5139 "an unknown name is still undefined",
5142 "x = Nope\n".to_owned(),
5143 vec![],
5144 "`Nope` is not defined",
5145 ),
5146 ] {
5147 let err = execute_with_modules(&main, &modules).await.unwrap_err();
5148 assert_eq!(err.message(), message, "case: {case}");
5149 }
5150 }
5151
5152 #[tokio::test(flavor = "multi_thread")]
5153 async fn enum_use_gated_by_consuming_module() {
5154 let main = r#"import "colors.kcl"
5162x = colors::Color::Red
5163"#;
5164 let result = execute_with_modules(
5165 main,
5166 &[(
5167 "colors.kcl",
5168 "@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
5169 )],
5170 )
5171 .await
5172 .unwrap();
5173
5174 let issues = &result.exec_state.global.issues;
5175 assert_eq!(issues.len(), 1, "issues: {issues:?}");
5176 assert_eq!(
5177 issues[0].message,
5178 "Use of the enum `Color` is experimental and may change or be removed."
5179 );
5180 assert_eq!(issues[0].severity, Severity::Error);
5181 }
5182
5183 #[tokio::test(flavor = "multi_thread")]
5184 async fn enum_use_not_gated_when_consumer_allows_it() {
5185 let code = r#"@settings(experimentalFeatures = allow)
5188type Color { | Red }
5189x = Color::Red
5190"#;
5191 let result = parse_execute(code).await.unwrap();
5192 assert!(
5193 result.exec_state.global.issues.is_empty(),
5194 "issues: {:?}",
5195 result.exec_state.global.issues
5196 );
5197 }
5198
5199 #[tokio::test(flavor = "multi_thread")]
5200 async fn enum_allows_name_sharing_outside_modules() {
5201 for (case, main, modules) in [
5208 (
5209 "an alias may share a name with a module",
5212 "@settings(experimentalFeatures = allow)\ntype Temperature = number(_)\nimport \"Temperature.kcl\"\n",
5213 vec![("Temperature.kcl", "export x = 1\n")],
5214 ),
5215 (
5216 "a value may share a name with an enum",
5217 "@settings(experimentalFeatures = allow)\ntype Color { | Red }\nColor = 5\n",
5218 vec![],
5219 ),
5220 ] {
5221 if let Err(err) = execute_with_modules(main, &modules).await {
5222 panic!("case: {case}: {}", err.message());
5223 }
5224 }
5225 }
5226
5227 #[tokio::test(flavor = "multi_thread")]
5228 async fn enum_declaration_rejects_redefinition() {
5229 let code = r#"@settings(experimentalFeatures = allow)
5230type Color { | Red }
5231type Color { | Green }
5232"#;
5233 assert_eq!(
5234 parse_execute(code).await.unwrap_err().message(),
5235 "Redefinition of type Color."
5236 );
5237 }
5238
5239 #[tokio::test(flavor = "multi_thread")]
5245 async fn enum_projects_to_string() {
5246 let header = r#"
5247 @settings(experimentalFeatures = allow)
5248 type Color { | Red | Green }
5249 type Label = string
5250 "#;
5251
5252 for (case, body, expected) in [
5253 ("a variant", "x = Color::Red: string", "Red"),
5254 ("another variant of the same enum", "x = Color::Green: string", "Green"),
5255 ("an alias of the target type", "x = Color::Red: Label", "Red"),
5256 (
5257 "an element of a projected array",
5258 r#"
5259 pair = [Color::Red, Color::Green]: [string]
5260 x = pair[1]
5261 "#,
5262 "Green",
5263 ),
5264 (
5265 "an element of a nested projected array",
5266 r#"
5267 grid = [[Color::Green]]: [[string]]
5268 x = grid[0][0]
5269 "#,
5270 "Green",
5271 ),
5272 (
5273 "a one-element array against a bare string",
5274 "x = [Color::Red]: string",
5275 "Red",
5276 ),
5277 ] {
5278 let result = parse_execute(&format!("{header}{body}\n"))
5279 .await
5280 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
5281 let KclValue::String { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
5282 panic!("case: {case}: `x` should hold a string");
5283 };
5284 assert_eq!(value, expected, "case: {case}");
5285 }
5286 }
5287
5288 #[tokio::test(flavor = "multi_thread")]
5292 async fn enum_ascription_keeps_the_enum() {
5293 let header = r#"
5294 @settings(experimentalFeatures = allow)
5295 type Color { | Red | Green }
5296 type Paint = Color
5297 "#;
5298
5299 for (case, expression, expected) in [
5300 ("its own type", "(Color::Red: Color) == Color::Red", true),
5301 ("an alias of its own type", "(Color::Red: Paint) == Color::Red", true),
5302 (
5303 "the ascription does not change which variant it is",
5304 "(Color::Red: Color) == Color::Green",
5305 false,
5306 ),
5307 ] {
5308 let result = parse_execute(&format!("{header}x = {expression}\n"))
5309 .await
5310 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
5311 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
5312 panic!("case: {case}: `x` should hold a bool");
5313 };
5314 assert_eq!(value, expected, "case: {case}");
5315 }
5316 }
5317
5318 #[tokio::test(flavor = "multi_thread")]
5322 async fn enum_projection_is_not_implicit() {
5323 let header = r#"
5324 @settings(experimentalFeatures = allow)
5325 type Color { | Red | Green }
5326 "#;
5327 let found = "but found a value of enum `Color` (with type `Color`).";
5328
5329 for (case, body, expected) in [
5330 (
5331 "unlabeled argument",
5332 r#"
5333 fn label(@text: string) { return text }
5334 x = label(Color::Red)
5335 "#,
5336 format!("The input argument of `label` requires a value with type `string`, {found}"),
5337 ),
5338 (
5339 "labeled argument",
5340 r#"
5341 fn label(text: string) { return text }
5342 x = label(text = Color::Red)
5343 "#,
5344 format!("text requires a value with type `string`, {found}"),
5345 ),
5346 (
5347 "return",
5348 r#"
5349 fn label(): string { return Color::Red }
5350 x = label()
5351 "#,
5352 format!("This function requires its result to be a value with type `string`, {found}"),
5353 ),
5354 (
5355 "inside an array at an argument boundary",
5360 r#"
5361 fn labels(@text: [string]) { return text }
5362 x = labels([Color::Red])
5363 "#,
5364 "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(),
5365 ),
5366 ] {
5367 assert_eq!(
5368 parse_execute(&format!("{header}{body}\n")).await.unwrap_err().message(),
5369 expected,
5370 "case: {case}"
5371 );
5372 }
5373 }
5374
5375 #[tokio::test(flavor = "multi_thread")]
5378 async fn enum_ascription_rejections() {
5379 let header = r#"
5380 @settings(experimentalFeatures = allow)
5381 type Color { | Red }
5382 type Shade { | Red }
5383 "#;
5384 let no_number = "Cannot project enum `Color` to a number. An enum projects to `string`; projecting to a number is not supported yet.";
5385
5386 for (case, expression, expected) in [
5387 ("a number target", "Color::Red: number(_)", no_number.to_owned()),
5388 (
5389 "a number target reached through an array, so the reason survives the walk",
5390 "[Color::Red]: [number(_)]",
5391 no_number.to_owned(),
5392 ),
5393 (
5394 "a boolean target, which is not a projection at all",
5395 "Color::Red: bool",
5396 "could not coerce a value of enum `Color` (with type `Color`) to type `bool`".to_owned(),
5397 ),
5398 (
5399 "another enum whose variants happen to match",
5400 "Color::Red: Shade",
5401 "could not coerce a value of enum `Color` (with type `Color`) to type `Shade`".to_owned(),
5402 ),
5403 ] {
5404 assert_eq!(
5405 parse_execute(&format!("{header}x = {expression}\n"))
5406 .await
5407 .unwrap_err()
5408 .message(),
5409 expected,
5410 "case: {case}"
5411 );
5412 }
5413 }
5414
5415 #[tokio::test(flavor = "multi_thread")]
5422 async fn enum_flows_through_declared_types() {
5423 let header = r#"
5424 @settings(experimentalFeatures = allow)
5425 type Color { | Red | Green }
5426 type Shade { | Red }
5427 "#;
5428
5429 for (case, body, expected) in [
5430 (
5431 "an unlabeled parameter",
5432 r#"
5433 fn paint(@c: Color) { return c }
5434 x = paint(Color::Red) == Color::Red
5435 "#,
5436 None,
5437 ),
5438 (
5439 "a labeled parameter",
5440 r#"
5441 fn paint(c: Color) { return c }
5442 x = paint(c = Color::Green) == Color::Green
5443 "#,
5444 None,
5445 ),
5446 (
5447 "a declared return type",
5448 r#"
5449 fn pick(): Color { return Color::Red }
5450 x = pick() == Color::Red
5451 "#,
5452 None,
5453 ),
5454 (
5455 "an array parameter",
5456 r#"
5457 fn firstOf(@cs: [Color]) { return cs[0] }
5458 x = firstOf([Color::Red, Color::Green]) == Color::Red
5459 "#,
5460 None,
5461 ),
5462 (
5463 "an object field",
5468 r#"
5469 fn take(@o: { c: Color }) { return o.c }
5470 x = take({ c = Color::Green }) == Color::Green
5471 "#,
5472 None,
5473 ),
5474 (
5475 "a union that names the enum",
5476 r#"
5477 fn either(@v: Color | string) { return v }
5478 x = either(Color::Red) == Color::Red
5479 "#,
5480 None,
5481 ),
5482 (
5483 "the same union given the other member",
5484 r#"
5485 fn either(@v: Color | string) { return v }
5486 x = either("plain") == "plain"
5487 "#,
5488 None,
5489 ),
5490 (
5491 "another declaration at the same boundary",
5492 r#"
5493 fn paint(@c: Color) { return c }
5494 x = paint(Shade::Red) == Shade::Red
5495 "#,
5496 Some(
5497 "The input argument of `paint` requires a value with type `Color`, but found a value of enum `Shade` (with type `Shade`).",
5498 ),
5499 ),
5500 ] {
5501 let code = format!("{header}{body}\n");
5502 match expected {
5503 None => {
5504 let result = parse_execute(&code)
5505 .await
5506 .unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
5507 let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x")
5508 else {
5509 panic!("case: {case}: `x` should hold a bool");
5510 };
5511 assert!(value, "case: {case}: the value did not survive the boundary");
5512 }
5513 Some(message) => assert_eq!(
5514 parse_execute(&code).await.unwrap_err().message(),
5515 message,
5516 "case: {case}"
5517 ),
5518 }
5519 }
5520 }
5521}