Bind all unary, binary and constants to KCL (#1781)
This commit is contained in:
@ -2,13 +2,16 @@ use std::collections::HashMap;
|
||||
|
||||
use kcl_lib::ast::types::{LiteralIdentifier, LiteralValue};
|
||||
|
||||
use kittycad_execution_plan::constants;
|
||||
use kittycad_execution_plan_traits::Primitive;
|
||||
|
||||
use super::{native_functions, Address};
|
||||
use crate::{CompileError, KclFunction};
|
||||
|
||||
/// KCL values which can be written to KCEP memory.
|
||||
/// This is recursive. For example, the bound value might be an array, which itself contains bound values.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub enum EpBinding {
|
||||
/// A KCL value which gets stored in a particular address in KCEP memory.
|
||||
Single(Address),
|
||||
@ -23,6 +26,8 @@ pub enum EpBinding {
|
||||
properties: HashMap<String, EpBinding>,
|
||||
},
|
||||
/// Not associated with a KCEP address.
|
||||
Constant(Primitive),
|
||||
/// Not associated with a KCEP address.
|
||||
Function(KclFunction),
|
||||
/// SketchGroups have their own storage.
|
||||
SketchGroup { index: usize },
|
||||
@ -52,11 +57,13 @@ impl EpBinding {
|
||||
EpBinding::SketchGroup { .. } => Err(CompileError::CannotIndex),
|
||||
EpBinding::Single(_) => Err(CompileError::CannotIndex),
|
||||
EpBinding::Function(_) => Err(CompileError::CannotIndex),
|
||||
EpBinding::Constant(_) => 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::Constant(_) => Err(CompileError::CannotIndex),
|
||||
EpBinding::SketchGroup { .. } => Err(CompileError::NoProperties),
|
||||
EpBinding::Sequence { .. } => Err(CompileError::ArrayDoesNotHaveProperties),
|
||||
EpBinding::Map {
|
||||
@ -103,8 +110,58 @@ impl BindingScope {
|
||||
// TODO: Actually put the stdlib prelude in here,
|
||||
// things like `startSketchAt` and `line`.
|
||||
ep_bindings: HashMap::from([
|
||||
("E".into(), EpBinding::Constant(constants::E)),
|
||||
("PI".into(), EpBinding::Constant(constants::PI)),
|
||||
("id".into(), EpBinding::from(KclFunction::Id(native_functions::Id))),
|
||||
("abs".into(), EpBinding::from(KclFunction::Abs(native_functions::Abs))),
|
||||
(
|
||||
"acos".into(),
|
||||
EpBinding::from(KclFunction::Acos(native_functions::Acos)),
|
||||
),
|
||||
(
|
||||
"asin".into(),
|
||||
EpBinding::from(KclFunction::Asin(native_functions::Asin)),
|
||||
),
|
||||
(
|
||||
"atan".into(),
|
||||
EpBinding::from(KclFunction::Atan(native_functions::Atan)),
|
||||
),
|
||||
(
|
||||
"ceil".into(),
|
||||
EpBinding::from(KclFunction::Ceil(native_functions::Ceil)),
|
||||
),
|
||||
("cos".into(), EpBinding::from(KclFunction::Cos(native_functions::Cos))),
|
||||
(
|
||||
"floor".into(),
|
||||
EpBinding::from(KclFunction::Floor(native_functions::Floor)),
|
||||
),
|
||||
("ln".into(), EpBinding::from(KclFunction::Ln(native_functions::Ln))),
|
||||
(
|
||||
"log10".into(),
|
||||
EpBinding::from(KclFunction::Log10(native_functions::Log10)),
|
||||
),
|
||||
(
|
||||
"log2".into(),
|
||||
EpBinding::from(KclFunction::Log2(native_functions::Log2)),
|
||||
),
|
||||
("sin".into(), EpBinding::from(KclFunction::Sin(native_functions::Sin))),
|
||||
(
|
||||
"sqrt".into(),
|
||||
EpBinding::from(KclFunction::Sqrt(native_functions::Sqrt)),
|
||||
),
|
||||
("tan".into(), EpBinding::from(KclFunction::Tan(native_functions::Tan))),
|
||||
(
|
||||
"toDegrees".into(),
|
||||
EpBinding::from(KclFunction::ToDegrees(native_functions::ToDegrees)),
|
||||
),
|
||||
(
|
||||
"toRadians".into(),
|
||||
EpBinding::from(KclFunction::ToRadians(native_functions::ToRadians)),
|
||||
),
|
||||
("add".into(), EpBinding::from(KclFunction::Add(native_functions::Add))),
|
||||
("log".into(), EpBinding::from(KclFunction::Log(native_functions::Log))),
|
||||
("max".into(), EpBinding::from(KclFunction::Max(native_functions::Max))),
|
||||
("min".into(), EpBinding::from(KclFunction::Min(native_functions::Min))),
|
||||
(
|
||||
"startSketchAt".into(),
|
||||
EpBinding::from(KclFunction::StartSketchAt(native_functions::sketch::StartSketchAt)),
|
||||
|
||||
@ -259,6 +259,24 @@ impl Planner {
|
||||
binding,
|
||||
} = match callee {
|
||||
KclFunction::Id(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Abs(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Acos(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Asin(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Atan(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Ceil(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Cos(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Floor(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Ln(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Log10(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Log2(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Sin(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Sqrt(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Tan(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::ToDegrees(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::ToRadians(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Log(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Max(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Min(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::StartSketchAt(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::Extrude(f) => f.call(&mut ctx, args)?,
|
||||
KclFunction::LineTo(f) => f.call(&mut ctx, args)?,
|
||||
@ -634,6 +652,21 @@ impl Eq for UserDefinedFunction {}
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
enum KclFunction {
|
||||
Id(native_functions::Id),
|
||||
Abs(native_functions::Abs),
|
||||
Acos(native_functions::Acos),
|
||||
Asin(native_functions::Asin),
|
||||
Atan(native_functions::Atan),
|
||||
Ceil(native_functions::Ceil),
|
||||
Cos(native_functions::Cos),
|
||||
Floor(native_functions::Floor),
|
||||
Ln(native_functions::Ln),
|
||||
Log10(native_functions::Log10),
|
||||
Log2(native_functions::Log2),
|
||||
Sin(native_functions::Sin),
|
||||
Sqrt(native_functions::Sqrt),
|
||||
Tan(native_functions::Tan),
|
||||
ToDegrees(native_functions::ToDegrees),
|
||||
ToRadians(native_functions::ToRadians),
|
||||
StartSketchAt(native_functions::sketch::StartSketchAt),
|
||||
LineTo(native_functions::sketch::LineTo),
|
||||
Line(native_functions::sketch::Line),
|
||||
@ -642,6 +675,9 @@ enum KclFunction {
|
||||
YLineTo(native_functions::sketch::YLineTo),
|
||||
YLine(native_functions::sketch::YLine),
|
||||
Add(native_functions::Add),
|
||||
Log(native_functions::Log),
|
||||
Max(native_functions::Max),
|
||||
Min(native_functions::Min),
|
||||
UserDefined(UserDefinedFunction),
|
||||
Extrude(native_functions::sketch::Extrude),
|
||||
Close(native_functions::sketch::Close),
|
||||
|
||||
@ -2,18 +2,15 @@
|
||||
//! This includes some of the stdlib, e.g. `startSketchAt`.
|
||||
//! But some other stdlib functions will be written in KCL.
|
||||
|
||||
use kittycad_execution_plan::{BinaryArithmetic, Destination, Instruction};
|
||||
use kittycad_execution_plan::{
|
||||
BinaryArithmetic, BinaryOperation, Destination, Instruction, Operand, UnaryArithmetic, UnaryOperation,
|
||||
};
|
||||
use kittycad_execution_plan_traits::Address;
|
||||
|
||||
use crate::{CompileError, EpBinding, EvalPlan};
|
||||
|
||||
pub mod sketch;
|
||||
|
||||
/// The identity function. Always returns its first input.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
pub struct Id;
|
||||
|
||||
pub trait Callable {
|
||||
fn call(&self, ctx: &mut Context<'_>, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError>;
|
||||
}
|
||||
@ -32,6 +29,65 @@ impl<'a> Context<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Unary operator macro to quickly create new bindings.
|
||||
macro_rules! define_unary {
|
||||
() => {};
|
||||
($h:ident$( $r:ident)*) => {
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
pub struct $h;
|
||||
|
||||
impl Callable for $h {
|
||||
fn call(&self, ctx: &mut Context<'_>, mut args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
|
||||
if args.len() > 1 {
|
||||
return Err(CompileError::TooManyArgs {
|
||||
fn_name: "$h".into(),
|
||||
maximum: 1,
|
||||
actual: args.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let not_enough_args = CompileError::NotEnoughArgs {
|
||||
fn_name: "$h".into(),
|
||||
required: 1,
|
||||
actual: args.len(),
|
||||
};
|
||||
|
||||
let EpBinding::Single(arg0) = args.pop().ok_or(not_enough_args.clone())? else {
|
||||
return Err(CompileError::InvalidOperand("A single value binding is expected"));
|
||||
};
|
||||
|
||||
let destination = ctx.next_address.offset_by(1);
|
||||
let instructions = vec![
|
||||
Instruction::UnaryArithmetic {
|
||||
arithmetic: UnaryArithmetic {
|
||||
operation: UnaryOperation::$h,
|
||||
operand: Operand::Reference(arg0)
|
||||
},
|
||||
destination: Destination::Address(destination)
|
||||
}
|
||||
];
|
||||
|
||||
Ok(EvalPlan {
|
||||
instructions,
|
||||
binding: EpBinding::Single(destination),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
define_unary!($($r)*);
|
||||
};
|
||||
}
|
||||
|
||||
define_unary!(Abs Acos Asin Atan Ceil Cos Floor Ln Log10 Log2 Sin Sqrt Tan ToDegrees ToRadians);
|
||||
|
||||
/// The identity function. Always returns its first input.
|
||||
/// Implemented purely on the KCL side so it doesn't need to be in the
|
||||
/// define_unary! macro above.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
pub struct Id;
|
||||
|
||||
impl Callable for Id {
|
||||
fn call(&self, _: &mut Context<'_>, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
|
||||
if args.len() > 1 {
|
||||
@ -56,44 +112,53 @@ impl Callable for Id {
|
||||
}
|
||||
}
|
||||
|
||||
/// A test function that adds two numbers.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
pub struct Add;
|
||||
/// Binary operator macro to quickly create new bindings.
|
||||
macro_rules! define_binary {
|
||||
() => {};
|
||||
($h:ident$( $r:ident)*) => {
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(Eq, PartialEq))]
|
||||
pub struct $h;
|
||||
|
||||
impl Callable for Add {
|
||||
fn call(&self, ctx: &mut Context<'_>, mut args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
|
||||
let len = args.len();
|
||||
if len > 2 {
|
||||
return Err(CompileError::TooManyArgs {
|
||||
fn_name: "add".into(),
|
||||
maximum: 2,
|
||||
actual: len,
|
||||
});
|
||||
impl Callable for $h {
|
||||
fn call(&self, ctx: &mut Context<'_>, mut args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
|
||||
let len = args.len();
|
||||
if len > 2 {
|
||||
return Err(CompileError::TooManyArgs {
|
||||
fn_name: "$h".into(),
|
||||
maximum: 2,
|
||||
actual: len,
|
||||
});
|
||||
}
|
||||
let not_enough_args = CompileError::NotEnoughArgs {
|
||||
fn_name: "$h".into(),
|
||||
required: 2,
|
||||
actual: len,
|
||||
};
|
||||
const ERR: &str = "cannot use composite values (e.g. array) as arguments to $h";
|
||||
let EpBinding::Single(arg1) = args.pop().ok_or(not_enough_args.clone())? else {
|
||||
return Err(CompileError::InvalidOperand(ERR));
|
||||
};
|
||||
let EpBinding::Single(arg0) = args.pop().ok_or(not_enough_args)? else {
|
||||
return Err(CompileError::InvalidOperand(ERR));
|
||||
};
|
||||
let destination = ctx.next_address.offset_by(1);
|
||||
Ok(EvalPlan {
|
||||
instructions: vec![Instruction::BinaryArithmetic {
|
||||
arithmetic: BinaryArithmetic {
|
||||
operation: BinaryOperation::$h,
|
||||
operand0: Operand::Reference(arg0),
|
||||
operand1: Operand::Reference(arg1),
|
||||
},
|
||||
destination: Destination::Address(destination),
|
||||
}],
|
||||
binding: EpBinding::Single(destination),
|
||||
})
|
||||
}
|
||||
let not_enough_args = CompileError::NotEnoughArgs {
|
||||
fn_name: "add".into(),
|
||||
required: 2,
|
||||
actual: len,
|
||||
};
|
||||
const ERR: &str = "cannot use composite values (e.g. array) as arguments to Add";
|
||||
let EpBinding::Single(arg1) = args.pop().ok_or(not_enough_args.clone())? else {
|
||||
return Err(CompileError::InvalidOperand(ERR));
|
||||
};
|
||||
let EpBinding::Single(arg0) = args.pop().ok_or(not_enough_args)? else {
|
||||
return Err(CompileError::InvalidOperand(ERR));
|
||||
};
|
||||
let destination = ctx.next_address.offset_by(1);
|
||||
Ok(EvalPlan {
|
||||
instructions: vec![Instruction::BinaryArithmetic {
|
||||
arithmetic: BinaryArithmetic {
|
||||
operation: kittycad_execution_plan::BinaryOperation::Add,
|
||||
operand0: kittycad_execution_plan::Operand::Reference(arg0),
|
||||
operand1: kittycad_execution_plan::Operand::Reference(arg1),
|
||||
},
|
||||
destination: Destination::Address(destination),
|
||||
}],
|
||||
binding: EpBinding::Single(destination),
|
||||
})
|
||||
}
|
||||
|
||||
define_binary!($($r)*);
|
||||
};
|
||||
}
|
||||
|
||||
define_binary!(Add Log Max Min);
|
||||
|
||||
@ -67,6 +67,12 @@ pub fn sg_binding(
|
||||
actual: "function".to_owned(),
|
||||
arg_number,
|
||||
}),
|
||||
EpBinding::Constant(_) => Err(CompileError::ArgWrongType {
|
||||
fn_name,
|
||||
expected,
|
||||
actual: "constant".to_owned(),
|
||||
arg_number,
|
||||
}),
|
||||
}
|
||||
}
|
||||
pub fn single_binding(
|
||||
@ -101,6 +107,12 @@ pub fn single_binding(
|
||||
actual: "function".to_owned(),
|
||||
arg_number,
|
||||
}),
|
||||
EpBinding::Constant(_) => Err(CompileError::ArgWrongType {
|
||||
fn_name,
|
||||
expected,
|
||||
actual: "constant".to_owned(),
|
||||
arg_number,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,6 +148,12 @@ pub fn sequence_binding(
|
||||
actual: "function".to_owned(),
|
||||
arg_number,
|
||||
}),
|
||||
EpBinding::Constant(_) => Err(CompileError::ArgWrongType {
|
||||
fn_name,
|
||||
expected,
|
||||
actual: "constant".to_owned(),
|
||||
arg_number,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use std::{collections::HashMap, env};
|
||||
|
||||
use ep::{sketch_types, Destination, UnaryArithmetic};
|
||||
use ept::{ListHeader, ObjectHeader};
|
||||
use ep::{constants, sketch_types, Destination, UnaryArithmetic};
|
||||
use ept::{ListHeader, ObjectHeader, Primitive};
|
||||
use kittycad_modeling_cmds::shared::Point2d;
|
||||
use kittycad_modeling_session::SessionBuilder;
|
||||
use pretty_assertions::assert_eq;
|
||||
@ -1414,3 +1414,31 @@ fn mod_and_pow() {
|
||||
]
|
||||
);
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn cos_sin_pi() {
|
||||
let program = "
|
||||
let x = cos(45.0)*10
|
||||
let y = sin(45.0)*10
|
||||
let z = PI
|
||||
";
|
||||
let (_plan, scope, _) = must_plan(program);
|
||||
let Some(EpBinding::Single(x)) = scope.get("x") else {
|
||||
panic!("Unexpected binding for variable 'x': {:?}", scope.get("x"));
|
||||
};
|
||||
let Some(EpBinding::Single(y)) = scope.get("y") else {
|
||||
panic!("Unexpected binding for variable 'y': {:?}", scope.get("y"));
|
||||
};
|
||||
let Some(EpBinding::Constant(z)) = scope.get("z") else {
|
||||
panic!("Unexpected binding for variable 'z': {:?}", scope.get("z"));
|
||||
};
|
||||
let ast = kcl_lib::parser::Parser::new(kcl_lib::token::lexer(program))
|
||||
.ast()
|
||||
.unwrap();
|
||||
let mem = crate::execute(ast, &mut None).await.unwrap();
|
||||
use ept::ReadMemory;
|
||||
assert_eq!(*mem.get(x).unwrap(), Primitive::from(5.253219888177298));
|
||||
assert_eq!(*mem.get(y).unwrap(), Primitive::from(8.509035245341185));
|
||||
|
||||
// Constants don't live in memory.
|
||||
assert_eq!(*z, constants::PI);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user