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