septima-lang 0.0.21 → 0.2.0
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/README.md +435 -0
- package/change-log.md +57 -0
- package/dist/src/ast-node.d.ts +13 -0
- package/dist/src/ast-node.js +18 -1
- package/dist/src/parser.d.ts +1 -0
- package/dist/src/parser.js +75 -5
- package/dist/src/runtime.js +20 -1
- package/dist/tests/parser.spec.d.ts +1 -0
- package/dist/tests/parser.spec.js +75 -0
- package/dist/tests/septima-compile.spec.d.ts +1 -0
- package/dist/tests/septima-compile.spec.js +118 -0
- package/dist/tests/septima.spec.d.ts +1 -0
- package/dist/tests/septima.spec.js +1090 -0
- package/dist/tests/value.spec.d.ts +1 -0
- package/dist/tests/value.spec.js +263 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +3 -3
- package/src/a.js +66 -0
- package/src/ast-node.ts +340 -0
- package/src/extract-message.ts +5 -0
- package/src/fail-me.ts +7 -0
- package/src/find-array-method.ts +124 -0
- package/src/find-string-method.ts +84 -0
- package/src/index.ts +1 -0
- package/src/location.ts +13 -0
- package/src/parser.ts +698 -0
- package/src/result.ts +54 -0
- package/src/runtime.ts +462 -0
- package/src/scanner.ts +136 -0
- package/src/septima.ts +218 -0
- package/src/should-never-happen.ts +4 -0
- package/src/source-code.ts +101 -0
- package/src/stack.ts +18 -0
- package/src/switch-on.ts +4 -0
- package/src/symbol-table.ts +9 -0
- package/src/value.ts +823 -0
- package/tests/parser.spec.ts +81 -0
- package/tests/septima-compile.spec.ts +187 -0
- package/tests/septima.spec.ts +1169 -0
- package/tests/value.spec.ts +291 -0
package/src/result.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AstNode, UnitId } from './ast-node'
|
|
2
|
+
import { failMe } from './fail-me'
|
|
3
|
+
import { Span } from './location'
|
|
4
|
+
import { SourceUnit } from './septima'
|
|
5
|
+
|
|
6
|
+
export type ResultSink = {
|
|
7
|
+
tag: 'sink'
|
|
8
|
+
where: Span | undefined
|
|
9
|
+
trace: string | undefined
|
|
10
|
+
symbols: Record<string, unknown> | undefined
|
|
11
|
+
message: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Result =
|
|
15
|
+
| {
|
|
16
|
+
tag: 'ok'
|
|
17
|
+
value: unknown
|
|
18
|
+
}
|
|
19
|
+
| ResultSink
|
|
20
|
+
|
|
21
|
+
const sourceCode = (unitId: string, unitByUnitId: Map<UnitId, SourceUnit>) =>
|
|
22
|
+
(unitByUnitId.get(unitId) ?? failMe(`source code not found for ${unitId}`)).sourceCode
|
|
23
|
+
|
|
24
|
+
export function formatTrace(trace: AstNode[] | undefined, unitByUnitId: Map<UnitId, SourceUnit>) {
|
|
25
|
+
if (!trace) {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
const format = (ast: AstNode) => sourceCode(ast.unitId, unitByUnitId).formatAst(ast)
|
|
29
|
+
|
|
30
|
+
const spacer = ' '
|
|
31
|
+
const joined = trace
|
|
32
|
+
.map(at => format(at))
|
|
33
|
+
.reverse()
|
|
34
|
+
.join(`\n${spacer}`)
|
|
35
|
+
return joined ? `${spacer}${joined}` : undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// TODO(imaman): generate a stack trace that is similar to node's a-la:
|
|
39
|
+
// $ node a.js
|
|
40
|
+
// /home/imaman/code/imaman/bigband/d.js:3
|
|
41
|
+
// return arg.n.foo()
|
|
42
|
+
// ^
|
|
43
|
+
//
|
|
44
|
+
// TypeError: Cannot read properties of undefined (reading 'foo')
|
|
45
|
+
// at d (/home/imaman/code/imaman/bigband/d.js:3:18)
|
|
46
|
+
// at c (/home/imaman/code/imaman/bigband/c.js:4:15)
|
|
47
|
+
// at b (/home/imaman/code/imaman/bigband/b.js:4:15)
|
|
48
|
+
// at Object.<anonymous> (/home/imaman/code/imaman/bigband/a.js:3:1)
|
|
49
|
+
// at Module._compile (node:internal/modules/cjs/loader:1155:14)
|
|
50
|
+
// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1209:10)
|
|
51
|
+
// at Module.load (node:internal/modules/cjs/loader:1033:32)
|
|
52
|
+
// at Function.Module._load (node:internal/modules/cjs/loader:868:12)
|
|
53
|
+
// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
|
|
54
|
+
// at node:internal/main/run_main_module:22:47
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
|
|
3
|
+
import { AstNode, show, Unit, UnitId } from './ast-node'
|
|
4
|
+
import { extractMessage } from './extract-message'
|
|
5
|
+
import { failMe } from './fail-me'
|
|
6
|
+
import { shouldNeverHappen } from './should-never-happen'
|
|
7
|
+
import * as Stack from './stack'
|
|
8
|
+
import { switchOn } from './switch-on'
|
|
9
|
+
import { SymbolTable, Visibility } from './symbol-table'
|
|
10
|
+
import { Value } from './value'
|
|
11
|
+
|
|
12
|
+
interface Placeholder {
|
|
13
|
+
destination: undefined | Value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class SymbolFrame implements SymbolTable {
|
|
17
|
+
constructor(
|
|
18
|
+
readonly symbol: string,
|
|
19
|
+
readonly placeholder: Placeholder,
|
|
20
|
+
private readonly earlier: SymbolTable,
|
|
21
|
+
private readonly visibility: Visibility,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
lookup(sym: string): Value {
|
|
25
|
+
if (this.symbol === sym) {
|
|
26
|
+
const ret = this.placeholder.destination
|
|
27
|
+
if (ret === undefined) {
|
|
28
|
+
throw new Error(`Unresolved definition: ${this.symbol}`)
|
|
29
|
+
}
|
|
30
|
+
return ret
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return this.earlier.lookup(sym)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export() {
|
|
37
|
+
const ret = this.earlier.export()
|
|
38
|
+
ret[this.symbol] = this.placeholder.destination?.export() ?? failMe(`Unbounded symbol: ${this.symbol}`)
|
|
39
|
+
return ret
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
exportValue(): Record<string, Value> {
|
|
43
|
+
const ret = this.earlier.exportValue()
|
|
44
|
+
if (this.visibility === 'INTERNAL') {
|
|
45
|
+
return ret
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (this.visibility === 'EXPORTED') {
|
|
49
|
+
ret[this.symbol] = this.placeholder.destination ?? failMe(`Unbounded symbol: ${this.symbol}`)
|
|
50
|
+
return ret
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
shouldNeverHappen(this.visibility)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class EmptySymbolTable implements SymbolTable {
|
|
58
|
+
lookup(sym: string): Value {
|
|
59
|
+
throw new Error(`Symbol ${sym} was not found`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export() {
|
|
63
|
+
return {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
exportValue(): Record<string, Value> {
|
|
67
|
+
return {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type Verbosity = 'quiet' | 'trace'
|
|
72
|
+
export type Outputter = (u: unknown) => void
|
|
73
|
+
|
|
74
|
+
export class Runtime {
|
|
75
|
+
private stack: Stack.T = undefined
|
|
76
|
+
constructor(
|
|
77
|
+
private readonly root: AstNode,
|
|
78
|
+
private readonly verbosity: Verbosity = 'quiet',
|
|
79
|
+
private readonly getAstOf: (importerAsPathFromSourceRoot: string, relativePathFromImporter: string) => Unit,
|
|
80
|
+
private readonly args: Record<string, unknown>,
|
|
81
|
+
private readonly consoleLog?: Outputter,
|
|
82
|
+
) {}
|
|
83
|
+
|
|
84
|
+
private output(v: Value) {
|
|
85
|
+
const logger = this.consoleLog ?? console.log // eslint-disable-line no-console
|
|
86
|
+
logger(JSON.stringify(v))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private buildInitialSymbolTable(generateTheArgsObject: boolean) {
|
|
90
|
+
const empty = new EmptySymbolTable()
|
|
91
|
+
|
|
92
|
+
const keys = Value.foreign(o => o.keys())
|
|
93
|
+
const entries = Value.foreign(o => o.entries())
|
|
94
|
+
const fromEntries = Value.foreign(o => o.fromEntries())
|
|
95
|
+
const isArray = Value.foreign(o => o.isArray())
|
|
96
|
+
const log = Value.foreign(o => {
|
|
97
|
+
this.output(o)
|
|
98
|
+
return o
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const parse = Value.foreign(o => JSON.parse(o.toString()))
|
|
102
|
+
const hash224 = Value.foreign(o => crypto.createHash('sha224').update(JSON.stringify(o.unwrap())).digest('hex'))
|
|
103
|
+
|
|
104
|
+
let lib = new SymbolFrame('Object', { destination: Value.obj({ keys, entries, fromEntries }) }, empty, 'INTERNAL')
|
|
105
|
+
lib = new SymbolFrame('String', { destination: Value.foreign(o => Value.str(o.toString())) }, lib, 'INTERNAL')
|
|
106
|
+
lib = new SymbolFrame('Boolean', { destination: Value.foreign(o => Value.bool(o.toBoolean())) }, lib, 'INTERNAL')
|
|
107
|
+
lib = new SymbolFrame('Number', { destination: Value.foreign(o => Value.num(o.toNumber())) }, lib, 'INTERNAL')
|
|
108
|
+
lib = new SymbolFrame('Array', { destination: Value.obj({ isArray }) }, lib, 'INTERNAL')
|
|
109
|
+
lib = new SymbolFrame('console', { destination: Value.obj({ log }) }, lib, 'INTERNAL')
|
|
110
|
+
lib = new SymbolFrame('JSON', { destination: Value.obj({ parse }) }, lib, 'INTERNAL')
|
|
111
|
+
lib = new SymbolFrame('crypto', { destination: Value.obj({ hash224 }) }, lib, 'INTERNAL')
|
|
112
|
+
|
|
113
|
+
if (generateTheArgsObject) {
|
|
114
|
+
lib = new SymbolFrame('args', { destination: Value.from(this.args) }, lib, 'INTERNAL')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return lib
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
compute() {
|
|
121
|
+
try {
|
|
122
|
+
const value = this.evalNode(this.root, this.buildInitialSymbolTable(true))
|
|
123
|
+
return { value }
|
|
124
|
+
} catch (e) {
|
|
125
|
+
const trace: AstNode[] = []
|
|
126
|
+
for (let curr = this.stack; curr; curr = curr?.next) {
|
|
127
|
+
trace.push(curr.ast)
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
expressionTrace: trace,
|
|
131
|
+
errorMessage: extractMessage(e),
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
133
|
+
stack: (e as { stack?: string[] }).stack,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private evalNode(ast: AstNode, table: SymbolTable): Value {
|
|
139
|
+
this.stack = Stack.push(ast, this.stack)
|
|
140
|
+
const ret = this.evalNodeImpl(ast, table)
|
|
141
|
+
switchOn(this.verbosity, {
|
|
142
|
+
quiet: () => {},
|
|
143
|
+
trace: () => {
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.log(`output of <|${show(ast)}|> is ${JSON.stringify(ret)} // ${ast.tag}`)
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
this.stack = Stack.pop(this.stack)
|
|
149
|
+
return ret
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private importDefinitions(importerAsPathFromSourceRoot: UnitId, relativePathFromImporter: string): Value {
|
|
153
|
+
const importee = this.getAstOf(importerAsPathFromSourceRoot, relativePathFromImporter)
|
|
154
|
+
const exp = importee.expression
|
|
155
|
+
if (
|
|
156
|
+
exp.tag === 'arrayLiteral' ||
|
|
157
|
+
exp.tag === 'binaryOperator' ||
|
|
158
|
+
exp.tag === 'dot' ||
|
|
159
|
+
exp.tag === 'export*' ||
|
|
160
|
+
exp.tag === 'functionCall' ||
|
|
161
|
+
exp.tag === 'ident' ||
|
|
162
|
+
exp.tag === 'formalArg' ||
|
|
163
|
+
exp.tag === 'if' ||
|
|
164
|
+
exp.tag === 'ternary' ||
|
|
165
|
+
exp.tag === 'indexAccess' ||
|
|
166
|
+
exp.tag === 'lambda' ||
|
|
167
|
+
exp.tag === 'literal' ||
|
|
168
|
+
exp.tag === 'objectLiteral' ||
|
|
169
|
+
exp.tag === 'templateLiteral' ||
|
|
170
|
+
exp.tag === 'unaryOperator' ||
|
|
171
|
+
exp.tag === 'unit'
|
|
172
|
+
) {
|
|
173
|
+
// TODO(imaman): throw an error on non-exporting unit?
|
|
174
|
+
return Value.obj({})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (exp.tag === 'topLevelExpression') {
|
|
178
|
+
// Construct a syntehtic unit which is similar to importedUnit but override its expression with an expression that
|
|
179
|
+
// just returns the importee's definitions bundled in a single object (an export* expression), and evaluate it.
|
|
180
|
+
// This is the trick that allows the importer to gain access to the importee's own stuff.
|
|
181
|
+
const exporStarUnit: AstNode = {
|
|
182
|
+
tag: 'unit',
|
|
183
|
+
imports: importee.imports,
|
|
184
|
+
unitId: importee.unitId,
|
|
185
|
+
expression: {
|
|
186
|
+
tag: 'topLevelExpression',
|
|
187
|
+
definitions: exp.definitions,
|
|
188
|
+
unitId: importee.unitId,
|
|
189
|
+
computation: { tag: 'export*', unitId: importee.unitId },
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
return this.evalNode(exporStarUnit, this.buildInitialSymbolTable(false))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
shouldNeverHappen(exp)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private evalNodeImpl(ast: AstNode, table: SymbolTable): Value {
|
|
199
|
+
if (ast.tag === 'unit') {
|
|
200
|
+
let newTable = table
|
|
201
|
+
for (const imp of ast.imports) {
|
|
202
|
+
const o = this.importDefinitions(ast.unitId, imp.pathToImportFrom.text)
|
|
203
|
+
newTable = new SymbolFrame(imp.ident.t.text, { destination: o }, newTable, 'INTERNAL')
|
|
204
|
+
}
|
|
205
|
+
return this.evalNode(ast.expression, newTable)
|
|
206
|
+
}
|
|
207
|
+
if (ast.tag === 'topLevelExpression') {
|
|
208
|
+
let newTable = table
|
|
209
|
+
for (const def of ast.definitions) {
|
|
210
|
+
const name = def.ident.t.text
|
|
211
|
+
const placeholder: Placeholder = { destination: undefined }
|
|
212
|
+
newTable = new SymbolFrame(name, placeholder, newTable, def.isExported ? 'EXPORTED' : 'INTERNAL')
|
|
213
|
+
const v = this.evalNode(def.value, newTable)
|
|
214
|
+
placeholder.destination = v
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!ast.computation) {
|
|
218
|
+
return Value.str('')
|
|
219
|
+
}
|
|
220
|
+
const c = this.evalNode(ast.computation, newTable)
|
|
221
|
+
if (ast.throwToken) {
|
|
222
|
+
throw new Error(JSON.stringify(c))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return c
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (ast.tag === 'export*') {
|
|
229
|
+
return Value.obj(table.exportValue())
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (ast.tag === 'binaryOperator') {
|
|
233
|
+
const lhs = this.evalNode(ast.lhs, table)
|
|
234
|
+
if (ast.operator === '||') {
|
|
235
|
+
return lhs.or(() => this.evalNode(ast.rhs, table))
|
|
236
|
+
}
|
|
237
|
+
if (ast.operator === '&&') {
|
|
238
|
+
return lhs.and(() => this.evalNode(ast.rhs, table))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const rhs = this.evalNode(ast.rhs, table)
|
|
242
|
+
if (ast.operator === '!=') {
|
|
243
|
+
return lhs.equalsTo(rhs).not()
|
|
244
|
+
}
|
|
245
|
+
if (ast.operator === '==') {
|
|
246
|
+
return lhs.equalsTo(rhs)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (ast.operator === '<=') {
|
|
250
|
+
const comp = lhs.order(rhs)
|
|
251
|
+
return comp.isToZero('<=')
|
|
252
|
+
}
|
|
253
|
+
if (ast.operator === '<') {
|
|
254
|
+
const comp = lhs.order(rhs)
|
|
255
|
+
return comp.isToZero('<')
|
|
256
|
+
}
|
|
257
|
+
if (ast.operator === '>=') {
|
|
258
|
+
const comp = lhs.order(rhs)
|
|
259
|
+
return comp.isToZero('>=')
|
|
260
|
+
}
|
|
261
|
+
if (ast.operator === '>') {
|
|
262
|
+
const comp = lhs.order(rhs)
|
|
263
|
+
return comp.isToZero('>')
|
|
264
|
+
}
|
|
265
|
+
if (ast.operator === '%') {
|
|
266
|
+
return lhs.modulo(rhs)
|
|
267
|
+
}
|
|
268
|
+
if (ast.operator === '*') {
|
|
269
|
+
return lhs.times(rhs)
|
|
270
|
+
}
|
|
271
|
+
if (ast.operator === '**') {
|
|
272
|
+
return lhs.power(rhs)
|
|
273
|
+
}
|
|
274
|
+
if (ast.operator === '+') {
|
|
275
|
+
return lhs.plus(rhs)
|
|
276
|
+
}
|
|
277
|
+
if (ast.operator === '-') {
|
|
278
|
+
return lhs.minus(rhs)
|
|
279
|
+
}
|
|
280
|
+
if (ast.operator === '/') {
|
|
281
|
+
return lhs.over(rhs)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (ast.operator === '??') {
|
|
285
|
+
return lhs.coalesce(() => this.evalNode(ast.rhs, table))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
shouldNeverHappen(ast.operator)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (ast.tag === 'unaryOperator') {
|
|
292
|
+
const operand = this.evalNode(ast.operand, table)
|
|
293
|
+
if (ast.operator === '!') {
|
|
294
|
+
return operand.not()
|
|
295
|
+
}
|
|
296
|
+
if (ast.operator === '+') {
|
|
297
|
+
// We intentionally do <0 + operand> instead of just <operand>. This is due to type-checking: the latter will
|
|
298
|
+
// evaluate to the operand as-is, making expression such as `+true` dynamically valid (which is not the desired
|
|
299
|
+
// behavior)
|
|
300
|
+
return Value.num(0).plus(operand)
|
|
301
|
+
}
|
|
302
|
+
if (ast.operator === '-') {
|
|
303
|
+
return operand.negate()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
shouldNeverHappen(ast.operator)
|
|
307
|
+
}
|
|
308
|
+
if (ast.tag === 'ident') {
|
|
309
|
+
return table.lookup(ast.t.text)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (ast.tag === 'formalArg') {
|
|
313
|
+
if (ast.defaultValue) {
|
|
314
|
+
return this.evalNode(ast.defaultValue, table)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// This error should not be reached. The call flow should evaluate a formalArg node only when if it has
|
|
318
|
+
// a default value sud-node.
|
|
319
|
+
throw new Error(`no default value for ${ast}`)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (ast.tag === 'literal') {
|
|
323
|
+
if (ast.type === 'bool') {
|
|
324
|
+
// TODO(imaman): stricter checking of 'false'
|
|
325
|
+
return Value.bool(ast.t.text === 'true' ? true : false)
|
|
326
|
+
}
|
|
327
|
+
if (ast.type === 'num') {
|
|
328
|
+
return Value.num(Number(ast.t.text))
|
|
329
|
+
}
|
|
330
|
+
if (ast.type === 'str') {
|
|
331
|
+
return Value.str(ast.t.text)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (ast.type === 'undef') {
|
|
335
|
+
return Value.undef()
|
|
336
|
+
}
|
|
337
|
+
shouldNeverHappen(ast.type)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (ast.tag === 'templateLiteral') {
|
|
341
|
+
const result = ast.parts
|
|
342
|
+
.map(part => {
|
|
343
|
+
if (part.tag === 'string') {
|
|
344
|
+
return part.value
|
|
345
|
+
}
|
|
346
|
+
if (part.tag === 'expression') {
|
|
347
|
+
return this.evalNode(part.expr, table).toString()
|
|
348
|
+
}
|
|
349
|
+
shouldNeverHappen(part)
|
|
350
|
+
})
|
|
351
|
+
.join('')
|
|
352
|
+
return Value.str(result)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (ast.tag === 'arrayLiteral') {
|
|
356
|
+
const arr: Value[] = []
|
|
357
|
+
for (const curr of ast.parts) {
|
|
358
|
+
if (curr.tag === 'element') {
|
|
359
|
+
arr.push(this.evalNode(curr.v, table))
|
|
360
|
+
} else if (curr.tag === 'spread') {
|
|
361
|
+
const v = this.evalNode(curr.v, table)
|
|
362
|
+
if (v.isUndefined()) {
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
arr.push(...v.assertArr())
|
|
366
|
+
} else {
|
|
367
|
+
shouldNeverHappen(curr)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return Value.arr(arr)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (ast.tag === 'objectLiteral') {
|
|
375
|
+
const entries: [string, Value][] = ast.parts.flatMap(at => {
|
|
376
|
+
if (at.tag === 'hardName') {
|
|
377
|
+
return [[at.k.t.text, this.evalNode(at.v, table)]]
|
|
378
|
+
}
|
|
379
|
+
if (at.tag === 'quotedString') {
|
|
380
|
+
return [[at.k.t.text, this.evalNode(at.v, table)]]
|
|
381
|
+
}
|
|
382
|
+
if (at.tag === 'computedName') {
|
|
383
|
+
return [[this.evalNode(at.k, table).assertStr(), this.evalNode(at.v, table)]]
|
|
384
|
+
}
|
|
385
|
+
if (at.tag === 'spread') {
|
|
386
|
+
const o = this.evalNode(at.o, table)
|
|
387
|
+
if (o.isUndefined()) {
|
|
388
|
+
return []
|
|
389
|
+
}
|
|
390
|
+
return Object.entries(o.assertObj())
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
shouldNeverHappen(at)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
// TODO(imaman): verify type of all keys (strings, maybe also numbers)
|
|
397
|
+
return Value.obj(Object.fromEntries(entries.filter(([_, v]) => !v.isUndefined())))
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (ast.tag === 'lambda') {
|
|
401
|
+
return Value.lambda(ast, table)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (ast.tag === 'functionCall') {
|
|
405
|
+
const argValues = ast.actualArgs.map(a => this.evalNode(a, table))
|
|
406
|
+
const callee = this.evalNode(ast.callee, table)
|
|
407
|
+
|
|
408
|
+
return this.call(callee, argValues)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (ast.tag === 'if' || ast.tag === 'ternary') {
|
|
412
|
+
const c = this.evalNode(ast.condition, table)
|
|
413
|
+
return c.ifElse(
|
|
414
|
+
() => this.evalNode(ast.positive, table),
|
|
415
|
+
() => this.evalNode(ast.negative, table),
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (ast.tag === 'dot') {
|
|
420
|
+
const rec = this.evalNode(ast.receiver, table)
|
|
421
|
+
if (rec === undefined || rec === null) {
|
|
422
|
+
throw new Error(`Cannot access attribute .${ast.ident.t.text} of ${rec}`)
|
|
423
|
+
}
|
|
424
|
+
return rec.access(ast.ident.t.text, (callee, args) => this.call(callee, args))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (ast.tag === 'indexAccess') {
|
|
428
|
+
const rec = this.evalNode(ast.receiver, table)
|
|
429
|
+
const index = this.evalNode(ast.index, table)
|
|
430
|
+
return rec.access(index, (callee, args) => this.call(callee, args))
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
shouldNeverHappen(ast)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
call(callee: Value, actualValues: Value[]) {
|
|
437
|
+
return callee.call(actualValues, (formals, body, lambdaTable: SymbolTable) => {
|
|
438
|
+
const requiredCount = formals.filter(f => !f.defaultValue).length
|
|
439
|
+
if (actualValues.length < requiredCount) {
|
|
440
|
+
throw new Error(`Expected at least ${requiredCount} argument(s) but got ${actualValues.length}`)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let newTable = lambdaTable
|
|
444
|
+
for (let i = 0; i < formals.length; ++i) {
|
|
445
|
+
const formal = formals[i]
|
|
446
|
+
let actual = actualValues.at(i)
|
|
447
|
+
const useDefault = actual === undefined || (actual.isUndefined() && formal.defaultValue)
|
|
448
|
+
|
|
449
|
+
if (useDefault && formal.defaultValue) {
|
|
450
|
+
actual = this.evalNode(formal.defaultValue, lambdaTable)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (actual === undefined) {
|
|
454
|
+
throw new Error(`A value must be passed to formal argument: ${show(formal.ident)}`)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
newTable = new SymbolFrame(formal.ident.t.text, { destination: actual }, newTable, 'INTERNAL')
|
|
458
|
+
}
|
|
459
|
+
return this.evalNode(body, newTable)
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
}
|
package/src/scanner.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
type ConsumeIfBasicPattern = RegExp | string
|
|
10
|
+
type ConsumeIfPattern = { either: ConsumeIfBasicPattern[]; noneOf: ConsumeIfBasicPattern[] }
|
|
11
|
+
|
|
12
|
+
export class Scanner {
|
|
13
|
+
constructor(readonly sourceCode: SourceCode, private offset = 0) {
|
|
14
|
+
if (this.offset === 0) {
|
|
15
|
+
this.eatWhitespace()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get sourceRef() {
|
|
20
|
+
return this.sourceCode.sourceRef(this.sourceCode.expandToEndOfLine({ offset: this.offset }))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private curr() {
|
|
24
|
+
return this.sourceCode.input.substring(this.offset)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private eatBlockComment() {
|
|
28
|
+
const startedAt = this.sourceRef
|
|
29
|
+
while (true) {
|
|
30
|
+
if (this.eof()) {
|
|
31
|
+
throw new Error(`Block comment that started at ${startedAt} is missing its closing (*/)`)
|
|
32
|
+
}
|
|
33
|
+
if (this.consumeIf('*/')) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// By default, the . symbol in regexp does _not_ match newline. we use /./s to override the default.
|
|
38
|
+
this.consume(/./s, false)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private eatWhitespace() {
|
|
43
|
+
while (true) {
|
|
44
|
+
if (this.consumeIf(/\s*/, false)) {
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
if (this.consumeIf('//', false)) {
|
|
48
|
+
this.consume(/[^\n]*/, false)
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
if (this.consumeIf('/*', false)) {
|
|
52
|
+
this.eatBlockComment()
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
eof(): boolean {
|
|
61
|
+
return this.offset >= this.sourceCode.input.length
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
synopsis() {
|
|
65
|
+
const c = this.curr()
|
|
66
|
+
let lookingAt = c.substring(0, 20)
|
|
67
|
+
if (lookingAt.length !== c.length) {
|
|
68
|
+
lookingAt = `${lookingAt}...`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
position: this.offset,
|
|
73
|
+
lookingAt,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
headMatches(...patterns: (ConsumeIfPattern | ConsumeIfBasicPattern)[]): boolean {
|
|
78
|
+
const alt = new Scanner(this.sourceCode, this.offset)
|
|
79
|
+
for (const p of patterns) {
|
|
80
|
+
const t = alt.consumeIf(p, true)
|
|
81
|
+
if (t === undefined) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
consume(r: RegExp | string, eatWhitespace = true): Token {
|
|
90
|
+
const text = this.match(r)
|
|
91
|
+
if (text === undefined) {
|
|
92
|
+
throw new Error(`Expected ${r} ${this.sourceRef}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const offset = this.offset
|
|
96
|
+
this.offset += text.length
|
|
97
|
+
|
|
98
|
+
if (eatWhitespace) {
|
|
99
|
+
this.eatWhitespace()
|
|
100
|
+
}
|
|
101
|
+
return { location: { offset }, text }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
consumeIf(p: ConsumeIfBasicPattern | ConsumeIfPattern, eatWhitespace = true): Token | undefined {
|
|
105
|
+
const pattern = typeof p === 'string' ? { either: [p], noneOf: [] } : isRegExp(p) ? { either: [p], noneOf: [] } : p
|
|
106
|
+
|
|
107
|
+
const found = pattern.either.find(r => this.match(r))
|
|
108
|
+
const hasNegative = pattern.noneOf.some(r => this.match(r))
|
|
109
|
+
const ret = found && !hasNegative
|
|
110
|
+
if (!ret) {
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return this.consume(found, eatWhitespace)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private match(r: RegExp | string): string | undefined {
|
|
118
|
+
if (typeof r === 'string') {
|
|
119
|
+
if (this.curr().startsWith(r)) {
|
|
120
|
+
return r
|
|
121
|
+
}
|
|
122
|
+
return undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const m = this.curr().match(r)
|
|
126
|
+
if (m && m.index === 0) {
|
|
127
|
+
return m[0]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isRegExp(u: object): u is RegExp {
|
|
135
|
+
return u.constructor.name === 'RegExp'
|
|
136
|
+
}
|