forma-dsl-mcp 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/package.json +52 -0
- package/src/bin.js +30 -0
- package/src/catalogue.js +703 -0
- package/src/document.js +285 -0
- package/src/examples.js +212 -0
- package/src/guide.js +313 -0
- package/src/reference/README.md +153 -0
- package/src/reference/api/describe-parameters.md +167 -0
- package/src/reference/api/evaluation-context.md +221 -0
- package/src/reference/api/export.md +185 -0
- package/src/reference/api/geometry-node.md +215 -0
- package/src/reference/api/kernel.md +182 -0
- package/src/reference/api/low-level.md +239 -0
- package/src/reference/api/render.md +217 -0
- package/src/reference/api/values.md +223 -0
- package/src/reference/errors.md +253 -0
- package/src/reference/language/control-flow.md +160 -0
- package/src/reference/language/expressions.md +206 -0
- package/src/reference/language/overview.md +156 -0
- package/src/reference/language/scope.md +184 -0
- package/src/reference/language/syntax.md +210 -0
- package/src/reference/reference/align.md +189 -0
- package/src/reference/reference/booleans.md +231 -0
- package/src/reference/reference/component.md +248 -0
- package/src/reference/reference/conversions.md +269 -0
- package/src/reference/reference/functions.md +314 -0
- package/src/reference/reference/local.md +197 -0
- package/src/reference/reference/model.md +213 -0
- package/src/reference/reference/param.md +230 -0
- package/src/reference/reference/part.md +229 -0
- package/src/reference/reference/refinement.md +284 -0
- package/src/reference/reference/shapes-2d.md +309 -0
- package/src/reference/reference/shapes-3d.md +279 -0
- package/src/reference/reference/transforms.md +246 -0
- package/src/reference.js +160 -0
- package/src/server.js +219 -0
- package/src/tools.js +523 -0
- package/src/workspace.js +204 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Expressions
|
|
2
|
+
|
|
3
|
+
Expressions appear on the right of every attribute, in `for` sequences, in `if` conditions,
|
|
4
|
+
in `part` labels and inside `${ }` splices. The language is dynamically typed: values carry
|
|
5
|
+
their type, and a mismatch is reported where it is used.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Values
|
|
10
|
+
|
|
11
|
+
| Type | Literals | Notes |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| **number** | `12`, `0.5`, `1e3` | Double precision. Infinities and NaN cannot be written and are rejected where a block reads a number |
|
|
14
|
+
| **string** | `"text"`, `"a ${b} c"` | Double quotes only; see [Syntax → Strings](syntax.md#strings) |
|
|
15
|
+
| **bool** | `true`, `false` | |
|
|
16
|
+
| **list** | `[1, 2, 3]`, `[[0,0], [1,0]]` | Heterogeneous and nestable. A list of numbers is what every block means by a *vector* |
|
|
17
|
+
| **object** | `{ w = 3, h = 4 }`, `{ w: 3 }` | `=` and `:` are interchangeable. Read with `.key` |
|
|
18
|
+
| **null** | `null` | Falsy; renders as the empty string in interpolation |
|
|
19
|
+
|
|
20
|
+
There is no separate vector type in the language — a vector is a list of numbers, and blocks
|
|
21
|
+
say how many components they want. Where a block wants a vector, a **single number is
|
|
22
|
+
accepted as shorthand for every component**, so `scale { factor = 2 }` and
|
|
23
|
+
`scale { factor = [2, 2, 2] }` are the same.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Operators
|
|
28
|
+
|
|
29
|
+
Listed loosest-binding first. Everything in one row has equal precedence and associates left
|
|
30
|
+
to right.
|
|
31
|
+
|
|
32
|
+
| Precedence | Operators | Notes |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| 1 (loosest) | `cond ? a : b` | Right-associative |
|
|
35
|
+
| 2 | `\|\|` | Short-circuits; always yields a bool |
|
|
36
|
+
| 3 | `&&` | Short-circuits; always yields a bool |
|
|
37
|
+
| 4 | `==` `!=` | Structural equality |
|
|
38
|
+
| 5 | `<` `<=` `>` `>=` | Both sides must be the same type |
|
|
39
|
+
| 6 | `+` `-` | |
|
|
40
|
+
| 7 | `*` `/` `%` | |
|
|
41
|
+
| 8 | `-x` `!x` | Prefix |
|
|
42
|
+
| 9 (tightest) | `a.b` `a[i]` `f(x)` | Postfix |
|
|
43
|
+
|
|
44
|
+
### Arithmetic
|
|
45
|
+
|
|
46
|
+
`+`, `-`, `*` and `/` are overloaded so that vector maths reads the way it is meant to.
|
|
47
|
+
|
|
48
|
+
```hcl
|
|
49
|
+
2 + 3 // 5
|
|
50
|
+
[10, 20] + [1, 2] // [11, 22] componentwise
|
|
51
|
+
[10, 20] - [1, 2] // [9, 18] componentwise
|
|
52
|
+
[10, 20] * 2 // [20, 40] scalar broadcast
|
|
53
|
+
2 * [10, 20] // [20, 40] either order
|
|
54
|
+
[10, 20] * [2, 3] // [20, 60] componentwise
|
|
55
|
+
[10, 20] / 2 // [5, 10] scalar only
|
|
56
|
+
10 % 3 // 1
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
What each accepts:
|
|
60
|
+
|
|
61
|
+
| Operator | Number ∘ number | List ∘ list | List ∘ number | Number ∘ list | Anything with a string |
|
|
62
|
+
| --- | --- | --- | --- | --- | --- |
|
|
63
|
+
| `+` | sum | componentwise | ✗ | ✗ | concatenation |
|
|
64
|
+
| `-` | difference | componentwise | ✗ | ✗ | ✗ |
|
|
65
|
+
| `*` | product | componentwise | broadcast | broadcast | ✗ |
|
|
66
|
+
| `/` | quotient | ✗ | broadcast | ✗ | ✗ |
|
|
67
|
+
| `%` | remainder | ✗ | ✗ | ✗ | ✗ |
|
|
68
|
+
|
|
69
|
+
Two lists of different lengths cannot be combined:
|
|
70
|
+
`cannot combine vectors of length 2 and 3`.
|
|
71
|
+
|
|
72
|
+
**Division and modulo by zero are errors**, not `Infinity` or `NaN` — a NaN would otherwise
|
|
73
|
+
travel all the way into the kernel and surface as a degenerate vertex a long way from its
|
|
74
|
+
cause.
|
|
75
|
+
|
|
76
|
+
### Concatenation
|
|
77
|
+
|
|
78
|
+
`+` concatenates when either side is a string. The other side is rendered by the same rules
|
|
79
|
+
as [interpolation](#string-interpolation):
|
|
80
|
+
|
|
81
|
+
```hcl
|
|
82
|
+
"part_" + 3 // "part_3"
|
|
83
|
+
"size " + [1, 2] // "size [1, 2]"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Comparison and equality
|
|
87
|
+
|
|
88
|
+
`==` and `!=` compare structurally, so two lists with the same numbers are equal:
|
|
89
|
+
|
|
90
|
+
```hcl
|
|
91
|
+
[1, 2] == [1, 2] // true
|
|
92
|
+
1 == "1" // false — different types are never equal
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`<`, `<=`, `>` and `>=` require both sides to be the same type, and compare numbers
|
|
96
|
+
numerically and strings lexicographically. Mixing them is an error:
|
|
97
|
+
`cannot compare a number with a string`.
|
|
98
|
+
|
|
99
|
+
### Logic
|
|
100
|
+
|
|
101
|
+
`&&` and `||` short-circuit, so the right side is not evaluated when the left decides the
|
|
102
|
+
answer — `count > 0 && list[0] > 1` is safe on an empty list. Both always return a bool, even
|
|
103
|
+
when given non-bool operands: `1 || false` is `true`, not `1`.
|
|
104
|
+
|
|
105
|
+
`!x` negates the [truthiness](#truthiness) of any value. Unary `-` requires a number:
|
|
106
|
+
`cannot negate a non-number`.
|
|
107
|
+
|
|
108
|
+
### Conditional
|
|
109
|
+
|
|
110
|
+
```hcl
|
|
111
|
+
color = i % 2 == 0 ? "#3f7fbf" : "#bf6b3f"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Binds looser than everything else, so the condition needs no parentheses. It is
|
|
115
|
+
right-associative, so `a ? b : c ? d : e` chains as `a ? b : (c ? d : e)`.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Truthiness
|
|
120
|
+
|
|
121
|
+
Conditions — in `? :`, `&&`, `||`, `!` and [`if`](control-flow.md#if) — accept any value.
|
|
122
|
+
|
|
123
|
+
| Falsy | Truthy |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `false` | `true` |
|
|
126
|
+
| `0` | any other number |
|
|
127
|
+
| `""` | any non-empty string |
|
|
128
|
+
| `[]` | any non-empty list |
|
|
129
|
+
| `null` | any object, including `{}` |
|
|
130
|
+
|
|
131
|
+
Empty strings and empty lists being falsy is what makes `if var.label { ... }` and
|
|
132
|
+
`if var.sizes { ... }` read the way they are written.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Member access and indexing
|
|
137
|
+
|
|
138
|
+
```hcl
|
|
139
|
+
local size = [40, 30, 12]
|
|
140
|
+
size.x // 40
|
|
141
|
+
size.y // 30
|
|
142
|
+
size.z // 12
|
|
143
|
+
size[0] // 40
|
|
144
|
+
|
|
145
|
+
local opts = { wall = 2, gap = 0.4 }
|
|
146
|
+
opts.wall // 2
|
|
147
|
+
opts["wall"] // 2
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`.x`, `.y` and `.z` on a **list** are sugar for indices 0, 1 and 2 — they are not a vector
|
|
151
|
+
type. They are read on a list of any length, so `[1, 2].z` yields nothing rather than
|
|
152
|
+
raising; the failure surfaces later, wherever that nothing is used.
|
|
153
|
+
|
|
154
|
+
On an **object**, a missing key is an error: `no attribute "wdith"`. Reading a property of
|
|
155
|
+
`null` is too: `cannot read "x" of nothing`.
|
|
156
|
+
|
|
157
|
+
Indexing past the end of a list is *not* an error; it yields undefined, which then usually
|
|
158
|
+
fails where it is used. Indexing something that is neither a list, a string nor an object is
|
|
159
|
+
an error: `cannot index this value`.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Function calls
|
|
164
|
+
|
|
165
|
+
```hcl
|
|
166
|
+
max(sizes) // variadic, flattens its arguments
|
|
167
|
+
pow(2, 10)
|
|
168
|
+
sin(30) // 0.5 — every angle is in degrees
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Functions take **positional arguments only**. Named arguments are an error:
|
|
172
|
+
`pow: functions take positional arguments`. A fixed-arity function checks its count:
|
|
173
|
+
`len takes 1 argument(s), got 2`.
|
|
174
|
+
|
|
175
|
+
There are no user-defined functions. To name a piece of geometry, declare a
|
|
176
|
+
[`component`](../reference/component.md); to name a value, declare a
|
|
177
|
+
[`local`](../reference/local.md).
|
|
178
|
+
|
|
179
|
+
See the [function reference](../reference/functions.md) for all thirty of them.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## String interpolation
|
|
184
|
+
|
|
185
|
+
`${ ... }` splices any expression into a string.
|
|
186
|
+
|
|
187
|
+
```hcl
|
|
188
|
+
part "leg_${i}" { ... }
|
|
189
|
+
color = "#${hex}"
|
|
190
|
+
description = "wall ${var.wall}mm, ${len(sizes)} slots"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
How each value renders:
|
|
194
|
+
|
|
195
|
+
| Value | Rendered as |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| number | `3`, `0.5` — JavaScript's default number formatting |
|
|
198
|
+
| string | itself |
|
|
199
|
+
| bool | `true` / `false` |
|
|
200
|
+
| list | `[1, 2, 3]` — elements joined by `, ` inside brackets, recursively |
|
|
201
|
+
| `null` | the empty string |
|
|
202
|
+
| object | `{w: 3, h: 4}` — each entry rendered by these same rules. Interpolating one is usually a mistake, but it at least names which object |
|
|
203
|
+
|
|
204
|
+
A splice must contain exactly one expression; anything left over is an error:
|
|
205
|
+
`trailing input in ${ }`. Errors raised inside a splice are reported at the position of the
|
|
206
|
+
string that contains it, since the splice has no line of its own.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Document structure
|
|
2
|
+
|
|
3
|
+
A forma document is a flat list of top-level blocks. Only four kinds may appear there:
|
|
4
|
+
[`param`](../reference/param.md), [`local`](../reference/local.md),
|
|
5
|
+
[`component`](../reference/component.md) and [`model`](../reference/model.md). Anything else
|
|
6
|
+
at the top level is an error.
|
|
7
|
+
|
|
8
|
+
```hcl
|
|
9
|
+
param wall = 2 // an input
|
|
10
|
+
local sizes = [10, 20, 30] // a computed value
|
|
11
|
+
|
|
12
|
+
component "plate" { // a reusable block
|
|
13
|
+
param size = [20, 20]
|
|
14
|
+
extrude { height = wall rect { size = size center = true } }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
model "stack" { // something to render
|
|
18
|
+
for i, s in sizes {
|
|
19
|
+
translate { offset = [0, 0, i * wall] plate { size = [s, s] } }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Order does not matter between declarations of different kinds — a `model` may use a
|
|
25
|
+
`component` declared below it. Order does matter among `local`s, which are evaluated top to
|
|
26
|
+
bottom. See [Scope and names](scope.md).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## The pipeline
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
source ──▶ lexer ──▶ parser ──▶ evaluator ──▶ geometry nodes ──▶ Manifold (WASM) ──▶ mesh
|
|
34
|
+
│ │
|
|
35
|
+
params digest cache
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Each stage has a distinct job, and knowing which stage raised an error tells you what kind of
|
|
39
|
+
mistake it was.
|
|
40
|
+
|
|
41
|
+
1. **Lexer** turns characters into tokens. It rejects unknown characters, unterminated
|
|
42
|
+
strings and unterminated block comments.
|
|
43
|
+
2. **Parser** turns tokens into an abstract syntax tree. It rejects anything that is not
|
|
44
|
+
grammatical — a missing brace, a keyword used as a block type, a `param` block containing
|
|
45
|
+
nested blocks.
|
|
46
|
+
3. **Evaluator** resolves params and locals, then walks the models. It rejects unknown names,
|
|
47
|
+
bad operands, unknown blocks, unknown attributes and dimensionality mismatches. Its output
|
|
48
|
+
is a tree of [geometry nodes](../api/geometry-node.md) — it never calls the kernel itself.
|
|
49
|
+
4. **Kernel** solves the node tree into concrete solids, memoized by node digest in an
|
|
50
|
+
[`EvaluationContext`](../api/evaluation-context.md).
|
|
51
|
+
|
|
52
|
+
The separation between steps 3 and 4 is what makes editing cheap. The evaluator is pure and
|
|
53
|
+
fast; the kernel is where the time goes, and it is asked only about subtrees whose content
|
|
54
|
+
actually changed.
|
|
55
|
+
|
|
56
|
+
The one exception is [`align`](../reference/align.md), which has to measure its child before
|
|
57
|
+
it can decide where to put it. It is the only block that reaches into the kernel during
|
|
58
|
+
evaluation, and the only one that fails when there is no kernel context.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Nesting is composition
|
|
63
|
+
|
|
64
|
+
There is no separate expression language for geometry. A block's children *are* its operands,
|
|
65
|
+
written inside its braces:
|
|
66
|
+
|
|
67
|
+
```hcl
|
|
68
|
+
difference { // subtract…
|
|
69
|
+
box { size = [20, 20, 10] } // …this is the first operand
|
|
70
|
+
cylinder { radius = 5 height = 30 } // …and this is cut out of it
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Several children in a row are implicitly unioned.** Every operation except the three
|
|
75
|
+
booleans unions its children before acting on them, so a transform applied to two shapes
|
|
76
|
+
moves both together:
|
|
77
|
+
|
|
78
|
+
```hcl
|
|
79
|
+
translate {
|
|
80
|
+
offset = [0, 0, 10]
|
|
81
|
+
box { size = [4, 4, 4] } // these two are unioned first,
|
|
82
|
+
sphere { radius = 3 } // then the union is moved
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
[`difference`](../reference/booleans.md#difference) is the operation where order matters: the
|
|
87
|
+
first child is the body and every later child is cut out of it. `union` and `intersection`
|
|
88
|
+
receive their children as a list too, but the result does not depend on the order.
|
|
89
|
+
|
|
90
|
+
An operation with no children is an error; a shape with children is an error.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Dimensionality
|
|
95
|
+
|
|
96
|
+
Every piece of geometry is either 2D or 3D, and the language tracks which.
|
|
97
|
+
|
|
98
|
+
- **2D shapes** — `rect`, `rounded_rect`, `circle`, `ellipse`, `regular_polygon`, `stadium`,
|
|
99
|
+
`polygon` — produce cross-sections.
|
|
100
|
+
- **3D shapes** — `box`, `sphere`, `cylinder`, `cone`, `torus` — produce solids.
|
|
101
|
+
- **Operations** either fix what they accept (`extrude` takes 2D, `refine` takes 3D) or pass
|
|
102
|
+
their children's dimensionality through (`translate`, `rotate`, `hull`, the booleans).
|
|
103
|
+
|
|
104
|
+
Three rules follow, and each has its own error message:
|
|
105
|
+
|
|
106
|
+
| Rule | Error when broken |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| The children of one block must all be the same dimensionality | `"union" cannot mix 2D and 3D shapes` |
|
|
109
|
+
| A block that fixes what it takes must get it | `"extrude" takes 2D geometry, got 3D` |
|
|
110
|
+
| Anything rendered must be 3D | `part "body" is 2D — extrude or revolve it before rendering` |
|
|
111
|
+
|
|
112
|
+
To get from 2D to 3D use [`extrude`](../reference/conversions.md#extrude) or
|
|
113
|
+
[`revolve`](../reference/conversions.md#revolve); to go back use
|
|
114
|
+
[`project`](../reference/conversions.md#project) or
|
|
115
|
+
[`slice`](../reference/conversions.md#slice).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Units and orientation
|
|
120
|
+
|
|
121
|
+
- **Lengths are unitless.** Every consumer treats them as millimetres — STL has no unit
|
|
122
|
+
either — but nothing in the language enforces that.
|
|
123
|
+
- **Angles are always in degrees.** Everywhere: `rotate { z = 90 }`, `revolve { angle = 180 }`,
|
|
124
|
+
`sin(30)`, `param a { type = angle }`. There is no radian form in the language. The
|
|
125
|
+
JavaScript [`Angle`](../api/values.md#angle) class stores radians internally, and that is
|
|
126
|
+
the only place radians appear.
|
|
127
|
+
- **Z is up.** `extrude` sweeps along +Z. `revolve` spins a profile written in the XY plane
|
|
128
|
+
about the Y axis, mapping the profile's Y to the result's Z, so a profile drawn from
|
|
129
|
+
`[0, 0]` upwards produces a solid standing on the XY plane.
|
|
130
|
+
- **3D rotations apply X, then Y, then Z**, matching OpenSCAD, so a model ported from there
|
|
131
|
+
lands in the same orientation.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Identity is content
|
|
136
|
+
|
|
137
|
+
The evaluator does not produce a call graph — it produces a tree of immutable nodes, each
|
|
138
|
+
carrying a 128-bit digest of its own content and its children's digests. Two structurally
|
|
139
|
+
identical subtrees have the same digest and therefore *are* the same node.
|
|
140
|
+
|
|
141
|
+
That has consequences worth designing around:
|
|
142
|
+
|
|
143
|
+
- A component used twenty times is evaluated once. Eight spheres in a loop cost one sphere
|
|
144
|
+
and eight transforms.
|
|
145
|
+
- Two shapes written with their attributes in a different order still share a cache entry —
|
|
146
|
+
object keys are sorted before they are digested.
|
|
147
|
+
- The members of a `union` are sorted by subtree size, so two unions written in a different
|
|
148
|
+
order also share a digest. `difference` is never reordered, because its first child means
|
|
149
|
+
something different from the rest.
|
|
150
|
+
- Nested transforms collapse into a single matrix, so a deep stack of `translate` blocks is
|
|
151
|
+
one kernel call rather than one per level.
|
|
152
|
+
- The digest is computed from the IEEE bits of every number, never from a runtime-seeded
|
|
153
|
+
hash, so it is the same in every browser and every run.
|
|
154
|
+
|
|
155
|
+
See [`GeometryNode`](../api/geometry-node.md) for the node types themselves, and
|
|
156
|
+
[`EvaluationContext`](../api/evaluation-context.md) for the cache built on top of them.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Scope and names
|
|
2
|
+
|
|
3
|
+
Every name in a document resolves through a chain of scopes. This page says exactly what is
|
|
4
|
+
in each one, and in what order they are built — which is what determines whether a
|
|
5
|
+
declaration may refer to another.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The root scope
|
|
10
|
+
|
|
11
|
+
Built once, before any geometry, in this order:
|
|
12
|
+
|
|
13
|
+
1. **Constants** — `pi` and `e`.
|
|
14
|
+
2. **Type names** — `number`, `string`, `bool`, `vector`, `list`, `angle`, each bound to
|
|
15
|
+
itself as a string, so `type = number` in a param block resolves with no quoting.
|
|
16
|
+
3. **Params**, in source order. Each is bound under its bare name as it is resolved, so a
|
|
17
|
+
later param's default may refer to an earlier one.
|
|
18
|
+
4. **`var`** — an object holding every resolved param.
|
|
19
|
+
5. **Locals**, in source order. Each is bound as it is resolved, so a later local may refer
|
|
20
|
+
to an earlier one.
|
|
21
|
+
|
|
22
|
+
The order has two consequences worth knowing:
|
|
23
|
+
|
|
24
|
+
```hcl
|
|
25
|
+
param a = 3
|
|
26
|
+
param b = a * 2 // ✓ an earlier param, by its bare name
|
|
27
|
+
param c = var.a * 2 // ✗ unknown name "var" — var does not exist yet
|
|
28
|
+
|
|
29
|
+
local d = var.a + 1 // ✓ locals are built after var
|
|
30
|
+
local e = f // ✗ unknown name "f" — a later local
|
|
31
|
+
local f = 2
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## `var.` versus the bare name
|
|
37
|
+
|
|
38
|
+
Every param is reachable two ways: bare, and under `var`.
|
|
39
|
+
|
|
40
|
+
```hcl
|
|
41
|
+
param wall = 2
|
|
42
|
+
|
|
43
|
+
model "m" {
|
|
44
|
+
box { size = [wall, var.wall, 1] } // the same value, twice
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
They differ when something shadows the bare name. `var` holds **params only** — never
|
|
49
|
+
locals, never component params, never loop variables — so `var.NAME` is the unambiguous way
|
|
50
|
+
to reach an input:
|
|
51
|
+
|
|
52
|
+
```hcl
|
|
53
|
+
param a = 1
|
|
54
|
+
local a = 2 // shadows the bare name
|
|
55
|
+
|
|
56
|
+
model "m" {
|
|
57
|
+
box { size = [a, var.a, 1] } // [2, 1, 1]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Reading a local through `var` fails, because it is not there: `no attribute "a"`.
|
|
62
|
+
|
|
63
|
+
Prefer `var.` in models for anything that came from a param. It is the convention the
|
|
64
|
+
editor's examples follow, and it makes an input visually distinct from a local at the point
|
|
65
|
+
of use.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Body scopes
|
|
70
|
+
|
|
71
|
+
Every block body — a `model`, a `component`, a builtin block, a `for` iteration, an `if`
|
|
72
|
+
branch — evaluates in a fresh scope nested inside its parent.
|
|
73
|
+
|
|
74
|
+
**Locals declared in a body are visible to the whole body**, not only to what follows them,
|
|
75
|
+
because every local in a body is resolved before any of its blocks are built:
|
|
76
|
+
|
|
77
|
+
```hcl
|
|
78
|
+
model "m" {
|
|
79
|
+
box { size = [s, 1, 1] } // ✓ s is already bound
|
|
80
|
+
local s = 4
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
That is the opposite of the top-level rule, where locals are strictly ordered. Within one
|
|
85
|
+
body, ordering between locals still applies — a body local may not refer to a later body
|
|
86
|
+
local.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Component isolation
|
|
91
|
+
|
|
92
|
+
A [`component`](../reference/component.md) body gets a fresh scope rooted at the **document
|
|
93
|
+
root**, not at the call site. A name that happens to exist where the component is used does
|
|
94
|
+
not leak into it.
|
|
95
|
+
|
|
96
|
+
```hcl
|
|
97
|
+
local g = 7
|
|
98
|
+
|
|
99
|
+
component "c" {
|
|
100
|
+
param s = 1
|
|
101
|
+
box { size = [s, g, 1] } // ✓ its own param, and a document local
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
model "m" {
|
|
105
|
+
local q = 5
|
|
106
|
+
c { } // c cannot see q
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Inside a component body, these are in scope:
|
|
111
|
+
|
|
112
|
+
- its own `param`s, bound from the attributes at the call site
|
|
113
|
+
- everything in the root scope — constants, type names, document params (bare and via `var.`)
|
|
114
|
+
and document locals
|
|
115
|
+
- its own body locals
|
|
116
|
+
|
|
117
|
+
Nothing from the caller. Values travel in through params and no other way.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Model params
|
|
122
|
+
|
|
123
|
+
A `model` may declare its own `param` blocks. They behave like locals with a declaration
|
|
124
|
+
syntax, not like document params:
|
|
125
|
+
|
|
126
|
+
- Each **must** have a default — `model "m": param "s" needs a default`.
|
|
127
|
+
- They are **not** settable from outside. `render(source, { params: { s: 9 } })` sets
|
|
128
|
+
document params only; a model param keeps its default.
|
|
129
|
+
- They are bound in the model's scope under the bare name, and are not added to `var`.
|
|
130
|
+
|
|
131
|
+
```hcl
|
|
132
|
+
model "bracket" {
|
|
133
|
+
param thickness { default = 4 } // a fixed constant for this model
|
|
134
|
+
box { size = [20, 20, thickness] }
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For an input the caller can change, declare the param at the top level instead.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Loop variables
|
|
143
|
+
|
|
144
|
+
[`for`](control-flow.md#for) binds one or two names in a child scope, live for one iteration:
|
|
145
|
+
|
|
146
|
+
```hcl
|
|
147
|
+
for size in sizes { ... } // size is the value
|
|
148
|
+
for i, size in sizes { ... } // i is the index, size the value
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The names shadow anything of the same name in an enclosing scope, and are gone after the
|
|
152
|
+
loop.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Shadowing summary
|
|
157
|
+
|
|
158
|
+
Innermost wins. From outermost to innermost:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
constants and type names
|
|
162
|
+
└─ params (bare) · var · locals
|
|
163
|
+
└─ model scope: model params, model body locals
|
|
164
|
+
└─ block body locals
|
|
165
|
+
└─ for-loop variables
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
A `component` body cuts the chain: it starts again from the root scope regardless of where
|
|
169
|
+
it was called.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## What cannot be shadowed
|
|
174
|
+
|
|
175
|
+
- **Builtin block names.** Declaring `component "box"` is an error:
|
|
176
|
+
`component "box" shadows a builtin block`. Components and builtins share one namespace so
|
|
177
|
+
a call site reads the same either way.
|
|
178
|
+
- **Builtin function names.** There are no user-defined functions, so nothing can shadow
|
|
179
|
+
them.
|
|
180
|
+
- **Duplicate declarations** of the same kind are errors: `param "a" is declared twice`,
|
|
181
|
+
`component "c" is declared twice`, `model "m" is declared twice`. Locals are the exception:
|
|
182
|
+
a second `local a` is accepted and silently replaces the first everywhere, including in
|
|
183
|
+
code written above it. Nothing warns about it, so treat it as a mistake to avoid rather
|
|
184
|
+
than a feature.
|