septima-lang 0.1.0 → 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/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 +1 -1
- 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,1169 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
|
|
3
|
+
import { Septima } from '../src/septima'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runs a Septima program for testing purposes. If the program evaluates to `sink` an `undefined` is
|
|
7
|
+
* returned.
|
|
8
|
+
* @param input the Septima program to run
|
|
9
|
+
*/
|
|
10
|
+
function run(input: string) {
|
|
11
|
+
return Septima.run(input, { onSink: () => undefined })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('septima', () => {
|
|
15
|
+
test('basics', () => {
|
|
16
|
+
expect(run(`5`)).toEqual(5)
|
|
17
|
+
expect(() => run(`6 789`)).toThrowError(`Loitering input at (<inline>:1:3..5) 789`)
|
|
18
|
+
expect(run(`3.14`)).toEqual(3.14)
|
|
19
|
+
})
|
|
20
|
+
test('const keyword behaves like let', () => {
|
|
21
|
+
expect(run(`const x = 5; x`)).toEqual(5)
|
|
22
|
+
expect(run(`const f = (a, b) => a + b; f(3, 4)`)).toEqual(7)
|
|
23
|
+
expect(run(`const a = 1; let b = 2; const c = 3; a + b + c`)).toEqual(6)
|
|
24
|
+
})
|
|
25
|
+
test('an optional return keyword can be placed before the result', () => {
|
|
26
|
+
expect(run(`return 5`)).toEqual(5)
|
|
27
|
+
expect(run(`return 3.14`)).toEqual(3.14)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('booleans', () => {
|
|
31
|
+
expect(run(`true`)).toEqual(true)
|
|
32
|
+
expect(run(`false`)).toEqual(false)
|
|
33
|
+
expect(run(`!true`)).toEqual(false)
|
|
34
|
+
expect(run(`!false`)).toEqual(true)
|
|
35
|
+
expect(run(`!!true`)).toEqual(true)
|
|
36
|
+
expect(run(`!!false`)).toEqual(false)
|
|
37
|
+
|
|
38
|
+
expect(run(`true||true`)).toEqual(true)
|
|
39
|
+
expect(run(`true||false`)).toEqual(true)
|
|
40
|
+
expect(run(`false||true`)).toEqual(true)
|
|
41
|
+
expect(run(`false||false`)).toEqual(false)
|
|
42
|
+
|
|
43
|
+
expect(run(`true && true`)).toEqual(true)
|
|
44
|
+
expect(run(`true && false`)).toEqual(false)
|
|
45
|
+
expect(run(`false && true`)).toEqual(false)
|
|
46
|
+
expect(run(`false && false`)).toEqual(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('arithmetics', () => {
|
|
50
|
+
expect(run(`8*2`)).toEqual(16)
|
|
51
|
+
expect(run(`3+1`)).toEqual(4)
|
|
52
|
+
expect(run(`20-3`)).toEqual(17)
|
|
53
|
+
expect(run(`48/6`)).toEqual(8)
|
|
54
|
+
expect(run(`(1+4)*6`)).toEqual(30)
|
|
55
|
+
expect(run(`1+4*6`)).toEqual(25)
|
|
56
|
+
expect(run(`20%6`)).toEqual(2)
|
|
57
|
+
expect(run(`20%8`)).toEqual(4)
|
|
58
|
+
expect(run(`40%15`)).toEqual(10)
|
|
59
|
+
expect(run(`6**3`)).toEqual(216)
|
|
60
|
+
expect(run(`6**4`)).toEqual(1296)
|
|
61
|
+
expect(run(`2*3**4`)).toEqual(162)
|
|
62
|
+
expect(run(`(2*3)**4`)).toEqual(1296)
|
|
63
|
+
|
|
64
|
+
expect(run(`8/0`)).toEqual(Infinity)
|
|
65
|
+
expect(run(`(-4) ** 0.5`)).toEqual(NaN)
|
|
66
|
+
|
|
67
|
+
expect(() => run(`!5`)).toThrowError(`value type error: expected bool but found 5`)
|
|
68
|
+
expect(() => run(`!0`)).toThrowError(`value type error: expected bool but found 0`)
|
|
69
|
+
expect(() => run(`!!0`)).toThrowError(`value type error: expected bool but found 0`)
|
|
70
|
+
expect(() => run(`!!4`)).toThrowError(`value type error: expected bool but found 4`)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('error message specifies the location in the file', () => {
|
|
74
|
+
expect(() => run(`7+\n6+\n5+4+3+!2`)).toThrowError(`value type error: expected bool but found 2`)
|
|
75
|
+
|
|
76
|
+
const expected = [
|
|
77
|
+
`value type error: expected num but found "zxcvbnm" when evaluating:`,
|
|
78
|
+
` at (<inline>:1:1..21) 9 * 8 * 'zxcvbnm' * 7`,
|
|
79
|
+
` at (<inline>:1:1..21) 9 * 8 * 'zxcvbnm' * 7`,
|
|
80
|
+
` at (<inline>:1:5..21) 8 * 'zxcvbnm' * 7`,
|
|
81
|
+
` at (<inline>:1:10..21) zxcvbnm' * 7`,
|
|
82
|
+
].join('\n')
|
|
83
|
+
|
|
84
|
+
expect(() => run(`9 * 8 * 'zxcvbnm' * 7`)).toThrowError(expected)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('equality', () => {
|
|
88
|
+
test('of numbers', () => {
|
|
89
|
+
expect(run(`3==4`)).toEqual(false)
|
|
90
|
+
expect(run(`3==3`)).toEqual(true)
|
|
91
|
+
expect(run(`3!=4`)).toEqual(true)
|
|
92
|
+
expect(run(`3!=3`)).toEqual(false)
|
|
93
|
+
})
|
|
94
|
+
test('of strings', () => {
|
|
95
|
+
expect(run(`'alpha' == 'beta'`)).toEqual(false)
|
|
96
|
+
expect(run(`'alpha' == 'alpha'`)).toEqual(true)
|
|
97
|
+
})
|
|
98
|
+
test('of boolean', () => {
|
|
99
|
+
expect(run(`false == false`)).toEqual(true)
|
|
100
|
+
expect(run(`false == true`)).toEqual(false)
|
|
101
|
+
expect(run(`true == false`)).toEqual(false)
|
|
102
|
+
expect(run(`true == true`)).toEqual(true)
|
|
103
|
+
})
|
|
104
|
+
test('of values of different types is always false', () => {
|
|
105
|
+
expect(run(`false == 5`)).toEqual(false)
|
|
106
|
+
expect(run(`'6' == 6`)).toEqual(false)
|
|
107
|
+
expect(run(`['alpha'] == 'alpha'`)).toEqual(false)
|
|
108
|
+
expect(run(`{} == []`)).toEqual(false)
|
|
109
|
+
expect(run(`((x) => (x+3)) == 6`)).toEqual(false)
|
|
110
|
+
})
|
|
111
|
+
test('of objects', () => {
|
|
112
|
+
expect(run(`{} == {}`)).toEqual(true)
|
|
113
|
+
expect(run(`{} == {a: 1}`)).toEqual(false)
|
|
114
|
+
expect(run(`{x: 1, y: {z: "ab".length}} == {x: 1, y: {z: 2}}`)).toEqual(true)
|
|
115
|
+
expect(run(`{x: 1, y: {z: "ab".length}} == {x: 1, y: {z: -2}}`)).toEqual(false)
|
|
116
|
+
})
|
|
117
|
+
test('object equality is not sensitive to the order of the attributes', () => {
|
|
118
|
+
expect(run(`{x: 1, y: 2} == {y: 2, x: 1}`)).toEqual(true)
|
|
119
|
+
})
|
|
120
|
+
test('of arrays', () => {
|
|
121
|
+
expect(run(`[10, 30, 19, 500] == [10, 3*10, 20-1, 5*100]`)).toEqual(true)
|
|
122
|
+
expect(run(`[10, 30, 19, -500] == [10, 3*10, 20-1, 5*100]`)).toEqual(false)
|
|
123
|
+
})
|
|
124
|
+
test('array equality is sensitive to the order of the items', () => {
|
|
125
|
+
expect(run(`['alpha', 'beta'] == ['beta', 'alpha']`)).toEqual(false)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('comparison', () => {
|
|
130
|
+
expect(run(`3>2`)).toEqual(true)
|
|
131
|
+
expect(run(`3>3`)).toEqual(false)
|
|
132
|
+
expect(run(`3>4`)).toEqual(false)
|
|
133
|
+
|
|
134
|
+
expect(run(`3>=2`)).toEqual(true)
|
|
135
|
+
expect(run(`3>=3`)).toEqual(true)
|
|
136
|
+
expect(run(`3>=4`)).toEqual(false)
|
|
137
|
+
|
|
138
|
+
expect(run(`3<=2`)).toEqual(false)
|
|
139
|
+
expect(run(`3<=3`)).toEqual(true)
|
|
140
|
+
expect(run(`3<=4`)).toEqual(true)
|
|
141
|
+
|
|
142
|
+
expect(run(`3<2`)).toEqual(false)
|
|
143
|
+
expect(run(`3<3`)).toEqual(false)
|
|
144
|
+
expect(run(`3<4`)).toEqual(true)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('combined arithmetics and logical expressions', () => {
|
|
148
|
+
expect(run(`(5 + 3 > 6) && (10*20 > 150)`)).toEqual(true)
|
|
149
|
+
expect(run(`(5 + 3 > 9) && (10*20 > 150)`)).toEqual(false)
|
|
150
|
+
expect(run(`(5 + 3 > 6) && (10*20 > 201)`)).toEqual(false)
|
|
151
|
+
expect(run(`(5 + 3 > 9) && (10*20 > 201)`)).toEqual(false)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('the rhs of a logical-or expression is evaluated only if lhs is false', () => {
|
|
155
|
+
expect(run(`true || x`)).toEqual(true)
|
|
156
|
+
expect(() => run(`false || x`)).toThrowError('Symbol x was not found')
|
|
157
|
+
})
|
|
158
|
+
test('the rhs of a logical-and expression is evaluated only if lhs is true', () => {
|
|
159
|
+
expect(run(`false && x`)).toEqual(false)
|
|
160
|
+
expect(() => run(`true && x`)).toThrowError('Symbol x was not found')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('eats whitespace', () => {
|
|
164
|
+
expect(run(` 8 * 2 `)).toEqual(16)
|
|
165
|
+
expect(run(`3 + 1`)).toEqual(4)
|
|
166
|
+
expect(run(`20 - 3`)).toEqual(17)
|
|
167
|
+
expect(run(`48 / 6`)).toEqual(8)
|
|
168
|
+
expect(run(`(1 + 4 ) *7`)).toEqual(35)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe('unary expressions', () => {
|
|
172
|
+
test('+', () => {
|
|
173
|
+
expect(run(`+7`)).toEqual(7)
|
|
174
|
+
expect(run(`3*+7`)).toEqual(21)
|
|
175
|
+
expect(run(`3 * +7`)).toEqual(21)
|
|
176
|
+
})
|
|
177
|
+
test('errors if + is applied to non-number', () => {
|
|
178
|
+
expect(() => run(`+true`)).toThrowError('expected num but found true')
|
|
179
|
+
expect(() => run(`+[]`)).toThrowError('expected num but found []')
|
|
180
|
+
expect(() => run(`+{}`)).toThrowError('expected num but found {}')
|
|
181
|
+
expect(() => run(`+(fun (x) x*2)`)).toThrowError('expected num but found "fun (x) (x * 2)"')
|
|
182
|
+
expect(() => run(`+'abc'`)).toThrowError(`expected num but found "abc"`)
|
|
183
|
+
})
|
|
184
|
+
test('-', () => {
|
|
185
|
+
expect(run(`-7`)).toEqual(-7)
|
|
186
|
+
expect(run(`3+-7`)).toEqual(-4)
|
|
187
|
+
expect(run(`3*-7`)).toEqual(-21)
|
|
188
|
+
expect(run(`-3*-7`)).toEqual(21)
|
|
189
|
+
expect(run(`3 + -7`)).toEqual(-4)
|
|
190
|
+
expect(run(`3 * -7`)).toEqual(-21)
|
|
191
|
+
expect(run(`-3 * -7`)).toEqual(21)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('strings', () => {
|
|
196
|
+
test('can be specified via the double-quotes notation', () => {
|
|
197
|
+
expect(run(`""`)).toEqual('')
|
|
198
|
+
expect(run(`"ab"`)).toEqual('ab')
|
|
199
|
+
expect(run(`"ab" + "cd"`)).toEqual('abcd')
|
|
200
|
+
})
|
|
201
|
+
test('can be specified via the single-quotes notation', () => {
|
|
202
|
+
expect(run(`''`)).toEqual('')
|
|
203
|
+
expect(run(`'ab'`)).toEqual('ab')
|
|
204
|
+
expect(run(`'ab' + 'cd'`)).toEqual('abcd')
|
|
205
|
+
})
|
|
206
|
+
test('does not trim leading/trailing whitespace', () => {
|
|
207
|
+
expect(run(`' ab'`)).toEqual(' ab')
|
|
208
|
+
expect(run(`'ab '`)).toEqual('ab ')
|
|
209
|
+
expect(run(`' '`)).toEqual(' ')
|
|
210
|
+
expect(run(`' ab '`)).toEqual(' ab ')
|
|
211
|
+
expect(run(`" ab"`)).toEqual(' ab')
|
|
212
|
+
expect(run(`"ab "`)).toEqual('ab ')
|
|
213
|
+
expect(run(`" "`)).toEqual(' ')
|
|
214
|
+
expect(run(`" ab "`)).toEqual(' ab ')
|
|
215
|
+
})
|
|
216
|
+
test('supports string methods', () => {
|
|
217
|
+
expect(run(`'bigbird'.substring(3, 7)`)).toEqual('bird')
|
|
218
|
+
expect(run(`'bigbird'.indexOf('g')`)).toEqual(2)
|
|
219
|
+
expect(run(`'ab-cde-fghi-jkl'.split('-')`)).toEqual(['ab', 'cde', 'fghi', 'jkl'])
|
|
220
|
+
expect(run(`let s = ' ab cd '; [s.trimStart(), s.trimEnd(), s.trim()]`)).toEqual([
|
|
221
|
+
'ab cd ',
|
|
222
|
+
' ab cd',
|
|
223
|
+
'ab cd',
|
|
224
|
+
])
|
|
225
|
+
})
|
|
226
|
+
test('supports optional arguments of string methods', () => {
|
|
227
|
+
expect(run(`'bigbird'.substring(5)`)).toEqual('rd')
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
describe('let', () => {
|
|
231
|
+
test('binds values to variables', () => {
|
|
232
|
+
expect(run(`let x = 5; x+3`)).toEqual(8)
|
|
233
|
+
expect(run(`let x = 5; let y = 20; x*y+4`)).toEqual(104)
|
|
234
|
+
})
|
|
235
|
+
test('do not need the trailing semicolon', () => {
|
|
236
|
+
expect(run(`let x = 5 x+3`)).toEqual(8)
|
|
237
|
+
expect(run(`let x = 5 let y = 20 x*y+4`)).toEqual(104)
|
|
238
|
+
})
|
|
239
|
+
test('fails if the variable was not defined', () => {
|
|
240
|
+
expect(() => run(`let x = 5; x+y`)).toThrowError('Symbol y was not found')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('parenthsized expression can have let defintions', () => {
|
|
244
|
+
expect(
|
|
245
|
+
run(`
|
|
246
|
+
let x = 5;
|
|
247
|
+
let y = 20;
|
|
248
|
+
|
|
249
|
+
x*y+(let n = 4; n*7)`),
|
|
250
|
+
).toEqual(128)
|
|
251
|
+
expect(
|
|
252
|
+
run(`
|
|
253
|
+
let x = 5;
|
|
254
|
+
let y = 20;
|
|
255
|
+
|
|
256
|
+
x*y+(let n = 4; let o = 7; o*n)`),
|
|
257
|
+
).toEqual(128)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('inner expressions can access variables from enclosing scopes', () => {
|
|
261
|
+
expect(
|
|
262
|
+
run(`
|
|
263
|
+
let x = 5;
|
|
264
|
+
let y = 20;
|
|
265
|
+
|
|
266
|
+
x*y+(let n = 4; n+x)`),
|
|
267
|
+
).toEqual(109)
|
|
268
|
+
})
|
|
269
|
+
test('definitions from inner scopes overshadow definitions from outer scopes', () => {
|
|
270
|
+
expect(
|
|
271
|
+
run(`
|
|
272
|
+
let x = 5;
|
|
273
|
+
let y = 20;
|
|
274
|
+
|
|
275
|
+
x*y+(let n = 4; let x = 200; n+x)`),
|
|
276
|
+
).toEqual(304)
|
|
277
|
+
})
|
|
278
|
+
test('the body of a definition can reference an earlier definition from the same scope', () => {
|
|
279
|
+
expect(run(`let x = 10; let y = x*2; y*2`)).toEqual(40)
|
|
280
|
+
})
|
|
281
|
+
test('the body of a definition cannot reference a latter definition from the same scope', () => {
|
|
282
|
+
expect(() => run(`let y = x*2; let x = 10; y*2`)).toThrowError(`Symbol x was not found`)
|
|
283
|
+
})
|
|
284
|
+
test('the body of a definition cannot reference itself', () => {
|
|
285
|
+
expect(() => run(`let x = 10; let y = if (x > 0) y else x; y*2`)).toThrowError(`Unresolved definition: y`)
|
|
286
|
+
})
|
|
287
|
+
test('uses lexical scoping (and not dynamic scoping)', () => {
|
|
288
|
+
const actual = run(`let x = (let a = 1; a+1); let y = (let a=100; x+1); y`)
|
|
289
|
+
expect(actual).toEqual(3)
|
|
290
|
+
})
|
|
291
|
+
test('definitions go out of scope', () => {
|
|
292
|
+
expect(() => run(`let x = (let a = 1; a+1); a+100`)).toThrowError('Symbol a was not found')
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
describe('semicolons before the expression', () => {
|
|
297
|
+
test('are allowed', () => {
|
|
298
|
+
expect(run(`let a = 5; ;;;;4.8`)).toEqual(4.8)
|
|
299
|
+
expect(run(`;;;"abc"`)).toEqual('abc')
|
|
300
|
+
expect(run(`;4.8`)).toEqual(4.8)
|
|
301
|
+
})
|
|
302
|
+
test('can be interleaved with whitspace', () => {
|
|
303
|
+
expect(run(`let a = 5; ;; ;; 4.8`)).toEqual(4.8)
|
|
304
|
+
expect(run(`;; ; "abc"`)).toEqual('abc')
|
|
305
|
+
expect(run(` ;\n4.8`)).toEqual(4.8)
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
describe('arrays', () => {
|
|
310
|
+
test('array literals are specified via the enclosing brackets notation ([])', () => {
|
|
311
|
+
expect(run(`["ab", 5]`)).toEqual(['ab', 5])
|
|
312
|
+
expect(run(`[]`)).toEqual([])
|
|
313
|
+
})
|
|
314
|
+
test('allow a dangling comma', () => {
|
|
315
|
+
expect(run(`[,]`)).toEqual([])
|
|
316
|
+
expect(run(`[,,]`)).toEqual([])
|
|
317
|
+
expect(run(`[246,]`)).toEqual([246])
|
|
318
|
+
expect(run(`[246,531,]`)).toEqual([246, 531])
|
|
319
|
+
})
|
|
320
|
+
test('individual elements of an array can be accessed via the [<index>] notation', () => {
|
|
321
|
+
expect(run(`let a = ['sun', 'mon', 'tue', 'wed']; a[1]`)).toEqual('mon')
|
|
322
|
+
})
|
|
323
|
+
test('the <index> value at the [<index>] notation can be a computed value', () => {
|
|
324
|
+
expect(run(`let a = ['sun', 'mon', 'tue', 'wed']; let f = fun(n) n-5; [a[3-1], a[18/6], a[f(5)]]`)).toEqual([
|
|
325
|
+
'tue',
|
|
326
|
+
'wed',
|
|
327
|
+
'sun',
|
|
328
|
+
])
|
|
329
|
+
})
|
|
330
|
+
test('arrayness of a value can be tested via Array.isArry()', () => {
|
|
331
|
+
expect(run(`Array.isArray([1,2,'abc'])`)).toEqual(true)
|
|
332
|
+
expect(run(`Array.isArray('abc')`)).toEqual(false)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
describe('objects', () => {
|
|
337
|
+
describe('literals', () => {
|
|
338
|
+
test('are specified via JSON format', () => {
|
|
339
|
+
expect(run(`{}`)).toEqual({})
|
|
340
|
+
expect(run(`{a: 1}`)).toEqual({ a: 1 })
|
|
341
|
+
expect(run(`{a: 1, b: 2}`)).toEqual({ a: 1, b: 2 })
|
|
342
|
+
expect(run(`{a: "A", b: "B", c: "CCC"}`)).toEqual({ a: 'A', b: 'B', c: 'CCC' })
|
|
343
|
+
})
|
|
344
|
+
test('attribute names can be double/single quoted', () => {
|
|
345
|
+
expect(run(`{"a": 1}`)).toEqual({ a: 1 })
|
|
346
|
+
expect(run(`{'b': 2}`)).toEqual({ b: 2 })
|
|
347
|
+
expect(run(`{"the quick brown": "fox", 'jumps over': "the"}`)).toEqual({
|
|
348
|
+
'the quick brown': 'fox',
|
|
349
|
+
'jumps over': 'the',
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
test('allow a dangling comma', () => {
|
|
353
|
+
expect(run(`{a: 1,}`)).toEqual({ a: 1 })
|
|
354
|
+
expect(run(`{a: 1, b: 2,}`)).toEqual({ a: 1, b: 2 })
|
|
355
|
+
expect(run(`{a: "A", b: "B", c: "CCC",}`)).toEqual({ a: 'A', b: 'B', c: 'CCC' })
|
|
356
|
+
})
|
|
357
|
+
test('a dangling comma in an empty object is not allowed', () => {
|
|
358
|
+
expect(() => run(`{,}`)).toThrowError('Expected an identifier at (<inline>:1:2..3) ,}')
|
|
359
|
+
})
|
|
360
|
+
test('supports computed attributes names via the [<expression>]: <value> notation', () => {
|
|
361
|
+
expect(run(`{["a" + 'b']: 'a-and-b'}`)).toEqual({ ab: 'a-and-b' })
|
|
362
|
+
})
|
|
363
|
+
test('supports shorthand notation for initializing an attribute from an identifier', () => {
|
|
364
|
+
expect(run(`let a = 'A'; let b = 42; {a, b}`)).toEqual({ a: 'A', b: 42 })
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
describe('attributes', () => {
|
|
368
|
+
test('can be accessed via the .<ident> notation', () => {
|
|
369
|
+
expect(run(`let x = {a: 3, b: 4}; x.a`)).toEqual(3)
|
|
370
|
+
expect(run(`let x = {a: 3, b: 4}; x.a * x.b`)).toEqual(12)
|
|
371
|
+
expect(run(`let x = {a: 3, b: {x: {Jan: 1, Feb: 2, May: 5}, y: 300}}; [x.b.x.Jan, x.b.x.May, x.b.y]`)).toEqual([
|
|
372
|
+
1, 5, 300,
|
|
373
|
+
])
|
|
374
|
+
expect(run(`let x = {a: 3, calendar: ["A"] }; x.calendar`)).toEqual(['A'])
|
|
375
|
+
expect(
|
|
376
|
+
run(
|
|
377
|
+
`let x = {a: 3, calendar: {months: { Jan: 1, Feb: 2, May: 5}, days: ["Mon", "Tue", "Wed" ] } }; [x.calendar.months, x.calendar.days]`,
|
|
378
|
+
),
|
|
379
|
+
).toEqual([{ Jan: 1, Feb: 2, May: 5 }, ['Mon', 'Tue', 'Wed']])
|
|
380
|
+
})
|
|
381
|
+
test('can be accessed via the [<name>] notation', () => {
|
|
382
|
+
expect(run(`let x = {a: 3, b: 4}; x['a']`)).toEqual(3)
|
|
383
|
+
expect(run(`let x = {a: 3, b: 4}; [x['a'], x["b"]]`)).toEqual([3, 4])
|
|
384
|
+
expect(run(`let x = {a: 3, b: {x: {Jan: 1, Feb: 2, May: 5}, y: 300}}; x["b"]['x']["May"]`)).toEqual(5)
|
|
385
|
+
})
|
|
386
|
+
test('supports chains of attribute accesses mixing the .<ident> and the [<name>] notations', () => {
|
|
387
|
+
expect(run(`let o = {b: {x: {M: 5}}}; [o["b"].x["M"], o.b["x"].M, o.b.x["M"]]`)).toEqual([5, 5, 5])
|
|
388
|
+
})
|
|
389
|
+
test('supports chains of calls to nested attributes which are lambda expressions', () => {
|
|
390
|
+
expect(run(`let o = {a: fun () { b: fun () { c: fun () { d: 'x' }}}}; o.a().b().c().d`)).toEqual('x')
|
|
391
|
+
expect(run(`let o = {a: fun () { b: { c: fun () { d: 'x' }}}}; o.a().b.c().d`)).toEqual('x')
|
|
392
|
+
})
|
|
393
|
+
test('the <name> value at the [<name>] notation can be a computed value', () => {
|
|
394
|
+
expect(run(`let q = fun (x) x + "eb"; let o = {Jan: 1, Feb: 2, May: 5}; [o["Ja" + 'n'], o[q('F')]]`)).toEqual([
|
|
395
|
+
1, 2,
|
|
396
|
+
])
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
describe('spread operator in objects', () => {
|
|
402
|
+
test('shallow copies an object into an object literal', () => {
|
|
403
|
+
expect(run(`let o = {a: 1, b: 2}; {...o}`)).toEqual({ a: 1, b: 2 })
|
|
404
|
+
})
|
|
405
|
+
test('can be combined with hard-coded (literal) attributes', () => {
|
|
406
|
+
expect(run(`let o = {a: 1}; {...o, b: 2}`)).toEqual({ a: 1, b: 2 })
|
|
407
|
+
expect(run(`let o = {b: 2}; {...o, a: 1, ...o}`)).toEqual({ a: 1, b: 2 })
|
|
408
|
+
})
|
|
409
|
+
test('can be used multiple times inside a single object literal', () => {
|
|
410
|
+
expect(run(`let o1 = {b: 2}; let o2 = {c: 3}; {a: 1, ...o1, ...o2, d: 4}`)).toEqual({
|
|
411
|
+
a: 1,
|
|
412
|
+
b: 2,
|
|
413
|
+
c: 3,
|
|
414
|
+
d: 4,
|
|
415
|
+
})
|
|
416
|
+
})
|
|
417
|
+
test('overrides attributes to its left', () => {
|
|
418
|
+
expect(run(`let o = {b: 2}; {a: 100, b: 200, c: 300, ...o}`)).toEqual({ a: 100, b: 2, c: 300 })
|
|
419
|
+
})
|
|
420
|
+
test('overridden by attributes to its right', () => {
|
|
421
|
+
expect(run(`let o = {a: 1, b: 2, c: 3}; {...o, b: 200}`)).toEqual({ a: 1, b: 200, c: 3 })
|
|
422
|
+
})
|
|
423
|
+
test('can be mixed with computed attribute names', () => {
|
|
424
|
+
expect(run(`let o = {ab: 'anteater'}; {...o, ['c' + 'd']: 'cat'}`)).toEqual({ ab: 'anteater', cd: 'cat' })
|
|
425
|
+
})
|
|
426
|
+
test('errors if applied to a non-object value', () => {
|
|
427
|
+
expect(() => run(`let o = ['a']; {...o}`)).toThrowError(`value type error: expected obj but found ["a"]`)
|
|
428
|
+
expect(() => run(`let o = true; {...o}`)).toThrowError('value type error: expected obj but found true')
|
|
429
|
+
expect(() => run(`let o = 5; {...o}`)).toThrowError('value type error: expected obj but found 5')
|
|
430
|
+
expect(() => run(`let o = 'a'; {...o}`)).toThrowError('value type error: expected obj but found "a"')
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
describe('spread operator in arrays', () => {
|
|
435
|
+
test('shallow copies an array into an array literal', () => {
|
|
436
|
+
expect(run(`let a = ['x', 'y']; [...a]`)).toEqual(['x', 'y'])
|
|
437
|
+
})
|
|
438
|
+
test('can be mixed with array elements', () => {
|
|
439
|
+
expect(run(`let a = ['x', 'y']; ['p', ...a, 'q']`)).toEqual(['p', 'x', 'y', 'q'])
|
|
440
|
+
})
|
|
441
|
+
test('can be used multiple times inside an array literal', () => {
|
|
442
|
+
expect(run(`let a1 = ['x', 'y']; let a2 = ['z']; ['p', ...a1, 'q', ...a2, 'r']`)).toEqual([
|
|
443
|
+
'p',
|
|
444
|
+
'x',
|
|
445
|
+
'y',
|
|
446
|
+
'q',
|
|
447
|
+
'z',
|
|
448
|
+
'r',
|
|
449
|
+
])
|
|
450
|
+
})
|
|
451
|
+
test('errors if applied to a non-array value', () => {
|
|
452
|
+
expect(() => run(`let a = true; [...a]`)).toThrowError('value type error: expected arr but found true')
|
|
453
|
+
expect(() => run(`let a = 5; [...a]`)).toThrowError('value type error: expected arr but found 5')
|
|
454
|
+
expect(() => run(`let a = {x: 1}; [...a]`)).toThrowError(`value type error: expected arr but found {"x":1}`)
|
|
455
|
+
expect(() => run(`let a = 'a'; [...a]`)).toThrowError('value type error: expected arr but found "a"')
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
describe('if', () => {
|
|
460
|
+
test('returns the value of the first branch if the condition is true', () => {
|
|
461
|
+
expect(run(`if (4 > 3) 200 else -100`)).toEqual(200)
|
|
462
|
+
})
|
|
463
|
+
test('evaluates the first branch only if the condition is true', () => {
|
|
464
|
+
expect(() => run(`if (true) x else -100`)).toThrowError('Symbol x was not found')
|
|
465
|
+
expect(run(`if (false) x else -100`)).toEqual(-100)
|
|
466
|
+
})
|
|
467
|
+
test('returns the value of the second branch if the condition is false', () => {
|
|
468
|
+
expect(run(`if (4 < 3) 200 else -100`)).toEqual(-100)
|
|
469
|
+
})
|
|
470
|
+
test('evaluates the second branch only if the condition is false', () => {
|
|
471
|
+
expect(() => run(`if (false) 200 else x`)).toThrowError('Symbol x was not found')
|
|
472
|
+
expect(run(`if (true) 200 else x`)).toEqual(200)
|
|
473
|
+
})
|
|
474
|
+
test('yells if conditions is not boolean', () => {
|
|
475
|
+
expect(() => run(`if (5+8) 200 else -100`)).toThrowError('value type error: expected bool but found 13')
|
|
476
|
+
})
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
describe('ternary', () => {
|
|
480
|
+
test('returns the value of the first branch if the condition is true', () => {
|
|
481
|
+
expect(run(`(4 > 3) ? 200 : -100`)).toEqual(200)
|
|
482
|
+
})
|
|
483
|
+
test('evaluates the first branch only if the condition is true', () => {
|
|
484
|
+
expect(() => run(`true ? x : -100`)).toThrowError('Symbol x was not found')
|
|
485
|
+
expect(run(`false ? x : -100`)).toEqual(-100)
|
|
486
|
+
})
|
|
487
|
+
test('returns the value of the second branch if the condition is false', () => {
|
|
488
|
+
expect(run(`(4 < 3) ? 200 : -100`)).toEqual(-100)
|
|
489
|
+
})
|
|
490
|
+
test('evaluates the second branch only if the condition is false', () => {
|
|
491
|
+
expect(() => run(`false ? 200 : x`)).toThrowError('Symbol x was not found')
|
|
492
|
+
expect(run(`true ? 200 : x`)).toEqual(200)
|
|
493
|
+
})
|
|
494
|
+
test('yells if conditions is not boolean', () => {
|
|
495
|
+
expect(() => run(`5+8 ? 200 : -100`)).toThrowError('value type error: expected bool but found 13')
|
|
496
|
+
})
|
|
497
|
+
test('higher precendence than lambda', () => {
|
|
498
|
+
expect(run(`let f = (a,b) => a > b ? 'ABOVE' : 'BELOW'; f(1,2) + '_' + f(2,1)`)).toEqual('BELOW_ABOVE')
|
|
499
|
+
})
|
|
500
|
+
test('higher precendence than if', () => {
|
|
501
|
+
expect(run(`if (5 < 2) "Y" else 3+4>8? 'ABOVE' : 'BELOW'`)).toEqual('BELOW')
|
|
502
|
+
})
|
|
503
|
+
test('can span multiple lines', () => {
|
|
504
|
+
expect(run(`3 + 4 > 6\n? 'ABOVE'\n: 'BELOW'`)).toEqual('ABOVE')
|
|
505
|
+
expect(run(`3 + 4 > 8\n? 'ABOVE'\n: 'BELOW'`)).toEqual('BELOW')
|
|
506
|
+
expect(run(`3 + 4 > 6?\n 'ABOVE':\n 'BELOW'`)).toEqual('ABOVE')
|
|
507
|
+
expect(run(`3 + 4 > 8?\n 'ABOVE':\n 'BELOW'`)).toEqual('BELOW')
|
|
508
|
+
})
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
describe('lambda expressions', () => {
|
|
512
|
+
test('binds the value of the actual arg to the formal arg', () => {
|
|
513
|
+
expect(run(`(fun(a) 2*a)(3)`)).toEqual(6)
|
|
514
|
+
expect(run(`(fun(a, b) a*a-b*b)(3,4)`)).toEqual(-7)
|
|
515
|
+
expect(run(`(fun(a, b) a*a-b*b)(4,3)`)).toEqual(7)
|
|
516
|
+
})
|
|
517
|
+
test('can be stored in a variable', () => {
|
|
518
|
+
expect(run(`let triple = (fun(a) 3*a); triple(100) - triple(90)`)).toEqual(30)
|
|
519
|
+
expect(run(`let triple = fun(a) 3*a; triple(100) - triple(90)`)).toEqual(30)
|
|
520
|
+
})
|
|
521
|
+
test('allows a dangling comma, at the call site, after the last actual argument', () => {
|
|
522
|
+
expect(run(`let triple = (fun(a) 3*a); triple(100,)`)).toEqual(300)
|
|
523
|
+
expect(run(`let mean = (fun(a,b) (a+b)/2); mean(4, 28,)`)).toEqual(16)
|
|
524
|
+
})
|
|
525
|
+
describe('arrow function notation', () => {
|
|
526
|
+
test('a single formal argument does not need to be surrounded with parenthesis', () => {
|
|
527
|
+
expect(run(`let triple = a => 3*a; triple(100)`)).toEqual(300)
|
|
528
|
+
})
|
|
529
|
+
test('(a) => <expression>', () => {
|
|
530
|
+
expect(run(`let triple = (a) => 3*a; triple(100)`)).toEqual(300)
|
|
531
|
+
})
|
|
532
|
+
test('() => <expression>', () => {
|
|
533
|
+
expect(run(`let five = () => 5; five()`)).toEqual(5)
|
|
534
|
+
})
|
|
535
|
+
test('(a,b) => <expression>', () => {
|
|
536
|
+
expect(run(`let conc = (a,b) => a+b; conc('al', 'pha')`)).toEqual('alpha')
|
|
537
|
+
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')
|
|
538
|
+
})
|
|
539
|
+
test('body of an arrow function can be { return <expression>}', () => {
|
|
540
|
+
expect(run(`let triple = a => { return 3*a }; triple(100)`)).toEqual(300)
|
|
541
|
+
expect(run(`let triple = (a) => { return 3*a }; triple(100)`)).toEqual(300)
|
|
542
|
+
expect(run(`let five = () => { return 5 }; five()`)).toEqual(5)
|
|
543
|
+
expect(run(`let concat = (a,b) => { return a+b }; concat('al', 'pha')`)).toEqual('alpha')
|
|
544
|
+
})
|
|
545
|
+
test('body of an arrow function can include let definitions', () => {
|
|
546
|
+
expect(run(`let triple = a => { let factor = 3; return factor*a }; triple(100)`)).toEqual(300)
|
|
547
|
+
expect(run(`let triple = (a) => { let factor = 3; return 3*a }; triple(100)`)).toEqual(300)
|
|
548
|
+
expect(run(`let five = () => { let two = 2; let three = 3; return three+two }; five()`)).toEqual(5)
|
|
549
|
+
expect(run(`let concat = (a,b) => { let u = '_'; return u+a+b+u }; concat('a', 'b')`)).toEqual('_ab_')
|
|
550
|
+
})
|
|
551
|
+
test('allows a dangling comma after last formal arg', () => {
|
|
552
|
+
expect(run(`let f = (a,) => a+1000; f(3)`)).toEqual(1003)
|
|
553
|
+
expect(run(`let f = (a,b,) => a+b+1000; f(5,900)`)).toEqual(1905)
|
|
554
|
+
})
|
|
555
|
+
})
|
|
556
|
+
test('can have no args', () => {
|
|
557
|
+
expect(run(`let pi = fun() 3.14; 2*pi()`)).toEqual(6.28)
|
|
558
|
+
expect(run(`(fun() 3.14)()*2`)).toEqual(6.28)
|
|
559
|
+
})
|
|
560
|
+
test('errors on arg list mismatch', () => {
|
|
561
|
+
expect(() => run(`let quadSum = fun(a,b,c,d) a+b+c+d; quadSum(4,8,2)`)).toThrowError(
|
|
562
|
+
'Expected at least 4 argument(s) but got 3 when evaluating',
|
|
563
|
+
)
|
|
564
|
+
expect(run(`let quadSum = fun(a,b,c,d) a+b+c+d; quadSum(4,8,2,6)`)).toEqual(20)
|
|
565
|
+
})
|
|
566
|
+
test('can be recursive', () => {
|
|
567
|
+
expect(run(`let factorial = fun(n) if (n > 0) n*factorial(n-1) else 1; factorial(6)`)).toEqual(720)
|
|
568
|
+
expect(run(`let gcd = fun(a, b) if (b == 0) a else gcd(b, a % b); [gcd(24, 60), gcd(1071, 462)]`)).toEqual([
|
|
569
|
+
12, 21,
|
|
570
|
+
])
|
|
571
|
+
})
|
|
572
|
+
test('can access definitions from the enclosing scope', () => {
|
|
573
|
+
expect(run(`let a = 1; (let inc = fun(n) n+a; inc(2))`)).toEqual(3)
|
|
574
|
+
expect(run(`let by2 = fun(x) x*2; (let by10 = (let by5 = fun(x) x*5; fun(x) by2(by5(x))); by10(20))`)).toEqual(
|
|
575
|
+
200,
|
|
576
|
+
)
|
|
577
|
+
})
|
|
578
|
+
test('expression trace on error', () => {
|
|
579
|
+
const expected = [
|
|
580
|
+
' at (<inline>:1:1..88) let d = fun(x1) x2; let c = fun(x) d(x); let b = fun (x) c(x); let a = fun(x) b(...',
|
|
581
|
+
' at (<inline>:1:85..88) a(5)',
|
|
582
|
+
' at (<inline>:1:79..82) b(x)',
|
|
583
|
+
' at (<inline>:1:58..61) c(x)',
|
|
584
|
+
' at (<inline>:1:36..39) d(x)',
|
|
585
|
+
' at (<inline>:1:17..18) x2',
|
|
586
|
+
].join('\n')
|
|
587
|
+
|
|
588
|
+
expect(() =>
|
|
589
|
+
run(`let d = fun(x1) x2; let c = fun(x) d(x); let b = fun (x) c(x); let a = fun(x) b(x); a(5)`),
|
|
590
|
+
).toThrowError(expected)
|
|
591
|
+
})
|
|
592
|
+
test('only lexical scope is considered when looking up a definition', () => {
|
|
593
|
+
expect(run(`let a = 1; let inc = fun(n) n+a; (let a = 100; inc(2))`)).toEqual(3)
|
|
594
|
+
})
|
|
595
|
+
test('can return another lambda expression (a-la currying)', () => {
|
|
596
|
+
expect(run(`let sum = fun(a) fun(b,c) a+b+c; sum(1)(600,20)`)).toEqual(621)
|
|
597
|
+
expect(run(`let sum = fun(a) fun(b) fun(c) a+b+c; sum(1)(600)(20)`)).toEqual(621)
|
|
598
|
+
expect(run(`let sum = fun(a) fun(b,c) a+b+c; let plusOne = sum(1); plusOne(600,20)`)).toEqual(621)
|
|
599
|
+
expect(run(`let sum = fun(a) fun(b) fun(c) a+b+c; let plusOne = sum(1); plusOne(600)(20)`)).toEqual(621)
|
|
600
|
+
})
|
|
601
|
+
describe('optional arguments', () => {
|
|
602
|
+
test('takes the default value if no valu for that arg was not passed', () => {
|
|
603
|
+
expect(run(`let sum = (a, b = 50) => a + b; [sum(9), sum(9,1)]`)).toEqual([59, 10])
|
|
604
|
+
})
|
|
605
|
+
test('the default value can be an arry or an object', () => {
|
|
606
|
+
expect(run(`let f = (i, vs = ['alpha', 'beta']) => vs[i]; [f(0), f(1)]`)).toEqual(['alpha', 'beta'])
|
|
607
|
+
expect(run(`let f = (s, vs = {a: 1, b: 2}) => vs[s]; [f('a'), f('b')]`)).toEqual([1, 2])
|
|
608
|
+
})
|
|
609
|
+
test('the default value can be an expression computed from other definision in the enclosing scope', () => {
|
|
610
|
+
expect(run(`let s = 'word'; let n = 100; let f = (a, g = s + n) => a + g.toUpperCase(); f('_')`)).toEqual(
|
|
611
|
+
'_WORD100',
|
|
612
|
+
)
|
|
613
|
+
})
|
|
614
|
+
test('a single argument arrow function can have a default value', () => {
|
|
615
|
+
expect(run(`let s = 'word'; let n = 100; let f = (a = s.toUpperCase()) => '%' + a + '%'; f()`)).toEqual(
|
|
616
|
+
'%WORD%',
|
|
617
|
+
)
|
|
618
|
+
})
|
|
619
|
+
test('errors if there is an argument without a default value after an arugument with a default value', () => {
|
|
620
|
+
expect(() => run(`let f = (a, b = 2000, c) => a+b+c`)).toThrowError(
|
|
621
|
+
'A required parameter cannot follow an optional parameter: at (<inline>:1:23..33) c) => a+b+c',
|
|
622
|
+
)
|
|
623
|
+
})
|
|
624
|
+
test('when undefined is passed to an arg with a default value, the default value is used', () => {
|
|
625
|
+
expect(run(`let f = (a, b = 2000, c = 3) => a+b+c; f(1, undefined, 5)`)).toEqual(2006)
|
|
626
|
+
})
|
|
627
|
+
test('a dangling comma is allowed after last default value', () => {
|
|
628
|
+
expect(run(`let f = (a, b = 2000,) => a+b; f(5)`)).toEqual(2005)
|
|
629
|
+
expect(run(`let f = (a, b = 2000,) => a+b; f(5,20)`)).toEqual(25)
|
|
630
|
+
})
|
|
631
|
+
test('errors if too few arguments are passed', () => {
|
|
632
|
+
expect(() => run(`let f = (a, b) => a+b; f(1)`)).toThrowError('Expected at least 2 argument(s) but got 1')
|
|
633
|
+
expect(() => run(`let f = (a, b, c = 3) => a+b+c; f(1)`)).toThrowError(
|
|
634
|
+
'Expected at least 2 argument(s) but got 1',
|
|
635
|
+
)
|
|
636
|
+
})
|
|
637
|
+
test('extra arguments are silently ignored', () => {
|
|
638
|
+
expect(run(`let f = (a, b) => a+b; f(1, 2, 3)`)).toEqual(3)
|
|
639
|
+
expect(run(`let f = (a, b = 2) => a+b; f(1, 2, 3)`)).toEqual(3)
|
|
640
|
+
expect(run(`let f = () => 5; f(1)`)).toEqual(5)
|
|
641
|
+
})
|
|
642
|
+
test('accepts correct number of arguments at boundaries', () => {
|
|
643
|
+
expect(run(`let f = (a, b = 2) => a+b; f(1)`)).toEqual(3)
|
|
644
|
+
expect(run(`let f = (a, b = 2) => a+b; f(1, 10)`)).toEqual(11)
|
|
645
|
+
expect(run(`let f = (a = 1, b = 2) => a+b; f()`)).toEqual(3)
|
|
646
|
+
expect(run(`let f = (a = 1, b = 2) => a+b; f(10)`)).toEqual(12)
|
|
647
|
+
expect(run(`let f = (a = 1, b = 2) => a+b; f(10, 20)`)).toEqual(30)
|
|
648
|
+
})
|
|
649
|
+
})
|
|
650
|
+
})
|
|
651
|
+
describe('array methods', () => {
|
|
652
|
+
test('concat', () => {
|
|
653
|
+
expect(run(`['foo', 'bar', 'goo'].concat(['zoo', 'poo'])`)).toEqual(['foo', 'bar', 'goo', 'zoo', 'poo'])
|
|
654
|
+
})
|
|
655
|
+
test('every', () => {
|
|
656
|
+
expect(run(`["", 'x', 'xx'].every(fun (item, i) item.length == i)`)).toEqual(true)
|
|
657
|
+
expect(run(`["", 'yy', 'zz'].every(fun (item, i) item.length == i)`)).toEqual(false)
|
|
658
|
+
expect(
|
|
659
|
+
run(`let cb = fun (item, i, a) item == a[(a.length - i) - 1]; [[2, 7, 2].every(cb), [2, 7, 7].every(cb)]`),
|
|
660
|
+
).toEqual([true, false])
|
|
661
|
+
})
|
|
662
|
+
test('filter', () => {
|
|
663
|
+
expect(run(`['foo', 'bar', 'goo'].filter(fun (item) item.endsWith('oo'))`)).toEqual(['foo', 'goo'])
|
|
664
|
+
expect(run(`['a', 'b', 'c', 'd'].filter(fun (item, i) i % 2 == 1)`)).toEqual(['b', 'd'])
|
|
665
|
+
expect(run(`[8, 8, 2, 2, 2, 7].filter(fun (x, i, a) x == a[(i + 1) % a.length])`)).toEqual([8, 2, 2])
|
|
666
|
+
})
|
|
667
|
+
test('find', () => {
|
|
668
|
+
expect(run(`[10, 20, 30, 40].find(fun (item, i) item + i == 21)`)).toEqual(20)
|
|
669
|
+
expect(run(`[8, 3, 7, 7, 6, 9].find(fun (x, i, a) x == a[a.length - (i+1)])`)).toEqual(7)
|
|
670
|
+
})
|
|
671
|
+
test('findIndex', () => {
|
|
672
|
+
expect(run(`[10, 20, 30, 40].findIndex(fun (item, i) item + i == 32)`)).toEqual(2)
|
|
673
|
+
expect(run(`[8, 3, 7, 7, 6, 9].findIndex(fun (x, i, a) x == a[a.length - (i+1)])`)).toEqual(2)
|
|
674
|
+
})
|
|
675
|
+
test('findIndex returns -1 if no matching element exists', () => {
|
|
676
|
+
expect(run(`[3, 5, 7, 9].findIndex(fun (x) x % 2 == 0)`)).toEqual(-1)
|
|
677
|
+
expect(run(`[].findIndex(fun () true)`)).toEqual(-1)
|
|
678
|
+
})
|
|
679
|
+
test('flatMap', () => {
|
|
680
|
+
expect(run(`['Columbia', 'Eagle'].flatMap(fun (x) [x, x.length])`)).toEqual(['Columbia', 8, 'Eagle', 5])
|
|
681
|
+
expect(run(`[6,7,9].flatMap(fun (x,i) if (i % 2 == 0) [x, x/3] else [])`)).toEqual([6, 2, 9, 3])
|
|
682
|
+
expect(run(`[2,1,6,5,9,8].flatMap(fun (x,i,a) if (i % 2 == 1) [x, a[i-1]] else [])`)).toEqual([1, 2, 5, 6, 8, 9])
|
|
683
|
+
})
|
|
684
|
+
test('map', () => {
|
|
685
|
+
expect(run(`['foo', 'bar', 'goo'].map(fun (s) s.charAt(0))`)).toEqual(['f', 'b', 'g'])
|
|
686
|
+
expect(run(`['a', 'b'].map(fun (item, i) item + ':' + i)`)).toEqual(['a:0', 'b:1'])
|
|
687
|
+
expect(run(`['a', 'b', 'p', 'q'].map(fun (x, i, a) x + a[a.length - (i+1)])`)).toEqual(['aq', 'bp', 'pb', 'qa'])
|
|
688
|
+
})
|
|
689
|
+
test('reduce', () => {
|
|
690
|
+
expect(run(`['a','b','c','d'].reduce(fun (w, x) w+x, '')`)).toEqual('abcd')
|
|
691
|
+
expect(run(`['a','b','c','d','e'].reduce(fun (w, x, i) if (i % 2 == 0) w+x else w, '')`)).toEqual('ace')
|
|
692
|
+
expect(run(`[['w',2], ['x',0], ['y',1]].reduce(fun (w, x, i, a) w+a[x[1]][0], '')`)).toEqual('ywx')
|
|
693
|
+
})
|
|
694
|
+
test('reduceRight', () => {
|
|
695
|
+
expect(run(`['a','b','c','d'].reduceRight(fun (w, x) w+x, '')`)).toEqual('dcba')
|
|
696
|
+
expect(run(`['a','b','c','d','e'].reduceRight(fun (w, x, i) if (i % 2 == 0) w+x else w, '')`)).toEqual('eca')
|
|
697
|
+
expect(run(`[['w',2], ['x',0], ['y',1]].reduceRight(fun (w, x, i, a) w+a[x[1]][0], '')`)).toEqual('xwy')
|
|
698
|
+
})
|
|
699
|
+
test('some', () => {
|
|
700
|
+
expect(run(`['foo', 'bar', 'goo'].some(fun (item) item.endsWith('oo'))`)).toEqual(true)
|
|
701
|
+
expect(run(`['foo', 'bar', 'goo'].some(fun (item) item.endsWith('pp'))`)).toEqual(false)
|
|
702
|
+
expect(run(`['a', 'xyz', 'bc'].some(fun (item, i) i == item.length)`)).toEqual(true)
|
|
703
|
+
expect(run(`[8, 3, 7, 7, 6, 9].some(fun (x, i, a) x == a[a.length - (i+1)])`)).toEqual(true)
|
|
704
|
+
})
|
|
705
|
+
describe('sort', () => {
|
|
706
|
+
test('can sort numbers', () => {
|
|
707
|
+
expect(run(`[5, 9, 3, 8, 6, 4].sort()`)).toEqual([3, 4, 5, 6, 8, 9])
|
|
708
|
+
})
|
|
709
|
+
test('does not change the array', () => {
|
|
710
|
+
expect(run(`let a = [4,3]; let b = a.sort(); {a,b}`)).toEqual({ a: [4, 3], b: [3, 4] })
|
|
711
|
+
})
|
|
712
|
+
test('can sort strings', () => {
|
|
713
|
+
expect(run(`['Bob', 'Dan', 'Alice', 'Callie'].sort()`)).toEqual(['Alice', 'Bob', 'Callie', 'Dan'])
|
|
714
|
+
})
|
|
715
|
+
test('allows a custom sorting callback to be passed in', () => {
|
|
716
|
+
expect(run(`['John', 'Ben', 'Emilia', 'Alice'].sort((a, b) => a.length - b.length)`)).toEqual([
|
|
717
|
+
'Ben',
|
|
718
|
+
'John',
|
|
719
|
+
'Alice',
|
|
720
|
+
'Emilia',
|
|
721
|
+
])
|
|
722
|
+
})
|
|
723
|
+
test('does not change the array when a custom sorting callback is used', () => {
|
|
724
|
+
expect(run(`let a = ['xx', 'y']; let b = a.sort((a, b) => a.length - b.length); {a,b}`)).toEqual({
|
|
725
|
+
a: ['xx', 'y'],
|
|
726
|
+
b: ['y', 'xx'],
|
|
727
|
+
})
|
|
728
|
+
})
|
|
729
|
+
})
|
|
730
|
+
test('push is not allowed', () => {
|
|
731
|
+
expect(() => run(`let a = [1,2]; a.push(5)`)).toThrowError('Unrecognized array method: push')
|
|
732
|
+
})
|
|
733
|
+
})
|
|
734
|
+
describe('constructor', () => {
|
|
735
|
+
test('.name reflects the type of the value', () => {
|
|
736
|
+
expect(run('5.constructor.name')).toEqual('Number')
|
|
737
|
+
expect(run('true.constructor.name')).toEqual('Boolean')
|
|
738
|
+
expect(run('false.constructor.name')).toEqual('Boolean')
|
|
739
|
+
expect(run('"abc".constructor.name')).toEqual('String')
|
|
740
|
+
expect(run('[].constructor.name')).toEqual('Array')
|
|
741
|
+
expect(run('{}.constructor.name')).toEqual('Object')
|
|
742
|
+
expect(run('(() => 99).constructor.name')).toEqual('Function')
|
|
743
|
+
})
|
|
744
|
+
test('works also if the attribute name is calculated', () => {
|
|
745
|
+
expect(run('5["const" + "ructor"].name')).toEqual('Number')
|
|
746
|
+
})
|
|
747
|
+
})
|
|
748
|
+
describe('Object.keys()', () => {
|
|
749
|
+
test('returns names of all attributes of the given object', () => {
|
|
750
|
+
expect(run(`Object.keys({a: 1, b: 2, w: 30})`)).toEqual(['a', 'b', 'w'])
|
|
751
|
+
// expect(run(`Object.entries({a: 1, b: 2, w: 30})`)).toEqual([['a', 1], ['b', 2], ['w', 30]])
|
|
752
|
+
})
|
|
753
|
+
test('fails if applied to a non-object value', () => {
|
|
754
|
+
expect(() => run(`Object.keys('a')`)).toThrowError('value type error: expected obj but found "a"')
|
|
755
|
+
expect(() => run(`Object.keys(5)`)).toThrowError('value type error: expected obj but found 5')
|
|
756
|
+
expect(() => run(`Object.keys(false)`)).toThrowError('value type error: expected obj but found false')
|
|
757
|
+
expect(() => run(`Object.keys(['a'])`)).toThrowError('value type error: expected obj but found ["a"]')
|
|
758
|
+
expect(() => run(`Object.keys(fun () 5)`)).toThrowError('value type error: expected obj but found "fun () 5"')
|
|
759
|
+
})
|
|
760
|
+
})
|
|
761
|
+
describe('Object.entries()', () => {
|
|
762
|
+
test('returns a [key, value] pair for each attribute of the given object', () => {
|
|
763
|
+
expect(run(`Object.entries({a: 1, b: 2, w: 30})`)).toEqual([
|
|
764
|
+
['a', 1],
|
|
765
|
+
['b', 2],
|
|
766
|
+
['w', 30],
|
|
767
|
+
])
|
|
768
|
+
})
|
|
769
|
+
test('fails if applied to a non-object value', () => {
|
|
770
|
+
expect(() => run(`Object.entries('a')`)).toThrowError('type error: expected obj but found "a"')
|
|
771
|
+
expect(() => run(`Object.entries(5)`)).toThrowError('type error: expected obj but found 5')
|
|
772
|
+
expect(() => run(`Object.entries(false)`)).toThrowError('type error: expected obj but found false')
|
|
773
|
+
expect(() => run(`Object.entries(['a'])`)).toThrowError('type error: expected obj but found ["a"]')
|
|
774
|
+
expect(() => run(`Object.entries(fun () 5)`)).toThrowError('type error: expected obj but found "fun () 5"')
|
|
775
|
+
})
|
|
776
|
+
})
|
|
777
|
+
describe('Object.fromEntries()', () => {
|
|
778
|
+
test('constructs an object from a list of [key, value] pairs describing its attributes', () => {
|
|
779
|
+
expect(run(`Object.fromEntries([['a', 1], ['b', 2], ['w', 30], ['y', 'yoo'], ['z', true]])`)).toEqual({
|
|
780
|
+
a: 1,
|
|
781
|
+
b: 2,
|
|
782
|
+
w: 30,
|
|
783
|
+
y: 'yoo',
|
|
784
|
+
z: true,
|
|
785
|
+
})
|
|
786
|
+
})
|
|
787
|
+
test('fails if applied to a non-array value', () => {
|
|
788
|
+
expect(() => run(`Object.fromEntries('a')`)).toThrowError('type error: expected arr but found "a"')
|
|
789
|
+
expect(() => run(`Object.fromEntries(5)`)).toThrowError('type error: expected arr but found 5')
|
|
790
|
+
expect(() => run(`Object.fromEntries(false)`)).toThrowError('type error: expected arr but found false')
|
|
791
|
+
expect(() => run(`Object.fromEntries({x: 1})`)).toThrowError('type error: expected arr but found {"x":1}')
|
|
792
|
+
expect(() => run(`Object.fromEntries(fun () 5)`)).toThrowError('type error: expected arr but found "fun () 5"')
|
|
793
|
+
})
|
|
794
|
+
test('the input array must be an array of pairs', () => {
|
|
795
|
+
expect(() => run(`Object.fromEntries([['a', 1], ['b']])`)).toThrowError('each entry must be a [key, value] pair')
|
|
796
|
+
})
|
|
797
|
+
test('the first element in each pair must be a string', () => {
|
|
798
|
+
expect(() => run(`Object.fromEntries([[1, 'a']])`)).toThrowError('value type error: expected str but found 1')
|
|
799
|
+
})
|
|
800
|
+
})
|
|
801
|
+
describe('line comments', () => {
|
|
802
|
+
test(`anything from '//' up to the end-of-line is ignored`, () => {
|
|
803
|
+
expect(
|
|
804
|
+
run(`
|
|
805
|
+
1 + 20 + // 300
|
|
806
|
+
4000`),
|
|
807
|
+
).toEqual(4021)
|
|
808
|
+
})
|
|
809
|
+
test(`allow consecutive lines which are all commented out`, () => {
|
|
810
|
+
expect(
|
|
811
|
+
run(`
|
|
812
|
+
1 +
|
|
813
|
+
// 20 +
|
|
814
|
+
// 300 +
|
|
815
|
+
// 4000 +
|
|
816
|
+
50000`),
|
|
817
|
+
).toEqual(50001)
|
|
818
|
+
})
|
|
819
|
+
test(`a comment inside a comment has no effect`, () => {
|
|
820
|
+
expect(
|
|
821
|
+
run(`
|
|
822
|
+
1 +
|
|
823
|
+
// 20 + // 300 +
|
|
824
|
+
4000`),
|
|
825
|
+
).toEqual(4001)
|
|
826
|
+
})
|
|
827
|
+
})
|
|
828
|
+
describe('block comments', () => {
|
|
829
|
+
test(`anything from '/*' up to the next '*/' is ignored`, () => {
|
|
830
|
+
expect(run(`1 + 20 + /* 300 */ 4000`)).toEqual(4021)
|
|
831
|
+
})
|
|
832
|
+
test(`can span multiple lines`, () => {
|
|
833
|
+
expect(
|
|
834
|
+
run(`
|
|
835
|
+
1 + /*
|
|
836
|
+
20 +
|
|
837
|
+
300
|
|
838
|
+
*/ 4000`),
|
|
839
|
+
).toEqual(4001)
|
|
840
|
+
})
|
|
841
|
+
test(`errors if the block comment start but does not end`, () => {
|
|
842
|
+
expect(() => run(`1 + 20 + /* 300`)).toThrowError(
|
|
843
|
+
'Block comment that started at at (<inline>:1:12..15) 300 is missing its closing (*/)',
|
|
844
|
+
)
|
|
845
|
+
})
|
|
846
|
+
test(`errors if a block comment closer does not have a matching opener`, () => {
|
|
847
|
+
expect(() => run(`1 + 20 + */ 300`)).toThrowError('Unparsable input at (<inline>:1:10..15) */ 300')
|
|
848
|
+
})
|
|
849
|
+
})
|
|
850
|
+
describe('evaluation stack', () => {
|
|
851
|
+
test('max recursion depth', () => {
|
|
852
|
+
expect(run(`let count = fun (n) if (n <= 0) 0 else 1 + count(n-1); count(260)`)).toEqual(260)
|
|
853
|
+
})
|
|
854
|
+
})
|
|
855
|
+
describe('args', () => {
|
|
856
|
+
test('are bounded at runtime to a special variable called "args"', () => {
|
|
857
|
+
expect(
|
|
858
|
+
Septima.run(
|
|
859
|
+
`args.a + '_' + args.color[0] + '_' + args.b + '_' + args.color[1]`,
|
|
860
|
+
{},
|
|
861
|
+
{
|
|
862
|
+
a: 'Sunday',
|
|
863
|
+
b: 'Monday',
|
|
864
|
+
color: ['Red', 'Green'],
|
|
865
|
+
},
|
|
866
|
+
),
|
|
867
|
+
).toEqual('Sunday_Red_Monday_Green')
|
|
868
|
+
})
|
|
869
|
+
test('are shadowed by a program-defined "args" symbol', () => {
|
|
870
|
+
expect(Septima.run(`let args = {color: 'Green' }; args.color`, {}, { color: 'Red' })).toEqual('Green')
|
|
871
|
+
})
|
|
872
|
+
})
|
|
873
|
+
describe('export', () => {
|
|
874
|
+
test('a top level definition can have the "export" qualifier', () => {
|
|
875
|
+
expect(run(`export let x = 5; x+3`)).toEqual(8)
|
|
876
|
+
})
|
|
877
|
+
test('allows multiple exported definitions', () => {
|
|
878
|
+
expect(run(`export let x = 5; export let twice = n => n*2; export let a = r => r*r*3.14; twice(3)`)).toEqual(6)
|
|
879
|
+
})
|
|
880
|
+
test('multiple exported definitions can be interleaved with non-exported ones', () => {
|
|
881
|
+
expect(run(`export let x = 5; let twice = n => n*2; export let a = r => r*r*3.14; twice(3)`)).toEqual(6)
|
|
882
|
+
})
|
|
883
|
+
test('errors if a nested definition has the "export" qualifier', () => {
|
|
884
|
+
expect(() => run(`let x = (export let y = 4; y+1); x+3`)).toThrowError(
|
|
885
|
+
'non-top-level definition cannot be exported at (<inline>:1:10..36) export let y = 4; y+1); x+3',
|
|
886
|
+
)
|
|
887
|
+
})
|
|
888
|
+
})
|
|
889
|
+
describe('import', () => {
|
|
890
|
+
test('makes a definition from one file to be available in another file', () => {
|
|
891
|
+
const septima = new Septima()
|
|
892
|
+
const files: Partial<Record<string, string>> = {
|
|
893
|
+
a: `import * as b from './b'; 'sum=' + b.sum(5, 3)`,
|
|
894
|
+
b: `export let sum = (x,y) => x+y`,
|
|
895
|
+
}
|
|
896
|
+
expect(septima.compileSync('a', f => files[f]).execute({})).toEqual({ tag: 'ok', value: 'sum=8' })
|
|
897
|
+
})
|
|
898
|
+
test('all exported defintions are available at the import site', () => {
|
|
899
|
+
const septima = new Septima()
|
|
900
|
+
const files: Partial<Record<string, string>> = {
|
|
901
|
+
a: `import * as b from './b'; b.sum(b.four, b.six)`,
|
|
902
|
+
b: `export let sum = (x,y) => x+y; export let four = 4; export let six = 6`,
|
|
903
|
+
}
|
|
904
|
+
expect(septima.compileSync('a', f => files[f]).execute({})).toEqual({ tag: 'ok', value: 10 })
|
|
905
|
+
})
|
|
906
|
+
test('non-exported definitions become undefined', () => {
|
|
907
|
+
const septima = new Septima()
|
|
908
|
+
const files: Partial<Record<string, string>> = {
|
|
909
|
+
a: `import * as b from './b';\n[b.four,\nb.six]`,
|
|
910
|
+
b: `export let four = 4; let six = 6`,
|
|
911
|
+
}
|
|
912
|
+
expect(septima.compileSync('a', f => files[f]).execute({})).toEqual({ tag: 'ok', value: [4, undefined] })
|
|
913
|
+
})
|
|
914
|
+
test('can import from multiple files', () => {
|
|
915
|
+
const septima = new Septima()
|
|
916
|
+
const files: Partial<Record<string, string>> = {
|
|
917
|
+
a: `import * as b from './b';\nimport * as c from './c'\nimport * as d from './d'; [b.val, c.val, d.val]`,
|
|
918
|
+
b: `export let val = 100`,
|
|
919
|
+
c: `export let val = 20`,
|
|
920
|
+
d: `export let val = 3`,
|
|
921
|
+
}
|
|
922
|
+
expect(septima.compileSync('a', f => files[f]).execute({})).toEqual({ tag: 'ok', value: [100, 20, 3] })
|
|
923
|
+
})
|
|
924
|
+
})
|
|
925
|
+
describe('unit', () => {
|
|
926
|
+
test('evaluates to an empty string if it contains only definitions', () => {
|
|
927
|
+
expect(run(`export let x = 5`)).toEqual('')
|
|
928
|
+
})
|
|
929
|
+
})
|
|
930
|
+
describe('undefined', () => {
|
|
931
|
+
// We want to verify that attributes with an undefined values do not exist in the object. To verify that we look
|
|
932
|
+
// at the keys of the object.
|
|
933
|
+
const keysOf = (u: unknown) => {
|
|
934
|
+
const casted = u as Record<string, unknown> // eslint-disable-line @typescript-eslint/consistent-type-assertions
|
|
935
|
+
return Object.keys(casted)
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
test(`the 'undefined' literal evaluates to (a JS) undefined`, () => {
|
|
939
|
+
expect(run(`let x = undefined; x`)).toBe(undefined)
|
|
940
|
+
})
|
|
941
|
+
test('accessing a non-existing attribute evaulates to undefined', () => {
|
|
942
|
+
expect(run(`let x = {a: 42}; [x.a, x.b]`)).toEqual([42, undefined])
|
|
943
|
+
})
|
|
944
|
+
test('.at() method returns undefined when the index is out of range', () => {
|
|
945
|
+
expect(run(`let x = ['a', 'b', 'c']; [x.at(0), x.at(2), x.at(3)]`)).toEqual(['a', 'c', undefined])
|
|
946
|
+
})
|
|
947
|
+
test('can be stored in an array', () => {
|
|
948
|
+
expect(run(`['a', undefined, 'c']`)).toEqual(['a', undefined, 'c'])
|
|
949
|
+
})
|
|
950
|
+
test('an object attribute with a value of undefined is dropped from the object', () => {
|
|
951
|
+
expect(keysOf(run(`{n: 42, o: undefined, p: 'poo'}`))).toEqual(['n', 'p'])
|
|
952
|
+
expect(keysOf(run(`Object.fromEntries([['n', 42], ['o', undefined], ['p', 'poo']])`))).toEqual(['n', 'p'])
|
|
953
|
+
})
|
|
954
|
+
test.todo('decide how overwriting with undefined works')
|
|
955
|
+
test('spreading an undefined in object is a no-op', () => {
|
|
956
|
+
expect(run(`{n: 42, ...undefined, p: 'poo'}`)).toEqual({ n: 42, p: 'poo' })
|
|
957
|
+
})
|
|
958
|
+
test('spreading an undefined in an array is a no-op', () => {
|
|
959
|
+
expect(run(`[42, ...undefined, 'poo']`)).toEqual([42, 'poo'])
|
|
960
|
+
})
|
|
961
|
+
test('produces a full trace when an undefined-reference-error is fired', () => {
|
|
962
|
+
let message
|
|
963
|
+
try {
|
|
964
|
+
run(`let x = undefined; x.a`)
|
|
965
|
+
} catch (e) {
|
|
966
|
+
message = String(e)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
expect(message?.split('\n')).toEqual([
|
|
970
|
+
'Error: value type error: expected either str, arr or obj but found undefined when evaluating:',
|
|
971
|
+
' at (<inline>:1:1..22) let x = undefined; x.a',
|
|
972
|
+
' at (<inline>:1:1..22) let x = undefined; x.a',
|
|
973
|
+
' at (<inline>:1:20..22) x.a',
|
|
974
|
+
])
|
|
975
|
+
})
|
|
976
|
+
test('errors when calling a method on undefined', () => {
|
|
977
|
+
expect(() => run(`let x = undefined; x.a()`)).toThrowError('at (<inline>:1:20..24) x.a()')
|
|
978
|
+
})
|
|
979
|
+
test('errors when using undefined in arithmetic expressions', () => {
|
|
980
|
+
expect(() => run(`4 + undefined`)).toThrowError('at (<inline>:1:1..13) 4 + undefined')
|
|
981
|
+
expect(() => run(`4 - undefined`)).toThrowError('at (<inline>:1:1..13) 4 - undefined')
|
|
982
|
+
expect(() => run(`4 * undefined`)).toThrowError('at (<inline>:1:1..13) 4 * undefined')
|
|
983
|
+
expect(() => run(`4 / undefined`)).toThrowError('at (<inline>:1:1..13) 4 / undefined')
|
|
984
|
+
expect(() => run(`undefined + 4`)).toThrowError('at (<inline>:1:1..13) undefined + 4')
|
|
985
|
+
expect(() => run(`undefined - 4`)).toThrowError('at (<inline>:1:1..13) undefined - 4')
|
|
986
|
+
expect(() => run(`undefined * 4`)).toThrowError('at (<inline>:1:1..13) undefined * 4')
|
|
987
|
+
expect(() => run(`undefined / 4`)).toThrowError('at (<inline>:1:1..13) undefined / 4')
|
|
988
|
+
})
|
|
989
|
+
describe('??', () => {
|
|
990
|
+
test('if the lhs is undefined evaluates to the rhs', () => {
|
|
991
|
+
expect(run(`undefined ?? 42`)).toEqual(42)
|
|
992
|
+
expect(run(`undefined ?? 900`)).toEqual(900)
|
|
993
|
+
expect(run(`undefined ?? 'Luke'`)).toEqual('Luke')
|
|
994
|
+
})
|
|
995
|
+
test('if the lhs is not undefined evaluates to the lhs', () => {
|
|
996
|
+
expect(run(`43 ?? 42`)).toEqual(43)
|
|
997
|
+
expect(run(`43 ?? 900`)).toEqual(43)
|
|
998
|
+
expect(run(`43 ?? 'Luke'`)).toEqual(43)
|
|
999
|
+
expect(run(`'Han' ?? 42`)).toEqual('Han')
|
|
1000
|
+
expect(run(`'Han' ?? 900`)).toEqual('Han')
|
|
1001
|
+
expect(run(`'Han' ?? 'Luke'`)).toEqual('Han')
|
|
1002
|
+
})
|
|
1003
|
+
})
|
|
1004
|
+
})
|
|
1005
|
+
describe('Casting functions', () => {
|
|
1006
|
+
test('String()', () => {
|
|
1007
|
+
expect(run(`String(42)`)).toEqual('42')
|
|
1008
|
+
expect(run(`String("abc")`)).toEqual('abc')
|
|
1009
|
+
expect(run(`String(true)`)).toEqual('true')
|
|
1010
|
+
expect(run(`String(false)`)).toEqual('false')
|
|
1011
|
+
expect(run(`String(undefined)`)).toEqual('undefined')
|
|
1012
|
+
expect(run(`String({a: "alpha", b: [3,1,4], n: 42})`)).toEqual('{"a":"alpha","b":[3,1,4],"n":42}')
|
|
1013
|
+
expect(run(`String(["abc", 3.14159, false, true, undefined])`)).toEqual('["abc",3.14159,false,true,null]')
|
|
1014
|
+
})
|
|
1015
|
+
test('Boolean()', () => {
|
|
1016
|
+
expect(run(`Boolean(42)`)).toEqual(true)
|
|
1017
|
+
expect(run(`Boolean(0)`)).toEqual(false)
|
|
1018
|
+
expect(run(`Boolean("abc")`)).toEqual(true)
|
|
1019
|
+
expect(run(`Boolean("")`)).toEqual(false)
|
|
1020
|
+
expect(run(`Boolean(true)`)).toEqual(true)
|
|
1021
|
+
expect(run(`Boolean(false)`)).toEqual(false)
|
|
1022
|
+
expect(run(`Boolean(undefined)`)).toEqual(false)
|
|
1023
|
+
expect(run(`Boolean({})`)).toEqual(true)
|
|
1024
|
+
expect(run(`Boolean([])`)).toEqual(true)
|
|
1025
|
+
})
|
|
1026
|
+
test('Number()', () => {
|
|
1027
|
+
expect(run(`Number(42)`)).toEqual(42)
|
|
1028
|
+
expect(run(`Number(0)`)).toEqual(0)
|
|
1029
|
+
expect(run(`Number("42")`)).toEqual(42)
|
|
1030
|
+
expect(run(`Number("abc")`)).toEqual(NaN)
|
|
1031
|
+
expect(run(`Number(true)`)).toEqual(1)
|
|
1032
|
+
expect(run(`Number(false)`)).toEqual(0)
|
|
1033
|
+
expect(run(`Number(undefined)`)).toEqual(NaN)
|
|
1034
|
+
expect(run(`Number({})`)).toEqual(NaN)
|
|
1035
|
+
expect(run(`Number([])`)).toEqual(NaN)
|
|
1036
|
+
})
|
|
1037
|
+
})
|
|
1038
|
+
describe('console.log', () => {
|
|
1039
|
+
const runLog = (input: string) => {
|
|
1040
|
+
const lines: unknown[] = []
|
|
1041
|
+
const result = Septima.run(input, { onSink: () => undefined, consoleLog: u => lines.push(u) })
|
|
1042
|
+
return { lines, result }
|
|
1043
|
+
}
|
|
1044
|
+
test('prints its input', () => {
|
|
1045
|
+
expect(runLog(`console.log(2*2*2*2)`).lines).toEqual(['16'])
|
|
1046
|
+
expect(runLog(`console.log({a: 1, b: 2, c: ['d', 'e']})`).lines).toEqual(['{"a":1,"b":2,"c":["d","e"]}'])
|
|
1047
|
+
})
|
|
1048
|
+
test('a program can have multiple console.log() calls', () => {
|
|
1049
|
+
expect(runLog(`["red", "green", "blue"].map(at => console.log(at))`).lines).toEqual([
|
|
1050
|
+
'"red"',
|
|
1051
|
+
'"green"',
|
|
1052
|
+
'"blue"',
|
|
1053
|
+
])
|
|
1054
|
+
})
|
|
1055
|
+
test('returns its input', () => {
|
|
1056
|
+
expect(runLog(`32*console.log(8)`)).toEqual({
|
|
1057
|
+
result: 256,
|
|
1058
|
+
lines: ['8'],
|
|
1059
|
+
})
|
|
1060
|
+
})
|
|
1061
|
+
})
|
|
1062
|
+
describe(`JSON.parse`, () => {
|
|
1063
|
+
test('parses a string', () => {
|
|
1064
|
+
expect(run(`JSON.parse('{"a": 1, "b": "beta"}')`)).toEqual({ a: 1, b: 'beta' })
|
|
1065
|
+
})
|
|
1066
|
+
test('roundtrips a value that was converted to JSON', () => {
|
|
1067
|
+
expect(run(`JSON.parse(String({"a": 1, "b": "beta", c: {arr: [100, 200]}}))`)).toEqual({
|
|
1068
|
+
a: 1,
|
|
1069
|
+
b: 'beta',
|
|
1070
|
+
c: { arr: [100, 200] },
|
|
1071
|
+
})
|
|
1072
|
+
})
|
|
1073
|
+
test('keeps non-string as-is', () => {
|
|
1074
|
+
expect(run(`JSON.parse(5000)`)).toEqual(5000)
|
|
1075
|
+
})
|
|
1076
|
+
})
|
|
1077
|
+
describe('hash224', () => {
|
|
1078
|
+
const hashOf = (u: unknown) => crypto.createHash('sha224').update(JSON.stringify(u)).digest('hex')
|
|
1079
|
+
test('can compute hash values of strings', () => {
|
|
1080
|
+
expect(run(`crypto.hash224('A')`)).toEqual(hashOf('A'))
|
|
1081
|
+
})
|
|
1082
|
+
test('can compute hash values of complex objects', () => {
|
|
1083
|
+
expect(run(`crypto.hash224({a: 1, b: [{x: 'X'}, ["Y"]]})`)).toEqual(hashOf({ a: 1, b: [{ x: 'X' }, ['Y']] }))
|
|
1084
|
+
})
|
|
1085
|
+
test('the hash changes when the input changes', () => {
|
|
1086
|
+
expect(run(`crypto.hash224(110002)`)).toEqual(hashOf(110002))
|
|
1087
|
+
expect(run(`crypto.hash224(110003)`)).toEqual(hashOf(110003))
|
|
1088
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
1089
|
+
const [h1, h2] = run(`[crypto.hash224(110002), crypto.hash224(110003)]`) as string[]
|
|
1090
|
+
expect(h1).not.toEqual(h2)
|
|
1091
|
+
})
|
|
1092
|
+
})
|
|
1093
|
+
describe('throw', () => {
|
|
1094
|
+
test('it raises an error that is propagated all the way out', () => {
|
|
1095
|
+
expect(() => run(`throw "bo" + "om"`)).toThrowError(
|
|
1096
|
+
`"boom" when evaluating:\n` + ` at (<inline>:1:8..16) bo" + "om\n` + ` at (<inline>:1:8..16) bo" + "om`,
|
|
1097
|
+
)
|
|
1098
|
+
})
|
|
1099
|
+
test('the error message contains a septima stack trace that reflects the entire call chain', () => {
|
|
1100
|
+
expect(() => run(`let g = (n) => throw "n=" + n;\nlet f = (n) => n > 0 ? f(n-1) : g(n);\nf(3)`)).toThrowError(
|
|
1101
|
+
`"n=0" when evaluating:\n` +
|
|
1102
|
+
` at (<inline>:1:1..3:4) let g = (n) => throw \"n=\" + n;...\n` +
|
|
1103
|
+
` at (<inline>:1:1..3:4) let g = (n) => throw \"n=\" + n;...\n` +
|
|
1104
|
+
` at (<inline>:3:1..4) f(3)\n` +
|
|
1105
|
+
` at (<inline>:2:16..36) n > 0 ? f(n-1) : g(n)\n` +
|
|
1106
|
+
` at (<inline>:2:24..29) f(n-1)\n` +
|
|
1107
|
+
` at (<inline>:2:16..36) n > 0 ? f(n-1) : g(n)\n` +
|
|
1108
|
+
` at (<inline>:2:24..29) f(n-1)\n` +
|
|
1109
|
+
` at (<inline>:2:16..36) n > 0 ? f(n-1) : g(n)\n` +
|
|
1110
|
+
` at (<inline>:2:24..29) f(n-1)\n` +
|
|
1111
|
+
` at (<inline>:2:16..36) n > 0 ? f(n-1) : g(n)\n` +
|
|
1112
|
+
` at (<inline>:2:33..36) g(n)\n` +
|
|
1113
|
+
` at (<inline>:1:23..29) n=" + n`,
|
|
1114
|
+
)
|
|
1115
|
+
})
|
|
1116
|
+
})
|
|
1117
|
+
test.todo('support file names in locations')
|
|
1118
|
+
|
|
1119
|
+
test('template literals', () => {
|
|
1120
|
+
expect(run('``')).toEqual('')
|
|
1121
|
+
expect(run('`hello world`')).toEqual('hello world')
|
|
1122
|
+
expect(run("let name = 'Alice'; `Hello ${name}`")).toEqual('Hello Alice')
|
|
1123
|
+
expect(run("let first = 'John'; let last = 'Doe'; `${first} ${last}`")).toEqual('John Doe')
|
|
1124
|
+
expect(run('`The answer is ${40 + 2}`')).toEqual('The answer is 42')
|
|
1125
|
+
expect(run('`Result: ${(5 + 3) * 2}`')).toEqual('Result: 16')
|
|
1126
|
+
expect(run("let greet = (n) => 'Hello ' + n; `${greet('world')}`")).toEqual('Hello world')
|
|
1127
|
+
expect(run('let obj = {x: 5}; `Value: ${obj.x}`')).toEqual('Value: 5')
|
|
1128
|
+
expect(run("let arr = ['a', 'b', 'c']; `Item: ${arr[1]}`")).toEqual('Item: b')
|
|
1129
|
+
expect(run('`Is true: ${true}`')).toEqual('Is true: true')
|
|
1130
|
+
expect(run('`Is false: ${false}`')).toEqual('Is false: false')
|
|
1131
|
+
expect(run('`Number: ${42}`')).toEqual('Number: 42')
|
|
1132
|
+
expect(run('`Float: ${3.14}`')).toEqual('Float: 3.14')
|
|
1133
|
+
expect(run('`Value: ${undefined}`')).toEqual('Value: undefined')
|
|
1134
|
+
expect(run('`Array: ${[1, 2, 3]}`')).toEqual('Array: [1,2,3]')
|
|
1135
|
+
expect(run('`Object: ${{a: 1}}`')).toEqual('Object: {"a":1}')
|
|
1136
|
+
expect(run('`${1}${2}${3}`')).toEqual('123')
|
|
1137
|
+
expect(run('`${42} is the answer`')).toEqual('42 is the answer')
|
|
1138
|
+
expect(run('`The answer is ${42}`')).toEqual('The answer is 42')
|
|
1139
|
+
expect(run('` spaces `')).toEqual(' spaces ')
|
|
1140
|
+
expect(run('`Cost: \\$100`')).toEqual('Cost: $100')
|
|
1141
|
+
expect(run('`This is a backtick: \\``')).toEqual('This is a backtick: `')
|
|
1142
|
+
expect(run('`Path: C:\\\\Users`')).toEqual('Path: C:\\Users')
|
|
1143
|
+
expect(run('`line1\\nline2`')).toEqual('line1\nline2')
|
|
1144
|
+
expect(run('`col1\\tcol2`')).toEqual('col1\tcol2')
|
|
1145
|
+
expect(run('`Price: $50`')).toEqual('Price: $50')
|
|
1146
|
+
expect(run("let x = 5; `Value: ${x}` + ' done'")).toEqual('Value: 5 done')
|
|
1147
|
+
expect(run('let x = true; x ? `yes` : `no`')).toEqual('yes')
|
|
1148
|
+
expect(run('let x = false; x ? `yes` : `no`')).toEqual('no')
|
|
1149
|
+
expect(run('let f = (o) => o.name; `Hello ${f({name: "World"})}`')).toEqual('Hello World')
|
|
1150
|
+
expect(run('`Result: ${({a: 1}).a}`')).toEqual('Result: 1')
|
|
1151
|
+
expect(run('`line1\nline2\nline3`')).toEqual('line1\nline2\nline3')
|
|
1152
|
+
expect(run('let inner = `world`; `Hello ${inner}!`')).toEqual('Hello world!')
|
|
1153
|
+
expect(run('`outer ${`inner ${42}`}`')).toEqual('outer inner 42')
|
|
1154
|
+
})
|
|
1155
|
+
test.todo('optional type annotations?')
|
|
1156
|
+
test.todo('allow redundant commas')
|
|
1157
|
+
test.todo('left associativity of +/-')
|
|
1158
|
+
test.todo('comparison of arrays')
|
|
1159
|
+
test.todo('comparison of lambdas?')
|
|
1160
|
+
test.todo('"abcdef"[1] == "b"')
|
|
1161
|
+
test.todo('an object literal cannot have a repeated attribute name that')
|
|
1162
|
+
test.todo('quoting of a ticks inside a string')
|
|
1163
|
+
test.todo('number in scientific notation')
|
|
1164
|
+
test.todo('number methods')
|
|
1165
|
+
test.todo('drop the fun () notation and use just arrow functions')
|
|
1166
|
+
test.todo('proper internal representation of arrow function, in particular: show(), span()')
|
|
1167
|
+
test.todo('sink sinkifies arrays and objects it is stored at')
|
|
1168
|
+
test.todo('{foo}')
|
|
1169
|
+
})
|