@vertekum/schema-builder 0.1.7 → 0.2.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/CHANGELOG.md +29 -0
- package/README.md +6 -2
- package/docs/build.md +31 -6
- package/docs/emission.md +110 -48
- package/docs/expressions.md +12 -2
- package/docs/format.md +75 -0
- package/docs/language.md +41 -26
- package/docs/lint.md +23 -2
- package/docs/modules.md +8 -5
- package/docs/scales.md +29 -2
- package/grammar/dfn.tmLanguage.json +33 -4
- package/package.json +4 -4
- package/src/api.ts +16 -3
- package/src/ast.ts +23 -9
- package/src/build.ts +177 -25
- package/src/cli.ts +360 -32
- package/src/emit.ts +189 -65
- package/src/format.ts +273 -0
- package/src/index.ts +27 -1
- package/src/lint.ts +21 -4
- package/src/parser.ts +98 -13
- package/src/resolve.ts +12 -5
- package/src/tokenizer.ts +56 -4
package/docs/scales.md
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
# Scale expressions
|
|
2
2
|
|
|
3
3
|
A scale expression enumerates a numeric name set at build time — `100-900/100` instead
|
|
4
|
-
of nine names by hand. Two modes:
|
|
4
|
+
of nine names by hand. Two modes, plus word affixes around either:
|
|
5
5
|
|
|
6
6
|
| Syntax | Mode |
|
|
7
7
|
| ---------------------- | --------------------------------------------------------------------- |
|
|
8
8
|
| [`min-max/step`](#additive-scales) | **additive** — arithmetic steps |
|
|
9
9
|
| [`min-max*factor~quantum`](#geometric-scales) | **geometric** — a ratio, optionally quantized |
|
|
10
|
+
| [`(formula)xs`, `xs(formula)`, `x(formula)s`](#affixed-scales) | **affixed** — word fragments wrapping the names |
|
|
10
11
|
|
|
11
12
|
Both are inclusive at both ends, and both enumerate **names** — every step must land on
|
|
12
13
|
a whole number, because a token name cannot be `31.25`.
|
|
13
14
|
|
|
14
15
|
## Additive scales
|
|
15
16
|
|
|
16
|
-
`min-max/step`: from `min`, adding `step`, up to and including `max`.
|
|
17
|
+
`min-max/step`: from `min`, adding `step`, up to and including `max`. The step is
|
|
18
|
+
optional — `2-4` means `2-4/1`.
|
|
17
19
|
|
|
18
20
|
```dfn
|
|
19
21
|
scale = 100-900/100 # 100 200 300 400 500 600 700 800 900
|
|
22
|
+
steps = 2-4 # 2 3 4
|
|
20
23
|
```
|
|
21
24
|
|
|
22
25
|
Different regions of one scale are alternation:
|
|
@@ -59,6 +62,30 @@ type-scale = 16-64*1.25~4
|
|
|
59
62
|
|
|
60
63
|
(The next raw step, 76.29…, exceeds 64 — the series ends.)
|
|
61
64
|
|
|
65
|
+
## Affixed scales
|
|
66
|
+
|
|
67
|
+
A parenthesized formula may carry word fragments on either or both ends, as **one
|
|
68
|
+
term** — no whitespace between affix and parenthesis. The full formula grammar applies
|
|
69
|
+
inside the parentheses:
|
|
70
|
+
|
|
71
|
+
| Expression | Names |
|
|
72
|
+
| ---------------- | ------------------------------- |
|
|
73
|
+
| `(2-4)xs` | `2xs 3xs 4xs` |
|
|
74
|
+
| `xs(2-4)` | `xs2 xs3 xs4` |
|
|
75
|
+
| `x(2-8/2)s` | `x2s x4s x6s x8s` |
|
|
76
|
+
| `(02-08/2)xxl` | `02xxl 04xxl 06xxl 08xxl` |
|
|
77
|
+
| `(16-64*1.25~4)` | `16 20 24 32 40 48 60` — bare parentheses are grouping only |
|
|
78
|
+
|
|
79
|
+
An affix is a word fragment: letters first, then letters, digits, and hyphens. The
|
|
80
|
+
affixes wrap the **name**; the numeric series underneath is unchanged — padding sits
|
|
81
|
+
inside the affixes (`02xxl`, never `xxl02` from a suffix form), and every rule below
|
|
82
|
+
(inclusive bounds, integrality, collisions) binds on the numbers exactly as if the
|
|
83
|
+
affixes were not there. One formula per term — `(2-4)x(1-2)` is refused.
|
|
84
|
+
|
|
85
|
+
```dfn
|
|
86
|
+
t-shirt = (2-4)xs | xsmall | small | medium | large | x(2-8/2)l
|
|
87
|
+
```
|
|
88
|
+
|
|
62
89
|
## The semantics, precisely
|
|
63
90
|
|
|
64
91
|
These rules are fixed; the same evaluator (in `@vertekum/core`) generates the names
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"patterns": []
|
|
35
35
|
},
|
|
36
36
|
"pragma": {
|
|
37
|
-
"match": "^(id|title|description|scope)\\s+(?=\")",
|
|
37
|
+
"match": "^(id|title|description|scope|sealed)\\s+(?=\")",
|
|
38
38
|
"captures": {
|
|
39
39
|
"1": {
|
|
40
40
|
"name": "keyword.other.pragma.dfn"
|
|
@@ -61,12 +61,15 @@
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
"production": {
|
|
64
|
-
"match": "^(\\d+[A-Za-z][A-Za-z0-9-]*|[A-Za-z][A-Za-z0-9-]*)\\s*(=)",
|
|
64
|
+
"match": "^(:)?(\\d+[A-Za-z][A-Za-z0-9-]*|[A-Za-z][A-Za-z0-9-]*)\\s*(=)",
|
|
65
65
|
"captures": {
|
|
66
66
|
"1": {
|
|
67
|
-
"name": "
|
|
67
|
+
"name": "storage.modifier.private.dfn"
|
|
68
68
|
},
|
|
69
69
|
"2": {
|
|
70
|
+
"name": "entity.name.function.production.dfn"
|
|
71
|
+
},
|
|
72
|
+
"3": {
|
|
70
73
|
"name": "keyword.operator.assignment.dfn"
|
|
71
74
|
}
|
|
72
75
|
}
|
|
@@ -79,6 +82,9 @@
|
|
|
79
82
|
{
|
|
80
83
|
"include": "#reference"
|
|
81
84
|
},
|
|
85
|
+
{
|
|
86
|
+
"include": "#affixed-range"
|
|
87
|
+
},
|
|
82
88
|
{
|
|
83
89
|
"include": "#range"
|
|
84
90
|
},
|
|
@@ -195,6 +201,9 @@
|
|
|
195
201
|
}
|
|
196
202
|
},
|
|
197
203
|
"patterns": [
|
|
204
|
+
{
|
|
205
|
+
"include": "#reference"
|
|
206
|
+
},
|
|
198
207
|
{
|
|
199
208
|
"name": "variable.parameter.member.dfn",
|
|
200
209
|
"match": "\\d+[A-Za-z][A-Za-z0-9-]*|[A-Za-z][A-Za-z0-9-]*|\\d+"
|
|
@@ -209,7 +218,7 @@
|
|
|
209
218
|
},
|
|
210
219
|
"range": {
|
|
211
220
|
"name": "constant.numeric.range.dfn",
|
|
212
|
-
"match": "\\d+-\\d+[*/]\\d+(?:\\.\\d+)?(?:~\\d+)?"
|
|
221
|
+
"match": "\\d+-\\d+(?:[*/]\\d+(?:\\.\\d+)?)?(?:~\\d+)?"
|
|
213
222
|
},
|
|
214
223
|
"number": {
|
|
215
224
|
"name": "constant.numeric.dfn",
|
|
@@ -246,6 +255,26 @@
|
|
|
246
255
|
"name": {
|
|
247
256
|
"name": "constant.language.name.dfn",
|
|
248
257
|
"match": "\\d+[A-Za-z][A-Za-z0-9-]*|[A-Za-z][A-Za-z0-9-]*"
|
|
258
|
+
},
|
|
259
|
+
"affixed-range": {
|
|
260
|
+
"match": "([A-Za-z][A-Za-z0-9-]*)?(\\()(\\d+-\\d+(?:[*/]\\d+(?:\\.\\d+)?)?(?:~\\d+)?)(\\))([A-Za-z][A-Za-z0-9-]*)?",
|
|
261
|
+
"captures": {
|
|
262
|
+
"1": {
|
|
263
|
+
"name": "constant.language.name.dfn"
|
|
264
|
+
},
|
|
265
|
+
"2": {
|
|
266
|
+
"name": "punctuation.definition.scale.begin.dfn"
|
|
267
|
+
},
|
|
268
|
+
"3": {
|
|
269
|
+
"name": "constant.numeric.range.dfn"
|
|
270
|
+
},
|
|
271
|
+
"4": {
|
|
272
|
+
"name": "punctuation.definition.scale.end.dfn"
|
|
273
|
+
},
|
|
274
|
+
"5": {
|
|
275
|
+
"name": "constant.language.name.dfn"
|
|
276
|
+
}
|
|
277
|
+
}
|
|
249
278
|
}
|
|
250
279
|
}
|
|
251
280
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertekum/schema-builder",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Declare token vocabularies as grammar; build them into JSON Schema files",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"./cli": "./src/cli.ts"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@vertekum/core": "0.1
|
|
40
|
+
"@vertekum/core": "0.2.1"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"zod": "^3.25.76"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@vertekum/
|
|
47
|
-
"@vertekum/
|
|
46
|
+
"@vertekum/core": "0.2.1",
|
|
47
|
+
"@vertekum/schema-vocabulary": "0.1.0"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/api.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ActivateContext } from '@vertekum/core';
|
|
2
|
-
import { schemaBuildCommand, schemaLintCommand } from './cli';
|
|
2
|
+
import { schemaBuildCommand, schemaFmtCommand, schemaLintCommand } from './cli';
|
|
3
3
|
import type { schemaBuilderManifest } from './index';
|
|
4
4
|
|
|
5
5
|
export {
|
|
@@ -8,16 +8,29 @@ export {
|
|
|
8
8
|
evaluateProduction,
|
|
9
9
|
type TreeNode,
|
|
10
10
|
} from './build';
|
|
11
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
buildModule,
|
|
13
|
+
schemaBuildCommand,
|
|
14
|
+
schemaFmtCommand,
|
|
15
|
+
schemaLintCommand,
|
|
16
|
+
} from './cli';
|
|
12
17
|
export { emit, isStamped, STAMP_PREFIX, stamp } from './emit';
|
|
18
|
+
export {
|
|
19
|
+
type DfnFix,
|
|
20
|
+
type FormatOptions,
|
|
21
|
+
fixSource,
|
|
22
|
+
formatSource,
|
|
23
|
+
resolveIndent,
|
|
24
|
+
} from './format';
|
|
13
25
|
export { type DfnDiagnostic, lintModule } from './lint';
|
|
14
26
|
export { parse } from './parser';
|
|
15
27
|
export { type ResolvedModule, resolveModule } from './resolve';
|
|
16
28
|
|
|
17
|
-
/** Headless activation: contributes the `schema build` and `schema
|
|
29
|
+
/** Headless activation: contributes the `schema build`, `schema lint`, and `schema fmt` commands. */
|
|
18
30
|
export function activate(
|
|
19
31
|
ctx: ActivateContext<typeof schemaBuilderManifest>,
|
|
20
32
|
): void {
|
|
21
33
|
ctx.commands.register(schemaBuildCommand);
|
|
22
34
|
ctx.commands.register(schemaLintCommand);
|
|
35
|
+
ctx.commands.register(schemaFmtCommand);
|
|
23
36
|
}
|
package/src/ast.ts
CHANGED
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
export interface Module {
|
|
10
10
|
/** `use "<specifier>" [as <alias>]` statements, in order. */
|
|
11
11
|
uses: Array<{ spec: string; alias?: string }>;
|
|
12
|
-
/** Productions by name. `root` is never stored here. */
|
|
12
|
+
/** Productions by name, declaration order. `root` is never stored here. */
|
|
13
13
|
productions: Map<string, Node>;
|
|
14
|
+
/** Productions declared `:name = …` — invisible to importers, always inlined. */
|
|
15
|
+
private: Set<string>;
|
|
14
16
|
/** The reserved `root` production, when declared. */
|
|
15
17
|
root: Node | undefined;
|
|
16
|
-
/** Pragmas
|
|
18
|
+
/** Pragmas — document metadata and the file's nature. */
|
|
17
19
|
meta: ModuleMeta;
|
|
20
|
+
/** Lint-surfaced notes recorded while parsing (deprecated forms), with positions. */
|
|
21
|
+
notes: Array<{ line: number; column: number; message: string }>;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/** The pragma statements a module may carry, each at most once. */
|
|
@@ -23,11 +27,17 @@ export interface ModuleMeta {
|
|
|
23
27
|
title?: string;
|
|
24
28
|
description?: string;
|
|
25
29
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
30
|
+
* The file's NATURE. `document`: emitted, root required — consumers reference its
|
|
31
|
+
* top-level names. `def`: emitted, root optional — a pattern source; its root also
|
|
32
|
+
* lands as `$defs.<filename>`. `inline`: never emitted — expansion only. Absent:
|
|
33
|
+
* root → document, rootless → def.
|
|
29
34
|
*/
|
|
30
|
-
scope?: 'document' | '
|
|
35
|
+
scope?: 'document' | 'def' | 'inline';
|
|
36
|
+
/**
|
|
37
|
+
* `sealed "false"` leaves the document top unsealed so sibling vocabularies can bind
|
|
38
|
+
* over the same files. Default true. (`scope "branch"` parses as a deprecated alias.)
|
|
39
|
+
*/
|
|
40
|
+
sealed?: boolean;
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
export type Node = Alt | Path | Name | Range | Ref | Group;
|
|
@@ -80,12 +90,16 @@ export interface Range extends Position {
|
|
|
80
90
|
step: number;
|
|
81
91
|
quantum?: number;
|
|
82
92
|
pad?: number;
|
|
93
|
+
/** Name affixes from the parenthesized form (`(2-4)xs`); values stay numeric. */
|
|
94
|
+
prefix?: string;
|
|
95
|
+
suffix?: string;
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
/**
|
|
86
99
|
* `<name>` / `<@name>`; `open` records a trailing `*` (the set admits additions). Set
|
|
87
100
|
* modifiers: `[a, b]` is PICK (only the listed members), `![a, b]` is OMIT (the set minus
|
|
88
|
-
* them) — a modified reference is a new set.
|
|
101
|
+
* them) — a modified reference is a new set. A member may itself be a REFERENCE, denoting
|
|
102
|
+
* every name in the referenced set (set algebra with sets as operands).
|
|
89
103
|
*/
|
|
90
104
|
export interface Ref extends Position {
|
|
91
105
|
kind: 'ref';
|
|
@@ -94,8 +108,8 @@ export interface Ref extends Position {
|
|
|
94
108
|
/** `<@module/production>` — resolve `name` in this import alone (the collision resolver). */
|
|
95
109
|
from?: string;
|
|
96
110
|
open: boolean;
|
|
97
|
-
pick: string
|
|
98
|
-
omit: string
|
|
111
|
+
pick: Array<string | Ref>;
|
|
112
|
+
omit: Array<string | Ref>;
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
/** `[ … ]`; `open` records a trailing `*` before the closing bracket. */
|
package/src/build.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
1
2
|
import { evaluateScale } from '@vertekum/core';
|
|
2
3
|
import type { Node, Ref } from './ast';
|
|
3
4
|
import { DfnError } from './error';
|
|
@@ -11,14 +12,39 @@ import type { ResolvedModule } from './resolve';
|
|
|
11
12
|
export interface TreeNode {
|
|
12
13
|
children: Map<string, TreeNode>;
|
|
13
14
|
open: boolean;
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* PATTERNS whose full member sets landed at this position — unmodified terminal
|
|
17
|
+
* references to public productions (local or imported) or to an imported def-scope
|
|
18
|
+
* module's root. Emission deep-checks each against the position's children and, when
|
|
19
|
+
* intact, emits a `$ref` to the pattern's `$def` instead of expanding; a merge that
|
|
20
|
+
* grew a member drops the pattern back to expansion. Keyed collision-proof.
|
|
21
|
+
*/
|
|
22
|
+
patterns?: Map<string, PatternRef>;
|
|
23
|
+
/**
|
|
24
|
+
* This subtree is verbatim one top-level branch of an imported DOCUMENT module's own
|
|
25
|
+
* artifact. Emission MAY replace it with a `$ref` into `…#/properties/<top>` (linked
|
|
26
|
+
* mode); inline mode ignores the tag.
|
|
27
|
+
*/
|
|
28
|
+
link?: { module: ResolvedModule; top: string };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One pattern source: a production (local when `module` is absent) or a def-scope root. */
|
|
32
|
+
export interface PatternRef {
|
|
33
|
+
name: string;
|
|
34
|
+
module?: ResolvedModule;
|
|
35
|
+
/** The pattern is the module's ROOT def (`$defs.<filename>`), not a production. */
|
|
36
|
+
root?: boolean;
|
|
15
37
|
}
|
|
16
38
|
|
|
17
39
|
const leaf = (): TreeNode => ({ children: new Map(), open: false });
|
|
18
40
|
|
|
19
41
|
function merge(into: TreeNode, from: TreeNode): void {
|
|
20
42
|
into.open = into.open || from.open;
|
|
21
|
-
|
|
43
|
+
into.link = undefined;
|
|
44
|
+
if (from.patterns) {
|
|
45
|
+
into.patterns = into.patterns ?? new Map();
|
|
46
|
+
for (const [key, ref] of from.patterns) into.patterns.set(key, ref);
|
|
47
|
+
}
|
|
22
48
|
for (const [name, child] of from.children) {
|
|
23
49
|
const existing = into.children.get(name);
|
|
24
50
|
if (existing) merge(existing, child);
|
|
@@ -31,17 +57,23 @@ interface Scope {
|
|
|
31
57
|
module: ResolvedModule;
|
|
32
58
|
/** Local productions currently expanding, for reference-cycle detection. */
|
|
33
59
|
expanding: Set<string>;
|
|
60
|
+
/** Lint's warning sink (open-merge, …); absent in plain builds. */
|
|
61
|
+
warn?: (file: string, line: number, column: number, message: string) => void;
|
|
34
62
|
}
|
|
35
63
|
|
|
36
64
|
/** The `<@key/name>, …` listing a miss suggests — an error should name the fix, not just the gap. */
|
|
37
65
|
function productionsOf(key: string, imported: ResolvedModule): string {
|
|
38
66
|
return [...imported.module.productions.keys()]
|
|
67
|
+
.filter((p) => !imported.module.private.has(p))
|
|
39
68
|
.map((p) => `<@${key}/${p}>`)
|
|
40
69
|
.join(', ');
|
|
41
70
|
}
|
|
42
71
|
|
|
43
72
|
/** Find what `<name>` / `<@name>` means in this scope. Imported roots go by module name. */
|
|
44
|
-
function target(
|
|
73
|
+
function target(
|
|
74
|
+
ref: Ref,
|
|
75
|
+
scope: Scope,
|
|
76
|
+
): { node: Node; scope: Scope; importedRoot?: ResolvedModule } {
|
|
45
77
|
// Every error here names the module whose TEXT holds the ref — `scope.module` — so a
|
|
46
78
|
// failure inside an imported production's body is attributed to that file, not to
|
|
47
79
|
// whichever module the walk started from.
|
|
@@ -71,6 +103,14 @@ function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
|
|
|
71
103
|
);
|
|
72
104
|
}
|
|
73
105
|
const production = imported.module.productions.get(ref.name);
|
|
106
|
+
if (production && imported.module.private.has(ref.name)) {
|
|
107
|
+
throw new DfnError(
|
|
108
|
+
`'${ref.name}' is private to ${basename(imported.path)} — its public productions: ${productionsOf(ref.from, imported)}`,
|
|
109
|
+
ref.line,
|
|
110
|
+
ref.column,
|
|
111
|
+
file,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
74
114
|
if (!production) {
|
|
75
115
|
throw new DfnError(
|
|
76
116
|
`'${ref.from}' has no production '${ref.name}' — it declares: ${productionsOf(ref.from, imported)}`,
|
|
@@ -81,17 +121,31 @@ function target(ref: Ref, scope: Scope): { node: Node; scope: Scope } {
|
|
|
81
121
|
}
|
|
82
122
|
return {
|
|
83
123
|
node: production,
|
|
84
|
-
scope: { module: imported, expanding: new Set() },
|
|
124
|
+
scope: { module: imported, expanding: new Set(), warn: scope.warn },
|
|
85
125
|
};
|
|
86
126
|
}
|
|
87
127
|
|
|
88
|
-
const hits: Array<{
|
|
128
|
+
const hits: Array<{
|
|
129
|
+
node: Node;
|
|
130
|
+
scope: Scope;
|
|
131
|
+
importedRoot?: ResolvedModule;
|
|
132
|
+
}> = [];
|
|
89
133
|
for (const [key, imported] of scope.module.imports) {
|
|
90
|
-
const importedScope: Scope = {
|
|
134
|
+
const importedScope: Scope = {
|
|
135
|
+
module: imported,
|
|
136
|
+
expanding: new Set(),
|
|
137
|
+
warn: scope.warn,
|
|
138
|
+
};
|
|
91
139
|
const production = imported.module.productions.get(ref.name);
|
|
92
|
-
if (production
|
|
140
|
+
if (production && !imported.module.private.has(ref.name)) {
|
|
141
|
+
hits.push({ node: production, scope: importedScope });
|
|
142
|
+
}
|
|
93
143
|
if (key === ref.name && imported.module.root) {
|
|
94
|
-
hits.push({
|
|
144
|
+
hits.push({
|
|
145
|
+
node: imported.module.root,
|
|
146
|
+
scope: importedScope,
|
|
147
|
+
importedRoot: imported,
|
|
148
|
+
});
|
|
95
149
|
}
|
|
96
150
|
}
|
|
97
151
|
if (hits.length === 0) {
|
|
@@ -148,6 +202,8 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
148
202
|
max: node.max,
|
|
149
203
|
step: node.step,
|
|
150
204
|
pad: node.pad,
|
|
205
|
+
prefix: node.prefix,
|
|
206
|
+
suffix: node.suffix,
|
|
151
207
|
}
|
|
152
208
|
: {
|
|
153
209
|
kind: 'multiplied',
|
|
@@ -156,6 +212,8 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
156
212
|
factor: node.step,
|
|
157
213
|
quantum: node.quantum,
|
|
158
214
|
pad: node.pad,
|
|
215
|
+
prefix: node.prefix,
|
|
216
|
+
suffix: node.suffix,
|
|
159
217
|
},
|
|
160
218
|
);
|
|
161
219
|
} catch (error) {
|
|
@@ -182,9 +240,26 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
182
240
|
}
|
|
183
241
|
case 'alt': {
|
|
184
242
|
const forest = leaf();
|
|
185
|
-
|
|
186
|
-
|
|
243
|
+
const forests = node.options.map((option) => ({
|
|
244
|
+
option,
|
|
245
|
+
forest: evaluate(option, scope, tail),
|
|
246
|
+
}));
|
|
247
|
+
// Opening a subset of a position's set opens the WHOLE position — additions carry
|
|
248
|
+
// no mark of which family they claim. Legal, but never silent.
|
|
249
|
+
const open = forests.find((f) => f.forest.open);
|
|
250
|
+
const closed = forests.find(
|
|
251
|
+
(f) => !f.forest.open && f.forest.children.size > 0,
|
|
252
|
+
);
|
|
253
|
+
if (open && closed && scope.warn) {
|
|
254
|
+
const at = open.option as { line?: number; column?: number };
|
|
255
|
+
scope.warn(
|
|
256
|
+
scope.module.path,
|
|
257
|
+
at.line ?? 1,
|
|
258
|
+
at.column ?? 1,
|
|
259
|
+
'an open set merges with closed siblings here — the whole position becomes open (additions are permitted beside every member)',
|
|
260
|
+
);
|
|
187
261
|
}
|
|
262
|
+
for (const f of forests) merge(forest, f.forest);
|
|
188
263
|
return forest;
|
|
189
264
|
}
|
|
190
265
|
case 'path': {
|
|
@@ -214,7 +289,11 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
214
289
|
return forest;
|
|
215
290
|
}
|
|
216
291
|
case 'ref': {
|
|
217
|
-
const {
|
|
292
|
+
const {
|
|
293
|
+
node: production,
|
|
294
|
+
scope: refScope,
|
|
295
|
+
importedRoot,
|
|
296
|
+
} = target(node, scope);
|
|
218
297
|
if (!node.imported) {
|
|
219
298
|
if (scope.expanding.has(node.name)) {
|
|
220
299
|
throw new DfnError(
|
|
@@ -228,7 +307,21 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
228
307
|
}
|
|
229
308
|
const forest = evaluate(production, refScope, tail);
|
|
230
309
|
if (!node.imported) scope.expanding.delete(node.name);
|
|
231
|
-
|
|
310
|
+
// Set-valued operands: a member REFERENCE contributes every top-level name of the
|
|
311
|
+
// set it denotes (evaluated in this scope, privacy and all); strings pass through.
|
|
312
|
+
const expandOperands = (list: Ref['omit']): string[] => {
|
|
313
|
+
const out: string[] = [];
|
|
314
|
+
for (const member of list) {
|
|
315
|
+
if (typeof member === 'string') {
|
|
316
|
+
out.push(member);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const operand = evaluate(member, scope, leaf);
|
|
320
|
+
for (const name of operand.children.keys()) out.push(name);
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
};
|
|
324
|
+
for (const omitted of expandOperands(node.omit)) {
|
|
232
325
|
if (!forest.children.delete(omitted)) {
|
|
233
326
|
throw new DfnError(
|
|
234
327
|
`'<${node.name}>' has no member '${omitted}' to omit`,
|
|
@@ -239,7 +332,8 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
239
332
|
}
|
|
240
333
|
}
|
|
241
334
|
if (node.pick.length > 0) {
|
|
242
|
-
|
|
335
|
+
const picks = expandOperands(node.pick);
|
|
336
|
+
for (const picked of picks) {
|
|
243
337
|
if (!forest.children.has(picked)) {
|
|
244
338
|
throw new DfnError(
|
|
245
339
|
`'<${node.name}>' has no member '${picked}' to pick`,
|
|
@@ -249,20 +343,56 @@ function evaluate(node: Node, scope: Scope, tail: () => TreeNode): TreeNode {
|
|
|
249
343
|
);
|
|
250
344
|
}
|
|
251
345
|
}
|
|
252
|
-
const keep = new Set(
|
|
346
|
+
const keep = new Set(picks);
|
|
253
347
|
for (const name of [...forest.children.keys()]) {
|
|
254
348
|
if (!keep.has(name)) forest.children.delete(name);
|
|
255
349
|
}
|
|
256
350
|
}
|
|
257
351
|
if (node.open) forest.open = true;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
352
|
+
const unmodified =
|
|
353
|
+
node.pick.length === 0 && node.omit.length === 0 && !node.open;
|
|
354
|
+
// A modified set is a different set — it never shares a pattern's $def; a tail
|
|
355
|
+
// forces expansion (a $ref cannot be parameterized). Tags are inert unless
|
|
356
|
+
// emission chooses to reference them.
|
|
357
|
+
if (unmodified && isTerminal(tail())) {
|
|
358
|
+
if (importedRoot) {
|
|
359
|
+
const nature =
|
|
360
|
+
importedRoot.module.meta.scope ??
|
|
361
|
+
(importedRoot.module.root ? 'document' : 'def');
|
|
362
|
+
if (nature === 'document') {
|
|
363
|
+
// Verbatim top-level branches of the child DOCUMENT's artifact.
|
|
364
|
+
for (const [top, child] of forest.children) {
|
|
365
|
+
child.link = { module: importedRoot, top };
|
|
366
|
+
}
|
|
367
|
+
} else if (nature === 'def') {
|
|
368
|
+
forest.patterns = forest.patterns ?? new Map();
|
|
369
|
+
forest.patterns.set(`@${importedRoot.path}//root`, {
|
|
370
|
+
name: importedRoot.name,
|
|
371
|
+
module: importedRoot,
|
|
372
|
+
root: true,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
// inline nature: pure expansion, no tag.
|
|
376
|
+
} else if (!node.imported) {
|
|
377
|
+
if (!scope.module.module.private.has(node.name)) {
|
|
378
|
+
forest.patterns = forest.patterns ?? new Map();
|
|
379
|
+
forest.patterns.set(node.name, { name: node.name });
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
// An imported PRODUCTION (qualified or search-resolved): public by
|
|
383
|
+
// construction (target refuses privates). Pattern in the child's artifact
|
|
384
|
+
// unless the child is inline-natured.
|
|
385
|
+
const child = refScope.module;
|
|
386
|
+
const nature =
|
|
387
|
+
child.module.meta.scope ?? (child.module.root ? 'document' : 'def');
|
|
388
|
+
if (nature !== 'inline') {
|
|
389
|
+
forest.patterns = forest.patterns ?? new Map();
|
|
390
|
+
forest.patterns.set(`@${child.path}/${node.name}`, {
|
|
391
|
+
name: node.name,
|
|
392
|
+
module: child,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
266
396
|
}
|
|
267
397
|
return forest;
|
|
268
398
|
}
|
|
@@ -277,6 +407,9 @@ function isNameSet(node: Node): boolean {
|
|
|
277
407
|
return true;
|
|
278
408
|
case 'alt':
|
|
279
409
|
return node.options.every(isNameSet);
|
|
410
|
+
case 'group':
|
|
411
|
+
// Decorative brackets do not change a set's nature.
|
|
412
|
+
return isNameSet(node.node);
|
|
280
413
|
default:
|
|
281
414
|
return false;
|
|
282
415
|
}
|
|
@@ -287,7 +420,17 @@ function isTerminal(node: TreeNode): boolean {
|
|
|
287
420
|
}
|
|
288
421
|
|
|
289
422
|
/** Expand a module's root into its name tree. Fragments-only modules cannot build. */
|
|
290
|
-
export function build(
|
|
423
|
+
export function build(
|
|
424
|
+
resolved: ResolvedModule,
|
|
425
|
+
options: {
|
|
426
|
+
warn?: (
|
|
427
|
+
file: string,
|
|
428
|
+
line: number,
|
|
429
|
+
column: number,
|
|
430
|
+
message: string,
|
|
431
|
+
) => void;
|
|
432
|
+
} = {},
|
|
433
|
+
): TreeNode {
|
|
291
434
|
const root = resolved.module.root;
|
|
292
435
|
if (!root) {
|
|
293
436
|
throw new DfnError(
|
|
@@ -297,7 +440,11 @@ export function build(resolved: ResolvedModule): TreeNode {
|
|
|
297
440
|
resolved.path,
|
|
298
441
|
);
|
|
299
442
|
}
|
|
300
|
-
const scope: Scope = {
|
|
443
|
+
const scope: Scope = {
|
|
444
|
+
module: resolved,
|
|
445
|
+
expanding: new Set(),
|
|
446
|
+
warn: options.warn,
|
|
447
|
+
};
|
|
301
448
|
|
|
302
449
|
// An aggregate root unions imported roots; the same top-level name arriving from two different
|
|
303
450
|
// module refs is a collision, not a merge — each module owns its top. `[a | b]` and `a | b`
|
|
@@ -338,12 +485,17 @@ export function build(resolved: ResolvedModule): TreeNode {
|
|
|
338
485
|
export function evaluateProduction(
|
|
339
486
|
resolved: ResolvedModule,
|
|
340
487
|
name: string,
|
|
488
|
+
warn?: (file: string, line: number, column: number, message: string) => void,
|
|
341
489
|
): TreeNode {
|
|
342
490
|
const production = resolved.module.productions.get(name);
|
|
343
491
|
if (!production) {
|
|
344
492
|
throw new DfnError(`no production '${name}'`, 1, 1);
|
|
345
493
|
}
|
|
346
|
-
const scope: Scope = {
|
|
494
|
+
const scope: Scope = {
|
|
495
|
+
module: resolved,
|
|
496
|
+
expanding: new Set([name]),
|
|
497
|
+
warn,
|
|
498
|
+
};
|
|
347
499
|
return evaluate(production, scope, leaf);
|
|
348
500
|
}
|
|
349
501
|
|