Bug 589664 - Rewrite the JSON parser. r=njn, anticipating more review but getting it in-tree now for simplicity, even if more changes need to be made later
☠☠ backed out by 722ab9ce914e ☠ ☠
authorJeff Walden <jwalden@mit.edu>
Wed, 23 Mar 2011 16:34:53 -0700
changeset 67932 d2f2cac2e9802c29a3628aeebbd77f868d17c0b0
parent 67931 0756cd76cb066d9f7399612873bd21e3e0c7de26
child 67933 722ab9ce914e02776b6c0f74249772ff4afe7261
push idunknown
push userunknown
push dateunknown
reviewersnjn, anticipating
bugs589664
milestone2.2a1pre
Bug 589664 - Rewrite the JSON parser. r=njn, anticipating more review but getting it in-tree now for simplicity, even if more changes need to be made later
js/src/Makefile.in
js/src/js.msg
js/src/jscntxt.h
js/src/jsobj.cpp
js/src/json.cpp
js/src/json.h
js/src/jsonparser.cpp
js/src/jsonparser.h
js/src/jsversion.h
js/src/shell/js.cpp
js/src/tests/ecma_5/JSON/jstests.list
js/src/tests/ecma_5/JSON/parse-number-syntax.js
js/src/tests/ecma_5/JSON/parse-syntax-errors-03.js
js/src/tests/ecma_5/JSON/shell.js
js/src/tests/ecma_5/extensions/jstests.list
js/src/tests/ecma_5/extensions/legacy-JSON.js
--- a/js/src/Makefile.in
+++ b/js/src/Makefile.in
@@ -152,16 +152,17 @@ CPPSRCS		= \
 		jsiter.cpp \
 		jslock.cpp \
 		jslog2.cpp \
 		jsmath.cpp \
 		jsnativestack.cpp \
 		jsnum.cpp \
 		jsobj.cpp \
 		json.cpp \
+		jsonparser.cpp \
 		jsopcode.cpp \
 		jsparse.cpp \
 		jsproxy.cpp \
 		jsprf.cpp \
 		jsprobes.cpp \
 		jspropertycache.cpp \
 		jspropertytree.cpp \
 		jsreflect.cpp \
@@ -213,16 +214,17 @@ INSTALLED_HEADERS = \
 		jsiter.h \
 		jslock.h \
 		jslong.h \
 		jsmath.h \
 		jsnum.h \
 		jsobj.h \
 		jsobjinlines.h \
 		json.h \
+		jsonparser.h \
 		jsopcode.tbl \
 		jsopcode.h \
 		jsopcodeinlines.h \
 		jsotypes.h \
 		jsparse.h \
 		jsproxy.h \
 		jsprf.h \
 		jsprobes.h \
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -302,17 +302,17 @@ MSG_DEF(JSMSG_NON_LIST_XML_METHOD,    21
 MSG_DEF(JSMSG_BAD_DELETE_OPERAND,     220, 0, JSEXN_REFERENCEERR, "invalid delete operand")
 MSG_DEF(JSMSG_BAD_INCOP_OPERAND,      221, 0, JSEXN_REFERENCEERR, "invalid increment/decrement operand")
 MSG_DEF(JSMSG_UNEXPECTED_TYPE,        222, 2, JSEXN_TYPEERR, "{0} is {1}")
 MSG_DEF(JSMSG_LET_DECL_NOT_IN_BLOCK,  223, 0, JSEXN_SYNTAXERR, "let declaration not directly within block")
 MSG_DEF(JSMSG_BAD_OBJECT_INIT,        224, 0, JSEXN_SYNTAXERR, "invalid object initializer")
 MSG_DEF(JSMSG_CANT_SET_ARRAY_ATTRS,   225, 0, JSEXN_INTERNALERR, "can't set attributes on indexed array properties")
 MSG_DEF(JSMSG_EVAL_ARITY,             226, 0, JSEXN_TYPEERR, "eval accepts only one parameter")
 MSG_DEF(JSMSG_MISSING_FUN_ARG,        227, 2, JSEXN_TYPEERR, "missing argument {0} when calling function {1}")
-MSG_DEF(JSMSG_JSON_BAD_PARSE,         228, 0, JSEXN_SYNTAXERR, "JSON.parse")
+MSG_DEF(JSMSG_JSON_BAD_PARSE,         228, 1, JSEXN_SYNTAXERR, "JSON.parse: {0}")
 MSG_DEF(JSMSG_JSON_BAD_STRINGIFY,     229, 0, JSEXN_ERR, "JSON.stringify")
 MSG_DEF(JSMSG_XDR_CLOSURE_WRAPPER,    230, 1, JSEXN_INTERNALERR, "can't XDR closure wrapper for function {0}")
 MSG_DEF(JSMSG_NOT_NONNULL_OBJECT,     231, 0, JSEXN_TYPEERR, "value is not a non-null object")
 MSG_DEF(JSMSG_DEPRECATED_OCTAL,       232, 0, JSEXN_SYNTAXERR, "octal literals and octal escape sequences are deprecated")
 MSG_DEF(JSMSG_STRICT_CODE_WITH,       233, 0, JSEXN_SYNTAXERR, "strict mode code may not contain 'with' statements")
 MSG_DEF(JSMSG_DUPLICATE_PROPERTY,     234, 1, JSEXN_SYNTAXERR, "property name {0} appears more than once in object literal")
 MSG_DEF(JSMSG_DEPRECATED_DELETE_OPERAND, 235, 0, JSEXN_SYNTAXERR, "applying the 'delete' operator to an unqualified name is deprecated")
 MSG_DEF(JSMSG_DEPRECATED_ASSIGN,      236, 1, JSEXN_SYNTAXERR, "assignment to {0} is deprecated")
--- a/js/src/jscntxt.h
+++ b/js/src/jscntxt.h
@@ -3211,16 +3211,17 @@ class AutoVectorRooter : protected AutoG
     size_t length() const { return vector.length(); }
 
     bool append(const T &v) { return vector.append(v); }
 
     /* For use when space has already been reserved. */
     void infallibleAppend(const T &v) { vector.infallibleAppend(v); }
 
     void popBack() { vector.popBack(); }
+    T popCopy() { return vector.popCopy(); }
 
     bool growBy(size_t inc) {
         size_t oldLength = vector.length();
         if (!vector.growByUninitialized(inc))
             return false;
         MakeRangeGCSafe(vector.begin() + oldLength, vector.end());
         return true;
     }
--- a/js/src/jsobj.cpp
+++ b/js/src/jsobj.cpp
@@ -61,16 +61,17 @@
 #include "jsemit.h"
 #include "jsfun.h"
 #include "jsgc.h"
 #include "jsinterp.h"
 #include "jsiter.h"
 #include "jslock.h"
 #include "jsnum.h"
 #include "jsobj.h"
+#include "jsonparser.h"
 #include "jsopcode.h"
 #include "jsparse.h"
 #include "jsproxy.h"
 #include "jsscope.h"
 #include "jsscript.h"
 #include "jsstaticcheck.h"
 #include "jsstdint.h"
 #include "jsstr.h"
@@ -1210,24 +1211,33 @@ EvalKernel(JSContext *cx, uintN argc, Va
 
     /*
      * If the eval string starts with '(' and ends with ')', it may be JSON.
      * Try the JSON parser first because it's much faster.  If the eval string
      * isn't JSON, JSON parsing will probably fail quickly, so little time
      * will be lost.
      */
     if (length > 2 && chars[0] == '(' && chars[length - 1] == ')') {
+#if USE_OLD_AND_BUSTED_JSON_PARSER
         JSONParser *jp = js_BeginJSONParse(cx, vp, /* suppressErrors = */true);
         if (jp != NULL) {
             /* Run JSON-parser on string inside ( and ). */
             bool ok = js_ConsumeJSONText(cx, jp, chars + 1, length - 2);
             ok &= js_FinishJSONParse(cx, jp, NullValue());
             if (ok)
                 return true;
         }
+#else
+        JSONSourceParser parser(cx, chars + 1, length - 2, JSONSourceParser::StrictJSON,
+                                JSONSourceParser::NoError);
+        if (!parser.parse(vp))
+            return false;
+        if (!vp->isUndefined())
+            return true;
+#endif
     }
 
     /*
      * Direct calls to eval are supposed to see the caller's |this|. If we
      * haven't wrapped that yet, do so now, before we make a copy of it for
      * the eval code to use.
      */
     if (evalType == DIRECT_EVAL && !caller->computeThis(cx))
--- a/js/src/json.cpp
+++ b/js/src/json.cpp
@@ -46,16 +46,17 @@
 #include "jsatom.h"
 #include "jsbool.h"
 #include "jscntxt.h"
 #include "jsfun.h"
 #include "jsinterp.h"
 #include "jsiter.h"
 #include "jsnum.h"
 #include "jsobj.h"
+#include "jsonparser.h"
 #include "jsprf.h"
 #include "jsscan.h"
 #include "jsstr.h"
 #include "jstypes.h"
 #include "jsstdint.h"
 #include "jsutil.h"
 #include "jsxml.h"
 #include "jsvector.h"
@@ -110,35 +111,27 @@ Class js_JSONClass = {
     ConvertStub
 };
 
 JSBool
 js_json_parse(JSContext *cx, uintN argc, Value *vp)
 {
     JSString *s = NULL;
     Value *argv = vp + 2;
-    AutoValueRooter reviver(cx);
+    Value reviver = UndefinedValue();
 
-    if (!JS_ConvertArguments(cx, argc, Jsvalify(argv), "S / v", &s, reviver.addr()))
+    if (!JS_ConvertArguments(cx, argc, Jsvalify(argv), "S / v", &s, &reviver))
         return JS_FALSE;
 
     JSLinearString *linearStr = s->ensureLinear(cx);
     if (!linearStr)
         return JS_FALSE;
+    JS::Anchor<JSString *> anchor(linearStr);
 
-    JSONParser *jp = js_BeginJSONParse(cx, vp);
-    JSBool ok = jp != NULL;
-    if (ok) {
-        const jschar *chars = linearStr->chars();
-        size_t length = linearStr->length();
-        ok = js_ConsumeJSONText(cx, jp, chars, length);
-        ok &= !!js_FinishJSONParse(cx, jp, reviver.value());
-    }
-
-    return ok;
+    return ParseJSONWithReviver(cx, linearStr->chars(), linearStr->length(), reviver, vp);
 }
 
 JSBool
 js_json_stringify(JSContext *cx, uintN argc, Value *vp)
 {
     Value *argv = vp + 2;
     AutoValueRooter space(cx);
     AutoObjectRooter replacer(cx);
@@ -763,17 +756,17 @@ Walk(JSContext *cx, jsid id, JSObject *h
     *vp = reviverResult;
     return true;
 }
 
 static JSBool
 JSONParseError(JSONParser *jp, JSContext *cx)
 {
     if (!jp->suppressErrors)
-        JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_JSON_BAD_PARSE);
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_JSON_BAD_PARSE, "syntax error");
     return JS_FALSE;
 }
 
 static bool
 Revive(JSContext *cx, const Value &reviver, Value *vp)
 {
 
     JSObject *obj = NewBuiltinClassInstance(cx, &js_ObjectClass);
@@ -846,37 +839,49 @@ js_FinishJSONParse(JSContext *cx, JSONPa
 
     bool ok = *jp->statep == JSON_PARSE_STATE_FINISHED;
     Value *vp = jp->rootVal;
 
     if (!early_ok) {
         ok = false;
     } else if (!ok) {
         JSONParseError(jp, cx);
-    } else if (reviver.isObject() && reviver.toObject().isCallable()) {
+    } else if (js_IsCallable(reviver)) {
         ok = Revive(cx, reviver, vp);
     }
 
     cx->delete_(jp);
 
     return ok;
 }
 
 namespace js {
 
 JSBool
-ParseJSONWithReviver(JSContext *cx, const jschar *chars, uint32 length, const Value &reviver,
+ParseJSONWithReviver(JSContext *cx, const jschar *chars, size_t length, const Value &reviver,
                      Value *vp, DecodingMode decodingMode /* = STRICT */)
 {
+#if USE_OLD_AND_BUSTED_JSON_PARSER
     JSONParser *jp = js_BeginJSONParse(cx, vp);
     if (!jp)
         return false;
     JSBool ok = js_ConsumeJSONText(cx, jp, chars, length, decodingMode);
     ok &= !!js_FinishJSONParse(cx, jp, reviver);
     return ok;
+#else
+    JSONSourceParser parser(cx, chars, length,
+                            decodingMode == STRICT
+                            ? JSONSourceParser::StrictJSON
+                            : JSONSourceParser::LegacyJSON);
+    if (!parser.parse(vp))
+        return false;
+    if (js_IsCallable(reviver))
+        return Revive(cx, reviver, vp);
+    return true;
+#endif
 }
 
 } /* namespace js */
 
 static JSBool
 PushState(JSContext *cx, JSONParser *jp, JSONParserState state)
 {
     if (*jp->statep == JSON_PARSE_STATE_FINISHED) {
--- a/js/src/json.h
+++ b/js/src/json.h
@@ -132,14 +132,14 @@ js_ConsumeJSONText(JSContext *cx, JSONPa
                    DecodingMode decodingMode = STRICT);
 
 extern bool
 js_FinishJSONParse(JSContext *cx, JSONParser *jp, const js::Value &reviver);
 
 namespace js {
 
 extern JS_FRIEND_API(JSBool)
-ParseJSONWithReviver(JSContext *cx, const jschar *chars, uint32 length, const Value &filter,
+ParseJSONWithReviver(JSContext *cx, const jschar *chars, size_t length, const Value &filter,
                      Value *vp, DecodingMode decodingMode = STRICT);
 
 } /* namespace js */
 
 #endif /* json_h___ */
new file mode 100644
--- /dev/null
+++ b/js/src/jsonparser.cpp
@@ -0,0 +1,695 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sw=4 et tw=99:
+ *
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is SpiderMonkey JSON.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jeff Walden <jwalden+code@mit.edu> (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#include "jsarray.h"
+#include "jsnum.h"
+#include "jsonparser.h"
+
+#include "jsobjinlines.h"
+#include "jsstrinlines.h"
+
+using namespace js;
+
+void
+JSONSourceParser::error(const char *msg)
+{
+    if (errorHandling == RaiseError)
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_JSON_BAD_PARSE, msg);
+}
+
+bool
+JSONSourceParser::errorReturn()
+{
+    return errorHandling == NoError;
+}
+
+template<JSONSourceParser::StringType ST>
+JSONSourceParser::Token
+JSONSourceParser::readString()
+{
+    JS_ASSERT(current < end);
+    JS_ASSERT(*current == '"');
+
+    /*
+     * JSONString:
+     *   /^"([^\u0000-\u001F"\\]|\\(["/\\bfnrt]|u[0-9a-fA-F]{4}))*"$/
+     */
+
+    if (++current == end) {
+        error("unterminated string literal");
+        return token(Error);
+    }
+
+    /*
+     * Optimization: if the source contains no escaped characters, create the
+     * string directly from the source text.
+     */
+    RangeCheckedPointer<const jschar> start = current;
+    for (; current < end; current++) {
+        if (*current == '"') {
+            size_t length = current - start;
+            current++;
+            JSFlatString *str = (ST == JSONSourceParser::PropertyName)
+                                ? js_AtomizeChars(cx, start, length, 0)
+                                : js_NewStringCopyN(cx, start, length);
+            if (!str)
+                return token(OOM);
+            return stringToken(str);
+        }
+
+        if (*current == '\\')
+            break;
+
+        if (*current <= 0x001F) {
+            error("bad control character in string literal");
+            return token(Error);
+        }
+    }
+
+    /*
+     * Slow case: string contains escaped characters.  Copy a maximal sequence
+     * of unescaped characters into a temporary buffer, then an escaped
+     * character, and repeat until the entire string is consumed.
+     */
+    StringBuffer buffer(cx);
+    do {
+        if (start < current && !buffer.append(start, current))
+            return token(OOM);
+
+        if (current >= end)
+            break;
+
+        jschar c = *current++;
+        if (c == '"') {
+            JSFlatString *str = (ST == JSONSourceParser::PropertyName)
+                                ? buffer.finishAtom()
+                                : buffer.finishString();
+            if (!str)
+                return token(OOM);
+            return stringToken(str);
+        }
+
+        if (c != '\\') {
+            error("bad character in string literal");
+            return token(Error);
+        }
+
+        if (current >= end)
+            break;
+
+        switch (*current++) {
+          case '"':  c = '"';  break;
+          case '/':  c = '/';  break;
+          case '\\': c = '\\'; break;
+          case 'b':  c = '\b'; break;
+          case 'f':  c = '\f'; break;
+          case 'n':  c = '\n'; break;
+          case 'r':  c = '\r'; break;
+          case 't':  c = '\t'; break;
+
+          case 'u':
+            if (end - current < 4) {
+                error("bad Unicode escape");
+                return token(Error);
+            }
+            if (JS7_ISHEX(current[0]) &&
+                JS7_ISHEX(current[1]) &&
+                JS7_ISHEX(current[2]) &&
+                JS7_ISHEX(current[3]))
+            {
+                c = (JS7_UNHEX(current[0]) << 12)
+                  | (JS7_UNHEX(current[1]) << 8)
+                  | (JS7_UNHEX(current[2]) << 4)
+                  | (JS7_UNHEX(current[3]));
+                current += 4;
+                break;
+            }
+            /* FALL THROUGH */
+
+          default:
+            error("bad escaped character");
+            return token(Error);
+        }
+        if (!buffer.append(c))
+            return token(OOM);
+
+        start = current;
+        for (; current < end; current++) {
+            if (*current == '"' || *current == '\\' || *current <= 0x001F)
+                break;
+        }
+    } while (current < end);
+
+    error("unterminated string");
+    return token(Error);
+}
+
+JSONSourceParser::Token
+JSONSourceParser::readNumber()
+{
+    JS_ASSERT(current < end);
+    JS_ASSERT(JS7_ISDEC(*current) || *current == '-');
+
+    /*
+     * JSONNumber:
+     *   /^-?(0|[1-9][0-9]+)(\.[0-9]+)?([eE][\+\-]?[0-9]+)?$/
+     */
+
+    bool negative = *current == '-';
+
+    /* -? */
+    if (negative && ++current == end) {
+        error("no number after minus sign");
+        return token(Error);
+    }
+
+    const RangeCheckedPointer<const jschar> digitStart = current;
+
+    /* 0|[1-9][0-9]+ */
+    if (!JS7_ISDEC(*current)) {
+        error("unexpected non-digit");
+        return token(Error);
+    }
+    if (*current++ != '0') {
+        for (; current < end; current++) {
+            if (!JS7_ISDEC(*current))
+                break;
+        }
+    }
+
+    /* Fast path: no fractional or exponent part. */
+    if (current == end || (*current != '.' && *current != 'e' && *current != 'E')) {
+        const jschar *dummy;
+        jsdouble d;
+        if (!GetPrefixInteger(cx, digitStart, current, 10, &dummy, &d))
+            return token(OOM);
+        JS_ASSERT(current == dummy);
+        return numberToken(negative ? -d : d);
+    }
+
+    /* (\.[0-9]+)? */
+    if (current < end && *current == '.') {
+        if (++current == end) {
+            error("missing digits after decimal point");
+            return token(Error);
+        }
+        if (!JS7_ISDEC(*current)) {
+            error("unterminated fractional number");
+            return token(Error);
+        }
+        while (++current < end) {
+            if (!JS7_ISDEC(*current))
+                break;
+        }
+    }
+
+    /* ([eE][\+\-]?[0-9]+)? */
+    if (current < end && (*current == 'e' || *current == 'E')) {
+        if (++current == end) {
+            error("missing digits after exponent indicator");
+            return token(Error);
+        }
+        if (*current == '+' || *current == '-') {
+            if (++current == end) {
+                error("missing digits after exponent sign");
+                return token(Error);
+            }
+        }
+        if (!JS7_ISDEC(*current)) {
+            error("exponent part is missing a number");
+            return token(Error);
+        }
+        while (++current < end) {
+            if (!JS7_ISDEC(*current))
+                break;
+        }
+    }
+
+    jsdouble d;
+    const jschar *finish;
+    if (!js_strtod(cx, digitStart, current, &finish, &d))
+        return token(OOM);
+    JS_ASSERT(current == finish);
+    return numberToken(negative ? -d : d);
+}
+
+static inline bool
+IsJSONWhitespace(jschar c)
+{
+    return c == '\t' || c == '\r' || c == '\n' || c == ' ';
+}
+
+JSONSourceParser::Token
+JSONSourceParser::advance()
+{
+    while (current < end && IsJSONWhitespace(*current))
+        current++;
+    if (current >= end) {
+        error("unexpected end of data");
+        return token(Error);
+    }
+
+    switch (*current) {
+      case '"':
+        return readString<LiteralValue>();
+
+      case '-':
+      case '0':
+      case '1':
+      case '2':
+      case '3':
+      case '4':
+      case '5':
+      case '6':
+      case '7':
+      case '8':
+      case '9':
+        return readNumber();
+
+      case 't':
+        if (end - current < 4 || current[1] != 'r' || current[2] != 'u' || current[3] != 'e') {
+            error("unexpected keyword");
+            return token(Error);
+        }
+        current += 4;
+        return token(True);
+
+      case 'f':
+        if (end - current < 5 ||
+            current[1] != 'a' || current[2] != 'l' || current[3] != 's' || current[4] != 'e')
+        {
+            error("unexpected keyword");
+            return token(Error);
+        }
+        current += 5;
+        return token(False);
+
+      case 'n':
+        if (end - current < 4 || current[1] != 'u' || current[2] != 'l' || current[3] != 'l') {
+            error("unexpected keyword");
+            return token(Error);
+        }
+        current += 4;
+        return token(Null);
+
+      case '[':
+        current++;
+        return token(ArrayOpen);
+      case ']':
+        current++;
+        return token(ArrayClose);
+
+      case '{':
+        current++;
+        return token(ObjectOpen);
+      case '}':
+        current++;
+        return token(ObjectClose);
+
+      case ',':
+        current++;
+        return token(Comma);
+
+      case ':':
+        current++;
+        return token(Colon);
+
+      default:
+        error("unexpected character");
+        return token(Error);
+    }
+}
+
+JSONSourceParser::Token
+JSONSourceParser::advanceAfterObjectOpen()
+{
+    JS_ASSERT(current[-1] == '{');
+
+    while (current < end && IsJSONWhitespace(*current))
+        current++;
+    if (current >= end) {
+        error("end of data while reading object contents");
+        return token(Error);
+    }
+
+    if (*current == '"')
+        return readString<PropertyName>();
+
+    if (*current == '}') {
+        current++;
+        return token(ObjectClose);
+    }
+
+    error("expected property name or '}'");
+    return token(Error);
+}
+
+static inline void
+AssertPastValue(const jschar *current)
+{
+    /*
+     * We're past an arbitrary JSON value, so the previous character is
+     * *somewhat* constrained, even if this assertion is pretty broad.  Don't
+     * knock it till you tried it: this assertion *did* catch a bug once.
+     */
+    JS_ASSERT((current[-1] == 'l' &&
+               current[-2] == 'l' &&
+               current[-3] == 'u' &&
+               current[-4] == 'n') ||
+              (current[-1] == 'e' &&
+               current[-2] == 'u' &&
+               current[-3] == 'r' &&
+               current[-4] == 't') ||
+              (current[-1] == 'e' &&
+               current[-2] == 's' &&
+               current[-3] == 'l' &&
+               current[-4] == 'a' &&
+               current[-5] == 'f') ||
+              current[-1] == '}' ||
+              current[-1] == ']' ||
+              current[-1] == '"' ||
+              JS7_ISDEC(current[-1]));
+}
+
+JSONSourceParser::Token
+JSONSourceParser::advanceAfterArrayElement()
+{
+    AssertPastValue(current);
+
+    while (current < end && IsJSONWhitespace(*current))
+        current++;
+    if (current >= end) {
+        error("end of data when ',' or ']' was expected");
+        return token(Error);
+    }
+
+    if (*current == ',') {
+        current++;
+        return token(Comma);
+    }
+
+    if (*current == ']') {
+        current++;
+        return token(ArrayClose);
+    }
+
+    error("expected ',' or ']' after array element");
+    return token(Error);
+}
+
+JSONSourceParser::Token
+JSONSourceParser::advancePropertyName()
+{
+    JS_ASSERT(current[-1] == ',');
+
+    while (current < end && IsJSONWhitespace(*current))
+        current++;
+    if (current >= end) {
+        error("end of data when property name was expected");
+        return token(Error);
+    }
+
+    if (*current == '"')
+        return readString<PropertyName>();
+
+    if (parsingMode == LegacyJSON && *current == '}') {
+        /*
+         * Previous JSON parsing accepted trailing commas in non-empty object
+         * syntax, and some users depend on this.  (Specifically, Places data
+         * serialization in versions of Firefox before 4.0.  We can remove this
+         * mode when profile upgrades from 3.6 become unsupported.)  Permit
+         * such trailing commas only when legacy parsing is specifically
+         * requested.
+         */
+        current++;
+        return token(ObjectClose);
+    }
+
+    error("expected double-quoted property name");
+    return token(Error);
+}
+
+JSONSourceParser::Token
+JSONSourceParser::advancePropertyColon()
+{
+    JS_ASSERT(current[-1] == '"');
+
+    while (current < end && IsJSONWhitespace(*current))
+        current++;
+    if (current >= end) {
+        error("end of data after property name when ':' was expected");
+        return token(Error);
+    }
+
+    if (*current == ':') {
+        current++;
+        return token(Colon);
+    }
+
+    error("expected ':' after property name in object");
+    return token(Error);
+}
+
+JSONSourceParser::Token
+JSONSourceParser::advanceAfterProperty()
+{
+    AssertPastValue(current);
+
+    while (current < end && IsJSONWhitespace(*current))
+        current++;
+    if (current >= end) {
+        error("end of data after property value in object");
+        return token(Error);
+    }
+
+    if (*current == ',') {
+        current++;
+        return token(Comma);
+    }
+
+    if (*current == '}') {
+        current++;
+        return token(ObjectClose);
+    }
+
+    error("expected ',' or '}' after property value in object");
+    return token(Error);
+}
+
+/*
+ * This enum is local to JSONSourceParser::parse, below, but ISO C++98 doesn't
+ * allow templates to depend on local types.  Boo-urns!
+ */
+enum ParserState { FinishArrayElement, FinishObjectMember, JSONValue };
+
+bool
+JSONSourceParser::parse(Value *vp)
+{
+    Vector<ParserState> stateStack(cx);
+    AutoValueVector valueStack(cx);
+
+    *vp = UndefinedValue();
+
+    Token token;
+    ParserState state = JSONValue;
+    while (true) {
+        switch (state) {
+          case FinishObjectMember: {
+            Value v = valueStack.popCopy();
+            /*
+             * NB: Relies on js_DefineNativeProperty performing
+             *     js_CheckForStringIndex.
+             */
+            jsid propid = ATOM_TO_JSID(&valueStack.popCopy().toString()->asAtom());
+            if (!js_DefineNativeProperty(cx, &valueStack.back().toObject(), propid, v,
+                                         PropertyStub, StrictPropertyStub, JSPROP_ENUMERATE,
+                                         0, 0, NULL))
+            {
+                return false;
+            }
+            token = advanceAfterProperty();
+            if (token == ObjectClose)
+                break;
+            if (token != Comma) {
+                if (token == OOM)
+                    return false;
+                if (token != Error)
+                    error("expected ',' or '}' after property-value pair in object literal");
+                return errorReturn();
+            }
+            token = advancePropertyName();
+            /* FALL THROUGH */
+          }
+
+          JSONMember:
+            if (token == String) {
+                if (!valueStack.append(atomValue()))
+                    return false;
+                token = advancePropertyColon();
+                if (token != Colon) {
+                    JS_ASSERT(token == Error);
+                    return errorReturn();
+                }
+                if (!stateStack.append(FinishObjectMember))
+                    return false;
+                goto JSONValue;
+            }
+            if (token == ObjectClose) {
+                JS_ASSERT(state == FinishObjectMember);
+                JS_ASSERT(parsingMode == LegacyJSON);
+                break;
+            }
+            if (token == OOM)
+                return false;
+            if (token != Error)
+                error("property names must be double-quoted strings");
+            return errorReturn();
+
+          case FinishArrayElement: {
+            Value v = valueStack.popCopy();
+            if (!js_ArrayCompPush(cx, &valueStack.back().toObject(), v))
+                return false;
+            token = advanceAfterArrayElement();
+            if (token == Comma) {
+                if (!stateStack.append(FinishArrayElement))
+                    return false;
+                goto JSONValue;
+            }
+            if (token == ArrayClose)
+                break;
+            JS_ASSERT(token == Error);
+            return errorReturn();
+          }
+
+          JSONValue:
+          case JSONValue:
+            token = advance();
+          JSONValueSwitch:
+            switch (token) {
+              case String:
+              case Number:
+                if (!valueStack.append(token == String ? stringValue() : numberValue()))
+                    return false;
+                break;
+              case True:
+                if (!valueStack.append(BooleanValue(true)))
+                    return false;
+                break;
+              case False:
+                if (!valueStack.append(BooleanValue(false)))
+                    return false;
+                break;
+              case Null:
+                if (!valueStack.append(NullValue()))
+                    return false;
+                break;
+
+              case ArrayOpen: {
+                JSObject *obj = NewDenseEmptyArray(cx);
+                if (!obj || !valueStack.append(ObjectValue(*obj)))
+                    return false;
+                token = advance();
+                if (token == ArrayClose)
+                    break;
+                if (!stateStack.append(FinishArrayElement))
+                    return false;
+                goto JSONValueSwitch;
+              }
+
+              case ObjectOpen: {
+                JSObject *obj = NewBuiltinClassInstance(cx, &js_ObjectClass);
+                if (!obj || !valueStack.append(ObjectValue(*obj)))
+                    return false;
+                token = advanceAfterObjectOpen();
+                if (token == ObjectClose)
+                    break;
+                goto JSONMember;
+              }
+
+              case ArrayClose:
+                if (parsingMode == LegacyJSON &&
+                    !stateStack.empty() &&
+                    stateStack.back() == FinishArrayElement) {
+                    /*
+                     * Previous JSON parsing accepted trailing commas in
+                     * non-empty array syntax, and some users depend on this.
+                     * (Specifically, Places data serialization in versions of
+                     * Firefox prior to 4.0.  We can remove this mode when
+                     * profile upgrades from 3.6 become unsupported.)  Permit
+                     * such trailing commas only when specifically
+                     * instructed to do so.
+                     */
+                    stateStack.popBack();
+                    break;
+                }
+                /* FALL THROUGH */
+
+              case ObjectClose:
+              case Colon:
+              case Comma:
+                error("unexpected character");
+                return errorReturn();
+
+              case OOM:
+                return false;
+
+              case Error:
+                return errorReturn();
+            }
+            break;
+        }
+
+        if (stateStack.empty())
+            break;
+        state = stateStack.popCopy();
+    }
+
+    for (; current < end; current++) {
+        if (!IsJSONWhitespace(*current)) {
+            error("unexpected non-whitespace character after JSON data");
+            return errorReturn();
+        }
+    }
+
+    JS_ASSERT(end == current);
+    JS_ASSERT(valueStack.length() == 1);
+    *vp = valueStack[0];
+    return true;
+}
new file mode 100644
--- /dev/null
+++ b/js/src/jsonparser.h
@@ -0,0 +1,178 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sw=4 et tw=99:
+ *
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is SpiderMonkey JSON.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jeff Walden <jwalden+code@mit.edu> (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either of the GNU General Public License Version 2 or later (the "GPL"),
+ * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef jsonparser_h___
+#define jsonparser_h___
+
+#include "jscntxt.h"
+#include "jsstr.h"
+#include "jstl.h"
+#include "jsvalue.h"
+
+/*
+ * This class should be JSONParser, but the old JSON parser uses that name, so
+ * until we remove the old parser the class name will be overlong.
+ *
+ * NB: This class must only be used on the stack as it contains a js::Value.
+ */
+class JSONSourceParser
+{
+  public:
+    enum ErrorHandling { RaiseError, NoError };
+    enum ParsingMode { StrictJSON, LegacyJSON };
+
+  private:
+    /* Data members */
+
+    JSContext * const cx;
+    js::RangeCheckedPointer<const jschar> current;
+    const js::RangeCheckedPointer<const jschar> end;
+
+    js::Value v;
+
+    const ParsingMode parsingMode;
+    const ErrorHandling errorHandling;
+
+    enum Token { String, Number, True, False, Null,
+                 ArrayOpen, ArrayClose,
+                 ObjectOpen, ObjectClose,
+                 Colon, Comma,
+                 OOM, Error };
+#ifdef DEBUG
+    Token lastToken;
+#endif
+
+  public:
+    /* Public API */
+
+    /*
+     * Create a parser for the provided JSON data.  The parser will accept
+     * certain legacy, non-JSON syntax if decodingMode is LegacyJSON.
+     * Description of this syntax is deliberately omitted: new code should only
+     * use strict JSON parsing.
+     */
+    JSONSourceParser(JSContext *cx, const jschar *data, size_t length,
+                     ParsingMode parsingMode = StrictJSON,
+                     ErrorHandling errorHandling = RaiseError)
+      : cx(cx),
+        current(data, data, length),
+        end(data + length, data, length),
+        parsingMode(parsingMode),
+        errorHandling(errorHandling)
+#ifdef DEBUG
+      , lastToken(Error)
+#endif
+    {
+        JS_ASSERT(current <= end);
+    }
+
+    /*
+     * Parse the JSON data specified at construction time.  If it parses
+     * successfully, store the prescribed value in *vp and return true.  If an
+     * internal error (e.g. OOM) occurs during parsing, return false.
+     * Otherwise, if invalid input was specifed but no internal error occurred,
+     * behavior depends upon the error handling specified at construction: if
+     * error handling is RaiseError then throw a SyntaxError and return false,
+     * otherwise return true and set *vp to |undefined|.  (JSON syntax can't
+     * represent |undefined|, so the JSON data couldn't have specified it.)
+     */
+    bool parse(js::Value *vp);
+
+  private:
+    js::Value numberValue() const {
+        JS_ASSERT(lastToken == Number);
+        JS_ASSERT(v.isNumber());
+        return v;
+    }
+
+    js::Value stringValue() const {
+        JS_ASSERT(lastToken == String);
+        JS_ASSERT(v.isString());
+        return v;
+    }
+
+    js::Value atomValue() const {
+        js::Value strval = stringValue();
+        JS_ASSERT(strval.toString()->isAtom());
+        return strval;
+    }
+
+    Token token(Token t) {
+        JS_ASSERT(t != String);
+        JS_ASSERT(t != Number);
+#ifdef DEBUG
+        lastToken = t;
+#endif
+        return t;
+    }
+
+    Token stringToken(JSString *str) {
+        this->v = js::StringValue(str);
+#ifdef DEBUG
+        lastToken = String;
+#endif
+        return String;
+    }
+
+    Token numberToken(jsdouble d) {
+        this->v = js::NumberValue(d);
+#ifdef DEBUG
+        lastToken = Number;
+#endif
+        return Number;
+    }
+
+    enum StringType { PropertyName, LiteralValue };
+    template<StringType ST> Token readString();
+
+    Token readNumber();
+
+    Token advance();
+    Token advancePropertyName();
+    Token advancePropertyColon();
+    Token advanceAfterProperty();
+    Token advanceAfterObjectOpen();
+    Token advanceAfterArrayElement();
+
+    void error(const char *msg);
+    bool errorReturn();
+};
+
+#endif /* jsonparser_h___ */
--- a/js/src/jsversion.h
+++ b/js/src/jsversion.h
@@ -214,8 +214,18 @@
 /* Feature-test macro for evolving destructuring support. */
 #define JS_HAS_DESTRUCTURING_SHORTHAND  (JS_HAS_DESTRUCTURING == 2)
 
 /*
  * Feature for Object.prototype.__{define,lookup}{G,S}etter__ legacy support;
  * support likely to be made opt-in at some future time.
  */
 #define OLD_GETTER_SETTER_METHODS       1
+
+/*
+ * Embedders: don't change this: it's a bake-until-ready hack only!
+ *
+ * NB: Changing this value requires adjusting the pass/fail state of a handful
+ *     of tests in ecma_5/JSON/ which the old parser implemented incorrectly.
+ *     Also make sure to rename JSONSourceParser to just JSONParser when the
+ *     old parser is removed completely.
+ */
+#define USE_OLD_AND_BUSTED_JSON_PARSER 0
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -65,16 +65,17 @@
 #include "jsdbgapi.h"
 #include "jsemit.h"
 #include "jsfun.h"
 #include "jsgc.h"
 #include "jsiter.h"
 #include "jslock.h"
 #include "jsnum.h"
 #include "jsobj.h"
+#include "json.h"
 #include "jsparse.h"
 #include "jsreflect.h"
 #include "jsscope.h"
 #include "jsscript.h"
 #include "jstypedarray.h"
 #include "jsxml.h"
 #include "jsperf.h"
 
@@ -4590,16 +4591,33 @@ NewGlobal(JSContext *cx, uintN argc, jsv
     JSObject *global = NewGlobalObject(cx, equalSame ? SAME_COMPARTMENT : NEW_COMPARTMENT);
     if (!global)
         return false;
 
     JS_SET_RVAL(cx, vp, OBJECT_TO_JSVAL(global));
     return true;
 }
 
+static JSBool
+ParseLegacyJSON(JSContext *cx, uintN argc, jsval *vp)
+{
+    if (argc != 1 || !JSVAL_IS_STRING(JS_ARGV(cx, vp)[0])) {
+        JS_ReportErrorNumber(cx, my_GetErrorMessage, NULL, JSSMSG_INVALID_ARGS, "parseLegacyJSON");
+        return false;
+    }
+
+    JSString *str = JSVAL_TO_STRING(JS_ARGV(cx, vp)[0]);
+
+    size_t length;
+    const jschar *chars = JS_GetStringCharsAndLength(cx, str, &length);
+    if (!chars)
+        return false;
+    return js::ParseJSONWithReviver(cx, chars, length, js::NullValue(), js::Valueify(vp), LEGACY);
+}
+
 static JSFunctionSpec shell_functions[] = {
     JS_FN("version",        Version,        0,0),
     JS_FN("revertVersion",  RevertVersion,  0,0),
     JS_FN("options",        Options,        0,0),
     JS_FN("load",           Load,           1,0),
     JS_FN("evaluate",       Evaluate,       1,0),
     JS_FN("run",            Run,            1,0),
     JS_FN("readline",       ReadLine,       0,0),
@@ -4689,16 +4707,17 @@ static JSFunctionSpec shell_functions[] 
     JS_FN("wrap",           Wrap,           1,0),
     JS_FN("serialize",      Serialize,      1,0),
     JS_FN("deserialize",    Deserialize,    1,0),
 #ifdef JS_METHODJIT
     JS_FN("mjitstats",      MJitStats,      0,0),
 #endif
     JS_FN("stringstats",    StringStats,    0,0),
     JS_FN("newGlobal",      NewGlobal,      1,0),
+    JS_FN("parseLegacyJSON",ParseLegacyJSON,1,0),
     JS_FS_END
 };
 
 static const char shell_help_header[] =
 "Command                  Description\n"
 "=======                  ===========\n";
 
 static const char *const shell_help_messages[] = {
@@ -4824,16 +4843,18 @@ static const char *const shell_help_mess
 "deserialize(a)           Deserialize data generated by serialize.",
 #ifdef JS_METHODJIT
 "mjitstats()              Return stats on mjit memory usage.",
 #endif
 "stringstats()            Return stats on string memory usage.",
 "newGlobal(kind)          Return a new global object, in the current\n"
 "                         compartment if kind === 'same-compartment' or in a\n"
 "                         new compartment if kind === 'new-compartment'",
+"parseLegacyJSON(str)     Parse str as legacy JSON, returning the result if the\n"
+"                         parse succeeded and throwing a SyntaxError if not.",
 
 /* Keep these last: see the static assertion below. */
 #ifdef MOZ_PROFILING
 "startProfiling()         Start a profiling session.\n"
 "                         Profiler must be running with programatic sampling",
 "stopProfiling()          Stop a running profiling session\n"
 #endif
 };
--- a/js/src/tests/ecma_5/JSON/jstests.list
+++ b/js/src/tests/ecma_5/JSON/jstests.list
@@ -1,22 +1,29 @@
 url-prefix ../../jsreftest.html?test=ecma_5/JSON/
 script cyclic-stringify.js
 script small-codepoints.js
 script parse.js
 script parse-crockford-01.js
 script parse-primitives.js
 script parse-reviver.js
-fails script parse-octal-syntax-error.js # our JSON.parse wrongly accepts octal numbers
 script parse-syntax-errors-01.js
 script parse-syntax-errors-02.js
 script stringify.js
 script stringify-boxed-primitives.js
 script stringify-call-replacer-once.js
 script stringify-call-toJSON-once.js
 script stringify-dropping-elements.js
 script stringify-gap.js
 script stringify-ignore-noncallable-toJSON.js
 script stringify-primitives.js
 script stringify-replacer.js
 script stringify-replacer-with-array-indexes.js
 script stringify-toJSON-arguments.js
 script trailing-comma.js
+
+# These tests pass with the new parser but fail with the old one.  If you're
+# changing which parser is used, you'll need to change these test declarations
+# accordingly.  (And if you're removing the old parser, re-alphabetize these
+# tests into the list above.)
+script parse-number-syntax.js
+script parse-octal-syntax-error.js
+script parse-syntax-errors-03.js
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_5/JSON/parse-number-syntax.js
@@ -0,0 +1,32 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/licenses/publicdomain/
+
+testJSON('-', true);
+testJSON('+', true);
+testJSON('-f', true);
+testJSON('+f', true);
+testJSON('00', true);
+testJSON('01', true);
+testJSON('1.', true);
+testJSON('1.0e', true);
+testJSON('1.0e+', true);
+testJSON('1.0e-', true);
+testJSON('1.0e+z', true);
+testJSON('1.0e-z', true);
+testJSON('1.0ee', true);
+testJSON('1.e1', true);
+testJSON('1.e+1', true);
+testJSON('1.e-1', true);
+testJSON('.', true);
+testJSON('.1', true);
+testJSON('.1e', true);
+testJSON('.1e1', true);
+testJSON('.1e+1', true);
+testJSON('.1e-1', true);
+
+/******************************************************************************/
+
+if (typeof reportCompare === "function")
+  reportCompare(true, true);
+
+print("Tests complete");
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_5/JSON/parse-syntax-errors-03.js
@@ -0,0 +1,55 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/licenses/publicdomain/
+
+testJSON('[', true);
+testJSON('[1', true);
+testJSON('[1,]', true);
+testJSON('[1,{', true);
+testJSON('[1,}', true);
+testJSON('[1,{]', true);
+testJSON('[1,}]', true);
+testJSON('[1,{"', true);
+testJSON('[1,}"', true);
+testJSON('[1,{"\\', true);
+testJSON('[1,}"\\', true);
+testJSON('[1,"', true);
+testJSON('[1,"\\', true);
+
+testJSON('{', true);
+testJSON('{1', true);
+testJSON('{,', true);
+testJSON('{"', true);
+testJSON('{"\\', true);
+testJSON('{"\\u', true);
+testJSON('{"\\uG', true);
+testJSON('{"\\u0', true);
+testJSON('{"\\u01', true);
+testJSON('{"\\u012', true);
+testJSON('{"\\u0123', true);
+testJSON('{"\\u0123"', true);
+testJSON('{"a"', true);
+testJSON('{"a"}', true);
+testJSON('{"a":', true);
+testJSON('{"a",}', true);
+testJSON('{"a":}', true);
+testJSON('{"a":,}', true);
+testJSON('{"a":5,}', true);
+testJSON('{"a":5,[', true);
+testJSON('{"a":5,"', true);
+testJSON('{"a":5,"', true);
+testJSON('{"a":5,"\\', true);
+testJSON("a[false ]".substring(1, 7) /* "[false" */, true);
+
+testJSON('this', true);
+
+testJSON('[1,{}]', false);
+testJSON('{}', false);
+testJSON('{"a":5}', false);
+testJSON('{"\\u0123":5}', false);
+
+/******************************************************************************/
+
+if (typeof reportCompare === "function")
+  reportCompare(true, true);
+
+print("Tests complete");
--- a/js/src/tests/ecma_5/JSON/shell.js
+++ b/js/src/tests/ecma_5/JSON/shell.js
@@ -1,12 +1,17 @@
 gTestsubsuite='JSON';
 
 function testJSON(str, expectSyntaxError)
 {
+  // Leading and trailing whitespace never affect parsing, so test the string
+  // multiple times with and without whitespace around it as it's easy and can
+  // potentially detect bugs.
+
+  // Try the provided string
   try
   {
     JSON.parse(str);
     reportCompare(false, expectSyntaxError,
                   "string <" + str + "> " +
                   "should" + (expectSyntaxError ? "n't" : "") + " " +
                   "have parsed as JSON");
   }
@@ -21,9 +26,87 @@ function testJSON(str, expectSyntaxError
     else
     {
       reportCompare(true, expectSyntaxError,
                     "string <" + str + "> " +
                     "should" + (expectSyntaxError ? "n't" : "") + " " +
                     "have parsed as JSON, exception: " + e);
     }
   }
+
+  // Now try the provided string with trailing whitespace
+  try
+  {
+    JSON.parse(str + " ");
+    reportCompare(false, expectSyntaxError,
+                  "string <" + str + " > " +
+                  "should" + (expectSyntaxError ? "n't" : "") + " " +
+                  "have parsed as JSON");
+  }
+  catch (e)
+  {
+    if (!(e instanceof SyntaxError))
+    {
+      reportCompare(true, false,
+                    "parsing string <" + str + " > threw a non-SyntaxError " +
+                    "exception: " + e);
+    }
+    else
+    {
+      reportCompare(true, expectSyntaxError,
+                    "string <" + str + " > " +
+                    "should" + (expectSyntaxError ? "n't" : "") + " " +
+                    "have parsed as JSON, exception: " + e);
+    }
+  }
+
+  // Now try the provided string with leading whitespace
+  try
+  {
+    JSON.parse(" " + str);
+    reportCompare(false, expectSyntaxError,
+                  "string < " + str + "> " +
+                  "should" + (expectSyntaxError ? "n't" : "") + " " +
+                  "have parsed as JSON");
+  }
+  catch (e)
+  {
+    if (!(e instanceof SyntaxError))
+    {
+      reportCompare(true, false,
+                    "parsing string < " + str + "> threw a non-SyntaxError " +
+                    "exception: " + e);
+    }
+    else
+    {
+      reportCompare(true, expectSyntaxError,
+                    "string < " + str + "> " +
+                    "should" + (expectSyntaxError ? "n't" : "") + " " +
+                    "have parsed as JSON, exception: " + e);
+    }
+  }
+
+  // Now try the provided string with whitespace surrounding it
+  try
+  {
+    JSON.parse(" " + str + " ");
+    reportCompare(false, expectSyntaxError,
+                  "string < " + str + " > " +
+                  "should" + (expectSyntaxError ? "n't" : "") + " " +
+                  "have parsed as JSON");
+  }
+  catch (e)
+  {
+    if (!(e instanceof SyntaxError))
+    {
+      reportCompare(true, false,
+                    "parsing string < " + str + " > threw a non-SyntaxError " +
+                    "exception: " + e);
+    }
+    else
+    {
+      reportCompare(true, expectSyntaxError,
+                    "string < " + str + " > " +
+                    "should" + (expectSyntaxError ? "n't" : "") + " " +
+                    "have parsed as JSON, exception: " + e);
+    }
+  }
 }
--- a/js/src/tests/ecma_5/extensions/jstests.list
+++ b/js/src/tests/ecma_5/extensions/jstests.list
@@ -9,16 +9,17 @@ script bug352085.js
 script bug472534.js
 script bug496985.js
 script bug566661.js
 skip-if(!xulRuntime.shell) script cross-global-eval-is-indirect.js # needs newGlobal()
 script eval-native-callback-is-indirect.js
 script extension-methods-reject-null-undefined-this.js
 skip-if(!xulRuntime.shell) script function-definition-with.js # needs evaluate()
 script iterator-in-catch.js
+skip-if(!xulRuntime.shell) script legacy-JSON.js # needs parseLegacyJSON
 fails script nested-delete-name-in-evalcode.js # bug 604301, at a minimum
 script proxy-strict.js
 script regress-bug567606.js
 script regress-bug607284.js
 script regress-bug629723.js
 script strict-function-statements.js
 script strict-option-redeclared-parameter.js
 script string-literal-getter-setter-decompilation.js
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_5/extensions/legacy-JSON.js
@@ -0,0 +1,54 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/licenses/publicdomain/
+
+try
+{
+  parseLegacyJSON("[,]");
+  throw new Error("didn't throw");
+}
+catch (e)
+{
+  assertEq(e instanceof SyntaxError, true, "didn't get syntax error, got: " + e);
+}
+
+try
+{
+  parseLegacyJSON("{,}");
+  throw new Error("didn't throw");
+}
+catch (e)
+{
+  assertEq(e instanceof SyntaxError, true, "didn't get syntax error, got: " + e);
+}
+
+assertEq(parseLegacyJSON("[1,]").length, 1);
+assertEq(parseLegacyJSON("[1, ]").length, 1);
+assertEq(parseLegacyJSON("[1 , ]").length, 1);
+assertEq(parseLegacyJSON("[1 ,]").length, 1);
+assertEq(parseLegacyJSON("[1,2,]").length, 2);
+assertEq(parseLegacyJSON("[1,2, ]").length, 2);
+assertEq(parseLegacyJSON("[1,2 , ]").length, 2);
+assertEq(parseLegacyJSON("[1,2 ,]").length, 2);
+
+assertEq(parseLegacyJSON('{"a": 2,}').a, 2);
+assertEq(parseLegacyJSON('{"a": 2, }').a, 2);
+assertEq(parseLegacyJSON('{"a": 2 , }').a, 2);
+assertEq(parseLegacyJSON('{"a": 2 ,}').a, 2);
+
+var obj;
+
+obj = parseLegacyJSON('{"a": 2,"b": 3,}');
+assertEq(obj.a + obj.b, 5);
+obj = parseLegacyJSON('{"a": 2,"b": 3, }');
+assertEq(obj.a + obj.b, 5);
+obj = parseLegacyJSON('{"a": 2,"b": 3 , }');
+assertEq(obj.a + obj.b, 5);
+obj = parseLegacyJSON('{"a": 2,"b": 3 ,}');
+assertEq(obj.a + obj.b, 5);
+
+/******************************************************************************/
+
+if (typeof reportCompare === "function")
+  reportCompare(true, true);
+
+print("Tests complete");