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:
@@ -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})
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user