KCL: Keyword function calls for stdlib (#4647)
Part of https://github.com/KittyCAD/modeling-app/issues/4600 Adds support for keyword arguments to the stdlib, and calling stdlib functions with keyword arguments. So far, I've changed one function: `rem`. Previously you would have used `rem(7, 2)` but now it's `rem(7, divisor: 2)`. This is a proof-of-concept. If it's approved, we will: 1. Support closures with keyword arguments, and calling them 2. Move the rest of the stdlib to use kw arguments
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -173,6 +173,30 @@ function moreNodePathFromSourceRange(
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
if (_node.type === 'CallExpressionKw' && isInRange) {
|
||||
const { callee, arguments: args } = _node
|
||||
if (
|
||||
callee.type === 'Identifier' &&
|
||||
callee.start <= start &&
|
||||
callee.end >= end
|
||||
) {
|
||||
path.push(['callee', 'CallExpressionKw'])
|
||||
return path
|
||||
}
|
||||
if (args.length > 0) {
|
||||
for (let argIndex = 0; argIndex < args.length; argIndex++) {
|
||||
const arg = args[argIndex].arg
|
||||
if (arg.start <= start && arg.end >= end) {
|
||||
path.push(['arguments', 'CallExpressionKw'])
|
||||
path.push([argIndex, 'index'])
|
||||
return moreNodePathFromSourceRange(arg, sourceRange, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
if (_node.type === 'BinaryExpression' && isInRange) {
|
||||
const { left, right } = _node
|
||||
if (left.start <= start && left.end >= end) {
|
||||
|
@ -23,17 +23,30 @@ use unbox::unbox;
|
||||
struct StdlibMetadata {
|
||||
/// The name of the function in the API.
|
||||
name: String,
|
||||
|
||||
/// Tags for the function.
|
||||
#[serde(default)]
|
||||
tags: Vec<String>,
|
||||
|
||||
/// Whether the function is unpublished.
|
||||
/// Then docs will not be generated.
|
||||
#[serde(default)]
|
||||
unpublished: bool,
|
||||
|
||||
/// Whether the function is deprecated.
|
||||
/// Then specific docs detailing that this is deprecated will be generated.
|
||||
#[serde(default)]
|
||||
deprecated: bool,
|
||||
|
||||
/// If true, expects keyword arguments.
|
||||
/// If false, expects positional arguments.
|
||||
#[serde(default)]
|
||||
keywords: bool,
|
||||
|
||||
/// If true, the first argument is unlabeled.
|
||||
/// If false, all arguments require labels.
|
||||
#[serde(default)]
|
||||
unlabeled_first: bool,
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
@ -225,6 +238,12 @@ fn do_stdlib_inner(
|
||||
quote! { false }
|
||||
};
|
||||
|
||||
let uses_keyword_arguments = if metadata.keywords {
|
||||
quote! { true }
|
||||
} else {
|
||||
quote! { false }
|
||||
};
|
||||
|
||||
let docs_crate = get_crate(None);
|
||||
|
||||
// When the user attaches this proc macro to a function with the wrong type
|
||||
@ -233,7 +252,7 @@ fn do_stdlib_inner(
|
||||
// of the various parameters. We do this by calling dummy functions that
|
||||
// require a type that satisfies SharedExtractor or ExclusiveExtractor.
|
||||
let mut arg_types = Vec::new();
|
||||
for arg in ast.sig.inputs.iter() {
|
||||
for (i, arg) in ast.sig.inputs.iter().enumerate() {
|
||||
// Get the name of the argument.
|
||||
let arg_name = match arg {
|
||||
syn::FnArg::Receiver(pat) => {
|
||||
@ -263,7 +282,7 @@ fn do_stdlib_inner(
|
||||
|
||||
let ty_string = rust_type_to_openapi_type(&ty_string);
|
||||
let required = !ty_ident.to_string().starts_with("Option <");
|
||||
|
||||
let label_required = !(i == 0 && metadata.unlabeled_first);
|
||||
if ty_string != "ExecState" && ty_string != "Args" {
|
||||
let schema = quote! {
|
||||
generator.root_schema_for::<#ty_ident>()
|
||||
@ -274,6 +293,7 @@ fn do_stdlib_inner(
|
||||
type_: #ty_string.to_string(),
|
||||
schema: #schema,
|
||||
required: #required,
|
||||
label_required: #label_required,
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -334,6 +354,7 @@ fn do_stdlib_inner(
|
||||
type_: #ret_ty_string.to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@ -400,6 +421,10 @@ fn do_stdlib_inner(
|
||||
vec![#(#tags),*]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
#uses_keyword_arguments
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<#docs_crate::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
// We set this to false so we can recurse them later.
|
||||
|
@ -74,6 +74,10 @@ impl crate::docs::StdLibFn for SomeFn {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -83,6 +87,7 @@ impl crate::docs::StdLibFn for SomeFn {
|
||||
type_: "Foo".to_string(),
|
||||
schema: generator.root_schema_for::<Foo>(),
|
||||
required: true,
|
||||
label_required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
@ -96,6 +101,7 @@ impl crate::docs::StdLibFn for SomeFn {
|
||||
type_: "i32".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,10 @@ impl crate::docs::StdLibFn for SomeFn {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -83,6 +87,7 @@ impl crate::docs::StdLibFn for SomeFn {
|
||||
type_: "string".to_string(),
|
||||
schema: generator.root_schema_for::<str>(),
|
||||
required: true,
|
||||
label_required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
@ -96,6 +101,7 @@ impl crate::docs::StdLibFn for SomeFn {
|
||||
type_: "i32".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -108,6 +108,10 @@ impl crate::docs::StdLibFn for Show {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -117,6 +121,7 @@ impl crate::docs::StdLibFn for Show {
|
||||
type_: "[number]".to_string(),
|
||||
schema: generator.root_schema_for::<[f64; 2usize]>(),
|
||||
required: true,
|
||||
label_required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
@ -130,6 +135,7 @@ impl crate::docs::StdLibFn for Show {
|
||||
type_: "number".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,10 @@ impl crate::docs::StdLibFn for Show {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -83,6 +87,7 @@ impl crate::docs::StdLibFn for Show {
|
||||
type_: "number".to_string(),
|
||||
schema: generator.root_schema_for::<f64>(),
|
||||
required: true,
|
||||
label_required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
@ -96,6 +101,7 @@ impl crate::docs::StdLibFn for Show {
|
||||
type_: "number".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -108,6 +108,10 @@ impl crate::docs::StdLibFn for MyFunc {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -117,6 +121,7 @@ impl crate::docs::StdLibFn for MyFunc {
|
||||
type_: "kittycad::types::InputFormat".to_string(),
|
||||
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
|
||||
required: false,
|
||||
label_required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
@ -130,6 +135,7 @@ impl crate::docs::StdLibFn for MyFunc {
|
||||
type_: "[Sketch]".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -108,6 +108,10 @@ impl crate::docs::StdLibFn for LineTo {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -118,12 +122,14 @@ impl crate::docs::StdLibFn for LineTo {
|
||||
type_: "LineToData".to_string(),
|
||||
schema: generator.root_schema_for::<LineToData>(),
|
||||
required: true,
|
||||
label_required: true,
|
||||
},
|
||||
crate::docs::StdLibFnArg {
|
||||
name: "sketch".to_string(),
|
||||
type_: "Sketch".to_string(),
|
||||
schema: generator.root_schema_for::<Sketch>(),
|
||||
required: true,
|
||||
label_required: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -138,6 +144,7 @@ impl crate::docs::StdLibFn for LineTo {
|
||||
type_: "Sketch".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -108,6 +108,10 @@ impl crate::docs::StdLibFn for Min {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -117,6 +121,7 @@ impl crate::docs::StdLibFn for Min {
|
||||
type_: "[number]".to_string(),
|
||||
schema: generator.root_schema_for::<Vec<f64>>(),
|
||||
required: true,
|
||||
label_required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
@ -130,6 +135,7 @@ impl crate::docs::StdLibFn for Min {
|
||||
type_: "number".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,10 @@ impl crate::docs::StdLibFn for Show {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -83,6 +87,7 @@ impl crate::docs::StdLibFn for Show {
|
||||
type_: "number".to_string(),
|
||||
schema: generator.root_schema_for::<Option<f64>>(),
|
||||
required: false,
|
||||
label_required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
@ -96,6 +101,7 @@ impl crate::docs::StdLibFn for Show {
|
||||
type_: "number".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,10 @@ impl crate::docs::StdLibFn for Import {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -83,6 +87,7 @@ impl crate::docs::StdLibFn for Import {
|
||||
type_: "kittycad::types::InputFormat".to_string(),
|
||||
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
|
||||
required: false,
|
||||
label_required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
@ -96,6 +101,7 @@ impl crate::docs::StdLibFn for Import {
|
||||
type_: "number".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,10 @@ impl crate::docs::StdLibFn for Import {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -83,6 +87,7 @@ impl crate::docs::StdLibFn for Import {
|
||||
type_: "kittycad::types::InputFormat".to_string(),
|
||||
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
|
||||
required: false,
|
||||
label_required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
@ -96,6 +101,7 @@ impl crate::docs::StdLibFn for Import {
|
||||
type_: "[Sketch]".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,10 @@ impl crate::docs::StdLibFn for Import {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -83,6 +87,7 @@ impl crate::docs::StdLibFn for Import {
|
||||
type_: "kittycad::types::InputFormat".to_string(),
|
||||
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
|
||||
required: false,
|
||||
label_required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
@ -96,6 +101,7 @@ impl crate::docs::StdLibFn for Import {
|
||||
type_: "[Sketch]".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,10 @@ impl crate::docs::StdLibFn for Show {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -83,6 +87,7 @@ impl crate::docs::StdLibFn for Show {
|
||||
type_: "[number]".to_string(),
|
||||
schema: generator.root_schema_for::<Vec<f64>>(),
|
||||
required: true,
|
||||
label_required: true,
|
||||
}]
|
||||
}
|
||||
|
||||
@ -96,6 +101,7 @@ impl crate::docs::StdLibFn for Show {
|
||||
type_: "()".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,10 @@ impl crate::docs::StdLibFn for SomeFunction {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn keyword_arguments(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||
settings.inline_subschemas = inline_subschemas;
|
||||
@ -91,6 +95,7 @@ impl crate::docs::StdLibFn for SomeFunction {
|
||||
type_: "i32".to_string(),
|
||||
schema,
|
||||
required: true,
|
||||
label_required: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,8 @@ pub struct StdLibFnData {
|
||||
pub description: String,
|
||||
/// The tags of the function.
|
||||
pub tags: Vec<String>,
|
||||
/// If this function uses keyword arguments, or positional arguments.
|
||||
pub keyword_arguments: bool,
|
||||
/// The args of the function.
|
||||
pub args: Vec<StdLibFnArg>,
|
||||
/// The return value of the function.
|
||||
@ -55,6 +57,18 @@ pub struct StdLibFnArg {
|
||||
pub schema: schemars::schema::RootSchema,
|
||||
/// If the argument is required.
|
||||
pub required: bool,
|
||||
/// Even in functions that use keyword arguments, not every parameter requires a label (most do though).
|
||||
/// Some functions allow one unlabeled parameter, which has to be first in the
|
||||
/// argument list.
|
||||
///
|
||||
/// This field is ignored for functions that still use positional arguments.
|
||||
/// Defaults to true.
|
||||
#[serde(default = "its_true")]
|
||||
pub label_required: bool,
|
||||
}
|
||||
|
||||
fn its_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl StdLibFnArg {
|
||||
@ -120,6 +134,9 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
|
||||
/// The description of the function.
|
||||
fn description(&self) -> String;
|
||||
|
||||
/// Does this use keyword arguments, or positional?
|
||||
fn keyword_arguments(&self) -> bool;
|
||||
|
||||
/// The tags of the function.
|
||||
fn tags(&self) -> Vec<String>;
|
||||
|
||||
@ -151,6 +168,7 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
|
||||
summary: self.summary(),
|
||||
description: self.description(),
|
||||
tags: self.tags(),
|
||||
keyword_arguments: self.keyword_arguments(),
|
||||
args: self.args(false),
|
||||
return_value: self.return_value(false),
|
||||
unpublished: self.unpublished(),
|
||||
@ -806,7 +824,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_function() {
|
||||
let some_function_string = r#"{"type":"StdLib","func":{"name":"line","summary":"","description":"","tags":[],"returnValue":{"type":"","required":false,"name":"","schema":{},"schemaDefinitions":{}},"args":[],"unpublished":false,"deprecated":false, "examples": []}}"#;
|
||||
let some_function_string = r#"{"type":"StdLib","func":{"name":"line","keywordArguments":false,"summary":"","description":"","tags":[],"returnValue":{"type":"","required":false,"name":"","schema":{},"schemaDefinitions":{}},"args":[],"unpublished":false,"deprecated":false, "examples": []}}"#;
|
||||
let some_function: crate::parsing::ast::types::Function = serde_json::from_str(some_function_string).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
|
@ -358,8 +358,47 @@ async fn inner_execute_pipe_body(
|
||||
}
|
||||
|
||||
impl Node<CallExpressionKw> {
|
||||
pub async fn execute(&self, _exec_state: &mut ExecState, _ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||
todo!()
|
||||
#[async_recursion]
|
||||
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
|
||||
let fn_name = &self.callee.name;
|
||||
|
||||
// Build a hashmap from argument labels to the final evaluated values.
|
||||
let mut fn_args = HashMap::with_capacity(self.arguments.len());
|
||||
for arg_expr in &self.arguments {
|
||||
let source_range = SourceRange::from(arg_expr.arg.clone());
|
||||
let metadata = Metadata { source_range };
|
||||
let value = ctx
|
||||
.execute_expr(&arg_expr.arg, exec_state, &metadata, StatementKind::Expression)
|
||||
.await?;
|
||||
fn_args.insert(arg_expr.label.name.clone(), Arg::new(value, source_range));
|
||||
}
|
||||
let fn_args = fn_args; // remove mutability
|
||||
|
||||
// Evaluate the unlabeled first param, if any exists.
|
||||
let unlabeled = if let Some(ref arg_expr) = self.unlabeled {
|
||||
let source_range = SourceRange::from(arg_expr.clone());
|
||||
let metadata = Metadata { source_range };
|
||||
let value = ctx
|
||||
.execute_expr(arg_expr, exec_state, &metadata, StatementKind::Expression)
|
||||
.await?;
|
||||
Some(Arg::new(value, source_range))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let args = crate::std::Args::new_kw(fn_args, unlabeled, self.into(), ctx.clone());
|
||||
match ctx.stdlib.get_either(fn_name) {
|
||||
FunctionKind::Core(func) => {
|
||||
// Attempt to call the function.
|
||||
let mut result = func.std_lib_fn()(exec_state, args).await?;
|
||||
update_memory_for_tags_of_geometry(&mut result, exec_state)?;
|
||||
Ok(result)
|
||||
}
|
||||
FunctionKind::UserDefined => {
|
||||
todo!("Part of modeling-app#4600: Support keyword arguments for user-defined functions")
|
||||
}
|
||||
FunctionKind::Std(_) => todo!("There is no KCL std anymore, it's all core."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -381,76 +420,12 @@ impl Node<CallExpression> {
|
||||
fn_args.push(arg);
|
||||
}
|
||||
|
||||
match ctx.stdlib.get_either(&self.callee.name) {
|
||||
match ctx.stdlib.get_either(fn_name) {
|
||||
FunctionKind::Core(func) => {
|
||||
// Attempt to call the function.
|
||||
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
|
||||
let mut result = func.std_lib_fn()(exec_state, args).await?;
|
||||
|
||||
// If the return result is a sketch or solid, we want to update the
|
||||
// memory for the tags of the group.
|
||||
// TODO: This could probably be done in a better way, but as of now this was my only idea
|
||||
// and it works.
|
||||
match result {
|
||||
KclValue::Sketch { value: ref mut sketch } => {
|
||||
for (_, tag) in sketch.tags.iter() {
|
||||
exec_state.memory.update_tag(&tag.value, tag.clone())?;
|
||||
}
|
||||
}
|
||||
KclValue::Solid(ref mut solid) => {
|
||||
for value in &solid.value {
|
||||
if let Some(tag) = value.get_tag() {
|
||||
// Get the past tag and update it.
|
||||
let mut t = if let Some(t) = solid.sketch.tags.get(&tag.name) {
|
||||
t.clone()
|
||||
} else {
|
||||
// It's probably a fillet or a chamfer.
|
||||
// Initialize it.
|
||||
TagIdentifier {
|
||||
value: tag.name.clone(),
|
||||
info: Some(TagEngineInfo {
|
||||
id: value.get_id(),
|
||||
surface: Some(value.clone()),
|
||||
path: None,
|
||||
sketch: solid.id,
|
||||
}),
|
||||
meta: vec![Metadata {
|
||||
source_range: tag.clone().into(),
|
||||
}],
|
||||
}
|
||||
};
|
||||
|
||||
let Some(ref info) = t.info else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Tag {} does not have path info", tag.name),
|
||||
source_ranges: vec![tag.into()],
|
||||
}));
|
||||
};
|
||||
|
||||
let mut info = info.clone();
|
||||
info.surface = Some(value.clone());
|
||||
info.sketch = solid.id;
|
||||
t.info = Some(info);
|
||||
|
||||
exec_state.memory.update_tag(&tag.name, t.clone())?;
|
||||
|
||||
// update the sketch tags.
|
||||
solid.sketch.tags.insert(tag.name.clone(), t);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the stale sketch in memory and update it.
|
||||
if let Some(current_env) = exec_state
|
||||
.memory
|
||||
.environments
|
||||
.get_mut(exec_state.memory.current_env.index())
|
||||
{
|
||||
current_env.update_sketch_tags(&solid.sketch);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
update_memory_for_tags_of_geometry(&mut result, exec_state)?;
|
||||
Ok(result)
|
||||
}
|
||||
FunctionKind::Std(func) => {
|
||||
@ -570,6 +545,73 @@ impl Node<CallExpression> {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut ExecState) -> Result<(), KclError> {
|
||||
// If the return result is a sketch or solid, we want to update the
|
||||
// memory for the tags of the group.
|
||||
// TODO: This could probably be done in a better way, but as of now this was my only idea
|
||||
// and it works.
|
||||
match result {
|
||||
KclValue::Sketch { value: ref mut sketch } => {
|
||||
for (_, tag) in sketch.tags.iter() {
|
||||
exec_state.memory.update_tag(&tag.value, tag.clone())?;
|
||||
}
|
||||
}
|
||||
KclValue::Solid(ref mut solid) => {
|
||||
for value in &solid.value {
|
||||
if let Some(tag) = value.get_tag() {
|
||||
// Get the past tag and update it.
|
||||
let mut t = if let Some(t) = solid.sketch.tags.get(&tag.name) {
|
||||
t.clone()
|
||||
} else {
|
||||
// It's probably a fillet or a chamfer.
|
||||
// Initialize it.
|
||||
TagIdentifier {
|
||||
value: tag.name.clone(),
|
||||
info: Some(TagEngineInfo {
|
||||
id: value.get_id(),
|
||||
surface: Some(value.clone()),
|
||||
path: None,
|
||||
sketch: solid.id,
|
||||
}),
|
||||
meta: vec![Metadata {
|
||||
source_range: tag.clone().into(),
|
||||
}],
|
||||
}
|
||||
};
|
||||
|
||||
let Some(ref info) = t.info else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!("Tag {} does not have path info", tag.name),
|
||||
source_ranges: vec![tag.into()],
|
||||
}));
|
||||
};
|
||||
|
||||
let mut info = info.clone();
|
||||
info.surface = Some(value.clone());
|
||||
info.sketch = solid.id;
|
||||
t.info = Some(info);
|
||||
|
||||
exec_state.memory.update_tag(&tag.name, t.clone())?;
|
||||
|
||||
// update the sketch tags.
|
||||
solid.sketch.tags.insert(tag.name.clone(), t);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the stale sketch in memory and update it.
|
||||
if let Some(current_env) = exec_state
|
||||
.memory
|
||||
.environments
|
||||
.get_mut(exec_state.memory.current_env.index())
|
||||
{
|
||||
current_env.update_sketch_tags(&solid.sketch);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Node<TagDeclarator> {
|
||||
pub async fn execute(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
|
||||
let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::{any::type_name, num::NonZeroU32};
|
||||
use std::{any::type_name, collections::HashMap, num::NonZeroU32};
|
||||
|
||||
use anyhow::Result;
|
||||
use kcmc::{websocket::OkWebSocketResponseData, ModelingCmd};
|
||||
@ -45,7 +45,12 @@ impl Arg {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Args {
|
||||
/// Positional args.
|
||||
pub args: Vec<Arg>,
|
||||
/// Keyword args.
|
||||
pub kw_args: HashMap<String, Arg>,
|
||||
/// Unlabeled keyword args. Currently only the first arg can be unlabeled.
|
||||
pub unlabeled_kw_arg: Option<Arg>,
|
||||
pub source_range: SourceRange,
|
||||
pub ctx: ExecutorContext,
|
||||
}
|
||||
@ -54,6 +59,24 @@ impl Args {
|
||||
pub fn new(args: Vec<Arg>, source_range: SourceRange, ctx: ExecutorContext) -> Self {
|
||||
Self {
|
||||
args,
|
||||
kw_args: Default::default(),
|
||||
unlabeled_kw_arg: Default::default(),
|
||||
source_range,
|
||||
ctx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect the given keyword arguments.
|
||||
pub fn new_kw(
|
||||
kw_args: HashMap<String, Arg>,
|
||||
unlabeled_kw_arg: Option<Arg>,
|
||||
source_range: SourceRange,
|
||||
ctx: ExecutorContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
args: Default::default(),
|
||||
kw_args,
|
||||
unlabeled_kw_arg,
|
||||
source_range,
|
||||
ctx,
|
||||
}
|
||||
@ -65,6 +88,8 @@ impl Args {
|
||||
|
||||
Ok(Self {
|
||||
args: Vec::new(),
|
||||
kw_args: Default::default(),
|
||||
unlabeled_kw_arg: Default::default(),
|
||||
source_range: SourceRange::default(),
|
||||
ctx: ExecutorContext {
|
||||
engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await?)),
|
||||
@ -76,6 +101,50 @@ impl Args {
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a keyword argument. If not set, returns None.
|
||||
pub(crate) fn get_kw_arg_opt<'a, T>(&'a self, label: &str) -> Option<T>
|
||||
where
|
||||
T: FromKclValue<'a>,
|
||||
{
|
||||
self.kw_args.get(label).and_then(|arg| T::from_kcl_val(&arg.value))
|
||||
}
|
||||
|
||||
/// Get a keyword argument. If not set, returns Err.
|
||||
pub(crate) fn get_kw_arg<'a, T>(&'a self, label: &str) -> Result<T, KclError>
|
||||
where
|
||||
T: FromKclValue<'a>,
|
||||
{
|
||||
self.get_kw_arg_opt(label).ok_or_else(|| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: vec![self.source_range],
|
||||
message: format!("This function requires a keyword argument '{label}'"),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the unlabeled keyword argument. If not set, returns Err.
|
||||
pub(crate) fn get_unlabeled_kw_arg<'a, T>(&'a self, label: &str) -> Result<T, KclError>
|
||||
where
|
||||
T: FromKclValue<'a>,
|
||||
{
|
||||
let Some(ref arg) = self.unlabeled_kw_arg else {
|
||||
return Err(KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: vec![self.source_range],
|
||||
message: format!("This function requires a value for the special unlabeled first parameter, '{label}'"),
|
||||
}));
|
||||
};
|
||||
T::from_kcl_val(&arg.value).ok_or_else(|| {
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: arg.source_ranges(),
|
||||
message: format!(
|
||||
"Expected a {} but found {}",
|
||||
type_name::<T>(),
|
||||
arg.value.human_friendly_type()
|
||||
),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Add a modeling command to the batch but don't fire it right away.
|
||||
pub(crate) async fn batch_modeling_cmd(
|
||||
&self,
|
||||
|
@ -3,7 +3,6 @@
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
|
||||
use super::args::FromArgs;
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{ExecState, KclValue},
|
||||
@ -13,7 +12,8 @@ use crate::{
|
||||
/// Compute the remainder after dividing `num` by `div`.
|
||||
/// If `num` is negative, the result will be too.
|
||||
pub async fn rem(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
|
||||
let (n, d) = FromArgs::from_args(&args, 0)?;
|
||||
let n = args.get_unlabeled_kw_arg("number to divide")?;
|
||||
let d = args.get_kw_arg("divisor")?;
|
||||
let result = inner_rem(n, d)?;
|
||||
|
||||
Ok(args.make_user_val_from_i64(result))
|
||||
@ -23,13 +23,15 @@ pub async fn rem(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
|
||||
/// If `num` is negative, the result will be too.
|
||||
///
|
||||
/// ```no_run
|
||||
/// assertEqual(rem(7, 4), 3, 0.01, "remainder is 3")
|
||||
/// assertEqual(rem(-7, 4), -3, 0.01, "remainder is 3")
|
||||
/// assertEqual(rem(7, -4), 3, 0.01, "remainder is 3")
|
||||
/// assertEqual(rem(7, divisor: 4), 3, 0.01, "remainder is 3")
|
||||
/// assertEqual(rem(-7, divisor: 4), -3, 0.01, "remainder is 3")
|
||||
/// assertEqual(rem(7, divisor: -4), 3, 0.01, "remainder is 3")
|
||||
/// ```
|
||||
#[stdlib {
|
||||
name = "rem",
|
||||
tags = ["math"],
|
||||
keywords = true,
|
||||
unlabeled_first = true,
|
||||
}]
|
||||
fn inner_rem(num: i64, divisor: i64) -> Result<i64, KclError> {
|
||||
Ok(num % divisor)
|
||||
|
Reference in New Issue
Block a user