septima-lang 0.0.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.
- package/dist/src/ast-node.d.ts +85 -0
- package/dist/src/ast-node.js +114 -0
- package/dist/src/cdl.d.ts +33 -0
- package/dist/src/cdl.js +63 -0
- package/dist/src/extract-message.d.ts +1 -0
- package/dist/src/extract-message.js +10 -0
- package/dist/src/fail-me.d.ts +1 -0
- package/dist/src/fail-me.js +11 -0
- package/dist/src/find-array-method.d.ts +15 -0
- package/dist/src/find-array-method.js +104 -0
- package/dist/src/find-string-method.d.ts +2 -0
- package/dist/src/find-string-method.js +88 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +18 -0
- package/dist/src/location.d.ts +11 -0
- package/dist/src/location.js +3 -0
- package/dist/src/parser.d.ts +37 -0
- package/dist/src/parser.js +345 -0
- package/dist/src/result.d.ts +24 -0
- package/dist/src/result.js +29 -0
- package/dist/src/runtime.d.ts +25 -0
- package/dist/src/runtime.js +287 -0
- package/dist/src/scanner.d.ts +22 -0
- package/dist/src/scanner.js +76 -0
- package/dist/src/should-never-happen.d.ts +1 -0
- package/dist/src/should-never-happen.js +9 -0
- package/dist/src/source-code.d.ts +19 -0
- package/dist/src/source-code.js +90 -0
- package/dist/src/stack.d.ts +11 -0
- package/dist/src/stack.js +19 -0
- package/dist/src/switch-on.d.ts +1 -0
- package/dist/src/switch-on.js +9 -0
- package/dist/src/symbol-table.d.ts +5 -0
- package/dist/src/symbol-table.js +3 -0
- package/dist/src/value.d.ts +128 -0
- package/dist/src/value.js +634 -0
- package/dist/tests/cdl.spec.d.ts +1 -0
- package/dist/tests/cdl.spec.js +692 -0
- package/dist/tests/parser.spec.d.ts +1 -0
- package/dist/tests/parser.spec.js +39 -0
- package/dist/tests/value.spec.d.ts +1 -0
- package/dist/tests/value.spec.js +355 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/jest-output.json +1 -0
- package/package.json +17 -0
- package/src/ast-node.ts +205 -0
- package/src/cdl.ts +78 -0
- package/src/extract-message.ts +5 -0
- package/src/fail-me.ts +7 -0
- package/src/find-array-method.ts +115 -0
- package/src/find-string-method.ts +84 -0
- package/src/index.ts +1 -0
- package/src/location.ts +13 -0
- package/src/parser.ts +399 -0
- package/src/result.ts +45 -0
- package/src/runtime.ts +295 -0
- package/src/scanner.ts +94 -0
- package/src/should-never-happen.ts +4 -0
- package/src/source-code.ts +101 -0
- package/src/stack.ts +18 -0
- package/src/switch-on.ts +4 -0
- package/src/symbol-table.ts +6 -0
- package/src/value.ts +742 -0
- package/tests/cdl.spec.ts +755 -0
- package/tests/parser.spec.ts +14 -0
- package/tests/value.spec.ts +387 -0
- package/tsconfig.json +11 -0
package/src/location.ts
ADDED
package/src/parser.ts
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { ArrayLiteralPart, AstNode, Ident, Let, ObjectLiteralPart } from './ast-node'
|
|
2
|
+
import { Scanner, Token } from './scanner'
|
|
3
|
+
|
|
4
|
+
export class Parser {
|
|
5
|
+
constructor(private readonly scanner: Scanner) {}
|
|
6
|
+
|
|
7
|
+
parse() {
|
|
8
|
+
const ret = this.expression()
|
|
9
|
+
if (!this.scanner.eof()) {
|
|
10
|
+
throw new Error(`Loitering input ${this.scanner.sourceRef}`)
|
|
11
|
+
}
|
|
12
|
+
return ret
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
definitions(): Let[] {
|
|
16
|
+
const ret: Let[] = []
|
|
17
|
+
while (true) {
|
|
18
|
+
const start = this.scanner.consumeIf('let ')
|
|
19
|
+
if (!start) {
|
|
20
|
+
return ret
|
|
21
|
+
}
|
|
22
|
+
const ident = this.identifier()
|
|
23
|
+
this.scanner.consume('=')
|
|
24
|
+
const value = this.lambda()
|
|
25
|
+
this.scanner.consume(';')
|
|
26
|
+
|
|
27
|
+
ret.push({ start, ident, value })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
expression(): AstNode {
|
|
32
|
+
const definitions = this.definitions()
|
|
33
|
+
const computation = this.lambda()
|
|
34
|
+
|
|
35
|
+
if (definitions.length === 0) {
|
|
36
|
+
return computation
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { tag: 'topLevelExpression', definitions, computation }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lambda(): AstNode {
|
|
43
|
+
const start = this.scanner.consumeIf('fun')
|
|
44
|
+
if (!start) {
|
|
45
|
+
return this.ifExpression()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.scanner.consume('(')
|
|
49
|
+
const args: Ident[] = []
|
|
50
|
+
|
|
51
|
+
if (this.scanner.consumeIf(')')) {
|
|
52
|
+
// no formal args
|
|
53
|
+
} else {
|
|
54
|
+
while (true) {
|
|
55
|
+
const arg = this.identifier()
|
|
56
|
+
args.push(arg)
|
|
57
|
+
if (this.scanner.consumeIf(')')) {
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.scanner.consume(',')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const body = this.expression()
|
|
66
|
+
return { tag: 'lambda', start, formalArgs: args, body }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ifExpression(): AstNode {
|
|
70
|
+
if (!this.scanner.consumeIf('if')) {
|
|
71
|
+
return this.unsink()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.scanner.consume('(')
|
|
75
|
+
const condition = this.expression()
|
|
76
|
+
this.scanner.consume(')')
|
|
77
|
+
|
|
78
|
+
const positive = this.expression()
|
|
79
|
+
|
|
80
|
+
this.scanner.consume('else')
|
|
81
|
+
|
|
82
|
+
const negative = this.expression()
|
|
83
|
+
|
|
84
|
+
return { tag: 'if', condition, positive, negative }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
unsink(): AstNode {
|
|
88
|
+
const lhs = this.or()
|
|
89
|
+
if (this.scanner.consumeIf('??')) {
|
|
90
|
+
return { tag: 'binaryOperator', operator: '??', lhs, rhs: this.unsink() }
|
|
91
|
+
}
|
|
92
|
+
return lhs
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
or(): AstNode {
|
|
96
|
+
const lhs = this.and()
|
|
97
|
+
if (this.scanner.consumeIf('||')) {
|
|
98
|
+
return { tag: 'binaryOperator', operator: '||', lhs, rhs: this.or() }
|
|
99
|
+
}
|
|
100
|
+
return lhs
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
and(): AstNode {
|
|
104
|
+
const lhs = this.equality()
|
|
105
|
+
if (this.scanner.consumeIf('&&')) {
|
|
106
|
+
return { tag: 'binaryOperator', operator: '&&', lhs, rhs: this.and() }
|
|
107
|
+
}
|
|
108
|
+
return lhs
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
equality(): AstNode {
|
|
112
|
+
const lhs = this.comparison()
|
|
113
|
+
if (this.scanner.consumeIf('==')) {
|
|
114
|
+
return { tag: 'binaryOperator', operator: '==', lhs, rhs: this.equality() }
|
|
115
|
+
}
|
|
116
|
+
if (this.scanner.consumeIf('!=')) {
|
|
117
|
+
return { tag: 'binaryOperator', operator: '!=', lhs, rhs: this.equality() }
|
|
118
|
+
}
|
|
119
|
+
return lhs
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
comparison(): AstNode {
|
|
123
|
+
const lhs = this.addition()
|
|
124
|
+
if (this.scanner.consumeIf('>=')) {
|
|
125
|
+
return { tag: 'binaryOperator', operator: '>=', lhs, rhs: this.comparison() }
|
|
126
|
+
}
|
|
127
|
+
if (this.scanner.consumeIf('<=')) {
|
|
128
|
+
return { tag: 'binaryOperator', operator: '<=', lhs, rhs: this.comparison() }
|
|
129
|
+
}
|
|
130
|
+
if (this.scanner.consumeIf('>')) {
|
|
131
|
+
return { tag: 'binaryOperator', operator: '>', lhs, rhs: this.comparison() }
|
|
132
|
+
}
|
|
133
|
+
if (this.scanner.consumeIf('<')) {
|
|
134
|
+
return { tag: 'binaryOperator', operator: '<', lhs, rhs: this.comparison() }
|
|
135
|
+
}
|
|
136
|
+
return lhs
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
addition(): AstNode {
|
|
140
|
+
const lhs = this.multiplication()
|
|
141
|
+
if (this.scanner.consumeIf('+')) {
|
|
142
|
+
return { tag: 'binaryOperator', operator: '+', lhs, rhs: this.addition() }
|
|
143
|
+
}
|
|
144
|
+
if (this.scanner.consumeIf('-')) {
|
|
145
|
+
return { tag: 'binaryOperator', operator: '-', lhs, rhs: this.addition() }
|
|
146
|
+
}
|
|
147
|
+
return lhs
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
multiplication(): AstNode {
|
|
151
|
+
const lhs = this.power()
|
|
152
|
+
if (this.scanner.consumeIf('*')) {
|
|
153
|
+
return { tag: 'binaryOperator', operator: '*', lhs, rhs: this.multiplication() }
|
|
154
|
+
}
|
|
155
|
+
if (this.scanner.consumeIf('/')) {
|
|
156
|
+
return { tag: 'binaryOperator', operator: '/', lhs, rhs: this.multiplication() }
|
|
157
|
+
}
|
|
158
|
+
if (this.scanner.consumeIf('%')) {
|
|
159
|
+
return { tag: 'binaryOperator', operator: '%', lhs, rhs: this.multiplication() }
|
|
160
|
+
}
|
|
161
|
+
return lhs
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
power(): AstNode {
|
|
165
|
+
const lhs = this.unary()
|
|
166
|
+
if (this.scanner.consumeIf('**')) {
|
|
167
|
+
return { tag: 'binaryOperator', operator: '**', lhs, rhs: this.power() }
|
|
168
|
+
}
|
|
169
|
+
return lhs
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
unary(): AstNode {
|
|
173
|
+
let operatorToken = this.scanner.consumeIf('!')
|
|
174
|
+
if (operatorToken) {
|
|
175
|
+
return { tag: 'unaryOperator', operand: this.unary(), operator: '!', operatorToken }
|
|
176
|
+
}
|
|
177
|
+
operatorToken = this.scanner.consumeIf('+')
|
|
178
|
+
if (operatorToken) {
|
|
179
|
+
return { tag: 'unaryOperator', operand: this.unary(), operator: '+', operatorToken }
|
|
180
|
+
}
|
|
181
|
+
operatorToken = this.scanner.consumeIf('-')
|
|
182
|
+
if (operatorToken) {
|
|
183
|
+
return { tag: 'unaryOperator', operand: this.unary(), operator: '-', operatorToken }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return this.call()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
call(): AstNode {
|
|
190
|
+
const callee = this.memberAccess()
|
|
191
|
+
|
|
192
|
+
if (!this.scanner.consumeIf('(')) {
|
|
193
|
+
return callee
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const { actualArgs, end } = this.actualArgList()
|
|
197
|
+
return { tag: 'functionCall', actualArgs, callee, end }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private actualArgList() {
|
|
201
|
+
const actualArgs: AstNode[] = []
|
|
202
|
+
const endEmpty = this.scanner.consumeIf(')')
|
|
203
|
+
if (endEmpty) {
|
|
204
|
+
// no actual args
|
|
205
|
+
return { actualArgs, end: endEmpty }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
while (true) {
|
|
209
|
+
const arg = this.expression()
|
|
210
|
+
actualArgs.push(arg)
|
|
211
|
+
const end = this.scanner.consumeIf(')')
|
|
212
|
+
if (end) {
|
|
213
|
+
return { actualArgs, end }
|
|
214
|
+
}
|
|
215
|
+
this.scanner.consume(',')
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
memberAccess(): AstNode {
|
|
220
|
+
let ret = this.parenthesized()
|
|
221
|
+
|
|
222
|
+
while (true) {
|
|
223
|
+
if (this.scanner.consumeIf('.')) {
|
|
224
|
+
ret = { tag: 'dot', receiver: ret, ident: this.identifier() }
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (this.scanner.consumeIf('[')) {
|
|
229
|
+
ret = { tag: 'indexAccess', receiver: ret, index: this.expression() }
|
|
230
|
+
this.scanner.consume(']')
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (this.scanner.consumeIf('(')) {
|
|
235
|
+
const { actualArgs, end } = this.actualArgList()
|
|
236
|
+
ret = { tag: 'functionCall', actualArgs, callee: ret, end }
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return ret
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
parenthesized(): AstNode {
|
|
245
|
+
if (this.scanner.consumeIf('(')) {
|
|
246
|
+
const ret = this.expression()
|
|
247
|
+
this.scanner.consume(')')
|
|
248
|
+
return ret
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return this.literalOrIdent()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
literalOrIdent(): AstNode {
|
|
255
|
+
let t = this.scanner.consumeIf('sink!!')
|
|
256
|
+
if (t) {
|
|
257
|
+
return { tag: 'literal', type: 'sink!!', t }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
t = this.scanner.consumeIf('sink!')
|
|
261
|
+
if (t) {
|
|
262
|
+
return { tag: 'literal', type: 'sink!', t }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
t = this.scanner.consumeIf('sink')
|
|
266
|
+
if (t) {
|
|
267
|
+
return { tag: 'literal', type: 'sink', t }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
t = this.scanner.consumeIf('true')
|
|
271
|
+
if (t) {
|
|
272
|
+
return { tag: 'literal', type: 'bool', t }
|
|
273
|
+
}
|
|
274
|
+
t = this.scanner.consumeIf('false')
|
|
275
|
+
if (t) {
|
|
276
|
+
return { tag: 'literal', type: 'bool', t }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
t = this.scanner.consumeIf(/([0-9]*[.])?[0-9]+/)
|
|
280
|
+
if (t) {
|
|
281
|
+
return { tag: 'literal', type: 'num', t }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// double-quotes-enclosd string
|
|
285
|
+
if (this.scanner.consumeIf(`"`, false)) {
|
|
286
|
+
t = this.scanner.consume(/[^"]*/)
|
|
287
|
+
this.scanner.consume(`"`)
|
|
288
|
+
return { tag: 'literal', type: 'str', t }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// single-quotes-enclosd string
|
|
292
|
+
if (this.scanner.consumeIf(`'`, false)) {
|
|
293
|
+
t = this.scanner.consume(/[^']*/)
|
|
294
|
+
this.scanner.consume(`'`)
|
|
295
|
+
return { tag: 'literal', type: 'str', t }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
t = this.scanner.consumeIf('[')
|
|
299
|
+
if (t) {
|
|
300
|
+
return this.arrayBody(t)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
t = this.scanner.consumeIf('{')
|
|
304
|
+
if (t) {
|
|
305
|
+
return this.objectBody(t)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const ident = this.maybeIdentifier()
|
|
309
|
+
if (ident) {
|
|
310
|
+
return ident
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* This method assumes that the caller consumed the opening '[' token. It consumes the array's elements
|
|
318
|
+
* (comma-separated list of expressions) as well as the closing ']' token.
|
|
319
|
+
*/
|
|
320
|
+
arrayBody(start: Token): AstNode {
|
|
321
|
+
const t = this.scanner.consumeIf(']')
|
|
322
|
+
if (t) {
|
|
323
|
+
// an empty array literal
|
|
324
|
+
return { tag: 'arrayLiteral', start, parts: [], end: t }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const parts: ArrayLiteralPart[] = []
|
|
328
|
+
while (true) {
|
|
329
|
+
if (this.scanner.consumeIf('...')) {
|
|
330
|
+
parts.push({ tag: 'spread', v: this.expression() })
|
|
331
|
+
} else {
|
|
332
|
+
const exp = this.expression()
|
|
333
|
+
parts.push({ tag: 'element', v: exp })
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const end = this.scanner.consumeIf(']')
|
|
337
|
+
if (end) {
|
|
338
|
+
return { tag: 'arrayLiteral', start, parts, end }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
this.scanner.consume(',')
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* This method assumes that the caller consumed the opening '{' token. It consumes the object's attributes
|
|
347
|
+
* (comma-separated list of key:value parirs) as well as the closing '}' token.
|
|
348
|
+
*/
|
|
349
|
+
objectBody(start: Token): AstNode {
|
|
350
|
+
const t = this.scanner.consumeIf('}')
|
|
351
|
+
if (t) {
|
|
352
|
+
// an empty array literal
|
|
353
|
+
return { tag: 'objectLiteral', start, parts: [], end: t }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const parts: ObjectLiteralPart[] = []
|
|
357
|
+
while (true) {
|
|
358
|
+
if (this.scanner.consumeIf('...')) {
|
|
359
|
+
parts.push({ tag: 'spread', o: this.expression() })
|
|
360
|
+
} else if (this.scanner.consumeIf('[')) {
|
|
361
|
+
const k = this.expression()
|
|
362
|
+
this.scanner.consume(']')
|
|
363
|
+
this.scanner.consume(':')
|
|
364
|
+
const v = this.expression()
|
|
365
|
+
parts.push({ tag: 'computedName', k, v })
|
|
366
|
+
} else {
|
|
367
|
+
const k = this.identifier()
|
|
368
|
+
this.scanner.consume(':')
|
|
369
|
+
const v = this.expression()
|
|
370
|
+
parts.push({ tag: 'hardName', k, v })
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const end = this.scanner.consumeIf('}')
|
|
374
|
+
if (end) {
|
|
375
|
+
return { tag: 'objectLiteral', start, parts, end }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
this.scanner.consume(',')
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private identifier(): Ident {
|
|
383
|
+
const ret = this.maybeIdentifier()
|
|
384
|
+
if (!ret) {
|
|
385
|
+
throw new Error(`Expected an identifier ${this.scanner.sourceRef}`)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return ret
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private maybeIdentifier(): Ident | undefined {
|
|
392
|
+
const t = this.scanner.consumeIf(/[a-zA-Z][0-9A-Za-z_]*/)
|
|
393
|
+
if (t) {
|
|
394
|
+
return { tag: 'ident', t }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return undefined
|
|
398
|
+
}
|
|
399
|
+
}
|
package/src/result.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Span } from './location'
|
|
2
|
+
import { SourceCode } from './source-code'
|
|
3
|
+
import { Value } from './value'
|
|
4
|
+
|
|
5
|
+
export type ResultSink = {
|
|
6
|
+
tag: 'sink'
|
|
7
|
+
where: Span | undefined
|
|
8
|
+
trace: string | undefined
|
|
9
|
+
symbols: Record<string, unknown> | undefined
|
|
10
|
+
message: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Result =
|
|
14
|
+
| {
|
|
15
|
+
tag: 'ok'
|
|
16
|
+
value: unknown
|
|
17
|
+
}
|
|
18
|
+
| ResultSink
|
|
19
|
+
|
|
20
|
+
export class ResultSinkImpl {
|
|
21
|
+
readonly tag = 'sink'
|
|
22
|
+
constructor(private readonly sink: Value, private readonly sourceCode: SourceCode) {}
|
|
23
|
+
|
|
24
|
+
get where(): Span | undefined {
|
|
25
|
+
return this.sink.span()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get trace() {
|
|
29
|
+
const trace = this.sink.trace()
|
|
30
|
+
if (trace) {
|
|
31
|
+
return this.sourceCode.formatTrace(trace)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get symbols() {
|
|
38
|
+
return this.sink.symbols()?.export()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get message(): string {
|
|
42
|
+
const at = this.trace ?? this.sourceCode.sourceRef(this.where)
|
|
43
|
+
return `Evaluated to sink: ${at}`
|
|
44
|
+
}
|
|
45
|
+
}
|