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,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();
}
}