septima-lang 0.0.6 → 0.0.7
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/main.js +1 -0
- package/package.json +3 -3
- package/dist/src/ast-node.d.ts +0 -98
- package/dist/src/ast-node.js +0 -139
- package/dist/src/extract-message.d.ts +0 -1
- package/dist/src/extract-message.js +0 -10
- package/dist/src/fail-me.d.ts +0 -1
- package/dist/src/fail-me.js +0 -11
- package/dist/src/find-array-method.d.ts +0 -15
- package/dist/src/find-array-method.js +0 -104
- package/dist/src/find-string-method.d.ts +0 -2
- package/dist/src/find-string-method.js +0 -88
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -18
- package/dist/src/location.d.ts +0 -11
- package/dist/src/location.js +0 -3
- package/dist/src/parser.d.ts +0 -44
- package/dist/src/parser.js +0 -462
- package/dist/src/result.d.ts +0 -24
- package/dist/src/result.js +0 -29
- package/dist/src/runtime.d.ts +0 -28
- package/dist/src/runtime.js +0 -347
- package/dist/src/scanner.d.ts +0 -23
- package/dist/src/scanner.js +0 -88
- package/dist/src/septima.d.ts +0 -32
- package/dist/src/septima.js +0 -91
- package/dist/src/should-never-happen.d.ts +0 -1
- package/dist/src/should-never-happen.js +0 -9
- package/dist/src/source-code.d.ts +0 -19
- package/dist/src/source-code.js +0 -90
- package/dist/src/stack.d.ts +0 -11
- package/dist/src/stack.js +0 -19
- package/dist/src/switch-on.d.ts +0 -1
- package/dist/src/switch-on.js +0 -9
- package/dist/src/symbol-table.d.ts +0 -6
- package/dist/src/symbol-table.js +0 -3
- package/dist/src/value.d.ts +0 -128
- package/dist/src/value.js +0 -634
- package/dist/tests/parser.spec.d.ts +0 -1
- package/dist/tests/parser.spec.js +0 -31
- package/dist/tests/septima-compute-module.spec.d.ts +0 -1
- package/dist/tests/septima-compute-module.spec.js +0 -36
- package/dist/tests/septima.spec.d.ts +0 -1
- package/dist/tests/septima.spec.js +0 -807
- package/dist/tests/value.spec.d.ts +0 -1
- package/dist/tests/value.spec.js +0 -355
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/jest-output.json +0 -1
- package/src/a.js +0 -66
- package/src/ast-node.ts +0 -249
- package/src/extract-message.ts +0 -5
- package/src/fail-me.ts +0 -7
- package/src/find-array-method.ts +0 -115
- package/src/find-string-method.ts +0 -84
- package/src/index.ts +0 -1
- package/src/location.ts +0 -13
- package/src/parser.ts +0 -530
- package/src/result.ts +0 -45
- package/src/runtime.ts +0 -365
- package/src/scanner.ts +0 -106
- package/src/septima.ts +0 -121
- package/src/should-never-happen.ts +0 -4
- package/src/source-code.ts +0 -101
- package/src/stack.ts +0 -18
- package/src/switch-on.ts +0 -4
- package/src/symbol-table.ts +0 -7
- package/src/value.ts +0 -742
- package/tests/parser.spec.ts +0 -30
- package/tests/septima-compute-module.spec.ts +0 -41
- package/tests/septima.spec.ts +0 -880
- package/tests/value.spec.ts +0 -387
- package/tsconfig.json +0 -11
package/src/runtime.ts
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
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 === 'indexAccess' ||
|
|
134
|
-
exp.tag === 'lambda' ||
|
|
135
|
-
exp.tag === 'literal' ||
|
|
136
|
-
exp.tag === 'objectLiteral' ||
|
|
137
|
-
exp.tag === 'unaryOperator' ||
|
|
138
|
-
exp.tag === 'unit'
|
|
139
|
-
) {
|
|
140
|
-
// TODO(imaman): throw an error on non-exporting unit?
|
|
141
|
-
return Value.obj({})
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (exp.tag === 'topLevelExpression') {
|
|
145
|
-
const unit: AstNode = {
|
|
146
|
-
tag: 'unit',
|
|
147
|
-
imports,
|
|
148
|
-
expression: { tag: 'topLevelExpression', definitions: exp.definitions, computation: { tag: 'export*' } },
|
|
149
|
-
}
|
|
150
|
-
return this.evalNode(unit, this.buildInitialSymbolTable(false))
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
shouldNeverHappen(exp)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
private evalNodeImpl(ast: AstNode, table: SymbolTable): Value {
|
|
157
|
-
if (ast.tag === 'unit') {
|
|
158
|
-
let newTable = table
|
|
159
|
-
for (const imp of ast.imports) {
|
|
160
|
-
const o = this.importDefinitions(imp.pathToImportFrom.text)
|
|
161
|
-
newTable = new SymbolFrame(imp.ident.t.text, { destination: o }, newTable)
|
|
162
|
-
}
|
|
163
|
-
return this.evalNode(ast.expression, newTable)
|
|
164
|
-
}
|
|
165
|
-
if (ast.tag === 'topLevelExpression') {
|
|
166
|
-
let newTable = table
|
|
167
|
-
for (const def of ast.definitions) {
|
|
168
|
-
const name = def.ident.t.text
|
|
169
|
-
const placeholder: Placeholder = { destination: undefined }
|
|
170
|
-
newTable = new SymbolFrame(name, placeholder, newTable)
|
|
171
|
-
const v = this.evalNode(def.value, newTable)
|
|
172
|
-
placeholder.destination = v
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return this.evalNode(ast.computation, newTable)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (ast.tag === 'export*') {
|
|
179
|
-
return Value.obj(table.exportValue())
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (ast.tag === 'binaryOperator') {
|
|
183
|
-
const lhs = this.evalNode(ast.lhs, table)
|
|
184
|
-
if (ast.operator === '||') {
|
|
185
|
-
return lhs.or(() => this.evalNode(ast.rhs, table))
|
|
186
|
-
}
|
|
187
|
-
if (ast.operator === '&&') {
|
|
188
|
-
return lhs.and(() => this.evalNode(ast.rhs, table))
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (ast.operator === '??') {
|
|
192
|
-
return lhs.unsink(() => this.evalNode(ast.rhs, table))
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const rhs = this.evalNode(ast.rhs, table)
|
|
196
|
-
if (ast.operator === '!=') {
|
|
197
|
-
return lhs.equalsTo(rhs).not()
|
|
198
|
-
}
|
|
199
|
-
if (ast.operator === '==') {
|
|
200
|
-
return lhs.equalsTo(rhs)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (ast.operator === '<=') {
|
|
204
|
-
const comp = lhs.order(rhs)
|
|
205
|
-
return comp.isToZero('<=')
|
|
206
|
-
}
|
|
207
|
-
if (ast.operator === '<') {
|
|
208
|
-
const comp = lhs.order(rhs)
|
|
209
|
-
return comp.isToZero('<')
|
|
210
|
-
}
|
|
211
|
-
if (ast.operator === '>=') {
|
|
212
|
-
const comp = lhs.order(rhs)
|
|
213
|
-
return comp.isToZero('>=')
|
|
214
|
-
}
|
|
215
|
-
if (ast.operator === '>') {
|
|
216
|
-
const comp = lhs.order(rhs)
|
|
217
|
-
return comp.isToZero('>')
|
|
218
|
-
}
|
|
219
|
-
if (ast.operator === '%') {
|
|
220
|
-
return lhs.modulo(rhs)
|
|
221
|
-
}
|
|
222
|
-
if (ast.operator === '*') {
|
|
223
|
-
return lhs.times(rhs)
|
|
224
|
-
}
|
|
225
|
-
if (ast.operator === '**') {
|
|
226
|
-
return lhs.power(rhs)
|
|
227
|
-
}
|
|
228
|
-
if (ast.operator === '+') {
|
|
229
|
-
return lhs.plus(rhs)
|
|
230
|
-
}
|
|
231
|
-
if (ast.operator === '-') {
|
|
232
|
-
return lhs.minus(rhs)
|
|
233
|
-
}
|
|
234
|
-
if (ast.operator === '/') {
|
|
235
|
-
return lhs.over(rhs)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
shouldNeverHappen(ast.operator)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (ast.tag === 'unaryOperator') {
|
|
242
|
-
const operand = this.evalNode(ast.operand, table)
|
|
243
|
-
if (ast.operator === '!') {
|
|
244
|
-
return operand.not()
|
|
245
|
-
}
|
|
246
|
-
if (ast.operator === '+') {
|
|
247
|
-
// We intentionally do <0 + operand> instead of just <operand>. This is due to type-checking: the latter will
|
|
248
|
-
// evaluate to the operand as-is, making expression such as `+true` dynamically valid (which is not the desired
|
|
249
|
-
// behavior)
|
|
250
|
-
return Value.num(0).plus(operand)
|
|
251
|
-
}
|
|
252
|
-
if (ast.operator === '-') {
|
|
253
|
-
return operand.negate()
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
shouldNeverHappen(ast.operator)
|
|
257
|
-
}
|
|
258
|
-
if (ast.tag === 'ident') {
|
|
259
|
-
return table.lookup(ast.t.text)
|
|
260
|
-
}
|
|
261
|
-
if (ast.tag === 'literal') {
|
|
262
|
-
if (ast.type === 'bool') {
|
|
263
|
-
// TODO(imaman): stricter checking of 'false'
|
|
264
|
-
return Value.bool(ast.t.text === 'true' ? true : false)
|
|
265
|
-
}
|
|
266
|
-
if (ast.type === 'num') {
|
|
267
|
-
return Value.num(Number(ast.t.text))
|
|
268
|
-
}
|
|
269
|
-
if (ast.type === 'sink!!') {
|
|
270
|
-
return Value.sink(undefined, this.stack, table)
|
|
271
|
-
}
|
|
272
|
-
if (ast.type === 'sink!') {
|
|
273
|
-
return Value.sink(undefined, this.stack)
|
|
274
|
-
}
|
|
275
|
-
if (ast.type === 'sink') {
|
|
276
|
-
return Value.sink()
|
|
277
|
-
}
|
|
278
|
-
if (ast.type === 'str') {
|
|
279
|
-
return Value.str(ast.t.text)
|
|
280
|
-
}
|
|
281
|
-
shouldNeverHappen(ast.type)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (ast.tag === 'arrayLiteral') {
|
|
285
|
-
const arr: Value[] = []
|
|
286
|
-
for (const curr of ast.parts) {
|
|
287
|
-
if (curr.tag === 'element') {
|
|
288
|
-
arr.push(this.evalNode(curr.v, table))
|
|
289
|
-
} else if (curr.tag === 'spread') {
|
|
290
|
-
const v = this.evalNode(curr.v, table)
|
|
291
|
-
arr.push(...v.assertArr())
|
|
292
|
-
} else {
|
|
293
|
-
shouldNeverHappen(curr)
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return Value.arr(arr)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (ast.tag === 'objectLiteral') {
|
|
301
|
-
const entries: [string, Value][] = ast.parts.flatMap(at => {
|
|
302
|
-
if (at.tag === 'hardName') {
|
|
303
|
-
return [[at.k.t.text, this.evalNode(at.v, table)]]
|
|
304
|
-
}
|
|
305
|
-
if (at.tag === 'computedName') {
|
|
306
|
-
return [[this.evalNode(at.k, table).assertStr(), this.evalNode(at.v, table)]]
|
|
307
|
-
}
|
|
308
|
-
if (at.tag === 'spread') {
|
|
309
|
-
const o = this.evalNode(at.o, table)
|
|
310
|
-
return Object.entries(o.assertObj())
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
shouldNeverHappen(at)
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
// TODO(imaman): verify type of all keys (strings, maybe also numbers)
|
|
317
|
-
return Value.obj(Object.fromEntries(entries))
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (ast.tag === 'lambda') {
|
|
321
|
-
return Value.lambda(ast, table)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (ast.tag === 'functionCall') {
|
|
325
|
-
const argValues = ast.actualArgs.map(a => this.evalNode(a, table))
|
|
326
|
-
const callee = this.evalNode(ast.callee, table)
|
|
327
|
-
|
|
328
|
-
return this.call(callee, argValues)
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (ast.tag === 'if') {
|
|
332
|
-
const c = this.evalNode(ast.condition, table)
|
|
333
|
-
return c.ifElse(
|
|
334
|
-
() => this.evalNode(ast.positive, table),
|
|
335
|
-
() => this.evalNode(ast.negative, table),
|
|
336
|
-
)
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (ast.tag === 'dot') {
|
|
340
|
-
const rec = this.evalNode(ast.receiver, table)
|
|
341
|
-
if (rec === undefined || rec === null) {
|
|
342
|
-
throw new Error(`Cannot access attribute .${ast.ident.t.text} of ${rec}`)
|
|
343
|
-
}
|
|
344
|
-
return rec.access(ast.ident.t.text, (callee, args) => this.call(callee, args))
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (ast.tag === 'indexAccess') {
|
|
348
|
-
const rec = this.evalNode(ast.receiver, table)
|
|
349
|
-
const index = this.evalNode(ast.index, table)
|
|
350
|
-
return rec.access(index, (callee, args) => this.call(callee, args))
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
shouldNeverHappen(ast)
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
call(callee: Value, argValues: Value[]) {
|
|
357
|
-
return callee.call(argValues, (names, body, lambdaTable: SymbolTable) => {
|
|
358
|
-
if (names.length > argValues.length) {
|
|
359
|
-
throw new Error(`Arg list length mismatch: expected ${names.length} but got ${argValues.length}`)
|
|
360
|
-
}
|
|
361
|
-
const newTable = names.reduce((t, n, i) => new SymbolFrame(n, { destination: argValues[i] }, t), lambdaTable)
|
|
362
|
-
return this.evalNode(body, newTable)
|
|
363
|
-
})
|
|
364
|
-
}
|
|
365
|
-
}
|
package/src/scanner.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
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
|
-
}
|
package/src/source-code.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
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
|
-
}
|