1use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use anyhow::Result;
7pub use artifact::ArtifactCommand;
8pub(crate) use artifact::sketch_block_constraint_type;
9use cache::GlobalState;
10pub use cache::bust_cache;
11pub use cache::clear_mem_cache;
12pub use geometry::*;
13pub use id_generator::IdGenerator;
14pub(crate) use import::PreImportedGeometry;
15use indexmap::IndexMap;
16pub use kcl_api::Operation;
17pub use kcl_api::artifact::Artifact;
18pub use kcl_api::artifact::ArtifactGraph;
19pub use kcl_api::artifact::CapSubType;
20pub use kcl_api::artifact::CodeRef;
21pub use kcl_api::artifact::GdtAnnotationArtifact;
22pub use kcl_api::artifact::SketchBlock;
23pub use kcl_api::artifact::SketchBlockConstraint;
24#[allow(unused_imports)]
25pub use kcl_api::artifact::SketchBlockConstraintType;
26pub use kcl_api::artifact::StartSketchOnFace;
27pub use kcl_api::artifact::StartSketchOnPlane;
28use kcl_api::ast::node_path::NodePath;
29pub use kcl_value::KclObjectFields;
30pub use kcl_value::KclObjectKind;
31pub use kcl_value::KclValue;
32pub use kcl_value_view::KclValueView;
33use kcmc::ImageFormat;
34use kcmc::ModelingCmd;
35use kcmc::each_cmd as mcmd;
36use kcmc::ok_response::OkModelingCmdResponse;
37use kcmc::ok_response::output::TakeSnapshot;
38use kcmc::websocket::ModelingSessionData;
39use kcmc::websocket::OkWebSocketResponseData;
40use kittycad_modeling_cmds::id::ModelingCmdId;
41use kittycad_modeling_cmds::{self as kcmc};
42pub use memory::EnvironmentRef;
43#[cfg(test)]
44pub(crate) use memory::MemoryBackendKind;
45pub(crate) use modeling::ModelingCmdMeta;
46use serde::Deserialize;
47use serde::Serialize;
48pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
49pub(crate) use sketch_solve::solver_numeric_type;
50pub use sketch_transpiler::pre_execute_transpile;
51pub use sketch_transpiler::transpile_all_old_sketches_to_new;
52pub use sketch_transpiler::transpile_old_sketch_to_new;
53pub use sketch_transpiler::transpile_old_sketch_to_new_ast;
54pub use sketch_transpiler::transpile_old_sketch_to_new_with_execution;
55pub(crate) use state::ConstraintKey;
56pub(crate) use state::ConstraintState;
57pub(crate) use state::ConsumedSolidInfo;
58pub(crate) use state::ConsumedSolidKey;
59pub(crate) use state::ConsumedSolidOperation;
60pub use state::DirectTagFilletMeta;
61pub use state::DirectTagFilletTagEntry;
62pub use state::EdgeRefactorMeta;
63pub use state::EdgeRefactorStdlibFn;
64pub use state::ExecState;
65pub(crate) use state::KclVersion;
66pub use state::MetaSettings;
67pub(crate) use state::ModuleArtifactState;
68pub(crate) use state::PendingEdgeRefactorMeta;
69pub use state::RefactorMetadata;
70pub(crate) use state::TangencyMode;
71
72use crate::CompilationIssue;
73use crate::ExecError;
74use crate::KclErrorWithOutputs;
75use crate::NodePathExt;
76use crate::SourceRange;
77use crate::collections::AhashIndexSet;
78use crate::engine::EngineBatchContext;
79use crate::engine::GridScaleBehavior;
80use crate::engine::engine_manager::EngineManager;
81use crate::errors::KclError;
82use crate::errors::KclErrorDetails;
83use crate::execution::cache::CacheInformation;
84use crate::execution::cache::CacheResult;
85use crate::execution::cad_op::OperationExt;
86use crate::execution::import_graph::Universe;
87use crate::execution::import_graph::UniverseMap;
88use crate::execution::typed_path::TypedPath;
89use crate::front::Number;
90use crate::front::Object;
91use crate::front::ObjectId;
92use crate::fs::FileManager;
93use crate::fs::FileSystemHandle;
94use crate::modules::ModuleExecutionOutcome;
95use crate::modules::ModuleId;
96use crate::modules::ModulePath;
97use crate::modules::ModuleRepr;
98use crate::parsing::ast::types::Expr;
99use crate::parsing::ast::types::ImportPath;
100use crate::parsing::ast::types::NodeRef;
101
102#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
103#[ts(export)]
104pub struct OperationsByModule {
105 pub map: IndexMap<ModuleId, Vec<Operation>>,
106}
107
108#[derive(Clone, Serialize, ts_rs::TS)]
109#[ts(export)]
110#[serde(rename_all = "camelCase")]
111pub struct OperationCallbackArgs {
112 pub module_id: ModuleId,
113 pub operation: Operation,
114 pub index: usize,
115}
116
117pub trait ExecutionCallbacks: std::fmt::Debug + Send + Sync + 'static {
118 fn on_operation(&self, _args: OperationCallbackArgs) {}
119}
120
121impl OperationsByModule {
122 pub fn count(&self) -> usize {
123 self.map.values().map(Vec::len).sum()
124 }
125
126 pub fn is_empty(&self) -> bool {
127 self.map.values().all(Vec::is_empty)
128 }
129
130 pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
131 self.map.get(module_id)
132 }
133
134 pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
135 self.map.values()
136 }
137
138 pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
139 self.map.insert(module_id, operations);
140 }
141}
142
143pub(crate) mod annotations;
144mod artifact;
145#[cfg(test)]
146pub(crate) use artifact::mermaid_tests::ArtifactGraphMermaidExt;
147pub(crate) mod cache;
148mod cad_op;
149mod exec_ast;
150pub mod fn_call;
151#[cfg(test)]
152mod freedom_analysis_tests;
153mod geometry;
154mod id_generator;
155mod import;
156mod import_graph;
157pub(crate) mod kcl_value;
158pub(crate) mod kcl_value_view;
159mod memory;
160mod modeling;
161mod sketch_solve;
162mod sketch_transpiler;
163mod state;
164pub mod typed_path;
165pub(crate) mod types;
166
167pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
168pub(crate) const SKETCH_OBJECT_META: &str = "meta";
169pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
170
171macro_rules! control_continue {
176 ($control_flow:expr) => {{
177 let cf = $control_flow;
178 if cf.is_some_return() {
179 return Ok(cf);
180 } else {
181 cf.into_value()
182 }
183 }};
184}
185pub(crate) use control_continue;
187
188macro_rules! early_return {
193 ($control_flow:expr) => {{
194 let cf = $control_flow;
195 if cf.is_some_return() {
196 return Err(EarlyReturn::from(cf));
197 } else {
198 cf.into_value()
199 }
200 }};
201}
202pub(crate) use early_return;
204
205#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
206pub enum ControlFlowKind {
207 #[default]
208 Continue,
209 Exit,
210}
211
212impl ControlFlowKind {
213 pub fn is_some_return(&self) -> bool {
215 match self {
216 ControlFlowKind::Continue => false,
217 ControlFlowKind::Exit => true,
218 }
219 }
220}
221
222#[must_use = "You should always handle the control flow value when it is returned"]
223#[derive(Debug, Clone, PartialEq, Serialize)]
224pub struct KclValueControlFlow {
225 value: Box<KclValue>,
227 pub control: ControlFlowKind,
228}
229
230impl KclValue {
231 pub(crate) fn continue_(self) -> KclValueControlFlow {
232 KclValueControlFlow {
233 value: Box::new(self),
234 control: ControlFlowKind::Continue,
235 }
236 }
237
238 pub(crate) fn exit(self) -> KclValueControlFlow {
239 KclValueControlFlow {
240 value: Box::new(self),
241 control: ControlFlowKind::Exit,
242 }
243 }
244}
245
246impl KclValueControlFlow {
247 pub fn is_some_return(&self) -> bool {
249 self.control.is_some_return()
250 }
251
252 pub(crate) fn into_value(self) -> KclValue {
253 *self.value
254 }
255}
256
257#[must_use = "You should always handle the control flow value when it is returned"]
264#[allow(clippy::large_enum_variant)]
265#[derive(Debug, Clone)]
266pub(crate) enum EarlyReturn {
267 Value(KclValueControlFlow),
269 Error(KclError),
271}
272
273impl From<KclValueControlFlow> for EarlyReturn {
274 fn from(cf: KclValueControlFlow) -> Self {
275 EarlyReturn::Value(cf)
276 }
277}
278
279impl From<KclError> for EarlyReturn {
280 fn from(err: KclError) -> Self {
281 EarlyReturn::Error(err)
282 }
283}
284
285pub(crate) enum StatementKind<'a> {
286 Declaration { name: &'a str },
287 Expression,
288}
289
290#[derive(Debug, Clone, Copy)]
291pub enum PreserveMem {
292 Normal,
293 Always,
294}
295
296impl PreserveMem {
297 fn normal(self) -> bool {
298 match self {
299 PreserveMem::Normal => true,
300 PreserveMem::Always => false,
301 }
302 }
303}
304
305#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
307#[ts(export)]
308#[serde(rename_all = "camelCase")]
309pub struct ExecOutcome {
310 pub variables: IndexMap<String, KclValueView>,
312 pub operations: OperationsByModule,
315 pub artifact_graph: ArtifactGraph,
317 #[serde(skip)]
319 pub scene_objects: Vec<Object>,
320 #[serde(skip)]
323 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
324 #[serde(skip)]
325 pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
326 pub refactor_metadata: Vec<RefactorMetadata>,
328 pub issues: Vec<CompilationIssue>,
330 pub filenames: IndexMap<ModuleId, ModulePath>,
332 pub default_planes: Option<DefaultPlanes>,
334}
335
336#[derive(Debug, Clone, Copy, PartialEq)]
340enum SegmentFreedom {
341 Free,
342 Fixed,
343 Conflict,
344 Error,
346}
347
348impl From<crate::front::Freedom> for SegmentFreedom {
349 fn from(f: crate::front::Freedom) -> Self {
350 match f {
351 crate::front::Freedom::Free => Self::Free,
352 crate::front::Freedom::Fixed => Self::Fixed,
353 crate::front::Freedom::Conflict => Self::Conflict,
354 }
355 }
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
360pub enum ConstraintKind {
361 FullyConstrained,
362 UnderConstrained,
363 OverConstrained,
364 Error,
368}
369
370#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
377pub struct SketchConstraintStatus {
378 pub name: String,
380 pub status: ConstraintKind,
382 pub free_count: usize,
384 pub conflict_count: usize,
386 pub total_count: usize,
388}
389
390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
392pub struct SketchConstraintReport {
393 pub fully_constrained: Vec<SketchConstraintStatus>,
394 pub under_constrained: Vec<SketchConstraintStatus>,
395 pub over_constrained: Vec<SketchConstraintStatus>,
396 pub errors: Vec<SketchConstraintStatus>,
399}
400
401pub(crate) fn sketch_constraint_status_for_sketch(
410 scene_objects: &[Object],
411 sketch_obj: &Object,
412) -> Option<SketchConstraintStatus> {
413 use crate::front::ObjectKind;
414 use crate::front::Segment;
415
416 let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
417 return None;
418 };
419
420 let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
422 let obj = scene_objects.get(id.0)?;
423 if let ObjectKind::Segment {
424 segment: Segment::Point(p),
425 } = &obj.kind
426 {
427 Some(p.freedom())
428 } else {
429 None
430 }
431 };
432
433 let mut free_count: usize = 0;
434 let mut conflict_count: usize = 0;
435 let mut error_count: usize = 0;
436 let mut total_count: usize = 0;
437
438 for &seg_id in &sketch.segments {
439 let Some(seg_obj) = scene_objects.get(seg_id.0) else {
440 continue;
441 };
442 let ObjectKind::Segment { segment } = &seg_obj.kind else {
443 continue;
444 };
445 if let Segment::Point(p) = segment
448 && p.owner.is_some()
449 {
450 continue;
451 }
452 let freedom = segment
453 .freedom(lookup)
454 .map(SegmentFreedom::from)
455 .unwrap_or(SegmentFreedom::Error);
456 total_count += 1;
457 match freedom {
458 SegmentFreedom::Free => free_count += 1,
459 SegmentFreedom::Conflict => conflict_count += 1,
460 SegmentFreedom::Error => error_count += 1,
461 SegmentFreedom::Fixed => {}
462 }
463 }
464
465 let status = if error_count > 0 {
466 ConstraintKind::Error
467 } else if conflict_count > 0 {
468 ConstraintKind::OverConstrained
469 } else if free_count > 0 {
470 ConstraintKind::UnderConstrained
471 } else {
472 ConstraintKind::FullyConstrained
473 };
474
475 Some(SketchConstraintStatus {
476 name: sketch_obj.label.clone(),
477 status,
478 free_count,
479 conflict_count,
480 total_count,
481 })
482}
483
484pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
485 let mut fully_constrained = Vec::new();
486 let mut under_constrained = Vec::new();
487 let mut over_constrained = Vec::new();
488 let mut errors = Vec::new();
489
490 for obj in scene_objects {
491 let Some(entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
492 continue;
493 };
494 match entry.status {
495 ConstraintKind::FullyConstrained => fully_constrained.push(entry),
496 ConstraintKind::UnderConstrained => under_constrained.push(entry),
497 ConstraintKind::OverConstrained => over_constrained.push(entry),
498 ConstraintKind::Error => errors.push(entry),
499 }
500 }
501
502 SketchConstraintReport {
503 fully_constrained,
504 under_constrained,
505 over_constrained,
506 errors,
507 }
508}
509
510impl ExecOutcome {
511 pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
512 debug_assert!(
513 id.0 < self.scene_objects.len(),
514 "Requested object ID {} but only have {} objects",
515 id.0,
516 self.scene_objects.len()
517 );
518 self.scene_objects.get(id.0)
519 }
520
521 pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
523 self.issues.iter().filter(|error| error.is_err())
524 }
525
526 pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
533 sketch_constraint_report_from_scene_objects(&self.scene_objects)
534 }
535}
536
537#[derive(Debug, Clone, PartialEq)]
539pub struct MockConfig {
540 pub use_prev_memory: bool,
541 pub sketch_block_id: Option<ObjectId>,
544 pub freedom_analysis: bool,
547 pub segment_ids_edited: AhashIndexSet<ObjectId>,
549 pub drag_anchors: Vec<SegmentDragAnchor>,
551}
552
553#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
554#[ts(export, export_to = "FrontendApi.ts")]
555#[serde(rename_all = "camelCase")]
556pub struct SegmentDragAnchor {
557 pub segment_id: ObjectId,
558 pub target: crate::front::Point2d<Number>,
559}
560
561impl Default for MockConfig {
562 fn default() -> Self {
563 Self {
564 use_prev_memory: true,
566 sketch_block_id: None,
567 freedom_analysis: true,
568 segment_ids_edited: AhashIndexSet::default(),
569 drag_anchors: Vec::new(),
570 }
571 }
572}
573
574impl MockConfig {
575 pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
577 Self {
578 sketch_block_id: Some(sketch_block_id),
579 ..Default::default()
580 }
581 }
582
583 #[must_use]
584 pub(crate) fn no_freedom_analysis(mut self) -> Self {
585 self.freedom_analysis = false;
586 self
587 }
588}
589
590#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
591#[ts(export)]
592#[serde(rename_all = "camelCase")]
593pub struct DefaultPlanes {
594 pub xy: uuid::Uuid,
595 pub xz: uuid::Uuid,
596 pub yz: uuid::Uuid,
597 pub neg_xy: uuid::Uuid,
598 pub neg_xz: uuid::Uuid,
599 pub neg_yz: uuid::Uuid,
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
603#[ts(export)]
604#[serde(tag = "type", rename_all = "camelCase")]
605pub struct TagIdentifier {
606 pub value: String,
607 #[serde(skip)]
610 pub info: Vec<(usize, TagEngineInfo)>,
611 #[serde(skip)]
612 pub meta: Vec<Metadata>,
613}
614
615impl TagIdentifier {
616 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
618 for (e, info) in self.info.iter().rev() {
619 if *e <= at_epoch {
620 return Some(info);
621 }
622 }
623
624 None
625 }
626
627 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
629 self.info.last().map(|i| &i.1)
630 }
631
632 pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
635 let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
636 return vec![];
637 };
638 self.info
639 .iter()
640 .rev()
641 .take_while(|(e, _)| *e == cur_epoch)
642 .map(|(_, info)| info)
643 .collect()
644 }
645
646 pub fn merge_info(&mut self, other: &TagIdentifier) {
648 assert_eq!(&self.value, &other.value);
649 for (oe, ot) in &other.info {
650 if let Some((e, t)) = self.info.last_mut() {
651 if *e > *oe {
653 continue;
654 }
655 if e == oe {
657 *t = ot.clone();
658 continue;
659 }
660 }
661 self.info.push((*oe, ot.clone()));
662 }
663 }
664
665 pub fn geometry(&self) -> Option<Geometry> {
666 self.get_cur_info().map(|info| info.geometry.clone())
667 }
668
669 pub(crate) fn is_body_created_tag(&self) -> bool {
670 self.get_cur_info().is_some_and(|info| {
671 matches!(&info.geometry, Geometry::Solid(_)) && info.path.is_none() && info.surface.is_some()
672 })
673 }
674}
675
676impl Eq for TagIdentifier {}
677
678impl std::fmt::Display for TagIdentifier {
679 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
680 write!(f, "{}", self.value)
681 }
682}
683
684impl std::str::FromStr for TagIdentifier {
685 type Err = KclError;
686
687 fn from_str(s: &str) -> Result<Self, Self::Err> {
688 Ok(Self {
689 value: s.to_string(),
690 info: Vec::new(),
691 meta: Default::default(),
692 })
693 }
694}
695
696impl Ord for TagIdentifier {
697 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
698 self.value.cmp(&other.value)
699 }
700}
701
702impl PartialOrd for TagIdentifier {
703 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
704 Some(self.cmp(other))
705 }
706}
707
708impl std::hash::Hash for TagIdentifier {
709 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
710 self.value.hash(state);
711 }
712}
713
714#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
716#[ts(export)]
717#[serde(tag = "type", rename_all = "camelCase")]
718pub struct TagEngineInfo {
719 pub id: uuid::Uuid,
721 pub geometry: Geometry,
723 pub path: Option<Path>,
725 pub surface: Option<ExtrudeSurface>,
727}
728
729#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
730pub enum BodyType {
731 Root,
732 Block,
733}
734
735#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
737#[ts(export)]
738#[serde(rename_all = "camelCase")]
739pub struct Metadata {
740 pub source_range: SourceRange,
742}
743
744impl From<Metadata> for Vec<SourceRange> {
745 fn from(meta: Metadata) -> Self {
746 vec![meta.source_range]
747 }
748}
749
750impl From<&Metadata> for SourceRange {
751 fn from(meta: &Metadata) -> Self {
752 meta.source_range
753 }
754}
755
756impl From<SourceRange> for Metadata {
757 fn from(source_range: SourceRange) -> Self {
758 Self { source_range }
759 }
760}
761
762impl<T> From<NodeRef<'_, T>> for Metadata {
763 fn from(node: NodeRef<'_, T>) -> Self {
764 Self {
765 source_range: SourceRange::new(node.start, node.end, node.module_id),
766 }
767 }
768}
769
770impl From<&Expr> for Metadata {
771 fn from(expr: &Expr) -> Self {
772 Self {
773 source_range: SourceRange::from(expr),
774 }
775 }
776}
777
778impl Metadata {
779 pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
780 if meta.len() == 1 {
781 let meta = &meta[0];
782 return crate::front::SourceRef::Simple {
783 range: meta.source_range,
784 node_path,
785 };
786 }
787 crate::front::SourceRef::BackTrace {
788 ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
789 }
790 }
791}
792
793#[derive(PartialEq, Debug, Default, Clone)]
795pub enum ContextType {
796 #[default]
798 Live,
799
800 Mock,
804
805 MockCustomForwarded,
807}
808
809#[derive(Clone)]
813pub struct ExecutorContext {
814 pub engine: Arc<EngineManager>,
815 pub engine_batch: EngineBatchContext,
816 pub fs: FileSystemHandle,
817 pub settings: ExecutorSettings,
818 pub context_type: ContextType,
819 pub execution_callbacks: Option<Arc<dyn ExecutionCallbacks>>,
820}
821
822impl std::fmt::Debug for ExecutorContext {
823 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
824 f.debug_struct("ExecutorContext")
825 .field("engine", &self.engine)
826 .field("engine_batch", &self.engine_batch)
827 .field("settings", &self.settings)
828 .field("context_type", &self.context_type)
829 .field("execution_callbacks", &self.execution_callbacks)
830 .finish()
831 }
832}
833
834#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
836#[ts(export)]
837pub struct ExecutorSettings {
838 pub highlight_edges: bool,
840 pub enable_ssao: bool,
842 pub show_grid: bool,
844 pub replay: Option<String>,
847 pub project_directory: Option<TypedPath>,
850 pub current_file: Option<TypedPath>,
853 pub fixed_size_grid: bool,
855 #[serde(default, skip_serializing_if = "is_false")]
861 pub skip_artifact_graph: bool,
862 #[serde(default, skip_serializing_if = "Option::is_none")]
865 pub heartbeats: Option<u64>,
866 #[serde(default, skip_serializing_if = "Option::is_none")]
869 pub default_backface_color: Option<String>,
870}
871
872fn is_false(b: &bool) -> bool {
873 !*b
874}
875
876impl Default for ExecutorSettings {
877 fn default() -> Self {
878 Self {
879 highlight_edges: true,
880 enable_ssao: false,
881 show_grid: false,
882 replay: None,
883 project_directory: None,
884 current_file: None,
885 fixed_size_grid: true,
886 skip_artifact_graph: false,
887 heartbeats: None,
888 default_backface_color: None,
889 }
890 }
891}
892
893impl From<crate::settings::types::Configuration> for ExecutorSettings {
894 fn from(config: crate::settings::types::Configuration) -> Self {
895 Self::from(config.settings)
896 }
897}
898
899impl From<crate::settings::types::Settings> for ExecutorSettings {
900 fn from(settings: crate::settings::types::Settings) -> Self {
901 let modeling_settings = settings.modeling.unwrap_or_default();
902 Self {
903 highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
904 enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
905 show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
906 replay: None,
907 project_directory: None,
908 current_file: None,
909 fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
910 skip_artifact_graph: false,
911 heartbeats: None,
912 default_backface_color: modeling_settings.backface_color.map(|color| color.0),
913 }
914 }
915}
916
917impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
918 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
919 Self::from(config.settings.modeling)
920 }
921}
922
923impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
924 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
925 Self {
926 highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
927 enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
928 show_grid: modeling.show_scale_grid.unwrap_or_default(),
929 replay: None,
930 project_directory: None,
931 current_file: None,
932 fixed_size_grid: true,
933 skip_artifact_graph: false,
934 heartbeats: None,
935 default_backface_color: modeling.backface_color.map(|color| color.0),
936 }
937 }
938}
939
940impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
941 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
942 Self {
943 highlight_edges: modeling.highlight_edges.into(),
944 enable_ssao: modeling.enable_ssao.into(),
945 show_grid: Default::default(),
946 replay: None,
947 project_directory: None,
948 current_file: None,
949 fixed_size_grid: true,
950 skip_artifact_graph: false,
951 heartbeats: None,
952 default_backface_color: None,
953 }
954 }
955}
956
957impl ExecutorSettings {
958 pub fn with_current_file(&mut self, current_file: TypedPath) {
960 if current_file.extension() == Some("kcl") {
962 self.current_file = Some(current_file.clone());
963 if let Some(parent) = current_file.parent() {
965 self.project_directory = Some(parent);
966 } else {
967 self.project_directory = Some(TypedPath::from(""));
968 }
969 } else {
970 self.project_directory = Some(current_file);
971 }
972 }
973}
974
975impl ExecutorContext {
976 pub fn new_with_engine_and_fs(
978 engine: Arc<EngineManager>,
979 fs: FileSystemHandle,
980 settings: ExecutorSettings,
981 ) -> Self {
982 ExecutorContext {
983 engine,
984 engine_batch: EngineBatchContext::default(),
985 fs,
986 settings,
987 context_type: ContextType::Live,
988 execution_callbacks: Default::default(),
989 }
990 }
991
992 fn clone_with_fresh_execution_batch(&self) -> Self {
993 Self {
994 engine: self.engine.clone(),
995 engine_batch: EngineBatchContext::new(),
996 fs: self.fs.clone(),
997 settings: self.settings.clone(),
998 context_type: self.context_type.clone(),
999 execution_callbacks: self.execution_callbacks.clone(),
1000 }
1001 }
1002
1003 #[cfg(not(target_arch = "wasm32"))]
1005 pub fn new_with_engine(engine: Arc<EngineManager>, settings: ExecutorSettings) -> Self {
1006 Self::new_with_engine_and_fs(engine, crate::fs::new_file_system_handle(FileManager::new()), settings)
1007 }
1008
1009 #[cfg(not(target_arch = "wasm32"))]
1011 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
1012 let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
1013 let (ws, _headers) = client
1014 .modeling()
1015 .commands_ws(kittycad::modeling::CommandsWsParams {
1016 api_call_id: None,
1017 fps: None,
1018 order_independent_transparency: None,
1019 post_effect: if settings.enable_ssao {
1020 Some(kittycad::types::PostEffectType::Ssao)
1021 } else {
1022 None
1023 },
1024 replay: settings.replay.clone(),
1025 show_grid: if settings.show_grid { Some(true) } else { None },
1026 pool: None,
1027 pr,
1028 unlocked_framerate: None,
1029 webrtc: Some(false),
1030 video_res_width: None,
1031 video_res_height: None,
1032 })
1033 .await?;
1034
1035 let engine_conn = EngineManager::new_websocket_transport(ws, settings.heartbeats).await;
1036 let engine = Arc::new(engine_conn);
1037
1038 Ok(Self::new_with_engine(engine, settings))
1039 }
1040
1041 #[cfg(target_arch = "wasm32")]
1042 pub fn new(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1043 Self::new_with_engine_and_fs(engine, fs, settings)
1044 }
1045
1046 #[cfg(not(target_arch = "wasm32"))]
1047 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
1048 ExecutorContext {
1049 engine: Arc::new(EngineManager::new_mock()),
1050 engine_batch: EngineBatchContext::default(),
1051 fs: crate::fs::new_file_system_handle(FileManager::new()),
1052 settings: settings.unwrap_or_default(),
1053 context_type: ContextType::Mock,
1054 execution_callbacks: Default::default(),
1055 }
1056 }
1057
1058 #[cfg(target_arch = "wasm32")]
1059 pub fn new_mock(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
1060 ExecutorContext {
1061 engine,
1062 engine_batch: EngineBatchContext::default(),
1063 fs,
1064 settings,
1065 context_type: ContextType::Mock,
1066 execution_callbacks: Default::default(),
1067 }
1068 }
1069
1070 #[cfg(target_arch = "wasm32")]
1073 pub fn new_mock_for_lsp(
1074 fs_manager: crate::fs::wasm::FileSystemManager,
1075 settings: ExecutorSettings,
1076 ) -> Result<Self, String> {
1077 let fs = crate::fs::new_file_system_handle(FileManager::new(fs_manager));
1078
1079 Ok(ExecutorContext {
1080 engine: Arc::new(EngineManager::new_mock()),
1081 engine_batch: EngineBatchContext::default(),
1082 fs,
1083 settings,
1084 context_type: ContextType::Mock,
1085 execution_callbacks: Default::default(),
1086 })
1087 }
1088
1089 #[cfg(not(target_arch = "wasm32"))]
1090 pub fn new_forwarded_mock(engine: Arc<EngineManager>) -> Self {
1091 ExecutorContext {
1092 engine,
1093 engine_batch: EngineBatchContext::default(),
1094 fs: crate::fs::new_file_system_handle(FileManager::new()),
1095 settings: Default::default(),
1096 context_type: ContextType::MockCustomForwarded,
1097 execution_callbacks: Default::default(),
1098 }
1099 }
1100
1101 #[cfg(not(target_arch = "wasm32"))]
1107 pub async fn new_with_client(
1108 settings: ExecutorSettings,
1109 token: Option<String>,
1110 engine_addr: Option<String>,
1111 ) -> Result<Self> {
1112 let client = crate::engine::new_zoo_client(token, engine_addr)?;
1114
1115 let ctx = Self::new(&client, settings).await?;
1116 Ok(ctx)
1117 }
1118
1119 #[cfg(not(target_arch = "wasm32"))]
1124 pub async fn new_with_default_client() -> Result<Self> {
1125 let ctx = Self::new_with_client(Default::default(), None, None).await?;
1127 Ok(ctx)
1128 }
1129
1130 #[cfg(not(target_arch = "wasm32"))]
1132 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
1133 let ctx = ExecutorContext::new_with_client(
1134 ExecutorSettings {
1135 highlight_edges: true,
1136 enable_ssao: false,
1137 show_grid: false,
1138 replay: None,
1139 project_directory: None,
1140 current_file: None,
1141 fixed_size_grid: false,
1142 skip_artifact_graph: false,
1143 heartbeats: None,
1144 default_backface_color: None,
1145 },
1146 None,
1147 engine_addr,
1148 )
1149 .await?;
1150 Ok(ctx)
1151 }
1152
1153 pub fn is_mock(&self) -> bool {
1154 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1155 }
1156
1157 pub async fn no_engine_commands(&self) -> bool {
1159 self.is_mock()
1160 }
1161
1162 pub async fn send_clear_scene(
1163 &self,
1164 exec_state: &mut ExecState,
1165 source_range: crate::execution::SourceRange,
1166 ) -> Result<(), KclError> {
1167 exec_state.mod_local.artifacts.clear();
1170 exec_state.global.root_module_artifacts.clear();
1171 exec_state.global.artifacts.clear();
1172
1173 self.engine
1174 .clear_scene(&self.engine_batch, &mut exec_state.mod_local.id_generator, source_range)
1175 .await?;
1176 if self.settings.enable_ssao {
1179 let cmd_id = exec_state.next_uuid();
1180 exec_state
1181 .batch_modeling_cmd(
1182 ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1183 ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1184 )
1185 .await?;
1186 }
1187 Ok(())
1188 }
1189
1190 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1191 cache::bust_cache().await;
1192
1193 let outcome = self.run_with_caching(crate::Program::empty()).await?;
1198
1199 Ok(outcome)
1200 }
1201
1202 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1203 self.eval_prelude(exec_state, SourceRange::synthetic())
1204 .await
1205 .map_err(KclErrorWithOutputs::no_outputs)?;
1206 exec_state
1207 .mut_stack()
1208 .push_new_root_env(true)
1209 .map_err(KclErrorWithOutputs::no_outputs)?;
1210 Ok(())
1211 }
1212
1213 fn restore_mock_memory(
1214 exec_state: &mut ExecState,
1215 mem: cache::SketchModeState,
1216 _mock_config: &MockConfig,
1217 ) -> Result<(), KclErrorWithOutputs> {
1218 *exec_state.mut_stack() = mem.stack;
1219 exec_state.global.module_infos = mem.module_infos;
1220 exec_state.global.path_to_source_id = mem.path_to_source_id;
1221 exec_state.global.id_to_source = mem.id_to_source;
1222 exec_state.mod_local.constraint_state = mem.constraint_state;
1223 let len = _mock_config
1224 .sketch_block_id
1225 .map(|sketch_block_id| sketch_block_id.0)
1226 .unwrap_or(0);
1227 if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1228 exec_state
1229 .global
1230 .root_module_artifacts
1231 .restore_scene_objects(scene_objects);
1232 } else {
1233 let message = format!(
1234 "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1235 mem.scene_objects.len(),
1236 len
1237 );
1238 debug_assert!(false, "{message}");
1239 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1240 KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1241 )));
1242 }
1243
1244 Ok(())
1245 }
1246
1247 pub async fn run_mock(
1248 &self,
1249 program: &crate::Program,
1250 mock_config: &MockConfig,
1251 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1252 assert!(
1253 self.is_mock(),
1254 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1255 );
1256
1257 let use_prev_memory = mock_config.use_prev_memory;
1258 let mut exec_state = ExecState::new_mock(self, mock_config);
1259 if use_prev_memory {
1260 match cache::read_old_memory().await {
1261 Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
1262 None => self.prepare_mem(&mut exec_state).await?,
1263 }
1264 } else {
1265 self.prepare_mem(&mut exec_state).await?
1266 };
1267
1268 exec_state
1271 .mut_stack()
1272 .push_new_env_for_scope()
1273 .map_err(KclErrorWithOutputs::no_outputs)?;
1274
1275 let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1276
1277 let mut stack = exec_state.stack().clone();
1282 let module_infos = exec_state.global.module_infos.clone();
1283 let path_to_source_id = exec_state.global.path_to_source_id.clone();
1284 let id_to_source = exec_state.global.id_to_source.clone();
1285 let constraint_state = exec_state.mod_local.constraint_state.clone();
1286 let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1287 let outcome = exec_state
1288 .into_exec_outcome(result.0, self)
1289 .await
1290 .map_err(KclErrorWithOutputs::no_outputs)?;
1291
1292 stack.squash_env(result.0).map_err(KclErrorWithOutputs::no_outputs)?;
1293 let state = cache::SketchModeState {
1294 stack,
1295 module_infos,
1296 path_to_source_id,
1297 id_to_source,
1298 constraint_state,
1299 scene_objects,
1300 };
1301 cache::write_old_memory(state).await;
1302
1303 Ok(outcome)
1304 }
1305
1306 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1307 assert!(!self.is_mock());
1308 let grid_scale = if self.settings.fixed_size_grid {
1309 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1310 } else {
1311 GridScaleBehavior::ScaleWithZoom
1312 };
1313
1314 let original_program = program.clone();
1315
1316 let (_program, exec_state, result) = match cache::read_old_ast().await {
1317 Some(mut cached_state) => {
1318 let old = CacheInformation {
1319 ast: &cached_state.main.ast,
1320 settings: &cached_state.settings,
1321 };
1322 let new = CacheInformation {
1323 ast: &program.ast,
1324 settings: &self.settings,
1325 };
1326
1327 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1329 CacheResult::ReExecute {
1330 clear_scene,
1331 reapply_settings,
1332 program: changed_program,
1333 } => {
1334 if reapply_settings
1335 && self
1336 .engine
1337 .reapply_settings(
1338 &self.engine_batch,
1339 &self.settings,
1340 Default::default(),
1341 &mut cached_state.main.exec_state.id_generator,
1342 grid_scale,
1343 )
1344 .await
1345 .is_err()
1346 {
1347 (true, program, None)
1348 } else {
1349 (
1350 clear_scene,
1351 crate::Program {
1352 ast: changed_program,
1353 original_file_contents: program.original_file_contents,
1354 },
1355 None,
1356 )
1357 }
1358 }
1359 CacheResult::CheckImportsOnly {
1360 reapply_settings,
1361 ast: changed_program,
1362 } => {
1363 let mut reapply_failed = false;
1364 if reapply_settings {
1365 if self
1366 .engine
1367 .reapply_settings(
1368 &self.engine_batch,
1369 &self.settings,
1370 Default::default(),
1371 &mut cached_state.main.exec_state.id_generator,
1372 grid_scale,
1373 )
1374 .await
1375 .is_ok()
1376 {
1377 cache::write_old_ast(GlobalState::with_settings(
1378 cached_state.clone(),
1379 self.settings.clone(),
1380 ))
1381 .await;
1382 } else {
1383 reapply_failed = true;
1384 }
1385 }
1386
1387 if reapply_failed {
1388 (true, program, None)
1389 } else {
1390 let mut new_exec_state = ExecState::new(self);
1392 let (new_universe, new_universe_map) =
1393 self.get_universe(&program, &mut new_exec_state).await?;
1394
1395 let clear_scene = new_universe.values().any(|value| {
1396 let id = value.1;
1397 match (
1398 cached_state.exec_state.get_source(id),
1399 new_exec_state.global.get_source(id),
1400 ) {
1401 (Some(s0), Some(s1)) => s0.source != s1.source,
1402 _ => false,
1403 }
1404 });
1405
1406 if !clear_scene {
1407 cache::write_old_memory(
1409 cached_state
1410 .mock_memory_state()
1411 .map_err(KclErrorWithOutputs::no_outputs)?,
1412 )
1413 .await;
1414 return cached_state
1415 .into_exec_outcome(self)
1416 .await
1417 .map_err(KclErrorWithOutputs::no_outputs);
1418 }
1419
1420 (
1421 true,
1422 crate::Program {
1423 ast: changed_program,
1424 original_file_contents: program.original_file_contents,
1425 },
1426 Some((new_universe, new_universe_map, new_exec_state)),
1427 )
1428 }
1429 }
1430 CacheResult::NoAction(true) => {
1431 if self
1432 .engine
1433 .reapply_settings(
1434 &self.engine_batch,
1435 &self.settings,
1436 Default::default(),
1437 &mut cached_state.main.exec_state.id_generator,
1438 grid_scale,
1439 )
1440 .await
1441 .is_ok()
1442 {
1443 cache::write_old_ast(GlobalState::with_settings(
1445 cached_state.clone(),
1446 self.settings.clone(),
1447 ))
1448 .await;
1449
1450 cache::write_old_memory(
1451 cached_state
1452 .mock_memory_state()
1453 .map_err(KclErrorWithOutputs::no_outputs)?,
1454 )
1455 .await;
1456 return cached_state
1457 .into_exec_outcome(self)
1458 .await
1459 .map_err(KclErrorWithOutputs::no_outputs);
1460 }
1461 (true, program, None)
1462 }
1463 CacheResult::NoAction(false) => {
1464 cache::write_old_memory(
1465 cached_state
1466 .mock_memory_state()
1467 .map_err(KclErrorWithOutputs::no_outputs)?,
1468 )
1469 .await;
1470 return cached_state
1471 .into_exec_outcome(self)
1472 .await
1473 .map_err(KclErrorWithOutputs::no_outputs);
1474 }
1475 };
1476
1477 let (exec_state, result) = match import_check_info {
1478 Some((new_universe, new_universe_map, mut new_exec_state)) => {
1479 self.send_clear_scene(&mut new_exec_state, Default::default())
1481 .await
1482 .map_err(KclErrorWithOutputs::no_outputs)?;
1483
1484 let result = self
1485 .run_concurrent(
1486 &program,
1487 &mut new_exec_state,
1488 Some((new_universe, new_universe_map)),
1489 PreserveMem::Normal,
1490 )
1491 .await;
1492
1493 (new_exec_state, result)
1494 }
1495 None if clear_scene => {
1496 let mut exec_state = cached_state.reconstitute_exec_state(self);
1498 exec_state.reset(self);
1499
1500 self.send_clear_scene(&mut exec_state, Default::default())
1501 .await
1502 .map_err(KclErrorWithOutputs::no_outputs)?;
1503
1504 let result = self
1505 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1506 .await;
1507
1508 (exec_state, result)
1509 }
1510 None => {
1511 let mut exec_state = cached_state.reconstitute_exec_state(self);
1512 exec_state
1513 .mut_stack()
1514 .restore_env(cached_state.main.result_env)
1515 .map_err(KclErrorWithOutputs::no_outputs)?;
1516
1517 let result = self
1518 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1519 .await;
1520
1521 (exec_state, result)
1522 }
1523 };
1524
1525 (program, exec_state, result)
1526 }
1527 None => {
1528 let mut exec_state = ExecState::new(self);
1529 self.send_clear_scene(&mut exec_state, Default::default())
1530 .await
1531 .map_err(KclErrorWithOutputs::no_outputs)?;
1532
1533 let result = self
1534 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1535 .await;
1536
1537 (program, exec_state, result)
1538 }
1539 };
1540
1541 if result.is_err() {
1542 cache::bust_cache().await;
1543 }
1544
1545 let result = result?;
1547
1548 cache::write_old_ast(GlobalState::new(
1552 exec_state.clone(),
1553 self.settings.clone(),
1554 original_program.ast,
1555 result.0,
1556 ))
1557 .await;
1558
1559 let outcome = exec_state
1560 .into_exec_outcome(result.0, self)
1561 .await
1562 .map_err(KclErrorWithOutputs::no_outputs)?;
1563 Ok(outcome)
1564 }
1565
1566 pub async fn run(
1570 &self,
1571 program: &crate::Program,
1572 exec_state: &mut ExecState,
1573 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1574 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1575 .await
1576 }
1577
1578 pub async fn run_concurrent(
1583 &self,
1584 program: &crate::Program,
1585 exec_state: &mut ExecState,
1586 universe_info: Option<(Universe, UniverseMap)>,
1587 preserve_mem: PreserveMem,
1588 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1589 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1592 (universe, universe_map)
1593 } else {
1594 self.get_universe(program, exec_state).await?
1595 };
1596
1597 let mut sorted_imports: Vec<_> = universe_map.iter().collect();
1603 sorted_imports.sort_by_key(|(_, import_stmt)| SourceRange::from(*import_stmt));
1604 for (_path, import_stmt) in sorted_imports {
1605 let filename = match &import_stmt.path {
1609 ImportPath::Kcl { filename } => filename.to_string(),
1610 ImportPath::Foreign { path } => path.to_string(),
1611 ImportPath::Std { .. } => continue,
1612 };
1613 if let Some((_, module_id, module_path, _)) = universe.get(&filename)
1614 && let ModulePath::Local { value, .. } = module_path
1615 {
1616 let name = import_stmt
1617 .module_name()
1618 .unwrap_or_else(|| value.file_name().unwrap_or_default());
1619 let source_range = SourceRange::from(import_stmt);
1620 exec_state.push_op(crate::execution::cad_op::Operation::ModuleInstance {
1621 name,
1622 module_id: *module_id,
1623 glob: matches!(
1624 import_stmt.selector,
1625 crate::parsing::ast::types::ImportSelector::Glob(_)
1626 ),
1627 node_path: crate::NodePath::placeholder(),
1628 source_range,
1629 });
1630 }
1631 }
1632
1633 let default_planes = self.engine.get_default_planes().read().await.clone();
1634
1635 self.eval_prelude(exec_state, SourceRange::synthetic())
1637 .await
1638 .map_err(KclErrorWithOutputs::no_outputs)?;
1639
1640 for modules in import_graph::import_graph(&universe, self)
1641 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1642 .into_iter()
1643 {
1644 #[cfg(not(target_arch = "wasm32"))]
1645 let mut set = tokio::task::JoinSet::new();
1646
1647 #[allow(clippy::type_complexity)]
1648 let (results_tx, mut results_rx): (
1649 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1650 tokio::sync::mpsc::Receiver<_>,
1651 ) = tokio::sync::mpsc::channel(1);
1652
1653 for module in modules {
1654 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1655 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1656 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1657 )));
1658 };
1659 let module_id = *module_id;
1660 let module_path = module_path.clone();
1661 let source_range = SourceRange::from(import_stmt);
1662 let module_exec_state = exec_state.clone();
1664
1665 let repr = repr.clone();
1666 let exec_ctxt = self.clone_with_fresh_execution_batch();
1667 let results_tx = results_tx.clone();
1668
1669 let exec_module = async |exec_ctxt: &ExecutorContext,
1670 repr: &ModuleRepr,
1671 module_id: ModuleId,
1672 module_path: &ModulePath,
1673 exec_state: &mut ExecState,
1674 source_range: SourceRange|
1675 -> Result<ModuleRepr, KclError> {
1676 match repr {
1677 ModuleRepr::Kcl(program, _) => {
1678 let result = exec_ctxt
1679 .exec_module_from_ast(
1680 program,
1681 module_id,
1682 module_path,
1683 exec_state,
1684 source_range,
1685 PreserveMem::Normal,
1686 )
1687 .await;
1688
1689 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1690 }
1691 ModuleRepr::Foreign(geom, _) => {
1692 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1693 .await
1694 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1695
1696 result.map(|val| ModuleRepr::Foreign(geom.clone(), Some((val, Default::default()))))
1701 }
1702 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1703 format!("Module {module_path} not found in universe"),
1704 vec![source_range],
1705 ))),
1706 }
1707 };
1708
1709 #[cfg(target_arch = "wasm32")]
1710 {
1711 wasm_bindgen_futures::spawn_local(async move {
1712 let mut exec_state = module_exec_state;
1713 let exec_ctxt = exec_ctxt;
1714
1715 let result = exec_module(
1716 &exec_ctxt,
1717 &repr,
1718 module_id,
1719 &module_path,
1720 &mut exec_state,
1721 source_range,
1722 )
1723 .await;
1724
1725 results_tx
1726 .send((module_id, module_path, result))
1727 .await
1728 .unwrap_or_default();
1729 });
1730 }
1731 #[cfg(not(target_arch = "wasm32"))]
1732 {
1733 set.spawn(async move {
1734 let mut exec_state = module_exec_state;
1735 let exec_ctxt = exec_ctxt;
1736
1737 let result = exec_module(
1738 &exec_ctxt,
1739 &repr,
1740 module_id,
1741 &module_path,
1742 &mut exec_state,
1743 source_range,
1744 )
1745 .await;
1746
1747 results_tx
1748 .send((module_id, module_path, result))
1749 .await
1750 .unwrap_or_default();
1751 });
1752 }
1753 }
1754
1755 drop(results_tx);
1756
1757 while let Some((module_id, _, result)) = results_rx.recv().await {
1758 match result {
1759 Ok(new_repr) => {
1760 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1761
1762 match &mut repr {
1763 ModuleRepr::Kcl(_, cache) => {
1764 let ModuleRepr::Kcl(_, session_data) = new_repr else {
1765 unreachable!();
1766 };
1767 *cache = session_data;
1768 }
1769 ModuleRepr::Foreign(_, cache) => {
1770 let ModuleRepr::Foreign(_, session_data) = new_repr else {
1771 unreachable!();
1772 };
1773 *cache = session_data;
1774 }
1775 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1776 }
1777
1778 exec_state.global.module_infos[&module_id].restore_repr(repr);
1779 }
1780 Err(e) => {
1781 return Err(exec_state.error_with_outputs(e, None, default_planes));
1782 }
1783 }
1784 }
1785 }
1786
1787 exec_state.mod_local.artifacts.operations.clear();
1792
1793 exec_state
1796 .global
1797 .root_module_artifacts
1798 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1799
1800 self.inner_run(program, exec_state, preserve_mem).await
1801 }
1802
1803 async fn get_universe(
1806 &self,
1807 program: &crate::Program,
1808 exec_state: &mut ExecState,
1809 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1810 exec_state.add_root_module_contents(program);
1811
1812 let mut universe = std::collections::HashMap::new();
1813
1814 let default_planes = self.engine.get_default_planes().read().await.clone();
1815
1816 let root_imports = import_graph::import_universe(
1817 self,
1818 &ModulePath::Main,
1819 &ModuleRepr::Kcl(program.ast.clone(), None),
1820 &mut universe,
1821 exec_state,
1822 )
1823 .await
1824 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1825
1826 Ok((universe, root_imports))
1827 }
1828
1829 async fn inner_run(
1832 &self,
1833 program: &crate::Program,
1834 exec_state: &mut ExecState,
1835 preserve_mem: PreserveMem,
1836 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1837 let _stats = crate::log::LogPerfStats::new("Interpretation");
1838
1839 let grid_scale = if self.settings.fixed_size_grid {
1841 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1842 } else {
1843 GridScaleBehavior::ScaleWithZoom
1844 };
1845 self.engine
1846 .reapply_settings(
1847 &self.engine_batch,
1848 &self.settings,
1849 Default::default(),
1850 exec_state.id_generator(),
1851 grid_scale,
1852 )
1853 .await
1854 .map_err(KclErrorWithOutputs::no_outputs)?;
1855
1856 let default_planes = self.engine.get_default_planes().read().await.clone();
1857 let result = self
1858 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1859 .await;
1860
1861 crate::log::log(format!(
1862 "Post interpretation KCL memory stats: {:#?}",
1863 exec_state.stack().memory.stats()
1864 ));
1865 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1866
1867 async fn write_old_memory(
1870 ctx: &ExecutorContext,
1871 exec_state: &ExecState,
1872 env_ref: EnvironmentRef,
1873 ) -> Result<(), KclError> {
1874 if ctx.is_mock() {
1875 return Ok(());
1876 }
1877 let mut stack = exec_state.stack().deep_clone()?;
1878 stack.restore_env(env_ref)?;
1879 let state = cache::SketchModeState {
1880 stack,
1881 module_infos: exec_state.global.module_infos.clone(),
1882 path_to_source_id: exec_state.global.path_to_source_id.clone(),
1883 id_to_source: exec_state.global.id_to_source.clone(),
1884 constraint_state: exec_state.mod_local.constraint_state.clone(),
1885 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1886 };
1887 cache::write_old_memory(state).await;
1888 Ok(())
1889 }
1890
1891 let env_ref = match result {
1892 Ok(env_ref) => env_ref,
1893 Err((err, env_ref)) => {
1894 if let Some(env_ref) = env_ref {
1897 write_old_memory(self, exec_state, env_ref)
1898 .await
1899 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
1900 }
1901 return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
1902 }
1903 };
1904
1905 write_old_memory(self, exec_state, env_ref)
1906 .await
1907 .map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
1908
1909 let session_data = self.engine.get_session_data().await;
1910
1911 Ok((env_ref, session_data))
1912 }
1913
1914 async fn execute_and_build_graph(
1917 &self,
1918 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1919 exec_state: &mut ExecState,
1920 preserve_mem: PreserveMem,
1921 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1922 let start_op = exec_state.global.root_module_artifacts.operations.len();
1928
1929 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1930 .await
1931 .map_err(|e| (e, None))?;
1932
1933 let exec_result = self
1934 .exec_module_body(
1935 program,
1936 exec_state,
1937 preserve_mem,
1938 ModuleId::default(),
1939 &ModulePath::Main,
1940 )
1941 .await
1942 .map(
1943 |ModuleExecutionOutcome {
1944 environment: env_ref,
1945 artifacts: module_artifacts,
1946 ..
1947 }| {
1948 exec_state.global.root_module_artifacts.extend(module_artifacts);
1951 env_ref
1952 },
1953 )
1954 .map_err(|(err, env_ref, module_artifacts)| {
1955 if let Some(module_artifacts) = module_artifacts {
1956 exec_state.global.root_module_artifacts.extend(module_artifacts);
1959 }
1960 (err, env_ref)
1961 });
1962
1963 let programs = &exec_state.build_program_lookup(program.clone());
1965 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1966 for op in exec_state
1967 .global
1968 .root_module_artifacts
1969 .operations
1970 .iter_mut()
1971 .skip(start_op)
1972 {
1973 op.fill_node_paths(programs, cached_body_items);
1974 }
1975 for module in exec_state.global.module_infos.values_mut() {
1976 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1977 for op in &mut outcome.artifacts.operations {
1978 op.fill_node_paths(programs, cached_body_items);
1979 }
1980 }
1981 }
1982
1983 self.engine
1985 .ensure_async_commands_completed(&self.engine_batch)
1986 .await
1987 .map_err(|e| {
1988 match &exec_result {
1989 Ok(env_ref) => (e, Some(*env_ref)),
1990 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1992 }
1993 })?;
1994
1995 self.engine.clear_queues(&self.engine_batch).await;
1998
1999 match exec_state.build_artifact_graph(&self.engine, program).await {
2000 Ok(_) => exec_result,
2001 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
2002 }
2003 }
2004
2005 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
2009 if exec_state.stack().memory.requires_std() {
2010 let initial_ops = exec_state.mod_local.artifacts.operations.len();
2011
2012 let path = vec!["std".to_owned(), "prelude".to_owned()];
2013 let resolved_path = ModulePath::from_std_import_path(&path)?;
2014 let id = self
2015 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
2016 .await?;
2017 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
2018
2019 exec_state.mut_stack().memory.set_std(module_memory)?;
2020
2021 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
2027 }
2028
2029 Ok(())
2030 }
2031
2032 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
2034 self.engine
2036 .send_modeling_cmd(
2037 &self.engine_batch,
2038 uuid::Uuid::new_v4(),
2039 crate::execution::SourceRange::default(),
2040 &ModelingCmd::from(
2041 mcmd::ZoomToFit::builder()
2042 .object_ids(Default::default())
2043 .animated(false)
2044 .padding(0.1)
2045 .build(),
2046 ),
2047 )
2048 .await
2049 .map_err(KclErrorWithOutputs::no_outputs)?;
2050
2051 let resp = self
2053 .engine
2054 .send_modeling_cmd(
2055 &self.engine_batch,
2056 uuid::Uuid::new_v4(),
2057 crate::execution::SourceRange::default(),
2058 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
2059 )
2060 .await
2061 .map_err(KclErrorWithOutputs::no_outputs)?;
2062
2063 let OkWebSocketResponseData::Modeling {
2064 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
2065 } = resp
2066 else {
2067 return Err(ExecError::BadPng(format!(
2068 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
2069 )));
2070 };
2071 Ok(contents)
2072 }
2073
2074 pub async fn export(
2076 &self,
2077 format: kittycad_modeling_cmds::format::OutputFormat3d,
2078 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2079 let resp = self
2080 .engine
2081 .send_modeling_cmd(
2082 &self.engine_batch,
2083 uuid::Uuid::new_v4(),
2084 crate::SourceRange::default(),
2085 &kittycad_modeling_cmds::ModelingCmd::Export(
2086 kittycad_modeling_cmds::Export::builder()
2087 .entity_ids(vec![])
2088 .format(format)
2089 .build(),
2090 ),
2091 )
2092 .await?;
2093
2094 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
2095 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
2096 format!("Expected Export response, got {resp:?}",),
2097 vec![SourceRange::default()],
2098 )));
2099 };
2100
2101 Ok(files)
2102 }
2103
2104 pub async fn export_step(
2106 &self,
2107 deterministic_time: bool,
2108 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
2109 let files = self
2110 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
2111 kittycad_modeling_cmds::format::step::export::Options::builder()
2112 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
2113 .maybe_created(if deterministic_time {
2114 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
2115 KclError::new_internal(crate::errors::KclErrorDetails::new(
2116 format!("Failed to parse date: {e}"),
2117 vec![SourceRange::default()],
2118 ))
2119 })?)
2120 } else {
2121 None
2122 })
2123 .build(),
2124 ))
2125 .await?;
2126
2127 Ok(files)
2128 }
2129
2130 pub async fn close(&self) {
2131 self.engine.close().await;
2132 }
2133}
2134
2135pub use kcl_api::ArtifactId;
2136
2137pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
2138 ArtifactId::new(*id.as_ref())
2139}
2140
2141#[cfg(test)]
2142pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2143 parse_execute_with_project_dir(code, None).await
2144}
2145
2146#[cfg(test)]
2147pub(crate) async fn parse_execute_with_project_dir(
2148 code: &str,
2149 project_directory: Option<TypedPath>,
2150) -> Result<ExecTestResults, KclError> {
2151 let program = crate::Program::parse_no_errs(code)?;
2152
2153 let exec_ctxt = ExecutorContext {
2154 engine: Arc::new(EngineManager::new_mock()),
2155 engine_batch: EngineBatchContext::default(),
2156 fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2157 settings: ExecutorSettings {
2158 project_directory,
2159 ..Default::default()
2160 },
2161 context_type: ContextType::Mock,
2162 execution_callbacks: Default::default(),
2163 };
2164 let mut exec_state = ExecState::new(&exec_ctxt);
2165 let result = exec_ctxt.run(&program, &mut exec_state).await?;
2166
2167 Ok(ExecTestResults {
2168 program,
2169 mem_env: result.0,
2170 exec_ctxt,
2171 exec_state,
2172 })
2173}
2174
2175#[cfg(test)]
2176#[derive(Debug)]
2177pub(crate) struct ExecTestResults {
2178 program: crate::Program,
2179 mem_env: EnvironmentRef,
2180 exec_ctxt: ExecutorContext,
2181 exec_state: ExecState,
2182}
2183
2184#[cfg(test)]
2185impl ExecTestResults {
2186 pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2187 &self.exec_state.global.root_module_artifacts.commands
2188 }
2189}
2190
2191pub struct ProgramLookup {
2195 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2196}
2197
2198impl ProgramLookup {
2199 pub fn new(
2202 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2203 module_infos: state::ModuleInfoMap,
2204 ) -> Self {
2205 let mut programs = IndexMap::with_capacity(module_infos.len());
2206 for (id, info) in module_infos {
2207 if let ModuleRepr::Kcl(program, _) = info.repr {
2208 programs.insert(id, program);
2209 }
2210 }
2211 programs.insert(ModuleId::default(), current);
2212 Self { programs }
2213 }
2214
2215 pub fn program_for_module(
2216 &self,
2217 module_id: ModuleId,
2218 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2219 self.programs.get(&module_id)
2220 }
2221}
2222
2223#[cfg(test)]
2224mod tests {
2225 use kcl_api::NumericType;
2226 use pretty_assertions::assert_eq;
2227
2228 use super::*;
2229 use crate::ModuleId;
2230 use crate::errors::KclErrorDetails;
2231 use crate::errors::Severity;
2232 use crate::execution::memory::Stack;
2233 use crate::execution::types::RuntimeType;
2234
2235 macro_rules! kcl_input {
2236 ($file:literal) => {
2237 include_str!(concat!("../../e2e/executor/inputs/", $file, ".kcl"))
2238 };
2239 }
2240
2241 #[track_caller]
2243 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2244 memory.memory.get_from_unchecked(name, env).unwrap()
2245 }
2246
2247 async fn execute_variables_with_backend(
2248 code: &str,
2249 backend: memory::MemoryBackendKind,
2250 ) -> IndexMap<String, KclValueView> {
2251 execute_outcome_with_backend(code, backend).await.variables
2252 }
2253
2254 async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
2255 let program = crate::Program::parse_no_errs(code).unwrap();
2256 let ctx = ExecutorContext::new_mock(None).await;
2257 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2258 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2259 let outcome = exec_state
2260 .into_exec_outcome(env_ref, &ctx)
2261 .await
2262 .expect("test execution outcome should collect variables");
2263 ctx.close().await;
2264 outcome
2265 }
2266
2267 async fn execute_error_variables_with_backend(
2268 code: &str,
2269 backend: memory::MemoryBackendKind,
2270 ) -> IndexMap<String, KclValueView> {
2271 let program = crate::Program::parse_no_errs(code).unwrap();
2272 let ctx = ExecutorContext::new_mock(None).await;
2273 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2274 let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
2275 ctx.close().await;
2276 error.variables
2277 }
2278
2279 async fn execute_project_variables_with_backend(
2280 main_code: &str,
2281 files: &[(&str, &str)],
2282 backend: memory::MemoryBackendKind,
2283 ) -> IndexMap<String, KclValueView> {
2284 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
2285 for (name, contents) in files {
2286 tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
2287 }
2288
2289 let program = crate::Program::parse_no_errs(main_code).unwrap();
2290 let ctx = ExecutorContext {
2291 engine: Arc::new(EngineManager::new_mock()),
2292 engine_batch: EngineBatchContext::default(),
2293 fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2294 settings: ExecutorSettings {
2295 project_directory: Some(crate::TypedPath(tmpdir.path().into())),
2296 ..Default::default()
2297 },
2298 context_type: ContextType::Mock,
2299 execution_callbacks: Default::default(),
2300 };
2301 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2302 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2303 let outcome = exec_state
2304 .into_exec_outcome(env_ref, &ctx)
2305 .await
2306 .expect("test execution outcome should collect variables");
2307 ctx.close().await;
2308 outcome.variables
2309 }
2310
2311 async fn run_with_caching_variables_with_backend(
2312 code: &str,
2313 backend: memory::MemoryBackendKind,
2314 ) -> IndexMap<String, KclValueView> {
2315 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2316 cache::bust_cache().await;
2317 clear_mem_cache().await;
2318
2319 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
2320 let program = crate::Program::parse_no_errs(code).unwrap();
2321 ctx.run_with_caching(program.clone()).await.unwrap();
2322 let cached = ctx.run_with_caching(program).await.unwrap();
2323
2324 cache::bust_cache().await;
2325 clear_mem_cache().await;
2326 ctx.close().await;
2327 cached.variables
2328 }
2329
2330 async fn run_mock_variables_with_backend(
2331 code: &str,
2332 backend: memory::MemoryBackendKind,
2333 ) -> IndexMap<String, KclValueView> {
2334 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2335 clear_mem_cache().await;
2336
2337 let ctx = ExecutorContext::new_mock(None).await;
2338 let first = crate::Program::parse_no_errs("x = 2").unwrap();
2339 ctx.run_mock(
2340 &first,
2341 &MockConfig {
2342 use_prev_memory: false,
2343 ..Default::default()
2344 },
2345 )
2346 .await
2347 .unwrap();
2348
2349 let program = crate::Program::parse_no_errs(code).unwrap();
2350 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
2351
2352 clear_mem_cache().await;
2353 ctx.close().await;
2354 outcome.variables
2355 }
2356
2357 fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
2358 let mut keys = variables.keys().cloned().collect::<Vec<_>>();
2359 keys.sort();
2360 keys
2361 }
2362
2363 async fn collect_backend_results<T, Fut>(
2364 mut run: impl FnMut(memory::MemoryBackendKind) -> Fut,
2365 ) -> Vec<(memory::MemoryBackendKind, T)>
2366 where
2367 Fut: std::future::Future<Output = T>,
2368 {
2369 let all = memory::MemoryBackendKind::all();
2370 let mut results = Vec::with_capacity(all.len());
2371 for &kind in all {
2372 results.push((kind, run(kind).await));
2373 }
2374 results
2375 }
2376
2377 fn assert_backend_results_match<T>(results: &[(memory::MemoryBackendKind, T)])
2378 where
2379 T: std::fmt::Debug + PartialEq,
2380 {
2381 let (first, rest) = results.split_first().expect("expected at least one memory backend");
2382 let (first_kind, first_result) = first;
2383 for (kind, result) in rest {
2384 assert_eq!(
2385 result, first_result,
2386 "memory kind {kind:?} doesn't match {first_kind:?}"
2387 );
2388 }
2389 }
2390
2391 fn assert_backend_variable_results_match_expected_keys(
2392 results: &[(memory::MemoryBackendKind, IndexMap<String, KclValueView>)],
2393 expected_keys: &[&str],
2394 ) {
2395 let (first_kind, first_variables) = results.first().expect("expected at least one memory backend");
2396 let expected_keys = expected_keys.iter().map(|key| (*key).to_owned()).collect::<Vec<_>>();
2397 assert_eq!(
2398 sorted_variable_keys(first_variables),
2399 expected_keys,
2400 "memory kind {first_kind:?} doesn't match expected variables"
2401 );
2402 assert_backend_results_match(results);
2403 }
2404
2405 fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
2406 let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
2407 let KclValueView::Number { value, .. } = value else {
2408 panic!("expected `{key}` to be a number, got {value:?}");
2409 };
2410 assert_eq!(*value, expected, "{key}: {value:?}");
2411 }
2412
2413 #[tokio::test(flavor = "multi_thread")]
2414 async fn exec_outcome_variables_match_between_memory_backends() {
2415 let code = "x = 2\ny = x + 1\narr = [x, y]";
2416
2417 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2418
2419 assert_backend_variable_results_match_expected_keys(&results, &["arr", "x", "y"]);
2420 }
2421
2422 #[tokio::test(flavor = "multi_thread")]
2423 async fn error_output_variables_match_between_memory_backends() {
2424 let code = "x = 2\ny = missing + 1";
2425
2426 let results = collect_backend_results(|kind| execute_error_variables_with_backend(code, kind)).await;
2427
2428 assert_backend_variable_results_match_expected_keys(&results, &["x"]);
2429 }
2430
2431 #[tokio::test(flavor = "multi_thread")]
2432 async fn cached_execution_variables_match_between_memory_backends() {
2433 let code = "x = 2\ny = x + 1";
2434
2435 let results = collect_backend_results(|kind| run_with_caching_variables_with_backend(code, kind)).await;
2436
2437 assert_backend_variable_results_match_expected_keys(&results, &["x", "y"]);
2438 }
2439
2440 #[tokio::test(flavor = "multi_thread")]
2441 async fn mock_execution_variables_match_between_memory_backends() {
2442 let code = "y = x + 1";
2443
2444 let results = collect_backend_results(|kind| run_mock_variables_with_backend(code, kind)).await;
2445
2446 assert_backend_variable_results_match_expected_keys(&results, &["y"]);
2447 }
2448
2449 #[tokio::test(flavor = "multi_thread")]
2450 async fn module_imports_and_exported_closures_match_between_memory_backends() {
2451 let module_code = r#"
2452export base = 40
2453
2454export fn addBase(n) {
2455 return n + base
2456}
2457"#;
2458 let main_code = r#"
2459import base, addBase from 'math.kcl'
2460import 'math.kcl'
2461
2462named = addBase(n = 2)
2463qualified = math::addBase(n = 1)
2464direct = math::base
2465"#;
2466
2467 let files = [("math.kcl", module_code)];
2468 let results =
2469 collect_backend_results(|kind| execute_project_variables_with_backend(main_code, &files, kind)).await;
2470
2471 let (_, first_variables) = results.first().expect("expected at least one memory backend");
2472 assert_number_variable(first_variables, "named", 42.0);
2473 assert_number_variable(first_variables, "qualified", 41.0);
2474 assert_number_variable(first_variables, "direct", 40.0);
2475 assert_backend_results_match(&results);
2476 }
2477
2478 #[tokio::test(flavor = "multi_thread")]
2479 async fn sketch_block_variables_match_between_memory_backends() {
2480 let code = r#"
2481sketch001 = sketch(on = XY) {
2482 line1 = line(start = [0, 0], end = [1, 0])
2483 line2 = line(start = [1, 0], end = [0, 1])
2484}
2485lineCount = 2
2486"#;
2487
2488 let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
2489
2490 let (_, first_variables) = results.first().expect("expected at least one memory backend");
2491 assert!(first_variables.contains_key("sketch001"), "actual: {first_variables:?}");
2492 assert_number_variable(first_variables, "lineCount", 2.0);
2493 assert_backend_results_match(&results);
2494 }
2495
2496 #[tokio::test(flavor = "multi_thread")]
2497 async fn tag_call_stack_lookup_matches_between_memory_backends() {
2498 let code = r#"
2499sketch001 = startSketchOn(XY)
2500 |> startProfile(at = [0, 0])
2501 |> xLine(length = 10, tag = $seg01)
2502
2503segLength = segLen(seg01)
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_number_variable(first_variables, "segLength", 10.0);
2510 assert_backend_results_match(&results);
2511 }
2512
2513 #[tokio::test(flavor = "multi_thread")]
2514 async fn sketch_transpiler_exec_outcome_variables_match_between_memory_backends() {
2515 let code = r#"
2516sketch001 = startSketchOn(XY)
2517 |> startProfile(at = [0, 0])
2518 |> line(end = [1, 0])
2519"#;
2520 let program = crate::Program::parse_no_errs(code).unwrap();
2521
2522 let outcomes = collect_backend_results(|kind| execute_outcome_with_backend(code, kind)).await;
2523 let mut transpiled = Vec::with_capacity(outcomes.len());
2524 for (kind, outcome) in &outcomes {
2525 let sketch = transpile_old_sketch_to_new(outcome, &program, "sketch001").unwrap();
2526 transpiled.push((*kind, sketch));
2527 }
2528
2529 assert_backend_results_match(&transpiled);
2530 }
2531
2532 #[tokio::test(flavor = "multi_thread")]
2533 async fn test_execute_warn() {
2534 let text = "@blah";
2535 let result = parse_execute(text).await.unwrap();
2536 let errs = result.exec_state.issues();
2537 assert_eq!(errs.len(), 1);
2538 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2539 assert!(
2540 errs[0].message.contains("Unknown annotation"),
2541 "unexpected warning message: {}",
2542 errs[0].message
2543 );
2544 }
2545
2546 #[tokio::test(flavor = "multi_thread")]
2547 async fn test_execute_fn_definitions() {
2548 let ast = r#"fn def(@x) {
2549 return x
2550}
2551fn ghi(@x) {
2552 return x
2553}
2554fn jkl(@x) {
2555 return x
2556}
2557fn hmm(@x) {
2558 return x
2559}
2560
2561yo = 5 + 6
2562
2563abc = 3
2564identifierGuy = 5
2565part001 = startSketchOn(XY)
2566|> startProfile(at = [-1.2, 4.83])
2567|> line(end = [2.8, 0])
2568|> angledLine(angle = 100 + 100, length = 3.01)
2569|> angledLine(angle = abc, length = 3.02)
2570|> angledLine(angle = def(yo), length = 3.03)
2571|> angledLine(angle = ghi(2), length = 3.04)
2572|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2573|> close()
2574yo2 = hmm([identifierGuy + 5])"#;
2575
2576 parse_execute(ast).await.unwrap();
2577 }
2578
2579 #[tokio::test(flavor = "multi_thread")]
2580 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2581 let code = r#"
2582firstProfile = sketch(on = XY) {
2583 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2584 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2585 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2586 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2587 coincident([edge1.end, edge2.start])
2588 coincident([edge2.end, edge3.start])
2589 coincident([edge3.end, edge4.start])
2590 coincident([edge4.end, edge1.start])
2591}
2592
2593secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2594 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2595 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2596 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2597 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2598 coincident([edge5.end, edge6.start])
2599 coincident([edge6.end, edge7.start])
2600 coincident([edge7.end, edge8.start])
2601 coincident([edge8.end, edge5.start])
2602}
2603
2604firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2605secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2606"#;
2607
2608 let result = parse_execute(code).await.unwrap();
2609 assert!(result.exec_state.issues().is_empty());
2610 }
2611
2612 #[tokio::test(flavor = "multi_thread")]
2613 async fn sketch_block_artifact_preserves_standard_plane_name() {
2614 let code = r#"
2615sketch001 = sketch(on = -YZ) {
2616 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
2617}
2618"#;
2619
2620 let result = parse_execute(code).await.unwrap();
2621 let sketch_blocks = result
2622 .exec_state
2623 .global
2624 .artifacts
2625 .graph
2626 .values()
2627 .filter_map(|artifact| match artifact {
2628 Artifact::SketchBlock(block) => Some(block),
2629 _ => None,
2630 })
2631 .collect::<Vec<_>>();
2632
2633 assert_eq!(sketch_blocks.len(), 1);
2634 assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
2635 }
2636
2637 #[tokio::test(flavor = "multi_thread")]
2638 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2639 let code = r#"
2640sketch001 = sketch(on = YZ) {
2641 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2642 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2643 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2644 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2645 coincident([line1.end, line2.start])
2646 coincident([line2.end, line3.start])
2647 coincident([line3.end, line4.start])
2648 coincident([line4.end, line1.start])
2649}
2650
2651sketch002 = sketch(on = -XZ) {
2652 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2653 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2654 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2655 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2656 coincident([line5.end, line6.start])
2657 coincident([line6.end, line7.start])
2658 coincident([line7.end, line8.start])
2659 coincident([line8.end, line5.start])
2660}
2661
2662region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2663extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2664region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2665extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2666
2667myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2668"#;
2669
2670 let result = parse_execute(code).await.unwrap();
2671 assert!(result.exec_state.issues().is_empty());
2672 }
2673
2674 #[tokio::test(flavor = "multi_thread")]
2675 async fn issue_10741_point_circle_coincident_executes() {
2676 let code = r#"
2677sketch001 = sketch(on = YZ) {
2678 circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
2679 line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
2680 coincident([line1.start, circle1])
2681}
2682"#;
2683
2684 let result = parse_execute(code).await.unwrap();
2685 assert!(
2686 result
2687 .exec_state
2688 .issues()
2689 .iter()
2690 .all(|issue| issue.severity != Severity::Error),
2691 "unexpected execution issues: {:#?}",
2692 result.exec_state.issues()
2693 );
2694 }
2695
2696 #[tokio::test(flavor = "multi_thread")]
2697 async fn test_execute_with_pipe_substitutions_unary() {
2698 let ast = r#"myVar = 3
2699part001 = startSketchOn(XY)
2700 |> startProfile(at = [0, 0])
2701 |> line(end = [3, 4], tag = $seg01)
2702 |> line(end = [
2703 min([segLen(seg01), myVar]),
2704 -legLen(hypotenuse = segLen(seg01), leg = myVar)
2705])
2706"#;
2707
2708 parse_execute(ast).await.unwrap();
2709 }
2710
2711 #[tokio::test(flavor = "multi_thread")]
2712 async fn test_execute_with_pipe_substitutions() {
2713 let ast = r#"myVar = 3
2714part001 = startSketchOn(XY)
2715 |> startProfile(at = [0, 0])
2716 |> line(end = [3, 4], tag = $seg01)
2717 |> line(end = [
2718 min([segLen(seg01), myVar]),
2719 legLen(hypotenuse = segLen(seg01), leg = myVar)
2720])
2721"#;
2722
2723 parse_execute(ast).await.unwrap();
2724 }
2725
2726 #[tokio::test(flavor = "multi_thread")]
2727 async fn test_execute_with_inline_comment() {
2728 let ast = r#"baseThick = 1
2729armAngle = 60
2730
2731baseThickHalf = baseThick / 2
2732halfArmAngle = armAngle / 2
2733
2734arrExpShouldNotBeIncluded = [1, 2, 3]
2735objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2736
2737part001 = startSketchOn(XY)
2738 |> startProfile(at = [0, 0])
2739 |> yLine(endAbsolute = 1)
2740 |> xLine(length = 3.84) // selection-range-7ish-before-this
2741
2742variableBelowShouldNotBeIncluded = 3
2743"#;
2744
2745 parse_execute(ast).await.unwrap();
2746 }
2747
2748 #[tokio::test(flavor = "multi_thread")]
2749 async fn test_execute_with_function_literal_in_pipe() {
2750 let ast = r#"w = 20
2751l = 8
2752h = 10
2753
2754fn thing() {
2755 return -8
2756}
2757
2758firstExtrude = startSketchOn(XY)
2759 |> startProfile(at = [0,0])
2760 |> line(end = [0, l])
2761 |> line(end = [w, 0])
2762 |> line(end = [0, thing()])
2763 |> close()
2764 |> extrude(length = h)"#;
2765
2766 parse_execute(ast).await.unwrap();
2767 }
2768
2769 #[tokio::test(flavor = "multi_thread")]
2770 async fn test_execute_with_function_unary_in_pipe() {
2771 let ast = r#"w = 20
2772l = 8
2773h = 10
2774
2775fn thing(@x) {
2776 return -x
2777}
2778
2779firstExtrude = startSketchOn(XY)
2780 |> startProfile(at = [0,0])
2781 |> line(end = [0, l])
2782 |> line(end = [w, 0])
2783 |> line(end = [0, thing(8)])
2784 |> close()
2785 |> extrude(length = h)"#;
2786
2787 parse_execute(ast).await.unwrap();
2788 }
2789
2790 #[tokio::test(flavor = "multi_thread")]
2791 async fn test_execute_with_function_array_in_pipe() {
2792 let ast = r#"w = 20
2793l = 8
2794h = 10
2795
2796fn thing(@x) {
2797 return [0, -x]
2798}
2799
2800firstExtrude = startSketchOn(XY)
2801 |> startProfile(at = [0,0])
2802 |> line(end = [0, l])
2803 |> line(end = [w, 0])
2804 |> line(end = thing(8))
2805 |> close()
2806 |> extrude(length = h)"#;
2807
2808 parse_execute(ast).await.unwrap();
2809 }
2810
2811 #[tokio::test(flavor = "multi_thread")]
2812 async fn test_execute_with_function_call_in_pipe() {
2813 let ast = r#"w = 20
2814l = 8
2815h = 10
2816
2817fn other_thing(@y) {
2818 return -y
2819}
2820
2821fn thing(@x) {
2822 return other_thing(x)
2823}
2824
2825firstExtrude = startSketchOn(XY)
2826 |> startProfile(at = [0,0])
2827 |> line(end = [0, l])
2828 |> line(end = [w, 0])
2829 |> line(end = [0, thing(8)])
2830 |> close()
2831 |> extrude(length = h)"#;
2832
2833 parse_execute(ast).await.unwrap();
2834 }
2835
2836 #[tokio::test(flavor = "multi_thread")]
2837 async fn test_execute_with_function_sketch() {
2838 let ast = r#"fn box(h, l, w) {
2839 myBox = startSketchOn(XY)
2840 |> startProfile(at = [0,0])
2841 |> line(end = [0, l])
2842 |> line(end = [w, 0])
2843 |> line(end = [0, -l])
2844 |> close()
2845 |> extrude(length = h)
2846
2847 return myBox
2848}
2849
2850fnBox = box(h = 3, l = 6, w = 10)"#;
2851
2852 parse_execute(ast).await.unwrap();
2853 }
2854
2855 #[tokio::test(flavor = "multi_thread")]
2856 async fn test_get_member_of_object_with_function_period() {
2857 let ast = r#"fn box(@obj) {
2858 myBox = startSketchOn(XY)
2859 |> startProfile(at = obj.start)
2860 |> line(end = [0, obj.l])
2861 |> line(end = [obj.w, 0])
2862 |> line(end = [0, -obj.l])
2863 |> close()
2864 |> extrude(length = obj.h)
2865
2866 return myBox
2867}
2868
2869thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2870"#;
2871 parse_execute(ast).await.unwrap();
2872 }
2873
2874 #[tokio::test(flavor = "multi_thread")]
2875 #[ignore] async fn test_object_member_starting_pipeline() {
2877 let ast = r#"
2878fn test2() {
2879 return {
2880 thing: startSketchOn(XY)
2881 |> startProfile(at = [0, 0])
2882 |> line(end = [0, 1])
2883 |> line(end = [1, 0])
2884 |> line(end = [0, -1])
2885 |> close()
2886 }
2887}
2888
2889x2 = test2()
2890
2891x2.thing
2892 |> extrude(length = 10)
2893"#;
2894 parse_execute(ast).await.unwrap();
2895 }
2896
2897 #[tokio::test(flavor = "multi_thread")]
2898 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
2900 let ast = r#"fn box(obj) {
2901let myBox = startSketchOn(XY)
2902 |> startProfile(at = obj.start)
2903 |> line(end = [0, obj.l])
2904 |> line(end = [obj.w, 0])
2905 |> line(end = [0, -obj.l])
2906 |> close()
2907 |> extrude(length = obj.h)
2908
2909 return myBox
2910}
2911
2912for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2913 thisBox = box(var)
2914}"#;
2915
2916 parse_execute(ast).await.unwrap();
2917 }
2918
2919 #[tokio::test(flavor = "multi_thread")]
2920 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
2922 let ast = r#"fn box(h, l, w, start) {
2923 myBox = startSketchOn(XY)
2924 |> startProfile(at = [0,0])
2925 |> line(end = [0, l])
2926 |> line(end = [w, 0])
2927 |> line(end = [0, -l])
2928 |> close()
2929 |> extrude(length = h)
2930
2931 return myBox
2932}
2933
2934
2935for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2936 const thisBox = box(var[0], var[1], var[2], var[3])
2937}"#;
2938
2939 parse_execute(ast).await.unwrap();
2940 }
2941
2942 #[tokio::test(flavor = "multi_thread")]
2943 async fn test_get_member_of_array_with_function() {
2944 let ast = r#"fn box(@arr) {
2945 myBox =startSketchOn(XY)
2946 |> startProfile(at = arr[0])
2947 |> line(end = [0, arr[1]])
2948 |> line(end = [arr[2], 0])
2949 |> line(end = [0, -arr[1]])
2950 |> close()
2951 |> extrude(length = arr[3])
2952
2953 return myBox
2954}
2955
2956thisBox = box([[0,0], 6, 10, 3])
2957
2958"#;
2959 parse_execute(ast).await.unwrap();
2960 }
2961
2962 #[tokio::test(flavor = "multi_thread")]
2963 async fn test_function_cannot_access_future_definitions() {
2964 let ast = r#"
2965fn returnX() {
2966 // x shouldn't be defined yet.
2967 return x
2968}
2969
2970x = 5
2971
2972answer = returnX()"#;
2973
2974 let result = parse_execute(ast).await;
2975 let err = result.unwrap_err();
2976 assert_eq!(err.message(), "`x` is not defined");
2977 }
2978
2979 #[tokio::test(flavor = "multi_thread")]
2980 async fn test_override_prelude() {
2981 let text = "PI = 3.0";
2982 let result = parse_execute(text).await.unwrap();
2983 let issues = result.exec_state.issues();
2984 assert!(issues.is_empty(), "issues={issues:#?}");
2985 }
2986
2987 #[tokio::test(flavor = "multi_thread")]
2988 async fn type_aliases() {
2989 let text = r#"@settings(experimentalFeatures = allow)
2990type MyTy = [number; 2]
2991fn foo(@x: MyTy) {
2992 return x[0]
2993}
2994
2995foo([0, 1])
2996
2997type Other = MyTy | Helix
2998"#;
2999 let result = parse_execute(text).await.unwrap();
3000 let issues = result.exec_state.issues();
3001 assert!(issues.is_empty(), "issues={issues:#?}");
3002 }
3003
3004 #[tokio::test(flavor = "multi_thread")]
3005 async fn test_cannot_shebang_in_fn() {
3006 let ast = r#"
3007fn foo() {
3008 #!hello
3009 return true
3010}
3011
3012foo
3013"#;
3014
3015 let result = parse_execute(ast).await;
3016 let err = result.unwrap_err();
3017 assert_eq!(
3018 err,
3019 KclError::new_syntax(KclErrorDetails::new(
3020 "Unexpected token: #".to_owned(),
3021 vec![SourceRange::new(14, 15, ModuleId::default())],
3022 )),
3023 );
3024 }
3025
3026 #[tokio::test(flavor = "multi_thread")]
3027 async fn test_pattern_transform_function_cannot_access_future_definitions() {
3028 let ast = r#"
3029fn transform(@replicaId) {
3030 // x shouldn't be defined yet.
3031 scale = x
3032 return {
3033 translate = [0, 0, replicaId * 10],
3034 scale = [scale, 1, 0],
3035 }
3036}
3037
3038fn layer() {
3039 return startSketchOn(XY)
3040 |> circle( center= [0, 0], radius= 1, tag = $tag1)
3041 |> extrude(length = 10)
3042}
3043
3044x = 5
3045
3046// The 10 layers are replicas of each other, with a transform applied to each.
3047shape = layer() |> patternTransform(instances = 10, transform = transform)
3048"#;
3049
3050 let result = parse_execute(ast).await;
3051 let err = result.unwrap_err();
3052 assert_eq!(err.message(), "`x` is not defined",);
3053 }
3054
3055 #[tokio::test(flavor = "multi_thread")]
3058 async fn test_math_execute_with_functions() {
3059 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
3060 let result = parse_execute(ast).await.unwrap();
3061 assert_eq!(
3062 5.0,
3063 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3064 .as_f64()
3065 .unwrap()
3066 );
3067 }
3068
3069 #[tokio::test(flavor = "multi_thread")]
3070 async fn test_math_execute() {
3071 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
3072 let result = parse_execute(ast).await.unwrap();
3073 assert_eq!(
3074 7.4,
3075 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3076 .as_f64()
3077 .unwrap()
3078 );
3079 }
3080
3081 #[tokio::test(flavor = "multi_thread")]
3082 async fn test_string_uppercase() {
3083 let composed = "\u{e9}";
3084 let uppercase_composed = "\u{c9}";
3085 let decomposed = "e\u{301}";
3086 let uppercase_decomposed = "E\u{301}";
3087 let code = format!(
3088 r#"
3089ascii = string::uppercase("Kcl")
3090unicode_expansion = string::uppercase("Straße")
3091uncased = string::uppercase("東京")
3092empty = string::uppercase("")
3093composed = string::uppercase("{composed}")
3094decomposed = string::uppercase("{decomposed}")
3095piped = "ready" |> string::uppercase()
3096"#
3097 );
3098
3099 let result = parse_execute(&code).await.unwrap();
3100 for (name, expected) in [
3101 ("ascii", "KCL"),
3102 ("unicode_expansion", "STRASSE"),
3103 ("uncased", "東京"),
3104 ("empty", ""),
3105 ("composed", uppercase_composed),
3106 ("decomposed", uppercase_decomposed),
3107 ("piped", "READY"),
3108 ] {
3109 assert_eq!(
3110 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3111 .as_str()
3112 .unwrap(),
3113 expected,
3114 "{name}"
3115 );
3116 }
3117 }
3118
3119 #[tokio::test(flavor = "multi_thread")]
3120 async fn test_string_lowercase() {
3121 let composed = "\u{c9}";
3122 let lowercase_composed = "\u{e9}";
3123 let decomposed = "E\u{301}";
3124 let lowercase_decomposed = "e\u{301}";
3125 let expanded = "i\u{307}";
3126 let code = format!(
3127 r#"
3128ascii = string::lowercase("KCL")
3129final_sigma = string::lowercase("ΟΣ")
3130medial_sigma = string::lowercase("ΟΣΑ")
3131unicode_expansion = string::lowercase("İ")
3132uncased = string::lowercase("東京")
3133empty = string::lowercase("")
3134composed = string::lowercase("{composed}")
3135decomposed = string::lowercase("{decomposed}")
3136piped = "READY" |> string::lowercase()
3137"#
3138 );
3139
3140 let result = parse_execute(&code).await.unwrap();
3141 for (name, expected) in [
3142 ("ascii", "kcl"),
3143 ("final_sigma", "ος"),
3144 ("medial_sigma", "οσα"),
3145 ("unicode_expansion", expanded),
3146 ("uncased", "東京"),
3147 ("empty", ""),
3148 ("composed", lowercase_composed),
3149 ("decomposed", lowercase_decomposed),
3150 ("piped", "ready"),
3151 ] {
3152 assert_eq!(
3153 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3154 .as_str()
3155 .unwrap(),
3156 expected,
3157 "{name}"
3158 );
3159 }
3160 }
3161
3162 #[tokio::test(flavor = "multi_thread")]
3163 async fn test_string_is_equal() {
3164 let composed = "\u{e9}";
3165 let decomposed = "e\u{301}";
3166 let code = format!(
3167 r#"
3168exact_same = string::isEqual("KCL", to = "KCL")
3169exact_different_case = string::isEqual("KCL", to = "kcl")
3170explicit_case_sensitive = string::isEqual("KCL", to = "kcl", caseInsensitive = false)
3171case_insensitive_ascii = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3172case_fold_expansion = string::isEqual("Straße", to = "STRASSE", caseInsensitive = true)
3173case_fold_expansion_reversed = string::isEqual("STRASSE", to = "Straße", caseInsensitive = true)
3174case_fold_sigma = string::isEqual("ος", to = "οσ", caseInsensitive = true)
3175case_fold_non_turkic = string::isEqual("I", to = "i", caseInsensitive = true)
3176case_fold_not_turkic = string::isEqual("I", to = "ı", caseInsensitive = true)
3177empty_same = string::isEqual("", to = "")
3178empty_different = string::isEqual("", to = "KCL")
3179exact_without_normalization = string::isEqual("{composed}", to = "{decomposed}")
3180case_fold_without_normalization = string::isEqual("{composed}", to = "{decomposed}", caseInsensitive = true)
3181piped = "ready" |> string::isEqual(to = "READY", caseInsensitive = true)
3182"#
3183 );
3184
3185 let result = parse_execute(&code).await.unwrap();
3186 for (name, expected) in [
3187 ("exact_same", true),
3188 ("exact_different_case", false),
3189 ("explicit_case_sensitive", false),
3190 ("case_insensitive_ascii", true),
3191 ("case_fold_expansion", true),
3192 ("case_fold_expansion_reversed", true),
3193 ("case_fold_sigma", true),
3194 ("case_fold_non_turkic", true),
3195 ("case_fold_not_turkic", false),
3196 ("empty_same", true),
3197 ("empty_different", false),
3198 ("exact_without_normalization", false),
3199 ("case_fold_without_normalization", false),
3200 ("piped", true),
3201 ] {
3202 assert_eq!(
3203 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3204 .as_bool()
3205 .unwrap(),
3206 expected,
3207 "{name}"
3208 );
3209 }
3210 }
3211
3212 #[tokio::test(flavor = "multi_thread")]
3213 async fn test_string_is_equal_inside_sketch_block_is_predicate() {
3214 let code = r#"
3215@settings(experimentalFeatures = allow)
3216
3217sketch(on = XY) {
3218 stringsAreEqual = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
3219}
3220"#;
3221
3222 parse_execute(code).await.unwrap();
3223 }
3224
3225 #[tokio::test(flavor = "multi_thread")]
3226 async fn test_string_trim() {
3227 let ascii_whitespace = " \t\n";
3228 let tab = "\t";
3229 let non_breaking_space = "\u{a0}";
3230 let em_space = "\u{2003}";
3231 let ideographic_space = "\u{3000}";
3232 let zero_width_space = "\u{200b}";
3233 let decomposed = "e\u{301}";
3234 let code = format!(
3235 r#"
3236ascii = string::trim("{ascii_whitespace}KCL{ascii_whitespace}")
3237internal = string::trim(" KCL{tab}strings ")
3238unicode = string::trim("{non_breaking_space}{em_space}KCL{ideographic_space}")
3239all_whitespace = string::trim("{ascii_whitespace}{non_breaking_space}")
3240empty = string::trim("")
3241unchanged = string::trim("KCL")
3242without_normalization = string::trim(" {decomposed} ")
3243non_whitespace = string::trim("{zero_width_space}KCL{zero_width_space}")
3244piped = " ready " |> string::trim()
3245"#
3246 );
3247
3248 let result = parse_execute(&code).await.unwrap();
3249 let non_whitespace = format!("{zero_width_space}KCL{zero_width_space}");
3250 for (name, expected) in [
3251 ("ascii", "KCL"),
3252 ("internal", "KCL\tstrings"),
3253 ("unicode", "KCL"),
3254 ("all_whitespace", ""),
3255 ("empty", ""),
3256 ("unchanged", "KCL"),
3257 ("without_normalization", decomposed),
3258 ("non_whitespace", non_whitespace.as_str()),
3259 ("piped", "ready"),
3260 ] {
3261 assert_eq!(
3262 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3263 .as_str()
3264 .unwrap(),
3265 expected,
3266 "{name}"
3267 );
3268 }
3269 }
3270
3271 #[tokio::test(flavor = "multi_thread")]
3272 async fn test_string_trim_start() {
3273 let ascii_whitespace = " \t\n";
3274 let tab = "\t";
3275 let non_breaking_space = "\u{a0}";
3276 let em_space = "\u{2003}";
3277 let ideographic_space = "\u{3000}";
3278 let zero_width_space = "\u{200b}";
3279 let decomposed = "e\u{301}";
3280 let code = format!(
3281 r#"
3282ascii = string::trimStart("{ascii_whitespace}KCL{ascii_whitespace}")
3283internal = string::trimStart(" KCL{tab}strings")
3284unicode = string::trimStart("{non_breaking_space}{em_space}KCL{ideographic_space}")
3285all_whitespace = string::trimStart("{ascii_whitespace}{non_breaking_space}")
3286empty = string::trimStart("")
3287unchanged = string::trimStart("KCL")
3288without_normalization = string::trimStart(" {decomposed}")
3289non_whitespace_prefix = string::trimStart("{zero_width_space}{ascii_whitespace}KCL")
3290piped = " ready " |> string::trimStart()
3291"#
3292 );
3293
3294 let result = parse_execute(&code).await.unwrap();
3295 let ascii = format!("KCL{ascii_whitespace}");
3296 let unicode = format!("KCL{ideographic_space}");
3297 let non_whitespace_prefix = format!("{zero_width_space}{ascii_whitespace}KCL");
3298 for (name, expected) in [
3299 ("ascii", ascii.as_str()),
3300 ("internal", "KCL\tstrings"),
3301 ("unicode", unicode.as_str()),
3302 ("all_whitespace", ""),
3303 ("empty", ""),
3304 ("unchanged", "KCL"),
3305 ("without_normalization", decomposed),
3306 ("non_whitespace_prefix", non_whitespace_prefix.as_str()),
3307 ("piped", "ready "),
3308 ] {
3309 assert_eq!(
3310 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3311 .as_str()
3312 .unwrap(),
3313 expected,
3314 "{name}"
3315 );
3316 }
3317 }
3318
3319 #[tokio::test(flavor = "multi_thread")]
3320 async fn test_string_trim_end() {
3321 let ascii_whitespace = " \t\n";
3322 let tab = "\t";
3323 let non_breaking_space = "\u{a0}";
3324 let em_space = "\u{2003}";
3325 let ideographic_space = "\u{3000}";
3326 let zero_width_space = "\u{200b}";
3327 let decomposed = "e\u{301}";
3328 let code = format!(
3329 r#"
3330ascii = string::trimEnd("{ascii_whitespace}KCL{ascii_whitespace}")
3331internal = string::trimEnd("KCL{tab}strings ")
3332unicode = string::trimEnd("{non_breaking_space}KCL{em_space}{ideographic_space}")
3333all_whitespace = string::trimEnd("{ascii_whitespace}{non_breaking_space}")
3334empty = string::trimEnd("")
3335unchanged = string::trimEnd("KCL")
3336without_normalization = string::trimEnd("{decomposed} ")
3337non_whitespace_suffix = string::trimEnd("KCL{ascii_whitespace}{zero_width_space}")
3338piped = " ready " |> string::trimEnd()
3339"#
3340 );
3341
3342 let result = parse_execute(&code).await.unwrap();
3343 let ascii = format!("{ascii_whitespace}KCL");
3344 let unicode = format!("{non_breaking_space}KCL");
3345 let non_whitespace_suffix = format!("KCL{ascii_whitespace}{zero_width_space}");
3346 for (name, expected) in [
3347 ("ascii", ascii.as_str()),
3348 ("internal", "KCL\tstrings"),
3349 ("unicode", unicode.as_str()),
3350 ("all_whitespace", ""),
3351 ("empty", ""),
3352 ("unchanged", "KCL"),
3353 ("without_normalization", decomposed),
3354 ("non_whitespace_suffix", non_whitespace_suffix.as_str()),
3355 ("piped", " ready"),
3356 ] {
3357 assert_eq!(
3358 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3359 .as_str()
3360 .unwrap(),
3361 expected,
3362 "{name}"
3363 );
3364 }
3365 }
3366
3367 #[tokio::test(flavor = "multi_thread")]
3368 async fn test_string_equality_operators() {
3369 let composed = "\u{e9}";
3370 let decomposed = "e\u{301}";
3371 let code = format!(
3372 r#"
3373equal_same_ascii = "KCL" == "KCL"
3374equal_different_case = "KCL" == "kcl"
3375not_equal_same_ascii = "KCL" != "KCL"
3376not_equal_different_case = "KCL" != "kcl"
3377equal_same_unicode = "{composed}" == "{composed}"
3378not_equal_same_unicode = "{composed}" != "{composed}"
3379equal_without_normalization = "{composed}" == "{decomposed}"
3380not_equal_without_normalization = "{composed}" != "{decomposed}"
3381"#
3382 );
3383
3384 let result = parse_execute(&code).await.unwrap();
3385 for (name, expected) in [
3386 ("equal_same_ascii", true),
3387 ("equal_different_case", false),
3388 ("not_equal_same_ascii", false),
3389 ("not_equal_different_case", true),
3390 ("equal_same_unicode", true),
3391 ("not_equal_same_unicode", false),
3392 ("equal_without_normalization", false),
3393 ("not_equal_without_normalization", true),
3394 ] {
3395 assert_eq!(
3396 mem_get_json(result.exec_state.stack(), result.mem_env, name)
3397 .as_bool()
3398 .unwrap(),
3399 expected,
3400 "{name}"
3401 );
3402 }
3403 }
3404
3405 #[tokio::test(flavor = "multi_thread")]
3406 async fn test_string_equality_inside_sketch_block_fails_like_number_equality() {
3407 let string_code = r#"
3408@settings(experimentalFeatures = allow)
3409
3410sketch(on = XY) {
3411 stringsAreEqual = "KCL" == "KCL"
3412}
3413"#;
3414 let number_code = r#"
3415@settings(experimentalFeatures = allow)
3416
3417sketch(on = XY) {
3418 numbersAreEqual = 1 == 1
3419}
3420"#;
3421
3422 assert_eq!(
3423 parse_execute(string_code).await.unwrap_err().message(),
3424 "Cannot create an equivalence constraint between values of these types: a string and a string"
3425 );
3426 assert_eq!(
3427 parse_execute(number_code).await.unwrap_err().message(),
3428 "Cannot create an equivalence constraint between values of these types: a number and a number"
3429 );
3430 }
3431
3432 #[tokio::test(flavor = "multi_thread")]
3433 async fn test_math_execute_start_negative() {
3434 let ast = r#"myVar = -5 + 6"#;
3435 let result = parse_execute(ast).await.unwrap();
3436 assert_eq!(
3437 1.0,
3438 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3439 .as_f64()
3440 .unwrap()
3441 );
3442 }
3443
3444 #[tokio::test(flavor = "multi_thread")]
3445 async fn test_math_execute_with_pi() {
3446 let ast = r#"myVar = PI * 2"#;
3447 let result = parse_execute(ast).await.unwrap();
3448 assert_eq!(
3449 std::f64::consts::TAU,
3450 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3451 .as_f64()
3452 .unwrap()
3453 );
3454 }
3455
3456 #[tokio::test(flavor = "multi_thread")]
3457 async fn test_math_define_decimal_without_leading_zero() {
3458 let ast = r#"thing = .4 + 7"#;
3459 let result = parse_execute(ast).await.unwrap();
3460 assert_eq!(
3461 7.4,
3462 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
3463 .as_f64()
3464 .unwrap()
3465 );
3466 }
3467
3468 #[tokio::test(flavor = "multi_thread")]
3469 async fn pass_std_to_std() {
3470 let ast = r#"sketch001 = startSketchOn(XY)
3471profile001 = circle(sketch001, center = [0, 0], radius = 2)
3472extrude001 = extrude(profile001, length = 5)
3473extrudes = patternLinear3d(
3474 extrude001,
3475 instances = 3,
3476 distance = 5,
3477 axis = [1, 1, 0],
3478)
3479clone001 = map(extrudes, f = clone)
3480"#;
3481 parse_execute(ast).await.unwrap();
3482 }
3483
3484 #[tokio::test(flavor = "multi_thread")]
3485 async fn test_array_reduce_nested_array() {
3486 let code = r#"
3487fn id(@el, accum) { return accum }
3488
3489answer = reduce([], initial=[[[0,0]]], f=id)
3490"#;
3491 let result = parse_execute(code).await.unwrap();
3492 assert_eq!(
3493 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
3494 KclValue::HomArray {
3495 value: vec![KclValue::HomArray {
3496 value: vec![KclValue::HomArray {
3497 value: vec![
3498 KclValue::Number {
3499 value: 0.0,
3500 ty: NumericType::default(),
3501 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
3502 },
3503 KclValue::Number {
3504 value: 0.0,
3505 ty: NumericType::default(),
3506 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
3507 }
3508 ],
3509 ty: RuntimeType::any(),
3510 }],
3511 ty: RuntimeType::any(),
3512 }],
3513 ty: RuntimeType::any(),
3514 }
3515 );
3516 }
3517
3518 #[tokio::test(flavor = "multi_thread")]
3519 async fn test_zero_param_fn() {
3520 let ast = r#"sigmaAllow = 35000 // psi
3521leg1 = 5 // inches
3522leg2 = 8 // inches
3523fn thickness() { return 0.56 }
3524
3525bracket = startSketchOn(XY)
3526 |> startProfile(at = [0,0])
3527 |> line(end = [0, leg1])
3528 |> line(end = [leg2, 0])
3529 |> line(end = [0, -thickness()])
3530 |> line(end = [-leg2 + thickness(), 0])
3531"#;
3532 parse_execute(ast).await.unwrap();
3533 }
3534
3535 #[tokio::test(flavor = "multi_thread")]
3536 async fn test_unary_operator_not_succeeds() {
3537 let ast = r#"
3538fn returnTrue() { return !false }
3539t = true
3540f = false
3541notTrue = !t
3542notFalse = !f
3543c = !!true
3544d = !returnTrue()
3545
3546assertIs(!false, error = "expected to pass")
3547
3548fn check(x) {
3549 assertIs(!x, error = "expected argument to be false")
3550 return true
3551}
3552check(x = false)
3553"#;
3554 let result = parse_execute(ast).await.unwrap();
3555 assert_eq!(
3556 false,
3557 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
3558 .as_bool()
3559 .unwrap()
3560 );
3561 assert_eq!(
3562 true,
3563 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
3564 .as_bool()
3565 .unwrap()
3566 );
3567 assert_eq!(
3568 true,
3569 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
3570 .as_bool()
3571 .unwrap()
3572 );
3573 assert_eq!(
3574 false,
3575 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
3576 .as_bool()
3577 .unwrap()
3578 );
3579 }
3580
3581 #[tokio::test(flavor = "multi_thread")]
3582 async fn test_unary_operator_not_on_non_bool_fails() {
3583 let code1 = r#"
3584// Yup, this is null.
3585myNull = 0 / 0
3586notNull = !myNull
3587"#;
3588 assert_eq!(
3589 parse_execute(code1).await.unwrap_err().message(),
3590 "Cannot apply unary operator ! to non-boolean value: a number",
3591 );
3592
3593 let code2 = "notZero = !0";
3594 assert_eq!(
3595 parse_execute(code2).await.unwrap_err().message(),
3596 "Cannot apply unary operator ! to non-boolean value: a number",
3597 );
3598
3599 let code3 = r#"
3600notEmptyString = !""
3601"#;
3602 assert_eq!(
3603 parse_execute(code3).await.unwrap_err().message(),
3604 "Cannot apply unary operator ! to non-boolean value: a string",
3605 );
3606
3607 let code4 = r#"
3608obj = { a = 1 }
3609notMember = !obj.a
3610"#;
3611 assert_eq!(
3612 parse_execute(code4).await.unwrap_err().message(),
3613 "Cannot apply unary operator ! to non-boolean value: a number",
3614 );
3615
3616 let code5 = "
3617a = []
3618notArray = !a";
3619 assert_eq!(
3620 parse_execute(code5).await.unwrap_err().message(),
3621 "Cannot apply unary operator ! to non-boolean value: an empty array",
3622 );
3623
3624 let code6 = "
3625x = {}
3626notObject = !x";
3627 assert_eq!(
3628 parse_execute(code6).await.unwrap_err().message(),
3629 "Cannot apply unary operator ! to non-boolean value: an object",
3630 );
3631
3632 let code7 = "
3633fn x() { return 1 }
3634notFunction = !x";
3635 let fn_err = parse_execute(code7).await.unwrap_err();
3636 assert!(
3639 fn_err
3640 .message()
3641 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
3642 "Actual error: {fn_err:?}"
3643 );
3644
3645 let code8 = "
3646myTagDeclarator = $myTag
3647notTagDeclarator = !myTagDeclarator";
3648 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
3649 assert!(
3652 tag_declarator_err
3653 .message()
3654 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
3655 "Actual error: {tag_declarator_err:?}"
3656 );
3657
3658 let code9 = "
3659myTagDeclarator = $myTag
3660notTagIdentifier = !myTag";
3661 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
3662 assert!(
3665 tag_identifier_err
3666 .message()
3667 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
3668 "Actual error: {tag_identifier_err:?}"
3669 );
3670
3671 let code10 = "notPipe = !(1 |> 2)";
3672 assert_eq!(
3673 parse_execute(code10).await.unwrap_err(),
3676 KclError::new_syntax(KclErrorDetails::new(
3677 "Unexpected token: !".to_owned(),
3678 vec![SourceRange::new(10, 11, ModuleId::default())],
3679 ))
3680 );
3681
3682 let code11 = "
3683fn identity(x) { return x }
3684notPipeSub = 1 |> identity(!%))";
3685 assert_eq!(
3686 parse_execute(code11).await.unwrap_err(),
3689 KclError::new_syntax(KclErrorDetails::new(
3690 "There was an unexpected `!`. Try removing it.".to_owned(),
3691 vec![SourceRange::new(56, 57, ModuleId::default())],
3692 ))
3693 );
3694
3695 }
3699
3700 #[tokio::test(flavor = "multi_thread")]
3701 async fn test_start_sketch_on_invalid_kwargs() {
3702 let current_dir = std::env::current_dir().unwrap();
3703 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
3704 let mut code = std::fs::read_to_string(&path).unwrap();
3705 assert_eq!(
3706 parse_execute(&code).await.unwrap_err().message(),
3707 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
3708 );
3709
3710 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
3711 code = std::fs::read_to_string(&path).unwrap();
3712
3713 assert_eq!(
3714 parse_execute(&code).await.unwrap_err().message(),
3715 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
3716 );
3717
3718 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
3719 code = std::fs::read_to_string(&path).unwrap();
3720
3721 assert_eq!(
3722 parse_execute(&code).await.unwrap_err().message(),
3723 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
3724 );
3725
3726 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
3727 code = std::fs::read_to_string(&path).unwrap();
3728
3729 assert_eq!(
3730 parse_execute(&code).await.unwrap_err().message(),
3731 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
3732 );
3733
3734 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
3735 code = std::fs::read_to_string(&path).unwrap();
3736
3737 assert_eq!(
3738 parse_execute(&code).await.unwrap_err().message(),
3739 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
3740 );
3741 }
3742
3743 #[tokio::test(flavor = "multi_thread")]
3744 async fn test_math_negative_variable_in_binary_expression() {
3745 let ast = r#"sigmaAllow = 35000 // psi
3746width = 1 // inch
3747
3748p = 150 // lbs
3749distance = 6 // inches
3750FOS = 2
3751
3752leg1 = 5 // inches
3753leg2 = 8 // inches
3754
3755thickness_squared = distance * p * FOS * 6 / sigmaAllow
3756thickness = 0.56 // inches. App does not support square root function yet
3757
3758bracket = startSketchOn(XY)
3759 |> startProfile(at = [0,0])
3760 |> line(end = [0, leg1])
3761 |> line(end = [leg2, 0])
3762 |> line(end = [0, -thickness])
3763 |> line(end = [-leg2 + thickness, 0])
3764"#;
3765 parse_execute(ast).await.unwrap();
3766 }
3767
3768 #[tokio::test(flavor = "multi_thread")]
3769 async fn test_execute_function_no_return() {
3770 let ast = r#"fn test(@origin) {
3771 origin
3772}
3773
3774test([0, 0])
3775"#;
3776 let result = parse_execute(ast).await;
3777 assert!(result.is_err());
3778 assert!(result.unwrap_err().to_string().contains("undefined"));
3779 }
3780
3781 #[tokio::test(flavor = "multi_thread")]
3782 async fn test_max_stack_size_exceeded_error() {
3783 let ast = r#"
3784fn forever(@n) {
3785 return 1 + forever(n)
3786}
3787
3788forever(1)
3789"#;
3790 let result = parse_execute(ast).await;
3791 let err = result.unwrap_err();
3792 assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
3793 }
3794
3795 #[tokio::test(flavor = "multi_thread")]
3796 async fn test_math_doubly_nested_parens() {
3797 let ast = r#"sigmaAllow = 35000 // psi
3798width = 4 // inch
3799p = 150 // Force on shelf - lbs
3800distance = 6 // inches
3801FOS = 2
3802leg1 = 5 // inches
3803leg2 = 8 // inches
3804thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
3805thickness = 0.32 // inches. App does not support square root function yet
3806bracket = startSketchOn(XY)
3807 |> startProfile(at = [0,0])
3808 |> line(end = [0, leg1])
3809 |> line(end = [leg2, 0])
3810 |> line(end = [0, -thickness])
3811 |> line(end = [-1 * leg2 + thickness, 0])
3812 |> line(end = [0, -1 * leg1 + thickness])
3813 |> close()
3814 |> extrude(length = width)
3815"#;
3816 parse_execute(ast).await.unwrap();
3817 }
3818
3819 #[tokio::test(flavor = "multi_thread")]
3820 async fn test_math_nested_parens_one_less() {
3821 let ast = r#" sigmaAllow = 35000 // psi
3822width = 4 // inch
3823p = 150 // Force on shelf - lbs
3824distance = 6 // inches
3825FOS = 2
3826leg1 = 5 // inches
3827leg2 = 8 // inches
3828thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
3829thickness = 0.32 // inches. App does not support square root function yet
3830bracket = startSketchOn(XY)
3831 |> startProfile(at = [0,0])
3832 |> line(end = [0, leg1])
3833 |> line(end = [leg2, 0])
3834 |> line(end = [0, -thickness])
3835 |> line(end = [-1 * leg2 + thickness, 0])
3836 |> line(end = [0, -1 * leg1 + thickness])
3837 |> close()
3838 |> extrude(length = width)
3839"#;
3840 parse_execute(ast).await.unwrap();
3841 }
3842
3843 #[tokio::test(flavor = "multi_thread")]
3844 async fn test_fn_as_operand() {
3845 let ast = r#"fn f() { return 1 }
3846x = f()
3847y = x + 1
3848z = f() + 1
3849w = f() + f()
3850"#;
3851 parse_execute(ast).await.unwrap();
3852 }
3853
3854 #[tokio::test(flavor = "multi_thread")]
3855 async fn kcl_test_ids_stable_between_executions() {
3856 let code = r#"sketch001 = startSketchOn(XZ)
3857|> startProfile(at = [61.74, 206.13])
3858|> xLine(length = 305.11, tag = $seg01)
3859|> yLine(length = -291.85)
3860|> xLine(length = -segLen(seg01))
3861|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3862|> close()
3863|> extrude(length = 40.14)
3864|> shell(
3865 thickness = 3.14,
3866 faces = [seg01]
3867)
3868"#;
3869
3870 let ctx = crate::test_server::new_context(true, None).await.unwrap();
3871 let old_program = crate::Program::parse_no_errs(code).unwrap();
3872
3873 if let Err(err) = ctx.run_with_caching(old_program).await {
3875 let report = err.into_miette_report_with_outputs(code).unwrap();
3876 let report = miette::Report::new(report);
3877 panic!("Error executing program: {report:?}");
3878 }
3879
3880 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3882
3883 let code = r#"sketch001 = startSketchOn(XZ)
3884|> startProfile(at = [62.74, 206.13])
3885|> xLine(length = 305.11, tag = $seg01)
3886|> yLine(length = -291.85)
3887|> xLine(length = -segLen(seg01))
3888|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3889|> close()
3890|> extrude(length = 40.14)
3891|> shell(
3892 faces = [seg01],
3893 thickness = 3.14,
3894)
3895"#;
3896
3897 let program = crate::Program::parse_no_errs(code).unwrap();
3899 ctx.run_with_caching(program).await.unwrap();
3901
3902 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3903
3904 assert_eq!(id_generator, new_id_generator);
3905 }
3906
3907 #[tokio::test(flavor = "multi_thread")]
3908 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
3909 let code = r#"sketch001 = startSketchOn(XZ)
3910|> startProfile(at = [61.74, 206.13])
3911|> xLine(length = 305.11, tag = $seg01)
3912|> yLine(length = -291.85)
3913|> xLine(length = -segLen(seg01))
3914|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3915|> close()
3916|> extrude(length = 40.14)
3917|> shell(
3918 thickness = 3.14,
3919 faces = [seg01]
3920)
3921"#;
3922
3923 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
3924 let old_program = crate::Program::parse_no_errs(code).unwrap();
3925
3926 ctx.run_with_caching(old_program.clone()).await.unwrap();
3928
3929 let settings_state = cache::read_old_ast().await.unwrap().settings;
3930
3931 assert_eq!(settings_state, ctx.settings);
3933
3934 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3936
3937 ctx.run_with_caching(old_program.clone()).await.unwrap();
3939
3940 let settings_state = cache::read_old_ast().await.unwrap().settings;
3941
3942 assert_eq!(settings_state, ctx.settings);
3944
3945 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3947
3948 ctx.run_with_caching(old_program).await.unwrap();
3950
3951 let settings_state = cache::read_old_ast().await.unwrap().settings;
3952
3953 assert_eq!(settings_state, ctx.settings);
3955
3956 ctx.close().await;
3957 }
3958
3959 #[tokio::test(flavor = "multi_thread")]
3960 async fn mock_after_not_mock() {
3961 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3962 let program = crate::Program::parse_no_errs("x = 2").unwrap();
3963 let result = ctx.run_with_caching(program).await.unwrap();
3964 assert_number_variable(&result.variables, "x", 2.0);
3965
3966 let ctx2 = ExecutorContext::new_mock(None).await;
3967 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
3968 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
3969 assert_number_variable(&result.variables, "z", 3.0);
3970
3971 ctx.close().await;
3972 ctx2.close().await;
3973 }
3974
3975 #[tokio::test(flavor = "multi_thread")]
3977 async fn mock_execution_succeeds_after_split() {
3978 let code = kcl_input!("repro_mock_extrude");
3979 let ctx = ExecutorContext::new_mock(None).await;
3980 let program = crate::Program::parse_no_errs(code).unwrap();
3981 let _result = match ctx.run_mock(&program, &MockConfig::default()).await {
3982 Ok(res) => res,
3983 Err(e) => panic!("{}", e.error),
3984 };
3985 }
3986
3987 #[tokio::test(flavor = "multi_thread")]
3988 async fn mock_then_add_extrude_then_mock_again() {
3989 let code = "s = sketch(on = XY) {
3990 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
3991 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
3992 coincident([line1.end, line2.start])
3993 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
3994 coincident([line2.end, line3.start])
3995 coincident([line1.start, line3.end])
3996}
3997 ";
3998 let ctx = ExecutorContext::new_mock(None).await;
3999 let program = crate::Program::parse_no_errs(code).unwrap();
4000 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
4001 assert!(result.variables.contains_key("s"), "actual: {:?}", result.variables);
4002
4003 let code2 = code.to_owned()
4004 + "
4005region001 = region(point = [1mm, 1mm], sketch = s)
4006extrude001 = extrude(region001, length = 1)
4007 ";
4008 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4009 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
4010 assert!(
4011 result.variables.contains_key("region001"),
4012 "actual: {:?}",
4013 result.variables
4014 );
4015
4016 ctx.close().await;
4017 }
4018
4019 #[tokio::test(flavor = "multi_thread")]
4020 async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
4021 let code = format!(
4022 r#"{}
4023
4024face7 = faceOf(solid6, face = r6.tags.line1)
4025r7 = squareRegion(onSurface = face7)
4026solid7 = extrude(r7, length = width)
4027"#,
4028 include_str!("../../tests/endless_impeller/input.kcl")
4029 );
4030
4031 let result = parse_execute(&code).await.unwrap();
4032 let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
4033 assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
4034
4035 let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
4036 KclValue::Face { value } => value,
4037 value => panic!("expected face7 to be a Face, got {value:?}"),
4038 };
4039 assert!(face7.parent_solid.creator_sketch_id.is_some());
4040 }
4041
4042 #[tokio::test(flavor = "multi_thread")]
4043 async fn mock_has_stable_ids() {
4044 let ctx = ExecutorContext::new_mock(None).await;
4045 let mock_config = MockConfig {
4046 use_prev_memory: false,
4047 ..Default::default()
4048 };
4049 let code = "sk = startSketchOn(XY)
4050 |> startProfile(at = [0, 0])";
4051 let program = crate::Program::parse_no_errs(code).unwrap();
4052 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
4053 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4054 assert!(!ids.is_empty(), "IDs should not be empty");
4055
4056 let ctx2 = ExecutorContext::new_mock(None).await;
4057 let program2 = crate::Program::parse_no_errs(code).unwrap();
4058 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
4059 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
4060
4061 assert_eq!(ids, ids2, "Generated IDs should match");
4062 ctx.close().await;
4063 ctx2.close().await;
4064 }
4065
4066 #[tokio::test(flavor = "multi_thread")]
4067 async fn mock_memory_restore_preserves_module_maps() {
4068 clear_mem_cache().await;
4069
4070 let ctx = ExecutorContext::new_mock(None).await;
4071 let cold_start = MockConfig {
4072 use_prev_memory: false,
4073 ..Default::default()
4074 };
4075 ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
4076
4077 let mut mem = cache::read_old_memory().await.unwrap();
4078 assert!(
4079 mem.path_to_source_id.len() > 3,
4080 "expected prelude imports to populate multiple modules, got {:?}",
4081 mem.path_to_source_id
4082 );
4083 mem.constraint_state.insert(
4084 crate::front::ObjectId(1),
4085 indexmap::indexmap! {
4086 crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
4087 crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
4088 },
4089 );
4090
4091 let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
4092 ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
4093
4094 assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
4095 assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
4096 assert_eq!(exec_state.global.module_infos, mem.module_infos);
4097 assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
4098
4099 clear_mem_cache().await;
4100 ctx.close().await;
4101 }
4102
4103 #[tokio::test(flavor = "multi_thread")]
4104 async fn run_with_caching_no_action_refreshes_mock_memory() {
4105 cache::bust_cache().await;
4106 clear_mem_cache().await;
4107
4108 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
4109 let program = crate::Program::parse_no_errs(
4110 r#"sketch001 = sketch(on = XY) {
4111 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
4112}
4113"#,
4114 )
4115 .unwrap();
4116
4117 ctx.run_with_caching(program.clone()).await.unwrap();
4118 let baseline_memory = cache::read_old_memory().await.unwrap();
4119 assert!(
4120 !baseline_memory.scene_objects.is_empty(),
4121 "expected engine execution to persist full-scene mock memory"
4122 );
4123
4124 cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
4125 assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
4126
4127 ctx.run_with_caching(program).await.unwrap();
4128 let refreshed_memory = cache::read_old_memory().await.unwrap();
4129 assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
4130 assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
4131 assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
4132
4133 cache::bust_cache().await;
4134 clear_mem_cache().await;
4135 ctx.close().await;
4136 }
4137
4138 #[tokio::test(flavor = "multi_thread")]
4139 async fn sim_sketch_mode_real_mock_real() {
4140 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4141 let code = r#"sketch001 = startSketchOn(XY)
4142profile001 = startProfile(sketch001, at = [0, 0])
4143 |> line(end = [10, 0])
4144 |> line(end = [0, 10])
4145 |> line(end = [-10, 0])
4146 |> line(end = [0, -10])
4147 |> close()
4148"#;
4149 let program = crate::Program::parse_no_errs(code).unwrap();
4150 let result = ctx.run_with_caching(program).await.unwrap();
4151 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4152
4153 let mock_ctx = ExecutorContext::new_mock(None).await;
4154 let mock_program = crate::Program::parse_no_errs(code).unwrap();
4155 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
4156 assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
4157
4158 let code2 = code.to_owned()
4159 + r#"
4160extrude001 = extrude(profile001, length = 10)
4161"#;
4162 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
4163 let result = ctx.run_with_caching(program2).await.unwrap();
4164 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
4165
4166 ctx.close().await;
4167 mock_ctx.close().await;
4168 }
4169
4170 #[tokio::test(flavor = "multi_thread")]
4171 async fn read_tag_version() {
4172 let ast = r#"fn bar(@t) {
4173 return startSketchOn(XY)
4174 |> startProfile(at = [0,0])
4175 |> angledLine(
4176 angle = -60,
4177 length = segLen(t),
4178 )
4179 |> line(end = [0, 0])
4180 |> close()
4181}
4182
4183sketch = startSketchOn(XY)
4184 |> startProfile(at = [0,0])
4185 |> line(end = [0, 10])
4186 |> line(end = [10, 0], tag = $tag0)
4187 |> line(endAbsolute = [0, 0])
4188
4189fn foo() {
4190 // tag0 tags an edge
4191 return bar(tag0)
4192}
4193
4194solid = sketch |> extrude(length = 10)
4195// tag0 tags a face
4196sketch2 = startSketchOn(solid, face = tag0)
4197 |> startProfile(at = [0,0])
4198 |> line(end = [0, 1])
4199 |> line(end = [1, 0])
4200 |> line(end = [0, 0])
4201
4202foo() |> extrude(length = 1)
4203"#;
4204 parse_execute(ast).await.unwrap();
4205 }
4206
4207 #[tokio::test(flavor = "multi_thread")]
4208 async fn experimental() {
4209 let code = r#"
4210startSketchOn(XY)
4211 |> startProfile(at = [0, 0], tag = $start)
4212 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4213"#;
4214 let result = parse_execute(code).await.unwrap();
4215 let issues = result.exec_state.issues();
4216 assert_eq!(issues.len(), 1);
4217 assert_eq!(issues[0].severity, Severity::Error);
4218 let msg = &issues[0].message;
4219 assert!(msg.contains("experimental"), "found {msg}");
4220
4221 let code = r#"@settings(experimentalFeatures = allow)
4222startSketchOn(XY)
4223 |> startProfile(at = [0, 0], tag = $start)
4224 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4225"#;
4226 let result = parse_execute(code).await.unwrap();
4227 let issues = result.exec_state.issues();
4228 assert!(issues.is_empty(), "issues={issues:#?}");
4229
4230 let code = r#"@settings(experimentalFeatures = warn)
4231startSketchOn(XY)
4232 |> startProfile(at = [0, 0], tag = $start)
4233 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4234"#;
4235 let result = parse_execute(code).await.unwrap();
4236 let issues = result.exec_state.issues();
4237 assert_eq!(issues.len(), 1);
4238 assert_eq!(issues[0].severity, Severity::Warning);
4239 let msg = &issues[0].message;
4240 assert!(msg.contains("experimental"), "found {msg}");
4241
4242 let code = r#"@settings(experimentalFeatures = deny)
4243startSketchOn(XY)
4244 |> startProfile(at = [0, 0], tag = $start)
4245 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4246"#;
4247 let result = parse_execute(code).await.unwrap();
4248 let issues = result.exec_state.issues();
4249 assert_eq!(issues.len(), 1);
4250 assert_eq!(issues[0].severity, Severity::Error);
4251 let msg = &issues[0].message;
4252 assert!(msg.contains("experimental"), "found {msg}");
4253
4254 let code = r#"@settings(experimentalFeatures = foo)
4255startSketchOn(XY)
4256 |> startProfile(at = [0, 0], tag = $start)
4257 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
4258"#;
4259 parse_execute(code).await.unwrap_err();
4260 }
4261
4262 #[tokio::test(flavor = "multi_thread")]
4263 async fn experimental_parameter() {
4264 let code = r#"
4265fn inc(@x, @(experimental = true) amount? = 1) {
4266 return x + amount
4267}
4268
4269answer = inc(5, amount = 2)
4270"#;
4271 let result = parse_execute(code).await.unwrap();
4272 let issues = result.exec_state.issues();
4273 assert_eq!(issues.len(), 1);
4274 assert_eq!(issues[0].severity, Severity::Error);
4275 let msg = &issues[0].message;
4276 assert!(msg.contains("experimental"), "found {msg}");
4277
4278 let code = r#"
4280fn inc(@x, @(experimental = true) amount? = 1) {
4281 return x + amount
4282}
4283
4284answer = inc(5)
4285"#;
4286 let result = parse_execute(code).await.unwrap();
4287 let issues = result.exec_state.issues();
4288 assert!(issues.is_empty(), "issues={issues:#?}");
4289 }
4290
4291 #[tokio::test(flavor = "multi_thread")]
4292 async fn experimental_scalar_fixed_constraint() {
4293 let code_left = r#"@settings(experimentalFeatures = warn)
4294sketch(on = XY) {
4295 point1 = point(at = [var 0mm, var 0mm])
4296 point1.at[0] == 1mm
4297}
4298"#;
4299 let code_right = r#"@settings(experimentalFeatures = warn)
4301sketch(on = XY) {
4302 point1 = point(at = [var 0mm, var 0mm])
4303 1mm == point1.at[0]
4304}
4305"#;
4306
4307 for code in [code_left, code_right] {
4308 let result = parse_execute(code).await.unwrap();
4309 let issues = result.exec_state.issues();
4310 let Some(error) = issues
4311 .iter()
4312 .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
4313 else {
4314 panic!("found {issues:#?}");
4315 };
4316 assert_eq!(error.severity, Severity::Warning);
4317 }
4318 }
4319
4320 #[tokio::test(flavor = "multi_thread")]
4324 async fn test_tangent_line_arc_executes_with_mock_engine() {
4325 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
4326 parse_execute(&code).await.unwrap();
4327 }
4328
4329 #[tokio::test(flavor = "multi_thread")]
4330 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
4331 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
4332 parse_execute(&code).await.unwrap();
4333 }
4334
4335 #[tokio::test(flavor = "multi_thread")]
4336 async fn test_tangent_line_circle_executes_with_mock_engine() {
4337 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
4338 parse_execute(&code).await.unwrap();
4339 }
4340
4341 #[tokio::test(flavor = "multi_thread")]
4342 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
4343 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
4344 parse_execute(&code).await.unwrap();
4345 }
4346
4347 #[tokio::test(flavor = "multi_thread")]
4348 async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
4349 let code = r#"startX = 2
4350
4351baseSketch = sketch(on = XY) {
4352 yoyo = line(start = [startX, 0], end = [7, 6])
4353 line2 = line(start = [7, 6], end = [7, 12])
4354 hi = line(start = [7, 12], end = [startX, 0])
4355}
4356
4357baseRegion = region(point = [5.5, 6], sketch = baseSketch)
4358myExtrude = extrude(
4359 baseRegion,
4360 length = 5,
4361 tagEnd = $endCap,
4362 tagStart = $startCap,
4363)
4364yodawg = getCommonEdge(faces = [
4365 baseRegion.tags.hi,
4366 baseRegion.tags.yoyo
4367])
4368
4369cutSketch = sketch(on = YZ) {
4370 myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
4371 myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
4372 line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
4373}
4374
4375cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
4376extrude001 = extrude(cutRegion, length = 5)
4377solid001 = subtract(myExtrude, tools = extrude001)
4378
4379yoyo = getOppositeEdge(baseRegion.tags.hi)
4380fillet(solid001, radius = 0.1, tags = yoyo)
4381"#;
4382
4383 parse_execute(code).await.unwrap();
4384 }
4385
4386 async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
4391 let program = crate::Program::parse_no_errs(kcl).unwrap();
4392 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4393 let mut exec_state = ExecState::new(&ctx);
4394 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
4395 let outcome = exec_state
4396 .into_exec_outcome(env_ref, &ctx)
4397 .await
4398 .expect("constraint report test outcome should collect variables");
4399 let report = outcome.sketch_constraint_report();
4400 ctx.close().await;
4401 report
4402 }
4403
4404 #[tokio::test(flavor = "multi_thread")]
4405 async fn warn_when_sketch_is_over_constrained() {
4406 let code = r#"
4407sketch001 = sketch(on = XY) {
4408 line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
4409 fixed([line1.start, ORIGIN])
4410 fixed([line1.start, [20, 20]])
4411}
4412"#;
4413 let result = parse_execute(code).await.unwrap();
4414 let issues = result.exec_state.issues();
4415 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
4416 panic!("expected over-constrained warning; found {issues:#?}");
4417 };
4418 assert_eq!(warning.severity, Severity::Warning);
4419 }
4420
4421 #[tokio::test(flavor = "multi_thread")]
4422 async fn no_warning_when_sketch_is_not_over_constrained() {
4423 let code = r#"
4425sketch001 = sketch(on = XY) {
4426 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4427}
4428"#;
4429 let result = parse_execute(code).await.unwrap();
4430 let issues = result.exec_state.issues();
4431 assert!(
4432 !issues.iter().any(|issue| issue.message.contains("over-constrained")),
4433 "did not expect over-constrained warning; found {issues:#?}"
4434 );
4435 }
4436
4437 #[tokio::test(flavor = "multi_thread")]
4438 async fn test_constraint_report_fully_constrained() {
4439 let kcl = r#"
4441@settings(experimentalFeatures = allow)
4442
4443sketch(on = YZ) {
4444 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4445 line1.start.at[0] == 2
4446 line1.start.at[1] == 8
4447 line1.end.at[0] == 5
4448 line1.end.at[1] == 7
4449}
4450"#;
4451 let report = run_constraint_report(kcl).await;
4452 assert_eq!(report.fully_constrained.len(), 1);
4453 assert_eq!(report.under_constrained.len(), 0);
4454 assert_eq!(report.over_constrained.len(), 0);
4455 assert_eq!(report.errors.len(), 0);
4456 assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
4457 }
4458
4459 #[tokio::test(flavor = "multi_thread")]
4460 async fn test_constraint_report_under_constrained() {
4461 let kcl = r#"
4463sketch(on = YZ) {
4464 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
4465}
4466"#;
4467 let report = run_constraint_report(kcl).await;
4468 assert_eq!(report.fully_constrained.len(), 0);
4469 assert_eq!(report.under_constrained.len(), 1);
4470 assert_eq!(report.over_constrained.len(), 0);
4471 assert_eq!(report.errors.len(), 0);
4472 assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
4473 assert!(report.under_constrained[0].free_count > 0);
4474 }
4475
4476 #[tokio::test(flavor = "multi_thread")]
4477 async fn test_constraint_report_over_constrained() {
4478 let kcl = r#"
4480@settings(experimentalFeatures = allow)
4481
4482sketch(on = YZ) {
4483 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4484 line1.start.at[0] == 2
4485 line1.start.at[1] == 8
4486 line1.end.at[0] == 5
4487 line1.end.at[1] == 7
4488 distance([line1.start, line1.end]) == 100mm
4489}
4490"#;
4491 let report = run_constraint_report(kcl).await;
4492 assert_eq!(report.over_constrained.len(), 1);
4493 assert_eq!(report.errors.len(), 0);
4494 assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
4495 assert!(report.over_constrained[0].conflict_count > 0);
4496 }
4497
4498 #[tokio::test(flavor = "multi_thread")]
4499 async fn test_constraint_report_multiple_sketches() {
4500 let kcl = r#"
4502@settings(experimentalFeatures = allow)
4503
4504s1 = sketch(on = YZ) {
4505 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4506 line1.start.at[0] == 2
4507 line1.start.at[1] == 8
4508 line1.end.at[0] == 5
4509 line1.end.at[1] == 7
4510}
4511
4512s2 = sketch(on = XZ) {
4513 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4514}
4515"#;
4516 let report = run_constraint_report(kcl).await;
4517 assert_eq!(
4518 report.fully_constrained.len()
4519 + report.under_constrained.len()
4520 + report.over_constrained.len()
4521 + report.errors.len(),
4522 2,
4523 "Expected 2 sketches total"
4524 );
4525 assert_eq!(report.fully_constrained.len(), 1);
4526 assert_eq!(report.under_constrained.len(), 1);
4527 }
4528
4529 #[tokio::test(flavor = "multi_thread")]
4530 async fn test_enum_declaration_is_experimental() {
4531 let code = "type Color { | Red }";
4534 assert_eq!(
4535 parse_execute(code).await.unwrap_err().message(),
4536 "Use of enum declarations is experimental and may change or be removed."
4537 );
4538 }
4539
4540 #[tokio::test(flavor = "multi_thread")]
4541 async fn test_enum_declaration_execution_not_yet_supported() {
4542 let code = r#"@settings(experimentalFeatures = allow)
4545type Color { | Red }
4546"#;
4547 assert_eq!(
4548 parse_execute(code).await.unwrap_err().message(),
4549 "Enum declarations are not yet supported."
4550 );
4551
4552 let code = r#"@settings(experimentalFeatures = allow)
4554export type Color { | Red }
4555"#;
4556 assert_eq!(
4557 parse_execute(code).await.unwrap_err().message(),
4558 "Enum declarations are not yet supported."
4559 );
4560 }
4561}