strict-eval-bindings.js (7269B)
1 // |jit-test| skip-if: !('disassemble' in this) 2 // Strict direct eval supports static binding of identifiers. 3 4 "use strict"; 5 6 // Check that a script contains a particular bytecode sequence. 7 // 8 // `actual` is the output of the `disassemble()` shell builtin. 9 // `expected` is a semicolon-separated string of opcodes. 10 // Can include regular expression syntax, e.g. "GetLocal .* x$" 11 // to match a GetLocal instruction with ` x` at the end of the line. 12 // `message` is a string to include in the error message if the test fails. 13 // 14 function assertBytecode(actual, expected, message) { 15 // Grab the opcode name and everything after to the end of the line. This 16 // intentionally includes the expression stack, as that is what makes the 17 // `GetLocal .* y$` trick work. The disassemble() output is like this: 18 // 19 // 00016: 10 GetLocal 0 # x y 20 // 21 let actualOps = 22 actual.split('\n') 23 .map(s => /^\d{5}: +\d+ +(.*)$/.exec(s)?.[1]) 24 .filter(x => x !== undefined); 25 26 // Turn the expectations into regular expressions. 27 let expectedOps = 28 expected.split(';') 29 .map(s => { 30 s = s.trim(); 31 // If the op is a single word, like `Dup`, add `\b` to rule out 32 // similarly named ops like `Dup2`. 33 if (/^\w+$/.test(s)) { 34 s += "\\b"; 35 } 36 return new RegExp("^" + s); 37 }); 38 39 // The condition on this for-loop is saying, "continue as long as the range 40 // [i..i+expectedOps.length] is entirely within in the actualOps array". 41 // Hence the rare use of `<=` in a for-loop! 42 for (let i = 0; i + expectedOps.length <= actualOps.length; i++) { 43 if (expectedOps.every((expectRegExp, j) => expectRegExp.test(actualOps[i + j]))) { 44 // Found a complete match. 45 return; 46 } 47 } 48 throw new Error(`Assertion failed: ${message}\nexpected ${uneval(expected)}, got:\n${actual}`); 49 } 50 51 52 // --- Tests 53 54 var bytecode; 55 56 // `var`s in strict eval code are statically bound as locals. 57 eval(` 58 var pet = "ostrich"; 59 bytecode = disassemble(); 60 pet 61 `); 62 assertEq(globalThis.hasOwnProperty('pet'), false); 63 assertBytecode(bytecode, 'String "ostrich"; SetLocal; Pop', 64 "`pet` is stored in a stack local"); 65 assertBytecode(bytecode, "GetLocal; SetRval; RetRval", 66 "`pet` is loaded from the local at the end of the eval code"); 67 68 // Same for top-level `function`s. 69 eval(` 70 function banana() { return "potassium"; } 71 bytecode = disassemble(); 72 `); 73 assertEq(globalThis.hasOwnProperty('banana'), false); 74 assertBytecode(bytecode, 'Lambda .* banana; SetLocal; Pop', 75 "`banana` is stored in a stack local"); 76 77 // Same for let/const. 78 eval(` 79 let a = "ushiko-san"; 80 const b = "umao-san"; 81 bytecode = disassemble(); 82 [a, b] 83 `); 84 assertBytecode(bytecode, 'String "ushiko-san"; InitLexical; Pop', 85 "`let a` is stored in a stack local"); 86 assertBytecode(bytecode, 'String "umao-san"; InitLexical; Pop', 87 "`const b` is stored in a stack local"); 88 assertBytecode(bytecode, 'GetLocal .* a$; InitElemArray; GetLocal .* b$; InitElemArray', 89 "lexical variables are loaded from stack locals"); 90 91 // Same for arguments and locals in functions declared in strict eval code. 92 let g = eval(` 93 function f(a) { 94 let x = 'x'; 95 function g(b) { 96 let y = "wye"; 97 return [f, a, x, g, b, y]; 98 } 99 return g; 100 } 101 f(); 102 `); 103 bytecode = disassemble(g); 104 assertBytecode(bytecode, 'GetAliasedVar "f"', 105 "closed-over eval-scope `function` is accessed via aliased op"); 106 assertBytecode(bytecode, 'GetAliasedVar "a"', 107 "closed-over argument is accessed via aliased op"); 108 assertBytecode(bytecode, 'GetAliasedVar "x"', 109 "closed-over local `let` variable is accessed via aliased op"); 110 assertBytecode(bytecode, 'GetAliasedVar "g"', 111 "closed-over local `function` is accessed via aliased op"); 112 assertBytecode(bytecode, 'GetArg .* b$', 113 "non-closed-over arguments are optimized"); 114 assertBytecode(bytecode, 'GetLocal .* y$', 115 "non-closed-over locals are optimized"); 116 117 // Closed-over bindings declared in strict eval code are statically bound. 118 var fac = eval(` 119 bytecode = disassemble(); 120 function fac(x) { return x <= 1 ? 1 : x * fac(x - 1); } 121 fac 122 `); 123 assertBytecode(bytecode, 'SetAliasedVar "fac"', 124 "strict eval code accesses closed-over top-level function using aliased ops"); 125 assertBytecode(disassemble(fac), 'GetAliasedVar "fac"', 126 "function in strict eval accesses itself using aliased ops"); 127 128 // References to `this` in an enclosing method are statically bound. 129 let obj = { 130 m(s) { return eval(s); } 131 }; 132 let result = obj.m(` 133 bytecode = disassemble(); 134 this; 135 `); 136 assertEq(result, obj); 137 assertBytecode(bytecode, 'GetAliasedVar ".this"', 138 "strict eval in a method can access `this` using aliased ops"); 139 140 // Same for `arguments`. 141 function fn_with_args() { 142 return eval(` 143 bytecode = disassemble(); 144 arguments[0]; 145 `); 146 } 147 assertEq(fn_with_args(117), 117); 148 assertBytecode(bytecode, 'GetAliasedVar "arguments"', 149 "strict eval in a function can access `arguments` using aliased ops"); 150 151 // The frontend can emit GName ops in strict eval. 152 result = eval(` 153 bytecode = disassemble(); 154 fn_with_args; 155 `); 156 assertEq(result, fn_with_args); 157 assertBytecode(bytecode, 'GetGName "fn_with_args"', 158 "strict eval code can optimize access to globals"); 159 160 // Even within a function. 161 function test_globals_in_function() { 162 result = eval(` 163 bytecode = disassemble(); 164 fn_with_args; 165 `); 166 assertEq(result, fn_with_args); 167 assertBytecode(bytecode, 'GetGName "fn_with_args"', 168 "strict eval code in a function can optimize access to globals"); 169 } 170 test_globals_in_function(); 171 172 // Nested eval is no obstacle. 173 { 174 let outer = "outer"; 175 const f = function (code, a, b) { 176 return eval(code); 177 }; 178 let result = f(` 179 eval("bytecode = disassemble();\\n" + 180 "outer += a + b;\\n"); 181 `, 3, 4); 182 assertEq(outer, "outer7"); 183 assertBytecode(bytecode, 'GetAliasedVar "outer"', 184 "access to outer bindings is optimized even through nested strict evals"); 185 assertBytecode(bytecode, 'GetAliasedVar "a"', 186 "access to outer bindings is optimized even through nested strict evals"); 187 assertBytecode(bytecode, 'SetAliasedVar "outer"', 188 "assignment to outer bindings is optimized even through nested strict evals"); 189 } 190 191 // Assignment to an outer const is handled correctly. 192 { 193 const doNotSetMe = "i already have a value, thx"; 194 let f = eval(`() => { doNotSetMe = 34; }`); 195 assertBytecode(disassemble(f), 'ThrowSetConst "doNotSetMe"', 196 "assignment to outer const in strict eval code emits ThrowSetConst"); 197 } 198 199 // OK, there are other scopes but let's just do one more: the 200 // computed-property-name scope. 201 { 202 let stashed; 203 (class C { 204 [( 205 eval(` 206 var secret = () => C; 207 stashed = () => secret; 208 `), 209 "method" 210 )]() { 211 return "ok"; 212 } 213 }); 214 215 bytecode = disassemble(stashed()); 216 assertBytecode(bytecode, 'GetAliasedVar "C"', 217 "access to class name uses aliased ops"); 218 let C = stashed()(); 219 assertEq(new C().method(), "ok"); 220 }