@sprig-and-prose/sprig-universe 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -2
- package/src/ast.js +62 -8
- package/src/graph.js +454 -11
- package/src/ir.js +39 -2
- package/src/parser.js +714 -51
- package/src/scanner.js +1 -0
- package/test/aliases.test.js +91 -0
- package/test/fixtures/aliases-basic.prose +7 -0
- package/test/fixtures/aliases-conflict.prose +6 -0
- package/test/fixtures/aliases-no-leak.prose +5 -0
- package/test/fixtures/aliases-shadowing.prose +11 -0
- package/test/fixtures/aliases-single-line.prose +7 -0
- package/test/fixtures/relationship-errors.prose +20 -0
- package/test/fixtures/relationship-paired.prose +28 -0
- package/test/fixtures/relationship-scoped.prose +23 -0
- package/test/fixtures/relationship-symmetric.prose +18 -0
- package/test/fixtures/relationship-usage.prose +31 -0
package/src/parser.js
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
import { mergeSpans } from './util/span.js';
|
|
6
6
|
|
|
7
|
+
const DECLARATION_KINDS = new Set([
|
|
8
|
+
'universe',
|
|
9
|
+
'anthology',
|
|
10
|
+
'series',
|
|
11
|
+
'book',
|
|
12
|
+
'chapter',
|
|
13
|
+
'concept',
|
|
14
|
+
'relates',
|
|
15
|
+
'relationship',
|
|
16
|
+
'repository',
|
|
17
|
+
'reference',
|
|
18
|
+
]);
|
|
19
|
+
|
|
7
20
|
/**
|
|
8
21
|
* @typedef {import('./ast.js').FileAST} FileAST
|
|
9
22
|
* @typedef {import('./ast.js').UniverseDecl} UniverseDecl
|
|
@@ -13,9 +26,11 @@ import { mergeSpans } from './util/span.js';
|
|
|
13
26
|
* @typedef {import('./ast.js').ChapterDecl} ChapterDecl
|
|
14
27
|
* @typedef {import('./ast.js').ConceptDecl} ConceptDecl
|
|
15
28
|
* @typedef {import('./ast.js').RelatesDecl} RelatesDecl
|
|
29
|
+
* @typedef {import('./ast.js').RelationshipDecl} RelationshipDecl
|
|
16
30
|
* @typedef {import('./ast.js').DescribeBlock} DescribeBlock
|
|
17
31
|
* @typedef {import('./ast.js').UnknownBlock} UnknownBlock
|
|
18
32
|
* @typedef {import('./ast.js').ReferencesBlock} ReferencesBlock
|
|
33
|
+
* @typedef {import('./ast.js').OrderingBlock} OrderingBlock
|
|
19
34
|
* @typedef {import('./ast.js').RepositoryDecl} RepositoryDecl
|
|
20
35
|
* @typedef {import('./ast.js').ReferenceDecl} ReferenceDecl
|
|
21
36
|
* @typedef {import('./ast.js').DocumentationBlock} DocumentationBlock
|
|
@@ -47,6 +62,10 @@ class Parser {
|
|
|
47
62
|
this.file = file;
|
|
48
63
|
this.sourceText = sourceText;
|
|
49
64
|
this.pos = 0;
|
|
65
|
+
this.aliasScopes = [new Map()];
|
|
66
|
+
this.aliasDeclarations = [];
|
|
67
|
+
this.relationshipScopes = [new Map()];
|
|
68
|
+
this.relationshipDeclarations = [];
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
/**
|
|
@@ -99,6 +118,205 @@ class Parser {
|
|
|
99
118
|
return token;
|
|
100
119
|
}
|
|
101
120
|
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} [value]
|
|
123
|
+
* @returns {Token}
|
|
124
|
+
* @throws {Error}
|
|
125
|
+
*/
|
|
126
|
+
expectIdentifierOrKeyword(value) {
|
|
127
|
+
const token = this.advance();
|
|
128
|
+
if (!token || (token.type !== 'KEYWORD' && token.type !== 'IDENTIFIER')) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Expected identifier or keyword, got ${token ? token.type : 'EOF'} at ${this.file}:${token ? token.span.start.line : '?'}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (value !== undefined && token.value !== value) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Expected identifier or keyword "${value}", got "${token.value}" at ${this.file}:${token.span.start.line}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return token;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} value
|
|
143
|
+
* @returns {Token}
|
|
144
|
+
* @throws {Error}
|
|
145
|
+
*/
|
|
146
|
+
expectKindToken(value) {
|
|
147
|
+
return this.expectIdentifierOrKeyword(value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
pushAliasScope() {
|
|
151
|
+
const parent = this.aliasScopes[this.aliasScopes.length - 1] || new Map();
|
|
152
|
+
this.aliasScopes.push(new Map(parent));
|
|
153
|
+
this.aliasDeclarations.push(new Map());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @returns {Map<string, string>}
|
|
158
|
+
*/
|
|
159
|
+
popAliasScope() {
|
|
160
|
+
this.aliasScopes.pop();
|
|
161
|
+
return this.aliasDeclarations.pop() || new Map();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @returns {Map<string, string>}
|
|
166
|
+
*/
|
|
167
|
+
currentAliasMap() {
|
|
168
|
+
return this.aliasScopes[this.aliasScopes.length - 1] || new Map();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
pushRelationshipScope() {
|
|
172
|
+
const parent = this.relationshipScopes[this.relationshipScopes.length - 1] || new Map();
|
|
173
|
+
this.relationshipScopes.push(new Map(parent));
|
|
174
|
+
this.relationshipDeclarations.push(new Map());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @returns {Map<string, RelationshipDecl>}
|
|
179
|
+
*/
|
|
180
|
+
popRelationshipScope() {
|
|
181
|
+
this.relationshipScopes.pop();
|
|
182
|
+
return this.relationshipDeclarations.pop() || new Map();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @returns {Map<string, RelationshipDecl>}
|
|
187
|
+
*/
|
|
188
|
+
currentRelationshipMap() {
|
|
189
|
+
return this.relationshipScopes[this.relationshipScopes.length - 1] || new Map();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {RelationshipDecl} relationshipDecl
|
|
194
|
+
*/
|
|
195
|
+
declareRelationship(relationshipDecl) {
|
|
196
|
+
const declaredMap = this.relationshipDeclarations[this.relationshipDeclarations.length - 1];
|
|
197
|
+
if (!declaredMap) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Relationship must be declared inside a container at ${this.file}:${relationshipDecl.source.start.line}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Get identifiers to check for duplicates
|
|
204
|
+
const identifiers = [];
|
|
205
|
+
if (relationshipDecl.type === 'symmetric' && relationshipDecl.id) {
|
|
206
|
+
identifiers.push(relationshipDecl.id);
|
|
207
|
+
} else if (relationshipDecl.type === 'paired') {
|
|
208
|
+
if (relationshipDecl.leftId) identifiers.push(relationshipDecl.leftId);
|
|
209
|
+
if (relationshipDecl.rightId) identifiers.push(relationshipDecl.rightId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const id of identifiers) {
|
|
213
|
+
if (declaredMap.has(id)) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Duplicate relationship identifier "${id}" in this scope at ${this.file}:${relationshipDecl.source.start.line}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const relationshipMap = this.currentRelationshipMap();
|
|
219
|
+
relationshipMap.set(id, relationshipDecl);
|
|
220
|
+
declaredMap.set(id, relationshipDecl);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {Token} nameToken
|
|
226
|
+
* @param {Token} targetToken
|
|
227
|
+
*/
|
|
228
|
+
declareAlias(nameToken, targetToken) {
|
|
229
|
+
const aliasName = nameToken.value;
|
|
230
|
+
const targetKind = targetToken.value;
|
|
231
|
+
const declaredMap = this.aliasDeclarations[this.aliasDeclarations.length - 1];
|
|
232
|
+
if (!declaredMap) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Alias "${aliasName}" must be declared inside a container at ${this.file}:${nameToken.span.start.line}`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (DECLARATION_KINDS.has(aliasName)) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Alias "${aliasName}" conflicts with built-in kind "${aliasName}" at ${this.file}:${nameToken.span.start.line}`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (!DECLARATION_KINDS.has(targetKind)) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Unknown alias target kind "${targetKind}" at ${this.file}:${targetToken.span.start.line}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (declaredMap.has(aliasName)) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Duplicate alias "${aliasName}" in this scope at ${this.file}:${nameToken.span.start.line}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const aliasMap = this.currentAliasMap();
|
|
253
|
+
aliasMap.set(aliasName, targetKind);
|
|
254
|
+
declaredMap.set(aliasName, targetKind);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
parseAliasStatement() {
|
|
258
|
+
this.expectIdentifierOrKeyword('alias');
|
|
259
|
+
const nameToken = this.expectIdentifierOrKeyword();
|
|
260
|
+
this.expect('LBRACE');
|
|
261
|
+
const targetToken = this.expectIdentifierOrKeyword();
|
|
262
|
+
this.expect('RBRACE');
|
|
263
|
+
this.declareAlias(nameToken, targetToken);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
parseAliasesBlock() {
|
|
267
|
+
this.expectIdentifierOrKeyword('aliases');
|
|
268
|
+
this.expect('LBRACE');
|
|
269
|
+
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
270
|
+
if (!this.match('IDENTIFIER') && !this.match('KEYWORD')) {
|
|
271
|
+
const token = this.peek();
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Expected alias name in aliases block at ${this.file}:${token ? token.span.start.line : '?'}`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const nameToken = this.expectIdentifierOrKeyword();
|
|
277
|
+
this.expect('LBRACE');
|
|
278
|
+
const targetToken = this.expectIdentifierOrKeyword();
|
|
279
|
+
this.expect('RBRACE');
|
|
280
|
+
this.declareAlias(nameToken, targetToken);
|
|
281
|
+
}
|
|
282
|
+
this.expect('RBRACE');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @param {string} resolvedKind
|
|
287
|
+
* @param {string} spelledKind
|
|
288
|
+
*/
|
|
289
|
+
parseDeclarationByKind(resolvedKind, spelledKind) {
|
|
290
|
+
if (resolvedKind === 'anthology') {
|
|
291
|
+
return this.parseAnthology(spelledKind);
|
|
292
|
+
}
|
|
293
|
+
if (resolvedKind === 'series') {
|
|
294
|
+
return this.parseSeries(spelledKind);
|
|
295
|
+
}
|
|
296
|
+
if (resolvedKind === 'book') {
|
|
297
|
+
return this.parseBook(spelledKind);
|
|
298
|
+
}
|
|
299
|
+
if (resolvedKind === 'chapter') {
|
|
300
|
+
return this.parseChapter(spelledKind);
|
|
301
|
+
}
|
|
302
|
+
if (resolvedKind === 'concept') {
|
|
303
|
+
return this.parseConcept(spelledKind);
|
|
304
|
+
}
|
|
305
|
+
if (resolvedKind === 'relates') {
|
|
306
|
+
return this.parseRelates(spelledKind);
|
|
307
|
+
}
|
|
308
|
+
if (resolvedKind === 'repository') {
|
|
309
|
+
return this.parseRepository(spelledKind);
|
|
310
|
+
}
|
|
311
|
+
if (resolvedKind === 'reference') {
|
|
312
|
+
return this.parseReferenceDecl(spelledKind);
|
|
313
|
+
}
|
|
314
|
+
if (resolvedKind === 'relationship') {
|
|
315
|
+
return this.parseRelationship(spelledKind);
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
102
320
|
/**
|
|
103
321
|
* @returns {FileAST}
|
|
104
322
|
*/
|
|
@@ -173,16 +391,40 @@ class Parser {
|
|
|
173
391
|
/**
|
|
174
392
|
* @returns {UniverseDecl}
|
|
175
393
|
*/
|
|
176
|
-
parseUniverse() {
|
|
177
|
-
const startToken = this.
|
|
394
|
+
parseUniverse(spelledKind = 'universe') {
|
|
395
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
178
396
|
const nameToken = this.expect('IDENTIFIER');
|
|
179
397
|
const lbrace = this.expect('LBRACE');
|
|
180
|
-
|
|
398
|
+
this.pushAliasScope();
|
|
399
|
+
this.pushRelationshipScope();
|
|
400
|
+
const body = this.parseBlockBody(
|
|
401
|
+
[
|
|
402
|
+
'anthology',
|
|
403
|
+
'series',
|
|
404
|
+
'book',
|
|
405
|
+
'chapter',
|
|
406
|
+
'concept',
|
|
407
|
+
'relates',
|
|
408
|
+
'relationship',
|
|
409
|
+
'describe',
|
|
410
|
+
'title',
|
|
411
|
+
'repository',
|
|
412
|
+
'reference',
|
|
413
|
+
'references',
|
|
414
|
+
'ordering',
|
|
415
|
+
'documentation',
|
|
416
|
+
],
|
|
417
|
+
{ allowAliases: true },
|
|
418
|
+
);
|
|
181
419
|
const rbrace = this.expect('RBRACE');
|
|
420
|
+
const aliases = this.popAliasScope();
|
|
421
|
+
const relationships = this.popRelationshipScope();
|
|
182
422
|
|
|
183
423
|
return {
|
|
184
424
|
kind: 'universe',
|
|
425
|
+
spelledKind,
|
|
185
426
|
name: nameToken.value,
|
|
427
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
186
428
|
body,
|
|
187
429
|
source: {
|
|
188
430
|
file: this.file,
|
|
@@ -194,10 +436,12 @@ class Parser {
|
|
|
194
436
|
|
|
195
437
|
/**
|
|
196
438
|
* @param {string[]} allowedKeywords - Keywords allowed in this body
|
|
439
|
+
* @param {{ allowAliases?: boolean }} [options]
|
|
197
440
|
* @returns {Array<SeriesDecl | BookDecl | ChapterDecl | ConceptDecl | RelatesDecl | DescribeBlock | TitleBlock | FromBlock | RelationshipsBlock | ReferencesBlock | DocumentationBlock | RepositoryDecl | ReferenceDecl | NamedDocumentBlock | UnknownBlock>}
|
|
198
441
|
*/
|
|
199
|
-
parseBlockBody(allowedKeywords) {
|
|
442
|
+
parseBlockBody(allowedKeywords, options = {}) {
|
|
200
443
|
const body = [];
|
|
444
|
+
const allowAliases = options.allowAliases === true;
|
|
201
445
|
|
|
202
446
|
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
203
447
|
// Check for keywords or identifiers that might start a block
|
|
@@ -205,6 +449,15 @@ class Parser {
|
|
|
205
449
|
const keyword = this.peek()?.value;
|
|
206
450
|
if (!keyword) break;
|
|
207
451
|
|
|
452
|
+
if (allowAliases && keyword === 'alias') {
|
|
453
|
+
this.parseAliasStatement();
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (allowAliases && keyword === 'aliases') {
|
|
457
|
+
this.parseAliasesBlock();
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
208
461
|
// Check for named document: document <IDENTIFIER> { ... }
|
|
209
462
|
if (keyword === 'document') {
|
|
210
463
|
const nextPos = this.pos + 1;
|
|
@@ -220,19 +473,16 @@ class Parser {
|
|
|
220
473
|
// Fall through to unknown block handling
|
|
221
474
|
}
|
|
222
475
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
} else if (keyword === 'relates' && allowedKeywords.includes('relates')) {
|
|
234
|
-
body.push(this.parseRelates());
|
|
235
|
-
} else if (keyword === 'describe' && allowedKeywords.includes('describe')) {
|
|
476
|
+
const resolvedKind = this.currentAliasMap().get(keyword) || keyword;
|
|
477
|
+
if (allowedKeywords.includes(resolvedKind)) {
|
|
478
|
+
const decl = this.parseDeclarationByKind(resolvedKind, keyword);
|
|
479
|
+
if (decl) {
|
|
480
|
+
body.push(decl);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (keyword === 'describe' && allowedKeywords.includes('describe')) {
|
|
236
486
|
body.push(this.parseDescribe());
|
|
237
487
|
} else if (keyword === 'title' && allowedKeywords.includes('title')) {
|
|
238
488
|
body.push(this.parseTitle());
|
|
@@ -246,6 +496,8 @@ class Parser {
|
|
|
246
496
|
body.push(this.parseRelationships());
|
|
247
497
|
} else if (keyword === 'references' && allowedKeywords.includes('references')) {
|
|
248
498
|
body.push(this.parseReferences());
|
|
499
|
+
} else if (keyword === 'ordering' && allowedKeywords.includes('ordering')) {
|
|
500
|
+
body.push(this.parseOrdering());
|
|
249
501
|
} else if (keyword === 'documentation' && allowedKeywords.includes('documentation')) {
|
|
250
502
|
body.push(this.parseDocumentation());
|
|
251
503
|
} else {
|
|
@@ -271,8 +523,8 @@ class Parser {
|
|
|
271
523
|
/**
|
|
272
524
|
* @returns {AnthologyDecl}
|
|
273
525
|
*/
|
|
274
|
-
parseAnthology() {
|
|
275
|
-
const startToken = this.
|
|
526
|
+
parseAnthology(spelledKind = 'anthology') {
|
|
527
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
276
528
|
const nameToken = this.expect('IDENTIFIER');
|
|
277
529
|
let parentName = undefined;
|
|
278
530
|
if (this.match('KEYWORD') && this.peek()?.value === 'in') {
|
|
@@ -281,25 +533,37 @@ class Parser {
|
|
|
281
533
|
parentName = parentToken.value;
|
|
282
534
|
}
|
|
283
535
|
const lbrace = this.expect('LBRACE');
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
536
|
+
this.pushAliasScope();
|
|
537
|
+
this.pushRelationshipScope();
|
|
538
|
+
const body = this.parseBlockBody(
|
|
539
|
+
[
|
|
540
|
+
'describe',
|
|
541
|
+
'title',
|
|
542
|
+
'series',
|
|
543
|
+
'book',
|
|
544
|
+
'chapter',
|
|
545
|
+
'concept',
|
|
546
|
+
'relationship',
|
|
547
|
+
'relates',
|
|
548
|
+
'relationship',
|
|
549
|
+
'references',
|
|
550
|
+
'ordering',
|
|
551
|
+
'documentation',
|
|
552
|
+
'repository',
|
|
553
|
+
'reference',
|
|
554
|
+
],
|
|
555
|
+
{ allowAliases: true },
|
|
556
|
+
);
|
|
297
557
|
const rbrace = this.expect('RBRACE');
|
|
558
|
+
const aliases = this.popAliasScope();
|
|
559
|
+
const relationships = this.popRelationshipScope();
|
|
298
560
|
|
|
299
561
|
return {
|
|
300
562
|
kind: 'anthology',
|
|
563
|
+
spelledKind,
|
|
301
564
|
name: nameToken.value,
|
|
302
565
|
parentName,
|
|
566
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
303
567
|
body,
|
|
304
568
|
source: {
|
|
305
569
|
file: this.file,
|
|
@@ -312,8 +576,8 @@ class Parser {
|
|
|
312
576
|
/**
|
|
313
577
|
* @returns {SeriesDecl}
|
|
314
578
|
*/
|
|
315
|
-
parseSeries() {
|
|
316
|
-
const startToken = this.
|
|
579
|
+
parseSeries(spelledKind = 'series') {
|
|
580
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
317
581
|
const nameToken = this.expect('IDENTIFIER');
|
|
318
582
|
|
|
319
583
|
// Optional "in <AnthologyName>" syntax
|
|
@@ -325,13 +589,20 @@ class Parser {
|
|
|
325
589
|
}
|
|
326
590
|
|
|
327
591
|
const lbrace = this.expect('LBRACE');
|
|
328
|
-
|
|
592
|
+
this.pushAliasScope();
|
|
593
|
+
const body = this.parseBlockBody(
|
|
594
|
+
['book', 'chapter', 'concept', 'describe', 'title', 'references', 'ordering', 'documentation', 'repository', 'reference'],
|
|
595
|
+
{ allowAliases: true },
|
|
596
|
+
);
|
|
329
597
|
const rbrace = this.expect('RBRACE');
|
|
598
|
+
const aliases = this.popAliasScope();
|
|
330
599
|
|
|
331
600
|
return {
|
|
332
601
|
kind: 'series',
|
|
602
|
+
spelledKind,
|
|
333
603
|
name: nameToken.value,
|
|
334
604
|
parentName,
|
|
605
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
335
606
|
body,
|
|
336
607
|
source: {
|
|
337
608
|
file: this.file,
|
|
@@ -344,8 +615,8 @@ class Parser {
|
|
|
344
615
|
/**
|
|
345
616
|
* @returns {BookDecl}
|
|
346
617
|
*/
|
|
347
|
-
parseBook() {
|
|
348
|
-
const startToken = this.
|
|
618
|
+
parseBook(spelledKind = 'book') {
|
|
619
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
349
620
|
const nameToken = this.expect('IDENTIFIER');
|
|
350
621
|
|
|
351
622
|
// Optional "in <ParentName>" syntax
|
|
@@ -357,13 +628,20 @@ class Parser {
|
|
|
357
628
|
}
|
|
358
629
|
|
|
359
630
|
const lbrace = this.expect('LBRACE');
|
|
360
|
-
|
|
631
|
+
this.pushAliasScope();
|
|
632
|
+
const body = this.parseBlockBody(
|
|
633
|
+
['series', 'chapter', 'concept', 'describe', 'title', 'relationships', 'references', 'ordering', 'documentation', 'repository', 'reference'],
|
|
634
|
+
{ allowAliases: true },
|
|
635
|
+
);
|
|
361
636
|
const rbrace = this.expect('RBRACE');
|
|
637
|
+
const aliases = this.popAliasScope();
|
|
362
638
|
|
|
363
639
|
return {
|
|
364
640
|
kind: 'book',
|
|
641
|
+
spelledKind,
|
|
365
642
|
name: nameToken.value,
|
|
366
643
|
parentName,
|
|
644
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
367
645
|
body,
|
|
368
646
|
source: {
|
|
369
647
|
file: this.file,
|
|
@@ -376,8 +654,8 @@ class Parser {
|
|
|
376
654
|
/**
|
|
377
655
|
* @returns {ChapterDecl}
|
|
378
656
|
*/
|
|
379
|
-
parseChapter() {
|
|
380
|
-
const startToken = this.
|
|
657
|
+
parseChapter(spelledKind = 'chapter') {
|
|
658
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
381
659
|
const nameToken = this.expect('IDENTIFIER');
|
|
382
660
|
|
|
383
661
|
// Chapters must belong to a book - check for "in" keyword
|
|
@@ -392,13 +670,20 @@ class Parser {
|
|
|
392
670
|
this.expect('KEYWORD', 'in');
|
|
393
671
|
const parentToken = this.expect('IDENTIFIER');
|
|
394
672
|
const lbrace = this.expect('LBRACE');
|
|
395
|
-
|
|
673
|
+
this.pushAliasScope();
|
|
674
|
+
const body = this.parseBlockBody(
|
|
675
|
+
['concept', 'describe', 'title', 'references', 'relationships', 'documentation', 'repository', 'reference'],
|
|
676
|
+
{ allowAliases: true },
|
|
677
|
+
);
|
|
396
678
|
const rbrace = this.expect('RBRACE');
|
|
679
|
+
const aliases = this.popAliasScope();
|
|
397
680
|
|
|
398
681
|
return {
|
|
399
682
|
kind: 'chapter',
|
|
683
|
+
spelledKind,
|
|
400
684
|
name: nameToken.value,
|
|
401
685
|
parentName: parentToken.value,
|
|
686
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
402
687
|
body,
|
|
403
688
|
source: {
|
|
404
689
|
file: this.file,
|
|
@@ -411,8 +696,8 @@ class Parser {
|
|
|
411
696
|
/**
|
|
412
697
|
* @returns {ConceptDecl}
|
|
413
698
|
*/
|
|
414
|
-
parseConcept() {
|
|
415
|
-
const startToken = this.
|
|
699
|
+
parseConcept(spelledKind = 'concept') {
|
|
700
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
416
701
|
const nameToken = this.expect('IDENTIFIER');
|
|
417
702
|
|
|
418
703
|
// Optional "in <ParentName>" syntax
|
|
@@ -424,13 +709,20 @@ class Parser {
|
|
|
424
709
|
}
|
|
425
710
|
|
|
426
711
|
const lbrace = this.expect('LBRACE');
|
|
427
|
-
|
|
712
|
+
this.pushAliasScope();
|
|
713
|
+
const body = this.parseBlockBody(
|
|
714
|
+
['describe', 'title', 'references', 'relationships', 'ordering', 'documentation', 'repository', 'reference'],
|
|
715
|
+
{ allowAliases: true },
|
|
716
|
+
);
|
|
428
717
|
const rbrace = this.expect('RBRACE');
|
|
718
|
+
const aliases = this.popAliasScope();
|
|
429
719
|
|
|
430
720
|
return {
|
|
431
721
|
kind: 'concept',
|
|
722
|
+
spelledKind,
|
|
432
723
|
name: nameToken.value,
|
|
433
724
|
parentName,
|
|
725
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
434
726
|
body,
|
|
435
727
|
source: {
|
|
436
728
|
file: this.file,
|
|
@@ -443,19 +735,23 @@ class Parser {
|
|
|
443
735
|
/**
|
|
444
736
|
* @returns {RelatesDecl}
|
|
445
737
|
*/
|
|
446
|
-
parseRelates() {
|
|
447
|
-
const startToken = this.
|
|
738
|
+
parseRelates(spelledKind = 'relates') {
|
|
739
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
448
740
|
const aToken = this.expect('IDENTIFIER');
|
|
449
741
|
this.expect('KEYWORD', 'and');
|
|
450
742
|
const bToken = this.expect('IDENTIFIER');
|
|
451
743
|
const lbrace = this.expect('LBRACE');
|
|
452
|
-
|
|
744
|
+
this.pushAliasScope();
|
|
745
|
+
const body = this.parseBlockBody(['describe', 'title', 'from', 'relationships'], { allowAliases: true });
|
|
453
746
|
const rbrace = this.expect('RBRACE');
|
|
747
|
+
const aliases = this.popAliasScope();
|
|
454
748
|
|
|
455
749
|
return {
|
|
456
750
|
kind: 'relates',
|
|
751
|
+
spelledKind,
|
|
457
752
|
a: aToken.value,
|
|
458
753
|
b: bToken.value,
|
|
754
|
+
aliases: aliases.size > 0 ? Object.fromEntries(aliases) : undefined,
|
|
459
755
|
body,
|
|
460
756
|
source: {
|
|
461
757
|
file: this.file,
|
|
@@ -465,6 +761,191 @@ class Parser {
|
|
|
465
761
|
};
|
|
466
762
|
}
|
|
467
763
|
|
|
764
|
+
/**
|
|
765
|
+
* Parses a relationship declaration (symmetric or paired)
|
|
766
|
+
* @returns {RelationshipDecl}
|
|
767
|
+
*/
|
|
768
|
+
parseRelationship(spelledKind = 'relationship') {
|
|
769
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
770
|
+
const firstIdToken = this.expectIdentifierOrKeyword();
|
|
771
|
+
|
|
772
|
+
// Check if this is paired (has "and" after first identifier)
|
|
773
|
+
if (this.match('KEYWORD') && this.peek()?.value === 'and') {
|
|
774
|
+
// Paired relationship: relationship <leftId> and <rightId> { ... }
|
|
775
|
+
return this.parseRelationshipPaired(startToken, firstIdToken, spelledKind);
|
|
776
|
+
} else {
|
|
777
|
+
// Symmetric relationship: relationship <id> { ... }
|
|
778
|
+
return this.parseRelationshipSymmetric(startToken, firstIdToken, spelledKind);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Parses a symmetric relationship declaration
|
|
784
|
+
* @param {Token} startToken
|
|
785
|
+
* @param {Token} idToken
|
|
786
|
+
* @param {string} spelledKind
|
|
787
|
+
* @returns {RelationshipDecl}
|
|
788
|
+
*/
|
|
789
|
+
parseRelationshipSymmetric(startToken, idToken, spelledKind) {
|
|
790
|
+
const lbrace = this.expect('LBRACE');
|
|
791
|
+
let describe = null;
|
|
792
|
+
let label = null;
|
|
793
|
+
|
|
794
|
+
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
795
|
+
if (this.match('KEYWORD') || this.match('IDENTIFIER')) {
|
|
796
|
+
const keyword = this.peek()?.value;
|
|
797
|
+
if (keyword === 'describe') {
|
|
798
|
+
if (describe !== null) {
|
|
799
|
+
throw new Error(
|
|
800
|
+
`Multiple describe blocks in relationship "${idToken.value}" at ${this.file}:${startToken.span.start.line}`,
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
describe = this.parseDescribe();
|
|
804
|
+
} else if (keyword === 'label') {
|
|
805
|
+
if (label !== null) {
|
|
806
|
+
throw new Error(
|
|
807
|
+
`Multiple label blocks in relationship "${idToken.value}" at ${this.file}:${startToken.span.start.line}`,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
label = this.parseStringBlock('label');
|
|
811
|
+
} else {
|
|
812
|
+
this.advance();
|
|
813
|
+
}
|
|
814
|
+
} else {
|
|
815
|
+
this.advance();
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const rbrace = this.expect('RBRACE');
|
|
820
|
+
|
|
821
|
+
const relationshipDecl = {
|
|
822
|
+
kind: 'relationship',
|
|
823
|
+
spelledKind,
|
|
824
|
+
type: 'symmetric',
|
|
825
|
+
id: idToken.value,
|
|
826
|
+
describe: describe || undefined,
|
|
827
|
+
label: label || undefined,
|
|
828
|
+
source: {
|
|
829
|
+
file: this.file,
|
|
830
|
+
start: startToken.span.start,
|
|
831
|
+
end: rbrace.span.end,
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
// Declare the relationship in current scope
|
|
836
|
+
this.declareRelationship(relationshipDecl);
|
|
837
|
+
|
|
838
|
+
return relationshipDecl;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Parses a paired (directional) relationship declaration
|
|
843
|
+
* @param {Token} startToken
|
|
844
|
+
* @param {Token} leftIdToken
|
|
845
|
+
* @param {string} spelledKind
|
|
846
|
+
* @returns {RelationshipDecl}
|
|
847
|
+
*/
|
|
848
|
+
parseRelationshipPaired(startToken, leftIdToken, spelledKind) {
|
|
849
|
+
this.expect('KEYWORD', 'and');
|
|
850
|
+
const rightIdToken = this.expectIdentifierOrKeyword();
|
|
851
|
+
const lbrace = this.expect('LBRACE');
|
|
852
|
+
|
|
853
|
+
let pairDescribe = null;
|
|
854
|
+
const fromBlocks = {};
|
|
855
|
+
|
|
856
|
+
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
857
|
+
if (this.match('KEYWORD') || this.match('IDENTIFIER')) {
|
|
858
|
+
const keyword = this.peek()?.value;
|
|
859
|
+
if (keyword === 'describe') {
|
|
860
|
+
if (pairDescribe !== null) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
`Multiple describe blocks in relationship "${leftIdToken.value} and ${rightIdToken.value}" at ${this.file}:${startToken.span.start.line}`,
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
pairDescribe = this.parseDescribe();
|
|
866
|
+
} else if (keyword === 'from') {
|
|
867
|
+
const fromStartToken = this.advance();
|
|
868
|
+
const endpointToken = this.expect('IDENTIFIER');
|
|
869
|
+
|
|
870
|
+
// Validate: from block must reference leftId or rightId
|
|
871
|
+
if (endpointToken.value !== leftIdToken.value && endpointToken.value !== rightIdToken.value) {
|
|
872
|
+
throw new Error(
|
|
873
|
+
`from block references "${endpointToken.value}" which is not declared in relationship "${leftIdToken.value} and ${rightIdToken.value}" at ${this.file}:${endpointToken.span.start.line}`,
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const fromLbrace = this.expect('LBRACE');
|
|
878
|
+
let fromLabel = null;
|
|
879
|
+
let fromDescribe = null;
|
|
880
|
+
|
|
881
|
+
// Parse from block body (label and describe)
|
|
882
|
+
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
883
|
+
if (this.match('KEYWORD') || this.match('IDENTIFIER')) {
|
|
884
|
+
const fromKeyword = this.peek()?.value;
|
|
885
|
+
if (fromKeyword === 'label') {
|
|
886
|
+
if (fromLabel !== null) {
|
|
887
|
+
throw new Error(
|
|
888
|
+
`Multiple label blocks in from "${endpointToken.value}" at ${this.file}:${endpointToken.span.start.line}`,
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
fromLabel = this.parseStringBlock('label');
|
|
892
|
+
} else if (fromKeyword === 'describe') {
|
|
893
|
+
if (fromDescribe !== null) {
|
|
894
|
+
throw new Error(
|
|
895
|
+
`Multiple describe blocks in from "${endpointToken.value}" at ${this.file}:${endpointToken.span.start.line}`,
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
fromDescribe = this.parseDescribe();
|
|
899
|
+
} else {
|
|
900
|
+
this.advance();
|
|
901
|
+
}
|
|
902
|
+
} else {
|
|
903
|
+
this.advance();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const fromRbrace = this.expect('RBRACE');
|
|
908
|
+
|
|
909
|
+
const fromSide = {};
|
|
910
|
+
if (fromLabel !== null) {
|
|
911
|
+
fromSide.label = fromLabel;
|
|
912
|
+
}
|
|
913
|
+
if (fromDescribe !== null) {
|
|
914
|
+
fromSide.describe = fromDescribe;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
fromBlocks[endpointToken.value] = fromSide;
|
|
918
|
+
} else {
|
|
919
|
+
this.advance();
|
|
920
|
+
}
|
|
921
|
+
} else {
|
|
922
|
+
this.advance();
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const rbrace = this.expect('RBRACE');
|
|
927
|
+
|
|
928
|
+
const relationshipDecl = {
|
|
929
|
+
kind: 'relationship',
|
|
930
|
+
spelledKind,
|
|
931
|
+
type: 'paired',
|
|
932
|
+
leftId: leftIdToken.value,
|
|
933
|
+
rightId: rightIdToken.value,
|
|
934
|
+
describe: pairDescribe || undefined,
|
|
935
|
+
from: Object.keys(fromBlocks).length > 0 ? fromBlocks : undefined,
|
|
936
|
+
source: {
|
|
937
|
+
file: this.file,
|
|
938
|
+
start: startToken.span.start,
|
|
939
|
+
end: rbrace.span.end,
|
|
940
|
+
},
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
// Declare the relationship in current scope (both identifiers)
|
|
944
|
+
this.declareRelationship(relationshipDecl);
|
|
945
|
+
|
|
946
|
+
return relationshipDecl;
|
|
947
|
+
}
|
|
948
|
+
|
|
468
949
|
/**
|
|
469
950
|
* @returns {FromBlock}
|
|
470
951
|
*/
|
|
@@ -488,11 +969,54 @@ class Parser {
|
|
|
488
969
|
}
|
|
489
970
|
|
|
490
971
|
/**
|
|
972
|
+
* Parses a relationships block
|
|
973
|
+
* Supports both legacy syntax (string literals for relates blocks) and new syntax (relationship IDs + targets)
|
|
491
974
|
* @returns {RelationshipsBlock}
|
|
492
975
|
*/
|
|
493
976
|
parseRelationships() {
|
|
494
977
|
const startToken = this.expect('KEYWORD', 'relationships');
|
|
495
978
|
const lbrace = this.expect('LBRACE');
|
|
979
|
+
const startOffset = lbrace.span.end.offset;
|
|
980
|
+
|
|
981
|
+
// Try to detect which syntax we're using
|
|
982
|
+
// If first token after brace is a STRING, use legacy syntax
|
|
983
|
+
// Otherwise, use new syntax (relationship ID + targets)
|
|
984
|
+
|
|
985
|
+
// Peek ahead to see what comes next
|
|
986
|
+
let peekPos = this.pos;
|
|
987
|
+
let foundString = false;
|
|
988
|
+
let foundIdentifier = false;
|
|
989
|
+
|
|
990
|
+
while (peekPos < this.tokens.length && peekPos < this.pos + 10) {
|
|
991
|
+
const token = this.tokens[peekPos];
|
|
992
|
+
if (token.type === 'RBRACE' || token.type === 'EOF') break;
|
|
993
|
+
if (token.type === 'STRING') {
|
|
994
|
+
foundString = true;
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
if (token.type === 'IDENTIFIER' || token.type === 'KEYWORD') {
|
|
998
|
+
foundIdentifier = true;
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
peekPos++;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// If we found a string first, use legacy syntax (for relates blocks)
|
|
1005
|
+
if (foundString && !foundIdentifier) {
|
|
1006
|
+
return this.parseRelationshipsLegacy(startToken, lbrace);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Otherwise, use new syntax (relationship ID + targets)
|
|
1010
|
+
return this.parseRelationshipsNew(startToken, lbrace);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Parses legacy relationships block (string literals for relates blocks)
|
|
1015
|
+
* @param {Token} startToken
|
|
1016
|
+
* @param {Token} lbrace
|
|
1017
|
+
* @returns {RelationshipsBlock}
|
|
1018
|
+
*/
|
|
1019
|
+
parseRelationshipsLegacy(startToken, lbrace) {
|
|
496
1020
|
const values = [];
|
|
497
1021
|
let relationshipsSource = {
|
|
498
1022
|
file: this.file,
|
|
@@ -526,6 +1050,89 @@ class Parser {
|
|
|
526
1050
|
};
|
|
527
1051
|
}
|
|
528
1052
|
|
|
1053
|
+
/**
|
|
1054
|
+
* Parses new relationships block syntax (relationship ID + target identifiers)
|
|
1055
|
+
* @param {Token} startToken
|
|
1056
|
+
* @param {Token} lbrace
|
|
1057
|
+
* @returns {RelationshipsBlock}
|
|
1058
|
+
*/
|
|
1059
|
+
parseRelationshipsNew(startToken, lbrace) {
|
|
1060
|
+
const entries = [];
|
|
1061
|
+
let relationshipsSource = {
|
|
1062
|
+
file: this.file,
|
|
1063
|
+
start: lbrace.span.end,
|
|
1064
|
+
end: lbrace.span.end,
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
1068
|
+
// Parse relationship ID (identifier or keyword)
|
|
1069
|
+
if (this.match('IDENTIFIER') || this.match('KEYWORD')) {
|
|
1070
|
+
const relationshipIdToken = this.advance();
|
|
1071
|
+
const relationshipId = relationshipIdToken.value;
|
|
1072
|
+
|
|
1073
|
+
// Expect opening brace for targets
|
|
1074
|
+
if (!this.match('LBRACE')) {
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
`Expected '{' after relationship identifier "${relationshipId}" at ${this.file}:${relationshipIdToken.span.start.line}`,
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
const targetLbrace = this.advance();
|
|
1080
|
+
|
|
1081
|
+
const targets = [];
|
|
1082
|
+
|
|
1083
|
+
// Parse targets until closing brace
|
|
1084
|
+
while (!this.match('RBRACE') && !this.match('EOF')) {
|
|
1085
|
+
if (this.match('IDENTIFIER') || this.match('KEYWORD')) {
|
|
1086
|
+
const targetIdToken = this.advance();
|
|
1087
|
+
const targetId = targetIdToken.value;
|
|
1088
|
+
|
|
1089
|
+
// Check if this target has a metadata block
|
|
1090
|
+
let metadata = null;
|
|
1091
|
+
if (this.match('LBRACE')) {
|
|
1092
|
+
// Parse metadata block (describe)
|
|
1093
|
+
const metadataLbrace = this.advance();
|
|
1094
|
+
const metadataBody = this.parseBlockBody(['describe'], {});
|
|
1095
|
+
const metadataRbrace = this.expect('RBRACE');
|
|
1096
|
+
|
|
1097
|
+
const describeBlock = metadataBody.find((b) => b.kind === 'describe');
|
|
1098
|
+
if (describeBlock) {
|
|
1099
|
+
metadata = describeBlock;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
targets.push({
|
|
1104
|
+
id: targetId,
|
|
1105
|
+
metadata: metadata || undefined,
|
|
1106
|
+
});
|
|
1107
|
+
} else {
|
|
1108
|
+
// Skip other tokens
|
|
1109
|
+
this.advance();
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const targetRbrace = this.expect('RBRACE');
|
|
1114
|
+
relationshipsSource.end = targetRbrace.span.end;
|
|
1115
|
+
|
|
1116
|
+
entries.push({
|
|
1117
|
+
relationshipId,
|
|
1118
|
+
targets,
|
|
1119
|
+
});
|
|
1120
|
+
} else {
|
|
1121
|
+
// Skip other tokens
|
|
1122
|
+
this.advance();
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const rbrace = this.expect('RBRACE');
|
|
1127
|
+
relationshipsSource.end = rbrace.span.start;
|
|
1128
|
+
|
|
1129
|
+
return {
|
|
1130
|
+
kind: 'relationships',
|
|
1131
|
+
entries,
|
|
1132
|
+
source: relationshipsSource,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
529
1136
|
/**
|
|
530
1137
|
* Parses a describe block, consuming raw text until matching closing brace
|
|
531
1138
|
* Treats braces inside as plain text (doesn't parse nested structures)
|
|
@@ -641,6 +1248,62 @@ class Parser {
|
|
|
641
1248
|
};
|
|
642
1249
|
}
|
|
643
1250
|
|
|
1251
|
+
/**
|
|
1252
|
+
* Parses an ordering block containing a list of identifiers
|
|
1253
|
+
* @returns {OrderingBlock}
|
|
1254
|
+
*/
|
|
1255
|
+
parseOrdering() {
|
|
1256
|
+
const startToken = this.match('KEYWORD') ? this.expect('KEYWORD', 'ordering') : this.expect('IDENTIFIER', 'ordering');
|
|
1257
|
+
const lbrace = this.expect('LBRACE');
|
|
1258
|
+
const identifiers = [];
|
|
1259
|
+
let depth = 1;
|
|
1260
|
+
|
|
1261
|
+
while (this.pos < this.tokens.length && !this.match('EOF')) {
|
|
1262
|
+
if (this.match('LBRACE')) {
|
|
1263
|
+
throw new Error(`Unexpected '{' in ordering block at ${this.file}:${this.peek()?.span.start.line}. Use identifiers only.`);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
if (this.match('RBRACE')) {
|
|
1267
|
+
depth -= 1;
|
|
1268
|
+
this.advance();
|
|
1269
|
+
if (depth === 0) {
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (depth !== 1) {
|
|
1276
|
+
this.advance();
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
if (this.match('COMMA')) {
|
|
1281
|
+
this.advance();
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (this.match('IDENTIFIER') || this.match('KEYWORD')) {
|
|
1286
|
+
const identifierToken = this.advance();
|
|
1287
|
+
if (identifierToken) {
|
|
1288
|
+
identifiers.push(identifierToken.value);
|
|
1289
|
+
}
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
this.advance();
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
return {
|
|
1297
|
+
kind: 'ordering',
|
|
1298
|
+
identifiers,
|
|
1299
|
+
source: {
|
|
1300
|
+
file: this.file,
|
|
1301
|
+
start: startToken.span.start,
|
|
1302
|
+
end: this.tokens[this.pos - 1]?.span.end || lbrace.span.end,
|
|
1303
|
+
},
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
|
|
644
1307
|
/**
|
|
645
1308
|
* Parses a references block containing a list of identifier paths
|
|
646
1309
|
* @returns {ReferencesBlock}
|
|
@@ -715,8 +1378,8 @@ class Parser {
|
|
|
715
1378
|
* Parses a reference declaration: reference <Name> [in <RepoName>] { ... }
|
|
716
1379
|
* @returns {ReferenceDecl}
|
|
717
1380
|
*/
|
|
718
|
-
parseReferenceDecl() {
|
|
719
|
-
const startToken = this.
|
|
1381
|
+
parseReferenceDecl(spelledKind = 'reference') {
|
|
1382
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
720
1383
|
let nameToken = null;
|
|
721
1384
|
if (this.match('IDENTIFIER')) {
|
|
722
1385
|
nameToken = this.expect('IDENTIFIER');
|
|
@@ -940,8 +1603,8 @@ class Parser {
|
|
|
940
1603
|
* Parses a repository block: repository <Identifier> { url { ... } ... }
|
|
941
1604
|
* @returns {RepositoryDecl}
|
|
942
1605
|
*/
|
|
943
|
-
parseRepository() {
|
|
944
|
-
const startToken = this.
|
|
1606
|
+
parseRepository(spelledKind = 'repository') {
|
|
1607
|
+
const startToken = this.expectKindToken(spelledKind);
|
|
945
1608
|
const nameToken = this.expect('IDENTIFIER');
|
|
946
1609
|
let parentName = undefined;
|
|
947
1610
|
|