forked from PostFixJS/PostFixJS
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathInterpreter.test.js
More file actions
212 lines (187 loc) · 7.09 KB
/
Interpreter.test.js
File metadata and controls
212 lines (187 loc) · 7.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import test from 'ava'
const { execute, runPostFixTests, throwsErrorMessage, checkErrorMessage } = require('./test/helpers/util')
const types = require('./types')
test('Param list in ExeArrs should work as expected', async (t) => {
const { stack } = await execute('{ ( n :Int ) } exec')
t.is(stack.count, 1)
t.true(stack.pop() instanceof types.Params)
})
test('Operators should not be executed in a param list', async (t) => {
const { stack } = await execute('(a b err)') // in the buggy implementation, err would be executed
t.is(stack.count, 1)
const params = stack.pop()
t.true(params instanceof types.Params)
t.is(params.params.length, 3)
t.is(params.returns, null)
})
test('Trying to get elements from an empty stack should throw an error', async (t) => {
await throwsErrorMessage(t, async () => {
await execute('42 pop pop')
}, (e) => e instanceof types.Err && e.message === 'The stack is empty' && e.origin.line === 0 && e.origin.col === 7)
await throwsErrorMessage(t, async () => {
await execute('{ 42 pop pop } exec')
}, (e) => e instanceof types.Err && e.message === 'The stack is empty' && e.origin.line === 0 && e.origin.col === 9)
})
test('Trying to get elements from below the stack should throw an error', async (t) => {
await throwsErrorMessage(t, async () => {
await execute('42 1 copy')
}, (e) => e instanceof types.Err && e.message === 'Stack access is out of range' && e.origin.line === 0 && e.origin.col === 5)
await throwsErrorMessage(t, async () => {
await execute('{ 42 1 copy } exec')
}, (e) => e instanceof types.Err && e.message === 'Stack access is out of range' && e.origin.line === 0 && e.origin.col === 7)
})
test('Trying to access the foreign stack in a function with a parameter list should throw an error', async (t) => {
await throwsErrorMessage(t, async () => {
await execute('test: () { pop } fun 42 test')
}, (e) => e instanceof types.Err && e.message === 'Stack underflow in function or lambda expression')
})
test('Variables defined while executing a function do not affect the lambda dict', async (t) => {
const { stack } = await execute(`
1 a!
:foo { 2 x! } fun
foo
:foo vref
`)
const foo = stack.pop()
t.true(foo instanceof types.Lam)
t.truthy(foo.dict.a, 'a should be in the lambda dict')
t.falsy(foo.dict.x, 'x should not be in the lambda dict')
})
test('The interpreter handles escape sequences properly', async (t) => {
const { stack } = await execute(`[
"\\"" length 1 =
"\\\\" length 1 =
"\\n" length 1 =
"\\r" length 1 =
"\\t" length 1 =
]`)
t.true(stack.pop().items.every((x) => x.value === true))
})
test('Reference counting should copy arrays before modification', async (t) => {
const { stack } = await execute('[1 2] dup 3 append')
t.deepEqual(stack.pop().items.map((i) => i.value), [1, 2, 3])
t.deepEqual(stack.pop().items.map((i) => i.value), [1, 2])
})
test('Reference counting should copy deep arrays before modification', async (t) => {
const { stack } = await execute('[ 1, 2, [ 1, 2, 3 ] ] dup [2 1] 42 path-set !=')
t.is(stack.pop().value, true)
})
test('Reference counting should copy objects from the dict', async (t) => {
const { stack } = await execute('[0] x! x 1 append x !=')
t.is(stack.pop().value, true)
})
test('It should not be possible to use a number as variable name', async (t) => {
await throwsErrorMessage(t, () => execute(`"hello" 1!`), checkErrorMessage('Invalid variable name "1"'))
await throwsErrorMessage(t, () => execute(`42 "hello" !`), checkErrorMessage('Expected operand 1 to be :Sym but got :Int instead'))
})
test('BinTree example', async (t) => {
await runPostFixTests(`
BinTree: {
Leaf: ()
Node: (value :Num, left :BinTree, right :BinTree)
} datadef
tree-sum: (t :BinTree -> :Num) {
{ t leaf? } { 0 }
{ t node? } {
t node-value
t node-left tree-sum +
t node-right tree-sum +
}
} cond-fun
1 leaf leaf node node1!
4 leaf leaf node node4!
3 leaf node4 node node3!
5 node1 node3 node node5!
node5 tree-sum 13 test=
`, t)
})
test('The interpreter should support proper tail calls in if bodies', async (t) => {
const { stack } = await execute(`
factorial_tr: (acc :Int, n :Int) {
n 1 > { acc n * n 1 - recur } { acc } if
} fun
factorial: (n :Int) { 1 n factorial_tr } fun
6 factorial
`, { maximumDictStackHeight: 2 })
t.is(stack.count, 1)
t.is(stack.pop().value, 720)
})
test('The interpreter should support proper tail calls in cond bodies', async (t) => {
const { stack } = await execute(`
factorial_tr: (acc :Int, n :Int) {
{ { n 1 > } { acc n * n 1 - recur }
{ true } { acc } } cond
} fun
factorial: (n :Int) { 1 n factorial_tr } fun
6 factorial
`, { maximumDictStackHeight: 2 })
t.is(stack.count, 1)
t.is(stack.pop().value, 720)
})
test('The interpreter should support proper tail calls in cond-fun bodies', async (t) => {
const { stack } = await execute(`
factorial_tr: (acc :Int, n :Int) {
{ n 1 > } { acc n * n 1 - recur }
{ true } { acc }
} cond-fun
factorial: (n :Int) { 1 n factorial_tr } fun
6 factorial
`, { maximumDictStackHeight: 2 })
t.is(stack.count, 1)
t.is(stack.pop().value, 720)
})
test('The interpreter should support proper tail calls when using exec', async (t) => {
const { stack, dictStack } = await execute(`
factorial_tr: (acc :Int, n :Int) {
{ { n 1 > } { acc n * n 1 - recur: vref exec }
{ true } { acc } } cond
} fun
factorial: (n :Int) { 1 n factorial_tr: vref exec } fun
6 factorial: vref exec
`, { maximumDictStackHeight: 2 })
t.is(stack.count, 1)
t.is(stack.pop().value, 720)
t.is(dictStack.count, 1)
})
test('Proper tail calls can be disabled', async (t) => {
await throwsErrorMessage(t, () => execute(`
factorial_tr: (acc :Int, n :Int) {
{ n 1 > } { acc n * n 1 - recur }
{ true } { acc }
} cond-fun
factorial: (n :Int) { 1 n factorial_tr } fun
6 factorial
`, {
maximumDictStackHeight: 2,
interpreterOptions: { enableProperTailCalls: false }
}),
checkErrorMessage('Exceeded the expected maximum dict stack height of 2'))
})
test('Explicit tail calls should still work if proper tail calls are disabled', async (t) => {
await t.notThrowsAsync(() => execute(`
factorial_tr: (acc :Int, n :Int) {
{ n 1 > } { acc n * n 1 - :recur tailcall }
{ true } { acc }
} cond-fun
factorial: (n :Int) { 1 n :factorial_tr tailcall } fun
6 factorial
`, {
maximumDictStackHeight: 2,
interpreterOptions: { enableProperTailCalls: false }
}))
})
test('Numbers support E-notation', async (t) => {
await runPostFixTests(`
2e5 200000 test=
2e-2 0.02 test=
`, t)
})
test('The interpreter can be copied', async (t) => {
const Interpreter = require('./Interpreter')
const Stack = require('./Stack')
const DictStack = require('./DictStack')
const interpreter = new Interpreter()
const copy = interpreter.copy()
t.true(copy._stack instanceof Stack, 'Missing stack')
t.true(copy._dictStack instanceof DictStack, 'Missing dictionary stack')
})