septima-lang 0.0.1
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 +85 -0
- package/dist/src/ast-node.js +114 -0
- package/dist/src/cdl.d.ts +33 -0
- package/dist/src/cdl.js +63 -0
- package/dist/src/extract-message.d.ts +1 -0
- package/dist/src/extract-message.js +10 -0
- package/dist/src/fail-me.d.ts +1 -0
- package/dist/src/fail-me.js +11 -0
- package/dist/src/find-array-method.d.ts +15 -0
- package/dist/src/find-array-method.js +104 -0
- package/dist/src/find-string-method.d.ts +2 -0
- package/dist/src/find-string-method.js +88 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +18 -0
- package/dist/src/location.d.ts +11 -0
- package/dist/src/location.js +3 -0
- package/dist/src/parser.d.ts +37 -0
- package/dist/src/parser.js +345 -0
- package/dist/src/result.d.ts +24 -0
- package/dist/src/result.js +29 -0
- package/dist/src/runtime.d.ts +25 -0
- package/dist/src/runtime.js +287 -0
- package/dist/src/scanner.d.ts +22 -0
- package/dist/src/scanner.js +76 -0
- package/dist/src/should-never-happen.d.ts +1 -0
- package/dist/src/should-never-happen.js +9 -0
- package/dist/src/source-code.d.ts +19 -0
- package/dist/src/source-code.js +90 -0
- package/dist/src/stack.d.ts +11 -0
- package/dist/src/stack.js +19 -0
- package/dist/src/switch-on.d.ts +1 -0
- package/dist/src/switch-on.js +9 -0
- package/dist/src/symbol-table.d.ts +5 -0
- package/dist/src/symbol-table.js +3 -0
- package/dist/src/value.d.ts +128 -0
- package/dist/src/value.js +634 -0
- package/dist/tests/cdl.spec.d.ts +1 -0
- package/dist/tests/cdl.spec.js +692 -0
- package/dist/tests/parser.spec.d.ts +1 -0
- package/dist/tests/parser.spec.js +39 -0
- package/dist/tests/value.spec.d.ts +1 -0
- package/dist/tests/value.spec.js +355 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/jest-output.json +1 -0
- package/package.json +17 -0
- package/src/ast-node.ts +205 -0
- package/src/cdl.ts +78 -0
- package/src/extract-message.ts +5 -0
- package/src/fail-me.ts +7 -0
- package/src/find-array-method.ts +115 -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 +399 -0
- package/src/result.ts +45 -0
- package/src/runtime.ts +295 -0
- package/src/scanner.ts +94 -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 +6 -0
- package/src/value.ts +742 -0
- package/tests/cdl.spec.ts +755 -0
- package/tests/parser.spec.ts +14 -0
- package/tests/value.spec.ts +387 -0
- package/tsconfig.json +11 -0
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { AstNode, show, span } from './ast-node'
|
|
2
|
+
import { extractMessage } from './extract-message'
|
|
3
|
+
import { failMe } from './fail-me'
|
|
4
|
+
import { Parser } from './parser'
|
|
5
|
+
import { shouldNeverHappen } from './should-never-happen'
|
|
6
|
+
import * as Stack from './stack'
|
|
7
|
+
import { switchOn } from './switch-on'
|
|
8
|
+
import { SymbolTable } from './symbol-table'
|
|
9
|
+
import { Value } from './value'
|
|
10
|
+
|
|
11
|
+
interface Placeholder {
|
|
12
|
+
destination: undefined | Value
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class SymbolFrame implements SymbolTable {
|
|
16
|
+
constructor(readonly symbol: string, readonly placeholder: Placeholder, private readonly earlier: SymbolTable) {}
|
|
17
|
+
|
|
18
|
+
lookup(sym: string): Value {
|
|
19
|
+
if (this.symbol === sym) {
|
|
20
|
+
const ret = this.placeholder.destination
|
|
21
|
+
if (ret === undefined) {
|
|
22
|
+
throw new Error(`Unresolved definition: ${this.symbol}`)
|
|
23
|
+
}
|
|
24
|
+
return ret
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return this.earlier.lookup(sym)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export() {
|
|
31
|
+
const ret = this.earlier.export()
|
|
32
|
+
ret[this.symbol] = this.placeholder.destination?.export() ?? failMe(`Unbounded symbol: ${this.symbol}`)
|
|
33
|
+
return ret
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class EmptySymbolTable implements SymbolTable {
|
|
38
|
+
lookup(sym: string): Value {
|
|
39
|
+
throw new Error(`Symbol ${sym} was not found`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export() {
|
|
43
|
+
return {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type Verbosity = 'quiet' | 'trace'
|
|
48
|
+
|
|
49
|
+
export class Runtime {
|
|
50
|
+
private stack: Stack.T = undefined
|
|
51
|
+
constructor(
|
|
52
|
+
private readonly root: AstNode,
|
|
53
|
+
private readonly verbosity: Verbosity = 'quiet',
|
|
54
|
+
private readonly parser: Parser,
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
compute() {
|
|
58
|
+
const empty = new EmptySymbolTable()
|
|
59
|
+
|
|
60
|
+
const keys = Value.foreign(o => o.keys())
|
|
61
|
+
const entries = Value.foreign(o => o.entries())
|
|
62
|
+
const fromEntries = Value.foreign(o => o.fromEntries())
|
|
63
|
+
const lib = new SymbolFrame('Object', { destination: Value.obj({ keys, entries, fromEntries }) }, empty)
|
|
64
|
+
try {
|
|
65
|
+
const value = this.evalNode(this.root, lib)
|
|
66
|
+
return { value }
|
|
67
|
+
} catch (e) {
|
|
68
|
+
const trace: AstNode[] = []
|
|
69
|
+
for (let curr = this.stack; curr; curr = curr?.next) {
|
|
70
|
+
trace.push(curr.ast)
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
expressionTrace: trace,
|
|
74
|
+
errorMessage: extractMessage(e),
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
76
|
+
stack: (e as { stack?: string[] }).stack,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private evalNode(ast: AstNode, table: SymbolTable): Value {
|
|
82
|
+
this.stack = Stack.push(ast, this.stack)
|
|
83
|
+
let ret = this.evalNodeImpl(ast, table)
|
|
84
|
+
if (ret.isSink() && !ret.span()) {
|
|
85
|
+
ret = ret.bindToSpan(span(ast))
|
|
86
|
+
}
|
|
87
|
+
switchOn(this.verbosity, {
|
|
88
|
+
quiet: () => {},
|
|
89
|
+
trace: () => {
|
|
90
|
+
// eslint-disable-next-line no-console
|
|
91
|
+
console.log(`output of <|${show(ast)}|> is ${JSON.stringify(ret)} // ${ast.tag}`)
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
this.stack = Stack.pop(this.stack)
|
|
95
|
+
return ret
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private evalNodeImpl(ast: AstNode, table: SymbolTable): Value {
|
|
99
|
+
if (ast.tag === 'topLevelExpression') {
|
|
100
|
+
let newTable = table
|
|
101
|
+
for (const def of ast.definitions) {
|
|
102
|
+
const name = def.ident.t.text
|
|
103
|
+
const placeholder: Placeholder = { destination: undefined }
|
|
104
|
+
newTable = new SymbolFrame(name, placeholder, newTable)
|
|
105
|
+
const v = this.evalNode(def.value, newTable)
|
|
106
|
+
placeholder.destination = v
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this.evalNode(ast.computation, newTable)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (ast.tag === 'binaryOperator') {
|
|
113
|
+
const lhs = this.evalNode(ast.lhs, table)
|
|
114
|
+
if (ast.operator === '||') {
|
|
115
|
+
return lhs.or(() => this.evalNode(ast.rhs, table))
|
|
116
|
+
}
|
|
117
|
+
if (ast.operator === '&&') {
|
|
118
|
+
return lhs.and(() => this.evalNode(ast.rhs, table))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (ast.operator === '??') {
|
|
122
|
+
return lhs.unsink(() => this.evalNode(ast.rhs, table))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const rhs = this.evalNode(ast.rhs, table)
|
|
126
|
+
if (ast.operator === '!=') {
|
|
127
|
+
return lhs.equalsTo(rhs).not()
|
|
128
|
+
}
|
|
129
|
+
if (ast.operator === '==') {
|
|
130
|
+
return lhs.equalsTo(rhs)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (ast.operator === '<=') {
|
|
134
|
+
const comp = lhs.order(rhs)
|
|
135
|
+
return comp.isToZero('<=')
|
|
136
|
+
}
|
|
137
|
+
if (ast.operator === '<') {
|
|
138
|
+
const comp = lhs.order(rhs)
|
|
139
|
+
return comp.isToZero('<')
|
|
140
|
+
}
|
|
141
|
+
if (ast.operator === '>=') {
|
|
142
|
+
const comp = lhs.order(rhs)
|
|
143
|
+
return comp.isToZero('>=')
|
|
144
|
+
}
|
|
145
|
+
if (ast.operator === '>') {
|
|
146
|
+
const comp = lhs.order(rhs)
|
|
147
|
+
return comp.isToZero('>')
|
|
148
|
+
}
|
|
149
|
+
if (ast.operator === '%') {
|
|
150
|
+
return lhs.modulo(rhs)
|
|
151
|
+
}
|
|
152
|
+
if (ast.operator === '*') {
|
|
153
|
+
return lhs.times(rhs)
|
|
154
|
+
}
|
|
155
|
+
if (ast.operator === '**') {
|
|
156
|
+
return lhs.power(rhs)
|
|
157
|
+
}
|
|
158
|
+
if (ast.operator === '+') {
|
|
159
|
+
return lhs.plus(rhs)
|
|
160
|
+
}
|
|
161
|
+
if (ast.operator === '-') {
|
|
162
|
+
return lhs.minus(rhs)
|
|
163
|
+
}
|
|
164
|
+
if (ast.operator === '/') {
|
|
165
|
+
return lhs.over(rhs)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
shouldNeverHappen(ast.operator)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (ast.tag === 'unaryOperator') {
|
|
172
|
+
const operand = this.evalNode(ast.operand, table)
|
|
173
|
+
if (ast.operator === '!') {
|
|
174
|
+
return operand.not()
|
|
175
|
+
}
|
|
176
|
+
if (ast.operator === '+') {
|
|
177
|
+
// We intentionally do <0 + operand> instead of just <operand>. This is due to type-checking: the latter will
|
|
178
|
+
// evaluate to the operand as-is, making expression such as `+true` dynamically valid (which is not the desired
|
|
179
|
+
// behavior)
|
|
180
|
+
return Value.num(0).plus(operand)
|
|
181
|
+
}
|
|
182
|
+
if (ast.operator === '-') {
|
|
183
|
+
return operand.negate()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
shouldNeverHappen(ast.operator)
|
|
187
|
+
}
|
|
188
|
+
if (ast.tag === 'ident') {
|
|
189
|
+
return table.lookup(ast.t.text)
|
|
190
|
+
}
|
|
191
|
+
if (ast.tag === 'literal') {
|
|
192
|
+
if (ast.type === 'bool') {
|
|
193
|
+
// TODO(imaman): stricter checking of 'false'
|
|
194
|
+
return Value.bool(ast.t.text === 'true' ? true : false)
|
|
195
|
+
}
|
|
196
|
+
if (ast.type === 'num') {
|
|
197
|
+
return Value.num(Number(ast.t.text))
|
|
198
|
+
}
|
|
199
|
+
if (ast.type === 'sink!!') {
|
|
200
|
+
return Value.sink(undefined, this.stack, table)
|
|
201
|
+
}
|
|
202
|
+
if (ast.type === 'sink!') {
|
|
203
|
+
return Value.sink(undefined, this.stack)
|
|
204
|
+
}
|
|
205
|
+
if (ast.type === 'sink') {
|
|
206
|
+
return Value.sink()
|
|
207
|
+
}
|
|
208
|
+
if (ast.type === 'str') {
|
|
209
|
+
return Value.str(ast.t.text)
|
|
210
|
+
}
|
|
211
|
+
shouldNeverHappen(ast.type)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (ast.tag === 'arrayLiteral') {
|
|
215
|
+
const arr: Value[] = []
|
|
216
|
+
for (const curr of ast.parts) {
|
|
217
|
+
if (curr.tag === 'element') {
|
|
218
|
+
arr.push(this.evalNode(curr.v, table))
|
|
219
|
+
} else if (curr.tag === 'spread') {
|
|
220
|
+
const v = this.evalNode(curr.v, table)
|
|
221
|
+
arr.push(...v.assertArr())
|
|
222
|
+
} else {
|
|
223
|
+
shouldNeverHappen(curr)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return Value.arr(arr)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (ast.tag === 'objectLiteral') {
|
|
231
|
+
const entries: [string, Value][] = ast.parts.flatMap(at => {
|
|
232
|
+
if (at.tag === 'hardName') {
|
|
233
|
+
return [[at.k.t.text, this.evalNode(at.v, table)]]
|
|
234
|
+
}
|
|
235
|
+
if (at.tag === 'computedName') {
|
|
236
|
+
return [[this.evalNode(at.k, table).assertStr(), this.evalNode(at.v, table)]]
|
|
237
|
+
}
|
|
238
|
+
if (at.tag === 'spread') {
|
|
239
|
+
const o = this.evalNode(at.o, table)
|
|
240
|
+
return Object.entries(o.assertObj())
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
shouldNeverHappen(at)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// TODO(imaman): verify type of all keys (strings, maybe also numbers)
|
|
247
|
+
return Value.obj(Object.fromEntries(entries))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (ast.tag === 'lambda') {
|
|
251
|
+
return Value.lambda(ast, table)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (ast.tag === 'functionCall') {
|
|
255
|
+
const argValues = ast.actualArgs.map(a => this.evalNode(a, table))
|
|
256
|
+
const callee = this.evalNode(ast.callee, table)
|
|
257
|
+
|
|
258
|
+
return this.call(callee, argValues)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (ast.tag === 'if') {
|
|
262
|
+
const c = this.evalNode(ast.condition, table)
|
|
263
|
+
return c.ifElse(
|
|
264
|
+
() => this.evalNode(ast.positive, table),
|
|
265
|
+
() => this.evalNode(ast.negative, table),
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (ast.tag === 'dot') {
|
|
270
|
+
const rec = this.evalNode(ast.receiver, table)
|
|
271
|
+
if (rec === undefined || rec === null) {
|
|
272
|
+
throw new Error(`Cannot access attribute .${ast.ident.t.text} of ${rec}`)
|
|
273
|
+
}
|
|
274
|
+
return rec.access(ast.ident.t.text, (callee, args) => this.call(callee, args))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (ast.tag === 'indexAccess') {
|
|
278
|
+
const rec = this.evalNode(ast.receiver, table)
|
|
279
|
+
const index = this.evalNode(ast.index, table)
|
|
280
|
+
return rec.access(index, (callee, args) => this.call(callee, args))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
shouldNeverHappen(ast)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
call(callee: Value, argValues: Value[]) {
|
|
287
|
+
return callee.call(argValues, (names, body, lambdaTable: SymbolTable) => {
|
|
288
|
+
if (names.length > argValues.length) {
|
|
289
|
+
throw new Error(`Arg list length mismatch: expected ${names.length} but got ${argValues.length}`)
|
|
290
|
+
}
|
|
291
|
+
const newTable = names.reduce((t, n, i) => new SymbolFrame(n, { destination: argValues[i] }, t), lambdaTable)
|
|
292
|
+
return this.evalNode(body, newTable)
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
}
|
package/src/scanner.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Location } from './location'
|
|
2
|
+
import { SourceCode } from './source-code'
|
|
3
|
+
|
|
4
|
+
export interface Token {
|
|
5
|
+
readonly text: string
|
|
6
|
+
readonly location: Location
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class Scanner {
|
|
10
|
+
private offset = 0
|
|
11
|
+
|
|
12
|
+
constructor(private readonly sourceCode: SourceCode) {
|
|
13
|
+
this.eatWhitespace()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get sourceRef() {
|
|
17
|
+
return this.sourceCode.sourceRef(this.sourceCode.expandToEndOfLine({ offset: this.offset }))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private curr() {
|
|
21
|
+
return this.sourceCode.input.substring(this.offset)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private eatWhitespace() {
|
|
25
|
+
while (true) {
|
|
26
|
+
if (this.consumeIf(/\s*/, false)) {
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
if (this.consumeIf('//', false)) {
|
|
30
|
+
this.consume(/[^\n]*/, false)
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
eof(): boolean {
|
|
39
|
+
return this.offset >= this.sourceCode.input.length
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
synopsis() {
|
|
43
|
+
const c = this.curr()
|
|
44
|
+
let lookingAt = c.substring(0, 20)
|
|
45
|
+
if (lookingAt.length !== c.length) {
|
|
46
|
+
lookingAt = `${lookingAt}...`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
position: this.offset,
|
|
51
|
+
lookingAt,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
consume(r: RegExp | string, eatWhitespace = true): Token {
|
|
56
|
+
const text = this.match(r)
|
|
57
|
+
if (text === undefined) {
|
|
58
|
+
throw new Error(`Expected ${r} ${this.sourceRef}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const offset = this.offset
|
|
62
|
+
this.offset += text.length
|
|
63
|
+
|
|
64
|
+
if (eatWhitespace) {
|
|
65
|
+
this.eatWhitespace()
|
|
66
|
+
}
|
|
67
|
+
return { location: { offset }, text }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
consumeIf(r: RegExp | string, eatWhitespace = true): Token | undefined {
|
|
71
|
+
const ret = this.match(r)
|
|
72
|
+
if (!ret) {
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return this.consume(r, eatWhitespace)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private match(r: RegExp | string): string | undefined {
|
|
80
|
+
if (typeof r === 'string') {
|
|
81
|
+
if (this.curr().startsWith(r)) {
|
|
82
|
+
return r
|
|
83
|
+
}
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const m = this.curr().match(r)
|
|
88
|
+
if (m && m.index === 0) {
|
|
89
|
+
return m[0]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { AstNode, span } from './ast-node'
|
|
2
|
+
import { Location, Location2d, Span } from './location'
|
|
3
|
+
|
|
4
|
+
export class SourceCode {
|
|
5
|
+
constructor(readonly input: string) {}
|
|
6
|
+
|
|
7
|
+
formatTrace(trace: AstNode[]): string {
|
|
8
|
+
const spacer = ' '
|
|
9
|
+
|
|
10
|
+
const formatted = trace
|
|
11
|
+
.map(ast => this.sourceRef(span(ast)))
|
|
12
|
+
.reverse()
|
|
13
|
+
.join(`\n${spacer}`)
|
|
14
|
+
return `${spacer}${formatted}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
sourceRefOfLocation(loc: Location) {
|
|
18
|
+
return this.sourceRef(this.expandToEndOfLine(loc))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
sourceRef(span: Span | undefined) {
|
|
22
|
+
if (!span) {
|
|
23
|
+
return `at <unknown location>`
|
|
24
|
+
}
|
|
25
|
+
return `at ${this.formatSpan(span)} ${this.interestingPart(span)}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
formatSpan(span: Span) {
|
|
29
|
+
const f = this.resolveLocation(span.from)
|
|
30
|
+
const t = this.resolveLocation(span.to)
|
|
31
|
+
if (f.line === t.line) {
|
|
32
|
+
return `(${f.line + 1}:${f.col + 1}..${t.col + 1})`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return `(${f.line + 1}:${f.col + 1}..${t.line + 1}:${t.col + 1})`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private interestingPart(span: Span) {
|
|
39
|
+
const f = this.resolveLocation(span.from)
|
|
40
|
+
const t = this.resolveLocation(span.to)
|
|
41
|
+
|
|
42
|
+
const strip = (s: string) => s.replace(/^[\n]*/, '').replace(/[\n]*$/, '')
|
|
43
|
+
const limit = 80
|
|
44
|
+
|
|
45
|
+
const lineAtFrom = this.lineAt(span.from)
|
|
46
|
+
if (f.line !== t.line) {
|
|
47
|
+
return `${strip(lineAtFrom).substring(0, limit)}...`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const stripped = strip(lineAtFrom.substring(f.col, t.col + 1))
|
|
51
|
+
if (stripped.length <= limit) {
|
|
52
|
+
return stripped
|
|
53
|
+
}
|
|
54
|
+
return `${stripped.substring(0, limit)}...`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
resolveLocation(loc: Location): Location2d {
|
|
58
|
+
const prefix = this.input.slice(0, loc.offset)
|
|
59
|
+
let line = 0
|
|
60
|
+
for (let i = 0; i < prefix.length; ++i) {
|
|
61
|
+
const ch = prefix[i]
|
|
62
|
+
if (ch === '\n') {
|
|
63
|
+
line += 1
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let col = 0
|
|
68
|
+
for (let i = prefix.length - 1; i >= 0; --i, ++col) {
|
|
69
|
+
const ch = prefix[i]
|
|
70
|
+
if (ch === '\n') {
|
|
71
|
+
break
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { line, col }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns a span starting a the given input location that runs all the way to the end of the line. Specifically, the
|
|
80
|
+
* returned span's `.to` location will point at the character (in the same line as `loc`) that is immediately before
|
|
81
|
+
* the terminating newline character.
|
|
82
|
+
*/
|
|
83
|
+
expandToEndOfLine(loc: Location): Span {
|
|
84
|
+
let endOfLine = this.input.indexOf('\n', loc.offset)
|
|
85
|
+
if (endOfLine < 0) {
|
|
86
|
+
endOfLine = this.input.length - 1
|
|
87
|
+
}
|
|
88
|
+
return { from: loc, to: { offset: endOfLine } }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
lineAt(loc: Location) {
|
|
92
|
+
const precedingNewline = this.input.lastIndexOf('\n', loc.offset)
|
|
93
|
+
// add a + 1 to skip over the '\n' character (it is not part of the line). Also works if precedingNewLine is -1 (no
|
|
94
|
+
// preceding newline exists)
|
|
95
|
+
const startOfLine = precedingNewline + 1
|
|
96
|
+
const endOfLine = this.expandToEndOfLine(loc).to
|
|
97
|
+
|
|
98
|
+
const ret = this.input.substring(startOfLine, endOfLine.offset + 1)
|
|
99
|
+
return ret
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/stack.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AstNode } from './ast-node'
|
|
2
|
+
export type T = { ast: AstNode; next: T } | undefined
|
|
3
|
+
|
|
4
|
+
export function push(ast: AstNode, s: T) {
|
|
5
|
+
return { ast, next: s }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function pop(s: T) {
|
|
9
|
+
if (typeof s === 'undefined') {
|
|
10
|
+
throw new Error(`Cannot pop from an empty stack`)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return s.next
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function empty(): T {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
package/src/switch-on.ts
ADDED