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