Merge remote-tracking branch 'origin/main' into jess/cleaned-imports

* origin/main:
  Quick app rename typo fix in settings.md (#6198)
  Add point-and-click Insert from local project files (#6129)
  Install and start Vector on macOS CI runners (#6147)
  Implement polar std function in KCL (#6180)
  Bump typescript from 5.8.2 to 5.8.3 in /packages/codemirror-lsp-client in the patch group (#6188)
  Bump @types/node from 22.13.13 to 22.14.0 in /packages/codemirror-lsp-client in the minor group (#6189)
  Bump the major group in /packages/codemirror-lang-kcl with 2 updates (#6194)
  Bump taiki-e/install-action from 2.49.30 to 2.49.45 in the patch group (#6185)
  Bump the patch group with 6 updates (#6186)
  Bump the patch group in /rust/kcl-language-server with 3 updates (#6183)
  Bump the patch group in /packages/codemirror-lang-kcl with 2 updates (#6193)
  Remove unnecessary timeouts waiting for command bar (#6199)
  Stream handling / Stream idle mode v2; a ton of network related changes (ping; scene indicator -> stream indicator, stream resizing (even on pause)) (#5312)
  More propagation of numeric types (#6177)
  Apply type-directed coercions to arguments in calls of user functions (#6179)
  Erase comment start positions from snapshot tests (#6178)
  Implement coercion of numeric types for ascription and arithmetic (off by default) (#6175)
  Reduce the number of reps in the add_lots test (#6174)
  take things off the batch in a more safe way (#6171)
This commit is contained in:
Jess Frazelle
2025-04-07 14:19:20 -07:00
338 changed files with 55999 additions and 53816 deletions

View File

@ -3,11 +3,11 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::{types::NumericType, ArtifactId, KclValue};
use crate::{docs::StdLibFn, std::get_stdlib_fn, ModuleId, SourceRange};
use crate::{docs::StdLibFn, ModuleId, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations
/// timeline.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export_to = "Operation.ts")]
#[serde(tag = "type")]
pub enum Operation {
@ -60,7 +60,7 @@ impl Operation {
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export_to = "Operation.ts")]
#[serde(tag = "type")]
#[expect(clippy::large_enum_variant)]
@ -90,7 +90,7 @@ pub enum Group {
}
/// An argument to a CAD modeling operation.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct OpArg {
@ -110,7 +110,7 @@ impl OpArg {
/// A reference to a standard library function. This exists to implement
/// `PartialEq` and `Eq` for `Operation`.
#[derive(Debug, Clone, Deserialize, Serialize, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, ts_rs::TS, JsonSchema)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct StdLibFnRef {
@ -156,25 +156,13 @@ where
serializer.serialize_str(&name)
}
fn std_lib_fn_from_name<'de, D>(deserializer: D) -> Result<Box<dyn StdLibFn>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if let Some(std_lib_fn) = get_stdlib_fn(&s) {
Ok(std_lib_fn)
} else {
Err(serde::de::Error::custom(format!("not a KCL stdlib function: {}", s)))
}
}
fn is_false(b: &bool) -> bool {
!*b
}
/// A KCL value used in Operations. `ArtifactId`s are used to refer to the
/// actual scene objects. Any data not needed in the UI may be omitted.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export_to = "Operation.ts")]
#[serde(tag = "type")]
pub enum OpKclValue {

View File

@ -3,7 +3,11 @@ use std::collections::HashMap;
use async_recursion::async_recursion;
use indexmap::IndexMap;
use super::{cad_op::Group, kcl_value::TypeDef, types::PrimitiveType};
use super::{
cad_op::Group,
kcl_value::TypeDef,
types::{PrimitiveType, CHECK_NUMERIC_TYPES},
};
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
@ -25,7 +29,7 @@ use crate::{
},
source_range::SourceRange,
std::{
args::{Arg, KwArgs},
args::{Arg, KwArgs, TyF64},
FunctionKind,
},
CompilationError,
@ -681,14 +685,14 @@ impl ExecutorContext {
let result = self
.execute_expr(&expr.expr, exec_state, metadata, &[], statement_kind)
.await?;
coerce(&result, &expr.ty, exec_state, expr.into())?
apply_ascription(&result, &expr.ty, exec_state, expr.into())?
}
};
Ok(item)
}
}
fn coerce(
fn apply_ascription(
value: &KclValue,
ty: &Node<Type>,
exec_state: &mut ExecState,
@ -697,7 +701,24 @@ fn coerce(
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into())
.map_err(|e| KclError::Semantic(e.into()))?;
value.coerce(&ty, exec_state).ok_or_else(|| {
if let KclValue::Number {
ty: NumericType::Unknown,
value,
meta,
} = value
{
// If the number has unknown units but the user is explicitly specifying them, treat the value as having had it's units erased,
// rather than forcing the user to explicitly erase them.
KclValue::Number {
ty: NumericType::Any,
value: *value,
meta: meta.clone(),
}
.coerce(&ty, exec_state)
} else {
value.coerce(&ty, exec_state)
}
.map_err(|_| {
KclError::Semantic(KclErrorDetails {
message: format!("could not coerce {} value to type {}", value.human_friendly_type(), ty),
source_ranges: vec![source_range],
@ -953,69 +974,85 @@ impl Node<BinaryExpression> {
return Ok(KclValue::Bool { value: raw_value, meta });
}
let (left, lty) = parse_number_as_f64(&left_value, self.left.clone().into())?;
let (right, rty) = parse_number_as_f64(&right_value, self.right.clone().into())?;
let left = number_as_f64(&left_value, self.left.clone().into())?;
let right = number_as_f64(&right_value, self.right.clone().into())?;
let value = match self.operator {
BinaryOperator::Add => KclValue::Number {
value: left + right,
meta,
ty: NumericType::combine_add(lty, rty),
},
BinaryOperator::Sub => KclValue::Number {
value: left - right,
meta,
ty: NumericType::combine_add(lty, rty),
},
BinaryOperator::Mul => KclValue::Number {
value: left * right,
meta,
ty: NumericType::combine_mul(lty, rty),
},
BinaryOperator::Div => KclValue::Number {
value: left / right,
meta,
ty: NumericType::combine_div(lty, rty),
},
BinaryOperator::Mod => KclValue::Number {
value: left % right,
meta,
ty: NumericType::combine_div(lty, rty),
},
BinaryOperator::Add => {
let (l, r, ty) = NumericType::combine_eq(left, right);
self.warn_on_unknown(&ty, "Adding", exec_state);
KclValue::Number { value: l + r, meta, ty }
}
BinaryOperator::Sub => {
let (l, r, ty) = NumericType::combine_eq(left, right);
self.warn_on_unknown(&ty, "Subtracting", exec_state);
KclValue::Number { value: l - r, meta, ty }
}
BinaryOperator::Mul => {
let (l, r, ty) = NumericType::combine_mul(left, right);
self.warn_on_unknown(&ty, "Multiplying", exec_state);
KclValue::Number { value: l * r, meta, ty }
}
BinaryOperator::Div => {
let (l, r, ty) = NumericType::combine_div(left, right);
self.warn_on_unknown(&ty, "Dividing", exec_state);
KclValue::Number { value: l / r, meta, ty }
}
BinaryOperator::Mod => {
let (l, r, ty) = NumericType::combine_div(left, right);
self.warn_on_unknown(&ty, "Modulo of", exec_state);
KclValue::Number { value: l % r, meta, ty }
}
BinaryOperator::Pow => KclValue::Number {
value: left.powf(right),
value: left.n.powf(right.n),
meta,
ty: NumericType::Unknown,
},
BinaryOperator::Neq => KclValue::Bool {
value: left != right,
meta,
},
BinaryOperator::Gt => KclValue::Bool {
value: left > right,
meta,
},
BinaryOperator::Gte => KclValue::Bool {
value: left >= right,
meta,
},
BinaryOperator::Lt => KclValue::Bool {
value: left < right,
meta,
},
BinaryOperator::Lte => KclValue::Bool {
value: left <= right,
meta,
},
BinaryOperator::Eq => KclValue::Bool {
value: left == right,
meta,
},
BinaryOperator::Neq => {
let (l, r, ty) = NumericType::combine_eq(left, right);
self.warn_on_unknown(&ty, "Comparing", exec_state);
KclValue::Bool { value: l != r, meta }
}
BinaryOperator::Gt => {
let (l, r, ty) = NumericType::combine_eq(left, right);
self.warn_on_unknown(&ty, "Comparing", exec_state);
KclValue::Bool { value: l > r, meta }
}
BinaryOperator::Gte => {
let (l, r, ty) = NumericType::combine_eq(left, right);
self.warn_on_unknown(&ty, "Comparing", exec_state);
KclValue::Bool { value: l >= r, meta }
}
BinaryOperator::Lt => {
let (l, r, ty) = NumericType::combine_eq(left, right);
self.warn_on_unknown(&ty, "Comparing", exec_state);
KclValue::Bool { value: l < r, meta }
}
BinaryOperator::Lte => {
let (l, r, ty) = NumericType::combine_eq(left, right);
self.warn_on_unknown(&ty, "Comparing", exec_state);
KclValue::Bool { value: l <= r, meta }
}
BinaryOperator::Eq => {
let (l, r, ty) = NumericType::combine_eq(left, right);
self.warn_on_unknown(&ty, "Comparing", exec_state);
KclValue::Bool { value: l == r, meta }
}
BinaryOperator::And | BinaryOperator::Or => unreachable!(),
};
Ok(value)
}
fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) {
if *CHECK_NUMERIC_TYPES && 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),
));
}
}
}
impl Node<UnaryExpression> {
@ -1731,21 +1768,15 @@ fn article_for(s: &str) -> &'static str {
}
}
pub fn parse_number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<(f64, NumericType), KclError> {
if let KclValue::Number { value: n, ty, .. } = &v {
Ok((*n, ty.clone()))
} else {
fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
v.as_ty_f64().ok_or_else(|| {
let actual_type = v.human_friendly_type();
let article = if actual_type.starts_with(['a', 'e', 'i', 'o', 'u']) {
"an"
} else {
"a"
};
Err(KclError::Semantic(KclErrorDetails {
let article = article_for(actual_type);
KclError::Semantic(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Expected a number, but found {article} {actual_type}",),
}))
}
})
})
}
impl Node<IfExpression> {
@ -1947,32 +1978,102 @@ fn assign_args_to_params(
Ok(())
}
fn assign_args_to_params_kw(
fn type_check_params_kw(
fn_name: Option<&str>,
function_expression: NodeRef<'_, FunctionExpression>,
mut args: crate::std::args::KwArgs,
args: &mut crate::std::args::KwArgs,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
for (label, arg) in &args.labeled {
for (label, arg) in &mut args.labeled {
match function_expression.params.iter().find(|p| &p.identifier.name == label) {
Some(p) => {
if !p.labeled {
exec_state.err(CompilationError::err(
arg.source_range,
format!(
"This function expects an unlabeled first parameter (`{label}`), but it is labelled in the call"
"{} expects an unlabeled first parameter (`{label}`), but it is labelled in the call",
fn_name
.map(|n| format!("The function `{}`", n))
.unwrap_or_else(|| "This function".to_owned()),
),
));
}
if let Some(ty) = &p.type_ {
arg.value = arg
.value
.coerce(
&RuntimeType::from_parsed(ty.inner.clone(), exec_state, arg.source_range).unwrap(),
exec_state,
)
.map_err(|e| {
let mut message = format!(
"{label} requires a value with type `{}`, but found {}",
ty.inner,
arg.value.human_friendly_type(),
);
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.
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(): number({ty})`");
}
KclError::Semantic(KclErrorDetails {
message,
source_ranges: vec![arg.source_range],
})
})?;
}
}
None => {
exec_state.err(CompilationError::err(
arg.source_range,
format!("`{label}` is not an argument of this function"),
format!(
"`{label}` is not an argument of {}",
fn_name
.map(|n| format!("`{}`", n))
.unwrap_or_else(|| "this function".to_owned()),
),
));
}
}
}
if let Some(arg) = &mut args.unlabeled {
if let Some(p) = function_expression.params.iter().find(|p| !p.labeled) {
if let Some(ty) = &p.type_ {
arg.value = arg
.value
.coerce(
&RuntimeType::from_parsed(ty.inner.clone(), exec_state, arg.source_range).unwrap(),
exec_state,
)
.map_err(|_| {
KclError::Semantic(KclErrorDetails {
message: 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.inner,
arg.value.human_friendly_type()
),
source_ranges: vec![arg.source_range],
})
})?;
}
}
}
Ok(())
}
fn assign_args_to_params_kw(
fn_name: Option<&str>,
function_expression: NodeRef<'_, FunctionExpression>,
mut args: crate::std::args::KwArgs,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
type_check_params_kw(fn_name, function_expression, &mut args, exec_state)?;
// Add the arguments to the memory. A new call frame should have already
// been created.
let source_ranges = vec![function_expression.into()];
@ -2059,6 +2160,7 @@ async fn call_user_defined_function(
}
async fn call_user_defined_function_kw(
fn_name: Option<&str>,
args: crate::std::args::KwArgs,
memory: EnvironmentRef,
function_expression: NodeRef<'_, FunctionExpression>,
@ -2069,7 +2171,7 @@ async fn call_user_defined_function_kw(
// variables shadow variables in the parent scope. The new environment's
// parent should be the environment of the closure.
exec_state.mut_stack().push_new_env_for_call(memory);
if let Err(e) = assign_args_to_params_kw(function_expression, args, exec_state) {
if let Err(e) = assign_args_to_params_kw(fn_name, function_expression, args, exec_state) {
exec_state.mut_stack().pop_env();
return Err(e);
}
@ -2147,47 +2249,7 @@ impl FunctionSource {
));
}
for (label, arg) in &mut args.kw_args.labeled {
match ast.params.iter().find(|p| &p.identifier.name == label) {
Some(p) => {
if !p.labeled {
exec_state.err(CompilationError::err(
arg.source_range,
format!(
"The function `{}` expects an unlabeled first parameter (`{label}`), but it is labelled in the call",
props.name
),
));
}
if let Some(ty) = &p.type_ {
arg.value = arg
.value
.coerce(
&RuntimeType::from_parsed(ty.inner.clone(), exec_state, arg.source_range)
.unwrap(),
exec_state,
)
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!(
"{label} requires a value with type `{}`, but found {}",
ty.inner,
arg.value.human_friendly_type()
),
source_ranges: vec![callsite],
})
})?;
}
}
None => {
exec_state.err(CompilationError::err(
arg.source_range,
format!("`{label}` is not an argument of `{}`", props.name),
));
}
}
}
type_check_params_kw(Some(&props.name), ast, &mut args.kw_args, exec_state)?;
if let Some(arg) = &mut args.kw_args.unlabeled {
if let Some(p) = ast.params.iter().find(|p| !p.labeled) {
@ -2198,13 +2260,13 @@ impl FunctionSource {
&RuntimeType::from_parsed(ty.inner.clone(), exec_state, arg.source_range).unwrap(),
exec_state,
)
.ok_or_else(|| {
.map_err(|_| {
KclError::Semantic(KclErrorDetails {
message: format!(
"The input argument of {} requires a value with type `{}`, but found {}",
props.name,
ty.inner,
arg.value.human_friendly_type()
arg.value.human_friendly_type(),
),
source_ranges: vec![callsite],
})
@ -2266,7 +2328,7 @@ impl FunctionSource {
.collect();
exec_state.global.operations.push(Operation::GroupBegin {
group: Group::FunctionCall {
name: fn_name,
name: fn_name.clone(),
function_source_range: ast.as_source_range(),
unlabeled_arg: args
.kw_args
@ -2278,7 +2340,7 @@ impl FunctionSource {
source_range: callsite,
});
call_user_defined_function_kw(args.kw_args, *memory, ast, exec_state, ctx).await
call_user_defined_function_kw(fn_name.as_deref(), args.kw_args, *memory, ast, exec_state, ctx).await
}
FunctionSource::None => unreachable!(),
}
@ -2603,4 +2665,25 @@ d = b + c
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn user_coercion() {
let program = r#"fn foo(x: Axis2d) {
return 0
}
foo(x = { direction = [0, 0], origin = [0, 0]})
"#;
parse_execute(program).await.unwrap();
let program = r#"fn foo(x: Axis3d) {
return 0
}
foo(x = { direction = [0, 0], origin = [0, 0]})
"#;
parse_execute(program).await.unwrap_err();
}
}

View File

@ -10,16 +10,16 @@ use serde::{Deserialize, Serialize};
use crate::{
errors::KclError,
execution::{ArtifactId, ExecState, Metadata, TagEngineInfo, TagIdentifier, UnitLen},
execution::{types::NumericType, ArtifactId, ExecState, Metadata, TagEngineInfo, TagIdentifier, UnitLen},
parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode},
std::sketch::PlaneData,
std::{args::TyF64, sketch::PlaneData},
};
type Point2D = kcmc::shared::Point2d<f64>;
type Point3D = kcmc::shared::Point3d<f64>;
/// A geometry.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum Geometry {
@ -47,7 +47,7 @@ impl Geometry {
}
/// A set of geometry.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
#[allow(clippy::vec_box)]
@ -66,7 +66,7 @@ impl From<Geometry> for Geometries {
}
/// Data for an imported geometry.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ImportedGeometry {
@ -79,7 +79,7 @@ pub struct ImportedGeometry {
}
/// Data for a solid or an imported geometry.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[allow(clippy::vec_box)]
@ -159,7 +159,7 @@ pub struct Helix {
pub meta: Vec<Metadata>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Plane {
@ -353,7 +353,7 @@ impl Plane {
}
/// A face.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Face {
@ -377,7 +377,7 @@ pub struct Face {
}
/// Type for a plane.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display)]
#[ts(export)]
#[display(style = "camelCase")]
pub enum PlaneType {
@ -398,7 +398,7 @@ pub enum PlaneType {
Uninit,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct Sketch {
@ -463,7 +463,7 @@ impl Sketch {
}
/// A sketch type.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum SketchSurface {
@ -582,7 +582,7 @@ impl Sketch {
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct Solid {
@ -616,7 +616,7 @@ impl Solid {
}
/// A fillet or a chamfer.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum EdgeCut {
@ -678,6 +678,12 @@ impl From<[f64; 2]> for Point2d {
}
}
impl From<[TyF64; 2]> for Point2d {
fn from(p: [TyF64; 2]) -> Self {
Self { x: p[0].n, y: p[1].n }
}
}
impl From<&[f64; 2]> for Point2d {
fn from(p: &[f64; 2]) -> Self {
Self { x: p[0], y: p[1] }
@ -767,7 +773,7 @@ impl Mul<f64> for Point3d {
}
/// A base path.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct BasePath {
@ -786,7 +792,7 @@ pub struct BasePath {
}
/// Geometry metadata.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct GeoMeta {
@ -798,7 +804,7 @@ pub struct GeoMeta {
}
/// A path.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum Path {
@ -979,19 +985,23 @@ impl Path {
}
/// Where does this path segment start?
pub fn get_from(&self) -> &[f64; 2] {
&self.get_base().from
pub fn get_from(&self) -> [TyF64; 2] {
let p = &self.get_base().from;
let ty: NumericType = self.get_base().units.into();
[TyF64::new(p[0], ty.clone()), TyF64::new(p[1], ty)]
}
/// Where does this path segment end?
pub fn get_to(&self) -> &[f64; 2] {
&self.get_base().to
pub fn get_to(&self) -> [TyF64; 2] {
let p = &self.get_base().to;
let ty: NumericType = self.get_base().units.into();
[TyF64::new(p[0], ty.clone()), TyF64::new(p[1], ty)]
}
/// Length of this path segment, in cartesian plane.
pub fn length(&self) -> f64 {
match self {
pub fn length(&self) -> TyF64 {
let n = match self {
Self::ToPoint { .. } | Self::Base { .. } | Self::Horizontal { .. } | Self::AngledLineTo { .. } => {
linear_distance(self.get_from(), self.get_to())
linear_distance(&self.get_base().from, &self.get_base().to)
}
Self::TangentialArc {
base: _,
@ -1005,10 +1015,10 @@ impl Path {
} => {
// The radius can be calculated as the linear distance between `to` and `center`,
// or between `from` and `center`. They should be the same.
let radius = linear_distance(self.get_from(), center);
debug_assert_eq!(radius, linear_distance(self.get_to(), center));
let radius = linear_distance(&self.get_base().from, center);
debug_assert_eq!(radius, linear_distance(&self.get_base().to, center));
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
linear_distance(&self.get_base().from, &self.get_base().to)
}
Self::Circle { radius, .. } => 2.0 * std::f64::consts::PI * radius,
Self::CircleThreePoint { .. } => {
@ -1022,13 +1032,14 @@ impl Path {
}
Self::Arc { .. } => {
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
linear_distance(&self.get_base().from, &self.get_base().to)
}
Self::ArcThreePoint { .. } => {
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
linear_distance(&self.get_base().from, &self.get_base().to)
}
}
};
TyF64::new(n, self.get_base().units.into())
}
pub fn get_base_mut(&mut self) -> Option<&mut BasePath> {
@ -1103,7 +1114,7 @@ fn linear_distance(
}
/// An extrude surface.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ExtrudeSurface {
@ -1115,7 +1126,7 @@ pub enum ExtrudeSurface {
}
// Chamfer surface.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ChamferSurface {
@ -1129,7 +1140,7 @@ pub struct ChamferSurface {
}
// Fillet surface.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct FilletSurface {
@ -1143,7 +1154,7 @@ pub struct FilletSurface {
}
/// An extruded plane.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExtrudePlane {
@ -1157,7 +1168,7 @@ pub struct ExtrudePlane {
}
/// An extruded arc.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExtrudeArc {

View File

@ -15,7 +15,7 @@ use crate::{
parsing::ast::types::{
DefaultParamVal, FunctionExpression, KclNone, Literal, LiteralValue, Node, TagDeclarator, TagNode,
},
std::StdFnProps,
std::{args::TyF64, StdFnProps},
CompilationError, KclError, ModuleId, SourceRange,
};
@ -495,6 +495,7 @@ impl KclValue {
None
}
}
pub fn as_f64(&self) -> Option<f64> {
if let KclValue::Number { value, .. } = &self {
Some(*value)
@ -503,6 +504,14 @@ impl KclValue {
}
}
pub fn as_ty_f64(&self) -> Option<TyF64> {
if let KclValue::Number { value, ty, .. } = &self {
Some(TyF64::new(*value, ty.clone()))
} else {
None
}
}
pub fn as_bool(&self) -> Option<bool> {
if let KclValue::Bool { value, meta: _ } = &self {
Some(*value)

View File

@ -175,7 +175,7 @@ impl std::hash::Hash for TagIdentifier {
}
/// Engine information for a tag.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct TagEngineInfo {

View File

@ -22,6 +22,8 @@ use crate::{
CompilationError,
};
use super::types::NumericType;
/// State for executing a program.
#[derive(Debug, Clone)]
pub struct ExecState {
@ -232,6 +234,13 @@ impl ExecState {
self.global.module_infos.get(&id)
}
pub fn current_default_units(&self) -> NumericType {
NumericType::Default {
len: self.length_unit(),
angle: self.angle_unit(),
}
}
pub fn length_unit(&self) -> UnitLen {
self.mod_local.settings.default_length_units
}

View File

@ -14,10 +14,20 @@ use crate::{
ast::types::{PrimitiveType as AstPrimitiveType, Type},
token::NumericSuffix,
},
std::args::FromKclValue,
std::args::{FromKclValue, TyF64},
CompilationError, SourceRange,
};
lazy_static::lazy_static! {
pub(super) static ref CHECK_NUMERIC_TYPES: bool = {
let env_var = std::env::var("ZOO_NUM_TYS");
let Ok(env_var) = env_var else {
return false;
};
!env_var.is_empty()
};
}
#[derive(Debug, Clone, PartialEq)]
pub enum RuntimeType {
Primitive(PrimitiveType),
@ -337,7 +347,7 @@ impl fmt::Display for PrimitiveType {
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum NumericType {
@ -351,62 +361,94 @@ pub enum NumericType {
Any,
}
impl Default for NumericType {
fn default() -> Self {
NumericType::Default {
len: UnitLen::default(),
angle: UnitAngle::default(),
}
}
}
impl NumericType {
pub fn count() -> Self {
pub const fn count() -> Self {
NumericType::Known(UnitType::Count)
}
pub fn mm() -> Self {
pub const fn mm() -> Self {
NumericType::Known(UnitType::Length(UnitLen::Mm))
}
pub const fn radians() -> Self {
NumericType::Known(UnitType::Angle(UnitAngle::Radians))
}
pub const fn degrees() -> Self {
NumericType::Known(UnitType::Angle(UnitAngle::Degrees))
}
/// Combine two types when we expect them to be equal.
pub fn combine_eq(self, other: &NumericType) -> NumericType {
if &self == other {
self
} else {
NumericType::Unknown
}
}
pub fn combine_eq(a: TyF64, b: TyF64) -> (f64, f64, NumericType) {
use NumericType::*;
match (a.ty, b.ty) {
(at, bt) if at == bt => (a.n, b.n, at),
(at, Any) => (a.n, b.n, at),
(Any, bt) => (a.n, b.n, bt),
(Default { .. }, Default { .. }) | (_, Unknown) | (Unknown, _) => (a.n, b.n, Unknown),
/// Combine n types when we expect them to be equal.
///
/// Precondition: tys.len() > 0
pub fn combine_n_eq(tys: &[NumericType]) -> NumericType {
let ty0 = tys[0].clone();
for t in &tys[1..] {
if t != &ty0 {
return NumericType::Unknown;
// Known types and compatible, but needs adjustment.
(t @ Known(UnitType::Length(l1)), Known(UnitType::Length(l2))) => (a.n, l2.adjust_to(b.n, l1), t),
(t @ Known(UnitType::Angle(a1)), Known(UnitType::Angle(a2))) => (a.n, a2.adjust_to(b.n, a1), t),
// Known but incompatible.
(Known(_), Known(_)) => (a.n, b.n, Unknown),
// Known and unknown => we assume the known one, possibly with adjustment
(Known(UnitType::Count), Default { .. }) | (Default { .. }, Known(UnitType::Count)) => {
(a.n, b.n, Known(UnitType::Count))
}
(t @ Known(UnitType::Length(l1)), Default { len: l2, .. }) => (a.n, l2.adjust_to(b.n, l1), t),
(Default { len: l1, .. }, t @ Known(UnitType::Length(l2))) => (l1.adjust_to(a.n, l2), b.n, t),
(t @ Known(UnitType::Angle(a1)), Default { angle: a2, .. }) => (a.n, a2.adjust_to(b.n, a1), t),
(Default { angle: a1, .. }, t @ Known(UnitType::Angle(a2))) => (a1.adjust_to(a.n, a2), b.n, t),
}
ty0
}
/// Combine two types in addition-like operations.
pub fn combine_add(a: NumericType, b: NumericType) -> NumericType {
if a == b {
return a;
/// Combine two types for multiplication-like operations.
pub fn combine_mul(a: TyF64, b: TyF64) -> (f64, f64, NumericType) {
use NumericType::*;
match (a.ty, b.ty) {
(at @ Default { .. }, bt @ Default { .. }) if at != bt => (a.n, b.n, Unknown),
(Known(UnitType::Count) | Default { .. }, bt) => (a.n, b.n, bt),
(at, Known(UnitType::Count) | Default { .. }) => (a.n, b.n, at),
(Any, Any) => (a.n, b.n, Any),
_ => (a.n, b.n, Unknown),
}
NumericType::Unknown
}
/// Combine two types in multiplication-like operations.
pub fn combine_mul(a: NumericType, b: NumericType) -> NumericType {
if a == NumericType::count() {
return b;
/// Combine two types for division-like operations.
pub fn combine_div(a: TyF64, b: TyF64) -> (f64, f64, NumericType) {
use NumericType::*;
match (a.ty, b.ty) {
(at, bt) if at == bt => (a.n, b.n, Known(UnitType::Count)),
(Default { .. }, Default { .. }) => (a.n, b.n, Unknown),
(at, Known(UnitType::Count) | Default { .. } | Any) => (a.n, b.n, at),
(Known(UnitType::Length(l1)), Known(UnitType::Length(l2))) => {
(a.n, l2.adjust_to(b.n, l1), Known(UnitType::Count))
}
(Known(UnitType::Angle(a1)), Known(UnitType::Angle(a2))) => {
(a.n, a2.adjust_to(b.n, a1), Known(UnitType::Count))
}
(Default { len: l1, .. }, Known(UnitType::Length(l2))) => {
(l1.adjust_to(a.n, l2), b.n, Known(UnitType::Count))
}
(Default { angle: a1, .. }, Known(UnitType::Angle(a2))) => {
(a1.adjust_to(a.n, a2), b.n, Known(UnitType::Count))
}
_ => (a.n, b.n, Unknown),
}
if b == NumericType::count() {
return a;
}
NumericType::Unknown
}
/// Combine two types in division-like operations.
pub fn combine_div(a: NumericType, b: NumericType) -> NumericType {
if b == NumericType::count() {
return a;
}
NumericType::Unknown
}
pub fn from_parsed(suffix: NumericSuffix, settings: &super::MetaSettings) -> Self {
@ -431,12 +473,104 @@ impl NumericType {
use NumericType::*;
match (self, other) {
(Unknown, _) | (_, Unknown) => false,
(a, b) if a == b => true,
(_, Any) => true,
(a, b) if a == b => true,
(Unknown, _) | (_, Unknown) => false,
(_, _) => false,
}
}
fn example_ty(&self) -> Option<String> {
match self {
Self::Known(t) => Some(t.to_string()),
Self::Default { len, .. } => Some(len.to_string()),
_ => None,
}
}
fn coerce(&self, val: &KclValue) -> Result<KclValue, CoercionError> {
let KclValue::Number { value, ty, meta } = val else {
return Err(val.into());
};
if !*CHECK_NUMERIC_TYPES {
return Ok(val.clone());
}
if ty.subtype(self) {
return Ok(KclValue::Number {
value: *value,
ty: ty.clone(),
meta: meta.clone(),
});
}
// Not subtypes, but might be able to coerce
use NumericType::*;
match (ty, self) {
// We don't have enough information to coerce.
(Unknown, _) => Err(CoercionError::from(val).with_explicit(self.example_ty().unwrap_or("mm".to_owned()))),
(_, Unknown) => Err(val.into()),
(Any, _) => Ok(KclValue::Number {
value: *value,
ty: self.clone(),
meta: meta.clone(),
}),
// We don't actually need to coerce, since we just keep the partially-known type with the value.
(Default { .. }, Default { .. }) => Ok(KclValue::Number {
value: *value,
ty: ty.clone(),
meta: meta.clone(),
}),
// Known types and compatible, but needs adjustment.
(Known(UnitType::Length(l1)), Known(UnitType::Length(l2))) => Ok(KclValue::Number {
value: l1.adjust_to(*value, *l2),
ty: self.clone(),
meta: meta.clone(),
}),
(Known(UnitType::Angle(a1)), Known(UnitType::Angle(a2))) => Ok(KclValue::Number {
value: a1.adjust_to(*value, *a2),
ty: self.clone(),
meta: meta.clone(),
}),
// Known but incompatible.
(Known(_), Known(_)) => Err(val.into()),
// Known and unknown => we assume the rhs, possibly with adjustment
(Known(UnitType::Count), Default { .. }) | (Default { .. }, Known(UnitType::Count)) => {
Ok(KclValue::Number {
value: *value,
ty: Known(UnitType::Count),
meta: meta.clone(),
})
}
(Known(UnitType::Length(l1)), Default { len: l2, .. })
| (Default { len: l1, .. }, Known(UnitType::Length(l2))) => Ok(KclValue::Number {
value: l1.adjust_to(*value, *l2),
ty: Known(UnitType::Length(*l2)),
meta: meta.clone(),
}),
(Known(UnitType::Angle(a1)), Default { angle: a2, .. })
| (Default { angle: a1, .. }, Known(UnitType::Angle(a2))) => Ok(KclValue::Number {
value: a1.adjust_to(*value, *a2),
ty: Known(UnitType::Angle(*a2)),
meta: meta.clone(),
}),
(_, _) => unreachable!(),
}
}
}
impl From<NumericType> for RuntimeType {
fn from(t: NumericType) -> RuntimeType {
RuntimeType::Primitive(PrimitiveType::Number(t))
}
}
impl From<UnitLen> for NumericType {
@ -485,6 +619,39 @@ pub enum UnitLen {
Yards,
}
impl UnitLen {
fn adjust_to(self, value: f64, to: UnitLen) -> f64 {
if self == to {
return value;
}
use UnitLen::*;
let (base, base_unit) = match self {
Mm => (value, Mm),
Cm => (value * 10.0, Mm),
M => (value * 1000.0, Mm),
Inches => (value, Inches),
Feet => (value * 12.0, Inches),
Yards => (value * 36.0, Inches),
};
let (base, base_unit) = match (base_unit, to) {
(Mm, Inches) | (Mm, Feet) | (Mm, Yards) => (base / 25.4, Inches),
(Inches, Mm) | (Inches, Cm) | (Inches, M) => (base * 25.4, Mm),
_ => (base, base_unit),
};
match (base_unit, to) {
(Mm, Mm) => base,
(Mm, Cm) => base / 10.0,
(Mm, M) => base / 1000.0,
(Inches, Inches) => base,
(Inches, Feet) => base / 12.0,
(Inches, Yards) => base / 36.0,
_ => unreachable!(),
}
}
}
impl std::fmt::Display for UnitLen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@ -563,6 +730,19 @@ pub enum UnitAngle {
Radians,
}
impl UnitAngle {
fn adjust_to(self, value: f64, to: UnitAngle) -> f64 {
use std::f64::consts::PI;
use UnitAngle::*;
match (self, to) {
(Degrees, Degrees) => value,
(Degrees, Radians) => (value / 180.0) * PI,
(Radians, Degrees) => 180.0 * value / PI,
(Radians, Radians) => value,
}
}
}
impl std::fmt::Display for UnitAngle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@ -584,6 +764,28 @@ impl TryFrom<NumericSuffix> for UnitAngle {
}
}
#[derive(Debug, Clone)]
pub struct CoercionError {
pub found: Option<RuntimeType>,
pub explicit_coercion: Option<String>,
}
impl CoercionError {
fn with_explicit(mut self, c: String) -> Self {
self.explicit_coercion = Some(c);
self
}
}
impl From<&'_ KclValue> for CoercionError {
fn from(value: &'_ KclValue) -> Self {
CoercionError {
found: value.principal_type(),
explicit_coercion: None,
}
}
}
impl KclValue {
/// True if `self` has a type which is a subtype of `ty` without coercion.
pub fn has_type(&self, ty: &RuntimeType) -> bool {
@ -600,7 +802,7 @@ impl KclValue {
/// - result.principal_type().unwrap().subtype(ty)
///
/// If self.principal_type() == ty then result == self
pub fn coerce(&self, ty: &RuntimeType, exec_state: &mut ExecState) -> Option<KclValue> {
pub fn coerce(&self, ty: &RuntimeType, exec_state: &mut ExecState) -> Result<KclValue, CoercionError> {
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, false),
@ -610,40 +812,52 @@ impl KclValue {
}
}
fn coerce_to_primitive_type(&self, ty: &PrimitiveType, exec_state: &mut ExecState) -> Option<KclValue> {
fn coerce_to_primitive_type(
&self,
ty: &PrimitiveType,
exec_state: &mut ExecState,
) -> Result<KclValue, CoercionError> {
let value = match self {
KclValue::MixedArray { value, .. } | KclValue::HomArray { value, .. } if value.len() == 1 => &value[0],
_ => self,
};
match ty {
// TODO numeric type coercions
PrimitiveType::Number(_ty) => match value {
KclValue::Number { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::Number(ty) => ty.coerce(value),
PrimitiveType::String => match value {
KclValue::String { .. } => Some(value.clone()),
_ => None,
KclValue::String { .. } => Ok(value.clone()),
_ => Err(self.into()),
},
PrimitiveType::Boolean => match value {
KclValue::Bool { .. } => Some(value.clone()),
_ => None,
KclValue::Bool { .. } => Ok(value.clone()),
_ => Err(self.into()),
},
PrimitiveType::Sketch => match value {
KclValue::Sketch { .. } => Some(value.clone()),
_ => None,
KclValue::Sketch { .. } => Ok(value.clone()),
_ => Err(self.into()),
},
PrimitiveType::Solid => match value {
KclValue::Solid { .. } => Some(value.clone()),
_ => None,
KclValue::Solid { .. } => Ok(value.clone()),
_ => Err(self.into()),
},
PrimitiveType::Plane => match value {
KclValue::Plane { .. } => Some(value.clone()),
KclValue::Plane { .. } => Ok(value.clone()),
KclValue::Object { value, meta } => {
let origin = value.get("origin").and_then(Point3d::from_kcl_val)?;
let x_axis = value.get("xAxis").and_then(Point3d::from_kcl_val)?;
let y_axis = value.get("yAxis").and_then(Point3d::from_kcl_val)?;
let z_axis = value.get("zAxis").and_then(Point3d::from_kcl_val)?;
let origin = value
.get("origin")
.and_then(Point3d::from_kcl_val)
.ok_or(CoercionError::from(self))?;
let x_axis = value
.get("xAxis")
.and_then(Point3d::from_kcl_val)
.ok_or(CoercionError::from(self))?;
let y_axis = value
.get("yAxis")
.and_then(Point3d::from_kcl_val)
.ok_or(CoercionError::from(self))?;
let z_axis = value
.get("zAxis")
.and_then(Point3d::from_kcl_val)
.ok_or(CoercionError::from(self))?;
let id = exec_state.mod_local.id_generator.next_uuid();
let plane = Plane {
@ -659,75 +873,87 @@ impl KclValue {
meta: meta.clone(),
};
Some(KclValue::Plane { value: Box::new(plane) })
Ok(KclValue::Plane { value: Box::new(plane) })
}
_ => None,
_ => Err(self.into()),
},
PrimitiveType::Face => match value {
KclValue::Face { .. } => Some(value.clone()),
_ => None,
KclValue::Face { .. } => Ok(value.clone()),
_ => Err(self.into()),
},
PrimitiveType::Helix => match value {
KclValue::Helix { .. } => Some(value.clone()),
_ => None,
KclValue::Helix { .. } => Ok(value.clone()),
_ => Err(self.into()),
},
PrimitiveType::Edge => match value {
KclValue::Uuid { .. } => Some(value.clone()),
KclValue::TagIdentifier { .. } => Some(value.clone()),
_ => None,
KclValue::Uuid { .. } => Ok(value.clone()),
KclValue::TagIdentifier { .. } => Ok(value.clone()),
_ => Err(self.into()),
},
PrimitiveType::Axis2d => match value {
KclValue::Object { value: values, meta } => {
if values.get("origin")?.has_type(&RuntimeType::point2d())
&& values.get("direction")?.has_type(&RuntimeType::point2d())
if values
.get("origin")
.ok_or(CoercionError::from(self))?
.has_type(&RuntimeType::point2d())
&& values
.get("direction")
.ok_or(CoercionError::from(self))?
.has_type(&RuntimeType::point2d())
{
return Some(value.clone());
return Ok(value.clone());
}
let origin = values.get("origin").and_then(|p| {
let origin = values.get("origin").ok_or(self.into()).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| {
let direction = values.get("direction").ok_or(self.into()).and_then(|p| {
p.coerce_to_array_type(&RuntimeType::number_any(), ArrayLen::Known(2), exec_state, true)
})?;
Some(KclValue::Object {
Ok(KclValue::Object {
value: [("origin".to_owned(), origin), ("direction".to_owned(), direction)].into(),
meta: meta.clone(),
})
}
_ => None,
_ => Err(self.into()),
},
PrimitiveType::Axis3d => match value {
KclValue::Object { value: values, meta } => {
if values.get("origin")?.has_type(&RuntimeType::point3d())
&& values.get("direction")?.has_type(&RuntimeType::point3d())
if values
.get("origin")
.ok_or(CoercionError::from(self))?
.has_type(&RuntimeType::point3d())
&& values
.get("direction")
.ok_or(CoercionError::from(self))?
.has_type(&RuntimeType::point3d())
{
return Some(value.clone());
return Ok(value.clone());
}
let origin = values.get("origin").and_then(|p| {
let origin = values.get("origin").ok_or(self.into()).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| {
let direction = values.get("direction").ok_or(self.into()).and_then(|p| {
p.coerce_to_array_type(&RuntimeType::number_any(), ArrayLen::Known(3), exec_state, true)
})?;
Some(KclValue::Object {
Ok(KclValue::Object {
value: [("origin".to_owned(), origin), ("direction".to_owned(), direction)].into(),
meta: meta.clone(),
})
}
_ => None,
_ => Err(self.into()),
},
PrimitiveType::ImportedGeometry => match value {
KclValue::ImportedGeometry { .. } => Some(value.clone()),
_ => None,
KclValue::ImportedGeometry { .. } => Ok(value.clone()),
_ => Err(self.into()),
},
PrimitiveType::Tag => match value {
KclValue::TagDeclarator { .. } => Some(value.clone()),
KclValue::TagIdentifier { .. } => Some(value.clone()),
_ => None,
KclValue::TagDeclarator { .. } => Ok(value.clone()),
KclValue::TagIdentifier { .. } => Ok(value.clone()),
_ => Err(self.into()),
},
}
}
@ -738,37 +964,40 @@ impl KclValue {
len: ArrayLen,
exec_state: &mut ExecState,
allow_shrink: bool,
) -> Option<KclValue> {
) -> Result<KclValue, CoercionError> {
match self {
KclValue::HomArray { value, ty: aty } if aty.subtype(ty) => {
len.satisfied(value.len(), allow_shrink).map(|len| KclValue::HomArray {
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, false).is_some() && value.has_type(ty) => Some(KclValue::HomArray {
.ok_or(self.into()),
value if len.satisfied(1, false).is_some() && value.has_type(ty) => Ok(KclValue::HomArray {
value: vec![value.clone()],
ty: ty.clone(),
}),
KclValue::MixedArray { value, .. } => {
let len = len.satisfied(value.len(), allow_shrink)?;
let len = len
.satisfied(value.len(), allow_shrink)
.ok_or(CoercionError::from(self))?;
let value = value[..len]
.iter()
.map(|v| v.coerce(ty, exec_state))
.collect::<Option<Vec<_>>>()?;
.collect::<Result<Vec<_>, _>>()?;
Some(KclValue::HomArray { value, ty: ty.clone() })
Ok(KclValue::HomArray { value, ty: ty.clone() })
}
KclValue::KclNone { .. } if len.satisfied(0, false).is_some() => Some(KclValue::HomArray {
KclValue::KclNone { .. } if len.satisfied(0, false).is_some() => Ok(KclValue::HomArray {
value: Vec::new(),
ty: ty.clone(),
}),
_ => None,
_ => Err(self.into()),
}
}
fn coerce_to_tuple_type(&self, tys: &[RuntimeType], exec_state: &mut ExecState) -> Option<KclValue> {
fn coerce_to_tuple_type(&self, tys: &[RuntimeType], exec_state: &mut ExecState) -> Result<KclValue, CoercionError> {
match self {
KclValue::MixedArray { value, .. } | KclValue::HomArray { value, .. } if value.len() == tys.len() => {
let mut result = Vec::new();
@ -776,50 +1005,54 @@ impl KclValue {
result.push(value[i].coerce(t, exec_state)?);
}
Some(KclValue::MixedArray {
Ok(KclValue::MixedArray {
value: result,
meta: Vec::new(),
})
}
KclValue::KclNone { meta, .. } if tys.is_empty() => Some(KclValue::MixedArray {
KclValue::KclNone { meta, .. } if tys.is_empty() => Ok(KclValue::MixedArray {
value: Vec::new(),
meta: meta.clone(),
}),
value if tys.len() == 1 && value.has_type(&tys[0]) => Some(KclValue::MixedArray {
value if tys.len() == 1 && value.has_type(&tys[0]) => Ok(KclValue::MixedArray {
value: vec![value.clone()],
meta: Vec::new(),
}),
_ => None,
_ => Err(self.into()),
}
}
fn coerce_to_union_type(&self, tys: &[RuntimeType], exec_state: &mut ExecState) -> Option<KclValue> {
fn coerce_to_union_type(&self, tys: &[RuntimeType], exec_state: &mut ExecState) -> Result<KclValue, CoercionError> {
for t in tys {
if let Some(v) = self.coerce(t, exec_state) {
return Some(v);
if let Ok(v) = self.coerce(t, exec_state) {
return Ok(v);
}
}
None
Err(self.into())
}
fn coerce_to_object_type(&self, tys: &[(String, RuntimeType)], _exec_state: &mut ExecState) -> Option<KclValue> {
fn coerce_to_object_type(
&self,
tys: &[(String, RuntimeType)],
_exec_state: &mut ExecState,
) -> Result<KclValue, CoercionError> {
match self {
KclValue::Object { value, .. } => {
for (s, t) in tys {
// TODO coerce fields
if !value.get(s)?.has_type(t) {
return None;
if !value.get(s).ok_or(CoercionError::from(self))?.has_type(t) {
return Err(self.into());
}
}
// TODO remove non-required fields
Some(self.clone())
Ok(self.clone())
}
KclValue::KclNone { meta, .. } if tys.is_empty() => Some(KclValue::Object {
KclValue::KclNone { meta, .. } if tys.is_empty() => Ok(KclValue::Object {
value: HashMap::new(),
meta: meta.clone(),
}),
_ => None,
_ => Err(self.into()),
}
}
@ -859,6 +1092,8 @@ impl KclValue {
#[cfg(test)]
mod test {
use crate::execution::{parse_execute, ExecTestResults};
use super::*;
fn values(exec_state: &mut ExecState) -> Vec<KclValue> {
@ -988,9 +1223,8 @@ mod test {
for v in &values[1..] {
// Not a subtype
assert!(v
.coerce(&RuntimeType::Primitive(PrimitiveType::Boolean), &mut exec_state)
.is_none());
v.coerce(&RuntimeType::Primitive(PrimitiveType::Boolean), &mut exec_state)
.unwrap_err();
}
}
@ -1024,8 +1258,8 @@ mod test {
},
&mut exec_state,
);
assert!(none.coerce(&aty1, &mut exec_state).is_none());
assert!(none.coerce(&aty1p, &mut exec_state).is_none());
none.coerce(&aty1, &mut exec_state).unwrap_err();
none.coerce(&aty1p, &mut exec_state).unwrap_err();
let tty = RuntimeType::Tuple(vec![]);
let tty1 = RuntimeType::Tuple(vec![RuntimeType::solid()]);
@ -1038,7 +1272,7 @@ mod test {
},
&mut exec_state,
);
assert!(none.coerce(&tty1, &mut exec_state).is_none());
none.coerce(&tty1, &mut exec_state).unwrap_err();
let oty = RuntimeType::Object(vec![]);
assert_coerce_results(
@ -1107,7 +1341,7 @@ mod test {
assert_coerce_results(&obj2, &ty0, &obj2, &mut exec_state);
let ty1 = RuntimeType::Object(vec![("foo".to_owned(), RuntimeType::Primitive(PrimitiveType::Boolean))]);
assert!(&obj0.coerce(&ty1, &mut exec_state).is_none());
obj0.coerce(&ty1, &mut exec_state).unwrap_err();
assert_coerce_results(&obj1, &ty1, &obj1, &mut exec_state);
assert_coerce_results(&obj2, &ty1, &obj2, &mut exec_state);
@ -1119,19 +1353,19 @@ mod test {
),
("foo".to_owned(), RuntimeType::Primitive(PrimitiveType::Boolean)),
]);
assert!(&obj0.coerce(&ty2, &mut exec_state).is_none());
assert!(&obj1.coerce(&ty2, &mut exec_state).is_none());
obj0.coerce(&ty2, &mut exec_state).unwrap_err();
obj1.coerce(&ty2, &mut exec_state).unwrap_err();
assert_coerce_results(&obj2, &ty2, &obj2, &mut exec_state);
// field not present
let tyq = RuntimeType::Object(vec![("qux".to_owned(), RuntimeType::Primitive(PrimitiveType::Boolean))]);
assert!(&obj0.coerce(&tyq, &mut exec_state).is_none());
assert!(&obj1.coerce(&tyq, &mut exec_state).is_none());
assert!(&obj2.coerce(&tyq, &mut exec_state).is_none());
obj0.coerce(&tyq, &mut exec_state).unwrap_err();
obj1.coerce(&tyq, &mut exec_state).unwrap_err();
obj2.coerce(&tyq, &mut exec_state).unwrap_err();
// field with different type
let ty1 = RuntimeType::Object(vec![("bar".to_owned(), RuntimeType::Primitive(PrimitiveType::Boolean))]);
assert!(&obj2.coerce(&ty1, &mut exec_state).is_none());
obj2.coerce(&ty1, &mut exec_state).unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
@ -1209,8 +1443,8 @@ mod test {
assert_coerce_results(&hom_arr, &tyh, &hom_arr, &mut exec_state);
assert_coerce_results(&mixed1, &tym1, &mixed1, &mut exec_state);
assert_coerce_results(&mixed2, &tym2, &mixed2, &mut exec_state);
assert!(&mixed1.coerce(&tym2, &mut exec_state).is_none());
assert!(&mixed2.coerce(&tym1, &mut exec_state).is_none());
mixed1.coerce(&tym2, &mut exec_state).unwrap_err();
mixed2.coerce(&tym1, &mut exec_state).unwrap_err();
// Length subtyping
let tyhn = RuntimeType::Array(
@ -1227,15 +1461,15 @@ mod test {
);
assert_coerce_results(&hom_arr, &tyhn, &hom_arr, &mut exec_state);
assert_coerce_results(&hom_arr, &tyh1, &hom_arr, &mut exec_state);
assert!(&hom_arr.coerce(&tyh3, &mut exec_state).is_none());
hom_arr.coerce(&tyh3, &mut exec_state).unwrap_err();
let hom_arr0 = KclValue::HomArray {
value: vec![],
ty: RuntimeType::Primitive(PrimitiveType::Number(NumericType::count())),
};
assert_coerce_results(&hom_arr0, &tyhn, &hom_arr0, &mut exec_state);
assert!(&hom_arr0.coerce(&tyh1, &mut exec_state).is_none());
assert!(&hom_arr0.coerce(&tyh3, &mut exec_state).is_none());
hom_arr0.coerce(&tyh1, &mut exec_state).unwrap_err();
hom_arr0.coerce(&tyh3, &mut exec_state).unwrap_err();
// Covariance
// let tyh = RuntimeType::Array(Box::new(RuntimeType::Primitive(PrimitiveType::Number(NumericType::Any))), ArrayLen::Known(4));
@ -1275,16 +1509,16 @@ mod test {
assert_coerce_results(&mixed1, &tyhn, &hom_arr_2, &mut exec_state);
assert_coerce_results(&mixed1, &tyh1, &hom_arr_2, &mut exec_state);
assert_coerce_results(&mixed0, &tyhn, &hom_arr0, &mut exec_state);
assert!(&mixed0.coerce(&tyh, &mut exec_state).is_none());
assert!(&mixed0.coerce(&tyh1, &mut exec_state).is_none());
mixed0.coerce(&tyh, &mut exec_state).unwrap_err();
mixed0.coerce(&tyh1, &mut exec_state).unwrap_err();
// Homogehous to mixed
assert_coerce_results(&hom_arr_2, &tym1, &mixed1, &mut exec_state);
assert!(&hom_arr.coerce(&tym1, &mut exec_state).is_none());
assert!(&hom_arr_2.coerce(&tym2, &mut exec_state).is_none());
hom_arr.coerce(&tym1, &mut exec_state).unwrap_err();
hom_arr_2.coerce(&tym2, &mut exec_state).unwrap_err();
assert!(&mixed0.coerce(&tym1, &mut exec_state).is_none());
assert!(&mixed0.coerce(&tym2, &mut exec_state).is_none());
mixed0.coerce(&tym1, &mut exec_state).unwrap_err();
mixed0.coerce(&tym2, &mut exec_state).unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
@ -1334,8 +1568,8 @@ mod test {
RuntimeType::Primitive(PrimitiveType::Boolean),
RuntimeType::Primitive(PrimitiveType::String),
]);
assert!(count.coerce(&tyb, &mut exec_state).is_none());
assert!(count.coerce(&tyb2, &mut exec_state).is_none());
count.coerce(&tyb, &mut exec_state).unwrap_err();
count.coerce(&tyb2, &mut exec_state).unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
@ -1450,6 +1684,196 @@ mod test {
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());
a2d.coerce(&ty3d, &mut exec_state).unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
async fn coerce_numeric() {
let mut exec_state = ExecState::new(&crate::ExecutorContext::new_mock().await);
let count = KclValue::Number {
value: 1.0,
ty: NumericType::count(),
meta: Vec::new(),
};
let mm = KclValue::Number {
value: 1.0,
ty: NumericType::mm(),
meta: Vec::new(),
};
let inches = KclValue::Number {
value: 1.0,
ty: NumericType::Known(UnitType::Length(UnitLen::Inches)),
meta: Vec::new(),
};
let rads = KclValue::Number {
value: 1.0,
ty: NumericType::Known(UnitType::Angle(UnitAngle::Radians)),
meta: Vec::new(),
};
let default = KclValue::Number {
value: 1.0,
ty: NumericType::default(),
meta: Vec::new(),
};
let any = KclValue::Number {
value: 1.0,
ty: NumericType::Any,
meta: Vec::new(),
};
let unknown = KclValue::Number {
value: 1.0,
ty: NumericType::Unknown,
meta: Vec::new(),
};
// Trivial coercions
assert_coerce_results(&count, &NumericType::count().into(), &count, &mut exec_state);
assert_coerce_results(&mm, &NumericType::mm().into(), &mm, &mut exec_state);
assert_coerce_results(&any, &NumericType::Any.into(), &any, &mut exec_state);
assert_coerce_results(&unknown, &NumericType::Unknown.into(), &unknown, &mut exec_state);
assert_coerce_results(&default, &NumericType::default().into(), &default, &mut exec_state);
assert_coerce_results(&count, &NumericType::Any.into(), &count, &mut exec_state);
assert_coerce_results(&mm, &NumericType::Any.into(), &mm, &mut exec_state);
assert_coerce_results(&unknown, &NumericType::Any.into(), &unknown, &mut exec_state);
assert_coerce_results(&default, &NumericType::Any.into(), &default, &mut exec_state);
if !*CHECK_NUMERIC_TYPES {
return;
}
assert_eq!(
default
.coerce(
&NumericType::Default {
len: UnitLen::Yards,
angle: UnitAngle::default()
}
.into(),
&mut exec_state
)
.unwrap(),
default
);
// No coercion
count.coerce(&NumericType::mm().into(), &mut exec_state).unwrap_err();
mm.coerce(&NumericType::count().into(), &mut exec_state).unwrap_err();
unknown.coerce(&NumericType::mm().into(), &mut exec_state).unwrap_err();
unknown
.coerce(&NumericType::default().into(), &mut exec_state)
.unwrap_err();
count.coerce(&NumericType::Unknown.into(), &mut exec_state).unwrap_err();
mm.coerce(&NumericType::Unknown.into(), &mut exec_state).unwrap_err();
default
.coerce(&NumericType::Unknown.into(), &mut exec_state)
.unwrap_err();
assert_eq!(
inches
.coerce(&NumericType::mm().into(), &mut exec_state)
.unwrap()
.as_f64()
.unwrap()
.round(),
25.0
);
assert_eq!(
rads.coerce(
&NumericType::Known(UnitType::Angle(UnitAngle::Degrees)).into(),
&mut exec_state
)
.unwrap()
.as_f64()
.unwrap()
.round(),
57.0
);
assert_eq!(
inches
.coerce(&NumericType::default().into(), &mut exec_state)
.unwrap()
.as_f64()
.unwrap()
.round(),
25.0
);
assert_eq!(
rads.coerce(&NumericType::default().into(), &mut exec_state)
.unwrap()
.as_f64()
.unwrap()
.round(),
57.0
);
}
#[track_caller]
fn assert_value_and_type(name: &str, result: &ExecTestResults, expected: f64, expected_ty: NumericType) {
let mem = result.exec_state.stack();
match mem
.memory
.get_from(name, result.mem_env, SourceRange::default(), 0)
.unwrap()
{
KclValue::Number { value, ty, .. } => {
assert_eq!(value.round(), expected);
assert_eq!(*ty, expected_ty);
}
_ => unreachable!(),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn combine_numeric() {
let program = r#"a = 5 + 4
b = 5 - 2
c = 5mm - 2mm + 10mm
d = 5mm - 2 + 10
e = 5 - 2mm + 10
f = 30mm - 1inch
g = 2 * 10
h = 2 * 10mm
i = 2mm * 10mm
j = 2_ * 10
k = 2_ * 3mm * 3mm
l = 1 / 10
m = 2mm / 1mm
n = 10inch / 2mm
o = 3mm / 3
p = 3_ / 4
q = 4inch / 2_
"#;
let result = parse_execute(program).await.unwrap();
if *CHECK_NUMERIC_TYPES {
assert_eq!(result.exec_state.errors().len(), 2);
} else {
assert!(result.exec_state.errors().is_empty());
}
assert_value_and_type("a", &result, 9.0, NumericType::default());
assert_value_and_type("b", &result, 3.0, NumericType::default());
assert_value_and_type("c", &result, 13.0, NumericType::mm());
assert_value_and_type("d", &result, 13.0, NumericType::mm());
assert_value_and_type("e", &result, 13.0, NumericType::mm());
assert_value_and_type("f", &result, 5.0, NumericType::mm());
assert_value_and_type("g", &result, 20.0, NumericType::default());
assert_value_and_type("h", &result, 20.0, NumericType::mm());
assert_value_and_type("i", &result, 20.0, NumericType::Unknown);
assert_value_and_type("j", &result, 20.0, NumericType::default());
assert_value_and_type("k", &result, 18.0, NumericType::Unknown);
assert_value_and_type("l", &result, 0.0, NumericType::count());
assert_value_and_type("m", &result, 2.0, NumericType::count());
assert_value_and_type("n", &result, 127.0, NumericType::count());
assert_value_and_type("o", &result, 1.0, NumericType::mm());
assert_value_and_type("p", &result, 1.0, NumericType::count());
assert_value_and_type("q", &result, 2.0, NumericType::Known(UnitType::Length(UnitLen::Inches)));
}
}