forma-dsl 0.2.2 → 0.3.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forma-dsl",
3
- "version": "0.2.2",
3
+ "version": "0.3.2",
4
4
  "description": "A declarative DSL for 3D modeling and scene composition, reusable components, and live previews.",
5
5
  "keywords": [
6
6
  "cad",
@@ -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
 
@@ -1,9 +1,11 @@
1
- // A 128-bit content digest, computed once per node and never recomputed.
2
- //
3
- // It must be stable across runs, machines and browsers: two structurally identical trees
4
- // have to produce the same digest every time, or the evaluation cache silently misses and
5
- // a rebuilt model recomputes everything. That rules out anything seeded per process, and
6
- // it rules out Web Crypto, which is async and cannot be called from a constructor.
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
 
@@ -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';
@@ -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
- * Hand-written rather than generated: the runtime is plain JavaScript, and the shapes worth
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, and a context to reuse.
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. A context created here is
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
+ }
@@ -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';
@@ -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, and whether
184
- * params must all resolve.
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, or nesting past {@link MAX_DEPTH}.
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 {Array<{ kind: 'text', value: string } | { kind: 'expr', source: string, loc: SourceLocation }>} */
221
+ /** @type {Part[]} */
216
222
  const parts = [];
217
223
  let text = '';
218
224
 
@@ -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
 
@@ -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
- */