summaryrefslogtreecommitdiffstats
path: root/js/src/jit-test/tests/environments/strict-eval-bindings.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/jit-test/tests/environments/strict-eval-bindings.js')
-rw-r--r--js/src/jit-test/tests/environments/strict-eval-bindings.js221
1 files changed, 221 insertions, 0 deletions
diff --git a/js/src/jit-test/tests/environments/strict-eval-bindings.js b/js/src/jit-test/tests/environments/strict-eval-bindings.js
new file mode 100644
index 0000000000..8e60574d6b
--- /dev/null
+++ b/js/src/jit-test/tests/environments/strict-eval-bindings.js
@@ -0,0 +1,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");
+}
+