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 |