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
@@ -0,0 +1,13 @@
1
+ export interface Location {
2
+ readonly offset: number
3
+ }
4
+
5
+ export interface Span {
6
+ from: Location
7
+ to: Location
8
+ }
9
+
10
+ export interface Location2d {
11
+ line: number
12
+ col: number
13
+ }
package/src/parser.ts ADDED
@@ -0,0 +1,399 @@
1
+ import { ArrayLiteralPart, AstNode, Ident, Let, ObjectLiteralPart } from './ast-node'
2
+ import { Scanner, Token } from './scanner'
3
+
4
+ export class Parser {
5
+ constructor(private readonly scanner: Scanner) {}
6
+
7
+ parse() {
8
+ const ret = this.expression()
9
+ if (!this.scanner.eof()) {
10
+ throw new Error(`Loitering input ${this.scanner.sourceRef}`)
11
+ }
12
+ return ret
13
+ }
14
+
15
+ definitions(): Let[] {
16
+ const ret: Let[] = []
17
+ while (true) {
18
+ const start = this.scanner.consumeIf('let ')
19
+ if (!start) {
20
+ return ret
21
+ }
22
+ const ident = this.identifier()
23
+ this.scanner.consume('=')
24
+ const value = this.lambda()
25
+ this.scanner.consume(';')
26
+
27
+ ret.push({ start, ident, value })
28
+ }
29
+ }
30
+
31
+ expression(): AstNode {
32
+ const definitions = this.definitions()
33
+ const computation = this.lambda()
34
+
35
+ if (definitions.length === 0) {
36
+ return computation
37
+ }
38
+
39
+ return { tag: 'topLevelExpression', definitions, computation }
40
+ }
41
+
42
+ lambda(): AstNode {
43
+ const start = this.scanner.consumeIf('fun')
44
+ if (!start) {
45
+ return this.ifExpression()
46
+ }
47
+
48
+ this.scanner.consume('(')
49
+ const args: Ident[] = []
50
+
51
+ if (this.scanner.consumeIf(')')) {
52
+ // no formal args
53
+ } else {
54
+ while (true) {
55
+ const arg = this.identifier()
56
+ args.push(arg)
57
+ if (this.scanner.consumeIf(')')) {
58
+ break
59
+ }
60
+
61
+ this.scanner.consume(',')
62
+ }
63
+ }
64
+
65
+ const body = this.expression()
66
+ return { tag: 'lambda', start, formalArgs: args, body }
67
+ }
68
+
69
+ ifExpression(): AstNode {
70
+ if (!this.scanner.consumeIf('if')) {
71
+ return this.unsink()
72
+ }
73
+
74
+ this.scanner.consume('(')
75
+ const condition = this.expression()
76
+ this.scanner.consume(')')
77
+
78
+ const positive = this.expression()
79
+
80
+ this.scanner.consume('else')
81
+
82
+ const negative = this.expression()
83
+
84
+ return { tag: 'if', condition, positive, negative }
85
+ }
86
+
87
+ unsink(): AstNode {
88
+ const lhs = this.or()
89
+ if (this.scanner.consumeIf('??')) {
90
+ return { tag: 'binaryOperator', operator: '??', lhs, rhs: this.unsink() }
91
+ }
92
+ return lhs
93
+ }
94
+
95
+ or(): AstNode {
96
+ const lhs = this.and()
97
+ if (this.scanner.consumeIf('||')) {
98
+ return { tag: 'binaryOperator', operator: '||', lhs, rhs: this.or() }
99
+ }
100
+ return lhs
101
+ }
102
+
103
+ and(): AstNode {
104
+ const lhs = this.equality()
105
+ if (this.scanner.consumeIf('&&')) {
106
+ return { tag: 'binaryOperator', operator: '&&', lhs, rhs: this.and() }
107
+ }
108
+ return lhs
109
+ }
110
+
111
+ equality(): AstNode {
112
+ const lhs = this.comparison()
113
+ if (this.scanner.consumeIf('==')) {
114
+ return { tag: 'binaryOperator', operator: '==', lhs, rhs: this.equality() }
115
+ }
116
+ if (this.scanner.consumeIf('!=')) {
117
+ return { tag: 'binaryOperator', operator: '!=', lhs, rhs: this.equality() }
118
+ }
119
+ return lhs
120
+ }
121
+
122
+ comparison(): AstNode {
123
+ const lhs = this.addition()
124
+ if (this.scanner.consumeIf('>=')) {
125
+ return { tag: 'binaryOperator', operator: '>=', lhs, rhs: this.comparison() }
126
+ }
127
+ if (this.scanner.consumeIf('<=')) {
128
+ return { tag: 'binaryOperator', operator: '<=', lhs, rhs: this.comparison() }
129
+ }
130
+ if (this.scanner.consumeIf('>')) {
131
+ return { tag: 'binaryOperator', operator: '>', lhs, rhs: this.comparison() }
132
+ }
133
+ if (this.scanner.consumeIf('<')) {
134
+ return { tag: 'binaryOperator', operator: '<', lhs, rhs: this.comparison() }
135
+ }
136
+ return lhs
137
+ }
138
+
139
+ addition(): AstNode {
140
+ const lhs = this.multiplication()
141
+ if (this.scanner.consumeIf('+')) {
142
+ return { tag: 'binaryOperator', operator: '+', lhs, rhs: this.addition() }
143
+ }
144
+ if (this.scanner.consumeIf('-')) {
145
+ return { tag: 'binaryOperator', operator: '-', lhs, rhs: this.addition() }
146
+ }
147
+ return lhs
148
+ }
149
+
150
+ multiplication(): AstNode {
151
+ const lhs = this.power()
152
+ if (this.scanner.consumeIf('*')) {
153
+ return { tag: 'binaryOperator', operator: '*', lhs, rhs: this.multiplication() }
154
+ }
155
+ if (this.scanner.consumeIf('/')) {
156
+ return { tag: 'binaryOperator', operator: '/', lhs, rhs: this.multiplication() }
157
+ }
158
+ if (this.scanner.consumeIf('%')) {
159
+ return { tag: 'binaryOperator', operator: '%', lhs, rhs: this.multiplication() }
160
+ }
161
+ return lhs
162
+ }
163
+
164
+ power(): AstNode {
165
+ const lhs = this.unary()
166
+ if (this.scanner.consumeIf('**')) {
167
+ return { tag: 'binaryOperator', operator: '**', lhs, rhs: this.power() }
168
+ }
169
+ return lhs
170
+ }
171
+
172
+ unary(): AstNode {
173
+ let operatorToken = this.scanner.consumeIf('!')
174
+ if (operatorToken) {
175
+ return { tag: 'unaryOperator', operand: this.unary(), operator: '!', operatorToken }
176
+ }
177
+ operatorToken = this.scanner.consumeIf('+')
178
+ if (operatorToken) {
179
+ return { tag: 'unaryOperator', operand: this.unary(), operator: '+', operatorToken }
180
+ }
181
+ operatorToken = this.scanner.consumeIf('-')
182
+ if (operatorToken) {
183
+ return { tag: 'unaryOperator', operand: this.unary(), operator: '-', operatorToken }
184
+ }
185
+
186
+ return this.call()
187
+ }
188
+
189
+ call(): AstNode {
190
+ const callee = this.memberAccess()
191
+
192
+ if (!this.scanner.consumeIf('(')) {
193
+ return callee
194
+ }
195
+
196
+ const { actualArgs, end } = this.actualArgList()
197
+ return { tag: 'functionCall', actualArgs, callee, end }
198
+ }
199
+
200
+ private actualArgList() {
201
+ const actualArgs: AstNode[] = []
202
+ const endEmpty = this.scanner.consumeIf(')')
203
+ if (endEmpty) {
204
+ // no actual args
205
+ return { actualArgs, end: endEmpty }
206
+ }
207
+
208
+ while (true) {
209
+ const arg = this.expression()
210
+ actualArgs.push(arg)
211
+ const end = this.scanner.consumeIf(')')
212
+ if (end) {
213
+ return { actualArgs, end }
214
+ }
215
+ this.scanner.consume(',')
216
+ }
217
+ }
218
+
219
+ memberAccess(): AstNode {
220
+ let ret = this.parenthesized()
221
+
222
+ while (true) {
223
+ if (this.scanner.consumeIf('.')) {
224
+ ret = { tag: 'dot', receiver: ret, ident: this.identifier() }
225
+ continue
226
+ }
227
+
228
+ if (this.scanner.consumeIf('[')) {
229
+ ret = { tag: 'indexAccess', receiver: ret, index: this.expression() }
230
+ this.scanner.consume(']')
231
+ continue
232
+ }
233
+
234
+ if (this.scanner.consumeIf('(')) {
235
+ const { actualArgs, end } = this.actualArgList()
236
+ ret = { tag: 'functionCall', actualArgs, callee: ret, end }
237
+ continue
238
+ }
239
+
240
+ return ret
241
+ }
242
+ }
243
+
244
+ parenthesized(): AstNode {
245
+ if (this.scanner.consumeIf('(')) {
246
+ const ret = this.expression()
247
+ this.scanner.consume(')')
248
+ return ret
249
+ }
250
+
251
+ return this.literalOrIdent()
252
+ }
253
+
254
+ literalOrIdent(): AstNode {
255
+ let t = this.scanner.consumeIf('sink!!')
256
+ if (t) {
257
+ return { tag: 'literal', type: 'sink!!', t }
258
+ }
259
+
260
+ t = this.scanner.consumeIf('sink!')
261
+ if (t) {
262
+ return { tag: 'literal', type: 'sink!', t }
263
+ }
264
+
265
+ t = this.scanner.consumeIf('sink')
266
+ if (t) {
267
+ return { tag: 'literal', type: 'sink', t }
268
+ }
269
+
270
+ t = this.scanner.consumeIf('true')
271
+ if (t) {
272
+ return { tag: 'literal', type: 'bool', t }
273
+ }
274
+ t = this.scanner.consumeIf('false')
275
+ if (t) {
276
+ return { tag: 'literal', type: 'bool', t }
277
+ }
278
+
279
+ t = this.scanner.consumeIf(/([0-9]*[.])?[0-9]+/)
280
+ if (t) {
281
+ return { tag: 'literal', type: 'num', t }
282
+ }
283
+
284
+ // double-quotes-enclosd string
285
+ if (this.scanner.consumeIf(`"`, false)) {
286
+ t = this.scanner.consume(/[^"]*/)
287
+ this.scanner.consume(`"`)
288
+ return { tag: 'literal', type: 'str', t }
289
+ }
290
+
291
+ // single-quotes-enclosd string
292
+ if (this.scanner.consumeIf(`'`, false)) {
293
+ t = this.scanner.consume(/[^']*/)
294
+ this.scanner.consume(`'`)
295
+ return { tag: 'literal', type: 'str', t }
296
+ }
297
+
298
+ t = this.scanner.consumeIf('[')
299
+ if (t) {
300
+ return this.arrayBody(t)
301
+ }
302
+
303
+ t = this.scanner.consumeIf('{')
304
+ if (t) {
305
+ return this.objectBody(t)
306
+ }
307
+
308
+ const ident = this.maybeIdentifier()
309
+ if (ident) {
310
+ return ident
311
+ }
312
+
313
+ throw new Error(`Unparsable input ${this.scanner.sourceRef}`)
314
+ }
315
+
316
+ /**
317
+ * This method assumes that the caller consumed the opening '[' token. It consumes the array's elements
318
+ * (comma-separated list of expressions) as well as the closing ']' token.
319
+ */
320
+ arrayBody(start: Token): AstNode {
321
+ const t = this.scanner.consumeIf(']')
322
+ if (t) {
323
+ // an empty array literal
324
+ return { tag: 'arrayLiteral', start, parts: [], end: t }
325
+ }
326
+
327
+ const parts: ArrayLiteralPart[] = []
328
+ while (true) {
329
+ if (this.scanner.consumeIf('...')) {
330
+ parts.push({ tag: 'spread', v: this.expression() })
331
+ } else {
332
+ const exp = this.expression()
333
+ parts.push({ tag: 'element', v: exp })
334
+ }
335
+
336
+ const end = this.scanner.consumeIf(']')
337
+ if (end) {
338
+ return { tag: 'arrayLiteral', start, parts, end }
339
+ }
340
+
341
+ this.scanner.consume(',')
342
+ }
343
+ }
344
+
345
+ /**
346
+ * This method assumes that the caller consumed the opening '{' token. It consumes the object's attributes
347
+ * (comma-separated list of key:value parirs) as well as the closing '}' token.
348
+ */
349
+ objectBody(start: Token): AstNode {
350
+ const t = this.scanner.consumeIf('}')
351
+ if (t) {
352
+ // an empty array literal
353
+ return { tag: 'objectLiteral', start, parts: [], end: t }
354
+ }
355
+
356
+ const parts: ObjectLiteralPart[] = []
357
+ while (true) {
358
+ if (this.scanner.consumeIf('...')) {
359
+ parts.push({ tag: 'spread', o: this.expression() })
360
+ } else if (this.scanner.consumeIf('[')) {
361
+ const k = this.expression()
362
+ this.scanner.consume(']')
363
+ this.scanner.consume(':')
364
+ const v = this.expression()
365
+ parts.push({ tag: 'computedName', k, v })
366
+ } else {
367
+ const k = this.identifier()
368
+ this.scanner.consume(':')
369
+ const v = this.expression()
370
+ parts.push({ tag: 'hardName', k, v })
371
+ }
372
+
373
+ const end = this.scanner.consumeIf('}')
374
+ if (end) {
375
+ return { tag: 'objectLiteral', start, parts, end }
376
+ }
377
+
378
+ this.scanner.consume(',')
379
+ }
380
+ }
381
+
382
+ private identifier(): Ident {
383
+ const ret = this.maybeIdentifier()
384
+ if (!ret) {
385
+ throw new Error(`Expected an identifier ${this.scanner.sourceRef}`)
386
+ }
387
+
388
+ return ret
389
+ }
390
+
391
+ private maybeIdentifier(): Ident | undefined {
392
+ const t = this.scanner.consumeIf(/[a-zA-Z][0-9A-Za-z_]*/)
393
+ if (t) {
394
+ return { tag: 'ident', t }
395
+ }
396
+
397
+ return undefined
398
+ }
399
+ }
package/src/result.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { Span } from './location'
2
+ import { SourceCode } from './source-code'
3
+ import { Value } from './value'
4
+
5
+ export type ResultSink = {
6
+ tag: 'sink'
7
+ where: Span | undefined
8
+ trace: string | undefined
9
+ symbols: Record<string, unknown> | undefined
10
+ message: string
11
+ }
12
+
13
+ export type Result =
14
+ | {
15
+ tag: 'ok'
16
+ value: unknown
17
+ }
18
+ | ResultSink
19
+
20
+ export class ResultSinkImpl {
21
+ readonly tag = 'sink'
22
+ constructor(private readonly sink: Value, private readonly sourceCode: SourceCode) {}
23
+
24
+ get where(): Span | undefined {
25
+ return this.sink.span()
26
+ }
27
+
28
+ get trace() {
29
+ const trace = this.sink.trace()
30
+ if (trace) {
31
+ return this.sourceCode.formatTrace(trace)
32
+ }
33
+
34
+ return undefined
35
+ }
36
+
37
+ get symbols() {
38
+ return this.sink.symbols()?.export()
39
+ }
40
+
41
+ get message(): string {
42
+ const at = this.trace ?? this.sourceCode.sourceRef(this.where)
43
+ return `Evaluated to sink: ${at}`
44
+ }
45
+ }