septima-lang 0.0.3 → 0.0.6
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 +7 -2
- package/dist/src/parser.js +74 -13
- package/dist/src/runtime.d.ts +6 -2
- package/dist/src/runtime.js +61 -4
- package/dist/src/scanner.d.ts +1 -1
- package/dist/src/scanner.js +1 -1
- package/dist/src/septima.d.ts +6 -6
- package/dist/src/septima.js +26 -14
- 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 +36 -0
- package/dist/tests/septima.spec.js +60 -12
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/jest-output.json +1 -1
- package/package.json +1 -1
- package/src/ast-node.ts +52 -8
- package/src/parser.ts +82 -15
- package/src/runtime.ts +70 -4
- package/src/scanner.ts +1 -1
- package/src/septima.ts +41 -12
- package/src/symbol-table.ts +1 -0
- package/tests/parser.spec.ts +16 -0
- package/tests/septima-compute-module.spec.ts +41 -0
- package/tests/septima.spec.ts +69 -11
package/src/ast-node.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { Span } from './location'
|
|
2
2
|
import { Token } from './scanner'
|
|
3
3
|
import { shouldNeverHappen } from './should-never-happen'
|
|
4
|
+
import { switchOn } from './switch-on'
|
|
4
5
|
|
|
5
6
|
export type Let = { start: Token; ident: Ident; value: AstNode }
|
|
6
7
|
|
|
8
|
+
export type Import = {
|
|
9
|
+
start: Token
|
|
10
|
+
ident: Ident
|
|
11
|
+
pathToImportFrom: Token
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Literal = {
|
|
15
|
+
tag: 'literal'
|
|
16
|
+
type: 'str' | 'bool' | 'num' | 'sink' | 'sink!' | 'sink!!'
|
|
17
|
+
t: Token
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
export type ObjectLiteralPart =
|
|
8
21
|
| { tag: 'hardName'; k: Ident; v: AstNode }
|
|
9
22
|
| { tag: 'computedName'; k: AstNode; v: AstNode }
|
|
@@ -23,8 +36,16 @@ export type Lambda = {
|
|
|
23
36
|
body: AstNode
|
|
24
37
|
}
|
|
25
38
|
|
|
39
|
+
export type Unit = {
|
|
40
|
+
tag: 'unit'
|
|
41
|
+
imports: Import[]
|
|
42
|
+
expression: AstNode
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
export type AstNode =
|
|
27
46
|
| Ident
|
|
47
|
+
| Literal
|
|
48
|
+
| Unit
|
|
28
49
|
| {
|
|
29
50
|
start: Token
|
|
30
51
|
tag: 'arrayLiteral'
|
|
@@ -37,11 +58,6 @@ export type AstNode =
|
|
|
37
58
|
parts: ObjectLiteralPart[]
|
|
38
59
|
end: Token
|
|
39
60
|
}
|
|
40
|
-
| {
|
|
41
|
-
tag: 'literal'
|
|
42
|
-
type: 'str' | 'bool' | 'num' | 'sink' | 'sink!' | 'sink!!'
|
|
43
|
-
t: Token
|
|
44
|
-
}
|
|
45
61
|
| {
|
|
46
62
|
tag: 'binaryOperator'
|
|
47
63
|
operator: '+' | '-' | '*' | '/' | '**' | '%' | '&&' | '||' | '>' | '<' | '>=' | '<=' | '==' | '!=' | '??'
|
|
@@ -82,6 +98,12 @@ export type AstNode =
|
|
|
82
98
|
receiver: AstNode
|
|
83
99
|
index: AstNode
|
|
84
100
|
}
|
|
101
|
+
| {
|
|
102
|
+
// A sepcial AST node meant to be generated internally (needed for exporting definition from one unit to another).
|
|
103
|
+
// Not intended to be parsed from source code. Hence, it is effectively empty, and its location cannot be
|
|
104
|
+
// determined.
|
|
105
|
+
tag: 'export*'
|
|
106
|
+
}
|
|
85
107
|
|
|
86
108
|
export function show(ast: AstNode | AstNode[]): string {
|
|
87
109
|
if (Array.isArray(ast)) {
|
|
@@ -105,11 +127,12 @@ export function show(ast: AstNode | AstNode[]): string {
|
|
|
105
127
|
if (ast.tag === 'binaryOperator') {
|
|
106
128
|
return `(${show(ast.lhs)} ${ast.operator} ${show(ast.rhs)})`
|
|
107
129
|
}
|
|
108
|
-
|
|
109
130
|
if (ast.tag === 'dot') {
|
|
110
131
|
return `${show(ast.receiver)}.${show(ast.ident)}`
|
|
111
132
|
}
|
|
112
|
-
|
|
133
|
+
if (ast.tag === 'export*') {
|
|
134
|
+
return `(export*)`
|
|
135
|
+
}
|
|
113
136
|
if (ast.tag === 'functionCall') {
|
|
114
137
|
return `${show(ast.callee)}(${show(ast.actualArgs)})`
|
|
115
138
|
}
|
|
@@ -126,7 +149,14 @@ export function show(ast: AstNode | AstNode[]): string {
|
|
|
126
149
|
return `fun (${show(ast.formalArgs)}) ${show(ast.body)}`
|
|
127
150
|
}
|
|
128
151
|
if (ast.tag === 'literal') {
|
|
129
|
-
return ast.
|
|
152
|
+
return switchOn(ast.type, {
|
|
153
|
+
bool: () => ast.t.text,
|
|
154
|
+
num: () => ast.t.text,
|
|
155
|
+
sink: () => 'sink',
|
|
156
|
+
'sink!': () => 'sink!',
|
|
157
|
+
'sink!!': () => 'sink!!',
|
|
158
|
+
str: () => `'${ast.t.text}'`,
|
|
159
|
+
})
|
|
130
160
|
}
|
|
131
161
|
if (ast.tag === 'objectLiteral') {
|
|
132
162
|
const pairs = ast.parts.map(p => {
|
|
@@ -153,6 +183,12 @@ export function show(ast: AstNode | AstNode[]): string {
|
|
|
153
183
|
if (ast.tag === 'unaryOperator') {
|
|
154
184
|
return `${ast.operator}${show(ast.operand)}`
|
|
155
185
|
}
|
|
186
|
+
if (ast.tag === 'unit') {
|
|
187
|
+
const imports = ast.imports
|
|
188
|
+
.map(imp => `import * as ${show(imp.ident)} from '${imp.pathToImportFrom.text}';`)
|
|
189
|
+
.join('\n')
|
|
190
|
+
return `${imports ? imports + '\n' : ''}${show(ast.expression)}`
|
|
191
|
+
}
|
|
156
192
|
|
|
157
193
|
shouldNeverHappen(ast)
|
|
158
194
|
}
|
|
@@ -176,6 +212,9 @@ export function span(ast: AstNode): Span {
|
|
|
176
212
|
if (ast.tag === 'ident') {
|
|
177
213
|
return ofToken(ast.t)
|
|
178
214
|
}
|
|
215
|
+
if (ast.tag === 'export*') {
|
|
216
|
+
return { from: { offset: 0 }, to: { offset: 0 } }
|
|
217
|
+
}
|
|
179
218
|
if (ast.tag === 'if') {
|
|
180
219
|
return ofRange(span(ast.condition), span(ast.negative))
|
|
181
220
|
}
|
|
@@ -200,6 +239,11 @@ export function span(ast: AstNode): Span {
|
|
|
200
239
|
if (ast.tag === 'unaryOperator') {
|
|
201
240
|
return ofRange(ofToken(ast.operatorToken), span(ast.operand))
|
|
202
241
|
}
|
|
242
|
+
if (ast.tag === 'unit') {
|
|
243
|
+
const i0 = ast.imports.find(Boolean)
|
|
244
|
+
const exp = span(ast.expression)
|
|
245
|
+
return ofRange(i0 ? ofToken(i0.start) : exp, exp)
|
|
246
|
+
}
|
|
203
247
|
|
|
204
248
|
shouldNeverHappen(ast)
|
|
205
249
|
}
|
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) {
|
|
@@ -266,11 +307,15 @@ export class Parser {
|
|
|
266
307
|
while (true) {
|
|
267
308
|
const arg = this.expression()
|
|
268
309
|
actualArgs.push(arg)
|
|
269
|
-
|
|
310
|
+
let end = this.scanner.consumeIf(')')
|
|
270
311
|
if (end) {
|
|
271
312
|
return { actualArgs, end }
|
|
272
313
|
}
|
|
273
314
|
this.scanner.consume(',')
|
|
315
|
+
end = this.scanner.consumeIf(')')
|
|
316
|
+
if (end) {
|
|
317
|
+
return { actualArgs, end }
|
|
318
|
+
}
|
|
274
319
|
}
|
|
275
320
|
}
|
|
276
321
|
|
|
@@ -310,17 +355,29 @@ export class Parser {
|
|
|
310
355
|
}
|
|
311
356
|
|
|
312
357
|
literalOrIdent(): AstNode {
|
|
313
|
-
|
|
358
|
+
const ret = this.maybeLiteral() ?? this.maybeIdentifier()
|
|
359
|
+
if (!ret) {
|
|
360
|
+
throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
|
|
361
|
+
}
|
|
362
|
+
return ret
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
maybeLiteral(): AstNode | undefined {
|
|
366
|
+
return this.maybePrimitiveLiteral() ?? this.maybeCompositeLiteral()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
maybePrimitiveLiteral(): Literal | undefined {
|
|
370
|
+
let t = this.scanner.consumeIf('sink!!') || this.scanner.consumeIf('undefined!!')
|
|
314
371
|
if (t) {
|
|
315
372
|
return { tag: 'literal', type: 'sink!!', t }
|
|
316
373
|
}
|
|
317
374
|
|
|
318
|
-
t = this.scanner.consumeIf('sink!')
|
|
375
|
+
t = this.scanner.consumeIf('sink!') || this.scanner.consumeIf('undefined!')
|
|
319
376
|
if (t) {
|
|
320
377
|
return { tag: 'literal', type: 'sink!', t }
|
|
321
378
|
}
|
|
322
379
|
|
|
323
|
-
t = this.scanner.consumeIf('sink')
|
|
380
|
+
t = this.scanner.consumeIf('sink') || this.scanner.consumeIf('undefined')
|
|
324
381
|
if (t) {
|
|
325
382
|
return { tag: 'literal', type: 'sink', t }
|
|
326
383
|
}
|
|
@@ -353,7 +410,11 @@ export class Parser {
|
|
|
353
410
|
return { tag: 'literal', type: 'str', t }
|
|
354
411
|
}
|
|
355
412
|
|
|
356
|
-
|
|
413
|
+
return undefined
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
maybeCompositeLiteral(): AstNode | undefined {
|
|
417
|
+
let t = this.scanner.consumeIf('[')
|
|
357
418
|
if (t) {
|
|
358
419
|
return this.arrayBody(t)
|
|
359
420
|
}
|
|
@@ -363,12 +424,7 @@ export class Parser {
|
|
|
363
424
|
return this.objectBody(t)
|
|
364
425
|
}
|
|
365
426
|
|
|
366
|
-
|
|
367
|
-
if (ident) {
|
|
368
|
-
return ident
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
|
|
427
|
+
return undefined
|
|
372
428
|
}
|
|
373
429
|
|
|
374
430
|
/**
|
|
@@ -384,6 +440,13 @@ export class Parser {
|
|
|
384
440
|
|
|
385
441
|
const parts: ArrayLiteralPart[] = []
|
|
386
442
|
while (true) {
|
|
443
|
+
if (this.scanner.consumeIf(',')) {
|
|
444
|
+
const end = this.scanner.consumeIf(']')
|
|
445
|
+
if (end) {
|
|
446
|
+
return { tag: 'arrayLiteral', start, parts, end }
|
|
447
|
+
}
|
|
448
|
+
continue
|
|
449
|
+
}
|
|
387
450
|
if (this.scanner.consumeIf('...')) {
|
|
388
451
|
parts.push({ tag: 'spread', v: this.expression() })
|
|
389
452
|
} else {
|
|
@@ -391,12 +454,16 @@ export class Parser {
|
|
|
391
454
|
parts.push({ tag: 'element', v: exp })
|
|
392
455
|
}
|
|
393
456
|
|
|
394
|
-
|
|
457
|
+
let end = this.scanner.consumeIf(']')
|
|
395
458
|
if (end) {
|
|
396
459
|
return { tag: 'arrayLiteral', start, parts, end }
|
|
397
460
|
}
|
|
398
461
|
|
|
399
462
|
this.scanner.consume(',')
|
|
463
|
+
end = this.scanner.consumeIf(']')
|
|
464
|
+
if (end) {
|
|
465
|
+
return { tag: 'arrayLiteral', start, parts, end }
|
|
466
|
+
}
|
|
400
467
|
}
|
|
401
468
|
}
|
|
402
469
|
|
package/src/runtime.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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
4
|
import { shouldNeverHappen } from './should-never-happen'
|
|
@@ -31,6 +31,12 @@ class SymbolFrame implements SymbolTable {
|
|
|
31
31
|
ret[this.symbol] = this.placeholder.destination?.export() ?? failMe(`Unbounded symbol: ${this.symbol}`)
|
|
32
32
|
return ret
|
|
33
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
|
+
}
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
class EmptySymbolTable implements SymbolTable {
|
|
@@ -41,6 +47,10 @@ class EmptySymbolTable implements SymbolTable {
|
|
|
41
47
|
export() {
|
|
42
48
|
return {}
|
|
43
49
|
}
|
|
50
|
+
|
|
51
|
+
exportValue(): Record<string, Value> {
|
|
52
|
+
return {}
|
|
53
|
+
}
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
export type Verbosity = 'quiet' | 'trace'
|
|
@@ -50,10 +60,12 @@ export class Runtime {
|
|
|
50
60
|
constructor(
|
|
51
61
|
private readonly root: AstNode,
|
|
52
62
|
private readonly verbosity: Verbosity = 'quiet',
|
|
53
|
-
private readonly preimports: Record<string, Value
|
|
63
|
+
private readonly preimports: Record<string, Value>,
|
|
64
|
+
private readonly getAstOf: (fileName: string) => Unit,
|
|
65
|
+
private readonly args: Record<string, unknown>,
|
|
54
66
|
) {}
|
|
55
67
|
|
|
56
|
-
|
|
68
|
+
private buildInitialSymbolTable(generateTheArgsObject: boolean) {
|
|
57
69
|
const empty = new EmptySymbolTable()
|
|
58
70
|
|
|
59
71
|
const keys = Value.foreign(o => o.keys())
|
|
@@ -61,12 +73,19 @@ export class Runtime {
|
|
|
61
73
|
const fromEntries = Value.foreign(o => o.fromEntries())
|
|
62
74
|
let lib = new SymbolFrame('Object', { destination: Value.obj({ keys, entries, fromEntries }) }, empty)
|
|
63
75
|
|
|
76
|
+
if (generateTheArgsObject) {
|
|
77
|
+
lib = new SymbolFrame('args', { destination: Value.from(this.args) }, lib)
|
|
78
|
+
}
|
|
79
|
+
|
|
64
80
|
for (const [importName, importValue] of Object.entries(this.preimports)) {
|
|
65
81
|
lib = new SymbolFrame(importName, { destination: importValue }, lib)
|
|
66
82
|
}
|
|
83
|
+
return lib
|
|
84
|
+
}
|
|
67
85
|
|
|
86
|
+
compute() {
|
|
68
87
|
try {
|
|
69
|
-
const value = this.evalNode(this.root,
|
|
88
|
+
const value = this.evalNode(this.root, this.buildInitialSymbolTable(true))
|
|
70
89
|
return { value }
|
|
71
90
|
} catch (e) {
|
|
72
91
|
const trace: AstNode[] = []
|
|
@@ -99,7 +118,50 @@ export class Runtime {
|
|
|
99
118
|
return ret
|
|
100
119
|
}
|
|
101
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
|
+
|
|
102
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
|
+
}
|
|
103
165
|
if (ast.tag === 'topLevelExpression') {
|
|
104
166
|
let newTable = table
|
|
105
167
|
for (const def of ast.definitions) {
|
|
@@ -113,6 +175,10 @@ export class Runtime {
|
|
|
113
175
|
return this.evalNode(ast.computation, newTable)
|
|
114
176
|
}
|
|
115
177
|
|
|
178
|
+
if (ast.tag === 'export*') {
|
|
179
|
+
return Value.obj(table.exportValue())
|
|
180
|
+
}
|
|
181
|
+
|
|
116
182
|
if (ast.tag === 'binaryOperator') {
|
|
117
183
|
const lhs = this.evalNode(ast.lhs, table)
|
|
118
184
|
if (ast.operator === '||') {
|
package/src/scanner.ts
CHANGED
package/src/septima.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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'
|
|
@@ -27,13 +28,13 @@ export class Septima {
|
|
|
27
28
|
* @param options
|
|
28
29
|
* @returns the value that `input` evaluates to
|
|
29
30
|
*/
|
|
30
|
-
static run(input: string, options?: Options): unknown {
|
|
31
|
+
static run(input: string, options?: Options, args: Record<string, unknown> = {}): unknown {
|
|
31
32
|
const onSink =
|
|
32
33
|
options?.onSink ??
|
|
33
34
|
((r: ResultSink) => {
|
|
34
35
|
throw new Error(r.message)
|
|
35
36
|
})
|
|
36
|
-
const res = new Septima(
|
|
37
|
+
const res = new Septima().compute(input, {}, 'quiet', args)
|
|
37
38
|
if (res.tag === 'ok') {
|
|
38
39
|
return res.value
|
|
39
40
|
}
|
|
@@ -45,13 +46,28 @@ export class Septima {
|
|
|
45
46
|
shouldNeverHappen(res)
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
constructor(
|
|
49
|
+
constructor() {}
|
|
49
50
|
|
|
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 {
|
|
51
67
|
const lib: Record<string, Value> = {}
|
|
52
|
-
for (const [importName, importCode] of Object.entries(
|
|
68
|
+
for (const [importName, importCode] of Object.entries(preimports)) {
|
|
53
69
|
const sourceCode = new SourceCode(importCode)
|
|
54
|
-
const value = this.computeImpl(sourceCode, verbosity, {})
|
|
70
|
+
const value = this.computeImpl(sourceCode, verbosity, {}, undefined, {})
|
|
55
71
|
if (value.isSink()) {
|
|
56
72
|
// TODO(imaman): cover!
|
|
57
73
|
const r = new ResultSinkImpl(value, sourceCode)
|
|
@@ -60,20 +76,33 @@ export class Septima {
|
|
|
60
76
|
lib[importName] = value
|
|
61
77
|
}
|
|
62
78
|
|
|
63
|
-
const sourceCode = new SourceCode(
|
|
64
|
-
const value = this.computeImpl(sourceCode, verbosity, lib)
|
|
79
|
+
const sourceCode = new SourceCode(input)
|
|
80
|
+
const value = this.computeImpl(sourceCode, verbosity, lib, undefined, args)
|
|
65
81
|
if (!value.isSink()) {
|
|
66
82
|
return { value: value.export(), tag: 'ok' }
|
|
67
83
|
}
|
|
68
84
|
return new ResultSinkImpl(value, sourceCode)
|
|
69
85
|
}
|
|
70
86
|
|
|
71
|
-
private computeImpl(
|
|
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
|
+
) {
|
|
72
94
|
const scanner = new Scanner(sourceCode)
|
|
73
95
|
const parser = new Parser(scanner)
|
|
74
|
-
|
|
75
96
|
const ast = parse(parser)
|
|
76
|
-
|
|
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)
|
|
77
106
|
const c = runtime.compute()
|
|
78
107
|
|
|
79
108
|
if (c.value) {
|
|
@@ -85,7 +114,7 @@ export class Septima {
|
|
|
85
114
|
}
|
|
86
115
|
}
|
|
87
116
|
|
|
88
|
-
export function parse(arg: string | Parser) {
|
|
117
|
+
export function parse(arg: string | Parser): Unit {
|
|
89
118
|
const parser = typeof arg === 'string' ? new Parser(new Scanner(new SourceCode(arg))) : arg
|
|
90
119
|
const ast = parser.parse()
|
|
91
120
|
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,41 @@
|
|
|
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>, args: Record<string, unknown> = {}) {
|
|
8
|
+
const septima = new Septima()
|
|
9
|
+
const res = septima.computeModule(mainModule, (m: string) => inputs[m], args)
|
|
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
|
+
test('support the passing of args into the runtime', async () => {
|
|
34
|
+
expect(await run('a', { a: `args.x * args.y` }, { x: 5, y: 9 })).toEqual(45)
|
|
35
|
+
})
|
|
36
|
+
test('the args object is available only at the main module', async () => {
|
|
37
|
+
await expect(
|
|
38
|
+
run('a', { a: `import * as b from 'b'; args.x + '_' + b.foo`, b: `let foo = args.x ?? 'N/A'; {}` }, { x: 'Red' }),
|
|
39
|
+
).rejects.toThrowError(/Symbol args was not found when evaluating/)
|
|
40
|
+
})
|
|
41
|
+
})
|