From 4903f6b9fca131f3d5f9821563e9c47e4928d067 Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Tue, 23 Jan 2024 11:30:00 +1100 Subject: [PATCH] Grackle: compile and execute user-defined KCL functions (#1306) * Grackle: compile KCL function definitions Definitions like `fn x = () => { return 1 }` can now be compiled. These functions can't be _called_ yet, but just defining them and mapping them to names works now. * Failing test for executing a user-defined function * Refactor: KclFunction is now an enum, not a trait It's a pain in the ass to work with trait objects in Rust, so I'm refactoring to avoid needing traits at all. We can just use enums. This simplifies future work. * Zero-parameter functions can be called Finally, Grackle can actually run user-defined KCL functions! It basically treats them as a new, separate program (with its own scope of variables, nested within the existing parent scope). * Failing test for multi-param KCL functions * Execute user-defined functions which declare parameters Previous commits in this PR got user-defined functions working, but only if they had zero parameters. In this commit, call arguments are bound to function parameters, so you can now compile functions with params. * Users get a compile error if they try to pass more args to a function than it has parameters This will help users get clear error messages. * More test coverage Among other things, this verify that Grackle compiles KCL functions which themselves either return or accept functions --- src/wasm-lib/grackle/src/binding_scope.rs | 65 ++++-- src/wasm-lib/grackle/src/kcl_value_group.rs | 5 +- src/wasm-lib/grackle/src/lib.rs | 162 ++++++++++++-- src/wasm-lib/grackle/src/native_functions.rs | 21 +- src/wasm-lib/grackle/src/tests.rs | 218 ++++++++++++++++++- src/wasm-lib/kcl/src/ast/types.rs | 49 ++++- 6 files changed, 464 insertions(+), 56 deletions(-) diff --git a/src/wasm-lib/grackle/src/binding_scope.rs b/src/wasm-lib/grackle/src/binding_scope.rs index 03d883f6a..b8f108ce5 100644 --- a/src/wasm-lib/grackle/src/binding_scope.rs +++ b/src/wasm-lib/grackle/src/binding_scope.rs @@ -2,11 +2,10 @@ use kcl_lib::ast::types::LiteralIdentifier; use kcl_lib::ast::types::LiteralValue; use crate::CompileError; +use crate::KclFunction; use super::native_functions; use super::Address; -use super::KclFunction; -use super::String2; use std::collections::HashMap; @@ -21,6 +20,14 @@ pub enum EpBinding { Sequence(Vec), /// A sequence of KCL values, indexed by their identifier. Map(HashMap), + /// Not associated with a KCEP address. + Function(KclFunction), +} + +impl From for EpBinding { + fn from(f: KclFunction) -> Self { + Self::Function(f) + } } impl EpBinding { @@ -31,16 +38,18 @@ impl EpBinding { LiteralIdentifier::Literal(litval) => match litval.value { // Arrays can be indexed by integers. LiteralValue::IInteger(i) => match self { - EpBinding::Single(_) => Err(CompileError::CannotIndex), EpBinding::Sequence(seq) => { let i = usize::try_from(i).map_err(|_| CompileError::InvalidIndex(i.to_string()))?; seq.get(i).ok_or(CompileError::IndexOutOfBounds { i, len: seq.len() }) } EpBinding::Map(_) => Err(CompileError::CannotIndex), + EpBinding::Single(_) => Err(CompileError::CannotIndex), + EpBinding::Function(_) => Err(CompileError::CannotIndex), }, // Objects can be indexed by string properties. LiteralValue::String(property) => match self { EpBinding::Single(_) => Err(CompileError::NoProperties), + EpBinding::Function(_) => Err(CompileError::NoProperties), EpBinding::Sequence(_) => Err(CompileError::ArrayDoesNotHaveProperties), EpBinding::Map(map) => map.get(&property).ok_or(CompileError::UndefinedProperty { property }), }, @@ -67,7 +76,6 @@ pub struct BindingScope { // KCL value which are stored in EP memory. ep_bindings: HashMap, /// KCL functions. They do NOT get stored in EP memory. - function_bindings: HashMap>, parent: Option>, } @@ -80,29 +88,39 @@ impl BindingScope { Self { // TODO: Actually put the stdlib prelude in here, // things like `startSketchAt` and `line`. - function_bindings: HashMap::from([ - ("id".into(), Box::new(native_functions::Id) as _), - ("add".into(), Box::new(native_functions::Add) as _), + ep_bindings: HashMap::from([ + ("id".into(), EpBinding::from(KclFunction::Id(native_functions::Id))), + ("add".into(), EpBinding::from(KclFunction::Add(native_functions::Add))), + ( + "startSketchAt".into(), + EpBinding::from(KclFunction::StartSketchAt(native_functions::StartSketchAt)), + ), ]), - ep_bindings: Default::default(), parent: None, } } /// Add a new scope, e.g. for new function calls. - #[allow(dead_code)] // TODO: when we implement function expressions. - pub fn add_scope(self) -> Self { - Self { - function_bindings: Default::default(), - ep_bindings: Default::default(), - parent: Some(Box::new(self)), - } + pub fn add_scope(&mut self) { + // Move all data from `self` into `this`. + let this_parent = self.parent.take(); + let this_ep_bindings = self.ep_bindings.drain().collect(); + let this = Self { + ep_bindings: this_ep_bindings, + parent: this_parent, + }; + // Turn `self` into a new scope, with the old `self` as its parent. + self.parent = Some(Box::new(this)); } //// Remove a scope, e.g. when exiting a function call. - #[allow(dead_code)] // TODO: when we implement function expressions. - pub fn remove_scope(self) -> Self { - *self.parent.unwrap() + pub fn remove_scope(&mut self) { + // The scope is finished, so erase all its local variables. + self.ep_bindings.clear(); + // Pop the stack -- the parent scope is now the current scope. + let p = self.parent.take().expect("cannot remove the root scope"); + self.parent = p.parent; + self.ep_bindings = p.ep_bindings; } /// Add a binding (e.g. defining a new variable) @@ -126,10 +144,11 @@ impl BindingScope { /// Look up a function bound to the given identifier. pub fn get_fn(&self, identifier: &str) -> GetFnResult { - if let Some(f) = self.function_bindings.get(identifier) { - GetFnResult::Found(f.as_ref()) - } else if self.get(identifier).is_some() { - GetFnResult::NonCallable + if let Some(x) = self.get(identifier) { + match x { + EpBinding::Function(f) => GetFnResult::Found(f), + _ => GetFnResult::NonCallable, + } } else if let Some(ref parent) = self.parent { parent.get_fn(identifier) } else { @@ -139,7 +158,7 @@ impl BindingScope { } pub enum GetFnResult<'a> { - Found(&'a dyn KclFunction), + Found(&'a KclFunction), NonCallable, NotFound, } diff --git a/src/wasm-lib/grackle/src/kcl_value_group.rs b/src/wasm-lib/grackle/src/kcl_value_group.rs index 480a4e867..820c68d06 100644 --- a/src/wasm-lib/grackle/src/kcl_value_group.rs +++ b/src/wasm-lib/grackle/src/kcl_value_group.rs @@ -18,6 +18,7 @@ pub enum SingleValue { UnaryExpression(Box), KclNoneExpression(ast::types::KclNone), MemberExpression(Box), + FunctionExpression(Box), } impl From for KclValueGroup { @@ -59,7 +60,8 @@ impl From for KclValueGroup { ast::types::Value::ArrayExpression(e) => Self::ArrayExpression(e), ast::types::Value::ObjectExpression(e) => Self::ObjectExpression(e), ast::types::Value::MemberExpression(e) => Self::Single(SingleValue::MemberExpression(e)), - ast::types::Value::PipeSubstitution(_) | ast::types::Value::FunctionExpression(_) => todo!(), + ast::types::Value::FunctionExpression(e) => Self::Single(SingleValue::FunctionExpression(e)), + ast::types::Value::PipeSubstitution(_) => todo!(), } } } @@ -76,6 +78,7 @@ impl From for ast::types::Value { SingleValue::UnaryExpression(e) => ast::types::Value::UnaryExpression(e), SingleValue::KclNoneExpression(e) => ast::types::Value::None(e), SingleValue::MemberExpression(e) => ast::types::Value::MemberExpression(e), + SingleValue::FunctionExpression(e) => ast::types::Value::FunctionExpression(e), }, KclValueGroup::ArrayExpression(e) => ast::types::Value::ArrayExpression(e), KclValueGroup::ObjectExpression(e) => ast::types::Value::ObjectExpression(e), diff --git a/src/wasm-lib/grackle/src/lib.rs b/src/wasm-lib/grackle/src/lib.rs index 13a87245c..a5389f3bf 100644 --- a/src/wasm-lib/grackle/src/lib.rs +++ b/src/wasm-lib/grackle/src/lib.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use kcl_lib::{ ast, - ast::types::{BodyItem, KclNone, LiteralValue, Program}, + ast::types::{BodyItem, FunctionExpressionParts, KclNone, LiteralValue, Program, RequiredParamAfterOptionalParam}, }; use kittycad_execution_plan as ep; use kittycad_execution_plan::{Address, ExecutionError, Instruction}; @@ -22,7 +22,7 @@ use self::kcl_value_group::{KclValueGroup, SingleValue}; /// Execute a KCL program by compiling into an execution plan, then running that. pub async fn execute(ast: Program, session: Session) -> Result<(), Error> { let mut planner = Planner::new(); - let plan = planner.build_plan(ast)?; + let (plan, _retval) = planner.build_plan(ast)?; let mut mem = kittycad_execution_plan::Memory::default(); kittycad_execution_plan::execute(&mut mem, plan, session).await?; Ok(()) @@ -44,20 +44,36 @@ impl Planner { } } - fn build_plan(&mut self, program: Program) -> PlanRes { - program.body.into_iter().try_fold(Vec::new(), |mut instructions, item| { - let instructions_for_this_node = match item { - BodyItem::ExpressionStatement(node) => match KclValueGroup::from(node.expression) { - KclValueGroup::Single(value) => self.plan_to_compute_single(value)?.instructions, - KclValueGroup::ArrayExpression(_) => todo!(), - KclValueGroup::ObjectExpression(_) => todo!(), - }, - BodyItem::VariableDeclaration(node) => self.plan_to_bind(node)?, - BodyItem::ReturnStatement(_) => todo!(), - }; - instructions.extend(instructions_for_this_node); - Ok(instructions) - }) + /// If successful, return the KCEP instructions for executing the given program. + /// If the program is a function with a return, then it also returns the KCL function's return value. + fn build_plan(&mut self, program: Program) -> Result<(Vec, Option), CompileError> { + program + .body + .into_iter() + .try_fold((Vec::new(), None), |(mut instructions, mut retval), item| { + if retval.is_some() { + return Err(CompileError::MultipleReturns); + } + let instructions_for_this_node = match item { + BodyItem::ExpressionStatement(node) => match KclValueGroup::from(node.expression) { + KclValueGroup::Single(value) => self.plan_to_compute_single(value)?.instructions, + KclValueGroup::ArrayExpression(_) => todo!(), + KclValueGroup::ObjectExpression(_) => todo!(), + }, + BodyItem::VariableDeclaration(node) => self.plan_to_bind(node)?, + BodyItem::ReturnStatement(node) => match KclValueGroup::from(node.argument) { + KclValueGroup::Single(value) => { + let EvalPlan { instructions, binding } = self.plan_to_compute_single(value)?; + retval = Some(binding); + instructions + } + KclValueGroup::ArrayExpression(_) => todo!(), + KclValueGroup::ObjectExpression(_) => todo!(), + }, + }; + instructions.extend(instructions_for_this_node); + Ok((instructions, retval)) + }) } /// Emits instructions which, when run, compute a given KCL value and store it in memory. @@ -74,6 +90,23 @@ impl Planner { binding: EpBinding::Single(address), }) } + SingleValue::FunctionExpression(expr) => { + let FunctionExpressionParts { + start: _, + end: _, + params_required, + params_optional, + body, + } = expr.into_parts().map_err(CompileError::BadParamOrder)?; + Ok(EvalPlan { + instructions: Vec::new(), + binding: EpBinding::from(KclFunction::UserDefined(UserDefinedFunction { + params_optional, + params_required, + body, + })), + }) + } SingleValue::Literal(expr) => { let kcep_val = kcl_literal_to_kcep_literal(expr.value); // KCEP primitives always have size of 1, because each address holds 1 primitive. @@ -142,6 +175,7 @@ impl Planner { }) } SingleValue::CallExpression(expr) => { + // Make a plan to compute all the arguments to this call. let (mut instructions, args) = expr.arguments.into_iter().try_fold( (Vec::new(), Vec::new()), |(mut acc_instrs, mut acc_args), argument| { @@ -158,6 +192,7 @@ impl Planner { Ok((acc_instrs, acc_args)) }, )?; + // Look up the function being called. let callee = match self.binding_scope.get_fn(&expr.callee.name) { GetFnResult::Found(f) => f, GetFnResult::NonCallable => { @@ -172,10 +207,67 @@ impl Planner { } }; + // Emit instructions to call that function with the given arguments. + use native_functions::Callable; let EvalPlan { instructions: eval_instrs, binding, - } = callee.call(&mut self.next_addr, args)?; + } = match callee { + KclFunction::Id(f) => f.call(&mut self.next_addr, args)?, + KclFunction::StartSketchAt(f) => f.call(&mut self.next_addr, args)?, + KclFunction::Add(f) => f.call(&mut self.next_addr, args)?, + KclFunction::UserDefined(f) => { + let UserDefinedFunction { + params_optional, + params_required, + body: function_body, + } = f.clone(); + let num_required_params = params_required.len(); + self.binding_scope.add_scope(); + + // Bind the call's arguments to the names of the function's parameters. + let num_actual_params = args.len(); + let mut arg_iter = args.into_iter(); + let max_params = params_required.len() + params_optional.len(); + if num_actual_params > max_params { + return Err(CompileError::TooManyArgs { + fn_name: "".into(), + maximum: max_params, + actual: num_actual_params, + }); + } + + // Bind required parameters + for param in params_required { + let arg = arg_iter.next().ok_or(CompileError::NotEnoughArgs { + fn_name: "".into(), + required: num_required_params, + actual: num_actual_params, + })?; + self.binding_scope.bind(param.identifier.name, arg); + } + + // Bind optional parameters + for param in params_optional { + let Some(arg) = arg_iter.next() else { + break; + }; + self.binding_scope.bind(param.identifier.name, arg); + } + + let (instructions, retval) = self.build_plan(function_body)?; + let Some(retval) = retval else { + return Err(CompileError::NoReturnStmt); + }; + self.binding_scope.remove_scope(); + EvalPlan { + instructions, + binding: retval, + } + } + }; + + // Combine the "evaluate arguments" plan with the "call function" plan. instructions.extend(eval_instrs); Ok(EvalPlan { instructions, binding }) } @@ -387,6 +479,12 @@ pub enum CompileError { "you tried to read the '.{property}' of an object, but the object doesn't have any properties with that key" )] UndefinedProperty { property: String }, + #[error("{0}")] + BadParamOrder(RequiredParamAfterOptionalParam), + #[error("A KCL function cannot have anything after its return value")] + MultipleReturns, + #[error("A KCL function must end with a return statement, but your function doesn't have one.")] + NoReturnStmt, } #[derive(Debug, thiserror::Error)] @@ -397,8 +495,6 @@ pub enum Error { Execution(#[from] ExecutionError), } -type PlanRes = Result, CompileError>; - /// Every KCL literal value is equivalent to an Execution Plan value, and therefore can be /// bound to some KCL name and Execution Plan address. fn kcl_literal_to_kcep_literal(expr: LiteralValue) -> ept::Primitive { @@ -417,9 +513,29 @@ struct EvalPlan { binding: EpBinding, } -trait KclFunction: std::fmt::Debug { - fn call(&self, next_addr: &mut Address, args: Vec) -> Result; -} - /// Either an owned string, or a static string. Either way it can be read and moved around. pub type String2 = std::borrow::Cow<'static, str>; + +#[derive(Debug, Clone)] +struct UserDefinedFunction { + params_optional: Vec, + params_required: Vec, + body: ast::types::Program, +} + +impl PartialEq for UserDefinedFunction { + fn eq(&self, other: &Self) -> bool { + self.params_optional == other.params_optional && self.params_required == other.params_required + } +} + +impl Eq for UserDefinedFunction {} + +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] +enum KclFunction { + Id(native_functions::Id), + StartSketchAt(native_functions::StartSketchAt), + Add(native_functions::Add), + UserDefined(UserDefinedFunction), +} diff --git a/src/wasm-lib/grackle/src/native_functions.rs b/src/wasm-lib/grackle/src/native_functions.rs index cf86b45ce..44328f81d 100644 --- a/src/wasm-lib/grackle/src/native_functions.rs +++ b/src/wasm-lib/grackle/src/native_functions.rs @@ -6,13 +6,18 @@ use kcl_lib::std::sketch::PlaneData; use kittycad_execution_plan::{Address, Arithmetic, Instruction}; use kittycad_execution_plan_traits::Value; -use crate::{CompileError, EpBinding, EvalPlan, KclFunction}; +use crate::{CompileError, EpBinding, EvalPlan}; /// The identity function. Always returns its first input. -#[derive(Debug)] +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] pub struct Id; -impl KclFunction for Id { +pub trait Callable { + fn call(&self, next_addr: &mut Address, args: Vec) -> Result; +} + +impl Callable for Id { fn call(&self, _: &mut Address, args: Vec) -> Result { if args.len() > 1 { return Err(CompileError::TooManyArgs { @@ -36,10 +41,11 @@ impl KclFunction for Id { } } -#[derive(Debug)] +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] pub struct StartSketchAt; -impl KclFunction for StartSketchAt { +impl Callable for StartSketchAt { fn call(&self, next_addr: &mut Address, _args: Vec) -> Result { let mut instructions = Vec::new(); // Store the plane. @@ -64,10 +70,11 @@ impl KclFunction for StartSketchAt { } /// A test function that adds two numbers. -#[derive(Debug)] +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(Eq, PartialEq))] pub struct Add; -impl KclFunction for Add { +impl Callable for Add { fn call(&self, next_address: &mut Address, mut args: Vec) -> Result { let len = args.len(); if len > 2 { diff --git a/src/wasm-lib/grackle/src/tests.rs b/src/wasm-lib/grackle/src/tests.rs index 2116b49c9..f5515222f 100644 --- a/src/wasm-lib/grackle/src/tests.rs +++ b/src/wasm-lib/grackle/src/tests.rs @@ -7,7 +7,7 @@ fn must_plan(program: &str) -> (Vec, BindingScope) { let parser = kcl_lib::parser::Parser::new(tokens); let ast = parser.ast().unwrap(); let mut p = Planner::new(); - let instrs = p.build_plan(ast).unwrap(); + let (instrs, _) = p.build_plan(ast).unwrap(); (instrs, p.binding_scope) } @@ -109,6 +109,17 @@ fn bind_arrays_with_objects_elements() { ); } +#[test] +fn statement_after_return() { + let program = "fn f = () => { + return 1 + let x = 2 + } + f()"; + let err = should_not_compile(program); + assert_eq!(err, CompileError::MultipleReturns); +} + #[test] fn name_not_found() { // Users can't assign `y` to anything because `y` is undefined. @@ -361,6 +372,211 @@ fn composite_binary_exprs() { ); } +#[test] +fn use_kcl_functions_zero_params() { + let (plan, scope) = must_plan( + "fn triple = () => { return 123 } + let x = triple()", + ); + assert_eq!( + plan, + vec![Instruction::SetPrimitive { + address: Address::ZERO, + value: 123i64.into() + }] + ); + match scope.get("x").unwrap() { + EpBinding::Single(addr) => { + assert_eq!(addr, &Address::ZERO); + } + other => { + panic!("expected 'x' bound to an address but it was bound to {other:?}"); + } + } +} + +#[test] +fn use_kcl_functions_with_optional_params() { + for (i, program) in ["fn triple = (x, y?) => { return x*3 } + let x = triple(1, 888)"] + .into_iter() + .enumerate() + { + let (plan, scope) = must_plan(program); + let destination = Address::ZERO + 3; + assert_eq!( + plan, + vec![ + Instruction::SetPrimitive { + address: Address::ZERO, + value: 1i64.into(), + }, + Instruction::SetPrimitive { + address: Address::ZERO + 1, + value: 888i64.into(), + }, + Instruction::SetPrimitive { + address: Address::ZERO + 2, + value: 3i64.into(), + }, + Instruction::Arithmetic { + arithmetic: ep::Arithmetic { + operation: ep::Operation::Mul, + operand0: ep::Operand::Reference(Address::ZERO), + operand1: ep::Operand::Reference(Address::ZERO + 2) + }, + destination, + } + ], + "failed test {i}" + ); + match scope.get("x").unwrap() { + EpBinding::Single(addr) => { + assert_eq!(addr, &destination, "failed test {i}"); + } + other => { + panic!("expected 'x' bound to an address but it was bound to {other:?}, so failed test {i}"); + } + } + } +} + +#[test] +fn use_kcl_functions_with_too_many_params() { + let program = "fn triple = (x, y?) => { return x*3 } + let x = triple(1, 2, 3)"; + let err = should_not_compile(program); + assert!(matches!( + err, + CompileError::TooManyArgs { + maximum: 2, + actual: 3, + .. + } + )) +} + +#[test] +fn use_kcl_function_as_return_value() { + let program = "fn twotwotwo = () => { + return () => { return 222 } + } + let f = twotwotwo() + let x = f()"; + let (plan, scope) = must_plan(program); + match scope.get("x").unwrap() { + EpBinding::Single(addr) => { + assert_eq!(addr, &Address::ZERO); + } + other => { + panic!("expected 'x' bound to an address but it was bound to {other:?}, so failed test"); + } + } + assert_eq!( + plan, + vec![Instruction::SetPrimitive { + address: Address::ZERO, + value: 222i64.into() + }] + ) +} + +#[test] +fn define_recursive_function() { + let program = "fn add_infinitely = (i) => { + return add_infinitely(i+1) + }"; + let (plan, _scope) = must_plan(program); + assert_eq!(plan, Vec::new()) +} +#[test] +fn use_kcl_function_as_param() { + let program = "fn wrapper = (f) => { + return f() + } + fn twotwotwo = () => { + return 222 + } + let x = wrapper(twotwotwo)"; + let (plan, scope) = must_plan(program); + match scope.get("x").unwrap() { + EpBinding::Single(addr) => { + assert_eq!(addr, &Address::ZERO); + } + other => { + panic!("expected 'x' bound to an address but it was bound to {other:?}, so failed test"); + } + } + assert_eq!( + plan, + vec![Instruction::SetPrimitive { + address: Address::ZERO, + value: 222i64.into() + }] + ) +} + +#[test] +fn use_kcl_functions_with_params() { + for (i, program) in [ + "fn triple = (x) => { return x*3 } + let x = triple(1)", + "fn triple = (x,y?) => { return x*3 } + let x = triple(1)", + ] + .into_iter() + .enumerate() + { + let (plan, scope) = must_plan(program); + let destination = Address::ZERO + 2; + assert_eq!( + plan, + vec![ + Instruction::SetPrimitive { + address: Address::ZERO, + value: 1i64.into(), + }, + Instruction::SetPrimitive { + address: Address::ZERO + 1, + value: 3i64.into(), + }, + Instruction::Arithmetic { + arithmetic: ep::Arithmetic { + operation: ep::Operation::Mul, + operand0: ep::Operand::Reference(Address::ZERO), + operand1: ep::Operand::Reference(Address::ZERO.offset(1)) + }, + destination, + } + ], + "failed test {i}" + ); + match scope.get("x").unwrap() { + EpBinding::Single(addr) => { + assert_eq!(addr, &destination, "failed test {i}"); + } + other => { + panic!("expected 'x' bound to an address but it was bound to {other:?}, so failed test {i}"); + } + } + } +} + +#[test] +fn define_kcl_functions() { + let (plan, scope) = must_plan("fn triple = (x) => { return x * 3 }"); + assert!(plan.is_empty()); + match scope.get("triple").unwrap() { + EpBinding::Function(KclFunction::UserDefined(expr)) => { + assert!(expr.params_optional.is_empty()); + assert_eq!(expr.params_required.len(), 1); + } + other => { + panic!("expected 'triple' bound to a user-defined KCL function but it was bound to {other:?}"); + } + } +} + #[test] fn aliases_dont_affect_plans() { let (plan1, _) = must_plan( diff --git a/src/wasm-lib/kcl/src/ast/types.rs b/src/wasm-lib/kcl/src/ast/types.rs index 40b4cc8cf..641404a63 100644 --- a/src/wasm-lib/kcl/src/ast/types.rs +++ b/src/wasm-lib/kcl/src/ast/types.rs @@ -2631,7 +2631,7 @@ async fn execute_pipe_body( } /// Parameter of a KCL function. -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema, Bake)] #[databake(path = kcl_lib::ast::types)] #[ts(export)] #[serde(tag = "type")] @@ -2655,6 +2655,23 @@ pub struct FunctionExpression { impl_value_meta!(FunctionExpression); +pub struct FunctionExpressionParts { + pub start: usize, + pub end: usize, + pub params_required: Vec, + pub params_optional: Vec, + pub body: Program, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct RequiredParamAfterOptionalParam(pub Parameter); + +impl std::fmt::Display for RequiredParamAfterOptionalParam { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "KCL functions must declare any optional parameters after all the required parameters. But your required parameter {} is _after_ an optional parameter. You must move it to before the optional parameters instead.", self.0.identifier.name) + } +} + impl FunctionExpression { /// Function expressions don't really apply. pub fn get_constraint_level(&self) -> ConstraintLevel { @@ -2663,6 +2680,36 @@ impl FunctionExpression { } } + pub fn into_parts(self) -> Result { + let Self { + start, + end, + params, + body, + } = self; + let mut params_required = Vec::with_capacity(params.len()); + let mut params_optional = Vec::with_capacity(params.len()); + for param in params { + if param.optional { + params_optional.push(param); + } else { + if !params_optional.is_empty() { + return Err(RequiredParamAfterOptionalParam(param)); + } + params_required.push(param); + } + } + params_required.shrink_to_fit(); + params_optional.shrink_to_fit(); + Ok(FunctionExpressionParts { + start, + end, + params_required, + params_optional, + body, + }) + } + /// Required parameters must be declared before optional parameters. /// This gets all the required parameters. pub fn required_params(&self) -> &[Parameter] {