Treat singletons and arrays as subtypes rather than coercible (#7181)

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2025-05-28 16:29:23 +12:00
committed by GitHub
parent 9dfb67cf61
commit 783b6ed76c
71 changed files with 5119 additions and 6067 deletions

View File

@ -944,6 +944,9 @@ impl Node<MemberExpression> {
)))
}
}
// Singletons and single-element arrays should be interchangeable, but only indexing by 0 should work.
// This is kind of a silly property, but it's possible it occurs in generic code or something.
(obj, Property::UInt(0), _) => Ok(obj),
(KclValue::HomArray { .. }, p, _) => {
let t = p.type_name();
let article = article_for(t);
@ -1981,6 +1984,38 @@ startSketchOn(XY)
assert!(e.message().contains("sqrt"), "Error message: '{}'", e.message());
}
#[tokio::test(flavor = "multi_thread")]
async fn non_array_fns() {
let ast = r#"push(1, item = 2)
pop(1)
map(1, f = fn(@x) { return x + 1 })
reduce(1, f = fn(@x, accum) { return accum + x}, initial = 0)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn non_array_indexing() {
let good = r#"a = 42
good = a[0]
"#;
let result = parse_execute(good).await.unwrap();
let mem = result.exec_state.stack();
let num = mem
.memory
.get_from("good", result.mem_env, SourceRange::default(), 0)
.unwrap()
.as_ty_f64()
.unwrap();
assert_eq!(num.n, 42.0);
let bad = r#"a = 42
bad = a[1]
"#;
parse_execute(bad).await.unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
async fn coerce_unknown_to_length() {
let ast = r#"x = 2mm * 2mm

View File

@ -1,7 +1,6 @@
use async_recursion::async_recursion;
use indexmap::IndexMap;
use super::{types::ArrayLen, EnvironmentRef};
use crate::{
docs::StdLibFn,
errors::{KclError, KclErrorDetails},
@ -10,7 +9,8 @@ use crate::{
kcl_value::FunctionSource,
memory,
types::RuntimeType,
BodyType, ExecState, ExecutorContext, KclValue, Metadata, StatementKind, TagEngineInfo, TagIdentifier,
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, StatementKind, TagEngineInfo,
TagIdentifier,
},
parsing::ast::types::{CallExpressionKw, DefaultParamVal, FunctionExpression, Node, Program, Type},
source_range::SourceRange,
@ -294,7 +294,7 @@ impl Node<CallExpressionKw> {
// exec_state.
let func = fn_name.get_result(exec_state, ctx).await?.clone();
let Some(fn_src) = func.as_fn() else {
let Some(fn_src) = func.as_function() else {
return Err(KclError::Semantic(KclErrorDetails::new(
"cannot call this because it isn't a function".to_string(),
vec![callsite],
@ -787,18 +787,8 @@ fn coerce_result_type(
) -> Result<Option<KclValue>, KclError> {
if let Ok(Some(val)) = result {
if let Some(ret_ty) = &fn_def.return_type {
let mut ty = RuntimeType::from_parsed(ret_ty.inner.clone(), exec_state, ret_ty.as_source_range())
let ty = RuntimeType::from_parsed(ret_ty.inner.clone(), exec_state, ret_ty.as_source_range())
.map_err(|e| KclError::Semantic(e.into()))?;
// Treat `[T; 1+]` as `T | [T; 1+]` (which can't yet be expressed in our syntax of types).
// This is a very specific hack which exists because some std functions can produce arrays
// but usually only make a singleton and the frontend expects the singleton.
// If we can make the frontend work on arrays (or at least arrays of length 1), then this
// can be removed.
// I believe this is safe, since anywhere which requires an array should coerce the singleton
// to an array and we only do this hack for return values.
if let RuntimeType::Array(inner, ArrayLen::Minimum(1)) = &ty {
ty = RuntimeType::Union(vec![(**inner).clone(), ty]);
}
let val = val.coerce(&ty, true, exec_state).map_err(|_| {
KclError::Semantic(KclErrorDetails::new(
format!(

View File

@ -290,15 +290,15 @@ impl KclValue {
// 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 Some(elements) = self.as_array() {
if let KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } = self {
// If it's empty, we want to show the type of the array.
if !elements.is_empty() {
if !value.is_empty() {
// A max of 3 is good because it's common to use 3D points.
let max = 3;
let len = elements.len();
let len = value.len();
let ellipsis = if len > max { ", ..." } else { "" };
let element_label = if len == 1 { "value" } else { "values" };
let element_tys = elements
let element_tys = value
.iter()
.take(max)
.map(|elem| elem.inner_human_friendly_type(max_depth - 1))
@ -442,144 +442,128 @@ impl KclValue {
}
pub fn as_object(&self) -> Option<&KclObjectFields> {
if let KclValue::Object { value, meta: _ } = &self {
Some(value)
} else {
None
}
}
pub fn into_object(self) -> Option<KclObjectFields> {
if let KclValue::Object { value, meta: _ } = self {
Some(value)
} else {
None
}
}
pub fn as_str(&self) -> Option<&str> {
if let KclValue::String { value, meta: _ } = &self {
Some(value)
} else {
None
}
}
pub fn as_array(&self) -> Option<&[KclValue]> {
match self {
KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => Some(value),
KclValue::Object { value, .. } => Some(value),
_ => None,
}
}
pub fn into_object(self) -> Option<KclObjectFields> {
match self {
KclValue::Object { value, .. } => Some(value),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
KclValue::String { value, .. } => Some(value),
_ => None,
}
}
pub fn into_array(self) -> Vec<KclValue> {
match self {
KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => value,
_ => vec![self],
}
}
pub fn as_point2d(&self) -> Option<[TyF64; 2]> {
let arr = self.as_array()?;
if arr.len() != 2 {
let value = match self {
KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => value,
_ => return None,
};
if value.len() != 2 {
return None;
}
let x = arr[0].as_ty_f64()?;
let y = arr[1].as_ty_f64()?;
let x = value[0].as_ty_f64()?;
let y = value[1].as_ty_f64()?;
Some([x, y])
}
pub fn as_point3d(&self) -> Option<[TyF64; 3]> {
let arr = self.as_array()?;
if arr.len() != 3 {
let value = match self {
KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => value,
_ => return None,
};
if value.len() != 3 {
return None;
}
let x = arr[0].as_ty_f64()?;
let y = arr[1].as_ty_f64()?;
let z = arr[2].as_ty_f64()?;
let x = value[0].as_ty_f64()?;
let y = value[1].as_ty_f64()?;
let z = value[2].as_ty_f64()?;
Some([x, y, z])
}
pub fn as_uuid(&self) -> Option<uuid::Uuid> {
if let KclValue::Uuid { value, meta: _ } = &self {
Some(*value)
} else {
None
match self {
KclValue::Uuid { value, .. } => Some(*value),
_ => None,
}
}
pub fn as_plane(&self) -> Option<&Plane> {
if let KclValue::Plane { value } = &self {
Some(value)
} else {
None
match self {
KclValue::Plane { value, .. } => Some(value),
_ => None,
}
}
pub fn as_solid(&self) -> Option<&Solid> {
if let KclValue::Solid { value } = &self {
Some(value)
} else {
None
match self {
KclValue::Solid { value, .. } => Some(value),
_ => None,
}
}
pub fn as_sketch(&self) -> Option<&Sketch> {
if let KclValue::Sketch { value } = self {
Some(value)
} else {
None
match self {
KclValue::Sketch { value, .. } => Some(value),
_ => None,
}
}
pub fn as_mut_sketch(&mut self) -> Option<&mut Sketch> {
if let KclValue::Sketch { value } = self {
Some(value)
} else {
None
match self {
KclValue::Sketch { value } => Some(value),
_ => None,
}
}
pub fn as_mut_tag(&mut self) -> Option<&mut TagIdentifier> {
if let KclValue::TagIdentifier(value) = self {
Some(value)
} else {
None
match self {
KclValue::TagIdentifier(value) => Some(value),
_ => None,
}
}
#[cfg(test)]
pub fn as_f64(&self) -> Option<f64> {
if let KclValue::Number { value, .. } = &self {
Some(*value)
} else {
None
match self {
KclValue::Number { value, .. } => Some(*value),
_ => None,
}
}
pub fn as_ty_f64(&self) -> Option<TyF64> {
if let KclValue::Number { value, ty, .. } = &self {
Some(TyF64::new(*value, ty.clone()))
} else {
None
match self {
KclValue::Number { value, ty, .. } => Some(TyF64::new(*value, ty.clone())),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
if let KclValue::Bool { value, meta: _ } = &self {
Some(*value)
} else {
None
match self {
KclValue::Bool { value, .. } => Some(*value),
_ => None,
}
}
/// If this value fits in a u32, return it.
pub fn get_u32(&self, source_ranges: Vec<SourceRange>) -> Result<u32, KclError> {
let u = self.as_int().and_then(|n| u64::try_from(n).ok()).ok_or_else(|| {
KclError::Semantic(KclErrorDetails::new(
"Expected an integer >= 0".to_owned(),
source_ranges.clone(),
))
})?;
u32::try_from(u)
.map_err(|_| KclError::Semantic(KclErrorDetails::new("Number was too big".to_owned(), source_ranges)))
}
/// If this value is of type function, return it.
pub fn get_function(&self) -> Option<&FunctionSource> {
pub fn as_function(&self) -> Option<&FunctionSource> {
match self {
KclValue::Function { value, .. } => Some(value),
_ => None,
@ -610,20 +594,12 @@ impl KclValue {
/// If this KCL value is a bool, retrieve it.
pub fn get_bool(&self) -> Result<bool, KclError> {
let Self::Bool { value: b, .. } = self else {
return Err(KclError::Type(KclErrorDetails::new(
self.as_bool().ok_or_else(|| {
KclError::Type(KclErrorDetails::new(
format!("Expected bool, found {}", self.human_friendly_type()),
self.into(),
)));
};
Ok(*b)
}
pub fn as_fn(&self) -> Option<&FunctionSource> {
match self {
KclValue::Function { value, .. } => Some(value),
_ => None,
}
))
})
}
pub fn value_str(&self) -> Option<String> {

View File

@ -32,6 +32,10 @@ impl RuntimeType {
RuntimeType::Primitive(PrimitiveType::Any)
}
pub fn any_array() -> Self {
RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Any)), ArrayLen::None)
}
pub fn edge() -> Self {
RuntimeType::Primitive(PrimitiveType::Edge)
}
@ -238,12 +242,21 @@ impl RuntimeType {
(Primitive(t1), Primitive(t2)) => t1.subtype(t2),
(Array(t1, l1), Array(t2, l2)) => t1.subtype(t2) && l1.subtype(*l2),
(Tuple(t1), Tuple(t2)) => t1.len() == t2.len() && t1.iter().zip(t2).all(|(t1, t2)| t1.subtype(t2)),
(Union(ts1), Union(ts2)) => ts1.iter().all(|t| ts2.contains(t)),
(t1, Union(ts2)) => ts2.iter().any(|t| t1.subtype(t)),
(Object(t1), Object(t2)) => t2
.iter()
.all(|(f, t)| t1.iter().any(|(ff, tt)| f == ff && tt.subtype(t))),
// Equality between Axis types and their object representation.
// Equivalence between singleton types and single-item arrays/tuples of the same type (plus transitivity with the array subtyping).
(t1, RuntimeType::Array(t2, l)) if t1.subtype(t2) && ArrayLen::Known(1).subtype(*l) => true,
(RuntimeType::Array(t1, ArrayLen::Known(1)), t2) if t1.subtype(t2) => true,
(t1, RuntimeType::Tuple(t2)) if !t2.is_empty() && t1.subtype(&t2[0]) => true,
(RuntimeType::Tuple(t1), t2) if t1.len() == 1 && t1[0].subtype(t2) => true,
// Equivalence between Axis types and their object representation.
(Object(t1), Primitive(PrimitiveType::Axis2d)) => {
t1.iter()
.any(|(n, t)| n == "origin" && t.subtype(&RuntimeType::point2d()))
@ -1051,6 +1064,20 @@ impl KclValue {
convert_units: bool,
exec_state: &mut ExecState,
) -> Result<KclValue, CoercionError> {
match self {
KclValue::Tuple { value, .. } if value.len() == 1 && !matches!(ty, RuntimeType::Tuple(..)) => {
if let Ok(coerced) = value[0].coerce(ty, convert_units, exec_state) {
return Ok(coerced);
}
}
KclValue::HomArray { value, .. } if value.len() == 1 && !matches!(ty, RuntimeType::Array(..)) => {
if let Ok(coerced) = value[0].coerce(ty, convert_units, exec_state) {
return Ok(coerced);
}
}
_ => {}
}
match ty {
RuntimeType::Primitive(ty) => self.coerce_to_primitive_type(ty, convert_units, exec_state),
RuntimeType::Array(ty, len) => self.coerce_to_array_type(ty, convert_units, *len, exec_state, false),
@ -1066,15 +1093,11 @@ impl KclValue {
convert_units: bool,
exec_state: &mut ExecState,
) -> Result<KclValue, CoercionError> {
let value = match self {
KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } if value.len() == 1 => &value[0],
_ => self,
};
match ty {
PrimitiveType::Any => Ok(value.clone()),
PrimitiveType::Any => Ok(self.clone()),
PrimitiveType::Number(ty) => {
if convert_units {
return ty.coerce(value);
return ty.coerce(self);
}
// Instead of converting units, reinterpret the number as having
@ -1083,7 +1106,7 @@ impl KclValue {
// If the user is explicitly specifying units, treat the value
// as having had its units erased, rather than forcing the user
// to explicitly erase them.
if let KclValue::Number { value: n, meta, .. } = &value {
if let KclValue::Number { value: n, meta, .. } = &self {
if ty.is_fully_specified() {
let value = KclValue::Number {
ty: NumericType::Any,
@ -1093,34 +1116,34 @@ impl KclValue {
return ty.coerce(&value);
}
}
ty.coerce(value)
ty.coerce(self)
}
PrimitiveType::String => match value {
KclValue::String { .. } => Ok(value.clone()),
PrimitiveType::String => match self {
KclValue::String { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::Boolean => match value {
KclValue::Bool { .. } => Ok(value.clone()),
PrimitiveType::Boolean => match self {
KclValue::Bool { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::Sketch => match value {
KclValue::Sketch { .. } => Ok(value.clone()),
PrimitiveType::Sketch => match self {
KclValue::Sketch { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::Solid => match value {
KclValue::Solid { .. } => Ok(value.clone()),
PrimitiveType::Solid => match self {
KclValue::Solid { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::Plane => match value {
PrimitiveType::Plane => match self {
KclValue::String { value: s, .. }
if [
"xy", "xz", "yz", "-xy", "-xz", "-yz", "XY", "XZ", "YZ", "-XY", "-XZ", "-YZ",
]
.contains(&&**s) =>
{
Ok(value.clone())
Ok(self.clone())
}
KclValue::Plane { .. } => Ok(value.clone()),
KclValue::Plane { .. } => Ok(self.clone()),
KclValue::Object { value, meta } => {
let origin = value
.get("origin")
@ -1159,20 +1182,20 @@ impl KclValue {
}
_ => Err(self.into()),
},
PrimitiveType::Face => match value {
KclValue::Face { .. } => Ok(value.clone()),
PrimitiveType::Face => match self {
KclValue::Face { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::Helix => match value {
KclValue::Helix { .. } => Ok(value.clone()),
PrimitiveType::Helix => match self {
KclValue::Helix { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::Edge => match value {
KclValue::Uuid { .. } => Ok(value.clone()),
KclValue::TagIdentifier { .. } => Ok(value.clone()),
PrimitiveType::Edge => match self {
KclValue::Uuid { .. } => Ok(self.clone()),
KclValue::TagIdentifier { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::Axis2d => match value {
PrimitiveType::Axis2d => match self {
KclValue::Object { value: values, meta } => {
if values
.get("origin")
@ -1183,7 +1206,7 @@ impl KclValue {
.ok_or(CoercionError::from(self))?
.has_type(&RuntimeType::point2d())
{
return Ok(value.clone());
return Ok(self.clone());
}
let origin = values.get("origin").ok_or(self.into()).and_then(|p| {
@ -1212,7 +1235,7 @@ impl KclValue {
}
_ => Err(self.into()),
},
PrimitiveType::Axis3d => match value {
PrimitiveType::Axis3d => match self {
KclValue::Object { value: values, meta } => {
if values
.get("origin")
@ -1223,7 +1246,7 @@ impl KclValue {
.ok_or(CoercionError::from(self))?
.has_type(&RuntimeType::point3d())
{
return Ok(value.clone());
return Ok(self.clone());
}
let origin = values.get("origin").ok_or(self.into()).and_then(|p| {
@ -1252,21 +1275,21 @@ impl KclValue {
}
_ => Err(self.into()),
},
PrimitiveType::ImportedGeometry => match value {
KclValue::ImportedGeometry { .. } => Ok(value.clone()),
PrimitiveType::ImportedGeometry => match self {
KclValue::ImportedGeometry { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::Function => match value {
KclValue::Function { .. } => Ok(value.clone()),
PrimitiveType::Function => match self {
KclValue::Function { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::TagId => match value {
KclValue::TagIdentifier { .. } => Ok(value.clone()),
PrimitiveType::TagId => match self {
KclValue::TagIdentifier { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::Tag => match value {
PrimitiveType::Tag => match self {
KclValue::TagDeclarator { .. } | KclValue::TagIdentifier { .. } | KclValue::Uuid { .. } => {
Ok(value.clone())
Ok(self.clone())
}
s @ KclValue::String { value, .. } if ["start", "end", "START", "END"].contains(&&**value) => {
Ok(s.clone())
@ -1366,10 +1389,7 @@ impl KclValue {
value: Vec::new(),
ty: ty.clone(),
}),
_ if len.satisfied(1, false).is_some() => Ok(KclValue::HomArray {
value: vec![self.coerce(ty, convert_units, exec_state)?],
ty: ty.clone(),
}),
_ if len.satisfied(1, false).is_some() => self.coerce(ty, convert_units, exec_state),
_ => Err(self.into()),
}
}
@ -1396,10 +1416,7 @@ impl KclValue {
value: Vec::new(),
meta: meta.clone(),
}),
value if tys.len() == 1 && value.has_type(&tys[0]) => Ok(KclValue::Tuple {
value: vec![value.clone()],
meta: Vec::new(),
}),
_ if tys.len() == 1 => self.coerce(&tys[0], convert_units, exec_state),
_ => Err(self.into()),
}
}
@ -1531,7 +1548,8 @@ mod test {
exec_state: &mut ExecState,
) {
let is_subtype = value == expected_value;
assert_eq!(&value.coerce(super_type, true, exec_state).unwrap(), expected_value);
let actual = value.coerce(super_type, true, exec_state).unwrap();
assert_eq!(&actual, expected_value);
assert_eq!(
is_subtype,
value.principal_type().is_some() && value.principal_type().unwrap().subtype(super_type),
@ -1566,7 +1584,7 @@ mod test {
let aty0 = RuntimeType::Array(Box::new(ty.clone()), ArrayLen::Minimum(1));
match v {
KclValue::Tuple { .. } | KclValue::HomArray { .. } => {
KclValue::HomArray { .. } => {
// These will not get wrapped if possible.
assert_coerce_results(
v,
@ -1577,53 +1595,22 @@ mod test {
},
&mut exec_state,
);
// Coercing an empty tuple or array to an array of length 1
// Coercing an empty array to an array of length 1
// should fail.
v.coerce(&aty1, true, &mut exec_state).unwrap_err();
// Coercing an empty tuple or array to an array that's
// Coercing an empty array to an array that's
// non-empty should fail.
v.coerce(&aty0, true, &mut exec_state).unwrap_err();
}
KclValue::Tuple { .. } => {}
_ => {
assert_coerce_results(
v,
&aty,
&KclValue::HomArray {
value: vec![v.clone()],
ty: ty.clone(),
},
&mut exec_state,
);
assert_coerce_results(
v,
&aty1,
&KclValue::HomArray {
value: vec![v.clone()],
ty: ty.clone(),
},
&mut exec_state,
);
assert_coerce_results(
v,
&aty0,
&KclValue::HomArray {
value: vec![v.clone()],
ty: ty.clone(),
},
&mut exec_state,
);
assert_coerce_results(v, &aty, v, &mut exec_state);
assert_coerce_results(v, &aty1, v, &mut exec_state);
assert_coerce_results(v, &aty0, v, &mut exec_state);
// Tuple subtype
let tty = RuntimeType::Tuple(vec![ty.clone()]);
assert_coerce_results(
v,
&tty,
&KclValue::Tuple {
value: vec![v.clone()],
meta: Vec::new(),
},
&mut exec_state,
);
assert_coerce_results(v, &tty, v, &mut exec_state);
}
}
}