js-on-sx: JSON.stringify + JSON.parse

Recursive-descent parser/serializer in SX.

stringify: type-of dispatch for primitives, lists, dicts. Strings
escape \\ \" \n \r \t.

parse: {:s src :i idx} state dict threaded through helpers.
Handles primitives, strings (with escapes), numbers, arrays,
objects.

Wired into js-global.

391/393 unit (+10), 148/148 slice unchanged.
This commit is contained in:
2026-04-23 21:22:32 +00:00
parent 6f0b4fb476
commit ebaec1659e
3 changed files with 288 additions and 1 deletions

View File

@@ -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})

View File

@@ -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"

View File

@@ -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: