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