Improve error messages around PI and other numbers with unknown units (#7457)

* Improve docs around PI

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor and polish type error messages

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Add suggestion to fix unknown numbers error

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Don't warn so often about unknown units

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2025-06-13 02:20:04 +12:00
committed by GitHub
parent bf87c23ea8
commit 1443f3ab39
21 changed files with 308 additions and 251 deletions

View File

@ -783,6 +783,7 @@ impl Severity {
pub enum Tag {
Deprecated,
Unnecessary,
UnknownNumericUnits,
None,
}

View File

@ -801,6 +801,10 @@ fn apply_ascription(
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into())
.map_err(|e| KclError::new_semantic(e.into()))?;
if matches!(&ty, &RuntimeType::Primitive(PrimitiveType::Number(..))) {
exec_state.clear_units_warnings(&source_range);
}
value.coerce(&ty, false, exec_state).map_err(|_| {
let suggestion = if ty == RuntimeType::length() {
", you might try coercing to a fully specified numeric type such as `number(mm)`"
@ -809,9 +813,14 @@ fn apply_ascription(
} else {
""
};
let ty_str = if let Some(ty) = value.principal_type() {
format!("(with type `{ty}`) ")
} else {
String::new()
};
KclError::new_semantic(KclErrorDetails::new(
format!(
"could not coerce value of type {} to type {ty}{suggestion}",
"could not coerce {} {ty_str}to type `{ty}`{suggestion}",
value.human_friendly_type()
),
vec![source_range],
@ -1021,14 +1030,13 @@ impl Node<MemberExpression> {
.map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
.collect(),
}),
(being_indexed, _, _) => {
let t = being_indexed.human_friendly_type();
let article = article_for(&t);
Err(KclError::new_semantic(KclErrorDetails::new(
format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
vec![self.clone().into()],
)))
}
(being_indexed, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Only arrays can be indexed, but you're trying to index {}",
being_indexed.human_friendly_type()
),
vec![self.clone().into()],
))),
}
}
}
@ -1203,11 +1211,14 @@ impl Node<BinaryExpression> {
fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) {
if ty == &NumericType::Unknown {
// TODO suggest how to fix this
exec_state.warn(CompilationError::err(
self.as_source_range(),
format!("{} numbers which have unknown or incompatible units.", verb),
));
let sr = self.as_source_range();
exec_state.clear_units_warnings(&sr);
let mut err = CompilationError::err(
sr,
format!("{} numbers which have unknown or incompatible units.\nYou can probably fix this error by specifying the units using type ascription, e.g., `len: number(mm)` or `(a * b): number(deg)`.", verb),
);
err.tag = crate::errors::Tag::UnknownNumericUnits;
exec_state.warn(err);
}
}
}
@ -1756,7 +1767,7 @@ a = 42: string
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce value of type number(default units) to type string"),
.contains("could not coerce a number (with type `number`) to type `string`"),
"Expected error but found {err:?}"
);
@ -1767,7 +1778,7 @@ a = 42: Plane
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce value of type number(default units) to type Plane"),
.contains("could not coerce a number (with type `number`) to type `Plane`"),
"Expected error but found {err:?}"
);
@ -1778,7 +1789,7 @@ arr = [0]: [string]
let err = result.unwrap_err();
assert!(
err.to_string().contains(
"could not coerce value of type array of number(default units) with 1 value to type [string]"
"could not coerce an array of `number` with 1 value (with type `[any; 1]`) to type `[string]`"
),
"Expected error but found {err:?}"
);
@ -1789,8 +1800,9 @@ mixedArr = [0, "a"]: [number(mm)]
let result = parse_execute(program).await;
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce value of type array of number(default units), string with 2 values to type [number(mm)]"),
err.to_string().contains(
"could not coerce an array of `number`, `string` (with type `[any; 2]`) to type `[number(mm)]`"
),
"Expected error but found {err:?}"
);
}
@ -2095,4 +2107,19 @@ y = x: number(Length)"#;
assert_eq!(num.n, 2.0);
assert_eq!(num.ty, NumericType::mm());
}
#[tokio::test(flavor = "multi_thread")]
async fn one_warning_unknown() {
let ast = r#"
// Should warn once
a = PI * 2
// Should warn once
b = (PI * 2) / 3
// Should not warn
c = ((PI * 2) / 3): number(deg)
"#;
let result = parse_execute(ast).await.unwrap();
assert_eq!(result.exec_state.errors().len(), 2);
}
}

View File

@ -532,6 +532,44 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
Ok(())
}
fn type_err_str(expected: &Type, found: &KclValue, source_range: &SourceRange, exec_state: &mut ExecState) -> String {
fn strip_backticks(s: &str) -> &str {
let mut result = s;
if s.starts_with('`') {
result = &result[1..]
}
if s.ends_with('`') {
result = &result[..result.len() - 1]
}
result
}
let expected_human = expected.human_friendly_type();
let expected_ty = expected.to_string();
let expected_str =
if expected_human == expected_ty || expected_human == format!("a value with type `{expected_ty}`") {
format!("a value with type `{expected_ty}`")
} else {
format!("{expected_human} (`{expected_ty}`)")
};
let found_human = found.human_friendly_type();
let found_ty = found.principal_type_string();
let found_str = if found_human == found_ty || found_human == format!("a {}", strip_backticks(&found_ty)) {
format!("a value with type {}", found_ty)
} else {
format!("{found_human} (with type {})", found_ty)
};
let mut result = format!("{expected_str}, but found {found_str}.");
if found.is_unknown_number() {
exec_state.clear_units_warnings(source_range);
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: number(mm)` or `(a * b): number(deg)`.");
}
result
}
fn type_check_params_kw(
fn_name: Option<&str>,
fn_def: &FunctionDefinition<'_>,
@ -556,18 +594,19 @@ fn type_check_params_kw(
// For optional args, passing None should be the same as not passing an arg.
if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) {
if let Some(ty) = ty {
let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range)
.map_err(|e| KclError::new_semantic(e.into()))?;
arg.value = arg
.value
.coerce(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::new_semantic(e.into()))?,
&rty,
true,
exec_state,
)
.map_err(|e| {
let mut message = format!(
"{label} requires a value with type `{}`, but found {}",
ty,
arg.value.human_friendly_type(),
"{label} requires {}",
type_err_str(ty, &arg.value, &arg.source_range, exec_state),
);
if let Some(ty) = e.explicit_coercion {
// TODO if we have access to the AST for the argument we could choose which example to suggest.
@ -630,28 +669,20 @@ fn type_check_params_kw(
if let Some(arg) = &mut args.unlabeled {
if let Some((_, Some(ty))) = &fn_def.input_arg {
arg.1.value = arg
.1
.value
.coerce(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
.map_err(|e| KclError::new_semantic(e.into()))?,
true,
exec_state,
)
.map_err(|_| {
KclError::new_semantic(KclErrorDetails::new(
format!(
"The input argument of {} requires a value with type `{}`, but found {}",
fn_name
.map(|n| format!("`{}`", n))
.unwrap_or_else(|| "this function".to_owned()),
ty,
arg.1.value.human_friendly_type()
),
vec![arg.1.source_range],
))
})?;
let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
.map_err(|e| KclError::new_semantic(e.into()))?;
arg.1.value = arg.1.value.coerce(&rty, true, exec_state).map_err(|_| {
KclError::new_semantic(KclErrorDetails::new(
format!(
"The input argument of {} requires {}",
fn_name
.map(|n| format!("`{}`", n))
.unwrap_or_else(|| "this function".to_owned()),
type_err_str(ty, &arg.1.value, &arg.1.source_range, exec_state),
),
vec![arg.1.source_range],
))
})?;
}
} else if let Some((name, _)) = &fn_def.input_arg {
if let Some(arg) = args.labeled.get(name) {
@ -747,9 +778,8 @@ fn coerce_result_type(
let val = val.coerce(&ty, true, exec_state).map_err(|_| {
KclError::new_semantic(KclErrorDetails::new(
format!(
"This function requires its result to be of type `{}`, but found {}",
ty.human_friendly_type(),
val.human_friendly_type(),
"This function requires its result to be {}",
type_err_str(ret_ty, &val, &(&val).into(), exec_state)
),
ret_ty.as_source_ranges(),
))
@ -928,7 +958,7 @@ msg2 = makeMessage(prefix = 1, suffix = 3)"#;
let err = parse_execute(program).await.unwrap_err();
assert_eq!(
err.message(),
"prefix requires a value with type `string`, but found number(default units)"
"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: number(mm)` or `(a * b): number(deg)`."
)
}
}

View File

@ -4,7 +4,6 @@ use anyhow::Result;
use schemars::JsonSchema;
use serde::Serialize;
use super::types::UnitType;
use crate::{
errors::KclErrorDetails,
execution::{
@ -281,69 +280,57 @@ impl KclValue {
/// Human readable type name used in error messages. Should not be relied
/// on for program logic.
pub(crate) fn human_friendly_type(&self) -> String {
self.inner_human_friendly_type(1)
}
fn inner_human_friendly_type(&self, max_depth: usize) -> String {
if let Some(pt) = self.principal_type() {
if max_depth > 0 {
// The principal type of an array uses the array's element type,
// which is oftentimes `any`, and that's not a helpful message. So
// we show the actual elements.
if let KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } = self {
// If it's empty, we want to show the type of the array.
if !value.is_empty() {
// A max of 3 is good because it's common to use 3D points.
let max = 3;
let len = value.len();
let ellipsis = if len > max { ", ..." } else { "" };
let element_label = if len == 1 { "value" } else { "values" };
let element_tys = value
.iter()
.take(max)
.map(|elem| elem.inner_human_friendly_type(max_depth - 1))
.collect::<Vec<_>>()
.join(", ");
return format!("array of {element_tys}{ellipsis} with {len} {element_label}");
}
}
}
return pt.to_string();
}
match self {
KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid { .. } => "Solid",
KclValue::Sketch { .. } => "Sketch",
KclValue::Helix { .. } => "Helix",
KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function",
KclValue::Plane { .. } => "Plane",
KclValue::Face { .. } => "Face",
KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Uuid { .. } => "a unique ID (uuid)".to_owned(),
KclValue::TagDeclarator(_) => "a tag declarator".to_owned(),
KclValue::TagIdentifier(_) => "a tag identifier".to_owned(),
KclValue::Solid { .. } => "a solid".to_owned(),
KclValue::Sketch { .. } => "a sketch".to_owned(),
KclValue::Helix { .. } => "a helix".to_owned(),
KclValue::ImportedGeometry(_) => "an imported geometry".to_owned(),
KclValue::Function { .. } => "a function".to_owned(),
KclValue::Plane { .. } => "a plane".to_owned(),
KclValue::Face { .. } => "a face".to_owned(),
KclValue::Bool { .. } => "a boolean (`true` or `false`)".to_owned(),
KclValue::Number {
ty: NumericType::Unknown,
..
} => "number(unknown units)",
} => "a number with unknown units".to_owned(),
KclValue::Number {
ty: NumericType::Known(UnitType::Length(_)),
ty: NumericType::Known(units),
..
} => "number(Length)",
KclValue::Number {
ty: NumericType::Known(UnitType::Angle(_)),
..
} => "number(Angle)",
KclValue::Number { .. } => "number",
KclValue::String { .. } => "string (text)",
KclValue::Tuple { .. } => "tuple (list)",
KclValue::HomArray { .. } => "array (list)",
KclValue::Object { .. } => "object",
KclValue::Module { .. } => "module",
KclValue::Type { .. } => "type",
KclValue::KclNone { .. } => "None",
} => format!("a number ({units})"),
KclValue::Number { .. } => "a number".to_owned(),
KclValue::String { .. } => "a string".to_owned(),
KclValue::Object { .. } => "an object".to_owned(),
KclValue::Module { .. } => "a module".to_owned(),
KclValue::Type { .. } => "a type".to_owned(),
KclValue::KclNone { .. } => "none".to_owned(),
KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
if value.is_empty() {
"an empty array".to_owned()
} else {
// A max of 3 is good because it's common to use 3D points.
const MAX: usize = 3;
let len = value.len();
let element_tys = value
.iter()
.take(MAX)
.map(|elem| elem.principal_type_string())
.collect::<Vec<_>>()
.join(", ");
let mut result = format!("an array of {element_tys}");
if len > MAX {
result.push_str(&format!(", ... with {len} values"));
}
if len == 1 {
result.push_str(" with 1 value");
}
result
}
}
}
.to_owned()
}
pub(crate) fn from_literal(literal: Node<Literal>, exec_state: &mut ExecState) -> Self {
@ -602,6 +589,13 @@ impl KclValue {
})
}
pub fn is_unknown_number(&self) -> bool {
match self {
KclValue::Number { ty, .. } => !ty.is_fully_specified(),
_ => false,
}
}
pub fn value_str(&self) -> Option<String> {
match self {
KclValue::Bool { value, .. } => Some(format!("{value}")),
@ -649,6 +643,8 @@ impl From<GeometryWithImportedGeometry> for KclValue {
#[cfg(test)]
mod tests {
use crate::exec::UnitType;
use super::*;
#[test]
@ -658,21 +654,21 @@ mod tests {
ty: NumericType::Known(UnitType::Length(UnitLen::Unknown)),
meta: vec![],
};
assert_eq!(len.human_friendly_type(), "number(Length)".to_string());
assert_eq!(len.human_friendly_type(), "a number (Length)".to_string());
let unknown = KclValue::Number {
value: 1.0,
ty: NumericType::Unknown,
meta: vec![],
};
assert_eq!(unknown.human_friendly_type(), "number(unknown units)".to_string());
assert_eq!(unknown.human_friendly_type(), "a number with unknown units".to_string());
let mm = KclValue::Number {
value: 1.0,
ty: NumericType::Known(UnitType::Length(UnitLen::Mm)),
meta: vec![],
};
assert_eq!(mm.human_friendly_type(), "number(mm)".to_string());
assert_eq!(mm.human_friendly_type(), "a number (mm)".to_string());
let array1_mm = KclValue::HomArray {
value: vec![mm.clone()],
@ -680,7 +676,7 @@ mod tests {
};
assert_eq!(
array1_mm.human_friendly_type(),
"array of number(mm) with 1 value".to_string()
"an array of `number(mm)` with 1 value".to_string()
);
let array2_mm = KclValue::HomArray {
@ -689,7 +685,7 @@ mod tests {
};
assert_eq!(
array2_mm.human_friendly_type(),
"array of number(mm), number(mm) with 2 values".to_string()
"an array of `number(mm)`, `number(mm)`".to_string()
);
let array3_mm = KclValue::HomArray {
@ -698,7 +694,7 @@ mod tests {
};
assert_eq!(
array3_mm.human_friendly_type(),
"array of number(mm), number(mm), number(mm) with 3 values".to_string()
"an array of `number(mm)`, `number(mm)`, `number(mm)`".to_string()
);
let inches = KclValue::Number {
@ -712,14 +708,14 @@ mod tests {
};
assert_eq!(
array4.human_friendly_type(),
"array of number(mm), number(mm), number(in), ... with 4 values".to_string()
"an array of `number(mm)`, `number(mm)`, `number(in)`, ... with 4 values".to_string()
);
let empty_array = KclValue::HomArray {
value: vec![],
ty: RuntimeType::any(),
};
assert_eq!(empty_array.human_friendly_type(), "[any; 0]".to_string());
assert_eq!(empty_array.human_friendly_type(), "an empty array".to_string());
let array_nested = KclValue::HomArray {
value: vec![array2_mm.clone()],
@ -727,7 +723,7 @@ mod tests {
};
assert_eq!(
array_nested.human_friendly_type(),
"array of [any; 2] with 1 value".to_string()
"an array of `[any; 2]` with 1 value".to_string()
);
}
}

View File

@ -1931,13 +1931,13 @@ notNull = !myNull
"#;
assert_eq!(
parse_execute(code1).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: number(default units)",
"Cannot apply unary operator ! to non-boolean value: a number",
);
let code2 = "notZero = !0";
assert_eq!(
parse_execute(code2).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: number(default units)",
"Cannot apply unary operator ! to non-boolean value: a number",
);
let code3 = r#"
@ -1945,7 +1945,7 @@ notEmptyString = !""
"#;
assert_eq!(
parse_execute(code3).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: string",
"Cannot apply unary operator ! to non-boolean value: a string",
);
let code4 = r#"
@ -1954,7 +1954,7 @@ notMember = !obj.a
"#;
assert_eq!(
parse_execute(code4).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: number(default units)",
"Cannot apply unary operator ! to non-boolean value: a number",
);
let code5 = "
@ -1962,7 +1962,7 @@ a = []
notArray = !a";
assert_eq!(
parse_execute(code5).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: [any; 0]",
"Cannot apply unary operator ! to non-boolean value: an empty array",
);
let code6 = "
@ -1970,7 +1970,7 @@ x = {}
notObject = !x";
assert_eq!(
parse_execute(code6).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: { }",
"Cannot apply unary operator ! to non-boolean value: an object",
);
let code7 = "
@ -1996,7 +1996,7 @@ notTagDeclarator = !myTagDeclarator";
assert!(
tag_declarator_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
.starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
"Actual error: {:?}",
tag_declarator_err
);
@ -2010,7 +2010,7 @@ notTagIdentifier = !myTag";
assert!(
tag_identifier_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
.starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
"Actual error: {:?}",
tag_identifier_err
);

View File

@ -145,6 +145,17 @@ impl ExecState {
self.global.errors.push(e);
}
pub fn clear_units_warnings(&mut self, source_range: &SourceRange) {
self.global.errors = std::mem::take(&mut self.global.errors)
.into_iter()
.filter(|e| {
e.severity != Severity::Warning
|| !source_range.contains_range(&e.source_range)
|| e.tag != crate::errors::Tag::UnknownNumericUnits
})
.collect();
}
pub fn errors(&self) -> &[CompilationError] {
&self.global.errors
}

View File

@ -438,7 +438,7 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Any => write!(f, "any"),
PrimitiveType::Number(NumericType::Known(unit)) => write!(f, "number({unit})"),
PrimitiveType::Number(NumericType::Unknown) => write!(f, "number(unknown units)"),
PrimitiveType::Number(NumericType::Default { .. }) => write!(f, "number(default units)"),
PrimitiveType::Number(NumericType::Default { .. }) => write!(f, "number"),
PrimitiveType::Number(NumericType::Any) => write!(f, "number(any units)"),
PrimitiveType::String => write!(f, "string"),
PrimitiveType::Boolean => write!(f, "bool"),
@ -453,8 +453,8 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Axis2d => write!(f, "Axis2d"),
PrimitiveType::Axis3d => write!(f, "Axis3d"),
PrimitiveType::Helix => write!(f, "Helix"),
PrimitiveType::ImportedGeometry => write!(f, "imported geometry"),
PrimitiveType::Function => write!(f, "function"),
PrimitiveType::ImportedGeometry => write!(f, "ImportedGeometry"),
PrimitiveType::Function => write!(f, "fn"),
}
}
}
@ -1508,6 +1508,23 @@ impl KclValue {
KclValue::Module { .. } | KclValue::KclNone { .. } | KclValue::Type { .. } => None,
}
}
pub fn principal_type_string(&self) -> String {
if let Some(ty) = self.principal_type() {
return format!("`{ty}`");
}
match self {
KclValue::Module { .. } => "module",
KclValue::KclNone { .. } => "none",
KclValue::Type { .. } => "type",
_ => {
debug_assert!(false);
"<unexpected type>"
}
}
.to_owned()
}
}
#[cfg(test)]

View File

@ -47,7 +47,7 @@ impl Tag {
match self {
Tag::Deprecated => Some(vec![DiagnosticTag::DEPRECATED]),
Tag::Unnecessary => Some(vec![DiagnosticTag::UNNECESSARY]),
Tag::None => None,
Tag::UnknownNumericUnits | Tag::None => None,
}
}
}

View File

@ -950,7 +950,7 @@ startSketchOn(XY)
match hover.unwrap().contents {
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
assert!(value.contains("foo: number(default units) = 42"));
assert!(value.contains("foo: number = 42"));
}
_ => unreachable!(),
}
@ -3900,7 +3900,7 @@ startSketchOn(XY)
match hover.unwrap().contents {
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
assert!(value.contains("foo: number(default units) = 42"));
assert!(value.contains("foo: number = 42"));
}
_ => unreachable!(),
}

View File

@ -3174,6 +3174,19 @@ impl PrimitiveType {
_ => None,
}
}
fn display_multiple(&self) -> String {
match self {
PrimitiveType::Any => "values".to_owned(),
PrimitiveType::Number(_) => "numbers".to_owned(),
PrimitiveType::String => "strings".to_owned(),
PrimitiveType::Boolean => "bools".to_owned(),
PrimitiveType::ImportedGeometry => "imported geometries".to_owned(),
PrimitiveType::Function(_) => "functions".to_owned(),
PrimitiveType::Named { id } => format!("`{}`s", id.name),
PrimitiveType::Tag => "tags".to_owned(),
}
}
}
impl fmt::Display for PrimitiveType {
@ -3264,6 +3277,53 @@ pub enum Type {
},
}
impl Type {
pub fn human_friendly_type(&self) -> String {
match self {
Type::Primitive(ty) => format!("a value with type `{ty}`"),
Type::Array {
ty,
len: ArrayLen::None | ArrayLen::Minimum(0),
} => {
format!("an array of {}", ty.display_multiple())
}
Type::Array {
ty,
len: ArrayLen::Minimum(1),
} => format!("one or more {}", ty.display_multiple()),
Type::Array {
ty,
len: ArrayLen::Minimum(n),
} => {
format!("an array of {n} or more {}", ty.display_multiple())
}
Type::Array {
ty,
len: ArrayLen::Known(n),
} => format!("an array of {n} {}", ty.display_multiple()),
Type::Union { tys } => tys
.iter()
.map(|t| t.human_friendly_type())
.collect::<Vec<_>>()
.join(" or "),
Type::Object { .. } => format!("an object with fields `{}`", self),
}
}
fn display_multiple(&self) -> String {
match self {
Type::Primitive(ty) => ty.display_multiple(),
Type::Array { .. } => "arrays".to_owned(),
Type::Union { tys } => tys
.iter()
.map(|t| t.display_multiple())
.collect::<Vec<_>>()
.join(" or "),
Type::Object { .. } => format!("objects with fields `{self}`"),
}
}
}
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {

View File

@ -160,7 +160,7 @@ impl Args {
None => msg_base,
Some(sugg) => format!("{msg_base}. {sugg}"),
};
if message.contains("one or more Solids or imported geometry but it's actually of type Sketch") {
if message.contains("one or more Solids or ImportedGeometry but it's actually of type Sketch") {
message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
}
KclError::new_semantic(KclErrorDetails::new(message, arg.source_ranges()))
@ -257,7 +257,7 @@ impl Args {
Some(sugg) => format!("{msg_base}. {sugg}"),
};
if message.contains("one or more Solids or imported geometry but it's actually of type Sketch") {
if message.contains("one or more Solids or ImportedGeometry but it's actually of type Sketch") {
message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
}
KclError::new_semantic(KclErrorDetails::new(message, arg.source_ranges()))
@ -448,107 +448,12 @@ impl Args {
}
}
/// Types which impl this trait can be read out of the `Args` passed into a KCL function.
pub trait FromArgs<'a>: Sized {
/// Get this type from the args passed into a KCL function, at the given index in the argument list.
fn from_args(args: &'a Args, index: usize) -> Result<Self, KclError>;
}
/// Types which impl this trait can be extracted from a `KclValue`.
pub trait FromKclValue<'a>: Sized {
/// Try to convert a KclValue into this type.
fn from_kcl_val(arg: &'a KclValue) -> Option<Self>;
}
impl<'a, T> FromArgs<'a> for T
where
T: FromKclValue<'a> + Sized,
{
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let Some(arg) = args.args.get(i) else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!("Expected an argument at index {i}"),
vec![args.source_range],
)));
};
let Some(val) = T::from_kcl_val(&arg.value) else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Argument at index {i} was supposed to be type {} but found {}",
tynm::type_name::<T>(),
arg.value.human_friendly_type(),
),
arg.source_ranges(),
)));
};
Ok(val)
}
}
impl<'a, T> FromArgs<'a> for Option<T>
where
T: FromKclValue<'a> + Sized,
{
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let Some(arg) = args.args.get(i) else { return Ok(None) };
if crate::parsing::ast::types::KclNone::from_kcl_val(&arg.value).is_some() {
return Ok(None);
}
let Some(val) = T::from_kcl_val(&arg.value) else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Argument at index {i} was supposed to be type Option<{}> but found {}",
tynm::type_name::<T>(),
arg.value.human_friendly_type()
),
arg.source_ranges(),
)));
};
Ok(Some(val))
}
}
impl<'a, A, B> FromArgs<'a> for (A, B)
where
A: FromArgs<'a>,
B: FromArgs<'a>,
{
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let a = A::from_args(args, i)?;
let b = B::from_args(args, i + 1)?;
Ok((a, b))
}
}
impl<'a, A, B, C> FromArgs<'a> for (A, B, C)
where
A: FromArgs<'a>,
B: FromArgs<'a>,
C: FromArgs<'a>,
{
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let a = A::from_args(args, i)?;
let b = B::from_args(args, i + 1)?;
let c = C::from_args(args, i + 2)?;
Ok((a, b, c))
}
}
impl<'a, A, B, C, D> FromArgs<'a> for (A, B, C, D)
where
A: FromArgs<'a>,
B: FromArgs<'a>,
C: FromArgs<'a>,
D: FromArgs<'a>,
{
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let a = A::from_args(args, i)?;
let b = B::from_args(args, i + 1)?;
let c = C::from_args(args, i + 2)?;
let d = D::from_args(args, i + 3)?;
Ok((a, b, c, d))
}
}
impl<'a> FromKclValue<'a> for TagNode {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
arg.get_tag_declarator().ok()