septima-lang 0.0.2 → 0.0.5
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 +18 -5
- package/dist/src/ast-node.js +27 -2
- package/dist/src/parser.d.ts +9 -2
- package/dist/src/parser.js +126 -13
- package/dist/src/runtime.d.ts +6 -4
- package/dist/src/runtime.js +62 -6
- package/dist/src/scanner.d.ts +3 -2
- package/dist/src/scanner.js +16 -4
- package/dist/src/septima.d.ts +6 -7
- package/dist/src/septima.js +44 -16
- package/dist/src/symbol-table.d.ts +1 -0
- package/dist/tests/parser.spec.js +16 -1
- package/dist/tests/septima-compute-module.spec.d.ts +1 -0
- package/dist/tests/septima-compute-module.spec.js +30 -0
- package/dist/tests/septima.spec.js +85 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/jest-output.json +1 -1
- package/package.json +1 -1
- package/src/a.js +66 -0
- package/src/ast-node.ts +52 -8
- package/src/parser.ts +143 -16
- package/src/runtime.ts +71 -6
- package/src/scanner.ts +16 -4
- package/src/septima.ts +54 -18
- package/src/symbol-table.ts +1 -0
- package/tests/parser.spec.ts +16 -0
- package/tests/septima-compute-module.spec.ts +33 -0
- package/tests/septima.spec.ts +86 -2
package/src/parser.ts
CHANGED
|
@@ -1,17 +1,58 @@
|
|
|
1
|
-
import { ArrayLiteralPart, AstNode, Ident, Let, ObjectLiteralPart } from './ast-node'
|
|
1
|
+
import { ArrayLiteralPart, AstNode, Ident, Import, Let, Literal, ObjectLiteralPart, span, Unit } from './ast-node'
|
|
2
2
|
import { Scanner, Token } from './scanner'
|
|
3
|
+
import { switchOn } from './switch-on'
|
|
3
4
|
|
|
4
5
|
export class Parser {
|
|
5
6
|
constructor(private readonly scanner: Scanner) {}
|
|
6
7
|
|
|
7
|
-
parse() {
|
|
8
|
-
const ret = this.
|
|
8
|
+
parse(): Unit {
|
|
9
|
+
const ret = this.unit()
|
|
9
10
|
if (!this.scanner.eof()) {
|
|
10
11
|
throw new Error(`Loitering input ${this.scanner.sourceRef}`)
|
|
11
12
|
}
|
|
12
13
|
return ret
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
unit(): Unit {
|
|
17
|
+
const imports = this.imports()
|
|
18
|
+
const expression = this.expression()
|
|
19
|
+
return { tag: 'unit', imports, expression }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
imports(): Import[] {
|
|
23
|
+
const ret: Import[] = []
|
|
24
|
+
while (true) {
|
|
25
|
+
const start = this.scanner.consumeIf('import')
|
|
26
|
+
if (!start) {
|
|
27
|
+
return ret
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.scanner.consume('*')
|
|
31
|
+
this.scanner.consume('as')
|
|
32
|
+
const ident = this.identifier()
|
|
33
|
+
this.scanner.consume('from')
|
|
34
|
+
const pathToImportFrom = this.maybePrimitiveLiteral()
|
|
35
|
+
if (pathToImportFrom === undefined) {
|
|
36
|
+
throw new Error(`Expected a literal ${this.scanner.sourceRef}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const notString = () => {
|
|
40
|
+
throw new Error(`Expected a string literal ${this.scanner.sourceCode.sourceRef(span(pathToImportFrom))}`)
|
|
41
|
+
}
|
|
42
|
+
switchOn(pathToImportFrom.type, {
|
|
43
|
+
bool: notString,
|
|
44
|
+
num: notString,
|
|
45
|
+
str: () => {},
|
|
46
|
+
sink: notString,
|
|
47
|
+
'sink!': notString,
|
|
48
|
+
'sink!!': notString,
|
|
49
|
+
})
|
|
50
|
+
ret.push({ start, ident, pathToImportFrom: pathToImportFrom.t })
|
|
51
|
+
|
|
52
|
+
this.scanner.consumeIf(';')
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
15
56
|
definitions(): Let[] {
|
|
16
57
|
const ret: Let[] = []
|
|
17
58
|
while (true) {
|
|
@@ -22,14 +63,18 @@ export class Parser {
|
|
|
22
63
|
const ident = this.identifier()
|
|
23
64
|
this.scanner.consume('=')
|
|
24
65
|
const value = this.lambda()
|
|
25
|
-
this.scanner.consume(';')
|
|
26
|
-
|
|
27
66
|
ret.push({ start, ident, value })
|
|
67
|
+
|
|
68
|
+
this.scanner.consumeIf(';')
|
|
69
|
+
if (!this.scanner.headMatches('let ')) {
|
|
70
|
+
return ret
|
|
71
|
+
}
|
|
28
72
|
}
|
|
29
73
|
}
|
|
30
74
|
|
|
31
75
|
expression(): AstNode {
|
|
32
76
|
const definitions = this.definitions()
|
|
77
|
+
this.scanner.consumeIf('return')
|
|
33
78
|
const computation = this.lambda()
|
|
34
79
|
|
|
35
80
|
if (definitions.length === 0) {
|
|
@@ -42,7 +87,7 @@ export class Parser {
|
|
|
42
87
|
lambda(): AstNode {
|
|
43
88
|
const start = this.scanner.consumeIf('fun')
|
|
44
89
|
if (!start) {
|
|
45
|
-
return this.
|
|
90
|
+
return this.arrowFunction()
|
|
46
91
|
}
|
|
47
92
|
|
|
48
93
|
this.scanner.consume('(')
|
|
@@ -66,6 +111,60 @@ export class Parser {
|
|
|
66
111
|
return { tag: 'lambda', start, formalArgs: args, body }
|
|
67
112
|
}
|
|
68
113
|
|
|
114
|
+
arrowFunction(): AstNode {
|
|
115
|
+
if (this.scanner.headMatches('(', ')', '=>')) {
|
|
116
|
+
const start = this.scanner.consume('(')
|
|
117
|
+
this.scanner.consume(')')
|
|
118
|
+
this.scanner.consume('=>')
|
|
119
|
+
const body = this.lambdaBody()
|
|
120
|
+
return { tag: 'lambda', start, formalArgs: [], body }
|
|
121
|
+
}
|
|
122
|
+
if (this.scanner.headMatches(IDENT_PATTERN, '=>')) {
|
|
123
|
+
const ident = this.identifier()
|
|
124
|
+
this.scanner.consume('=>')
|
|
125
|
+
const body = this.lambdaBody()
|
|
126
|
+
return { tag: 'lambda', start: ident.t, formalArgs: [ident], body }
|
|
127
|
+
}
|
|
128
|
+
if (this.scanner.headMatches('(', IDENT_PATTERN, ')', '=>')) {
|
|
129
|
+
const start = this.scanner.consume('(')
|
|
130
|
+
const ident = this.identifier()
|
|
131
|
+
this.scanner.consume(')')
|
|
132
|
+
this.scanner.consume('=>')
|
|
133
|
+
const body = this.lambdaBody()
|
|
134
|
+
return { tag: 'lambda', start, formalArgs: [ident], body }
|
|
135
|
+
}
|
|
136
|
+
if (this.scanner.headMatches('(', IDENT_PATTERN, ',')) {
|
|
137
|
+
const start = this.scanner.consume('(')
|
|
138
|
+
const formalArgs: Ident[] = []
|
|
139
|
+
while (true) {
|
|
140
|
+
const ident = this.identifier()
|
|
141
|
+
formalArgs.push(ident)
|
|
142
|
+
|
|
143
|
+
if (this.scanner.consumeIf(')')) {
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.scanner.consume(',')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.scanner.consume('=>')
|
|
151
|
+
const body = this.lambdaBody()
|
|
152
|
+
return { tag: 'lambda', start, formalArgs, body }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return this.ifExpression()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private lambdaBody() {
|
|
159
|
+
if (this.scanner.consumeIf('{')) {
|
|
160
|
+
const ret = this.expression()
|
|
161
|
+
this.scanner.consume('}')
|
|
162
|
+
return ret
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this.expression()
|
|
166
|
+
}
|
|
167
|
+
|
|
69
168
|
ifExpression(): AstNode {
|
|
70
169
|
if (!this.scanner.consumeIf('if')) {
|
|
71
170
|
return this.unsink()
|
|
@@ -252,6 +351,18 @@ export class Parser {
|
|
|
252
351
|
}
|
|
253
352
|
|
|
254
353
|
literalOrIdent(): AstNode {
|
|
354
|
+
const ret = this.maybeLiteral() ?? this.maybeIdentifier()
|
|
355
|
+
if (!ret) {
|
|
356
|
+
throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
|
|
357
|
+
}
|
|
358
|
+
return ret
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
maybeLiteral(): AstNode | undefined {
|
|
362
|
+
return this.maybePrimitiveLiteral() ?? this.maybeCompositeLiteral()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
maybePrimitiveLiteral(): Literal | undefined {
|
|
255
366
|
let t = this.scanner.consumeIf('sink!!')
|
|
256
367
|
if (t) {
|
|
257
368
|
return { tag: 'literal', type: 'sink!!', t }
|
|
@@ -295,7 +406,11 @@ export class Parser {
|
|
|
295
406
|
return { tag: 'literal', type: 'str', t }
|
|
296
407
|
}
|
|
297
408
|
|
|
298
|
-
|
|
409
|
+
return undefined
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
maybeCompositeLiteral(): AstNode | undefined {
|
|
413
|
+
let t = this.scanner.consumeIf('[')
|
|
299
414
|
if (t) {
|
|
300
415
|
return this.arrayBody(t)
|
|
301
416
|
}
|
|
@@ -305,12 +420,7 @@ export class Parser {
|
|
|
305
420
|
return this.objectBody(t)
|
|
306
421
|
}
|
|
307
422
|
|
|
308
|
-
|
|
309
|
-
if (ident) {
|
|
310
|
-
return ident
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
|
|
423
|
+
return undefined
|
|
314
424
|
}
|
|
315
425
|
|
|
316
426
|
/**
|
|
@@ -326,6 +436,13 @@ export class Parser {
|
|
|
326
436
|
|
|
327
437
|
const parts: ArrayLiteralPart[] = []
|
|
328
438
|
while (true) {
|
|
439
|
+
if (this.scanner.consumeIf(',')) {
|
|
440
|
+
const end = this.scanner.consumeIf(']')
|
|
441
|
+
if (end) {
|
|
442
|
+
return { tag: 'arrayLiteral', start, parts, end }
|
|
443
|
+
}
|
|
444
|
+
continue
|
|
445
|
+
}
|
|
329
446
|
if (this.scanner.consumeIf('...')) {
|
|
330
447
|
parts.push({ tag: 'spread', v: this.expression() })
|
|
331
448
|
} else {
|
|
@@ -333,12 +450,16 @@ export class Parser {
|
|
|
333
450
|
parts.push({ tag: 'element', v: exp })
|
|
334
451
|
}
|
|
335
452
|
|
|
336
|
-
|
|
453
|
+
let end = this.scanner.consumeIf(']')
|
|
337
454
|
if (end) {
|
|
338
455
|
return { tag: 'arrayLiteral', start, parts, end }
|
|
339
456
|
}
|
|
340
457
|
|
|
341
458
|
this.scanner.consume(',')
|
|
459
|
+
end = this.scanner.consumeIf(']')
|
|
460
|
+
if (end) {
|
|
461
|
+
return { tag: 'arrayLiteral', start, parts, end }
|
|
462
|
+
}
|
|
342
463
|
}
|
|
343
464
|
}
|
|
344
465
|
|
|
@@ -370,12 +491,16 @@ export class Parser {
|
|
|
370
491
|
parts.push({ tag: 'hardName', k, v })
|
|
371
492
|
}
|
|
372
493
|
|
|
373
|
-
|
|
494
|
+
let end = this.scanner.consumeIf('}')
|
|
374
495
|
if (end) {
|
|
375
496
|
return { tag: 'objectLiteral', start, parts, end }
|
|
376
497
|
}
|
|
377
498
|
|
|
378
499
|
this.scanner.consume(',')
|
|
500
|
+
end = this.scanner.consumeIf('}')
|
|
501
|
+
if (end) {
|
|
502
|
+
return { tag: 'objectLiteral', start, parts, end }
|
|
503
|
+
}
|
|
379
504
|
}
|
|
380
505
|
}
|
|
381
506
|
|
|
@@ -389,7 +514,7 @@ export class Parser {
|
|
|
389
514
|
}
|
|
390
515
|
|
|
391
516
|
private maybeIdentifier(): Ident | undefined {
|
|
392
|
-
const t = this.scanner.consumeIf(
|
|
517
|
+
const t = this.scanner.consumeIf(IDENT_PATTERN)
|
|
393
518
|
if (t) {
|
|
394
519
|
return { tag: 'ident', t }
|
|
395
520
|
}
|
|
@@ -397,3 +522,5 @@ export class Parser {
|
|
|
397
522
|
return undefined
|
|
398
523
|
}
|
|
399
524
|
}
|
|
525
|
+
|
|
526
|
+
const IDENT_PATTERN = /[a-zA-Z][0-9A-Za-z_]*/
|
package/src/runtime.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { AstNode, show, span } from './ast-node'
|
|
1
|
+
import { AstNode, show, span, Unit } from './ast-node'
|
|
2
2
|
import { extractMessage } from './extract-message'
|
|
3
3
|
import { failMe } from './fail-me'
|
|
4
|
-
import { Parser } from './parser'
|
|
5
4
|
import { shouldNeverHappen } from './should-never-happen'
|
|
6
5
|
import * as Stack from './stack'
|
|
7
6
|
import { switchOn } from './switch-on'
|
|
@@ -32,6 +31,12 @@ class SymbolFrame implements SymbolTable {
|
|
|
32
31
|
ret[this.symbol] = this.placeholder.destination?.export() ?? failMe(`Unbounded symbol: ${this.symbol}`)
|
|
33
32
|
return ret
|
|
34
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
|
+
}
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
class EmptySymbolTable implements SymbolTable {
|
|
@@ -42,6 +47,10 @@ class EmptySymbolTable implements SymbolTable {
|
|
|
42
47
|
export() {
|
|
43
48
|
return {}
|
|
44
49
|
}
|
|
50
|
+
|
|
51
|
+
exportValue(): Record<string, Value> {
|
|
52
|
+
return {}
|
|
53
|
+
}
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
export type Verbosity = 'quiet' | 'trace'
|
|
@@ -51,18 +60,27 @@ export class Runtime {
|
|
|
51
60
|
constructor(
|
|
52
61
|
private readonly root: AstNode,
|
|
53
62
|
private readonly verbosity: Verbosity = 'quiet',
|
|
54
|
-
private readonly
|
|
63
|
+
private readonly preimports: Record<string, Value>,
|
|
64
|
+
private readonly getAstOf: (fileName: string) => Unit,
|
|
55
65
|
) {}
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
private buildInitialSymbolTable() {
|
|
58
68
|
const empty = new EmptySymbolTable()
|
|
59
69
|
|
|
60
70
|
const keys = Value.foreign(o => o.keys())
|
|
61
71
|
const entries = Value.foreign(o => o.entries())
|
|
62
72
|
const fromEntries = Value.foreign(o => o.fromEntries())
|
|
63
|
-
|
|
73
|
+
let lib = new SymbolFrame('Object', { destination: Value.obj({ keys, entries, fromEntries }) }, empty)
|
|
74
|
+
|
|
75
|
+
for (const [importName, importValue] of Object.entries(this.preimports)) {
|
|
76
|
+
lib = new SymbolFrame(importName, { destination: importValue }, lib)
|
|
77
|
+
}
|
|
78
|
+
return lib
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
compute() {
|
|
64
82
|
try {
|
|
65
|
-
const value = this.evalNode(this.root,
|
|
83
|
+
const value = this.evalNode(this.root, this.buildInitialSymbolTable())
|
|
66
84
|
return { value }
|
|
67
85
|
} catch (e) {
|
|
68
86
|
const trace: AstNode[] = []
|
|
@@ -95,7 +113,50 @@ export class Runtime {
|
|
|
95
113
|
return ret
|
|
96
114
|
}
|
|
97
115
|
|
|
116
|
+
private importDefinitions(pathToImportFrom: string): Value {
|
|
117
|
+
const ast = this.getAstOf(pathToImportFrom)
|
|
118
|
+
const exp = ast.expression
|
|
119
|
+
const imports = ast.imports
|
|
120
|
+
if (
|
|
121
|
+
exp.tag === 'arrayLiteral' ||
|
|
122
|
+
exp.tag === 'binaryOperator' ||
|
|
123
|
+
exp.tag === 'dot' ||
|
|
124
|
+
exp.tag === 'export*' ||
|
|
125
|
+
exp.tag === 'functionCall' ||
|
|
126
|
+
exp.tag === 'ident' ||
|
|
127
|
+
exp.tag === 'if' ||
|
|
128
|
+
exp.tag === 'indexAccess' ||
|
|
129
|
+
exp.tag === 'lambda' ||
|
|
130
|
+
exp.tag === 'literal' ||
|
|
131
|
+
exp.tag === 'objectLiteral' ||
|
|
132
|
+
exp.tag === 'unaryOperator' ||
|
|
133
|
+
exp.tag === 'unit'
|
|
134
|
+
) {
|
|
135
|
+
// TODO(imaman): throw an error on non-exporting unit?
|
|
136
|
+
return Value.obj({})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (exp.tag === 'topLevelExpression') {
|
|
140
|
+
const unit: AstNode = {
|
|
141
|
+
tag: 'unit',
|
|
142
|
+
imports,
|
|
143
|
+
expression: { tag: 'topLevelExpression', definitions: exp.definitions, computation: { tag: 'export*' } },
|
|
144
|
+
}
|
|
145
|
+
return this.evalNode(unit, this.buildInitialSymbolTable())
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
shouldNeverHappen(exp)
|
|
149
|
+
}
|
|
150
|
+
|
|
98
151
|
private evalNodeImpl(ast: AstNode, table: SymbolTable): Value {
|
|
152
|
+
if (ast.tag === 'unit') {
|
|
153
|
+
let newTable = table
|
|
154
|
+
for (const imp of ast.imports) {
|
|
155
|
+
const o = this.importDefinitions(imp.pathToImportFrom.text)
|
|
156
|
+
newTable = new SymbolFrame(imp.ident.t.text, { destination: o }, newTable)
|
|
157
|
+
}
|
|
158
|
+
return this.evalNode(ast.expression, newTable)
|
|
159
|
+
}
|
|
99
160
|
if (ast.tag === 'topLevelExpression') {
|
|
100
161
|
let newTable = table
|
|
101
162
|
for (const def of ast.definitions) {
|
|
@@ -109,6 +170,10 @@ export class Runtime {
|
|
|
109
170
|
return this.evalNode(ast.computation, newTable)
|
|
110
171
|
}
|
|
111
172
|
|
|
173
|
+
if (ast.tag === 'export*') {
|
|
174
|
+
return Value.obj(table.exportValue())
|
|
175
|
+
}
|
|
176
|
+
|
|
112
177
|
if (ast.tag === 'binaryOperator') {
|
|
113
178
|
const lhs = this.evalNode(ast.lhs, table)
|
|
114
179
|
if (ast.operator === '||') {
|
package/src/scanner.ts
CHANGED
|
@@ -7,10 +7,10 @@ export interface Token {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export class Scanner {
|
|
10
|
-
private offset = 0
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
constructor(readonly sourceCode: SourceCode, private offset = 0) {
|
|
11
|
+
if (this.offset === 0) {
|
|
12
|
+
this.eatWhitespace()
|
|
13
|
+
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
get sourceRef() {
|
|
@@ -52,6 +52,18 @@ export class Scanner {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
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
|
+
|
|
55
67
|
consume(r: RegExp | string, eatWhitespace = true): Token {
|
|
56
68
|
const text = this.match(r)
|
|
57
69
|
if (text === undefined) {
|
package/src/septima.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { Unit } from './ast-node'
|
|
1
2
|
import { Parser } from './parser'
|
|
2
3
|
import { Result, ResultSink, ResultSinkImpl } from './result'
|
|
3
4
|
import { Runtime, Verbosity } from './runtime'
|
|
4
5
|
import { Scanner } from './scanner'
|
|
5
6
|
import { shouldNeverHappen } from './should-never-happen'
|
|
6
7
|
import { SourceCode } from './source-code'
|
|
8
|
+
import { Value } from './value'
|
|
7
9
|
|
|
8
10
|
interface Options {
|
|
9
11
|
/**
|
|
@@ -15,10 +17,6 @@ interface Options {
|
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export class Septima {
|
|
18
|
-
private readonly scanner
|
|
19
|
-
private readonly sourceCode
|
|
20
|
-
private readonly parser
|
|
21
|
-
|
|
22
20
|
/**
|
|
23
21
|
* Runs a Septima program and returns the value it evaluates to. If it evaluates to `sink`, returns the value computed
|
|
24
22
|
* by `options.onSink()` - if present, or throws an error - otherwise.
|
|
@@ -36,7 +34,7 @@ export class Septima {
|
|
|
36
34
|
((r: ResultSink) => {
|
|
37
35
|
throw new Error(r.message)
|
|
38
36
|
})
|
|
39
|
-
const res = new Septima(
|
|
37
|
+
const res = new Septima().compute(input)
|
|
40
38
|
if (res.tag === 'ok') {
|
|
41
39
|
return res.value
|
|
42
40
|
}
|
|
@@ -48,30 +46,68 @@ export class Septima {
|
|
|
48
46
|
shouldNeverHappen(res)
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
constructor(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
constructor() {}
|
|
50
|
+
|
|
51
|
+
computeModule(moduleName: string, moduleReader: (m: string) => string): Result {
|
|
52
|
+
const input = moduleReader(moduleName)
|
|
53
|
+
const sourceCode = new SourceCode(input)
|
|
54
|
+
const value = this.computeImpl(sourceCode, 'quiet', {}, moduleReader)
|
|
55
|
+
if (!value.isSink()) {
|
|
56
|
+
return { value: value.export(), tag: 'ok' }
|
|
57
|
+
}
|
|
58
|
+
return new ResultSinkImpl(value, sourceCode)
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
compute(verbosity: Verbosity = 'quiet'): Result {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
61
|
+
compute(input: string, preimports: Record<string, string> = {}, verbosity: Verbosity = 'quiet'): Result {
|
|
62
|
+
const lib: Record<string, Value> = {}
|
|
63
|
+
for (const [importName, importCode] of Object.entries(preimports)) {
|
|
64
|
+
const sourceCode = new SourceCode(importCode)
|
|
65
|
+
const value = this.computeImpl(sourceCode, verbosity, {}, undefined)
|
|
66
|
+
if (value.isSink()) {
|
|
67
|
+
// TODO(imaman): cover!
|
|
68
|
+
const r = new ResultSinkImpl(value, sourceCode)
|
|
69
|
+
throw new Error(`preimport (${importName}) evaluated to sink: ${r.message}`)
|
|
70
|
+
}
|
|
71
|
+
lib[importName] = value
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sourceCode = new SourceCode(input)
|
|
75
|
+
const value = this.computeImpl(sourceCode, verbosity, lib, undefined)
|
|
76
|
+
if (!value.isSink()) {
|
|
77
|
+
return { value: value.export(), tag: 'ok' }
|
|
78
|
+
}
|
|
79
|
+
return new ResultSinkImpl(value, sourceCode)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private computeImpl(
|
|
83
|
+
sourceCode: SourceCode,
|
|
84
|
+
verbosity: Verbosity,
|
|
85
|
+
lib: Record<string, Value>,
|
|
86
|
+
moduleReader: undefined | ((m: string) => string),
|
|
87
|
+
) {
|
|
88
|
+
const scanner = new Scanner(sourceCode)
|
|
89
|
+
const parser = new Parser(scanner)
|
|
90
|
+
const ast = parse(parser)
|
|
91
|
+
|
|
92
|
+
const getAstOf = (fileName: string) => {
|
|
93
|
+
if (!moduleReader) {
|
|
94
|
+
throw new Error(`cannot read modules`)
|
|
95
|
+
}
|
|
96
|
+
return parse(moduleReader(fileName))
|
|
97
|
+
}
|
|
98
|
+
const runtime = new Runtime(ast, verbosity, lib, getAstOf)
|
|
60
99
|
const c = runtime.compute()
|
|
61
100
|
|
|
62
101
|
if (c.value) {
|
|
63
|
-
|
|
64
|
-
return { value: c.value.export(), tag: 'ok' }
|
|
65
|
-
}
|
|
66
|
-
return new ResultSinkImpl(c.value, this.sourceCode)
|
|
102
|
+
return c.value
|
|
67
103
|
}
|
|
68
104
|
|
|
69
|
-
const runtimeErrorMessage = `${c.errorMessage} when evaluating:\n${
|
|
105
|
+
const runtimeErrorMessage = `${c.errorMessage} when evaluating:\n${sourceCode.formatTrace(c.expressionTrace)}`
|
|
70
106
|
throw new Error(runtimeErrorMessage)
|
|
71
107
|
}
|
|
72
108
|
}
|
|
73
109
|
|
|
74
|
-
export function parse(arg: string | Parser) {
|
|
110
|
+
export function parse(arg: string | Parser): Unit {
|
|
75
111
|
const parser = typeof arg === 'string' ? new Parser(new Scanner(new SourceCode(arg))) : arg
|
|
76
112
|
const ast = parser.parse()
|
|
77
113
|
return ast
|
package/src/symbol-table.ts
CHANGED
package/tests/parser.spec.ts
CHANGED
|
@@ -11,4 +11,20 @@ describe('parser', () => {
|
|
|
11
11
|
expect(() => parse(`{#$%x: 8}`)).toThrowError('Expected an identifier at (1:2..9) #$%x: 8}')
|
|
12
12
|
expect(() => parse(`"foo" "goo"`)).toThrowError('Loitering input at (1:7..11) "goo"')
|
|
13
13
|
})
|
|
14
|
+
|
|
15
|
+
describe('unit', () => {
|
|
16
|
+
test('show', () => {
|
|
17
|
+
expect(show(parse(`import * as foo from './bar';'a'`))).toEqual(`import * as foo from './bar';\n'a'`)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
describe('expression', () => {
|
|
21
|
+
test('show', () => {
|
|
22
|
+
expect(show(parse(`'sunday'`))).toEqual(`'sunday'`)
|
|
23
|
+
expect(show(parse(`true`))).toEqual(`true`)
|
|
24
|
+
expect(show(parse(`500`))).toEqual(`500`)
|
|
25
|
+
expect(show(parse(`sink`))).toEqual(`sink`)
|
|
26
|
+
expect(show(parse(`sink!`))).toEqual(`sink!`)
|
|
27
|
+
expect(show(parse(`sink!!`))).toEqual(`sink!!`)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
14
30
|
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Septima } from '../src/septima'
|
|
2
|
+
import { shouldNeverHappen } from '../src/should-never-happen'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runs a Septima program for testing purposes. Throws an error If the program evaluated to `sink`.
|
|
6
|
+
*/
|
|
7
|
+
async function run(mainModule: string, inputs: Record<string, string>) {
|
|
8
|
+
const septima = new Septima()
|
|
9
|
+
const res = septima.computeModule(mainModule, (m: string) => inputs[m])
|
|
10
|
+
if (res.tag === 'ok') {
|
|
11
|
+
return res.value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (res.tag === 'sink') {
|
|
15
|
+
throw new Error(res.message)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
shouldNeverHappen(res)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('septima-compute-module', () => {
|
|
22
|
+
test('fetches the content of the module to compute from the given callback function', async () => {
|
|
23
|
+
expect(await run('a', { a: `3+8` })).toEqual(11)
|
|
24
|
+
})
|
|
25
|
+
test('can use exported definitions from another module', async () => {
|
|
26
|
+
expect(await run('a', { a: `import * as b from 'b'; 3+b.eight`, b: `let eight = 8; {}` })).toEqual(11)
|
|
27
|
+
})
|
|
28
|
+
test('errors if the path to input from is not a string literal', async () => {
|
|
29
|
+
await expect(run('a', { a: `import * as foo from 500` })).rejects.toThrowError(
|
|
30
|
+
'Expected a string literal at (1:22..24) 500',
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
})
|