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