@xdbml/parse 0.4.0 → 0.5.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Name resolution pass (spec §26.10 / §26.15, parser batch P6).
2
+ * Name resolution pass (spec §27.10 / §27.15, parser batch P6).
3
3
  *
4
4
  * `resolveNames(doc)` walks an xDBML document (the flattened view; clone
5
5
  * blocks have been merged) and produces:
@@ -41,6 +41,7 @@
41
41
  import { SCALAR_TYPES, BSON_TYPES } from "./keywords.js";
42
42
  import { flatten } from "./module-resolver.js";
43
43
  import { checkRelationships } from "./relationships.js";
44
+ import { checkSupertypeGroups } from "./supertypes.js";
44
45
  /**
45
46
  * Read-only handle on the collected symbol table.
46
47
  *
@@ -95,11 +96,18 @@ export class SymbolTable {
95
96
  * Built-in type recognition
96
97
  *
97
98
  * Field type expressions can name a builtin scalar (`int`, `varchar`),
98
- * a BSON type (`objectId`), or a user-defined Named Type (`Email`).
99
- * The parser doesn't distinguish at parse time -- they all land as
99
+ * a BSON type (`objectId`), a user-defined Named Type (`Email`) or Enum,
100
+ * or any other target-native type (`number`, `clob`, `serial`). The
101
+ * parser doesn't distinguish at parse time -- they all land as
100
102
  * ScalarType nodes (or NamedTypeReference in some contexts). The
101
- * resolver uses these sets to decide whether to look up a name in the
102
- * symbol table.
103
+ * resolver uses these sets only to skip the symbol-table lookup for
104
+ * names that can never be a Named Type reference.
105
+ *
106
+ * The sets are not a type vocabulary. Scalar type names pass through
107
+ * as written (spec §1.2, principle 4), so a name that is neither a
108
+ * builtin nor a declared Type or Enum is a target-native type, not an
109
+ * error. See `nearMissTypeName()` for the one diagnostic such a name
110
+ * can raise.
103
111
  *
104
112
  * Matching is case-insensitive: `Int`, `int`, `INT` all map to the same
105
113
  * builtin per spec §3.8.
@@ -111,6 +119,87 @@ const BUILTIN_TYPES = new Set([
111
119
  function isBuiltinType(name) {
112
120
  return BUILTIN_TYPES.has(name.toLowerCase());
113
121
  }
122
+ /**
123
+ * The declared Types and Enums a bare type name can refer to. More than
124
+ * one entry means the name is declared in several containers (Enums can
125
+ * be container-scoped); the name still refers to a declaration, so it
126
+ * is neither a target-native type nor a near miss.
127
+ */
128
+ function typeOrEnumDeclarations(name, symbols) {
129
+ const qualified = symbols.lookup(name);
130
+ if (qualified && (qualified.kind === 'type' || qualified.kind === 'enum'))
131
+ return [qualified];
132
+ return symbols.lookupAllBare(name).filter((e) => e.kind === 'type' || e.kind === 'enum');
133
+ }
134
+ /**
135
+ * Near-miss detection for target-native type names.
136
+ *
137
+ * A type name that is neither a builtin nor a declared Type or Enum is
138
+ * accepted as a target-native type. The one case worth flagging is a
139
+ * name that differs only slightly from a declared Type or Enum -- most
140
+ * likely a misspelling (`Adress` for `Address`, `email` for `Email`).
141
+ * Returns the declared name to suggest, or undefined when nothing is
142
+ * close enough.
143
+ *
144
+ * Comparison is case-insensitive, so a difference of case alone counts
145
+ * as a near miss (identifiers are case-sensitive, spec §3.8). The edit
146
+ * distance allowed grows with the length of the name: none beyond case
147
+ * for three characters or fewer, one edit up to seven characters, two
148
+ * edits from eight. An adjacent transposition counts as one edit.
149
+ */
150
+ function nearMissTypeName(name, symbols) {
151
+ const lower = name.toLowerCase();
152
+ const maxDistance = lower.length <= 3 ? 0 : lower.length <= 7 ? 1 : 2;
153
+ // A qualified name (`core.job_stauts`) compares against qualified
154
+ // declarations, a bare name against bare ones.
155
+ const qualified = name.includes('.');
156
+ let best;
157
+ for (const entry of symbols.entries()) {
158
+ if (entry.kind !== 'type' && entry.kind !== 'enum')
159
+ continue;
160
+ const declared = qualified ? entry.qualifiedName : entry.name;
161
+ const candidate = declared.toLowerCase();
162
+ if (Math.abs(candidate.length - lower.length) > maxDistance)
163
+ continue;
164
+ const distance = editDistance(lower, candidate, maxDistance);
165
+ if (distance <= maxDistance && (!best || distance < best.distance)) {
166
+ best = { name: declared, distance };
167
+ }
168
+ }
169
+ return best?.name;
170
+ }
171
+ /**
172
+ * Optimal string alignment distance (Levenshtein plus adjacent
173
+ * transposition). Returns `limit + 1` as soon as every alignment of a
174
+ * row exceeds `limit`, since callers only compare against the limit.
175
+ */
176
+ function editDistance(a, b, limit) {
177
+ const rows = a.length + 1;
178
+ const cols = b.length + 1;
179
+ const d = [];
180
+ for (let i = 0; i < rows; i++) {
181
+ d.push(new Array(cols).fill(0));
182
+ d[i][0] = i;
183
+ }
184
+ for (let j = 0; j < cols; j++)
185
+ d[0][j] = j;
186
+ for (let i = 1; i < rows; i++) {
187
+ let rowMin = Infinity;
188
+ for (let j = 1; j < cols; j++) {
189
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
190
+ let v = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
191
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
192
+ v = Math.min(v, d[i - 2][j - 2] + 1);
193
+ }
194
+ d[i][j] = v;
195
+ if (v < rowMin)
196
+ rowMin = v;
197
+ }
198
+ if (rowMin > limit)
199
+ return limit + 1;
200
+ }
201
+ return d[rows - 1][cols - 1];
202
+ }
114
203
  /* -------------------------------------------------------------------------
115
204
  * Main entry point
116
205
  * ----------------------------------------------------------------------- */
@@ -137,6 +226,8 @@ export function resolveNames(doc) {
137
226
  resolveReferences(flat, symbols, diagnostics);
138
227
  // Pass 3: relationship rules that the grammar cannot express (spec 11.11).
139
228
  diagnostics.push(...checkRelationships(flat));
229
+ // Pass 4: supertype group rules (spec 12.8).
230
+ diagnostics.push(...checkSupertypeGroups(flat));
140
231
  return { diagnostics, symbols };
141
232
  }
142
233
  /* -------------------------------------------------------------------------
@@ -156,9 +247,20 @@ function addTopLevelDeclaration(stmt, entries, diagnostics, seen) {
156
247
  case 'TypeDeclaration':
157
248
  addEntry(stmt.name, undefined, 'type', stmt, stmt.span, entries, diagnostics, seen);
158
249
  return;
159
- case 'EnumDeclaration':
160
- addEntry(stmt.name, undefined, 'enum', stmt, stmt.span, entries, diagnostics, seen);
250
+ case 'EnumDeclaration': {
251
+ // `enum core.job_status { ... }` (the DBML form) files the Enum
252
+ // under container `core`, exactly as `Container core { Enum
253
+ // job_status { ... } }` does: same qualified name, same bare name,
254
+ // and declaring both is a duplicate (spec §16).
255
+ const dot = stmt.name.lastIndexOf('.');
256
+ if (dot > 0) {
257
+ addEntry(stmt.name.slice(dot + 1), stmt.name.slice(0, dot), 'enum', stmt, stmt.span, entries, diagnostics, seen);
258
+ }
259
+ else {
260
+ addEntry(stmt.name, undefined, 'enum', stmt, stmt.span, entries, diagnostics, seen);
261
+ }
161
262
  return;
263
+ }
162
264
  case 'EdgeDeclaration':
163
265
  addEntry(stmt.name, undefined, 'edge', stmt, stmt.span, entries, diagnostics, seen);
164
266
  return;
@@ -360,16 +462,18 @@ function resolveFieldDeclaration(field, symbols, diagnostics) {
360
462
  function resolveTypeExpression(expr, symbols, diagnostics) {
361
463
  switch (expr.kind) {
362
464
  case 'ScalarType':
363
- // A ScalarType is either a builtin or a reference to a Named Type
364
- // (the parser doesn't distinguish at parse time). Look up only
365
- // when the name isn't a builtin.
366
- if (!isBuiltinType(expr.name)) {
367
- const found = symbols.lookup(expr.name) ?? symbols.lookupBare(expr.name);
368
- if (!found || found.kind !== 'type') {
465
+ // A ScalarType is a builtin, a reference to a declared Type or
466
+ // Enum, or a target-native type passed through as written (spec
467
+ // §1.2, principle 4) -- the parser doesn't distinguish at parse
468
+ // time. A target-native name is valid; the only diagnostic is a
469
+ // warning when it is a near miss of a declared Type or Enum.
470
+ if (!isBuiltinType(expr.name) && typeOrEnumDeclarations(expr.name, symbols).length === 0) {
471
+ const suggestion = nearMissTypeName(expr.name, symbols);
472
+ if (suggestion) {
369
473
  diagnostics.push({
370
- severity: 'error',
371
- code: 'unresolved-type',
372
- message: `Type '${expr.name}' is not a built-in type or declared Named Type.`,
474
+ severity: 'warning',
475
+ code: 'possible-type-typo',
476
+ message: `Type '${expr.name}' is not declared; did you mean '${suggestion}'? Otherwise it is kept as a target-native type.`,
373
477
  span: expr.span,
374
478
  });
375
479
  }
@@ -821,9 +925,18 @@ function dereferenceNamedType(type, symbols, depth) {
821
925
  return type;
822
926
  }
823
927
  const sym = symbols.lookup(name) ?? symbols.lookupBare(name);
928
+ if (type.kind === 'ScalarType' && (!sym || sym.kind !== 'type')) {
929
+ // An Enum or a target-native type: an opaque scalar, so a path that
930
+ // navigates into it gets the walker's shape diagnostic. A name
931
+ // declared as a Type or Enum in several containers stays unresolved.
932
+ if (typeOrEnumDeclarations(name, symbols).length > 1)
933
+ return undefined;
934
+ return type;
935
+ }
824
936
  if (!sym || sym.kind !== 'type' || sym.declaration.kind !== 'TypeDeclaration') {
825
- // Unresolved -- the field-type pass will diagnose this. Return
826
- // undefined so the walker bails without emitting a duplicate error.
937
+ // Unresolved Named Type reference -- the field-type pass diagnoses
938
+ // this. Return undefined so the walker bails without emitting a
939
+ // duplicate error.
827
940
  return undefined;
828
941
  }
829
942
  const td = sym.declaration;
package/dist/parser.d.ts CHANGED
@@ -13,8 +13,22 @@ import type { ParseOptions, Position, XDbmlDocument } from './ast.ts';
13
13
  import type { Token } from './lexer.ts';
14
14
  export declare class ParseError extends Error {
15
15
  position: Position;
16
- constructor(message: string, position: Position);
16
+ /**
17
+ * Optional machine-readable reason. Set for the errors a caller may want
18
+ * to tell apart from a plain syntax error: `unsupported-version` when a
19
+ * document declares a newer version than this parser supports (spec 4.1).
20
+ */
21
+ code?: string;
22
+ constructor(message: string, position: Position, code?: string);
17
23
  }
24
+ /**
25
+ * The newest specification version this parser implements. A document
26
+ * declaring a later version is refused (spec 4.1) rather than parsed with
27
+ * semantics it does not have.
28
+ */
29
+ export declare const SUPPORTED_XDBML_VERSION = "0.5";
30
+ /** Compare dotted version strings numerically: -1, 0 or 1. */
31
+ export declare function compareVersions(a: string, b: string): number;
18
32
  export declare class Parser {
19
33
  private tokens;
20
34
  private idx;
@@ -30,7 +44,7 @@ export declare class Parser {
30
44
  * The set of file paths currently being parsed in the resolution chain.
31
45
  * Used for cycle detection: when resolving a directive whose `from` path
32
46
  * is already in this set, the parser produces an empty clone for that
33
- * directive rather than recursing (matching spec §26.15: cycles are
47
+ * directive rather than recursing (matching spec §27.15: cycles are
34
48
  * allowed; name resolution handles them). The set is passed by reference
35
49
  * across recursive parse() calls so all transitive levels see it.
36
50
  *
@@ -83,13 +97,13 @@ export declare class Parser {
83
97
  private parseEntityBody;
84
98
  private parsePartialInjection;
85
99
  /**
86
- * Parse a `records { ... }` block inside an entity body (§25.1, implicit
100
+ * Parse a `records { ... }` block inside an entity body (§26.1, implicit
87
101
  * column list). Values are stored as SettingValue cells; row boundaries
88
102
  * are determined by source line (see `parseRecordRow`).
89
103
  */
90
104
  private parseRecordsBlock;
91
105
  /**
92
- * Top-level records declaration (§25.2, new in v0.2):
106
+ * Top-level records declaration (§26.2, new in v0.2):
93
107
  *
94
108
  * records users (id, name, email) { ... }
95
109
  * records core.users (id, name, email) { ... }
@@ -151,12 +165,12 @@ export declare class Parser {
151
165
  * that match the import items by name and element type (matching is
152
166
  * downstream-consumer's job; the parser is permissive).
153
167
  *
154
- * Per spec §26.6, clone content uses the importing file's vocabulary
168
+ * Per spec §27.6, clone content uses the importing file's vocabulary
155
169
  * (aliases already applied) and is parsed under the importing file's
156
170
  * xdbml version directive.
157
171
  *
158
172
  * Most clone-block content uses TopLevelStatement shapes (Entity, Type,
159
- * Container, etc.). The exception is field imports (§26.8): when the
173
+ * Container, etc.). The exception is field imports (§27.8): when the
160
174
  * directive imports one or more fields via `field <path>` items, the
161
175
  * clone block holds each field as a bare FieldDeclaration with no entity
162
176
  * wrapper. The dispatch below checks whether the next token starts a
@@ -278,7 +292,7 @@ export declare class Parser {
278
292
  private parseCardinalityOperator;
279
293
  private parseRefEndpoint;
280
294
  /**
281
- * Parse a dotted path with the §18 segment vocabulary:
295
+ * Parse a dotted path with the §20 segment vocabulary:
282
296
  *
283
297
  * IDENTIFIER -- a field segment
284
298
  * .IDENTIFIER -- field
@@ -297,6 +311,16 @@ export declare class Parser {
297
311
  private parsePathSegments;
298
312
  private parseTablePartial;
299
313
  private parseTableGroup;
314
+ /**
315
+ * `SupertypeGroup <name> [supertype: X, ...] { Sub1 Sub2 [strategy: y] }`.
316
+ * Members are separated like TableGroup members: newline, comma or
317
+ * semicolon (spec §3.9). A name is required (§12.1); a writer exporting a
318
+ * group that has none emits `undefinedGroup1`, `undefinedGroup2`, ... so
319
+ * the parser never supplies one. Values are validated after parsing, by
320
+ * `checkSupertypeGroups()`, so an unknown value gets a located diagnostic
321
+ * rather than stopping the parse.
322
+ */
323
+ private parseSupertypeGroup;
300
324
  private parseIndexes;
301
325
  private parseIndexEntry;
302
326
  private parseIndexComponent;
package/dist/parser.js CHANGED
@@ -13,11 +13,37 @@ import { TokenKind, tokenize, } from "./lexer.js";
13
13
  import { resolveImport, classifyModuleSource, ModuleSourceError } from "./module-resolver.js";
14
14
  export class ParseError extends Error {
15
15
  position;
16
- constructor(message, position) {
16
+ /**
17
+ * Optional machine-readable reason. Set for the errors a caller may want
18
+ * to tell apart from a plain syntax error: `unsupported-version` when a
19
+ * document declares a newer version than this parser supports (spec 4.1).
20
+ */
21
+ code;
22
+ constructor(message, position, code) {
17
23
  super(`${message} (line ${position.line}, column ${position.column})`);
18
24
  this.position = position;
25
+ if (code)
26
+ this.code = code;
19
27
  }
20
28
  }
29
+ /**
30
+ * The newest specification version this parser implements. A document
31
+ * declaring a later version is refused (spec 4.1) rather than parsed with
32
+ * semantics it does not have.
33
+ */
34
+ export const SUPPORTED_XDBML_VERSION = '0.5';
35
+ /** Compare dotted version strings numerically: -1, 0 or 1. */
36
+ export function compareVersions(a, b) {
37
+ const pa = a.split('.').map((n) => Number(n) || 0);
38
+ const pb = b.split('.').map((n) => Number(n) || 0);
39
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
40
+ const x = pa[i] ?? 0;
41
+ const y = pb[i] ?? 0;
42
+ if (x !== y)
43
+ return x < y ? -1 : 1;
44
+ }
45
+ return 0;
46
+ }
21
47
  /* -------------------------------------------------------------------------
22
48
  * Keyword recognition.
23
49
  *
@@ -31,13 +57,13 @@ const CONTAINER_KEYWORDS = new Set([
31
57
  const ENTITY_KEYWORDS = new Set(['table', 'entity', 'collection', 'record']);
32
58
  /**
33
59
  * Element-type keywords accepted in module-system import items
34
- * (spec §26.3). Stored lowercased; matching is case-insensitive.
60
+ * (spec §27.3). Stored lowercased; matching is case-insensitive.
35
61
  *
36
62
  * `field` is recognized but explicitly rejected by parseImportItem in P4
37
63
  * (field-level imports have special declaration-vs-placement semantics
38
64
  * that will land in a later batch).
39
65
  *
40
- * `project` is intentionally excluded -- spec §26.1 forbids importing
66
+ * `project` is intentionally excluded -- spec §27.1 forbids importing
41
67
  * Project declarations.
42
68
  */
43
69
  const IMPORT_ELEMENT_TYPES = new Set([
@@ -45,7 +71,7 @@ const IMPORT_ELEMENT_TYPES = new Set([
45
71
  'enum', 'tablepartial', 'note',
46
72
  'schema', 'container', 'tablegroup',
47
73
  'type', 'edge', 'view', 'diagramview',
48
- 'field',
74
+ 'field', 'supertypegroup',
49
75
  ]);
50
76
  const STRUCTURAL_TYPE_KEYWORDS = new Set([
51
77
  'object', 'struct', 'record', 'array', 'list', 'map', 'dict', 'dictionary',
@@ -98,7 +124,7 @@ export class Parser {
98
124
  * The set of file paths currently being parsed in the resolution chain.
99
125
  * Used for cycle detection: when resolving a directive whose `from` path
100
126
  * is already in this set, the parser produces an empty clone for that
101
- * directive rather than recursing (matching spec §26.15: cycles are
127
+ * directive rather than recursing (matching spec §27.15: cycles are
102
128
  * allowed; name resolution handles them). The set is passed by reference
103
129
  * across recursive parse() calls so all transitive levels see it.
104
130
  *
@@ -179,6 +205,11 @@ export class Parser {
179
205
  this.advance(); // xdbml
180
206
  this.expect(TokenKind.Colon, "Expected ':' after 'xdbml'");
181
207
  const numTok = this.expect(TokenKind.NumberLiteral, 'Expected version number');
208
+ if (compareVersions(numTok.text, SUPPORTED_XDBML_VERSION) > 0) {
209
+ throw new ParseError(`This document declares 'xdbml: ${numTok.text}', which is newer than the ` +
210
+ `latest version this parser supports (${SUPPORTED_XDBML_VERSION}). ` +
211
+ 'Use a newer parser, or declare a supported version.', numTok.start, 'unsupported-version');
212
+ }
182
213
  return {
183
214
  kind: 'VersionDeclaration',
184
215
  version: numTok.text,
@@ -231,6 +262,8 @@ export class Parser {
231
262
  return this.parseTablePartial();
232
263
  if (k === 'tablegroup')
233
264
  return this.parseTableGroup();
265
+ if (k === 'supertypegroup')
266
+ return this.parseSupertypeGroup();
234
267
  if (k === 'note')
235
268
  return this.parseNoteDeclaration();
236
269
  if (k === 'records')
@@ -477,7 +510,7 @@ export class Parser {
477
510
  };
478
511
  }
479
512
  /**
480
- * Parse a `records { ... }` block inside an entity body (§25.1, implicit
513
+ * Parse a `records { ... }` block inside an entity body (§26.1, implicit
481
514
  * column list). Values are stored as SettingValue cells; row boundaries
482
515
  * are determined by source line (see `parseRecordRow`).
483
516
  */
@@ -497,7 +530,7 @@ export class Parser {
497
530
  };
498
531
  }
499
532
  /**
500
- * Top-level records declaration (§25.2, new in v0.2):
533
+ * Top-level records declaration (§26.2, new in v0.2):
501
534
  *
502
535
  * records users (id, name, email) { ... }
503
536
  * records core.users (id, name, email) { ... }
@@ -587,7 +620,7 @@ export class Parser {
587
620
  span: this.spanFrom(start),
588
621
  };
589
622
  }
590
- /* ----- Module-system directives (spec §26, new in v0.2) ----- */
623
+ /* ----- Module-system directives (spec §27, new in v0.2) ----- */
591
624
  /**
592
625
  * Parse a `use` or `reuse` directive. Called from both the top-level
593
626
  * dispatcher and the Container body dispatcher; the caller indicates
@@ -701,7 +734,7 @@ export class Parser {
701
734
  resolvedPath = result.resolvedPath;
702
735
  break;
703
736
  case 'cycle':
704
- // Per spec §26.15, cycles are allowed; the parser produces a
737
+ // Per spec §27.15, cycles are allowed; the parser produces a
705
738
  // directive with no clone, and name resolution (P6+) is
706
739
  // expected to bridge the cycle. We leave clone undefined.
707
740
  resolvedPath = result.resolvedPath;
@@ -759,12 +792,12 @@ export class Parser {
759
792
  `Expected one of: ${Array.from(IMPORT_ELEMENT_TYPES).join(', ')}.`, elemTok.start);
760
793
  }
761
794
  if (elementType === 'field' && context !== 'file-scope') {
762
- // Spec §26.8: field imports must appear at file scope. Inside a
795
+ // Spec §27.8: field imports must appear at file scope. Inside a
763
796
  // Container body, the field's eventual placement (as a Named Type)
764
797
  // would have no meaningful container scope -- field imports are
765
798
  // always lifted to file scope by flatten(), regardless of where
766
799
  // the directive sits.
767
- throw new ParseError(`Field-level imports must appear at file scope, not inside a Container body (spec §26.8).`, elemTok.start);
800
+ throw new ParseError(`Field-level imports must appear at file scope, not inside a Container body (spec §27.8).`, elemTok.start);
768
801
  }
769
802
  this.advance(); // consume element type keyword
770
803
  // Dotted source path.
@@ -795,12 +828,12 @@ export class Parser {
795
828
  * that match the import items by name and element type (matching is
796
829
  * downstream-consumer's job; the parser is permissive).
797
830
  *
798
- * Per spec §26.6, clone content uses the importing file's vocabulary
831
+ * Per spec §27.6, clone content uses the importing file's vocabulary
799
832
  * (aliases already applied) and is parsed under the importing file's
800
833
  * xdbml version directive.
801
834
  *
802
835
  * Most clone-block content uses TopLevelStatement shapes (Entity, Type,
803
- * Container, etc.). The exception is field imports (§26.8): when the
836
+ * Container, etc.). The exception is field imports (§27.8): when the
804
837
  * directive imports one or more fields via `field <path>` items, the
805
838
  * clone block holds each field as a bare FieldDeclaration with no entity
806
839
  * wrapper. The dispatch below checks whether the next token starts a
@@ -816,7 +849,7 @@ export class Parser {
816
849
  statements.push(this.parseTopLevelStatement());
817
850
  }
818
851
  else {
819
- // Bare field declaration -- the field-import case. Per spec §26.6
852
+ // Bare field declaration -- the field-import case. Per spec §27.6
820
853
  // the field appears without an entity wrapper.
821
854
  statements.push(this.parseFieldDeclaration());
822
855
  }
@@ -1256,7 +1289,14 @@ export class Parser {
1256
1289
  throw new ParseError(`Expected type name, got ${t.kind} ${JSON.stringify(t.text)}`, t.start);
1257
1290
  }
1258
1291
  this.advance();
1259
- const name = t.kind === TokenKind.QuotedIdentifier ? (t.value ?? '') : t.text;
1292
+ let name = t.kind === TokenKind.QuotedIdentifier ? (t.value ?? '') : t.text;
1293
+ // A qualified type name, `core.job_status`, names an Enum declared in
1294
+ // a container (spec §16), or a target-native type such as
1295
+ // `public.geometry` that passes through as written.
1296
+ while (this.check(TokenKind.Dot)) {
1297
+ this.advance();
1298
+ name += `.${this.parseIdentLikeName('type name after dot')}`;
1299
+ }
1260
1300
  let params;
1261
1301
  if (this.check(TokenKind.LParen)) {
1262
1302
  this.advance();
@@ -1284,7 +1324,7 @@ export class Parser {
1284
1324
  }
1285
1325
  throw new ParseError(`Expected type parameter, got ${t.kind}`, t.start);
1286
1326
  }
1287
- /* ----- Type declaration (§13) ----- */
1327
+ /* ----- Type declaration (§15) ----- */
1288
1328
  parseTypeDecl() {
1289
1329
  const start = this.peek().start;
1290
1330
  this.advance(); // Type
@@ -1293,7 +1333,7 @@ export class Parser {
1293
1333
  //
1294
1334
  // { ... } v0.1 object form, no pre-body settings
1295
1335
  // [ settings ] { ... } v0.1 object form, pre-body settings (permissive)
1296
- // typeExpression v0.2 scalar form (spec §14.7)
1336
+ // typeExpression v0.2 scalar form (spec §15.7)
1297
1337
  // typeExpression [ settings ] v0.2 scalar form with field-level settings
1298
1338
  //
1299
1339
  // Note that LBrace and LBracket are distinct from any start-of-type-expression
@@ -1422,7 +1462,13 @@ export class Parser {
1422
1462
  parseEnum() {
1423
1463
  const start = this.peek().start;
1424
1464
  const kwTok = this.advance(); // enum
1425
- const name = this.parseIdentLikeName('enum name');
1465
+ // DBML form `enum core.job_status { ... }`: the qualifier names the
1466
+ // container, as for a schema-qualified Table (spec §7.3, §16).
1467
+ let name = this.parseIdentLikeName('enum name');
1468
+ while (this.check(TokenKind.Dot)) {
1469
+ this.advance();
1470
+ name += `.${this.parseIdentLikeName('enum name after dot')}`;
1471
+ }
1426
1472
  this.expect(TokenKind.LBrace, "Expected '{' after enum name");
1427
1473
  const values = [];
1428
1474
  while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
@@ -1572,7 +1618,7 @@ export class Parser {
1572
1618
  };
1573
1619
  }
1574
1620
  /**
1575
- * Parse a dotted path with the §18 segment vocabulary:
1621
+ * Parse a dotted path with the §20 segment vocabulary:
1576
1622
  *
1577
1623
  * IDENTIFIER -- a field segment
1578
1624
  * .IDENTIFIER -- field
@@ -1746,6 +1792,67 @@ export class Parser {
1746
1792
  span: this.spanFrom(start),
1747
1793
  };
1748
1794
  }
1795
+ /* ----- SupertypeGroup (spec §12, new in v0.5) ----- */
1796
+ /**
1797
+ * `SupertypeGroup <name> [supertype: X, ...] { Sub1 Sub2 [strategy: y] }`.
1798
+ * Members are separated like TableGroup members: newline, comma or
1799
+ * semicolon (spec §3.9). A name is required (§12.1); a writer exporting a
1800
+ * group that has none emits `undefinedGroup1`, `undefinedGroup2`, ... so
1801
+ * the parser never supplies one. Values are validated after parsing, by
1802
+ * `checkSupertypeGroups()`, so an unknown value gets a located diagnostic
1803
+ * rather than stopping the parse.
1804
+ */
1805
+ parseSupertypeGroup() {
1806
+ const start = this.peek().start;
1807
+ this.advance(); // SupertypeGroup
1808
+ const nameTok = this.peek();
1809
+ if (nameTok.kind !== TokenKind.Identifier && nameTok.kind !== TokenKind.QuotedIdentifier) {
1810
+ throw new ParseError('Expected a name after SupertypeGroup. Every group has a name (spec §12.1); ' +
1811
+ 'a group exported without one is written undefinedGroup1, undefinedGroup2, ...', nameTok.start);
1812
+ }
1813
+ const name = this.parseIdentLikeName('SupertypeGroup name');
1814
+ const settings = this.maybeSettingsBlock();
1815
+ this.expect(TokenKind.LBrace, "Expected '{' after SupertypeGroup name and settings");
1816
+ const members = [];
1817
+ while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
1818
+ const t = this.peek();
1819
+ if (t.kind === TokenKind.Identifier || t.kind === TokenKind.QuotedIdentifier) {
1820
+ const memberStart = t.start;
1821
+ this.advance();
1822
+ let n = t.kind === TokenKind.QuotedIdentifier ? (t.value ?? '') : t.text;
1823
+ while (this.check(TokenKind.Dot)) {
1824
+ this.advance();
1825
+ const next = this.peek();
1826
+ if (next.kind !== TokenKind.Identifier && next.kind !== TokenKind.QuotedIdentifier) {
1827
+ throw new ParseError('Expected identifier after dot in subtype path', next.start);
1828
+ }
1829
+ this.advance();
1830
+ n += `.${next.kind === TokenKind.QuotedIdentifier ? (next.value ?? '') : next.text}`;
1831
+ }
1832
+ const memberSettings = this.maybeSettingsBlock();
1833
+ members.push({
1834
+ kind: 'SupertypeGroupMember',
1835
+ name: n,
1836
+ settings: memberSettings,
1837
+ span: this.spanFrom(memberStart),
1838
+ });
1839
+ }
1840
+ else if (t.kind === TokenKind.Semicolon || t.kind === TokenKind.Comma) {
1841
+ this.advance();
1842
+ }
1843
+ else {
1844
+ throw new ParseError(`Unexpected ${t.kind} in SupertypeGroup body; expected a subtype entity name`, t.start);
1845
+ }
1846
+ }
1847
+ this.expect(TokenKind.RBrace, "Expected '}' closing SupertypeGroup");
1848
+ return {
1849
+ kind: 'SupertypeGroupDeclaration',
1850
+ name,
1851
+ settings,
1852
+ members,
1853
+ span: this.spanFrom(start),
1854
+ };
1855
+ }
1749
1856
  /* ----- Indexes ----- */
1750
1857
  parseIndexes() {
1751
1858
  const start = this.peek().start;