Move axes to std constants; move helix, revolve, and mirror2d to be declared in KCL (#6105)

Move axes to std constants; move helix, revolve, and mirror2d to be declated in KCL

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2025-04-03 22:44:52 +13:00
committed by GitHub
parent 3e4505e2e3
commit aad583be2e
167 changed files with 11811 additions and 38646 deletions

View File

@ -27,6 +27,19 @@ pub enum Operation {
is_error: bool,
},
#[serde(rename_all = "camelCase")]
KclStdLibCall {
name: String,
/// The unlabeled argument to the function.
unlabeled_arg: Option<OpArg>,
/// The labeled keyword arguments to the function.
labeled_args: IndexMap<String, OpArg>,
/// The source range of the operation in the source code.
source_range: SourceRange,
/// True if the operation resulted in an error.
#[serde(default, skip_serializing_if = "is_false")]
is_error: bool,
},
#[serde(rename_all = "camelCase")]
UserDefinedFunctionCall {
/// The name of the user-defined function being called. Anonymous
/// functions have no name.
@ -49,6 +62,7 @@ impl Operation {
pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) {
match self {
Self::StdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::KclStdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::UserDefinedFunctionCall { .. } | Self::UserDefinedFunctionReturn => {}
}
}

View File

@ -3,7 +3,7 @@ use std::collections::HashMap;
use async_recursion::async_recursion;
use indexmap::IndexMap;
use super::kcl_value::TypeDef;
use super::{kcl_value::TypeDef, types::PrimitiveType};
use crate::{
engine::ExecutionKind,
errors::{KclError, KclErrorDetails},
@ -1059,6 +1059,15 @@ impl Node<UnaryExpression> {
}
let value = &self.argument.get_result(exec_state, ctx).await?;
let err = || {
KclError::Semantic(KclErrorDetails {
message: format!(
"You can only negate numbers, planes, or lines, but this is a {}",
value.human_friendly_type()
),
source_ranges: vec![self.into()],
})
};
match value {
KclValue::Number { value, ty, .. } => {
let meta = vec![Metadata {
@ -1080,13 +1089,63 @@ impl Node<UnaryExpression> {
plane.id = exec_state.next_uuid();
Ok(KclValue::Plane { value: plane })
}
_ => Err(KclError::Semantic(KclErrorDetails {
message: format!(
"You can only negate numbers or planes, but this is a {}",
value.human_friendly_type()
),
source_ranges: vec![self.into()],
})),
KclValue::Object { value: values, meta } => {
// Special-case for negating line-like objects.
let Some(direction) = values.get("direction") else {
return Err(err());
};
let direction = match direction {
KclValue::MixedArray { value: values, meta } => {
let values = values
.iter()
.map(|v| match v {
KclValue::Number { value, ty, meta } => Ok(KclValue::Number {
value: *value * -1.0,
ty: ty.clone(),
meta: meta.clone(),
}),
_ => Err(err()),
})
.collect::<Result<Vec<_>, _>>()?;
KclValue::MixedArray {
value: values,
meta: meta.clone(),
}
}
KclValue::HomArray {
value: values,
ty: ty @ RuntimeType::Primitive(PrimitiveType::Number(_)),
} => {
let values = values
.iter()
.map(|v| match v {
KclValue::Number { value, ty, meta } => Ok(KclValue::Number {
value: *value * -1.0,
ty: ty.clone(),
meta: meta.clone(),
}),
_ => Err(err()),
})
.collect::<Result<Vec<_>, _>>()?;
KclValue::HomArray {
value: values,
ty: ty.clone(),
}
}
_ => return Err(err()),
};
let mut value = values.clone();
value.insert("direction".to_owned(), direction);
Ok(KclValue::Object {
value,
meta: meta.clone(),
})
}
_ => Err(err()),
}
}
}
@ -1272,27 +1331,6 @@ impl Node<CallExpressionKw> {
// exec_state.
let func = fn_name.get_result(exec_state, ctx).await?.clone();
if !ctx.is_isolated_execution().await {
// Track call operation.
let op_labeled_args = args
.kw_args
.labeled
.iter()
.map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range)))
.collect();
exec_state.global.operations.push(Operation::UserDefinedFunctionCall {
name: Some(fn_name.to_string()),
function_source_range: func.function_def_source_range().unwrap_or_default(),
unlabeled_arg: args
.kw_args
.unlabeled
.as_ref()
.map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
labeled_args: op_labeled_args,
source_range: callsite,
});
}
let Some(fn_src) = func.as_fn() else {
return Err(KclError::Semantic(KclErrorDetails {
message: "cannot call this because it isn't a function".to_string(),
@ -1300,11 +1338,19 @@ impl Node<CallExpressionKw> {
}));
};
let return_value = fn_src.call_kw(exec_state, ctx, args, callsite).await.map_err(|e| {
// Add the call expression to the source ranges.
// TODO currently ignored by the frontend
e.add_source_ranges(vec![callsite])
})?;
let return_value = fn_src
.call_kw(Some(fn_name.to_string()), exec_state, ctx, args, callsite)
.await
.map_err(|e| {
// Add the call expression to the source ranges.
// TODO currently ignored by the frontend
e.add_source_ranges(vec![callsite])
})?;
if matches!(fn_src, FunctionSource::User { .. }) && !ctx.is_isolated_execution().await {
// Track return operation.
exec_state.global.operations.push(Operation::UserDefinedFunctionReturn);
}
let result = return_value.ok_or_else(move || {
let mut source_ranges: Vec<SourceRange> = vec![callsite];
@ -1318,11 +1364,6 @@ impl Node<CallExpressionKw> {
})
})?;
if !ctx.is_isolated_execution().await {
// Track return operation.
exec_state.global.operations.push(Operation::UserDefinedFunctionReturn);
}
Ok(result)
}
}
@ -1434,11 +1475,14 @@ impl Node<CallExpression> {
source_ranges: vec![source_range],
}));
};
let return_value = fn_src.call(exec_state, ctx, fn_args, source_range).await.map_err(|e| {
// Add the call expression to the source ranges.
// TODO currently ignored by the frontend
e.add_source_ranges(vec![source_range])
})?;
let return_value = fn_src
.call(Some(fn_name.to_string()), exec_state, ctx, fn_args, source_range)
.await
.map_err(|e| {
// Add the call expression to the source ranges.
// TODO currently ignored by the frontend
e.add_source_ranges(vec![source_range])
})?;
let result = return_value.ok_or_else(move || {
let mut source_ranges: Vec<SourceRange> = vec![source_range];
@ -2064,6 +2108,7 @@ async fn call_user_defined_function_kw(
impl FunctionSource {
pub async fn call(
&self,
fn_name: Option<String>,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
mut args: Vec<Arg>,
@ -2081,7 +2126,7 @@ impl FunctionSource {
ctx.clone(),
exec_state.mod_local.pipe_value.clone().map(|v| Arg::new(v, callsite)),
);
self.call_kw(exec_state, ctx, args, callsite).await
self.call_kw(fn_name, exec_state, ctx, args, callsite).await
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("{} requires its arguments to be labelled", props.name),
@ -2098,6 +2143,7 @@ impl FunctionSource {
pub async fn call_kw(
&self,
fn_name: Option<String>,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
mut args: crate::std::Args,
@ -2181,6 +2227,26 @@ impl FunctionSource {
}
}
let op = if props.include_in_feature_tree && !ctx.is_isolated_execution().await {
let op_labeled_args = args
.kw_args
.labeled
.iter()
.map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range)))
.collect();
Some(Operation::KclStdLibCall {
name: fn_name.unwrap_or_default(),
unlabeled_arg: args
.unlabeled_kw_arg_unconverted()
.map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
labeled_args: op_labeled_args,
source_range: callsite,
is_error: false,
})
} else {
None
};
// Attempt to call the function.
exec_state.mut_stack().push_new_env_for_rust_call();
let mut result = {
@ -2188,7 +2254,15 @@ impl FunctionSource {
let result = func(exec_state, args).await;
exec_state.mut_stack().pop_env();
// TODO support recording op into the feature tree
if let Some(mut op) = op {
op.set_std_lib_call_is_error(result.is_err());
// Track call operation. We do this after the call
// since things like patternTransform may call user code
// before running, and we will likely want to use the
// return value. The call takes ownership of the args,
// so we need to build the op before the call.
exec_state.global.operations.push(op);
}
result
}?;
@ -2197,6 +2271,27 @@ impl FunctionSource {
Ok(Some(result))
}
FunctionSource::User { ast, memory, .. } => {
if !ctx.is_isolated_execution().await {
// Track call operation.
let op_labeled_args = args
.kw_args
.labeled
.iter()
.map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range)))
.collect();
exec_state.global.operations.push(Operation::UserDefinedFunctionCall {
name: fn_name,
function_source_range: ast.as_source_range(),
unlabeled_arg: args
.kw_args
.unlabeled
.as_ref()
.map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
labeled_args: op_labeled_args,
source_range: callsite,
});
}
call_user_defined_function_kw(args.kw_args, *memory, ast, exec_state, ctx).await
}
FunctionSource::None => unreachable!(),

View File

@ -56,6 +56,20 @@ impl RuntimeType {
RuntimeType::Primitive(PrimitiveType::ImportedGeometry)
}
/// `[number; 2]`
pub fn point2d() -> Self {
RuntimeType::Array(Box::new(RuntimeType::number_any()), ArrayLen::Known(2))
}
/// `[number; 3]`
pub fn point3d() -> Self {
RuntimeType::Array(Box::new(RuntimeType::number_any()), ArrayLen::Known(3))
}
pub fn number_any() -> Self {
RuntimeType::Primitive(PrimitiveType::Number(NumericType::Any))
}
pub fn from_parsed(
value: Type,
exec_state: &mut ExecState,
@ -93,24 +107,30 @@ impl RuntimeType {
AstPrimitiveType::Number(suffix) => RuntimeType::Primitive(PrimitiveType::Number(
NumericType::from_parsed(suffix, &exec_state.mod_local.settings),
)),
AstPrimitiveType::Named(name) => {
let ty_val = exec_state
.stack()
.get(&format!("{}{}", memory::TYPE_PREFIX, name.name), source_range)
.map_err(|_| CompilationError::err(source_range, format!("Unknown type: {}", name.name)))?;
match ty_val {
KclValue::Type { value, .. } => match value {
TypeDef::RustRepr(ty, _) => RuntimeType::Primitive(ty.clone()),
TypeDef::Alias(ty) => ty.clone(),
},
_ => unreachable!(),
}
}
AstPrimitiveType::Named(name) => Self::from_alias(&name.name, exec_state, source_range)?,
AstPrimitiveType::Tag => RuntimeType::Primitive(PrimitiveType::Tag),
})
}
pub fn from_alias(
alias: &str,
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<Self, CompilationError> {
let ty_val = exec_state
.stack()
.get(&format!("{}{}", memory::TYPE_PREFIX, alias), source_range)
.map_err(|_| CompilationError::err(source_range, format!("Unknown type: {}", alias)))?;
Ok(match ty_val {
KclValue::Type { value, .. } => match value {
TypeDef::RustRepr(ty, _) => RuntimeType::Primitive(ty.clone()),
TypeDef::Alias(ty) => ty.clone(),
},
_ => unreachable!(),
})
}
pub fn human_friendly_type(&self) -> String {
match self {
RuntimeType::Primitive(ty) => ty.to_string(),
@ -143,6 +163,35 @@ impl RuntimeType {
(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.
(Object(t1), Primitive(PrimitiveType::Axis2d)) => {
t1.iter()
.any(|(n, t)| n == "origin" && t.subtype(&RuntimeType::point2d()))
&& t1
.iter()
.any(|(n, t)| n == "direction" && t.subtype(&RuntimeType::point2d()))
}
(Object(t1), Primitive(PrimitiveType::Axis3d)) => {
t1.iter()
.any(|(n, t)| n == "origin" && t.subtype(&RuntimeType::point3d()))
&& t1
.iter()
.any(|(n, t)| n == "direction" && t.subtype(&RuntimeType::point3d()))
}
(Primitive(PrimitiveType::Axis2d), Object(t2)) => {
t2.iter()
.any(|(n, t)| n == "origin" && t.subtype(&RuntimeType::point2d()))
&& t2
.iter()
.any(|(n, t)| n == "direction" && t.subtype(&RuntimeType::point2d()))
}
(Primitive(PrimitiveType::Axis3d), Object(t2)) => {
t2.iter()
.any(|(n, t)| n == "origin" && t.subtype(&RuntimeType::point3d()))
&& t2
.iter()
.any(|(n, t)| n == "direction" && t.subtype(&RuntimeType::point3d()))
}
_ => false,
}
}
@ -213,11 +262,11 @@ impl ArrayLen {
}
/// True if the length constraint is satisfied by the supplied length.
fn satisfied(self, len: usize) -> bool {
fn satisfied(self, len: usize, allow_shrink: bool) -> Option<usize> {
match self {
ArrayLen::None => true,
ArrayLen::NonEmpty => len > 0,
ArrayLen::Known(s) => len == s,
ArrayLen::None => Some(len),
ArrayLen::NonEmpty => (len > 0).then_some(len),
ArrayLen::Known(s) => (if allow_shrink { len >= s } else { len == s }).then_some(s),
}
}
}
@ -233,6 +282,9 @@ pub enum PrimitiveType {
Plane,
Helix,
Face,
Edge,
Axis2d,
Axis3d,
ImportedGeometry,
}
@ -248,6 +300,9 @@ impl PrimitiveType {
PrimitiveType::Plane => "Planes".to_owned(),
PrimitiveType::Helix => "Helices".to_owned(),
PrimitiveType::Face => "Faces".to_owned(),
PrimitiveType::Edge => "Edges".to_owned(),
PrimitiveType::Axis2d => "2d axes".to_owned(),
PrimitiveType::Axis3d => "3d axes".to_owned(),
PrimitiveType::ImportedGeometry => "imported geometries".to_owned(),
PrimitiveType::Tag => "tags".to_owned(),
}
@ -273,6 +328,9 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Solid => write!(f, "Solid"),
PrimitiveType::Plane => write!(f, "Plane"),
PrimitiveType::Face => write!(f, "Face"),
PrimitiveType::Edge => write!(f, "Edge"),
PrimitiveType::Axis2d => write!(f, "Axis2d"),
PrimitiveType::Axis3d => write!(f, "Axis3d"),
PrimitiveType::Helix => write!(f, "Helix"),
PrimitiveType::ImportedGeometry => write!(f, "imported geometry"),
}
@ -298,6 +356,10 @@ impl NumericType {
NumericType::Known(UnitType::Count)
}
pub fn mm() -> Self {
NumericType::Known(UnitType::Length(UnitLen::Mm))
}
/// Combine two types when we expect them to be equal.
pub fn combine_eq(self, other: &NumericType) -> NumericType {
if &self == other {
@ -541,7 +603,7 @@ impl KclValue {
pub fn coerce(&self, ty: &RuntimeType, exec_state: &mut ExecState) -> Option<KclValue> {
match ty {
RuntimeType::Primitive(ty) => self.coerce_to_primitive_type(ty, exec_state),
RuntimeType::Array(ty, len) => self.coerce_to_array_type(ty, *len, exec_state),
RuntimeType::Array(ty, len) => self.coerce_to_array_type(ty, *len, exec_state, false),
RuntimeType::Tuple(tys) => self.coerce_to_tuple_type(tys, exec_state),
RuntimeType::Union(tys) => self.coerce_to_union_type(tys, exec_state),
RuntimeType::Object(tys) => self.coerce_to_object_type(tys, exec_state),
@ -609,6 +671,55 @@ impl KclValue {
KclValue::Helix { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::Edge => match value {
KclValue::Uuid { .. } => Some(value.clone()),
KclValue::TagIdentifier { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::Axis2d => match value {
KclValue::Object { value: values, meta } => {
if values.get("origin")?.has_type(&RuntimeType::point2d())
&& values.get("direction")?.has_type(&RuntimeType::point2d())
{
return Some(value.clone());
}
let origin = values.get("origin").and_then(|p| {
p.coerce_to_array_type(&RuntimeType::number_any(), ArrayLen::Known(2), exec_state, true)
})?;
let direction = values.get("direction").and_then(|p| {
p.coerce_to_array_type(&RuntimeType::number_any(), ArrayLen::Known(2), exec_state, true)
})?;
Some(KclValue::Object {
value: [("origin".to_owned(), origin), ("direction".to_owned(), direction)].into(),
meta: meta.clone(),
})
}
_ => None,
},
PrimitiveType::Axis3d => match value {
KclValue::Object { value: values, meta } => {
if values.get("origin")?.has_type(&RuntimeType::point3d())
&& values.get("direction")?.has_type(&RuntimeType::point3d())
{
return Some(value.clone());
}
let origin = values.get("origin").and_then(|p| {
p.coerce_to_array_type(&RuntimeType::number_any(), ArrayLen::Known(3), exec_state, true)
})?;
let direction = values.get("direction").and_then(|p| {
p.coerce_to_array_type(&RuntimeType::number_any(), ArrayLen::Known(3), exec_state, true)
})?;
Some(KclValue::Object {
value: [("origin".to_owned(), origin), ("direction".to_owned(), direction)].into(),
meta: meta.clone(),
})
}
_ => None,
},
PrimitiveType::ImportedGeometry => match value {
KclValue::ImportedGeometry { .. } => Some(value.clone()),
_ => None,
@ -621,60 +732,35 @@ impl KclValue {
}
}
fn coerce_to_array_type(&self, ty: &RuntimeType, len: ArrayLen, exec_state: &mut ExecState) -> Option<KclValue> {
fn coerce_to_array_type(
&self,
ty: &RuntimeType,
len: ArrayLen,
exec_state: &mut ExecState,
allow_shrink: bool,
) -> Option<KclValue> {
match self {
KclValue::HomArray { value, ty: aty } if aty == ty => {
let value = match len {
ArrayLen::None => value.clone(),
ArrayLen::NonEmpty => {
if value.is_empty() {
return None;
}
value.clone()
}
ArrayLen::Known(n) => {
if n != value.len() {
return None;
}
value[..n].to_vec()
}
};
Some(KclValue::HomArray { value, ty: ty.clone() })
KclValue::HomArray { value, ty: aty } if aty.subtype(ty) => {
len.satisfied(value.len(), allow_shrink).map(|len| KclValue::HomArray {
value: value[..len].to_vec(),
ty: aty.clone(),
})
}
value if len.satisfied(1) && value.has_type(ty) => Some(KclValue::HomArray {
value if len.satisfied(1, false).is_some() && value.has_type(ty) => Some(KclValue::HomArray {
value: vec![value.clone()],
ty: ty.clone(),
}),
KclValue::MixedArray { value, .. } => {
let value = match len {
ArrayLen::None => value.clone(),
ArrayLen::NonEmpty => {
if value.is_empty() {
return None;
}
let len = len.satisfied(value.len(), allow_shrink)?;
value.clone()
}
ArrayLen::Known(n) => {
if n != value.len() {
return None;
}
value[..n].to_vec()
}
};
let value = value
let value = value[..len]
.iter()
.map(|v| v.coerce(ty, exec_state))
.collect::<Option<Vec<_>>>()?;
Some(KclValue::HomArray { value, ty: ty.clone() })
}
KclValue::KclNone { .. } if len.satisfied(0) => Some(KclValue::HomArray {
KclValue::KclNone { .. } if len.satisfied(0, false).is_some() => Some(KclValue::HomArray {
value: Vec::new(),
ty: ty.clone(),
}),
@ -1251,4 +1337,119 @@ mod test {
assert!(count.coerce(&tyb, &mut exec_state).is_none());
assert!(count.coerce(&tyb2, &mut exec_state).is_none());
}
#[tokio::test(flavor = "multi_thread")]
async fn coerce_axes() {
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock().await);
// Subtyping
assert!(RuntimeType::Primitive(PrimitiveType::Axis2d).subtype(&RuntimeType::Primitive(PrimitiveType::Axis2d)));
assert!(RuntimeType::Primitive(PrimitiveType::Axis3d).subtype(&RuntimeType::Primitive(PrimitiveType::Axis3d)));
assert!(!RuntimeType::Primitive(PrimitiveType::Axis3d).subtype(&RuntimeType::Primitive(PrimitiveType::Axis2d)));
assert!(!RuntimeType::Primitive(PrimitiveType::Axis2d).subtype(&RuntimeType::Primitive(PrimitiveType::Axis3d)));
// Coercion
let a2d = KclValue::Object {
value: [
(
"origin".to_owned(),
KclValue::HomArray {
value: vec![
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
],
ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
},
),
(
"direction".to_owned(),
KclValue::HomArray {
value: vec![
KclValue::Number {
value: 1.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
],
ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
},
),
]
.into(),
meta: Vec::new(),
};
let a3d = KclValue::Object {
value: [
(
"origin".to_owned(),
KclValue::HomArray {
value: vec![
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
],
ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
},
),
(
"direction".to_owned(),
KclValue::HomArray {
value: vec![
KclValue::Number {
value: 1.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
KclValue::Number {
value: 0.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
KclValue::Number {
value: 1.0,
ty: NumericType::mm(),
meta: Vec::new(),
},
],
ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::mm())),
},
),
]
.into(),
meta: Vec::new(),
};
let ty2d = RuntimeType::Primitive(PrimitiveType::Axis2d);
let ty3d = RuntimeType::Primitive(PrimitiveType::Axis3d);
assert_coerce_results(&a2d, &ty2d, &a2d, &mut exec_state);
assert_coerce_results(&a3d, &ty3d, &a3d, &mut exec_state);
assert_coerce_results(&a3d, &ty2d, &a2d, &mut exec_state);
assert!(a2d.coerce(&ty3d, &mut exec_state).is_none());
}
}