diff --git a/scripts/test262-runner.js b/scripts/test262-runner.js index 1f41c248a..d5646bdfa 100644 --- a/scripts/test262-runner.js +++ b/scripts/test262-runner.js @@ -33,6 +33,9 @@ import tty from "tty"; import minimist from "minimist"; import process from "process"; import vm from "vm"; +import * as babelTypes from "@babel/types"; +import traverse from "@babel/traverse"; +import generate from "@babel/generator"; const EOL = os.EOL; const cpus = os.cpus(); @@ -45,7 +48,7 @@ type GroupsMap = { [key: string]: TestRecord[] }; type TestRunOptions = {| +timeout: number, - +serializer: boolean, + +serializer: boolean | "abstract-scalar", |}; // A TestTask is a task for a worker process to execute, which contains a @@ -242,7 +245,7 @@ class MasterProgramArgs { filterString: string; singleThreaded: boolean; relativeTestPath: string; - serializer: boolean; + serializer: boolean | "abstract-scalar"; expectedES5: number; expectedES6: number; expectedTimeouts: number; @@ -256,7 +259,7 @@ class MasterProgramArgs { filterString: string, singleThreaded: boolean, relativeTestPath: string, - serializer: boolean, + serializer: boolean | "abstract-scalar", expectedES5: number, expectedES6: number, expectedTimeouts: number @@ -279,9 +282,9 @@ class MasterProgramArgs { class WorkerProgramArgs { relativeTestPath: string; timeout: number; - serializer: boolean; + serializer: boolean | "abstract-scalar"; - constructor(relativeTestPath: string, timeout: number, serializer: boolean) { + constructor(relativeTestPath: string, timeout: number, serializer: boolean | "abstract-scalar") { this.timeout = timeout; this.serializer = serializer; this.relativeTestPath = relativeTestPath; @@ -353,7 +356,7 @@ function usage(): string { function masterArgsParse(): MasterProgramArgs { let parsedArgs = minimist(process.argv.slice(2), { string: ["statusFile", "relativeTestPath"], - boolean: ["verbose", "singleThreaded", "serializer"], + boolean: ["verbose", "singleThreaded"], default: { verbose: process.stdout instanceof tty.WriteStream ? false : true, statusFile: "", @@ -395,8 +398,10 @@ function masterArgsParse(): MasterProgramArgs { throw new ArgsParseError("relativeTestPath must be a string (--relativeTestPath /../test/test262)"); } let relativeTestPath = parsedArgs.relativeTestPath; - if (typeof parsedArgs.serializer !== "boolean") { - throw new ArgsParseError("serializer must be a boolean (either --serializer or not)"); + if (!(typeof parsedArgs.serializer === "boolean" || parsedArgs.serializer === "abstract-scalar")) { + throw new ArgsParseError( + "serializer must be a boolean or must be the string 'abstract-scalar' (--serializer or --serializer abstract-scalar)" + ); } let serializer = parsedArgs.serializer; if (typeof parsedArgs.expectedCounts !== "string") { @@ -438,8 +443,10 @@ function workerArgsParse(): WorkerProgramArgs { if (typeof parsedArgs.timeout !== "number") { throw new ArgsParseError("timeout must be a number (in seconds) (--timeout 10)"); } - if (typeof parsedArgs.serializer !== "boolean") { - throw new ArgsParseError("serializer must be a boolean (either --serializer or not)"); + if (!(typeof parsedArgs.serializer === "boolean" || parsedArgs.serializer === "abstract-scalar")) { + throw new ArgsParseError( + "serializer must be a boolean or must be the string 'abstract-scalar' (--serializer or --serializer abstract-scalar)" + ); } return new WorkerProgramArgs(parsedArgs.relativeTestPath, parsedArgs.timeout, parsedArgs.serializer); } @@ -1023,13 +1030,13 @@ function runTest( // eslint-disable-next-line flowtype/no-weak-types harnesses: Object, strict: boolean, - { timeout, serializer }: TestRunOptions + options: TestRunOptions ): ?TestResult { - if (serializer) { - return executeTestUsingSerializer(test, testFileContents, data, harnesses, strict, timeout); + if (options.serializer) { + return executeTestUsingSerializer(test, testFileContents, data, harnesses, strict, options); } - let { realm } = createRealm(timeout); + let { realm } = createRealm(options.timeout); // Run the test. try { @@ -1136,8 +1143,9 @@ function executeTestUsingSerializer( // eslint-disable-next-line flowtype/no-weak-types harnesses: Object, strict: boolean, - timeout: number + options: TestRunOptions ) { + let { timeout } = options; let sources = []; // Add the test262 intrinsics. @@ -1172,13 +1180,38 @@ var print = () => {}; // noop for now if (diag.severity !== "Warning") return "Fail"; return "Recover"; }, - onParse: () => { - // TODO(calebmer): Turn all literals into abstract values + onParse: ast => { + // Transform all statements which come from our test source file. Do not transform statements from our + // harness files. + if (options.serializer === "abstract-scalar") { + ast.program.body.forEach(node => { + if ((node.loc: any).filename === test.location) { + transformScalarsToAbstractValues(node); + } + }); + } }, }); } catch (error) { if (error.message === "Timed out") return new TestResult(false, strict, error); if (error.message.includes("Syntax error")) return null; + // Uncomment the following JS code to do analysis on what kinds of Prepack errors we get. + // + // ```js + // console.error( + // `${error.name.replace(/\n/g, "\\n")}: ${error.message.replace(/\n/g, "\\n")} (${error.stack + // .match(/at .+$/gm) + // .slice(0, 3) + // .join(", ")})` + // ); + // ``` + // + // Analysis bash command: + // + // ```bash + // yarn test-test262 --serializer 2> result.err + // cat result.err | sort | uniq -c | sort -nr + // ``` return new TestResult(false, strict, new Error(`Prepack error:\n${error.stack}`)); } @@ -1205,6 +1238,83 @@ var print = () => {}; // noop for now } } +const TransformScalarsToAbstractValuesVisitor = (() => { + const t = babelTypes; + + function createAbstractCall(type, actual, { allowDuplicateNames, disablePlaceholders } = {}) { + const args = [type, actual]; + if (allowDuplicateNames) { + args.push( + t.objectExpression([ + t.objectProperty(t.identifier("allowDuplicateNames"), t.booleanLiteral(!!allowDuplicateNames)), + t.objectProperty(t.identifier("disablePlaceholders"), t.booleanLiteral(!!disablePlaceholders)), + ]) + ); + } + return t.callExpression(t.identifier("__abstract"), args); + } + + const defaultOptions = { + allowDuplicateNames: true, + disablePlaceholders: true, + }; + + const symbolOptions = { + // Intentionally false since two symbol calls will be referentially not equal, but Prepack will share + // a variable. + allowDuplicateNames: false, + disablePlaceholders: true, + }; + + return { + noScope: true, + + BooleanLiteral(p) { + p.node = p.container[p.key] = createAbstractCall( + t.stringLiteral("boolean"), + t.stringLiteral(p.node.value.toString()), + defaultOptions + ); + }, + StringLiteral(p) { + // `eval()` does not support abstract arguments and we don't care to fix that. + if ( + p.parent.type === "CallExpression" && + p.parent.callee.type === "Identifier" && + p.parent.callee.name === "eval" + ) { + return; + } + p.node = p.container[p.key] = createAbstractCall( + t.stringLiteral("string"), + t.stringLiteral(JSON.stringify(p.node.value)), + defaultOptions + ); + }, + CallExpression(p) { + if (p.node.callee.type === "Identifier" && p.node.callee.name === "Symbol") { + p.node = p.container[p.key] = createAbstractCall( + t.stringLiteral("symbol"), + t.stringLiteral(generate(p.node).code), + symbolOptions + ); + } + }, + NumericLiteral(p) { + p.node = p.container[p.key] = createAbstractCall( + t.stringLiteral(Number.isInteger(p.node.value) ? "integral" : "number"), + t.stringLiteral(p.node.extra.raw), + defaultOptions + ); + }, + }; +})(); + +function transformScalarsToAbstractValues(ast) { + traverse(ast, TransformScalarsToAbstractValuesVisitor); + traverse.cache.clear(); +} + /** * Returns true if ${test} should be run, false otherwise */ diff --git a/src/intrinsics/prepack/global.js b/src/intrinsics/prepack/global.js index 7abc8c23e..828384590 100644 --- a/src/intrinsics/prepack/global.js +++ b/src/intrinsics/prepack/global.js @@ -80,6 +80,8 @@ export default function(realm: Realm): void { // options is an optional object that may contain: // - allowDuplicateNames: boolean representing whether the name of the abstract value may be // repeated, by default they must be unique + // - disablePlaceholders: boolean representing whether placeholders should be substituted in + // the abstract value's name. // If the abstract value gets somehow embedded in the final heap, // it will be referred to by the supplied name in the generated code. global.$DefineOwnProperty("__abstract", { diff --git a/src/intrinsics/prepack/utils.js b/src/intrinsics/prepack/utils.js index 27cbeae67..d755a2a1f 100644 --- a/src/intrinsics/prepack/utils.js +++ b/src/intrinsics/prepack/utils.js @@ -104,7 +104,13 @@ export function createAbstract( } else { realm.saveNameString(name); } - result = AbstractValue.createFromTemplate(realm, buildExpressionTemplate(name), type, [], kind); + result = AbstractValue.createFromTemplate( + realm, + buildExpressionTemplate(name, { disablePlaceholders: !!optionsMap.get("disablePlaceholders") }), + type, + [], + kind + ); result.intrinsicName = name; } diff --git a/src/utils/builder.js b/src/utils/builder.js index 41d8dbe3d..d2c781147 100644 --- a/src/utils/builder.js +++ b/src/utils/builder.js @@ -15,12 +15,20 @@ import type { PreludeGenerator } from "./PreludeGenerator.js"; import invariant from "../invariant.js"; const placeholders = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); -const placeholderWhitelist = new Set(["global", ...placeholders]); +const placeholderDefaultWhiteList = new Set(["global"]); +const placeholderWhitelist = new Set([...placeholderDefaultWhiteList, ...placeholders]); -export default function buildExpressionTemplate(code: string): (void | PreludeGenerator) => any => BabelNodeExpression { +export default function buildExpressionTemplate( + code: string, + { disablePlaceholders }: { disablePlaceholders?: boolean } = {} +): (void | PreludeGenerator) => any => BabelNodeExpression { let template; return (preludeGenerator: void | PreludeGenerator) => (obj: any): BabelNodeExpression => { - if (template === undefined) template = buildTemplate(code, { placeholderPattern: false, placeholderWhitelist }); + if (template === undefined) + template = buildTemplate(code, { + placeholderPattern: false, + placeholderWhitelist: disablePlaceholders ? placeholderDefaultWhiteList : placeholderWhitelist, + }); if (preludeGenerator !== undefined && code.includes("global")) obj = Object.assign( {