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