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