septima-lang 0.0.1 → 0.0.4
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/index.d.ts +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/parser.d.ts +2 -0
- package/dist/src/parser.js +73 -6
- package/dist/src/runtime.d.ts +2 -3
- package/dist/src/runtime.js +7 -4
- package/dist/src/scanner.d.ts +2 -1
- package/dist/src/scanner.js +16 -4
- package/dist/src/septima.d.ts +32 -0
- package/dist/src/septima.js +79 -0
- package/dist/tests/parser.spec.js +7 -30
- package/dist/tests/{cdl.spec.d.ts → septima.spec.d.ts} +0 -0
- package/dist/tests/septima.spec.js +765 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/jest-output.json +1 -1
- package/package.json +1 -1
- package/src/a.js +66 -0
- package/src/index.ts +1 -1
- package/src/parser.ts +81 -6
- package/src/runtime.ts +7 -3
- package/src/scanner.ts +16 -4
- package/src/septima.ts +92 -0
- package/tests/parser.spec.ts +6 -6
- package/tests/{cdl.spec.ts → septima.spec.ts} +83 -10
- package/dist/src/cdl.d.ts +0 -33
- package/dist/src/cdl.js +0 -63
- package/dist/tests/cdl.spec.js +0 -692
- package/src/cdl.ts +0 -78
package/src/parser.ts
CHANGED
|
@@ -22,14 +22,18 @@ export class Parser {
|
|
|
22
22
|
const ident = this.identifier()
|
|
23
23
|
this.scanner.consume('=')
|
|
24
24
|
const value = this.lambda()
|
|
25
|
-
this.scanner.consume(';')
|
|
26
|
-
|
|
27
25
|
ret.push({ start, ident, value })
|
|
26
|
+
|
|
27
|
+
this.scanner.consumeIf(';')
|
|
28
|
+
if (!this.scanner.headMatches('let ')) {
|
|
29
|
+
return ret
|
|
30
|
+
}
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
expression(): AstNode {
|
|
32
35
|
const definitions = this.definitions()
|
|
36
|
+
this.scanner.consumeIf('return')
|
|
33
37
|
const computation = this.lambda()
|
|
34
38
|
|
|
35
39
|
if (definitions.length === 0) {
|
|
@@ -42,7 +46,7 @@ export class Parser {
|
|
|
42
46
|
lambda(): AstNode {
|
|
43
47
|
const start = this.scanner.consumeIf('fun')
|
|
44
48
|
if (!start) {
|
|
45
|
-
return this.
|
|
49
|
+
return this.arrowFunction()
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
this.scanner.consume('(')
|
|
@@ -66,6 +70,60 @@ export class Parser {
|
|
|
66
70
|
return { tag: 'lambda', start, formalArgs: args, body }
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
arrowFunction(): AstNode {
|
|
74
|
+
if (this.scanner.headMatches('(', ')', '=>')) {
|
|
75
|
+
const start = this.scanner.consume('(')
|
|
76
|
+
this.scanner.consume(')')
|
|
77
|
+
this.scanner.consume('=>')
|
|
78
|
+
const body = this.lambdaBody()
|
|
79
|
+
return { tag: 'lambda', start, formalArgs: [], body }
|
|
80
|
+
}
|
|
81
|
+
if (this.scanner.headMatches(IDENT_PATTERN, '=>')) {
|
|
82
|
+
const ident = this.identifier()
|
|
83
|
+
this.scanner.consume('=>')
|
|
84
|
+
const body = this.lambdaBody()
|
|
85
|
+
return { tag: 'lambda', start: ident.t, formalArgs: [ident], body }
|
|
86
|
+
}
|
|
87
|
+
if (this.scanner.headMatches('(', IDENT_PATTERN, ')', '=>')) {
|
|
88
|
+
const start = this.scanner.consume('(')
|
|
89
|
+
const ident = this.identifier()
|
|
90
|
+
this.scanner.consume(')')
|
|
91
|
+
this.scanner.consume('=>')
|
|
92
|
+
const body = this.lambdaBody()
|
|
93
|
+
return { tag: 'lambda', start, formalArgs: [ident], body }
|
|
94
|
+
}
|
|
95
|
+
if (this.scanner.headMatches('(', IDENT_PATTERN, ',')) {
|
|
96
|
+
const start = this.scanner.consume('(')
|
|
97
|
+
const formalArgs: Ident[] = []
|
|
98
|
+
while (true) {
|
|
99
|
+
const ident = this.identifier()
|
|
100
|
+
formalArgs.push(ident)
|
|
101
|
+
|
|
102
|
+
if (this.scanner.consumeIf(')')) {
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.scanner.consume(',')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.scanner.consume('=>')
|
|
110
|
+
const body = this.lambdaBody()
|
|
111
|
+
return { tag: 'lambda', start, formalArgs, body }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return this.ifExpression()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private lambdaBody() {
|
|
118
|
+
if (this.scanner.consumeIf('{')) {
|
|
119
|
+
const ret = this.expression()
|
|
120
|
+
this.scanner.consume('}')
|
|
121
|
+
return ret
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return this.expression()
|
|
125
|
+
}
|
|
126
|
+
|
|
69
127
|
ifExpression(): AstNode {
|
|
70
128
|
if (!this.scanner.consumeIf('if')) {
|
|
71
129
|
return this.unsink()
|
|
@@ -326,6 +384,13 @@ export class Parser {
|
|
|
326
384
|
|
|
327
385
|
const parts: ArrayLiteralPart[] = []
|
|
328
386
|
while (true) {
|
|
387
|
+
if (this.scanner.consumeIf(',')) {
|
|
388
|
+
const end = this.scanner.consumeIf(']')
|
|
389
|
+
if (end) {
|
|
390
|
+
return { tag: 'arrayLiteral', start, parts, end }
|
|
391
|
+
}
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
329
394
|
if (this.scanner.consumeIf('...')) {
|
|
330
395
|
parts.push({ tag: 'spread', v: this.expression() })
|
|
331
396
|
} else {
|
|
@@ -333,12 +398,16 @@ export class Parser {
|
|
|
333
398
|
parts.push({ tag: 'element', v: exp })
|
|
334
399
|
}
|
|
335
400
|
|
|
336
|
-
|
|
401
|
+
let end = this.scanner.consumeIf(']')
|
|
337
402
|
if (end) {
|
|
338
403
|
return { tag: 'arrayLiteral', start, parts, end }
|
|
339
404
|
}
|
|
340
405
|
|
|
341
406
|
this.scanner.consume(',')
|
|
407
|
+
end = this.scanner.consumeIf(']')
|
|
408
|
+
if (end) {
|
|
409
|
+
return { tag: 'arrayLiteral', start, parts, end }
|
|
410
|
+
}
|
|
342
411
|
}
|
|
343
412
|
}
|
|
344
413
|
|
|
@@ -370,12 +439,16 @@ export class Parser {
|
|
|
370
439
|
parts.push({ tag: 'hardName', k, v })
|
|
371
440
|
}
|
|
372
441
|
|
|
373
|
-
|
|
442
|
+
let end = this.scanner.consumeIf('}')
|
|
374
443
|
if (end) {
|
|
375
444
|
return { tag: 'objectLiteral', start, parts, end }
|
|
376
445
|
}
|
|
377
446
|
|
|
378
447
|
this.scanner.consume(',')
|
|
448
|
+
end = this.scanner.consumeIf('}')
|
|
449
|
+
if (end) {
|
|
450
|
+
return { tag: 'objectLiteral', start, parts, end }
|
|
451
|
+
}
|
|
379
452
|
}
|
|
380
453
|
}
|
|
381
454
|
|
|
@@ -389,7 +462,7 @@ export class Parser {
|
|
|
389
462
|
}
|
|
390
463
|
|
|
391
464
|
private maybeIdentifier(): Ident | undefined {
|
|
392
|
-
const t = this.scanner.consumeIf(
|
|
465
|
+
const t = this.scanner.consumeIf(IDENT_PATTERN)
|
|
393
466
|
if (t) {
|
|
394
467
|
return { tag: 'ident', t }
|
|
395
468
|
}
|
|
@@ -397,3 +470,5 @@ export class Parser {
|
|
|
397
470
|
return undefined
|
|
398
471
|
}
|
|
399
472
|
}
|
|
473
|
+
|
|
474
|
+
const IDENT_PATTERN = /[a-zA-Z][0-9A-Za-z_]*/
|
package/src/runtime.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AstNode, show, span } from './ast-node'
|
|
2
2
|
import { extractMessage } from './extract-message'
|
|
3
3
|
import { failMe } from './fail-me'
|
|
4
|
-
import { Parser } from './parser'
|
|
5
4
|
import { shouldNeverHappen } from './should-never-happen'
|
|
6
5
|
import * as Stack from './stack'
|
|
7
6
|
import { switchOn } from './switch-on'
|
|
@@ -51,7 +50,7 @@ export class Runtime {
|
|
|
51
50
|
constructor(
|
|
52
51
|
private readonly root: AstNode,
|
|
53
52
|
private readonly verbosity: Verbosity = 'quiet',
|
|
54
|
-
private readonly
|
|
53
|
+
private readonly preimports: Record<string, Value> = {},
|
|
55
54
|
) {}
|
|
56
55
|
|
|
57
56
|
compute() {
|
|
@@ -60,7 +59,12 @@ export class Runtime {
|
|
|
60
59
|
const keys = Value.foreign(o => o.keys())
|
|
61
60
|
const entries = Value.foreign(o => o.entries())
|
|
62
61
|
const fromEntries = Value.foreign(o => o.fromEntries())
|
|
63
|
-
|
|
62
|
+
let lib = new SymbolFrame('Object', { destination: Value.obj({ keys, entries, fromEntries }) }, empty)
|
|
63
|
+
|
|
64
|
+
for (const [importName, importValue] of Object.entries(this.preimports)) {
|
|
65
|
+
lib = new SymbolFrame(importName, { destination: importValue }, lib)
|
|
66
|
+
}
|
|
67
|
+
|
|
64
68
|
try {
|
|
65
69
|
const value = this.evalNode(this.root, lib)
|
|
66
70
|
return { value }
|
package/src/scanner.ts
CHANGED
|
@@ -7,10 +7,10 @@ export interface Token {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export class Scanner {
|
|
10
|
-
private offset = 0
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
constructor(private readonly sourceCode: SourceCode, private offset = 0) {
|
|
11
|
+
if (this.offset === 0) {
|
|
12
|
+
this.eatWhitespace()
|
|
13
|
+
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
get sourceRef() {
|
|
@@ -52,6 +52,18 @@ export class Scanner {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
headMatches(...patterns: (RegExp | string)[]): boolean {
|
|
56
|
+
const alt = new Scanner(this.sourceCode, this.offset)
|
|
57
|
+
for (const p of patterns) {
|
|
58
|
+
const t = alt.consumeIf(p, true)
|
|
59
|
+
if (t === undefined) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
consume(r: RegExp | string, eatWhitespace = true): Token {
|
|
56
68
|
const text = this.match(r)
|
|
57
69
|
if (text === undefined) {
|
package/src/septima.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Parser } from './parser'
|
|
2
|
+
import { Result, ResultSink, ResultSinkImpl } from './result'
|
|
3
|
+
import { Runtime, Verbosity } from './runtime'
|
|
4
|
+
import { Scanner } from './scanner'
|
|
5
|
+
import { shouldNeverHappen } from './should-never-happen'
|
|
6
|
+
import { SourceCode } from './source-code'
|
|
7
|
+
import { Value } from './value'
|
|
8
|
+
|
|
9
|
+
interface Options {
|
|
10
|
+
/**
|
|
11
|
+
* A callback function to be invoked when the Septima program evaluated to `sink`. Allows the caller to determine
|
|
12
|
+
* which value will be returned in that case. For instance, passing `() => undefined` will translate a `sink` value
|
|
13
|
+
* to `undefined`. The default behavior is to throw an error.
|
|
14
|
+
*/
|
|
15
|
+
onSink?: (res: ResultSink) => unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Septima {
|
|
19
|
+
/**
|
|
20
|
+
* Runs a Septima program and returns the value it evaluates to. If it evaluates to `sink`, returns the value computed
|
|
21
|
+
* by `options.onSink()` - if present, or throws an error - otherwise.
|
|
22
|
+
*
|
|
23
|
+
* This method is the simplest way to evaluate a Septima program, and it fits many common use cases. One can also use
|
|
24
|
+
* `.compute()` to get additional details about the execution.
|
|
25
|
+
*
|
|
26
|
+
* @param input the source code of the Septima program
|
|
27
|
+
* @param options
|
|
28
|
+
* @returns the value that `input` evaluates to
|
|
29
|
+
*/
|
|
30
|
+
static run(input: string, options?: Options): unknown {
|
|
31
|
+
const onSink =
|
|
32
|
+
options?.onSink ??
|
|
33
|
+
((r: ResultSink) => {
|
|
34
|
+
throw new Error(r.message)
|
|
35
|
+
})
|
|
36
|
+
const res = new Septima(input).compute()
|
|
37
|
+
if (res.tag === 'ok') {
|
|
38
|
+
return res.value
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (res.tag === 'sink') {
|
|
42
|
+
return onSink(res)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
shouldNeverHappen(res)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
constructor(readonly input: string, private readonly preimports: Record<string, string> = {}) {}
|
|
49
|
+
|
|
50
|
+
compute(verbosity: Verbosity = 'quiet'): Result {
|
|
51
|
+
const lib: Record<string, Value> = {}
|
|
52
|
+
for (const [importName, importCode] of Object.entries(this.preimports)) {
|
|
53
|
+
const sourceCode = new SourceCode(importCode)
|
|
54
|
+
const value = this.computeImpl(sourceCode, verbosity, {})
|
|
55
|
+
if (value.isSink()) {
|
|
56
|
+
// TODO(imaman): cover!
|
|
57
|
+
const r = new ResultSinkImpl(value, sourceCode)
|
|
58
|
+
throw new Error(`preimport (${importName}) evaluated to sink: ${r.message}`)
|
|
59
|
+
}
|
|
60
|
+
lib[importName] = value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sourceCode = new SourceCode(this.input)
|
|
64
|
+
const value = this.computeImpl(sourceCode, verbosity, lib)
|
|
65
|
+
if (!value.isSink()) {
|
|
66
|
+
return { value: value.export(), tag: 'ok' }
|
|
67
|
+
}
|
|
68
|
+
return new ResultSinkImpl(value, sourceCode)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private computeImpl(sourceCode: SourceCode, verbosity: Verbosity, lib: Record<string, Value>) {
|
|
72
|
+
const scanner = new Scanner(sourceCode)
|
|
73
|
+
const parser = new Parser(scanner)
|
|
74
|
+
|
|
75
|
+
const ast = parse(parser)
|
|
76
|
+
const runtime = new Runtime(ast, verbosity, lib)
|
|
77
|
+
const c = runtime.compute()
|
|
78
|
+
|
|
79
|
+
if (c.value) {
|
|
80
|
+
return c.value
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const runtimeErrorMessage = `${c.errorMessage} when evaluating:\n${sourceCode.formatTrace(c.expressionTrace)}`
|
|
84
|
+
throw new Error(runtimeErrorMessage)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parse(arg: string | Parser) {
|
|
89
|
+
const parser = typeof arg === 'string' ? new Parser(new Scanner(new SourceCode(arg))) : arg
|
|
90
|
+
const ast = parser.parse()
|
|
91
|
+
return ast
|
|
92
|
+
}
|
package/tests/parser.spec.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { show } from '../src/ast-node'
|
|
2
|
-
import
|
|
2
|
+
import { parse } from '../src/septima'
|
|
3
3
|
|
|
4
4
|
describe('parser', () => {
|
|
5
5
|
test('show()', () => {
|
|
6
|
-
expect(show(
|
|
7
|
-
expect(show(
|
|
6
|
+
expect(show(parse(`5`))).toEqual('5')
|
|
7
|
+
expect(show(parse(`fun (x) x*9`))).toEqual('fun (x) (x * 9)')
|
|
8
8
|
})
|
|
9
9
|
test('syntax errors', () => {
|
|
10
|
-
expect(() =>
|
|
11
|
-
expect(() =>
|
|
12
|
-
expect(() =>
|
|
10
|
+
expect(() => parse(`a + #$%x`)).toThrowError('Unparsable input at (1:5..8) #$%x')
|
|
11
|
+
expect(() => parse(`{#$%x: 8}`)).toThrowError('Expected an identifier at (1:2..9) #$%x: 8}')
|
|
12
|
+
expect(() => parse(`"foo" "goo"`)).toThrowError('Loitering input at (1:7..11) "goo"')
|
|
13
13
|
})
|
|
14
14
|
})
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Septima } from '../src/septima'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Runs a
|
|
4
|
+
* Runs a Septima program for testing purposes. If the program evaluates to `sink` an `undefined` is
|
|
5
5
|
* returned.
|
|
6
|
-
* @param input the
|
|
6
|
+
* @param input the Septima program to run
|
|
7
7
|
*/
|
|
8
8
|
function run(input: string) {
|
|
9
|
-
return
|
|
9
|
+
return Septima.run(input, { onSink: () => undefined })
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Runs a
|
|
14
|
-
* is not
|
|
15
|
-
* @param input the
|
|
13
|
+
* Runs a Septima program for testing purposes. The program is expected to evaluate to `sink`. Throws an exception if
|
|
14
|
+
* this expectation is not met.
|
|
15
|
+
* @param input the Septima program to run
|
|
16
16
|
*/
|
|
17
17
|
function runSink(input: string) {
|
|
18
|
-
const
|
|
19
|
-
const res =
|
|
18
|
+
const septima = new Septima(input)
|
|
19
|
+
const res = septima.compute()
|
|
20
20
|
|
|
21
21
|
if (res.tag !== 'sink') {
|
|
22
22
|
throw new Error(`Not a sink: ${res.value}`)
|
|
@@ -24,12 +24,16 @@ function runSink(input: string) {
|
|
|
24
24
|
return res
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
describe('
|
|
27
|
+
describe('septima', () => {
|
|
28
28
|
test('basics', () => {
|
|
29
29
|
expect(run(`5`)).toEqual(5)
|
|
30
30
|
expect(() => run(`6 789`)).toThrowError(`Loitering input at (1:3..5) 789`)
|
|
31
31
|
expect(run(`3.14`)).toEqual(3.14)
|
|
32
32
|
})
|
|
33
|
+
test('an optional return keyword can be placed before the result', () => {
|
|
34
|
+
expect(run(`return 5`)).toEqual(5)
|
|
35
|
+
expect(run(`return 3.14`)).toEqual(3.14)
|
|
36
|
+
})
|
|
33
37
|
|
|
34
38
|
test('booleans', () => {
|
|
35
39
|
expect(run(`true`)).toEqual(true)
|
|
@@ -200,6 +204,10 @@ describe('cdl', () => {
|
|
|
200
204
|
expect(run(`let x = 5; x+3`)).toEqual(8)
|
|
201
205
|
expect(run(`let x = 5; let y = 20; x*y+4`)).toEqual(104)
|
|
202
206
|
})
|
|
207
|
+
test('do not need the trailing semicolon', () => {
|
|
208
|
+
expect(run(`let x = 5 x+3`)).toEqual(8)
|
|
209
|
+
expect(run(`let x = 5 let y = 20 x*y+4`)).toEqual(104)
|
|
210
|
+
})
|
|
203
211
|
test('fails if the variable was not defined', () => {
|
|
204
212
|
expect(() => run(`let x = 5; x+y`)).toThrowError('Symbol y was not found')
|
|
205
213
|
})
|
|
@@ -262,6 +270,12 @@ describe('cdl', () => {
|
|
|
262
270
|
expect(run(`["ab", 5]`)).toEqual(['ab', 5])
|
|
263
271
|
expect(run(`[]`)).toEqual([])
|
|
264
272
|
})
|
|
273
|
+
test('allow a dangling comma', () => {
|
|
274
|
+
expect(run(`[,]`)).toEqual([])
|
|
275
|
+
expect(run(`[,,]`)).toEqual([])
|
|
276
|
+
expect(run(`[246,]`)).toEqual([246])
|
|
277
|
+
expect(run(`[246,531,]`)).toEqual([246, 531])
|
|
278
|
+
})
|
|
265
279
|
test('individual elements of an array can be accessed via the [<index>] notation', () => {
|
|
266
280
|
expect(run(`let a = ['sun', 'mon', 'tue', 'wed']; a[1]`)).toEqual('mon')
|
|
267
281
|
})
|
|
@@ -282,6 +296,14 @@ describe('cdl', () => {
|
|
|
282
296
|
expect(run(`{a: 1, b: 2}`)).toEqual({ a: 1, b: 2 })
|
|
283
297
|
expect(run(`{a: "A", b: "B", c: "CCC"}`)).toEqual({ a: 'A', b: 'B', c: 'CCC' })
|
|
284
298
|
})
|
|
299
|
+
test('allow a dangling comma', () => {
|
|
300
|
+
expect(run(`{a: 1,}`)).toEqual({ a: 1 })
|
|
301
|
+
expect(run(`{a: 1, b: 2,}`)).toEqual({ a: 1, b: 2 })
|
|
302
|
+
expect(run(`{a: "A", b: "B", c: "CCC",}`)).toEqual({ a: 'A', b: 'B', c: 'CCC' })
|
|
303
|
+
})
|
|
304
|
+
test('a dangling comma in an empty object is not allowed', () => {
|
|
305
|
+
expect(() => run(`{,}`)).toThrowError('Expected an identifier at (1:2..3) ,}')
|
|
306
|
+
})
|
|
285
307
|
test('supports computed attributes names via the [<expression>]: <value> notation', () => {
|
|
286
308
|
expect(run(`{["a" + 'b']: 'a-and-b'}`)).toEqual({ ab: 'a-and-b' })
|
|
287
309
|
})
|
|
@@ -408,6 +430,33 @@ describe('cdl', () => {
|
|
|
408
430
|
expect(run(`let triple = (fun(a) 3*a); triple(100) - triple(90)`)).toEqual(30)
|
|
409
431
|
expect(run(`let triple = fun(a) 3*a; triple(100) - triple(90)`)).toEqual(30)
|
|
410
432
|
})
|
|
433
|
+
describe('arrow function notation', () => {
|
|
434
|
+
test('a single formal argument does not need to be surrounded with parenthesis', () => {
|
|
435
|
+
expect(run(`let triple = a => 3*a; triple(100)`)).toEqual(300)
|
|
436
|
+
})
|
|
437
|
+
test('(a) => <expression>', () => {
|
|
438
|
+
expect(run(`let triple = (a) => 3*a; triple(100)`)).toEqual(300)
|
|
439
|
+
})
|
|
440
|
+
test('() => <expression>', () => {
|
|
441
|
+
expect(run(`let five = () => 5; five()`)).toEqual(5)
|
|
442
|
+
})
|
|
443
|
+
test('(a,b) => <expression>', () => {
|
|
444
|
+
expect(run(`let conc = (a,b) => a+b; conc('al', 'pha')`)).toEqual('alpha')
|
|
445
|
+
expect(run(`let conc = (a,b,c,d,e,f) => a+b+c+d+e+f; conc('M', 'o', 'n', 'd', 'a', 'y')`)).toEqual('Monday')
|
|
446
|
+
})
|
|
447
|
+
test('body of an arrow function can be { return <expression>}', () => {
|
|
448
|
+
expect(run(`let triple = a => { return 3*a }; triple(100)`)).toEqual(300)
|
|
449
|
+
expect(run(`let triple = (a) => { return 3*a }; triple(100)`)).toEqual(300)
|
|
450
|
+
expect(run(`let five = () => { return 5 }; five()`)).toEqual(5)
|
|
451
|
+
expect(run(`let concat = (a,b) => { return a+b }; concat('al', 'pha')`)).toEqual('alpha')
|
|
452
|
+
})
|
|
453
|
+
test('body of an arrow function can include let definitions', () => {
|
|
454
|
+
expect(run(`let triple = a => { let factor = 3; return factor*a }; triple(100)`)).toEqual(300)
|
|
455
|
+
expect(run(`let triple = (a) => { let factor = 3; return 3*a }; triple(100)`)).toEqual(300)
|
|
456
|
+
expect(run(`let five = () => { let two = 2; let three = 3; return three+two }; five()`)).toEqual(5)
|
|
457
|
+
expect(run(`let concat = (a,b) => { let u = '_'; return u+a+b+u }; concat('a', 'b')`)).toEqual('_ab_')
|
|
458
|
+
})
|
|
459
|
+
})
|
|
411
460
|
test('can have no args', () => {
|
|
412
461
|
expect(run(`let pi = fun() 3.14; 2*pi()`)).toEqual(6.28)
|
|
413
462
|
expect(run(`(fun() 3.14)()*2`)).toEqual(6.28)
|
|
@@ -743,6 +792,28 @@ describe('cdl', () => {
|
|
|
743
792
|
expect(run(`let count = fun (n) if (n <= 0) 0 else 1 + count(n-1); count(330)`)).toEqual(330)
|
|
744
793
|
})
|
|
745
794
|
})
|
|
795
|
+
describe('preimport', () => {
|
|
796
|
+
test('definitions from a preimported file can be used', () => {
|
|
797
|
+
const septima = new Septima(`libA.plus10(4) + libA.plus20(2)`, {
|
|
798
|
+
libA: `{ plus10: fun (n) n+10, plus20: fun (n) n+20}`,
|
|
799
|
+
})
|
|
800
|
+
expect(septima.compute()).toMatchObject({ value: 36 })
|
|
801
|
+
})
|
|
802
|
+
test('supports multiple preimports', () => {
|
|
803
|
+
const septima = new Septima(`a.calc(4) + b.calc(1)`, {
|
|
804
|
+
a: `{ calc: fun (n) n+10 }`,
|
|
805
|
+
b: `{ calc: fun (n) n+20 }`,
|
|
806
|
+
})
|
|
807
|
+
expect(septima.compute()).toMatchObject({ value: 35 })
|
|
808
|
+
})
|
|
809
|
+
})
|
|
810
|
+
test.todo('support file names in locations')
|
|
811
|
+
test.todo('string interpolation via `foo` strings')
|
|
812
|
+
test.todo('imports')
|
|
813
|
+
test.todo('arrow functions')
|
|
814
|
+
test.todo('optional parameters')
|
|
815
|
+
test.todo('optional type annotations?')
|
|
816
|
+
test.todo('allow redundant commas')
|
|
746
817
|
test.todo('left associativity of +/-')
|
|
747
818
|
test.todo('comparison of arrays')
|
|
748
819
|
test.todo('comparison of lambdas?')
|
|
@@ -752,4 +823,6 @@ describe('cdl', () => {
|
|
|
752
823
|
test.todo('quoting of a ticks inside a string')
|
|
753
824
|
test.todo('number in scientific notation')
|
|
754
825
|
test.todo('number methods')
|
|
826
|
+
test.todo('drop the fun () notation and use just arrow functions')
|
|
827
|
+
test.todo('proper internal representation of arrow function, in particular: show(), span()')
|
|
755
828
|
})
|
package/dist/src/cdl.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Parser } from './parser';
|
|
2
|
-
import { Result, ResultSink } from './result';
|
|
3
|
-
import { Verbosity } from './runtime';
|
|
4
|
-
interface Options {
|
|
5
|
-
/**
|
|
6
|
-
* A callback function to be invoked when the CDL program evaluated to `sink`. Allows the caller to determine which
|
|
7
|
-
* value will be returned in that case. For instance, passing `() => undefined` will translate a `sink` value to
|
|
8
|
-
* `undefined`. The default behavior is to throw an error.
|
|
9
|
-
*/
|
|
10
|
-
onSink?: (res: ResultSink) => unknown;
|
|
11
|
-
}
|
|
12
|
-
export declare class Cdl {
|
|
13
|
-
readonly input: string;
|
|
14
|
-
private readonly scanner;
|
|
15
|
-
private readonly sourceCode;
|
|
16
|
-
private readonly parser;
|
|
17
|
-
/**
|
|
18
|
-
* Runs a CDL program and returns the value it evaluates to. If it evaluates to `sink`, returns the value computed
|
|
19
|
-
* by `options.onSink()` - if present, or throws an error - otherwise.
|
|
20
|
-
*
|
|
21
|
-
* This method is the simplest way to evaluate a CDL program. One can also use `.compute()` to get a higher degree
|
|
22
|
-
* of details about the result.
|
|
23
|
-
*
|
|
24
|
-
* @param input the source code of the CDL program
|
|
25
|
-
* @param options
|
|
26
|
-
* @returns the value that `input` evaluates to
|
|
27
|
-
*/
|
|
28
|
-
static run(input: string, options?: Options): unknown;
|
|
29
|
-
constructor(input: string);
|
|
30
|
-
compute(verbosity?: Verbosity): Result;
|
|
31
|
-
}
|
|
32
|
-
export declare function parse(arg: string | Parser): import("./ast-node").AstNode;
|
|
33
|
-
export {};
|
package/dist/src/cdl.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parse = exports.Cdl = void 0;
|
|
4
|
-
const parser_1 = require("./parser");
|
|
5
|
-
const result_1 = require("./result");
|
|
6
|
-
const runtime_1 = require("./runtime");
|
|
7
|
-
const scanner_1 = require("./scanner");
|
|
8
|
-
const should_never_happen_1 = require("./should-never-happen");
|
|
9
|
-
const source_code_1 = require("./source-code");
|
|
10
|
-
class Cdl {
|
|
11
|
-
constructor(input) {
|
|
12
|
-
this.input = input;
|
|
13
|
-
this.sourceCode = new source_code_1.SourceCode(this.input);
|
|
14
|
-
this.scanner = new scanner_1.Scanner(this.sourceCode);
|
|
15
|
-
this.parser = new parser_1.Parser(this.scanner);
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Runs a CDL program and returns the value it evaluates to. If it evaluates to `sink`, returns the value computed
|
|
19
|
-
* by `options.onSink()` - if present, or throws an error - otherwise.
|
|
20
|
-
*
|
|
21
|
-
* This method is the simplest way to evaluate a CDL program. One can also use `.compute()` to get a higher degree
|
|
22
|
-
* of details about the result.
|
|
23
|
-
*
|
|
24
|
-
* @param input the source code of the CDL program
|
|
25
|
-
* @param options
|
|
26
|
-
* @returns the value that `input` evaluates to
|
|
27
|
-
*/
|
|
28
|
-
static run(input, options) {
|
|
29
|
-
const onSink = options?.onSink ??
|
|
30
|
-
((r) => {
|
|
31
|
-
throw new Error(r.message);
|
|
32
|
-
});
|
|
33
|
-
const res = new Cdl(input).compute();
|
|
34
|
-
if (res.tag === 'ok') {
|
|
35
|
-
return res.value;
|
|
36
|
-
}
|
|
37
|
-
if (res.tag === 'sink') {
|
|
38
|
-
return onSink(res);
|
|
39
|
-
}
|
|
40
|
-
(0, should_never_happen_1.shouldNeverHappen)(res);
|
|
41
|
-
}
|
|
42
|
-
compute(verbosity = 'quiet') {
|
|
43
|
-
const ast = parse(this.parser);
|
|
44
|
-
const runtime = new runtime_1.Runtime(ast, verbosity, this.parser);
|
|
45
|
-
const c = runtime.compute();
|
|
46
|
-
if (c.value) {
|
|
47
|
-
if (!c.value.isSink()) {
|
|
48
|
-
return { value: c.value.export(), tag: 'ok' };
|
|
49
|
-
}
|
|
50
|
-
return new result_1.ResultSinkImpl(c.value, this.sourceCode);
|
|
51
|
-
}
|
|
52
|
-
const runtimeErrorMessage = `${c.errorMessage} when evaluating:\n${this.sourceCode.formatTrace(c.expressionTrace)}`;
|
|
53
|
-
throw new Error(runtimeErrorMessage);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
exports.Cdl = Cdl;
|
|
57
|
-
function parse(arg) {
|
|
58
|
-
const parser = typeof arg === 'string' ? new parser_1.Parser(new scanner_1.Scanner(new source_code_1.SourceCode(arg))) : arg;
|
|
59
|
-
const ast = parser.parse();
|
|
60
|
-
return ast;
|
|
61
|
-
}
|
|
62
|
-
exports.parse = parse;
|
|
63
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2RsLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2NkbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxxQ0FBaUM7QUFDakMscUNBQTZEO0FBQzdELHVDQUE4QztBQUM5Qyx1Q0FBbUM7QUFDbkMsK0RBQXlEO0FBQ3pELCtDQUEwQztBQVcxQyxNQUFhLEdBQUc7SUFrQ2QsWUFBcUIsS0FBYTtRQUFiLFVBQUssR0FBTCxLQUFLLENBQVE7UUFDaEMsSUFBSSxDQUFDLFVBQVUsR0FBRyxJQUFJLHdCQUFVLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFBO1FBQzVDLElBQUksQ0FBQyxPQUFPLEdBQUcsSUFBSSxpQkFBTyxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsQ0FBQTtRQUMzQyxJQUFJLENBQUMsTUFBTSxHQUFHLElBQUksZUFBTSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQTtJQUN4QyxDQUFDO0lBakNEOzs7Ozs7Ozs7O09BVUc7SUFDSCxNQUFNLENBQUMsR0FBRyxDQUFDLEtBQWEsRUFBRSxPQUFpQjtRQUN6QyxNQUFNLE1BQU0sR0FDVixPQUFPLEVBQUUsTUFBTTtZQUNmLENBQUMsQ0FBQyxDQUFhLEVBQUUsRUFBRTtnQkFDakIsTUFBTSxJQUFJLEtBQUssQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUE7WUFDNUIsQ0FBQyxDQUFDLENBQUE7UUFDSixNQUFNLEdBQUcsR0FBRyxJQUFJLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQTtRQUNwQyxJQUFJLEdBQUcsQ0FBQyxHQUFHLEtBQUssSUFBSSxFQUFFO1lBQ3BCLE9BQU8sR0FBRyxDQUFDLEtBQUssQ0FBQTtTQUNqQjtRQUVELElBQUksR0FBRyxDQUFDLEdBQUcsS0FBSyxNQUFNLEVBQUU7WUFDdEIsT0FBTyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUE7U0FDbkI7UUFFRCxJQUFBLHVDQUFpQixFQUFDLEdBQUcsQ0FBQyxDQUFBO0lBQ3hCLENBQUM7SUFRRCxPQUFPLENBQUMsWUFBdUIsT0FBTztRQUNwQyxNQUFNLEdBQUcsR0FBRyxLQUFLLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFBO1FBQzlCLE1BQU0sT0FBTyxHQUFHLElBQUksaUJBQU8sQ0FBQyxHQUFHLEVBQUUsU0FBUyxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQTtRQUN4RCxNQUFNLENBQUMsR0FBRyxPQUFPLENBQUMsT0FBTyxFQUFFLENBQUE7UUFFM0IsSUFBSSxDQUFDLENBQUMsS0FBSyxFQUFFO1lBQ1gsSUFBSSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsTUFBTSxFQUFFLEVBQUU7Z0JBQ3JCLE9BQU8sRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDLEtBQUssQ0FBQyxNQUFNLEVBQUUsRUFBRSxHQUFHLEVBQUUsSUFBSSxFQUFFLENBQUE7YUFDOUM7WUFDRCxPQUFPLElBQUksdUJBQWMsQ0FBQyxDQUFDLENBQUMsS0FBSyxFQUFFLElBQUksQ0FBQyxVQUFVLENBQUMsQ0FBQTtTQUNwRDtRQUVELE1BQU0sbUJBQW1CLEdBQUcsR0FBRyxDQUFDLENBQUMsWUFBWSxzQkFBc0IsSUFBSSxDQUFDLFVBQVUsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLGVBQWUsQ0FBQyxFQUFFLENBQUE7UUFDbkgsTUFBTSxJQUFJLEtBQUssQ0FBQyxtQkFBbUIsQ0FBQyxDQUFBO0lBQ3RDLENBQUM7Q0FDRjtBQXZERCxrQkF1REM7QUFFRCxTQUFnQixLQUFLLENBQUMsR0FBb0I7SUFDeEMsTUFBTSxNQUFNLEdBQUcsT0FBTyxHQUFHLEtBQUssUUFBUSxDQUFDLENBQUMsQ0FBQyxJQUFJLGVBQU0sQ0FBQyxJQUFJLGlCQUFPLENBQUMsSUFBSSx3QkFBVSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFBO0lBQzNGLE1BQU0sR0FBRyxHQUFHLE1BQU0sQ0FBQyxLQUFLLEVBQUUsQ0FBQTtJQUMxQixPQUFPLEdBQUcsQ0FBQTtBQUNaLENBQUM7QUFKRCxzQkFJQyJ9
|