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
This commit is contained in:
@ -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<EpBinding>),
|
||||
/// A sequence of KCL values, indexed by their identifier.
|
||||
Map(HashMap<String, EpBinding>),
|
||||
/// Not associated with a KCEP address.
|
||||
Function(KclFunction),
|
||||
}
|
||||
|
||||
impl From<KclFunction> 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<String, EpBinding>,
|
||||
/// KCL functions. They do NOT get stored in EP memory.
|
||||
function_bindings: HashMap<String2, Box<dyn KclFunction>>,
|
||||
parent: Option<Box<BindingScope>>,
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ pub enum SingleValue {
|
||||
UnaryExpression(Box<ast::types::UnaryExpression>),
|
||||
KclNoneExpression(ast::types::KclNone),
|
||||
MemberExpression(Box<ast::types::MemberExpression>),
|
||||
FunctionExpression(Box<ast::types::FunctionExpression>),
|
||||
}
|
||||
|
||||
impl From<ast::types::BinaryPart> for KclValueGroup {
|
||||
@ -59,7 +60,8 @@ impl From<ast::types::Value> 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<KclValueGroup> 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),
|
||||
|
||||
@ -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<Instruction>, Option<EpBinding>), 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<Vec<Instruction>, 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<EpBinding>) -> Result<EvalPlan, CompileError>;
|
||||
}
|
||||
|
||||
/// 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<ast::types::Parameter>,
|
||||
params_required: Vec<ast::types::Parameter>,
|
||||
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),
|
||||
}
|
||||
|
||||
@ -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<EpBinding>) -> Result<EvalPlan, CompileError>;
|
||||
}
|
||||
|
||||
impl Callable for Id {
|
||||
fn call(&self, _: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
|
||||
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<EpBinding>) -> Result<EvalPlan, CompileError> {
|
||||
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<EpBinding>) -> Result<EvalPlan, CompileError> {
|
||||
let len = args.len();
|
||||
if len > 2 {
|
||||
|
||||
@ -7,7 +7,7 @@ fn must_plan(program: &str) -> (Vec<Instruction>, 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(
|
||||
|
||||
@ -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<Parameter>,
|
||||
pub params_optional: Vec<Parameter>,
|
||||
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<FunctionExpressionParts, RequiredParamAfterOptionalParam> {
|
||||
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] {
|
||||
|
||||
Reference in New Issue
Block a user