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.
Files changed (67) hide show
  1. package/dist/src/ast-node.d.ts +85 -0
  2. package/dist/src/ast-node.js +114 -0
  3. package/dist/src/cdl.d.ts +33 -0
  4. package/dist/src/cdl.js +63 -0
  5. package/dist/src/extract-message.d.ts +1 -0
  6. package/dist/src/extract-message.js +10 -0
  7. package/dist/src/fail-me.d.ts +1 -0
  8. package/dist/src/fail-me.js +11 -0
  9. package/dist/src/find-array-method.d.ts +15 -0
  10. package/dist/src/find-array-method.js +104 -0
  11. package/dist/src/find-string-method.d.ts +2 -0
  12. package/dist/src/find-string-method.js +88 -0
  13. package/dist/src/index.d.ts +1 -0
  14. package/dist/src/index.js +18 -0
  15. package/dist/src/location.d.ts +11 -0
  16. package/dist/src/location.js +3 -0
  17. package/dist/src/parser.d.ts +37 -0
  18. package/dist/src/parser.js +345 -0
  19. package/dist/src/result.d.ts +24 -0
  20. package/dist/src/result.js +29 -0
  21. package/dist/src/runtime.d.ts +25 -0
  22. package/dist/src/runtime.js +287 -0
  23. package/dist/src/scanner.d.ts +22 -0
  24. package/dist/src/scanner.js +76 -0
  25. package/dist/src/should-never-happen.d.ts +1 -0
  26. package/dist/src/should-never-happen.js +9 -0
  27. package/dist/src/source-code.d.ts +19 -0
  28. package/dist/src/source-code.js +90 -0
  29. package/dist/src/stack.d.ts +11 -0
  30. package/dist/src/stack.js +19 -0
  31. package/dist/src/switch-on.d.ts +1 -0
  32. package/dist/src/switch-on.js +9 -0
  33. package/dist/src/symbol-table.d.ts +5 -0
  34. package/dist/src/symbol-table.js +3 -0
  35. package/dist/src/value.d.ts +128 -0
  36. package/dist/src/value.js +634 -0
  37. package/dist/tests/cdl.spec.d.ts +1 -0
  38. package/dist/tests/cdl.spec.js +692 -0
  39. package/dist/tests/parser.spec.d.ts +1 -0
  40. package/dist/tests/parser.spec.js +39 -0
  41. package/dist/tests/value.spec.d.ts +1 -0
  42. package/dist/tests/value.spec.js +355 -0
  43. package/dist/tsconfig.tsbuildinfo +1 -0
  44. package/jest-output.json +1 -0
  45. package/package.json +17 -0
  46. package/src/ast-node.ts +205 -0
  47. package/src/cdl.ts +78 -0
  48. package/src/extract-message.ts +5 -0
  49. package/src/fail-me.ts +7 -0
  50. package/src/find-array-method.ts +115 -0
  51. package/src/find-string-method.ts +84 -0
  52. package/src/index.ts +1 -0
  53. package/src/location.ts +13 -0
  54. package/src/parser.ts +399 -0
  55. package/src/result.ts +45 -0
  56. package/src/runtime.ts +295 -0
  57. package/src/scanner.ts +94 -0
  58. package/src/should-never-happen.ts +4 -0
  59. package/src/source-code.ts +101 -0
  60. package/src/stack.ts +18 -0
  61. package/src/switch-on.ts +4 -0
  62. package/src/symbol-table.ts +6 -0
  63. package/src/value.ts +742 -0
  64. package/tests/cdl.spec.ts +755 -0
  65. package/tests/parser.spec.ts +14 -0
  66. package/tests/value.spec.ts +387 -0
  67. 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,4 @@
1
+ export function shouldNeverHappen(n: never): never {
2
+ // This following line never gets executed. It is here just to make the compiler happy.
3
+ throw new Error(`This should never happen ${n}`)
4
+ }
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export function switchOn<G, K extends string>(selector: K, cases: Record<K, () => G>): G {
2
+ const f = cases[selector]
3
+ return f()
4
+ }
@@ -0,0 +1,6 @@
1
+ import { Value } from './value'
2
+
3
+ export interface SymbolTable {
4
+ lookup(sym: string): Value
5
+ export(): Record<string, unknown>
6
+ }