@duckcodeailabs/dql-core 0.1.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.
Files changed (74) hide show
  1. package/LICENSE +123 -0
  2. package/dist/ast/index.d.ts +4 -0
  3. package/dist/ast/index.d.ts.map +1 -0
  4. package/dist/ast/index.js +4 -0
  5. package/dist/ast/index.js.map +1 -0
  6. package/dist/ast/nodes.d.ts +205 -0
  7. package/dist/ast/nodes.d.ts.map +1 -0
  8. package/dist/ast/nodes.js +33 -0
  9. package/dist/ast/nodes.js.map +1 -0
  10. package/dist/ast/printer.d.ts +3 -0
  11. package/dist/ast/printer.d.ts.map +1 -0
  12. package/dist/ast/printer.js +157 -0
  13. package/dist/ast/printer.js.map +1 -0
  14. package/dist/ast/visitor.d.ts +45 -0
  15. package/dist/ast/visitor.d.ts.map +1 -0
  16. package/dist/ast/visitor.js +155 -0
  17. package/dist/ast/visitor.js.map +1 -0
  18. package/dist/errors/diagnostic.d.ts +23 -0
  19. package/dist/errors/diagnostic.d.ts.map +1 -0
  20. package/dist/errors/diagnostic.js +13 -0
  21. package/dist/errors/diagnostic.js.map +1 -0
  22. package/dist/errors/index.d.ts +3 -0
  23. package/dist/errors/index.d.ts.map +1 -0
  24. package/dist/errors/index.js +3 -0
  25. package/dist/errors/index.js.map +1 -0
  26. package/dist/errors/reporter.d.ts +22 -0
  27. package/dist/errors/reporter.d.ts.map +1 -0
  28. package/dist/errors/reporter.js +64 -0
  29. package/dist/errors/reporter.js.map +1 -0
  30. package/dist/formatter/formatter.d.ts +8 -0
  31. package/dist/formatter/formatter.d.ts.map +1 -0
  32. package/dist/formatter/formatter.js +243 -0
  33. package/dist/formatter/formatter.js.map +1 -0
  34. package/dist/formatter/index.d.ts +2 -0
  35. package/dist/formatter/index.d.ts.map +1 -0
  36. package/dist/formatter/index.js +2 -0
  37. package/dist/formatter/index.js.map +1 -0
  38. package/dist/index.d.ts +7 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +9 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/lexer/index.d.ts +3 -0
  43. package/dist/lexer/index.d.ts.map +1 -0
  44. package/dist/lexer/index.js +3 -0
  45. package/dist/lexer/index.js.map +1 -0
  46. package/dist/lexer/lexer.d.ts +43 -0
  47. package/dist/lexer/lexer.d.ts.map +1 -0
  48. package/dist/lexer/lexer.js +346 -0
  49. package/dist/lexer/lexer.js.map +1 -0
  50. package/dist/lexer/token.d.ts +72 -0
  51. package/dist/lexer/token.d.ts.map +1 -0
  52. package/dist/lexer/token.js +170 -0
  53. package/dist/lexer/token.js.map +1 -0
  54. package/dist/parser/index.d.ts +2 -0
  55. package/dist/parser/index.d.ts.map +1 -0
  56. package/dist/parser/index.js +2 -0
  57. package/dist/parser/index.js.map +1 -0
  58. package/dist/parser/parser.d.ts +50 -0
  59. package/dist/parser/parser.d.ts.map +1 -0
  60. package/dist/parser/parser.js +1024 -0
  61. package/dist/parser/parser.js.map +1 -0
  62. package/dist/semantic/analyzer.d.ts +27 -0
  63. package/dist/semantic/analyzer.d.ts.map +1 -0
  64. package/dist/semantic/analyzer.js +580 -0
  65. package/dist/semantic/analyzer.js.map +1 -0
  66. package/dist/semantic/index.d.ts +4 -0
  67. package/dist/semantic/index.d.ts.map +1 -0
  68. package/dist/semantic/index.js +3 -0
  69. package/dist/semantic/index.js.map +1 -0
  70. package/dist/semantic/semantic-layer.d.ts +118 -0
  71. package/dist/semantic/semantic-layer.d.ts.map +1 -0
  72. package/dist/semantic/semantic-layer.js +264 -0
  73. package/dist/semantic/semantic-layer.js.map +1 -0
  74. package/package.json +49 -0
@@ -0,0 +1,1024 @@
1
+ import { DiagnosticReporter, DQLSyntaxError } from '../errors/reporter.js';
2
+ import { Lexer } from '../lexer/lexer.js';
3
+ import { TokenType, CHART_TYPES, FILTER_TYPES, normalizeChartType } from '../lexer/token.js';
4
+ import { NodeKind, } from '../ast/nodes.js';
5
+ export class Parser {
6
+ tokens;
7
+ pos = 0;
8
+ source;
9
+ file;
10
+ reporter;
11
+ constructor(source, file) {
12
+ this.source = source;
13
+ this.file = file;
14
+ this.reporter = new DiagnosticReporter();
15
+ const lexer = new Lexer(source, file);
16
+ this.tokens = lexer.tokenize();
17
+ }
18
+ parse() {
19
+ const start = this.currentSpan();
20
+ const statements = [];
21
+ while (!this.isAtEnd()) {
22
+ try {
23
+ const stmt = this.parseStatement();
24
+ if (stmt) {
25
+ statements.push(stmt);
26
+ }
27
+ }
28
+ catch (e) {
29
+ // Skip to next statement-level token on error
30
+ this.synchronize();
31
+ }
32
+ }
33
+ if (this.reporter.hasErrors()) {
34
+ throw new DQLSyntaxError(`Parse errors in ${this.file ?? '<input>'}`, this.reporter.getErrors());
35
+ }
36
+ return {
37
+ kind: NodeKind.Program,
38
+ statements,
39
+ span: this.makeSpan(start, this.currentSpan()),
40
+ };
41
+ }
42
+ parseStatement() {
43
+ // Collect leading decorators
44
+ const decorators = this.parseDecoratorList();
45
+ if (this.check(TokenType.WorkbookKeyword)) {
46
+ return this.parseWorkbook(decorators);
47
+ }
48
+ if (this.check(TokenType.DashboardKeyword)) {
49
+ return this.parseDashboard(decorators);
50
+ }
51
+ if (this.check(TokenType.ChartKeyword)) {
52
+ return this.parseChartCall(decorators);
53
+ }
54
+ if (this.check(TokenType.BlockKeyword)) {
55
+ return this.parseBlockDecl(decorators);
56
+ }
57
+ if (this.check(TokenType.ImportKeyword)) {
58
+ if (decorators.length > 0) {
59
+ this.error('Decorators cannot be applied to import statements.');
60
+ }
61
+ return this.parseImportDecl();
62
+ }
63
+ if (this.check(TokenType.EOF)) {
64
+ return null;
65
+ }
66
+ this.error(`Unexpected token '${this.current().value}'. Expected 'dashboard', 'workbook', 'chart', 'block', or 'import'.`);
67
+ return null;
68
+ }
69
+ parseFilterCall() {
70
+ const start = this.currentSpan();
71
+ this.expect(TokenType.FilterKeyword);
72
+ this.expect(TokenType.Dot);
73
+ const filterTypeToken = this.expect(TokenType.Identifier);
74
+ if (!FILTER_TYPES.has(filterTypeToken.value)) {
75
+ this.reporter.error(`Unknown filter type '${filterTypeToken.value}'. Valid types: ${[...FILTER_TYPES].join(', ')}`, filterTypeToken.span, `Did you mean one of: ${[...FILTER_TYPES].join(', ')}?`);
76
+ }
77
+ this.expect(TokenType.LeftParen);
78
+ // Check if there's a SQL query (SELECT/WITH) or just named args
79
+ let query;
80
+ const currentToken = this.current();
81
+ const tokenVal = currentToken.value.toUpperCase();
82
+ if (tokenVal === 'SELECT' || tokenVal === 'WITH') {
83
+ query = this.parseSQLQuery();
84
+ }
85
+ // Parse named arguments
86
+ const args = [];
87
+ while (this.check(TokenType.Comma) || (!query && (this.check(TokenType.Identifier) || this.isKeywordUsableAsArgName(this.current().type)))) {
88
+ if (this.check(TokenType.Comma)) {
89
+ this.advance(); // consume comma
90
+ }
91
+ if (this.check(TokenType.RightParen))
92
+ break; // trailing comma
93
+ args.push(this.parseNamedArg());
94
+ }
95
+ this.expect(TokenType.RightParen);
96
+ return {
97
+ kind: NodeKind.FilterCall,
98
+ filterType: filterTypeToken.value,
99
+ query,
100
+ args,
101
+ span: this.makeSpan(start, this.previousSpan()),
102
+ };
103
+ }
104
+ parseDashboard(decorators) {
105
+ const start = decorators.length > 0 ? decorators[0].span : this.currentSpan();
106
+ this.expect(TokenType.DashboardKeyword);
107
+ const titleToken = this.expect(TokenType.StringLiteral);
108
+ this.expect(TokenType.LeftBrace);
109
+ const body = [];
110
+ while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
111
+ // Collect decorators for items inside dashboard
112
+ const itemDecorators = this.parseDecoratorList();
113
+ if (this.check(TokenType.LetKeyword)) {
114
+ if (itemDecorators.length > 0) {
115
+ this.error('Decorators cannot be applied to variable declarations.');
116
+ }
117
+ body.push(this.parseVariableDecl());
118
+ }
119
+ else if (this.check(TokenType.ParamKeyword)) {
120
+ if (itemDecorators.length > 0) {
121
+ this.error('Decorators cannot be applied to param declarations.');
122
+ }
123
+ body.push(this.parseParamDecl());
124
+ }
125
+ else if (this.check(TokenType.ChartKeyword)) {
126
+ body.push(this.parseChartCall(itemDecorators));
127
+ }
128
+ else if (this.check(TokenType.FilterKeyword)) {
129
+ if (itemDecorators.length > 0) {
130
+ this.error('Decorators cannot be applied to filter declarations.');
131
+ }
132
+ body.push(this.parseFilterCall());
133
+ }
134
+ else if (this.check(TokenType.UseKeyword)) {
135
+ if (itemDecorators.length > 0) {
136
+ this.error('Decorators cannot be applied to use declarations.');
137
+ }
138
+ body.push(this.parseUseDecl());
139
+ }
140
+ else if (this.check(TokenType.LayoutKeyword)) {
141
+ if (itemDecorators.length > 0) {
142
+ this.error('Decorators cannot be applied to layout blocks.');
143
+ }
144
+ body.push(this.parseLayoutBlock());
145
+ }
146
+ else if (this.check(TokenType.RightBrace)) {
147
+ break;
148
+ }
149
+ else {
150
+ this.error(`Unexpected token '${this.current().value}' inside dashboard. Expected 'let', 'param', 'chart', 'filter', 'use', 'layout', or '}'.`);
151
+ this.advance();
152
+ }
153
+ }
154
+ this.expect(TokenType.RightBrace);
155
+ return {
156
+ kind: NodeKind.Dashboard,
157
+ title: titleToken.value,
158
+ decorators,
159
+ body,
160
+ span: this.makeSpan(start, this.previousSpan()),
161
+ };
162
+ }
163
+ parseChartCall(decorators) {
164
+ const start = decorators.length > 0 ? decorators[0].span : this.currentSpan();
165
+ this.expect(TokenType.ChartKeyword);
166
+ this.expect(TokenType.Dot);
167
+ const chartTypeStart = this.currentSpan();
168
+ const chartTypeToken = this.expect(TokenType.Identifier);
169
+ let rawChartType = chartTypeToken.value;
170
+ while (this.check(TokenType.Minus)) {
171
+ this.advance();
172
+ rawChartType += `-${this.expect(TokenType.Identifier).value}`;
173
+ }
174
+ const chartTypeSpan = this.makeSpan(chartTypeStart, this.previousSpan());
175
+ if (!CHART_TYPES.has(rawChartType)) {
176
+ this.reporter.error(`Unknown chart type '${rawChartType}'. Valid types: ${[...CHART_TYPES].join(', ')}`, chartTypeSpan, `Did you mean one of: ${[...CHART_TYPES].join(', ')}?`);
177
+ }
178
+ const normalizedChartType = normalizeChartType(rawChartType);
179
+ this.expect(TokenType.LeftParen);
180
+ // Parse the SQL query using boundary detection on the raw source
181
+ const query = this.parseSQLQuery();
182
+ // Parse named arguments
183
+ const args = [];
184
+ while (this.check(TokenType.Comma)) {
185
+ this.advance(); // consume comma
186
+ if (this.check(TokenType.RightParen))
187
+ break; // trailing comma
188
+ args.push(this.parseNamedArg());
189
+ }
190
+ this.expect(TokenType.RightParen);
191
+ return {
192
+ kind: NodeKind.ChartCall,
193
+ chartType: normalizedChartType,
194
+ query,
195
+ args,
196
+ decorators,
197
+ span: this.makeSpan(start, this.previousSpan()),
198
+ };
199
+ }
200
+ parseSQLQuery() {
201
+ const start = this.currentSpan();
202
+ // We need to find the SQL text from the raw source.
203
+ // The current token should be the start of the SQL (SELECT, WITH, etc.)
204
+ // We use Lexer.scanSQLBoundary on the raw source to find where SQL ends.
205
+ const currentToken = this.current();
206
+ const sqlStartOffset = currentToken.span.start.offset;
207
+ // Scan the raw source for SQL boundary
208
+ const { sql, endOffset } = Lexer.scanSQLBoundary(this.source, sqlStartOffset, 1);
209
+ // Extract template interpolations from the SQL text
210
+ const interpolations = this.extractInterpolations(sql, start);
211
+ // Now advance our token position past all tokens that fall within the SQL range
212
+ while (!this.isAtEnd() &&
213
+ this.current().span.start.offset < endOffset) {
214
+ this.advance();
215
+ }
216
+ const span = this.makeSpan(start, this.currentSpan());
217
+ return {
218
+ kind: NodeKind.SQLQuery,
219
+ rawSQL: sql,
220
+ interpolations,
221
+ span,
222
+ };
223
+ }
224
+ extractInterpolations(sql, baseSpan) {
225
+ const interpolations = [];
226
+ const regex = /\$\{(\w+)\}|\{(\w+)\}/g;
227
+ let match;
228
+ while ((match = regex.exec(sql)) !== null) {
229
+ const variableName = match[1] || match[2];
230
+ if (!variableName)
231
+ continue;
232
+ interpolations.push({
233
+ variableName,
234
+ offsetInSQL: match.index,
235
+ span: baseSpan, // simplified; ideally compute exact position
236
+ });
237
+ }
238
+ return interpolations;
239
+ }
240
+ parseNamedArg() {
241
+ const start = this.currentSpan();
242
+ // Accept identifiers and keywords that may be used as argument names (e.g. 'param', 'label', 'from')
243
+ const nameToken = this.current();
244
+ if (nameToken.type === TokenType.Identifier || this.isKeywordUsableAsArgName(nameToken.type)) {
245
+ this.advance();
246
+ }
247
+ else {
248
+ this.error(`Expected argument name, got '${nameToken.value}'.`);
249
+ this.advance();
250
+ }
251
+ this.expect(TokenType.Equals);
252
+ const value = this.parseExpression();
253
+ return {
254
+ kind: NodeKind.NamedArg,
255
+ name: nameToken.value,
256
+ value,
257
+ span: this.makeSpan(start, this.previousSpan()),
258
+ };
259
+ }
260
+ isKeywordUsableAsArgName(type) {
261
+ return type === TokenType.ParamKeyword
262
+ || type === TokenType.FromKeyword
263
+ || type === TokenType.LayoutKeyword
264
+ || type === TokenType.RowKeyword
265
+ || type === TokenType.UseKeyword
266
+ || type === TokenType.ImportKeyword;
267
+ }
268
+ parseVariableDecl() {
269
+ const start = this.currentSpan();
270
+ this.expect(TokenType.LetKeyword);
271
+ const nameToken = this.expect(TokenType.Identifier);
272
+ this.expect(TokenType.Equals);
273
+ const initializer = this.parseExpression();
274
+ return {
275
+ kind: NodeKind.VariableDecl,
276
+ name: nameToken.value,
277
+ initializer,
278
+ span: this.makeSpan(start, this.previousSpan()),
279
+ };
280
+ }
281
+ parseDecoratorList() {
282
+ const decorators = [];
283
+ while (this.check(TokenType.AtSign)) {
284
+ decorators.push(this.parseDecorator());
285
+ }
286
+ return decorators;
287
+ }
288
+ parseDecorator() {
289
+ const start = this.currentSpan();
290
+ this.expect(TokenType.AtSign);
291
+ const nameToken = this.expect(TokenType.Identifier);
292
+ const args = [];
293
+ if (this.check(TokenType.LeftParen)) {
294
+ this.advance(); // consume (
295
+ if (!this.check(TokenType.RightParen)) {
296
+ args.push(this.parseExpression());
297
+ while (this.check(TokenType.Comma)) {
298
+ this.advance();
299
+ args.push(this.parseExpression());
300
+ }
301
+ }
302
+ this.expect(TokenType.RightParen);
303
+ }
304
+ return {
305
+ kind: NodeKind.Decorator,
306
+ name: nameToken.value,
307
+ arguments: args,
308
+ span: this.makeSpan(start, this.previousSpan()),
309
+ };
310
+ }
311
+ parseExpression() {
312
+ let left = this.parsePrimary();
313
+ // Handle binary + and - operators
314
+ while (this.check(TokenType.Plus) || this.check(TokenType.Minus)) {
315
+ const opToken = this.advance();
316
+ const right = this.parsePrimary();
317
+ left = {
318
+ kind: NodeKind.BinaryExpr,
319
+ operator: opToken.value,
320
+ left,
321
+ right,
322
+ span: this.makeSpan(left.span, right.span),
323
+ };
324
+ }
325
+ return left;
326
+ }
327
+ parsePrimary() {
328
+ const token = this.current();
329
+ // String literal
330
+ if (this.check(TokenType.StringLiteral)) {
331
+ this.advance();
332
+ return {
333
+ kind: NodeKind.StringLiteral,
334
+ value: token.value,
335
+ span: token.span,
336
+ };
337
+ }
338
+ // Number literal
339
+ if (this.check(TokenType.NumberLiteral)) {
340
+ this.advance();
341
+ return {
342
+ kind: NodeKind.NumberLiteral,
343
+ value: Number(token.value),
344
+ span: token.span,
345
+ };
346
+ }
347
+ // Boolean literal
348
+ if (this.check(TokenType.BooleanLiteral)) {
349
+ this.advance();
350
+ return {
351
+ kind: NodeKind.BooleanLiteral,
352
+ value: token.value === 'true',
353
+ span: token.span,
354
+ };
355
+ }
356
+ // Array literal
357
+ if (this.check(TokenType.LeftBracket)) {
358
+ return this.parseArrayLiteral();
359
+ }
360
+ // INTERVAL expression
361
+ if (this.check(TokenType.IntervalKeyword)) {
362
+ const start = this.currentSpan();
363
+ this.advance(); // consume INTERVAL
364
+ const valueToken = this.expect(TokenType.StringLiteral);
365
+ return {
366
+ kind: NodeKind.IntervalExpr,
367
+ value: valueToken.value,
368
+ span: this.makeSpan(start, this.previousSpan()),
369
+ };
370
+ }
371
+ // Identifier (may be function call)
372
+ if (this.check(TokenType.Identifier)) {
373
+ this.advance();
374
+ const name = token.value;
375
+ // Check for function call: ident(
376
+ if (this.check(TokenType.LeftParen)) {
377
+ return this.parseFunctionCall(name, token.span);
378
+ }
379
+ return {
380
+ kind: NodeKind.Identifier,
381
+ name,
382
+ span: token.span,
383
+ };
384
+ }
385
+ // Negative number (minus followed by number)
386
+ if (this.check(TokenType.Minus)) {
387
+ const start = this.currentSpan();
388
+ this.advance();
389
+ const numToken = this.expect(TokenType.NumberLiteral);
390
+ return {
391
+ kind: NodeKind.NumberLiteral,
392
+ value: -Number(numToken.value),
393
+ span: this.makeSpan(start, numToken.span),
394
+ };
395
+ }
396
+ this.error(`Unexpected token '${token.value}'. Expected an expression.`);
397
+ this.advance();
398
+ return {
399
+ kind: NodeKind.Identifier,
400
+ name: '__error__',
401
+ span: token.span,
402
+ };
403
+ }
404
+ parseArrayLiteral() {
405
+ const start = this.currentSpan();
406
+ this.expect(TokenType.LeftBracket);
407
+ const elements = [];
408
+ if (!this.check(TokenType.RightBracket)) {
409
+ elements.push(this.parseExpression());
410
+ while (this.check(TokenType.Comma)) {
411
+ this.advance();
412
+ if (this.check(TokenType.RightBracket))
413
+ break; // trailing comma
414
+ elements.push(this.parseExpression());
415
+ }
416
+ }
417
+ this.expect(TokenType.RightBracket);
418
+ return {
419
+ kind: NodeKind.ArrayLiteral,
420
+ elements,
421
+ span: this.makeSpan(start, this.previousSpan()),
422
+ };
423
+ }
424
+ parseFunctionCall(callee, start) {
425
+ this.expect(TokenType.LeftParen);
426
+ const args = [];
427
+ if (!this.check(TokenType.RightParen)) {
428
+ args.push(this.parseExpression());
429
+ while (this.check(TokenType.Comma)) {
430
+ this.advance();
431
+ args.push(this.parseExpression());
432
+ }
433
+ }
434
+ this.expect(TokenType.RightParen);
435
+ return {
436
+ kind: NodeKind.FunctionCall,
437
+ callee,
438
+ arguments: args,
439
+ span: this.makeSpan(start, this.previousSpan()),
440
+ };
441
+ }
442
+ // ---- Helpers ----
443
+ current() {
444
+ return this.tokens[this.pos] ?? this.tokens[this.tokens.length - 1];
445
+ }
446
+ previous() {
447
+ return this.tokens[this.pos - 1] ?? this.tokens[0];
448
+ }
449
+ check(type) {
450
+ return this.current().type === type;
451
+ }
452
+ advance() {
453
+ const token = this.current();
454
+ if (!this.isAtEnd()) {
455
+ this.pos++;
456
+ }
457
+ return token;
458
+ }
459
+ expect(type) {
460
+ if (this.check(type)) {
461
+ return this.advance();
462
+ }
463
+ const token = this.current();
464
+ this.error(`Expected ${type} but found '${token.value}' (${token.type}).`);
465
+ // Return a synthetic token to keep parsing
466
+ return {
467
+ type,
468
+ value: '',
469
+ span: token.span,
470
+ };
471
+ }
472
+ isAtEnd() {
473
+ return this.current().type === TokenType.EOF;
474
+ }
475
+ error(message) {
476
+ this.reporter.error(message, this.currentSpan());
477
+ }
478
+ currentSpan() {
479
+ return this.current().span;
480
+ }
481
+ previousSpan() {
482
+ return this.previous().span;
483
+ }
484
+ makeSpan(start, end) {
485
+ return { start: start.start, end: end.end };
486
+ }
487
+ // ---- Block Declaration ----
488
+ parseBlockDecl(decorators) {
489
+ const start = decorators.length > 0 ? decorators[0].span : this.currentSpan();
490
+ this.expect(TokenType.BlockKeyword);
491
+ const nameToken = this.expect(TokenType.StringLiteral);
492
+ this.expect(TokenType.LeftBrace);
493
+ let domain;
494
+ let blockType;
495
+ let metricRef;
496
+ let description;
497
+ let tags;
498
+ let owner;
499
+ let params;
500
+ let query;
501
+ let visualization;
502
+ let tests;
503
+ while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
504
+ if (this.check(TokenType.DomainKeyword)) {
505
+ this.advance();
506
+ this.expect(TokenType.Equals);
507
+ const val = this.expect(TokenType.StringLiteral);
508
+ domain = val.value;
509
+ }
510
+ else if (this.check(TokenType.TypeKeyword)) {
511
+ this.advance();
512
+ this.expect(TokenType.Equals);
513
+ const val = this.expect(TokenType.StringLiteral);
514
+ const raw = val.value;
515
+ if (raw === 'semantic' || raw === 'custom') {
516
+ blockType = raw;
517
+ }
518
+ else {
519
+ this.error(`Block type must be "semantic" or "custom", got "${raw}". Use type = "semantic" for dbt metric blocks or type = "custom" for SQL blocks.`);
520
+ blockType = 'custom'; // recover gracefully
521
+ }
522
+ }
523
+ else if (this.check(TokenType.MetricKeyword)) {
524
+ this.advance();
525
+ this.expect(TokenType.Equals);
526
+ const val = this.expect(TokenType.StringLiteral);
527
+ metricRef = val.value;
528
+ }
529
+ else if (this.check(TokenType.DescriptionKeyword)) {
530
+ this.advance();
531
+ this.expect(TokenType.Equals);
532
+ const val = this.expect(TokenType.StringLiteral);
533
+ description = val.value;
534
+ }
535
+ else if (this.check(TokenType.TagsKeyword)) {
536
+ this.advance();
537
+ this.expect(TokenType.Equals);
538
+ const arrExpr = this.parseArrayLiteral();
539
+ if (arrExpr.kind === NodeKind.ArrayLiteral) {
540
+ tags = arrExpr.elements
541
+ .filter((e) => e.kind === NodeKind.StringLiteral)
542
+ .map((e) => e.value);
543
+ }
544
+ }
545
+ else if (this.check(TokenType.OwnerKeyword)) {
546
+ this.advance();
547
+ this.expect(TokenType.Equals);
548
+ const val = this.expect(TokenType.StringLiteral);
549
+ owner = val.value;
550
+ }
551
+ else if (this.check(TokenType.ParamsKeyword)) {
552
+ params = this.parseBlockParams();
553
+ }
554
+ else if (this.check(TokenType.QueryKeyword)) {
555
+ this.advance();
556
+ this.expect(TokenType.Equals);
557
+ if (this.check(TokenType.TripleQuoteString)) {
558
+ const sqlToken = this.advance();
559
+ const interpolations = this.extractInterpolations(sqlToken.value, sqlToken.span);
560
+ query = {
561
+ kind: NodeKind.SQLQuery,
562
+ rawSQL: sqlToken.value,
563
+ interpolations,
564
+ span: sqlToken.span,
565
+ };
566
+ }
567
+ else {
568
+ // Fallback: parse as inline SQL
569
+ query = this.parseSQLQuery();
570
+ }
571
+ }
572
+ else if (this.check(TokenType.VisualizationKeyword)) {
573
+ visualization = this.parseBlockVisualization();
574
+ }
575
+ else if (this.check(TokenType.TestsKeyword)) {
576
+ tests = this.parseBlockTests();
577
+ }
578
+ else if (this.check(TokenType.RightBrace)) {
579
+ break;
580
+ }
581
+ else {
582
+ this.error(`Unexpected token '${this.current().value}' inside block. Expected 'domain', 'type', 'metric', 'description', 'tags', 'owner', 'params', 'query', 'visualization', 'tests', or '}'.`);
583
+ this.advance();
584
+ }
585
+ }
586
+ if (!blockType) {
587
+ this.error(`Block "${nameToken.value}" is missing a required type declaration. Add type = "semantic" for dbt metric blocks or type = "custom" for SQL blocks.`);
588
+ blockType = 'custom'; // error-recovery default — keeps downstream analysis working
589
+ }
590
+ this.expect(TokenType.RightBrace);
591
+ return {
592
+ kind: NodeKind.BlockDecl,
593
+ name: nameToken.value,
594
+ domain,
595
+ blockType,
596
+ metricRef,
597
+ description,
598
+ tags,
599
+ owner,
600
+ params,
601
+ query,
602
+ visualization,
603
+ tests,
604
+ decorators,
605
+ span: this.makeSpan(start, this.previousSpan()),
606
+ };
607
+ }
608
+ parseBlockParams() {
609
+ const start = this.currentSpan();
610
+ this.expect(TokenType.ParamsKeyword);
611
+ this.expect(TokenType.LeftBrace);
612
+ const paramEntries = [];
613
+ while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
614
+ const paramStart = this.currentSpan();
615
+ const nameToken = this.current();
616
+ if (nameToken.type === TokenType.Identifier || this.isBlockFieldKeyword(nameToken.type)) {
617
+ this.advance();
618
+ }
619
+ else {
620
+ this.error(`Expected parameter name, got '${nameToken.value}'.`);
621
+ this.advance();
622
+ continue;
623
+ }
624
+ this.expect(TokenType.Equals);
625
+ const initializer = this.parseExpression();
626
+ paramEntries.push({
627
+ name: nameToken.value,
628
+ initializer,
629
+ span: this.makeSpan(paramStart, this.previousSpan()),
630
+ });
631
+ }
632
+ this.expect(TokenType.RightBrace);
633
+ return {
634
+ kind: NodeKind.BlockParams,
635
+ params: paramEntries,
636
+ span: this.makeSpan(start, this.previousSpan()),
637
+ };
638
+ }
639
+ parseBlockVisualization() {
640
+ const start = this.currentSpan();
641
+ this.expect(TokenType.VisualizationKeyword);
642
+ this.expect(TokenType.LeftBrace);
643
+ const properties = [];
644
+ while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
645
+ const propStart = this.currentSpan();
646
+ const nameToken = this.current();
647
+ if (nameToken.type === TokenType.Identifier || this.isBlockFieldKeyword(nameToken.type)) {
648
+ this.advance();
649
+ }
650
+ else {
651
+ this.error(`Expected property name, got '${nameToken.value}'.`);
652
+ this.advance();
653
+ continue;
654
+ }
655
+ this.expect(TokenType.Equals);
656
+ const value = this.parseExpression();
657
+ properties.push({
658
+ kind: NodeKind.NamedArg,
659
+ name: nameToken.value,
660
+ value,
661
+ span: this.makeSpan(propStart, this.previousSpan()),
662
+ });
663
+ }
664
+ this.expect(TokenType.RightBrace);
665
+ return {
666
+ kind: NodeKind.BlockVisualization,
667
+ properties,
668
+ span: this.makeSpan(start, this.previousSpan()),
669
+ };
670
+ }
671
+ parseBlockTests() {
672
+ const start = this.currentSpan();
673
+ this.expect(TokenType.TestsKeyword);
674
+ this.expect(TokenType.LeftBrace);
675
+ const testNodes = [];
676
+ while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
677
+ if (this.check(TokenType.AssertKeyword)) {
678
+ const assertStart = this.currentSpan();
679
+ this.advance(); // consume 'assert'
680
+ const fieldToken = this.current();
681
+ if (fieldToken.type === TokenType.Identifier || this.isBlockFieldKeyword(fieldToken.type)) {
682
+ this.advance();
683
+ }
684
+ else {
685
+ this.error(`Expected field name after 'assert', got '${fieldToken.value}'.`);
686
+ this.advance();
687
+ continue;
688
+ }
689
+ // Parse operator
690
+ let operator;
691
+ if (this.check(TokenType.GreaterThan)) {
692
+ this.advance();
693
+ operator = '>';
694
+ }
695
+ else if (this.check(TokenType.LessThan)) {
696
+ this.advance();
697
+ operator = '<';
698
+ }
699
+ else if (this.check(TokenType.GreaterThanOrEqual)) {
700
+ this.advance();
701
+ operator = '>=';
702
+ }
703
+ else if (this.check(TokenType.LessThanOrEqual)) {
704
+ this.advance();
705
+ operator = '<=';
706
+ }
707
+ else if (this.check(TokenType.DoubleEquals)) {
708
+ this.advance();
709
+ operator = '==';
710
+ }
711
+ else if (this.check(TokenType.NotEquals)) {
712
+ this.advance();
713
+ operator = '!=';
714
+ }
715
+ else if (this.check(TokenType.InKeyword)) {
716
+ this.advance();
717
+ operator = 'IN';
718
+ }
719
+ else {
720
+ this.error(`Expected comparison operator (>, <, >=, <=, ==, !=, IN) after field name, got '${this.current().value}'.`);
721
+ this.advance();
722
+ continue;
723
+ }
724
+ const expected = this.parseExpression();
725
+ testNodes.push({
726
+ kind: NodeKind.BlockTest,
727
+ field: fieldToken.value,
728
+ operator,
729
+ expected,
730
+ span: this.makeSpan(assertStart, this.previousSpan()),
731
+ });
732
+ }
733
+ else if (this.check(TokenType.RightBrace)) {
734
+ break;
735
+ }
736
+ else {
737
+ this.error(`Unexpected token '${this.current().value}' inside tests. Expected 'assert' or '}'.`);
738
+ this.advance();
739
+ }
740
+ }
741
+ this.expect(TokenType.RightBrace);
742
+ return testNodes;
743
+ }
744
+ isBlockFieldKeyword(type) {
745
+ return type === TokenType.DomainKeyword
746
+ || type === TokenType.TypeKeyword
747
+ || type === TokenType.MetricKeyword
748
+ || type === TokenType.DescriptionKeyword
749
+ || type === TokenType.TagsKeyword
750
+ || type === TokenType.OwnerKeyword
751
+ || type === TokenType.ChartKeyword
752
+ || type === TokenType.QueryKeyword
753
+ || type === TokenType.DefaultKeyword
754
+ || type === TokenType.FromKeyword;
755
+ }
756
+ // ---- Workbook / Page ----
757
+ parseWorkbook(decorators) {
758
+ const start = decorators.length > 0 ? decorators[0].span : this.currentSpan();
759
+ this.expect(TokenType.WorkbookKeyword);
760
+ const titleToken = this.expect(TokenType.StringLiteral);
761
+ this.expect(TokenType.LeftBrace);
762
+ const pages = [];
763
+ while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
764
+ if (this.check(TokenType.PageKeyword)) {
765
+ pages.push(this.parsePage());
766
+ }
767
+ else if (this.check(TokenType.RightBrace)) {
768
+ break;
769
+ }
770
+ else {
771
+ this.error(`Unexpected token '${this.current().value}' inside workbook. Expected 'page' or '}'.`);
772
+ this.advance();
773
+ }
774
+ }
775
+ this.expect(TokenType.RightBrace);
776
+ return {
777
+ kind: NodeKind.Workbook,
778
+ title: titleToken.value,
779
+ decorators,
780
+ pages,
781
+ span: this.makeSpan(start, this.previousSpan()),
782
+ };
783
+ }
784
+ parsePage() {
785
+ const start = this.currentSpan();
786
+ this.expect(TokenType.PageKeyword);
787
+ const titleToken = this.expect(TokenType.StringLiteral);
788
+ this.expect(TokenType.LeftBrace);
789
+ const body = [];
790
+ while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
791
+ const itemDecorators = this.parseDecoratorList();
792
+ if (this.check(TokenType.LetKeyword)) {
793
+ if (itemDecorators.length > 0) {
794
+ this.error('Decorators cannot be applied to variable declarations.');
795
+ }
796
+ body.push(this.parseVariableDecl());
797
+ }
798
+ else if (this.check(TokenType.ParamKeyword)) {
799
+ if (itemDecorators.length > 0) {
800
+ this.error('Decorators cannot be applied to param declarations.');
801
+ }
802
+ body.push(this.parseParamDecl());
803
+ }
804
+ else if (this.check(TokenType.ChartKeyword)) {
805
+ body.push(this.parseChartCall(itemDecorators));
806
+ }
807
+ else if (this.check(TokenType.FilterKeyword)) {
808
+ if (itemDecorators.length > 0) {
809
+ this.error('Decorators cannot be applied to filter declarations.');
810
+ }
811
+ body.push(this.parseFilterCall());
812
+ }
813
+ else if (this.check(TokenType.UseKeyword)) {
814
+ if (itemDecorators.length > 0) {
815
+ this.error('Decorators cannot be applied to use declarations.');
816
+ }
817
+ body.push(this.parseUseDecl());
818
+ }
819
+ else if (this.check(TokenType.LayoutKeyword)) {
820
+ if (itemDecorators.length > 0) {
821
+ this.error('Decorators cannot be applied to layout blocks.');
822
+ }
823
+ body.push(this.parseLayoutBlock());
824
+ }
825
+ else if (this.check(TokenType.RightBrace)) {
826
+ break;
827
+ }
828
+ else {
829
+ this.error(`Unexpected token '${this.current().value}' inside page. Expected 'let', 'param', 'chart', 'filter', 'use', 'layout', or '}'.`);
830
+ this.advance();
831
+ }
832
+ }
833
+ this.expect(TokenType.RightBrace);
834
+ return {
835
+ kind: NodeKind.Page,
836
+ title: titleToken.value,
837
+ body,
838
+ span: this.makeSpan(start, this.previousSpan()),
839
+ };
840
+ }
841
+ // ---- Param Declaration ----
842
+ parseParamDecl() {
843
+ const start = this.currentSpan();
844
+ this.expect(TokenType.ParamKeyword);
845
+ const nameToken = this.expect(TokenType.Identifier);
846
+ // Optional type annotation: param name: type
847
+ let paramType = 'string';
848
+ if (this.check(TokenType.ColonToken)) {
849
+ this.advance(); // consume ':'
850
+ const typeToken = this.expect(TokenType.Identifier);
851
+ const validTypes = ['string', 'number', 'boolean', 'date'];
852
+ if (validTypes.includes(typeToken.value)) {
853
+ paramType = typeToken.value;
854
+ }
855
+ else {
856
+ this.reporter.error(`Invalid param type '${typeToken.value}'. Valid types: ${validTypes.join(', ')}`, typeToken.span);
857
+ }
858
+ }
859
+ // Optional default value: = expression
860
+ let defaultValue;
861
+ if (this.check(TokenType.Equals)) {
862
+ this.advance(); // consume '='
863
+ defaultValue = this.parseExpression();
864
+ }
865
+ return {
866
+ kind: NodeKind.ParamDecl,
867
+ name: nameToken.value,
868
+ paramType,
869
+ defaultValue,
870
+ span: this.makeSpan(start, this.previousSpan()),
871
+ };
872
+ }
873
+ // ---- Import / Use ----
874
+ parseImportDecl() {
875
+ const start = this.currentSpan();
876
+ this.expect(TokenType.ImportKeyword);
877
+ // import { name1, name2 } from "path"
878
+ this.expect(TokenType.LeftBrace);
879
+ const names = [];
880
+ if (!this.check(TokenType.RightBrace)) {
881
+ names.push(this.expect(TokenType.Identifier).value);
882
+ while (this.check(TokenType.Comma)) {
883
+ this.advance();
884
+ if (this.check(TokenType.RightBrace))
885
+ break; // trailing comma
886
+ names.push(this.expect(TokenType.Identifier).value);
887
+ }
888
+ }
889
+ this.expect(TokenType.RightBrace);
890
+ this.expect(TokenType.FromKeyword);
891
+ const pathToken = this.expect(TokenType.StringLiteral);
892
+ return {
893
+ kind: NodeKind.ImportDecl,
894
+ names,
895
+ path: pathToken.value,
896
+ span: this.makeSpan(start, this.previousSpan()),
897
+ };
898
+ }
899
+ parseUseDecl() {
900
+ const start = this.currentSpan();
901
+ this.expect(TokenType.UseKeyword);
902
+ let nameToken;
903
+ if (this.check(TokenType.Identifier) || this.check(TokenType.StringLiteral)) {
904
+ nameToken = this.advance();
905
+ }
906
+ else {
907
+ nameToken = this.expect(TokenType.Identifier);
908
+ }
909
+ return {
910
+ kind: NodeKind.UseDecl,
911
+ name: nameToken.value,
912
+ span: this.makeSpan(start, this.previousSpan()),
913
+ };
914
+ }
915
+ // ---- Layout ----
916
+ parseLayoutBlock() {
917
+ const start = this.currentSpan();
918
+ this.expect(TokenType.LayoutKeyword);
919
+ // Optional: layout(columns = 12)
920
+ let columns = 12;
921
+ if (this.check(TokenType.LeftParen)) {
922
+ this.advance();
923
+ const args = [];
924
+ if (!this.check(TokenType.RightParen)) {
925
+ args.push(this.parseNamedArg());
926
+ while (this.check(TokenType.Comma)) {
927
+ this.advance();
928
+ if (this.check(TokenType.RightParen))
929
+ break;
930
+ args.push(this.parseNamedArg());
931
+ }
932
+ }
933
+ this.expect(TokenType.RightParen);
934
+ const colArg = args.find((a) => a.name === 'columns');
935
+ if (colArg && colArg.value.kind === NodeKind.NumberLiteral) {
936
+ columns = colArg.value.value;
937
+ }
938
+ }
939
+ this.expect(TokenType.LeftBrace);
940
+ const rows = [];
941
+ while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
942
+ if (this.check(TokenType.RowKeyword)) {
943
+ rows.push(this.parseLayoutRow());
944
+ }
945
+ else if (this.check(TokenType.RightBrace)) {
946
+ break;
947
+ }
948
+ else {
949
+ this.error(`Unexpected token '${this.current().value}' inside layout. Expected 'row' or '}'.`);
950
+ this.advance();
951
+ }
952
+ }
953
+ this.expect(TokenType.RightBrace);
954
+ return {
955
+ kind: NodeKind.LayoutBlock,
956
+ columns,
957
+ rows,
958
+ span: this.makeSpan(start, this.previousSpan()),
959
+ };
960
+ }
961
+ parseLayoutRow() {
962
+ const start = this.currentSpan();
963
+ this.expect(TokenType.RowKeyword);
964
+ this.expect(TokenType.LeftBrace);
965
+ const items = [];
966
+ while (!this.check(TokenType.RightBrace) && !this.isAtEnd()) {
967
+ const itemDecorators = this.parseDecoratorList();
968
+ let node;
969
+ if (this.check(TokenType.ChartKeyword)) {
970
+ node = this.parseChartCall(itemDecorators);
971
+ }
972
+ else if (this.check(TokenType.FilterKeyword)) {
973
+ if (itemDecorators.length > 0) {
974
+ this.error('Decorators cannot be applied to filter declarations.');
975
+ }
976
+ node = this.parseFilterCall();
977
+ }
978
+ else if (this.check(TokenType.RightBrace)) {
979
+ break;
980
+ }
981
+ else {
982
+ this.error(`Unexpected token '${this.current().value}' inside row. Expected 'chart', 'filter', or '}'.`);
983
+ this.advance();
984
+ continue;
985
+ }
986
+ // Check for optional 'span N' after the chart/filter call
987
+ let span;
988
+ if (this.check(TokenType.Identifier) && this.current().value === 'span') {
989
+ this.advance(); // consume 'span'
990
+ const spanToken = this.expect(TokenType.NumberLiteral);
991
+ span = Number(spanToken.value);
992
+ }
993
+ items.push({ node, span });
994
+ }
995
+ this.expect(TokenType.RightBrace);
996
+ return {
997
+ kind: NodeKind.LayoutRow,
998
+ items,
999
+ span: this.makeSpan(start, this.previousSpan()),
1000
+ };
1001
+ }
1002
+ // ---- Helpers ----
1003
+ synchronize() {
1004
+ while (!this.isAtEnd()) {
1005
+ const token = this.current();
1006
+ if (token.type === TokenType.DashboardKeyword ||
1007
+ token.type === TokenType.WorkbookKeyword ||
1008
+ token.type === TokenType.ChartKeyword ||
1009
+ token.type === TokenType.FilterKeyword ||
1010
+ token.type === TokenType.ImportKeyword ||
1011
+ token.type === TokenType.BlockKeyword ||
1012
+ token.type === TokenType.AtSign ||
1013
+ token.type === TokenType.RightBrace) {
1014
+ return;
1015
+ }
1016
+ this.advance();
1017
+ }
1018
+ }
1019
+ }
1020
+ export function parse(source, file) {
1021
+ const parser = new Parser(source, file);
1022
+ return parser.parse();
1023
+ }
1024
+ //# sourceMappingURL=parser.js.map