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 cad_op::Group;
22pub use cad_op::Operation;
23pub use geometry::*;
24pub use id_generator::IdGenerator;
25pub(crate) use import::PreImportedGeometry;
26use indexmap::IndexMap;
27pub use kcl_value::KclObjectFields;
28pub use kcl_value::KclValue;
29use kcmc::ImageFormat;
30use kcmc::ModelingCmd;
31use kcmc::each_cmd as mcmd;
32use kcmc::ok_response::OkModelingCmdResponse;
33use kcmc::ok_response::output::TakeSnapshot;
34use kcmc::websocket::ModelingSessionData;
35use kcmc::websocket::OkWebSocketResponseData;
36use kittycad_modeling_cmds::id::ModelingCmdId;
37use kittycad_modeling_cmds::{self as kcmc};
38pub use memory::EnvironmentRef;
39pub(crate) use modeling::ModelingCmdMeta;
40use serde::Deserialize;
41use serde::Serialize;
42pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
43pub(crate) use sketch_solve::solver_numeric_type;
44pub use sketch_transpiler::pre_execute_transpile;
45pub use sketch_transpiler::transpile_all_old_sketches_to_new;
46pub use sketch_transpiler::transpile_old_sketch_to_new;
47pub use sketch_transpiler::transpile_old_sketch_to_new_ast;
48pub use sketch_transpiler::transpile_old_sketch_to_new_with_execution;
49pub(crate) use state::ConstraintKey;
50pub(crate) use state::ConstraintState;
51pub(crate) use state::ConsumedSolidInfo;
52pub(crate) use state::ConsumedSolidKey;
53pub(crate) use state::ConsumedSolidOperation;
54pub use state::ExecState;
55pub(crate) use state::KclVersion;
56pub use state::MetaSettings;
57pub(crate) use state::ModuleArtifactState;
58pub(crate) use state::TangencyMode;
59use uuid::Uuid;
60
61use crate::CompilationIssue;
62use crate::ExecError;
63use crate::KclErrorWithOutputs;
64use crate::NodePath;
65use crate::SourceRange;
66use crate::collections::AhashIndexSet;
67use crate::engine::EngineBatchContext;
68use crate::engine::EngineManager;
69use crate::engine::GridScaleBehavior;
70use crate::errors::KclError;
71use crate::errors::KclErrorDetails;
72use crate::execution::cache::CacheInformation;
73use crate::execution::cache::CacheResult;
74use crate::execution::import_graph::Universe;
75use crate::execution::import_graph::UniverseMap;
76use crate::execution::typed_path::TypedPath;
77use crate::front::Number;
78use crate::front::Object;
79use crate::front::ObjectId;
80use crate::fs::FileManager;
81use crate::modules::ModuleExecutionOutcome;
82use crate::modules::ModuleId;
83use crate::modules::ModulePath;
84use crate::modules::ModuleRepr;
85use crate::parsing::ast::types::Expr;
86use crate::parsing::ast::types::ImportPath;
87use crate::parsing::ast::types::NodeRef;
88
89#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
90#[ts(export)]
91pub struct OperationsByModule {
92 pub map: IndexMap<ModuleId, Vec<Operation>>,
93}
94
95impl OperationsByModule {
96 pub fn count(&self) -> usize {
97 self.map.values().map(Vec::len).sum()
98 }
99
100 pub fn is_empty(&self) -> bool {
101 self.map.values().all(Vec::is_empty)
102 }
103
104 pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
105 self.map.get(module_id)
106 }
107
108 pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
109 self.map.values()
110 }
111
112 pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
113 self.map.insert(module_id, operations);
114 }
115}
116
117pub(crate) mod annotations;
118mod artifact;
119pub(crate) mod cache;
120mod cad_op;
121mod exec_ast;
122pub mod fn_call;
123#[cfg(test)]
124mod freedom_analysis_tests;
125mod geometry;
126mod id_generator;
127mod import;
128mod import_graph;
129pub(crate) mod kcl_value;
130mod memory;
131mod modeling;
132mod sketch_solve;
133mod sketch_transpiler;
134mod state;
135pub mod typed_path;
136pub(crate) mod types;
137
138pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
139pub(crate) const SKETCH_OBJECT_META: &str = "meta";
140pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
141
142macro_rules! control_continue {
147 ($control_flow:expr) => {{
148 let cf = $control_flow;
149 if cf.is_some_return() {
150 return Ok(cf);
151 } else {
152 cf.into_value()
153 }
154 }};
155}
156pub(crate) use control_continue;
158
159macro_rules! early_return {
164 ($control_flow:expr) => {{
165 let cf = $control_flow;
166 if cf.is_some_return() {
167 return Err(EarlyReturn::from(cf));
168 } else {
169 cf.into_value()
170 }
171 }};
172}
173pub(crate) use early_return;
175
176#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
177pub enum ControlFlowKind {
178 #[default]
179 Continue,
180 Exit,
181}
182
183impl ControlFlowKind {
184 pub fn is_some_return(&self) -> bool {
186 match self {
187 ControlFlowKind::Continue => false,
188 ControlFlowKind::Exit => true,
189 }
190 }
191}
192
193#[must_use = "You should always handle the control flow value when it is returned"]
194#[derive(Debug, Clone, PartialEq, Serialize)]
195pub struct KclValueControlFlow {
196 value: KclValue,
198 pub control: ControlFlowKind,
199}
200
201impl KclValue {
202 pub(crate) fn continue_(self) -> KclValueControlFlow {
203 KclValueControlFlow {
204 value: self,
205 control: ControlFlowKind::Continue,
206 }
207 }
208
209 pub(crate) fn exit(self) -> KclValueControlFlow {
210 KclValueControlFlow {
211 value: self,
212 control: ControlFlowKind::Exit,
213 }
214 }
215}
216
217impl KclValueControlFlow {
218 pub fn is_some_return(&self) -> bool {
220 self.control.is_some_return()
221 }
222
223 pub(crate) fn into_value(self) -> KclValue {
224 self.value
225 }
226}
227
228#[must_use = "You should always handle the control flow value when it is returned"]
235#[derive(Debug, Clone)]
236pub(crate) enum EarlyReturn {
237 Value(KclValueControlFlow),
239 Error(KclError),
241}
242
243impl From<KclValueControlFlow> for EarlyReturn {
244 fn from(cf: KclValueControlFlow) -> Self {
245 EarlyReturn::Value(cf)
246 }
247}
248
249impl From<KclError> for EarlyReturn {
250 fn from(err: KclError) -> Self {
251 EarlyReturn::Error(err)
252 }
253}
254
255pub(crate) enum StatementKind<'a> {
256 Declaration { name: &'a str },
257 Expression,
258}
259
260#[derive(Debug, Clone, Copy)]
261pub enum PreserveMem {
262 Normal,
263 Always,
264}
265
266impl PreserveMem {
267 fn normal(self) -> bool {
268 match self {
269 PreserveMem::Normal => true,
270 PreserveMem::Always => false,
271 }
272 }
273}
274
275#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
277#[ts(export)]
278#[serde(rename_all = "camelCase")]
279pub struct ExecOutcome {
280 pub variables: IndexMap<String, KclValue>,
282 pub operations: OperationsByModule,
285 pub artifact_graph: ArtifactGraph,
287 #[serde(skip)]
289 pub scene_objects: Vec<Object>,
290 #[serde(skip)]
293 pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
294 #[serde(skip)]
295 pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
296 pub issues: Vec<CompilationIssue>,
298 pub filenames: IndexMap<ModuleId, ModulePath>,
300 pub default_planes: Option<DefaultPlanes>,
302}
303
304#[derive(Debug, Clone, Copy, PartialEq)]
308enum SegmentFreedom {
309 Free,
310 Fixed,
311 Conflict,
312 Error,
314}
315
316impl From<crate::front::Freedom> for SegmentFreedom {
317 fn from(f: crate::front::Freedom) -> Self {
318 match f {
319 crate::front::Freedom::Free => Self::Free,
320 crate::front::Freedom::Fixed => Self::Fixed,
321 crate::front::Freedom::Conflict => Self::Conflict,
322 }
323 }
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
328pub enum ConstraintKind {
329 FullyConstrained,
330 UnderConstrained,
331 OverConstrained,
332 Error,
336}
337
338#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
345pub struct SketchConstraintStatus {
346 pub name: String,
348 pub status: ConstraintKind,
350 pub free_count: usize,
352 pub conflict_count: usize,
354 pub total_count: usize,
356}
357
358#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
360pub struct SketchConstraintReport {
361 pub fully_constrained: Vec<SketchConstraintStatus>,
362 pub under_constrained: Vec<SketchConstraintStatus>,
363 pub over_constrained: Vec<SketchConstraintStatus>,
364 pub errors: Vec<SketchConstraintStatus>,
367}
368
369pub(crate) fn sketch_constraint_status_for_sketch(
378 scene_objects: &[Object],
379 sketch_obj: &Object,
380) -> Option<SketchConstraintStatus> {
381 use crate::front::ObjectKind;
382 use crate::front::Segment;
383
384 let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
385 return None;
386 };
387
388 let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
390 let obj = scene_objects.get(id.0)?;
391 if let ObjectKind::Segment {
392 segment: Segment::Point(p),
393 } = &obj.kind
394 {
395 Some(p.freedom())
396 } else {
397 None
398 }
399 };
400
401 let mut free_count: usize = 0;
402 let mut conflict_count: usize = 0;
403 let mut error_count: usize = 0;
404 let mut total_count: usize = 0;
405
406 for &seg_id in &sketch.segments {
407 let Some(seg_obj) = scene_objects.get(seg_id.0) else {
408 continue;
409 };
410 let ObjectKind::Segment { segment } = &seg_obj.kind else {
411 continue;
412 };
413 if let Segment::Point(p) = segment
416 && p.owner.is_some()
417 {
418 continue;
419 }
420 let freedom = segment
421 .freedom(lookup)
422 .map(SegmentFreedom::from)
423 .unwrap_or(SegmentFreedom::Error);
424 total_count += 1;
425 match freedom {
426 SegmentFreedom::Free => free_count += 1,
427 SegmentFreedom::Conflict => conflict_count += 1,
428 SegmentFreedom::Error => error_count += 1,
429 SegmentFreedom::Fixed => {}
430 }
431 }
432
433 let status = if error_count > 0 {
434 ConstraintKind::Error
435 } else if conflict_count > 0 {
436 ConstraintKind::OverConstrained
437 } else if free_count > 0 {
438 ConstraintKind::UnderConstrained
439 } else {
440 ConstraintKind::FullyConstrained
441 };
442
443 Some(SketchConstraintStatus {
444 name: sketch_obj.label.clone(),
445 status,
446 free_count,
447 conflict_count,
448 total_count,
449 })
450}
451
452pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
453 let mut fully_constrained = Vec::new();
454 let mut under_constrained = Vec::new();
455 let mut over_constrained = Vec::new();
456 let mut errors = Vec::new();
457
458 for obj in scene_objects {
459 let Some(entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
460 continue;
461 };
462 match entry.status {
463 ConstraintKind::FullyConstrained => fully_constrained.push(entry),
464 ConstraintKind::UnderConstrained => under_constrained.push(entry),
465 ConstraintKind::OverConstrained => over_constrained.push(entry),
466 ConstraintKind::Error => errors.push(entry),
467 }
468 }
469
470 SketchConstraintReport {
471 fully_constrained,
472 under_constrained,
473 over_constrained,
474 errors,
475 }
476}
477
478impl ExecOutcome {
479 pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
480 debug_assert!(
481 id.0 < self.scene_objects.len(),
482 "Requested object ID {} but only have {} objects",
483 id.0,
484 self.scene_objects.len()
485 );
486 self.scene_objects.get(id.0)
487 }
488
489 pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
491 self.issues.iter().filter(|error| error.is_err())
492 }
493
494 pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
501 sketch_constraint_report_from_scene_objects(&self.scene_objects)
502 }
503}
504
505#[derive(Debug, Clone, PartialEq)]
507pub struct MockConfig {
508 pub use_prev_memory: bool,
509 pub sketch_block_id: Option<ObjectId>,
512 pub freedom_analysis: bool,
515 pub segment_ids_edited: AhashIndexSet<ObjectId>,
517 pub drag_anchors: Vec<SegmentDragAnchor>,
519}
520
521#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
522#[ts(export, export_to = "FrontendApi.ts")]
523#[serde(rename_all = "camelCase")]
524pub struct SegmentDragAnchor {
525 pub segment_id: ObjectId,
526 pub target: crate::front::Point2d<Number>,
527}
528
529impl Default for MockConfig {
530 fn default() -> Self {
531 Self {
532 use_prev_memory: true,
534 sketch_block_id: None,
535 freedom_analysis: true,
536 segment_ids_edited: AhashIndexSet::default(),
537 drag_anchors: Vec::new(),
538 }
539 }
540}
541
542impl MockConfig {
543 pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
545 Self {
546 sketch_block_id: Some(sketch_block_id),
547 ..Default::default()
548 }
549 }
550
551 #[must_use]
552 pub(crate) fn no_freedom_analysis(mut self) -> Self {
553 self.freedom_analysis = false;
554 self
555 }
556}
557
558#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
559#[ts(export)]
560#[serde(rename_all = "camelCase")]
561pub struct DefaultPlanes {
562 pub xy: uuid::Uuid,
563 pub xz: uuid::Uuid,
564 pub yz: uuid::Uuid,
565 pub neg_xy: uuid::Uuid,
566 pub neg_xz: uuid::Uuid,
567 pub neg_yz: uuid::Uuid,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
571#[ts(export)]
572#[serde(tag = "type", rename_all = "camelCase")]
573pub struct TagIdentifier {
574 pub value: String,
575 #[serde(skip)]
578 pub info: Vec<(usize, TagEngineInfo)>,
579 #[serde(skip)]
580 pub meta: Vec<Metadata>,
581}
582
583impl TagIdentifier {
584 pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
586 for (e, info) in self.info.iter().rev() {
587 if *e <= at_epoch {
588 return Some(info);
589 }
590 }
591
592 None
593 }
594
595 pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
597 self.info.last().map(|i| &i.1)
598 }
599
600 pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
603 let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
604 return vec![];
605 };
606 self.info
607 .iter()
608 .rev()
609 .take_while(|(e, _)| *e == cur_epoch)
610 .map(|(_, info)| info)
611 .collect()
612 }
613
614 pub fn merge_info(&mut self, other: &TagIdentifier) {
616 assert_eq!(&self.value, &other.value);
617 for (oe, ot) in &other.info {
618 if let Some((e, t)) = self.info.last_mut() {
619 if *e > *oe {
621 continue;
622 }
623 if e == oe {
625 *t = ot.clone();
626 continue;
627 }
628 }
629 self.info.push((*oe, ot.clone()));
630 }
631 }
632
633 pub fn geometry(&self) -> Option<Geometry> {
634 self.get_cur_info().map(|info| info.geometry.clone())
635 }
636}
637
638impl Eq for TagIdentifier {}
639
640impl std::fmt::Display for TagIdentifier {
641 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642 write!(f, "{}", self.value)
643 }
644}
645
646impl std::str::FromStr for TagIdentifier {
647 type Err = KclError;
648
649 fn from_str(s: &str) -> Result<Self, Self::Err> {
650 Ok(Self {
651 value: s.to_string(),
652 info: Vec::new(),
653 meta: Default::default(),
654 })
655 }
656}
657
658impl Ord for TagIdentifier {
659 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
660 self.value.cmp(&other.value)
661 }
662}
663
664impl PartialOrd for TagIdentifier {
665 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
666 Some(self.cmp(other))
667 }
668}
669
670impl std::hash::Hash for TagIdentifier {
671 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
672 self.value.hash(state);
673 }
674}
675
676#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
678#[ts(export)]
679#[serde(tag = "type", rename_all = "camelCase")]
680pub struct TagEngineInfo {
681 pub id: uuid::Uuid,
683 pub geometry: Geometry,
685 pub path: Option<Path>,
687 pub surface: Option<ExtrudeSurface>,
689}
690
691#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
692pub enum BodyType {
693 Root,
694 Block,
695}
696
697#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
699#[ts(export)]
700#[serde(rename_all = "camelCase")]
701pub struct Metadata {
702 pub source_range: SourceRange,
704}
705
706impl From<Metadata> for Vec<SourceRange> {
707 fn from(meta: Metadata) -> Self {
708 vec![meta.source_range]
709 }
710}
711
712impl From<&Metadata> for SourceRange {
713 fn from(meta: &Metadata) -> Self {
714 meta.source_range
715 }
716}
717
718impl From<SourceRange> for Metadata {
719 fn from(source_range: SourceRange) -> Self {
720 Self { source_range }
721 }
722}
723
724impl<T> From<NodeRef<'_, T>> for Metadata {
725 fn from(node: NodeRef<'_, T>) -> Self {
726 Self {
727 source_range: SourceRange::new(node.start, node.end, node.module_id),
728 }
729 }
730}
731
732impl From<&Expr> for Metadata {
733 fn from(expr: &Expr) -> Self {
734 Self {
735 source_range: SourceRange::from(expr),
736 }
737 }
738}
739
740impl Metadata {
741 pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
742 if meta.len() == 1 {
743 let meta = &meta[0];
744 return crate::front::SourceRef::Simple {
745 range: meta.source_range,
746 node_path,
747 };
748 }
749 crate::front::SourceRef::BackTrace {
750 ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
751 }
752 }
753}
754
755#[derive(PartialEq, Debug, Default, Clone)]
757pub enum ContextType {
758 #[default]
760 Live,
761
762 Mock,
766
767 MockCustomForwarded,
769}
770
771#[derive(Debug, Clone)]
775pub struct ExecutorContext {
776 pub engine: Arc<Box<dyn EngineManager>>,
777 pub engine_batch: EngineBatchContext,
778 pub fs: Arc<FileManager>,
779 pub settings: ExecutorSettings,
780 pub context_type: ContextType,
781}
782
783#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
785#[ts(export)]
786pub struct ExecutorSettings {
787 pub highlight_edges: bool,
789 pub enable_ssao: bool,
791 pub show_grid: bool,
793 pub replay: Option<String>,
796 pub project_directory: Option<TypedPath>,
799 pub current_file: Option<TypedPath>,
802 pub fixed_size_grid: bool,
804 #[serde(default, skip_serializing_if = "is_false")]
810 pub skip_artifact_graph: bool,
811 #[serde(default, skip_serializing_if = "Option::is_none")]
814 pub heartbeats: Option<u64>,
815 #[serde(default, skip_serializing_if = "Option::is_none")]
818 pub default_backface_color: Option<String>,
819}
820
821fn is_false(b: &bool) -> bool {
822 !*b
823}
824
825impl Default for ExecutorSettings {
826 fn default() -> Self {
827 Self {
828 highlight_edges: true,
829 enable_ssao: false,
830 show_grid: false,
831 replay: None,
832 project_directory: None,
833 current_file: None,
834 fixed_size_grid: true,
835 skip_artifact_graph: false,
836 heartbeats: None,
837 default_backface_color: None,
838 }
839 }
840}
841
842impl From<crate::settings::types::Configuration> for ExecutorSettings {
843 fn from(config: crate::settings::types::Configuration) -> Self {
844 Self::from(config.settings)
845 }
846}
847
848impl From<crate::settings::types::Settings> for ExecutorSettings {
849 fn from(settings: crate::settings::types::Settings) -> Self {
850 let modeling_settings = settings.modeling.unwrap_or_default();
851 Self {
852 highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
853 enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
854 show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
855 replay: None,
856 project_directory: None,
857 current_file: None,
858 fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
859 skip_artifact_graph: false,
860 heartbeats: None,
861 default_backface_color: modeling_settings.backface_color.map(|color| color.0),
862 }
863 }
864}
865
866impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
867 fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
868 Self::from(config.settings.modeling)
869 }
870}
871
872impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
873 fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
874 Self {
875 highlight_edges: modeling.highlight_edges.unwrap_or_default().into(),
876 enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
877 show_grid: modeling.show_scale_grid.unwrap_or_default(),
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: modeling.backface_color.map(|color| color.0),
885 }
886 }
887}
888
889impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
890 fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
891 Self {
892 highlight_edges: modeling.highlight_edges.into(),
893 enable_ssao: modeling.enable_ssao.into(),
894 show_grid: Default::default(),
895 replay: None,
896 project_directory: None,
897 current_file: None,
898 fixed_size_grid: true,
899 skip_artifact_graph: false,
900 heartbeats: None,
901 default_backface_color: None,
902 }
903 }
904}
905
906impl ExecutorSettings {
907 pub fn with_current_file(&mut self, current_file: TypedPath) {
909 if current_file.extension() == Some("kcl") {
911 self.current_file = Some(current_file.clone());
912 if let Some(parent) = current_file.parent() {
914 self.project_directory = Some(parent);
915 } else {
916 self.project_directory = Some(TypedPath::from(""));
917 }
918 } else {
919 self.project_directory = Some(current_file);
920 }
921 }
922}
923
924impl ExecutorContext {
925 pub fn new_with_engine_and_fs(
927 engine: Arc<Box<dyn EngineManager>>,
928 fs: Arc<FileManager>,
929 settings: ExecutorSettings,
930 ) -> Self {
931 ExecutorContext {
932 engine,
933 engine_batch: EngineBatchContext::default(),
934 fs,
935 settings,
936 context_type: ContextType::Live,
937 }
938 }
939
940 fn clone_with_fresh_execution_batch(&self) -> Self {
941 Self {
942 engine: self.engine.clone(),
943 engine_batch: EngineBatchContext::new(),
944 fs: self.fs.clone(),
945 settings: self.settings.clone(),
946 context_type: self.context_type.clone(),
947 }
948 }
949
950 #[cfg(not(target_arch = "wasm32"))]
952 pub fn new_with_engine(engine: Arc<Box<dyn EngineManager>>, settings: ExecutorSettings) -> Self {
953 Self::new_with_engine_and_fs(engine, Arc::new(FileManager::new()), settings)
954 }
955
956 #[cfg(not(target_arch = "wasm32"))]
958 pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
959 let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
960 let (ws, _headers) = client
961 .modeling()
962 .commands_ws(kittycad::modeling::CommandsWsParams {
963 api_call_id: None,
964 fps: None,
965 order_independent_transparency: None,
966 post_effect: if settings.enable_ssao {
967 Some(kittycad::types::PostEffectType::Ssao)
968 } else {
969 None
970 },
971 replay: settings.replay.clone(),
972 show_grid: if settings.show_grid { Some(true) } else { None },
973 pool: None,
974 pr,
975 unlocked_framerate: None,
976 webrtc: Some(false),
977 video_res_width: None,
978 video_res_height: None,
979 })
980 .await?;
981
982 let engine: Arc<Box<dyn EngineManager>> = Arc::new(Box::new(
983 crate::engine::conn::EngineConnection::new(ws, settings.heartbeats).await?,
984 ));
985
986 Ok(Self::new_with_engine(engine, settings))
987 }
988
989 #[cfg(target_arch = "wasm32")]
990 pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
991 Self::new_with_engine_and_fs(engine, fs, settings)
992 }
993
994 #[cfg(not(target_arch = "wasm32"))]
995 pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
996 ExecutorContext {
997 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
998 engine_batch: EngineBatchContext::default(),
999 fs: Arc::new(FileManager::new()),
1000 settings: settings.unwrap_or_default(),
1001 context_type: ContextType::Mock,
1002 }
1003 }
1004
1005 #[cfg(target_arch = "wasm32")]
1006 pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
1007 ExecutorContext {
1008 engine,
1009 engine_batch: EngineBatchContext::default(),
1010 fs,
1011 settings,
1012 context_type: ContextType::Mock,
1013 }
1014 }
1015
1016 #[cfg(target_arch = "wasm32")]
1019 pub fn new_mock_for_lsp(
1020 fs_manager: crate::fs::wasm::FileSystemManager,
1021 settings: ExecutorSettings,
1022 ) -> Result<Self, String> {
1023 use crate::mock_engine;
1024
1025 let mock_engine = Arc::new(Box::new(
1026 mock_engine::EngineConnection::new().map_err(|e| format!("Failed to create mock engine: {:?}", e))?,
1027 ) as Box<dyn EngineManager>);
1028
1029 let fs = Arc::new(FileManager::new(fs_manager));
1030
1031 Ok(ExecutorContext {
1032 engine: mock_engine,
1033 engine_batch: EngineBatchContext::default(),
1034 fs,
1035 settings,
1036 context_type: ContextType::Mock,
1037 })
1038 }
1039
1040 #[cfg(not(target_arch = "wasm32"))]
1041 pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
1042 ExecutorContext {
1043 engine,
1044 engine_batch: EngineBatchContext::default(),
1045 fs: Arc::new(FileManager::new()),
1046 settings: Default::default(),
1047 context_type: ContextType::MockCustomForwarded,
1048 }
1049 }
1050
1051 #[cfg(not(target_arch = "wasm32"))]
1057 pub async fn new_with_client(
1058 settings: ExecutorSettings,
1059 token: Option<String>,
1060 engine_addr: Option<String>,
1061 ) -> Result<Self> {
1062 let client = crate::engine::new_zoo_client(token, engine_addr)?;
1064
1065 let ctx = Self::new(&client, settings).await?;
1066 Ok(ctx)
1067 }
1068
1069 #[cfg(not(target_arch = "wasm32"))]
1074 pub async fn new_with_default_client() -> Result<Self> {
1075 let ctx = Self::new_with_client(Default::default(), None, None).await?;
1077 Ok(ctx)
1078 }
1079
1080 #[cfg(not(target_arch = "wasm32"))]
1082 pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
1083 let ctx = ExecutorContext::new_with_client(
1084 ExecutorSettings {
1085 highlight_edges: true,
1086 enable_ssao: false,
1087 show_grid: false,
1088 replay: None,
1089 project_directory: None,
1090 current_file: None,
1091 fixed_size_grid: false,
1092 skip_artifact_graph: false,
1093 heartbeats: None,
1094 default_backface_color: None,
1095 },
1096 None,
1097 engine_addr,
1098 )
1099 .await?;
1100 Ok(ctx)
1101 }
1102
1103 pub fn is_mock(&self) -> bool {
1104 self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
1105 }
1106
1107 pub async fn no_engine_commands(&self) -> bool {
1109 self.is_mock()
1110 }
1111
1112 pub async fn send_clear_scene(
1113 &self,
1114 exec_state: &mut ExecState,
1115 source_range: crate::execution::SourceRange,
1116 ) -> Result<(), KclError> {
1117 exec_state.mod_local.artifacts.clear();
1120 exec_state.global.root_module_artifacts.clear();
1121 exec_state.global.artifacts.clear();
1122
1123 self.engine
1124 .clear_scene(&self.engine_batch, &mut exec_state.mod_local.id_generator, source_range)
1125 .await?;
1126 if self.settings.enable_ssao {
1129 let cmd_id = exec_state.next_uuid();
1130 exec_state
1131 .batch_modeling_cmd(
1132 ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
1133 ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
1134 )
1135 .await?;
1136 }
1137 Ok(())
1138 }
1139
1140 pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
1141 cache::bust_cache().await;
1142
1143 let outcome = self.run_with_caching(crate::Program::empty()).await?;
1148
1149 Ok(outcome)
1150 }
1151
1152 async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
1153 self.eval_prelude(exec_state, SourceRange::synthetic())
1154 .await
1155 .map_err(KclErrorWithOutputs::no_outputs)?;
1156 exec_state.mut_stack().push_new_root_env(true);
1157 Ok(())
1158 }
1159
1160 fn restore_mock_memory(
1161 exec_state: &mut ExecState,
1162 mem: cache::SketchModeState,
1163 _mock_config: &MockConfig,
1164 ) -> Result<(), KclErrorWithOutputs> {
1165 *exec_state.mut_stack() = mem.stack;
1166 exec_state.global.module_infos = mem.module_infos;
1167 exec_state.global.path_to_source_id = mem.path_to_source_id;
1168 exec_state.global.id_to_source = mem.id_to_source;
1169 exec_state.mod_local.constraint_state = mem.constraint_state;
1170 let len = _mock_config
1171 .sketch_block_id
1172 .map(|sketch_block_id| sketch_block_id.0)
1173 .unwrap_or(0);
1174 if let Some(scene_objects) = mem.scene_objects.get(0..len) {
1175 exec_state
1176 .global
1177 .root_module_artifacts
1178 .restore_scene_objects(scene_objects);
1179 } else {
1180 let message = format!(
1181 "Cached scene objects length {} is less than expected length from cached object ID generator {}",
1182 mem.scene_objects.len(),
1183 len
1184 );
1185 debug_assert!(false, "{message}");
1186 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1187 KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
1188 )));
1189 }
1190
1191 Ok(())
1192 }
1193
1194 pub async fn run_mock(
1195 &self,
1196 program: &crate::Program,
1197 mock_config: &MockConfig,
1198 ) -> Result<ExecOutcome, KclErrorWithOutputs> {
1199 assert!(
1200 self.is_mock(),
1201 "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
1202 );
1203
1204 let use_prev_memory = mock_config.use_prev_memory;
1205 let mut exec_state = ExecState::new_mock(self, mock_config);
1206 if use_prev_memory {
1207 match cache::read_old_memory().await {
1208 Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
1209 None => self.prepare_mem(&mut exec_state).await?,
1210 }
1211 } else {
1212 self.prepare_mem(&mut exec_state).await?
1213 };
1214
1215 exec_state.mut_stack().push_new_env_for_scope();
1218
1219 let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
1220
1221 let mut stack = exec_state.stack().clone();
1226 let module_infos = exec_state.global.module_infos.clone();
1227 let path_to_source_id = exec_state.global.path_to_source_id.clone();
1228 let id_to_source = exec_state.global.id_to_source.clone();
1229 let constraint_state = exec_state.mod_local.constraint_state.clone();
1230 let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
1231 let outcome = exec_state.into_exec_outcome(result.0, self).await;
1232
1233 stack.squash_env(result.0);
1234 let state = cache::SketchModeState {
1235 stack,
1236 module_infos,
1237 path_to_source_id,
1238 id_to_source,
1239 constraint_state,
1240 scene_objects,
1241 };
1242 cache::write_old_memory(state).await;
1243
1244 Ok(outcome)
1245 }
1246
1247 pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
1248 assert!(!self.is_mock());
1249 let grid_scale = if self.settings.fixed_size_grid {
1250 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1251 } else {
1252 GridScaleBehavior::ScaleWithZoom
1253 };
1254
1255 let original_program = program.clone();
1256
1257 let (_program, exec_state, result) = match cache::read_old_ast().await {
1258 Some(mut cached_state) => {
1259 let old = CacheInformation {
1260 ast: &cached_state.main.ast,
1261 settings: &cached_state.settings,
1262 };
1263 let new = CacheInformation {
1264 ast: &program.ast,
1265 settings: &self.settings,
1266 };
1267
1268 let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
1270 CacheResult::ReExecute {
1271 clear_scene,
1272 reapply_settings,
1273 program: changed_program,
1274 } => {
1275 if reapply_settings
1276 && self
1277 .engine
1278 .reapply_settings(
1279 &self.engine_batch,
1280 &self.settings,
1281 Default::default(),
1282 &mut cached_state.main.exec_state.id_generator,
1283 grid_scale,
1284 )
1285 .await
1286 .is_err()
1287 {
1288 (true, program, None)
1289 } else {
1290 (
1291 clear_scene,
1292 crate::Program {
1293 ast: changed_program,
1294 original_file_contents: program.original_file_contents,
1295 },
1296 None,
1297 )
1298 }
1299 }
1300 CacheResult::CheckImportsOnly {
1301 reapply_settings,
1302 ast: changed_program,
1303 } => {
1304 let mut reapply_failed = false;
1305 if reapply_settings {
1306 if self
1307 .engine
1308 .reapply_settings(
1309 &self.engine_batch,
1310 &self.settings,
1311 Default::default(),
1312 &mut cached_state.main.exec_state.id_generator,
1313 grid_scale,
1314 )
1315 .await
1316 .is_ok()
1317 {
1318 cache::write_old_ast(GlobalState::with_settings(
1319 cached_state.clone(),
1320 self.settings.clone(),
1321 ))
1322 .await;
1323 } else {
1324 reapply_failed = true;
1325 }
1326 }
1327
1328 if reapply_failed {
1329 (true, program, None)
1330 } else {
1331 let mut new_exec_state = ExecState::new(self);
1333 let (new_universe, new_universe_map) =
1334 self.get_universe(&program, &mut new_exec_state).await?;
1335
1336 let clear_scene = new_universe.values().any(|value| {
1337 let id = value.1;
1338 match (
1339 cached_state.exec_state.get_source(id),
1340 new_exec_state.global.get_source(id),
1341 ) {
1342 (Some(s0), Some(s1)) => s0.source != s1.source,
1343 _ => false,
1344 }
1345 });
1346
1347 if !clear_scene {
1348 cache::write_old_memory(cached_state.mock_memory_state()).await;
1350 return Ok(cached_state.into_exec_outcome(self).await);
1351 }
1352
1353 (
1354 true,
1355 crate::Program {
1356 ast: changed_program,
1357 original_file_contents: program.original_file_contents,
1358 },
1359 Some((new_universe, new_universe_map, new_exec_state)),
1360 )
1361 }
1362 }
1363 CacheResult::NoAction(true) => {
1364 if self
1365 .engine
1366 .reapply_settings(
1367 &self.engine_batch,
1368 &self.settings,
1369 Default::default(),
1370 &mut cached_state.main.exec_state.id_generator,
1371 grid_scale,
1372 )
1373 .await
1374 .is_ok()
1375 {
1376 cache::write_old_ast(GlobalState::with_settings(
1378 cached_state.clone(),
1379 self.settings.clone(),
1380 ))
1381 .await;
1382
1383 cache::write_old_memory(cached_state.mock_memory_state()).await;
1384 return Ok(cached_state.into_exec_outcome(self).await);
1385 }
1386 (true, program, None)
1387 }
1388 CacheResult::NoAction(false) => {
1389 cache::write_old_memory(cached_state.mock_memory_state()).await;
1390 return Ok(cached_state.into_exec_outcome(self).await);
1391 }
1392 };
1393
1394 let (exec_state, result) = match import_check_info {
1395 Some((new_universe, new_universe_map, mut new_exec_state)) => {
1396 self.send_clear_scene(&mut new_exec_state, Default::default())
1398 .await
1399 .map_err(KclErrorWithOutputs::no_outputs)?;
1400
1401 let result = self
1402 .run_concurrent(
1403 &program,
1404 &mut new_exec_state,
1405 Some((new_universe, new_universe_map)),
1406 PreserveMem::Normal,
1407 )
1408 .await;
1409
1410 (new_exec_state, result)
1411 }
1412 None if clear_scene => {
1413 let mut exec_state = cached_state.reconstitute_exec_state();
1415 exec_state.reset(self);
1416
1417 self.send_clear_scene(&mut exec_state, Default::default())
1418 .await
1419 .map_err(KclErrorWithOutputs::no_outputs)?;
1420
1421 let result = self
1422 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1423 .await;
1424
1425 (exec_state, result)
1426 }
1427 None => {
1428 let mut exec_state = cached_state.reconstitute_exec_state();
1429 exec_state.mut_stack().restore_env(cached_state.main.result_env);
1430
1431 let result = self
1432 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1433 .await;
1434
1435 (exec_state, result)
1436 }
1437 };
1438
1439 (program, exec_state, result)
1440 }
1441 None => {
1442 let mut exec_state = ExecState::new(self);
1443 self.send_clear_scene(&mut exec_state, Default::default())
1444 .await
1445 .map_err(KclErrorWithOutputs::no_outputs)?;
1446
1447 let result = self
1448 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1449 .await;
1450
1451 (program, exec_state, result)
1452 }
1453 };
1454
1455 if result.is_err() {
1456 cache::bust_cache().await;
1457 }
1458
1459 let result = result?;
1461
1462 cache::write_old_ast(GlobalState::new(
1466 exec_state.clone(),
1467 self.settings.clone(),
1468 original_program.ast,
1469 result.0,
1470 ))
1471 .await;
1472
1473 let outcome = exec_state.into_exec_outcome(result.0, self).await;
1474 Ok(outcome)
1475 }
1476
1477 pub async fn run(
1481 &self,
1482 program: &crate::Program,
1483 exec_state: &mut ExecState,
1484 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1485 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1486 .await
1487 }
1488
1489 pub async fn run_concurrent(
1494 &self,
1495 program: &crate::Program,
1496 exec_state: &mut ExecState,
1497 universe_info: Option<(Universe, UniverseMap)>,
1498 preserve_mem: PreserveMem,
1499 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1500 let (universe, _universe_map) = if let Some((universe, universe_map)) = universe_info {
1503 (universe, universe_map)
1504 } else {
1505 self.get_universe(program, exec_state).await?
1506 };
1507
1508 let default_planes = self.engine.get_default_planes().read().await.clone();
1509
1510 self.eval_prelude(exec_state, SourceRange::synthetic())
1512 .await
1513 .map_err(KclErrorWithOutputs::no_outputs)?;
1514
1515 for modules in import_graph::import_graph(&universe, self)
1516 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1517 .into_iter()
1518 {
1519 #[cfg(not(target_arch = "wasm32"))]
1520 let mut set = tokio::task::JoinSet::new();
1521
1522 #[allow(clippy::type_complexity)]
1523 let (results_tx, mut results_rx): (
1524 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1525 tokio::sync::mpsc::Receiver<_>,
1526 ) = tokio::sync::mpsc::channel(1);
1527
1528 for module in modules {
1529 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1530 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1531 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1532 )));
1533 };
1534 let module_id = *module_id;
1535 let module_path = module_path.clone();
1536 let source_range = SourceRange::from(import_stmt);
1537 let module_exec_state = exec_state.clone();
1539
1540 let repr = repr.clone();
1541 let exec_ctxt = self.clone_with_fresh_execution_batch();
1542 let results_tx = results_tx.clone();
1543
1544 let exec_module = async |exec_ctxt: &ExecutorContext,
1545 repr: &ModuleRepr,
1546 module_id: ModuleId,
1547 module_path: &ModulePath,
1548 exec_state: &mut ExecState,
1549 source_range: SourceRange|
1550 -> Result<ModuleRepr, KclError> {
1551 match repr {
1552 ModuleRepr::Kcl(program, _) => {
1553 let result = exec_ctxt
1554 .exec_module_from_ast(
1555 program,
1556 module_id,
1557 module_path,
1558 exec_state,
1559 source_range,
1560 PreserveMem::Normal,
1561 )
1562 .await;
1563
1564 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1565 }
1566 ModuleRepr::Foreign(geom, _) => {
1567 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1568 .await
1569 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1570
1571 result.map(|val| {
1572 ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
1573 })
1574 }
1575 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1576 format!("Module {module_path} not found in universe"),
1577 vec![source_range],
1578 ))),
1579 }
1580 };
1581
1582 #[cfg(target_arch = "wasm32")]
1583 {
1584 wasm_bindgen_futures::spawn_local(async move {
1585 let mut exec_state = module_exec_state;
1586 let exec_ctxt = exec_ctxt;
1587
1588 let result = exec_module(
1589 &exec_ctxt,
1590 &repr,
1591 module_id,
1592 &module_path,
1593 &mut exec_state,
1594 source_range,
1595 )
1596 .await;
1597
1598 results_tx
1599 .send((module_id, module_path, result))
1600 .await
1601 .unwrap_or_default();
1602 });
1603 }
1604 #[cfg(not(target_arch = "wasm32"))]
1605 {
1606 set.spawn(async move {
1607 let mut exec_state = module_exec_state;
1608 let exec_ctxt = exec_ctxt;
1609
1610 let result = exec_module(
1611 &exec_ctxt,
1612 &repr,
1613 module_id,
1614 &module_path,
1615 &mut exec_state,
1616 source_range,
1617 )
1618 .await;
1619
1620 results_tx
1621 .send((module_id, module_path, result))
1622 .await
1623 .unwrap_or_default();
1624 });
1625 }
1626 }
1627
1628 drop(results_tx);
1629
1630 while let Some((module_id, _, result)) = results_rx.recv().await {
1631 match result {
1632 Ok(new_repr) => {
1633 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1634
1635 match &mut repr {
1636 ModuleRepr::Kcl(_, cache) => {
1637 let ModuleRepr::Kcl(_, session_data) = new_repr else {
1638 unreachable!();
1639 };
1640 *cache = session_data;
1641 }
1642 ModuleRepr::Foreign(_, cache) => {
1643 let ModuleRepr::Foreign(_, session_data) = new_repr else {
1644 unreachable!();
1645 };
1646 *cache = session_data;
1647 }
1648 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1649 }
1650
1651 exec_state.global.module_infos[&module_id].restore_repr(repr);
1652 }
1653 Err(e) => {
1654 return Err(exec_state.error_with_outputs(e, None, default_planes));
1655 }
1656 }
1657 }
1658 }
1659
1660 exec_state
1663 .global
1664 .root_module_artifacts
1665 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1666
1667 self.inner_run(program, exec_state, preserve_mem).await
1668 }
1669
1670 async fn get_universe(
1673 &self,
1674 program: &crate::Program,
1675 exec_state: &mut ExecState,
1676 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1677 exec_state.add_root_module_contents(program);
1678
1679 let mut universe = std::collections::HashMap::new();
1680
1681 let default_planes = self.engine.get_default_planes().read().await.clone();
1682
1683 let root_imports = import_graph::import_universe(
1684 self,
1685 &ModulePath::Main,
1686 &ModuleRepr::Kcl(program.ast.clone(), None),
1687 &mut universe,
1688 exec_state,
1689 )
1690 .await
1691 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1692
1693 Ok((universe, root_imports))
1694 }
1695
1696 async fn inner_run(
1699 &self,
1700 program: &crate::Program,
1701 exec_state: &mut ExecState,
1702 preserve_mem: PreserveMem,
1703 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1704 let _stats = crate::log::LogPerfStats::new("Interpretation");
1705
1706 let grid_scale = if self.settings.fixed_size_grid {
1708 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1709 } else {
1710 GridScaleBehavior::ScaleWithZoom
1711 };
1712 self.engine
1713 .reapply_settings(
1714 &self.engine_batch,
1715 &self.settings,
1716 Default::default(),
1717 exec_state.id_generator(),
1718 grid_scale,
1719 )
1720 .await
1721 .map_err(KclErrorWithOutputs::no_outputs)?;
1722
1723 let default_planes = self.engine.get_default_planes().read().await.clone();
1724 let result = self
1725 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1726 .await;
1727
1728 crate::log::log(format!(
1729 "Post interpretation KCL memory stats: {:#?}",
1730 exec_state.stack().memory.stats
1731 ));
1732 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1733
1734 async fn write_old_memory(ctx: &ExecutorContext, exec_state: &ExecState, env_ref: EnvironmentRef) {
1737 if ctx.is_mock() {
1738 return;
1739 }
1740 let mut stack = exec_state.stack().deep_clone();
1741 stack.restore_env(env_ref);
1742 let state = cache::SketchModeState {
1743 stack,
1744 module_infos: exec_state.global.module_infos.clone(),
1745 path_to_source_id: exec_state.global.path_to_source_id.clone(),
1746 id_to_source: exec_state.global.id_to_source.clone(),
1747 constraint_state: exec_state.mod_local.constraint_state.clone(),
1748 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1749 };
1750 cache::write_old_memory(state).await;
1751 }
1752
1753 let env_ref = match result {
1754 Ok(env_ref) => env_ref,
1755 Err((err, env_ref)) => {
1756 if let Some(env_ref) = env_ref {
1759 write_old_memory(self, exec_state, env_ref).await;
1760 }
1761 return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
1762 }
1763 };
1764
1765 write_old_memory(self, exec_state, env_ref).await;
1766
1767 let session_data = self.engine.get_session_data().await;
1768
1769 Ok((env_ref, session_data))
1770 }
1771
1772 async fn execute_and_build_graph(
1775 &self,
1776 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1777 exec_state: &mut ExecState,
1778 preserve_mem: PreserveMem,
1779 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1780 let start_op = exec_state.global.root_module_artifacts.operations.len();
1786
1787 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1788 .await
1789 .map_err(|e| (e, None))?;
1790
1791 let exec_result = self
1792 .exec_module_body(
1793 program,
1794 exec_state,
1795 preserve_mem,
1796 ModuleId::default(),
1797 &ModulePath::Main,
1798 )
1799 .await
1800 .map(
1801 |ModuleExecutionOutcome {
1802 environment: env_ref,
1803 artifacts: module_artifacts,
1804 ..
1805 }| {
1806 exec_state.global.root_module_artifacts.extend(module_artifacts);
1809 env_ref
1810 },
1811 )
1812 .map_err(|(err, env_ref, module_artifacts)| {
1813 if let Some(module_artifacts) = module_artifacts {
1814 exec_state.global.root_module_artifacts.extend(module_artifacts);
1817 }
1818 (err, env_ref)
1819 });
1820
1821 let programs = &exec_state.build_program_lookup(program.clone());
1823 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1824 for op in exec_state
1825 .global
1826 .root_module_artifacts
1827 .operations
1828 .iter_mut()
1829 .skip(start_op)
1830 {
1831 op.fill_node_paths(programs, cached_body_items);
1832 }
1833 for module in exec_state.global.module_infos.values_mut() {
1834 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1835 for op in &mut outcome.artifacts.operations {
1836 op.fill_node_paths(programs, cached_body_items);
1837 }
1838 }
1839 }
1840
1841 self.engine
1843 .ensure_async_commands_completed(&self.engine_batch)
1844 .await
1845 .map_err(|e| {
1846 match &exec_result {
1847 Ok(env_ref) => (e, Some(*env_ref)),
1848 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1850 }
1851 })?;
1852
1853 self.engine.clear_queues(&self.engine_batch).await;
1856
1857 match exec_state.build_artifact_graph(&self.engine, program).await {
1858 Ok(_) => exec_result,
1859 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1860 }
1861 }
1862
1863 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1867 if exec_state.stack().memory.requires_std() {
1868 let initial_ops = exec_state.mod_local.artifacts.operations.len();
1869
1870 let path = vec!["std".to_owned(), "prelude".to_owned()];
1871 let resolved_path = ModulePath::from_std_import_path(&path)?;
1872 let id = self
1873 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1874 .await?;
1875 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1876
1877 exec_state.mut_stack().memory.set_std(module_memory);
1878
1879 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1885 }
1886
1887 Ok(())
1888 }
1889
1890 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1892 self.engine
1894 .send_modeling_cmd(
1895 &self.engine_batch,
1896 uuid::Uuid::new_v4(),
1897 crate::execution::SourceRange::default(),
1898 &ModelingCmd::from(
1899 mcmd::ZoomToFit::builder()
1900 .object_ids(Default::default())
1901 .animated(false)
1902 .padding(0.1)
1903 .build(),
1904 ),
1905 )
1906 .await
1907 .map_err(KclErrorWithOutputs::no_outputs)?;
1908
1909 let resp = self
1911 .engine
1912 .send_modeling_cmd(
1913 &self.engine_batch,
1914 uuid::Uuid::new_v4(),
1915 crate::execution::SourceRange::default(),
1916 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
1917 )
1918 .await
1919 .map_err(KclErrorWithOutputs::no_outputs)?;
1920
1921 let OkWebSocketResponseData::Modeling {
1922 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1923 } = resp
1924 else {
1925 return Err(ExecError::BadPng(format!(
1926 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1927 )));
1928 };
1929 Ok(contents)
1930 }
1931
1932 pub async fn export(
1934 &self,
1935 format: kittycad_modeling_cmds::format::OutputFormat3d,
1936 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1937 let resp = self
1938 .engine
1939 .send_modeling_cmd(
1940 &self.engine_batch,
1941 uuid::Uuid::new_v4(),
1942 crate::SourceRange::default(),
1943 &kittycad_modeling_cmds::ModelingCmd::Export(
1944 kittycad_modeling_cmds::Export::builder()
1945 .entity_ids(vec![])
1946 .format(format)
1947 .build(),
1948 ),
1949 )
1950 .await?;
1951
1952 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1953 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1954 format!("Expected Export response, got {resp:?}",),
1955 vec![SourceRange::default()],
1956 )));
1957 };
1958
1959 Ok(files)
1960 }
1961
1962 pub async fn export_step(
1964 &self,
1965 deterministic_time: bool,
1966 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1967 let files = self
1968 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1969 kittycad_modeling_cmds::format::step::export::Options::builder()
1970 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
1971 .maybe_created(if deterministic_time {
1972 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1973 KclError::new_internal(crate::errors::KclErrorDetails::new(
1974 format!("Failed to parse date: {e}"),
1975 vec![SourceRange::default()],
1976 ))
1977 })?)
1978 } else {
1979 None
1980 })
1981 .build(),
1982 ))
1983 .await?;
1984
1985 Ok(files)
1986 }
1987
1988 pub async fn close(&self) {
1989 self.engine.close().await;
1990 }
1991}
1992
1993#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1994pub struct ArtifactId(Uuid);
1995
1996impl ArtifactId {
1997 pub fn new(uuid: Uuid) -> Self {
1998 Self(uuid)
1999 }
2000
2001 pub fn placeholder() -> Self {
2003 Self(Uuid::nil())
2004 }
2005}
2006
2007impl From<Uuid> for ArtifactId {
2008 fn from(uuid: Uuid) -> Self {
2009 Self::new(uuid)
2010 }
2011}
2012
2013impl From<&Uuid> for ArtifactId {
2014 fn from(uuid: &Uuid) -> Self {
2015 Self::new(*uuid)
2016 }
2017}
2018
2019impl From<ArtifactId> for Uuid {
2020 fn from(id: ArtifactId) -> Self {
2021 id.0
2022 }
2023}
2024
2025impl From<&ArtifactId> for Uuid {
2026 fn from(id: &ArtifactId) -> Self {
2027 id.0
2028 }
2029}
2030
2031impl From<ModelingCmdId> for ArtifactId {
2032 fn from(id: ModelingCmdId) -> Self {
2033 Self::new(*id.as_ref())
2034 }
2035}
2036
2037impl From<&ModelingCmdId> for ArtifactId {
2038 fn from(id: &ModelingCmdId) -> Self {
2039 Self::new(*id.as_ref())
2040 }
2041}
2042
2043#[cfg(test)]
2044pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2045 parse_execute_with_project_dir(code, None).await
2046}
2047
2048#[cfg(test)]
2049pub(crate) async fn parse_execute_with_project_dir(
2050 code: &str,
2051 project_directory: Option<TypedPath>,
2052) -> Result<ExecTestResults, KclError> {
2053 let program = crate::Program::parse_no_errs(code)?;
2054
2055 let exec_ctxt = ExecutorContext {
2056 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
2057 |err| {
2058 KclError::new_internal(crate::errors::KclErrorDetails::new(
2059 format!("Failed to create mock engine connection: {err}"),
2060 vec![SourceRange::default()],
2061 ))
2062 },
2063 )?)),
2064 engine_batch: EngineBatchContext::default(),
2065 fs: Arc::new(crate::fs::FileManager::new()),
2066 settings: ExecutorSettings {
2067 project_directory,
2068 ..Default::default()
2069 },
2070 context_type: ContextType::Mock,
2071 };
2072 let mut exec_state = ExecState::new(&exec_ctxt);
2073 let result = exec_ctxt.run(&program, &mut exec_state).await?;
2074
2075 Ok(ExecTestResults {
2076 program,
2077 mem_env: result.0,
2078 exec_ctxt,
2079 exec_state,
2080 })
2081}
2082
2083#[cfg(test)]
2084#[derive(Debug)]
2085pub(crate) struct ExecTestResults {
2086 program: crate::Program,
2087 mem_env: EnvironmentRef,
2088 exec_ctxt: ExecutorContext,
2089 exec_state: ExecState,
2090}
2091
2092#[cfg(test)]
2093impl ExecTestResults {
2094 pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2095 &self.exec_state.global.root_module_artifacts.commands
2096 }
2097}
2098
2099pub struct ProgramLookup {
2103 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2104}
2105
2106impl ProgramLookup {
2107 pub fn new(
2110 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2111 module_infos: state::ModuleInfoMap,
2112 ) -> Self {
2113 let mut programs = IndexMap::with_capacity(module_infos.len());
2114 for (id, info) in module_infos {
2115 if let ModuleRepr::Kcl(program, _) = info.repr {
2116 programs.insert(id, program);
2117 }
2118 }
2119 programs.insert(ModuleId::default(), current);
2120 Self { programs }
2121 }
2122
2123 pub fn program_for_module(
2124 &self,
2125 module_id: ModuleId,
2126 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2127 self.programs.get(&module_id)
2128 }
2129}
2130
2131#[cfg(test)]
2132mod tests {
2133 use pretty_assertions::assert_eq;
2134
2135 use super::*;
2136 use crate::ModuleId;
2137 use crate::errors::KclErrorDetails;
2138 use crate::errors::Severity;
2139 use crate::exec::NumericType;
2140 use crate::execution::memory::Stack;
2141 use crate::execution::types::RuntimeType;
2142
2143 #[track_caller]
2145 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2146 memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
2147 }
2148
2149 #[tokio::test(flavor = "multi_thread")]
2150 async fn test_execute_warn() {
2151 let text = "@blah";
2152 let result = parse_execute(text).await.unwrap();
2153 let errs = result.exec_state.issues();
2154 assert_eq!(errs.len(), 1);
2155 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2156 assert!(
2157 errs[0].message.contains("Unknown annotation"),
2158 "unexpected warning message: {}",
2159 errs[0].message
2160 );
2161 }
2162
2163 #[tokio::test(flavor = "multi_thread")]
2164 async fn test_execute_fn_definitions() {
2165 let ast = r#"fn def(@x) {
2166 return x
2167}
2168fn ghi(@x) {
2169 return x
2170}
2171fn jkl(@x) {
2172 return x
2173}
2174fn hmm(@x) {
2175 return x
2176}
2177
2178yo = 5 + 6
2179
2180abc = 3
2181identifierGuy = 5
2182part001 = startSketchOn(XY)
2183|> startProfile(at = [-1.2, 4.83])
2184|> line(end = [2.8, 0])
2185|> angledLine(angle = 100 + 100, length = 3.01)
2186|> angledLine(angle = abc, length = 3.02)
2187|> angledLine(angle = def(yo), length = 3.03)
2188|> angledLine(angle = ghi(2), length = 3.04)
2189|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2190|> close()
2191yo2 = hmm([identifierGuy + 5])"#;
2192
2193 parse_execute(ast).await.unwrap();
2194 }
2195
2196 #[tokio::test(flavor = "multi_thread")]
2197 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2198 let code = r#"
2199firstProfile = sketch(on = XY) {
2200 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2201 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2202 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2203 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2204 coincident([edge1.end, edge2.start])
2205 coincident([edge2.end, edge3.start])
2206 coincident([edge3.end, edge4.start])
2207 coincident([edge4.end, edge1.start])
2208}
2209
2210secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2211 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2212 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2213 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2214 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2215 coincident([edge5.end, edge6.start])
2216 coincident([edge6.end, edge7.start])
2217 coincident([edge7.end, edge8.start])
2218 coincident([edge8.end, edge5.start])
2219}
2220
2221firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2222secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2223"#;
2224
2225 let result = parse_execute(code).await.unwrap();
2226 assert!(result.exec_state.issues().is_empty());
2227 }
2228
2229 #[tokio::test(flavor = "multi_thread")]
2230 async fn sketch_block_artifact_preserves_standard_plane_name() {
2231 let code = r#"
2232sketch001 = sketch(on = -YZ) {
2233 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
2234}
2235"#;
2236
2237 let result = parse_execute(code).await.unwrap();
2238 let sketch_blocks = result
2239 .exec_state
2240 .global
2241 .artifacts
2242 .graph
2243 .values()
2244 .filter_map(|artifact| match artifact {
2245 Artifact::SketchBlock(block) => Some(block),
2246 _ => None,
2247 })
2248 .collect::<Vec<_>>();
2249
2250 assert_eq!(sketch_blocks.len(), 1);
2251 assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
2252 }
2253
2254 #[tokio::test(flavor = "multi_thread")]
2255 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2256 let code = r#"
2257sketch001 = sketch(on = YZ) {
2258 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2259 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2260 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2261 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2262 coincident([line1.end, line2.start])
2263 coincident([line2.end, line3.start])
2264 coincident([line3.end, line4.start])
2265 coincident([line4.end, line1.start])
2266}
2267
2268sketch002 = sketch(on = -XZ) {
2269 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2270 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2271 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2272 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2273 coincident([line5.end, line6.start])
2274 coincident([line6.end, line7.start])
2275 coincident([line7.end, line8.start])
2276 coincident([line8.end, line5.start])
2277}
2278
2279region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2280extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2281region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2282extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2283
2284myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2285"#;
2286
2287 let result = parse_execute(code).await.unwrap();
2288 assert!(result.exec_state.issues().is_empty());
2289 }
2290
2291 #[tokio::test(flavor = "multi_thread")]
2292 async fn issue_10741_point_circle_coincident_executes() {
2293 let code = r#"
2294sketch001 = sketch(on = YZ) {
2295 circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
2296 line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
2297 coincident([line1.start, circle1])
2298}
2299"#;
2300
2301 let result = parse_execute(code).await.unwrap();
2302 assert!(
2303 result
2304 .exec_state
2305 .issues()
2306 .iter()
2307 .all(|issue| issue.severity != Severity::Error),
2308 "unexpected execution issues: {:#?}",
2309 result.exec_state.issues()
2310 );
2311 }
2312
2313 #[tokio::test(flavor = "multi_thread")]
2314 async fn test_execute_with_pipe_substitutions_unary() {
2315 let ast = r#"myVar = 3
2316part001 = startSketchOn(XY)
2317 |> startProfile(at = [0, 0])
2318 |> line(end = [3, 4], tag = $seg01)
2319 |> line(end = [
2320 min([segLen(seg01), myVar]),
2321 -legLen(hypotenuse = segLen(seg01), leg = myVar)
2322])
2323"#;
2324
2325 parse_execute(ast).await.unwrap();
2326 }
2327
2328 #[tokio::test(flavor = "multi_thread")]
2329 async fn test_execute_with_pipe_substitutions() {
2330 let ast = r#"myVar = 3
2331part001 = startSketchOn(XY)
2332 |> startProfile(at = [0, 0])
2333 |> line(end = [3, 4], tag = $seg01)
2334 |> line(end = [
2335 min([segLen(seg01), myVar]),
2336 legLen(hypotenuse = segLen(seg01), leg = myVar)
2337])
2338"#;
2339
2340 parse_execute(ast).await.unwrap();
2341 }
2342
2343 #[tokio::test(flavor = "multi_thread")]
2344 async fn test_execute_with_inline_comment() {
2345 let ast = r#"baseThick = 1
2346armAngle = 60
2347
2348baseThickHalf = baseThick / 2
2349halfArmAngle = armAngle / 2
2350
2351arrExpShouldNotBeIncluded = [1, 2, 3]
2352objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2353
2354part001 = startSketchOn(XY)
2355 |> startProfile(at = [0, 0])
2356 |> yLine(endAbsolute = 1)
2357 |> xLine(length = 3.84) // selection-range-7ish-before-this
2358
2359variableBelowShouldNotBeIncluded = 3
2360"#;
2361
2362 parse_execute(ast).await.unwrap();
2363 }
2364
2365 #[tokio::test(flavor = "multi_thread")]
2366 async fn test_execute_with_function_literal_in_pipe() {
2367 let ast = r#"w = 20
2368l = 8
2369h = 10
2370
2371fn thing() {
2372 return -8
2373}
2374
2375firstExtrude = startSketchOn(XY)
2376 |> startProfile(at = [0,0])
2377 |> line(end = [0, l])
2378 |> line(end = [w, 0])
2379 |> line(end = [0, thing()])
2380 |> close()
2381 |> extrude(length = h)"#;
2382
2383 parse_execute(ast).await.unwrap();
2384 }
2385
2386 #[tokio::test(flavor = "multi_thread")]
2387 async fn test_execute_with_function_unary_in_pipe() {
2388 let ast = r#"w = 20
2389l = 8
2390h = 10
2391
2392fn thing(@x) {
2393 return -x
2394}
2395
2396firstExtrude = startSketchOn(XY)
2397 |> startProfile(at = [0,0])
2398 |> line(end = [0, l])
2399 |> line(end = [w, 0])
2400 |> line(end = [0, thing(8)])
2401 |> close()
2402 |> extrude(length = h)"#;
2403
2404 parse_execute(ast).await.unwrap();
2405 }
2406
2407 #[tokio::test(flavor = "multi_thread")]
2408 async fn test_execute_with_function_array_in_pipe() {
2409 let ast = r#"w = 20
2410l = 8
2411h = 10
2412
2413fn thing(@x) {
2414 return [0, -x]
2415}
2416
2417firstExtrude = startSketchOn(XY)
2418 |> startProfile(at = [0,0])
2419 |> line(end = [0, l])
2420 |> line(end = [w, 0])
2421 |> line(end = thing(8))
2422 |> close()
2423 |> extrude(length = h)"#;
2424
2425 parse_execute(ast).await.unwrap();
2426 }
2427
2428 #[tokio::test(flavor = "multi_thread")]
2429 async fn test_execute_with_function_call_in_pipe() {
2430 let ast = r#"w = 20
2431l = 8
2432h = 10
2433
2434fn other_thing(@y) {
2435 return -y
2436}
2437
2438fn thing(@x) {
2439 return other_thing(x)
2440}
2441
2442firstExtrude = startSketchOn(XY)
2443 |> startProfile(at = [0,0])
2444 |> line(end = [0, l])
2445 |> line(end = [w, 0])
2446 |> line(end = [0, thing(8)])
2447 |> close()
2448 |> extrude(length = h)"#;
2449
2450 parse_execute(ast).await.unwrap();
2451 }
2452
2453 #[tokio::test(flavor = "multi_thread")]
2454 async fn test_execute_with_function_sketch() {
2455 let ast = r#"fn box(h, l, w) {
2456 myBox = startSketchOn(XY)
2457 |> startProfile(at = [0,0])
2458 |> line(end = [0, l])
2459 |> line(end = [w, 0])
2460 |> line(end = [0, -l])
2461 |> close()
2462 |> extrude(length = h)
2463
2464 return myBox
2465}
2466
2467fnBox = box(h = 3, l = 6, w = 10)"#;
2468
2469 parse_execute(ast).await.unwrap();
2470 }
2471
2472 #[tokio::test(flavor = "multi_thread")]
2473 async fn test_get_member_of_object_with_function_period() {
2474 let ast = r#"fn box(@obj) {
2475 myBox = startSketchOn(XY)
2476 |> startProfile(at = obj.start)
2477 |> line(end = [0, obj.l])
2478 |> line(end = [obj.w, 0])
2479 |> line(end = [0, -obj.l])
2480 |> close()
2481 |> extrude(length = obj.h)
2482
2483 return myBox
2484}
2485
2486thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2487"#;
2488 parse_execute(ast).await.unwrap();
2489 }
2490
2491 #[tokio::test(flavor = "multi_thread")]
2492 #[ignore] async fn test_object_member_starting_pipeline() {
2494 let ast = r#"
2495fn test2() {
2496 return {
2497 thing: startSketchOn(XY)
2498 |> startProfile(at = [0, 0])
2499 |> line(end = [0, 1])
2500 |> line(end = [1, 0])
2501 |> line(end = [0, -1])
2502 |> close()
2503 }
2504}
2505
2506x2 = test2()
2507
2508x2.thing
2509 |> extrude(length = 10)
2510"#;
2511 parse_execute(ast).await.unwrap();
2512 }
2513
2514 #[tokio::test(flavor = "multi_thread")]
2515 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
2517 let ast = r#"fn box(obj) {
2518let myBox = startSketchOn(XY)
2519 |> startProfile(at = obj.start)
2520 |> line(end = [0, obj.l])
2521 |> line(end = [obj.w, 0])
2522 |> line(end = [0, -obj.l])
2523 |> close()
2524 |> extrude(length = obj.h)
2525
2526 return myBox
2527}
2528
2529for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2530 thisBox = box(var)
2531}"#;
2532
2533 parse_execute(ast).await.unwrap();
2534 }
2535
2536 #[tokio::test(flavor = "multi_thread")]
2537 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
2539 let ast = r#"fn box(h, l, w, start) {
2540 myBox = startSketchOn(XY)
2541 |> startProfile(at = [0,0])
2542 |> line(end = [0, l])
2543 |> line(end = [w, 0])
2544 |> line(end = [0, -l])
2545 |> close()
2546 |> extrude(length = h)
2547
2548 return myBox
2549}
2550
2551
2552for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2553 const thisBox = box(var[0], var[1], var[2], var[3])
2554}"#;
2555
2556 parse_execute(ast).await.unwrap();
2557 }
2558
2559 #[tokio::test(flavor = "multi_thread")]
2560 async fn test_get_member_of_array_with_function() {
2561 let ast = r#"fn box(@arr) {
2562 myBox =startSketchOn(XY)
2563 |> startProfile(at = arr[0])
2564 |> line(end = [0, arr[1]])
2565 |> line(end = [arr[2], 0])
2566 |> line(end = [0, -arr[1]])
2567 |> close()
2568 |> extrude(length = arr[3])
2569
2570 return myBox
2571}
2572
2573thisBox = box([[0,0], 6, 10, 3])
2574
2575"#;
2576 parse_execute(ast).await.unwrap();
2577 }
2578
2579 #[tokio::test(flavor = "multi_thread")]
2580 async fn test_function_cannot_access_future_definitions() {
2581 let ast = r#"
2582fn returnX() {
2583 // x shouldn't be defined yet.
2584 return x
2585}
2586
2587x = 5
2588
2589answer = returnX()"#;
2590
2591 let result = parse_execute(ast).await;
2592 let err = result.unwrap_err();
2593 assert_eq!(err.message(), "`x` is not defined");
2594 }
2595
2596 #[tokio::test(flavor = "multi_thread")]
2597 async fn test_override_prelude() {
2598 let text = "PI = 3.0";
2599 let result = parse_execute(text).await.unwrap();
2600 let issues = result.exec_state.issues();
2601 assert!(issues.is_empty(), "issues={issues:#?}");
2602 }
2603
2604 #[tokio::test(flavor = "multi_thread")]
2605 async fn type_aliases() {
2606 let text = r#"@settings(experimentalFeatures = allow)
2607type MyTy = [number; 2]
2608fn foo(@x: MyTy) {
2609 return x[0]
2610}
2611
2612foo([0, 1])
2613
2614type Other = MyTy | Helix
2615"#;
2616 let result = parse_execute(text).await.unwrap();
2617 let issues = result.exec_state.issues();
2618 assert!(issues.is_empty(), "issues={issues:#?}");
2619 }
2620
2621 #[tokio::test(flavor = "multi_thread")]
2622 async fn test_cannot_shebang_in_fn() {
2623 let ast = r#"
2624fn foo() {
2625 #!hello
2626 return true
2627}
2628
2629foo
2630"#;
2631
2632 let result = parse_execute(ast).await;
2633 let err = result.unwrap_err();
2634 assert_eq!(
2635 err,
2636 KclError::new_syntax(KclErrorDetails::new(
2637 "Unexpected token: #".to_owned(),
2638 vec![SourceRange::new(14, 15, ModuleId::default())],
2639 )),
2640 );
2641 }
2642
2643 #[tokio::test(flavor = "multi_thread")]
2644 async fn test_pattern_transform_function_cannot_access_future_definitions() {
2645 let ast = r#"
2646fn transform(@replicaId) {
2647 // x shouldn't be defined yet.
2648 scale = x
2649 return {
2650 translate = [0, 0, replicaId * 10],
2651 scale = [scale, 1, 0],
2652 }
2653}
2654
2655fn layer() {
2656 return startSketchOn(XY)
2657 |> circle( center= [0, 0], radius= 1, tag = $tag1)
2658 |> extrude(length = 10)
2659}
2660
2661x = 5
2662
2663// The 10 layers are replicas of each other, with a transform applied to each.
2664shape = layer() |> patternTransform(instances = 10, transform = transform)
2665"#;
2666
2667 let result = parse_execute(ast).await;
2668 let err = result.unwrap_err();
2669 assert_eq!(err.message(), "`x` is not defined",);
2670 }
2671
2672 #[tokio::test(flavor = "multi_thread")]
2675 async fn test_math_execute_with_functions() {
2676 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
2677 let result = parse_execute(ast).await.unwrap();
2678 assert_eq!(
2679 5.0,
2680 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2681 .as_f64()
2682 .unwrap()
2683 );
2684 }
2685
2686 #[tokio::test(flavor = "multi_thread")]
2687 async fn test_math_execute() {
2688 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
2689 let result = parse_execute(ast).await.unwrap();
2690 assert_eq!(
2691 7.4,
2692 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2693 .as_f64()
2694 .unwrap()
2695 );
2696 }
2697
2698 #[tokio::test(flavor = "multi_thread")]
2699 async fn test_math_execute_start_negative() {
2700 let ast = r#"myVar = -5 + 6"#;
2701 let result = parse_execute(ast).await.unwrap();
2702 assert_eq!(
2703 1.0,
2704 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2705 .as_f64()
2706 .unwrap()
2707 );
2708 }
2709
2710 #[tokio::test(flavor = "multi_thread")]
2711 async fn test_math_execute_with_pi() {
2712 let ast = r#"myVar = PI * 2"#;
2713 let result = parse_execute(ast).await.unwrap();
2714 assert_eq!(
2715 std::f64::consts::TAU,
2716 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2717 .as_f64()
2718 .unwrap()
2719 );
2720 }
2721
2722 #[tokio::test(flavor = "multi_thread")]
2723 async fn test_math_define_decimal_without_leading_zero() {
2724 let ast = r#"thing = .4 + 7"#;
2725 let result = parse_execute(ast).await.unwrap();
2726 assert_eq!(
2727 7.4,
2728 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
2729 .as_f64()
2730 .unwrap()
2731 );
2732 }
2733
2734 #[tokio::test(flavor = "multi_thread")]
2735 async fn pass_std_to_std() {
2736 let ast = r#"sketch001 = startSketchOn(XY)
2737profile001 = circle(sketch001, center = [0, 0], radius = 2)
2738extrude001 = extrude(profile001, length = 5)
2739extrudes = patternLinear3d(
2740 extrude001,
2741 instances = 3,
2742 distance = 5,
2743 axis = [1, 1, 0],
2744)
2745clone001 = map(extrudes, f = clone)
2746"#;
2747 parse_execute(ast).await.unwrap();
2748 }
2749
2750 #[tokio::test(flavor = "multi_thread")]
2751 async fn test_array_reduce_nested_array() {
2752 let code = r#"
2753fn id(@el, accum) { return accum }
2754
2755answer = reduce([], initial=[[[0,0]]], f=id)
2756"#;
2757 let result = parse_execute(code).await.unwrap();
2758 assert_eq!(
2759 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2760 KclValue::HomArray {
2761 value: vec![KclValue::HomArray {
2762 value: vec![KclValue::HomArray {
2763 value: vec![
2764 KclValue::Number {
2765 value: 0.0,
2766 ty: NumericType::default(),
2767 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2768 },
2769 KclValue::Number {
2770 value: 0.0,
2771 ty: NumericType::default(),
2772 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2773 }
2774 ],
2775 ty: RuntimeType::any(),
2776 }],
2777 ty: RuntimeType::any(),
2778 }],
2779 ty: RuntimeType::any(),
2780 }
2781 );
2782 }
2783
2784 #[tokio::test(flavor = "multi_thread")]
2785 async fn test_zero_param_fn() {
2786 let ast = r#"sigmaAllow = 35000 // psi
2787leg1 = 5 // inches
2788leg2 = 8 // inches
2789fn thickness() { return 0.56 }
2790
2791bracket = startSketchOn(XY)
2792 |> startProfile(at = [0,0])
2793 |> line(end = [0, leg1])
2794 |> line(end = [leg2, 0])
2795 |> line(end = [0, -thickness()])
2796 |> line(end = [-leg2 + thickness(), 0])
2797"#;
2798 parse_execute(ast).await.unwrap();
2799 }
2800
2801 #[tokio::test(flavor = "multi_thread")]
2802 async fn test_unary_operator_not_succeeds() {
2803 let ast = r#"
2804fn returnTrue() { return !false }
2805t = true
2806f = false
2807notTrue = !t
2808notFalse = !f
2809c = !!true
2810d = !returnTrue()
2811
2812assertIs(!false, error = "expected to pass")
2813
2814fn check(x) {
2815 assertIs(!x, error = "expected argument to be false")
2816 return true
2817}
2818check(x = false)
2819"#;
2820 let result = parse_execute(ast).await.unwrap();
2821 assert_eq!(
2822 false,
2823 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2824 .as_bool()
2825 .unwrap()
2826 );
2827 assert_eq!(
2828 true,
2829 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2830 .as_bool()
2831 .unwrap()
2832 );
2833 assert_eq!(
2834 true,
2835 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2836 .as_bool()
2837 .unwrap()
2838 );
2839 assert_eq!(
2840 false,
2841 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2842 .as_bool()
2843 .unwrap()
2844 );
2845 }
2846
2847 #[tokio::test(flavor = "multi_thread")]
2848 async fn test_unary_operator_not_on_non_bool_fails() {
2849 let code1 = r#"
2850// Yup, this is null.
2851myNull = 0 / 0
2852notNull = !myNull
2853"#;
2854 assert_eq!(
2855 parse_execute(code1).await.unwrap_err().message(),
2856 "Cannot apply unary operator ! to non-boolean value: a number",
2857 );
2858
2859 let code2 = "notZero = !0";
2860 assert_eq!(
2861 parse_execute(code2).await.unwrap_err().message(),
2862 "Cannot apply unary operator ! to non-boolean value: a number",
2863 );
2864
2865 let code3 = r#"
2866notEmptyString = !""
2867"#;
2868 assert_eq!(
2869 parse_execute(code3).await.unwrap_err().message(),
2870 "Cannot apply unary operator ! to non-boolean value: a string",
2871 );
2872
2873 let code4 = r#"
2874obj = { a = 1 }
2875notMember = !obj.a
2876"#;
2877 assert_eq!(
2878 parse_execute(code4).await.unwrap_err().message(),
2879 "Cannot apply unary operator ! to non-boolean value: a number",
2880 );
2881
2882 let code5 = "
2883a = []
2884notArray = !a";
2885 assert_eq!(
2886 parse_execute(code5).await.unwrap_err().message(),
2887 "Cannot apply unary operator ! to non-boolean value: an empty array",
2888 );
2889
2890 let code6 = "
2891x = {}
2892notObject = !x";
2893 assert_eq!(
2894 parse_execute(code6).await.unwrap_err().message(),
2895 "Cannot apply unary operator ! to non-boolean value: an object",
2896 );
2897
2898 let code7 = "
2899fn x() { return 1 }
2900notFunction = !x";
2901 let fn_err = parse_execute(code7).await.unwrap_err();
2902 assert!(
2905 fn_err
2906 .message()
2907 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2908 "Actual error: {fn_err:?}"
2909 );
2910
2911 let code8 = "
2912myTagDeclarator = $myTag
2913notTagDeclarator = !myTagDeclarator";
2914 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2915 assert!(
2918 tag_declarator_err
2919 .message()
2920 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2921 "Actual error: {tag_declarator_err:?}"
2922 );
2923
2924 let code9 = "
2925myTagDeclarator = $myTag
2926notTagIdentifier = !myTag";
2927 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2928 assert!(
2931 tag_identifier_err
2932 .message()
2933 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2934 "Actual error: {tag_identifier_err:?}"
2935 );
2936
2937 let code10 = "notPipe = !(1 |> 2)";
2938 assert_eq!(
2939 parse_execute(code10).await.unwrap_err(),
2942 KclError::new_syntax(KclErrorDetails::new(
2943 "Unexpected token: !".to_owned(),
2944 vec![SourceRange::new(10, 11, ModuleId::default())],
2945 ))
2946 );
2947
2948 let code11 = "
2949fn identity(x) { return x }
2950notPipeSub = 1 |> identity(!%))";
2951 assert_eq!(
2952 parse_execute(code11).await.unwrap_err(),
2955 KclError::new_syntax(KclErrorDetails::new(
2956 "There was an unexpected `!`. Try removing it.".to_owned(),
2957 vec![SourceRange::new(56, 57, ModuleId::default())],
2958 ))
2959 );
2960
2961 }
2965
2966 #[tokio::test(flavor = "multi_thread")]
2967 async fn test_start_sketch_on_invalid_kwargs() {
2968 let current_dir = std::env::current_dir().unwrap();
2969 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2970 let mut code = std::fs::read_to_string(&path).unwrap();
2971 assert_eq!(
2972 parse_execute(&code).await.unwrap_err().message(),
2973 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2974 );
2975
2976 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2977 code = std::fs::read_to_string(&path).unwrap();
2978
2979 assert_eq!(
2980 parse_execute(&code).await.unwrap_err().message(),
2981 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2982 );
2983
2984 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2985 code = std::fs::read_to_string(&path).unwrap();
2986
2987 assert_eq!(
2988 parse_execute(&code).await.unwrap_err().message(),
2989 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2990 );
2991
2992 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2993 code = std::fs::read_to_string(&path).unwrap();
2994
2995 assert_eq!(
2996 parse_execute(&code).await.unwrap_err().message(),
2997 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2998 );
2999
3000 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
3001 code = std::fs::read_to_string(&path).unwrap();
3002
3003 assert_eq!(
3004 parse_execute(&code).await.unwrap_err().message(),
3005 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
3006 );
3007 }
3008
3009 #[tokio::test(flavor = "multi_thread")]
3010 async fn test_math_negative_variable_in_binary_expression() {
3011 let ast = r#"sigmaAllow = 35000 // psi
3012width = 1 // inch
3013
3014p = 150 // lbs
3015distance = 6 // inches
3016FOS = 2
3017
3018leg1 = 5 // inches
3019leg2 = 8 // inches
3020
3021thickness_squared = distance * p * FOS * 6 / sigmaAllow
3022thickness = 0.56 // inches. App does not support square root function yet
3023
3024bracket = startSketchOn(XY)
3025 |> startProfile(at = [0,0])
3026 |> line(end = [0, leg1])
3027 |> line(end = [leg2, 0])
3028 |> line(end = [0, -thickness])
3029 |> line(end = [-leg2 + thickness, 0])
3030"#;
3031 parse_execute(ast).await.unwrap();
3032 }
3033
3034 #[tokio::test(flavor = "multi_thread")]
3035 async fn test_execute_function_no_return() {
3036 let ast = r#"fn test(@origin) {
3037 origin
3038}
3039
3040test([0, 0])
3041"#;
3042 let result = parse_execute(ast).await;
3043 assert!(result.is_err());
3044 assert!(result.unwrap_err().to_string().contains("undefined"));
3045 }
3046
3047 #[tokio::test(flavor = "multi_thread")]
3048 async fn test_max_stack_size_exceeded_error() {
3049 let ast = r#"
3050fn forever(@n) {
3051 return 1 + forever(n)
3052}
3053
3054forever(1)
3055"#;
3056 let result = parse_execute(ast).await;
3057 let err = result.unwrap_err();
3058 assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
3059 }
3060
3061 #[tokio::test(flavor = "multi_thread")]
3062 async fn test_math_doubly_nested_parens() {
3063 let ast = r#"sigmaAllow = 35000 // psi
3064width = 4 // inch
3065p = 150 // Force on shelf - lbs
3066distance = 6 // inches
3067FOS = 2
3068leg1 = 5 // inches
3069leg2 = 8 // inches
3070thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
3071thickness = 0.32 // inches. App does not support square root function yet
3072bracket = startSketchOn(XY)
3073 |> startProfile(at = [0,0])
3074 |> line(end = [0, leg1])
3075 |> line(end = [leg2, 0])
3076 |> line(end = [0, -thickness])
3077 |> line(end = [-1 * leg2 + thickness, 0])
3078 |> line(end = [0, -1 * leg1 + thickness])
3079 |> close()
3080 |> extrude(length = width)
3081"#;
3082 parse_execute(ast).await.unwrap();
3083 }
3084
3085 #[tokio::test(flavor = "multi_thread")]
3086 async fn test_math_nested_parens_one_less() {
3087 let ast = r#" sigmaAllow = 35000 // psi
3088width = 4 // inch
3089p = 150 // Force on shelf - lbs
3090distance = 6 // inches
3091FOS = 2
3092leg1 = 5 // inches
3093leg2 = 8 // inches
3094thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
3095thickness = 0.32 // inches. App does not support square root function yet
3096bracket = startSketchOn(XY)
3097 |> startProfile(at = [0,0])
3098 |> line(end = [0, leg1])
3099 |> line(end = [leg2, 0])
3100 |> line(end = [0, -thickness])
3101 |> line(end = [-1 * leg2 + thickness, 0])
3102 |> line(end = [0, -1 * leg1 + thickness])
3103 |> close()
3104 |> extrude(length = width)
3105"#;
3106 parse_execute(ast).await.unwrap();
3107 }
3108
3109 #[tokio::test(flavor = "multi_thread")]
3110 async fn test_fn_as_operand() {
3111 let ast = r#"fn f() { return 1 }
3112x = f()
3113y = x + 1
3114z = f() + 1
3115w = f() + f()
3116"#;
3117 parse_execute(ast).await.unwrap();
3118 }
3119
3120 #[tokio::test(flavor = "multi_thread")]
3121 async fn kcl_test_ids_stable_between_executions() {
3122 let code = r#"sketch001 = startSketchOn(XZ)
3123|> startProfile(at = [61.74, 206.13])
3124|> xLine(length = 305.11, tag = $seg01)
3125|> yLine(length = -291.85)
3126|> xLine(length = -segLen(seg01))
3127|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3128|> close()
3129|> extrude(length = 40.14)
3130|> shell(
3131 thickness = 3.14,
3132 faces = [seg01]
3133)
3134"#;
3135
3136 let ctx = crate::test_server::new_context(true, None).await.unwrap();
3137 let old_program = crate::Program::parse_no_errs(code).unwrap();
3138
3139 if let Err(err) = ctx.run_with_caching(old_program).await {
3141 let report = err.into_miette_report_with_outputs(code).unwrap();
3142 let report = miette::Report::new(report);
3143 panic!("Error executing program: {report:?}");
3144 }
3145
3146 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3148
3149 let code = r#"sketch001 = startSketchOn(XZ)
3150|> startProfile(at = [62.74, 206.13])
3151|> xLine(length = 305.11, tag = $seg01)
3152|> yLine(length = -291.85)
3153|> xLine(length = -segLen(seg01))
3154|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3155|> close()
3156|> extrude(length = 40.14)
3157|> shell(
3158 faces = [seg01],
3159 thickness = 3.14,
3160)
3161"#;
3162
3163 let program = crate::Program::parse_no_errs(code).unwrap();
3165 ctx.run_with_caching(program).await.unwrap();
3167
3168 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3169
3170 assert_eq!(id_generator, new_id_generator);
3171 }
3172
3173 #[tokio::test(flavor = "multi_thread")]
3174 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
3175 let code = r#"sketch001 = startSketchOn(XZ)
3176|> startProfile(at = [61.74, 206.13])
3177|> xLine(length = 305.11, tag = $seg01)
3178|> yLine(length = -291.85)
3179|> xLine(length = -segLen(seg01))
3180|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3181|> close()
3182|> extrude(length = 40.14)
3183|> shell(
3184 thickness = 3.14,
3185 faces = [seg01]
3186)
3187"#;
3188
3189 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
3190 let old_program = crate::Program::parse_no_errs(code).unwrap();
3191
3192 ctx.run_with_caching(old_program.clone()).await.unwrap();
3194
3195 let settings_state = cache::read_old_ast().await.unwrap().settings;
3196
3197 assert_eq!(settings_state, ctx.settings);
3199
3200 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3202
3203 ctx.run_with_caching(old_program.clone()).await.unwrap();
3205
3206 let settings_state = cache::read_old_ast().await.unwrap().settings;
3207
3208 assert_eq!(settings_state, ctx.settings);
3210
3211 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3213
3214 ctx.run_with_caching(old_program).await.unwrap();
3216
3217 let settings_state = cache::read_old_ast().await.unwrap().settings;
3218
3219 assert_eq!(settings_state, ctx.settings);
3221
3222 ctx.close().await;
3223 }
3224
3225 #[tokio::test(flavor = "multi_thread")]
3226 async fn mock_after_not_mock() {
3227 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3228 let program = crate::Program::parse_no_errs("x = 2").unwrap();
3229 let result = ctx.run_with_caching(program).await.unwrap();
3230 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
3231
3232 let ctx2 = ExecutorContext::new_mock(None).await;
3233 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
3234 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
3235 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
3236
3237 ctx.close().await;
3238 ctx2.close().await;
3239 }
3240
3241 #[tokio::test(flavor = "multi_thread")]
3242 async fn mock_then_add_extrude_then_mock_again() {
3243 let code = "s = sketch(on = XY) {
3244 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
3245 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
3246 coincident([line1.end, line2.start])
3247 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
3248 coincident([line2.end, line3.start])
3249 coincident([line1.start, line3.end])
3250}
3251 ";
3252 let ctx = ExecutorContext::new_mock(None).await;
3253 let program = crate::Program::parse_no_errs(code).unwrap();
3254 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3255 assert!(result.variables.contains_key("s"), "actual: {:?}", &result.variables);
3256
3257 let code2 = code.to_owned()
3258 + "
3259region001 = region(point = [1mm, 1mm], sketch = s)
3260extrude001 = extrude(region001, length = 1)
3261 ";
3262 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3263 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
3264 assert!(
3265 result.variables.contains_key("region001"),
3266 "actual: {:?}",
3267 &result.variables
3268 );
3269
3270 ctx.close().await;
3271 }
3272
3273 #[tokio::test(flavor = "multi_thread")]
3274 async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
3275 let code = format!(
3276 r#"{}
3277
3278face7 = faceOf(solid6, face = r6.tags.line1)
3279r7 = squareRegion(onSurface = face7)
3280solid7 = extrude(r7, length = width)
3281"#,
3282 include_str!("../../tests/endless_impeller/input.kcl")
3283 );
3284
3285 let result = parse_execute(&code).await.unwrap();
3286 let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
3287 assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
3288
3289 let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
3290 KclValue::Face { value } => value,
3291 value => panic!("expected face7 to be a Face, got {value:?}"),
3292 };
3293 assert!(face7.parent_solid.creator_sketch_id.is_some());
3294 }
3295
3296 #[tokio::test(flavor = "multi_thread")]
3297 async fn mock_has_stable_ids() {
3298 let ctx = ExecutorContext::new_mock(None).await;
3299 let mock_config = MockConfig {
3300 use_prev_memory: false,
3301 ..Default::default()
3302 };
3303 let code = "sk = startSketchOn(XY)
3304 |> startProfile(at = [0, 0])";
3305 let program = crate::Program::parse_no_errs(code).unwrap();
3306 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
3307 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3308 assert!(!ids.is_empty(), "IDs should not be empty");
3309
3310 let ctx2 = ExecutorContext::new_mock(None).await;
3311 let program2 = crate::Program::parse_no_errs(code).unwrap();
3312 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
3313 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3314
3315 assert_eq!(ids, ids2, "Generated IDs should match");
3316 ctx.close().await;
3317 ctx2.close().await;
3318 }
3319
3320 #[tokio::test(flavor = "multi_thread")]
3321 async fn mock_memory_restore_preserves_module_maps() {
3322 clear_mem_cache().await;
3323
3324 let ctx = ExecutorContext::new_mock(None).await;
3325 let cold_start = MockConfig {
3326 use_prev_memory: false,
3327 ..Default::default()
3328 };
3329 ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
3330
3331 let mut mem = cache::read_old_memory().await.unwrap();
3332 assert!(
3333 mem.path_to_source_id.len() > 3,
3334 "expected prelude imports to populate multiple modules, got {:?}",
3335 mem.path_to_source_id
3336 );
3337 mem.constraint_state.insert(
3338 crate::front::ObjectId(1),
3339 indexmap::indexmap! {
3340 crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
3341 crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
3342 },
3343 );
3344
3345 let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
3346 ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
3347
3348 assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
3349 assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
3350 assert_eq!(exec_state.global.module_infos, mem.module_infos);
3351 assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
3352
3353 clear_mem_cache().await;
3354 ctx.close().await;
3355 }
3356
3357 #[tokio::test(flavor = "multi_thread")]
3358 async fn run_with_caching_no_action_refreshes_mock_memory() {
3359 cache::bust_cache().await;
3360 clear_mem_cache().await;
3361
3362 let ctx = ExecutorContext::new_with_engine(
3363 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
3364 Default::default(),
3365 );
3366 let program = crate::Program::parse_no_errs(
3367 r#"sketch001 = sketch(on = XY) {
3368 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
3369}
3370"#,
3371 )
3372 .unwrap();
3373
3374 ctx.run_with_caching(program.clone()).await.unwrap();
3375 let baseline_memory = cache::read_old_memory().await.unwrap();
3376 assert!(
3377 !baseline_memory.scene_objects.is_empty(),
3378 "expected engine execution to persist full-scene mock memory"
3379 );
3380
3381 cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
3382 assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
3383
3384 ctx.run_with_caching(program).await.unwrap();
3385 let refreshed_memory = cache::read_old_memory().await.unwrap();
3386 assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
3387 assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
3388 assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
3389
3390 cache::bust_cache().await;
3391 clear_mem_cache().await;
3392 ctx.close().await;
3393 }
3394
3395 #[tokio::test(flavor = "multi_thread")]
3396 async fn sim_sketch_mode_real_mock_real() {
3397 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3398 let code = r#"sketch001 = startSketchOn(XY)
3399profile001 = startProfile(sketch001, at = [0, 0])
3400 |> line(end = [10, 0])
3401 |> line(end = [0, 10])
3402 |> line(end = [-10, 0])
3403 |> line(end = [0, -10])
3404 |> close()
3405"#;
3406 let program = crate::Program::parse_no_errs(code).unwrap();
3407 let result = ctx.run_with_caching(program).await.unwrap();
3408 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
3409
3410 let mock_ctx = ExecutorContext::new_mock(None).await;
3411 let mock_program = crate::Program::parse_no_errs(code).unwrap();
3412 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
3413 assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
3414
3415 let code2 = code.to_owned()
3416 + r#"
3417extrude001 = extrude(profile001, length = 10)
3418"#;
3419 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3420 let result = ctx.run_with_caching(program2).await.unwrap();
3421 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
3422
3423 ctx.close().await;
3424 mock_ctx.close().await;
3425 }
3426
3427 #[tokio::test(flavor = "multi_thread")]
3428 async fn read_tag_version() {
3429 let ast = r#"fn bar(@t) {
3430 return startSketchOn(XY)
3431 |> startProfile(at = [0,0])
3432 |> angledLine(
3433 angle = -60,
3434 length = segLen(t),
3435 )
3436 |> line(end = [0, 0])
3437 |> close()
3438}
3439
3440sketch = startSketchOn(XY)
3441 |> startProfile(at = [0,0])
3442 |> line(end = [0, 10])
3443 |> line(end = [10, 0], tag = $tag0)
3444 |> line(endAbsolute = [0, 0])
3445
3446fn foo() {
3447 // tag0 tags an edge
3448 return bar(tag0)
3449}
3450
3451solid = sketch |> extrude(length = 10)
3452// tag0 tags a face
3453sketch2 = startSketchOn(solid, face = tag0)
3454 |> startProfile(at = [0,0])
3455 |> line(end = [0, 1])
3456 |> line(end = [1, 0])
3457 |> line(end = [0, 0])
3458
3459foo() |> extrude(length = 1)
3460"#;
3461 parse_execute(ast).await.unwrap();
3462 }
3463
3464 #[tokio::test(flavor = "multi_thread")]
3465 async fn experimental() {
3466 let code = r#"
3467startSketchOn(XY)
3468 |> startProfile(at = [0, 0], tag = $start)
3469 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3470"#;
3471 let result = parse_execute(code).await.unwrap();
3472 let issues = result.exec_state.issues();
3473 assert_eq!(issues.len(), 1);
3474 assert_eq!(issues[0].severity, Severity::Error);
3475 let msg = &issues[0].message;
3476 assert!(msg.contains("experimental"), "found {msg}");
3477
3478 let code = r#"@settings(experimentalFeatures = allow)
3479startSketchOn(XY)
3480 |> startProfile(at = [0, 0], tag = $start)
3481 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3482"#;
3483 let result = parse_execute(code).await.unwrap();
3484 let issues = result.exec_state.issues();
3485 assert!(issues.is_empty(), "issues={issues:#?}");
3486
3487 let code = r#"@settings(experimentalFeatures = warn)
3488startSketchOn(XY)
3489 |> startProfile(at = [0, 0], tag = $start)
3490 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3491"#;
3492 let result = parse_execute(code).await.unwrap();
3493 let issues = result.exec_state.issues();
3494 assert_eq!(issues.len(), 1);
3495 assert_eq!(issues[0].severity, Severity::Warning);
3496 let msg = &issues[0].message;
3497 assert!(msg.contains("experimental"), "found {msg}");
3498
3499 let code = r#"@settings(experimentalFeatures = deny)
3500startSketchOn(XY)
3501 |> startProfile(at = [0, 0], tag = $start)
3502 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3503"#;
3504 let result = parse_execute(code).await.unwrap();
3505 let issues = result.exec_state.issues();
3506 assert_eq!(issues.len(), 1);
3507 assert_eq!(issues[0].severity, Severity::Error);
3508 let msg = &issues[0].message;
3509 assert!(msg.contains("experimental"), "found {msg}");
3510
3511 let code = r#"@settings(experimentalFeatures = foo)
3512startSketchOn(XY)
3513 |> startProfile(at = [0, 0], tag = $start)
3514 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3515"#;
3516 parse_execute(code).await.unwrap_err();
3517 }
3518
3519 #[tokio::test(flavor = "multi_thread")]
3520 async fn experimental_parameter() {
3521 let code = r#"
3522fn inc(@x, @(experimental = true) amount? = 1) {
3523 return x + amount
3524}
3525
3526answer = inc(5, amount = 2)
3527"#;
3528 let result = parse_execute(code).await.unwrap();
3529 let issues = result.exec_state.issues();
3530 assert_eq!(issues.len(), 1);
3531 assert_eq!(issues[0].severity, Severity::Error);
3532 let msg = &issues[0].message;
3533 assert!(msg.contains("experimental"), "found {msg}");
3534
3535 let code = r#"
3537fn inc(@x, @(experimental = true) amount? = 1) {
3538 return x + amount
3539}
3540
3541answer = inc(5)
3542"#;
3543 let result = parse_execute(code).await.unwrap();
3544 let issues = result.exec_state.issues();
3545 assert!(issues.is_empty(), "issues={issues:#?}");
3546 }
3547
3548 #[tokio::test(flavor = "multi_thread")]
3549 async fn experimental_scalar_fixed_constraint() {
3550 let code_left = r#"@settings(experimentalFeatures = warn)
3551sketch(on = XY) {
3552 point1 = point(at = [var 0mm, var 0mm])
3553 point1.at[0] == 1mm
3554}
3555"#;
3556 let code_right = r#"@settings(experimentalFeatures = warn)
3558sketch(on = XY) {
3559 point1 = point(at = [var 0mm, var 0mm])
3560 1mm == point1.at[0]
3561}
3562"#;
3563
3564 for code in [code_left, code_right] {
3565 let result = parse_execute(code).await.unwrap();
3566 let issues = result.exec_state.issues();
3567 let Some(error) = issues
3568 .iter()
3569 .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
3570 else {
3571 panic!("found {issues:#?}");
3572 };
3573 assert_eq!(error.severity, Severity::Warning);
3574 }
3575 }
3576
3577 #[tokio::test(flavor = "multi_thread")]
3581 async fn test_tangent_line_arc_executes_with_mock_engine() {
3582 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
3583 parse_execute(&code).await.unwrap();
3584 }
3585
3586 #[tokio::test(flavor = "multi_thread")]
3587 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
3588 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
3589 parse_execute(&code).await.unwrap();
3590 }
3591
3592 #[tokio::test(flavor = "multi_thread")]
3593 async fn test_tangent_line_circle_executes_with_mock_engine() {
3594 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
3595 parse_execute(&code).await.unwrap();
3596 }
3597
3598 #[tokio::test(flavor = "multi_thread")]
3599 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
3600 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
3601 parse_execute(&code).await.unwrap();
3602 }
3603
3604 #[tokio::test(flavor = "multi_thread")]
3605 async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
3606 let code = r#"startX = 2
3607
3608baseSketch = sketch(on = XY) {
3609 yoyo = line(start = [startX, 0], end = [7, 6])
3610 line2 = line(start = [7, 6], end = [7, 12])
3611 hi = line(start = [7, 12], end = [startX, 0])
3612}
3613
3614baseRegion = region(point = [5.5, 6], sketch = baseSketch)
3615myExtrude = extrude(
3616 baseRegion,
3617 length = 5,
3618 tagEnd = $endCap,
3619 tagStart = $startCap,
3620)
3621yodawg = getCommonEdge(faces = [
3622 baseRegion.tags.hi,
3623 baseRegion.tags.yoyo
3624])
3625
3626cutSketch = sketch(on = YZ) {
3627 myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
3628 myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
3629 line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
3630}
3631
3632cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
3633extrude001 = extrude(cutRegion, length = 5)
3634solid001 = subtract(myExtrude, tools = extrude001)
3635
3636yoyo = getOppositeEdge(baseRegion.tags.hi)
3637fillet(solid001, radius = 0.1, tags = yoyo)
3638"#;
3639
3640 parse_execute(code).await.unwrap();
3641 }
3642
3643 async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
3648 let program = crate::Program::parse_no_errs(kcl).unwrap();
3649 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3650 let mut exec_state = ExecState::new(&ctx);
3651 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3652 let outcome = exec_state.into_exec_outcome(env_ref, &ctx).await;
3653 let report = outcome.sketch_constraint_report();
3654 ctx.close().await;
3655 report
3656 }
3657
3658 #[tokio::test(flavor = "multi_thread")]
3659 async fn warn_when_sketch_is_over_constrained() {
3660 let code = r#"
3661sketch001 = sketch(on = XY) {
3662 line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
3663 fixed([line1.start, ORIGIN])
3664 fixed([line1.start, [20, 20]])
3665}
3666"#;
3667 let result = parse_execute(code).await.unwrap();
3668 let issues = result.exec_state.issues();
3669 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
3670 panic!("expected over-constrained warning; found {issues:#?}");
3671 };
3672 assert_eq!(warning.severity, Severity::Warning);
3673 }
3674
3675 #[tokio::test(flavor = "multi_thread")]
3676 async fn no_warning_when_sketch_is_not_over_constrained() {
3677 let code = r#"
3679sketch001 = sketch(on = XY) {
3680 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
3681}
3682"#;
3683 let result = parse_execute(code).await.unwrap();
3684 let issues = result.exec_state.issues();
3685 assert!(
3686 !issues.iter().any(|issue| issue.message.contains("over-constrained")),
3687 "did not expect over-constrained warning; found {issues:#?}"
3688 );
3689 }
3690
3691 #[tokio::test(flavor = "multi_thread")]
3692 async fn test_constraint_report_fully_constrained() {
3693 let kcl = r#"
3695@settings(experimentalFeatures = allow)
3696
3697sketch(on = YZ) {
3698 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
3699 line1.start.at[0] == 2
3700 line1.start.at[1] == 8
3701 line1.end.at[0] == 5
3702 line1.end.at[1] == 7
3703}
3704"#;
3705 let report = run_constraint_report(kcl).await;
3706 assert_eq!(report.fully_constrained.len(), 1);
3707 assert_eq!(report.under_constrained.len(), 0);
3708 assert_eq!(report.over_constrained.len(), 0);
3709 assert_eq!(report.errors.len(), 0);
3710 assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
3711 }
3712
3713 #[tokio::test(flavor = "multi_thread")]
3714 async fn test_constraint_report_under_constrained() {
3715 let kcl = r#"
3717sketch(on = YZ) {
3718 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
3719}
3720"#;
3721 let report = run_constraint_report(kcl).await;
3722 assert_eq!(report.fully_constrained.len(), 0);
3723 assert_eq!(report.under_constrained.len(), 1);
3724 assert_eq!(report.over_constrained.len(), 0);
3725 assert_eq!(report.errors.len(), 0);
3726 assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
3727 assert!(report.under_constrained[0].free_count > 0);
3728 }
3729
3730 #[tokio::test(flavor = "multi_thread")]
3731 async fn test_constraint_report_over_constrained() {
3732 let kcl = r#"
3734@settings(experimentalFeatures = allow)
3735
3736sketch(on = YZ) {
3737 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
3738 line1.start.at[0] == 2
3739 line1.start.at[1] == 8
3740 line1.end.at[0] == 5
3741 line1.end.at[1] == 7
3742 distance([line1.start, line1.end]) == 100mm
3743}
3744"#;
3745 let report = run_constraint_report(kcl).await;
3746 assert_eq!(report.over_constrained.len(), 1);
3747 assert_eq!(report.errors.len(), 0);
3748 assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
3749 assert!(report.over_constrained[0].conflict_count > 0);
3750 }
3751
3752 #[tokio::test(flavor = "multi_thread")]
3753 async fn test_constraint_report_multiple_sketches() {
3754 let kcl = r#"
3756@settings(experimentalFeatures = allow)
3757
3758s1 = sketch(on = YZ) {
3759 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
3760 line1.start.at[0] == 2
3761 line1.start.at[1] == 8
3762 line1.end.at[0] == 5
3763 line1.end.at[1] == 7
3764}
3765
3766s2 = sketch(on = XZ) {
3767 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
3768}
3769"#;
3770 let report = run_constraint_report(kcl).await;
3771 assert_eq!(
3772 report.fully_constrained.len()
3773 + report.under_constrained.len()
3774 + report.over_constrained.len()
3775 + report.errors.len(),
3776 2,
3777 "Expected 2 sketches total"
3778 );
3779 assert_eq!(report.fully_constrained.len(), 1);
3780 assert_eq!(report.under_constrained.len(), 1);
3781 }
3782}