Bug 574132 - Implement rest parameters for JavaScript. r=jorendorff.

This commit is contained in:
Benjamin Peterson 2012-05-23 10:31:35 -05:00
parent 6dcdc8b048
commit a956936ed4
28 changed files with 307 additions and 26 deletions

View File

@ -84,7 +84,7 @@ frontend::CompileScript(JSContext *cx, JSObject *scopeChain, StackFrame *callerF
Probes::compileScriptBegin(filename, lineno);
}
~ProbesManager() { Probes::compileScriptEnd(filename, lineno); }
};
};
ProbesManager probesManager(filename, lineno);
/*
@ -231,6 +231,9 @@ frontend::CompileScript(JSContext *cx, JSObject *scopeChain, StackFrame *callerF
}
#endif
if (!parser.checkForArgumentsAndRest())
return NULL;
/*
* Nowadays the threaded interpreter needs a stop instruction, so we
* do have to emit that here.

View File

@ -5890,6 +5890,19 @@ frontend::EmitTree(JSContext *cx, BytecodeEmitter *bce, ParseNode *pn)
if (!bce->noteClosedArg(pn2))
return JS_FALSE;
}
if (pn2->pn_next == pnlast && bce->sc->fun()->hasRest()) {
/* Fill rest parameter. */
JS_ASSERT(!bce->sc->funArgumentsHasLocalBinding());
bce->switchToProlog();
if (Emit1(cx, bce, JSOP_REST) < 0)
return false;
CheckTypeSet(cx, bce, JSOP_REST);
if (!EmitVarOp(cx, pn2, JSOP_SETARG, bce))
return false;
if (Emit1(cx, bce, JSOP_POP) < 0)
return false;
bce->switchToMain();
}
}
ok = EmitTree(cx, bce, pnlast);
break;

View File

@ -669,22 +669,36 @@ Parser::functionBody(FunctionBodyType type)
}
}
/*
* Even if 'arguments' isn't explicitly mentioned, dynamic name lookup
* forces an 'arguments' binding.
*/
if (tc->sc->bindingsAccessedDynamically() && !tc->sc->bindings.hasBinding(context, arguments)) {
bool hasRest = tc->sc->fun()->hasRest();
BindingKind bindKind = tc->sc->bindings.lookup(context, arguments, NULL);
switch (bindKind) {
case NONE:
/* Functions with rest parameters are free from arguments. */
if (hasRest)
break;
/*
* Even if 'arguments' isn't explicitly mentioned, dynamic name lookup
* forces an 'arguments' binding.
*/
if (!tc->sc->bindingsAccessedDynamically())
break;
if (!tc->sc->bindings.addVariable(context, arguments))
return NULL;
}
/*
* Now that all possible 'arguments' bindings have been added, note whether
* 'arguments' has a local binding and whether it unconditionally needs an
* arguments object.
*/
BindingKind bindKind = tc->sc->bindings.lookup(context, arguments, NULL);
if (bindKind == VARIABLE || bindKind == CONSTANT) {
/* 'arguments' is now bound, so fall through. */
case VARIABLE:
case CONSTANT:
if (hasRest) {
reportErrorNumber(NULL, JSREPORT_ERROR, JSMSG_ARGUMENTS_AND_REST);
return NULL;
}
/*
* Now that all possible 'arguments' bindings have been added, note whether
* 'arguments' has a local binding and whether it unconditionally needs an
* arguments object.
*/
tc->sc->setFunArgumentsHasLocalBinding();
/* Dynamic scope access destroys all hope of optimization. */
@ -705,13 +719,34 @@ Parser::functionBody(FunctionBodyType type)
tc->sc->setFunDefinitelyNeedsArgsObj();
break;
}
}
}
}
break;
case ARGUMENT:
break;
}
return pn;
}
bool
Parser::checkForArgumentsAndRest()
{
JS_ASSERT(!tc->sc->inFunction);
if (callerFrame && callerFrame->isFunctionFrame() && callerFrame->fun()->hasRest()) {
PropertyName *arguments = context->runtime->atomState.argumentsAtom;
for (AtomDefnRange r = tc->lexdeps->all(); !r.empty(); r.popFront()) {
if (r.front().key() == arguments) {
reportErrorNumber(NULL, JSREPORT_ERROR, JSMSG_ARGUMENTS_AND_REST);
return false;
}
}
/* We're not in a function context, so we don't expect any bindings. */
JS_ASSERT(tc->sc->bindings.lookup(context, arguments, NULL) == NONE);
}
return true;
}
// Create a placeholder Definition node for |atom|.
// Nb: unlike most functions that are passed a Parser, this one gets a
// SharedContext passed in separately, because in this case |sc| may not equal
@ -1250,13 +1285,15 @@ LeaveFunction(ParseNode *fn, Parser *parser, PropertyName *funName = NULL,
}
bool
Parser::functionArguments(ParseNode **listp)
Parser::functionArguments(ParseNode **listp, bool &hasRest)
{
if (tokenStream.getToken() != TOK_LP) {
reportErrorNumber(NULL, JSREPORT_ERROR, JSMSG_PAREN_BEFORE_FORMAL);
return false;
}
hasRest = false;
if (!tokenStream.matchToken(TOK_RP)) {
#if JS_HAS_DESTRUCTURING
JSAtom *duplicatedArg = NULL;
@ -1265,6 +1302,10 @@ Parser::functionArguments(ParseNode **listp)
#endif
do {
if (hasRest) {
reportErrorNumber(NULL, JSREPORT_ERROR, JSMSG_PARAMETER_AFTER_REST);
return false;
}
switch (TokenKind tt = tokenStream.getToken()) {
#if JS_HAS_DESTRUCTURING
case TOK_LB:
@ -1326,6 +1367,18 @@ Parser::functionArguments(ParseNode **listp)
}
#endif /* JS_HAS_DESTRUCTURING */
case TOK_TRIPLEDOT:
{
hasRest = true;
tt = tokenStream.getToken();
if (tt != TOK_NAME) {
if (tt != TOK_ERROR)
reportErrorNumber(NULL, JSREPORT_ERROR, JSMSG_NO_REST_NAME);
return false;
}
/* Fall through */
}
case TOK_NAME:
{
RootedVar<PropertyName*> name(context, tokenStream.currentToken().name());
@ -1519,10 +1572,13 @@ Parser::functionDef(HandlePropertyName funName, FunctionType type, FunctionSynta
/* Now parse formal argument list and compute fun->nargs. */
ParseNode *prelude = NULL;
if (!functionArguments(&prelude))
bool hasRest;
if (!functionArguments(&prelude, hasRest))
return NULL;
fun->setArgCount(funsc.bindings.numArgs());
if (hasRest)
fun->setHasRest();
#if JS_HAS_DESTRUCTURING
/*

View File

@ -146,6 +146,8 @@ struct Parser : private AutoGCRooter
enum FunctionBodyType { StatementListBody, ExpressionBody };
ParseNode *functionBody(FunctionBodyType type);
bool checkForArgumentsAndRest();
private:
/*
* JS parsers, from lowest to highest precedence.
@ -208,7 +210,7 @@ struct Parser : private AutoGCRooter
* Additional JS parsers.
*/
enum FunctionType { Getter, Setter, Normal };
bool functionArguments(ParseNode **list);
bool functionArguments(ParseNode **list, bool &hasRest);
ParseNode *functionDef(HandlePropertyName name, FunctionType type, FunctionSyntaxKind kind);

View File

@ -1519,12 +1519,18 @@ TokenStream::getTokenInternal()
numStart = userbuf.addressOfNextRawChar() - 2;
goto decimal_dot;
}
#if JS_HAS_XML_SUPPORT
if (c == '.') {
qc = getCharIgnoreEOL();
if (qc == '.') {
tt = TOK_TRIPLEDOT;
goto out;
}
ungetCharIgnoreEOL(qc);
#if JS_HAS_XML_SUPPORT
tt = TOK_DBLDOT;
goto out;
}
#endif
}
ungetCharIgnoreEOL(c);
tt = TOK_DOT;
goto out;
@ -2157,6 +2163,7 @@ TokenKindToString(TokenKind tt)
case TOK_INC: return "TOK_INC";
case TOK_DEC: return "TOK_DEC";
case TOK_DOT: return "TOK_DOT";
case TOK_TRIPLEDOT: return "TOK_TRIPLEDOT";
case TOK_LB: return "TOK_LB";
case TOK_RB: return "TOK_RB";
case TOK_LC: return "TOK_LC";

View File

@ -47,6 +47,7 @@ enum TokenKind {
TOK_MOD, /* modulus */
TOK_INC, TOK_DEC, /* increment/decrement (++ --) */
TOK_DOT, /* member operator (.) */
TOK_TRIPLEDOT, /* for rest arguments (...) */
TOK_LB, TOK_RB, /* left and right brackets */
TOK_LC, TOK_RC, /* left and right curlies (braces) */
TOK_LP, TOK_RP, /* left and right parentheses */

View File

@ -0,0 +1,9 @@
function f1(...arguments) {
assertEq("1,2,3", arguments.toString());
}
f1(1, 2, 3);
function f2(arguments, ...rest) {
assertEq(arguments, 42);
assertEq("1,2,3", rest.toString());
}
f2(42, 1, 2, 3);

View File

@ -0,0 +1,15 @@
function check(expected, ...rest) {
assertEq(expected.toString(), rest.toString());
}
assertEq(check.length, 1);
check([]);
check(['a', 'b'], 'a', 'b');
check(['a', 'b', 'c', 'd'], 'a', 'b', 'c', 'd');
check.apply(null, [['a', 'b'], 'a', 'b'])
check.call(null, ['a', 'b'], 'a', 'b')
var g = newGlobal('new-compartment');
g.eval("function f(...rest) { return rest; }");
var a = g.f(1, 2, 3);
assertEq(a instanceof g.Array, true);

View File

@ -0,0 +1,17 @@
var g = newGlobal('new-compartment');
g.eval("function f(...x) {}");
var dbg = new Debugger;
var gw = dbg.addDebuggee(g);
var fw = gw.getOwnPropertyDescriptor("f").value;
assertEq(fw.parameterNames.toString(), "x");
var g = newGlobal('new-compartment');
g.eval("function f(...rest) { debugger; }");
var dbg = Debugger(g);
dbg.onDebuggerStatement = function (frame) {
var result = frame.eval("arguments");
assertEq("throw" in result, true);
var result2 = frame.evalWithBindings("exc instanceof SyntaxError", {exc: result.throw});
assertEq(result2.return, true);
};
g.f();

View File

@ -0,0 +1,2 @@
g = (function (...rest) { return rest; });
assertEq(g.toString(), "function (...rest) {\n return rest;\n}");

View File

@ -0,0 +1,8 @@
"use strict";
load(libdir + "asserts.js");
assertThrowsInstanceOf(function () {
eval("(function (...arguments) {})");
}, SyntaxError);
assertThrowsInstanceOf(function () {
eval("(function (...eval) {})");
}, SyntaxError);

View File

@ -0,0 +1,30 @@
load(libdir + "asserts.js");
var ieval = eval;
// Now for a tour of the various ways you can access arguments.
assertThrowsInstanceOf(function () {
ieval("function x(...rest) { arguments; }");
}, SyntaxError)
assertThrowsInstanceOf(function () {
Function("...rest", "arguments;");
}, SyntaxError);
assertThrowsInstanceOf(function (...rest) {
eval("arguments;");
}, SyntaxError);
assertThrowsInstanceOf(function (...rest) {
eval("arguments = 42;");
}, SyntaxError);
function g(...rest) {
assertThrowsInstanceOf(h, Error);
}
function h() {
g.arguments;
}
g();
// eval() is evil, but you can still use it with rest parameters!
function still_use_eval(...rest) {
eval("x = 4");
}
still_use_eval();

View File

@ -0,0 +1,3 @@
h = Function("a", "b", "c", "...rest", "return rest.toString();");
assertEq(h.length, 3);
assertEq(h(1, 2, 3, 4, 5), "4,5");

View File

@ -0,0 +1,12 @@
load(libdir + "asserts.js");
var ieval = eval;
var offenders = [["..."], ["...rest"," x"], ["...rest", "[x]"],
["...rest", "...rest2"]];
for (var arglist of offenders) {
assertThrowsInstanceOf(function () {
ieval("function x(" + arglist.join(", ") + ") {}");
}, SyntaxError);
assertThrowsInstanceOf(function () {
Function.apply(null, arglist.concat("return 0;"));
}, SyntaxError);
}

View File

@ -0,0 +1,7 @@
function f(...rest) {
function nested() {
return arguments.length;
}
return nested;
}
assertEq(f()(1, 2, 3), 3);

View File

@ -0,0 +1,7 @@
function f(...rest) {
function nested () {
return rest;
}
return nested;
}
assertEq(f(1, 2, 3)().toString(), [1, 2, 3].toString());

View File

@ -0,0 +1,9 @@
function f(a, b, c, ...rest) {
assertEq(a, 1);
assertEq(b, undefined);
assertEq(c, undefined);
assertEq(Array.isArray(rest), true);
assertEq(rest.length, 0);
assertEq(Object.getPrototypeOf(rest), Array.prototype);
}
f(1);

View File

@ -345,3 +345,7 @@ MSG_DEF(JSMSG_NOT_ITERABLE, 291, 1, JSEXN_TYPEERR, "{0} is not iterabl
MSG_DEF(JSMSG_QUERY_LINE_WITHOUT_URL, 292, 0, JSEXN_TYPEERR, "findScripts query object has 'line' property, but no 'url' property")
MSG_DEF(JSMSG_QUERY_INNERMOST_WITHOUT_LINE_URL, 293, 0, JSEXN_TYPEERR, "findScripts query object has 'innermost' property without both 'url' and 'line' properties")
MSG_DEF(JSMSG_DEBUG_VARIABLE_NOT_FOUND, 294, 0, JSEXN_TYPEERR, "variable not found in environment")
MSG_DEF(JSMSG_PARAMETER_AFTER_REST, 295, 0, JSEXN_SYNTAXERR, "parameter after rest parameter")
MSG_DEF(JSMSG_NO_REST_NAME, 296, 0, JSEXN_SYNTAXERR, "no parameter name after ...")
MSG_DEF(JSMSG_ARGUMENTS_AND_REST, 297, 0, JSEXN_SYNTAXERR, "'arguments' object may not be used in conjunction with a rest parameter")
MSG_DEF(JSMSG_FUNCTION_ARGUMENTS_AND_REST, 298, 0, JSEXN_ERR, "the 'arguments' property of a function with a rest parameter may not be used")

View File

@ -2192,7 +2192,7 @@ class AutoIdRooter : private AutoGCRooter
#define JSFUN_HEAVYWEIGHT_TEST(f) ((f) & JSFUN_HEAVYWEIGHT)
/* 0x0100 is unused */
#define JSFUN_HAS_REST 0x0100 /* function has a rest (...) parameter */
#define JSFUN_CONSTRUCTOR 0x0200 /* native that can be called as a ctor
without creating a this object */

View File

@ -102,6 +102,10 @@ fun_getProperty(JSContext *cx, HandleObject obj_, HandleId id, Value *vp)
StackFrame *fp = iter.fp();
if (JSID_IS_ATOM(id, cx->runtime->atomState.argumentsAtom)) {
if (fun->hasRest()) {
JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_FUNCTION_ARGUMENTS_AND_REST);
return false;
}
/* Warn if strict about f.arguments or equivalent unqualified uses. */
if (!JS_ReportErrorFlagsAndNumber(cx, JSREPORT_WARNING | JSREPORT_STRICT, js_GetErrorMessage,
NULL, JSMSG_DEPRECATED_USAGE, js_arguments_str)) {
@ -291,7 +295,7 @@ fun_resolve(JSContext *cx, HandleObject obj, HandleId id, unsigned flags,
Value v;
if (JSID_IS_ATOM(id, cx->runtime->atomState.lengthAtom))
v.setInt32(fun->nargs);
v.setInt32(fun->nargs - fun->hasRest());
else
v.setString(fun->atom ? fun->atom : cx->runtime->emptyString);
@ -1003,6 +1007,8 @@ Function(JSContext *cx, unsigned argc, Value *vp)
Bindings bindings(cx);
Bindings::StackRoot bindingsRoot(cx, &bindings);
bool hasRest = false;
const char *filename;
unsigned lineno;
JSPrincipals *originPrincipals;
@ -1092,8 +1098,28 @@ Function(JSContext *cx, unsigned argc, Value *vp)
* Check that it's a name. This also implicitly guards against
* TOK_ERROR, which was already reported.
*/
if (tt != TOK_NAME)
return OnBadFormal(cx, tt);
if (hasRest) {
ReportCompileErrorNumber(cx, &ts, NULL, JSREPORT_ERROR,
JSMSG_PARAMETER_AFTER_REST);
return false;
}
if (tt != TOK_NAME) {
if (tt == TOK_TRIPLEDOT) {
hasRest = true;
tt = ts.getToken();
if (tt != TOK_NAME) {
if (tt != TOK_ERROR)
ReportCompileErrorNumber(cx, &ts, NULL,
JSREPORT_ERROR,
JSMSG_NO_REST_NAME);
return false;
}
}
else {
return OnBadFormal(cx, tt);
}
}
/* Check for a duplicate parameter name. */
RootedVar<PropertyName*> name(cx, ts.currentToken().name());
@ -1156,6 +1182,9 @@ Function(JSContext *cx, unsigned argc, Value *vp)
if (!fun)
return false;
if (hasRest)
fun->setHasRest();
bool ok = frontend::CompileFunctionBody(cx, fun, principals, originPrincipals,
&bindings, chars, length, filename, lineno,
cx->findVersion());

View File

@ -63,6 +63,7 @@ struct JSFunction : public JSObject
} u;
js::HeapPtrAtom atom; /* name for diagnostics and decompiling */
bool hasRest() const { return flags & JSFUN_HAS_REST; }
bool isInterpreted() const { return kind() >= JSFUN_INTERPRETED; }
bool isNative() const { return !isInterpreted(); }
bool isNativeConstructor() const { return flags & JSFUN_CONSTRUCTOR; }
@ -85,6 +86,11 @@ struct JSFunction : public JSObject
this->nargs = nargs;
}
void setHasRest() {
JS_ASSERT(!hasRest());
this->flags |= JSFUN_HAS_REST;
}
/* uint16_t representation bounds number of call object dynamic slots. */
enum { MAX_ARGS_AND_VARS = 2 * ((1U << 16) - 1) };

View File

@ -3642,6 +3642,21 @@ ScriptAnalysis::analyzeTypesBytecode(JSContext *cx, unsigned offset,
pushed[0].addType(cx, Type::MagicArgType());
break;
case JSOP_REST: {
TypeSet *types = script->analysis()->bytecodeTypes(pc);
types->addSubset(cx, &pushed[0]);
if (script->hasGlobal()) {
TypeObject *rest = TypeScript::InitObject(cx, script, pc, JSProto_Array);
if (!rest)
return false;
types->addType(cx, Type::ObjectType(rest));
} else {
types->addType(cx, Type::UnknownType());
}
break;
}
case JSOP_SETPROP: {
jsid id = GetAtomId(cx, script, pc, 0);
poppedTypes(pc, 1)->addSetProperty(cx, script, pc, poppedTypes(pc, 0), id);

View File

@ -1516,7 +1516,6 @@ ADD_EMPTY_CASE(JSOP_UNUSED12)
ADD_EMPTY_CASE(JSOP_UNUSED13)
ADD_EMPTY_CASE(JSOP_UNUSED14)
ADD_EMPTY_CASE(JSOP_UNUSED15)
ADD_EMPTY_CASE(JSOP_UNUSED16)
ADD_EMPTY_CASE(JSOP_UNUSED17)
ADD_EMPTY_CASE(JSOP_UNUSED18)
ADD_EMPTY_CASE(JSOP_UNUSED19)
@ -2814,6 +2813,7 @@ END_VARLEN_CASE
}
BEGIN_CASE(JSOP_ARGUMENTS)
JS_ASSERT(!regs.fp()->fun()->hasRest());
if (script->needsArgsObj()) {
ArgumentsObject *obj = ArgumentsObject::create(cx, regs.fp());
if (!obj)
@ -2824,6 +2824,17 @@ BEGIN_CASE(JSOP_ARGUMENTS)
}
END_CASE(JSOP_ARGUMENTS)
BEGIN_CASE(JSOP_REST)
{
JSObject *rest = regs.fp()->createRestParameter(cx);
if (!rest)
goto error;
PUSH_COPY(ObjectValue(*rest));
if (!SetInitializerObjectType(cx, script, regs.pc, rest))
goto error;
}
END_CASE(JSOP_REST)
BEGIN_CASE(JSOP_CALLALIASEDVAR)
BEGIN_CASE(JSOP_GETALIASEDVAR)
{

View File

@ -5537,6 +5537,8 @@ js_DecompileFunction(JSPrinter *jp)
if (i > 0)
js_puts(jp, ", ");
if (i == unsigned(fun->nargs) - 1 && fun->hasRest())
js_puts(jp, "...");
JSAtom *param = GetArgOrVarAtom(jp, i);
#if JS_HAS_DESTRUCTURING

View File

@ -506,7 +506,8 @@ OPDEF(JSOP_UNUSED24, 220,"unused24", NULL, 1, 0, 0, 0, JOF_BYTE)
OPDEF(JSOP_UNUSED25, 221,"unused25", NULL, 1, 0, 0, 0, JOF_BYTE)
OPDEF(JSOP_UNUSED29, 222,"unused29", NULL, 1, 0, 0, 0, JOF_BYTE)
OPDEF(JSOP_UNUSED30, 223,"unused30", NULL, 1, 0, 0, 0, JOF_BYTE)
OPDEF(JSOP_UNUSED16, 224,"unused16", NULL, 1, 0, 0, 0, JOF_BYTE)
OPDEF(JSOP_REST, 224, "rest", NULL, 1, 0, 1, 0, JOF_BYTE|JOF_TYPESET)
/* Pop the stack, convert to a jsid (int or string), and push back. */
OPDEF(JSOP_TOID, 225, "toid", NULL, 1, 1, 1, 0, JOF_BYTE)

View File

@ -56,6 +56,7 @@ ArgumentsObject *
ArgumentsObject::create(JSContext *cx, uint32_t argc, HandleObject callee)
{
JS_ASSERT(argc <= StackSpace::ARGS_LENGTH_MAX);
JS_ASSERT(!callee->toFunction()->hasRest());
RootedVarObject proto(cx, callee->global().getOrCreateObjectPrototype(cx));
if (!proto)

View File

@ -171,6 +171,15 @@ StackFrame::initFixupFrame(StackFrame *prev, StackFrame::Flags flags, void *ncod
u.nactual = nactual;
}
inline JSObject *
StackFrame::createRestParameter(JSContext *cx)
{
JS_ASSERT(fun()->hasRest());
unsigned nformal = fun()->nargs - 1, nactual = numActualArgs();
unsigned nrest = (nactual > nformal) ? nactual - nformal : 0;
return NewDenseCopiedArray(cx, nrest, actualArgs() + nformal);
}
inline Value &
StackFrame::canonicalActualArg(unsigned i) const
{

View File

@ -502,6 +502,8 @@ class StackFrame
inline void initInlineFrame(JSFunction *fun, StackFrame *prevfp, jsbytecode *prevpc);
inline JSObject *createRestParameter(JSContext *cx);
/*
* Frame slots
*