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