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