Skip to content

Commit 865bf8a

Browse files
authored
KCL: Fix cryptic error when using duplicate edges in fillet call (#5755)
Fixes #4307 Now if you try to fillet the same edge twice in a single fillet command, the error message is clearer, and the source range will highlight the specific edges in the array which are duplicated. Same goes for chamfer. Note: although the Rust KCL interpreter sends back an array of SourceRange for each KCL error, the frontend only puts the first one into CodeMirror diagnostics. We should fix that: #5754
1 parent f8e53c6 commit 865bf8a

File tree

7 files changed

+125
-49
lines changed

7 files changed

+125
-49
lines changed
Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
const part001 = startSketchOn('XY')
2-
|> startProfileAt([0,0], %)
3-
|> line(end = [0, 10], tag = $thing)
4-
|> line(end = [10, 0])
5-
|> line(end = [0, -10], tag = $thing2)
6-
|> close()
7-
|> extrude(length = 10)
8-
|> fillet(radius = 0.5, tags = [thing, thing])
1+
startProfileAt([0, 0], startSketchOn("XY"))
2+
|> xLine(length = 10, tag = $line000)
3+
|> yLine(length = 10, tag = $line001)
4+
|> xLine(endAbsolute = profileStartX(%), tag = $line002)
5+
|> close(tag = $line003)
6+
|> extrude(length = 10)
7+
|> fillet(
8+
radius = 1,
9+
tags = [
10+
line003,
11+
getNextAdjacentEdge(line000),
12+
getPreviousAdjacentEdge(line001)
13+
],
14+
)
15+

rust/kcl-lib/e2e/executor/main.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@ async fn kcl_test_fillet_duplicate_tags() {
2727
let code = kcl_input!("fillet_duplicate_tags");
2828

2929
let result = execute_and_snapshot(code, UnitLength::Mm, None).await;
30-
assert!(result.is_err());
30+
let err = result.expect_err("Code should have failed due to the duplicate edges being filletted");
31+
32+
let err = err.as_kcl_error().unwrap();
3133
assert_eq!(
32-
result.err().unwrap().to_string(),
33-
r#"type: KclErrorDetails { source_ranges: [SourceRange([229, 272, 0])], message: "Duplicate tags are not allowed." }"#,
34+
err.message(),
35+
"The same edge ID is being referenced multiple times, which is not allowed. Please select a different edge"
3436
);
37+
assert_eq!(err.source_ranges().len(), 2);
3538
}
3639

3740
#[tokio::test(flavor = "multi_thread")]

rust/kcl-lib/src/errors.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ impl ExecErrorWithState {
4848
}
4949
}
5050

51+
impl ExecError {
52+
pub fn as_kcl_error(&self) -> Option<&crate::KclError> {
53+
let ExecError::Kcl(k) = &self else {
54+
return None;
55+
};
56+
Some(&k.error)
57+
}
58+
}
59+
5160
impl From<ExecError> for ExecErrorWithState {
5261
fn from(error: ExecError) -> Self {
5362
Self {

rust/kcl-lib/src/std/args.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,49 @@ impl Args {
159159
})
160160
}
161161

162+
/// Get a labelled keyword arg, check it's an array, and return all items in the array
163+
/// plus their source range.
164+
pub(crate) fn kw_arg_array_and_source<'a, T>(&'a self, label: &str) -> Result<Vec<(T, SourceRange)>, KclError>
165+
where
166+
T: FromKclValue<'a>,
167+
{
168+
let Some(arg) = self.kw_args.labeled.get(label) else {
169+
let err = KclError::Semantic(KclErrorDetails {
170+
source_ranges: vec![self.source_range],
171+
message: format!("This function requires a keyword argument '{label}'"),
172+
});
173+
return Err(err);
174+
};
175+
let Some(array) = arg.value.as_array() else {
176+
let err = KclError::Semantic(KclErrorDetails {
177+
source_ranges: vec![arg.source_range],
178+
message: format!(
179+
"Expected an array of {} but found {}",
180+
type_name::<T>(),
181+
arg.value.human_friendly_type()
182+
),
183+
});
184+
return Err(err);
185+
};
186+
array
187+
.iter()
188+
.map(|item| {
189+
let source = SourceRange::from(item);
190+
let val = FromKclValue::from_kcl_val(item).ok_or_else(|| {
191+
KclError::Semantic(KclErrorDetails {
192+
source_ranges: arg.source_ranges(),
193+
message: format!(
194+
"Expected a {} but found {}",
195+
type_name::<T>(),
196+
arg.value.human_friendly_type()
197+
),
198+
})
199+
})?;
200+
Ok((val, source))
201+
})
202+
.collect::<Result<Vec<_>, _>>()
203+
}
204+
162205
/// Get the unlabeled keyword argument. If not set, returns None.
163206
pub(crate) fn unlabeled_kw_arg_unconverted(&self) -> Option<&Arg> {
164207
self.kw_args

rust/kcl-lib/src/std/chamfer.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use kcl_derive_docs::stdlib;
55
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::CutType, ModelingCmd};
66
use kittycad_modeling_cmds as kcmc;
77

8-
use super::utils::unique_count;
98
use crate::{
109
errors::{KclError, KclErrorDetails},
1110
execution::{ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, Solid},
@@ -19,9 +18,11 @@ pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
1918
pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
2019
let solid = args.get_unlabeled_kw_arg("solid")?;
2120
let length = args.get_kw_arg("length")?;
22-
let tags = args.get_kw_arg("tags")?;
21+
let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
2322
let tag = args.get_kw_arg_opt("tag")?;
2423

24+
super::fillet::validate_unique(&tags)?;
25+
let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
2526
let value = inner_chamfer(solid, length, tags, tag, exec_state, args).await?;
2627
Ok(KclValue::Solid { value })
2728
}
@@ -109,15 +110,6 @@ async fn inner_chamfer(
109110
exec_state: &mut ExecState,
110111
args: Args,
111112
) -> Result<Box<Solid>, KclError> {
112-
// Check if tags contains any duplicate values.
113-
let unique_tags = unique_count(tags.clone());
114-
if unique_tags != tags.len() {
115-
return Err(KclError::Type(KclErrorDetails {
116-
message: "Duplicate tags are not allowed.".to_string(),
117-
source_ranges: vec![args.source_range],
118-
}));
119-
}
120-
121113
// If you try and tag multiple edges with a tagged chamfer, we want to return an
122114
// error to the user that they can only tag one edge at a time.
123115
if tag.is_some() && tags.len() > 1 {

rust/kcl-lib/src/std/fillet.rs

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Standard library fillets.
22
33
use anyhow::Result;
4+
use indexmap::IndexMap;
45
use kcl_derive_docs::stdlib;
56
use kcmc::{
67
each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, shared::CutType,
@@ -11,13 +12,13 @@ use schemars::JsonSchema;
1112
use serde::{Deserialize, Serialize};
1213
use uuid::Uuid;
1314

14-
use super::utils::unique_count;
1515
use crate::{
1616
errors::{KclError, KclErrorDetails},
1717
execution::{EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier},
1818
parsing::ast::types::TagNode,
1919
settings::types::UnitLength,
2020
std::Args,
21+
SourceRange,
2122
};
2223

2324
/// A tag or a uuid of an edge.
@@ -40,13 +41,39 @@ impl EdgeReference {
4041
}
4142
}
4243

44+
pub(super) fn validate_unique<T: Eq + std::hash::Hash>(tags: &[(T, SourceRange)]) -> Result<(), KclError> {
45+
// Check if tags contains any duplicate values.
46+
let mut tag_counts: IndexMap<&T, Vec<SourceRange>> = Default::default();
47+
for tag in tags {
48+
tag_counts.entry(&tag.0).or_insert(Vec::new()).push(tag.1);
49+
}
50+
let mut duplicate_tags_source = Vec::new();
51+
for (_tag, count) in tag_counts {
52+
if count.len() > 1 {
53+
duplicate_tags_source.extend(count)
54+
}
55+
}
56+
if !duplicate_tags_source.is_empty() {
57+
return Err(KclError::Type(KclErrorDetails {
58+
message: "The same edge ID is being referenced multiple times, which is not allowed. Please select a different edge".to_string(),
59+
source_ranges: duplicate_tags_source,
60+
}));
61+
}
62+
Ok(())
63+
}
64+
4365
/// Create fillets on tagged paths.
4466
pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
67+
// Get all args:
4568
let solid = args.get_unlabeled_kw_arg("solid")?;
4669
let radius = args.get_kw_arg("radius")?;
4770
let tolerance = args.get_kw_arg_opt("tolerance")?;
48-
let tags = args.get_kw_arg("tags")?;
71+
let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
4972
let tag = args.get_kw_arg_opt("tag")?;
73+
74+
// Run the function.
75+
validate_unique(&tags)?;
76+
let tags: Vec<EdgeReference> = tags.into_iter().map(|item| item.0).collect();
5077
let value = inner_fillet(solid, radius, tags, tolerance, tag, exec_state, args).await?;
5178
Ok(KclValue::Solid { value })
5279
}
@@ -129,15 +156,6 @@ async fn inner_fillet(
129156
exec_state: &mut ExecState,
130157
args: Args,
131158
) -> Result<Box<Solid>, KclError> {
132-
// Check if tags contains any duplicate values.
133-
let unique_tags = unique_count(tags.clone());
134-
if unique_tags != tags.len() {
135-
return Err(KclError::Type(KclErrorDetails {
136-
message: "Duplicate tags are not allowed.".to_string(),
137-
source_ranges: vec![args.source_range],
138-
}));
139-
}
140-
141159
let mut solid = solid.clone();
142160
for edge_tag in tags {
143161
let edge_id = edge_tag.get_engine_id(exec_state, &args)?;
@@ -432,3 +450,22 @@ pub(crate) fn default_tolerance(units: &UnitLength) -> f64 {
432450
UnitLength::M => 0.001,
433451
}
434452
}
453+
454+
#[cfg(test)]
455+
mod tests {
456+
use super::*;
457+
458+
#[test]
459+
fn test_validate_unique() {
460+
let dup_a = SourceRange::from([1, 3, 0]);
461+
let dup_b = SourceRange::from([10, 30, 0]);
462+
// Two entries are duplicates (abc) with different source ranges.
463+
let tags = vec![("abc", dup_a), ("abc", dup_b), ("def", SourceRange::from([2, 4, 0]))];
464+
let actual = validate_unique(&tags);
465+
// Both the duplicates should show up as errors, with both of the
466+
// source ranges they correspond to.
467+
// But the unique source range 'def' should not.
468+
let expected = vec![dup_a, dup_b];
469+
assert_eq!(actual.err().unwrap().source_ranges(), expected);
470+
}
471+
}

rust/kcl-lib/src/std/utils.rs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{collections::HashSet, f64::consts::PI};
1+
use std::f64::consts::PI;
22

33
use kittycad_modeling_cmds::shared::Angle;
44

@@ -8,16 +8,6 @@ use crate::{
88
source_range::SourceRange,
99
};
1010

11-
/// Count the number of unique items in a `Vec` in O(n) time.
12-
pub(crate) fn unique_count<T: Eq + std::hash::Hash>(vec: Vec<T>) -> usize {
13-
// Add to a set.
14-
let mut set = HashSet::with_capacity(vec.len());
15-
for item in vec {
16-
set.insert(item);
17-
}
18-
set.len()
19-
}
20-
2111
/// Get the distance between two points.
2212
pub fn distance(a: Point2d, b: Point2d) -> f64 {
2313
((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt()
@@ -686,11 +676,6 @@ mod get_tangential_arc_to_info_tests {
686676
(num * 1000.0).round() / 1000.0
687677
}
688678

689-
#[test]
690-
fn test_unique_count() {
691-
assert_eq!(unique_count(vec![1, 2, 2, 3, 2]), 3);
692-
}
693-
694679
#[test]
695680
fn test_basic_case() {
696681
let result = get_tangential_arc_to_info(TangentialArcInfoInput {

0 commit comments

Comments
 (0)