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:
Adam Chalmers
2024-12-05 14:27:51 -06:00
committed by GitHub
parent 7a21918223
commit 6366bc4766
21 changed files with 1030 additions and 374 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -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) {

View File

@ -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.

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -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!(

View File

@ -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 {

View File

@ -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,

View File

@ -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)