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