forma-dsl-mcp 0.3.8 → 0.4.0

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.
Files changed (41) hide show
  1. package/README.md +22 -18
  2. package/package.json +2 -2
  3. package/src/bin.js +1 -1
  4. package/src/catalogue.js +15 -15
  5. package/src/document.js +8 -38
  6. package/src/examples.js +1 -1
  7. package/src/guide.js +24 -24
  8. package/src/reference/README.md +15 -5
  9. package/src/reference/api/describe-parameters.md +4 -4
  10. package/src/reference/api/evaluation-context.md +13 -13
  11. package/src/reference/api/export.md +7 -7
  12. package/src/reference/api/geometry-node.md +9 -9
  13. package/src/reference/api/kernel.md +8 -8
  14. package/src/reference/api/low-level.md +9 -9
  15. package/src/reference/api/render.md +8 -8
  16. package/src/reference/api/values.md +12 -12
  17. package/src/reference/cli.md +199 -0
  18. package/src/reference/errors.md +14 -14
  19. package/src/reference/language/control-flow.md +3 -3
  20. package/src/reference/language/expressions.md +10 -10
  21. package/src/reference/language/overview.md +11 -11
  22. package/src/reference/language/scope.md +13 -13
  23. package/src/reference/language/syntax.md +4 -4
  24. package/src/reference/reference/align.md +9 -9
  25. package/src/reference/reference/booleans.md +5 -5
  26. package/src/reference/reference/component.md +12 -12
  27. package/src/reference/reference/conversions.md +8 -8
  28. package/src/reference/reference/functions.md +8 -8
  29. package/src/reference/reference/local.md +4 -4
  30. package/src/reference/reference/model.md +10 -10
  31. package/src/reference/reference/param.md +8 -8
  32. package/src/reference/reference/part.md +12 -12
  33. package/src/reference/reference/refinement.md +10 -10
  34. package/src/reference/reference/shapes-2d.md +15 -15
  35. package/src/reference/reference/shapes-3d.md +13 -13
  36. package/src/reference/reference/transforms.md +10 -10
  37. package/src/reference.js +4 -3
  38. package/src/roots.js +2 -2
  39. package/src/server.js +2 -2
  40. package/src/tools.js +15 -15
  41. package/src/workspace.js +7 -7
@@ -0,0 +1,199 @@
1
+ # Command line
2
+
3
+ [`forma-dsl-cli`](https://www.npmjs.com/package/forma-dsl-cli) solves a document and writes a
4
+ binary STL, with no browser and no build step. It is what puts a `.forma` file in a Makefile,
5
+ a CI job or a release pipeline.
6
+
7
+ ```bash
8
+ npm install -g forma-dsl-cli
9
+ forma render riser.forma -p height=32 -o build/riser.stl
10
+ ```
11
+
12
+ Or, without installing it, `npx forma-dsl-cli render riser.forma -o riser.stl`.
13
+
14
+ It knows nothing about the language itself. Every answer comes from `forma-dsl` - the same
15
+ package the editor and the MCP server import - so a block added to the language is one it
16
+ renders without being told, and an error it reports is the evaluator's own, with the
17
+ evaluator's own position.
18
+
19
+ ---
20
+
21
+ ## Commands
22
+
23
+ | Command | What it does |
24
+ | --- | --- |
25
+ | `forma render <file>` | solve a document and write a binary STL |
26
+ | `forma params <file>` | list the parameters the document declares |
27
+ | `forma models <file>` | list the models the document declares |
28
+
29
+ A `<file>` of `-` reads the document from stdin. `params` and `models` take `--json`.
30
+
31
+ A document, and an `-o`/`--out` that names one, must resolve inside the directory the command
32
+ is run in: a path that leaves it - by `..`, an absolute path, or a symlink pointing out of the
33
+ tree - is refused with exit code 2.
34
+
35
+ ### `render`
36
+
37
+ | Option | Default | What it does |
38
+ | --- | --- | --- |
39
+ | `-o, --out <file>` | `<file>.stl` | where to write. `-` is stdout |
40
+ | `-m, --model <name>` | the first declared | which [model](reference/model.md) to build |
41
+ | `--part <name>` | see below | one [part](reference/part.md) by name |
42
+ | `-p, --param <name=val>` | each param's own default | a value for one declared [param](reference/param.md). Repeatable |
43
+ | `--params <file>` | - | a JSON object of values by name. `-p` wins over it |
44
+ | `--segments <n>` | the kernel's own | a fixed segment count for every curve |
45
+ | `--min-angle <deg>` | the kernel's own | smallest angle between adjacent segments of a curve |
46
+ | `--min-edge <n>` | the kernel's own | smallest length a curve segment may be shortened to |
47
+ | `--max-nodes <n>` | no limit | refuse a document that would build more blocks than this |
48
+ | `-q, --quiet` | off | do not print the summary |
49
+
50
+ The three resolution options are [`setQuality`](api/kernel.md) under another name, and are
51
+ global to the kernel exactly as they are there. `--max-nodes` is
52
+ [`render`](api/render.md)'s own budget.
53
+
54
+ Without `--part`, a model with one part exports that part, and a model with several exports
55
+ them **unioned**. STL has no notion of a part, and a slicer reading two overlapping shells
56
+ makes a mess of them.
57
+
58
+ ---
59
+
60
+ ## Passing parameters
61
+
62
+ Three ways, and they compose.
63
+
64
+ **One at a time, on the command line.** A value is JSON where it parses as JSON, and the
65
+ string as written otherwise - so there is no second layer of quoting to get wrong:
66
+
67
+ ```bash
68
+ forma render part.forma -p count=6 -p center=true -p size=[10,20] -p label=M6
69
+ ```
70
+
71
+ | Written | Arrives as |
72
+ | --- | --- |
73
+ | `-p count=6` | the number `6` |
74
+ | `-p center=true` | the boolean `true` |
75
+ | `-p size=[10,20]` | the list `[10, 20]` |
76
+ | `-p label=M6` | the string `"M6"` |
77
+
78
+ **From a file.** `--params` takes a JSON object of values by name - the thing you commit and
79
+ diff, rather than assemble in a shell loop:
80
+
81
+ ```json
82
+ {
83
+ "height": 36,
84
+ "label": "M6",
85
+ "center": true
86
+ }
87
+ ```
88
+
89
+ ```bash
90
+ forma render riser.forma --params params.json -o riser.stl
91
+ ```
92
+
93
+ It has to be a JSON **object**. An array or a bare number parses but names nothing, and is
94
+ refused with exit code 2 rather than quietly contributing nothing. Like every other path, it
95
+ must resolve inside the directory the command runs in.
96
+
97
+ **Both.** `-p` wins over `--params`, which is what a build matrix wants: the values every
98
+ variant shares live in the file, and the one that varies goes on the line.
99
+
100
+ ```bash
101
+ for h in 8 20 40; do
102
+ forma render riser.forma --params shared.json -p "height=$h" -o "build/riser-$h.stl"
103
+ done
104
+ ```
105
+
106
+ And the document will tell you what it takes, so the first `params.json` need not be written
107
+ by hand:
108
+
109
+ ```bash
110
+ forma params riser.forma --json | jq 'map({(.name): .default}) | add' > params.json
111
+ ```
112
+
113
+ ```json
114
+ {
115
+ "height": 20,
116
+ "label": "A",
117
+ "center": false
118
+ }
119
+ ```
120
+
121
+ ---
122
+
123
+ ## In a pipeline
124
+
125
+ Only what was asked for goes to stdout - the STL under `-o -`, and the listings. The summary
126
+ and every diagnostic go to stderr, so a pipe stays clean:
127
+
128
+ ```bash
129
+ forma render riser.forma --segments 64 -o - > riser.stl
130
+ ```
131
+
132
+ Exit codes are the three a script needs to tell apart:
133
+
134
+ | Code | What happened |
135
+ | --- | --- |
136
+ | `0` | it worked |
137
+ | `1` | the document did not build - a syntax error, an unknown name, a degenerate solid |
138
+ | `2` | the invocation was wrong - an unknown flag, a missing file, a part that does not exist |
139
+
140
+ The summary names the cache hits, because that is the number explaining why the second render
141
+ of the same document in a loop is faster, and it is otherwise invisible:
142
+
143
+ ```
144
+ build/riser.stl - 252 triangles · model "riser", part "body" · 6 nodes, 6 solved, 0 cached · 9 ms
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Failures
150
+
151
+ A failure is reported where an editor and a CI annotation can both find it: the position as a
152
+ `file:line:column:` prefix, then the lines around it with a caret under the column.
153
+
154
+ ```
155
+ riser.forma:7:7: rounded_rect: unknown attribute "corner_radius" - rounded_rect reads "center", "radius", "segments", "size"
156
+ 5 | extrude {
157
+ 6 | height = var.height
158
+ 7 | rounded_rect { size = [40, 20] corner_radius = 4 }
159
+ | ^
160
+ 8 | }
161
+ 9 | }
162
+ ```
163
+
164
+ The position comes from the error itself - see [Errors](errors.md) for what each stage raises
165
+ and what it means.
166
+
167
+ ---
168
+
169
+ ## What a document declares
170
+
171
+ `params --json` is the document describing itself, which is what lets a script build every
172
+ variant without being told what the variants are:
173
+
174
+ ```bash
175
+ forma params riser.forma --json
176
+ ```
177
+
178
+ ```json
179
+ [
180
+ {
181
+ "name": "height",
182
+ "type": "number",
183
+ "default": 20,
184
+ "required": false,
185
+ "min": 8,
186
+ "max": 40
187
+ }
188
+ ]
189
+ ```
190
+
191
+ `required` is true for a param declared with no default, which is one that has to be supplied
192
+ before the document will build at all.
193
+
194
+ ---
195
+
196
+ ## Units
197
+
198
+ Lengths are unitless in forma, and STL has no unit either. Every consumer treats them as
199
+ millimetres.
@@ -25,7 +25,7 @@ Raised while turning characters into tokens. Always a typo.
25
25
 
26
26
  | Message | Cause |
27
27
  | --- | --- |
28
- | `unexpected character "…"` | A character that cannot begin a token — a smart quote, a stray backtick, a non-ASCII dash |
28
+ | `unexpected character "…"` | A character that cannot begin a token - a smart quote, a stray backtick, a non-ASCII dash |
29
29
  | `unterminated string` | A `"` with no closing `"`, or a literal newline inside one |
30
30
  | `unterminated block comment` | A `/*` with no `*/`. Block comments do not nest |
31
31
  | `unknown escape \q` | Only `\n` `\t` `\r` `\"` `\\` `\$` exist |
@@ -42,7 +42,7 @@ Raised while turning tokens into a tree. Always a grammar mistake.
42
42
  | `expected a block, found "…"` | Something at the top level that is not a block |
43
43
  | `unknown top-level block "shape"` | Only `param`, `local`, `component` and `model` may appear at the top level |
44
44
  | `expected an attribute or block, found "…"` | Inside a body, an entry that does not start with a name |
45
- | `expected {, found "…"` | A `param` with no `=` and no body — write `param s { }`. Also a `component` or `model` with no body |
45
+ | `expected {, found "…"` | A `param` with no `=` and no body - write `param s { }`. Also a `component` or `model` with no body |
46
46
  | `expected =, found "…"` | A `local` with no value |
47
47
  | `expected ident, found "…"` | A keyword where a name was required |
48
48
  | `"for" cannot be used as a block type` | One of the eleven [keywords](language/syntax.md#keywords) used as a block name |
@@ -93,7 +93,7 @@ Raised while evaluating.
93
93
  | Message | Cause |
94
94
  | --- | --- |
95
95
  | `unknown name "x"` | Nothing in scope binds it. See [Scope](language/scope.md) |
96
- | `no attribute "x"` | A missing key on an object — including `var.x` for something that is not a param |
96
+ | `no attribute "x"` | A missing key on an object - including `var.x` for something that is not a param |
97
97
  | `cannot read "x" of nothing` | A property read on `null` |
98
98
  | `cannot index this value` | Indexing something that is not a list, string or object |
99
99
  | `cannot negate a non-number` | Unary `-` on a string, list or bool |
@@ -115,7 +115,7 @@ operator: `outer - inner`. See [Identifiers](language/syntax.md#identifiers).
115
115
  ### `unknown name "var"`
116
116
 
117
117
  A param default referred to `var`, which is assembled only after every param has resolved.
118
- Use the earlier param's bare name instead — see
118
+ Use the earlier param's bare name instead - see
119
119
  [the root scope](language/scope.md#the-root-scope).
120
120
 
121
121
  ---
@@ -135,11 +135,11 @@ Raised while building geometry.
135
135
 
136
136
  | Message | Cause |
137
137
  | --- | --- |
138
- | `unknown block "bloop" — the blocks are …` | Not a builtin and not a declared component. The message lists every block there is, this document's own components first |
138
+ | `unknown block "bloop" - the blocks are …` | Not a builtin and not a declared component. The message lists every block there is, this document's own components first |
139
139
  | `"box" does not take a label` | Only [`part`](reference/part.md) takes a meaningful label |
140
140
  | `"box" is a shape and cannot contain other blocks` | A shape is a leaf. Usually a missing `}` swallowed the next block |
141
141
  | `"translate" needs at least one shape inside it` | An operation with nothing to act on |
142
- | `box: unknown attribute "colour" — box reads …` | Every block rejects attributes it did not read, so a typo fails loudly, and names what it would have read |
142
+ | `box: unknown attribute "colour" - box reads …` | Every block rejects attributes it did not read, so a typo fails loudly, and names what it would have read |
143
143
  | `attribute "size" is set twice` | The same attribute written twice in one block |
144
144
  | `box: "size" must be a number or an array of 3 numbers` | Wrong shape of value |
145
145
  | `circle: "radius" must be a finite number` | A `NaN` or `Infinity` reached the block |
@@ -149,7 +149,7 @@ Raised while building geometry.
149
149
  | `cylinder: give "radius"/"diameter", or both "bottom_radius" and "top_radius"` | One end radius on its own is ambiguous |
150
150
  | `offset: "join" must be one of "Square", "Round", "Miter"` | An enum attribute with an unrecognised value |
151
151
 
152
- ### `box: unknown attribute "colour" — box reads "center", "size"`
152
+ ### `box: unknown attribute "colour" - box reads "center", "size"`
153
153
 
154
154
  Worth dwelling on, because it is the error that saves the most time. A misspelled attribute
155
155
  would otherwise be silent, and the model would render subtly wrong with nothing to point at.
@@ -158,8 +158,8 @@ anything through.
158
158
 
159
159
  Both this and `unknown block` carry the alternatives, because the reader is often a program
160
160
  with no copy of this manual: an error that says only what is wrong leaves it guessing at
161
- spellings. A block that branches on which of two spellings it was given — `radius` against
162
- `diameter` — reads only one of them, so the list is what that call would accept rather than
161
+ spellings. A block that branches on which of two spellings it was given - `radius` against
162
+ `diameter` - reads only one of them, so the list is what that call would accept rather than
163
163
  always the whole set.
164
164
 
165
165
  ---
@@ -170,7 +170,7 @@ always the whole set.
170
170
  | --- | --- |
171
171
  | `"union" cannot mix 2D and 3D shapes` | Children of different dimensionality in one block |
172
172
  | `"extrude" takes 2D geometry, got 3D` | A block that fixes what it takes got the other thing |
173
- | `part "body" is 2D — extrude or revolve it before rendering` | A cross-section reached the renderer |
173
+ | `part "body" is 2D - extrude or revolve it before rendering` | A cross-section reached the renderer |
174
174
  | `cannot combine 2D and 3D geometry in a union` | The same mistake, from the [node API](api/geometry-node.md) rather than a document |
175
175
 
176
176
  See [Dimensionality](language/overview.md#dimensionality) for the three rules, and
@@ -186,7 +186,7 @@ See [Dimensionality](language/overview.md#dimensionality) for the three rules, a
186
186
  | `c: unknown param "t"` | Components have no pass-through: every value must be declared |
187
187
  | `component "c" produced no geometry` | An empty body, a loop over an empty list, or an `if` that took no branch |
188
188
  | `a component cannot declare parts` | Scene structure belongs in the model |
189
- | `nesting deeper than 64 blocks — is a component using itself?` | Direct or mutual recursion. Rewrite as a `for` loop |
189
+ | `nesting deeper than 64 blocks - is a component using itself?` | Direct or mutual recursion. Rewrite as a `for` loop |
190
190
 
191
191
  ---
192
192
 
@@ -200,7 +200,7 @@ See [Dimensionality](language/overview.md#dimensionality) for the three rules, a
200
200
  | `the document declares no model` | Add a `model` block |
201
201
  | `no model named "x"` | `render(source, { model })` named one that does not exist |
202
202
 
203
- A [`part`](reference/part.md) nested inside an operation raises **nothing** — it is silently
203
+ A [`part`](reference/part.md) nested inside an operation raises **nothing** - it is silently
204
204
  dropped. If a part is missing from a render, check it is written directly in the model.
205
205
 
206
206
  ---
@@ -217,7 +217,7 @@ dropped. If a part is missing from a render, check it is written directly in the
217
217
 
218
218
  | Message | Cause |
219
219
  | --- | --- |
220
- | `align needs a kernel context — render the model rather than building it` | An `Evaluator` built with no context. [`render`](api/render.md) always supplies one |
220
+ | `align needs a kernel context - render the model rather than building it` | An `Evaluator` built with no context. [`render`](api/render.md) always supplies one |
221
221
  | `align needs at least one shape inside it` | Nothing to measure |
222
222
  | `align: "x" must be "min", "center", "max" or a number` | The spelling is `"center"` |
223
223
 
@@ -239,7 +239,7 @@ Raised by the JavaScript API rather than by a document.
239
239
 
240
240
  ## Silent behaviours
241
241
 
242
- Not errors, and each is deliberate — but each has surprised someone. A parameter reaching the
242
+ Not errors, and each is deliberate - but each has surprised someone. A parameter reaching the
243
243
  end of its range should not break a model, so these degenerate quietly:
244
244
 
245
245
  | What | Result |
@@ -1,7 +1,7 @@
1
1
  # Control flow
2
2
 
3
3
  Two constructs, both of which appear where a block appears and produce geometry rather than
4
- a value. Neither is an expression — for a value that depends on a condition, use
4
+ a value. Neither is an expression - for a value that depends on a condition, use
5
5
  [`cond ? a : b`](expressions.md#conditional).
6
6
 
7
7
  ---
@@ -38,7 +38,7 @@ difference {
38
38
  | **Sequence** | Any expression that evaluates to a list. Iterating anything else is an error: `for: expected a list to iterate` |
39
39
  | **Names** | One name binds the value. Two names bind the index and then the value |
40
40
  | **Scope** | Each iteration gets a fresh child scope holding just those names |
41
- | **Result** | The concatenation of what every iteration produced — geometry, and any `part`s |
41
+ | **Result** | The concatenation of what every iteration produced - geometry, and any `part`s |
42
42
 
43
43
  #### Caveats
44
44
 
@@ -47,7 +47,7 @@ difference {
47
47
  [`if`](#if) when a parameter can legitimately reach zero.
48
48
  - `range(a, b)` **excludes** `b`, and produces an empty list rather than looping forever when
49
49
  the range runs backwards. See [`range`](../reference/functions.md#range).
50
- - Iterating a string does not work — strings are not lists here. Use
50
+ - Iterating a string does not work - strings are not lists here. Use
51
51
  [`len`](../reference/functions.md#len) and index into it if you need the characters.
52
52
  - There is no `break`, no `continue` and no accumulator. A loop that needs to skip items
53
53
  should filter the list it iterates, or wrap its body in an `if`.
@@ -17,7 +17,7 @@ their type, and a mismatch is reported where it is used.
17
17
  | **object** | `{ w = 3, h = 4 }`, `{ w: 3 }` | `=` and `:` are interchangeable. Read with `.key` |
18
18
  | **null** | `null` | Falsy; renders as the empty string in interpolation |
19
19
 
20
- There is no separate vector type in the language — a vector is a list of numbers, and blocks
20
+ There is no separate vector type in the language - a vector is a list of numbers, and blocks
21
21
  say how many components they want. Where a block wants a vector, a **single number is
22
22
  accepted as shorthand for every component**, so `scale { factor = 2 }` and
23
23
  `scale { factor = [2, 2, 2] }` are the same.
@@ -69,7 +69,7 @@ What each accepts:
69
69
  Two lists of different lengths cannot be combined:
70
70
  `cannot combine vectors of length 2 and 3`.
71
71
 
72
- **Division and modulo by zero are errors**, not `Infinity` or `NaN` — a NaN would otherwise
72
+ **Division and modulo by zero are errors**, not `Infinity` or `NaN` - a NaN would otherwise
73
73
  travel all the way into the kernel and surface as a degenerate vertex a long way from its
74
74
  cause.
75
75
 
@@ -89,7 +89,7 @@ as [interpolation](#string-interpolation):
89
89
 
90
90
  ```hcl
91
91
  [1, 2] == [1, 2] // true
92
- 1 == "1" // false — different types are never equal
92
+ 1 == "1" // false - different types are never equal
93
93
  ```
94
94
 
95
95
  `<`, `<=`, `>` and `>=` require both sides to be the same type, and compare numbers
@@ -99,7 +99,7 @@ numerically and strings lexicographically. Mixing them is an error:
99
99
  ### Logic
100
100
 
101
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
102
+ answer - `count > 0 && list[0] > 1` is safe on an empty list. Both always return a bool, even
103
103
  when given non-bool operands: `1 || false` is `true`, not `1`.
104
104
 
105
105
  `!x` negates the [truthiness](#truthiness) of any value. Unary `-` requires a number:
@@ -118,7 +118,7 @@ right-associative, so `a ? b : c ? d : e` chains as `a ? b : (c ? d : e)`.
118
118
 
119
119
  ## Truthiness
120
120
 
121
- Conditions — in `? :`, `&&`, `||`, `!` and [`if`](control-flow.md#if) — accept any value.
121
+ Conditions - in `? :`, `&&`, `||`, `!` and [`if`](control-flow.md#if) - accept any value.
122
122
 
123
123
  | Falsy | Truthy |
124
124
  | --- | --- |
@@ -147,7 +147,7 @@ opts.wall // 2
147
147
  opts["wall"] // 2
148
148
  ```
149
149
 
150
- `.x`, `.y` and `.z` on a **list** are sugar for indices 0, 1 and 2 — they are not a vector
150
+ `.x`, `.y` and `.z` on a **list** are sugar for indices 0, 1 and 2 - they are not a vector
151
151
  type. They are read on a list of any length, so `[1, 2].z` yields nothing rather than
152
152
  raising; the failure surfaces later, wherever that nothing is used.
153
153
 
@@ -165,7 +165,7 @@ an error: `cannot index this value`.
165
165
  ```hcl
166
166
  max(sizes) // variadic, flattens its arguments
167
167
  pow(2, 10)
168
- sin(30) // 0.5 — every angle is in degrees
168
+ sin(30) // 0.5 - every angle is in degrees
169
169
  ```
170
170
 
171
171
  Functions take **positional arguments only**. Named arguments are an error:
@@ -194,12 +194,12 @@ How each value renders:
194
194
 
195
195
  | Value | Rendered as |
196
196
  | --- | --- |
197
- | number | `3`, `0.5` — JavaScript's default number formatting |
197
+ | number | `3`, `0.5` - JavaScript's default number formatting |
198
198
  | string | itself |
199
199
  | bool | `true` / `false` |
200
- | list | `[1, 2, 3]` — elements joined by `, ` inside brackets, recursively |
200
+ | list | `[1, 2, 3]` - elements joined by `, ` inside brackets, recursively |
201
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 |
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
203
 
204
204
  A splice must contain exactly one expression; anything left over is an error:
205
205
  `trailing input in ${ }`. Errors raised inside a splice are reported at the position of the
@@ -21,7 +21,7 @@ model "stack" { // something to render
21
21
  }
22
22
  ```
23
23
 
24
- Order does not matter between declarations of different kinds — a `model` may use a
24
+ Order does not matter between declarations of different kinds - a `model` may use a
25
25
  `component` declared below it. Order does matter among `local`s, which are evaluated top to
26
26
  bottom. See [Scope and names](scope.md).
27
27
 
@@ -41,11 +41,11 @@ mistake it was.
41
41
  1. **Lexer** turns characters into tokens. It rejects unknown characters, unterminated
42
42
  strings and unterminated block comments.
43
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
44
+ grammatical - a missing brace, a keyword used as a block type, a `param` block containing
45
45
  nested blocks.
46
46
  3. **Evaluator** resolves params and locals, then walks the models. It rejects unknown names,
47
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.
48
+ is a tree of [geometry nodes](../api/geometry-node.md) - it never calls the kernel itself.
49
49
  4. **Kernel** solves the node tree into concrete solids, memoized by node digest in an
50
50
  [`EvaluationContext`](../api/evaluation-context.md).
51
51
 
@@ -95,9 +95,9 @@ An operation with no children is an error; a shape with children is an error.
95
95
 
96
96
  Every piece of geometry is either 2D or 3D, and the language tracks which.
97
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.
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
101
  - **Operations** either fix what they accept (`extrude` takes 2D, `refine` takes 3D) or pass
102
102
  their children's dimensionality through (`translate`, `rotate`, `hull`, the booleans).
103
103
 
@@ -107,7 +107,7 @@ Three rules follow, and each has its own error message:
107
107
  | --- | --- |
108
108
  | The children of one block must all be the same dimensionality | `"union" cannot mix 2D and 3D shapes` |
109
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` |
110
+ | Anything rendered must be 3D | `part "body" is 2D - extrude or revolve it before rendering` |
111
111
 
112
112
  To get from 2D to 3D use [`extrude`](../reference/conversions.md#extrude) or
113
113
  [`revolve`](../reference/conversions.md#revolve); to go back use
@@ -118,8 +118,8 @@ To get from 2D to 3D use [`extrude`](../reference/conversions.md#extrude) or
118
118
 
119
119
  ## Units and orientation
120
120
 
121
- - **Lengths are unitless.** Every consumer treats them as millimetres — STL has no unit
122
- either — but nothing in the language enforces that.
121
+ - **Lengths are unitless.** Every consumer treats them as millimetres - STL has no unit
122
+ either - but nothing in the language enforces that.
123
123
  - **Angles are always in degrees.** Everywhere: `rotate { z = 90 }`, `revolve { angle = 180 }`,
124
124
  `sin(30)`, `param a { type = angle }`. There is no radian form in the language. The
125
125
  JavaScript [`Angle`](../api/values.md#angle) class stores radians internally, and that is
@@ -134,7 +134,7 @@ To get from 2D to 3D use [`extrude`](../reference/conversions.md#extrude) or
134
134
 
135
135
  ## Identity is content
136
136
 
137
- The evaluator does not produce a call graph — it produces a tree of immutable nodes, each
137
+ The evaluator does not produce a call graph - it produces a tree of immutable nodes, each
138
138
  carrying a 128-bit digest of its own content and its children's digests. Two structurally
139
139
  identical subtrees have the same digest and therefore *are* the same node.
140
140
 
@@ -142,7 +142,7 @@ That has consequences worth designing around:
142
142
 
143
143
  - A component used twenty times is evaluated once. Eight spheres in a loop cost one sphere
144
144
  and eight transforms.
145
- - Two shapes written with their attributes in a different order still share a cache entry —
145
+ - Two shapes written with their attributes in a different order still share a cache entry -
146
146
  object keys are sorted before they are digested.
147
147
  - The members of a `union` are sorted by subtree size, so two unions written in a different
148
148
  order also share a digest. `difference` is never reordered, because its first child means
@@ -1,7 +1,7 @@
1
1
  # Scope and names
2
2
 
3
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
4
+ in each one, and in what order they are built - which is what determines whether a
5
5
  declaration may refer to another.
6
6
 
7
7
  ---
@@ -10,12 +10,12 @@ declaration may refer to another.
10
10
 
11
11
  Built once, before any geometry, in this order:
12
12
 
13
- 1. **Constants** — `pi` and `e`.
14
- 2. **Type names** — `number`, `string`, `bool`, `vector`, `list`, `angle`, each bound to
13
+ 1. **Constants** - `pi` and `e`.
14
+ 2. **Type names** - `number`, `string`, `bool`, `vector`, `list`, `angle`, each bound to
15
15
  itself as a string, so `type = number` in a param block resolves with no quoting.
16
16
  3. **Params**, in source order. Each is bound under its bare name as it is resolved, so a
17
17
  later param's default may refer to an earlier one.
18
- 4. **`var`** — an object holding every resolved param.
18
+ 4. **`var`** - an object holding every resolved param.
19
19
  5. **Locals**, in source order. Each is bound as it is resolved, so a later local may refer
20
20
  to an earlier one.
21
21
 
@@ -24,10 +24,10 @@ The order has two consequences worth knowing:
24
24
  ```hcl
25
25
  param a = 3
26
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
27
+ param c = var.a * 2 // ✗ unknown name "var" - var does not exist yet
28
28
 
29
29
  local d = var.a + 1 // ✓ locals are built after var
30
- local e = f // ✗ unknown name "f" — a later local
30
+ local e = f // ✗ unknown name "f" - a later local
31
31
  local f = 2
32
32
  ```
33
33
 
@@ -45,8 +45,8 @@ model "m" {
45
45
  }
46
46
  ```
47
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
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
50
  to reach an input:
51
51
 
52
52
  ```hcl
@@ -68,8 +68,8 @@ of use.
68
68
 
69
69
  ## Body scopes
70
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.
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
73
 
74
74
  **Locals declared in a body are visible to the whole body**, not only to what follows them,
75
75
  because every local in a body is resolved before any of its blocks are built:
@@ -82,7 +82,7 @@ model "m" {
82
82
  ```
83
83
 
84
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
85
+ body, ordering between locals still applies - a body local may not refer to a later body
86
86
  local.
87
87
 
88
88
  ---
@@ -110,7 +110,7 @@ model "m" {
110
110
  Inside a component body, these are in scope:
111
111
 
112
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.`)
113
+ - everything in the root scope - constants, type names, document params (bare and via `var.`)
114
114
  and document locals
115
115
  - its own body locals
116
116
 
@@ -123,7 +123,7 @@ Nothing from the caller. Values travel in through params and no other way.
123
123
  A `model` may declare its own `param` blocks. They behave like locals with a declaration
124
124
  syntax, not like document params:
125
125
 
126
- - Each **must** have a default — `model "m": param "s" needs a default`.
126
+ - Each **must** have a default - `model "m": param "s" needs a default`.
127
127
  - They are **not** settable from outside. `render(source, { params: { s: 9 } })` sets
128
128
  document params only; a model param keeps its default.
129
129
  - They are bound in the model's scope under the bare name, and are not added to `var`.
@@ -31,7 +31,7 @@ Decimal, with an optional fraction and an optional exponent.
31
31
  There is no hexadecimal, no separator, and no unit suffix. A leading `-` is the unary minus
32
32
  operator, not part of the literal.
33
33
 
34
- `2e` and `2em` are **not** numbers with exponents — the lexer backs out of a bare `e` and
34
+ `2e` and `2em` are **not** numbers with exponents - the lexer backs out of a bare `e` and
35
35
  lets it start an identifier, so `2e` lexes as the number `2` followed by the name `e`.
36
36
 
37
37
  ---
@@ -46,7 +46,7 @@ Double quotes only. A string may not contain a literal newline.
46
46
  "a tab\there, a quote\", a literal \${ not a splice"
47
47
  ```
48
48
 
49
- **Escapes.** Exactly six: `\n`, `\t`, `\r`, `\"`, `\\` and `\$`. Anything else is an error —
49
+ **Escapes.** Exactly six: `\n`, `\t`, `\r`, `\"`, `\\` and `\$`. Anything else is an error -
50
50
  `unknown escape \q`. `\$` exists so a literal `${` can be written.
51
51
 
52
52
  **Interpolation.** `${ ... }` splices in any expression. Braces inside the splice are
@@ -65,7 +65,7 @@ height wall_thickness _private min-width part2
65
65
  ```
66
66
 
67
67
  > **Hyphens are part of a name.** `a-b` is one identifier, not `a` minus `b`. Write
68
- > subtraction with spaces: `a - b`. This is the single most common surprise in the lexer —
68
+ > subtraction with spaces: `a - b`. This is the single most common surprise in the lexer -
69
69
  > `local gap = outer-inner` fails with `unknown name "outer-inner"`.
70
70
 
71
71
  ### Keywords
@@ -111,7 +111,7 @@ Labels come in two flavours:
111
111
  | `component "name"`, `model "name"` | Static | The name must be a plain string or identifier. `${ }` is rejected: `a label cannot contain ${ }` |
112
112
  | `part "name"` | An expression | Evaluated at build time, so a part in a loop can name itself: `part "leg_${i}"` |
113
113
 
114
- Blocks that take no label reject one — `"box" does not take a label`. Two blocks accept a
114
+ Blocks that take no label reject one - `"box" does not take a label`. Two blocks accept a
115
115
  label and quietly ignore it: [`align`](../reference/align.md) and
116
116
  [component calls](../reference/component.md).
117
117