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.
@@ -0,0 +1,291 @@
1
+ import { Value } from '../src/value'
2
+
3
+ const err = () => {
4
+ throw new Error(`should not run`)
5
+ }
6
+
7
+ const fixed = (u: unknown) => () => Value.from(u)
8
+
9
+ const notFromHere = () => {
10
+ throw new Error('should not be called from this test')
11
+ }
12
+
13
+ describe('value', () => {
14
+ test('arithmetics', () => {
15
+ expect(Value.num(5).plus(Value.num(3)).export()).toEqual(8)
16
+ expect(Value.num(5).minus(Value.num(3)).export()).toEqual(2)
17
+ expect(Value.num(5).times(Value.num(3)).export()).toEqual(15)
18
+ expect(Value.num(14).over(Value.num(4)).export()).toEqual(3.5)
19
+ expect(Value.num(5).negate().export()).toEqual(-5)
20
+ expect(Value.num(-12).negate().export()).toEqual(12)
21
+ expect(Value.num(3).power(Value.num(4)).export()).toEqual(81)
22
+ expect(Value.num(2).power(Value.num(8)).export()).toEqual(256)
23
+ })
24
+ test('comparisons of numbers', () => {
25
+ expect(Value.num(5).order(Value.num(3)).export()).toEqual(1)
26
+ expect(Value.num(5).order(Value.num(4)).export()).toEqual(1)
27
+ expect(Value.num(5).order(Value.num(5)).export()).toEqual(0)
28
+ expect(Value.num(5).order(Value.num(6)).export()).toEqual(-1)
29
+ expect(Value.num(5).order(Value.num(7)).export()).toEqual(-1)
30
+ })
31
+ test('booleans', () => {
32
+ expect(Value.bool(true).export()).toEqual(true)
33
+ expect(Value.bool(false).export()).toEqual(false)
34
+ expect(Value.bool(false).not().export()).toEqual(true)
35
+ expect(Value.bool(true).not().export()).toEqual(false)
36
+ })
37
+ describe('boolean operators', () => {
38
+ test('or', () => {
39
+ expect(
40
+ Value.bool(false)
41
+ .or(() => Value.bool(false))
42
+ .export(),
43
+ ).toEqual(false)
44
+ expect(
45
+ Value.bool(false)
46
+ .or(() => Value.bool(true))
47
+ .export(),
48
+ ).toEqual(true)
49
+ expect(
50
+ Value.bool(true)
51
+ .or(() => Value.bool(false))
52
+ .export(),
53
+ ).toEqual(true)
54
+ expect(
55
+ Value.bool(true)
56
+ .or(() => Value.bool(true))
57
+ .export(),
58
+ ).toEqual(true)
59
+ })
60
+ test('and', () => {
61
+ expect(
62
+ Value.bool(false)
63
+ .and(() => Value.bool(false))
64
+ .export(),
65
+ ).toEqual(false)
66
+ expect(
67
+ Value.bool(false)
68
+ .and(() => Value.bool(true))
69
+ .export(),
70
+ ).toEqual(false)
71
+ expect(
72
+ Value.bool(true)
73
+ .and(() => Value.bool(false))
74
+ .export(),
75
+ ).toEqual(false)
76
+ expect(
77
+ Value.bool(true)
78
+ .and(() => Value.bool(true))
79
+ .export(),
80
+ ).toEqual(true)
81
+ })
82
+ })
83
+ test('comparisons of booleans', () => {
84
+ expect(Value.bool(false).order(Value.bool(false)).export()).toEqual(0)
85
+ expect(Value.bool(false).order(Value.bool(true)).export()).toEqual(-1)
86
+ expect(Value.bool(true).order(Value.bool(false)).export()).toEqual(1)
87
+ expect(Value.bool(true).order(Value.bool(true)).export()).toEqual(0)
88
+ })
89
+ test('strings', () => {
90
+ expect(Value.str('abc').export()).toEqual('abc')
91
+ expect(Value.str('').export()).toEqual('')
92
+ expect(Value.str('a').plus(Value.str('b')).export()).toEqual('ab')
93
+ expect(Value.str('').plus(Value.str('')).export()).toEqual('')
94
+ expect(Value.str('').plus(Value.str('xyz')).export()).toEqual('xyz')
95
+ expect(Value.str('pqr').plus(Value.str('')).export()).toEqual('pqr')
96
+ expect(Value.str('zxcvb').plus(Value.str('nm')).export()).toEqual('zxcvbnm')
97
+ })
98
+ test('comparisons of strings', () => {
99
+ expect(Value.str('e').order(Value.str('c')).export()).toEqual(1)
100
+ expect(Value.str('e').order(Value.str('d')).export()).toEqual(1)
101
+ expect(Value.str('e').order(Value.str('e')).export()).toEqual(0)
102
+ expect(Value.str('e').order(Value.str('f')).export()).toEqual(-1)
103
+ expect(Value.str('e').order(Value.str('g')).export()).toEqual(-1)
104
+ })
105
+ test('arrays', () => {
106
+ expect(Value.arr([Value.num(10), Value.num(20)]).export()).toEqual([10, 20])
107
+ expect(Value.arr([]).export()).toEqual([])
108
+ expect(Value.arr([Value.str('ab'), Value.num(500), Value.bool(true)]).export()).toEqual(['ab', 500, true])
109
+ })
110
+ test('objects', () => {
111
+ expect(Value.obj({ x: Value.num(10), y: Value.num(20) }).export()).toEqual({ x: 10, y: 20 })
112
+ expect(Value.obj({}).export()).toEqual({})
113
+ expect(Value.obj({ the: Value.str('ab'), quick: Value.num(500), brown: Value.bool(true) }).export()).toEqual({
114
+ the: 'ab',
115
+ quick: 500,
116
+ brown: true,
117
+ })
118
+ const o = Value.obj({ the: Value.str('ab'), quick: Value.num(500), brown: Value.bool(true) })
119
+ expect(o.access('the', notFromHere).export()).toEqual('ab')
120
+ expect(o.access('quick', notFromHere).export()).toEqual(500)
121
+ expect(o.access('brown', notFromHere).export()).toEqual(true)
122
+ expect(o.access(Value.str('quick'), notFromHere).export()).toEqual(500)
123
+ })
124
+ test('yells if access() is called with value which is neither string or num', () => {
125
+ const o = Value.obj({ the: Value.str('ab'), quick: Value.num(500), brown: Value.bool(true) })
126
+ expect(() => o.access(Value.arr([]), notFromHere).export()).toThrowError(
127
+ 'value type error: expected either num or str but found []',
128
+ )
129
+ expect(() => o.access(Value.bool(false), notFromHere).export()).toThrowError(
130
+ 'value type error: expected either num or str but found false',
131
+ )
132
+ expect(() => o.access(Value.obj({ x: Value.num(1) }), notFromHere).export()).toThrowError(
133
+ 'value type error: expected either num or str but found {"x":1}',
134
+ )
135
+ })
136
+ test('json', () => {
137
+ const v = Value.obj({ x: Value.num(1) })
138
+ expect(JSON.stringify(v)).toEqual('{"x":1}')
139
+ })
140
+ describe('ifElse', () => {
141
+ test('when applied to true evaluates the positive branch', () => {
142
+ expect(Value.bool(true).ifElse(fixed('yes'), fixed('no')).export()).toEqual('yes')
143
+ })
144
+ test('when applied to false evaluates the positive branch', () => {
145
+ expect(Value.bool(false).ifElse(fixed('yes'), fixed('no')).export()).toEqual('no')
146
+ })
147
+ test('errors if applied to a non-boolean', () => {
148
+ expect(() => Value.num(1).ifElse(fixed('yes'), fixed('no')).export()).toThrowError('expected bool but found 1')
149
+ })
150
+ })
151
+ describe('type erros', () => {
152
+ const five = Value.num(1)
153
+ const t = Value.bool(true)
154
+ const f = Value.bool(false)
155
+
156
+ const check = (a: Value, b: Value | Value[], f: (lhs: Value, rhs: Value) => void) => {
157
+ const arr = Array.isArray(b) ? b : [b]
158
+ const r = /(^value type error: expected)|(^Cannot compare a )/
159
+ // /(^value type error: expected)|(^Type error: operator cannot be applied to operands of type)|(^Cannot compare when the left-hand-side value is of type)|(^Not a)/
160
+ for (const curr of arr) {
161
+ expect(() => f(a, curr)).toThrowError(r)
162
+ expect(() => f(curr, a)).toThrowError(r)
163
+ }
164
+ }
165
+
166
+ test('emits erros when numeric operations are applied to a boolean (either lhs or rhs)', () => {
167
+ check(five, t, (x, y) => x.plus(y))
168
+ check(five, t, (x, y) => x.minus(y))
169
+ check(five, t, (x, y) => x.times(y))
170
+ check(five, t, (x, y) => x.over(y))
171
+ check(five, t, (x, y) => x.power(y))
172
+ check(five, t, (x, y) => x.modulo(y))
173
+ check(five, t, (x, y) => x.order(y))
174
+ check(t, t, x => x.negate())
175
+ expect(1).toEqual(1) // make the linter happy
176
+ })
177
+ test('emits erros when boolean operations are applied to a number (either lhs or rhs)', () => {
178
+ check(five, f, (x, y) => x.or(() => y))
179
+ check(five, t, (x, y) => x.and(() => y))
180
+ check(five, five, x => x.not())
181
+ expect(1).toEqual(1) // make the linter happy
182
+ })
183
+ })
184
+ describe('foreign code calls', () => {
185
+ test('invokes the given function', () => {
186
+ const indexOf = Value.foreign(s => 'the quick brown fox jumps over the lazy dog'.indexOf(s.assertStr()))
187
+ expect(indexOf.call([Value.str('quick')], err).export()).toEqual(4)
188
+ })
189
+ })
190
+ describe('string operatios', () => {
191
+ test('.length', () => {
192
+ expect(Value.str('four scores AND seven').access('length', notFromHere).export()).toEqual(21)
193
+ })
194
+ test.each([
195
+ ['at', [Value.num(10)], 'e'],
196
+ ['at', [Value.num(-4)], 'v'],
197
+ ['charAt', [Value.num(10)], 'e'],
198
+ ['concat', [Value.str('years')], ' four scores AND seven years'],
199
+ ['endsWith', [Value.str('seven ')], true],
200
+ ['endsWith', [Value.str('years')], false],
201
+ ['includes', [Value.str('scores')], true],
202
+ ['includes', [Value.str('years')], false],
203
+ ['indexOf', [Value.str('e')], 10],
204
+ ['lastIndexOf', [Value.str('e')], 20],
205
+ ['match', [Value.str('r|f')], ['f']],
206
+ ['matchAll', [Value.str('r|f')], [['f'], ['r'], ['r']]],
207
+ ['padEnd', [Value.num(25), Value.str('#')], ' four scores AND seven ##'],
208
+ ['padStart', [Value.num(25), Value.str('#')], '## four scores AND seven '],
209
+ ['repeat', [Value.num(3)], ' four scores AND seven four scores AND seven four scores AND seven '],
210
+ ['replace', [Value.str('o'), Value.str('#')], ' f#ur scores AND seven '],
211
+ ['replaceAll', [Value.str('o'), Value.str('#')], ' f#ur sc#res AND seven '],
212
+ ['search', [Value.str('sco..s')], 6],
213
+ ['slice', [Value.num(13), Value.num(-7)], 'AND'],
214
+ ['split', [Value.str(' ')], ['', 'four', 'scores', 'AND', 'seven', '']],
215
+ ['startsWith', [Value.str(' four')], true],
216
+ ['startsWith', [Value.str('seven')], false],
217
+ ['substring', [Value.num(6), Value.num(12)], 'scores'],
218
+ ['substring', [Value.num(13), Value.num(-7)], ' four scores '],
219
+ ['toLowerCase', [], ' four scores and seven '],
220
+ ['toUpperCase', [], ' FOUR SCORES AND SEVEN '],
221
+ ['trim', [], 'four scores AND seven'],
222
+ ['trimEnd', [], ' four scores AND seven'],
223
+ ['trimStart', [], 'four scores AND seven '],
224
+ ])('provides the .%s() method', (name, args, expected) => {
225
+ const callee = Value.str(' four scores AND seven ').access(name, notFromHere)
226
+ const actual = callee.call(args, err)
227
+ expect(actual.export()).toEqual(expected)
228
+ })
229
+ })
230
+ describe('array operations', () => {
231
+ test('.length', () => {
232
+ expect(
233
+ Value.arr([Value.str('foo'), Value.str('bar'), Value.str('foo'), Value.str('goo')])
234
+ .access('length', notFromHere)
235
+ .export(),
236
+ ).toEqual(4)
237
+ })
238
+ test.each([
239
+ ['at', [Value.num(3)], 'goo'],
240
+ ['concat', [Value.arr([Value.str('boo'), Value.str('poo')])], ['foo', 'bar', 'foo', 'goo', 'boo', 'poo']],
241
+ [
242
+ 'entries',
243
+ [],
244
+ [
245
+ [0, 'foo'],
246
+ [1, 'bar'],
247
+ [2, 'foo'],
248
+ [3, 'goo'],
249
+ ],
250
+ ],
251
+ // TODO(imaman): ['find', [Value.foreign(v => v.assertStr() === 'lorem ipsum')], ??]"",
252
+ ['includes', [Value.str('bar')], true],
253
+ ['includes', [Value.str('lorem-ipsum')], false],
254
+ ['indexOf', [Value.str('goo')], 3],
255
+ ['join', [Value.str('; ')], 'foo; bar; foo; goo'],
256
+ ['lastIndexOf', [Value.str('foo')], 2],
257
+ ['lastIndexOf', [Value.str('lorem ipsum')], -1],
258
+ ['reverse', [], ['goo', 'foo', 'bar', 'foo']],
259
+ ['slice', [Value.num(1), Value.num(2)], ['bar']],
260
+ ['slice', [Value.num(1), Value.num(3)], ['bar', 'foo']],
261
+ ['slice', [Value.num(2), Value.num(4)], ['foo', 'goo']],
262
+ ])('provides the .%s() method', (name, args, expected) => {
263
+ const input = Value.arr([Value.str('foo'), Value.str('bar'), Value.str('foo'), Value.str('goo')])
264
+ const before = JSON.parse(JSON.stringify(input))
265
+ const callee = input.access(name, notFromHere)
266
+ const actual = callee.call(args, err)
267
+ expect(actual.export()).toEqual(expected)
268
+ // Make sure the input array was not accidentally mutated.
269
+ expect(JSON.parse(JSON.stringify(input))).toEqual(before)
270
+ })
271
+ test('.flat() flattens', () => {
272
+ const input = Value.arr([Value.arr([Value.str('a'), Value.str('b')]), Value.str('c')])
273
+ const callee = input.access('flat', notFromHere)
274
+ const actual = callee.call([], err)
275
+ expect(actual.export()).toEqual(['a', 'b', 'c'])
276
+ })
277
+ })
278
+ describe('undefined', () => {
279
+ test('can be constructed from either undefined or null', () => {
280
+ expect(Value.from(undefined).isUndefined()).toBe(true)
281
+ expect(Value.from(null).isUndefined()).toBe(true)
282
+ expect(Value.from(42).isUndefined()).toBe(false)
283
+ expect(Value.from('abc').isUndefined()).toBe(false)
284
+ })
285
+ })
286
+
287
+ test.todo('array.sort()')
288
+ test.todo('what happens when we get an undefined from a foreign call (like Array.get())')
289
+ test.todo('access to non-existing string method (a sad path test)')
290
+ test.todo('access to non-existing array method (a sad path test)')
291
+ })