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
213
214
215
216
217
218
219
220
221
|
// |jit-test| skip-if: !('disassemble' in this)
// Strict direct eval supports static binding of identifiers.
"use strict";
// Check that a script contains a particular bytecode sequence.
//
// `actual` is the output of the `disassemble()` shell builtin.
// `expected` is a semicolon-separated string of opcodes.
// Can include regular expression syntax, e.g. "GetLocal .* x$"
// to match a GetLocal instruction with ` x` at the end of the line.
// `message` is a string to include in the error message if the test fails.
//
function assertBytecode(actual, expected, message) {
// Grab the opcode name and everything after to the end of the line. This
// intentionally includes the expression stack, as that is what makes the
// `GetLocal .* y$` trick work. The disassemble() output is like this:
//
// 00016: 10 GetLocal 0 # x y
//
let actualOps =
actual.split('\n')
.map(s => /^\d{5}: +\d+ +(.*)$/.exec(s)?.[1])
.filter(x => x !== undefined);
// Turn the expectations into regular expressions.
let expectedOps =
expected.split(';')
.map(s => {
s = s.trim();
// If the op is a single word, like `Dup`, add `\b` to rule out
// similarly named ops like `Dup2`.
if (/^\w+$/.test(s)) {
s += "\\b";
}
return new RegExp("^" + s);
});
// The condition on this for-loop is saying, "continue as long as the range
// [i..i+expectedOps.length] is entirely within in the actualOps array".
// Hence the rare use of `<=` in a for-loop!
for (let i = 0; i + expectedOps.length <= actualOps.length; i++) {
if (expectedOps.every((expectRegExp, j) => expectRegExp.test(actualOps[i + j]))) {
// Found a complete match.
return;
}
}
throw new Error(`Assertion failed: ${message}\nexpected ${uneval(expected)}, got:\n${actual}`);
}
// --- Tests
var bytecode;
// `var`s in strict eval code are statically bound as locals.
eval(`
var pet = "ostrich";
bytecode = disassemble();
pet
`);
assertEq(globalThis.hasOwnProperty('pet'), false);
assertBytecode(bytecode, 'String "ostrich"; SetLocal; Pop',
"`pet` is stored in a stack local");
assertBytecode(bytecode, "GetLocal; SetRval; RetRval",
"`pet` is loaded from the local at the end of the eval code");
// Same for top-level `function`s.
eval(`
function banana() { return "potassium"; }
bytecode = disassemble();
`);
assertEq(globalThis.hasOwnProperty('banana'), false);
assertBytecode(bytecode, 'Lambda .* banana; SetLocal; Pop',
"`banana` is stored in a stack local");
// Same for let/const.
eval(`
let a = "ushiko-san";
const b = "umao-san";
bytecode = disassemble();
[a, b]
`);
assertBytecode(bytecode, 'String "ushiko-san"; InitLexical; Pop',
"`let a` is stored in a stack local");
assertBytecode(bytecode, 'String "umao-san"; InitLexical; Pop',
"`const b` is stored in a stack local");
assertBytecode(bytecode, 'GetLocal .* a$; InitElemArray; GetLocal .* b$; InitElemArray',
"lexical variables are loaded from stack locals");
// Same for arguments and locals in functions declared in strict eval code.
let g = eval(`
function f(a) {
let x = 'x';
function g(b) {
let y = "wye";
return [f, a, x, g, b, y];
}
return g;
}
f();
`);
bytecode = disassemble(g);
assertBytecode(bytecode, 'GetAliasedVar "f"',
"closed-over eval-scope `function` is accessed via aliased op");
assertBytecode(bytecode, 'GetAliasedVar "a"',
"closed-over argument is accessed via aliased op");
assertBytecode(bytecode, 'GetAliasedVar "x"',
"closed-over local `let` variable is accessed via aliased op");
assertBytecode(bytecode, 'GetAliasedVar "g"',
"closed-over local `function` is accessed via aliased op");
assertBytecode(bytecode, 'GetArg .* b$',
"non-closed-over arguments are optimized");
assertBytecode(bytecode, 'GetLocal .* y$',
"non-closed-over locals are optimized");
// Closed-over bindings declared in strict eval code are statically bound.
var fac = eval(`
bytecode = disassemble();
function fac(x) { return x <= 1 ? 1 : x * fac(x - 1); }
fac
`);
assertBytecode(bytecode, 'SetAliasedVar "fac"',
"strict eval code accesses closed-over top-level function using aliased ops");
assertBytecode(disassemble(fac), 'GetAliasedVar "fac"',
"function in strict eval accesses itself using aliased ops");
// References to `this` in an enclosing method are statically bound.
let obj = {
m(s) { return eval(s); }
};
let result = obj.m(`
bytecode = disassemble();
this;
`);
assertEq(result, obj);
assertBytecode(bytecode, 'GetAliasedVar ".this"',
"strict eval in a method can access `this` using aliased ops");
// Same for `arguments`.
function fn_with_args() {
return eval(`
bytecode = disassemble();
arguments[0];
`);
}
assertEq(fn_with_args(117), 117);
assertBytecode(bytecode, 'GetAliasedVar "arguments"',
"strict eval in a function can access `arguments` using aliased ops");
// The frontend can emit GName ops in strict eval.
result = eval(`
bytecode = disassemble();
fn_with_args;
`);
assertEq(result, fn_with_args);
assertBytecode(bytecode, 'GetGName "fn_with_args"',
"strict eval code can optimize access to globals");
// Even within a function.
function test_globals_in_function() {
result = eval(`
bytecode = disassemble();
fn_with_args;
`);
assertEq(result, fn_with_args);
assertBytecode(bytecode, 'GetGName "fn_with_args"',
"strict eval code in a function can optimize access to globals");
}
test_globals_in_function();
// Nested eval is no obstacle.
{
let outer = "outer";
const f = function (code, a, b) {
return eval(code);
};
let result = f(`
eval("bytecode = disassemble();\\n" +
"outer += a + b;\\n");
`, 3, 4);
assertEq(outer, "outer7");
assertBytecode(bytecode, 'GetAliasedVar "outer"',
"access to outer bindings is optimized even through nested strict evals");
assertBytecode(bytecode, 'GetAliasedVar "a"',
"access to outer bindings is optimized even through nested strict evals");
assertBytecode(bytecode, 'SetAliasedVar "outer"',
"assignment to outer bindings is optimized even through nested strict evals");
}
// Assignment to an outer const is handled correctly.
{
const doNotSetMe = "i already have a value, thx";
let f = eval(`() => { doNotSetMe = 34; }`);
assertBytecode(disassemble(f), 'ThrowSetConst "doNotSetMe"',
"assignment to outer const in strict eval code emits ThrowSetConst");
}
// OK, there are other scopes but let's just do one more: the
// computed-property-name scope.
{
let stashed;
(class C {
[(
eval(`
var secret = () => C;
stashed = () => secret;
`),
"method"
)]() {
return "ok";
}
});
bytecode = disassemble(stashed());
assertBytecode(bytecode, 'GetAliasedVar "C"',
"access to class name uses aliased ops");
let C = stashed()();
assertEq(new C().method(), "ok");
}
|