Source code
Revision control
Copy as Markdown
Other Tools
// |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");
}