Grackle: Runtime computed array indices (#1331)
This commit is contained in:
55
src/wasm-lib/Cargo.lock
generated
55
src/wasm-lib/Cargo.lock
generated
@ -430,6 +430,12 @@ version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.14.0"
|
||||
@ -1426,6 +1432,7 @@ dependencies = [
|
||||
"kittycad-modeling-session",
|
||||
"pretty_assertions",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1943,7 +1950,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "kittycad-execution-plan"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#cd3e2c339b4794478e995247ca693d17c6b8ac4d"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#0a891ad3d4a189410f457e00b644ff4e116e7175"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"insta",
|
||||
@ -1953,6 +1960,7 @@ dependencies = [
|
||||
"kittycad-modeling-session",
|
||||
"parse-display-derive",
|
||||
"serde",
|
||||
"tabled",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"uuid",
|
||||
@ -1972,7 +1980,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "kittycad-execution-plan-macros"
|
||||
version = "0.1.2"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#cd3e2c339b4794478e995247ca693d17c6b8ac4d"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#0a891ad3d4a189410f457e00b644ff4e116e7175"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1981,9 +1989,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-execution-plan-traits"
|
||||
version = "0.1.3"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1486416eacecf481188a253199461fde5dbbc9bccaf176d60c48181cd0465273"
|
||||
checksum = "29dc558ca98b726d998fe9617f7cab0c427f54b49b42fb68d0a69d85e1b52dc7"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
@ -1993,7 +2001,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "kittycad-modeling-cmds"
|
||||
version = "0.1.12"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#cd3e2c339b4794478e995247ca693d17c6b8ac4d"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#0a891ad3d4a189410f457e00b644ff4e116e7175"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -2020,7 +2028,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "kittycad-modeling-session"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#cd3e2c339b4794478e995247ca693d17c6b8ac4d"
|
||||
source = "git+https://github.com/KittyCAD/modeling-api?branch=main#0a891ad3d4a189410f457e00b644ff4e116e7175"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"kittycad",
|
||||
@ -2527,6 +2535,17 @@ dependencies = [
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "papergrid"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2ccbe15f2b6db62f9a9871642746427e297b0ceb85f9a7f1ee5ff47d184d0c8"
|
||||
dependencies = [
|
||||
"bytecount",
|
||||
"fnv",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
@ -3920,6 +3939,30 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tabled"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfe9c3632da101aba5131ed63f9eed38665f8b3c68703a6bb18124835c1a5d22"
|
||||
dependencies = [
|
||||
"papergrid",
|
||||
"tabled_derive",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tabled_derive"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99f688a08b54f4f02f0a3c382aefdb7884d3d69609f785bd253dc033243e3fe4"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "take_mut"
|
||||
version = "0.2.2"
|
||||
|
||||
@ -60,7 +60,7 @@ members = [
|
||||
[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-execution-plan-traits = "0.1.5"
|
||||
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" }
|
||||
|
||||
@ -71,3 +71,8 @@ path = "tests/executor/main.rs"
|
||||
[[test]]
|
||||
name = "modify"
|
||||
path = "tests/modify/main.rs"
|
||||
|
||||
# Example: how to point modeling-api at a different repo (e.g. a branch or a local clone)
|
||||
# [patch."https://github.com/KittyCAD/modeling-api"]
|
||||
# kittycad-execution-plan = { path = "../../../modeling-api/execution-plan" }
|
||||
# kittycad-modeling-session = { path = "../../../modeling-api/modeling-session" }
|
||||
@ -12,7 +12,8 @@ kittycad-execution-plan = { workspace = true }
|
||||
kittycad-execution-plan-traits = { workspace = true }
|
||||
kittycad-modeling-session = { workspace = true }
|
||||
thiserror = "1.0.56"
|
||||
tokio = { version = "1.35.1", features = ["macros", "rt"] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
pretty_assertions = "1"
|
||||
|
||||
@ -5,7 +5,6 @@ use kcl_lib::ast::{self, types::BinaryPart};
|
||||
/// 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>),
|
||||
}
|
||||
|
||||
@ -21,6 +20,7 @@ pub enum SingleValue {
|
||||
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 {
|
||||
@ -59,7 +59,7 @@ impl From<ast::types::Value> for KclValueGroup {
|
||||
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::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)),
|
||||
@ -82,8 +82,8 @@ impl From<KclValueGroup> for ast::types::Value {
|
||||
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::ArrayExpression(e) => ast::types::Value::ArrayExpression(e),
|
||||
KclValueGroup::ObjectExpression(e) => ast::types::Value::ObjectExpression(e),
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,13 +7,11 @@ mod tests;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ep::Destination;
|
||||
use kcl_lib::{
|
||||
ast,
|
||||
ast::types::{BodyItem, FunctionExpressionParts, KclNone, LiteralValue, Program},
|
||||
};
|
||||
use kittycad_execution_plan as ep;
|
||||
use kittycad_execution_plan::{Address, Instruction};
|
||||
use kittycad_execution_plan::{self as ep, Address, Destination, Instruction};
|
||||
use kittycad_execution_plan_traits as ept;
|
||||
use kittycad_execution_plan_traits::NumericPrimitive;
|
||||
use kittycad_modeling_session::Session;
|
||||
@ -23,12 +21,12 @@ use self::error::{CompileError, Error};
|
||||
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> {
|
||||
pub async fn execute(ast: Program, session: Option<Session>) -> Result<ep::Memory, Error> {
|
||||
let mut planner = Planner::new();
|
||||
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(())
|
||||
let mut mem = ep::Memory::default();
|
||||
ep::execute(&mut mem, plan, session).await?;
|
||||
Ok(mem)
|
||||
}
|
||||
|
||||
/// Compiles KCL programs into Execution Plans.
|
||||
@ -61,7 +59,6 @@ impl Planner {
|
||||
let instructions_for_this_node = match item {
|
||||
BodyItem::ExpressionStatement(node) => match KclValueGroup::from(node.expression) {
|
||||
KclValueGroup::Single(value) => self.plan_to_compute_single(&mut ctx, value)?.instructions,
|
||||
KclValueGroup::ArrayExpression(_) => todo!(),
|
||||
KclValueGroup::ObjectExpression(_) => todo!(),
|
||||
},
|
||||
BodyItem::VariableDeclaration(node) => self.plan_to_bind(node)?,
|
||||
@ -71,7 +68,6 @@ impl Planner {
|
||||
retval = Some(binding);
|
||||
instructions
|
||||
}
|
||||
KclValueGroup::ArrayExpression(_) => todo!(),
|
||||
KclValueGroup::ObjectExpression(_) => todo!(),
|
||||
},
|
||||
};
|
||||
@ -232,7 +228,6 @@ impl Planner {
|
||||
binding: arg,
|
||||
} = match KclValueGroup::from(argument) {
|
||||
KclValueGroup::Single(value) => self.plan_to_compute_single(ctx, value)?,
|
||||
KclValueGroup::ArrayExpression(expr) => self.plan_to_bind_array(ctx, *expr)?,
|
||||
KclValueGroup::ObjectExpression(expr) => self.plan_to_bind_object(ctx, *expr)?,
|
||||
};
|
||||
acc_instrs.extend(new_instructions);
|
||||
@ -335,17 +330,62 @@ impl Planner {
|
||||
let (properties, id) = parse();
|
||||
let name = id.name;
|
||||
let mut binding = self.binding_scope.get(&name).ok_or(CompileError::Undefined { name })?;
|
||||
for (property, computed) in properties {
|
||||
if computed {
|
||||
todo!("Support computed properties like '{:?}'", property);
|
||||
} else {
|
||||
if properties.iter().any(|(_property, computed)| *computed) {
|
||||
// There's a computed property, so the property/index can only be determined at runtime.
|
||||
let mut instructions: Vec<Instruction> = Vec::new();
|
||||
// For now we can only handle computed properties of *arrays*, no other type.
|
||||
let mut start_of_array = match binding {
|
||||
EpBinding::Sequence { length_at, elements: _ } => *length_at,
|
||||
_ => todo!("handle computed properties besides arrays"),
|
||||
};
|
||||
for (property, _computed) in properties {
|
||||
// Where is the index stored?
|
||||
let index = match property {
|
||||
// If it's some identifier, then look up where that identifier will be stored.
|
||||
// That's the memory address the index should be in.
|
||||
ast::types::LiteralIdentifier::Identifier(id) => {
|
||||
let b = self
|
||||
.binding_scope
|
||||
.get(&id.name)
|
||||
.ok_or(CompileError::Undefined { name: id.name })?;
|
||||
match b {
|
||||
EpBinding::Single(addr) => ep::Operand::Reference(*addr),
|
||||
// TODO use a better error message here
|
||||
other => return Err(CompileError::InvalidIndex(format!("{other:?}"))),
|
||||
}
|
||||
}
|
||||
// If the index is a literal, then just use it.
|
||||
ast::types::LiteralIdentifier::Literal(litval) => {
|
||||
ep::Operand::Literal(kcl_literal_to_kcep_literal(litval.value))
|
||||
}
|
||||
};
|
||||
instructions.push(Instruction::GetElement {
|
||||
index,
|
||||
start: start_of_array,
|
||||
});
|
||||
// Point `start_of_array` to the location we just indexed, so that if there's
|
||||
// another index expression to a child array, `start_of_array` is in the right place.
|
||||
start_of_array = self.next_addr.offset_by(1);
|
||||
instructions.push(Instruction::StackPop {
|
||||
destination: Some(start_of_array),
|
||||
});
|
||||
}
|
||||
Ok(EvalPlan {
|
||||
instructions,
|
||||
binding: EpBinding::Single(start_of_array),
|
||||
})
|
||||
} else {
|
||||
// Compiler optimization:
|
||||
// Because there are no computed properties, we can resolve the property chain
|
||||
// at compile-time. Just jump to the right property at each step in the chain.
|
||||
for (property, _) in properties {
|
||||
binding = binding.property_of(property)?;
|
||||
}
|
||||
Ok(EvalPlan {
|
||||
instructions: Vec::new(),
|
||||
binding: binding.clone(),
|
||||
})
|
||||
}
|
||||
Ok(EvalPlan {
|
||||
instructions: Vec::new(),
|
||||
binding: binding.clone(),
|
||||
})
|
||||
}
|
||||
SingleValue::PipeSubstitution(_expr) => {
|
||||
if let Some(ref binding) = ctx.pipe_substitution {
|
||||
@ -367,7 +407,6 @@ impl Planner {
|
||||
binding: mut current_value,
|
||||
} = match KclValueGroup::from(first) {
|
||||
KclValueGroup::Single(v) => self.plan_to_compute_single(ctx, v)?,
|
||||
KclValueGroup::ArrayExpression(_) => todo!(),
|
||||
KclValueGroup::ObjectExpression(_) => todo!(),
|
||||
};
|
||||
|
||||
@ -375,7 +414,6 @@ impl Planner {
|
||||
for body in bodies {
|
||||
let value = match KclValueGroup::from(body) {
|
||||
KclValueGroup::Single(v) => v,
|
||||
KclValueGroup::ArrayExpression(_) => todo!(),
|
||||
KclValueGroup::ObjectExpression(_) => todo!(),
|
||||
};
|
||||
// This body will probably contain a % (pipe substitution character).
|
||||
@ -397,6 +435,85 @@ impl Planner {
|
||||
binding: current_value,
|
||||
})
|
||||
}
|
||||
SingleValue::ArrayExpression(expr) => {
|
||||
let length_at = self.next_addr.offset_by(1);
|
||||
let element_count = expr.elements.len();
|
||||
// Compute elements
|
||||
let (instructions_for_each_element, bindings) = expr.elements.into_iter().try_fold(
|
||||
(Vec::new(), Vec::new()),
|
||||
|(mut acc_instrs, mut acc_bindings), element| {
|
||||
// Only write this element's length to memory if the element isn't an array.
|
||||
// Why?
|
||||
// Because if the element is an array, then it'll have an array header
|
||||
// which shows its length instead.
|
||||
let elem_is_array = matches!(
|
||||
KclValueGroup::from(element.clone()),
|
||||
KclValueGroup::Single(SingleValue::ArrayExpression(_))
|
||||
);
|
||||
let length_at = (!elem_is_array).then(|| self.next_addr.offset_by(1));
|
||||
|
||||
let instrs_for_this_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(ctx, value)?;
|
||||
acc_bindings.push(binding);
|
||||
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,
|
||||
// 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_bindings))
|
||||
},
|
||||
)?;
|
||||
// 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::ListHeader {
|
||||
count: element_count,
|
||||
size: (self.next_addr - length_at) - 1,
|
||||
}
|
||||
.into(),
|
||||
}];
|
||||
instructions.extend(instructions_for_each_element);
|
||||
let binding = EpBinding::Sequence {
|
||||
length_at,
|
||||
elements: bindings,
|
||||
};
|
||||
Ok(EvalPlan { instructions, binding })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -430,7 +547,6 @@ impl Planner {
|
||||
// and bind it to the KCL identifier.
|
||||
self.plan_to_compute_single(ctx, init_value)
|
||||
}
|
||||
KclValueGroup::ArrayExpression(expr) => self.plan_to_bind_array(ctx, *expr),
|
||||
KclValueGroup::ObjectExpression(expr) => self.plan_to_bind_object(ctx, *expr),
|
||||
}
|
||||
}
|
||||
@ -451,28 +567,6 @@ impl Planner {
|
||||
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 length_at = self.next_addr.offset_by(1);
|
||||
acc_instrs.push(Instruction::SetPrimitive {
|
||||
address: length_at,
|
||||
value: expr.elements.len().into(),
|
||||
});
|
||||
let binding = expr
|
||||
.elements
|
||||
.into_iter()
|
||||
.try_fold(Vec::with_capacity(n), |mut seq, child_element| {
|
||||
let EvalPlan { instructions, binding } = self.plan_to_bind_one(ctx, child_element)?;
|
||||
seq.push(binding);
|
||||
acc_instrs.extend(instructions);
|
||||
Ok(seq)
|
||||
})
|
||||
.map(|elements| EpBinding::Sequence { length_at, elements })?;
|
||||
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.
|
||||
@ -500,86 +594,6 @@ impl Planner {
|
||||
binding: EpBinding::Map(each_property_binding),
|
||||
})
|
||||
}
|
||||
|
||||
fn plan_to_bind_array(
|
||||
&mut self,
|
||||
ctx: &mut Context,
|
||||
expr: ast::types::ArrayExpression,
|
||||
) -> Result<EvalPlan, CompileError> {
|
||||
// First, emit a plan to compute each element of the array.
|
||||
// Collect all the bindings from each element too.
|
||||
let length_at = self.next_addr.offset_by(1);
|
||||
let mut instructions = vec![Instruction::SetPrimitive {
|
||||
address: length_at,
|
||||
value: expr.elements.len().into(),
|
||||
}];
|
||||
let (instrs, 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(ctx, 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 sublength_at = self.next_addr.offset_by(1);
|
||||
acc_instrs.push(Instruction::SetPrimitive {
|
||||
address: sublength_at,
|
||||
value: expr.elements.len().into(),
|
||||
});
|
||||
let binding = expr
|
||||
.elements
|
||||
.into_iter()
|
||||
.try_fold(Vec::new(), |mut seq, child_element| {
|
||||
let EvalPlan { instructions, binding } = self.plan_to_bind_one(ctx, child_element)?;
|
||||
acc_instrs.extend(instructions);
|
||||
seq.push(binding);
|
||||
Ok(seq)
|
||||
})
|
||||
.map(|elements| EpBinding::Sequence {
|
||||
length_at: sublength_at,
|
||||
elements,
|
||||
})?;
|
||||
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 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.push(binding);
|
||||
}
|
||||
};
|
||||
Ok((acc_instrs, acc_bindings))
|
||||
},
|
||||
)?;
|
||||
instructions.extend(instrs);
|
||||
Ok(EvalPlan {
|
||||
instructions,
|
||||
binding: EpBinding::Sequence {
|
||||
length_at,
|
||||
elements: bindings,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Every KCL literal value is equivalent to an Execution Plan value, and therefore can be
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use ep::{Destination, UnaryArithmetic};
|
||||
use ept::ListHeader;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
@ -42,7 +43,7 @@ fn assignments() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_array() {
|
||||
fn bind_array_simple() {
|
||||
let program = r#"let x = [44, 55, "sixty-six"]"#;
|
||||
let (plan, _scope) = must_plan(program);
|
||||
assert_eq!(
|
||||
@ -51,19 +52,40 @@ fn bind_array() {
|
||||
// Array length
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 3usize.into()
|
||||
value: ListHeader {
|
||||
// The list has 3 elements
|
||||
count: 3,
|
||||
// The 3 elements each take 2 primitives (one for length, one for value),
|
||||
// so 6 in total.
|
||||
size: 6
|
||||
}
|
||||
.into()
|
||||
},
|
||||
// Array contents
|
||||
// Elem 0
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 1,
|
||||
value: 44i64.into(),
|
||||
value: 1usize.into()
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 2,
|
||||
value: 55i64.into(),
|
||||
value: 44i64.into(),
|
||||
},
|
||||
// Elem 1
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 3,
|
||||
value: 1usize.into()
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 4,
|
||||
value: 55i64.into(),
|
||||
},
|
||||
// Elem 2
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 5,
|
||||
value: 1usize.into()
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 6,
|
||||
value: "sixty-six".to_owned().into(),
|
||||
}
|
||||
]
|
||||
@ -80,27 +102,48 @@ fn bind_nested_array() {
|
||||
// Outer array length
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 2usize.into(),
|
||||
value: ListHeader {
|
||||
count: 2,
|
||||
// 2 for each of the 3 elements, plus 1 for the inner array header.
|
||||
size: 2 + 2 + 2 + 1,
|
||||
}
|
||||
.into(),
|
||||
},
|
||||
// Outer array contents
|
||||
// Outer array element 0 length
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 1,
|
||||
value: 44i64.into(),
|
||||
value: 1usize.into(),
|
||||
},
|
||||
// Inner array length
|
||||
// Outer array element 0 value
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 2,
|
||||
value: 2usize.into(),
|
||||
value: 44i64.into(),
|
||||
},
|
||||
// Inner array length
|
||||
// Outer array element 1 length (i.e. inner array header)
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 3,
|
||||
value: 55i64.into(),
|
||||
value: ListHeader { count: 2, size: 4 }.into(),
|
||||
},
|
||||
// Inner array elem0 length
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 4,
|
||||
value: 1usize.into(),
|
||||
},
|
||||
// Inner array elem0 value
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 5,
|
||||
value: 55i64.into(),
|
||||
},
|
||||
// Inner array elem1 length
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 6,
|
||||
value: 1usize.into(),
|
||||
},
|
||||
// Inner array elem1 value
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 7,
|
||||
value: "sixty-six".to_owned().into(),
|
||||
}
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
@ -112,24 +155,32 @@ fn bind_arrays_with_objects_elements() {
|
||||
assert_eq!(
|
||||
plan,
|
||||
vec![
|
||||
// Array length
|
||||
// List header
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 2usize.into()
|
||||
value: ListHeader { count: 2, size: 5 }.into()
|
||||
},
|
||||
// Array contents
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 1,
|
||||
value: 44i64.into(),
|
||||
value: 1usize.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 2,
|
||||
value: 55i64.into(),
|
||||
value: 44i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 3,
|
||||
value: "sixty-six".to_owned().into(),
|
||||
}
|
||||
value: 2usize.into()
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 4,
|
||||
value: 55i64.into()
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 5,
|
||||
value: "sixty-six".to_owned().into()
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
@ -224,23 +275,101 @@ fn use_native_function_id() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "haven't done computed properties yet"]
|
||||
fn computed_array_index() {
|
||||
#[tokio::test]
|
||||
async fn computed_array_index() {
|
||||
let program = r#"
|
||||
let array = ["a", "b", "c"]
|
||||
let index = 1+1
|
||||
let index = 1+1 // should be "c"
|
||||
let prop = array[index]
|
||||
"#;
|
||||
let (_plan, scope) = must_plan(program);
|
||||
match scope.get("prop").unwrap() {
|
||||
EpBinding::Single(addr) => {
|
||||
assert_eq!(*addr, Address::ZERO + 1);
|
||||
}
|
||||
other => {
|
||||
panic!("expected 'prop' bound to 0x0 but it was bound to {other:?}");
|
||||
}
|
||||
let (plan, scope) = must_plan(program);
|
||||
let expected_address_of_prop = Address::ZERO + 10;
|
||||
if let Some(EpBinding::Single(addr)) = scope.get("prop") {
|
||||
assert_eq!(*addr, expected_address_of_prop);
|
||||
} else {
|
||||
panic!("expected 'prop' bound to 0 but it was {:?}", scope.get("prop"));
|
||||
}
|
||||
assert_eq!(
|
||||
plan,
|
||||
vec![
|
||||
// Setting the array
|
||||
// First, the length of the array (number of elements).
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: ListHeader { count: 3, size: 6 }.into()
|
||||
},
|
||||
// Elem 0 length
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 1,
|
||||
value: 1usize.into()
|
||||
},
|
||||
// Elem 0 value
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 2,
|
||||
value: "a".to_owned().into()
|
||||
},
|
||||
// Elem 1 length
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 3,
|
||||
value: 1usize.into()
|
||||
},
|
||||
// Elem 1 value
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 4,
|
||||
value: "b".to_owned().into()
|
||||
},
|
||||
// Elem 2 length
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 5,
|
||||
value: 1usize.into()
|
||||
},
|
||||
// Elem 2 value
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 6,
|
||||
value: "c".to_owned().into()
|
||||
},
|
||||
// Calculate the index (1+1)
|
||||
// First, the left operand
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 7,
|
||||
value: 1i64.to_owned().into()
|
||||
},
|
||||
// Then the right operand
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 8,
|
||||
value: 1i64.to_owned().into()
|
||||
},
|
||||
// Then index, which is left operand + right operand
|
||||
Instruction::BinaryArithmetic {
|
||||
arithmetic: ep::BinaryArithmetic {
|
||||
operation: ep::BinaryOperation::Add,
|
||||
operand0: ep::Operand::Reference(Address::ZERO + 7),
|
||||
operand1: ep::Operand::Reference(Address::ZERO + 8)
|
||||
},
|
||||
destination: Destination::Address(Address::ZERO + 9)
|
||||
},
|
||||
// Get the element at the index
|
||||
Instruction::GetElement {
|
||||
start: Address::ZERO,
|
||||
index: ep::Operand::Reference(Address::ZERO + 9)
|
||||
},
|
||||
// Write it to the next free address.
|
||||
Instruction::StackPop {
|
||||
destination: Some(expected_address_of_prop)
|
||||
},
|
||||
]
|
||||
);
|
||||
// Now let's run the program and check what's actually in the memory afterwards.
|
||||
let tokens = kcl_lib::token::lexer(program);
|
||||
let parser = kcl_lib::parser::Parser::new(tokens);
|
||||
let ast = parser.ast().unwrap();
|
||||
let mem = crate::execute(ast, None).await.unwrap();
|
||||
use ept::ReadMemory;
|
||||
// Should be "b", as pointed out in the KCL program's comment.
|
||||
assert_eq!(
|
||||
mem.get(&expected_address_of_prop).unwrap(),
|
||||
&ept::Primitive::String("c".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -281,26 +410,40 @@ fn member_expressions_object() {
|
||||
|
||||
#[test]
|
||||
fn member_expressions_array() {
|
||||
let program = "
|
||||
let array = [[1,2],[3,4]]
|
||||
let program = r#"
|
||||
let array = [["a", "b"],["c", "d"]]
|
||||
let first = array[0][0]
|
||||
let last = array[1][1]
|
||||
";
|
||||
"#;
|
||||
/*
|
||||
Memory layout:
|
||||
Header(2, 10) // outer array
|
||||
Header(2, 4) // first inner array
|
||||
1
|
||||
a
|
||||
1
|
||||
b
|
||||
Header(2,4) // second inner array
|
||||
1
|
||||
c
|
||||
1
|
||||
d
|
||||
*/
|
||||
let (_plan, scope) = must_plan(program);
|
||||
match scope.get("first").unwrap() {
|
||||
EpBinding::Single(addr) => {
|
||||
assert_eq!(*addr, Address::ZERO + 2);
|
||||
assert_eq!(*addr, Address::ZERO + 3);
|
||||
}
|
||||
other => {
|
||||
panic!("expected 'number' bound to 0x0 but it was bound to {other:?}");
|
||||
panic!("expected 'number' bound to addr 3 but it was bound to {other:?}");
|
||||
}
|
||||
}
|
||||
match scope.get("last").unwrap() {
|
||||
EpBinding::Single(addr) => {
|
||||
assert_eq!(*addr, Address::ZERO + 6);
|
||||
assert_eq!(*addr, Address::ZERO + 10);
|
||||
}
|
||||
other => {
|
||||
panic!("expected 'number' bound to 0x3 but it was bound to {other:?}");
|
||||
panic!("expected 'number' bound to addr 10 but it was bound to {other:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -702,7 +845,6 @@ fn store_object() {
|
||||
},
|
||||
];
|
||||
assert_eq!(actual, expected);
|
||||
eprintln!("{bindings:#?}");
|
||||
assert_eq!(
|
||||
bindings.get("x0").unwrap(),
|
||||
&EpBinding::Map(HashMap::from([
|
||||
@ -728,17 +870,25 @@ fn store_object_with_array_property() {
|
||||
address: Address::ZERO,
|
||||
value: 1i64.into(),
|
||||
},
|
||||
// Array length
|
||||
// Array header
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 1,
|
||||
value: 2usize.into(),
|
||||
value: ListHeader { count: 2, size: 4 }.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 2,
|
||||
value: 2i64.into(),
|
||||
value: 1usize.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 3,
|
||||
value: 2i64.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 4,
|
||||
value: 1usize.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 5,
|
||||
value: 3i64.into(),
|
||||
},
|
||||
];
|
||||
@ -752,8 +902,8 @@ fn store_object_with_array_property() {
|
||||
EpBinding::Sequence {
|
||||
length_at: Address::ZERO + 1,
|
||||
elements: vec![
|
||||
EpBinding::Single(Address::ZERO + 2),
|
||||
EpBinding::Single(Address::ZERO + 3),
|
||||
EpBinding::Single(Address::ZERO + 5),
|
||||
]
|
||||
}
|
||||
),
|
||||
@ -796,27 +946,42 @@ fn objects_as_parameters() {
|
||||
|
||||
#[test]
|
||||
fn arrays_as_parameters() {
|
||||
let program = "fn identity = (x) => { return x }
|
||||
let array = identity([1,2,3])";
|
||||
let program = r#"fn identity = (x) => { return x }
|
||||
let array = identity(["a","b","c"])"#;
|
||||
let (plan, scope) = must_plan(program);
|
||||
const INDEX_OF_A: usize = 2;
|
||||
const INDEX_OF_B: usize = 4;
|
||||
const INDEX_OF_C: usize = 6;
|
||||
let expected_plan = vec![
|
||||
// Array length
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO,
|
||||
value: 3usize.into(),
|
||||
value: ListHeader { count: 3, size: 6 }.into(),
|
||||
},
|
||||
// Array contents
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 1,
|
||||
value: 1i64.into(),
|
||||
value: 1usize.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 2,
|
||||
value: 2i64.into(),
|
||||
address: Address::ZERO + INDEX_OF_A,
|
||||
value: "a".to_owned().into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 3,
|
||||
value: 3i64.into(),
|
||||
value: 1usize.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + INDEX_OF_B,
|
||||
value: "b".to_owned().into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + 5,
|
||||
value: 1usize.into(),
|
||||
},
|
||||
Instruction::SetPrimitive {
|
||||
address: Address::ZERO + INDEX_OF_C,
|
||||
value: "c".to_owned().into(),
|
||||
},
|
||||
];
|
||||
assert_eq!(plan, expected_plan);
|
||||
@ -825,9 +990,9 @@ fn arrays_as_parameters() {
|
||||
&EpBinding::Sequence {
|
||||
length_at: Address::ZERO,
|
||||
elements: vec![
|
||||
EpBinding::Single(Address::ZERO + 1),
|
||||
EpBinding::Single(Address::ZERO + 2),
|
||||
EpBinding::Single(Address::ZERO + 3),
|
||||
EpBinding::Single(Address::ZERO + INDEX_OF_A),
|
||||
EpBinding::Single(Address::ZERO + INDEX_OF_B),
|
||||
EpBinding::Single(Address::ZERO + INDEX_OF_C),
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user