diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index df5757ca..0d9c3957 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -1636,6 +1636,257 @@ parseFloat (fn (&rest args) (if (= (len args) 0) 0 (js-to-number (nth args 0))))) +(define + js-json-stringify + (fn + (&rest args) + (if + (= (len args) 0) + js-undefined + (js-json-stringify-value (nth args 0))))) + +(define + js-json-stringify-value + (fn + (v) + (cond + ((= v nil) "null") + ((js-undefined? v) js-undefined) + ((= (type-of v) "boolean") (if v "true" "false")) + ((number? v) (js-number-to-string v)) + ((= (type-of v) "string") (js-json-escape-string v)) + ((list? v) + (let + ((parts (list))) + (for-each + (fn + (x) + (let + ((s (js-json-stringify-value x))) + (if + (js-undefined? s) + (append! parts "null") + (append! parts s)))) + v) + (str "[" (join "," parts) "]"))) + ((dict? v) + (let + ((parts (list))) + (for-each + (fn + (k) + (let + ((val (get v k))) + (let + ((vs (js-json-stringify-value val))) + (if + (not (js-undefined? vs)) + (append! parts (str (js-json-escape-string k) ":" vs)))))) + (keys v)) + (str "{" (join "," parts) "}"))) + (else "null")))) + +(define + js-json-escape-string + (fn (s) (str "\"" (js-json-escape-loop s 0 "") "\""))) + +(define + js-json-escape-loop + (fn + (s i acc) + (if + (>= i (len s)) + acc + (let + ((c (char-at s i))) + (cond + ((= c "\"") (js-json-escape-loop s (+ i 1) (str acc "\\\""))) + ((= c "\\") (js-json-escape-loop s (+ i 1) (str acc "\\\\"))) + ((= c "\n") (js-json-escape-loop s (+ i 1) (str acc "\\n"))) + ((= c "\r") (js-json-escape-loop s (+ i 1) (str acc "\\r"))) + ((= c "\t") (js-json-escape-loop s (+ i 1) (str acc "\\t"))) + (else (js-json-escape-loop s (+ i 1) (str acc c)))))))) + +(define + js-json-parse + (fn + (&rest args) + (if + (= (len args) 0) + js-undefined + (let + ((st (dict))) + (dict-set! st "s" (js-to-string (nth args 0))) + (dict-set! st "i" 0) + (js-json-parse-value st))))) + +(define + js-json-skip-ws! + (fn + (st) + (let + ((s (get st "s")) (i (get st "i"))) + (cond + ((>= i (len s)) nil) + ((or (= (char-at s i) " ") (= (char-at s i) "\t") (= (char-at s i) "\n") (= (char-at s i) "\r")) + (begin (dict-set! st "i" (+ i 1)) (js-json-skip-ws! st))) + (else nil))))) + +(define + js-json-parse-value + (fn + (st) + (js-json-skip-ws! st) + (let + ((s (get st "s")) (i (get st "i"))) + (cond + ((>= i (len s)) (error "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)) + ((= (char-at s i) "t") (begin (dict-set! st "i" (+ i 4)) true)) + ((= (char-at s i) "f") (begin (dict-set! st "i" (+ i 5)) false)) + ((= (char-at s i) "n") (begin (dict-set! st "i" (+ i 4)) nil)) + (else (js-json-parse-number st)))))) + +(define + js-json-parse-string + (fn + (st) + (let + ((s (get st "s"))) + (dict-set! st "i" (+ (get st "i") 1)) + (let + ((buf (list))) + (js-json-parse-string-loop st s buf) + (dict-set! st "i" (+ (get st "i") 1)) + (join "" buf))))) + +(define + js-json-parse-string-loop + (fn + (st s buf) + (let + ((i (get st "i"))) + (cond + ((>= i (len s)) nil) + ((= (char-at s i) "\"") nil) + ((= (char-at s i) "\\") + (begin + (when + (< (+ i 1) (len s)) + (let + ((e (char-at s (+ i 1)))) + (cond + ((= e "n") (append! buf "\n")) + ((= e "t") (append! buf "\t")) + ((= e "r") (append! buf "\r")) + ((= e "\"") (append! buf "\"")) + ((= e "\\") (append! buf "\\")) + ((= e "/") (append! buf "/")) + (else (append! buf e))))) + (dict-set! st "i" (+ i 2)) + (js-json-parse-string-loop st s buf))) + (else + (begin + (append! buf (char-at s i)) + (dict-set! st "i" (+ i 1)) + (js-json-parse-string-loop st s buf))))))) + +(define + js-json-parse-number + (fn + (st) + (let + ((s (get st "s")) (i (get st "i"))) + (let + ((start i)) + (js-json-parse-number-loop st s) + (js-to-number (js-string-slice s start (get st "i"))))))) + +(define + js-json-parse-number-loop + (fn + (st s) + (let + ((i (get st "i"))) + (cond + ((>= i (len s)) nil) + ((or (js-is-digit? (char-at s i)) (= (char-at s i) "-") (= (char-at s i) "+") (= (char-at s i) ".") (= (char-at s i) "e") (= (char-at s i) "E")) + (begin + (dict-set! st "i" (+ i 1)) + (js-json-parse-number-loop st s))) + (else nil))))) + +(define + js-json-parse-array + (fn + (st) + (let + ((result (list))) + (dict-set! st "i" (+ (get st "i") 1)) + (js-json-skip-ws! st) + (cond + ((and (< (get st "i") (len (get st "s"))) (= (char-at (get st "s") (get st "i")) "]")) + (begin (dict-set! st "i" (+ (get st "i") 1)) result)) + (else (begin (js-json-parse-array-loop st result) result)))))) + +(define + js-json-parse-array-loop + (fn + (st result) + (append! result (js-json-parse-value st)) + (js-json-skip-ws! st) + (let + ((c (char-at (get st "s") (get st "i")))) + (cond + ((= c ",") + (begin + (dict-set! st "i" (+ (get st "i") 1)) + (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 ]")))))) + +(define + js-json-parse-object + (fn + (st) + (let + ((result (dict))) + (dict-set! st "i" (+ (get st "i") 1)) + (js-json-skip-ws! st) + (cond + ((and (< (get st "i") (len (get st "s"))) (= (char-at (get st "s") (get st "i")) "}")) + (begin (dict-set! st "i" (+ (get st "i") 1)) result)) + (else (begin (js-json-parse-object-loop st result) result)))))) + +(define + js-json-parse-object-loop + (fn + (st result) + (js-json-skip-ws! st) + (let + ((k (js-json-parse-string st))) + (js-json-skip-ws! st) + (when + (not (= (char-at (get st "s") (get st "i")) ":")) + (error "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) + (let + ((c (char-at (get st "s") (get st "i")))) + (cond + ((= c ",") + (begin + (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 }"))))))) + +(define JSON {:parse js-json-parse :stringify js-json-stringify}) + (define js-promise-flush-callbacks! (fn @@ -2050,4 +2301,4 @@ (str "/" (get rx "source") "/" (get rx "flags"))) (else js-undefined)))) -(define js-global {:isFinite js-global-is-finite :console console :Number Number :Math Math :Array Array :String String :NaN 0 :Infinity inf :isNaN js-global-is-nan :Object Object :undefined js-undefined}) +(define js-global {:isFinite js-global-is-finite :console console :Number Number :parseFloat parseFloat :Math Math :Array Array :String String :NaN 0 :Infinity inf :isNaN js-global-is-nan :Object Object :parseInt parseInt :JSON JSON :undefined js-undefined}) diff --git a/lib/js/test.sh b/lib/js/test.sh index ad5a835d..ed680169 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -993,6 +993,28 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 1904) (eval "(js-eval \"parseFloat('3.14')\")") +;; ── Phase 11.json: JSON.stringify / JSON.parse ──────────────── +(epoch 2000) +(eval "(js-eval \"JSON.stringify(42)\")") +(epoch 2001) +(eval "(js-eval \"JSON.stringify('hi')\")") +(epoch 2002) +(eval "(js-eval \"JSON.stringify([1,2,3])\")") +(epoch 2003) +(eval "(js-eval \"JSON.stringify({a:1})\")") +(epoch 2004) +(eval "(js-eval \"JSON.stringify(true)\")") +(epoch 2005) +(eval "(js-eval \"JSON.parse('42')\")") +(epoch 2006) +(eval "(js-eval \"JSON.parse('true')\")") +(epoch 2007) +(eval "(js-eval \"JSON.parse('\\\"hello\\\"')\")") +(epoch 2008) +(eval "(js-eval \"JSON.parse('[1,2,3]').length\")") +(epoch 2009) +(eval "(js-eval \"JSON.parse('{\\\"a\\\":1}').a\")") + EPOCHS @@ -1525,6 +1547,18 @@ check 1902 "parseInt('42')" '42' check 1903 "parseInt(3.7)" '3' check 1904 "parseFloat('3.14')" '3.14' +# ── Phase 11.json ────────────────────────────────────────────── +check 2000 "stringify(42)" '"42"' +check 2001 "stringify('hi')" '"\"hi\""' +check 2002 "stringify array" '"[1,2,3]"' +check 2003 "stringify object" '"{\"a\":1}"' +check 2004 "stringify true" '"true"' +check 2005 "parse 42" '42' +check 2006 "parse true" 'true' +check 2007 "parse string" '"hello"' +check 2008 "parse array length" '3' +check 2009 "parse object.a" '1' + TOTAL=$((PASS + FAIL)) if [ $FAIL -eq 0 ]; then echo "✓ $PASS/$TOTAL JS-on-SX tests passed" diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index 123909c3..d576ac8b 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -187,6 +187,8 @@ Append-only record of completed iterations. Loop writes one line per iteration: - 2026-04-23 — **`String.fromCharCode`, `parseInt`, `parseFloat`.** `String` global with `fromCharCode` (variadic, loops through args and concatenates via `js-code-to-char`). `parseInt` truncates toward zero via `js-math-trunc`; `parseFloat` delegates to `js-to-number`. Wired into `js-global`. 5 new tests, **381/383** (376→+5). Conformance unchanged. +- 2026-04-23 — **JSON.stringify + JSON.parse.** Shipped a recursive-descent parser and serializer in SX. `js-json-stringify` dispatches on `type-of` for primitives, lists, dicts. `js-json-parse` uses a state dict `{:s src :i idx}` mutated in-place by helpers (`js-json-skip-ws!`, `js-json-parse-value`, `-string`, `-number`, `-array`, `-object`). String parser handles `\n \t \r \" \\ \/` escapes. Number parser collects digits/signs/e+E/. then delegates to `js-to-number`. Array and object loops recursively call parse-value. JSON wired into `js-global`. 10 new tests (stringify primitives/arrays/objects, parse primitives/string/array/object), **391/393** (381→+10). Conformance unchanged. + ## Phase 3-5 gotchas Worth remembering for later phases: