Implement dynamic ranges (#4151)

Closes #4021

Allows array ranges (e.g., `[0..10]`) to take expression instead of just numeric literals as their start and end values. Both expressions are required (we don't support `[0..]`, etc.).

I've created a new kind of expression in the AST. The alternative was to represent the internals of an array as some kind of pattern which could initially be fully explicit or ranges. I figured the chosen version was simpler and easier to extend to open ranges, whereas the latter would be easier to extend to mixed ranges or other patterns. I chose simpler, it'll be easy enough to refactor if necessary.

Parsing is tested implicitly by the tests of execution and unparsing.

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
This commit is contained in:
Nick Cameron
2024-10-17 02:58:04 +13:00
committed by GitHub
parent fbac9935fe
commit 248ef8ebb3
15 changed files with 655 additions and 284 deletions

File diff suppressed because one or more lines are too long

View File

@ -32,7 +32,7 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
fn decagon = (radius) => {
step = 1 / 10 * tau()
sketch001 = startSketchAt([cos(0) * radius, sin(0) * radius])
return reduce([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], sketch001, (i, sg) => {
return reduce([1..10], sketch001, (i, sg) => {
x = cos(step * i) * radius
y = sin(step * i) * radius
return lineTo([x, y], sg)

View File

@ -83233,6 +83233,56 @@
}
}
},
{
"type": "object",
"required": [
"end",
"endElement",
"endInclusive",
"start",
"startElement",
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"ArrayRangeExpression"
]
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"startElement": {
"$ref": "#/components/schemas/Expr"
},
"endElement": {
"$ref": "#/components/schemas/Expr"
},
"endInclusive": {
"description": "Is the `end_element` included in the range?",
"type": "boolean"
},
"digest": {
"type": "array",
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"maxItems": 32,
"minItems": 32,
"nullable": true
}
}
},
{
"type": "object",
"required": [
@ -86831,6 +86881,56 @@
}
}
},
{
"type": "object",
"required": [
"end",
"endElement",
"endInclusive",
"start",
"startElement",
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"ArrayRangeExpression"
]
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"startElement": {
"$ref": "#/components/schemas/Expr"
},
"endElement": {
"$ref": "#/components/schemas/Expr"
},
"endInclusive": {
"description": "Is the `end_element` included in the range?",
"type": "boolean"
},
"digest": {
"type": "array",
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"maxItems": 32,
"minItems": 32,
"nullable": true
}
}
},
{
"type": "object",
"required": [
@ -90433,6 +90533,56 @@
}
}
},
{
"type": "object",
"required": [
"end",
"endElement",
"endInclusive",
"start",
"startElement",
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"ArrayRangeExpression"
]
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"startElement": {
"$ref": "#/components/schemas/Expr"
},
"endElement": {
"$ref": "#/components/schemas/Expr"
},
"endInclusive": {
"description": "Is the `end_element` included in the range?",
"type": "boolean"
},
"digest": {
"type": "array",
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"maxItems": 32,
"minItems": 32,
"nullable": true
}
}
},
{
"type": "object",
"required": [
@ -91704,8 +91854,8 @@
"unpublished": false,
"deprecated": false,
"examples": [
"r = 10 // radius\nfn drawCircle = (id) => {\n return startSketchOn(\"XY\")\n |> circle({ center: [id * 2 * r, 0], radius: r }, %)\n}\n\n// Call `drawCircle`, passing in each element of the array.\n// The outputs from each `drawCircle` form a new array,\n// which is the return value from `map`.\ncircles = map([1, 2, 3], drawCircle)",
"r = 10 // radius\n// Call `map`, using an anonymous function instead of a named one.\ncircles = map([1, 2, 3], (id) => {\n return startSketchOn(\"XY\")\n |> circle({ center: [id * 2 * r, 0], radius: r }, %)\n})"
"r = 10 // radius\nfn drawCircle = (id) => {\n return startSketchOn(\"XY\")\n |> circle({ center: [id * 2 * r, 0], radius: r }, %)\n}\n\n// Call `drawCircle`, passing in each element of the array.\n// The outputs from each `drawCircle` form a new array,\n// which is the return value from `map`.\ncircles = map([1..3], drawCircle)",
"r = 10 // radius\n// Call `map`, using an anonymous function instead of a named one.\ncircles = map([1..3], (id) => {\n return startSketchOn(\"XY\")\n |> circle({ center: [id * 2 * r, 0], radius: r }, %)\n})"
]
},
{
@ -114887,6 +115037,56 @@
}
}
},
{
"type": "object",
"required": [
"end",
"endElement",
"endInclusive",
"start",
"startElement",
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"ArrayRangeExpression"
]
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"startElement": {
"$ref": "#/components/schemas/Expr"
},
"endElement": {
"$ref": "#/components/schemas/Expr"
},
"endInclusive": {
"description": "Is the `end_element` included in the range?",
"type": "boolean"
},
"digest": {
"type": "array",
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"maxItems": 32,
"minItems": 32,
"nullable": true
}
}
},
{
"type": "object",
"required": [
@ -118878,6 +119078,56 @@
}
}
},
{
"type": "object",
"required": [
"end",
"endElement",
"endInclusive",
"start",
"startElement",
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"ArrayRangeExpression"
]
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"startElement": {
"$ref": "#/components/schemas/Expr"
},
"endElement": {
"$ref": "#/components/schemas/Expr"
},
"endInclusive": {
"description": "Is the `end_element` included in the range?",
"type": "boolean"
},
"digest": {
"type": "array",
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"maxItems": 32,
"minItems": 32,
"nullable": true
}
}
},
{
"type": "object",
"required": [
@ -122476,6 +122726,56 @@
}
}
},
{
"type": "object",
"required": [
"end",
"endElement",
"endInclusive",
"start",
"startElement",
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"ArrayRangeExpression"
]
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"startElement": {
"$ref": "#/components/schemas/Expr"
},
"endElement": {
"$ref": "#/components/schemas/Expr"
},
"endInclusive": {
"description": "Is the `end_element` included in the range?",
"type": "boolean"
},
"digest": {
"type": "array",
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"maxItems": 32,
"minItems": 32,
"nullable": true
}
}
},
{
"type": "object",
"required": [
@ -126072,6 +126372,56 @@
}
}
},
{
"type": "object",
"required": [
"end",
"endElement",
"endInclusive",
"start",
"startElement",
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"ArrayRangeExpression"
]
},
"start": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"end": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"startElement": {
"$ref": "#/components/schemas/Expr"
},
"endElement": {
"$ref": "#/components/schemas/Expr"
},
"endInclusive": {
"description": "Is the `end_element` included in the range?",
"type": "boolean"
},
"digest": {
"type": "array",
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
},
"maxItems": 32,
"minItems": 32,
"nullable": true
}
}
},
{
"type": "object",
"required": [
@ -127739,7 +128089,7 @@
"unpublished": false,
"deprecated": false,
"examples": [
"fn decagon = (radius) => {\n step = 1 / 10 * tau()\n sketch001 = startSketchAt([cos(0) * radius, sin(0) * radius])\n return reduce([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], sketch001, (i, sg) => {\n x = cos(step * i) * radius\n y = sin(step * i) * radius\n return lineTo([x, y], sg)\n})\n}\ndecagon(5.0)\n |> close(%)",
"fn decagon = (radius) => {\n step = 1 / 10 * tau()\n sketch001 = startSketchAt([cos(0) * radius, sin(0) * radius])\n return reduce([1..10], sketch001, (i, sg) => {\n x = cos(step * i) * radius\n y = sin(step * i) * radius\n return lineTo([x, y], sg)\n})\n}\ndecagon(5.0)\n |> close(%)",
"array = [1, 2, 3]\nsum = reduce(array, 0, (i, result_so_far) => {\n return i + result_so_far\n})\nassertEqual(sum, 6, 0.00001, \"1 + 2 + 3 summed is 6\")",
"fn add = (a, b) => {\n return a + b\n}\nfn sum = (array) => {\n return reduce(array, 0, add)\n}\nassertEqual(sum([1, 2, 3]), 6, 0.00001, \"1 + 2 + 3 summed is 6\")"
]

View File

@ -197,6 +197,27 @@ An expression can be evaluated to yield a single KCL value.
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayRangeExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `startElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endInclusive` |`boolean`| Is the `end_element` included in the range? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |

View File

@ -513,6 +513,7 @@ pub enum Expr {
PipeExpression(Box<PipeExpression>),
PipeSubstitution(Box<PipeSubstitution>),
ArrayExpression(Box<ArrayExpression>),
ArrayRangeExpression(Box<ArrayRangeExpression>),
ObjectExpression(Box<ObjectExpression>),
MemberExpression(Box<MemberExpression>),
UnaryExpression(Box<UnaryExpression>),
@ -532,6 +533,7 @@ impl Expr {
Expr::PipeExpression(pe) => pe.compute_digest(),
Expr::PipeSubstitution(ps) => ps.compute_digest(),
Expr::ArrayExpression(ae) => ae.compute_digest(),
Expr::ArrayRangeExpression(are) => are.compute_digest(),
Expr::ObjectExpression(oe) => oe.compute_digest(),
Expr::MemberExpression(me) => me.compute_digest(),
Expr::UnaryExpression(ue) => ue.compute_digest(),
@ -569,6 +571,7 @@ impl Expr {
match self {
Expr::BinaryExpression(_bin_exp) => None,
Expr::ArrayExpression(_array_exp) => None,
Expr::ArrayRangeExpression(_array_exp) => None,
Expr::ObjectExpression(_obj_exp) => None,
Expr::MemberExpression(_mem_exp) => None,
Expr::Literal(_literal) => None,
@ -593,6 +596,7 @@ impl Expr {
match self {
Expr::BinaryExpression(ref mut bin_exp) => bin_exp.replace_value(source_range, new_value),
Expr::ArrayExpression(ref mut array_exp) => array_exp.replace_value(source_range, new_value),
Expr::ArrayRangeExpression(ref mut array_range) => array_range.replace_value(source_range, new_value),
Expr::ObjectExpression(ref mut obj_exp) => obj_exp.replace_value(source_range, new_value),
Expr::MemberExpression(_) => {}
Expr::Literal(_) => {}
@ -619,6 +623,7 @@ impl Expr {
Expr::PipeExpression(pipe_expression) => pipe_expression.start(),
Expr::PipeSubstitution(pipe_substitution) => pipe_substitution.start(),
Expr::ArrayExpression(array_expression) => array_expression.start(),
Expr::ArrayRangeExpression(array_range) => array_range.start(),
Expr::ObjectExpression(object_expression) => object_expression.start(),
Expr::MemberExpression(member_expression) => member_expression.start(),
Expr::UnaryExpression(unary_expression) => unary_expression.start(),
@ -638,6 +643,7 @@ impl Expr {
Expr::PipeExpression(pipe_expression) => pipe_expression.end(),
Expr::PipeSubstitution(pipe_substitution) => pipe_substitution.end(),
Expr::ArrayExpression(array_expression) => array_expression.end(),
Expr::ArrayRangeExpression(array_range) => array_range.end(),
Expr::ObjectExpression(object_expression) => object_expression.end(),
Expr::MemberExpression(member_expression) => member_expression.end(),
Expr::UnaryExpression(unary_expression) => unary_expression.end(),
@ -657,6 +663,7 @@ impl Expr {
Expr::CallExpression(call_expression) => call_expression.get_hover_value_for_position(pos, code),
Expr::PipeExpression(pipe_expression) => pipe_expression.get_hover_value_for_position(pos, code),
Expr::ArrayExpression(array_expression) => array_expression.get_hover_value_for_position(pos, code),
Expr::ArrayRangeExpression(array_range) => array_range.get_hover_value_for_position(pos, code),
Expr::ObjectExpression(object_expression) => object_expression.get_hover_value_for_position(pos, code),
Expr::MemberExpression(member_expression) => member_expression.get_hover_value_for_position(pos, code),
Expr::UnaryExpression(unary_expression) => unary_expression.get_hover_value_for_position(pos, code),
@ -685,6 +692,7 @@ impl Expr {
Expr::PipeExpression(ref mut pipe_expression) => pipe_expression.rename_identifiers(old_name, new_name),
Expr::PipeSubstitution(_) => {}
Expr::ArrayExpression(ref mut array_expression) => array_expression.rename_identifiers(old_name, new_name),
Expr::ArrayRangeExpression(ref mut array_range) => array_range.rename_identifiers(old_name, new_name),
Expr::ObjectExpression(ref mut object_expression) => {
object_expression.rename_identifiers(old_name, new_name)
}
@ -712,6 +720,7 @@ impl Expr {
source_ranges: vec![pipe_substitution.into()],
},
Expr::ArrayExpression(array_expression) => array_expression.get_constraint_level(),
Expr::ArrayRangeExpression(array_range) => array_range.get_constraint_level(),
Expr::ObjectExpression(object_expression) => object_expression.get_constraint_level(),
Expr::MemberExpression(member_expression) => member_expression.get_constraint_level(),
Expr::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(),
@ -2182,6 +2191,114 @@ impl ArrayExpression {
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
#[serde(rename_all = "camelCase", tag = "type")]
pub struct ArrayRangeExpression {
pub start: usize,
pub end: usize,
pub start_element: Box<Expr>,
pub end_element: Box<Expr>,
/// Is the `end_element` included in the range?
pub end_inclusive: bool,
// TODO (maybe) comments on range components?
pub digest: Option<Digest>,
}
impl_value_meta!(ArrayRangeExpression);
impl From<ArrayRangeExpression> for Expr {
fn from(array_expression: ArrayRangeExpression) -> Self {
Expr::ArrayRangeExpression(Box::new(array_expression))
}
}
impl ArrayRangeExpression {
pub fn new(start_element: Box<Expr>, end_element: Box<Expr>) -> Self {
Self {
start: 0,
end: 0,
start_element,
end_element,
end_inclusive: true,
digest: None,
}
}
compute_digest!(|slf, hasher| {
hasher.update(slf.start_element.compute_digest());
hasher.update(slf.end_element.compute_digest());
});
pub fn replace_value(&mut self, source_range: SourceRange, new_value: Expr) {
self.start_element.replace_value(source_range, new_value.clone());
self.end_element.replace_value(source_range, new_value.clone());
}
pub fn get_constraint_level(&self) -> ConstraintLevel {
let mut constraint_levels = ConstraintLevels::new();
constraint_levels.push(self.start_element.get_constraint_level());
constraint_levels.push(self.end_element.get_constraint_level());
constraint_levels.get_constraint_level(self.into())
}
/// Returns a hover value that includes the given character position.
pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
for element in [&*self.start_element, &*self.end_element] {
let element_source_range: SourceRange = element.into();
if element_source_range.contains(pos) {
return element.get_hover_value_for_position(pos, code);
}
}
None
}
#[async_recursion::async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let metadata = Metadata::from(&*self.start_element);
let start = ctx
.execute_expr(&self.start_element, exec_state, &metadata, StatementKind::Expression)
.await?
.get_json_value()?;
let start = parse_json_number_as_u64(&start, (&*self.start_element).into())?;
let metadata = Metadata::from(&*self.end_element);
let end = ctx
.execute_expr(&self.end_element, exec_state, &metadata, StatementKind::Expression)
.await?
.get_json_value()?;
let end = parse_json_number_as_u64(&end, (&*self.end_element).into())?;
if end < start {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: format!("Range start is greater than range end: {start} .. {end}"),
}));
}
let range: Vec<_> = if self.end_inclusive {
(start..=end).map(JValue::from).collect()
} else {
(start..end).map(JValue::from).collect()
};
Ok(KclValue::UserVal(UserVal {
value: range.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
}
/// Rename all identifiers that have the old name to the new given name.
fn rename_identifiers(&mut self, old_name: &str, new_name: &str) {
self.start_element.rename_identifiers(old_name, new_name);
self.end_element.rename_identifiers(old_name, new_name);
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
@ -2817,6 +2934,22 @@ pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange
}
}
pub fn parse_json_number_as_u64(j: &serde_json::Value, source_range: SourceRange) -> Result<u64, KclError> {
if let serde_json::Value::Number(n) = &j {
n.as_u64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid integer: {}", j),
})
})
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid integer: {}", j),
}))
}
}
pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
if let serde_json::Value::String(n) = &j {
Some(n.clone())
@ -3208,6 +3341,7 @@ async fn inner_execute_pipe_body(
| Expr::PipeExpression(_)
| Expr::PipeSubstitution(_)
| Expr::ArrayExpression(_)
| Expr::ArrayRangeExpression(_)
| Expr::ObjectExpression(_)
| Expr::MemberExpression(_)
| Expr::UnaryExpression(_)

View File

@ -784,6 +784,9 @@ fn test_generate_stdlib_markdown_docs() {
#[test]
fn test_generate_stdlib_json_schema() {
// If this test fails and you've modified the AST or something else which affects the json repr
// of stdlib functions, you should rerun the test with `EXPECTORATE=overwrite` to create new
// test data, then check `/docs/kcl/std.json` to ensure the changes are expected.
let stdlib = StdLib::new();
let combined = stdlib.combined();

View File

@ -2139,6 +2139,7 @@ impl ExecutorContext {
},
},
Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?,
Expr::ArrayRangeExpression(range_expression) => range_expression.execute(exec_state, self).await?,
Expr::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?,
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,

View File

@ -10,12 +10,12 @@ use winnow::{
use crate::{
ast::types::{
ArrayExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, CommentStyle, ElseIf,
Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier, IfExpression, Literal,
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue,
ObjectExpression, ObjectProperty, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement,
TagDeclarator, UnaryExpression, UnaryOperator, ValueMeta, VariableDeclaration, VariableDeclarator,
VariableKind,
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier,
IfExpression, Literal, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, NonCodeMeta,
NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty, Parameter, PipeExpression, PipeSubstitution,
Program, ReturnStatement, TagDeclarator, UnaryExpression, UnaryOperator, ValueMeta, VariableDeclaration,
VariableDeclarator, VariableKind,
},
errors::{KclError, KclErrorDetails},
executor::SourceRange,
@ -336,6 +336,7 @@ fn operand(i: TokenSlice) -> PResult<BinaryPart> {
| Expr::PipeExpression(_)
| Expr::PipeSubstitution(_)
| Expr::ArrayExpression(_)
| Expr::ArrayRangeExpression(_)
| Expr::ObjectExpression(_) => {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges,
@ -466,8 +467,13 @@ pub enum NonCodeOr<T> {
}
/// Parse a KCL array of elements.
fn array(i: TokenSlice) -> PResult<ArrayExpression> {
alt((array_empty, array_elem_by_elem, array_end_start)).parse_next(i)
fn array(i: TokenSlice) -> PResult<Expr> {
alt((
array_empty.map(Box::new).map(Expr::ArrayExpression),
array_elem_by_elem.map(Box::new).map(Expr::ArrayExpression),
array_end_start.map(Box::new).map(Expr::ArrayRangeExpression),
))
.parse_next(i)
}
/// Match an empty array.
@ -539,42 +545,26 @@ pub(crate) fn array_elem_by_elem(i: TokenSlice) -> PResult<ArrayExpression> {
})
}
fn array_end_start(i: TokenSlice) -> PResult<ArrayExpression> {
fn array_end_start(i: TokenSlice) -> PResult<ArrayRangeExpression> {
let start = open_bracket(i)?.start;
ignore_whitespace(i);
let elements = integer_range
.context(expected("array contents, a numeric range (like 0..10)"))
.parse_next(i)?;
let start_element = Box::new(expression.parse_next(i)?);
ignore_whitespace(i);
double_period.parse_next(i)?;
ignore_whitespace(i);
let end_element = Box::new(expression.parse_next(i)?);
ignore_whitespace(i);
let end = close_bracket(i)?.end;
Ok(ArrayExpression {
Ok(ArrayRangeExpression {
start,
end,
elements,
non_code_meta: Default::default(),
start_element,
end_element,
end_inclusive: true,
digest: None,
})
}
/// Parse n..m into a vec of numbers [n, n+1, ..., m-1]
fn integer_range(i: TokenSlice) -> PResult<Vec<Expr>> {
let (token0, floor) = integer.parse_next(i)?;
double_period.parse_next(i)?;
let (_token1, ceiling) = integer.parse_next(i)?;
Ok((floor..=ceiling)
.map(|num| {
let num = num as i64;
Expr::Literal(Box::new(Literal {
start: token0.start,
end: token0.end,
value: num.into(),
raw: num.to_string(),
digest: None,
}))
})
.collect())
}
fn object_property(i: TokenSlice) -> PResult<ObjectProperty> {
let key = identifier.context(expected("the property's key (the name or identifier of the property), e.g. in 'height: 4', 'height' is the property key")).parse_next(i)?;
colon
@ -1195,7 +1185,7 @@ fn expr_allowed_in_pipe_expr(i: TokenSlice) -> PResult<Expr> {
literal.map(Box::new).map(Expr::Literal),
fn_call.map(Box::new).map(Expr::CallExpression),
identifier.map(Box::new).map(Expr::Identifier),
array.map(Box::new).map(Expr::ArrayExpression),
array,
object.map(Box::new).map(Expr::ObjectExpression),
pipe_sub.map(Box::new).map(Expr::PipeSubstitution),
function_expression.map(Box::new).map(Expr::FunctionExpression),
@ -1511,25 +1501,6 @@ fn expression_stmt(i: TokenSlice) -> PResult<ExpressionStatement> {
})
}
/// Parse a KCL integer, and the token that held it.
fn integer(i: TokenSlice) -> PResult<(Token, u64)> {
let num = one_of(TokenType::Number)
.context(expected("a number token e.g. 3"))
.try_map(|token: Token| {
let source_ranges = token.as_source_ranges();
let value = token.value.clone();
token.value.parse().map(|num| (token, num)).map_err(|e| {
KclError::Syntax(KclErrorDetails {
source_ranges,
message: format!("invalid integer {value}: {e}"),
})
})
})
.context(expected("an integer e.g. 3 (but not 3.1)"))
.parse_next(i)?;
Ok(num)
}
/// Parse the given brace symbol.
fn some_brace(symbol: &'static str, i: TokenSlice) -> PResult<Token> {
one_of((TokenType::Brace, symbol))
@ -3060,123 +3031,6 @@ e
}
}
#[test]
fn test_parse_expand_array() {
let code = "const myArray = [0..10]";
let parser = crate::parser::Parser::new(crate::token::lexer(code).unwrap());
let result = parser.ast().unwrap();
let expected_result = Program {
start: 0,
end: 23,
body: vec![BodyItem::VariableDeclaration(VariableDeclaration {
start: 0,
end: 23,
declarations: vec![VariableDeclarator {
start: 6,
end: 23,
id: Identifier {
start: 6,
end: 13,
name: "myArray".to_string(),
digest: None,
},
init: Expr::ArrayExpression(Box::new(ArrayExpression {
start: 16,
end: 23,
non_code_meta: Default::default(),
elements: vec![
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 0u32.into(),
raw: "0".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 1u32.into(),
raw: "1".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 2u32.into(),
raw: "2".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 3u32.into(),
raw: "3".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 4u32.into(),
raw: "4".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 5u32.into(),
raw: "5".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 6u32.into(),
raw: "6".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 7u32.into(),
raw: "7".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 8u32.into(),
raw: "8".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 9u32.into(),
raw: "9".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 10u32.into(),
raw: "10".to_string(),
digest: None,
})),
],
digest: None,
})),
digest: None,
}],
kind: VariableKind::Const,
digest: None,
})],
non_code_meta: NonCodeMeta::default(),
digest: None,
};
assert_eq!(result, expected_result);
}
#[test]
fn test_error_keyword_in_variable() {
let some_program_string = r#"const let = "thing""#;

View File

@ -24,12 +24,11 @@ expression: actual
"digest": null
},
"init": {
"type": "ArrayExpression",
"type": "ArrayExpression",
"type": "ArrayRangeExpression",
"type": "ArrayRangeExpression",
"start": 16,
"end": 23,
"elements": [
{
"startElement": {
"type": "Literal",
"type": "Literal",
"start": 17,
@ -38,97 +37,16 @@ expression: actual
"raw": "0",
"digest": null
},
{
"endElement": {
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 1,
"raw": "1",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 2,
"raw": "2",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 3,
"raw": "3",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 4,
"raw": "4",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 5,
"raw": "5",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 6,
"raw": "6",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 7,
"raw": "7",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 8,
"raw": "8",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 9,
"raw": "9",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"start": 20,
"end": 22,
"value": 10,
"raw": "10",
"digest": null
}
],
},
"endInclusive": true,
"digest": null
},
"digest": null

View File

@ -2,10 +2,10 @@ use std::fmt::Write;
use crate::{
ast::types::{
ArrayExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, Expr, FormatOptions,
FunctionExpression, IfExpression, Literal, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject,
NonCodeValue, ObjectExpression, PipeExpression, Program, TagDeclarator, UnaryExpression, VariableDeclaration,
VariableKind,
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
Expr, FormatOptions, FunctionExpression, IfExpression, Literal, LiteralIdentifier, LiteralValue,
MemberExpression, MemberObject, NonCodeValue, ObjectExpression, PipeExpression, Program, TagDeclarator,
UnaryExpression, VariableDeclaration, VariableKind,
},
parser::PIPE_OPERATOR,
};
@ -113,6 +113,7 @@ impl Expr {
match &self {
Expr::BinaryExpression(bin_exp) => bin_exp.recast(options),
Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, is_in_pipe),
Expr::ArrayRangeExpression(range_exp) => range_exp.recast(options, indentation_level, is_in_pipe),
Expr::ObjectExpression(ref obj_exp) => obj_exp.recast(options, indentation_level, is_in_pipe),
Expr::MemberExpression(mem_exp) => mem_exp.recast(),
Expr::Literal(literal) => literal.recast(),
@ -280,6 +281,44 @@ impl ArrayExpression {
}
}
/// An expression is syntactically trivial: i.e., a literal, identifier, or similar.
fn expr_is_trivial(expr: &Expr) -> bool {
match expr {
Expr::Literal(_) | Expr::Identifier(_) | Expr::TagDeclarator(_) | Expr::PipeSubstitution(_) | Expr::None(_) => {
true
}
Expr::BinaryExpression(_)
| Expr::FunctionExpression(_)
| Expr::CallExpression(_)
| Expr::PipeExpression(_)
| Expr::ArrayExpression(_)
| Expr::ArrayRangeExpression(_)
| Expr::ObjectExpression(_)
| Expr::MemberExpression(_)
| Expr::UnaryExpression(_)
| Expr::IfExpression(_) => false,
}
}
impl ArrayRangeExpression {
fn recast(&self, options: &FormatOptions, _: usize, _: bool) -> String {
let s1 = self.start_element.recast(options, 0, false);
let s2 = self.end_element.recast(options, 0, false);
// Format these items into a one-line array. Put spaces around the `..` if either expression
// is non-trivial. This is a bit arbitrary but people seem to like simple ranges to be formatted
// tightly, but this is a misleading visual representation of the precedence if the range
// components are compound expressions.
if expr_is_trivial(&self.start_element) && expr_is_trivial(&self.end_element) {
format!("[{s1}..{s2}]")
} else {
format!("[{s1} .. {s2}]")
}
// Assume a range expression fits on one line.
}
}
impl ObjectExpression {
fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String {
if self
@ -830,6 +869,20 @@ myNestedVar = [callExp(bing.yo)]
assert_eq!(recasted, some_program_string);
}
#[test]
fn test_recast_ranges() {
let some_program_string = r#"foo = [0..10]
ten = 10
bar = [0 + 1 .. ten]
"#;
let tokens = crate::token::lexer(some_program_string).unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let recasted = program.recast(&Default::default(), 0);
assert_eq!(recasted, some_program_string);
}
#[test]
fn test_recast_space_in_fn_call() {
let some_program_string = r#"fn thing = (x) => {

View File

@ -24,6 +24,7 @@ pub enum Node<'a> {
PipeExpression(&'a types::PipeExpression),
PipeSubstitution(&'a types::PipeSubstitution),
ArrayExpression(&'a types::ArrayExpression),
ArrayRangeExpression(&'a types::ArrayRangeExpression),
ObjectExpression(&'a types::ObjectExpression),
MemberExpression(&'a types::MemberExpression),
UnaryExpression(&'a types::UnaryExpression),
@ -54,6 +55,7 @@ impl From<&Node<'_>> for SourceRange {
Node::PipeExpression(p) => SourceRange([p.start(), p.end()]),
Node::PipeSubstitution(p) => SourceRange([p.start(), p.end()]),
Node::ArrayExpression(a) => SourceRange([a.start(), a.end()]),
Node::ArrayRangeExpression(a) => SourceRange([a.start(), a.end()]),
Node::ObjectExpression(o) => SourceRange([o.start(), o.end()]),
Node::MemberExpression(m) => SourceRange([m.start(), m.end()]),
Node::UnaryExpression(u) => SourceRange([u.start(), u.end()]),
@ -90,6 +92,7 @@ impl_from!(Node, CallExpression);
impl_from!(Node, PipeExpression);
impl_from!(Node, PipeSubstitution);
impl_from!(Node, ArrayExpression);
impl_from!(Node, ArrayRangeExpression);
impl_from!(Node, ObjectExpression);
impl_from!(Node, MemberExpression);
impl_from!(Node, UnaryExpression);

View File

@ -183,6 +183,18 @@ where
}
Ok(true)
}
Expr::ArrayRangeExpression(are) => {
if !f.walk(are.as_ref().into())? {
return Ok(false);
}
if !walk_value::<WalkT>(&are.start_element, f)? {
return Ok(false);
}
if !walk_value::<WalkT>(&are.end_element, f)? {
return Ok(false);
}
Ok(true)
}
Expr::ObjectExpression(oe) => walk_object_expression(oe, f),
Expr::MemberExpression(me) => walk_member_expression(me, f),
Expr::UnaryExpression(ue) => walk_unary_expression(ue, f),

View File

@ -0,0 +1,17 @@
r1 = [0..4]
assertEqual(r1[4], 4, 0.00001, "last element is included")
four = 4
zero = 0
r2 = [zero..four]
assertEqual(r2[4], 4, 0.00001, "last element is included")
five = int(four + 1)
r3 = [zero..five]
assertEqual(r3[4], 4, 0.00001, "second-to-last element is included")
assertEqual(r3[5], 5, 0.00001, "last element is included")
r4 = [int(zero + 1) .. int(five - 1)]
assertEqual(r4[0], 1, 0.00001, "first element is 1")
assertEqual(r4[2], 3, 0.00001, "second-to-last element is 3")
assertEqual(r4[3], 4, 0.00001, "last element is 4")

View File

@ -0,0 +1,3 @@
xs = [-5..5]
assertEqual(xs[0], -5, 0.001, "first element is -5")
assert(false)

View File

@ -66,6 +66,8 @@ async fn run_fail(code: &str) -> KclError {
gen_test!(property_of_object);
gen_test!(index_of_array);
gen_test!(comparisons);
gen_test!(array_range_expr);
gen_test_fail!(array_range_negative_expr, "syntax: Invalid integer: -5.0");
gen_test_fail!(
invalid_index_str,
"semantic: Only integers >= 0 can be used as the index of an array, but you're using a string"