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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/package.json +52 -0
  4. package/src/bin.js +30 -0
  5. package/src/catalogue.js +703 -0
  6. package/src/document.js +285 -0
  7. package/src/examples.js +212 -0
  8. package/src/guide.js +313 -0
  9. package/src/reference/README.md +153 -0
  10. package/src/reference/api/describe-parameters.md +167 -0
  11. package/src/reference/api/evaluation-context.md +221 -0
  12. package/src/reference/api/export.md +185 -0
  13. package/src/reference/api/geometry-node.md +215 -0
  14. package/src/reference/api/kernel.md +182 -0
  15. package/src/reference/api/low-level.md +239 -0
  16. package/src/reference/api/render.md +217 -0
  17. package/src/reference/api/values.md +223 -0
  18. package/src/reference/errors.md +253 -0
  19. package/src/reference/language/control-flow.md +160 -0
  20. package/src/reference/language/expressions.md +206 -0
  21. package/src/reference/language/overview.md +156 -0
  22. package/src/reference/language/scope.md +184 -0
  23. package/src/reference/language/syntax.md +210 -0
  24. package/src/reference/reference/align.md +189 -0
  25. package/src/reference/reference/booleans.md +231 -0
  26. package/src/reference/reference/component.md +248 -0
  27. package/src/reference/reference/conversions.md +269 -0
  28. package/src/reference/reference/functions.md +314 -0
  29. package/src/reference/reference/local.md +197 -0
  30. package/src/reference/reference/model.md +213 -0
  31. package/src/reference/reference/param.md +230 -0
  32. package/src/reference/reference/part.md +229 -0
  33. package/src/reference/reference/refinement.md +284 -0
  34. package/src/reference/reference/shapes-2d.md +309 -0
  35. package/src/reference/reference/shapes-3d.md +279 -0
  36. package/src/reference/reference/transforms.md +246 -0
  37. package/src/reference.js +160 -0
  38. package/src/server.js +219 -0
  39. package/src/tools.js +523 -0
  40. package/src/workspace.js +204 -0
@@ -0,0 +1,182 @@
1
+ # Kernel
2
+
3
+ Loading the Manifold WebAssembly module, and tuning how finely it approximates curves.
4
+
5
+ ```js
6
+ import { loadKernel, setQuality, resetQuality } from 'forma-dsl';
7
+ ```
8
+
9
+ Most callers never touch these. [`EvaluationContext.create()`](evaluation-context.md#evaluationcontextcreate)
10
+ loads the kernel for you, and `segments` on a shape handles resolution locally. Reach for
11
+ this page when bundling needs help finding the `.wasm`, or when a draft pass and a final pass
12
+ want different quality.
13
+
14
+ ---
15
+
16
+ ## `loadKernel`
17
+
18
+ ### `loadKernel(options?)`
19
+
20
+ Loads and sets up the Manifold module, once per page or process.
21
+
22
+ | | |
23
+ | --- | --- |
24
+ | **`options.locateFile`** | `() => string` — returns the URL of the `.wasm` file |
25
+ | **Returns** | `Promise<Kernel>` — the `manifold-3d` toplevel module |
26
+
27
+ #### Caveats
28
+
29
+ - **Memoized on the promise, not the result.** Two models evaluating concurrently at startup
30
+ share one instantiation rather than each starting their own.
31
+ - **Only the first call's options take effect.** Later calls get the memoized promise, so
32
+ `locateFile` has to be right the first time.
33
+ - A rejected load is *not* memoized — a WASM fetch that lost the network can be retried
34
+ rather than being permanently broken for the page.
35
+
36
+ ### Bundlers and `locateFile`
37
+
38
+ A bundler that fingerprints assets renames `manifold.wasm`, and the module cannot then find
39
+ it by its own relative path. Point at the built URL:
40
+
41
+ ```js
42
+ import wasmUrl from 'manifold-3d/manifold.wasm?url';
43
+ import { loadKernel } from 'forma-dsl';
44
+
45
+ await loadKernel({ locateFile: () => wasmUrl });
46
+ ```
47
+
48
+ Do this **before** the first `EvaluationContext.create()`, or the default path is what gets
49
+ memoized.
50
+
51
+ In Node, nothing is needed — the module resolves the file itself.
52
+
53
+ ---
54
+
55
+ ## `setQuality`
56
+
57
+ ### `setQuality(options?)`
58
+
59
+ Sets the resolution used for curved surfaces, globally.
60
+
61
+ | Option | Type | Description |
62
+ | --- | --- | --- |
63
+ | `minAngle` | `number` | Smallest angle, in degrees, between adjacent segments of a curve |
64
+ | `minEdgeLength` | `number` | Smallest length a curve segment may be shortened to |
65
+ | `segments` | `number` | A fixed segment count, overriding both limits above |
66
+
67
+ Returns `Promise<void>`, resolving once the kernel is loaded and configured. Each option is
68
+ left alone when omitted.
69
+
70
+ #### Caveats
71
+
72
+ - **These are global to the kernel, not per model.** A viewer that renders a draft and then a
73
+ final pass has to set them around each evaluation, not once at load.
74
+ - **Quality settings are not part of a node's digest.** The cache is keyed on content, so a
75
+ solid built at draft quality will be *reused* at final quality unless the cache is cleared.
76
+ See [Usage](#a-draft-pass-and-a-final-pass).
77
+ - A shape's own `segments` attribute overrides all of this for that shape. Prefer it whenever
78
+ a specific facet count matters — a hole that must clear a printed bolt should not depend on
79
+ a global.
80
+
81
+ ## `resetQuality`
82
+
83
+ ### `resetQuality()`
84
+
85
+ Restores the kernel's own defaults. Returns `Promise<void>`.
86
+
87
+ ---
88
+
89
+ ## Usage
90
+
91
+ ### A draft pass and a final pass
92
+
93
+ Because quality is global and digests do not include it, the cache has to be dropped between
94
+ passes — otherwise the final render happily reuses the draft's geometry:
95
+
96
+ ```js
97
+ import { setQuality, EvaluationContext, render } from 'forma-dsl';
98
+
99
+ // Draft: fast, coarse.
100
+ await setQuality({ segments: 16 });
101
+ let context = await EvaluationContext.create();
102
+ const draft = await render(source, { params, context });
103
+ draw(draft.parts);
104
+ context.dispose();
105
+
106
+ // Final: a fresh context, so nothing coarse survives.
107
+ await setQuality({ minAngle: 1, minEdgeLength: 0.2 });
108
+ context = await EvaluationContext.create();
109
+ const final = await render(source, { params, context });
110
+ draw(final.parts);
111
+ ```
112
+
113
+ If instead you want one context throughout, put the resolution in the document so it *is*
114
+ part of the digest:
115
+
116
+ ```hcl
117
+ param facets { type = number default = 32 min = 8 max = 160 step = 8 }
118
+
119
+ model "m" {
120
+ cylinder { radius = 10 height = 20 segments = var.facets }
121
+ }
122
+ ```
123
+
124
+ That is the better answer for an editor: changing `facets` changes the content, so the cache
125
+ does the right thing automatically.
126
+
127
+ ### Global quality for an export
128
+
129
+ A one-off high-quality export, then back to normal:
130
+
131
+ ```js
132
+ await setQuality({ minEdgeLength: 0.1 });
133
+ try {
134
+ const context = await EvaluationContext.create();
135
+ const result = await render(source, { params, context });
136
+ await writeFile('final.stl', toBinarySTL(result.parts[0].concrete));
137
+ context.dispose();
138
+ } finally {
139
+ await resetQuality();
140
+ }
141
+ ```
142
+
143
+ ### Reaching the kernel directly
144
+
145
+ `context.wasm` is the loaded module, and every Manifold API is on it:
146
+
147
+ ```js
148
+ const context = await EvaluationContext.create();
149
+ const { Manifold, CrossSection } = context.wasm;
150
+ ```
151
+
152
+ Anything you construct this way is **yours** to delete — the context only owns what it built
153
+ itself.
154
+
155
+ ---
156
+
157
+ ## Troubleshooting
158
+
159
+ ### The page 404s on `manifold.wasm`
160
+
161
+ The bundler renamed it. Pass `locateFile`, before the first context is created:
162
+
163
+ ```js
164
+ import wasmUrl from 'manifold-3d/manifold.wasm?url';
165
+ await loadKernel({ locateFile: () => wasmUrl });
166
+ ```
167
+
168
+ ### `locateFile` is being ignored
169
+
170
+ The kernel was already loaded — the promise is memoized, so only the first call's options
171
+ count. Move the `loadKernel` call earlier, before anything creates a context.
172
+
173
+ ### `setQuality` had no effect on an existing model
174
+
175
+ The geometry was already cached. Quality is not part of the digest, so the cached solid is
176
+ returned unchanged. Use a fresh context, or put the segment count in the document where it
177
+ becomes part of the content.
178
+
179
+ ### Curves are too coarse in one place only
180
+
181
+ Set `segments` on that shape rather than moving the global. It is per-shape, it is part of the
182
+ digest, and it is visible to whoever reads the model.
@@ -0,0 +1,239 @@
1
+ # Compiler stages
2
+
3
+ The pipeline, stage by stage, plus the registries the evaluator reads. These are exported so
4
+ that editors, linters and tools can reach into the compiler without re-implementing it — the
5
+ [editor](../../../editor) uses them for syntax
6
+ highlighting and inline diagnostics.
7
+
8
+ ```js
9
+ import {
10
+ tokenize, parse, Program, Evaluator, FormaError,
11
+ BLOCKS, FUNCTIONS, CONSTANTS,
12
+ } from 'forma-dsl';
13
+ ```
14
+
15
+ For ordinary use, [`render`](render.md) and
16
+ [`describeParameters`](describe-parameters.md) are the front door.
17
+
18
+ ---
19
+
20
+ ## `tokenize`
21
+
22
+ ### `tokenize(source)`
23
+
24
+ Turns source into tokens. Returns `Token[]`, ending with an `eof`.
25
+
26
+ | Field | Type | Description |
27
+ | --- | --- | --- |
28
+ | `type` | `'ident' \| 'number' \| 'string' \| 'punct' \| 'eof'` | |
29
+ | `value` | `any` | A string for `ident` and `punct`, a number for `number`, the parsed runs and splices for `string`, `null` for `eof` |
30
+ | `loc` | `SourceLocation` | `{ line, column, offset }` |
31
+ | `nlBefore` | `boolean` | Whether a line break preceded this token |
32
+
33
+ `nlBefore` is what the parser uses to know an attribute has ended. See
34
+ [the newline rule](../language/syntax.md#the-newline-rule).
35
+
36
+ ---
37
+
38
+ ## `parse`
39
+
40
+ ### `parse(source)`
41
+
42
+ Turns source into an abstract syntax tree. Returns a `FormaDocument`.
43
+
44
+ The AST's shape is an implementation detail of the evaluator and is typed loosely on purpose.
45
+ Use [`Program`](#program) to get at declarations in a stable form.
46
+
47
+ ---
48
+
49
+ ## `Program`
50
+
51
+ ### `Program.parse(source)` · `new Program(document)`
52
+
53
+ A parsed document, indexed by what each declaration is for. Constructing one is what catches
54
+ duplicate names and a component that shadows a builtin.
55
+
56
+ | Property | Type | Description |
57
+ | --- | --- | --- |
58
+ | `params` | `Map<string, unknown>` | Declared params, in source order |
59
+ | `locals` | `unknown[]` | Declared locals, in source order |
60
+ | `components` | `Map<string, unknown>` | Declared components |
61
+ | `models` | `Map<string, unknown>` | Declared models |
62
+
63
+ ### `program.parameterDescriptors(evaluator?)`
64
+
65
+ The declared params as [`ParameterDescriptor`](describe-parameters.md#returns)s. Without an
66
+ evaluator, every field but the name comes back `undefined` —
67
+ [`describeParameters`](describe-parameters.md) is this with one supplied.
68
+
69
+ #### Listing a document's models without rendering
70
+
71
+ ```js
72
+ import { Program } from 'forma-dsl';
73
+
74
+ const program = Program.parse(source);
75
+ [...program.models.keys()]; // ['printable', 'assembly']
76
+ ```
77
+
78
+ ---
79
+
80
+ ## `Evaluator`
81
+
82
+ ### `new Evaluator(program, options?)`
83
+
84
+ Turns a program into geometry nodes. Resolving params and locals happens during
85
+ **construction**, so every error about a missing or unreadable input surfaces before any
86
+ geometry is built.
87
+
88
+ | Option | Type | Description |
89
+ | --- | --- | --- |
90
+ | `params` | `Record<string, ParameterValue>` | Values for the document's params |
91
+ | `context` | [`EvaluationContext`](evaluation-context.md) `\| null` | Needed only by [`align`](../reference/align.md), which measures its child |
92
+ | `requireParams` | `boolean`, default `true` | Off lets a document declaring a required param still be inspected |
93
+
94
+ | Member | Description |
95
+ | --- | --- |
96
+ | `program` | The program being evaluated |
97
+ | `context` | The kernel context, if any |
98
+ | `root` | The document-level [scope](../language/scope.md#the-root-scope) |
99
+ | `expression(node, scope)` | Evaluates one expression |
100
+ | `model(name?)` | Builds one model into a `Scene`. Defaults to the first declared |
101
+
102
+ A `Scene` is `{ name, parts, node }`, where `parts` carry `GeometryNode`s and `node` is
103
+ everything unioned.
104
+
105
+ #### Caveats
106
+
107
+ - **The evaluator does not call the kernel** — except for `align`. Its output is a node tree;
108
+ solving it is [`EvaluationContext`](evaluation-context.md)'s job.
109
+ - Without a `context`, a document containing `align` fails with
110
+ `align needs a kernel context — render the model rather than building it`.
111
+ - `requireParams: false` is for inspection only. A param with no value is simply absent from
112
+ scope, so evaluating geometry that reads it will fail.
113
+
114
+ #### Building a node tree without solving it
115
+
116
+ ```js
117
+ import { Program, Evaluator } from 'forma-dsl';
118
+
119
+ const program = Program.parse(source);
120
+ const scene = await new Evaluator(program, { params: { height: 20 } }).model();
121
+
122
+ scene.node.digest; // identity of the whole model
123
+ scene.node.subtreeSize; // how many nodes it took
124
+ ```
125
+
126
+ Useful for diffing two documents, or for caching geometry identity without loading WASM at
127
+ all.
128
+
129
+ ---
130
+
131
+ ## `FormaError`
132
+
133
+ Every error the language raises. Extends `Error`.
134
+
135
+ | Property | Type | Description |
136
+ | --- | --- | --- |
137
+ | `name` | `'FormaError'` | |
138
+ | `loc` | `SourceLocation \| null` | Where it was raised, when known |
139
+
140
+ The position is appended to the message as well as kept on the error, so a caller that only
141
+ prints `error.message` still tells the reader where to look.
142
+
143
+ ```js
144
+ try {
145
+ await render(source, { context });
146
+ } catch (error) {
147
+ if (error.loc) {
148
+ markProblem(error.loc.line, error.loc.column, error.message);
149
+ } else {
150
+ showBanner(error.message);
151
+ }
152
+ }
153
+ ```
154
+
155
+ Not every failure is a `FormaError` — a few come from the kernel, and `render` raises a plain
156
+ `Error` for a 2D part. Check `error.loc` rather than the class when all you need is a
157
+ position. See [Errors](../errors.md) for the full catalogue.
158
+
159
+ ---
160
+
161
+ ## Registries
162
+
163
+ ### `BLOCKS`
164
+
165
+ Every block the language understands, by name, as `Record<string, BlockDefinition>`.
166
+
167
+ | Field | Description |
168
+ | --- | --- |
169
+ | `dim` | Output dimensionality, where it is fixed regardless of the children |
170
+ | `leaf` | True for a shape, which takes no children |
171
+ | `takes` | What the children must be; `'same'` passes their dimensionality through |
172
+ | `combine` | `'list'` receives every child; anything else receives them unioned |
173
+ | `build(args, children?)` | Produces the node |
174
+
175
+ **[`part`](../reference/part.md) and [`align`](../reference/align.md) are not in it** — the
176
+ evaluator handles both itself, because neither is an ordinary geometry operation.
177
+
178
+ ```js
179
+ import { BLOCKS } from 'forma-dsl';
180
+
181
+ Object.keys(BLOCKS).length; // the builtin blocks
182
+ Object.keys(BLOCKS).filter((k) => BLOCKS[k].leaf); // just the shapes
183
+ ```
184
+
185
+ ### `FUNCTIONS`
186
+
187
+ Every builtin function, as `Record<string, FunctionDefinition>` with `arity` — `null` for
188
+ variadic — and `call(args)`.
189
+
190
+ ### `CONSTANTS`
191
+
192
+ `{ pi, e }`.
193
+
194
+ #### Driving an editor from the registries
195
+
196
+ Keeping completion and highlighting honest means reading the registries rather than a
197
+ hand-kept list:
198
+
199
+ ```js
200
+ import { BLOCKS, FUNCTIONS, CONSTANTS } from 'forma-dsl';
201
+
202
+ const completions = [
203
+ ...Object.keys(BLOCKS).map((name) => ({ label: name, type: 'class' })),
204
+ ...Object.keys(FUNCTIONS).map((name) => ({ label: name, type: 'function' })),
205
+ ...Object.keys(CONSTANTS).map((name) => ({ label: name, type: 'constant' })),
206
+ ];
207
+ ```
208
+
209
+ The docs site does the same thing for its syntax highlighting, which is why a new block
210
+ cannot be added without the documentation's code samples learning about it.
211
+
212
+ ---
213
+
214
+ ## Troubleshooting
215
+
216
+ ### `align needs a kernel context — render the model rather than building it`
217
+
218
+ Pass a context, or avoid `align` in documents you intend to evaluate without one:
219
+
220
+ ```js
221
+ new Evaluator(program, { context: await EvaluationContext.create() });
222
+ ```
223
+
224
+ ### `param "x" has no default and no value was supplied`
225
+
226
+ Thrown from the `Evaluator` **constructor**, not from `model()`. Pass `requireParams: false`
227
+ to inspect such a document without values.
228
+
229
+ ### My hand-driven evaluation returns nodes, not solids
230
+
231
+ That is the design. Pass each `part.node` to
232
+ [`context.evaluate`](evaluation-context.md#contextevaluatenode), or use
233
+ [`render`](render.md), which does both.
234
+
235
+ ### `part` and `align` are missing from `BLOCKS`
236
+
237
+ Neither is an ordinary block: a `part` is a piece of the scene rather than an operand, and
238
+ `align` has to measure its child. Both are handled directly by the evaluator. Add them to any
239
+ list you build from `BLOCKS` for an editor.
@@ -0,0 +1,217 @@
1
+ # `render`
2
+
3
+ Compiles a document and evaluates one model into renderable parts.
4
+
5
+ ```js
6
+ import { render } from 'forma-dsl';
7
+
8
+ const result = await render(source, { params: { height: 20 } });
9
+ ```
10
+
11
+ ---
12
+
13
+ ## Reference
14
+
15
+ ### `render(source, options?)`
16
+
17
+ Parses `source`, resolves its params, evaluates one model into a geometry tree, and solves
18
+ every part into a concrete solid and a mesh.
19
+
20
+ #### Parameters
21
+
22
+ | Parameter | Type | Description |
23
+ | --- | --- | --- |
24
+ | `source` | `string` | The document |
25
+ | `options.model` | `string \| null` | Which model to build. Defaults to the **first** the document declares |
26
+ | `options.params` | `Record<string, ParameterValue>` | Values for the document's top-level params. Anything not declared is ignored |
27
+ | `options.context` | [`EvaluationContext`](evaluation-context.md) `\| null` | A context to reuse. One is created if you do not pass one |
28
+
29
+ #### Returns
30
+
31
+ A `Promise<RenderResult>`:
32
+
33
+ | Field | Type | Description |
34
+ | --- | --- | --- |
35
+ | `name` | `string` | The model's name |
36
+ | `parts` | `RenderedPart[]` | One per [`part`](../reference/part.md) — see below |
37
+ | `program` | [`Program`](low-level.md#program) | The parsed document, for inspecting params |
38
+ | `context` | `EvaluationContext` | The context that owns every solid in `parts` |
39
+ | `stats` | `RenderStats` | `nodes`, `evaluated`, `cacheHits`, `milliseconds` |
40
+
41
+ Each `RenderedPart`:
42
+
43
+ | Field | Type | Description |
44
+ | --- | --- | --- |
45
+ | `name` | `string` | From the part's label |
46
+ | `color` | `string` | From the part's `color` attribute |
47
+ | `opacity` | `number` | From the part's `opacity` attribute |
48
+ | `node` | [`GeometryNode`](geometry-node.md) | The part's tree. This is what `collect` wants |
49
+ | `concrete` | `Solid` | The solved Manifold object, **owned by the context** |
50
+ | `mesh` | `RenderMesh` | `positions`, `normals`, `triangleCount`, `vertexCount` |
51
+
52
+ #### Caveats
53
+
54
+ - **The caller owns `result.context` and must dispose it.** WebAssembly objects are not
55
+ garbage-collected; nothing is freed until you say so.
56
+ - **Never call `delete()` on `part.concrete` yourself.** It belongs to the context, which
57
+ may still have it cached under its digest.
58
+ - If `render` throws and it created the context itself, that context is disposed before the
59
+ error propagates. A context you supplied is left alone, since you may still want it.
60
+ - Params are resolved before any geometry is built, so a missing required param fails
61
+ immediately rather than halfway through a model.
62
+ - `options.params` sets **top-level** params only. A param declared inside a `model` is a
63
+ constant — see [model params](../reference/model.md#model-params).
64
+
65
+ ---
66
+
67
+ ## Usage
68
+
69
+ ### Rendering once
70
+
71
+ ```js
72
+ import { render, toBinarySTL } from 'forma-dsl';
73
+ import { writeFile } from 'node:fs/promises';
74
+
75
+ const result = await render(source);
76
+
77
+ await writeFile('part.stl', toBinarySTL(result.parts[0].concrete));
78
+
79
+ result.context.dispose();
80
+ ```
81
+
82
+ A single render can afford to let `render` create the context and dispose it immediately
83
+ afterwards. Anything more than that should reuse one.
84
+
85
+ ### Rendering many variants
86
+
87
+ Reusing a context is the whole point of the digest cache: only the subtrees whose content
88
+ actually changed are re-evaluated.
89
+
90
+ ```js
91
+ import { render, toBinarySTL, EvaluationContext } from 'forma-dsl';
92
+ import { writeFile } from 'node:fs/promises';
93
+
94
+ const context = await EvaluationContext.create();
95
+
96
+ for (const height of [10, 20, 30]) {
97
+ const result = await render(source, { params: { height }, context });
98
+ await writeFile(`part-${height}.stl`, toBinarySTL(result.parts[0].concrete));
99
+
100
+ // Free what this tree can no longer reach, so a long run stays bounded.
101
+ context.collect(result.parts.map((part) => part.node));
102
+ }
103
+
104
+ context.dispose();
105
+ ```
106
+
107
+ The `collect` call is what keeps memory flat across a long run. Without it the context
108
+ accumulates every intermediate solid from every variant.
109
+
110
+ ### Driving a live editor
111
+
112
+ The same pattern, with one context for the life of the page:
113
+
114
+ ```js
115
+ const context = await EvaluationContext.create();
116
+ let current = null;
117
+
118
+ async function rerender(source, params) {
119
+ try {
120
+ const result = await render(source, { params, context });
121
+ draw(result.parts);
122
+ current = result;
123
+ context.collect(result.parts.map((part) => part.node));
124
+ return result.stats;
125
+ } catch (error) {
126
+ showProblem(error.message, error.loc); // FormaError carries a position
127
+ return null;
128
+ }
129
+ }
130
+ ```
131
+
132
+ Typing a digit into a dimension re-solves that wall and nothing else — `stats.cacheHits`
133
+ against `stats.evaluated` shows how much was skipped.
134
+
135
+ ### Choosing a model
136
+
137
+ ```js
138
+ await render(source); // the first model declared
139
+ await render(source, { model: 'assembly' }); // by name
140
+ ```
141
+
142
+ To list what a document offers:
143
+
144
+ ```js
145
+ const { program } = await render(source);
146
+ [...program.models.keys()]; // ['printable', 'assembly']
147
+ ```
148
+
149
+ Or without rendering: `Program.parse(source).models.keys()`.
150
+
151
+ ### Exporting each part separately
152
+
153
+ ```js
154
+ const result = await render(source);
155
+
156
+ for (const part of result.parts) {
157
+ await writeFile(`${part.name}.stl`, toBinarySTL(part.concrete, part.name));
158
+ }
159
+
160
+ result.context.dispose();
161
+ ```
162
+
163
+ ### Reading the statistics
164
+
165
+ ```js
166
+ const { stats } = await render(source, { context });
167
+
168
+ stats.nodes; // nodes in the tree — a measure of the document's size
169
+ stats.evaluated; // kernel calls actually made, across the context's life
170
+ stats.cacheHits; // how many were avoided
171
+ stats.milliseconds; // wall clock for this render
172
+ ```
173
+
174
+ `evaluated` and `cacheHits` are running totals on the **context**, not per render, so compare
175
+ them across calls rather than reading one in isolation.
176
+
177
+ ---
178
+
179
+ ## Troubleshooting
180
+
181
+ ### `EvaluationContext has been disposed`
182
+
183
+ Something held on to a result after disposing the context that owns it. `part.concrete` and
184
+ `part.mesh` are only valid while the context is alive — copy the mesh arrays out first if you
185
+ need them later.
186
+
187
+ ### Memory grows across renders
188
+
189
+ Add the `collect` call. A reused context caches every solid it has ever built, by design;
190
+ `collect(roots)` frees what the current tree can no longer reach.
191
+
192
+ ```js
193
+ context.collect(result.parts.map((part) => part.node));
194
+ ```
195
+
196
+ ### `part "body" is 2D — extrude or revolve it before rendering`
197
+
198
+ The model produced a cross-section. See [2D ↔ 3D](../reference/conversions.md).
199
+
200
+ ### `align needs a kernel context — render the model rather than building it`
201
+
202
+ Only happens when driving [`Evaluator`](low-level.md#evaluator) directly without a context.
203
+ `render` always supplies one.
204
+
205
+ ### The error has no line number
206
+
207
+ Most do — `FormaError` carries a `loc` with `line`, `column` and `offset`, and appends it to
208
+ the message. A few errors are raised before any position is known (`the document declares no
209
+ model`) or inside a value helper that has none (`no attribute "a"`).
210
+
211
+ ```js
212
+ try {
213
+ await render(source);
214
+ } catch (error) {
215
+ error.loc?.line; // undefined when the error has no position
216
+ }
217
+ ```