septima-lang 0.0.4 → 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 +7 -2
- package/dist/src/parser.js +54 -8
- package/dist/src/runtime.d.ts +5 -2
- package/dist/src/runtime.js +57 -4
- package/dist/src/scanner.d.ts +1 -1
- package/dist/src/scanner.js +1 -1
- package/dist/src/septima.d.ts +5 -5
- package/dist/src/septima.js +25 -13
- 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 +18 -9
- 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 +62 -10
- package/src/runtime.ts +65 -4
- package/src/scanner.ts +1 -1
- package/src/septima.ts +33 -11
- 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 +19 -8
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) {
|
|
@@ -310,6 +351,18 @@ export class Parser {
|
|
|
310
351
|
}
|
|
311
352
|
|
|
312
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 {
|
|
313
366
|
let t = this.scanner.consumeIf('sink!!')
|
|
314
367
|
if (t) {
|
|
315
368
|
return { tag: 'literal', type: 'sink!!', t }
|
|
@@ -353,7 +406,11 @@ export class Parser {
|
|
|
353
406
|
return { tag: 'literal', type: 'str', t }
|
|
354
407
|
}
|
|
355
408
|
|
|
356
|
-
|
|
409
|
+
return undefined
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
maybeCompositeLiteral(): AstNode | undefined {
|
|
413
|
+
let t = this.scanner.consumeIf('[')
|
|
357
414
|
if (t) {
|
|
358
415
|
return this.arrayBody(t)
|
|
359
416
|
}
|
|
@@ -363,12 +420,7 @@ export class Parser {
|
|
|
363
420
|
return this.objectBody(t)
|
|
364
421
|
}
|
|
365
422
|
|
|
366
|
-
|
|
367
|
-
if (ident) {
|
|
368
|
-
return ident
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
|
|
423
|
+
return undefined
|
|
372
424
|
}
|
|
373
425
|
|
|
374
426
|
/**
|
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,11 @@ 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,
|
|
54
65
|
) {}
|
|
55
66
|
|
|
56
|
-
|
|
67
|
+
private buildInitialSymbolTable() {
|
|
57
68
|
const empty = new EmptySymbolTable()
|
|
58
69
|
|
|
59
70
|
const keys = Value.foreign(o => o.keys())
|
|
@@ -64,9 +75,12 @@ export class Runtime {
|
|
|
64
75
|
for (const [importName, importValue] of Object.entries(this.preimports)) {
|
|
65
76
|
lib = new SymbolFrame(importName, { destination: importValue }, lib)
|
|
66
77
|
}
|
|
78
|
+
return lib
|
|
79
|
+
}
|
|
67
80
|
|
|
81
|
+
compute() {
|
|
68
82
|
try {
|
|
69
|
-
const value = this.evalNode(this.root,
|
|
83
|
+
const value = this.evalNode(this.root, this.buildInitialSymbolTable())
|
|
70
84
|
return { value }
|
|
71
85
|
} catch (e) {
|
|
72
86
|
const trace: AstNode[] = []
|
|
@@ -99,7 +113,50 @@ export class Runtime {
|
|
|
99
113
|
return ret
|
|
100
114
|
}
|
|
101
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
|
+
|
|
102
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
|
+
}
|
|
103
160
|
if (ast.tag === 'topLevelExpression') {
|
|
104
161
|
let newTable = table
|
|
105
162
|
for (const def of ast.definitions) {
|
|
@@ -113,6 +170,10 @@ export class Runtime {
|
|
|
113
170
|
return this.evalNode(ast.computation, newTable)
|
|
114
171
|
}
|
|
115
172
|
|
|
173
|
+
if (ast.tag === 'export*') {
|
|
174
|
+
return Value.obj(table.exportValue())
|
|
175
|
+
}
|
|
176
|
+
|
|
116
177
|
if (ast.tag === 'binaryOperator') {
|
|
117
178
|
const lhs = this.evalNode(ast.lhs, table)
|
|
118
179
|
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'
|
|
@@ -33,7 +34,7 @@ export class Septima {
|
|
|
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)
|
|
37
38
|
if (res.tag === 'ok') {
|
|
38
39
|
return res.value
|
|
39
40
|
}
|
|
@@ -45,13 +46,23 @@ 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): 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)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
compute(input: string, preimports: Record<string, string> = {}, verbosity: Verbosity = 'quiet'): Result {
|
|
51
62
|
const lib: Record<string, Value> = {}
|
|
52
|
-
for (const [importName, importCode] of Object.entries(
|
|
63
|
+
for (const [importName, importCode] of Object.entries(preimports)) {
|
|
53
64
|
const sourceCode = new SourceCode(importCode)
|
|
54
|
-
const value = this.computeImpl(sourceCode, verbosity, {})
|
|
65
|
+
const value = this.computeImpl(sourceCode, verbosity, {}, undefined)
|
|
55
66
|
if (value.isSink()) {
|
|
56
67
|
// TODO(imaman): cover!
|
|
57
68
|
const r = new ResultSinkImpl(value, sourceCode)
|
|
@@ -60,20 +71,31 @@ export class Septima {
|
|
|
60
71
|
lib[importName] = value
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
const sourceCode = new SourceCode(
|
|
64
|
-
const value = this.computeImpl(sourceCode, verbosity, lib)
|
|
74
|
+
const sourceCode = new SourceCode(input)
|
|
75
|
+
const value = this.computeImpl(sourceCode, verbosity, lib, undefined)
|
|
65
76
|
if (!value.isSink()) {
|
|
66
77
|
return { value: value.export(), tag: 'ok' }
|
|
67
78
|
}
|
|
68
79
|
return new ResultSinkImpl(value, sourceCode)
|
|
69
80
|
}
|
|
70
81
|
|
|
71
|
-
private computeImpl(
|
|
82
|
+
private computeImpl(
|
|
83
|
+
sourceCode: SourceCode,
|
|
84
|
+
verbosity: Verbosity,
|
|
85
|
+
lib: Record<string, Value>,
|
|
86
|
+
moduleReader: undefined | ((m: string) => string),
|
|
87
|
+
) {
|
|
72
88
|
const scanner = new Scanner(sourceCode)
|
|
73
89
|
const parser = new Parser(scanner)
|
|
74
|
-
|
|
75
90
|
const ast = parse(parser)
|
|
76
|
-
|
|
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)
|
|
77
99
|
const c = runtime.compute()
|
|
78
100
|
|
|
79
101
|
if (c.value) {
|
|
@@ -85,7 +107,7 @@ export class Septima {
|
|
|
85
107
|
}
|
|
86
108
|
}
|
|
87
109
|
|
|
88
|
-
export function parse(arg: string | Parser) {
|
|
110
|
+
export function parse(arg: string | Parser): Unit {
|
|
89
111
|
const parser = typeof arg === 'string' ? new Parser(new Scanner(new SourceCode(arg))) : arg
|
|
90
112
|
const ast = parser.parse()
|
|
91
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
|
+
})
|
package/tests/septima.spec.ts
CHANGED
|
@@ -15,8 +15,8 @@ function run(input: string) {
|
|
|
15
15
|
* @param input the Septima program to run
|
|
16
16
|
*/
|
|
17
17
|
function runSink(input: string) {
|
|
18
|
-
const septima = new Septima(
|
|
19
|
-
const res = septima.compute()
|
|
18
|
+
const septima = new Septima()
|
|
19
|
+
const res = septima.compute(input)
|
|
20
20
|
|
|
21
21
|
if (res.tag !== 'sink') {
|
|
22
22
|
throw new Error(`Not a sink: ${res.value}`)
|
|
@@ -84,6 +84,7 @@ describe('septima', () => {
|
|
|
84
84
|
const expected = [
|
|
85
85
|
`value type error: expected num but found "zxcvbnm" when evaluating:`,
|
|
86
86
|
` at (1:1..21) 9 * 8 * 'zxcvbnm' * 7`,
|
|
87
|
+
` at (1:1..21) 9 * 8 * 'zxcvbnm' * 7`,
|
|
87
88
|
` at (1:5..21) 8 * 'zxcvbnm' * 7`,
|
|
88
89
|
` at (1:10..21) zxcvbnm' * 7`,
|
|
89
90
|
].join('\n')
|
|
@@ -612,6 +613,7 @@ describe('septima', () => {
|
|
|
612
613
|
test(`captures the expression trace at runtime`, () => {
|
|
613
614
|
expect(runSink(`1000 + 2000 + 3000 + sink!`).trace).toEqual(
|
|
614
615
|
[
|
|
616
|
+
` at (1:1..26) 1000 + 2000 + 3000 + sink!`,
|
|
615
617
|
` at (1:1..26) 1000 + 2000 + 3000 + sink!`,
|
|
616
618
|
` at (1:8..26) 2000 + 3000 + sink!`,
|
|
617
619
|
` at (1:15..26) 3000 + sink!`,
|
|
@@ -632,6 +634,7 @@ describe('septima', () => {
|
|
|
632
634
|
expect(Object.keys(actual.symbols ?? {})).toEqual(['Object', 'a', 'f', 'x', 'y'])
|
|
633
635
|
expect(actual.trace).toEqual(
|
|
634
636
|
[
|
|
637
|
+
` at (1:1..58) let a = 2; let f = fun(x, y) x * y * sink!! * a; f(30, 40)`,
|
|
635
638
|
` at (1:1..58) let a = 2; let f = fun(x, y) x * y * sink!! * a; f(30, 40)`,
|
|
636
639
|
` at (1:50..58) f(30, 40)`,
|
|
637
640
|
` at (1:30..47) x * y * sink!! * a`,
|
|
@@ -794,17 +797,23 @@ describe('septima', () => {
|
|
|
794
797
|
})
|
|
795
798
|
describe('preimport', () => {
|
|
796
799
|
test('definitions from a preimported file can be used', () => {
|
|
797
|
-
const septima = new Septima(
|
|
800
|
+
const septima = new Septima()
|
|
801
|
+
|
|
802
|
+
const input = `libA.plus10(4) + libA.plus20(2)`
|
|
803
|
+
const preimports = {
|
|
798
804
|
libA: `{ plus10: fun (n) n+10, plus20: fun (n) n+20}`,
|
|
799
|
-
}
|
|
800
|
-
expect(septima.compute()).toMatchObject({ value: 36 })
|
|
805
|
+
}
|
|
806
|
+
expect(septima.compute(input, preimports)).toMatchObject({ value: 36 })
|
|
801
807
|
})
|
|
802
808
|
test('supports multiple preimports', () => {
|
|
803
|
-
const septima = new Septima(
|
|
809
|
+
const septima = new Septima()
|
|
810
|
+
|
|
811
|
+
const input = `a.calc(4) + b.calc(1)`
|
|
812
|
+
const preimports = {
|
|
804
813
|
a: `{ calc: fun (n) n+10 }`,
|
|
805
814
|
b: `{ calc: fun (n) n+20 }`,
|
|
806
|
-
}
|
|
807
|
-
expect(septima.compute()).toMatchObject({ value: 35 })
|
|
815
|
+
}
|
|
816
|
+
expect(septima.compute(input, preimports)).toMatchObject({ value: 35 })
|
|
808
817
|
})
|
|
809
818
|
})
|
|
810
819
|
test.todo('support file names in locations')
|
|
@@ -825,4 +834,6 @@ describe('septima', () => {
|
|
|
825
834
|
test.todo('number methods')
|
|
826
835
|
test.todo('drop the fun () notation and use just arrow functions')
|
|
827
836
|
test.todo('proper internal representation of arrow function, in particular: show(), span()')
|
|
837
|
+
test.todo('sink sinkifies arrays and objects it is stored at')
|
|
838
|
+
test.todo('{foo}')
|
|
828
839
|
})
|