Grackle (KCL to EP compiler) (#1270)
* Start Grackle (KCL-to-EP compiler) This begins work on a second, different executor. The old executor is a tree-walk interpreter, this executor compiles the KCL programs into the Execution Plan virtual machine defined in its [own crate](https://github.com/KittyCAD/modeling-api/tree/main/execution-plan). This executor is called "Grackle", after an Austin bird, and it's got its own module in wasm-lib so that I can keep merging small PRs and developing incrementally, rather than building a complete executor which replaces the old executor in one PR. Grackle's "Planner" walks the AST, like the tree-walk executor. But it doesn't actually execute code. Instead, as it walks each AST node, it outputs a sequence of Execution Plan instructions which, when run, can compute that node's value. It also notes which Execution Plan virtual machine address will eventually contain each KCL variable. Done: - Storing KCL variables - Computing primitives, literals, binary expressions - Calling native (i.e. Rust) functions from KCL - Storing arrays Todo: - KCL functions (i.e. user-defined functions) - Member expressions - Port over existing executor's native funtions (e.g. `lineTo`, `extrude` and `startSketchAt`)
This commit is contained in:
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime
|
||||
skip: **/target,node_modules,build
|
||||
skip: **/target,node_modules,build,**/Cargo.lock
|
||||
|
1554
src/wasm-lib/Cargo.lock
generated
1554
src/wasm-lib/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,7 @@ image = "0.24.7"
|
||||
kittycad = { workspace = true, default-features = true }
|
||||
pretty_assertions = "1.4.0"
|
||||
reqwest = { version = "0.11.22", default-features = false }
|
||||
tokio = { version = "1.34.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||
tokio = { version = "1.35.1", features = ["rt-multi-thread", "macros", "time"] }
|
||||
twenty-twenty = "0.7"
|
||||
uuid = { version = "1.6.1", features = ["v4", "js", "serde"] }
|
||||
|
||||
@ -52,12 +52,17 @@ debug = true
|
||||
[workspace]
|
||||
members = [
|
||||
"derive-docs",
|
||||
"grackle",
|
||||
"kcl",
|
||||
"kcl-macros",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
kittycad = { version = "0.2.45", default-features = false, features = ["js"] }
|
||||
kittycad-execution-plan = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
||||
kittycad-execution-plan-traits = "0.1.2"
|
||||
kittycad-modeling-session = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
||||
kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
|
||||
|
||||
[[test]]
|
||||
name = "executor"
|
||||
|
18
src/wasm-lib/grackle/Cargo.toml
Normal file
18
src/wasm-lib/grackle/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "grackle"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "A new executor for KCL which compiles to Execution Plans"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
kcl-lib = { path = "../kcl" }
|
||||
kittycad-execution-plan = { workspace = true }
|
||||
kittycad-execution-plan-traits = { workspace = true }
|
||||
kittycad-modeling-session = { workspace = true }
|
||||
thiserror = "1.0.56"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
85
src/wasm-lib/grackle/src/kcl_value_group.rs
Normal file
85
src/wasm-lib/grackle/src/kcl_value_group.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use kcl_lib::ast::{self, types::BinaryPart};
|
||||
|
||||
/// Basically the same enum as `kcl_lib::ast::types::Value`, but grouped according to whether the
|
||||
/// value is singular or composite.
|
||||
/// You can convert losslessly between KclValueGroup and `kcl_lib::ast::types::Value` with From/Into.
|
||||
pub enum KclValueGroup {
|
||||
Single(SingleValue),
|
||||
ArrayExpression(Box<ast::types::ArrayExpression>),
|
||||
ObjectExpression(Box<ast::types::ObjectExpression>),
|
||||
}
|
||||
|
||||
pub enum SingleValue {
|
||||
Literal(Box<ast::types::Literal>),
|
||||
Identifier(Box<ast::types::Identifier>),
|
||||
BinaryExpression(Box<ast::types::BinaryExpression>),
|
||||
CallExpression(Box<ast::types::CallExpression>),
|
||||
PipeExpression(Box<ast::types::PipeExpression>),
|
||||
UnaryExpression(Box<ast::types::UnaryExpression>),
|
||||
KclNoneExpression(ast::types::KclNone),
|
||||
MemberExpression(Box<ast::types::MemberExpression>),
|
||||
}
|
||||
|
||||
impl From<ast::types::BinaryPart> for KclValueGroup {
|
||||
fn from(value: ast::types::BinaryPart) -> Self {
|
||||
match value {
|
||||
BinaryPart::Literal(e) => Self::Single(SingleValue::Literal(e)),
|
||||
BinaryPart::Identifier(e) => Self::Single(SingleValue::Identifier(e)),
|
||||
BinaryPart::BinaryExpression(e) => Self::Single(SingleValue::BinaryExpression(e)),
|
||||
BinaryPart::CallExpression(e) => Self::Single(SingleValue::CallExpression(e)),
|
||||
BinaryPart::UnaryExpression(e) => Self::Single(SingleValue::UnaryExpression(e)),
|
||||
BinaryPart::MemberExpression(e) => Self::Single(SingleValue::MemberExpression(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ast::types::BinaryPart> for SingleValue {
|
||||
fn from(value: ast::types::BinaryPart) -> Self {
|
||||
match value {
|
||||
BinaryPart::Literal(e) => Self::Literal(e),
|
||||
BinaryPart::Identifier(e) => Self::Identifier(e),
|
||||
BinaryPart::BinaryExpression(e) => Self::BinaryExpression(e),
|
||||
BinaryPart::CallExpression(e) => Self::CallExpression(e),
|
||||
BinaryPart::UnaryExpression(e) => Self::UnaryExpression(e),
|
||||
BinaryPart::MemberExpression(e) => Self::MemberExpression(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ast::types::Value> for KclValueGroup {
|
||||
fn from(value: ast::types::Value) -> Self {
|
||||
match value {
|
||||
ast::types::Value::Literal(e) => Self::Single(SingleValue::Literal(e)),
|
||||
ast::types::Value::Identifier(e) => Self::Single(SingleValue::Identifier(e)),
|
||||
ast::types::Value::BinaryExpression(e) => Self::Single(SingleValue::BinaryExpression(e)),
|
||||
ast::types::Value::CallExpression(e) => Self::Single(SingleValue::CallExpression(e)),
|
||||
ast::types::Value::PipeExpression(e) => Self::Single(SingleValue::PipeExpression(e)),
|
||||
ast::types::Value::None(e) => Self::Single(SingleValue::KclNoneExpression(e)),
|
||||
ast::types::Value::UnaryExpression(e) => Self::Single(SingleValue::UnaryExpression(e)),
|
||||
ast::types::Value::ArrayExpression(e) => Self::ArrayExpression(e),
|
||||
ast::types::Value::ObjectExpression(e) => Self::ObjectExpression(e),
|
||||
ast::types::Value::PipeSubstitution(_)
|
||||
| ast::types::Value::FunctionExpression(_)
|
||||
| ast::types::Value::MemberExpression(_) => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KclValueGroup> for ast::types::Value {
|
||||
fn from(value: KclValueGroup) -> Self {
|
||||
match value {
|
||||
KclValueGroup::Single(e) => match e {
|
||||
SingleValue::Literal(e) => ast::types::Value::Literal(e),
|
||||
SingleValue::Identifier(e) => ast::types::Value::Identifier(e),
|
||||
SingleValue::BinaryExpression(e) => ast::types::Value::BinaryExpression(e),
|
||||
SingleValue::CallExpression(e) => ast::types::Value::CallExpression(e),
|
||||
SingleValue::PipeExpression(e) => ast::types::Value::PipeExpression(e),
|
||||
SingleValue::UnaryExpression(e) => ast::types::Value::UnaryExpression(e),
|
||||
SingleValue::KclNoneExpression(e) => ast::types::Value::None(e),
|
||||
SingleValue::MemberExpression(e) => ast::types::Value::MemberExpression(e),
|
||||
},
|
||||
KclValueGroup::ArrayExpression(e) => ast::types::Value::ArrayExpression(e),
|
||||
KclValueGroup::ObjectExpression(e) => ast::types::Value::ObjectExpression(e),
|
||||
}
|
||||
}
|
||||
}
|
501
src/wasm-lib/grackle/src/lib.rs
Normal file
501
src/wasm-lib/grackle/src/lib.rs
Normal file
@ -0,0 +1,501 @@
|
||||
mod kcl_value_group;
|
||||
mod native_functions;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use kcl_lib::{
|
||||
ast,
|
||||
ast::types::{BodyItem, KclNone, LiteralValue, Program, VariableDeclaration},
|
||||
};
|
||||
use kittycad_execution_plan as ep;
|
||||
use kittycad_execution_plan::{Address, ExecutionError, Instruction};
|
||||
use kittycad_execution_plan_traits as ept;
|
||||
use kittycad_execution_plan_traits::NumericPrimitive;
|
||||
use kittycad_modeling_session::Session;
|
||||
|
||||
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 mut mem = kittycad_execution_plan::Memory::default();
|
||||
kittycad_execution_plan::execute(&mut mem, plan, session).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compiles KCL programs into Execution Plans.
|
||||
struct Planner {
|
||||
/// Maps KCL identifiers to what they hold, and where in KCEP virtual memory they'll be written to.
|
||||
binding_scope: BindingScope,
|
||||
/// Next available KCEP virtual machine memory address.
|
||||
next_addr: Address,
|
||||
}
|
||||
|
||||
impl Planner {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
binding_scope: BindingScope::prelude(),
|
||||
next_addr: Address::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_plan(&mut self, program: Program) -> PlanRes {
|
||||
let mut instructions = Vec::new();
|
||||
for item in program.body {
|
||||
instructions.extend(self.visit_body_item(item)?);
|
||||
}
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
/// Emits instructions which, when run, compute a given KCL value and store it in memory.
|
||||
/// Returns the instructions, and the destination address of the value.
|
||||
fn plan_to_compute_single(&mut self, value: SingleValue) -> Result<EvalPlan, CompileError> {
|
||||
match value {
|
||||
SingleValue::KclNoneExpression(KclNone { start: _, end: _ }) => {
|
||||
let address = self.next_addr.offset_by(1);
|
||||
Ok(EvalPlan {
|
||||
instructions: vec![Instruction::SetPrimitive {
|
||||
address,
|
||||
value: ept::Primitive::Nil,
|
||||
}],
|
||||
binding: EpBinding::Single(address),
|
||||
})
|
||||
}
|
||||
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.
|
||||
let size = 1;
|
||||
let address = self.next_addr.offset_by(size);
|
||||
Ok(EvalPlan {
|
||||
instructions: vec![Instruction::SetPrimitive {
|
||||
address,
|
||||
value: kcep_val,
|
||||
}],
|
||||
binding: EpBinding::Single(address),
|
||||
})
|
||||
}
|
||||
SingleValue::Identifier(expr) => {
|
||||
// This is just duplicating a binding.
|
||||
// So, don't emit any instructions, because the value has already been computed.
|
||||
// Just return the address that it was stored at after being computed.
|
||||
let previously_bound_to = self
|
||||
.binding_scope
|
||||
.get(&expr.name)
|
||||
.ok_or(CompileError::Undefined { name: expr.name })?;
|
||||
Ok(EvalPlan {
|
||||
instructions: Vec::new(),
|
||||
binding: previously_bound_to.clone(),
|
||||
})
|
||||
}
|
||||
SingleValue::BinaryExpression(expr) => {
|
||||
let l = self.plan_to_compute_single(SingleValue::from(expr.left))?;
|
||||
let r = self.plan_to_compute_single(SingleValue::from(expr.right))?;
|
||||
let EpBinding::Single(l_binding) = l.binding else {
|
||||
return Err(CompileError::InvalidOperand(
|
||||
"you tried to use a composite value (e.g. array or object) as the operand to some math",
|
||||
));
|
||||
};
|
||||
let EpBinding::Single(r_binding) = r.binding else {
|
||||
return Err(CompileError::InvalidOperand(
|
||||
"you tried to use a composite value (e.g. array or object) as the operand to some math",
|
||||
));
|
||||
};
|
||||
let destination = self.next_addr.offset_by(1);
|
||||
let mut plan = Vec::with_capacity(l.instructions.len() + r.instructions.len() + 1);
|
||||
plan.extend(l.instructions);
|
||||
plan.extend(r.instructions);
|
||||
plan.push(Instruction::Arithmetic {
|
||||
arithmetic: ep::Arithmetic {
|
||||
operation: match expr.operator {
|
||||
ast::types::BinaryOperator::Add => ep::Operation::Add,
|
||||
ast::types::BinaryOperator::Sub => ep::Operation::Sub,
|
||||
ast::types::BinaryOperator::Mul => ep::Operation::Mul,
|
||||
ast::types::BinaryOperator::Div => ep::Operation::Div,
|
||||
ast::types::BinaryOperator::Mod => {
|
||||
todo!("execution plan instruction set doesn't support Mod yet")
|
||||
}
|
||||
ast::types::BinaryOperator::Pow => {
|
||||
todo!("execution plan instruction set doesn't support Pow yet")
|
||||
}
|
||||
},
|
||||
operand0: ep::Operand::Reference(l_binding),
|
||||
operand1: ep::Operand::Reference(r_binding),
|
||||
},
|
||||
destination,
|
||||
});
|
||||
Ok(EvalPlan {
|
||||
instructions: plan,
|
||||
binding: EpBinding::Single(destination),
|
||||
})
|
||||
}
|
||||
SingleValue::CallExpression(expr) => {
|
||||
let (mut instructions, args) = expr.arguments.into_iter().try_fold(
|
||||
(Vec::new(), Vec::new()),
|
||||
|(mut acc_instrs, mut acc_args), argument| {
|
||||
let EvalPlan {
|
||||
instructions: new_instructions,
|
||||
binding: arg,
|
||||
} = match KclValueGroup::from(argument) {
|
||||
KclValueGroup::Single(value) => self.plan_to_compute_single(value)?,
|
||||
KclValueGroup::ArrayExpression(_) => todo!(),
|
||||
KclValueGroup::ObjectExpression(_) => todo!(),
|
||||
};
|
||||
acc_instrs.extend(new_instructions);
|
||||
acc_args.push(arg);
|
||||
Ok((acc_instrs, acc_args))
|
||||
},
|
||||
)?;
|
||||
let callee = match self.binding_scope.get_fn(&expr.callee.name) {
|
||||
GetFnResult::Found(f) => f,
|
||||
GetFnResult::NonCallable => {
|
||||
return Err(CompileError::NotCallable {
|
||||
name: expr.callee.name.clone(),
|
||||
});
|
||||
}
|
||||
GetFnResult::NotFound => {
|
||||
return Err(CompileError::Undefined {
|
||||
name: expr.callee.name.clone(),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let EvalPlan {
|
||||
instructions: eval_instrs,
|
||||
binding,
|
||||
} = callee.call(&mut self.next_addr, args)?;
|
||||
instructions.extend(eval_instrs);
|
||||
Ok(EvalPlan { instructions, binding })
|
||||
}
|
||||
SingleValue::PipeExpression(_) => todo!(),
|
||||
SingleValue::UnaryExpression(_) => todo!(),
|
||||
SingleValue::MemberExpression(_) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits instructions which, when run, compute a given KCL value and store it in memory.
|
||||
/// Returns the instructions.
|
||||
/// Also binds the value to a name.
|
||||
fn plan_to_bind(
|
||||
&mut self,
|
||||
declarations: ast::types::VariableDeclaration,
|
||||
) -> Result<Vec<Instruction>, CompileError> {
|
||||
declarations
|
||||
.declarations
|
||||
.into_iter()
|
||||
.try_fold(Vec::new(), |mut acc, declaration| {
|
||||
let (instrs, binding) = self.plan_to_bind_one(declaration.init)?;
|
||||
self.binding_scope.bind(declaration.id.name, binding);
|
||||
acc.extend(instrs);
|
||||
Ok(acc)
|
||||
})
|
||||
}
|
||||
|
||||
fn plan_to_bind_one(
|
||||
&mut self,
|
||||
value_being_bound: ast::types::Value,
|
||||
) -> Result<(Vec<Instruction>, EpBinding), CompileError> {
|
||||
match KclValueGroup::from(value_being_bound) {
|
||||
KclValueGroup::Single(init_value) => {
|
||||
// Simple! Just evaluate it, note where the final value will be stored in KCEP memory,
|
||||
// and bind it to the KCL identifier.
|
||||
let EvalPlan { instructions, binding } = self.plan_to_compute_single(init_value)?;
|
||||
Ok((instructions, binding))
|
||||
}
|
||||
KclValueGroup::ArrayExpression(expr) => {
|
||||
// First, emit a plan to compute each element of the array.
|
||||
// Collect all the bindings from each element too.
|
||||
let (instructions, bindings) = expr.elements.into_iter().try_fold(
|
||||
(Vec::new(), Vec::new()),
|
||||
|(mut acc_instrs, mut acc_bindings), element| {
|
||||
match KclValueGroup::from(element) {
|
||||
KclValueGroup::Single(value) => {
|
||||
// If this element of the array is a single value, then binding it is
|
||||
// straightforward -- you got a single binding, no need to change anything.
|
||||
let EvalPlan { instructions, binding } = self.plan_to_compute_single(value)?;
|
||||
acc_instrs.extend(instructions);
|
||||
acc_bindings.push(binding);
|
||||
}
|
||||
KclValueGroup::ArrayExpression(expr) => {
|
||||
// If this element of the array is _itself_ an array, then we need to
|
||||
// emit a plan to calculate each element of this child array.
|
||||
// Then we collect the child array's bindings, and bind them to one
|
||||
// element of the parent array.
|
||||
let binding = expr
|
||||
.elements
|
||||
.into_iter()
|
||||
.try_fold(Vec::new(), |mut seq, child_element| {
|
||||
let (instructions, binding) = self.plan_to_bind_one(child_element)?;
|
||||
acc_instrs.extend(instructions);
|
||||
seq.push(binding);
|
||||
Ok(seq)
|
||||
})
|
||||
.map(EpBinding::Sequence)?;
|
||||
acc_bindings.push(binding);
|
||||
}
|
||||
KclValueGroup::ObjectExpression(expr) => {
|
||||
// If this element of the array is an object, then we need to
|
||||
// emit a plan to calculate each value of each property of the object.
|
||||
// Then we collect the bindings for each child value, and bind them to one
|
||||
// element of the parent array.
|
||||
let map = HashMap::with_capacity(expr.properties.len());
|
||||
let binding = expr
|
||||
.properties
|
||||
.into_iter()
|
||||
.try_fold(map, |mut map, property| {
|
||||
let (instructions, binding) = self.plan_to_bind_one(property.value)?;
|
||||
map.insert(property.key.name, binding);
|
||||
acc_instrs.extend(instructions);
|
||||
Ok(map)
|
||||
})
|
||||
.map(EpBinding::Map)?;
|
||||
acc_bindings.push(binding);
|
||||
}
|
||||
};
|
||||
Ok((acc_instrs, acc_bindings))
|
||||
},
|
||||
)?;
|
||||
Ok((instructions, EpBinding::Sequence(bindings)))
|
||||
}
|
||||
KclValueGroup::ObjectExpression(expr) => {
|
||||
// Convert the object to a sequence of key-value pairs.
|
||||
let mut kvs = expr.properties.into_iter().map(|prop| (prop.key, prop.value));
|
||||
let (instructions, each_property_binding) = kvs.try_fold(
|
||||
(Vec::new(), HashMap::new()),
|
||||
|(mut acc_instrs, mut acc_bindings), (key, value)| {
|
||||
match KclValueGroup::from(value) {
|
||||
KclValueGroup::Single(value) => {
|
||||
let EvalPlan { instructions, binding } = self.plan_to_compute_single(value)?;
|
||||
acc_instrs.extend(instructions);
|
||||
acc_bindings.insert(key.name, binding);
|
||||
}
|
||||
KclValueGroup::ArrayExpression(expr) => {
|
||||
// If this value of the object is an array, then emit a plan to calculate
|
||||
// each element of that array. Collect their bindings, and bind them all
|
||||
// under one property of the parent object.
|
||||
let n = expr.elements.len();
|
||||
let binding = expr
|
||||
.elements
|
||||
.into_iter()
|
||||
.try_fold(Vec::with_capacity(n), |mut seq, child_element| {
|
||||
let (instructions, binding) = self.plan_to_bind_one(child_element)?;
|
||||
seq.push(binding);
|
||||
acc_instrs.extend(instructions);
|
||||
Ok(seq)
|
||||
})
|
||||
.map(EpBinding::Sequence)?;
|
||||
acc_bindings.insert(key.name, binding);
|
||||
}
|
||||
KclValueGroup::ObjectExpression(expr) => {
|
||||
// If this value of the object is _itself_ an object, then we need to
|
||||
// emit a plan to calculate each value of each property of the child object.
|
||||
// Then we collect the bindings for each child value, and bind them to one
|
||||
// property of the parent object.
|
||||
let n = expr.properties.len();
|
||||
let binding = expr
|
||||
.properties
|
||||
.into_iter()
|
||||
.try_fold(HashMap::with_capacity(n), |mut map, property| {
|
||||
let (instructions, binding) = self.plan_to_bind_one(property.value)?;
|
||||
map.insert(property.key.name, binding);
|
||||
acc_instrs.extend(instructions);
|
||||
Ok(map)
|
||||
})
|
||||
.map(EpBinding::Map)?;
|
||||
acc_bindings.insert(key.name, binding);
|
||||
}
|
||||
};
|
||||
Ok((acc_instrs, acc_bindings))
|
||||
},
|
||||
)?;
|
||||
Ok((instructions, EpBinding::Map(each_property_binding)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, Eq, PartialEq, Clone)]
|
||||
pub enum CompileError {
|
||||
#[error("the name {name} was not defined")]
|
||||
Undefined { name: String },
|
||||
#[error("the function {fn_name} requires at least {required} arguments but you only supplied {actual}")]
|
||||
NotEnoughArgs {
|
||||
fn_name: String2,
|
||||
required: usize,
|
||||
actual: usize,
|
||||
},
|
||||
#[error("the function {fn_name} accepts at most {maximum} arguments but you supplied {actual}")]
|
||||
TooManyArgs {
|
||||
fn_name: String2,
|
||||
maximum: usize,
|
||||
actual: usize,
|
||||
},
|
||||
#[error("you tried to call {name} but it's not a function")]
|
||||
NotCallable { name: String },
|
||||
#[error("you're trying to use an operand that isn't compatible with the given arithmetic operator: {0}")]
|
||||
InvalidOperand(&'static str),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("{0}")]
|
||||
Compile(#[from] CompileError),
|
||||
#[error("{0}")]
|
||||
Execution(#[from] ExecutionError),
|
||||
}
|
||||
|
||||
/// Something that can traverse expression trees, visiting nodes.
|
||||
/// When a node gets visited, it returns some type R.
|
||||
trait ExprVisitor<R> {
|
||||
fn visit_body_item(&mut self, item: BodyItem) -> R;
|
||||
fn visit_variable_declaration(&mut self, vd: VariableDeclaration) -> R;
|
||||
}
|
||||
|
||||
type PlanRes = Result<Vec<Instruction>, CompileError>;
|
||||
|
||||
impl ExprVisitor<PlanRes> for Planner {
|
||||
fn visit_body_item(&mut self, item: BodyItem) -> PlanRes {
|
||||
match item {
|
||||
BodyItem::VariableDeclaration(vd) => self.visit_variable_declaration(vd),
|
||||
BodyItem::ExpressionStatement(_) => todo!(),
|
||||
BodyItem::ReturnStatement(_) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_variable_declaration(&mut self, variable_declaration: VariableDeclaration) -> PlanRes {
|
||||
self.plan_to_bind(variable_declaration)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
match expr {
|
||||
LiteralValue::IInteger(x) => ept::Primitive::NumericValue(NumericPrimitive::Integer(x)),
|
||||
LiteralValue::Fractional(x) => ept::Primitive::NumericValue(NumericPrimitive::Float(x)),
|
||||
LiteralValue::String(x) => ept::Primitive::String(x),
|
||||
}
|
||||
}
|
||||
|
||||
/// Instructions that can compute some value.
|
||||
struct EvalPlan {
|
||||
/// The instructions which will compute the value.
|
||||
instructions: Vec<Instruction>,
|
||||
/// Where the value will be stored.
|
||||
binding: EpBinding,
|
||||
}
|
||||
|
||||
trait KclFunction: std::fmt::Debug {
|
||||
fn call(&self, next_addr: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError>;
|
||||
}
|
||||
|
||||
/// 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))]
|
||||
enum EpBinding {
|
||||
/// A KCL value which gets stored in a particular address in KCEP memory.
|
||||
Single(Address),
|
||||
/// A sequence of KCL values, indexed by their position in the sequence.
|
||||
Sequence(Vec<EpBinding>),
|
||||
/// A sequence of KCL values, indexed by their identifier.
|
||||
Map(HashMap<String, EpBinding>),
|
||||
}
|
||||
|
||||
/// A set of bindings in a particular scope.
|
||||
/// Bindings are KCL values that get "compiled" into KCEP values, which are stored in KCEP memory
|
||||
/// at a particular KCEP address.
|
||||
/// Bindings are referenced by the name of their KCL identifier.
|
||||
///
|
||||
/// KCL has multiple scopes -- each function has a scope for its own local variables and parameters.
|
||||
/// So when referencing a variable, it might be in this scope, or the parent scope. So, each environment
|
||||
/// has to keep track of parent environments. The root environment has no parent, and is used for KCL globals
|
||||
/// (e.g. the prelude of stdlib functions).
|
||||
///
|
||||
/// These are called "Environments" in the "Crafting Interpreters" book.
|
||||
#[derive(Debug)]
|
||||
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>>,
|
||||
}
|
||||
|
||||
/// Either an owned string, or a static string. Either way it can be read and moved around.
|
||||
type String2 = Cow<'static, str>;
|
||||
|
||||
impl BindingScope {
|
||||
/// The parent scope for every program, before the user has defined anything.
|
||||
/// Only includes some stdlib functions.
|
||||
/// This is usually known as the "prelude" in other languages. It's the stdlib functions that
|
||||
/// are already imported for you when you start coding.
|
||||
pub fn prelude() -> Self {
|
||||
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: 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)),
|
||||
}
|
||||
}
|
||||
|
||||
//// 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()
|
||||
}
|
||||
|
||||
/// Add a binding (e.g. defining a new variable)
|
||||
pub fn bind(&mut self, identifier: String, binding: EpBinding) {
|
||||
self.ep_bindings.insert(identifier, binding);
|
||||
}
|
||||
|
||||
/// Look up a binding.
|
||||
pub fn get(&self, identifier: &str) -> Option<&EpBinding> {
|
||||
if let Some(b) = self.ep_bindings.get(identifier) {
|
||||
// The name was found in this scope.
|
||||
Some(b)
|
||||
} else if let Some(ref parent) = self.parent {
|
||||
// Check the next scope outwards.
|
||||
parent.get(identifier)
|
||||
} else {
|
||||
// There's no outer scope, and it wasn't found, so there's nowhere else to look.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up a function bound to the given identifier.
|
||||
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
|
||||
} else if let Some(ref parent) = self.parent {
|
||||
parent.get_fn(identifier)
|
||||
} else {
|
||||
GetFnResult::NotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GetFnResult<'a> {
|
||||
Found(&'a dyn KclFunction),
|
||||
NonCallable,
|
||||
NotFound,
|
||||
}
|
105
src/wasm-lib/grackle/src/native_functions.rs
Normal file
105
src/wasm-lib/grackle/src/native_functions.rs
Normal file
@ -0,0 +1,105 @@
|
||||
//! Defines functions which are written in Rust, but called from KCL.
|
||||
//! This includes some of the stdlib, e.g. `startSketchAt`.
|
||||
//! But some other stdlib functions will be written in KCL.
|
||||
|
||||
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};
|
||||
|
||||
/// The identity function. Always returns its first input.
|
||||
#[derive(Debug)]
|
||||
pub struct Id;
|
||||
|
||||
impl KclFunction for Id {
|
||||
fn call(&self, _: &mut Address, args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
|
||||
if args.len() > 1 {
|
||||
return Err(CompileError::TooManyArgs {
|
||||
fn_name: "id".into(),
|
||||
maximum: 1,
|
||||
actual: args.len(),
|
||||
});
|
||||
}
|
||||
let arg = args
|
||||
.first()
|
||||
.ok_or(CompileError::NotEnoughArgs {
|
||||
fn_name: "id".into(),
|
||||
required: 1,
|
||||
actual: 0,
|
||||
})?
|
||||
.clone();
|
||||
Ok(EvalPlan {
|
||||
instructions: Vec::new(),
|
||||
binding: arg,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StartSketchAt;
|
||||
|
||||
impl KclFunction for StartSketchAt {
|
||||
fn call(&self, next_addr: &mut Address, _args: Vec<EpBinding>) -> Result<EvalPlan, CompileError> {
|
||||
let mut instructions = Vec::new();
|
||||
// Store the plane.
|
||||
let plane = PlaneData::XY.into_parts();
|
||||
instructions.push(Instruction::SetValue {
|
||||
address: next_addr.offset_by(plane.len()),
|
||||
value_parts: plane,
|
||||
});
|
||||
// TODO: Get the plane ID from global context.
|
||||
// TODO: Send this command:
|
||||
// ModelingCmd::SketchModeEnable {
|
||||
// animated: false,
|
||||
// ortho: false,
|
||||
// plane_id: plane.id,
|
||||
// // We pass in the normal for the plane here.
|
||||
// disable_camera_with_plane: Some(plane.z_axis.clone().into()),
|
||||
// },
|
||||
// TODO: Send ModelingCmd::StartPath at the given point.
|
||||
// TODO (maybe): Store the SketchGroup in KCEP memory.
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// A test function that adds two numbers.
|
||||
#[derive(Debug)]
|
||||
pub struct Add;
|
||||
|
||||
impl KclFunction for Add {
|
||||
fn call(&self, next_address: &mut Address, 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,
|
||||
});
|
||||
}
|
||||
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 = next_address.offset_by(1);
|
||||
Ok(EvalPlan {
|
||||
instructions: vec![Instruction::Arithmetic {
|
||||
arithmetic: Arithmetic {
|
||||
operation: kittycad_execution_plan::Operation::Add,
|
||||
operand0: kittycad_execution_plan::Operand::Reference(arg0),
|
||||
operand1: kittycad_execution_plan::Operand::Reference(arg1),
|
||||
},
|
||||
destination,
|
||||
}],
|
||||
binding: EpBinding::Single(destination),
|
||||
})
|
||||
}
|
||||
}
|
382
src/wasm-lib/grackle/src/tests.rs
Normal file
382
src/wasm-lib/grackle/src/tests.rs
Normal file
@ -0,0 +1,382 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn must_plan(program: &str) -> (Vec<Instruction>, BindingScope) {
|
||||
let tokens = kcl_lib::token::lexer(program);
|
||||
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();
|
||||
(instrs, p.binding_scope)
|
||||
}
|
||||
|
||||
fn should_not_compile(program: &str) -> CompileError {
|
||||
let tokens = kcl_lib::token::lexer(program);
|
||||
let parser = kcl_lib::parser::Parser::new(tokens);
|
||||
let ast = parser.ast().unwrap();
|
||||
let mut p = Planner::new();
|
||||
p.build_plan(ast).unwrap_err()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assignments() {
|
||||
let program = "
|
||||
let x = 1
|
||||
let y = 2";
|
||||
let (plan, _scope) = must_plan(program);
|
||||
assert_eq!(
|
||||
plan,
|
||||
vec![
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 1i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(1),
|
||||
value: 2i64.into(),
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_array() {
|
||||
let program = r#"let x = [44, 55, "sixty-six"]"#;
|
||||
let (plan, _scope) = must_plan(program);
|
||||
assert_eq!(
|
||||
plan,
|
||||
vec![
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 44i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(1),
|
||||
value: 55i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(2),
|
||||
value: "sixty-six".to_owned().into(),
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_nested_array() {
|
||||
let program = r#"let x = [44, [55, "sixty-six"]]"#;
|
||||
let (plan, _scope) = must_plan(program);
|
||||
assert_eq!(
|
||||
plan,
|
||||
vec![
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 44i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(1),
|
||||
value: 55i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(2),
|
||||
value: "sixty-six".to_owned().into(),
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_arrays_with_objects_elements() {
|
||||
let program = r#"let x = [44, {a: 55, b: "sixty-six"}]"#;
|
||||
let (plan, _scope) = must_plan(program);
|
||||
assert_eq!(
|
||||
plan,
|
||||
vec![
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 44i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(1),
|
||||
value: 55i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(2),
|
||||
value: "sixty-six".to_owned().into(),
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_not_found() {
|
||||
// Users can't assign `y` to anything because `y` is undefined.
|
||||
let err = should_not_compile("let x = y");
|
||||
assert_eq!(err, CompileError::Undefined { name: "y".to_owned() });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aliases() {
|
||||
let program = "
|
||||
let x = 1
|
||||
let y = x";
|
||||
let (plan, _scope) = must_plan(program);
|
||||
assert_eq!(
|
||||
plan,
|
||||
vec![Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 1i64.into(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_native_function_add() {
|
||||
let program = "let x = add(1,2)";
|
||||
let (plan, _scope) = must_plan(program);
|
||||
assert_eq!(
|
||||
plan,
|
||||
vec![
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 1i64.into()
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(1),
|
||||
value: 2i64.into()
|
||||
},
|
||||
Instruction::Arithmetic {
|
||||
arithmetic: ep::Arithmetic {
|
||||
operation: ep::Operation::Add,
|
||||
operand0: ep::Operand::Reference(Address::ZERO),
|
||||
operand1: ep::Operand::Reference(Address::ZERO.offset(1))
|
||||
},
|
||||
destination: Address::ZERO.offset(2),
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_native_function_id() {
|
||||
let program = "let x = id(2)";
|
||||
let (plan, _scope) = must_plan(program);
|
||||
assert_eq!(
|
||||
plan,
|
||||
vec![Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 2i64.into()
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_literals() {
|
||||
let program = "let x = 1 + 2";
|
||||
let (plan, _scope) = must_plan(program);
|
||||
assert_eq!(
|
||||
plan,
|
||||
vec![
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 1i64.into()
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(1),
|
||||
value: 2i64.into()
|
||||
},
|
||||
Instruction::Arithmetic {
|
||||
arithmetic: ep::Arithmetic {
|
||||
operation: ep::Operation::Add,
|
||||
operand0: ep::Operand::Reference(Address::ZERO),
|
||||
operand1: ep::Operand::Reference(Address::ZERO.offset(1)),
|
||||
},
|
||||
destination: Address::ZERO.offset(2),
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_vars() {
|
||||
let program = "
|
||||
let one = 1
|
||||
let two = 2
|
||||
let x = one + two";
|
||||
let (plan, _bindings) = must_plan(program);
|
||||
let addr0 = Address::ZERO;
|
||||
let addr1 = Address::ZERO.offset(1);
|
||||
assert_eq!(
|
||||
plan,
|
||||
vec![
|
||||
Instruction::SetPrimitive {
|
||||
address: addr0,
|
||||
value: 1i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: addr1,
|
||||
value: 2i64.into(),
|
||||
},
|
||||
Instruction::Arithmetic {
|
||||
arithmetic: ep::Arithmetic {
|
||||
operation: ep::Operation::Add,
|
||||
operand0: ep::Operand::Reference(addr0),
|
||||
operand1: ep::Operand::Reference(addr1),
|
||||
},
|
||||
destination: Address::ZERO.offset(2),
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composite_binary_exprs() {
|
||||
let program = "
|
||||
let x = 1
|
||||
let y = 2
|
||||
let z = 3
|
||||
let six = x + y + z
|
||||
";
|
||||
let (plan, _bindings) = must_plan(program);
|
||||
let addr0 = Address::ZERO;
|
||||
let addr1 = Address::ZERO.offset(1);
|
||||
let addr2 = Address::ZERO.offset(2);
|
||||
let addr3 = Address::ZERO.offset(3);
|
||||
assert_eq!(
|
||||
plan,
|
||||
vec![
|
||||
Instruction::SetPrimitive {
|
||||
address: addr0,
|
||||
value: 1i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: addr1,
|
||||
value: 2i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: addr2,
|
||||
value: 3i64.into(),
|
||||
},
|
||||
// Adds 1 + 2
|
||||
Instruction::Arithmetic {
|
||||
arithmetic: ep::Arithmetic {
|
||||
operation: ep::Operation::Add,
|
||||
operand0: ep::Operand::Reference(addr0),
|
||||
operand1: ep::Operand::Reference(addr1),
|
||||
},
|
||||
destination: addr3,
|
||||
},
|
||||
// Adds `x` + 3, where `x` is (1 + 2)
|
||||
Instruction::Arithmetic {
|
||||
arithmetic: ep::Arithmetic {
|
||||
operation: ep::Operation::Add,
|
||||
operand0: ep::Operand::Reference(addr3),
|
||||
operand1: ep::Operand::Reference(addr2),
|
||||
},
|
||||
destination: Address::ZERO.offset(4),
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aliases_dont_affect_plans() {
|
||||
let (plan1, _) = must_plan(
|
||||
"let one = 1
|
||||
let two = 2
|
||||
let x = one + two",
|
||||
);
|
||||
let (plan2, _) = must_plan(
|
||||
"let one = 1
|
||||
let two = 2
|
||||
let y = two
|
||||
let x = one + y",
|
||||
);
|
||||
assert_eq!(plan1, plan2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_object() {
|
||||
let program = "const x0 = {a: 1, b: 2, c: {d: 3}}";
|
||||
let (actual, bindings) = must_plan(program);
|
||||
let expected = vec![
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 1i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(1),
|
||||
value: 2i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(2),
|
||||
value: 3i64.into(),
|
||||
},
|
||||
];
|
||||
assert_eq!(actual, expected);
|
||||
eprintln!("{bindings:#?}");
|
||||
assert_eq!(
|
||||
bindings.get("x0").unwrap(),
|
||||
&EpBinding::Map(HashMap::from([
|
||||
("a".to_owned(), EpBinding::Single(Address::ZERO),),
|
||||
("b".to_owned(), EpBinding::Single(Address::ZERO.offset(1))),
|
||||
(
|
||||
"c".to_owned(),
|
||||
EpBinding::Map(HashMap::from([(
|
||||
"d".to_owned(),
|
||||
EpBinding::Single(Address::ZERO.offset(2))
|
||||
)]))
|
||||
),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_object_with_array_property() {
|
||||
let program = "const x0 = {a: 1, b: [2, 3]}";
|
||||
let (actual, bindings) = must_plan(program);
|
||||
let expected = vec![
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 1i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(1),
|
||||
value: 2i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO.offset(2),
|
||||
value: 3i64.into(),
|
||||
},
|
||||
];
|
||||
assert_eq!(actual, expected);
|
||||
eprintln!("{bindings:#?}");
|
||||
assert_eq!(
|
||||
bindings.get("x0").unwrap(),
|
||||
&EpBinding::Map(HashMap::from([
|
||||
("a".to_owned(), EpBinding::Single(Address::ZERO),),
|
||||
(
|
||||
"b".to_owned(),
|
||||
EpBinding::Sequence(vec![
|
||||
EpBinding::Single(Address::ZERO.offset(1)),
|
||||
EpBinding::Single(Address::ZERO.offset(2)),
|
||||
])
|
||||
),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[ignore = "haven't done API calls or stdlib yet"]
|
||||
#[test]
|
||||
fn stdlib_api_calls() {
|
||||
let program = "const x0 = startSketchAt([0, 0])
|
||||
const x1 = line([0, 10], x0)
|
||||
const x2 = line([10, 0], x1)
|
||||
const x3 = line([0, -10], x2)
|
||||
const x4 = line([0, 0], x3)
|
||||
const x5 = close(x4)
|
||||
const x6 = extrude(20, x5)
|
||||
show(x6)";
|
||||
must_plan(program);
|
||||
}
|
@ -21,6 +21,8 @@ databake = { version = "0.1.7", features = ["derive"] }
|
||||
derive-docs = { version = "0.1.5" }
|
||||
# derive-docs = { path = "../derive-docs" }
|
||||
kittycad = { workspace = true }
|
||||
kittycad-execution-plan-macros = { workspace = true }
|
||||
kittycad-execution-plan-traits = { workspace = true }
|
||||
lazy_static = "1.4.0"
|
||||
parse-display = "0.8.2"
|
||||
schemars = { version = "0.8.16", features = ["impl_json_schema", "url", "uuid1"] }
|
||||
|
@ -5,6 +5,7 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use anyhow::Result;
|
||||
use async_recursion::async_recursion;
|
||||
use kittycad::types::{Color, ModelingCmd, Point3D};
|
||||
use kittycad_execution_plan_macros::ExecutionPlanValue;
|
||||
use parse_display::{Display, FromStr};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -610,7 +611,7 @@ impl Point2d {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema)]
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema, ExecutionPlanValue)]
|
||||
#[ts(export)]
|
||||
pub struct Point3d {
|
||||
pub x: f64,
|
||||
|
@ -3,6 +3,7 @@
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use kittycad::types::{Angle, ModelingCmd, Point3D};
|
||||
use kittycad_execution_plan_macros::ExecutionPlanValue;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@ -648,7 +649,7 @@ async fn inner_start_sketch_at(data: LineData, args: Args) -> Result<Box<SketchG
|
||||
}
|
||||
|
||||
/// Data for a plane.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, ExecutionPlanValue)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PlaneData {
|
||||
|
Reference in New Issue
Block a user