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