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
authorJeff Walden <jwalden@mit.edu>
Wed, 23 Mar 2011 16:34:53 -0700
changeset 67934 6c8becdd1574ed5b05167ec2fa8dfccfd610e208
parent 67933 722ab9ce914e02776b6c0f74249772ff4afe7261
child 67935 90fa43dcb844d2f2051a3cdfe9ad00d3268d0ff9
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnjn, anticipating
bugs589664
milestone2.2a1pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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/jstl.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/jstl.h
+++ b/js/src/jstl.h
@@ -597,17 +597,17 @@ class RangeCheckedPointer
         return *this;
     }
 
     RangeCheckedPointer<T> &operator-=(size_t dec) {
         this->operator=<T>(*this - dec);
         return *this;
     }
 
-    T &operator[](intptr_t index) const {
+    T &operator[](int index) const {
         JS_ASSERT(size_t(index > 0 ? index : -index) <= size_t(-1) / sizeof(T));
         return *create(ptr + index);
     }
 
     T &operator*() const {
         return *ptr;
     }
 
--- 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");