Grackle: Runtime computed array indices (#1331)

This commit is contained in:
Adam Chalmers
2024-01-29 17:36:29 +11:00
committed by GitHub
parent dcbe5d7f75
commit c6249f36d2
6 changed files with 415 additions and 187 deletions

View File

@ -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"

View File

@ -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" }

View File

@ -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"

View File

@ -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),
}
}

View File

@ -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

View File

@ -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),
]
}
)