KCL keyword args: calling user-defined functions (#4722)

Part of https://github.com/KittyCAD/modeling-app/issues/4600

You can now call a user-defined function via keyword args. E.g.

```
fn increment(@x) {
  return x + 1
}

fn add(@x, delta) {
  return x + delta
}

two = increment(1)
three = add(1, delta: 2)
```
This commit is contained in:
Adam Chalmers
2024-12-09 22:11:16 -06:00
committed by GitHub
parent e27840219b
commit 210c78029d
14 changed files with 927 additions and 16 deletions

View File

@ -366,6 +366,7 @@ impl Node<CallExpressionKw> {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let fn_name = &self.callee.name;
let callsite: SourceRange = self.into();
// Build a hashmap from argument labels to the final evaluated values.
let mut fn_args = HashMap::with_capacity(self.arguments.len());
@ -412,7 +413,39 @@ impl Node<CallExpressionKw> {
Ok(result)
}
FunctionKind::UserDefined => {
todo!("Part of modeling-app#4600: Support keyword arguments for user-defined functions")
let source_range = SourceRange::from(self);
// Clone the function so that we can use a mutable reference to
// exec_state.
let func = exec_state.memory.get(fn_name, source_range)?.clone();
let fn_dynamic_state = exec_state.dynamic_state.merge(&exec_state.memory);
let return_value = {
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
let result = func
.call_fn_kw(args, exec_state, ctx.clone(), callsite)
.await
.map_err(|e| {
// Add the call expression to the source ranges.
// TODO currently ignored by the frontend
e.add_source_ranges(vec![source_range])
});
exec_state.dynamic_state = previous_dynamic_state;
result?
};
let result = return_value.ok_or_else(move || {
let mut source_ranges: Vec<SourceRange> = vec![source_range];
// We want to send the source range of the original function.
if let KclValue::Function { meta, .. } = func {
source_ranges = meta.iter().map(|m| m.source_range).collect();
};
KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of user-defined function {} is undefined", fn_name),
source_ranges,
})
})?;
Ok(result)
}
}
}

View File

@ -72,6 +72,10 @@ pub enum KclValue {
ImportedGeometry(ImportedGeometry),
#[ts(skip)]
Function {
/// Adam Chalmers speculation:
/// Reference to a KCL stdlib function (written in Rust).
/// Some if the KCL value is an alias of a stdlib function,
/// None if it's a KCL function written/declared in KCL.
#[serde(skip)]
func: Option<MemoryFunction>,
#[schemars(skip)]
@ -503,4 +507,39 @@ impl KclValue {
.await
}
}
/// If this is a function, call it by applying keyword arguments.
/// If it's not a function, returns an error.
pub async fn call_fn_kw(
&self,
args: crate::std::Args,
exec_state: &mut ExecState,
ctx: ExecutorContext,
callsite: SourceRange,
) -> Result<Option<KclValue>, KclError> {
let KclValue::Function {
func,
expression,
memory: closure_memory,
meta: _,
} = &self
else {
return Err(KclError::Semantic(KclErrorDetails {
message: "cannot call this because it isn't a function".to_string(),
source_ranges: vec![callsite],
}));
};
if let Some(_func) = func {
todo!("Implement calling KCL stdlib fns that are aliased. Part of https://github.com/KittyCAD/modeling-app/issues/4600");
} else {
crate::execution::call_user_defined_function_kw(
args.kw_args,
closure_memory.as_ref(),
expression.as_ref(),
exec_state,
&ctx,
)
.await
}
}
}

View File

@ -2285,6 +2285,59 @@ fn assign_args_to_params(
Ok(fn_memory)
}
fn assign_args_to_params_kw(
function_expression: NodeRef<'_, FunctionExpression>,
mut args: crate::std::args::KwArgs,
mut fn_memory: ProgramMemory,
) -> Result<ProgramMemory, KclError> {
// Add the arguments to the memory. A new call frame should have already
// been created.
let source_ranges = vec![function_expression.into()];
for param in function_expression.params.iter() {
if param.labeled {
let arg = args.labeled.get(&param.identifier.name);
let arg_val = match arg {
Some(arg) => arg.value.clone(),
None => match param.default_value {
Some(ref default_val) => KclValue::from(default_val.clone()),
None => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges,
message: format!(
"This function requires a parameter {}, but you haven't passed it one.",
param.identifier.name
),
}));
}
},
};
fn_memory.add(&param.identifier.name, arg_val, (&param.identifier).into())?;
} else {
let Some(unlabeled) = args.unlabeled.take() else {
let param_name = &param.identifier.name;
return Err(if args.labeled.contains_key(param_name) {
KclError::Semantic(KclErrorDetails {
source_ranges,
message: format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"),
})
} else {
KclError::Semantic(KclErrorDetails {
source_ranges,
message: "This function expects an unlabeled first parameter, but you haven't passed it one."
.to_owned(),
})
});
};
fn_memory.add(
&param.identifier.name,
unlabeled.value.clone(),
(&param.identifier).into(),
)?;
}
}
Ok(fn_memory)
}
pub(crate) async fn call_user_defined_function(
args: Vec<Arg>,
memory: &ProgramMemory,
@ -2315,6 +2368,36 @@ pub(crate) async fn call_user_defined_function(
result.map(|_| fn_memory.return_)
}
pub(crate) async fn call_user_defined_function_kw(
args: crate::std::args::KwArgs,
memory: &ProgramMemory,
function_expression: NodeRef<'_, FunctionExpression>,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<Option<KclValue>, KclError> {
// Create a new environment to execute the function body in so that local
// variables shadow variables in the parent scope. The new environment's
// parent should be the environment of the closure.
let mut body_memory = memory.clone();
let body_env = body_memory.new_env_for_call(memory.current_env);
body_memory.current_env = body_env;
let fn_memory = assign_args_to_params_kw(function_expression, args, body_memory)?;
// Execute the function body using the memory we just created.
let (result, fn_memory) = {
let previous_memory = std::mem::replace(&mut exec_state.memory, fn_memory);
let result = ctx
.inner_execute(&function_expression.body, exec_state, BodyType::Block)
.await;
// Restore the previous memory.
let fn_memory = std::mem::replace(&mut exec_state.memory, previous_memory);
(result, fn_memory)
};
result.map(|_| fn_memory.return_)
}
pub enum StatementKind<'a> {
Declaration { name: &'a str },
Expression,

View File

@ -1544,3 +1544,45 @@ mod tag_proxied_through_function_does_not_define_var {
super::execute(TEST_NAME, false).await
}
}
mod kw_fn_too_few_args {
const TEST_NAME: &str = "kw_fn_too_few_args";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod kw_fn_unlabeled_but_has_label {
const TEST_NAME: &str = "kw_fn_unlabeled_but_has_label";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}

View File

@ -50,6 +50,13 @@ pub struct KwArgs {
pub labeled: HashMap<String, Arg>,
}
impl KwArgs {
/// How many arguments are there?
pub fn len(&self) -> usize {
self.labeled.len() + if self.unlabeled.is_some() { 1 } else { 0 }
}
}
#[derive(Debug, Clone)]
pub struct Args {
/// Positional args.

View File

@ -78,46 +78,179 @@ snapshot_kind: text
},
{
"declaration": {
"end": 55,
"end": 77,
"id": {
"end": 40,
"end": 43,
"name": "add",
"start": 40,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 75,
"left": {
"end": 67,
"name": "x",
"start": 66,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 75,
"name": "delta",
"start": 70,
"type": "Identifier",
"type": "Identifier"
},
"start": 66,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 75,
"start": 59,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 77,
"start": 55
},
"end": 77,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 46,
"name": "x",
"start": 45,
"type": "Identifier"
},
"labeled": false
},
{
"type": "Parameter",
"identifier": {
"end": 53,
"name": "delta",
"start": 48,
"type": "Identifier"
}
}
],
"start": 43,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 40,
"type": "VariableDeclarator"
},
"end": 77,
"kind": "fn",
"start": 37,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 97,
"id": {
"end": 82,
"name": "two",
"start": 37,
"start": 79,
"type": "Identifier"
},
"init": {
"arguments": [
{
"end": 54,
"end": 96,
"raw": "1",
"start": 53,
"start": 95,
"type": "Literal",
"type": "Literal",
"value": 1.0
}
],
"callee": {
"end": 52,
"end": 94,
"name": "increment",
"start": 43,
"start": 85,
"type": "Identifier"
},
"end": 55,
"start": 43,
"end": 97,
"start": 85,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 37,
"start": 79,
"type": "VariableDeclarator"
},
"end": 55,
"end": 97,
"kind": "const",
"start": 37,
"start": 79,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 122,
"id": {
"end": 103,
"name": "three",
"start": 98,
"type": "Identifier"
},
"init": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "delta"
},
"arg": {
"end": 121,
"raw": "2",
"start": 120,
"type": "Literal",
"type": "Literal",
"value": 2.0
}
}
],
"callee": {
"end": 109,
"name": "add",
"start": 106,
"type": "Identifier"
},
"end": 122,
"start": 106,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"end": 111,
"raw": "1",
"start": 110,
"type": "Literal",
"type": "Literal",
"value": 1.0
}
},
"start": 98,
"type": "VariableDeclarator"
},
"end": 122,
"kind": "const",
"start": 98,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 56,
"end": 123,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
@ -129,6 +262,16 @@ snapshot_kind: text
"type": "newLine"
}
}
],
"1": [
{
"end": 79,
"start": 77,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []

View File

@ -2,4 +2,9 @@ fn increment(@x) {
return x + 1
}
fn add(@x, delta) {
return x + delta
}
two = increment(1)
three = add(1, delta: 2)

View File

@ -27,6 +27,202 @@ snapshot_kind: text
"value": 0.0,
"__meta": []
},
"add": {
"type": "Function",
"expression": {
"body": {
"body": [
{
"argument": {
"end": 75,
"left": {
"end": 67,
"name": "x",
"start": 66,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 75,
"name": "delta",
"start": 70,
"type": "Identifier",
"type": "Identifier"
},
"start": 66,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 75,
"start": 59,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 77,
"start": 55
},
"end": 77,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 46,
"name": "x",
"start": 45,
"type": "Identifier"
},
"labeled": false
},
{
"type": "Parameter",
"identifier": {
"end": 53,
"name": "delta",
"start": 48,
"type": "Identifier"
}
}
],
"start": 43,
"type": "FunctionExpression"
},
"memory": {
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
},
"increment": {
"type": "Function",
"expression": {
"body": {
"body": [
{
"argument": {
"end": 33,
"left": {
"end": 29,
"name": "x",
"start": 28,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 33,
"raw": "1",
"start": 32,
"type": "Literal",
"type": "Literal",
"value": 1.0
},
"start": 28,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 33,
"start": 21,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 35,
"start": 17
},
"end": 35,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 15,
"name": "x",
"start": 14,
"type": "Identifier"
},
"labeled": false
}
],
"start": 12,
"type": "FunctionExpression"
},
"memory": {
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
},
"__meta": [
{
"sourceRange": [
12,
35,
0
]
}
]
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
},
"__meta": [
{
"sourceRange": [
43,
77,
0
]
}
]
},
"increment": {
"type": "Function",
"expression": {
@ -121,14 +317,34 @@ snapshot_kind: text
}
]
},
"three": {
"type": "Number",
"value": 3.0,
"__meta": [
{
"sourceRange": [
110,
111,
0
]
},
{
"sourceRange": [
120,
121,
0
]
}
]
},
"two": {
"type": "Number",
"value": 2.0,
"__meta": [
{
"sourceRange": [
53,
54,
95,
96,
0
]
},

View File

@ -0,0 +1,153 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing kw_fn_too_few_args.kcl
snapshot_kind: text
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 31,
"id": {
"end": 6,
"name": "add",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 29,
"left": {
"end": 25,
"name": "x",
"start": 24,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 29,
"name": "y",
"start": 28,
"type": "Identifier",
"type": "Identifier"
},
"start": 24,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 29,
"start": 17,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 31,
"start": 13
},
"end": 31,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 8,
"name": "x",
"start": 7,
"type": "Identifier"
}
},
{
"type": "Parameter",
"identifier": {
"end": 11,
"name": "y",
"start": 10,
"type": "Identifier"
}
}
],
"start": 6,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 31,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 50,
"id": {
"end": 38,
"name": "three",
"start": 33,
"type": "Identifier"
},
"init": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "x"
},
"arg": {
"end": 49,
"raw": "1",
"start": 48,
"type": "Literal",
"type": "Literal",
"value": 1.0
}
}
],
"callee": {
"end": 44,
"name": "add",
"start": 41,
"type": "Identifier"
},
"end": 50,
"start": 41,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
"start": 33,
"type": "VariableDeclarator"
},
"end": 50,
"kind": "const",
"start": 33,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 51,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 33,
"start": 31,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -0,0 +1,17 @@
---
source: kcl/src/simulation_tests.rs
description: Error from executing kw_fn_too_few_args.kcl
snapshot_kind: text
---
KCL Semantic error
× semantic: This function requires a parameter y, but you haven't passed
│ it one.
╭─[1:7]
1 │ ╭─▶ fn add(x, y) {
2 │ │ return x + y
3 │ ╰─▶ }
4 │
5 │ three = add(x: 1)
· ─────────
╰────

View File

@ -0,0 +1,5 @@
fn add(x, y) {
return x + y
}
three = add(x: 1)

View File

@ -0,0 +1,146 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing kw_fn_unlabeled_but_has_label.kcl
snapshot_kind: text
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 29,
"id": {
"end": 6,
"name": "add",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 27,
"left": {
"end": 23,
"name": "x",
"start": 22,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 27,
"raw": "1",
"start": 26,
"type": "Literal",
"type": "Literal",
"value": 1.0
},
"start": 22,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 27,
"start": 15,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 29,
"start": 11
},
"end": 29,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 9,
"name": "x",
"start": 8,
"type": "Identifier"
},
"labeled": false
}
],
"start": 6,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 29,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 46,
"id": {
"end": 34,
"name": "two",
"start": 31,
"type": "Identifier"
},
"init": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "x"
},
"arg": {
"end": 45,
"raw": "1",
"start": 44,
"type": "Literal",
"type": "Literal",
"value": 1.0
}
}
],
"callee": {
"end": 40,
"name": "add",
"start": 37,
"type": "Identifier"
},
"end": 46,
"start": 37,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
"start": 31,
"type": "VariableDeclarator"
},
"end": 46,
"kind": "const",
"start": 31,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 47,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 31,
"start": 29,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -0,0 +1,17 @@
---
source: kcl/src/simulation_tests.rs
description: Error from executing kw_fn_unlabeled_but_has_label.kcl
snapshot_kind: text
---
KCL Semantic error
× semantic: The function does declare a parameter named 'x', but this
│ parameter doesn't use a label. Try removing the `x:`
╭─[1:7]
1 │ ╭─▶ fn add(@x) {
2 │ │ return x + 1
3 │ ╰─▶ }
4 │
5 │ two = add(x: 1)
· ─────────
╰────

View File

@ -0,0 +1,5 @@
fn add(@x) {
return x + 1
}
two = add(x: 1)