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