Grackle: Store KCL objects as KCEP objects (#1333)

* Grackle: Store KCL objects as KCEP objects

* Remove KCL SingleValue

* Fix a test, update map bindings

* Fix tests
This commit is contained in:
Adam Chalmers
2024-01-30 15:18:45 +11:00
committed by GitHub
parent c6249f36d2
commit 4ca341e132
6 changed files with 244 additions and 261 deletions

View File

@ -1950,7 +1950,7 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-execution-plan" name = "kittycad-execution-plan"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#0a891ad3d4a189410f457e00b644ff4e116e7175" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#0d1fb31b8624abea0d270365cc459e78577d26f8"
dependencies = [ dependencies = [
"bytes", "bytes",
"insta", "insta",
@ -1980,7 +1980,7 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-execution-plan-macros" name = "kittycad-execution-plan-macros"
version = "0.1.2" version = "0.1.2"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#0a891ad3d4a189410f457e00b644ff4e116e7175" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#0d1fb31b8624abea0d270365cc459e78577d26f8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1989,9 +1989,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-execution-plan-traits" name = "kittycad-execution-plan-traits"
version = "0.1.5" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29dc558ca98b726d998fe9617f7cab0c427f54b49b42fb68d0a69d85e1b52dc7" checksum = "d6f893f7837d95bb669781581842511307e42622221c6d241031f1c35c11a2af"
dependencies = [ dependencies = [
"serde", "serde",
"thiserror", "thiserror",
@ -2001,7 +2001,7 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-cmds" name = "kittycad-modeling-cmds"
version = "0.1.12" version = "0.1.12"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#0a891ad3d4a189410f457e00b644ff4e116e7175" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#0d1fb31b8624abea0d270365cc459e78577d26f8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -2028,7 +2028,7 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-session" name = "kittycad-modeling-session"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#0a891ad3d4a189410f457e00b644ff4e116e7175" source = "git+https://github.com/KittyCAD/modeling-api?branch=main#0d1fb31b8624abea0d270365cc459e78577d26f8"
dependencies = [ dependencies = [
"futures", "futures",
"kittycad", "kittycad",

View File

@ -60,7 +60,7 @@ members = [
[workspace.dependencies] [workspace.dependencies]
kittycad = { version = "0.2.45", default-features = false, features = ["js"] } 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 = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }
kittycad-execution-plan-traits = "0.1.5" kittycad-execution-plan-traits = "0.1.6"
kittycad-modeling-session = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" } 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" } kittycad-execution-plan-macros = { git = "https://github.com/KittyCAD/modeling-api", branch = "main" }

View File

@ -22,7 +22,10 @@ pub enum EpBinding {
elements: Vec<EpBinding>, elements: Vec<EpBinding>,
}, },
/// A sequence of KCL values, indexed by their identifier. /// A sequence of KCL values, indexed by their identifier.
Map(HashMap<String, EpBinding>), Map {
length_at: Address,
properties: HashMap<String, EpBinding>,
},
/// Not associated with a KCEP address. /// Not associated with a KCEP address.
Function(KclFunction), Function(KclFunction),
} }
@ -47,7 +50,7 @@ impl EpBinding {
.get(i) .get(i)
.ok_or(CompileError::IndexOutOfBounds { i, len: elements.len() }) .ok_or(CompileError::IndexOutOfBounds { i, len: elements.len() })
} }
EpBinding::Map(_) => Err(CompileError::CannotIndex), EpBinding::Map { .. } => Err(CompileError::CannotIndex),
EpBinding::Single(_) => Err(CompileError::CannotIndex), EpBinding::Single(_) => Err(CompileError::CannotIndex),
EpBinding::Function(_) => Err(CompileError::CannotIndex), EpBinding::Function(_) => Err(CompileError::CannotIndex),
}, },
@ -56,7 +59,12 @@ impl EpBinding {
EpBinding::Single(_) => Err(CompileError::NoProperties), EpBinding::Single(_) => Err(CompileError::NoProperties),
EpBinding::Function(_) => Err(CompileError::NoProperties), EpBinding::Function(_) => Err(CompileError::NoProperties),
EpBinding::Sequence { .. } => Err(CompileError::ArrayDoesNotHaveProperties), EpBinding::Sequence { .. } => Err(CompileError::ArrayDoesNotHaveProperties),
EpBinding::Map(map) => map.get(&property).ok_or(CompileError::UndefinedProperty { property }), EpBinding::Map {
properties,
length_at: _,
} => properties
.get(&property)
.ok_or(CompileError::UndefinedProperty { property }),
}, },
// It's never valid to index by a fractional number. // It's never valid to index by a fractional number.
LiteralValue::Fractional(num) => Err(CompileError::InvalidIndex(num.to_string())), LiteralValue::Fractional(num) => Err(CompileError::InvalidIndex(num.to_string())),

View File

@ -1,90 +1,14 @@
use kcl_lib::ast::{self, types::BinaryPart}; use kcl_lib::ast::{self, types::BinaryPart};
/// Basically the same enum as `kcl_lib::ast::types::Value`, but grouped according to whether the pub type SingleValue = kcl_lib::ast::types::Value;
/// 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),
ObjectExpression(Box<ast::types::ObjectExpression>),
}
#[derive(Debug)] pub fn into_single_value(value: ast::types::BinaryPart) -> SingleValue {
pub enum SingleValue { match value {
Literal(Box<ast::types::Literal>), BinaryPart::Literal(e) => SingleValue::Literal(e),
Identifier(Box<ast::types::Identifier>), BinaryPart::Identifier(e) => SingleValue::Identifier(e),
BinaryExpression(Box<ast::types::BinaryExpression>), BinaryPart::BinaryExpression(e) => SingleValue::BinaryExpression(e),
CallExpression(Box<ast::types::CallExpression>), BinaryPart::CallExpression(e) => SingleValue::CallExpression(e),
PipeExpression(Box<ast::types::PipeExpression>), BinaryPart::UnaryExpression(e) => SingleValue::UnaryExpression(e),
UnaryExpression(Box<ast::types::UnaryExpression>), BinaryPart::MemberExpression(e) => SingleValue::MemberExpression(e),
KclNoneExpression(ast::types::KclNone),
MemberExpression(Box<ast::types::MemberExpression>),
FunctionExpression(Box<ast::types::FunctionExpression>),
PipeSubstitution(Box<ast::types::PipeSubstitution>),
ArrayExpression(Box<ast::types::ArrayExpression>),
}
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::Single(SingleValue::ArrayExpression(e)),
ast::types::Value::ObjectExpression(e) => Self::ObjectExpression(e),
ast::types::Value::MemberExpression(e) => Self::Single(SingleValue::MemberExpression(e)),
ast::types::Value::FunctionExpression(e) => Self::Single(SingleValue::FunctionExpression(e)),
ast::types::Value::PipeSubstitution(e) => Self::Single(SingleValue::PipeSubstitution(e)),
}
}
}
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),
SingleValue::FunctionExpression(e) => ast::types::Value::FunctionExpression(e),
SingleValue::PipeSubstitution(e) => ast::types::Value::PipeSubstitution(e),
SingleValue::ArrayExpression(e) => ast::types::Value::ArrayExpression(e),
},
KclValueGroup::ObjectExpression(e) => ast::types::Value::ObjectExpression(e),
}
} }
} }

View File

@ -11,6 +11,7 @@ use kcl_lib::{
ast, ast,
ast::types::{BodyItem, FunctionExpressionParts, KclNone, LiteralValue, Program}, ast::types::{BodyItem, FunctionExpressionParts, KclNone, LiteralValue, Program},
}; };
use kcl_value_group::into_single_value;
use kittycad_execution_plan::{self as ep, Address, Destination, Instruction}; use kittycad_execution_plan::{self as ep, Address, Destination, Instruction};
use kittycad_execution_plan_traits as ept; use kittycad_execution_plan_traits as ept;
use kittycad_execution_plan_traits::NumericPrimitive; use kittycad_execution_plan_traits::NumericPrimitive;
@ -18,7 +19,7 @@ use kittycad_modeling_session::Session;
use self::binding_scope::{BindingScope, EpBinding, GetFnResult}; use self::binding_scope::{BindingScope, EpBinding, GetFnResult};
use self::error::{CompileError, Error}; use self::error::{CompileError, Error};
use self::kcl_value_group::{KclValueGroup, SingleValue}; use self::kcl_value_group::SingleValue;
/// Execute a KCL program by compiling into an execution plan, then running that. /// Execute a KCL program by compiling into an execution plan, then running that.
pub async fn execute(ast: Program, session: Option<Session>) -> Result<ep::Memory, Error> { pub async fn execute(ast: Program, session: Option<Session>) -> Result<ep::Memory, Error> {
@ -57,19 +58,17 @@ impl Planner {
} }
let mut ctx = Context::default(); let mut ctx = Context::default();
let instructions_for_this_node = match item { let instructions_for_this_node = match item {
BodyItem::ExpressionStatement(node) => match KclValueGroup::from(node.expression) { BodyItem::ExpressionStatement(node) => {
KclValueGroup::Single(value) => self.plan_to_compute_single(&mut ctx, value)?.instructions, self.plan_to_compute_single(&mut ctx, SingleValue::from(node.expression))?
KclValueGroup::ObjectExpression(_) => todo!(), .instructions
}, }
BodyItem::VariableDeclaration(node) => self.plan_to_bind(node)?, BodyItem::VariableDeclaration(node) => self.plan_to_bind(node)?,
BodyItem::ReturnStatement(node) => match KclValueGroup::from(node.argument) { BodyItem::ReturnStatement(node) => {
KclValueGroup::Single(value) => { let EvalPlan { instructions, binding } =
let EvalPlan { instructions, binding } = self.plan_to_compute_single(&mut ctx, value)?; self.plan_to_compute_single(&mut ctx, SingleValue::from(node.argument))?;
retval = Some(binding); retval = Some(binding);
instructions instructions
} }
KclValueGroup::ObjectExpression(_) => todo!(),
},
}; };
instructions.extend(instructions_for_this_node); instructions.extend(instructions_for_this_node);
Ok((instructions, retval)) Ok((instructions, retval))
@ -80,7 +79,7 @@ impl Planner {
/// Returns the instructions, and the destination address of the value. /// Returns the instructions, and the destination address of the value.
fn plan_to_compute_single(&mut self, ctx: &mut Context, value: SingleValue) -> Result<EvalPlan, CompileError> { fn plan_to_compute_single(&mut self, ctx: &mut Context, value: SingleValue) -> Result<EvalPlan, CompileError> {
match value { match value {
SingleValue::KclNoneExpression(KclNone { start: _, end: _ }) => { SingleValue::None(KclNone { start: _, end: _ }) => {
let address = self.next_addr.offset_by(1); let address = self.next_addr.offset_by(1);
Ok(EvalPlan { Ok(EvalPlan {
instructions: vec![Instruction::SetPrimitive { instructions: vec![Instruction::SetPrimitive {
@ -154,7 +153,7 @@ impl Planner {
}) })
} }
SingleValue::UnaryExpression(expr) => { SingleValue::UnaryExpression(expr) => {
let operand = self.plan_to_compute_single(ctx, SingleValue::from(expr.argument))?; let operand = self.plan_to_compute_single(ctx, into_single_value(expr.argument))?;
let EpBinding::Single(binding) = operand.binding else { let EpBinding::Single(binding) = operand.binding else {
return Err(CompileError::InvalidOperand( return Err(CompileError::InvalidOperand(
"you tried to use a composite value (e.g. array or object) as the operand to some math", "you tried to use a composite value (e.g. array or object) as the operand to some math",
@ -178,8 +177,8 @@ impl Planner {
}) })
} }
SingleValue::BinaryExpression(expr) => { SingleValue::BinaryExpression(expr) => {
let l = self.plan_to_compute_single(ctx, SingleValue::from(expr.left))?; let l = self.plan_to_compute_single(ctx, into_single_value(expr.left))?;
let r = self.plan_to_compute_single(ctx, SingleValue::from(expr.right))?; let r = self.plan_to_compute_single(ctx, into_single_value(expr.right))?;
let EpBinding::Single(l_binding) = l.binding else { let EpBinding::Single(l_binding) = l.binding else {
return Err(CompileError::InvalidOperand( return Err(CompileError::InvalidOperand(
"you tried to use a composite value (e.g. array or object) as the operand to some math", "you tried to use a composite value (e.g. array or object) as the operand to some math",
@ -226,10 +225,7 @@ impl Planner {
let EvalPlan { let EvalPlan {
instructions: new_instructions, instructions: new_instructions,
binding: arg, binding: arg,
} = match KclValueGroup::from(argument) { } = self.plan_to_compute_single(ctx, SingleValue::from(argument))?;
KclValueGroup::Single(value) => self.plan_to_compute_single(ctx, value)?,
KclValueGroup::ObjectExpression(expr) => self.plan_to_bind_object(ctx, *expr)?,
};
acc_instrs.extend(new_instructions); acc_instrs.extend(new_instructions);
acc_args.push(arg); acc_args.push(arg);
Ok((acc_instrs, acc_args)) Ok((acc_instrs, acc_args))
@ -405,17 +401,11 @@ impl Planner {
let EvalPlan { let EvalPlan {
mut instructions, mut instructions,
binding: mut current_value, binding: mut current_value,
} = match KclValueGroup::from(first) { } = self.plan_to_compute_single(ctx, SingleValue::from(first))?;
KclValueGroup::Single(v) => self.plan_to_compute_single(ctx, v)?,
KclValueGroup::ObjectExpression(_) => todo!(),
};
// Handle the remaining bodies. // Handle the remaining bodies.
for body in bodies { for body in bodies {
let value = match KclValueGroup::from(body) { let value = SingleValue::from(body);
KclValueGroup::Single(v) => v,
KclValueGroup::ObjectExpression(_) => todo!(),
};
// This body will probably contain a % (pipe substitution character). // This body will probably contain a % (pipe substitution character).
// So it needs to know what the previous pipeline body's value is, // So it needs to know what the previous pipeline body's value is,
// to replace the % with that value. // to replace the % with that value.
@ -435,6 +425,66 @@ impl Planner {
binding: current_value, binding: current_value,
}) })
} }
SingleValue::ObjectExpression(expr) => {
let length_at = self.next_addr.offset_by(1);
let key_count = expr.properties.len();
// Compute elements
let (instructions_for_each_element, bindings, keys) = expr.properties.into_iter().try_fold(
(Vec::new(), HashMap::new(), Vec::with_capacity(key_count)),
|(mut acc_instrs, mut acc_properties, mut acc_keys), property| {
let key = property.key.name;
acc_keys.push(key.clone());
// Some elements will have their own length header (e.g. arrays).
// For all other elements, we'll need to add a length header.
let element_has_its_own_header = matches!(
SingleValue::from(property.value.clone()),
SingleValue::ArrayExpression(_) | SingleValue::ObjectExpression(_)
);
let element_needs_its_own_header = !element_has_its_own_header;
let length_at = element_needs_its_own_header.then(|| self.next_addr.offset_by(1));
let instrs_for_this_element = {
// 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(ctx, SingleValue::from(property.value))?;
acc_properties.insert(key, binding);
instructions
};
// If we decided to add a length header for this element,
// this is where we actually add it.
if let Some(length_at) = length_at {
let length_of_this_element = (self.next_addr - length_at) - 1;
// Append element's length
acc_instrs.push(Instruction::SetPrimitive {
address: length_at,
value: length_of_this_element.into(),
});
}
// Append element's value
acc_instrs.extend(instrs_for_this_element);
Ok((acc_instrs, acc_properties, acc_keys))
},
)?;
// The array's overall instructions are:
// - Write a length header
// - Write everything to calculate its elements.
let mut instructions = vec![Instruction::SetPrimitive {
address: length_at,
value: ept::ObjectHeader {
properties: keys,
size: (self.next_addr - length_at) - 1,
}
.into(),
}];
instructions.extend(instructions_for_each_element);
let binding = EpBinding::Map {
length_at,
properties: bindings,
};
Ok(EvalPlan { instructions, binding })
}
SingleValue::ArrayExpression(expr) => { SingleValue::ArrayExpression(expr) => {
let length_at = self.next_addr.offset_by(1); let length_at = self.next_addr.offset_by(1);
let element_count = expr.elements.len(); let element_count = expr.elements.len();
@ -442,44 +492,22 @@ impl Planner {
let (instructions_for_each_element, bindings) = expr.elements.into_iter().try_fold( let (instructions_for_each_element, bindings) = expr.elements.into_iter().try_fold(
(Vec::new(), Vec::new()), (Vec::new(), Vec::new()),
|(mut acc_instrs, mut acc_bindings), element| { |(mut acc_instrs, mut acc_bindings), element| {
// Only write this element's length to memory if the element isn't an array. // Some elements will have their own length header (e.g. arrays).
// Why? // For all other elements, we'll need to add a length header.
// Because if the element is an array, then it'll have an array header let element_has_its_own_header = matches!(
// which shows its length instead. SingleValue::from(element.clone()),
let elem_is_array = matches!( SingleValue::ArrayExpression(_) | SingleValue::ObjectExpression(_)
KclValueGroup::from(element.clone()),
KclValueGroup::Single(SingleValue::ArrayExpression(_))
); );
let length_at = (!elem_is_array).then(|| self.next_addr.offset_by(1)); let element_needs_its_own_header = !element_has_its_own_header;
let length_at = element_needs_its_own_header.then(|| self.next_addr.offset_by(1));
let instrs_for_this_element = match KclValueGroup::from(element) { let instrs_for_this_element = {
KclValueGroup::Single(value) => { // If this element of the array is a single value, then binding it is
// 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.
// straightforward -- you got a single binding, no need to change anything. let EvalPlan { instructions, binding } =
let EvalPlan { instructions, binding } = self.plan_to_compute_single(ctx, value)?; self.plan_to_compute_single(ctx, SingleValue::from(element))?;
acc_bindings.push(binding); acc_bindings.push(binding);
instructions instructions
}
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 (instrs, binding) = expr
.properties
.into_iter()
.try_fold((Vec::new(), map), |(mut instrs, mut map), property| {
let EvalPlan { instructions, binding } =
self.plan_to_bind_one(ctx, property.value)?;
map.insert(property.key.name, binding);
instrs.extend(instructions);
Ok((instrs, map))
})
.map(|(ins, b)| (ins, EpBinding::Map(b)))?;
acc_bindings.push(binding);
instrs
}
}; };
// If we decided to add a length header for this element, // If we decided to add a length header for this element,
// this is where we actually add it. // this is where we actually add it.
@ -529,71 +557,13 @@ impl Planner {
.declarations .declarations
.into_iter() .into_iter()
.try_fold(Vec::new(), |mut acc, declaration| { .try_fold(Vec::new(), |mut acc, declaration| {
let EvalPlan { instructions, binding } = self.plan_to_bind_one(&mut ctx, declaration.init)?; let EvalPlan { instructions, binding } =
self.plan_to_compute_single(&mut ctx, SingleValue::from(declaration.init))?;
self.binding_scope.bind(declaration.id.name, binding); self.binding_scope.bind(declaration.id.name, binding);
acc.extend(instructions); acc.extend(instructions);
Ok(acc) Ok(acc)
}) })
} }
fn plan_to_bind_one(
&mut self,
ctx: &mut Context,
value_being_bound: ast::types::Value,
) -> Result<EvalPlan, 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.
self.plan_to_compute_single(ctx, init_value)
}
KclValueGroup::ObjectExpression(expr) => self.plan_to_bind_object(ctx, *expr),
}
}
fn plan_to_bind_object(
&mut self,
ctx: &mut Context,
expr: ast::types::ObjectExpression,
) -> Result<EvalPlan, CompileError> {
// 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(ctx, value)?;
acc_instrs.extend(instructions);
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 EvalPlan { instructions, binding } = self.plan_to_bind_one(ctx, 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(EvalPlan {
instructions,
binding: EpBinding::Map(each_property_binding),
})
}
} }
/// Every KCL literal value is equivalent to an Execution Plan value, and therefore can be /// Every KCL literal value is equivalent to an Execution Plan value, and therefore can be

View File

@ -1,5 +1,7 @@
use std::collections::HashMap;
use ep::{Destination, UnaryArithmetic}; use ep::{Destination, UnaryArithmetic};
use ept::ListHeader; use ept::{ListHeader, ObjectHeader};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*; use super::*;
@ -158,7 +160,7 @@ fn bind_arrays_with_objects_elements() {
// List header // List header
Instruction::SetPrimitive { Instruction::SetPrimitive {
address: Address::ZERO, address: Address::ZERO,
value: ListHeader { count: 2, size: 5 }.into() value: ListHeader { count: 2, size: 7 }.into()
}, },
// Array contents // Array contents
Instruction::SetPrimitive { Instruction::SetPrimitive {
@ -171,14 +173,26 @@ fn bind_arrays_with_objects_elements() {
}, },
Instruction::SetPrimitive { Instruction::SetPrimitive {
address: Address::ZERO + 3, address: Address::ZERO + 3,
value: 2usize.into() value: ObjectHeader {
size: 4,
properties: vec!["a".to_owned(), "b".to_owned(),]
}
.into(),
}, },
Instruction::SetPrimitive { Instruction::SetPrimitive {
address: Address::ZERO + 4, address: Address::ZERO + 4,
value: 55i64.into() value: 1usize.into(),
}, },
Instruction::SetPrimitive { Instruction::SetPrimitive {
address: Address::ZERO + 5, address: Address::ZERO + 5,
value: 55i64.into()
},
Instruction::SetPrimitive {
address: Address::ZERO + 6,
value: 1usize.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 7,
value: "sixty-six".to_owned().into() value: "sixty-six".to_owned().into()
}, },
] ]
@ -400,7 +414,7 @@ fn member_expressions_object() {
let (_plan, scope) = must_plan(program); let (_plan, scope) = must_plan(program);
match scope.get("prop").unwrap() { match scope.get("prop").unwrap() {
EpBinding::Single(addr) => { EpBinding::Single(addr) => {
assert_eq!(*addr, Address::ZERO + 1); assert_eq!(*addr, Address::ZERO + 4);
} }
other => { other => {
panic!("expected 'prop' bound to 0x0 but it was bound to {other:?}"); panic!("expected 'prop' bound to 0x0 but it was bound to {other:?}");
@ -833,32 +847,69 @@ fn store_object() {
let expected = vec![ let expected = vec![
Instruction::SetPrimitive { Instruction::SetPrimitive {
address: Address::ZERO, address: Address::ZERO,
value: ObjectHeader {
properties: vec!["a".to_owned(), "b".to_owned(), "c".to_owned()],
size: 7,
}
.into(),
},
// Key a header
Instruction::SetPrimitive {
address: Address::ZERO + 1,
value: 1usize.into(),
},
// Key a value
Instruction::SetPrimitive {
address: Address::ZERO + 2,
value: 1i64.into(), value: 1i64.into(),
}, },
// Key b header
Instruction::SetPrimitive { Instruction::SetPrimitive {
address: Address::ZERO.offset(1), address: Address::ZERO + 3,
value: 1usize.into(),
},
// Key b value
Instruction::SetPrimitive {
address: Address::ZERO + 4,
value: 2i64.into(), value: 2i64.into(),
}, },
// Inner object (i.e. key c) header
Instruction::SetPrimitive { Instruction::SetPrimitive {
address: Address::ZERO.offset(2), address: Address::ZERO + 5,
value: ObjectHeader {
properties: vec!["d".to_owned()],
size: 2,
}
.into(),
},
// Key d header
Instruction::SetPrimitive {
address: Address::ZERO + 6,
value: 1usize.into(),
},
// Key d value
Instruction::SetPrimitive {
address: Address::ZERO + 7,
value: 3i64.into(), value: 3i64.into(),
}, },
]; ];
assert_eq!(actual, expected); assert_eq!(actual, expected);
assert_eq!( let actual = bindings.get("x0").unwrap();
bindings.get("x0").unwrap(), let expected = EpBinding::Map {
&EpBinding::Map(HashMap::from([ length_at: Address::ZERO,
("a".to_owned(), EpBinding::Single(Address::ZERO),), properties: HashMap::from([
("b".to_owned(), EpBinding::Single(Address::ZERO.offset(1))), ("a".to_owned(), EpBinding::Single(Address::ZERO + 2)),
("b".to_owned(), EpBinding::Single(Address::ZERO + 4)),
( (
"c".to_owned(), "c".to_owned(),
EpBinding::Map(HashMap::from([( EpBinding::Map {
"d".to_owned(), length_at: Address::ZERO + 5,
EpBinding::Single(Address::ZERO.offset(2)) properties: HashMap::from([("d".to_owned(), EpBinding::Single(Address::ZERO + 7))]),
)])) },
), ),
])) ]),
) };
assert_eq!(actual, &expected)
} }
#[test] #[test]
@ -868,20 +919,24 @@ fn store_object_with_array_property() {
let expected = vec![ let expected = vec![
Instruction::SetPrimitive { Instruction::SetPrimitive {
address: Address::ZERO, address: Address::ZERO,
value: ObjectHeader {
properties: vec!["a".to_owned(), "b".to_owned()],
size: 7,
}
.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 1,
value: 1usize.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 2,
value: 1i64.into(), value: 1i64.into(),
}, },
// Array header // Array header
Instruction::SetPrimitive {
address: Address::ZERO + 1,
value: ListHeader { count: 2, size: 4 }.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 2,
value: 1usize.into(),
},
Instruction::SetPrimitive { Instruction::SetPrimitive {
address: Address::ZERO + 3, address: Address::ZERO + 3,
value: 2i64.into(), value: ListHeader { count: 2, size: 4 }.into(),
}, },
Instruction::SetPrimitive { Instruction::SetPrimitive {
address: Address::ZERO + 4, address: Address::ZERO + 4,
@ -889,25 +944,36 @@ fn store_object_with_array_property() {
}, },
Instruction::SetPrimitive { Instruction::SetPrimitive {
address: Address::ZERO + 5, address: Address::ZERO + 5,
value: 2i64.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 6,
value: 1usize.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 7,
value: 3i64.into(), value: 3i64.into(),
}, },
]; ];
assert_eq!(actual, expected); assert_eq!(actual, expected);
assert_eq!( assert_eq!(
bindings.get("x0").unwrap(), bindings.get("x0").unwrap(),
&EpBinding::Map(HashMap::from([ &EpBinding::Map {
("a".to_owned(), EpBinding::Single(Address::ZERO),), length_at: Address::ZERO,
( properties: HashMap::from([
"b".to_owned(), ("a".to_owned(), EpBinding::Single(Address::ZERO + 2)),
EpBinding::Sequence { (
length_at: Address::ZERO + 1, "b".to_owned(),
elements: vec![ EpBinding::Sequence {
EpBinding::Single(Address::ZERO + 3), length_at: Address::ZERO + 3,
EpBinding::Single(Address::ZERO + 5), elements: vec![
] EpBinding::Single(Address::ZERO + 5),
} EpBinding::Single(Address::ZERO + 7),
), ]
])) }
),
])
}
) )
} }
@ -934,13 +1000,28 @@ fn objects_as_parameters() {
// Object contents // Object contents
Instruction::SetPrimitive { Instruction::SetPrimitive {
address: Address::ZERO, address: Address::ZERO,
value: ObjectHeader {
properties: vec!["x".to_owned()],
size: 2,
}
.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 1,
value: 1usize.into(),
},
Instruction::SetPrimitive {
address: Address::ZERO + 2,
value: 1i64.into(), value: 1i64.into(),
}, },
]; ];
assert_eq!(plan, expected_plan); assert_eq!(plan, expected_plan);
assert_eq!( assert_eq!(
scope.get("obj").unwrap(), scope.get("obj").unwrap(),
&EpBinding::Map(HashMap::from([("x".to_owned(), EpBinding::Single(Address::ZERO)),])) &EpBinding::Map {
length_at: Address::ZERO,
properties: HashMap::from([("x".to_owned(), EpBinding::Single(Address::ZERO + 2))])
}
) )
} }