@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.
Files changed (40) hide show
  1. package/__tests__/engine/dsl/GraphDebugTest.ts +316 -0
  2. package/__tests__/engine/runtime/expression/ExpressionParsingTest.ts +402 -14
  3. package/dist/index.js +15 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/module.js +15 -1
  6. package/dist/module.js.map +1 -1
  7. package/dist/types.d.ts +416 -0
  8. package/dist/types.d.ts.map +1 -1
  9. package/package.json +1 -1
  10. package/src/engine/dsl/DSLCompiler.ts +104 -0
  11. package/src/engine/dsl/index.ts +30 -0
  12. package/src/engine/dsl/lexer/DSLLexer.ts +518 -0
  13. package/src/engine/dsl/lexer/DSLToken.ts +74 -0
  14. package/src/engine/dsl/lexer/Keywords.ts +80 -0
  15. package/src/engine/dsl/lexer/LexerError.ts +37 -0
  16. package/src/engine/dsl/monaco/DSLFunctionProvider.ts +187 -0
  17. package/src/engine/dsl/parser/DSLParser.ts +1075 -0
  18. package/src/engine/dsl/parser/DSLParserError.ts +29 -0
  19. package/src/engine/dsl/parser/ast/ASTNode.ts +23 -0
  20. package/src/engine/dsl/parser/ast/ArgumentNode.ts +43 -0
  21. package/src/engine/dsl/parser/ast/ComplexValueNode.ts +22 -0
  22. package/src/engine/dsl/parser/ast/EventDeclNode.ts +27 -0
  23. package/src/engine/dsl/parser/ast/ExpressionNode.ts +33 -0
  24. package/src/engine/dsl/parser/ast/FunctionCallNode.ts +29 -0
  25. package/src/engine/dsl/parser/ast/FunctionDefNode.ts +37 -0
  26. package/src/engine/dsl/parser/ast/ParameterDeclNode.ts +25 -0
  27. package/src/engine/dsl/parser/ast/SchemaLiteralNode.ts +26 -0
  28. package/src/engine/dsl/parser/ast/SchemaNode.ts +23 -0
  29. package/src/engine/dsl/parser/ast/StatementNode.ts +41 -0
  30. package/src/engine/dsl/parser/ast/index.ts +14 -0
  31. package/src/engine/dsl/transformer/ASTToJSON.ts +378 -0
  32. package/src/engine/dsl/transformer/ExpressionHandler.ts +48 -0
  33. package/src/engine/dsl/transformer/JSONToText.ts +694 -0
  34. package/src/engine/dsl/transformer/SchemaTransformer.ts +110 -0
  35. package/src/engine/model/FunctionDefinition.ts +23 -0
  36. package/src/engine/runtime/expression/Expression.ts +5 -1
  37. package/src/engine/runtime/expression/ExpressionEvaluator.ts +152 -139
  38. package/src/engine/runtime/expression/ExpressionParser.ts +80 -27
  39. package/src/engine/util/duplicate.ts +3 -1
  40. 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
+ }