septima-lang 0.0.7 → 0.0.8

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 (70) hide show
  1. package/dist/src/ast-node.d.ts +103 -0
  2. package/dist/src/ast-node.js +156 -0
  3. package/dist/src/extract-message.d.ts +1 -0
  4. package/dist/src/extract-message.js +10 -0
  5. package/dist/src/fail-me.d.ts +1 -0
  6. package/dist/src/fail-me.js +11 -0
  7. package/dist/src/find-array-method.d.ts +15 -0
  8. package/dist/src/find-array-method.js +104 -0
  9. package/dist/src/find-string-method.d.ts +2 -0
  10. package/dist/src/find-string-method.js +88 -0
  11. package/dist/src/index.d.ts +1 -0
  12. package/dist/src/index.js +18 -0
  13. package/dist/src/location.d.ts +11 -0
  14. package/dist/src/location.js +3 -0
  15. package/dist/src/parser.d.ts +45 -0
  16. package/dist/src/parser.js +483 -0
  17. package/dist/src/result.d.ts +24 -0
  18. package/dist/src/result.js +29 -0
  19. package/dist/src/runtime.d.ts +28 -0
  20. package/dist/src/runtime.js +351 -0
  21. package/dist/src/scanner.d.ts +23 -0
  22. package/dist/src/scanner.js +88 -0
  23. package/dist/src/septima.d.ts +32 -0
  24. package/dist/src/septima.js +91 -0
  25. package/dist/src/should-never-happen.d.ts +1 -0
  26. package/dist/src/should-never-happen.js +9 -0
  27. package/dist/src/source-code.d.ts +19 -0
  28. package/dist/src/source-code.js +90 -0
  29. package/dist/src/stack.d.ts +11 -0
  30. package/dist/src/stack.js +19 -0
  31. package/dist/src/switch-on.d.ts +1 -0
  32. package/dist/src/switch-on.js +9 -0
  33. package/dist/src/symbol-table.d.ts +6 -0
  34. package/dist/src/symbol-table.js +3 -0
  35. package/dist/src/value.d.ts +128 -0
  36. package/dist/src/value.js +634 -0
  37. package/dist/tests/parser.spec.d.ts +1 -0
  38. package/dist/tests/parser.spec.js +35 -0
  39. package/dist/tests/septima-compute-module.spec.d.ts +1 -0
  40. package/dist/tests/septima-compute-module.spec.js +36 -0
  41. package/dist/tests/septima.spec.d.ts +1 -0
  42. package/dist/tests/septima.spec.js +845 -0
  43. package/dist/tests/value.spec.d.ts +1 -0
  44. package/dist/tests/value.spec.js +355 -0
  45. package/dist/tsconfig.tsbuildinfo +1 -0
  46. package/package.json +3 -3
  47. package/src/a.js +66 -0
  48. package/src/ast-node.ts +269 -0
  49. package/src/extract-message.ts +5 -0
  50. package/src/fail-me.ts +7 -0
  51. package/src/find-array-method.ts +115 -0
  52. package/src/find-string-method.ts +84 -0
  53. package/src/index.ts +1 -0
  54. package/src/location.ts +13 -0
  55. package/src/parser.ts +554 -0
  56. package/src/result.ts +45 -0
  57. package/src/runtime.ts +370 -0
  58. package/src/scanner.ts +106 -0
  59. package/src/septima.ts +121 -0
  60. package/src/should-never-happen.ts +4 -0
  61. package/src/source-code.ts +101 -0
  62. package/src/stack.ts +18 -0
  63. package/src/switch-on.ts +4 -0
  64. package/src/symbol-table.ts +7 -0
  65. package/src/value.ts +742 -0
  66. package/tests/parser.spec.ts +36 -0
  67. package/tests/septima-compute-module.spec.ts +41 -0
  68. package/tests/septima.spec.ts +921 -0
  69. package/tests/value.spec.ts +387 -0
  70. package/main.js +0 -1
package/src/runtime.ts ADDED
@@ -0,0 +1,370 @@
1
+ import { AstNode, show, span, Unit } from './ast-node'
2
+ import { extractMessage } from './extract-message'
3
+ import { failMe } from './fail-me'
4
+ import { shouldNeverHappen } from './should-never-happen'
5
+ import * as Stack from './stack'
6
+ import { switchOn } from './switch-on'
7
+ import { SymbolTable } from './symbol-table'
8
+ import { Value } from './value'
9
+
10
+ interface Placeholder {
11
+ destination: undefined | Value
12
+ }
13
+
14
+ class SymbolFrame implements SymbolTable {
15
+ constructor(readonly symbol: string, readonly placeholder: Placeholder, private readonly earlier: SymbolTable) {}
16
+
17
+ lookup(sym: string): Value {
18
+ if (this.symbol === sym) {
19
+ const ret = this.placeholder.destination
20
+ if (ret === undefined) {
21
+ throw new Error(`Unresolved definition: ${this.symbol}`)
22
+ }
23
+ return ret
24
+ }
25
+
26
+ return this.earlier.lookup(sym)
27
+ }
28
+
29
+ export() {
30
+ const ret = this.earlier.export()
31
+ ret[this.symbol] = this.placeholder.destination?.export() ?? failMe(`Unbounded symbol: ${this.symbol}`)
32
+ return ret
33
+ }
34
+
35
+ exportValue(): Record<string, Value> {
36
+ const ret = this.earlier.exportValue()
37
+ ret[this.symbol] = this.placeholder.destination ?? failMe(`Unbounded symbol: ${this.symbol}`)
38
+ return ret
39
+ }
40
+ }
41
+
42
+ class EmptySymbolTable implements SymbolTable {
43
+ lookup(sym: string): Value {
44
+ throw new Error(`Symbol ${sym} was not found`)
45
+ }
46
+
47
+ export() {
48
+ return {}
49
+ }
50
+
51
+ exportValue(): Record<string, Value> {
52
+ return {}
53
+ }
54
+ }
55
+
56
+ export type Verbosity = 'quiet' | 'trace'
57
+
58
+ export class Runtime {
59
+ private stack: Stack.T = undefined
60
+ constructor(
61
+ private readonly root: AstNode,
62
+ private readonly verbosity: Verbosity = 'quiet',
63
+ private readonly preimports: Record<string, Value>,
64
+ private readonly getAstOf: (fileName: string) => Unit,
65
+ private readonly args: Record<string, unknown>,
66
+ ) {}
67
+
68
+ private buildInitialSymbolTable(generateTheArgsObject: boolean) {
69
+ const empty = new EmptySymbolTable()
70
+
71
+ const keys = Value.foreign(o => o.keys())
72
+ const entries = Value.foreign(o => o.entries())
73
+ const fromEntries = Value.foreign(o => o.fromEntries())
74
+ let lib = new SymbolFrame('Object', { destination: Value.obj({ keys, entries, fromEntries }) }, empty)
75
+
76
+ if (generateTheArgsObject) {
77
+ lib = new SymbolFrame('args', { destination: Value.from(this.args) }, lib)
78
+ }
79
+
80
+ for (const [importName, importValue] of Object.entries(this.preimports)) {
81
+ lib = new SymbolFrame(importName, { destination: importValue }, lib)
82
+ }
83
+ return lib
84
+ }
85
+
86
+ compute() {
87
+ try {
88
+ const value = this.evalNode(this.root, this.buildInitialSymbolTable(true))
89
+ return { value }
90
+ } catch (e) {
91
+ const trace: AstNode[] = []
92
+ for (let curr = this.stack; curr; curr = curr?.next) {
93
+ trace.push(curr.ast)
94
+ }
95
+ return {
96
+ expressionTrace: trace,
97
+ errorMessage: extractMessage(e),
98
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
99
+ stack: (e as { stack?: string[] }).stack,
100
+ }
101
+ }
102
+ }
103
+
104
+ private evalNode(ast: AstNode, table: SymbolTable): Value {
105
+ this.stack = Stack.push(ast, this.stack)
106
+ let ret = this.evalNodeImpl(ast, table)
107
+ if (ret.isSink() && !ret.span()) {
108
+ ret = ret.bindToSpan(span(ast))
109
+ }
110
+ switchOn(this.verbosity, {
111
+ quiet: () => {},
112
+ trace: () => {
113
+ // eslint-disable-next-line no-console
114
+ console.log(`output of <|${show(ast)}|> is ${JSON.stringify(ret)} // ${ast.tag}`)
115
+ },
116
+ })
117
+ this.stack = Stack.pop(this.stack)
118
+ return ret
119
+ }
120
+
121
+ private importDefinitions(pathToImportFrom: string): Value {
122
+ const ast = this.getAstOf(pathToImportFrom)
123
+ const exp = ast.expression
124
+ const imports = ast.imports
125
+ if (
126
+ exp.tag === 'arrayLiteral' ||
127
+ exp.tag === 'binaryOperator' ||
128
+ exp.tag === 'dot' ||
129
+ exp.tag === 'export*' ||
130
+ exp.tag === 'functionCall' ||
131
+ exp.tag === 'ident' ||
132
+ exp.tag === 'if' ||
133
+ exp.tag === 'ternary' ||
134
+ exp.tag === 'indexAccess' ||
135
+ exp.tag === 'lambda' ||
136
+ exp.tag === 'literal' ||
137
+ exp.tag === 'objectLiteral' ||
138
+ exp.tag === 'unaryOperator' ||
139
+ exp.tag === 'unit'
140
+ ) {
141
+ // TODO(imaman): throw an error on non-exporting unit?
142
+ return Value.obj({})
143
+ }
144
+
145
+ if (exp.tag === 'topLevelExpression') {
146
+ const unit: AstNode = {
147
+ tag: 'unit',
148
+ imports,
149
+ expression: { tag: 'topLevelExpression', definitions: exp.definitions, computation: { tag: 'export*' } },
150
+ }
151
+ return this.evalNode(unit, this.buildInitialSymbolTable(false))
152
+ }
153
+
154
+ shouldNeverHappen(exp)
155
+ }
156
+
157
+ private evalNodeImpl(ast: AstNode, table: SymbolTable): Value {
158
+ if (ast.tag === 'unit') {
159
+ let newTable = table
160
+ for (const imp of ast.imports) {
161
+ const o = this.importDefinitions(imp.pathToImportFrom.text)
162
+ newTable = new SymbolFrame(imp.ident.t.text, { destination: o }, newTable)
163
+ }
164
+ return this.evalNode(ast.expression, newTable)
165
+ }
166
+ if (ast.tag === 'topLevelExpression') {
167
+ let newTable = table
168
+ for (const def of ast.definitions) {
169
+ const name = def.ident.t.text
170
+ const placeholder: Placeholder = { destination: undefined }
171
+ newTable = new SymbolFrame(name, placeholder, newTable)
172
+ const v = this.evalNode(def.value, newTable)
173
+ placeholder.destination = v
174
+ }
175
+
176
+ if (ast.computation) {
177
+ return this.evalNode(ast.computation, newTable)
178
+ }
179
+
180
+ return Value.str('')
181
+ }
182
+
183
+ if (ast.tag === 'export*') {
184
+ return Value.obj(table.exportValue())
185
+ }
186
+
187
+ if (ast.tag === 'binaryOperator') {
188
+ const lhs = this.evalNode(ast.lhs, table)
189
+ if (ast.operator === '||') {
190
+ return lhs.or(() => this.evalNode(ast.rhs, table))
191
+ }
192
+ if (ast.operator === '&&') {
193
+ return lhs.and(() => this.evalNode(ast.rhs, table))
194
+ }
195
+
196
+ if (ast.operator === '??') {
197
+ return lhs.unsink(() => this.evalNode(ast.rhs, table))
198
+ }
199
+
200
+ const rhs = this.evalNode(ast.rhs, table)
201
+ if (ast.operator === '!=') {
202
+ return lhs.equalsTo(rhs).not()
203
+ }
204
+ if (ast.operator === '==') {
205
+ return lhs.equalsTo(rhs)
206
+ }
207
+
208
+ if (ast.operator === '<=') {
209
+ const comp = lhs.order(rhs)
210
+ return comp.isToZero('<=')
211
+ }
212
+ if (ast.operator === '<') {
213
+ const comp = lhs.order(rhs)
214
+ return comp.isToZero('<')
215
+ }
216
+ if (ast.operator === '>=') {
217
+ const comp = lhs.order(rhs)
218
+ return comp.isToZero('>=')
219
+ }
220
+ if (ast.operator === '>') {
221
+ const comp = lhs.order(rhs)
222
+ return comp.isToZero('>')
223
+ }
224
+ if (ast.operator === '%') {
225
+ return lhs.modulo(rhs)
226
+ }
227
+ if (ast.operator === '*') {
228
+ return lhs.times(rhs)
229
+ }
230
+ if (ast.operator === '**') {
231
+ return lhs.power(rhs)
232
+ }
233
+ if (ast.operator === '+') {
234
+ return lhs.plus(rhs)
235
+ }
236
+ if (ast.operator === '-') {
237
+ return lhs.minus(rhs)
238
+ }
239
+ if (ast.operator === '/') {
240
+ return lhs.over(rhs)
241
+ }
242
+
243
+ shouldNeverHappen(ast.operator)
244
+ }
245
+
246
+ if (ast.tag === 'unaryOperator') {
247
+ const operand = this.evalNode(ast.operand, table)
248
+ if (ast.operator === '!') {
249
+ return operand.not()
250
+ }
251
+ if (ast.operator === '+') {
252
+ // We intentionally do <0 + operand> instead of just <operand>. This is due to type-checking: the latter will
253
+ // evaluate to the operand as-is, making expression such as `+true` dynamically valid (which is not the desired
254
+ // behavior)
255
+ return Value.num(0).plus(operand)
256
+ }
257
+ if (ast.operator === '-') {
258
+ return operand.negate()
259
+ }
260
+
261
+ shouldNeverHappen(ast.operator)
262
+ }
263
+ if (ast.tag === 'ident') {
264
+ return table.lookup(ast.t.text)
265
+ }
266
+ if (ast.tag === 'literal') {
267
+ if (ast.type === 'bool') {
268
+ // TODO(imaman): stricter checking of 'false'
269
+ return Value.bool(ast.t.text === 'true' ? true : false)
270
+ }
271
+ if (ast.type === 'num') {
272
+ return Value.num(Number(ast.t.text))
273
+ }
274
+ if (ast.type === 'sink!!') {
275
+ return Value.sink(undefined, this.stack, table)
276
+ }
277
+ if (ast.type === 'sink!') {
278
+ return Value.sink(undefined, this.stack)
279
+ }
280
+ if (ast.type === 'sink') {
281
+ return Value.sink()
282
+ }
283
+ if (ast.type === 'str') {
284
+ return Value.str(ast.t.text)
285
+ }
286
+ shouldNeverHappen(ast.type)
287
+ }
288
+
289
+ if (ast.tag === 'arrayLiteral') {
290
+ const arr: Value[] = []
291
+ for (const curr of ast.parts) {
292
+ if (curr.tag === 'element') {
293
+ arr.push(this.evalNode(curr.v, table))
294
+ } else if (curr.tag === 'spread') {
295
+ const v = this.evalNode(curr.v, table)
296
+ arr.push(...v.assertArr())
297
+ } else {
298
+ shouldNeverHappen(curr)
299
+ }
300
+ }
301
+
302
+ return Value.arr(arr)
303
+ }
304
+
305
+ if (ast.tag === 'objectLiteral') {
306
+ const entries: [string, Value][] = ast.parts.flatMap(at => {
307
+ if (at.tag === 'hardName') {
308
+ return [[at.k.t.text, this.evalNode(at.v, table)]]
309
+ }
310
+ if (at.tag === 'computedName') {
311
+ return [[this.evalNode(at.k, table).assertStr(), this.evalNode(at.v, table)]]
312
+ }
313
+ if (at.tag === 'spread') {
314
+ const o = this.evalNode(at.o, table)
315
+ return Object.entries(o.assertObj())
316
+ }
317
+
318
+ shouldNeverHappen(at)
319
+ })
320
+
321
+ // TODO(imaman): verify type of all keys (strings, maybe also numbers)
322
+ return Value.obj(Object.fromEntries(entries))
323
+ }
324
+
325
+ if (ast.tag === 'lambda') {
326
+ return Value.lambda(ast, table)
327
+ }
328
+
329
+ if (ast.tag === 'functionCall') {
330
+ const argValues = ast.actualArgs.map(a => this.evalNode(a, table))
331
+ const callee = this.evalNode(ast.callee, table)
332
+
333
+ return this.call(callee, argValues)
334
+ }
335
+
336
+ if (ast.tag === 'if' || ast.tag === 'ternary') {
337
+ const c = this.evalNode(ast.condition, table)
338
+ return c.ifElse(
339
+ () => this.evalNode(ast.positive, table),
340
+ () => this.evalNode(ast.negative, table),
341
+ )
342
+ }
343
+
344
+ if (ast.tag === 'dot') {
345
+ const rec = this.evalNode(ast.receiver, table)
346
+ if (rec === undefined || rec === null) {
347
+ throw new Error(`Cannot access attribute .${ast.ident.t.text} of ${rec}`)
348
+ }
349
+ return rec.access(ast.ident.t.text, (callee, args) => this.call(callee, args))
350
+ }
351
+
352
+ if (ast.tag === 'indexAccess') {
353
+ const rec = this.evalNode(ast.receiver, table)
354
+ const index = this.evalNode(ast.index, table)
355
+ return rec.access(index, (callee, args) => this.call(callee, args))
356
+ }
357
+
358
+ shouldNeverHappen(ast)
359
+ }
360
+
361
+ call(callee: Value, argValues: Value[]) {
362
+ return callee.call(argValues, (names, body, lambdaTable: SymbolTable) => {
363
+ if (names.length > argValues.length) {
364
+ throw new Error(`Arg list length mismatch: expected ${names.length} but got ${argValues.length}`)
365
+ }
366
+ const newTable = names.reduce((t, n, i) => new SymbolFrame(n, { destination: argValues[i] }, t), lambdaTable)
367
+ return this.evalNode(body, newTable)
368
+ })
369
+ }
370
+ }
package/src/scanner.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { Location } from './location'
2
+ import { SourceCode } from './source-code'
3
+
4
+ export interface Token {
5
+ readonly text: string
6
+ readonly location: Location
7
+ }
8
+
9
+ export class Scanner {
10
+ constructor(readonly sourceCode: SourceCode, private offset = 0) {
11
+ if (this.offset === 0) {
12
+ this.eatWhitespace()
13
+ }
14
+ }
15
+
16
+ get sourceRef() {
17
+ return this.sourceCode.sourceRef(this.sourceCode.expandToEndOfLine({ offset: this.offset }))
18
+ }
19
+
20
+ private curr() {
21
+ return this.sourceCode.input.substring(this.offset)
22
+ }
23
+
24
+ private eatWhitespace() {
25
+ while (true) {
26
+ if (this.consumeIf(/\s*/, false)) {
27
+ continue
28
+ }
29
+ if (this.consumeIf('//', false)) {
30
+ this.consume(/[^\n]*/, false)
31
+ continue
32
+ }
33
+
34
+ return
35
+ }
36
+ }
37
+
38
+ eof(): boolean {
39
+ return this.offset >= this.sourceCode.input.length
40
+ }
41
+
42
+ synopsis() {
43
+ const c = this.curr()
44
+ let lookingAt = c.substring(0, 20)
45
+ if (lookingAt.length !== c.length) {
46
+ lookingAt = `${lookingAt}...`
47
+ }
48
+
49
+ return {
50
+ position: this.offset,
51
+ lookingAt,
52
+ }
53
+ }
54
+
55
+ headMatches(...patterns: (RegExp | string)[]): boolean {
56
+ const alt = new Scanner(this.sourceCode, this.offset)
57
+ for (const p of patterns) {
58
+ const t = alt.consumeIf(p, true)
59
+ if (t === undefined) {
60
+ return false
61
+ }
62
+ }
63
+
64
+ return true
65
+ }
66
+
67
+ consume(r: RegExp | string, eatWhitespace = true): Token {
68
+ const text = this.match(r)
69
+ if (text === undefined) {
70
+ throw new Error(`Expected ${r} ${this.sourceRef}`)
71
+ }
72
+
73
+ const offset = this.offset
74
+ this.offset += text.length
75
+
76
+ if (eatWhitespace) {
77
+ this.eatWhitespace()
78
+ }
79
+ return { location: { offset }, text }
80
+ }
81
+
82
+ consumeIf(r: RegExp | string, eatWhitespace = true): Token | undefined {
83
+ const ret = this.match(r)
84
+ if (!ret) {
85
+ return undefined
86
+ }
87
+
88
+ return this.consume(r, eatWhitespace)
89
+ }
90
+
91
+ private match(r: RegExp | string): string | undefined {
92
+ if (typeof r === 'string') {
93
+ if (this.curr().startsWith(r)) {
94
+ return r
95
+ }
96
+ return undefined
97
+ }
98
+
99
+ const m = this.curr().match(r)
100
+ if (m && m.index === 0) {
101
+ return m[0]
102
+ }
103
+
104
+ return undefined
105
+ }
106
+ }
package/src/septima.ts ADDED
@@ -0,0 +1,121 @@
1
+ import { Unit } from './ast-node'
2
+ import { Parser } from './parser'
3
+ import { Result, ResultSink, ResultSinkImpl } from './result'
4
+ import { Runtime, Verbosity } from './runtime'
5
+ import { Scanner } from './scanner'
6
+ import { shouldNeverHappen } from './should-never-happen'
7
+ import { SourceCode } from './source-code'
8
+ import { Value } from './value'
9
+
10
+ interface Options {
11
+ /**
12
+ * A callback function to be invoked when the Septima program evaluated to `sink`. Allows the caller to determine
13
+ * which value will be returned in that case. For instance, passing `() => undefined` will translate a `sink` value
14
+ * to `undefined`. The default behavior is to throw an error.
15
+ */
16
+ onSink?: (res: ResultSink) => unknown
17
+ }
18
+
19
+ export class Septima {
20
+ /**
21
+ * Runs a Septima program and returns the value it evaluates to. If it evaluates to `sink`, returns the value computed
22
+ * by `options.onSink()` - if present, or throws an error - otherwise.
23
+ *
24
+ * This method is the simplest way to evaluate a Septima program, and it fits many common use cases. One can also use
25
+ * `.compute()` to get additional details about the execution.
26
+ *
27
+ * @param input the source code of the Septima program
28
+ * @param options
29
+ * @returns the value that `input` evaluates to
30
+ */
31
+ static run(input: string, options?: Options, args: Record<string, unknown> = {}): unknown {
32
+ const onSink =
33
+ options?.onSink ??
34
+ ((r: ResultSink) => {
35
+ throw new Error(r.message)
36
+ })
37
+ const res = new Septima().compute(input, {}, 'quiet', args)
38
+ if (res.tag === 'ok') {
39
+ return res.value
40
+ }
41
+
42
+ if (res.tag === 'sink') {
43
+ return onSink(res)
44
+ }
45
+
46
+ shouldNeverHappen(res)
47
+ }
48
+
49
+ constructor() {}
50
+
51
+ computeModule(moduleName: string, moduleReader: (m: string) => string, args: Record<string, unknown>): Result {
52
+ const input = moduleReader(moduleName)
53
+ const sourceCode = new SourceCode(input)
54
+ const value = this.computeImpl(sourceCode, 'quiet', {}, moduleReader, args)
55
+ if (!value.isSink()) {
56
+ return { value: value.export(), tag: 'ok' }
57
+ }
58
+ return new ResultSinkImpl(value, sourceCode)
59
+ }
60
+
61
+ compute(
62
+ input: string,
63
+ preimports: Record<string, string> = {},
64
+ verbosity: Verbosity = 'quiet',
65
+ args: Record<string, unknown>,
66
+ ): Result {
67
+ const lib: Record<string, Value> = {}
68
+ for (const [importName, importCode] of Object.entries(preimports)) {
69
+ const sourceCode = new SourceCode(importCode)
70
+ const value = this.computeImpl(sourceCode, verbosity, {}, undefined, {})
71
+ if (value.isSink()) {
72
+ // TODO(imaman): cover!
73
+ const r = new ResultSinkImpl(value, sourceCode)
74
+ throw new Error(`preimport (${importName}) evaluated to sink: ${r.message}`)
75
+ }
76
+ lib[importName] = value
77
+ }
78
+
79
+ const sourceCode = new SourceCode(input)
80
+ const value = this.computeImpl(sourceCode, verbosity, lib, undefined, args)
81
+ if (!value.isSink()) {
82
+ return { value: value.export(), tag: 'ok' }
83
+ }
84
+ return new ResultSinkImpl(value, sourceCode)
85
+ }
86
+
87
+ private computeImpl(
88
+ sourceCode: SourceCode,
89
+ verbosity: Verbosity,
90
+ lib: Record<string, Value>,
91
+ moduleReader: undefined | ((m: string) => string),
92
+ args: Record<string, unknown>,
93
+ ) {
94
+ const scanner = new Scanner(sourceCode)
95
+ const parser = new Parser(scanner)
96
+ const ast = parse(parser)
97
+
98
+ const getAstOf = (fileName: string) => {
99
+ if (!moduleReader) {
100
+ throw new Error(`cannot read modules`)
101
+ }
102
+ return parse(moduleReader(fileName))
103
+ }
104
+
105
+ const runtime = new Runtime(ast, verbosity, lib, getAstOf, args)
106
+ const c = runtime.compute()
107
+
108
+ if (c.value) {
109
+ return c.value
110
+ }
111
+
112
+ const runtimeErrorMessage = `${c.errorMessage} when evaluating:\n${sourceCode.formatTrace(c.expressionTrace)}`
113
+ throw new Error(runtimeErrorMessage)
114
+ }
115
+ }
116
+
117
+ export function parse(arg: string | Parser): Unit {
118
+ const parser = typeof arg === 'string' ? new Parser(new Scanner(new SourceCode(arg))) : arg
119
+ const ast = parser.parse()
120
+ return ast
121
+ }
@@ -0,0 +1,4 @@
1
+ export function shouldNeverHappen(n: never): never {
2
+ // This following line never gets executed. It is here just to make the compiler happy.
3
+ throw new Error(`This should never happen ${n}`)
4
+ }
@@ -0,0 +1,101 @@
1
+ import { AstNode, span } from './ast-node'
2
+ import { Location, Location2d, Span } from './location'
3
+
4
+ export class SourceCode {
5
+ constructor(readonly input: string) {}
6
+
7
+ formatTrace(trace: AstNode[]): string {
8
+ const spacer = ' '
9
+
10
+ const formatted = trace
11
+ .map(ast => this.sourceRef(span(ast)))
12
+ .reverse()
13
+ .join(`\n${spacer}`)
14
+ return `${spacer}${formatted}`
15
+ }
16
+
17
+ sourceRefOfLocation(loc: Location) {
18
+ return this.sourceRef(this.expandToEndOfLine(loc))
19
+ }
20
+
21
+ sourceRef(span: Span | undefined) {
22
+ if (!span) {
23
+ return `at <unknown location>`
24
+ }
25
+ return `at ${this.formatSpan(span)} ${this.interestingPart(span)}`
26
+ }
27
+
28
+ formatSpan(span: Span) {
29
+ const f = this.resolveLocation(span.from)
30
+ const t = this.resolveLocation(span.to)
31
+ if (f.line === t.line) {
32
+ return `(${f.line + 1}:${f.col + 1}..${t.col + 1})`
33
+ }
34
+
35
+ return `(${f.line + 1}:${f.col + 1}..${t.line + 1}:${t.col + 1})`
36
+ }
37
+
38
+ private interestingPart(span: Span) {
39
+ const f = this.resolveLocation(span.from)
40
+ const t = this.resolveLocation(span.to)
41
+
42
+ const strip = (s: string) => s.replace(/^[\n]*/, '').replace(/[\n]*$/, '')
43
+ const limit = 80
44
+
45
+ const lineAtFrom = this.lineAt(span.from)
46
+ if (f.line !== t.line) {
47
+ return `${strip(lineAtFrom).substring(0, limit)}...`
48
+ }
49
+
50
+ const stripped = strip(lineAtFrom.substring(f.col, t.col + 1))
51
+ if (stripped.length <= limit) {
52
+ return stripped
53
+ }
54
+ return `${stripped.substring(0, limit)}...`
55
+ }
56
+
57
+ resolveLocation(loc: Location): Location2d {
58
+ const prefix = this.input.slice(0, loc.offset)
59
+ let line = 0
60
+ for (let i = 0; i < prefix.length; ++i) {
61
+ const ch = prefix[i]
62
+ if (ch === '\n') {
63
+ line += 1
64
+ }
65
+ }
66
+
67
+ let col = 0
68
+ for (let i = prefix.length - 1; i >= 0; --i, ++col) {
69
+ const ch = prefix[i]
70
+ if (ch === '\n') {
71
+ break
72
+ }
73
+ }
74
+
75
+ return { line, col }
76
+ }
77
+
78
+ /**
79
+ * Returns a span starting a the given input location that runs all the way to the end of the line. Specifically, the
80
+ * returned span's `.to` location will point at the character (in the same line as `loc`) that is immediately before
81
+ * the terminating newline character.
82
+ */
83
+ expandToEndOfLine(loc: Location): Span {
84
+ let endOfLine = this.input.indexOf('\n', loc.offset)
85
+ if (endOfLine < 0) {
86
+ endOfLine = this.input.length - 1
87
+ }
88
+ return { from: loc, to: { offset: endOfLine } }
89
+ }
90
+
91
+ lineAt(loc: Location) {
92
+ const precedingNewline = this.input.lastIndexOf('\n', loc.offset)
93
+ // add a + 1 to skip over the '\n' character (it is not part of the line). Also works if precedingNewLine is -1 (no
94
+ // preceding newline exists)
95
+ const startOfLine = precedingNewline + 1
96
+ const endOfLine = this.expandToEndOfLine(loc).to
97
+
98
+ const ret = this.input.substring(startOfLine, endOfLine.offset + 1)
99
+ return ret
100
+ }
101
+ }