@fincity/kirun-js 3.1.4 → 3.2.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/__tests__/engine/dsl/GraphDebugTest.ts +316 -0
- package/__tests__/engine/runtime/expression/ExpressionParsingTest.ts +402 -14
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/module.js +15 -1
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +416 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/engine/dsl/DSLCompiler.ts +104 -0
- package/src/engine/dsl/index.ts +30 -0
- package/src/engine/dsl/lexer/DSLLexer.ts +518 -0
- package/src/engine/dsl/lexer/DSLToken.ts +74 -0
- package/src/engine/dsl/lexer/Keywords.ts +80 -0
- package/src/engine/dsl/lexer/LexerError.ts +37 -0
- package/src/engine/dsl/monaco/DSLFunctionProvider.ts +187 -0
- package/src/engine/dsl/parser/DSLParser.ts +1075 -0
- package/src/engine/dsl/parser/DSLParserError.ts +29 -0
- package/src/engine/dsl/parser/ast/ASTNode.ts +23 -0
- package/src/engine/dsl/parser/ast/ArgumentNode.ts +43 -0
- package/src/engine/dsl/parser/ast/ComplexValueNode.ts +22 -0
- package/src/engine/dsl/parser/ast/EventDeclNode.ts +27 -0
- package/src/engine/dsl/parser/ast/ExpressionNode.ts +33 -0
- package/src/engine/dsl/parser/ast/FunctionCallNode.ts +29 -0
- package/src/engine/dsl/parser/ast/FunctionDefNode.ts +37 -0
- package/src/engine/dsl/parser/ast/ParameterDeclNode.ts +25 -0
- package/src/engine/dsl/parser/ast/SchemaLiteralNode.ts +26 -0
- package/src/engine/dsl/parser/ast/SchemaNode.ts +23 -0
- package/src/engine/dsl/parser/ast/StatementNode.ts +41 -0
- package/src/engine/dsl/parser/ast/index.ts +14 -0
- package/src/engine/dsl/transformer/ASTToJSON.ts +378 -0
- package/src/engine/dsl/transformer/ExpressionHandler.ts +48 -0
- package/src/engine/dsl/transformer/JSONToText.ts +694 -0
- package/src/engine/dsl/transformer/SchemaTransformer.ts +110 -0
- package/src/engine/model/FunctionDefinition.ts +23 -0
- package/src/engine/runtime/expression/Expression.ts +5 -1
- package/src/engine/runtime/expression/ExpressionEvaluator.ts +152 -139
- package/src/engine/runtime/expression/ExpressionParser.ts +80 -27
- package/src/engine/util/duplicate.ts +3 -1
- package/src/index.ts +1 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { DSLToken, DSLTokenType, SourceLocation } from './DSLToken';
|
|
2
|
+
import { isKeyword } from './Keywords';
|
|
3
|
+
import { LexerError } from './LexerError';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DSL Lexer - Tokenizes DSL text into tokens
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Keyword recognition
|
|
10
|
+
* - String literals with escape sequences
|
|
11
|
+
* - Number literals (integers and floats)
|
|
12
|
+
* - Comment stripping (single-line and block comments)
|
|
13
|
+
* - Position tracking for error messages
|
|
14
|
+
*/
|
|
15
|
+
export class DSLLexer {
|
|
16
|
+
private input: string;
|
|
17
|
+
private pos: number = 0;
|
|
18
|
+
private line: number = 1;
|
|
19
|
+
private column: number = 1;
|
|
20
|
+
private tokens: DSLToken[] = [];
|
|
21
|
+
|
|
22
|
+
constructor(input: string) {
|
|
23
|
+
this.input = input;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Tokenize the input string
|
|
28
|
+
*/
|
|
29
|
+
public tokenize(): DSLToken[] {
|
|
30
|
+
while (this.pos < this.input.length) {
|
|
31
|
+
this.skipWhitespace();
|
|
32
|
+
|
|
33
|
+
if (this.pos >= this.input.length) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Try to read a token
|
|
38
|
+
const token = this.readToken();
|
|
39
|
+
if (token) {
|
|
40
|
+
// Skip whitespace tokens but keep comments for attachment to statements
|
|
41
|
+
if (token.type !== DSLTokenType.WHITESPACE) {
|
|
42
|
+
this.tokens.push(token);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Add EOF token
|
|
48
|
+
this.tokens.push(
|
|
49
|
+
new DSLToken(
|
|
50
|
+
DSLTokenType.EOF,
|
|
51
|
+
'',
|
|
52
|
+
new SourceLocation(this.line, this.column, this.pos, this.pos),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return this.tokens;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Skip whitespace (spaces, tabs, newlines)
|
|
61
|
+
*/
|
|
62
|
+
private skipWhitespace(): void {
|
|
63
|
+
while (this.pos < this.input.length) {
|
|
64
|
+
const ch = this.input[this.pos];
|
|
65
|
+
if (ch === ' ' || ch === '\t' || ch === '\r') {
|
|
66
|
+
this.advance();
|
|
67
|
+
} else if (ch === '\n') {
|
|
68
|
+
this.advance();
|
|
69
|
+
this.line++;
|
|
70
|
+
this.column = 1;
|
|
71
|
+
} else {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read a single token
|
|
79
|
+
*/
|
|
80
|
+
private readToken(): DSLToken | null {
|
|
81
|
+
const startPos = this.pos;
|
|
82
|
+
const startLine = this.line;
|
|
83
|
+
const startColumn = this.column;
|
|
84
|
+
|
|
85
|
+
const ch = this.peek();
|
|
86
|
+
|
|
87
|
+
// Block comments only (// is not supported as it conflicts with integer division in expressions)
|
|
88
|
+
if (ch === '/' && this.peekAhead(1) === '*') {
|
|
89
|
+
return this.readBlockComment();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// String literals
|
|
93
|
+
if (ch === '"' || ch === "'") {
|
|
94
|
+
return this.readStringLiteral(ch);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Backtick strings (for expressions)
|
|
98
|
+
if (ch === '`') {
|
|
99
|
+
return this.readBacktickString();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Numbers
|
|
103
|
+
if (this.isDigit(ch) || (ch === '-' && this.isDigit(this.peekAhead(1)))) {
|
|
104
|
+
return this.readNumber();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Identifiers and keywords
|
|
108
|
+
if (this.isIdentifierStart(ch)) {
|
|
109
|
+
return this.readIdentifier();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Single-character tokens
|
|
113
|
+
const singleChar = this.readSingleCharToken();
|
|
114
|
+
if (singleChar) {
|
|
115
|
+
return singleChar;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Unknown character
|
|
119
|
+
throw new LexerError(
|
|
120
|
+
`Unexpected character '${ch}'`,
|
|
121
|
+
new SourceLocation(startLine, startColumn, startPos, this.pos),
|
|
122
|
+
this.input,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Read single-character or multi-character tokens
|
|
128
|
+
*/
|
|
129
|
+
private readSingleCharToken(): DSLToken | null {
|
|
130
|
+
const startPos = this.pos;
|
|
131
|
+
const startLine = this.line;
|
|
132
|
+
const startColumn = this.column;
|
|
133
|
+
const ch = this.peek();
|
|
134
|
+
|
|
135
|
+
// Check for multi-character operators first
|
|
136
|
+
const multiCharOp = this.tryReadMultiCharOperator();
|
|
137
|
+
if (multiCharOp) {
|
|
138
|
+
return new DSLToken(
|
|
139
|
+
DSLTokenType.OPERATOR,
|
|
140
|
+
multiCharOp,
|
|
141
|
+
new SourceLocation(startLine, startColumn, startPos, this.pos),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Advance past single character
|
|
146
|
+
this.advance();
|
|
147
|
+
let type: DSLTokenType | null = null;
|
|
148
|
+
|
|
149
|
+
switch (ch) {
|
|
150
|
+
case ':':
|
|
151
|
+
type = DSLTokenType.COLON;
|
|
152
|
+
break;
|
|
153
|
+
case ',':
|
|
154
|
+
type = DSLTokenType.COMMA;
|
|
155
|
+
break;
|
|
156
|
+
case '.':
|
|
157
|
+
type = DSLTokenType.DOT;
|
|
158
|
+
break;
|
|
159
|
+
case '=':
|
|
160
|
+
type = DSLTokenType.EQUALS;
|
|
161
|
+
break;
|
|
162
|
+
case '(':
|
|
163
|
+
type = DSLTokenType.LEFT_PAREN;
|
|
164
|
+
break;
|
|
165
|
+
case ')':
|
|
166
|
+
type = DSLTokenType.RIGHT_PAREN;
|
|
167
|
+
break;
|
|
168
|
+
case '{':
|
|
169
|
+
type = DSLTokenType.LEFT_BRACE;
|
|
170
|
+
break;
|
|
171
|
+
case '}':
|
|
172
|
+
type = DSLTokenType.RIGHT_BRACE;
|
|
173
|
+
break;
|
|
174
|
+
case '[':
|
|
175
|
+
type = DSLTokenType.LEFT_BRACKET;
|
|
176
|
+
break;
|
|
177
|
+
case ']':
|
|
178
|
+
type = DSLTokenType.RIGHT_BRACKET;
|
|
179
|
+
break;
|
|
180
|
+
// Operators for expressions (passed through to KIRun expression parser)
|
|
181
|
+
case '+':
|
|
182
|
+
case '-':
|
|
183
|
+
case '*':
|
|
184
|
+
case '/':
|
|
185
|
+
case '%':
|
|
186
|
+
case '<':
|
|
187
|
+
case '>':
|
|
188
|
+
case '!':
|
|
189
|
+
case '?':
|
|
190
|
+
case '&':
|
|
191
|
+
case '|':
|
|
192
|
+
case '@':
|
|
193
|
+
case '^':
|
|
194
|
+
case '~':
|
|
195
|
+
case '#': // Color values like #FFFFFF
|
|
196
|
+
case '\\': // Escape sequences in regex
|
|
197
|
+
type = DSLTokenType.OPERATOR;
|
|
198
|
+
break;
|
|
199
|
+
default:
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return new DSLToken(
|
|
204
|
+
type,
|
|
205
|
+
ch,
|
|
206
|
+
new SourceLocation(startLine, startColumn, startPos, this.pos),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Try to read multi-character operators like !=, ==, <=, >=, &&, ||, ??
|
|
212
|
+
*/
|
|
213
|
+
private tryReadMultiCharOperator(): string | null {
|
|
214
|
+
const ch = this.peek();
|
|
215
|
+
const next = this.peekAhead(1);
|
|
216
|
+
|
|
217
|
+
// Two-character operators
|
|
218
|
+
const twoCharOps = ['!=', '==', '<=', '>=', '&&', '||', '??', '?.', '++', '--', '+=', '-=', '*=', '/='];
|
|
219
|
+
const twoChar = ch + next;
|
|
220
|
+
if (twoCharOps.includes(twoChar)) {
|
|
221
|
+
this.advance();
|
|
222
|
+
this.advance();
|
|
223
|
+
return twoChar;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Read identifier or keyword
|
|
231
|
+
*/
|
|
232
|
+
private readIdentifier(): DSLToken {
|
|
233
|
+
const startPos = this.pos;
|
|
234
|
+
const startLine = this.line;
|
|
235
|
+
const startColumn = this.column;
|
|
236
|
+
let value = '';
|
|
237
|
+
|
|
238
|
+
while (this.pos < this.input.length) {
|
|
239
|
+
const ch = this.peek();
|
|
240
|
+
if (this.isIdentifierPart(ch)) {
|
|
241
|
+
value += this.advance();
|
|
242
|
+
} else {
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const location = new SourceLocation(startLine, startColumn, startPos, this.pos);
|
|
248
|
+
|
|
249
|
+
// Check if it's a keyword
|
|
250
|
+
if (isKeyword(value)) {
|
|
251
|
+
return new DSLToken(DSLTokenType.KEYWORD, value, location);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check if it's a boolean literal
|
|
255
|
+
if (value === 'true' || value === 'false') {
|
|
256
|
+
return new DSLToken(DSLTokenType.BOOLEAN, value, location);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check if it's null
|
|
260
|
+
if (value === 'null') {
|
|
261
|
+
return new DSLToken(DSLTokenType.NULL, value, location);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Otherwise, it's an identifier
|
|
265
|
+
return new DSLToken(DSLTokenType.IDENTIFIER, value, location);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Read number literal (integer or float)
|
|
270
|
+
*/
|
|
271
|
+
private readNumber(): DSLToken {
|
|
272
|
+
const startPos = this.pos;
|
|
273
|
+
const startLine = this.line;
|
|
274
|
+
const startColumn = this.column;
|
|
275
|
+
let value = '';
|
|
276
|
+
|
|
277
|
+
// Handle negative numbers
|
|
278
|
+
if (this.peek() === '-') {
|
|
279
|
+
value += this.advance();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Read integer part
|
|
283
|
+
while (this.pos < this.input.length && this.isDigit(this.peek())) {
|
|
284
|
+
value += this.advance();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check for decimal point
|
|
288
|
+
if (this.peek() === '.' && this.isDigit(this.peekAhead(1))) {
|
|
289
|
+
value += this.advance(); // consume '.'
|
|
290
|
+
while (this.pos < this.input.length && this.isDigit(this.peek())) {
|
|
291
|
+
value += this.advance();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return new DSLToken(
|
|
296
|
+
DSLTokenType.NUMBER,
|
|
297
|
+
value,
|
|
298
|
+
new SourceLocation(startLine, startColumn, startPos, this.pos),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Read string literal with escape sequences
|
|
304
|
+
*/
|
|
305
|
+
private readStringLiteral(quoteChar: string): DSLToken {
|
|
306
|
+
const startPos = this.pos;
|
|
307
|
+
const startLine = this.line;
|
|
308
|
+
const startColumn = this.column;
|
|
309
|
+
let value = '';
|
|
310
|
+
|
|
311
|
+
// Consume opening quote
|
|
312
|
+
this.advance();
|
|
313
|
+
value += quoteChar;
|
|
314
|
+
|
|
315
|
+
while (this.pos < this.input.length) {
|
|
316
|
+
const ch = this.peek();
|
|
317
|
+
|
|
318
|
+
// End of string
|
|
319
|
+
if (ch === quoteChar) {
|
|
320
|
+
value += this.advance();
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Escape sequences
|
|
325
|
+
if (ch === '\\') {
|
|
326
|
+
value += this.advance(); // backslash
|
|
327
|
+
if (this.pos < this.input.length) {
|
|
328
|
+
value += this.advance(); // escaped character
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Regular character
|
|
334
|
+
value += this.advance();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check if string was closed
|
|
338
|
+
if (!value.endsWith(quoteChar)) {
|
|
339
|
+
throw new LexerError(
|
|
340
|
+
`Unterminated string literal`,
|
|
341
|
+
new SourceLocation(startLine, startColumn, startPos, this.pos),
|
|
342
|
+
this.input,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return new DSLToken(
|
|
347
|
+
DSLTokenType.STRING,
|
|
348
|
+
value,
|
|
349
|
+
new SourceLocation(startLine, startColumn, startPos, this.pos),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Read backtick string literal (used for expressions)
|
|
355
|
+
* Content between backticks is treated as an expression
|
|
356
|
+
* Only \` (escaped backtick) and \\ (escaped backslash) are special
|
|
357
|
+
* All other escape sequences are passed through as-is
|
|
358
|
+
*/
|
|
359
|
+
private readBacktickString(): DSLToken {
|
|
360
|
+
const startPos = this.pos;
|
|
361
|
+
const startLine = this.line;
|
|
362
|
+
const startColumn = this.column;
|
|
363
|
+
let value = '';
|
|
364
|
+
|
|
365
|
+
// Consume opening backtick
|
|
366
|
+
this.advance();
|
|
367
|
+
|
|
368
|
+
while (this.pos < this.input.length) {
|
|
369
|
+
const ch = this.peek();
|
|
370
|
+
|
|
371
|
+
// End of string
|
|
372
|
+
if (ch === '`') {
|
|
373
|
+
this.advance();
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Escape sequences - only handle \` and \\
|
|
378
|
+
if (ch === '\\') {
|
|
379
|
+
const next = this.peekAhead(1);
|
|
380
|
+
if (next === '`' || next === '\\') {
|
|
381
|
+
// Consume backslash and add the escaped character
|
|
382
|
+
this.advance();
|
|
383
|
+
value += this.advance();
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
// For all other escapes, keep the backslash
|
|
387
|
+
value += this.advance();
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Regular character
|
|
392
|
+
value += this.advance();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return new DSLToken(
|
|
396
|
+
DSLTokenType.BACKTICK_STRING,
|
|
397
|
+
value,
|
|
398
|
+
new SourceLocation(startLine, startColumn, startPos, this.pos),
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Read single-line comment // ...
|
|
404
|
+
*/
|
|
405
|
+
private readLineComment(): DSLToken {
|
|
406
|
+
const startPos = this.pos;
|
|
407
|
+
const startLine = this.line;
|
|
408
|
+
const startColumn = this.column;
|
|
409
|
+
let value = '';
|
|
410
|
+
|
|
411
|
+
// Consume //
|
|
412
|
+
value += this.advance();
|
|
413
|
+
value += this.advance();
|
|
414
|
+
|
|
415
|
+
// Read until end of line
|
|
416
|
+
while (this.pos < this.input.length && this.peek() !== '\n') {
|
|
417
|
+
value += this.advance();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return new DSLToken(
|
|
421
|
+
DSLTokenType.COMMENT,
|
|
422
|
+
value,
|
|
423
|
+
new SourceLocation(startLine, startColumn, startPos, this.pos),
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Read block comment /* ... *\/
|
|
429
|
+
*/
|
|
430
|
+
private readBlockComment(): DSLToken {
|
|
431
|
+
const startPos = this.pos;
|
|
432
|
+
const startLine = this.line;
|
|
433
|
+
const startColumn = this.column;
|
|
434
|
+
let value = '';
|
|
435
|
+
|
|
436
|
+
// Consume /*
|
|
437
|
+
value += this.advance();
|
|
438
|
+
value += this.advance();
|
|
439
|
+
|
|
440
|
+
// Read until */
|
|
441
|
+
while (this.pos < this.input.length) {
|
|
442
|
+
if (this.peek() === '*' && this.peekAhead(1) === '/') {
|
|
443
|
+
value += this.advance(); // *
|
|
444
|
+
value += this.advance(); // /
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const ch = this.advance();
|
|
449
|
+
value += ch;
|
|
450
|
+
|
|
451
|
+
// Track line numbers
|
|
452
|
+
if (ch === '\n') {
|
|
453
|
+
this.line++;
|
|
454
|
+
this.column = 1;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Check if comment was closed
|
|
459
|
+
if (!value.endsWith('*/')) {
|
|
460
|
+
throw new LexerError(
|
|
461
|
+
`Unterminated block comment`,
|
|
462
|
+
new SourceLocation(startLine, startColumn, startPos, this.pos),
|
|
463
|
+
this.input,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return new DSLToken(
|
|
468
|
+
DSLTokenType.COMMENT,
|
|
469
|
+
value,
|
|
470
|
+
new SourceLocation(startLine, startColumn, startPos, this.pos),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Helper: peek at current character
|
|
476
|
+
*/
|
|
477
|
+
private peek(): string {
|
|
478
|
+
return this.input[this.pos] || '';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Helper: peek ahead n characters
|
|
483
|
+
*/
|
|
484
|
+
private peekAhead(n: number): string {
|
|
485
|
+
return this.input[this.pos + n] || '';
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Helper: advance position and return current character
|
|
490
|
+
*/
|
|
491
|
+
private advance(): string {
|
|
492
|
+
const ch = this.input[this.pos];
|
|
493
|
+
this.pos++;
|
|
494
|
+
this.column++;
|
|
495
|
+
return ch;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Helper: check if character is a digit
|
|
500
|
+
*/
|
|
501
|
+
private isDigit(ch: string): boolean {
|
|
502
|
+
return ch >= '0' && ch <= '9';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Helper: check if character can start an identifier
|
|
507
|
+
*/
|
|
508
|
+
private isIdentifierStart(ch: string): boolean {
|
|
509
|
+
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_';
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Helper: check if character can be part of an identifier
|
|
514
|
+
*/
|
|
515
|
+
private isIdentifierPart(ch: string): boolean {
|
|
516
|
+
return this.isIdentifierStart(ch) || this.isDigit(ch);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token types for DSL lexer
|
|
3
|
+
*/
|
|
4
|
+
export enum DSLTokenType {
|
|
5
|
+
// Keywords
|
|
6
|
+
KEYWORD = 'KEYWORD',
|
|
7
|
+
|
|
8
|
+
// Identifiers and literals
|
|
9
|
+
IDENTIFIER = 'IDENTIFIER',
|
|
10
|
+
NUMBER = 'NUMBER',
|
|
11
|
+
STRING = 'STRING',
|
|
12
|
+
BACKTICK_STRING = 'BACKTICK_STRING', // `...` for expressions
|
|
13
|
+
BOOLEAN = 'BOOLEAN',
|
|
14
|
+
NULL = 'NULL',
|
|
15
|
+
|
|
16
|
+
// Operators and delimiters
|
|
17
|
+
COLON = 'COLON', // :
|
|
18
|
+
COMMA = 'COMMA', // ,
|
|
19
|
+
DOT = 'DOT', // .
|
|
20
|
+
EQUALS = 'EQUALS', // =
|
|
21
|
+
OPERATOR = 'OPERATOR', // + - * / % < > ! ? & | @ ^ ~
|
|
22
|
+
|
|
23
|
+
// Brackets and parentheses
|
|
24
|
+
LEFT_PAREN = 'LEFT_PAREN', // (
|
|
25
|
+
RIGHT_PAREN = 'RIGHT_PAREN', // )
|
|
26
|
+
LEFT_BRACE = 'LEFT_BRACE', // {
|
|
27
|
+
RIGHT_BRACE = 'RIGHT_BRACE', // }
|
|
28
|
+
LEFT_BRACKET = 'LEFT_BRACKET', // [
|
|
29
|
+
RIGHT_BRACKET = 'RIGHT_BRACKET', // ]
|
|
30
|
+
|
|
31
|
+
// Special
|
|
32
|
+
NEWLINE = 'NEWLINE',
|
|
33
|
+
COMMENT = 'COMMENT',
|
|
34
|
+
WHITESPACE = 'WHITESPACE',
|
|
35
|
+
EOF = 'EOF',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Position information for tokens
|
|
40
|
+
*/
|
|
41
|
+
export class SourceLocation {
|
|
42
|
+
constructor(
|
|
43
|
+
public line: number,
|
|
44
|
+
public column: number,
|
|
45
|
+
public startPos: number,
|
|
46
|
+
public endPos: number,
|
|
47
|
+
) {}
|
|
48
|
+
|
|
49
|
+
public toString(): string {
|
|
50
|
+
return `Line ${this.line}, Column ${this.column} (pos ${this.startPos}-${this.endPos})`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Token class with value and position tracking
|
|
56
|
+
*/
|
|
57
|
+
export class DSLToken {
|
|
58
|
+
constructor(
|
|
59
|
+
public type: DSLTokenType,
|
|
60
|
+
public value: string,
|
|
61
|
+
public location: SourceLocation,
|
|
62
|
+
) {}
|
|
63
|
+
|
|
64
|
+
public toString(): string {
|
|
65
|
+
return `${this.type}(${JSON.stringify(this.value)}) at ${this.location}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public is(type: DSLTokenType, value?: string): boolean {
|
|
69
|
+
if (value !== undefined) {
|
|
70
|
+
return this.type === type && this.value === value;
|
|
71
|
+
}
|
|
72
|
+
return this.type === type;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSL Keywords and helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const KEYWORDS = new Set([
|
|
6
|
+
// Function structure keywords
|
|
7
|
+
'FUNCTION',
|
|
8
|
+
'NAMESPACE',
|
|
9
|
+
'PARAMETERS',
|
|
10
|
+
'EVENTS',
|
|
11
|
+
'LOGIC',
|
|
12
|
+
|
|
13
|
+
// Type and schema keywords
|
|
14
|
+
'AS',
|
|
15
|
+
'OF',
|
|
16
|
+
'WITH',
|
|
17
|
+
'DEFAULT',
|
|
18
|
+
'VALUE',
|
|
19
|
+
|
|
20
|
+
// Control flow modifiers
|
|
21
|
+
'AFTER',
|
|
22
|
+
'IF',
|
|
23
|
+
|
|
24
|
+
// Primitive types
|
|
25
|
+
'INTEGER',
|
|
26
|
+
'LONG',
|
|
27
|
+
'FLOAT',
|
|
28
|
+
'DOUBLE',
|
|
29
|
+
'STRING',
|
|
30
|
+
'BOOLEAN',
|
|
31
|
+
'NULL',
|
|
32
|
+
'ANY',
|
|
33
|
+
'ARRAY',
|
|
34
|
+
'OBJECT',
|
|
35
|
+
|
|
36
|
+
// Boolean literals (also keywords)
|
|
37
|
+
'true',
|
|
38
|
+
'false',
|
|
39
|
+
'null',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export const BLOCK_NAMES = new Set([
|
|
43
|
+
'iteration',
|
|
44
|
+
'true',
|
|
45
|
+
'false',
|
|
46
|
+
'output',
|
|
47
|
+
'error',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export const PRIMITIVE_TYPES = new Set([
|
|
51
|
+
'INTEGER',
|
|
52
|
+
'LONG',
|
|
53
|
+
'FLOAT',
|
|
54
|
+
'DOUBLE',
|
|
55
|
+
'STRING',
|
|
56
|
+
'BOOLEAN',
|
|
57
|
+
'NULL',
|
|
58
|
+
'ANY',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if a string is a keyword
|
|
63
|
+
*/
|
|
64
|
+
export function isKeyword(str: string): boolean {
|
|
65
|
+
return KEYWORDS.has(str);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if a string is a block name
|
|
70
|
+
*/
|
|
71
|
+
export function isBlockName(str: string): boolean {
|
|
72
|
+
return BLOCK_NAMES.has(str);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a string is a primitive type
|
|
77
|
+
*/
|
|
78
|
+
export function isPrimitiveType(str: string): boolean {
|
|
79
|
+
return PRIMITIVE_TYPES.has(str);
|
|
80
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SourceLocation } from './DSLToken';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lexer error with position information
|
|
5
|
+
*/
|
|
6
|
+
export class LexerError extends Error {
|
|
7
|
+
constructor(
|
|
8
|
+
message: string,
|
|
9
|
+
public location?: SourceLocation,
|
|
10
|
+
public context?: string,
|
|
11
|
+
) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'LexerError';
|
|
14
|
+
|
|
15
|
+
// Format error message with location if available
|
|
16
|
+
if (location) {
|
|
17
|
+
this.message = `${message} at ${location}`;
|
|
18
|
+
if (context) {
|
|
19
|
+
this.message += `\n\n${this.formatContext(context, location)}`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private formatContext(input: string, location: SourceLocation): string {
|
|
25
|
+
const lines = input.split('\n');
|
|
26
|
+
const lineIndex = location.line - 1;
|
|
27
|
+
|
|
28
|
+
if (lineIndex < 0 || lineIndex >= lines.length) {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const line = lines[lineIndex];
|
|
33
|
+
const pointer = ' '.repeat(location.column - 1) + '^';
|
|
34
|
+
|
|
35
|
+
return `${line}\n${pointer}`;
|
|
36
|
+
}
|
|
37
|
+
}
|