1use async_recursion::async_recursion;
2use indexmap::IndexMap;
3
4use crate::CompilationIssue;
5use crate::NodePath;
6use crate::SourceRange;
7use crate::errors::KclError;
8use crate::errors::KclErrorDetails;
9use crate::execution::BodyType;
10use crate::execution::ExecState;
11use crate::execution::ExecutorContext;
12use crate::execution::Geometry;
13use crate::execution::KclValue;
14use crate::execution::KclValueControlFlow;
15use crate::execution::Metadata;
16use crate::execution::Solid;
17use crate::execution::StatementKind;
18use crate::execution::TagEngineInfo;
19use crate::execution::TagIdentifier;
20use crate::execution::annotations;
21use crate::execution::cad_op::Group;
22use crate::execution::cad_op::OpArg;
23use crate::execution::cad_op::OpKclValue;
24use crate::execution::cad_op::Operation;
25use crate::execution::control_continue;
26use crate::execution::kcl_value::FunctionBody;
27use crate::execution::kcl_value::FunctionSource;
28use crate::execution::kcl_value::NamedParam;
29use crate::execution::memory;
30use crate::execution::types::RuntimeType;
31use crate::parsing::ast::types::CallExpressionKw;
32use crate::parsing::ast::types::Node;
33use crate::parsing::ast::types::Type;
34use crate::std::ConsumedSolidArgCheck;
35use crate::std::solid_consumption::validate_value_not_consumed;
36use crate::std::solid_consumption::warn_if_value_consumed_for_deprecated_call;
37
38#[derive(Debug, Clone)]
39pub struct Args<Status: ArgsStatus = Desugared> {
40 pub fn_name: Option<String>,
42 pub unlabeled: Vec<(Option<String>, Arg)>,
46 pub labeled: IndexMap<String, Arg>,
48 pub source_range: SourceRange,
49 pub node_path: Option<NodePath>,
50 pub ctx: ExecutorContext,
51 pub pipe_value: Option<Arg>,
54 _status: std::marker::PhantomData<Status>,
55}
56
57pub trait ArgsStatus: std::fmt::Debug + Clone {}
58
59#[derive(Debug, Clone)]
60pub struct Sugary;
61impl ArgsStatus for Sugary {}
62
63#[derive(Debug, Clone)]
69pub struct Desugared;
70impl ArgsStatus for Desugared {}
71
72impl Args<Sugary> {
73 pub fn new(
75 labeled: IndexMap<String, Arg>,
76 unlabeled: Vec<(Option<String>, Arg)>,
77 source_range: SourceRange,
78 node_path: Option<NodePath>,
79 exec_state: &mut ExecState,
80 ctx: ExecutorContext,
81 fn_name: Option<String>,
82 ) -> Args<Sugary> {
83 Args {
84 fn_name,
85 labeled,
86 unlabeled,
87 source_range,
88 node_path,
89 ctx,
90 pipe_value: exec_state.pipe_value().map(|v| Arg::new(v.clone(), source_range)),
91 _status: std::marker::PhantomData,
92 }
93 }
94}
95
96impl<Status: ArgsStatus> Args<Status> {
97 pub fn len(&self) -> usize {
99 self.labeled.len() + self.unlabeled.len()
100 }
101
102 pub fn is_empty(&self) -> bool {
104 self.labeled.is_empty() && self.unlabeled.is_empty()
105 }
106}
107
108impl Args<Desugared> {
109 pub fn new_no_args(
110 source_range: SourceRange,
111 node_path: Option<NodePath>,
112 ctx: ExecutorContext,
113 fn_name: Option<String>,
114 ) -> Args {
115 Args {
116 fn_name,
117 unlabeled: Default::default(),
118 labeled: Default::default(),
119 source_range,
120 node_path,
121 ctx,
122 pipe_value: None,
123 _status: std::marker::PhantomData,
124 }
125 }
126
127 pub(crate) fn unlabeled_kw_arg_unconverted(&self) -> Option<&Arg> {
129 self.unlabeled.first().map(|(_, a)| a)
130 }
131}
132
133#[derive(Debug, Clone)]
134pub struct Arg {
135 pub value: KclValue,
137 pub source_range: SourceRange,
139}
140
141impl Arg {
142 pub fn new(value: KclValue, source_range: SourceRange) -> Self {
143 Self { value, source_range }
144 }
145
146 pub fn synthetic(value: KclValue) -> Self {
147 Self {
148 value,
149 source_range: SourceRange::synthetic(),
150 }
151 }
152
153 pub fn source_ranges(&self) -> Vec<SourceRange> {
154 vec![self.source_range]
155 }
156}
157
158impl Node<CallExpressionKw> {
159 #[async_recursion]
160 pub(super) async fn execute(
161 &self,
162 exec_state: &mut ExecState,
163 ctx: &ExecutorContext,
164 ) -> Result<KclValueControlFlow, KclError> {
165 let fn_name = &self.callee;
166 let callsite: SourceRange = self.into();
167
168 let func: KclValue = fn_name.get_result(exec_state, ctx).await?;
171
172 let Some(fn_src) = func.as_function() else {
173 return Err(KclError::new_semantic(KclErrorDetails::new(
174 "cannot call this because it isn't a function".to_string(),
175 vec![callsite],
176 )));
177 };
178
179 let mut fn_args = IndexMap::with_capacity(self.arguments.len());
181 let mut unlabeled = Vec::new();
182
183 if let Some(ref arg_expr) = self.unlabeled {
185 let source_range = SourceRange::from(arg_expr.clone());
186 let metadata = Metadata { source_range };
187 let value_cf = ctx
188 .execute_expr(arg_expr, exec_state, &metadata, &[], StatementKind::Expression)
189 .await?;
190 let value = control_continue!(value_cf);
191
192 let label = arg_expr.ident_name().map(str::to_owned);
193
194 unlabeled.push((label, Arg::new(value, source_range)))
195 }
196
197 for arg_expr in &self.arguments {
198 let source_range = SourceRange::from(arg_expr.arg.clone());
199 let metadata = Metadata { source_range };
200 let value_cf = ctx
201 .execute_expr(&arg_expr.arg, exec_state, &metadata, &[], StatementKind::Expression)
202 .await?;
203 let value = control_continue!(value_cf);
204 let arg = Arg::new(value, source_range);
205 match &arg_expr.label {
206 Some(l) => {
207 fn_args.insert(l.name.clone(), arg);
208 }
209 None => {
210 unlabeled.push((arg_expr.arg.ident_name().map(str::to_owned), arg));
211 }
212 }
213 }
214
215 let args = Args::new(
216 fn_args,
217 unlabeled,
218 callsite,
219 self.node_path.clone(),
220 exec_state,
221 ctx.clone(),
222 Some(fn_name.name.name.clone()),
223 );
224
225 let return_value = fn_src
226 .call_kw(Some(fn_name.to_string()), exec_state, ctx, args, callsite)
227 .await
228 .map_err(|e| {
229 e.add_unwind_location(Some(fn_name.name.name.clone()), callsite)
234 })?;
235
236 let result = return_value.ok_or_else(move || {
237 let mut source_ranges: Vec<SourceRange> = vec![callsite];
238 if let KclValue::Function { meta, .. } = func {
240 source_ranges = meta.iter().map(|m| m.source_range).collect();
241 };
242 KclError::new_undefined_value(
243 KclErrorDetails::new(
244 format!("Result of user-defined function {fn_name} is undefined"),
245 source_ranges,
246 ),
247 None,
248 )
249 })?;
250
251 Ok(result)
252 }
253}
254
255impl FunctionSource {
256 pub(crate) async fn call_kw(
257 &self,
258 fn_name: Option<String>,
259 exec_state: &mut ExecState,
260 ctx: &ExecutorContext,
261 args: Args<Sugary>,
262 callsite: SourceRange,
263 ) -> Result<Option<KclValueControlFlow>, KclError> {
264 exec_state.inc_call_stack_size(callsite)?;
265
266 let result = self.inner_call_kw(fn_name, exec_state, ctx, args, callsite).await;
267
268 exec_state.dec_call_stack_size(callsite)?;
269 result
270 }
271
272 async fn inner_call_kw(
273 &self,
274 fn_name: Option<String>,
275 exec_state: &mut ExecState,
276 ctx: &ExecutorContext,
277 args: Args<Sugary>,
278 callsite: SourceRange,
279 ) -> Result<Option<KclValueControlFlow>, KclError> {
280 if self.deprecated {
281 exec_state.warn(
282 CompilationIssue::err(
283 callsite,
284 format!(
285 "{} is deprecated, see the docs for a recommended replacement",
286 match &fn_name {
287 Some(n) => format!("`{n}`"),
288 None => "This function".to_owned(),
289 }
290 ),
291 ),
292 annotations::WARN_DEPRECATED,
293 );
294 } else if let Some(since) = &self.deprecated_since
295 && annotations::version_ge(&exec_state.mod_local.settings.kcl_version, since)
296 {
297 exec_state.warn(
298 CompilationIssue::err(
299 callsite,
300 format!(
301 "{} is deprecated as of KCL {since}. See the docs for a recommended replacement.",
302 match &fn_name {
303 Some(n) => format!("`{n}`"),
304 None => "This function".to_owned(),
305 }
306 ),
307 ),
308 annotations::WARN_DEPRECATED,
309 );
310 }
311 if self.experimental {
312 exec_state.warn_experimental(
313 &match &fn_name {
314 Some(n) => format!("`{n}`"),
315 None => "This function".to_owned(),
316 },
317 callsite,
318 );
319 }
320
321 let args = type_check_params_kw(fn_name.as_deref(), self, args, exec_state)?;
322 let face_tag_names = face_tag_names_for_call(self, &args);
323
324 for (label, arg) in &args.labeled {
326 let Some(param) = self.named_args.get(label.as_str()) else {
327 continue;
328 };
329 if param.experimental {
330 exec_state.warn_experimental(
331 &match &fn_name {
332 Some(f) => format!("`{f}({label})`"),
333 None => label.to_owned(),
334 },
335 arg.source_range,
336 );
337 }
338 let deprecation_suffix = if param.deprecated {
341 Some("is deprecated, see the docs for a recommended replacement".to_owned())
342 } else if let Some(since) = ¶m.deprecated_since
343 && annotations::version_ge(&exec_state.mod_local.settings.kcl_version, since)
344 {
345 Some(format!(
346 "is deprecated as of KCL {since}. See the docs for a recommended replacement."
347 ))
348 } else {
349 None
350 };
351 if let Some(suffix) = deprecation_suffix {
352 let qualified = match &fn_name {
353 Some(f) => format!("`{f}({label})`"),
354 None => format!("`{label}`"),
355 };
356 exec_state.warn(
357 CompilationIssue::err(arg.source_range, format!("{qualified} {suffix}")),
358 annotations::WARN_DEPRECATED,
359 );
360 }
361 }
362
363 self.body.prep_mem(exec_state)?;
365
366 let would_trace_stdlib_internals = exec_state.mod_local.inside_stdlib && self.is_std();
378 let should_track_operation = !would_trace_stdlib_internals && self.include_in_feature_tree;
380 let op = if should_track_operation {
381 let op_labeled_args = args
382 .labeled
383 .iter()
384 .map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range)))
385 .collect();
386
387 if self.is_std() {
389 Some(Operation::StdLibCall {
390 name: fn_name.clone().unwrap_or_else(|| "unknown function".to_owned()),
391 unlabeled_arg: args
392 .unlabeled_kw_arg_unconverted()
393 .map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
394 labeled_args: op_labeled_args,
395 node_path: NodePath::placeholder(),
396 source_range: callsite,
397 stdlib_entry_source_range: exec_state.mod_local.stdlib_entry_source_range,
398 is_error: false,
399 })
400 } else {
401 exec_state.push_op(Operation::GroupBegin {
403 group: Group::FunctionCall {
404 name: fn_name.clone(),
405 function_source_range: self.ast.as_source_range(),
406 unlabeled_arg: args
407 .unlabeled_kw_arg_unconverted()
408 .map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
409 labeled_args: op_labeled_args,
410 },
411 node_path: NodePath::placeholder(),
412 source_range: callsite,
413 });
414
415 None
416 }
417 } else {
418 None
419 };
420
421 let is_calling_into_stdlib = match &self.body {
422 FunctionBody::Rust(_) => true,
423 FunctionBody::Kcl(_) => self.is_std(),
424 };
425 let is_crossing_into_stdlib = is_calling_into_stdlib && !exec_state.mod_local.inside_stdlib;
426 let is_crossing_out_of_stdlib = !is_calling_into_stdlib && exec_state.mod_local.inside_stdlib;
427 let stdlib_entry_source_range = if is_crossing_into_stdlib {
428 Some(callsite)
432 } else if is_crossing_out_of_stdlib {
433 None
437 } else {
438 exec_state.mod_local.stdlib_entry_source_range
441 };
442
443 let prev_inside_stdlib = std::mem::replace(&mut exec_state.mod_local.inside_stdlib, is_calling_into_stdlib);
444 let prev_stdlib_entry_source_range = std::mem::replace(
445 &mut exec_state.mod_local.stdlib_entry_source_range,
446 stdlib_entry_source_range,
447 );
448 let result = match &self.body {
452 FunctionBody::Rust(f) => f(exec_state, args).await.map(Some),
453 FunctionBody::Kcl(_) => {
454 if let Err(e) = assign_args_to_params_kw(self, args, exec_state) {
455 exec_state.mod_local.inside_stdlib = prev_inside_stdlib;
456 exec_state.mut_stack().pop_env()?;
457 return Err(e);
458 }
459
460 ctx.exec_block(&self.ast.body, exec_state, BodyType::Block)
461 .await
462 .map(|cf| {
463 if let Some(cf) = cf
464 && cf.is_some_return()
465 {
466 return Some(cf);
467 }
468 exec_state
471 .stack()
472 .get(memory::RETURN_NAME, self.ast.as_source_range())
473 .ok()
474 .map(KclValue::continue_)
475 })
476 }
477 };
478 exec_state.mod_local.inside_stdlib = prev_inside_stdlib;
479 exec_state.mod_local.stdlib_entry_source_range = prev_stdlib_entry_source_range;
480 exec_state.mut_stack().pop_env()?;
481
482 if should_track_operation {
483 if let Some(mut op) = op {
484 op.set_std_lib_call_is_error(result.is_err());
485 exec_state.push_op(op);
491 } else if !is_calling_into_stdlib {
492 exec_state.push_op(Operation::GroupEnd);
493 }
494 }
495
496 let mut result = match result {
497 Ok(Some(value)) => {
498 if value.is_some_return() {
499 return Ok(Some(value));
500 } else {
501 Ok(Some(value.into_value()))
502 }
503 }
504 Ok(None) => Ok(None),
505 Err(e) => Err(e),
506 };
507
508 if self.is_std()
509 && let Ok(Some(result)) = &mut result
510 {
511 update_memory_for_tags_of_geometry(result, exec_state)?;
512 if !face_tag_names.is_empty() {
513 attach_face_tags_to_geometry(result, exec_state, &face_tag_names);
514 }
515 }
516
517 coerce_result_type(result, self, exec_state).map(|r| r.map(KclValue::continue_))
518 }
519}
520
521impl FunctionBody {
522 fn prep_mem(&self, exec_state: &mut ExecState) -> Result<(), KclError> {
523 match self {
524 FunctionBody::Rust(_) => exec_state.mut_stack().push_new_root_env(true),
525 FunctionBody::Kcl(memory) => exec_state.mut_stack().push_new_env_for_call(*memory),
526 }
527 }
528}
529
530fn originates_from_sketch_block(value: &KclValue) -> bool {
531 match value {
532 KclValue::Uuid { .. } => false,
533 KclValue::Bool { .. } => false,
534 KclValue::Number { .. } => false,
535 KclValue::String { .. } => false,
536 KclValue::SketchVar { .. } => true,
537 KclValue::SketchConstraint { .. } => true,
538 KclValue::Tuple { value, .. } => value.iter().all(originates_from_sketch_block),
539 KclValue::HomArray { value, .. } => value.iter().all(originates_from_sketch_block),
540 KclValue::Object { value, .. } => value.values().all(originates_from_sketch_block),
542 KclValue::TagIdentifier(_) => false,
543 KclValue::TagDeclarator(_) => false,
544 KclValue::GdtAnnotation { .. } => false,
545 KclValue::Plane { .. } => false,
546 KclValue::Face { .. } => false,
547 KclValue::BoundedEdge { .. } => false,
548 KclValue::Segment { .. } => true,
549 KclValue::Sketch { value: sketch } => sketch.origin_sketch_id.is_some(),
550 KclValue::Solid { value: solid } => solid
551 .sketch()
552 .map(|sketch| sketch.origin_sketch_id.is_some())
553 .unwrap_or(false),
554 KclValue::Helix { .. } => false,
555 KclValue::ImportedGeometry(_) => false,
556 KclValue::Function { .. } => false,
557 KclValue::Module { .. } => false,
558 KclValue::Type { .. } => false,
559 KclValue::KclNone { .. } => false,
560 }
561}
562
563fn face_tag_names_for_call(fn_def: &FunctionSource, args: &Args<Desugared>) -> Vec<String> {
564 let Some(std_props) = &fn_def.std_props else {
565 return Vec::new();
566 };
567
568 if !std_function_allows_face_tags(&std_props.name) {
569 return Vec::new();
570 }
571
572 args.labeled
573 .iter()
574 .filter(|(label, _)| matches!(label.as_str(), "tag" | "tagStart" | "tagEnd"))
575 .filter_map(|(_, arg)| match &arg.value {
576 KclValue::TagDeclarator(tag) => Some(tag.name.clone()),
577 _ => None,
578 })
579 .collect()
580}
581
582fn std_function_allows_face_tags(std_fn_name: &str) -> bool {
583 matches!(
584 std_fn_name,
585 "std::sketch::extrude"
586 | "std::solid::chamfer"
587 | "std::solid::fillet"
588 | "std::sketch::sweep"
589 | "std::sketch::loft"
590 | "std::sketch::revolve"
591 )
592}
593
594fn attach_face_tags_to_geometry(result: &mut KclValue, exec_state: &ExecState, tag_names: &[String]) {
595 match result {
596 KclValue::Solid { value } => attach_face_tags_to_solid(value, exec_state, tag_names),
597 KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
598 for v in value {
599 attach_face_tags_to_geometry(v, exec_state, tag_names);
600 }
601 }
602 _ => {}
603 }
604}
605
606fn attach_face_tags_to_solid(solid: &mut Solid, exec_state: &ExecState, tag_names: &[String]) {
607 let surfaces = solid.value.clone();
608 for surface in surfaces {
609 let Some(tag) = surface.get_tag() else {
610 continue;
611 };
612 if !tag_names.iter().any(|tag_name| tag_name == &tag.name) {
613 continue;
614 }
615
616 let tag_id = solid
617 .sketch()
618 .and_then(|sketch| sketch.tags.get(&tag.name))
619 .cloned()
620 .unwrap_or_else(|| {
621 let mut solid_copy = solid.clone();
622 clear_tags_from_solid_copy(&mut solid_copy);
623 TagIdentifier {
624 value: tag.name.clone(),
625 info: vec![(
626 exec_state.stack().current_epoch(),
627 TagEngineInfo {
628 id: surface.get_id(),
629 surface: Some(surface.clone()),
630 path: None,
631 geometry: Geometry::Solid(solid_copy),
632 },
633 )],
634 meta: vec![Metadata {
635 source_range: tag.clone().into(),
636 }],
637 }
638 });
639
640 match solid.faces.get_mut(&tag.name) {
641 Some(existing_tag) => existing_tag.merge_info(&tag_id),
642 None => {
643 solid.faces.insert(tag.name.clone(), tag_id);
644 }
645 }
646 }
647}
648
649fn clear_tags_from_solid_copy(solid: &mut Solid) {
650 if let Some(sketch) = solid.sketch_mut() {
651 sketch.tags.clear(); }
653 solid.faces.clear();
654}
655
656fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut ExecState) -> Result<(), KclError> {
657 let is_sketch_block = originates_from_sketch_block(&*result);
658 match result {
663 KclValue::Sketch { value } if !is_sketch_block => {
664 for (name, tag) in value.tags.iter() {
665 if exec_state.stack().cur_frame_contains(name)? {
666 exec_state.mut_stack().update(name, |v, _| {
667 if let Some(existing_tag) = v.as_mut_tag() {
668 existing_tag.merge_info(tag);
669 }
670 })?;
671 } else {
672 exec_state.mut_stack().add(
673 name.to_owned(),
674 KclValue::TagIdentifier(Box::new(tag.clone())),
675 SourceRange::default(),
676 )?;
677 }
678 }
679 }
680 KclValue::Solid { value } => {
681 let surfaces = value.value.clone();
682 if value.sketch_mut().is_none() {
683 return Ok(());
686 };
687 let solid_copies: Vec<Box<Solid>> = surfaces.iter().map(|_| value.clone()).collect();
690 let Some(sketch) = value.sketch_mut() else {
693 return Ok(());
694 };
695 for (v, mut solid_copy) in surfaces.iter().zip(solid_copies) {
696 clear_tags_from_solid_copy(&mut solid_copy);
697 if let Some(tag) = v.get_tag() {
698 let mut is_part_of_sketch = false;
700 let tag_id = if let Some(t) = sketch.tags.get(&tag.name) {
701 is_part_of_sketch = true;
702 let mut t = t.clone();
703 let Some(info) = t.get_cur_info() else {
704 return Err(KclError::new_internal(KclErrorDetails::new(
705 format!("Tag {} does not have path info", tag.name),
706 vec![tag.into()],
707 )));
708 };
709
710 let mut info = info.clone();
711 info.id = v.get_id();
712 info.surface = Some(v.clone());
713 info.geometry = Geometry::Solid(*solid_copy);
714 t.info.push((exec_state.stack().current_epoch(), info));
715 t
716 } else {
717 TagIdentifier {
720 value: tag.name.clone(),
721 info: vec![(
722 exec_state.stack().current_epoch(),
723 TagEngineInfo {
724 id: v.get_id(),
725 surface: Some(v.clone()),
726 path: None,
727 geometry: Geometry::Solid(*solid_copy),
728 },
729 )],
730 meta: vec![Metadata {
731 source_range: tag.clone().into(),
732 }],
733 }
734 };
735
736 sketch.merge_tags(Some(&tag_id).into_iter());
738
739 if exec_state.stack().cur_frame_contains(&tag.name)? {
740 exec_state.mut_stack().update(&tag.name, |v, _| {
741 if let Some(existing_tag) = v.as_mut_tag() {
742 existing_tag.merge_info(&tag_id);
743 }
744 })?;
745 } else if !is_sketch_block || !is_part_of_sketch {
746 exec_state.mut_stack().add(
756 tag.name.clone(),
757 KclValue::TagIdentifier(Box::new(tag_id)),
758 SourceRange::default(),
759 )?;
760 }
761 }
762 }
763
764 if let Some(sketch) = value.sketch() {
766 if sketch.tags.is_empty() {
767 return Ok(());
768 }
769 let sketch_tags: Vec<_> = sketch.tags.values().cloned().collect();
770 let sketches_to_update: Vec<_> = exec_state.stack().find_keys_in_current_env(|v| match v {
771 KclValue::Sketch { value: sk } => sk.original_id == sketch.original_id,
772 _ => false,
773 })?;
774
775 for k in sketches_to_update {
776 exec_state.mut_stack().update(&k, |v, _| {
777 if let Some(sketch) = v.as_mut_sketch() {
778 sketch.merge_tags(sketch_tags.iter());
779 }
780 })?;
781 }
782 }
783 }
784 KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
785 for v in value {
786 update_memory_for_tags_of_geometry(v, exec_state)?;
787 }
788 }
789 _ => {}
790 }
791 Ok(())
792}
793
794fn type_err_str(expected: &Type, found: &KclValue, source_range: &SourceRange, exec_state: &mut ExecState) -> String {
795 fn strip_backticks(s: &str) -> &str {
796 let mut result = s;
797 if s.starts_with('`') {
798 result = &result[1..]
799 }
800 if s.ends_with('`') {
801 result = &result[..result.len() - 1]
802 }
803 result
804 }
805
806 let expected_human = expected.human_friendly_type();
807 let expected_ty = expected.to_string();
808 let expected_str =
809 if expected_human == expected_ty || expected_human == format!("a value with type `{expected_ty}`") {
810 format!("a value with type `{expected_ty}`")
811 } else {
812 format!("{expected_human} (`{expected_ty}`)")
813 };
814 let found_human = found.human_friendly_type();
815 let found_ty = found.principal_type_string();
816 let found_str = if found_human == found_ty || found_human == format!("a {}", strip_backticks(&found_ty)) {
817 format!("a value with type {found_ty}")
818 } else {
819 format!("{found_human} (with type {found_ty})")
820 };
821
822 let mut result = format!("{expected_str}, but found {found_str}.");
823
824 if found.is_unknown_number() {
825 exec_state.clear_units_warnings(source_range);
826 result.push_str("\nThe found value is a number but has incomplete units information. You can probably fix this error by specifying the units using type ascription, e.g., `len: mm` or `(a * b): deg`.");
827 }
828
829 result
830}
831
832fn type_check_params_kw(
833 fn_name: Option<&str>,
834 fn_def: &FunctionSource,
835 mut args: Args<Sugary>,
836 exec_state: &mut ExecState,
837) -> Result<Args<Desugared>, KclError> {
838 let fn_name = fn_name.or(args.fn_name.as_deref());
839 let mut result = Args::new_no_args(
840 args.source_range,
841 args.node_path.clone(),
842 args.ctx,
843 fn_name.map(|f| f.to_string()).or_else(|| args.fn_name.clone()),
844 );
845
846 if let Some((Some(label), _)) = args.unlabeled.first()
849 && args.unlabeled.len() == 1
850 && (fn_def.input_arg.is_none() || args.pipe_value.is_some())
851 && fn_def.named_args.iter().any(|p| p.0 == label)
852 && !args.labeled.contains_key(label)
853 {
854 let Some((label, arg)) = args.unlabeled.pop() else {
855 let message = "Expected unlabeled arg to be present".to_owned();
856 debug_assert!(false, "{}", &message);
857 return Err(KclError::new_internal(KclErrorDetails::new(
858 message,
859 vec![args.source_range],
860 )));
861 };
862 args.labeled.insert(label.unwrap(), arg);
863 }
864
865 let (labeled_unlabeled, unlabeled_unlabeled) = args.unlabeled.into_iter().partition(|(l, _)| {
867 if let Some(l) = l
868 && fn_def.named_args.contains_key(l)
869 && !args.labeled.contains_key(l)
870 {
871 true
872 } else {
873 false
874 }
875 });
876 args.unlabeled = unlabeled_unlabeled;
877 for (l, arg) in labeled_unlabeled {
878 let previous = args.labeled.insert(l.unwrap(), arg);
879 debug_assert!(previous.is_none());
880 }
881
882 if let Some((name, ty)) = &fn_def.input_arg {
883 if args.unlabeled.is_empty() {
886 if let Some(pipe) = args.pipe_value {
889 result.unlabeled = vec![(None, pipe)];
891 } else if let Some(arg) = args.labeled.swap_remove(name) {
892 exec_state.err(CompilationIssue::err(
894 arg.source_range,
895 format!(
896 "{} expects an unlabeled first argument (`@{name}`), but it is labelled in the call. You might try removing the `{name} = `",
897 fn_name
898 .map(|n| format!("The function `{n}`"))
899 .unwrap_or_else(|| "This function".to_owned()),
900 ),
901 ));
902 result.unlabeled = vec![(Some(name.clone()), arg)];
903 } else {
904 return Err(KclError::new_argument(KclErrorDetails::new(
906 "This function expects an unlabeled first parameter, but you haven't passed it one.".to_owned(),
907 fn_def.ast.as_source_ranges(),
908 )));
909 }
910 } else if args.unlabeled.len() == 1
911 && let Some(unlabeled_arg) = args.unlabeled.pop()
912 {
913 let mut arg = unlabeled_arg.1;
914 if let Some(ty) = ty {
915 let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range, false, true)
918 .map_err(|e| KclError::new_semantic(e.into()))?;
919 arg.value = arg.value.coerce(&rty, true, exec_state).map_err(|_| {
920 KclError::new_argument(KclErrorDetails::new(
921 format!(
922 "The input argument of {} requires {}",
923 fn_name
924 .map(|n| format!("`{n}`"))
925 .unwrap_or_else(|| "this function".to_owned()),
926 type_err_str(ty, &arg.value, &arg.source_range, exec_state),
927 ),
928 vec![arg.source_range],
929 ))
930 })?;
931 }
932 result.unlabeled = vec![(None, arg)]
933 } else {
934 if let Some(Type::Array { len, .. }) = ty {
938 if len.satisfied(args.unlabeled.len(), false).is_none() {
939 exec_state.err(CompilationIssue::err(
940 args.source_range,
941 format!(
942 "{} expects an array input argument with {} elements",
943 fn_name
944 .map(|n| format!("The function `{n}`"))
945 .unwrap_or_else(|| "This function".to_owned()),
946 len.human_friendly_type(),
947 ),
948 ));
949 }
950
951 let source_range = SourceRange::merge(args.unlabeled.iter().map(|(_, a)| a.source_range));
952 exec_state.warn_experimental("array input arguments", source_range);
953 result.unlabeled = vec![(
954 None,
955 Arg {
956 source_range,
957 value: KclValue::HomArray {
958 value: args.unlabeled.drain(..).map(|(_, a)| a.value).collect(),
959 ty: RuntimeType::any(),
960 },
961 },
962 )]
963 }
964 }
965 }
966
967 if !args.unlabeled.is_empty() {
969 let actuals = args.labeled.keys();
971 let formals: Vec<_> = fn_def
972 .named_args
973 .keys()
974 .filter_map(|name| {
975 if actuals.clone().any(|a| a == name) {
976 return None;
977 }
978
979 Some(format!("`{name}`"))
980 })
981 .collect();
982
983 let suggestion = if formals.is_empty() {
984 String::new()
985 } else {
986 format!("; suggested labels: {}", formals.join(", "))
987 };
988
989 let mut errors = args.unlabeled.iter().map(|(_, arg)| {
990 CompilationIssue::err(
991 arg.source_range,
992 format!("This argument needs a label, but it doesn't have one{suggestion}"),
993 )
994 });
995
996 let first = errors.next().unwrap();
997 errors.for_each(|e| exec_state.err(e));
998
999 return Err(KclError::new_argument(first.into()));
1000 }
1001
1002 for (label, mut arg) in args.labeled {
1003 match fn_def.named_args.get(&label) {
1004 Some(NamedParam {
1005 experimental: _,
1006 deprecated: _,
1007 deprecated_since: _,
1008 default_value: def,
1009 ty,
1010 }) => {
1011 if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) {
1013 if let Some(ty) = ty {
1014 let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range, false, true)
1018 .map_err(|e| KclError::new_semantic(e.into()))?;
1019 arg.value = arg
1020 .value
1021 .coerce(
1022 &rty,
1023 true,
1024 exec_state,
1025 )
1026 .map_err(|e| {
1027 let mut message = format!(
1028 "{label} requires {}",
1029 type_err_str(ty, &arg.value, &arg.source_range, exec_state),
1030 );
1031 if let Some(ty) = e.explicit_coercion {
1032 message = format!("{message}\n\nYou may need to add information about the type of the argument, for example:\n using a numeric suffix: `42{ty}`\n or using type ascription: `foo(): {ty}`");
1034 }
1035 KclError::new_argument(KclErrorDetails::new(
1036 message,
1037 vec![arg.source_range],
1038 ))
1039 })?;
1040 }
1041 result.labeled.insert(label, arg);
1042 }
1043 }
1044 None => {
1045 exec_state.err(CompilationIssue::err(
1046 arg.source_range,
1047 format!(
1048 "`{label}` is not an argument of {}",
1049 fn_name
1050 .map(|n| format!("`{n}`"))
1051 .unwrap_or_else(|| "this function".to_owned()),
1052 ),
1053 ));
1054 }
1055 }
1056 }
1057
1058 let consumed_solid_arg_check = fn_def
1059 .std_props
1060 .as_ref()
1061 .map_or(ConsumedSolidArgCheck::Error, |props| props.consumed_solid_arg_check);
1062 match consumed_solid_arg_check {
1063 ConsumedSolidArgCheck::Error => {
1064 result
1065 .unlabeled
1066 .iter()
1067 .map(|(_, arg)| arg)
1068 .chain(result.labeled.values())
1069 .try_for_each(|arg| validate_value_not_consumed(&arg.value, exec_state, arg.source_range))?;
1070 }
1071 ConsumedSolidArgCheck::WarnDeprecated => {
1072 let std_fn_name = fn_def
1073 .std_props
1074 .as_ref()
1075 .map(|props| props.name.as_str())
1076 .unwrap_or("function");
1077 for arg in result
1078 .unlabeled
1079 .iter()
1080 .map(|(_, arg)| arg)
1081 .chain(result.labeled.values())
1082 {
1083 warn_if_value_consumed_for_deprecated_call(&arg.value, exec_state, arg.source_range, std_fn_name)?;
1084 }
1085 }
1086 }
1087
1088 Ok(result)
1089}
1090
1091fn assign_args_to_params_kw(
1092 fn_def: &FunctionSource,
1093 args: Args<Desugared>,
1094 exec_state: &mut ExecState,
1095) -> Result<(), KclError> {
1096 let source_ranges = fn_def.ast.as_source_ranges();
1099
1100 for (name, param) in fn_def.named_args.iter() {
1101 let arg = args.labeled.get(name);
1102 match arg {
1103 Some(arg) => {
1104 exec_state.mut_stack().add(
1105 name.clone(),
1106 arg.value.clone(),
1107 arg.source_ranges().pop().unwrap_or(SourceRange::synthetic()),
1108 )?;
1109 }
1110 None => match ¶m.default_value {
1111 Some(default_val) => {
1112 let value = KclValue::from_default_param(default_val.clone(), exec_state);
1113 exec_state
1114 .mut_stack()
1115 .add(name.clone(), value, default_val.source_range())?;
1116 }
1117 None => {
1118 return Err(KclError::new_argument(KclErrorDetails::new(
1119 format!("This function requires a parameter {name}, but you haven't passed it one."),
1120 source_ranges,
1121 )));
1122 }
1123 },
1124 }
1125 }
1126
1127 if let Some((param_name, _)) = &fn_def.input_arg {
1128 let Some(unlabeled) = args.unlabeled_kw_arg_unconverted() else {
1129 debug_assert!(false, "Bad args");
1130 return Err(KclError::new_internal(KclErrorDetails::new(
1131 "Desugared arguments are inconsistent".to_owned(),
1132 source_ranges,
1133 )));
1134 };
1135 exec_state.mut_stack().add(
1136 param_name.clone(),
1137 unlabeled.value.clone(),
1138 unlabeled.source_ranges().pop().unwrap_or(SourceRange::synthetic()),
1139 )?;
1140 }
1141
1142 Ok(())
1143}
1144
1145fn coerce_result_type(
1146 result: Result<Option<KclValue>, KclError>,
1147 fn_def: &FunctionSource,
1148 exec_state: &mut ExecState,
1149) -> Result<Option<KclValue>, KclError> {
1150 if let Ok(Some(val)) = result {
1151 if let Some(ret_ty) = &fn_def.return_type {
1152 let ty = RuntimeType::from_parsed(ret_ty.inner.clone(), exec_state, ret_ty.as_source_range(), false, true)
1155 .map_err(|e| KclError::new_semantic(e.into()))?;
1156 let val = val.coerce(&ty, true, exec_state).map_err(|_| {
1157 KclError::new_type(KclErrorDetails::new(
1158 format!(
1159 "This function requires its result to be {}",
1160 type_err_str(ret_ty, &val, &(&val).into(), exec_state)
1161 ),
1162 ret_ty.as_source_ranges(),
1163 ))
1164 })?;
1165 Ok(Some(val))
1166 } else {
1167 Ok(Some(val))
1168 }
1169 } else {
1170 result
1171 }
1172}
1173
1174#[cfg(test)]
1175mod test {
1176 use std::sync::Arc;
1177
1178 use super::*;
1179 use crate::engine::engine_manager::EngineManager;
1180 use crate::errors::Severity;
1181 use crate::execution::ContextType;
1182 use crate::execution::EnvironmentRef;
1183 use crate::execution::ExecTestResults;
1184 use crate::execution::memory::Stack;
1185 use crate::execution::parse_execute;
1186 use crate::execution::types::NumericType;
1187 use crate::parsing::ast::types::DefaultParamVal;
1188 use crate::parsing::ast::types::FunctionExpression;
1189 use crate::parsing::ast::types::Identifier;
1190 use crate::parsing::ast::types::Parameter;
1191 use crate::parsing::ast::types::Program;
1192
1193 fn get_var(result: &ExecTestResults, name: &str) -> KclValue {
1194 result
1195 .exec_state
1196 .stack()
1197 .memory
1198 .get_from_owned(name, result.mem_env, SourceRange::default(), 0)
1199 .unwrap_or_else(|err| panic!("expected variable `{name}` to exist: {err:?}"))
1200 }
1201
1202 fn var_exists(result: &ExecTestResults, name: &str) -> bool {
1203 result
1204 .exec_state
1205 .stack()
1206 .memory
1207 .get_from_owned(name, result.mem_env, SourceRange::default(), 0)
1208 .is_ok()
1209 }
1210
1211 fn assert_vars_are_tags(result: &ExecTestResults, names: &[&str]) {
1212 for name in names {
1213 assert!(
1214 matches!(get_var(result, name), KclValue::TagIdentifier(_)),
1215 "expected variable `{name}` to be a tag identifier"
1216 );
1217 }
1218 }
1219
1220 fn assert_vars_are_missing(result: &ExecTestResults, names: &[&str]) {
1221 for name in names {
1222 assert!(!var_exists(result, name), "expected variable `{name}` to be absent");
1223 }
1224 }
1225
1226 fn assert_body_face_tags(result: &ExecTestResults, expected: &[&str], unexpected: &[&str]) {
1227 let body = get_var(result, "body");
1228 let KclValue::Solid { value: body } = body else {
1229 panic!("expected `body` to be a solid");
1230 };
1231
1232 for tag in expected {
1233 assert!(body.faces.contains_key(*tag), "expected body.faces to contain `{tag}`");
1234 }
1235
1236 for tag in unexpected {
1237 assert!(
1238 !body.faces.contains_key(*tag),
1239 "expected body.faces not to contain sketch tag `{tag}`"
1240 );
1241 }
1242 }
1243
1244 fn deprecated_solid_tag_access_warnings(result: &ExecTestResults) -> Vec<&CompilationIssue> {
1245 result
1246 .exec_state
1247 .issues()
1248 .iter()
1249 .filter(|issue| issue.message.contains("Accessing solid-created face"))
1250 .collect()
1251 }
1252
1253 #[tokio::test(flavor = "multi_thread")]
1254 async fn test_assign_args_to_params() {
1255 fn mem(number: usize) -> KclValue {
1257 KclValue::Number {
1258 value: number as f64,
1259 ty: NumericType::count(),
1260 meta: Default::default(),
1261 }
1262 }
1263 fn ident(s: &'static str) -> Node<Identifier> {
1264 Node::no_src(Identifier {
1265 name: s.to_owned(),
1266 digest: None,
1267 })
1268 }
1269 fn opt_param(s: &'static str) -> Parameter {
1270 Parameter {
1271 experimental: false,
1272 deprecated: false,
1273 deprecated_since: None,
1274 identifier: ident(s),
1275 param_type: None,
1276 default_value: Some(DefaultParamVal::none()),
1277 labeled: true,
1278 digest: None,
1279 }
1280 }
1281 fn req_param(s: &'static str) -> Parameter {
1282 Parameter {
1283 experimental: false,
1284 deprecated: false,
1285 deprecated_since: None,
1286 identifier: ident(s),
1287 param_type: None,
1288 default_value: None,
1289 labeled: true,
1290 digest: None,
1291 }
1292 }
1293 fn additional_program_memory(items: &[(String, KclValue)]) -> Stack {
1294 let mut program_memory = Stack::new_for_tests();
1295 for (name, item) in items {
1296 program_memory
1297 .add(name.clone(), item.clone(), SourceRange::default())
1298 .unwrap();
1299 }
1300 program_memory
1301 }
1302 for (test_name, params, args, expected) in [
1304 ("empty", Vec::new(), Vec::new(), Ok(additional_program_memory(&[]))),
1305 (
1306 "all params required, and all given, should be OK",
1307 vec![req_param("x")],
1308 vec![("x", mem(1))],
1309 Ok(additional_program_memory(&[("x".to_owned(), mem(1))])),
1310 ),
1311 (
1312 "all params required, none given, should error",
1313 vec![req_param("x")],
1314 vec![],
1315 Err(KclError::new_argument(KclErrorDetails::new(
1316 "This function requires a parameter x, but you haven't passed it one.".to_owned(),
1317 vec![SourceRange::default()],
1318 ))),
1319 ),
1320 (
1321 "all params optional, none given, should be OK",
1322 vec![opt_param("x")],
1323 vec![],
1324 Ok(additional_program_memory(&[("x".to_owned(), KclValue::none())])),
1325 ),
1326 (
1327 "mixed params, too few given",
1328 vec![req_param("x"), opt_param("y")],
1329 vec![],
1330 Err(KclError::new_argument(KclErrorDetails::new(
1331 "This function requires a parameter x, but you haven't passed it one.".to_owned(),
1332 vec![SourceRange::default()],
1333 ))),
1334 ),
1335 (
1336 "mixed params, minimum given, should be OK",
1337 vec![req_param("x"), opt_param("y")],
1338 vec![("x", mem(1))],
1339 Ok(additional_program_memory(&[
1340 ("x".to_owned(), mem(1)),
1341 ("y".to_owned(), KclValue::none()),
1342 ])),
1343 ),
1344 (
1345 "mixed params, maximum given, should be OK",
1346 vec![req_param("x"), opt_param("y")],
1347 vec![("x", mem(1)), ("y", mem(2))],
1348 Ok(additional_program_memory(&[
1349 ("x".to_owned(), mem(1)),
1350 ("y".to_owned(), mem(2)),
1351 ])),
1352 ),
1353 ] {
1354 let func_expr = Node::no_src(FunctionExpression {
1356 name: None,
1357 params,
1358 body: Program::empty(),
1359 return_type: None,
1360 digest: None,
1361 });
1362 let func_src = FunctionSource::kcl(
1363 Box::new(func_expr),
1364 EnvironmentRef::dummy(),
1365 crate::execution::kcl_value::KclFunctionSourceParams {
1366 std_props: None,
1367 experimental: false,
1368 include_in_feature_tree: false,
1369 },
1370 );
1371 let labeled = args
1372 .iter()
1373 .map(|(name, value)| {
1374 let arg = Arg::new(value.clone(), SourceRange::default());
1375 ((*name).to_owned(), arg)
1376 })
1377 .collect::<IndexMap<_, _>>();
1378 let exec_ctxt = ExecutorContext {
1379 engine: Arc::new(EngineManager::new_mock()),
1380 engine_batch: crate::engine::EngineBatchContext::default(),
1381 fs: Arc::new(crate::fs::FileManager::new()),
1382 settings: Default::default(),
1383 context_type: ContextType::Mock,
1384 execution_callbacks: Default::default(),
1385 };
1386 let mut exec_state = ExecState::new(&exec_ctxt);
1387 exec_state.mod_local.stack = Stack::new_for_tests();
1388
1389 let args = Args {
1390 fn_name: Some("test".to_owned()),
1391 labeled,
1392 unlabeled: Vec::new(),
1393 source_range: SourceRange::default(),
1394 node_path: None,
1395 ctx: exec_ctxt,
1396 pipe_value: None,
1397 _status: std::marker::PhantomData,
1398 };
1399
1400 let actual = assign_args_to_params_kw(&func_src, args, &mut exec_state).map(|_| exec_state.mod_local.stack);
1401 assert_eq!(
1402 actual, expected,
1403 "failed test '{test_name}':\ngot {actual:?}\nbut expected\n{expected:?}"
1404 );
1405 }
1406 }
1407
1408 #[tokio::test(flavor = "multi_thread")]
1409 async fn type_check_user_args() {
1410 let program = r#"fn makeMessage(prefix: string, suffix: string) {
1411 return prefix + suffix
1412}
1413
1414msg1 = makeMessage(prefix = "world", suffix = " hello")
1415msg2 = makeMessage(prefix = 1, suffix = 3)"#;
1416 let err = parse_execute(program).await.unwrap_err();
1417 assert_eq!(
1418 err.message(),
1419 "prefix requires a value with type `string`, but found a value with type `number`.\nThe found value is a number but has incomplete units information. You can probably fix this error by specifying the units using type ascription, e.g., `len: mm` or `(a * b): deg`."
1420 )
1421 }
1422
1423 #[tokio::test(flavor = "multi_thread")]
1424 async fn map_closure_error_mentions_fn_name() {
1425 let program = r#"
1426arr = ["hello"]
1427map(array = arr, f = fn(@item: number) { return item })
1428"#;
1429 let err = parse_execute(program).await.unwrap_err();
1430 assert!(
1431 err.message().contains("map closure"),
1432 "expected map closure errors to include the closure name, got: {}",
1433 err.message()
1434 );
1435 }
1436
1437 #[tokio::test(flavor = "multi_thread")]
1438 async fn array_input_arg() {
1439 let ast = r#"fn f(@input: [mm]) { return 1 }
1440f([1, 2, 3])
1441f(1, 2, 3)
1442"#;
1443 parse_execute(ast).await.unwrap();
1444 }
1445
1446 #[tokio::test(flavor = "multi_thread")]
1447 async fn extrude_tagged_body_gets_face_tags_and_keeps_legacy_bindings() {
1448 let program = r#"@settings(kclVersion = 2.0)
1449profile = sketch(on = XY) {
1450 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1451 line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1452 line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1453 line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1454 coincident([line1.end, line2.start])
1455 coincident([line2.end, line3.start])
1456 coincident([line3.end, line4.start])
1457 coincident([line4.end, line1.start])
1458}
1459region1 = region(point = [5mm, 5mm], sketch = profile)
1460
1461body = extrude(region1, length = 5mm, tagStart = $bottom, tagEnd = $top)
1462bottomFromBody = body.faces.bottom
1463topFromBody = body.faces.top
1464lineFromSketch = region1.tags.line1
1465legacyBottom = bottom
1466legacyTop = top
1467"#;
1468
1469 let result = parse_execute(program).await.unwrap();
1470 assert_body_face_tags(&result, &["bottom", "top"], &["line1"]);
1471 assert_vars_are_tags(
1472 &result,
1473 &[
1474 "bottom",
1475 "top",
1476 "bottomFromBody",
1477 "topFromBody",
1478 "lineFromSketch",
1479 "legacyBottom",
1480 "legacyTop",
1481 ],
1482 );
1483 assert_vars_are_missing(&result, &["line1"]);
1484 }
1485
1486 #[tokio::test(flavor = "multi_thread")]
1487 async fn extrude_without_tag_arguments_does_not_get_face_tags() {
1488 let program = r#"@settings(kclVersion = 2.0)
1489profile = sketch(on = XY) {
1490 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1491 line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1492 line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1493 line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1494 coincident([line1.end, line2.start])
1495 coincident([line2.end, line3.start])
1496 coincident([line3.end, line4.start])
1497 coincident([line4.end, line1.start])
1498}
1499region1 = region(point = [5mm, 5mm], sketch = profile)
1500
1501body = extrude(region1, length = 5mm)
1502"#;
1503
1504 let result = parse_execute(program).await.unwrap();
1505 let body = get_var(&result, "body");
1506 let KclValue::Solid { value: body } = body else {
1507 panic!("expected `body` to be a solid");
1508 };
1509
1510 assert!(
1511 body.faces.is_empty(),
1512 "body faces should only be populated for tagged calls"
1513 );
1514 }
1515
1516 #[tokio::test(flavor = "multi_thread")]
1517 async fn revolve_tagged_body_gets_face_tags() {
1518 let program = r#"@settings(kclVersion = 2.0)
1519profile = sketch(on = XY) {
1520 side = line(start = [var 5mm, var 0mm], end = [var 5mm, var 10mm])
1521 line2 = line(start = [var 5mm, var 10mm], end = [var 6mm, var 10mm])
1522 line3 = line(start = [var 6mm, var 10mm], end = [var 6mm, var 0mm])
1523 line4 = line(start = [var 6mm, var 0mm], end = [var 5mm, var 0mm])
1524 coincident([side.end, line2.start])
1525 coincident([line2.end, line3.start])
1526 coincident([line3.end, line4.start])
1527 coincident([line4.end, side.start])
1528}
1529region1 = region(point = [5.5mm, 5mm], sketch = profile)
1530
1531body = revolve(region1, axis = Y, angle = 90deg, tagStart = $startCap, tagEnd = $endCap)
1532startFromBody = body.faces.startCap
1533endFromBody = body.faces.endCap
1534sideFromSketch = region1.tags.side
1535legacyStart = startCap
1536legacyEnd = endCap
1537"#;
1538
1539 let result = parse_execute(program).await.unwrap();
1540 assert_body_face_tags(&result, &["startCap", "endCap"], &["side"]);
1541 assert_vars_are_tags(
1542 &result,
1543 &[
1544 "startCap",
1545 "endCap",
1546 "startFromBody",
1547 "endFromBody",
1548 "sideFromSketch",
1549 "legacyStart",
1550 "legacyEnd",
1551 ],
1552 );
1553 assert_vars_are_missing(&result, &["side"]);
1554 }
1555
1556 #[tokio::test(flavor = "multi_thread")]
1557 async fn sweep_tagged_body_gets_face_tags() {
1558 let program = r#"@settings(kclVersion = 2.0)
1559profile = sketch(on = XZ) {
1560 edge1 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 0mm])
1561 edge2 = line(start = [var 2mm, var 0mm], end = [var 2mm, var 2mm])
1562 edge3 = line(start = [var 2mm, var 2mm], end = [var 0mm, var 2mm])
1563 edge4 = line(start = [var 0mm, var 2mm], end = [var 0mm, var 0mm])
1564 coincident([edge1.end, edge2.start])
1565 coincident([edge2.end, edge3.start])
1566 coincident([edge3.end, edge4.start])
1567 coincident([edge4.end, edge1.start])
1568}
1569profileRegion = region(point = [1mm, 1mm], sketch = profile)
1570
1571pathSketch = sketch(on = offsetPlane(YZ, offset = -2mm)) {
1572 pathLine = line(start = [var 0mm, var 0mm], end = [var 0mm, var 5mm])
1573}
1574
1575body = sweep(profileRegion, path = pathSketch.pathLine, tagStart = $startCap, tagEnd = $endCap)
1576startFromBody = body.faces.startCap
1577endFromBody = body.faces.endCap
1578edgeFromSketch = profileRegion.tags.edge1
1579pathFromSketch = pathSketch.pathLine
1580legacyStart = startCap
1581legacyEnd = endCap
1582"#;
1583
1584 let result = parse_execute(program).await.unwrap();
1585 assert_body_face_tags(&result, &["startCap", "endCap"], &["edge1", "pathLine"]);
1586 assert_vars_are_tags(
1587 &result,
1588 &[
1589 "startCap",
1590 "endCap",
1591 "startFromBody",
1592 "endFromBody",
1593 "edgeFromSketch",
1594 "legacyStart",
1595 "legacyEnd",
1596 ],
1597 );
1598 assert_vars_are_missing(&result, &["edge1", "pathLine"]);
1599 }
1600
1601 #[tokio::test(flavor = "multi_thread")]
1602 async fn loft_tagged_body_gets_face_tags() {
1603 let program = r#"@settings(kclVersion = 2.0)
1604lowerProfile = sketch(on = XY) {
1605 edge1 = line(start = [var 0mm, var 0mm], end = [var 6mm, var 0mm])
1606 edge2 = line(start = [var 6mm, var 0mm], end = [var 6mm, var 4mm])
1607 edge3 = line(start = [var 6mm, var 4mm], end = [var 0mm, var 4mm])
1608 edge4 = line(start = [var 0mm, var 4mm], end = [var 0mm, var 0mm])
1609 coincident([edge1.end, edge2.start])
1610 coincident([edge2.end, edge3.start])
1611 coincident([edge3.end, edge4.start])
1612 coincident([edge4.end, edge1.start])
1613}
1614lowerRegion = region(point = [3mm, 2mm], sketch = lowerProfile)
1615
1616upperProfile = sketch(on = offsetPlane(XY, offset = 8mm)) {
1617 edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
1618 edge6 = line(start = [var 5mm, var 1mm], end = [var 4mm, var 3mm])
1619 edge7 = line(start = [var 4mm, var 3mm], end = [var 2mm, var 3mm])
1620 edge8 = line(start = [var 2mm, var 3mm], end = [var 1mm, var 1mm])
1621 coincident([edge5.end, edge6.start])
1622 coincident([edge6.end, edge7.start])
1623 coincident([edge7.end, edge8.start])
1624 coincident([edge8.end, edge5.start])
1625}
1626upperRegion = region(point = [3mm, 2mm], sketch = upperProfile)
1627
1628body = loft([lowerRegion, upperRegion], tagStart = $startCap, tagEnd = $endCap)
1629startFromBody = body.faces.startCap
1630endFromBody = body.faces.endCap
1631edgeFromSketch = lowerRegion.tags.edge1
1632legacyStart = startCap
1633legacyEnd = endCap
1634"#;
1635
1636 let result = parse_execute(program).await.unwrap();
1637 assert_body_face_tags(&result, &["startCap", "endCap"], &["edge1"]);
1638 assert_vars_are_tags(
1639 &result,
1640 &[
1641 "startCap",
1642 "endCap",
1643 "startFromBody",
1644 "endFromBody",
1645 "edgeFromSketch",
1646 "legacyStart",
1647 "legacyEnd",
1648 ],
1649 );
1650 assert_vars_are_missing(&result, &["edge1"]);
1651 }
1652
1653 #[tokio::test(flavor = "multi_thread")]
1654 async fn chamfer_tagged_body_gets_face_tags() {
1655 let program = r#"@settings(kclVersion = 2.0)
1656profile = sketch(on = XY) {
1657 edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1658 edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1659 edge3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1660 edge4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1661 coincident([edge1.end, edge2.start])
1662 coincident([edge2.end, edge3.start])
1663 coincident([edge3.end, edge4.start])
1664 coincident([edge4.end, edge1.start])
1665}
1666profileRegion = region(point = [5mm, 5mm], sketch = profile)
1667
1668base = extrude(profileRegion, length = 5mm, tagEnd = $top)
1669body = chamfer(base, tags = getCommonEdge(faces = [profileRegion.tags.edge1, top]), length = 1mm, tag = $chamferFace)
1670chamferFromBody = body.faces.chamferFace
1671topFromBody = body.faces.top
1672edgeFromSketch = profileRegion.tags.edge1
1673legacyChamfer = chamferFace
1674legacyTop = top
1675"#;
1676
1677 let result = parse_execute(program).await.unwrap();
1678 assert_body_face_tags(&result, &["top", "chamferFace"], &["edge1"]);
1679 assert_vars_are_tags(
1680 &result,
1681 &[
1682 "top",
1683 "chamferFace",
1684 "chamferFromBody",
1685 "topFromBody",
1686 "edgeFromSketch",
1687 "legacyChamfer",
1688 "legacyTop",
1689 ],
1690 );
1691 assert_vars_are_missing(&result, &["edge1"]);
1692 }
1693
1694 #[tokio::test(flavor = "multi_thread")]
1695 async fn fillet_tagged_body_gets_face_tags() {
1696 let program = r#"@settings(kclVersion = 2.0)
1697profile = sketch(on = XY) {
1698 edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1699 edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1700 edge3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1701 edge4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1702 coincident([edge1.end, edge2.start])
1703 coincident([edge2.end, edge3.start])
1704 coincident([edge3.end, edge4.start])
1705 coincident([edge4.end, edge1.start])
1706}
1707profileRegion = region(point = [5mm, 5mm], sketch = profile)
1708
1709base = extrude(profileRegion, length = 5mm, tagEnd = $top)
1710body = fillet(base, tags = getCommonEdge(faces = [profileRegion.tags.edge1, top]), radius = 1mm, tag = $filletFace)
1711filletFromBody = body.faces.filletFace
1712topFromBody = body.faces.top
1713edgeFromSketch = profileRegion.tags.edge1
1714legacyFillet = filletFace
1715legacyTop = top
1716"#;
1717
1718 let result = parse_execute(program).await.unwrap();
1719 assert_body_face_tags(&result, &["top", "filletFace"], &["edge1"]);
1720 assert_vars_are_tags(
1721 &result,
1722 &[
1723 "top",
1724 "filletFace",
1725 "filletFromBody",
1726 "topFromBody",
1727 "edgeFromSketch",
1728 "legacyFillet",
1729 "legacyTop",
1730 ],
1731 );
1732 assert_vars_are_missing(&result, &["edge1"]);
1733 }
1734
1735 #[tokio::test(flavor = "multi_thread")]
1736 async fn accessing_body_tag_through_body_sketch_tags_warns() {
1737 let program = r#"@settings(kclVersion = 2.0)
1738profile = startSketchOn(XY)
1739 |> startProfile(at = [0, 0])
1740 |> line(end = [10, 0], tag = $line1)
1741 |> line(end = [0, 10])
1742 |> line(end = [-10, 0])
1743 |> close()
1744
1745body = extrude(profile, length = 5, tagEnd = $top)
1746topFromSketch = body.sketch.tags.top
1747topFromBody = body.faces.top
1748"#;
1749
1750 let result = parse_execute(program).await.unwrap();
1751 assert!(matches!(get_var(&result, "topFromSketch"), KclValue::TagIdentifier(_)));
1752 assert!(matches!(get_var(&result, "topFromBody"), KclValue::TagIdentifier(_)));
1753
1754 let warnings = deprecated_solid_tag_access_warnings(&result);
1755 assert_eq!(warnings.len(), 1, "expected one deprecation warning, got {warnings:#?}");
1756 assert_eq!(warnings[0].severity, Severity::Warning);
1757 assert!(warnings[0].message.contains("`top`"), "found {}", warnings[0].message);
1758 assert!(
1759 warnings[0].message.contains("Accessing solid-created face `top` through sketch tags is deprecated. Use the body's faces instead, e.g. `body.faces.top`."),
1760 "found {}",
1761 warnings[0].message
1762 );
1763 }
1764
1765 #[tokio::test(flavor = "multi_thread")]
1766 async fn accessing_sketch_path_tag_through_body_sketch_tags_does_not_warn() {
1767 let program = r#"@settings(kclVersion = 2.0)
1768profile = startSketchOn(XY)
1769 |> startProfile(at = [0, 0])
1770 |> line(end = [10, 0], tag = $line1)
1771 |> line(end = [0, 10])
1772 |> line(end = [-10, 0])
1773 |> close()
1774
1775body = extrude(profile, length = 5, tagEnd = $top)
1776lineFromSketch = body.sketch.tags.line1
1777"#;
1778
1779 let result = parse_execute(program).await.unwrap();
1780 assert!(matches!(get_var(&result, "lineFromSketch"), KclValue::TagIdentifier(_)));
1781 let warnings = deprecated_solid_tag_access_warnings(&result);
1782 assert!(
1783 warnings.is_empty(),
1784 "sketch path tags should not get body-tag deprecation warnings: {warnings:#?}"
1785 );
1786 }
1787
1788 #[tokio::test(flavor = "multi_thread")]
1789 async fn accessing_body_tag_through_sketch_block_region_tags_warns() {
1790 let program = r#"@settings(kclVersion = 2.0)
1791profile = sketch(on = XY) {
1792 line1 = line(start = [0, 0], end = [10, 0])
1793 line2 = line(start = [10, 0], end = [10, 10])
1794 line3 = line(start = [10, 10], end = [0, 10])
1795 line4 = line(start = [0, 10], end = [0, 0])
1796}
1797
1798profileRegion = region(point = [1, 1], sketch = profile)
1799body = extrude(profileRegion, length = 5, tagEnd = $top)
1800topFromRegion = profileRegion.tags.top
1801"#;
1802
1803 let result = parse_execute(program).await.unwrap();
1804 assert!(matches!(get_var(&result, "topFromRegion"), KclValue::TagIdentifier(_)));
1805
1806 let warnings = deprecated_solid_tag_access_warnings(&result);
1807 assert_eq!(warnings.len(), 1, "expected one deprecation warning, got {warnings:#?}");
1808 assert_eq!(warnings[0].severity, Severity::Warning);
1809 assert!(warnings[0].message.contains("`top`"), "found {}", warnings[0].message);
1810 }
1811
1812 fn deprecation_warnings(result: &ExecTestResults) -> Vec<&CompilationIssue> {
1813 result
1814 .exec_state
1815 .issues()
1816 .iter()
1817 .filter(|issue| issue.message.contains("is deprecated"))
1818 .collect()
1819 }
1820
1821 #[tokio::test(flavor = "multi_thread")]
1822 async fn passing_param_deprecated_for_all_versions_warns() {
1823 let program = r#"@settings(kclVersion = 2.0)
1826fn f(
1827 @a: number,
1828 @(deprecated = true)
1829 oldArg?: number,
1830) {
1831 return a
1832}
1833x = f(1, oldArg = 2)
1834"#;
1835
1836 let result = parse_execute(program).await.unwrap();
1837 let warnings = deprecation_warnings(&result);
1838 assert_eq!(warnings.len(), 1, "expected one deprecation warning, got {warnings:#?}");
1839 assert_eq!(warnings[0].severity, Severity::Warning);
1840 assert!(
1841 warnings[0].message.contains("`f(oldArg)` is deprecated"),
1842 "found {}",
1843 warnings[0].message
1844 );
1845 }
1846
1847 #[tokio::test(flavor = "multi_thread")]
1848 async fn not_passing_deprecated_param_does_not_warn() {
1849 let program = r#"fn f(
1850 @a: number,
1851 @(deprecated = true)
1852 oldArg?: number,
1853) {
1854 return a
1855}
1856x = f(1)
1857"#;
1858
1859 let result = parse_execute(program).await.unwrap();
1860 let warnings = deprecation_warnings(&result);
1861 assert!(
1862 warnings.is_empty(),
1863 "unused deprecated parameter should not warn: {warnings:#?}"
1864 );
1865 }
1866}