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 cache::write_old_memory(cached_state.mock_memory_state()).await;
1273 return Ok(cached_state.into_exec_outcome(self).await);
1274 }
1275
1276 (
1277 true,
1278 crate::Program {
1279 ast: changed_program,
1280 original_file_contents: program.original_file_contents,
1281 },
1282 Some((new_universe, new_universe_map, new_exec_state)),
1283 )
1284 }
1285 }
1286 CacheResult::NoAction(true) => {
1287 if self
1288 .engine
1289 .reapply_settings(
1290 &self.settings,
1291 Default::default(),
1292 &mut cached_state.main.exec_state.id_generator,
1293 grid_scale,
1294 )
1295 .await
1296 .is_ok()
1297 {
1298 cache::write_old_ast(GlobalState::with_settings(
1300 cached_state.clone(),
1301 self.settings.clone(),
1302 ))
1303 .await;
1304
1305 cache::write_old_memory(cached_state.mock_memory_state()).await;
1306 return Ok(cached_state.into_exec_outcome(self).await);
1307 }
1308 (true, program, None)
1309 }
1310 CacheResult::NoAction(false) => {
1311 cache::write_old_memory(cached_state.mock_memory_state()).await;
1312 return Ok(cached_state.into_exec_outcome(self).await);
1313 }
1314 };
1315
1316 let (exec_state, result) = match import_check_info {
1317 Some((new_universe, new_universe_map, mut new_exec_state)) => {
1318 self.send_clear_scene(&mut new_exec_state, Default::default())
1320 .await
1321 .map_err(KclErrorWithOutputs::no_outputs)?;
1322
1323 let result = self
1324 .run_concurrent(
1325 &program,
1326 &mut new_exec_state,
1327 Some((new_universe, new_universe_map)),
1328 PreserveMem::Normal,
1329 )
1330 .await;
1331
1332 (new_exec_state, result)
1333 }
1334 None if clear_scene => {
1335 let mut exec_state = cached_state.reconstitute_exec_state();
1337 exec_state.reset(self);
1338
1339 self.send_clear_scene(&mut exec_state, Default::default())
1340 .await
1341 .map_err(KclErrorWithOutputs::no_outputs)?;
1342
1343 let result = self
1344 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1345 .await;
1346
1347 (exec_state, result)
1348 }
1349 None => {
1350 let mut exec_state = cached_state.reconstitute_exec_state();
1351 exec_state.mut_stack().restore_env(cached_state.main.result_env);
1352
1353 let result = self
1354 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
1355 .await;
1356
1357 (exec_state, result)
1358 }
1359 };
1360
1361 (program, exec_state, result)
1362 }
1363 None => {
1364 let mut exec_state = ExecState::new(self);
1365 self.send_clear_scene(&mut exec_state, Default::default())
1366 .await
1367 .map_err(KclErrorWithOutputs::no_outputs)?;
1368
1369 let result = self
1370 .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1371 .await;
1372
1373 (program, exec_state, result)
1374 }
1375 };
1376
1377 if result.is_err() {
1378 cache::bust_cache().await;
1379 }
1380
1381 let result = result?;
1383
1384 cache::write_old_ast(GlobalState::new(
1388 exec_state.clone(),
1389 self.settings.clone(),
1390 original_program.ast,
1391 result.0,
1392 ))
1393 .await;
1394
1395 let outcome = exec_state.into_exec_outcome(result.0, self).await;
1396 Ok(outcome)
1397 }
1398
1399 pub async fn run(
1403 &self,
1404 program: &crate::Program,
1405 exec_state: &mut ExecState,
1406 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1407 self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1408 .await
1409 }
1410
1411 pub async fn run_concurrent(
1416 &self,
1417 program: &crate::Program,
1418 exec_state: &mut ExecState,
1419 universe_info: Option<(Universe, UniverseMap)>,
1420 preserve_mem: PreserveMem,
1421 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1422 let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1425 (universe, universe_map)
1426 } else {
1427 self.get_universe(program, exec_state).await?
1428 };
1429
1430 let default_planes = self.engine.get_default_planes().read().await.clone();
1431
1432 self.eval_prelude(exec_state, SourceRange::synthetic())
1434 .await
1435 .map_err(KclErrorWithOutputs::no_outputs)?;
1436
1437 for modules in import_graph::import_graph(&universe, self)
1438 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1439 .into_iter()
1440 {
1441 #[cfg(not(target_arch = "wasm32"))]
1442 let mut set = tokio::task::JoinSet::new();
1443
1444 #[allow(clippy::type_complexity)]
1445 let (results_tx, mut results_rx): (
1446 tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1447 tokio::sync::mpsc::Receiver<_>,
1448 ) = tokio::sync::mpsc::channel(1);
1449
1450 for module in modules {
1451 let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1452 return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1453 KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1454 )));
1455 };
1456 let module_id = *module_id;
1457 let module_path = module_path.clone();
1458 let source_range = SourceRange::from(import_stmt);
1459 let module_exec_state = exec_state.clone();
1461
1462 self.add_import_module_ops(
1463 exec_state,
1464 &program.ast,
1465 module_id,
1466 &module_path,
1467 source_range,
1468 &universe_map,
1469 );
1470
1471 let repr = repr.clone();
1472 let exec_ctxt = self.clone();
1473 let results_tx = results_tx.clone();
1474
1475 let exec_module = async |exec_ctxt: &ExecutorContext,
1476 repr: &ModuleRepr,
1477 module_id: ModuleId,
1478 module_path: &ModulePath,
1479 exec_state: &mut ExecState,
1480 source_range: SourceRange|
1481 -> Result<ModuleRepr, KclError> {
1482 match repr {
1483 ModuleRepr::Kcl(program, _) => {
1484 let result = exec_ctxt
1485 .exec_module_from_ast(
1486 program,
1487 module_id,
1488 module_path,
1489 exec_state,
1490 source_range,
1491 PreserveMem::Normal,
1492 )
1493 .await;
1494
1495 result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1496 }
1497 ModuleRepr::Foreign(geom, _) => {
1498 let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1499 .await
1500 .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1501
1502 result.map(|val| {
1503 ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
1504 })
1505 }
1506 ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1507 format!("Module {module_path} not found in universe"),
1508 vec![source_range],
1509 ))),
1510 }
1511 };
1512
1513 #[cfg(target_arch = "wasm32")]
1514 {
1515 wasm_bindgen_futures::spawn_local(async move {
1516 let mut exec_state = module_exec_state;
1517 let exec_ctxt = exec_ctxt;
1518
1519 let result = exec_module(
1520 &exec_ctxt,
1521 &repr,
1522 module_id,
1523 &module_path,
1524 &mut exec_state,
1525 source_range,
1526 )
1527 .await;
1528
1529 results_tx
1530 .send((module_id, module_path, result))
1531 .await
1532 .unwrap_or_default();
1533 });
1534 }
1535 #[cfg(not(target_arch = "wasm32"))]
1536 {
1537 set.spawn(async move {
1538 let mut exec_state = module_exec_state;
1539 let exec_ctxt = exec_ctxt;
1540
1541 let result = exec_module(
1542 &exec_ctxt,
1543 &repr,
1544 module_id,
1545 &module_path,
1546 &mut exec_state,
1547 source_range,
1548 )
1549 .await;
1550
1551 results_tx
1552 .send((module_id, module_path, result))
1553 .await
1554 .unwrap_or_default();
1555 });
1556 }
1557 }
1558
1559 drop(results_tx);
1560
1561 while let Some((module_id, _, result)) = results_rx.recv().await {
1562 match result {
1563 Ok(new_repr) => {
1564 let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1565
1566 match &mut repr {
1567 ModuleRepr::Kcl(_, cache) => {
1568 let ModuleRepr::Kcl(_, session_data) = new_repr else {
1569 unreachable!();
1570 };
1571 *cache = session_data;
1572 }
1573 ModuleRepr::Foreign(_, cache) => {
1574 let ModuleRepr::Foreign(_, session_data) = new_repr else {
1575 unreachable!();
1576 };
1577 *cache = session_data;
1578 }
1579 ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1580 }
1581
1582 exec_state.global.module_infos[&module_id].restore_repr(repr);
1583 }
1584 Err(e) => {
1585 return Err(exec_state.error_with_outputs(e, None, default_planes));
1586 }
1587 }
1588 }
1589 }
1590
1591 exec_state
1595 .global
1596 .root_module_artifacts
1597 .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1598
1599 self.inner_run(program, exec_state, preserve_mem).await
1600 }
1601
1602 async fn get_universe(
1605 &self,
1606 program: &crate::Program,
1607 exec_state: &mut ExecState,
1608 ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1609 exec_state.add_root_module_contents(program);
1610
1611 let mut universe = std::collections::HashMap::new();
1612
1613 let default_planes = self.engine.get_default_planes().read().await.clone();
1614
1615 let root_imports = import_graph::import_universe(
1616 self,
1617 &ModulePath::Main,
1618 &ModuleRepr::Kcl(program.ast.clone(), None),
1619 &mut universe,
1620 exec_state,
1621 )
1622 .await
1623 .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1624
1625 Ok((universe, root_imports))
1626 }
1627
1628 #[cfg(not(feature = "artifact-graph"))]
1629 fn add_import_module_ops(
1630 &self,
1631 _exec_state: &mut ExecState,
1632 _program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1633 _module_id: ModuleId,
1634 _module_path: &ModulePath,
1635 _source_range: SourceRange,
1636 _universe_map: &UniverseMap,
1637 ) {
1638 }
1639
1640 #[cfg(feature = "artifact-graph")]
1641 fn add_import_module_ops(
1642 &self,
1643 exec_state: &mut ExecState,
1644 program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1645 module_id: ModuleId,
1646 module_path: &ModulePath,
1647 source_range: SourceRange,
1648 universe_map: &UniverseMap,
1649 ) {
1650 match module_path {
1651 ModulePath::Main => {
1652 }
1654 ModulePath::Local {
1655 value,
1656 original_import_path,
1657 } => {
1658 if universe_map.contains_key(value) {
1661 use crate::NodePath;
1662
1663 let node_path = if source_range.is_top_level_module() {
1664 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1665 NodePath::from_range(
1666 &exec_state.build_program_lookup(program.clone()),
1667 cached_body_items,
1668 source_range,
1669 )
1670 .unwrap_or_default()
1671 } else {
1672 NodePath::placeholder()
1675 };
1676
1677 let name = match original_import_path {
1678 Some(value) => value.to_string_lossy(),
1679 None => value.file_name().unwrap_or_default(),
1680 };
1681 exec_state.push_op(Operation::GroupBegin {
1682 group: Group::ModuleInstance { name, module_id },
1683 node_path,
1684 source_range,
1685 });
1686 exec_state.push_op(Operation::GroupEnd);
1690 }
1691 }
1692 ModulePath::Std { .. } => {
1693 }
1695 }
1696 }
1697
1698 async fn inner_run(
1701 &self,
1702 program: &crate::Program,
1703 exec_state: &mut ExecState,
1704 preserve_mem: PreserveMem,
1705 ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1706 let _stats = crate::log::LogPerfStats::new("Interpretation");
1707
1708 let grid_scale = if self.settings.fixed_size_grid {
1710 GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1711 } else {
1712 GridScaleBehavior::ScaleWithZoom
1713 };
1714 self.engine
1715 .reapply_settings(
1716 &self.settings,
1717 Default::default(),
1718 exec_state.id_generator(),
1719 grid_scale,
1720 )
1721 .await
1722 .map_err(KclErrorWithOutputs::no_outputs)?;
1723
1724 let default_planes = self.engine.get_default_planes().read().await.clone();
1725 let result = self
1726 .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1727 .await;
1728
1729 crate::log::log(format!(
1730 "Post interpretation KCL memory stats: {:#?}",
1731 exec_state.stack().memory.stats
1732 ));
1733 crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1734
1735 async fn write_old_memory(ctx: &ExecutorContext, exec_state: &ExecState, env_ref: EnvironmentRef) {
1738 if ctx.is_mock() {
1739 return;
1740 }
1741 let mut stack = exec_state.stack().deep_clone();
1742 stack.restore_env(env_ref);
1743 let state = cache::SketchModeState {
1744 stack,
1745 module_infos: exec_state.global.module_infos.clone(),
1746 path_to_source_id: exec_state.global.path_to_source_id.clone(),
1747 id_to_source: exec_state.global.id_to_source.clone(),
1748 #[cfg(feature = "artifact-graph")]
1749 scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1750 #[cfg(not(feature = "artifact-graph"))]
1751 scene_objects: Default::default(),
1752 };
1753 cache::write_old_memory(state).await;
1754 }
1755
1756 let env_ref = match result {
1757 Ok(env_ref) => env_ref,
1758 Err((err, env_ref)) => {
1759 if let Some(env_ref) = env_ref {
1762 write_old_memory(self, exec_state, env_ref).await;
1763 }
1764 return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
1765 }
1766 };
1767
1768 write_old_memory(self, exec_state, env_ref).await;
1769
1770 let session_data = self.engine.get_session_data().await;
1771
1772 Ok((env_ref, session_data))
1773 }
1774
1775 async fn execute_and_build_graph(
1778 &self,
1779 program: NodeRef<'_, crate::parsing::ast::types::Program>,
1780 exec_state: &mut ExecState,
1781 preserve_mem: PreserveMem,
1782 ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1783 #[cfg(feature = "artifact-graph")]
1789 let start_op = exec_state.global.root_module_artifacts.operations.len();
1790
1791 self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1792 .await
1793 .map_err(|e| (e, None))?;
1794
1795 let exec_result = self
1796 .exec_module_body(
1797 program,
1798 exec_state,
1799 preserve_mem,
1800 ModuleId::default(),
1801 &ModulePath::Main,
1802 )
1803 .await
1804 .map(
1805 |ModuleExecutionOutcome {
1806 environment: env_ref,
1807 artifacts: module_artifacts,
1808 ..
1809 }| {
1810 exec_state.global.root_module_artifacts.extend(module_artifacts);
1813 env_ref
1814 },
1815 )
1816 .map_err(|(err, env_ref, module_artifacts)| {
1817 if let Some(module_artifacts) = module_artifacts {
1818 exec_state.global.root_module_artifacts.extend(module_artifacts);
1821 }
1822 (err, env_ref)
1823 });
1824
1825 #[cfg(feature = "artifact-graph")]
1826 {
1827 let programs = &exec_state.build_program_lookup(program.clone());
1829 let cached_body_items = exec_state.global.artifacts.cached_body_items();
1830 for op in exec_state
1831 .global
1832 .root_module_artifacts
1833 .operations
1834 .iter_mut()
1835 .skip(start_op)
1836 {
1837 op.fill_node_paths(programs, cached_body_items);
1838 }
1839 for module in exec_state.global.module_infos.values_mut() {
1840 if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1841 for op in &mut outcome.artifacts.operations {
1842 op.fill_node_paths(programs, cached_body_items);
1843 }
1844 }
1845 }
1846 }
1847
1848 self.engine.ensure_async_commands_completed().await.map_err(|e| {
1850 match &exec_result {
1851 Ok(env_ref) => (e, Some(*env_ref)),
1852 Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1854 }
1855 })?;
1856
1857 self.engine.clear_queues().await;
1860
1861 match exec_state.build_artifact_graph(&self.engine, program).await {
1862 Ok(_) => exec_result,
1863 Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1864 }
1865 }
1866
1867 async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1871 if exec_state.stack().memory.requires_std() {
1872 #[cfg(feature = "artifact-graph")]
1873 let initial_ops = exec_state.mod_local.artifacts.operations.len();
1874
1875 let path = vec!["std".to_owned(), "prelude".to_owned()];
1876 let resolved_path = ModulePath::from_std_import_path(&path)?;
1877 let id = self
1878 .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1879 .await?;
1880 let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1881
1882 exec_state.mut_stack().memory.set_std(module_memory);
1883
1884 #[cfg(feature = "artifact-graph")]
1890 exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1891 }
1892
1893 Ok(())
1894 }
1895
1896 pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1898 self.engine
1900 .send_modeling_cmd(
1901 uuid::Uuid::new_v4(),
1902 crate::execution::SourceRange::default(),
1903 &ModelingCmd::from(
1904 mcmd::ZoomToFit::builder()
1905 .object_ids(Default::default())
1906 .animated(false)
1907 .padding(0.1)
1908 .build(),
1909 ),
1910 )
1911 .await
1912 .map_err(KclErrorWithOutputs::no_outputs)?;
1913
1914 let resp = self
1916 .engine
1917 .send_modeling_cmd(
1918 uuid::Uuid::new_v4(),
1919 crate::execution::SourceRange::default(),
1920 &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
1921 )
1922 .await
1923 .map_err(KclErrorWithOutputs::no_outputs)?;
1924
1925 let OkWebSocketResponseData::Modeling {
1926 modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1927 } = resp
1928 else {
1929 return Err(ExecError::BadPng(format!(
1930 "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1931 )));
1932 };
1933 Ok(contents)
1934 }
1935
1936 pub async fn export(
1938 &self,
1939 format: kittycad_modeling_cmds::format::OutputFormat3d,
1940 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1941 let resp = self
1942 .engine
1943 .send_modeling_cmd(
1944 uuid::Uuid::new_v4(),
1945 crate::SourceRange::default(),
1946 &kittycad_modeling_cmds::ModelingCmd::Export(
1947 kittycad_modeling_cmds::Export::builder()
1948 .entity_ids(vec![])
1949 .format(format)
1950 .build(),
1951 ),
1952 )
1953 .await?;
1954
1955 let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1956 return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1957 format!("Expected Export response, got {resp:?}",),
1958 vec![SourceRange::default()],
1959 )));
1960 };
1961
1962 Ok(files)
1963 }
1964
1965 pub async fn export_step(
1967 &self,
1968 deterministic_time: bool,
1969 ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1970 let files = self
1971 .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1972 kittycad_modeling_cmds::format::step::export::Options::builder()
1973 .coords(*kittycad_modeling_cmds::coord::KITTYCAD)
1974 .maybe_created(if deterministic_time {
1975 Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1976 KclError::new_internal(crate::errors::KclErrorDetails::new(
1977 format!("Failed to parse date: {e}"),
1978 vec![SourceRange::default()],
1979 ))
1980 })?)
1981 } else {
1982 None
1983 })
1984 .build(),
1985 ))
1986 .await?;
1987
1988 Ok(files)
1989 }
1990
1991 pub async fn close(&self) {
1992 self.engine.close().await;
1993 }
1994}
1995
1996#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1997pub struct ArtifactId(Uuid);
1998
1999impl ArtifactId {
2000 pub fn new(uuid: Uuid) -> Self {
2001 Self(uuid)
2002 }
2003
2004 pub fn placeholder() -> Self {
2006 Self(Uuid::nil())
2007 }
2008}
2009
2010impl From<Uuid> for ArtifactId {
2011 fn from(uuid: Uuid) -> Self {
2012 Self::new(uuid)
2013 }
2014}
2015
2016impl From<&Uuid> for ArtifactId {
2017 fn from(uuid: &Uuid) -> Self {
2018 Self::new(*uuid)
2019 }
2020}
2021
2022impl From<ArtifactId> for Uuid {
2023 fn from(id: ArtifactId) -> Self {
2024 id.0
2025 }
2026}
2027
2028impl From<&ArtifactId> for Uuid {
2029 fn from(id: &ArtifactId) -> Self {
2030 id.0
2031 }
2032}
2033
2034impl From<ModelingCmdId> for ArtifactId {
2035 fn from(id: ModelingCmdId) -> Self {
2036 Self::new(*id.as_ref())
2037 }
2038}
2039
2040impl From<&ModelingCmdId> for ArtifactId {
2041 fn from(id: &ModelingCmdId) -> Self {
2042 Self::new(*id.as_ref())
2043 }
2044}
2045
2046#[cfg(test)]
2047pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
2048 parse_execute_with_project_dir(code, None).await
2049}
2050
2051#[cfg(test)]
2052pub(crate) async fn parse_execute_with_project_dir(
2053 code: &str,
2054 project_directory: Option<TypedPath>,
2055) -> Result<ExecTestResults, KclError> {
2056 let program = crate::Program::parse_no_errs(code)?;
2057
2058 let exec_ctxt = ExecutorContext {
2059 engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
2060 |err| {
2061 KclError::new_internal(crate::errors::KclErrorDetails::new(
2062 format!("Failed to create mock engine connection: {err}"),
2063 vec![SourceRange::default()],
2064 ))
2065 },
2066 )?)),
2067 fs: Arc::new(crate::fs::FileManager::new()),
2068 settings: ExecutorSettings {
2069 project_directory,
2070 ..Default::default()
2071 },
2072 context_type: ContextType::Mock,
2073 };
2074 let mut exec_state = ExecState::new(&exec_ctxt);
2075 let result = exec_ctxt.run(&program, &mut exec_state).await?;
2076
2077 Ok(ExecTestResults {
2078 program,
2079 mem_env: result.0,
2080 exec_ctxt,
2081 exec_state,
2082 })
2083}
2084
2085#[cfg(test)]
2086#[derive(Debug)]
2087pub(crate) struct ExecTestResults {
2088 program: crate::Program,
2089 mem_env: EnvironmentRef,
2090 exec_ctxt: ExecutorContext,
2091 exec_state: ExecState,
2092}
2093
2094#[cfg(feature = "artifact-graph")]
2098pub struct ProgramLookup {
2099 programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
2100}
2101
2102#[cfg(feature = "artifact-graph")]
2103impl ProgramLookup {
2104 pub fn new(
2107 current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
2108 module_infos: state::ModuleInfoMap,
2109 ) -> Self {
2110 let mut programs = IndexMap::with_capacity(module_infos.len());
2111 for (id, info) in module_infos {
2112 if let ModuleRepr::Kcl(program, _) = info.repr {
2113 programs.insert(id, program);
2114 }
2115 }
2116 programs.insert(ModuleId::default(), current);
2117 Self { programs }
2118 }
2119
2120 pub fn program_for_module(
2121 &self,
2122 module_id: ModuleId,
2123 ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
2124 self.programs.get(&module_id)
2125 }
2126}
2127
2128#[cfg(test)]
2129mod tests {
2130 use pretty_assertions::assert_eq;
2131
2132 use super::*;
2133 use crate::ModuleId;
2134 use crate::errors::KclErrorDetails;
2135 use crate::errors::Severity;
2136 use crate::exec::NumericType;
2137 use crate::execution::memory::Stack;
2138 use crate::execution::types::RuntimeType;
2139
2140 #[track_caller]
2142 fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
2143 memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
2144 }
2145
2146 #[tokio::test(flavor = "multi_thread")]
2147 async fn test_execute_warn() {
2148 let text = "@blah";
2149 let result = parse_execute(text).await.unwrap();
2150 let errs = result.exec_state.issues();
2151 assert_eq!(errs.len(), 1);
2152 assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
2153 assert!(
2154 errs[0].message.contains("Unknown annotation"),
2155 "unexpected warning message: {}",
2156 errs[0].message
2157 );
2158 }
2159
2160 #[tokio::test(flavor = "multi_thread")]
2161 async fn test_execute_fn_definitions() {
2162 let ast = r#"fn def(@x) {
2163 return x
2164}
2165fn ghi(@x) {
2166 return x
2167}
2168fn jkl(@x) {
2169 return x
2170}
2171fn hmm(@x) {
2172 return x
2173}
2174
2175yo = 5 + 6
2176
2177abc = 3
2178identifierGuy = 5
2179part001 = startSketchOn(XY)
2180|> startProfile(at = [-1.2, 4.83])
2181|> line(end = [2.8, 0])
2182|> angledLine(angle = 100 + 100, length = 3.01)
2183|> angledLine(angle = abc, length = 3.02)
2184|> angledLine(angle = def(yo), length = 3.03)
2185|> angledLine(angle = ghi(2), length = 3.04)
2186|> angledLine(angle = jkl(yo) + 2, length = 3.05)
2187|> close()
2188yo2 = hmm([identifierGuy + 5])"#;
2189
2190 parse_execute(ast).await.unwrap();
2191 }
2192
2193 #[tokio::test(flavor = "multi_thread")]
2194 async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
2195 let code = r#"
2196firstProfile = sketch(on = XY) {
2197 edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
2198 edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
2199 edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
2200 edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
2201 coincident([edge1.end, edge2.start])
2202 coincident([edge2.end, edge3.start])
2203 coincident([edge3.end, edge4.start])
2204 coincident([edge4.end, edge1.start])
2205}
2206
2207secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
2208 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2209 edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
2210 edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
2211 edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
2212 coincident([edge5.end, edge6.start])
2213 coincident([edge6.end, edge7.start])
2214 coincident([edge7.end, edge8.start])
2215 coincident([edge8.end, edge5.start])
2216}
2217
2218firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
2219secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
2220"#;
2221
2222 let result = parse_execute(code).await.unwrap();
2223 assert!(result.exec_state.issues().is_empty());
2224 }
2225
2226 #[cfg(feature = "artifact-graph")]
2227 #[tokio::test(flavor = "multi_thread")]
2228 async fn sketch_block_artifact_preserves_standard_plane_name() {
2229 let code = r#"
2230sketch001 = sketch(on = -YZ) {
2231 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
2232}
2233"#;
2234
2235 let result = parse_execute(code).await.unwrap();
2236 let sketch_blocks = result
2237 .exec_state
2238 .global
2239 .artifacts
2240 .graph
2241 .values()
2242 .filter_map(|artifact| match artifact {
2243 Artifact::SketchBlock(block) => Some(block),
2244 _ => None,
2245 })
2246 .collect::<Vec<_>>();
2247
2248 assert_eq!(sketch_blocks.len(), 1);
2249 assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
2250 }
2251
2252 #[tokio::test(flavor = "multi_thread")]
2253 async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
2254 let code = r#"
2255sketch001 = sketch(on = YZ) {
2256 line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
2257 line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
2258 line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
2259 line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
2260 coincident([line1.end, line2.start])
2261 coincident([line2.end, line3.start])
2262 coincident([line3.end, line4.start])
2263 coincident([line4.end, line1.start])
2264}
2265
2266sketch002 = sketch(on = -XZ) {
2267 line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
2268 line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
2269 line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
2270 line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
2271 coincident([line5.end, line6.start])
2272 coincident([line6.end, line7.start])
2273 coincident([line7.end, line8.start])
2274 coincident([line8.end, line5.start])
2275}
2276
2277region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
2278extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
2279region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
2280extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
2281
2282myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
2283"#;
2284
2285 let result = parse_execute(code).await.unwrap();
2286 assert!(result.exec_state.issues().is_empty());
2287 }
2288
2289 #[tokio::test(flavor = "multi_thread")]
2290 async fn issue_10741_point_circle_coincident_executes() {
2291 let code = r#"
2292sketch001 = sketch(on = YZ) {
2293 circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
2294 line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
2295 coincident([line1.start, circle1])
2296}
2297"#;
2298
2299 let result = parse_execute(code).await.unwrap();
2300 assert!(
2301 result
2302 .exec_state
2303 .issues()
2304 .iter()
2305 .all(|issue| issue.severity != Severity::Error),
2306 "unexpected execution issues: {:#?}",
2307 result.exec_state.issues()
2308 );
2309 }
2310
2311 #[tokio::test(flavor = "multi_thread")]
2312 async fn test_execute_with_pipe_substitutions_unary() {
2313 let ast = r#"myVar = 3
2314part001 = startSketchOn(XY)
2315 |> startProfile(at = [0, 0])
2316 |> line(end = [3, 4], tag = $seg01)
2317 |> line(end = [
2318 min([segLen(seg01), myVar]),
2319 -legLen(hypotenuse = segLen(seg01), leg = myVar)
2320])
2321"#;
2322
2323 parse_execute(ast).await.unwrap();
2324 }
2325
2326 #[tokio::test(flavor = "multi_thread")]
2327 async fn test_execute_with_pipe_substitutions() {
2328 let ast = r#"myVar = 3
2329part001 = startSketchOn(XY)
2330 |> startProfile(at = [0, 0])
2331 |> line(end = [3, 4], tag = $seg01)
2332 |> line(end = [
2333 min([segLen(seg01), myVar]),
2334 legLen(hypotenuse = segLen(seg01), leg = myVar)
2335])
2336"#;
2337
2338 parse_execute(ast).await.unwrap();
2339 }
2340
2341 #[tokio::test(flavor = "multi_thread")]
2342 async fn test_execute_with_inline_comment() {
2343 let ast = r#"baseThick = 1
2344armAngle = 60
2345
2346baseThickHalf = baseThick / 2
2347halfArmAngle = armAngle / 2
2348
2349arrExpShouldNotBeIncluded = [1, 2, 3]
2350objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
2351
2352part001 = startSketchOn(XY)
2353 |> startProfile(at = [0, 0])
2354 |> yLine(endAbsolute = 1)
2355 |> xLine(length = 3.84) // selection-range-7ish-before-this
2356
2357variableBelowShouldNotBeIncluded = 3
2358"#;
2359
2360 parse_execute(ast).await.unwrap();
2361 }
2362
2363 #[tokio::test(flavor = "multi_thread")]
2364 async fn test_execute_with_function_literal_in_pipe() {
2365 let ast = r#"w = 20
2366l = 8
2367h = 10
2368
2369fn thing() {
2370 return -8
2371}
2372
2373firstExtrude = startSketchOn(XY)
2374 |> startProfile(at = [0,0])
2375 |> line(end = [0, l])
2376 |> line(end = [w, 0])
2377 |> line(end = [0, thing()])
2378 |> close()
2379 |> extrude(length = h)"#;
2380
2381 parse_execute(ast).await.unwrap();
2382 }
2383
2384 #[tokio::test(flavor = "multi_thread")]
2385 async fn test_execute_with_function_unary_in_pipe() {
2386 let ast = r#"w = 20
2387l = 8
2388h = 10
2389
2390fn thing(@x) {
2391 return -x
2392}
2393
2394firstExtrude = startSketchOn(XY)
2395 |> startProfile(at = [0,0])
2396 |> line(end = [0, l])
2397 |> line(end = [w, 0])
2398 |> line(end = [0, thing(8)])
2399 |> close()
2400 |> extrude(length = h)"#;
2401
2402 parse_execute(ast).await.unwrap();
2403 }
2404
2405 #[tokio::test(flavor = "multi_thread")]
2406 async fn test_execute_with_function_array_in_pipe() {
2407 let ast = r#"w = 20
2408l = 8
2409h = 10
2410
2411fn thing(@x) {
2412 return [0, -x]
2413}
2414
2415firstExtrude = startSketchOn(XY)
2416 |> startProfile(at = [0,0])
2417 |> line(end = [0, l])
2418 |> line(end = [w, 0])
2419 |> line(end = thing(8))
2420 |> close()
2421 |> extrude(length = h)"#;
2422
2423 parse_execute(ast).await.unwrap();
2424 }
2425
2426 #[tokio::test(flavor = "multi_thread")]
2427 async fn test_execute_with_function_call_in_pipe() {
2428 let ast = r#"w = 20
2429l = 8
2430h = 10
2431
2432fn other_thing(@y) {
2433 return -y
2434}
2435
2436fn thing(@x) {
2437 return other_thing(x)
2438}
2439
2440firstExtrude = startSketchOn(XY)
2441 |> startProfile(at = [0,0])
2442 |> line(end = [0, l])
2443 |> line(end = [w, 0])
2444 |> line(end = [0, thing(8)])
2445 |> close()
2446 |> extrude(length = h)"#;
2447
2448 parse_execute(ast).await.unwrap();
2449 }
2450
2451 #[tokio::test(flavor = "multi_thread")]
2452 async fn test_execute_with_function_sketch() {
2453 let ast = r#"fn box(h, l, w) {
2454 myBox = startSketchOn(XY)
2455 |> startProfile(at = [0,0])
2456 |> line(end = [0, l])
2457 |> line(end = [w, 0])
2458 |> line(end = [0, -l])
2459 |> close()
2460 |> extrude(length = h)
2461
2462 return myBox
2463}
2464
2465fnBox = box(h = 3, l = 6, w = 10)"#;
2466
2467 parse_execute(ast).await.unwrap();
2468 }
2469
2470 #[tokio::test(flavor = "multi_thread")]
2471 async fn test_get_member_of_object_with_function_period() {
2472 let ast = r#"fn box(@obj) {
2473 myBox = startSketchOn(XY)
2474 |> startProfile(at = obj.start)
2475 |> line(end = [0, obj.l])
2476 |> line(end = [obj.w, 0])
2477 |> line(end = [0, -obj.l])
2478 |> close()
2479 |> extrude(length = obj.h)
2480
2481 return myBox
2482}
2483
2484thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
2485"#;
2486 parse_execute(ast).await.unwrap();
2487 }
2488
2489 #[tokio::test(flavor = "multi_thread")]
2490 #[ignore] async fn test_object_member_starting_pipeline() {
2492 let ast = r#"
2493fn test2() {
2494 return {
2495 thing: startSketchOn(XY)
2496 |> startProfile(at = [0, 0])
2497 |> line(end = [0, 1])
2498 |> line(end = [1, 0])
2499 |> line(end = [0, -1])
2500 |> close()
2501 }
2502}
2503
2504x2 = test2()
2505
2506x2.thing
2507 |> extrude(length = 10)
2508"#;
2509 parse_execute(ast).await.unwrap();
2510 }
2511
2512 #[tokio::test(flavor = "multi_thread")]
2513 #[ignore] async fn test_execute_with_function_sketch_loop_objects() {
2515 let ast = r#"fn box(obj) {
2516let myBox = startSketchOn(XY)
2517 |> startProfile(at = obj.start)
2518 |> line(end = [0, obj.l])
2519 |> line(end = [obj.w, 0])
2520 |> line(end = [0, -obj.l])
2521 |> close()
2522 |> extrude(length = obj.h)
2523
2524 return myBox
2525}
2526
2527for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2528 thisBox = box(var)
2529}"#;
2530
2531 parse_execute(ast).await.unwrap();
2532 }
2533
2534 #[tokio::test(flavor = "multi_thread")]
2535 #[ignore] async fn test_execute_with_function_sketch_loop_array() {
2537 let ast = r#"fn box(h, l, w, start) {
2538 myBox = startSketchOn(XY)
2539 |> startProfile(at = [0,0])
2540 |> line(end = [0, l])
2541 |> line(end = [w, 0])
2542 |> line(end = [0, -l])
2543 |> close()
2544 |> extrude(length = h)
2545
2546 return myBox
2547}
2548
2549
2550for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2551 const thisBox = box(var[0], var[1], var[2], var[3])
2552}"#;
2553
2554 parse_execute(ast).await.unwrap();
2555 }
2556
2557 #[tokio::test(flavor = "multi_thread")]
2558 async fn test_get_member_of_array_with_function() {
2559 let ast = r#"fn box(@arr) {
2560 myBox =startSketchOn(XY)
2561 |> startProfile(at = arr[0])
2562 |> line(end = [0, arr[1]])
2563 |> line(end = [arr[2], 0])
2564 |> line(end = [0, -arr[1]])
2565 |> close()
2566 |> extrude(length = arr[3])
2567
2568 return myBox
2569}
2570
2571thisBox = box([[0,0], 6, 10, 3])
2572
2573"#;
2574 parse_execute(ast).await.unwrap();
2575 }
2576
2577 #[tokio::test(flavor = "multi_thread")]
2578 async fn test_function_cannot_access_future_definitions() {
2579 let ast = r#"
2580fn returnX() {
2581 // x shouldn't be defined yet.
2582 return x
2583}
2584
2585x = 5
2586
2587answer = returnX()"#;
2588
2589 let result = parse_execute(ast).await;
2590 let err = result.unwrap_err();
2591 assert_eq!(err.message(), "`x` is not defined");
2592 }
2593
2594 #[tokio::test(flavor = "multi_thread")]
2595 async fn test_override_prelude() {
2596 let text = "PI = 3.0";
2597 let result = parse_execute(text).await.unwrap();
2598 let issues = result.exec_state.issues();
2599 assert!(issues.is_empty(), "issues={issues:#?}");
2600 }
2601
2602 #[tokio::test(flavor = "multi_thread")]
2603 async fn type_aliases() {
2604 let text = r#"@settings(experimentalFeatures = allow)
2605type MyTy = [number; 2]
2606fn foo(@x: MyTy) {
2607 return x[0]
2608}
2609
2610foo([0, 1])
2611
2612type Other = MyTy | Helix
2613"#;
2614 let result = parse_execute(text).await.unwrap();
2615 let issues = result.exec_state.issues();
2616 assert!(issues.is_empty(), "issues={issues:#?}");
2617 }
2618
2619 #[tokio::test(flavor = "multi_thread")]
2620 async fn test_cannot_shebang_in_fn() {
2621 let ast = r#"
2622fn foo() {
2623 #!hello
2624 return true
2625}
2626
2627foo
2628"#;
2629
2630 let result = parse_execute(ast).await;
2631 let err = result.unwrap_err();
2632 assert_eq!(
2633 err,
2634 KclError::new_syntax(KclErrorDetails::new(
2635 "Unexpected token: #".to_owned(),
2636 vec![SourceRange::new(14, 15, ModuleId::default())],
2637 )),
2638 );
2639 }
2640
2641 #[tokio::test(flavor = "multi_thread")]
2642 async fn test_pattern_transform_function_cannot_access_future_definitions() {
2643 let ast = r#"
2644fn transform(@replicaId) {
2645 // x shouldn't be defined yet.
2646 scale = x
2647 return {
2648 translate = [0, 0, replicaId * 10],
2649 scale = [scale, 1, 0],
2650 }
2651}
2652
2653fn layer() {
2654 return startSketchOn(XY)
2655 |> circle( center= [0, 0], radius= 1, tag = $tag1)
2656 |> extrude(length = 10)
2657}
2658
2659x = 5
2660
2661// The 10 layers are replicas of each other, with a transform applied to each.
2662shape = layer() |> patternTransform(instances = 10, transform = transform)
2663"#;
2664
2665 let result = parse_execute(ast).await;
2666 let err = result.unwrap_err();
2667 assert_eq!(err.message(), "`x` is not defined",);
2668 }
2669
2670 #[tokio::test(flavor = "multi_thread")]
2673 async fn test_math_execute_with_functions() {
2674 let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
2675 let result = parse_execute(ast).await.unwrap();
2676 assert_eq!(
2677 5.0,
2678 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2679 .as_f64()
2680 .unwrap()
2681 );
2682 }
2683
2684 #[tokio::test(flavor = "multi_thread")]
2685 async fn test_math_execute() {
2686 let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
2687 let result = parse_execute(ast).await.unwrap();
2688 assert_eq!(
2689 7.4,
2690 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2691 .as_f64()
2692 .unwrap()
2693 );
2694 }
2695
2696 #[tokio::test(flavor = "multi_thread")]
2697 async fn test_math_execute_start_negative() {
2698 let ast = r#"myVar = -5 + 6"#;
2699 let result = parse_execute(ast).await.unwrap();
2700 assert_eq!(
2701 1.0,
2702 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2703 .as_f64()
2704 .unwrap()
2705 );
2706 }
2707
2708 #[tokio::test(flavor = "multi_thread")]
2709 async fn test_math_execute_with_pi() {
2710 let ast = r#"myVar = PI * 2"#;
2711 let result = parse_execute(ast).await.unwrap();
2712 assert_eq!(
2713 std::f64::consts::TAU,
2714 mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2715 .as_f64()
2716 .unwrap()
2717 );
2718 }
2719
2720 #[tokio::test(flavor = "multi_thread")]
2721 async fn test_math_define_decimal_without_leading_zero() {
2722 let ast = r#"thing = .4 + 7"#;
2723 let result = parse_execute(ast).await.unwrap();
2724 assert_eq!(
2725 7.4,
2726 mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
2727 .as_f64()
2728 .unwrap()
2729 );
2730 }
2731
2732 #[tokio::test(flavor = "multi_thread")]
2733 async fn pass_std_to_std() {
2734 let ast = r#"sketch001 = startSketchOn(XY)
2735profile001 = circle(sketch001, center = [0, 0], radius = 2)
2736extrude001 = extrude(profile001, length = 5)
2737extrudes = patternLinear3d(
2738 extrude001,
2739 instances = 3,
2740 distance = 5,
2741 axis = [1, 1, 0],
2742)
2743clone001 = map(extrudes, f = clone)
2744"#;
2745 parse_execute(ast).await.unwrap();
2746 }
2747
2748 #[tokio::test(flavor = "multi_thread")]
2749 async fn test_array_reduce_nested_array() {
2750 let code = r#"
2751fn id(@el, accum) { return accum }
2752
2753answer = reduce([], initial=[[[0,0]]], f=id)
2754"#;
2755 let result = parse_execute(code).await.unwrap();
2756 assert_eq!(
2757 mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2758 KclValue::HomArray {
2759 value: vec![KclValue::HomArray {
2760 value: vec![KclValue::HomArray {
2761 value: vec![
2762 KclValue::Number {
2763 value: 0.0,
2764 ty: NumericType::default(),
2765 meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2766 },
2767 KclValue::Number {
2768 value: 0.0,
2769 ty: NumericType::default(),
2770 meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2771 }
2772 ],
2773 ty: RuntimeType::any(),
2774 }],
2775 ty: RuntimeType::any(),
2776 }],
2777 ty: RuntimeType::any(),
2778 }
2779 );
2780 }
2781
2782 #[tokio::test(flavor = "multi_thread")]
2783 async fn test_zero_param_fn() {
2784 let ast = r#"sigmaAllow = 35000 // psi
2785leg1 = 5 // inches
2786leg2 = 8 // inches
2787fn thickness() { return 0.56 }
2788
2789bracket = startSketchOn(XY)
2790 |> startProfile(at = [0,0])
2791 |> line(end = [0, leg1])
2792 |> line(end = [leg2, 0])
2793 |> line(end = [0, -thickness()])
2794 |> line(end = [-leg2 + thickness(), 0])
2795"#;
2796 parse_execute(ast).await.unwrap();
2797 }
2798
2799 #[tokio::test(flavor = "multi_thread")]
2800 async fn test_unary_operator_not_succeeds() {
2801 let ast = r#"
2802fn returnTrue() { return !false }
2803t = true
2804f = false
2805notTrue = !t
2806notFalse = !f
2807c = !!true
2808d = !returnTrue()
2809
2810assertIs(!false, error = "expected to pass")
2811
2812fn check(x) {
2813 assertIs(!x, error = "expected argument to be false")
2814 return true
2815}
2816check(x = false)
2817"#;
2818 let result = parse_execute(ast).await.unwrap();
2819 assert_eq!(
2820 false,
2821 mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2822 .as_bool()
2823 .unwrap()
2824 );
2825 assert_eq!(
2826 true,
2827 mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2828 .as_bool()
2829 .unwrap()
2830 );
2831 assert_eq!(
2832 true,
2833 mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2834 .as_bool()
2835 .unwrap()
2836 );
2837 assert_eq!(
2838 false,
2839 mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2840 .as_bool()
2841 .unwrap()
2842 );
2843 }
2844
2845 #[tokio::test(flavor = "multi_thread")]
2846 async fn test_unary_operator_not_on_non_bool_fails() {
2847 let code1 = r#"
2848// Yup, this is null.
2849myNull = 0 / 0
2850notNull = !myNull
2851"#;
2852 assert_eq!(
2853 parse_execute(code1).await.unwrap_err().message(),
2854 "Cannot apply unary operator ! to non-boolean value: a number",
2855 );
2856
2857 let code2 = "notZero = !0";
2858 assert_eq!(
2859 parse_execute(code2).await.unwrap_err().message(),
2860 "Cannot apply unary operator ! to non-boolean value: a number",
2861 );
2862
2863 let code3 = r#"
2864notEmptyString = !""
2865"#;
2866 assert_eq!(
2867 parse_execute(code3).await.unwrap_err().message(),
2868 "Cannot apply unary operator ! to non-boolean value: a string",
2869 );
2870
2871 let code4 = r#"
2872obj = { a = 1 }
2873notMember = !obj.a
2874"#;
2875 assert_eq!(
2876 parse_execute(code4).await.unwrap_err().message(),
2877 "Cannot apply unary operator ! to non-boolean value: a number",
2878 );
2879
2880 let code5 = "
2881a = []
2882notArray = !a";
2883 assert_eq!(
2884 parse_execute(code5).await.unwrap_err().message(),
2885 "Cannot apply unary operator ! to non-boolean value: an empty array",
2886 );
2887
2888 let code6 = "
2889x = {}
2890notObject = !x";
2891 assert_eq!(
2892 parse_execute(code6).await.unwrap_err().message(),
2893 "Cannot apply unary operator ! to non-boolean value: an object",
2894 );
2895
2896 let code7 = "
2897fn x() { return 1 }
2898notFunction = !x";
2899 let fn_err = parse_execute(code7).await.unwrap_err();
2900 assert!(
2903 fn_err
2904 .message()
2905 .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2906 "Actual error: {fn_err:?}"
2907 );
2908
2909 let code8 = "
2910myTagDeclarator = $myTag
2911notTagDeclarator = !myTagDeclarator";
2912 let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2913 assert!(
2916 tag_declarator_err
2917 .message()
2918 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2919 "Actual error: {tag_declarator_err:?}"
2920 );
2921
2922 let code9 = "
2923myTagDeclarator = $myTag
2924notTagIdentifier = !myTag";
2925 let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2926 assert!(
2929 tag_identifier_err
2930 .message()
2931 .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2932 "Actual error: {tag_identifier_err:?}"
2933 );
2934
2935 let code10 = "notPipe = !(1 |> 2)";
2936 assert_eq!(
2937 parse_execute(code10).await.unwrap_err(),
2940 KclError::new_syntax(KclErrorDetails::new(
2941 "Unexpected token: !".to_owned(),
2942 vec![SourceRange::new(10, 11, ModuleId::default())],
2943 ))
2944 );
2945
2946 let code11 = "
2947fn identity(x) { return x }
2948notPipeSub = 1 |> identity(!%))";
2949 assert_eq!(
2950 parse_execute(code11).await.unwrap_err(),
2953 KclError::new_syntax(KclErrorDetails::new(
2954 "There was an unexpected `!`. Try removing it.".to_owned(),
2955 vec![SourceRange::new(56, 57, ModuleId::default())],
2956 ))
2957 );
2958
2959 }
2963
2964 #[tokio::test(flavor = "multi_thread")]
2965 async fn test_start_sketch_on_invalid_kwargs() {
2966 let current_dir = std::env::current_dir().unwrap();
2967 let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2968 let mut code = std::fs::read_to_string(&path).unwrap();
2969 assert_eq!(
2970 parse_execute(&code).await.unwrap_err().message(),
2971 "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2972 );
2973
2974 path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2975 code = std::fs::read_to_string(&path).unwrap();
2976
2977 assert_eq!(
2978 parse_execute(&code).await.unwrap_err().message(),
2979 "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2980 );
2981
2982 path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2983 code = std::fs::read_to_string(&path).unwrap();
2984
2985 assert_eq!(
2986 parse_execute(&code).await.unwrap_err().message(),
2987 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2988 );
2989
2990 path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2991 code = std::fs::read_to_string(&path).unwrap();
2992
2993 assert_eq!(
2994 parse_execute(&code).await.unwrap_err().message(),
2995 "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2996 );
2997
2998 path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2999 code = std::fs::read_to_string(&path).unwrap();
3000
3001 assert_eq!(
3002 parse_execute(&code).await.unwrap_err().message(),
3003 "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
3004 );
3005 }
3006
3007 #[tokio::test(flavor = "multi_thread")]
3008 async fn test_math_negative_variable_in_binary_expression() {
3009 let ast = r#"sigmaAllow = 35000 // psi
3010width = 1 // inch
3011
3012p = 150 // lbs
3013distance = 6 // inches
3014FOS = 2
3015
3016leg1 = 5 // inches
3017leg2 = 8 // inches
3018
3019thickness_squared = distance * p * FOS * 6 / sigmaAllow
3020thickness = 0.56 // inches. App does not support square root function yet
3021
3022bracket = startSketchOn(XY)
3023 |> startProfile(at = [0,0])
3024 |> line(end = [0, leg1])
3025 |> line(end = [leg2, 0])
3026 |> line(end = [0, -thickness])
3027 |> line(end = [-leg2 + thickness, 0])
3028"#;
3029 parse_execute(ast).await.unwrap();
3030 }
3031
3032 #[tokio::test(flavor = "multi_thread")]
3033 async fn test_execute_function_no_return() {
3034 let ast = r#"fn test(@origin) {
3035 origin
3036}
3037
3038test([0, 0])
3039"#;
3040 let result = parse_execute(ast).await;
3041 assert!(result.is_err());
3042 assert!(result.unwrap_err().to_string().contains("undefined"));
3043 }
3044
3045 #[tokio::test(flavor = "multi_thread")]
3046 async fn test_max_stack_size_exceeded_error() {
3047 let ast = r#"
3048fn forever(@n) {
3049 return 1 + forever(n)
3050}
3051
3052forever(1)
3053"#;
3054 let result = parse_execute(ast).await;
3055 let err = result.unwrap_err();
3056 assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
3057 }
3058
3059 #[tokio::test(flavor = "multi_thread")]
3060 async fn test_math_doubly_nested_parens() {
3061 let ast = r#"sigmaAllow = 35000 // psi
3062width = 4 // inch
3063p = 150 // Force on shelf - lbs
3064distance = 6 // inches
3065FOS = 2
3066leg1 = 5 // inches
3067leg2 = 8 // inches
3068thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
3069thickness = 0.32 // inches. App does not support square root function yet
3070bracket = startSketchOn(XY)
3071 |> startProfile(at = [0,0])
3072 |> line(end = [0, leg1])
3073 |> line(end = [leg2, 0])
3074 |> line(end = [0, -thickness])
3075 |> line(end = [-1 * leg2 + thickness, 0])
3076 |> line(end = [0, -1 * leg1 + thickness])
3077 |> close()
3078 |> extrude(length = width)
3079"#;
3080 parse_execute(ast).await.unwrap();
3081 }
3082
3083 #[tokio::test(flavor = "multi_thread")]
3084 async fn test_math_nested_parens_one_less() {
3085 let ast = r#" sigmaAllow = 35000 // psi
3086width = 4 // inch
3087p = 150 // Force on shelf - lbs
3088distance = 6 // inches
3089FOS = 2
3090leg1 = 5 // inches
3091leg2 = 8 // inches
3092thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
3093thickness = 0.32 // inches. App does not support square root function yet
3094bracket = startSketchOn(XY)
3095 |> startProfile(at = [0,0])
3096 |> line(end = [0, leg1])
3097 |> line(end = [leg2, 0])
3098 |> line(end = [0, -thickness])
3099 |> line(end = [-1 * leg2 + thickness, 0])
3100 |> line(end = [0, -1 * leg1 + thickness])
3101 |> close()
3102 |> extrude(length = width)
3103"#;
3104 parse_execute(ast).await.unwrap();
3105 }
3106
3107 #[tokio::test(flavor = "multi_thread")]
3108 async fn test_fn_as_operand() {
3109 let ast = r#"fn f() { return 1 }
3110x = f()
3111y = x + 1
3112z = f() + 1
3113w = f() + f()
3114"#;
3115 parse_execute(ast).await.unwrap();
3116 }
3117
3118 #[tokio::test(flavor = "multi_thread")]
3119 async fn kcl_test_ids_stable_between_executions() {
3120 let code = r#"sketch001 = startSketchOn(XZ)
3121|> startProfile(at = [61.74, 206.13])
3122|> xLine(length = 305.11, tag = $seg01)
3123|> yLine(length = -291.85)
3124|> xLine(length = -segLen(seg01))
3125|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3126|> close()
3127|> extrude(length = 40.14)
3128|> shell(
3129 thickness = 3.14,
3130 faces = [seg01]
3131)
3132"#;
3133
3134 let ctx = crate::test_server::new_context(true, None).await.unwrap();
3135 let old_program = crate::Program::parse_no_errs(code).unwrap();
3136
3137 if let Err(err) = ctx.run_with_caching(old_program).await {
3139 let report = err.into_miette_report_with_outputs(code).unwrap();
3140 let report = miette::Report::new(report);
3141 panic!("Error executing program: {report:?}");
3142 }
3143
3144 let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3146
3147 let code = r#"sketch001 = startSketchOn(XZ)
3148|> startProfile(at = [62.74, 206.13])
3149|> xLine(length = 305.11, tag = $seg01)
3150|> yLine(length = -291.85)
3151|> xLine(length = -segLen(seg01))
3152|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3153|> close()
3154|> extrude(length = 40.14)
3155|> shell(
3156 faces = [seg01],
3157 thickness = 3.14,
3158)
3159"#;
3160
3161 let program = crate::Program::parse_no_errs(code).unwrap();
3163 ctx.run_with_caching(program).await.unwrap();
3165
3166 let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
3167
3168 assert_eq!(id_generator, new_id_generator);
3169 }
3170
3171 #[tokio::test(flavor = "multi_thread")]
3172 async fn kcl_test_changing_a_setting_updates_the_cached_state() {
3173 let code = r#"sketch001 = startSketchOn(XZ)
3174|> startProfile(at = [61.74, 206.13])
3175|> xLine(length = 305.11, tag = $seg01)
3176|> yLine(length = -291.85)
3177|> xLine(length = -segLen(seg01))
3178|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
3179|> close()
3180|> extrude(length = 40.14)
3181|> shell(
3182 thickness = 3.14,
3183 faces = [seg01]
3184)
3185"#;
3186
3187 let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
3188 let old_program = crate::Program::parse_no_errs(code).unwrap();
3189
3190 ctx.run_with_caching(old_program.clone()).await.unwrap();
3192
3193 let settings_state = cache::read_old_ast().await.unwrap().settings;
3194
3195 assert_eq!(settings_state, ctx.settings);
3197
3198 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3200
3201 ctx.run_with_caching(old_program.clone()).await.unwrap();
3203
3204 let settings_state = cache::read_old_ast().await.unwrap().settings;
3205
3206 assert_eq!(settings_state, ctx.settings);
3208
3209 ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
3211
3212 ctx.run_with_caching(old_program).await.unwrap();
3214
3215 let settings_state = cache::read_old_ast().await.unwrap().settings;
3216
3217 assert_eq!(settings_state, ctx.settings);
3219
3220 ctx.close().await;
3221 }
3222
3223 #[tokio::test(flavor = "multi_thread")]
3224 async fn mock_after_not_mock() {
3225 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3226 let program = crate::Program::parse_no_errs("x = 2").unwrap();
3227 let result = ctx.run_with_caching(program).await.unwrap();
3228 assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
3229
3230 let ctx2 = ExecutorContext::new_mock(None).await;
3231 let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
3232 let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
3233 assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
3234
3235 ctx.close().await;
3236 ctx2.close().await;
3237 }
3238
3239 #[tokio::test(flavor = "multi_thread")]
3240 async fn mock_then_add_extrude_then_mock_again() {
3241 let code = "s = sketch(on = XY) {
3242 line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
3243 line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
3244 coincident([line1.end, line2.start])
3245 line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
3246 coincident([line2.end, line3.start])
3247 coincident([line1.start, line3.end])
3248}
3249 ";
3250 let ctx = ExecutorContext::new_mock(None).await;
3251 let program = crate::Program::parse_no_errs(code).unwrap();
3252 let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
3253 assert!(result.variables.contains_key("s"), "actual: {:?}", &result.variables);
3254
3255 let code2 = code.to_owned()
3256 + "
3257region001 = region(point = [1mm, 1mm], sketch = s)
3258extrude001 = extrude(region001, length = 1)
3259 ";
3260 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3261 let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
3262 assert!(
3263 result.variables.contains_key("region001"),
3264 "actual: {:?}",
3265 &result.variables
3266 );
3267
3268 ctx.close().await;
3269 }
3270
3271 #[cfg(feature = "artifact-graph")]
3272 #[tokio::test(flavor = "multi_thread")]
3273 async fn mock_has_stable_ids() {
3274 let ctx = ExecutorContext::new_mock(None).await;
3275 let mock_config = MockConfig {
3276 use_prev_memory: false,
3277 ..Default::default()
3278 };
3279 let code = "sk = startSketchOn(XY)
3280 |> startProfile(at = [0, 0])";
3281 let program = crate::Program::parse_no_errs(code).unwrap();
3282 let result = ctx.run_mock(&program, &mock_config).await.unwrap();
3283 let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3284 assert!(!ids.is_empty(), "IDs should not be empty");
3285
3286 let ctx2 = ExecutorContext::new_mock(None).await;
3287 let program2 = crate::Program::parse_no_errs(code).unwrap();
3288 let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
3289 let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
3290
3291 assert_eq!(ids, ids2, "Generated IDs should match");
3292 ctx.close().await;
3293 ctx2.close().await;
3294 }
3295
3296 #[tokio::test(flavor = "multi_thread")]
3297 async fn mock_memory_restore_preserves_module_maps() {
3298 clear_mem_cache().await;
3299
3300 let ctx = ExecutorContext::new_mock(None).await;
3301 let cold_start = MockConfig {
3302 use_prev_memory: false,
3303 ..Default::default()
3304 };
3305 ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
3306
3307 let mem = cache::read_old_memory().await.unwrap();
3308 assert!(
3309 mem.path_to_source_id.len() > 3,
3310 "expected prelude imports to populate multiple modules, got {:?}",
3311 mem.path_to_source_id
3312 );
3313
3314 let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
3315 ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
3316
3317 assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
3318 assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
3319 assert_eq!(exec_state.global.module_infos, mem.module_infos);
3320
3321 clear_mem_cache().await;
3322 ctx.close().await;
3323 }
3324
3325 #[tokio::test(flavor = "multi_thread")]
3326 async fn run_with_caching_no_action_refreshes_mock_memory() {
3327 cache::bust_cache().await;
3328 clear_mem_cache().await;
3329
3330 let ctx = ExecutorContext::new_with_engine(
3331 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
3332 Default::default(),
3333 );
3334 let program = crate::Program::parse_no_errs(
3335 r#"sketch001 = sketch(on = XY) {
3336 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
3337}
3338"#,
3339 )
3340 .unwrap();
3341
3342 ctx.run_with_caching(program.clone()).await.unwrap();
3343 let baseline_memory = cache::read_old_memory().await.unwrap();
3344 assert!(
3345 !baseline_memory.scene_objects.is_empty(),
3346 "expected engine execution to persist full-scene mock memory"
3347 );
3348
3349 cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
3350 assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
3351
3352 ctx.run_with_caching(program).await.unwrap();
3353 let refreshed_memory = cache::read_old_memory().await.unwrap();
3354 assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
3355 assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
3356 assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
3357
3358 cache::bust_cache().await;
3359 clear_mem_cache().await;
3360 ctx.close().await;
3361 }
3362
3363 #[cfg(feature = "artifact-graph")]
3364 #[tokio::test(flavor = "multi_thread")]
3365 async fn sim_sketch_mode_real_mock_real() {
3366 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3367 let code = r#"sketch001 = startSketchOn(XY)
3368profile001 = startProfile(sketch001, at = [0, 0])
3369 |> line(end = [10, 0])
3370 |> line(end = [0, 10])
3371 |> line(end = [-10, 0])
3372 |> line(end = [0, -10])
3373 |> close()
3374"#;
3375 let program = crate::Program::parse_no_errs(code).unwrap();
3376 let result = ctx.run_with_caching(program).await.unwrap();
3377 assert_eq!(result.operations.len(), 1);
3378
3379 let mock_ctx = ExecutorContext::new_mock(None).await;
3380 let mock_program = crate::Program::parse_no_errs(code).unwrap();
3381 let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
3382 assert_eq!(mock_result.operations.len(), 1);
3383
3384 let code2 = code.to_owned()
3385 + r#"
3386extrude001 = extrude(profile001, length = 10)
3387"#;
3388 let program2 = crate::Program::parse_no_errs(&code2).unwrap();
3389 let result = ctx.run_with_caching(program2).await.unwrap();
3390 assert_eq!(result.operations.len(), 2);
3391
3392 ctx.close().await;
3393 mock_ctx.close().await;
3394 }
3395
3396 #[tokio::test(flavor = "multi_thread")]
3397 async fn read_tag_version() {
3398 let ast = r#"fn bar(@t) {
3399 return startSketchOn(XY)
3400 |> startProfile(at = [0,0])
3401 |> angledLine(
3402 angle = -60,
3403 length = segLen(t),
3404 )
3405 |> line(end = [0, 0])
3406 |> close()
3407}
3408
3409sketch = startSketchOn(XY)
3410 |> startProfile(at = [0,0])
3411 |> line(end = [0, 10])
3412 |> line(end = [10, 0], tag = $tag0)
3413 |> line(endAbsolute = [0, 0])
3414
3415fn foo() {
3416 // tag0 tags an edge
3417 return bar(tag0)
3418}
3419
3420solid = sketch |> extrude(length = 10)
3421// tag0 tags a face
3422sketch2 = startSketchOn(solid, face = tag0)
3423 |> startProfile(at = [0,0])
3424 |> line(end = [0, 1])
3425 |> line(end = [1, 0])
3426 |> line(end = [0, 0])
3427
3428foo() |> extrude(length = 1)
3429"#;
3430 parse_execute(ast).await.unwrap();
3431 }
3432
3433 #[tokio::test(flavor = "multi_thread")]
3434 async fn experimental() {
3435 let code = r#"
3436startSketchOn(XY)
3437 |> startProfile(at = [0, 0], tag = $start)
3438 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3439"#;
3440 let result = parse_execute(code).await.unwrap();
3441 let issues = result.exec_state.issues();
3442 assert_eq!(issues.len(), 1);
3443 assert_eq!(issues[0].severity, Severity::Error);
3444 let msg = &issues[0].message;
3445 assert!(msg.contains("experimental"), "found {msg}");
3446
3447 let code = r#"@settings(experimentalFeatures = allow)
3448startSketchOn(XY)
3449 |> startProfile(at = [0, 0], tag = $start)
3450 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3451"#;
3452 let result = parse_execute(code).await.unwrap();
3453 let issues = result.exec_state.issues();
3454 assert!(issues.is_empty(), "issues={issues:#?}");
3455
3456 let code = r#"@settings(experimentalFeatures = warn)
3457startSketchOn(XY)
3458 |> startProfile(at = [0, 0], tag = $start)
3459 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3460"#;
3461 let result = parse_execute(code).await.unwrap();
3462 let issues = result.exec_state.issues();
3463 assert_eq!(issues.len(), 1);
3464 assert_eq!(issues[0].severity, Severity::Warning);
3465 let msg = &issues[0].message;
3466 assert!(msg.contains("experimental"), "found {msg}");
3467
3468 let code = r#"@settings(experimentalFeatures = deny)
3469startSketchOn(XY)
3470 |> startProfile(at = [0, 0], tag = $start)
3471 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3472"#;
3473 let result = parse_execute(code).await.unwrap();
3474 let issues = result.exec_state.issues();
3475 assert_eq!(issues.len(), 1);
3476 assert_eq!(issues[0].severity, Severity::Error);
3477 let msg = &issues[0].message;
3478 assert!(msg.contains("experimental"), "found {msg}");
3479
3480 let code = r#"@settings(experimentalFeatures = foo)
3481startSketchOn(XY)
3482 |> startProfile(at = [0, 0], tag = $start)
3483 |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
3484"#;
3485 parse_execute(code).await.unwrap_err();
3486 }
3487
3488 #[tokio::test(flavor = "multi_thread")]
3489 async fn experimental_parameter() {
3490 let code = r#"
3491fn inc(@x, @(experimental = true) amount? = 1) {
3492 return x + amount
3493}
3494
3495answer = inc(5, amount = 2)
3496"#;
3497 let result = parse_execute(code).await.unwrap();
3498 let issues = result.exec_state.issues();
3499 assert_eq!(issues.len(), 1);
3500 assert_eq!(issues[0].severity, Severity::Error);
3501 let msg = &issues[0].message;
3502 assert!(msg.contains("experimental"), "found {msg}");
3503
3504 let code = r#"
3506fn inc(@x, @(experimental = true) amount? = 1) {
3507 return x + amount
3508}
3509
3510answer = inc(5)
3511"#;
3512 let result = parse_execute(code).await.unwrap();
3513 let issues = result.exec_state.issues();
3514 assert!(issues.is_empty(), "issues={issues:#?}");
3515 }
3516
3517 #[tokio::test(flavor = "multi_thread")]
3518 async fn experimental_scalar_fixed_constraint() {
3519 let code_left = r#"@settings(experimentalFeatures = warn)
3520sketch(on = XY) {
3521 point1 = point(at = [var 0mm, var 0mm])
3522 point1.at[0] == 1mm
3523}
3524"#;
3525 let code_right = r#"@settings(experimentalFeatures = warn)
3527sketch(on = XY) {
3528 point1 = point(at = [var 0mm, var 0mm])
3529 1mm == point1.at[0]
3530}
3531"#;
3532
3533 for code in [code_left, code_right] {
3534 let result = parse_execute(code).await.unwrap();
3535 let issues = result.exec_state.issues();
3536 let Some(error) = issues
3537 .iter()
3538 .find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
3539 else {
3540 panic!("found {issues:#?}");
3541 };
3542 assert_eq!(error.severity, Severity::Warning);
3543 }
3544 }
3545
3546 #[tokio::test(flavor = "multi_thread")]
3550 async fn test_tangent_line_arc_executes_with_mock_engine() {
3551 let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
3552 parse_execute(&code).await.unwrap();
3553 }
3554
3555 #[tokio::test(flavor = "multi_thread")]
3556 async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
3557 let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
3558 parse_execute(&code).await.unwrap();
3559 }
3560
3561 #[tokio::test(flavor = "multi_thread")]
3562 async fn test_tangent_line_circle_executes_with_mock_engine() {
3563 let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
3564 parse_execute(&code).await.unwrap();
3565 }
3566
3567 #[tokio::test(flavor = "multi_thread")]
3568 async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
3569 let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
3570 parse_execute(&code).await.unwrap();
3571 }
3572
3573 #[cfg(feature = "artifact-graph")]
3578 async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
3579 let program = crate::Program::parse_no_errs(kcl).unwrap();
3580 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3581 let mut exec_state = ExecState::new(&ctx);
3582 let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
3583 let outcome = exec_state.into_exec_outcome(env_ref, &ctx).await;
3584 let report = outcome.sketch_constraint_report();
3585 ctx.close().await;
3586 report
3587 }
3588
3589 #[cfg(feature = "artifact-graph")]
3590 #[tokio::test(flavor = "multi_thread")]
3591 async fn test_constraint_report_fully_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}
3603"#;
3604 let report = run_constraint_report(kcl).await;
3605 assert_eq!(report.fully_constrained.len(), 1);
3606 assert_eq!(report.under_constrained.len(), 0);
3607 assert_eq!(report.over_constrained.len(), 0);
3608 assert_eq!(report.errors.len(), 0);
3609 assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
3610 }
3611
3612 #[cfg(feature = "artifact-graph")]
3613 #[tokio::test(flavor = "multi_thread")]
3614 async fn test_constraint_report_under_constrained() {
3615 let kcl = r#"
3617sketch(on = YZ) {
3618 line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
3619}
3620"#;
3621 let report = run_constraint_report(kcl).await;
3622 assert_eq!(report.fully_constrained.len(), 0);
3623 assert_eq!(report.under_constrained.len(), 1);
3624 assert_eq!(report.over_constrained.len(), 0);
3625 assert_eq!(report.errors.len(), 0);
3626 assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
3627 assert!(report.under_constrained[0].free_count > 0);
3628 }
3629
3630 #[cfg(feature = "artifact-graph")]
3631 #[tokio::test(flavor = "multi_thread")]
3632 async fn test_constraint_report_over_constrained() {
3633 let kcl = r#"
3635@settings(experimentalFeatures = allow)
3636
3637sketch(on = YZ) {
3638 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
3639 line1.start.at[0] == 2
3640 line1.start.at[1] == 8
3641 line1.end.at[0] == 5
3642 line1.end.at[1] == 7
3643 distance([line1.start, line1.end]) == 100mm
3644}
3645"#;
3646 let report = run_constraint_report(kcl).await;
3647 assert_eq!(report.over_constrained.len(), 1);
3648 assert_eq!(report.errors.len(), 0);
3649 assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
3650 assert!(report.over_constrained[0].conflict_count > 0);
3651 }
3652
3653 #[cfg(feature = "artifact-graph")]
3654 #[tokio::test(flavor = "multi_thread")]
3655 async fn test_constraint_report_multiple_sketches() {
3656 let kcl = r#"
3658@settings(experimentalFeatures = allow)
3659
3660s1 = sketch(on = YZ) {
3661 line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
3662 line1.start.at[0] == 2
3663 line1.start.at[1] == 8
3664 line1.end.at[0] == 5
3665 line1.end.at[1] == 7
3666}
3667
3668s2 = sketch(on = XZ) {
3669 line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
3670}
3671"#;
3672 let report = run_constraint_report(kcl).await;
3673 assert_eq!(
3674 report.fully_constrained.len()
3675 + report.under_constrained.len()
3676 + report.over_constrained.len()
3677 + report.errors.len(),
3678 2,
3679 "Expected 2 sketches total"
3680 );
3681 assert_eq!(report.fully_constrained.len(), 1);
3682 assert_eq!(report.under_constrained.len(), 1);
3683 }
3684}