forma-dsl 0.2.2 → 0.3.1
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/package.json +1 -1
- package/src/core/context.js +12 -0
- package/src/core/digest.js +8 -6
- package/src/core/kernel.js +9 -0
- package/src/core/node.js +13 -0
- package/src/export/mesh.js +9 -0
- package/src/export/stl.js +8 -0
- package/src/index.d.ts +25 -7
- package/src/index.js +20 -4
- package/src/lang/ast.d.ts +192 -0
- package/src/lang/builtins.js +10 -0
- package/src/lang/evaluate.js +54 -4
- package/src/lang/lexer.js +15 -9
- package/src/lang/parser.js +13 -0
- package/src/values/transform.js +9 -0
- package/src/lang/ast.js +0 -226
package/package.json
CHANGED
package/src/core/context.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where geometry is actually built: a node tree in, solved kernel objects out, memoized by
|
|
3
|
+
* digest.
|
|
4
|
+
*
|
|
5
|
+
* Two things earn this its own module. The cache is keyed on content rather than on object
|
|
6
|
+
* identity, so an edit that rebuilds the tree from scratch still hits every subtree it did
|
|
7
|
+
* not change — which is what makes re-solving on a keystroke affordable. And the kernel's
|
|
8
|
+
* objects are WASM allocations that no garbage collector will free, so something has to own
|
|
9
|
+
* them and let them go; that is what `dispose` and `collect` are for, and why the caller is
|
|
10
|
+
* told in `render`'s contract that the context is theirs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
1
13
|
/** @import { CollectOptions, ContextStats, Kernel, Shape, Solid } from '../index.js' */
|
|
2
14
|
/** @import { GeometryNode } from './node.js' */
|
|
3
15
|
|
package/src/core/digest.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* A 128-bit content digest, computed once per node and never recomputed.
|
|
3
|
+
*
|
|
4
|
+
* It must be stable across runs, machines and browsers: two structurally identical trees have
|
|
5
|
+
* to produce the same digest every time, or the evaluation cache silently misses and a
|
|
6
|
+
* rebuilt model recomputes everything. That rules out anything seeded per process, and it
|
|
7
|
+
* rules out Web Crypto, which is async and cannot be called from a constructor.
|
|
8
|
+
*/
|
|
7
9
|
|
|
8
10
|
const C1 = 0x239b961b, C2 = 0xab0e9789, C3 = 0x38b34ae5, C4 = 0xa1e38b93;
|
|
9
11
|
|
package/src/core/kernel.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading the Manifold WASM module, and the global knobs for curve resolution.
|
|
3
|
+
*
|
|
4
|
+
* One instance per page or process. Memoized here rather than left to each caller, because
|
|
5
|
+
* two models evaluating concurrently at startup must not each start an instantiation — and
|
|
6
|
+
* because the resolution controls below are global to the module, so there is nowhere else
|
|
7
|
+
* they could honestly live.
|
|
8
|
+
*/
|
|
9
|
+
|
|
1
10
|
/** @import { Kernel, QualityOptions } from '../index.js' */
|
|
2
11
|
|
|
3
12
|
import ManifoldModule from 'manifold-3d';
|
package/src/core/node.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The geometry tree: a description of geometry that has not been built yet.
|
|
3
|
+
*
|
|
4
|
+
* A node names an operation and its children, and carries a digest of the whole subtree
|
|
5
|
+
* beneath it. Nothing here touches the kernel — that separation is the point, and the
|
|
6
|
+
* evaluation context is the other half of it.
|
|
7
|
+
*
|
|
8
|
+
* The digest is what the evaluation cache is keyed on, so everything feeding it has to be
|
|
9
|
+
* canonical: attributes in sorted order, the operands of a commutative operation ordered
|
|
10
|
+
* too, and no dependence on locale or on the order a document happened to be written in. A
|
|
11
|
+
* digest that varies across machines is a cache that silently never hits.
|
|
12
|
+
*/
|
|
13
|
+
|
|
1
14
|
/** @import { BooleanOp, Dimensionality } from '../index.js' */
|
|
2
15
|
|
|
3
16
|
import { DigestWriter } from './digest.js';
|
package/src/export/mesh.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a solved kernel object into vertex arrays a renderer can upload.
|
|
3
|
+
*
|
|
4
|
+
* The kernel keeps an indexed mesh with vertices shared between faces, which is what makes it
|
|
5
|
+
* compact and what makes it wrong to draw directly: a shared vertex has one normal, so every
|
|
6
|
+
* flat face it touches gets smeared into its neighbours. Expanding the indices back out is
|
|
7
|
+
* the whole of what this file is for.
|
|
8
|
+
*/
|
|
9
|
+
|
|
1
10
|
/** @import { IndexedMesh, RenderMesh, Solid } from '../index.js' */
|
|
2
11
|
|
|
3
12
|
/**
|
package/src/export/stl.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary STL, for handing a solved part to a slicer.
|
|
3
|
+
*
|
|
4
|
+
* STL is the least a mesh format can carry — triangles and normals, no units, no colour and
|
|
5
|
+
* no notion of a part — and it is what every slicer reads. Anything richer belongs in its
|
|
6
|
+
* own exporter rather than in a dialect of this one.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/** @import { Solid } from '../index.js' */
|
|
2
10
|
|
|
3
11
|
import { toRenderMesh } from './mesh.js';
|
package/src/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type declarations for forma-dsl.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Handwritten rather than generated: the runtime is plain JavaScript, and the shapes worth
|
|
5
5
|
* publishing are the ones a caller actually touches. Internals that are exported only
|
|
6
6
|
* because a test or the editor reaches for them are typed loosely on purpose.
|
|
7
7
|
*/
|
|
@@ -90,8 +90,6 @@ export interface QualityOptions {
|
|
|
90
90
|
export declare function setQuality(options?: QualityOptions): Promise<void>;
|
|
91
91
|
export declare function resetQuality(): Promise<void>;
|
|
92
92
|
|
|
93
|
-
// --- values -----------------------------------------------------------------------------
|
|
94
|
-
|
|
95
93
|
export declare class Vector {
|
|
96
94
|
constructor(components: readonly number[]);
|
|
97
95
|
static of(value: Vector | number | readonly number[] | Record<string, number>, dimensions: number): Vector;
|
|
@@ -281,6 +279,15 @@ export interface EvaluatorOptions {
|
|
|
281
279
|
context?: EvaluationContext | null;
|
|
282
280
|
/** Off lets a document declaring a param with no default still be inspected. */
|
|
283
281
|
requireParams?: boolean;
|
|
282
|
+
/**
|
|
283
|
+
* How many blocks may be built before evaluation gives up. Omitted, there is no limit.
|
|
284
|
+
*
|
|
285
|
+
* Nesting limits and `range` bound one loop each; only this bounds their product. Pass one
|
|
286
|
+
* when building a document you did not write.
|
|
287
|
+
*/
|
|
288
|
+
maxNodes?: number | null;
|
|
289
|
+
/** Checked at every block, so a build can be abandoned before it finishes. */
|
|
290
|
+
signal?: AbortSignal | null;
|
|
284
291
|
}
|
|
285
292
|
|
|
286
293
|
export declare class Evaluator {
|
|
@@ -350,8 +357,6 @@ export interface FunctionDefinition {
|
|
|
350
357
|
export declare const FUNCTIONS: Record<string, FunctionDefinition>;
|
|
351
358
|
export declare const CONSTANTS: Record<string, number>;
|
|
352
359
|
|
|
353
|
-
// --- export ----------------------------------------------------------------------------------
|
|
354
|
-
|
|
355
360
|
export interface RenderMesh {
|
|
356
361
|
/** Three vertices per triangle, expanded so hard edges stay hard. */
|
|
357
362
|
positions: Float32Array;
|
|
@@ -364,14 +369,27 @@ export interface RenderMesh {
|
|
|
364
369
|
export declare function toRenderMesh(manifold: Solid): RenderMesh;
|
|
365
370
|
export declare function toBinarySTL(manifold: Solid, header?: string): Uint8Array;
|
|
366
371
|
|
|
367
|
-
// --- the front door ----------------------------------------------------------------------------
|
|
368
|
-
|
|
369
372
|
export interface RenderOptions {
|
|
370
373
|
/** Which model to render. Defaults to the first the document declares. */
|
|
371
374
|
model?: string | null;
|
|
372
375
|
params?: Record<string, ParameterValue>;
|
|
373
376
|
/** Reuse a context to keep its cache across renders. The caller still owns it. */
|
|
374
377
|
context?: EvaluationContext | null;
|
|
378
|
+
/**
|
|
379
|
+
* How many blocks may be built before evaluation gives up. Omitted, there is no limit.
|
|
380
|
+
*
|
|
381
|
+
* Nesting limits and `range` bound one loop each; only this bounds their product, which is
|
|
382
|
+
* what a document nesting two large loops exceeds. Pass one when rendering a document you
|
|
383
|
+
* did not write.
|
|
384
|
+
*/
|
|
385
|
+
maxNodes?: number | null;
|
|
386
|
+
/**
|
|
387
|
+
* Abandons the render when it fires, throwing the signal's reason.
|
|
388
|
+
*
|
|
389
|
+
* Checked at every block and between parts, so a superseded render stops before spending
|
|
390
|
+
* kernel time rather than after.
|
|
391
|
+
*/
|
|
392
|
+
signal?: AbortSignal | null;
|
|
375
393
|
}
|
|
376
394
|
|
|
377
395
|
export interface RenderedPart extends ScenePart {
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package's public surface: what a consumer may import, and the two calls that do the
|
|
3
|
+
* work.
|
|
4
|
+
*
|
|
5
|
+
* Everything under `src/` is reachable only through this file, and that is what the rest of
|
|
6
|
+
* the repository is held to: the editor, the landing page and the MCP server all import
|
|
7
|
+
* `forma-dsl` rather than its sources. An export one of them needs and this file does not
|
|
8
|
+
* name therefore fails here, rather than in someone's install.
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
/** @import { ParameterDescriptor, RenderOptions, RenderResult, RenderedPart, Solid } from './index.js' */
|
|
2
12
|
|
|
3
13
|
export { Program, Evaluator } from './lang/evaluate.js';
|
|
@@ -27,19 +37,21 @@ import { toRenderMesh } from './export/mesh.js';
|
|
|
27
37
|
* how much a reused context holds.
|
|
28
38
|
*
|
|
29
39
|
* @param {string} source The document.
|
|
30
|
-
* @param {RenderOptions} [options] Which model, what parameters,
|
|
40
|
+
* @param {RenderOptions} [options] Which model, what parameters, a context to reuse, and the
|
|
41
|
+
* budget and signal that bound the build.
|
|
31
42
|
* @returns {Promise<RenderResult>} The parts, with both kernel objects and meshes.
|
|
32
|
-
* @throws {Error} On any syntax, evaluation or geometry failure
|
|
43
|
+
* @throws {Error} On any syntax, evaluation or geometry failure, on a document that exceeds
|
|
44
|
+
* `maxNodes`, or with the signal's reason if the caller aborted. A context created here is
|
|
33
45
|
* disposed before the error propagates; one the caller supplied is left alone, since the
|
|
34
46
|
* caller may still want it.
|
|
35
47
|
*/
|
|
36
48
|
export async function render(source, options = {}) {
|
|
37
|
-
const { model = null, params = {}, context = null } = options;
|
|
49
|
+
const { model = null, params = {}, context = null, maxNodes = null, signal = null } = options;
|
|
38
50
|
|
|
39
51
|
const started = Date.now();
|
|
40
52
|
const program = Program.parse(source);
|
|
41
53
|
const owned = context ?? await EvaluationContext.create();
|
|
42
|
-
const evaluator = new Evaluator(program, { params, context: owned });
|
|
54
|
+
const evaluator = new Evaluator(program, { params, context: owned, maxNodes, signal });
|
|
43
55
|
|
|
44
56
|
try {
|
|
45
57
|
const scene = await evaluator.model(model);
|
|
@@ -47,6 +59,10 @@ export async function render(source, options = {}) {
|
|
|
47
59
|
/** @type {RenderedPart[]} */
|
|
48
60
|
const parts = [];
|
|
49
61
|
for (const part of scene.parts) {
|
|
62
|
+
// Between parts as well as inside the evaluator: solving is where the time actually
|
|
63
|
+
// goes, so a caller that gave up during the first part should not pay for the rest.
|
|
64
|
+
signal?.throwIfAborted();
|
|
65
|
+
|
|
50
66
|
// Checked before solving rather than after: a 2D part is going to be refused either
|
|
51
67
|
// way, and there is no reason to spend the kernel call first.
|
|
52
68
|
if (part.node.dim !== 3) {
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes the parser produces.
|
|
3
|
+
*
|
|
4
|
+
* It exists because the AST was previously implicit: the set of `kind` values, and what each
|
|
5
|
+
* one carries, could only be recovered by reading the parser and the evaluator side by side
|
|
6
|
+
* and hoping they agreed.
|
|
7
|
+
*
|
|
8
|
+
* Declarations rather than JSDoc typedefs, which is what this always was — there is no
|
|
9
|
+
* runtime content here, so there is nothing for a `.js` file to carry and nothing to build.
|
|
10
|
+
* Written as types, a discriminant is `kind: 'unary'` rather than a `@property` whose
|
|
11
|
+
* description the lint obliged someone to invent.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { SourceLocation } from '../index.js';
|
|
15
|
+
|
|
16
|
+
/** A number, string, boolean or null written directly in the source. */
|
|
17
|
+
export interface LiteralExpression {
|
|
18
|
+
kind: 'literal';
|
|
19
|
+
value: number | string | boolean | null;
|
|
20
|
+
loc?: SourceLocation;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A `"..."` string with at least one `${...}` splice in it. */
|
|
24
|
+
export interface TemplateExpression {
|
|
25
|
+
kind: 'template';
|
|
26
|
+
/** The literal runs and the spliced expressions, in source order. */
|
|
27
|
+
parts: Array<{ kind: 'text'; value: string } | { kind: 'expr'; expression: Expression }>;
|
|
28
|
+
loc: SourceLocation;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A `[a, b, c]` list. */
|
|
32
|
+
export interface ArrayExpression {
|
|
33
|
+
kind: 'array';
|
|
34
|
+
items: Expression[];
|
|
35
|
+
loc: SourceLocation;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A `{ key = value }` object. */
|
|
39
|
+
export interface ObjectExpression {
|
|
40
|
+
kind: 'object';
|
|
41
|
+
entries: Array<{ key: string; value: Expression }>;
|
|
42
|
+
loc: SourceLocation;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A bare name, resolved against the enclosing scopes. */
|
|
46
|
+
export interface IdentifierExpression {
|
|
47
|
+
kind: 'identifier';
|
|
48
|
+
name: string;
|
|
49
|
+
loc: SourceLocation;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A `target.property` access. Also covers `.x`/`.y`/`.z` on a list. */
|
|
53
|
+
export interface MemberExpression {
|
|
54
|
+
kind: 'member';
|
|
55
|
+
object: Expression;
|
|
56
|
+
property: string;
|
|
57
|
+
loc?: SourceLocation;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A `target[index]` access. */
|
|
61
|
+
export interface IndexExpression {
|
|
62
|
+
kind: 'index';
|
|
63
|
+
object: Expression;
|
|
64
|
+
index: Expression;
|
|
65
|
+
loc?: SourceLocation;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A prefix `-` or `!`. */
|
|
69
|
+
export interface UnaryExpression {
|
|
70
|
+
kind: 'unary';
|
|
71
|
+
op: '-' | '!';
|
|
72
|
+
operand: Expression;
|
|
73
|
+
loc: SourceLocation;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** An infix operator. `&&` and `||` short-circuit; the rest evaluate both sides. */
|
|
77
|
+
export interface BinaryExpression {
|
|
78
|
+
kind: 'binary';
|
|
79
|
+
op: string;
|
|
80
|
+
left: Expression;
|
|
81
|
+
right: Expression;
|
|
82
|
+
loc: SourceLocation;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A `condition ? consequent : alternate`. */
|
|
86
|
+
export interface ConditionalExpression {
|
|
87
|
+
kind: 'conditional';
|
|
88
|
+
condition: Expression;
|
|
89
|
+
consequent: Expression;
|
|
90
|
+
alternate: Expression;
|
|
91
|
+
loc?: SourceLocation;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A call to one of the builtin functions. There are no user-defined functions. */
|
|
95
|
+
export interface CallExpression {
|
|
96
|
+
kind: 'call';
|
|
97
|
+
/** Must resolve to an identifier naming a builtin. */
|
|
98
|
+
callee: Expression;
|
|
99
|
+
args: Array<{ name: string | null; value: Expression }>;
|
|
100
|
+
loc: SourceLocation;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Anything that evaluates to a value. */
|
|
104
|
+
export type Expression =
|
|
105
|
+
| LiteralExpression | TemplateExpression | ArrayExpression | ObjectExpression
|
|
106
|
+
| IdentifierExpression | MemberExpression | IndexExpression | UnaryExpression
|
|
107
|
+
| BinaryExpression | ConditionalExpression | CallExpression;
|
|
108
|
+
|
|
109
|
+
/** A `name = expression` inside a block body. */
|
|
110
|
+
export interface Attribute {
|
|
111
|
+
name: string;
|
|
112
|
+
value: Expression;
|
|
113
|
+
loc?: SourceLocation;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The contents of a `{ ... }`, sorted by what each entry is. */
|
|
117
|
+
export interface Body {
|
|
118
|
+
attributes: Attribute[];
|
|
119
|
+
blocks: Array<Block | ForBlock | IfBlock>;
|
|
120
|
+
params: ParamDeclaration[];
|
|
121
|
+
locals: LocalDeclaration[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A nested block: a shape, an operation, a part, or a call to a component. */
|
|
125
|
+
export interface Block {
|
|
126
|
+
kind: 'block';
|
|
127
|
+
/** Which block, such as `box` or `translate`. */
|
|
128
|
+
type: string;
|
|
129
|
+
/**
|
|
130
|
+
* The labels before the brace, as expressions, so a part built in a loop can name itself
|
|
131
|
+
* from the loop variable.
|
|
132
|
+
*/
|
|
133
|
+
labels: Expression[];
|
|
134
|
+
body: Body;
|
|
135
|
+
loc: SourceLocation;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A `for name in sequence { ... }`, optionally with an index variable. */
|
|
139
|
+
export interface ForBlock {
|
|
140
|
+
kind: 'for';
|
|
141
|
+
/** One name binds the value; two bind the index and the value. */
|
|
142
|
+
names: string[];
|
|
143
|
+
sequence: Expression;
|
|
144
|
+
body: Body;
|
|
145
|
+
loc: SourceLocation;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** An `if condition { ... } else { ... }`. */
|
|
149
|
+
export interface IfBlock {
|
|
150
|
+
kind: 'if';
|
|
151
|
+
condition: Expression;
|
|
152
|
+
consequent: Body;
|
|
153
|
+
/** The branch taken otherwise, if there is one. */
|
|
154
|
+
alternate: Body | null;
|
|
155
|
+
loc: SourceLocation;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A `param name { ... }` or `param name = expression`. */
|
|
159
|
+
export interface ParamDeclaration {
|
|
160
|
+
kind: 'param';
|
|
161
|
+
name: string;
|
|
162
|
+
/** Its metadata; only `default` affects geometry. */
|
|
163
|
+
attributes: Attribute[];
|
|
164
|
+
loc: SourceLocation;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A `local name = expression`. */
|
|
168
|
+
export interface LocalDeclaration {
|
|
169
|
+
kind: 'local';
|
|
170
|
+
name: string;
|
|
171
|
+
/** Its value, computed once. */
|
|
172
|
+
value: Expression;
|
|
173
|
+
loc: SourceLocation;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A `component "name" { ... }` or `model "name" { ... }`. */
|
|
177
|
+
export interface NamedDeclaration {
|
|
178
|
+
kind: 'component' | 'model';
|
|
179
|
+
/** The declared name, always static. */
|
|
180
|
+
name: string;
|
|
181
|
+
body: Body;
|
|
182
|
+
loc: SourceLocation;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Anything that can appear at the top level of a document. */
|
|
186
|
+
export type Declaration = ParamDeclaration | LocalDeclaration | NamedDeclaration;
|
|
187
|
+
|
|
188
|
+
/** A whole parsed document. */
|
|
189
|
+
export interface Document {
|
|
190
|
+
kind: 'document';
|
|
191
|
+
declarations: Declaration[];
|
|
192
|
+
}
|
package/src/lang/builtins.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registries: every block the language has, every function it can call, and the reader
|
|
3
|
+
* that turns a block's attributes into typed values.
|
|
4
|
+
*
|
|
5
|
+
* This is the one description of what the language contains. The editor's highlighting, the
|
|
6
|
+
* landing page's and the docs' code samples, and the MCP server's catalogue all read
|
|
7
|
+
* `BLOCKS` and `FUNCTIONS` from here rather than restating them — so a block added here is
|
|
8
|
+
* one they all learn about, and a block they claim without it existing fails their tests.
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
/** @import { AttributeReader, BlockDefinition, Dimensionality, FunctionDefinition, SourceLocation } from '../index.js' */
|
|
2
12
|
|
|
3
13
|
import { GeometryNode } from '../core/node.js';
|
package/src/lang/evaluate.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The evaluator: a parsed document in, a tree of geometry nodes out.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here calls the kernel. Evaluating a model produces {@link GeometryNode}s, which
|
|
5
|
+
* describe geometry without building any, and solving those is {@link EvaluationContext}'s
|
|
6
|
+
* job. Keeping the two apart is what makes a live preview affordable — a description is
|
|
7
|
+
* cheap to rebuild on every keystroke, and a solid is not.
|
|
8
|
+
*
|
|
9
|
+
* The three things in this file fail in three different ways, which is why they are three
|
|
10
|
+
* things: {@link Program} indexes what a document declares, {@link Scope} resolves names
|
|
11
|
+
* against it, and {@link Evaluator} turns blocks into nodes. Which one raised an error is
|
|
12
|
+
* most of what tells a reader whether they made a typo or a mistake about meaning.
|
|
13
|
+
*/
|
|
14
|
+
|
|
1
15
|
/** @import { EvaluationContext, EvaluatorOptions, ParameterDescriptor, ParameterValue, Scene, ScenePart } from '../index.js' */
|
|
2
16
|
/** @import { BinaryExpression, Block, Body, CallExpression, Document, Expression, ForBlock, IfBlock, MemberExpression, NamedDeclaration } from './ast.js' */
|
|
3
17
|
|
|
@@ -180,13 +194,13 @@ export class Evaluator {
|
|
|
180
194
|
* or unreadable input surfaces before any geometry is built.
|
|
181
195
|
*
|
|
182
196
|
* @param {Program} program The program to evaluate.
|
|
183
|
-
* @param {EvaluatorOptions} [options] Params, an optional kernel context,
|
|
184
|
-
*
|
|
197
|
+
* @param {EvaluatorOptions} [options] Params, an optional kernel context, whether params
|
|
198
|
+
* must all resolve, and the budget and signal that bound the build.
|
|
185
199
|
* @throws {FormaError} If a param has no default and no supplied value, unless
|
|
186
200
|
* `requireParams` is off.
|
|
187
201
|
*/
|
|
188
202
|
constructor(program, options = {}) {
|
|
189
|
-
const { params = {}, context = null, requireParams = true } = options;
|
|
203
|
+
const { params = {}, context = null, requireParams = true, maxNodes = null, signal = null } = options;
|
|
190
204
|
|
|
191
205
|
/** @type {Program} The program being evaluated. */
|
|
192
206
|
this.program = program;
|
|
@@ -199,6 +213,28 @@ export class Evaluator {
|
|
|
199
213
|
/** @type {number} How deep into component calls evaluation currently is. */
|
|
200
214
|
this.depth = 0;
|
|
201
215
|
|
|
216
|
+
/**
|
|
217
|
+
* How many blocks may be built before evaluation gives up, or null for no limit.
|
|
218
|
+
*
|
|
219
|
+
* {@link MAX_DEPTH} stops a component that calls itself, and `range` caps how many values
|
|
220
|
+
* a single loop produces — but neither bounds their product. Two loops of ten thousand
|
|
221
|
+
* nested in each other is a hundred million blocks, each one a kernel allocation, and no
|
|
222
|
+
* amount of waiting finishes it.
|
|
223
|
+
*
|
|
224
|
+
* Off by default, because the embedder that controls its own input does not need it and
|
|
225
|
+
* should not have a ceiling imposed. A host that builds documents it did not write — an
|
|
226
|
+
* MCP server, a shared editor — should pass one.
|
|
227
|
+
*
|
|
228
|
+
* @type {number | null}
|
|
229
|
+
*/
|
|
230
|
+
this.maxNodes = maxNodes;
|
|
231
|
+
|
|
232
|
+
/** @type {AbortSignal | null} Checked at every block, so a caller can give up mid-build. */
|
|
233
|
+
this.signal = signal;
|
|
234
|
+
|
|
235
|
+
/** @type {number} How many blocks have been built so far, counted against the budget. */
|
|
236
|
+
this.built = 0;
|
|
237
|
+
|
|
202
238
|
const root = new Scope();
|
|
203
239
|
// Constants and type names are in scope before any param default is read, so a default
|
|
204
240
|
// can use `pi` and a `type = number` attribute resolves.
|
|
@@ -431,9 +467,23 @@ export class Evaluator {
|
|
|
431
467
|
* @param {Block | ForBlock | IfBlock} block The block.
|
|
432
468
|
* @param {Scope} scope The enclosing scope.
|
|
433
469
|
* @returns {Promise<Produced>} What it produced.
|
|
434
|
-
* @throws {FormaError} On unknown blocks, misplaced labels,
|
|
470
|
+
* @throws {FormaError} On unknown blocks, misplaced labels, nesting past {@link MAX_DEPTH},
|
|
471
|
+
* or a document that has built more blocks than the budget allows.
|
|
472
|
+
* @throws {Error} The signal's reason, if the caller has aborted.
|
|
435
473
|
*/
|
|
436
474
|
async block(block, scope) {
|
|
475
|
+
// Both checks go here rather than at the loop, because a block is the one thing every
|
|
476
|
+
// path builds: a `for`, a component call and a plain shape all arrive through it.
|
|
477
|
+
this.signal?.throwIfAborted();
|
|
478
|
+
|
|
479
|
+
if (this.maxNodes !== null && ++this.built > this.maxNodes) {
|
|
480
|
+
throw new FormaError(
|
|
481
|
+
`more than ${this.maxNodes} blocks built — the document asks for more geometry than `
|
|
482
|
+
+ 'this budget allows; narrow a loop, or raise maxNodes if it is genuinely this large',
|
|
483
|
+
block.loc,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
437
487
|
if (this.depth > MAX_DEPTH) {
|
|
438
488
|
throw new FormaError(`nesting deeper than ${MAX_DEPTH} blocks — is a component using itself?`, block.loc);
|
|
439
489
|
}
|
package/src/lang/lexer.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lexer: source text in, a flat stream of tokens out.
|
|
3
|
+
*
|
|
4
|
+
* Strings are the one thing it does not hand on whole. A `"..."` carrying `${...}` is split
|
|
5
|
+
* here into literal runs and spliced source, because working out where a splice ends means
|
|
6
|
+
* counting braces — which is a job for something reading characters, not for a parser reading
|
|
7
|
+
* tokens.
|
|
8
|
+
*
|
|
9
|
+
* {@link FormaError} lives here rather than next to the evaluator because this is the first
|
|
10
|
+
* pass that can fail, and every later one raises the same type.
|
|
11
|
+
*/
|
|
12
|
+
|
|
1
13
|
/** @import { SourceLocation, Token, TokenType } from '../index.js' */
|
|
2
14
|
|
|
3
15
|
/**
|
|
@@ -31,13 +43,11 @@ const PUNCTUATION = [
|
|
|
31
43
|
'+', '-', '*', '/', '%', '!', '?', ':',
|
|
32
44
|
];
|
|
33
45
|
|
|
46
|
+
/** @typedef {{ kind: 'text', value: string } | { kind: 'expr', source: string, loc: SourceLocation }} Part */
|
|
47
|
+
|
|
34
48
|
/** Escapes recognised inside a string. `\$` is here so a literal `${` can be written. */
|
|
35
49
|
const ESCAPES = { n: '\n', t: '\t', r: '\r', '"': '"', '\\': '\\', $: '$' };
|
|
36
50
|
|
|
37
|
-
/**
|
|
38
|
-
* @param {string} c A single character.
|
|
39
|
-
* @returns {boolean} Whether a name may begin with it.
|
|
40
|
-
*/
|
|
41
51
|
const isIdentStart = (c) => /[A-Za-z_]/.test(c);
|
|
42
52
|
|
|
43
53
|
/**
|
|
@@ -47,10 +57,6 @@ const isIdentStart = (c) => /[A-Za-z_]/.test(c);
|
|
|
47
57
|
*/
|
|
48
58
|
const isIdentPart = (c) => /[A-Za-z0-9_-]/.test(c);
|
|
49
59
|
|
|
50
|
-
/**
|
|
51
|
-
* @param {string} c A single character.
|
|
52
|
-
* @returns {boolean} Whether it is a decimal digit.
|
|
53
|
-
*/
|
|
54
60
|
const isDigit = (c) => c >= '0' && c <= '9';
|
|
55
61
|
|
|
56
62
|
/**
|
|
@@ -212,7 +218,7 @@ class Lexer {
|
|
|
212
218
|
const loc = this.here();
|
|
213
219
|
this.advance();
|
|
214
220
|
|
|
215
|
-
/** @type {
|
|
221
|
+
/** @type {Part[]} */
|
|
216
222
|
const parts = [];
|
|
217
223
|
let text = '';
|
|
218
224
|
|
package/src/lang/parser.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parser: tokens in, a syntax tree out.
|
|
3
|
+
*
|
|
4
|
+
* Recursive descent with a precedence table for the infix operators. The grammar is small
|
|
5
|
+
* enough that a generator would cost more than it saved, and writing it by hand is what lets
|
|
6
|
+
* every node carry the source position that produced it — which is the whole of how errors
|
|
7
|
+
* point at something later on.
|
|
8
|
+
*
|
|
9
|
+
* It checks shape and nothing else. Whether `cylinder` is a real block, and whether its
|
|
10
|
+
* attributes mean anything, is the evaluator's question; a document that parses is only
|
|
11
|
+
* a document spelled like forma.
|
|
12
|
+
*/
|
|
13
|
+
|
|
1
14
|
/** @import { SourceLocation, Token, TokenType } from '../index.js' */
|
|
2
15
|
/** @import { Attribute, Block, Body, Declaration, Document, Expression, ForBlock, IfBlock, LocalDeclaration, ParamDeclaration } from './ast.js' */
|
|
3
16
|
|
package/src/values/transform.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Affine transforms, in the layout the kernel expects.
|
|
3
|
+
*
|
|
4
|
+
* A value type: every operation returns a new transform and none mutates the receiver, so a
|
|
5
|
+
* transform captured by a scope cannot be changed underneath it by a sibling block. That
|
|
6
|
+
* matters more than it sounds — nesting is how the language composes, and a mutable
|
|
7
|
+
* transform would make a block's meaning depend on what ran before it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
1
10
|
/** @import { Dimensionality } from '../index.js' */
|
|
2
11
|
/** @import { VectorLike } from './vector.js' */
|
|
3
12
|
|
package/src/lang/ast.js
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/** @import { SourceLocation } from '../index.js' */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The shapes the parser produces, as types only.
|
|
5
|
-
*
|
|
6
|
-
* This module has no runtime content. It exists because the AST was previously implicit:
|
|
7
|
-
* the set of `kind` values, and what each one carries, could only be recovered by reading
|
|
8
|
-
* the parser and the evaluator side by side and hoping they agreed.
|
|
9
|
-
*
|
|
10
|
-
* @module
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export {};
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* A number, string, boolean or null written directly in the source.
|
|
17
|
-
*
|
|
18
|
-
* @typedef {object} LiteralExpression
|
|
19
|
-
* @property {'literal'} kind Discriminant.
|
|
20
|
-
* @property {number | string | boolean | null} value The value.
|
|
21
|
-
* @property {SourceLocation} [loc] Where it was written.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* A `"..."` string with at least one `${...}` splice in it.
|
|
26
|
-
*
|
|
27
|
-
* @typedef {object} TemplateExpression
|
|
28
|
-
* @property {'template'} kind Discriminant.
|
|
29
|
-
* @property {Array<{ kind: 'text', value: string } | { kind: 'expr', expression: Expression }>} parts
|
|
30
|
-
* The literal runs and the spliced expressions, in source order.
|
|
31
|
-
* @property {SourceLocation} loc Where the string began.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* A `[a, b, c]` list.
|
|
36
|
-
*
|
|
37
|
-
* @typedef {object} ArrayExpression
|
|
38
|
-
* @property {'array'} kind Discriminant.
|
|
39
|
-
* @property {Expression[]} items The elements.
|
|
40
|
-
* @property {SourceLocation} loc Where the bracket was.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* A `{ key = value }` object.
|
|
45
|
-
*
|
|
46
|
-
* @typedef {object} ObjectExpression
|
|
47
|
-
* @property {'object'} kind Discriminant.
|
|
48
|
-
* @property {Array<{ key: string, value: Expression }>} entries The entries, in source order.
|
|
49
|
-
* @property {SourceLocation} loc Where the brace was.
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* A bare name, resolved against the enclosing scopes.
|
|
54
|
-
*
|
|
55
|
-
* @typedef {object} IdentifierExpression
|
|
56
|
-
* @property {'identifier'} kind Discriminant.
|
|
57
|
-
* @property {string} name The name.
|
|
58
|
-
* @property {SourceLocation} loc Where the name was.
|
|
59
|
-
*/
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* A `target.property` access. Also covers `.x`/`.y`/`.z` on a list.
|
|
63
|
-
*
|
|
64
|
-
* @typedef {object} MemberExpression
|
|
65
|
-
* @property {'member'} kind Discriminant.
|
|
66
|
-
* @property {Expression} object What is being read from.
|
|
67
|
-
* @property {string} property The property name.
|
|
68
|
-
* @property {SourceLocation} [loc] Where the dot was.
|
|
69
|
-
*/
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* A `target[index]` access.
|
|
73
|
-
*
|
|
74
|
-
* @typedef {object} IndexExpression
|
|
75
|
-
* @property {'index'} kind Discriminant.
|
|
76
|
-
* @property {Expression} object What is being indexed.
|
|
77
|
-
* @property {Expression} index The index.
|
|
78
|
-
* @property {SourceLocation} [loc] Where the bracket was.
|
|
79
|
-
*/
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* A prefix `-` or `!`.
|
|
83
|
-
*
|
|
84
|
-
* @typedef {object} UnaryExpression
|
|
85
|
-
* @property {'unary'} kind Discriminant.
|
|
86
|
-
* @property {'-' | '!'} op The operator.
|
|
87
|
-
* @property {Expression} operand What it applies to.
|
|
88
|
-
* @property {SourceLocation} loc Where the operator was.
|
|
89
|
-
*/
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* An infix operator. `&&` and `||` short-circuit; the rest evaluate both sides.
|
|
93
|
-
*
|
|
94
|
-
* @typedef {object} BinaryExpression
|
|
95
|
-
* @property {'binary'} kind Discriminant.
|
|
96
|
-
* @property {string} op The operator.
|
|
97
|
-
* @property {Expression} left The left operand.
|
|
98
|
-
* @property {Expression} right The right operand.
|
|
99
|
-
* @property {SourceLocation} loc Where the operator was.
|
|
100
|
-
*/
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* A `condition ? consequent : alternate`.
|
|
104
|
-
*
|
|
105
|
-
* @typedef {object} ConditionalExpression
|
|
106
|
-
* @property {'conditional'} kind Discriminant.
|
|
107
|
-
* @property {Expression} condition What decides.
|
|
108
|
-
* @property {Expression} consequent The value when truthy.
|
|
109
|
-
* @property {Expression} alternate The value when not.
|
|
110
|
-
* @property {SourceLocation} [loc] Where it was written.
|
|
111
|
-
*/
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* A call to one of the builtin functions. There are no user-defined functions.
|
|
115
|
-
*
|
|
116
|
-
* @typedef {object} CallExpression
|
|
117
|
-
* @property {'call'} kind Discriminant.
|
|
118
|
-
* @property {Expression} callee Must resolve to an identifier naming a builtin.
|
|
119
|
-
* @property {Array<{ name: string | null, value: Expression }>} args The arguments.
|
|
120
|
-
* @property {SourceLocation} loc Where the parenthesis was.
|
|
121
|
-
*/
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Anything that evaluates to a value.
|
|
125
|
-
*
|
|
126
|
-
* @typedef {LiteralExpression | TemplateExpression | ArrayExpression | ObjectExpression
|
|
127
|
-
* | IdentifierExpression | MemberExpression | IndexExpression | UnaryExpression
|
|
128
|
-
* | BinaryExpression | ConditionalExpression | CallExpression} Expression
|
|
129
|
-
*/
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* A `name = expression` inside a block body.
|
|
133
|
-
*
|
|
134
|
-
* @typedef {object} Attribute
|
|
135
|
-
* @property {string} name The attribute name.
|
|
136
|
-
* @property {Expression} value The value.
|
|
137
|
-
* @property {SourceLocation} [loc] Where the name was.
|
|
138
|
-
*/
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* The contents of a `{ ... }`, sorted by what each entry is.
|
|
142
|
-
*
|
|
143
|
-
* @typedef {object} Body
|
|
144
|
-
* @property {Attribute[]} attributes The `name = value` entries.
|
|
145
|
-
* @property {Array<Block | ForBlock | IfBlock>} blocks The nested blocks, in source order.
|
|
146
|
-
* @property {ParamDeclaration[]} params Params declared in this body.
|
|
147
|
-
* @property {LocalDeclaration[]} locals Locals declared in this body.
|
|
148
|
-
*/
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* A nested block: a shape, an operation, a part, or a call to a component.
|
|
152
|
-
*
|
|
153
|
-
* @typedef {object} Block
|
|
154
|
-
* @property {'block'} kind Discriminant.
|
|
155
|
-
* @property {string} type Which block, such as `box` or `translate`.
|
|
156
|
-
* @property {Expression[]} labels The labels before the brace, as expressions, so a part
|
|
157
|
-
* built in a loop can name itself from the loop variable.
|
|
158
|
-
* @property {Body} body Its attributes and children.
|
|
159
|
-
* @property {SourceLocation} loc Where the block began.
|
|
160
|
-
*/
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* A `for name in sequence { ... }`, optionally with an index variable.
|
|
164
|
-
*
|
|
165
|
-
* @typedef {object} ForBlock
|
|
166
|
-
* @property {'for'} kind Discriminant.
|
|
167
|
-
* @property {string[]} names One name binds the value; two bind the index and the value.
|
|
168
|
-
* @property {Expression} sequence What to iterate.
|
|
169
|
-
* @property {Body} body What to emit per iteration.
|
|
170
|
-
* @property {SourceLocation} loc Where the `for` was.
|
|
171
|
-
*/
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* An `if condition { ... } else { ... }`.
|
|
175
|
-
*
|
|
176
|
-
* @typedef {object} IfBlock
|
|
177
|
-
* @property {'if'} kind Discriminant.
|
|
178
|
-
* @property {Expression} condition What decides.
|
|
179
|
-
* @property {Body} consequent The branch taken when truthy.
|
|
180
|
-
* @property {Body | null} alternate The branch taken otherwise, if there is one.
|
|
181
|
-
* @property {SourceLocation} loc Where the `if` was.
|
|
182
|
-
*/
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* A `param name { ... }` or `param name = expression`.
|
|
186
|
-
*
|
|
187
|
-
* @typedef {object} ParamDeclaration
|
|
188
|
-
* @property {'param'} kind Discriminant.
|
|
189
|
-
* @property {string} name The param name.
|
|
190
|
-
* @property {Attribute[]} attributes Its metadata; only `default` affects geometry.
|
|
191
|
-
* @property {SourceLocation} loc Where the declaration began.
|
|
192
|
-
*/
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* A `local name = expression`.
|
|
196
|
-
*
|
|
197
|
-
* @typedef {object} LocalDeclaration
|
|
198
|
-
* @property {'local'} kind Discriminant.
|
|
199
|
-
* @property {string} name The local's name.
|
|
200
|
-
* @property {Expression} value Its value, computed once.
|
|
201
|
-
* @property {SourceLocation} loc Where the declaration began.
|
|
202
|
-
*/
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* A `component "name" { ... }` or `model "name" { ... }`.
|
|
206
|
-
*
|
|
207
|
-
* @typedef {object} NamedDeclaration
|
|
208
|
-
* @property {'component' | 'model'} kind Which of the two.
|
|
209
|
-
* @property {string} name The declared name, always static.
|
|
210
|
-
* @property {Body} body Its contents.
|
|
211
|
-
* @property {SourceLocation} loc Where the declaration began.
|
|
212
|
-
*/
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Anything that can appear at the top level of a document.
|
|
216
|
-
*
|
|
217
|
-
* @typedef {ParamDeclaration | LocalDeclaration | NamedDeclaration} Declaration
|
|
218
|
-
*/
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* A whole parsed document.
|
|
222
|
-
*
|
|
223
|
-
* @typedef {object} Document
|
|
224
|
-
* @property {'document'} kind Discriminant.
|
|
225
|
-
* @property {Declaration[]} declarations The top-level blocks, in source order.
|
|
226
|
-
*/
|