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