diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 0c8c698b..533be1ec 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -5321,12 +5321,21 @@ (&rest args) (if (= (len args) 0) - js-undefined + (raise (js-new-call SyntaxError (js-args "Unexpected token undefined"))) (let ((st (dict))) (dict-set! st "s" (js-to-string (nth args 0))) (dict-set! st "i" 0) - (js-json-parse-value st))))) + (let + ((result (js-json-parse-value st))) + (begin + (js-json-skip-ws! st) + (if + (< (get st "i") (len (get st "s"))) + (raise + (js-new-call SyntaxError + (js-args (str "Unexpected token at position " (get st "i"))))) + result))))))) (define js-json-skip-ws! @@ -5348,7 +5357,7 @@ (let ((s (get st "s")) (i (get st "i"))) (cond - ((>= i (len s)) (error "JSON: unexpected end")) + ((>= i (len s)) (raise (js-new-call SyntaxError (js-args "JSON: unexpected end")))) ((= (char-at s i) "\"") (js-json-parse-string st)) ((= (char-at s i) "[") (js-json-parse-array st)) ((= (char-at s i) "{") (js-json-parse-object st)) @@ -5380,8 +5389,13 @@ (let ((i (get st "i"))) (cond - ((>= i (len s)) nil) + ((>= i (len s)) + (raise (js-new-call SyntaxError (js-args "JSON: unterminated string")))) ((= (char-at s i) "\"") nil) + ((< (char-code (char-at s i)) 32) + (raise + (js-new-call SyntaxError + (js-args "JSON: control character in string")))) ((= (char-at s i) "\\") (begin (when @@ -5457,7 +5471,7 @@ (js-json-skip-ws! st) (js-json-parse-array-loop st result))) ((= c "]") (dict-set! st "i" (+ (get st "i") 1))) - (else (error "JSON: expected , or ]")))))) + (else (raise (js-new-call SyntaxError (js-args "JSON: expected , or ]")))))))) (define js-json-parse-object @@ -5482,7 +5496,7 @@ (js-json-skip-ws! st) (when (not (= (char-at (get st "s") (get st "i")) ":")) - (error "JSON: expected :")) + (raise (js-new-call SyntaxError (js-args "JSON: expected :")))) (dict-set! st "i" (+ (get st "i") 1)) (let ((v (js-json-parse-value st))) (dict-set! result k v)) (js-json-skip-ws! st) @@ -5494,7 +5508,7 @@ (dict-set! st "i" (+ (get st "i") 1)) (js-json-parse-object-loop st result))) ((= c "}") (dict-set! st "i" (+ (get st "i") 1))) - (else (error "JSON: expected , or }"))))))) + (else (raise (js-new-call SyntaxError (js-args "JSON: expected , or }"))))))))) (define JSON {:stringify js-json-stringify :parse js-json-parse}) diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index ef4f32b2..e7f660c2 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -158,6 +158,8 @@ Each item: implement → tests → update progress. Mark `[x]` when tests green. Append-only record of completed iterations. Loop writes one line per iteration: date, what was done, test count delta. +- 2026-05-10 — **`JSON.parse` raises spec-correct `SyntaxError` instances and rejects malformed input.** Previously `JSON.parse("12 34")` silently returned `12` (no trailing-content check), `JSON.parse('""')` accepted control chars in strings, an unterminated string read off the end, and the inner `(error "JSON: ...")` calls produced generic Errors not `instanceof SyntaxError`. Added: (1) post-value whitespace skip + trailing-content check in `js-json-parse`; (2) control-char rejection (code < 0x20) and unterminated-string check in `js-json-parse-string-loop`; (3) all internal "JSON: ..." errors now `(raise (js-new-call SyntaxError ...))`. Result: built-ins/JSON/parse 7/30 → 25/30 (+18). JSON 26/30. conformance.sh: 148/148. + - 2026-05-10 — **`arguments` object inside functions is now a mutable list.** `js-arguments-build-form` produced `(cons p1 (cons p2 __extra_args__))` which yielded a structurally-shared (immutable) list — `arguments[1] = 7; arguments[1]++` raised "set-nth!: list is immutable". Wrapping the build in `js-list-copy` so each function entry constructs a fresh mutable list. Existing reads (`arguments.length`, `arguments[i]`) unaffected. Result: language/expressions/postfix-increment 14/30 → 15/30. conformance.sh: 148/148. - 2026-05-10 — **`String.prototype.split(undefined)` returns `[wholeString]`; function-expression bodies have spec-correct implicit `undefined` return.** (1) `js-string-method "split"` was calling `js-to-string` on the separator unconditionally, so `"undefinedd".split(undefined)` produced `["", "d"]` (split by `"undefined"`); also `limit=0` returned the whole-string list instead of `[]`. New arms: `undefined` separator → `[s]`, `limit=0` → `[]`, otherwise existing string-split. (2) Function expressions wrapped the body in `(call/cc (fn (__return__) (begin )))` and used the begin's last expression as the implicit return value. So `function F(){ this.x = function(){return 99} }` returned the inner lambda (because `js-set-prop` returns the rhs), and `new F()` saw a callable return and replaced the freshly-allocated `this` with it — so `i.x` was missing. Append `nil` to the begin so the implicit completion is always `:js-undefined`; explicit `return` still works via call/cc as before. Result: built-ins/String/prototype/split 8/30 → 10/30. Constructors with function-valued `this.X` now keep their assignments. conformance.sh: 148/148.