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 geometry::*;
22pub use id_generator::IdGenerator;
23pub(crate) use import::PreImportedGeometry;
24use indexmap::IndexMap;
25pub use kcl_api::Operation;
26use kcl_api::ast::node_path::NodePath;
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;
63
64use crate::CompilationIssue;
65use crate::ExecError;
66use crate::KclErrorWithOutputs;
67use crate::NodePathExt;
68use crate::SourceRange;
69use crate::collections::AhashIndexSet;
70use crate::engine::EngineBatchContext;
71use crate::engine::GridScaleBehavior;
72use crate::engine::engine_manager::EngineManager;
73use crate::errors::KclError;
74use crate::errors::KclErrorDetails;
75use crate::execution::cache::CacheInformation;
76use crate::execution::cache::CacheResult;
77use crate::execution::cad_op::OperationExt;
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
2110pub use kcl_api::ArtifactId;
2111
2112pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
2113 ArtifactId::new(*id.as_ref())
2114}
2115
2116#[cfg(test)]
2117pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2118 parse_execute_with_project_dir(code, None).await
2119}
2120
2121#[cfg(test)]
2122pub(crate) async fn parse_execute_with_project_dir(
2123 code: &str,
2124 project_directory: Option<TypedPath>,
2125) -> Result<ExecTestResults, KclError> {
2126 let program = crate::Program::parse_no_errs(code)?;
2127
2128 let exec_ctxt = ExecutorContext {
2129 engine: Arc::new(EngineManager::new_mock()),
2130 engine_batch: EngineBatchContext::default(),
2131 fs: Arc::new(crate::fs::FileManager::new()),
2132 settings: ExecutorSettings {
2133 project_directory,
2134 ..Default::default()
2135 },
2136 context_type: ContextType::Mock,
2137 execution_callbacks: Default::default(),
2138 };
2139 let mut exec_state = ExecState::new(&exec_ctxt);
2140 let result = exec_ctxt.run(&program, &mut exec_state).await?;
2141
2142 Ok(ExecTestResults {
2143 program,
2144 mem_env: result.0,
2145 exec_ctxt,
2146 exec_state,
2147 })
2148}
2149
2150#[cfg(test)]
2151#[derive(Debug)]
2152pub(crate) struct ExecTestResults {
2153 program: crate::Program,
2154 mem_env: EnvironmentRef,
2155 exec_ctxt: ExecutorContext,
2156 exec_state: ExecState,
2157}
2158
2159#[cfg(test)]
2160impl ExecTestResults {
2161 pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
2162 &self.exec_state.global.root_module_artifacts.commands
2163 }
2164}
2165
2166pub struct ProgramLookup {
2170 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2171}
2172
2173impl ProgramLookup {
2174 pub fn new(
2177 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2178 module_infos: state::ModuleInfoMap,
2179 ) -> Self {
2180 let mut programs = IndexMap::with_capacity(module_infos.len());
2181 for (id, info) in module_infos {
2182 if let ModuleRepr::Kcl(program, _) = info.repr {
2183 programs.insert(id, program);
2184 }
2185 }
2186 programs.insert(ModuleId::default(), current);
2187 Self { programs }
2188 }
2189
2190 pub fn program_for_module(
2191 &self,
2192 module_id: ModuleId,
2193 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2194 self.programs.get(&module_id)
2195 }
2196}
2197
2198#[cfg(test)]
2199mod tests {
2200 use kcl_api::NumericType;
2201 use pretty_assertions::assert_eq;
2202
2203 use super::*;
2204 use crate::ModuleId;
2205 use crate::errors::KclErrorDetails;
2206 use crate::errors::Severity;
2207 use crate::execution::memory::Stack;
2208 use crate::execution::types::RuntimeType;
2209
2210 #[track_caller]
2212 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2213 memory.memory.get_from_unchecked(name, env).unwrap()
2214 }
2215
2216 async fn execute_variables_with_backend(
2217 code: &str,
2218 backend: memory::MemoryBackendKind,
2219 ) -> IndexMap<String, KclValueView> {
2220 execute_outcome_with_backend(code, backend).await.variables
2221 }
2222
2223 async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
2224 let program = crate::Program::parse_no_errs(code).unwrap();
2225 let ctx = ExecutorContext::new_mock(None).await;
2226 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2227 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
2228 let outcome = exec_state
2229 .into_exec_outcome(env_ref, &ctx)
2230 .await
2231 .expect("test execution outcome should collect variables");
2232 ctx.close().await;
2233 outcome
2234 }
2235
2236 async fn execute_error_variables_with_backend(
2237 code: &str,
2238 backend: memory::MemoryBackendKind,
2239 ) -> IndexMap<String, KclValueView> {
2240 let program = crate::Program::parse_no_errs(code).unwrap();
2241 let ctx = ExecutorContext::new_mock(None).await;
2242 let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
2243 let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
2244 ctx.close().await;
2245 error.variables
2246 }
2247
2248 async fn execute_project_variables_with_backend(
2249 main_code: &str,
2250 files: &[(&str, &str)],
2251 backend: memory::MemoryBackendKind,
2252 ) -> IndexMap<String, KclValueView> {
2253 let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
2254 for (name, contents) in files {
2255 tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
2256 }
2257
2258 let program = crate::Program::parse_no_errs(main_code).unwrap();
2259 let ctx = ExecutorContext {
2260 engine: Arc::new(EngineManager::new_mock()),
2261 engine_batch: EngineBatchContext::default(),
2262 fs: Arc::new(crate::fs::FileManager::new()),
2263 settings: ExecutorSettings {
2264 project_directory: Some(crate::TypedPath(tmpdir.path().into())),
2265 ..Default::default()
2266 },
2267 context_type: ContextType::Mock,
2268 execution_callbacks: Default::default(),
2269 };
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.variables
2278 }
2279
2280 async fn run_with_caching_variables_with_backend(
2281 code: &str,
2282 backend: memory::MemoryBackendKind,
2283 ) -> IndexMap<String, KclValueView> {
2284 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2285 cache::bust_cache().await;
2286 clear_mem_cache().await;
2287
2288 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
2289 let program = crate::Program::parse_no_errs(code).unwrap();
2290 ctx.run_with_caching(program.clone()).await.unwrap();
2291 let cached = ctx.run_with_caching(program).await.unwrap();
2292
2293 cache::bust_cache().await;
2294 clear_mem_cache().await;
2295 ctx.close().await;
2296 cached.variables
2297 }
2298
2299 async fn run_mock_variables_with_backend(
2300 code: &str,
2301 backend: memory::MemoryBackendKind,
2302 ) -> IndexMap<String, KclValueView> {
2303 let _backend = memory::MemoryBackendKind::override_for_test(backend);
2304 clear_mem_cache().await;
2305
2306 let ctx = ExecutorContext::new_mock(None).await;
2307 let first = crate::Program::parse_no_errs("x = 2").unwrap();
2308 ctx.run_mock(
2309 &first,
2310 &MockConfig {
2311 use_prev_memory: false,
2312 ..Default::default()
2313 },
2314 )
2315 .await
2316 .unwrap();
2317
2318 let program = crate::Program::parse_no_errs(code).unwrap();
2319 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
2320
2321 clear_mem_cache().await;
2322 ctx.close().await;
2323 outcome.variables
2324 }
2325
2326 fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
2327 let mut keys = variables.keys().cloned().collect::<Vec<_>>();
2328 keys.sort();
2329 keys
2330 }
2331
2332 fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
2333 let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
2334 let KclValueView::Number { value, .. } = value else {
2335 panic!("expected `{key}` to be a number, got {value:?}");
2336 };
2337 assert_eq!(*value, expected, "{key}: {value:?}");
2338 }
2339
2340 #[tokio::test(flavor = "multi_thread")]
2341 async fn exec_outcome_variables_match_between_memory_backends() {
2342 let code = "x = 2\ny = x + 1\narr = [x, y]";
2343
2344 let legacy = execute_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2345 let arena = execute_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2346
2347 assert_eq!(sorted_variable_keys(&legacy), vec!["arr", "x", "y"]);
2348 assert_eq!(arena, legacy);
2349 }
2350
2351 #[tokio::test(flavor = "multi_thread")]
2352 async fn error_output_variables_match_between_memory_backends() {
2353 let code = "x = 2\ny = missing + 1";
2354
2355 let legacy = execute_error_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2356 let arena = execute_error_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2357
2358 assert_eq!(sorted_variable_keys(&legacy), vec!["x"]);
2359 assert_eq!(arena, legacy);
2360 }
2361
2362 #[tokio::test(flavor = "multi_thread")]
2363 async fn cached_execution_variables_match_between_memory_backends() {
2364 let code = "x = 2\ny = x + 1";
2365
2366 let legacy = run_with_caching_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2367 let arena = run_with_caching_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2368
2369 assert_eq!(sorted_variable_keys(&legacy), vec!["x", "y"]);
2370 assert_eq!(arena, legacy);
2371 }
2372
2373 #[tokio::test(flavor = "multi_thread")]
2374 async fn mock_execution_variables_match_between_memory_backends() {
2375 let code = "y = x + 1";
2376
2377 let legacy = run_mock_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2378 let arena = run_mock_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2379
2380 assert_eq!(sorted_variable_keys(&legacy), vec!["y"]);
2381 assert_eq!(arena, legacy);
2382 }
2383
2384 #[tokio::test(flavor = "multi_thread")]
2385 async fn module_imports_and_exported_closures_match_between_memory_backends() {
2386 let module_code = r#"
2387export base = 40
2388
2389export fn addBase(n) {
2390 return n + base
2391}
2392"#;
2393 let main_code = r#"
2394import base, addBase from 'math.kcl'
2395import 'math.kcl'
2396
2397named = addBase(n = 2)
2398qualified = math::addBase(n = 1)
2399direct = math::base
2400"#;
2401
2402 let legacy = execute_project_variables_with_backend(
2403 main_code,
2404 &[("math.kcl", module_code)],
2405 memory::MemoryBackendKind::Legacy,
2406 )
2407 .await;
2408 let arena = execute_project_variables_with_backend(
2409 main_code,
2410 &[("math.kcl", module_code)],
2411 memory::MemoryBackendKind::Arena,
2412 )
2413 .await;
2414
2415 assert_number_variable(&legacy, "named", 42.0);
2416 assert_number_variable(&legacy, "qualified", 41.0);
2417 assert_number_variable(&legacy, "direct", 40.0);
2418 assert_eq!(arena, legacy);
2419 }
2420
2421 #[tokio::test(flavor = "multi_thread")]
2422 async fn sketch_block_variables_match_between_memory_backends() {
2423 let code = r#"
2424sketch001 = sketch(on = XY) {
2425 line1 = line(start = [0, 0], end = [1, 0])
2426 line2 = line(start = [1, 0], end = [0, 1])
2427}
2428lineCount = 2
2429"#;
2430
2431 let legacy = execute_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2432 let arena = execute_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2433
2434 assert!(legacy.contains_key("sketch001"), "actual: {legacy:?}");
2435 assert_number_variable(&legacy, "lineCount", 2.0);
2436 assert_eq!(arena, legacy);
2437 }
2438
2439 #[tokio::test(flavor = "multi_thread")]
2440 async fn tag_call_stack_lookup_matches_between_memory_backends() {
2441 let code = r#"
2442sketch001 = startSketchOn(XY)
2443 |> startProfile(at = [0, 0])
2444 |> xLine(length = 10, tag = $seg01)
2445
2446segLength = segLen(seg01)
2447"#;
2448
2449 let legacy = execute_variables_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2450 let arena = execute_variables_with_backend(code, memory::MemoryBackendKind::Arena).await;
2451
2452 assert_number_variable(&legacy, "segLength", 10.0);
2453 assert_eq!(arena, legacy);
2454 }
2455
2456 #[tokio::test(flavor = "multi_thread")]
2457 async fn sketch_transpiler_exec_outcome_variables_match_between_memory_backends() {
2458 let code = r#"
2459sketch001 = startSketchOn(XY)
2460 |> startProfile(at = [0, 0])
2461 |> line(end = [1, 0])
2462"#;
2463 let program = crate::Program::parse_no_errs(code).unwrap();
2464
2465 let legacy = execute_outcome_with_backend(code, memory::MemoryBackendKind::Legacy).await;
2466 let arena = execute_outcome_with_backend(code, memory::MemoryBackendKind::Arena).await;
2467
2468 let legacy_transpiled = transpile_old_sketch_to_new(&legacy, &program, "sketch001").unwrap();
2469 let arena_transpiled = transpile_old_sketch_to_new(&arena, &program, "sketch001").unwrap();
2470 assert_eq!(arena_transpiled, legacy_transpiled);
2471 }
2472
2473 #[tokio::test(flavor = "multi_thread")]
2474 async fn test_execute_warn() {
2475 let text = "@blah";
2476 let result = parse_execute(text).await.unwrap();
2477 let errs = result.exec_state.issues();
2478 assert_eq!(errs.len(), 1);
2479 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2480 assert!(
2481 errs[0].message.contains("Unknown annotation"),
2482 "unexpected warning message: {}",
2483 errs[0].message
2484 );
2485 }
2486
2487 #[tokio::test(flavor = "multi_thread")]
2488 async fn test_execute_fn_definitions() {
2489 let ast = r#"fn def(@x) {
2490 return x
2491}
2492fn ghi(@x) {
2493 return x
2494}
2495fn jkl(@x) {
2496 return x
2497}
2498fn hmm(@x) {
2499 return x
2500}
2501
2502yo = 5 + 6
2503
2504abc = 3
2505identifierGuy = 5
2506part001 = startSketchOn(XY)
2507|> startProfile(at = [-1.2, 4.83])
2508|> line(end = [2.8, 0])
2509|> angledLine(angle = 100 + 100, length = 3.01)
2510|> angledLine(angle = abc, length = 3.02)
2511|> angledLine(angle = def(yo), length = 3.03)
2512|> angledLine(angle = ghi(2), length = 3.04)
2513|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2514|> close()
2515yo2 = hmm([identifierGuy + 5])"#;
2516
2517 parse_execute(ast).await.unwrap();
2518 }
2519
2520 #[tokio::test(flavor = "multi_thread")]
2521 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2522 let code = r#"
2523firstProfile = sketch(on = XY) {
2524 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2525 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2526 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2527 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2528 coincident([edge1.end, edge2.start])
2529 coincident([edge2.end, edge3.start])
2530 coincident([edge3.end, edge4.start])
2531 coincident([edge4.end, edge1.start])
2532}
2533
2534secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2535 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2536 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2537 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2538 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2539 coincident([edge5.end, edge6.start])
2540 coincident([edge6.end, edge7.start])
2541 coincident([edge7.end, edge8.start])
2542 coincident([edge8.end, edge5.start])
2543}
2544
2545firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2546secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2547"#;
2548
2549 let result = parse_execute(code).await.unwrap();
2550 assert!(result.exec_state.issues().is_empty());
2551 }
2552
2553 #[tokio::test(flavor = "multi_thread")]
2554 async fn sketch_block_artifact_preserves_standard_plane_name() {
2555 let code = r#"
2556sketch001 = sketch(on = -YZ) {
2557 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
2558}
2559"#;
2560
2561 let result = parse_execute(code).await.unwrap();
2562 let sketch_blocks = result
2563 .exec_state
2564 .global
2565 .artifacts
2566 .graph
2567 .values()
2568 .filter_map(|artifact| match artifact {
2569 Artifact::SketchBlock(block) => Some(block),
2570 _ => None,
2571 })
2572 .collect::<Vec<_>>();
2573
2574 assert_eq!(sketch_blocks.len(), 1);
2575 assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
2576 }
2577
2578 #[tokio::test(flavor = "multi_thread")]
2579 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2580 let code = r#"
2581sketch001 = sketch(on = YZ) {
2582 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2583 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2584 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2585 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2586 coincident([line1.end, line2.start])
2587 coincident([line2.end, line3.start])
2588 coincident([line3.end, line4.start])
2589 coincident([line4.end, line1.start])
2590}
2591
2592sketch002 = sketch(on = -XZ) {
2593 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2594 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2595 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2596 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2597 coincident([line5.end, line6.start])
2598 coincident([line6.end, line7.start])
2599 coincident([line7.end, line8.start])
2600 coincident([line8.end, line5.start])
2601}
2602
2603region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2604extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2605region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2606extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2607
2608myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2609"#;
2610
2611 let result = parse_execute(code).await.unwrap();
2612 assert!(result.exec_state.issues().is_empty());
2613 }
2614
2615 #[tokio::test(flavor = "multi_thread")]
2616 async fn issue_10741_point_circle_coincident_executes() {
2617 let code = r#"
2618sketch001 = sketch(on = YZ) {
2619 circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
2620 line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
2621 coincident([line1.start, circle1])
2622}
2623"#;
2624
2625 let result = parse_execute(code).await.unwrap();
2626 assert!(
2627 result
2628 .exec_state
2629 .issues()
2630 .iter()
2631 .all(|issue| issue.severity != Severity::Error),
2632 "unexpected execution issues: {:#?}",
2633 result.exec_state.issues()
2634 );
2635 }
2636
2637 #[tokio::test(flavor = "multi_thread")]
2638 async fn test_execute_with_pipe_substitutions_unary() {
2639 let ast = r#"myVar = 3
2640part001 = startSketchOn(XY)
2641 |> startProfile(at = [0, 0])
2642 |> line(end = [3, 4], tag = $seg01)
2643 |> line(end = [
2644 min([segLen(seg01), myVar]),
2645 -legLen(hypotenuse = segLen(seg01), leg = myVar)
2646])
2647"#;
2648
2649 parse_execute(ast).await.unwrap();
2650 }
2651
2652 #[tokio::test(flavor = "multi_thread")]
2653 async fn test_execute_with_pipe_substitutions() {
2654 let ast = r#"myVar = 3
2655part001 = startSketchOn(XY)
2656 |> startProfile(at = [0, 0])
2657 |> line(end = [3, 4], tag = $seg01)
2658 |> line(end = [
2659 min([segLen(seg01), myVar]),
2660 legLen(hypotenuse = segLen(seg01), leg = myVar)
2661])
2662"#;
2663
2664 parse_execute(ast).await.unwrap();
2665 }
2666
2667 #[tokio::test(flavor = "multi_thread")]
2668 async fn test_execute_with_inline_comment() {
2669 let ast = r#"baseThick = 1
2670armAngle = 60
2671
2672baseThickHalf = baseThick / 2
2673halfArmAngle = armAngle / 2
2674
2675arrExpShouldNotBeIncluded = [1, 2, 3]
2676objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2677
2678part001 = startSketchOn(XY)
2679 |> startProfile(at = [0, 0])
2680 |> yLine(endAbsolute = 1)
2681 |> xLine(length = 3.84) // selection-range-7ish-before-this
2682
2683variableBelowShouldNotBeIncluded = 3
2684"#;
2685
2686 parse_execute(ast).await.unwrap();
2687 }
2688
2689 #[tokio::test(flavor = "multi_thread")]
2690 async fn test_execute_with_function_literal_in_pipe() {
2691 let ast = r#"w = 20
2692l = 8
2693h = 10
2694
2695fn thing() {
2696 return -8
2697}
2698
2699firstExtrude = startSketchOn(XY)
2700 |> startProfile(at = [0,0])
2701 |> line(end = [0, l])
2702 |> line(end = [w, 0])
2703 |> line(end = [0, thing()])
2704 |> close()
2705 |> extrude(length = h)"#;
2706
2707 parse_execute(ast).await.unwrap();
2708 }
2709
2710 #[tokio::test(flavor = "multi_thread")]
2711 async fn test_execute_with_function_unary_in_pipe() {
2712 let ast = r#"w = 20
2713l = 8
2714h = 10
2715
2716fn thing(@x) {
2717 return -x
2718}
2719
2720firstExtrude = startSketchOn(XY)
2721 |> startProfile(at = [0,0])
2722 |> line(end = [0, l])
2723 |> line(end = [w, 0])
2724 |> line(end = [0, thing(8)])
2725 |> close()
2726 |> extrude(length = h)"#;
2727
2728 parse_execute(ast).await.unwrap();
2729 }
2730
2731 #[tokio::test(flavor = "multi_thread")]
2732 async fn test_execute_with_function_array_in_pipe() {
2733 let ast = r#"w = 20
2734l = 8
2735h = 10
2736
2737fn thing(@x) {
2738 return [0, -x]
2739}
2740
2741firstExtrude = startSketchOn(XY)
2742 |> startProfile(at = [0,0])
2743 |> line(end = [0, l])
2744 |> line(end = [w, 0])
2745 |> line(end = thing(8))
2746 |> close()
2747 |> extrude(length = h)"#;
2748
2749 parse_execute(ast).await.unwrap();
2750 }
2751
2752 #[tokio::test(flavor = "multi_thread")]
2753 async fn test_execute_with_function_call_in_pipe() {
2754 let ast = r#"w = 20
2755l = 8
2756h = 10
2757
2758fn other_thing(@y) {
2759 return -y
2760}
2761
2762fn thing(@x) {
2763 return other_thing(x)
2764}
2765
2766firstExtrude = startSketchOn(XY)
2767 |> startProfile(at = [0,0])
2768 |> line(end = [0, l])
2769 |> line(end = [w, 0])
2770 |> line(end = [0, thing(8)])
2771 |> close()
2772 |> extrude(length = h)"#;
2773
2774 parse_execute(ast).await.unwrap();
2775 }
2776
2777 #[tokio::test(flavor = "multi_thread")]
2778 async fn test_execute_with_function_sketch() {
2779 let ast = r#"fn box(h, l, w) {
2780 myBox = startSketchOn(XY)
2781 |> startProfile(at = [0,0])
2782 |> line(end = [0, l])
2783 |> line(end = [w, 0])
2784 |> line(end = [0, -l])
2785 |> close()
2786 |> extrude(length = h)
2787
2788 return myBox
2789}
2790
2791fnBox = box(h = 3, l = 6, w = 10)"#;
2792
2793 parse_execute(ast).await.unwrap();
2794 }
2795
2796 #[tokio::test(flavor = "multi_thread")]
2797 async fn test_get_member_of_object_with_function_period() {
2798 let ast = r#"fn box(@obj) {
2799 myBox = startSketchOn(XY)
2800 |> startProfile(at = obj.start)
2801 |> line(end = [0, obj.l])
2802 |> line(end = [obj.w, 0])
2803 |> line(end = [0, -obj.l])
2804 |> close()
2805 |> extrude(length = obj.h)
2806
2807 return myBox
2808}
2809
2810thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2811"#;
2812 parse_execute(ast).await.unwrap();
2813 }
2814
2815 #[tokio::test(flavor = "multi_thread")]
2816 #[ignore] async fn test_object_member_starting_pipeline() {
2818 let ast = r#"
2819fn test2() {
2820 return {
2821 thing: startSketchOn(XY)
2822 |> startProfile(at = [0, 0])
2823 |> line(end = [0, 1])
2824 |> line(end = [1, 0])
2825 |> line(end = [0, -1])
2826 |> close()
2827 }
2828}
2829
2830x2 = test2()
2831
2832x2.thing
2833 |> extrude(length = 10)
2834"#;
2835 parse_execute(ast).await.unwrap();
2836 }
2837
2838 #[tokio::test(flavor = "multi_thread")]
2839 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
2841 let ast = r#"fn box(obj) {
2842let myBox = startSketchOn(XY)
2843 |> startProfile(at = obj.start)
2844 |> line(end = [0, obj.l])
2845 |> line(end = [obj.w, 0])
2846 |> line(end = [0, -obj.l])
2847 |> close()
2848 |> extrude(length = obj.h)
2849
2850 return myBox
2851}
2852
2853for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2854 thisBox = box(var)
2855}"#;
2856
2857 parse_execute(ast).await.unwrap();
2858 }
2859
2860 #[tokio::test(flavor = "multi_thread")]
2861 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
2863 let ast = r#"fn box(h, l, w, start) {
2864 myBox = startSketchOn(XY)
2865 |> startProfile(at = [0,0])
2866 |> line(end = [0, l])
2867 |> line(end = [w, 0])
2868 |> line(end = [0, -l])
2869 |> close()
2870 |> extrude(length = h)
2871
2872 return myBox
2873}
2874
2875
2876for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2877 const thisBox = box(var[0], var[1], var[2], var[3])
2878}"#;
2879
2880 parse_execute(ast).await.unwrap();
2881 }
2882
2883 #[tokio::test(flavor = "multi_thread")]
2884 async fn test_get_member_of_array_with_function() {
2885 let ast = r#"fn box(@arr) {
2886 myBox =startSketchOn(XY)
2887 |> startProfile(at = arr[0])
2888 |> line(end = [0, arr[1]])
2889 |> line(end = [arr[2], 0])
2890 |> line(end = [0, -arr[1]])
2891 |> close()
2892 |> extrude(length = arr[3])
2893
2894 return myBox
2895}
2896
2897thisBox = box([[0,0], 6, 10, 3])
2898
2899"#;
2900 parse_execute(ast).await.unwrap();
2901 }
2902
2903 #[tokio::test(flavor = "multi_thread")]
2904 async fn test_function_cannot_access_future_definitions() {
2905 let ast = r#"
2906fn returnX() {
2907 // x shouldn't be defined yet.
2908 return x
2909}
2910
2911x = 5
2912
2913answer = returnX()"#;
2914
2915 let result = parse_execute(ast).await;
2916 let err = result.unwrap_err();
2917 assert_eq!(err.message(), "`x` is not defined");
2918 }
2919
2920 #[tokio::test(flavor = "multi_thread")]
2921 async fn test_override_prelude() {
2922 let text = "PI = 3.0";
2923 let result = parse_execute(text).await.unwrap();
2924 let issues = result.exec_state.issues();
2925 assert!(issues.is_empty(), "issues={issues:#?}");
2926 }
2927
2928 #[tokio::test(flavor = "multi_thread")]
2929 async fn type_aliases() {
2930 let text = r#"@settings(experimentalFeatures = allow)
2931type MyTy = [number; 2]
2932fn foo(@x: MyTy) {
2933 return x[0]
2934}
2935
2936foo([0, 1])
2937
2938type Other = MyTy | Helix
2939"#;
2940 let result = parse_execute(text).await.unwrap();
2941 let issues = result.exec_state.issues();
2942 assert!(issues.is_empty(), "issues={issues:#?}");
2943 }
2944
2945 #[tokio::test(flavor = "multi_thread")]
2946 async fn test_cannot_shebang_in_fn() {
2947 let ast = r#"
2948fn foo() {
2949 #!hello
2950 return true
2951}
2952
2953foo
2954"#;
2955
2956 let result = parse_execute(ast).await;
2957 let err = result.unwrap_err();
2958 assert_eq!(
2959 err,
2960 KclError::new_syntax(KclErrorDetails::new(
2961 "Unexpected token: #".to_owned(),
2962 vec![SourceRange::new(14, 15, ModuleId::default())],
2963 )),
2964 );
2965 }
2966
2967 #[tokio::test(flavor = "multi_thread")]
2968 async fn test_pattern_transform_function_cannot_access_future_definitions() {
2969 let ast = r#"
2970fn transform(@replicaId) {
2971 // x shouldn't be defined yet.
2972 scale = x
2973 return {
2974 translate = [0, 0, replicaId * 10],
2975 scale = [scale, 1, 0],
2976 }
2977}
2978
2979fn layer() {
2980 return startSketchOn(XY)
2981 |> circle( center= [0, 0], radius= 1, tag = $tag1)
2982 |> extrude(length = 10)
2983}
2984
2985x = 5
2986
2987// The 10 layers are replicas of each other, with a transform applied to each.
2988shape = layer() |> patternTransform(instances = 10, transform = transform)
2989"#;
2990
2991 let result = parse_execute(ast).await;
2992 let err = result.unwrap_err();
2993 assert_eq!(err.message(), "`x` is not defined",);
2994 }
2995
2996 #[tokio::test(flavor = "multi_thread")]
2999 async fn test_math_execute_with_functions() {
3000 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
3001 let result = parse_execute(ast).await.unwrap();
3002 assert_eq!(
3003 5.0,
3004 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3005 .as_f64()
3006 .unwrap()
3007 );
3008 }
3009
3010 #[tokio::test(flavor = "multi_thread")]
3011 async fn test_math_execute() {
3012 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
3013 let result = parse_execute(ast).await.unwrap();
3014 assert_eq!(
3015 7.4,
3016 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3017 .as_f64()
3018 .unwrap()
3019 );
3020 }
3021
3022 #[tokio::test(flavor = "multi_thread")]
3023 async fn test_math_execute_start_negative() {
3024 let ast = r#"myVar = -5 + 6"#;
3025 let result = parse_execute(ast).await.unwrap();
3026 assert_eq!(
3027 1.0,
3028 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3029 .as_f64()
3030 .unwrap()
3031 );
3032 }
3033
3034 #[tokio::test(flavor = "multi_thread")]
3035 async fn test_math_execute_with_pi() {
3036 let ast = r#"myVar = PI * 2"#;
3037 let result = parse_execute(ast).await.unwrap();
3038 assert_eq!(
3039 std::f64::consts::TAU,
3040 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
3041 .as_f64()
3042 .unwrap()
3043 );
3044 }
3045
3046 #[tokio::test(flavor = "multi_thread")]
3047 async fn test_math_define_decimal_without_leading_zero() {
3048 let ast = r#"thing = .4 + 7"#;
3049 let result = parse_execute(ast).await.unwrap();
3050 assert_eq!(
3051 7.4,
3052 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
3053 .as_f64()
3054 .unwrap()
3055 );
3056 }
3057
3058 #[tokio::test(flavor = "multi_thread")]
3059 async fn pass_std_to_std() {
3060 let ast = r#"sketch001 = startSketchOn(XY)
3061profile001 = circle(sketch001, center = [0, 0], radius = 2)
3062extrude001 = extrude(profile001, length = 5)
3063extrudes = patternLinear3d(
3064 extrude001,
3065 instances = 3,
3066 distance = 5,
3067 axis = [1, 1, 0],
3068)
3069clone001 = map(extrudes, f = clone)
3070"#;
3071 parse_execute(ast).await.unwrap();
3072 }
3073
3074 #[tokio::test(flavor = "multi_thread")]
3075 async fn test_array_reduce_nested_array() {
3076 let code = r#"
3077fn id(@el, accum) { return accum }
3078
3079answer = reduce([], initial=[[[0,0]]], f=id)
3080"#;
3081 let result = parse_execute(code).await.unwrap();
3082 assert_eq!(
3083 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
3084 KclValue::HomArray {
3085 value: vec![KclValue::HomArray {
3086 value: vec![KclValue::HomArray {
3087 value: vec![
3088 KclValue::Number {
3089 value: 0.0,
3090 ty: NumericType::default(),
3091 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
3092 },
3093 KclValue::Number {
3094 value: 0.0,
3095 ty: NumericType::default(),
3096 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
3097 }
3098 ],
3099 ty: RuntimeType::any(),
3100 }],
3101 ty: RuntimeType::any(),
3102 }],
3103 ty: RuntimeType::any(),
3104 }
3105 );
3106 }
3107
3108 #[tokio::test(flavor = "multi_thread")]
3109 async fn test_zero_param_fn() {
3110 let ast = r#"sigmaAllow = 35000 // psi
3111leg1 = 5 // inches
3112leg2 = 8 // inches
3113fn thickness() { return 0.56 }
3114
3115bracket = startSketchOn(XY)
3116 |> startProfile(at = [0,0])
3117 |> line(end = [0, leg1])
3118 |> line(end = [leg2, 0])
3119 |> line(end = [0, -thickness()])
3120 |> line(end = [-leg2 + thickness(), 0])
3121"#;
3122 parse_execute(ast).await.unwrap();
3123 }
3124
3125 #[tokio::test(flavor = "multi_thread")]
3126 async fn test_unary_operator_not_succeeds() {
3127 let ast = r#"
3128fn returnTrue() { return !false }
3129t = true
3130f = false
3131notTrue = !t
3132notFalse = !f
3133c = !!true
3134d = !returnTrue()
3135
3136assertIs(!false, error = "expected to pass")
3137
3138fn check(x) {
3139 assertIs(!x, error = "expected argument to be false")
3140 return true
3141}
3142check(x = false)
3143"#;
3144 let result = parse_execute(ast).await.unwrap();
3145 assert_eq!(
3146 false,
3147 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
3148 .as_bool()
3149 .unwrap()
3150 );
3151 assert_eq!(
3152 true,
3153 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
3154 .as_bool()
3155 .unwrap()
3156 );
3157 assert_eq!(
3158 true,
3159 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
3160 .as_bool()
3161 .unwrap()
3162 );
3163 assert_eq!(
3164 false,
3165 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
3166 .as_bool()
3167 .unwrap()
3168 );
3169 }
3170
3171 #[tokio::test(flavor = "multi_thread")]
3172 async fn test_unary_operator_not_on_non_bool_fails() {
3173 let code1 = r#"
3174// Yup, this is null.
3175myNull = 0 / 0
3176notNull = !myNull
3177"#;
3178 assert_eq!(
3179 parse_execute(code1).await.unwrap_err().message(),
3180 "Cannot apply unary operator ! to non-boolean value: a number",
3181 );
3182
3183 let code2 = "notZero = !0";
3184 assert_eq!(
3185 parse_execute(code2).await.unwrap_err().message(),
3186 "Cannot apply unary operator ! to non-boolean value: a number",
3187 );
3188
3189 let code3 = r#"
3190notEmptyString = !""
3191"#;
3192 assert_eq!(
3193 parse_execute(code3).await.unwrap_err().message(),
3194 "Cannot apply unary operator ! to non-boolean value: a string",
3195 );
3196
3197 let code4 = r#"
3198obj = { a = 1 }
3199notMember = !obj.a
3200"#;
3201 assert_eq!(
3202 parse_execute(code4).await.unwrap_err().message(),
3203 "Cannot apply unary operator ! to non-boolean value: a number",
3204 );
3205
3206 let code5 = "
3207a = []
3208notArray = !a";
3209 assert_eq!(
3210 parse_execute(code5).await.unwrap_err().message(),
3211 "Cannot apply unary operator ! to non-boolean value: an empty array",
3212 );
3213
3214 let code6 = "
3215x = {}
3216notObject = !x";
3217 assert_eq!(
3218 parse_execute(code6).await.unwrap_err().message(),
3219 "Cannot apply unary operator ! to non-boolean value: an object",
3220 );
3221
3222 let code7 = "
3223fn x() { return 1 }
3224notFunction = !x";
3225 let fn_err = parse_execute(code7).await.unwrap_err();
3226 assert!(
3229 fn_err
3230 .message()
3231 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
3232 "Actual error: {fn_err:?}"
3233 );
3234
3235 let code8 = "
3236myTagDeclarator = $myTag
3237notTagDeclarator = !myTagDeclarator";
3238 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
3239 assert!(
3242 tag_declarator_err
3243 .message()
3244 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
3245 "Actual error: {tag_declarator_err:?}"
3246 );
3247
3248 let code9 = "
3249myTagDeclarator = $myTag
3250notTagIdentifier = !myTag";
3251 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
3252 assert!(
3255 tag_identifier_err
3256 .message()
3257 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
3258 "Actual error: {tag_identifier_err:?}"
3259 );
3260
3261 let code10 = "notPipe = !(1 |> 2)";
3262 assert_eq!(
3263 parse_execute(code10).await.unwrap_err(),
3266 KclError::new_syntax(KclErrorDetails::new(
3267 "Unexpected token: !".to_owned(),
3268 vec![SourceRange::new(10, 11, ModuleId::default())],
3269 ))
3270 );
3271
3272 let code11 = "
3273fn identity(x) { return x }
3274notPipeSub = 1 |> identity(!%))";
3275 assert_eq!(
3276 parse_execute(code11).await.unwrap_err(),
3279 KclError::new_syntax(KclErrorDetails::new(
3280 "There was an unexpected `!`. Try removing it.".to_owned(),
3281 vec![SourceRange::new(56, 57, ModuleId::default())],
3282 ))
3283 );
3284
3285 }
3289
3290 #[tokio::test(flavor = "multi_thread")]
3291 async fn test_start_sketch_on_invalid_kwargs() {
3292 let current_dir = std::env::current_dir().unwrap();
3293 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
3294 let mut code = std::fs::read_to_string(&path).unwrap();
3295 assert_eq!(
3296 parse_execute(&code).await.unwrap_err().message(),
3297 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
3298 );
3299
3300 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
3301 code = std::fs::read_to_string(&path).unwrap();
3302
3303 assert_eq!(
3304 parse_execute(&code).await.unwrap_err().message(),
3305 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
3306 );
3307
3308 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
3309 code = std::fs::read_to_string(&path).unwrap();
3310
3311 assert_eq!(
3312 parse_execute(&code).await.unwrap_err().message(),
3313 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
3314 );
3315
3316 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
3317 code = std::fs::read_to_string(&path).unwrap();
3318
3319 assert_eq!(
3320 parse_execute(&code).await.unwrap_err().message(),
3321 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
3322 );
3323
3324 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
3325 code = std::fs::read_to_string(&path).unwrap();
3326
3327 assert_eq!(
3328 parse_execute(&code).await.unwrap_err().message(),
3329 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
3330 );
3331 }
3332
3333 #[tokio::test(flavor = "multi_thread")]
3334 async fn test_math_negative_variable_in_binary_expression() {
3335 let ast = r#"sigmaAllow = 35000 // psi
3336width = 1 // inch
3337
3338p = 150 // lbs
3339distance = 6 // inches
3340FOS = 2
3341
3342leg1 = 5 // inches
3343leg2 = 8 // inches
3344
3345thickness_squared = distance * p * FOS * 6 / sigmaAllow
3346thickness = 0.56 // inches. App does not support square root function yet
3347
3348bracket = startSketchOn(XY)
3349 |> startProfile(at = [0,0])
3350 |> line(end = [0, leg1])
3351 |> line(end = [leg2, 0])
3352 |> line(end = [0, -thickness])
3353 |> line(end = [-leg2 + thickness, 0])
3354"#;
3355 parse_execute(ast).await.unwrap();
3356 }
3357
3358 #[tokio::test(flavor = "multi_thread")]
3359 async fn test_execute_function_no_return() {
3360 let ast = r#"fn test(@origin) {
3361 origin
3362}
3363
3364test([0, 0])
3365"#;
3366 let result = parse_execute(ast).await;
3367 assert!(result.is_err());
3368 assert!(result.unwrap_err().to_string().contains("undefined"));
3369 }
3370
3371 #[tokio::test(flavor = "multi_thread")]
3372 async fn test_max_stack_size_exceeded_error() {
3373 let ast = r#"
3374fn forever(@n) {
3375 return 1 + forever(n)
3376}
3377
3378forever(1)
3379"#;
3380 let result = parse_execute(ast).await;
3381 let err = result.unwrap_err();
3382 assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
3383 }
3384
3385 #[tokio::test(flavor = "multi_thread")]
3386 async fn test_math_doubly_nested_parens() {
3387 let ast = r#"sigmaAllow = 35000 // psi
3388width = 4 // inch
3389p = 150 // Force on shelf - lbs
3390distance = 6 // inches
3391FOS = 2
3392leg1 = 5 // inches
3393leg2 = 8 // inches
3394thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
3395thickness = 0.32 // inches. App does not support square root function yet
3396bracket = startSketchOn(XY)
3397 |> startProfile(at = [0,0])
3398 |> line(end = [0, leg1])
3399 |> line(end = [leg2, 0])
3400 |> line(end = [0, -thickness])
3401 |> line(end = [-1 * leg2 + thickness, 0])
3402 |> line(end = [0, -1 * leg1 + thickness])
3403 |> close()
3404 |> extrude(length = width)
3405"#;
3406 parse_execute(ast).await.unwrap();
3407 }
3408
3409 #[tokio::test(flavor = "multi_thread")]
3410 async fn test_math_nested_parens_one_less() {
3411 let ast = r#" sigmaAllow = 35000 // psi
3412width = 4 // inch
3413p = 150 // Force on shelf - lbs
3414distance = 6 // inches
3415FOS = 2
3416leg1 = 5 // inches
3417leg2 = 8 // inches
3418thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
3419thickness = 0.32 // inches. App does not support square root function yet
3420bracket = startSketchOn(XY)
3421 |> startProfile(at = [0,0])
3422 |> line(end = [0, leg1])
3423 |> line(end = [leg2, 0])
3424 |> line(end = [0, -thickness])
3425 |> line(end = [-1 * leg2 + thickness, 0])
3426 |> line(end = [0, -1 * leg1 + thickness])
3427 |> close()
3428 |> extrude(length = width)
3429"#;
3430 parse_execute(ast).await.unwrap();
3431 }
3432
3433 #[tokio::test(flavor = "multi_thread")]
3434 async fn test_fn_as_operand() {
3435 let ast = r#"fn f() { return 1 }
3436x = f()
3437y = x + 1
3438z = f() + 1
3439w = f() + f()
3440"#;
3441 parse_execute(ast).await.unwrap();
3442 }
3443
3444 #[tokio::test(flavor = "multi_thread")]
3445 async fn kcl_test_ids_stable_between_executions() {
3446 let code = r#"sketch001 = startSketchOn(XZ)
3447|> startProfile(at = [61.74, 206.13])
3448|> xLine(length = 305.11, tag = $seg01)
3449|> yLine(length = -291.85)
3450|> xLine(length = -segLen(seg01))
3451|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3452|> close()
3453|> extrude(length = 40.14)
3454|> shell(
3455 thickness = 3.14,
3456 faces = [seg01]
3457)
3458"#;
3459
3460 let ctx = crate::test_server::new_context(true, None).await.unwrap();
3461 let old_program = crate::Program::parse_no_errs(code).unwrap();
3462
3463 if let Err(err) = ctx.run_with_caching(old_program).await {
3465 let report = err.into_miette_report_with_outputs(code).unwrap();
3466 let report = miette::Report::new(report);
3467 panic!("Error executing program: {report:?}");
3468 }
3469
3470 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3472
3473 let code = r#"sketch001 = startSketchOn(XZ)
3474|> startProfile(at = [62.74, 206.13])
3475|> xLine(length = 305.11, tag = $seg01)
3476|> yLine(length = -291.85)
3477|> xLine(length = -segLen(seg01))
3478|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3479|> close()
3480|> extrude(length = 40.14)
3481|> shell(
3482 faces = [seg01],
3483 thickness = 3.14,
3484)
3485"#;
3486
3487 let program = crate::Program::parse_no_errs(code).unwrap();
3489 ctx.run_with_caching(program).await.unwrap();
3491
3492 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3493
3494 assert_eq!(id_generator, new_id_generator);
3495 }
3496
3497 #[tokio::test(flavor = "multi_thread")]
3498 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
3499 let code = r#"sketch001 = startSketchOn(XZ)
3500|> startProfile(at = [61.74, 206.13])
3501|> xLine(length = 305.11, tag = $seg01)
3502|> yLine(length = -291.85)
3503|> xLine(length = -segLen(seg01))
3504|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3505|> close()
3506|> extrude(length = 40.14)
3507|> shell(
3508 thickness = 3.14,
3509 faces = [seg01]
3510)
3511"#;
3512
3513 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
3514 let old_program = crate::Program::parse_no_errs(code).unwrap();
3515
3516 ctx.run_with_caching(old_program.clone()).await.unwrap();
3518
3519 let settings_state = cache::read_old_ast().await.unwrap().settings;
3520
3521 assert_eq!(settings_state, ctx.settings);
3523
3524 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3526
3527 ctx.run_with_caching(old_program.clone()).await.unwrap();
3529
3530 let settings_state = cache::read_old_ast().await.unwrap().settings;
3531
3532 assert_eq!(settings_state, ctx.settings);
3534
3535 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3537
3538 ctx.run_with_caching(old_program).await.unwrap();
3540
3541 let settings_state = cache::read_old_ast().await.unwrap().settings;
3542
3543 assert_eq!(settings_state, ctx.settings);
3545
3546 ctx.close().await;
3547 }
3548
3549 #[tokio::test(flavor = "multi_thread")]
3550 async fn mock_after_not_mock() {
3551 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3552 let program = crate::Program::parse_no_errs("x = 2").unwrap();
3553 let result = ctx.run_with_caching(program).await.unwrap();
3554 assert_number_variable(&result.variables, "x", 2.0);
3555
3556 let ctx2 = ExecutorContext::new_mock(None).await;
3557 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
3558 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
3559 assert_number_variable(&result.variables, "z", 3.0);
3560
3561 ctx.close().await;
3562 ctx2.close().await;
3563 }
3564
3565 #[tokio::test(flavor = "multi_thread")]
3566 async fn mock_then_add_extrude_then_mock_again() {
3567 let code = "s = sketch(on = XY) {
3568 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
3569 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
3570 coincident([line1.end, line2.start])
3571 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
3572 coincident([line2.end, line3.start])
3573 coincident([line1.start, line3.end])
3574}
3575 ";
3576 let ctx = ExecutorContext::new_mock(None).await;
3577 let program = crate::Program::parse_no_errs(code).unwrap();
3578 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3579 assert!(result.variables.contains_key("s"), "actual: {:?}", &result.variables);
3580
3581 let code2 = code.to_owned()
3582 + "
3583region001 = region(point = [1mm, 1mm], sketch = s)
3584extrude001 = extrude(region001, length = 1)
3585 ";
3586 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3587 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
3588 assert!(
3589 result.variables.contains_key("region001"),
3590 "actual: {:?}",
3591 &result.variables
3592 );
3593
3594 ctx.close().await;
3595 }
3596
3597 #[tokio::test(flavor = "multi_thread")]
3598 async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
3599 let code = format!(
3600 r#"{}
3601
3602face7 = faceOf(solid6, face = r6.tags.line1)
3603r7 = squareRegion(onSurface = face7)
3604solid7 = extrude(r7, length = width)
3605"#,
3606 include_str!("../../tests/endless_impeller/input.kcl")
3607 );
3608
3609 let result = parse_execute(&code).await.unwrap();
3610 let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
3611 assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
3612
3613 let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
3614 KclValue::Face { value } => value,
3615 value => panic!("expected face7 to be a Face, got {value:?}"),
3616 };
3617 assert!(face7.parent_solid.creator_sketch_id.is_some());
3618 }
3619
3620 #[tokio::test(flavor = "multi_thread")]
3621 async fn mock_has_stable_ids() {
3622 let ctx = ExecutorContext::new_mock(None).await;
3623 let mock_config = MockConfig {
3624 use_prev_memory: false,
3625 ..Default::default()
3626 };
3627 let code = "sk = startSketchOn(XY)
3628 |> startProfile(at = [0, 0])";
3629 let program = crate::Program::parse_no_errs(code).unwrap();
3630 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
3631 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3632 assert!(!ids.is_empty(), "IDs should not be empty");
3633
3634 let ctx2 = ExecutorContext::new_mock(None).await;
3635 let program2 = crate::Program::parse_no_errs(code).unwrap();
3636 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
3637 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3638
3639 assert_eq!(ids, ids2, "Generated IDs should match");
3640 ctx.close().await;
3641 ctx2.close().await;
3642 }
3643
3644 #[tokio::test(flavor = "multi_thread")]
3645 async fn mock_memory_restore_preserves_module_maps() {
3646 clear_mem_cache().await;
3647
3648 let ctx = ExecutorContext::new_mock(None).await;
3649 let cold_start = MockConfig {
3650 use_prev_memory: false,
3651 ..Default::default()
3652 };
3653 ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
3654
3655 let mut mem = cache::read_old_memory().await.unwrap();
3656 assert!(
3657 mem.path_to_source_id.len() > 3,
3658 "expected prelude imports to populate multiple modules, got {:?}",
3659 mem.path_to_source_id
3660 );
3661 mem.constraint_state.insert(
3662 crate::front::ObjectId(1),
3663 indexmap::indexmap! {
3664 crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
3665 crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
3666 },
3667 );
3668
3669 let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
3670 ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
3671
3672 assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
3673 assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
3674 assert_eq!(exec_state.global.module_infos, mem.module_infos);
3675 assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
3676
3677 clear_mem_cache().await;
3678 ctx.close().await;
3679 }
3680
3681 #[tokio::test(flavor = "multi_thread")]
3682 async fn run_with_caching_no_action_refreshes_mock_memory() {
3683 cache::bust_cache().await;
3684 clear_mem_cache().await;
3685
3686 let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
3687 let program = crate::Program::parse_no_errs(
3688 r#"sketch001 = sketch(on = XY) {
3689 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
3690}
3691"#,
3692 )
3693 .unwrap();
3694
3695 ctx.run_with_caching(program.clone()).await.unwrap();
3696 let baseline_memory = cache::read_old_memory().await.unwrap();
3697 assert!(
3698 !baseline_memory.scene_objects.is_empty(),
3699 "expected engine execution to persist full-scene mock memory"
3700 );
3701
3702 cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
3703 assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
3704
3705 ctx.run_with_caching(program).await.unwrap();
3706 let refreshed_memory = cache::read_old_memory().await.unwrap();
3707 assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
3708 assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
3709 assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
3710
3711 cache::bust_cache().await;
3712 clear_mem_cache().await;
3713 ctx.close().await;
3714 }
3715
3716 #[tokio::test(flavor = "multi_thread")]
3717 async fn sim_sketch_mode_real_mock_real() {
3718 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3719 let code = r#"sketch001 = startSketchOn(XY)
3720profile001 = startProfile(sketch001, at = [0, 0])
3721 |> line(end = [10, 0])
3722 |> line(end = [0, 10])
3723 |> line(end = [-10, 0])
3724 |> line(end = [0, -10])
3725 |> close()
3726"#;
3727 let program = crate::Program::parse_no_errs(code).unwrap();
3728 let result = ctx.run_with_caching(program).await.unwrap();
3729 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 1);
3730
3731 let mock_ctx = ExecutorContext::new_mock(None).await;
3732 let mock_program = crate::Program::parse_no_errs(code).unwrap();
3733 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
3734 assert_eq!(mock_result.operations.get(&ModuleId::default()).unwrap().len(), 1);
3735
3736 let code2 = code.to_owned()
3737 + r#"
3738extrude001 = extrude(profile001, length = 10)
3739"#;
3740 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3741 let result = ctx.run_with_caching(program2).await.unwrap();
3742 assert_eq!(result.operations.get(&ModuleId::default()).unwrap().len(), 2);
3743
3744 ctx.close().await;
3745 mock_ctx.close().await;
3746 }
3747
3748 #[tokio::test(flavor = "multi_thread")]
3749 async fn read_tag_version() {
3750 let ast = r#"fn bar(@t) {
3751 return startSketchOn(XY)
3752 |> startProfile(at = [0,0])
3753 |> angledLine(
3754 angle = -60,
3755 length = segLen(t),
3756 )
3757 |> line(end = [0, 0])
3758 |> close()
3759}
3760
3761sketch = startSketchOn(XY)
3762 |> startProfile(at = [0,0])
3763 |> line(end = [0, 10])
3764 |> line(end = [10, 0], tag = $tag0)
3765 |> line(endAbsolute = [0, 0])
3766
3767fn foo() {
3768 // tag0 tags an edge
3769 return bar(tag0)
3770}
3771
3772solid = sketch |> extrude(length = 10)
3773// tag0 tags a face
3774sketch2 = startSketchOn(solid, face = tag0)
3775 |> startProfile(at = [0,0])
3776 |> line(end = [0, 1])
3777 |> line(end = [1, 0])
3778 |> line(end = [0, 0])
3779
3780foo() |> extrude(length = 1)
3781"#;
3782 parse_execute(ast).await.unwrap();
3783 }
3784
3785 #[tokio::test(flavor = "multi_thread")]
3786 async fn experimental() {
3787 let code = r#"
3788startSketchOn(XY)
3789 |> startProfile(at = [0, 0], tag = $start)
3790 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3791"#;
3792 let result = parse_execute(code).await.unwrap();
3793 let issues = result.exec_state.issues();
3794 assert_eq!(issues.len(), 1);
3795 assert_eq!(issues[0].severity, Severity::Error);
3796 let msg = &issues[0].message;
3797 assert!(msg.contains("experimental"), "found {msg}");
3798
3799 let code = r#"@settings(experimentalFeatures = allow)
3800startSketchOn(XY)
3801 |> startProfile(at = [0, 0], tag = $start)
3802 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3803"#;
3804 let result = parse_execute(code).await.unwrap();
3805 let issues = result.exec_state.issues();
3806 assert!(issues.is_empty(), "issues={issues:#?}");
3807
3808 let code = r#"@settings(experimentalFeatures = warn)
3809startSketchOn(XY)
3810 |> startProfile(at = [0, 0], tag = $start)
3811 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3812"#;
3813 let result = parse_execute(code).await.unwrap();
3814 let issues = result.exec_state.issues();
3815 assert_eq!(issues.len(), 1);
3816 assert_eq!(issues[0].severity, Severity::Warning);
3817 let msg = &issues[0].message;
3818 assert!(msg.contains("experimental"), "found {msg}");
3819
3820 let code = r#"@settings(experimentalFeatures = deny)
3821startSketchOn(XY)
3822 |> startProfile(at = [0, 0], tag = $start)
3823 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3824"#;
3825 let result = parse_execute(code).await.unwrap();
3826 let issues = result.exec_state.issues();
3827 assert_eq!(issues.len(), 1);
3828 assert_eq!(issues[0].severity, Severity::Error);
3829 let msg = &issues[0].message;
3830 assert!(msg.contains("experimental"), "found {msg}");
3831
3832 let code = r#"@settings(experimentalFeatures = foo)
3833startSketchOn(XY)
3834 |> startProfile(at = [0, 0], tag = $start)
3835 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3836"#;
3837 parse_execute(code).await.unwrap_err();
3838 }
3839
3840 #[tokio::test(flavor = "multi_thread")]
3841 async fn experimental_parameter() {
3842 let code = r#"
3843fn inc(@x, @(experimental = true) amount? = 1) {
3844 return x + amount
3845}
3846
3847answer = inc(5, amount = 2)
3848"#;
3849 let result = parse_execute(code).await.unwrap();
3850 let issues = result.exec_state.issues();
3851 assert_eq!(issues.len(), 1);
3852 assert_eq!(issues[0].severity, Severity::Error);
3853 let msg = &issues[0].message;
3854 assert!(msg.contains("experimental"), "found {msg}");
3855
3856 let code = r#"
3858fn inc(@x, @(experimental = true) amount? = 1) {
3859 return x + amount
3860}
3861
3862answer = inc(5)
3863"#;
3864 let result = parse_execute(code).await.unwrap();
3865 let issues = result.exec_state.issues();
3866 assert!(issues.is_empty(), "issues={issues:#?}");
3867 }
3868
3869 #[tokio::test(flavor = "multi_thread")]
3870 async fn experimental_scalar_fixed_constraint() {
3871 let code_left = r#"@settings(experimentalFeatures = warn)
3872sketch(on = XY) {
3873 point1 = point(at = [var 0mm, var 0mm])
3874 point1.at[0] == 1mm
3875}
3876"#;
3877 let code_right = r#"@settings(experimentalFeatures = warn)
3879sketch(on = XY) {
3880 point1 = point(at = [var 0mm, var 0mm])
3881 1mm == point1.at[0]
3882}
3883"#;
3884
3885 for code in [code_left, code_right] {
3886 let result = parse_execute(code).await.unwrap();
3887 let issues = result.exec_state.issues();
3888 let Some(error) = issues
3889 .iter()
3890 .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
3891 else {
3892 panic!("found {issues:#?}");
3893 };
3894 assert_eq!(error.severity, Severity::Warning);
3895 }
3896 }
3897
3898 #[tokio::test(flavor = "multi_thread")]
3902 async fn test_tangent_line_arc_executes_with_mock_engine() {
3903 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
3904 parse_execute(&code).await.unwrap();
3905 }
3906
3907 #[tokio::test(flavor = "multi_thread")]
3908 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
3909 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
3910 parse_execute(&code).await.unwrap();
3911 }
3912
3913 #[tokio::test(flavor = "multi_thread")]
3914 async fn test_tangent_line_circle_executes_with_mock_engine() {
3915 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
3916 parse_execute(&code).await.unwrap();
3917 }
3918
3919 #[tokio::test(flavor = "multi_thread")]
3920 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
3921 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
3922 parse_execute(&code).await.unwrap();
3923 }
3924
3925 #[tokio::test(flavor = "multi_thread")]
3926 async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
3927 let code = r#"startX = 2
3928
3929baseSketch = sketch(on = XY) {
3930 yoyo = line(start = [startX, 0], end = [7, 6])
3931 line2 = line(start = [7, 6], end = [7, 12])
3932 hi = line(start = [7, 12], end = [startX, 0])
3933}
3934
3935baseRegion = region(point = [5.5, 6], sketch = baseSketch)
3936myExtrude = extrude(
3937 baseRegion,
3938 length = 5,
3939 tagEnd = $endCap,
3940 tagStart = $startCap,
3941)
3942yodawg = getCommonEdge(faces = [
3943 baseRegion.tags.hi,
3944 baseRegion.tags.yoyo
3945])
3946
3947cutSketch = sketch(on = YZ) {
3948 myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
3949 myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
3950 line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
3951}
3952
3953cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
3954extrude001 = extrude(cutRegion, length = 5)
3955solid001 = subtract(myExtrude, tools = extrude001)
3956
3957yoyo = getOppositeEdge(baseRegion.tags.hi)
3958fillet(solid001, radius = 0.1, tags = yoyo)
3959"#;
3960
3961 parse_execute(code).await.unwrap();
3962 }
3963
3964 async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
3969 let program = crate::Program::parse_no_errs(kcl).unwrap();
3970 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3971 let mut exec_state = ExecState::new(&ctx);
3972 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3973 let outcome = exec_state
3974 .into_exec_outcome(env_ref, &ctx)
3975 .await
3976 .expect("constraint report test outcome should collect variables");
3977 let report = outcome.sketch_constraint_report();
3978 ctx.close().await;
3979 report
3980 }
3981
3982 #[tokio::test(flavor = "multi_thread")]
3983 async fn warn_when_sketch_is_over_constrained() {
3984 let code = r#"
3985sketch001 = sketch(on = XY) {
3986 line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
3987 fixed([line1.start, ORIGIN])
3988 fixed([line1.start, [20, 20]])
3989}
3990"#;
3991 let result = parse_execute(code).await.unwrap();
3992 let issues = result.exec_state.issues();
3993 let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
3994 panic!("expected over-constrained warning; found {issues:#?}");
3995 };
3996 assert_eq!(warning.severity, Severity::Warning);
3997 }
3998
3999 #[tokio::test(flavor = "multi_thread")]
4000 async fn no_warning_when_sketch_is_not_over_constrained() {
4001 let code = r#"
4003sketch001 = sketch(on = XY) {
4004 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4005}
4006"#;
4007 let result = parse_execute(code).await.unwrap();
4008 let issues = result.exec_state.issues();
4009 assert!(
4010 !issues.iter().any(|issue| issue.message.contains("over-constrained")),
4011 "did not expect over-constrained warning; found {issues:#?}"
4012 );
4013 }
4014
4015 #[tokio::test(flavor = "multi_thread")]
4016 async fn test_constraint_report_fully_constrained() {
4017 let kcl = r#"
4019@settings(experimentalFeatures = allow)
4020
4021sketch(on = YZ) {
4022 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4023 line1.start.at[0] == 2
4024 line1.start.at[1] == 8
4025 line1.end.at[0] == 5
4026 line1.end.at[1] == 7
4027}
4028"#;
4029 let report = run_constraint_report(kcl).await;
4030 assert_eq!(report.fully_constrained.len(), 1);
4031 assert_eq!(report.under_constrained.len(), 0);
4032 assert_eq!(report.over_constrained.len(), 0);
4033 assert_eq!(report.errors.len(), 0);
4034 assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
4035 }
4036
4037 #[tokio::test(flavor = "multi_thread")]
4038 async fn test_constraint_report_under_constrained() {
4039 let kcl = r#"
4041sketch(on = YZ) {
4042 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
4043}
4044"#;
4045 let report = run_constraint_report(kcl).await;
4046 assert_eq!(report.fully_constrained.len(), 0);
4047 assert_eq!(report.under_constrained.len(), 1);
4048 assert_eq!(report.over_constrained.len(), 0);
4049 assert_eq!(report.errors.len(), 0);
4050 assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
4051 assert!(report.under_constrained[0].free_count > 0);
4052 }
4053
4054 #[tokio::test(flavor = "multi_thread")]
4055 async fn test_constraint_report_over_constrained() {
4056 let kcl = r#"
4058@settings(experimentalFeatures = allow)
4059
4060sketch(on = YZ) {
4061 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4062 line1.start.at[0] == 2
4063 line1.start.at[1] == 8
4064 line1.end.at[0] == 5
4065 line1.end.at[1] == 7
4066 distance([line1.start, line1.end]) == 100mm
4067}
4068"#;
4069 let report = run_constraint_report(kcl).await;
4070 assert_eq!(report.over_constrained.len(), 1);
4071 assert_eq!(report.errors.len(), 0);
4072 assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
4073 assert!(report.over_constrained[0].conflict_count > 0);
4074 }
4075
4076 #[tokio::test(flavor = "multi_thread")]
4077 async fn test_constraint_report_multiple_sketches() {
4078 let kcl = r#"
4080@settings(experimentalFeatures = allow)
4081
4082s1 = sketch(on = YZ) {
4083 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
4084 line1.start.at[0] == 2
4085 line1.start.at[1] == 8
4086 line1.end.at[0] == 5
4087 line1.end.at[1] == 7
4088}
4089
4090s2 = sketch(on = XZ) {
4091 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4092}
4093"#;
4094 let report = run_constraint_report(kcl).await;
4095 assert_eq!(
4096 report.fully_constrained.len()
4097 + report.under_constrained.len()
4098 + report.over_constrained.len()
4099 + report.errors.len(),
4100 2,
4101 "Expected 2 sketches total"
4102 );
4103 assert_eq!(report.fully_constrained.len(), 1);
4104 assert_eq!(report.under_constrained.len(), 1);
4105 }
4106}