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