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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { show, span } from '../src/ast-node'
|
|
2
|
+
import { Parser } from '../src/parser'
|
|
3
|
+
import { Scanner } from '../src/scanner'
|
|
4
|
+
import { SourceCode } from '../src/source-code'
|
|
5
|
+
|
|
6
|
+
function parse(arg: string) {
|
|
7
|
+
const parser = new Parser(new Scanner(new SourceCode(arg, '<test-file>')))
|
|
8
|
+
const ast = parser.parse()
|
|
9
|
+
return ast
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('parser', () => {
|
|
13
|
+
test('show()', () => {
|
|
14
|
+
expect(show(parse(`5`))).toEqual('5')
|
|
15
|
+
expect(show(parse(`fun (x) x*9`))).toEqual('fun (x) (x * 9)')
|
|
16
|
+
})
|
|
17
|
+
test('syntax errors', () => {
|
|
18
|
+
expect(() => parse(`a + #$%x`)).toThrowError('Unparsable input at (<test-file>:1:5..8) #$%x')
|
|
19
|
+
expect(() => parse(`{#$%x: 8}`)).toThrowError('Expected an identifier at (<test-file>:1:2..9) #$%x: 8}')
|
|
20
|
+
expect(() => parse(`"foo" "goo"`)).toThrowError('Loitering input at (<test-file>:1:7..11) "goo"')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('unit', () => {
|
|
24
|
+
test('show', () => {
|
|
25
|
+
expect(show(parse(`import * as foo from './bar';'a'`))).toEqual(`import * as foo from './bar';\n'a'`)
|
|
26
|
+
expect(show(parse(`let f = x => x*x; f(2)`))).toEqual(`let f = fun (x) (x * x); f(2)`)
|
|
27
|
+
expect(show(parse(`let f = x => x*x; let g = n => n+1`))).toEqual(
|
|
28
|
+
`let f = fun (x) (x * x); let g = fun (n) (n + 1);`,
|
|
29
|
+
)
|
|
30
|
+
expect(show(parse(`export let a = 1; let b = 2; export let c = 3;`))).toEqual(
|
|
31
|
+
`export let a = 1; let b = 2; export let c = 3;`,
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
test('const keyword', () => {
|
|
35
|
+
expect(show(parse(`const x = 5; x`))).toEqual(`let x = 5; x`)
|
|
36
|
+
expect(show(parse(`const f = x => x*x; f(2)`))).toEqual(`let f = fun (x) (x * x); f(2)`)
|
|
37
|
+
expect(show(parse(`const a = 1; let b = 2; const c = 3`))).toEqual(`let a = 1; let b = 2; let c = 3;`)
|
|
38
|
+
expect(show(parse(`export const a = 1;`))).toEqual(`export let a = 1;`)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
describe('expression', () => {
|
|
42
|
+
test('show', () => {
|
|
43
|
+
expect(show(parse(`'sunday'`))).toEqual(`'sunday'`)
|
|
44
|
+
expect(show(parse(`true`))).toEqual(`true`)
|
|
45
|
+
expect(show(parse(`500`))).toEqual(`500`)
|
|
46
|
+
expect(show(parse(`if (3+4 > 8) "above" else "below"`))).toEqual(`if (((3 + 4) > 8)) 'above' else 'below'`)
|
|
47
|
+
expect(show(parse(`(3+4 > 8) ? "above" : "below"`))).toEqual(`((3 + 4) > 8) ? 'above' : 'below'`)
|
|
48
|
+
})
|
|
49
|
+
test('can have an optional throw token', () => {
|
|
50
|
+
expect(show(parse(`throw 'sunday'`))).toEqual(`throw 'sunday'`)
|
|
51
|
+
expect(show(parse(`let a = 8;throw 'sunday'`))).toEqual(`let a = 8; throw 'sunday'`)
|
|
52
|
+
expect(show(parse(`{a: [45 + 8*3 > 2 + (throw 'err')]}`))).toEqual(`{a: [((45 + (8 * 3)) > (2 + throw 'err'))]}`)
|
|
53
|
+
expect(show(parse(`if (5 > 8) 'yes' else throw 'no'`))).toEqual(`if ((5 > 8)) 'yes' else throw 'no'`)
|
|
54
|
+
expect(show(parse(`let f = x >= 0 ? x : throw 'negative'`))).toEqual(`let f = (x >= 0) ? x : throw 'negative';`)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
describe('lambda', () => {
|
|
58
|
+
test('show', () => {
|
|
59
|
+
expect(show(parse(`(a) => a*2`))).toEqual(`fun (a) (a * 2)`)
|
|
60
|
+
expect(show(parse(`(a, b = {x: 1, y: ['bee', 'camel']}) => a*2 + b.x`))).toEqual(
|
|
61
|
+
`fun (a, b = {x: 1, y: ['bee', 'camel']}) ((a * 2) + b.x)`,
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
describe('template literal', () => {
|
|
66
|
+
test('show', () => {
|
|
67
|
+
expect(show(parse('`hello`'))).toEqual('`hello`')
|
|
68
|
+
expect(show(parse('`hello ${x} world`'))).toEqual('`hello ${x} world`')
|
|
69
|
+
expect(show(parse('`${a}${b}`'))).toEqual('`${a}${b}`')
|
|
70
|
+
expect(show(parse('`start ${x + y} end`'))).toEqual('`start ${(x + y)} end`')
|
|
71
|
+
})
|
|
72
|
+
test('span', () => {
|
|
73
|
+
expect(span(parse('`hello`'))).toEqual({ from: { offset: 0 }, to: { offset: 6 } })
|
|
74
|
+
expect(span(parse('`hi ${x} bye`'))).toEqual({ from: { offset: 0 }, to: { offset: 12 } })
|
|
75
|
+
})
|
|
76
|
+
test('unterminated template literal', () => {
|
|
77
|
+
expect(() => parse('`hello')).toThrowError('Unterminated template literal')
|
|
78
|
+
expect(() => parse('`hello ${x}')).toThrowError('Unterminated template literal')
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Executable, Septima } from '../src/septima'
|
|
2
|
+
import { shouldNeverHappen } from '../src/should-never-happen'
|
|
3
|
+
|
|
4
|
+
function runExecutable(executable: Executable, args: Record<string, unknown>) {
|
|
5
|
+
const res = executable.execute(args)
|
|
6
|
+
if (res.tag === 'ok') {
|
|
7
|
+
return res.value
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (res.tag === 'sink') {
|
|
11
|
+
throw new Error(res.message)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
shouldNeverHappen(res)
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Runs a Septima program for testing purposes. Throws an error If the program evaluated to `sink`.
|
|
18
|
+
*/
|
|
19
|
+
function run(
|
|
20
|
+
mainFileName: string,
|
|
21
|
+
inputs: Record<string, string>,
|
|
22
|
+
args: Record<string, unknown> = {},
|
|
23
|
+
sourceRoot = '',
|
|
24
|
+
) {
|
|
25
|
+
const septima = new Septima(sourceRoot)
|
|
26
|
+
return runExecutable(
|
|
27
|
+
septima.compileSync(mainFileName, (m: string) => inputs[m]),
|
|
28
|
+
args,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function runPromise(
|
|
33
|
+
mainFileName: string,
|
|
34
|
+
inputs: Record<string, string>,
|
|
35
|
+
args: Record<string, unknown> = {},
|
|
36
|
+
sourceRoot = '',
|
|
37
|
+
) {
|
|
38
|
+
const septima = new Septima(sourceRoot)
|
|
39
|
+
const executable = await septima.compile(mainFileName, (m: string) => Promise.resolve(inputs[m]))
|
|
40
|
+
return runExecutable(executable, args)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('septima-compile', () => {
|
|
44
|
+
test('fetches the content of the module to compute from the given callback function', () => {
|
|
45
|
+
expect(run('a', { a: `3+8` })).toEqual(11)
|
|
46
|
+
})
|
|
47
|
+
test('can use exported definitions from another module', () => {
|
|
48
|
+
expect(run('a', { a: `import * as b from 'b'; 3+b.eight`, b: `export let eight = 8; {}` })).toEqual(11)
|
|
49
|
+
})
|
|
50
|
+
test('errors if the imported definition is not qualified with "export"', () => {
|
|
51
|
+
expect(() => run('a', { a: `import * as b from 'b'; 3+b.eight`, b: `let eight = 8; {}` })).toThrowError(
|
|
52
|
+
'at (a:1:25..33) 3+b.eight',
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
test('errors if the path to input from is not a string literal', () => {
|
|
56
|
+
expect(() => run('a', { a: `import * as foo from 500` })).toThrowError(
|
|
57
|
+
'Expected a string literal at (a:1:22..24) 500',
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
test('allows specifying a custom source root', () => {
|
|
61
|
+
expect(
|
|
62
|
+
run('a', { 'p/q/r/a': `import * as b from 'b'; 3+b.eight`, 'p/q/r/b': `export let eight = 8; {}` }, {}, 'p/q/r'),
|
|
63
|
+
).toEqual(11)
|
|
64
|
+
})
|
|
65
|
+
test('allows the main file to be specified via an absolute path if it points to a file under source root', () => {
|
|
66
|
+
expect(run('/p/q/r/a', { '/p/q/r/a': `"apollo 11"` }, {}, '/p/q/r')).toEqual('apollo 11')
|
|
67
|
+
expect(() => run('/p/q/x', { '/p/q/x': `"apollo 11"` }, {}, '/p/q/r')).toThrowError(
|
|
68
|
+
'resolved path (/p/q/x) is pointing outside of source root (/p/q/r)',
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
test('allows importing from the same directory via a relative path', () => {
|
|
72
|
+
expect(run('s', { s: `import * as t from "./t"; t.ten+5`, t: `export let ten = 10` }, {})).toEqual(15)
|
|
73
|
+
})
|
|
74
|
+
test('allows importing from sub directories', () => {
|
|
75
|
+
expect(run('q', { q: `import * as t from "./r/s/t"; t.ten+5`, 'r/s/t': `export let ten = 10` }, {})).toEqual(15)
|
|
76
|
+
expect(
|
|
77
|
+
run('q', {
|
|
78
|
+
q: `import * as t from './r/s/t'; t.ten * t.ten`,
|
|
79
|
+
'r/s/t': `import * as f from './d/e/f'; export let ten = f.five*2`,
|
|
80
|
+
'r/s/d/e/f': `export let five = 5`,
|
|
81
|
+
}),
|
|
82
|
+
).toEqual(100)
|
|
83
|
+
})
|
|
84
|
+
test('allows a relative path to climb up (as long as it is below source root)', () => {
|
|
85
|
+
expect(
|
|
86
|
+
run(
|
|
87
|
+
'p/q/r/s',
|
|
88
|
+
{ 'd1/d2/p/q/r/s': `import * as t from "../../t"; t.ten+5`, 'd1/d2/p/t': `export let ten = 10` },
|
|
89
|
+
{},
|
|
90
|
+
'd1/d2',
|
|
91
|
+
),
|
|
92
|
+
).toEqual(15)
|
|
93
|
+
})
|
|
94
|
+
test('errors if a file tries to import a(nother) file which is outside of the source root tree', () => {
|
|
95
|
+
expect(() => run('q', { 'd1/d2/q': `import * as r from "../r"; 5` }, {}, 'd1/d2')).toThrowError(
|
|
96
|
+
`resolved path (d1/r) is pointing outside of source root (d1/d2)`,
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
test('errors if the main file is outside of the source root tree', () => {
|
|
100
|
+
expect(() => run('../q', { 'd1/q': `300` }, {}, 'd1/d2')).toThrowError(
|
|
101
|
+
`resolved path (d1/q) is pointing outside of source root (d1/d2)`,
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
test('disallow absolute paths for specifying an imported file', () => {
|
|
105
|
+
expect(() => run('q', { 'd1/d2/q': 'import * as r from "/d1/d2/r"; 300' }, {}, 'd1/d2')).toThrowError(
|
|
106
|
+
`An absolute path is not allowed in import (got: /d1/d2/r)`,
|
|
107
|
+
)
|
|
108
|
+
expect(() => run('q', { 'd1/d2/q': 'import * as r from "/r"; 300' }, {}, 'd1/d2')).toThrowError(
|
|
109
|
+
`An absolute path is not allowed in import (got: /r)`,
|
|
110
|
+
)
|
|
111
|
+
expect(() => run('q', { q: 'import * as r from "/r"; 300' }, {}, '')).toThrowError(
|
|
112
|
+
`An absolute path is not allowed in import (got: /r)`,
|
|
113
|
+
)
|
|
114
|
+
expect(() => run('q', { q: 'import * as r from "./r"; r.foo', r: 'import * as s from "/s"' }, {}, '')).toThrowError(
|
|
115
|
+
`An absolute path is not allowed in import (got: /s)`,
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
test('provides a clear error message when a file is not found', () => {
|
|
119
|
+
expect(() => run('a', { a: `import * as b from 'b'; 3+b.eight` })).toThrowError(`Cannot find file 'b'`)
|
|
120
|
+
})
|
|
121
|
+
test('the file-not-found error message includes the resolved path (i.e., with the source root)', () => {
|
|
122
|
+
expect(() => run('a', { 'p/q/r/a': `import * as b from 's/b'; 3+b.eight` }, {}, 'p/q/r')).toThrowError(
|
|
123
|
+
`Cannot find file 'p/q/r/s/b'`,
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
test.todo(`file not found error should include an import stack (a-la node's "require stack")`)
|
|
127
|
+
test('support the passing of args into the runtime', () => {
|
|
128
|
+
expect(run('a', { a: `args.x * args.y` }, { x: 5, y: 9 })).toEqual(45)
|
|
129
|
+
})
|
|
130
|
+
test('the args object is available only at the main module', () => {
|
|
131
|
+
expect(() =>
|
|
132
|
+
run('a', { a: `import * as b from 'b'; args.x + '_' + b.foo`, b: `let foo = args.x; {}` }, { x: 'Red' }),
|
|
133
|
+
).toThrowError('at (b:1:11..16) args.x')
|
|
134
|
+
})
|
|
135
|
+
describe('async compilation', () => {
|
|
136
|
+
test('can use exported definitions from another module', async () => {
|
|
137
|
+
expect(await runPromise('a', { a: `import * as b from 'b'; 3+b.eight`, b: `export let eight = 8; {}` })).toEqual(
|
|
138
|
+
11,
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
test('allows specifying a custom source root', async () => {
|
|
142
|
+
expect(
|
|
143
|
+
await runPromise(
|
|
144
|
+
'a',
|
|
145
|
+
{ 'p/q/r/a': `import * as b from 'b'; 3+b.eight`, 'p/q/r/b': `export let eight = 8; {}` },
|
|
146
|
+
{},
|
|
147
|
+
'p/q/r',
|
|
148
|
+
),
|
|
149
|
+
).toEqual(11)
|
|
150
|
+
})
|
|
151
|
+
test('allows importing from the same directory via a relative path', async () => {
|
|
152
|
+
expect(await runPromise('s', { s: `import * as t from "./t"; t.ten+5`, t: `export let ten = 10` }, {})).toEqual(
|
|
153
|
+
15,
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
test('allows importing from sub directories', async () => {
|
|
157
|
+
expect(
|
|
158
|
+
await runPromise('q', { q: `import * as t from "./r/s/t"; t.ten+5`, 'r/s/t': `export let ten = 10` }, {}),
|
|
159
|
+
).toEqual(15)
|
|
160
|
+
expect(
|
|
161
|
+
await runPromise('q', {
|
|
162
|
+
q: `import * as t from './r/s/t'; t.ten * t.ten`,
|
|
163
|
+
'r/s/t': `import * as f from './d/e/f'; export let ten = f.five*2`,
|
|
164
|
+
'r/s/d/e/f': `export let five = 5`,
|
|
165
|
+
}),
|
|
166
|
+
).toEqual(100)
|
|
167
|
+
})
|
|
168
|
+
test('errors if a file tries to import a(nother) file which is outside of the source root tree', async () => {
|
|
169
|
+
await expect(runPromise('q', { 'd1/d2/q': `import * as r from "../r"; 5` }, {}, 'd1/d2')).rejects.toThrowError(
|
|
170
|
+
`resolved path (d1/r) is pointing outside of source root (d1/d2)`,
|
|
171
|
+
)
|
|
172
|
+
})
|
|
173
|
+
test('allows the main file to be specified via an absolute path if it points to a file under source root', async () => {
|
|
174
|
+
expect(await runPromise('/p/q/r/a', { '/p/q/r/a': `"apollo 11"` }, {}, '/p/q/r')).toEqual('apollo 11')
|
|
175
|
+
await expect(runPromise('/p/q/x', { '/p/q/x': `"apollo 11"` }, {}, '/p/q/r')).rejects.toThrowError(
|
|
176
|
+
'resolved path (/p/q/x) is pointing outside of source root (/p/q/r)',
|
|
177
|
+
)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
describe('errors in imported files', () => {
|
|
181
|
+
test('stack trace includes the name of the imported and a correct snippet from it', () => {
|
|
182
|
+
expect(() =>
|
|
183
|
+
run('q', { q: `import * as r from './r'; r.foo()`, r: `let a = {}; export let foo = () => a.b.c` }),
|
|
184
|
+
).toThrowError('at (r:1:36..40) a.b.c')
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
})
|