aboutsummaryrefslogtreecommitdiff
var crypto = require("crypto");
var U = require("..");
var List = U.List;
var os = require("os");
var sandbox = require("./sandbox");

// Reduce a test case by iteratively replacing AST nodes with various
// permutations. Each AST_Statement in the tree is also speculatively dropped
// to determine whether it is needed.  If the altered tree and the last known
// good tree produce the same output after being run, then the permutation
// survives to the next generation and is the basis for subsequent iterations.
// The test case is reduced as a consequence of complex expressions being
// replaced with simpler ones.  Note that a reduced test case will have
// different runtime output - it is not functionally equivalent to the
// original. The only criteria is that once the generated reduced test case is
// run without minification, it will produce different output from the code
// minified with `minify_options`.  Returns a `minify` result object.

Error.stackTraceLimit = Infinity;
module.exports = function reduce_test(testcase, minify_options, reduce_options) {
    minify_options = minify_options || {};
    reduce_options = reduce_options || {};
    var print_options = {};
    [
        "ie",
        "v8",
        "webkit",
    ].forEach(function(name) {
        var value = minify_options[name] || minify_options.output && minify_options.output[name];
        if (value) print_options[name] = value;
    });
    if (testcase instanceof U.AST_Node) testcase = testcase.print_to_string(print_options);
    var max_iterations = reduce_options.max_iterations || 1000;
    var max_timeout = reduce_options.max_timeout || 10000;
    var warnings = [];
    var log = reduce_options.log || function(msg) {
        warnings.push(msg);
    };
    var verbose = reduce_options.verbose;
    var minify_options_json = JSON.stringify(minify_options, null, 2);
    var result_cache = Object.create(null);
    var test_for_diff = compare_run_code;
    // the initial timeout to assess the viability of the test case must be large
    var differs = test_for_diff(testcase, minify_options, result_cache, max_timeout);

    if (verbose) {
        log("// Node.js " + process.version + " on " + os.platform() + " " + os.arch());
    }
    if (differs && differs.error && [ "DefaultsError", "SyntaxError" ].indexOf(differs.error.name) < 0) {
        test_for_diff = test_minify;
        differs = test_for_diff(testcase, minify_options, result_cache, max_timeout);
    }
    if (!differs) {
        // same stdout result produced when minified
        return {
            code: [
                "// Can't reproduce test failure",
                "// minify options: " + to_comment(minify_options_json)
            ].join("\n"),
            warnings: warnings,
        };
    } else if (differs.timed_out) {
        return {
            code: [
                "// Can't reproduce test failure within " + max_timeout + "ms",
                "// minify options: " + to_comment(minify_options_json)
            ].join("\n"),
            warnings: warnings,
        };
    } else if (differs.error) {
        differs.warnings = warnings;
        return differs;
    } else if (sandbox.is_error(differs.unminified_result)
        && sandbox.is_error(differs.minified_result)
        && differs.unminified_result.name == differs.minified_result.name) {
        return {
            code: [
                "// No differences except in error message",
                "// minify options: " + to_comment(minify_options_json)
            ].join("\n"),
            warnings: warnings,
        };
    } else {
        max_timeout = Math.min(100 * differs.elapsed, max_timeout);
        // Replace expressions with constants that will be parsed into
        // AST_Nodes as required.  Each AST_Node has its own permutation count,
        // so these replacements can't be shared.
        // Although simpler replacements are generally faster and better,
        // feel free to experiment with a different replacement set.
        var REPLACEMENTS = [
            // "null", "''", "false", "'foo'", "undefined", "9",
            "1", "0",
        ];

        // There's a relationship between each node's _permute counter and
        // REPLACEMENTS.length which is why fractional _permutes were needed.
        // One could scale all _permute operations by a factor of `steps`
        // to only deal with integer operations, but this works well enough.
        var steps = 4;        // must be a power of 2
        var step = 1 / steps; // 0.25 is exactly representable in floating point

        var tt = new U.TreeTransformer(function(node, descend, in_list) {
            if (CHANGED) return;

            // quick ignores
            if (node instanceof U.AST_Accessor) return;
            if (node instanceof U.AST_Directive) return;
            if (!in_list && node instanceof U.AST_EmptyStatement) return;
            if (node instanceof U.AST_Label) return;
            if (node instanceof U.AST_LabelRef) return;
            if (node instanceof U.AST_Toplevel) return;
            var parent = tt.parent();
            if (node instanceof U.AST_SymbolFunarg && parent instanceof U.AST_Accessor) return;
            if (!in_list && parent.rest !== node && node instanceof U.AST_SymbolDeclaration) return;

            // ensure that the _permute prop is a number.
            // can not use `node.start._permute |= 0;` as it will erase fractional part.
            if (typeof node.start._permute === "undefined") node.start._permute = 0;

            // if node reached permutation limit - skip over it.
            // no structural AST changes before this point.
            if (node.start._permute >= REPLACEMENTS.length) return;

            // ignore lvalues
            if (parent instanceof U.AST_Assign && parent.left === node) return;
            if (parent instanceof U.AST_DefaultValue && parent.name === node) return;
            if (parent instanceof U.AST_DestructuredKeyVal && parent.value === node) return;
            if (parent instanceof U.AST_Unary && parent.expression === node) switch (parent.operator) {
              case "++":
              case "--":
              case "delete":
                return;
            }
            if (parent instanceof U.AST_VarDef && parent.name === node) return;
            // preserve class methods
            if (parent instanceof U.AST_ClassMethod && parent.value === node) return;
            // preserve exports
            if (parent instanceof U.AST_ExportDeclaration) return;
            if (parent instanceof U.AST_ExportDefault) return;
            if (parent instanceof U.AST_ExportForeign) return;
            if (parent instanceof U.AST_ExportReferences) return;
            // preserve sole definition of an export statement
            if (node instanceof U.AST_VarDef
                && parent.definitions.length == 1
                && tt.parent(1) instanceof U.AST_ExportDeclaration) {
                return;
            }
            // preserve for (var xxx; ...)
            if (parent instanceof U.AST_For && parent.init === node && node instanceof U.AST_Definitions) return node;
            // preserve for (xxx in/of ...)
            if (parent instanceof U.AST_ForEnumeration && parent.init === node) return node;
            // preserve super(...)
            if (node.TYPE == "Call" && node.expression instanceof U.AST_Super) return;
            if (node instanceof U.AST_Super && parent.TYPE == "Call" && parent.expression === node) return node;

            // node specific permutations with no parent logic

            if (node instanceof U.AST_Array) {
                var expr = node.elements[0];
                if (expr && !(expr instanceof U.AST_Hole)) {
                    node.start._permute++;
                    CHANGED = true;
                    return expr instanceof U.AST_Spread ? expr.expression : expr;
                }
            }
            else if (node instanceof U.AST_Binary) {
                var permute = ((node.start._permute += step) * steps | 0) % 4;
                var expr = [
                    node.left,
                    node.right,
                ][ permute & 1 ];
                if (expr instanceof U.AST_Destructured) expr = expr.transform(new U.TreeTransformer(function(node, descend) {
                    if (node instanceof U.AST_DefaultValue) return new U.AST_Assign({
                        operator: "=",
                        left: node.name.transform(this),
                        right: node.value,
                        start: {},
                    });
                    if (node instanceof U.AST_DestructuredKeyVal) return new U.AST_ObjectKeyVal(node);
                    if (node instanceof U.AST_Destructured) {
                        node = new (node instanceof U.AST_DestructuredArray ? U.AST_Array : U.AST_Object)(node);
                        descend(node, this);
                    }
                    return node;
                }));
                CHANGED = true;
                return permute < 2 ? expr : wrap_with_console_log(expr);
            }
            else if (node instanceof U.AST_BlockStatement) {
                if (in_list && node.body.filter(function(node) {
                    return node instanceof U.AST_Const;
                }).length == 0) {
                    node.start._permute++;
                    CHANGED = true;
                    return List.splice(node.body);
                }
            }
            else if (node instanceof U.AST_Call) {
                var expr = [
                    !(node.expression instanceof U.AST_Super) && node.expression,
                    node.args[0],
                    null,  // intentional
                ][ ((node.start._permute += step) * steps | 0) % 3 ];
                if (expr) {
                    CHANGED = true;
                    return expr instanceof U.AST_Spread ? expr.expression : expr;
                }
                if (node.expression instanceof U.AST_Arrow && node.expression.value) {
                    var seq = node.args.slice();
                    seq.push(node.expression.value);
                    CHANGED = true;
                    return to_sequence(seq);
                }
                if (node.expression instanceof U.AST_Function) {
                    // hoist and return expressions from the IIFE function expression
                    var seq = [];
                    node.expression.body.forEach(function(node) {
                        var expr = expr instanceof U.AST_Exit ? node.value : node.body;
                        if (expr instanceof U.AST_Node && !U.is_statement(expr) && can_hoist(expr)) {
                            // collect expressions from each statements' body
                            seq.push(expr);
                        }
                    });
                    CHANGED = true;
                    return to_sequence(seq);
                }
            }
            else if (node instanceof U.AST_Catch) {
                // drop catch block
                node.start._permute++;
                CHANGED = true;
                return null;
            }
            else if (node instanceof U.AST_Conditional) {
                CHANGED = true;
                return [
                    node.condition,
                    node.consequent,
                    node.alternative,
                ][ ((node.start._permute += step) * steps | 0) % 3 ];
            }
            else if (node instanceof U.AST_DefaultValue) {
                node.start._permute++;
                CHANGED = true;
                return node.name;
            }
            else if (node instanceof U.AST_DestructuredArray) {
                var expr = node.elements[0];
                if (expr && !(expr instanceof U.AST_Hole)) {
                    node.start._permute++;
                    CHANGED = true;
                    return expr;
                }
            }
            else if (node instanceof U.AST_DestructuredObject) {
                // first property's value
                var expr = node.properties[0];
                if (expr) {
                    node.start._permute++;
                    CHANGED = true;
                    return expr.value;
                }
            }
            else if (node instanceof U.AST_Defun) {
                switch (((node.start._permute += step) * steps | 0) % 2) {
                  case 0:
                    CHANGED = true;
                    return List.skip;
                  default:
                    if (!has_exit(node) && can_hoist(node)) {
                        // hoist function declaration body
                        var body = node.body;
                        node.body = [];
                        body.push(node); // retain function with empty body to be dropped later
                        CHANGED = true;
                        return List.splice(body);
                    }
                }
            }
            else if (node instanceof U.AST_DWLoop) {
                var expr = [
                    node.condition,
                    node.body,
                    null,  // intentional
                ][ (node.start._permute * steps | 0) % 3 ];
                node.start._permute += step;
                if (!expr) {
                    if (node.body[0] instanceof U.AST_Break) {
                        if (node instanceof U.AST_Do) {
                            CHANGED = true;
                            return List.skip;
                        }
                        expr = node.condition; // AST_While - fall through
                    }
                }
                if (expr && (expr !== node.body || !has_loopcontrol(expr, node, parent))) {
                    CHANGED = true;
                    return to_statement(expr);
                }
            }
            else if (node instanceof U.AST_Finally) {
                // drop finally block
                node.start._permute++;
                CHANGED = true;
                return null;
            }
            else if (node instanceof U.AST_For) {
                var expr = [
                    node.init,
                    node.condition,
                    node.step,
                    node.body,
                ][ (node.start._permute * steps | 0) % 4 ];
                node.start._permute += step;
                if (expr && (expr !== node.body || !has_loopcontrol(expr, node, parent))) {
                    CHANGED = true;
                    return to_statement_init(expr);
                }
            }
            else if (node instanceof U.AST_ForEnumeration) {
                var expr;
                switch ((node.start._permute * steps | 0) % 3) {
                  case 0:
                    if (node.init instanceof U.AST_Definitions) {
                        if (node.init instanceof U.AST_Const) break;
                        if (node.init.definitions[0].name instanceof U.AST_Destructured) break;
                    }
                    expr = node.init;
                    break;
                  case 1:
                    expr = node.object;
                    break;
                  case 2:
                    if (!has_loopcontrol(node.body, node, parent)) expr = node.body;
                    break;
                }
                node.start._permute += step;
                if (expr) {
                    CHANGED = true;
                    return to_statement_init(expr);
                }
            }
            else if (node instanceof U.AST_If) {
                var expr = [
                    node.condition,
                    node.body,
                    node.alternative,
                ][ (node.start._permute * steps | 0) % 3 ];
                node.start._permute += step;
                if (expr) {
                    // replace if statement with its condition, then block or else block
                    CHANGED = true;
                    return to_statement(expr);
                }
            }
            else if (node instanceof U.AST_Object) {
                // first property's value
                var expr = node.properties[0];
                if (expr instanceof U.AST_ObjectKeyVal) {
                    expr = expr.value;
                } else if (expr instanceof U.AST_Spread) {
                    expr = expr.expression;
                } else if (expr && expr.key instanceof U.AST_Node) {
                    expr = expr.key;
                } else {
                    expr = null;
                }
                if (expr) {
                    node.start._permute++;
                    CHANGED = true;
                    return expr;
                }
            }
            else if (node instanceof U.AST_PropAccess) {
                var expr = [
                    !(node.expression instanceof U.AST_Super) && node.expression,
                    node.property instanceof U.AST_Node && !(parent instanceof U.AST_Destructured) && node.property,
                ][ node.start._permute++ % 2 ];
                if (expr) {
                    CHANGED = true;
                    return expr;
                }
            }
            else if (node instanceof U.AST_SimpleStatement) {
                if (node.body instanceof U.AST_Call && node.body.expression instanceof U.AST_Function) {
                    // hoist simple statement IIFE function expression body
                    node.start._permute++;
                    if (!has_exit(node.body.expression) && can_hoist(node.body.expression)) {
                        CHANGED = true;
                        return List.splice(node.body.expression.body);
                    }
                }
            }
            else if (node instanceof U.AST_Switch) {
                var expr = [
                    node.expression,                         // switch expression
                    node.body[0] && node.body[0].expression, // first case expression or undefined
                    node.body[0] && node.body[0],            // first case body or undefined
                ][ (node.start._permute * steps | 0) % 4 ];
                node.start._permute += step;
                if (expr && (!(expr instanceof U.AST_Statement) || !has_loopcontrol(expr, node, parent))) {
                    CHANGED = true;
                    return expr instanceof U.AST_SwitchBranch ? new U.AST_BlockStatement({
                        body: expr.body.slice(),
                        start: {},
                    }) : to_statement(expr);
                }
            }
            else if (node instanceof U.AST_Try) {
                var body = [
                    node.body,
                    node.bcatch && node.bcatch.body,
                    node.bfinally && node.bfinally.body,
                    null,  // intentional
                ][ (node.start._permute * steps | 0) % 4 ];
                node.start._permute += step;
                if (body) {
                    // replace try statement with try block, catch block, or finally block
                    CHANGED = true;
                    return new U.AST_BlockStatement({
                        body: body,
                        start: {},
                    });
                } else {
                    // replace try with a break or return if first in try statement
                    if (node.body[0] instanceof U.AST_Break
                        || node.body[0] instanceof U.AST_Return) {
                        CHANGED = true;
                        return node.body[0];
                    }
                }
            }
            else if (node instanceof U.AST_Unary) {
                node.start._permute++;
                CHANGED = true;
                return node.expression;
            }
            else if (node instanceof U.AST_Var) {
                if (node.definitions.length == 1 && node.definitions[0].value) {
                    // first declaration value
                    node.start._permute++;
                    CHANGED = true;
                    return to_statement(node.definitions[0].value);
                }
            }
            else if (node instanceof U.AST_LabeledStatement) {
                if (node.body instanceof U.AST_Statement
                    && !has_loopcontrol(node.body, node.body, node)) {
                    // replace labelled statement with its non-labelled body
                    node.start._permute = REPLACEMENTS.length;
                    CHANGED = true;
                    return node.body;
                }
            }

            if (in_list) {
                // drop switch branches
                if (parent instanceof U.AST_Switch && parent.expression != node) {
                    node.start._permute++;
                    CHANGED = true;
                    return List.skip;
                }

                // replace or skip statement
                if (node instanceof U.AST_Statement) {
                    node.start._permute++;
                    CHANGED = true;
                    return List.skip;
                }

                // remove this node unless its the sole element of a (transient) sequence
                if (!(parent instanceof U.AST_Sequence) || parent.expressions.length > 1) {
                    node.start._permute++;
                    CHANGED = true;
                    return List.skip;
                }
            } else if (parent.rest === node) {
                node.start._permute++;
                CHANGED = true;
                return null;
            }

            // replace this node
            var newNode = U.is_statement(node) ? new U.AST_EmptyStatement({
                start: {},
            }) : U.parse(REPLACEMENTS[node.start._permute % REPLACEMENTS.length | 0], {
                expression: true,
            });
            newNode.start._permute = ++node.start._permute;
            CHANGED = true;
            return newNode;
        }, function(node, in_list) {
            if (node instanceof U.AST_Definitions) {
                // remove empty var statement
                if (node.definitions.length == 0) return in_list ? List.skip : new U.AST_EmptyStatement({
                    start: {},
                });
            } else if (node instanceof U.AST_ObjectMethod) {
                if (!/Function$/.test(node.value.TYPE)) return new U.AST_ObjectKeyVal({
                    key: node.key,
                    value: node.value,
                    start: {},
                });
            } else if (node instanceof U.AST_Sequence) {
                // expand single-element sequence
                if (node.expressions.length == 1) return node.expressions[0];
            } else if (node instanceof U.AST_Try) {
                // expand orphaned try block
                if (!node.bcatch && !node.bfinally) return new U.AST_BlockStatement({
                    body: node.body,
                    start: {},
                });
            }
        });

        var diff_error_message;
        for (var pass = 1; pass <= 3; ++pass) {
            var testcase_ast = U.parse(testcase);
            if (diff_error_message === testcase) {
                // only difference detected is in error message, so expose that and try again
                testcase_ast.transform(new U.TreeTransformer(function(node, descend) {
                    if (node.TYPE == "Call" && node.expression.print_to_string() == "console.log") {
                        return to_sequence(node.args);
                    }
                    if (node instanceof U.AST_Catch && node.argname instanceof U.AST_SymbolCatch) {
                        descend(node, this);
                        node.body.unshift(new U.AST_SimpleStatement({
                            body: wrap_with_console_log(new U.AST_SymbolRef(node.argname)),
                            start: {},
                        }));
                        return node;
                    }
                }));
                var code = testcase_ast.print_to_string(print_options);
                var diff = test_for_diff(code, minify_options, result_cache, max_timeout);
                if (diff && !diff.timed_out && !diff.error) {
                    testcase = code;
                    differs = diff;
                } else {
                    testcase_ast = U.parse(testcase);
                }
            }
            diff_error_message = null;
            testcase_ast.walk(new U.TreeWalker(function(node) {
                // unshare start props to retain visit data between iterations
                node.start = JSON.parse(JSON.stringify(node.start));
                node.start._permute = 0;
            }));
            var before_iterations = testcase;
            for (var c = 0; c < max_iterations; ++c) {
                if (verbose && pass == 1 && c % 25 == 0) {
                    log("// reduce test pass " + pass + ", iteration " + c + ": " + testcase.length + " bytes");
                }
                var CHANGED = false;
                var code_ast = testcase_ast.clone(true).transform(tt);
                if (!CHANGED) break;
                try {
                    var code = code_ast.print_to_string(print_options);
                } catch (ex) {
                    // AST is not well formed.
                    // no harm done - just log the error, ignore latest change and continue iterating.
                    log("*** Error generating code from AST.");
                    log(ex.stack);
                    log("*** Discarding permutation and continuing.");
                    continue;
                }
                var diff = test_for_diff(code, minify_options, result_cache, max_timeout);
                if (diff) {
                    if (diff.timed_out) {
                        // can't trust the validity of `code_ast` and `code` when timed out.
                        // no harm done - just ignore latest change and continue iterating.
                    } else if (diff.error) {
                        // something went wrong during minify() - could be malformed AST or genuine bug.
                        // no harm done - just log code & error, ignore latest change and continue iterating.
                        log("*** Error during minification.");
                        log(code);
                        log(diff.error.stack);
                        log("*** Discarding permutation and continuing.");
                    } else if (sandbox.is_error(diff.unminified_result)
                        && sandbox.is_error(diff.minified_result)
                        && diff.unminified_result.name == diff.minified_result.name) {
                        // ignore difference in error messages caused by minification
                        diff_error_message = testcase;
                    } else {
                        // latest permutation is valid, so use it as the basis of new changes
                        testcase_ast = code_ast;
                        testcase = code;
                        differs = diff;
                    }
                }
            }
            if (before_iterations === testcase) break;
            if (verbose) {
                log("// reduce test pass " + pass + ": " + testcase.length + " bytes");
            }
        }
        var beautified = U.minify(testcase, {
            compress: false,
            mangle: false,
            output: function() {
                var options = JSON.parse(JSON.stringify(print_options));
                options.beautify = true;
                options.braces = true;
                options.comments = true;
                return options;
            }(),
        });
        testcase = {
            code: testcase,
        };
        if (!beautified.error) {
            diff = test_for_diff(beautified.code, minify_options, result_cache, max_timeout);
            if (diff && !diff.timed_out && !diff.error) {
                testcase = beautified;
                testcase.code = "// (beautified)\n" + testcase.code;
                differs = diff;
            }
        }
        var lines = [ "" ];
        if (isNaN(max_timeout)) {
            lines.push("// minify error: " + to_comment(differs.minified_result.stack));
        } else {
            var unminified_result = differs.unminified_result;
            var minified_result = differs.minified_result;
            if (trim_trailing_whitespace(unminified_result) == trim_trailing_whitespace(minified_result)) {
                lines.push(
                    "// (stringified)",
                    "// output: " + JSON.stringify(unminified_result),
                    "// minify: " + JSON.stringify(minified_result)
                );
            } else {
                lines.push(
                    "// output: " + to_comment(unminified_result),
                    "// minify: " + to_comment(minified_result)
                );
            }
        }
        lines.push("// options: " + to_comment(minify_options_json));
        testcase.code += lines.join("\n");
        testcase.warnings = warnings;
        return testcase;
    }
};

function to_comment(value) {
    return ("" + value).replace(/\n/g, "\n// ");
}

function trim_trailing_whitespace(value) {
    return ("" + value).replace(/\s+$/, "");
}

function has_exit(fn) {
    var found = false;
    var tw = new U.TreeWalker(function(node) {
        if (found) return found;
        if (node instanceof U.AST_Exit) {
            return found = true;
        }
        if (node instanceof U.AST_Scope && node !== fn) {
            return true; // don't descend into nested functions
        }
    });
    fn.walk(tw);
    return found;
}

function has_loopcontrol(body, loop, label) {
    var found = false;
    var tw = new U.TreeWalker(function(node) {
        if (found) return true;
        if (node instanceof U.AST_LoopControl && this.loopcontrol_target(node) === loop) {
            return found = true;
        }
    });
    if (label instanceof U.AST_LabeledStatement) tw.push(label);
    tw.push(loop);
    body.walk(tw);
    return found;
}

function can_hoist(body) {
    var found = false;
    body.walk(new U.TreeWalker(function(node) {
        if (found) return true;
        if (node instanceof U.AST_NewTarget) return found = true;
        if (node instanceof U.AST_Scope) {
            if (node === body) return;
            return true;
        }
        if (node instanceof U.AST_Super) return found = true;
    }));
    return !found;
}

function is_timed_out(result) {
    return sandbox.is_error(result) && /timed out/.test(result.message);
}

function merge_sequence(array, node) {
    if (node instanceof U.AST_Sequence) {
        array.push.apply(array, node.expressions);
    } else {
        array.push(node);
    }
    return array;
}

function to_sequence(expressions) {
    if (expressions.length == 0) return new U.AST_Number({value: 0, start: {}});
    if (expressions.length == 1) return expressions[0];
    return new U.AST_Sequence({
        expressions: expressions.reduce(merge_sequence, []),
        start: {},
    });
}

function to_statement(node) {
    return U.is_statement(node) ? node : new U.AST_SimpleStatement({
        body: node,
        start: {},
    });
}

function to_statement_init(node) {
    return node instanceof U.AST_Const || node instanceof U.AST_Let ? new U.AST_BlockStatement({
        body: [ node ],
        start: {},
    }) : to_statement(node);
}

function wrap_with_console_log(node) {
    // wrap with console.log()
    return new U.AST_Call({
        expression: new U.AST_Dot({
            expression: new U.AST_SymbolRef({
                name: "console",
                start: {},
            }),
            property: "log",
            start: {},
        }),
        args: [ node ],
        start: {},
    });
}

function run_code(code, toplevel, result_cache, timeout) {
    var key = crypto.createHash("sha1").update(code).digest("base64");
    var value = result_cache[key];
    if (!value) {
        var start = Date.now();
        result_cache[key] = value = {
            result: sandbox.run_code(sandbox.patch_module_statements(code), toplevel, timeout),
            elapsed: Date.now() - start,
        };
    }
    return value;
}

function compare_run_code(code, minify_options, result_cache, max_timeout) {
    var minified = U.minify(code, minify_options);
    if (minified.error) return minified;

    var toplevel = sandbox.has_toplevel(minify_options);
    var unminified = run_code(code, toplevel, result_cache, max_timeout);
    var timeout = Math.min(100 * unminified.elapsed, max_timeout);
    var minified_result = run_code(minified.code, toplevel, result_cache, timeout).result;

    if (sandbox.same_stdout(unminified.result, minified_result)) {
        return is_timed_out(unminified.result) && is_timed_out(minified_result) && {
            timed_out: true,
        };
    }
    return {
        unminified_result: unminified.result,
        minified_result: minified_result,
        elapsed: unminified.elapsed,
    };
}

function test_minify(code, minify_options) {
    var minified = U.minify(code, minify_options);
    return minified.error && {
        minified_result: minified.error,
    };
}