;; ========================================================================== ;; test-parser.sx — Tests for the SX parser and serializer ;; ;; Requires: test-framework.sx loaded first. ;; Modules tested: parser.sx ;; ;; Platform functions required (beyond test framework): ;; sx-parse (source) -> list of AST expressions ;; sx-serialize (expr) -> SX source string ;; make-symbol (name) -> Symbol value ;; make-keyword (name) -> Keyword value ;; symbol-name (sym) -> string ;; keyword-name (kw) -> string ;; ========================================================================== ;; -------------------------------------------------------------------------- ;; Literal parsing ;; -------------------------------------------------------------------------- (defsuite "parser-literals" (deftest "parse integers" (assert-equal (list 42) (sx-parse "42")) (assert-equal (list 0) (sx-parse "0")) (assert-equal (list -7) (sx-parse "-7"))) (deftest "parse floats" (assert-equal (list 3.14) (sx-parse "3.14")) (assert-equal (list -0.5) (sx-parse "-0.5"))) (deftest "parse strings" (assert-equal (list "hello") (sx-parse "\"hello\"")) (assert-equal (list "") (sx-parse "\"\""))) (deftest "parse escape: newline" (assert-equal (list "a\nb") (sx-parse "\"a\\nb\""))) (deftest "parse escape: tab" (assert-equal (list "a\tb") (sx-parse "\"a\\tb\""))) (deftest "parse escape: quote" (assert-equal (list "a\"b") (sx-parse "\"a\\\"b\""))) (deftest "parse booleans" (assert-equal (list true) (sx-parse "true")) (assert-equal (list false) (sx-parse "false"))) (deftest "parse nil" (assert-equal (list nil) (sx-parse "nil"))) (deftest "parse keywords" (let ((result (sx-parse ":hello"))) (assert-length 1 result) (assert-equal "hello" (keyword-name (first result))))) (deftest "parse symbols" (let ((result (sx-parse "foo"))) (assert-length 1 result) (assert-equal "foo" (symbol-name (first result)))))) ;; -------------------------------------------------------------------------- ;; Composite parsing ;; -------------------------------------------------------------------------- (defsuite "parser-lists" (deftest "parse empty list" (let ((result (sx-parse "()"))) (assert-length 1 result) (assert-equal (list) (first result)))) (deftest "parse list of numbers" (let ((result (sx-parse "(1 2 3)"))) (assert-length 1 result) (assert-equal (list 1 2 3) (first result)))) (deftest "parse nested lists" (let ((result (sx-parse "(1 (2 3) 4)"))) (assert-length 1 result) (assert-equal (list 1 (list 2 3) 4) (first result)))) (deftest "parse sibling sublists" ;; Regression: closing paren of (b) must not swallow (c) as a child (let ((result (sx-parse "(a (b) (c))"))) (assert-length 1 result) (assert-length 3 (first result)) (assert-equal (list (make-symbol "a") (list (make-symbol "b")) (list (make-symbol "c"))) (first result)))) (deftest "parse multiple sibling sublists with content" (let ((result (sx-parse "(div (span 1) (span 2) (span 3))"))) (assert-length 1 result) (assert-length 4 (first result)))) (deftest "parse square brackets as list" (let ((result (sx-parse "[1 2 3]"))) (assert-length 1 result) (assert-equal (list 1 2 3) (first result)))) (deftest "parse mixed types" (let ((result (sx-parse "(42 \"hello\" true nil)"))) (assert-length 1 result) (let ((lst (first result))) (assert-equal 42 (nth lst 0)) (assert-equal "hello" (nth lst 1)) (assert-equal true (nth lst 2)) (assert-nil (nth lst 3)))))) ;; -------------------------------------------------------------------------- ;; Dict parsing ;; -------------------------------------------------------------------------- (defsuite "parser-dicts" (deftest "parse empty dict" (let ((result (sx-parse "{}"))) (assert-length 1 result) (assert-type "dict" (first result)))) (deftest "parse dict with keyword keys" (let ((result (sx-parse "{:a 1 :b 2}"))) (assert-length 1 result) (let ((d (first result))) (assert-type "dict" d) (assert-equal 1 (get d "a")) (assert-equal 2 (get d "b"))))) (deftest "parse dict with string values" (let ((result (sx-parse "{:name \"alice\"}"))) (assert-length 1 result) (assert-equal "alice" (get (first result) "name"))))) ;; -------------------------------------------------------------------------- ;; Comments and whitespace ;; -------------------------------------------------------------------------- (defsuite "parser-whitespace" (deftest "skip line comments" (assert-equal (list 42) (sx-parse ";; comment\n42")) (assert-equal (list 1 2) (sx-parse "1 ;; middle\n2"))) (deftest "skip whitespace" (assert-equal (list 42) (sx-parse " 42 ")) (assert-equal (list 1 2) (sx-parse " 1 \n\t 2 "))) (deftest "parse multiple top-level expressions" (assert-length 3 (sx-parse "1 2 3")) (assert-equal (list 1 2 3) (sx-parse "1 2 3"))) (deftest "empty input" (assert-equal (list) (sx-parse ""))) (deftest "only comments" (assert-equal (list) (sx-parse ";; just a comment\n;; another")))) ;; -------------------------------------------------------------------------- ;; Quote sugar ;; -------------------------------------------------------------------------- (defsuite "parser-quote-sugar" (deftest "quasiquote" (let ((result (sx-parse "`foo"))) (assert-length 1 result) (let ((expr (first result))) (assert-type "list" expr) (assert-equal "quasiquote" (symbol-name (first expr)))))) (deftest "unquote" (let ((result (sx-parse ",foo"))) (assert-length 1 result) (let ((expr (first result))) (assert-type "list" expr) (assert-equal "unquote" (symbol-name (first expr)))))) (deftest "splice-unquote" (let ((result (sx-parse ",@foo"))) (assert-length 1 result) (let ((expr (first result))) (assert-type "list" expr) (assert-equal "splice-unquote" (symbol-name (first expr))))))) ;; -------------------------------------------------------------------------- ;; Serializer ;; -------------------------------------------------------------------------- (defsuite "serializer" (deftest "serialize number" (assert-equal "42" (sx-serialize 42))) (deftest "serialize string" (assert-equal "\"hello\"" (sx-serialize "hello"))) (deftest "serialize boolean" (assert-equal "true" (sx-serialize true)) (assert-equal "false" (sx-serialize false))) (deftest "serialize nil" (assert-equal "nil" (sx-serialize nil))) (deftest "serialize keyword" (assert-equal ":foo" (sx-serialize (make-keyword "foo")))) (deftest "serialize symbol" (assert-equal "bar" (sx-serialize (make-symbol "bar")))) (deftest "serialize list" (assert-equal "(1 2 3)" (sx-serialize (list 1 2 3)))) (deftest "serialize empty list" (assert-equal "()" (sx-serialize (list)))) (deftest "serialize nested" (assert-equal "(1 (2 3) 4)" (sx-serialize (list 1 (list 2 3) 4))))) ;; -------------------------------------------------------------------------- ;; Round-trip: parse then serialize ;; -------------------------------------------------------------------------- (defsuite "parser-roundtrip" (deftest "roundtrip number" (assert-equal "42" (sx-serialize (first (sx-parse "42"))))) (deftest "roundtrip string" (assert-equal "\"hello\"" (sx-serialize (first (sx-parse "\"hello\""))))) (deftest "roundtrip list" (assert-equal "(1 2 3)" (sx-serialize (first (sx-parse "(1 2 3)"))))) (deftest "roundtrip nested" (assert-equal "(a (b c))" (sx-serialize (first (sx-parse "(a (b c))")))))) ;; -------------------------------------------------------------------------- ;; Reader macros ;; -------------------------------------------------------------------------- (defsuite "reader-macros" (deftest "datum comment discards expr" (assert-equal (list 42) (sx-parse "#;(ignored) 42"))) (deftest "datum comment in list" (assert-equal (list (list 1 3)) (sx-parse "(1 #;2 3)"))) (deftest "datum comment discards nested" (assert-equal (list 99) (sx-parse "#;(a (b c) d) 99"))) (deftest "raw string basic" (assert-equal (list "hello") (sx-parse "#|hello|"))) (deftest "raw string with quotes" (assert-equal (list "say \"hi\"") (sx-parse "#|say \"hi\"|"))) (deftest "raw string with backslashes" (assert-equal (list "a\\nb") (sx-parse "#|a\\nb|"))) (deftest "raw string empty" (assert-equal (list "") (sx-parse "#||"))) (deftest "quote shorthand symbol" (let ((result (first (sx-parse "#'foo")))) (assert-equal "quote" (symbol-name (first result))) (assert-equal "foo" (symbol-name (nth result 1))))) (deftest "quote shorthand list" (let ((result (first (sx-parse "#'(1 2 3)")))) (assert-equal "quote" (symbol-name (first result))) (assert-equal (list 1 2 3) (nth result 1)))) (deftest "apostrophe quote expands to (quote ...)" (let ((result (sx-parse "'x"))) (assert-length 1 result) (let ((expr (first result))) (assert-type "list" expr) (assert-equal "quote" (symbol-name (first expr))) (assert-equal "x" (symbol-name (nth expr 1)))))) (deftest "apostrophe quote on list" (let ((result (sx-parse "'(1 2 3)"))) (assert-length 1 result) (let ((expr (first result))) (assert-type "list" expr) (assert-equal "quote" (symbol-name (first expr))) (assert-equal (list 1 2 3) (nth expr 1))))) (deftest "quasiquote with unquote inside" (let ((result (sx-parse "`(a ,b)"))) (assert-length 1 result) (let ((expr (first result))) (assert-type "list" expr) (assert-equal "quasiquote" (symbol-name (first expr))) (let ((inner (nth expr 1))) (assert-type "list" inner) (assert-equal "a" (symbol-name (first inner))) (let ((unquoted (nth inner 1))) (assert-type "list" unquoted) (assert-equal "unquote" (symbol-name (first unquoted))))))))) ;; -------------------------------------------------------------------------- ;; Number formats ;; -------------------------------------------------------------------------- (defsuite "parser-numbers" (deftest "integer zero" (assert-equal (list 0) (sx-parse "0"))) (deftest "large integer" (assert-equal (list 1000000) (sx-parse "1000000"))) (deftest "negative float" (assert-equal (list -2.718) (sx-parse "-2.718"))) (deftest "exponent notation" (let ((result (sx-parse "1e10"))) (assert-length 1 result) (assert-type "number" (first result)) (assert-equal 10000000000 (first result)))) (deftest "negative exponent" (let ((result (sx-parse "2.5e-1"))) (assert-length 1 result) (assert-type "number" (first result)) (assert-equal 0.25 (first result)))) (deftest "uppercase exponent E" (let ((result (sx-parse "1E3"))) (assert-length 1 result) (assert-type "number" (first result)) (assert-equal 1000 (first result))))) ;; -------------------------------------------------------------------------- ;; Symbol naming conventions ;; -------------------------------------------------------------------------- (defsuite "parser-symbols" (deftest "symbol with hyphens" (let ((result (sx-parse "my-var"))) (assert-length 1 result) (assert-equal "my-var" (symbol-name (first result))))) (deftest "symbol with question mark" (let ((result (sx-parse "nil?"))) (assert-length 1 result) (assert-equal "nil?" (symbol-name (first result))))) (deftest "symbol with exclamation" (let ((result (sx-parse "set!"))) (assert-length 1 result) (assert-equal "set!" (symbol-name (first result))))) (deftest "symbol with tilde (component)" (let ((result (sx-parse "~my-comp"))) (assert-length 1 result) (assert-equal "~my-comp" (symbol-name (first result))))) (deftest "symbol with arrow" (let ((result (sx-parse "->"))) (assert-length 1 result) (assert-equal "->" (symbol-name (first result))))) (deftest "symbol with &" (let ((result (sx-parse "&key"))) (assert-length 1 result) (assert-equal "&key" (symbol-name (first result))))) (deftest "symbol with every? style" (let ((result (sx-parse "every?"))) (assert-length 1 result) (assert-equal "every?" (symbol-name (first result))))) (deftest "ellipsis is a symbol" (let ((result (sx-parse "..."))) (assert-length 1 result) (assert-equal "..." (symbol-name (first result)))))) ;; -------------------------------------------------------------------------- ;; Serializer — extended ;; -------------------------------------------------------------------------- (defsuite "serializer-extended" (deftest "serialize negative number" (assert-equal "-5" (sx-serialize -5))) (deftest "serialize float" (assert-equal "3.14" (sx-serialize 3.14))) (deftest "serialize string with escaped quote" (let ((s (sx-serialize "say \"hi\""))) (assert-true (string-contains? s "\\\"")))) (deftest "serialize dict round-trips" ;; Parse a dict literal, serialize it, parse again — values survive. (let ((d (first (sx-parse "{:x 1 :y 2}")))) (let ((s (sx-serialize d))) (assert-true (string-contains? s ":x")) (assert-true (string-contains? s ":y")) (let ((d2 (first (sx-parse s)))) (assert-equal 1 (get d2 "x")) (assert-equal 2 (get d2 "y")))))) (deftest "serialize symbol with hyphens" (assert-equal "my-fn" (sx-serialize (make-symbol "my-fn")))) (deftest "serialize keyword with hyphens" (assert-equal ":my-key" (sx-serialize (make-keyword "my-key")))) (deftest "serialize deeply nested list" (assert-equal "(1 (2 (3)))" (sx-serialize (list 1 (list 2 (list 3))))))) ;; -------------------------------------------------------------------------- ;; Round-trip — extended ;; -------------------------------------------------------------------------- (defsuite "parser-roundtrip-extended" (deftest "roundtrip keyword" (let ((parsed (first (sx-parse ":hello")))) (assert-equal ":hello" (sx-serialize parsed)))) (deftest "roundtrip negative number" (assert-equal "-7" (sx-serialize (first (sx-parse "-7"))))) (deftest "roundtrip float" (assert-equal "3.14" (sx-serialize (first (sx-parse "3.14"))))) (deftest "roundtrip string with newline escape" (let ((parsed (first (sx-parse "\"a\\nb\"")))) ;; Parsed value contains a real newline character. (assert-equal "a\nb" parsed) ;; Serialized form must escape it back. (let ((serialized (sx-serialize parsed))) (assert-true (string-contains? serialized "\\n"))))) (deftest "roundtrip symbol with question mark" (let ((parsed (first (sx-parse "empty?")))) (assert-equal "empty?" (sx-serialize parsed)))) (deftest "roundtrip component symbol" (let ((parsed (first (sx-parse "~card")))) (assert-equal "~card" (sx-serialize parsed)))) (deftest "roundtrip keyword arguments in list" (let ((src "(~comp :title \"Hi\" :count 3)")) (assert-equal src (sx-serialize (first (sx-parse src)))))) (deftest "roundtrip empty list" (assert-equal "()" (sx-serialize (first (sx-parse "()"))))) (deftest "roundtrip mixed-type list" (let ((src "(1 \"hello\" true nil)")) (assert-equal src (sx-serialize (first (sx-parse src))))))) ;; -------------------------------------------------------------------------- ;; Edge cases ;; -------------------------------------------------------------------------- (defsuite "parser-edge-cases" (deftest "empty string parses to empty list" (assert-equal (list) (sx-parse ""))) (deftest "whitespace-only parses to empty list" (assert-equal (list) (sx-parse " \n\t "))) (deftest "multiple top-level expressions" (let ((result (sx-parse "1 2 3"))) (assert-length 3 result) (assert-equal 1 (nth result 0)) (assert-equal 2 (nth result 1)) (assert-equal 3 (nth result 2)))) (deftest "multiple top-level mixed types" (let ((result (sx-parse "42 \"hello\" true nil"))) (assert-length 4 result) (assert-equal 42 (nth result 0)) (assert-equal "hello" (nth result 1)) (assert-equal true (nth result 2)) (assert-nil (nth result 3)))) (deftest "deeply nested list" ;; (((((1))))) — parser returns one top-level expression (let ((result (sx-parse "(((((1)))))"))) (assert-length 1 result))) (deftest "long string value" (let ((long-str (join "" (map (fn (x) "abcdefghij") (range 0 10))))) (let ((src (str "\"" long-str "\""))) (assert-equal (list long-str) (sx-parse src))))) (deftest "inline comment inside list" (let ((result (sx-parse "(+ 1 ;; comment\n 2)"))) (assert-length 1 result) (assert-equal (list (make-symbol "+") 1 2) (first result)))) (deftest "comment at end of file with no trailing newline" (assert-equal (list 1) (sx-parse "1 ;; trailing comment"))) (deftest "keyword with numeric suffix" (let ((result (sx-parse ":item-1"))) (assert-length 1 result) (assert-equal "item-1" (keyword-name (first result))))) (deftest "consecutive keywords parsed as separate atoms" (let ((result (sx-parse ":a :b :c"))) (assert-length 3 result) (assert-equal "a" (keyword-name (nth result 0))) (assert-equal "b" (keyword-name (nth result 1))) (assert-equal "c" (keyword-name (nth result 2))))) (deftest "symbol immediately after opening paren" (let ((result (first (sx-parse "(foo)")))) (assert-length 1 result) (assert-equal "foo" (symbol-name (first result))))) (deftest "parse boolean true is not a symbol" (let ((result (first (sx-parse "true")))) (assert-type "boolean" result) (assert-equal true result))) (deftest "parse boolean false is not a symbol" (let ((result (first (sx-parse "false")))) (assert-type "boolean" result) (assert-equal false result))) (deftest "parse nil is not a symbol" (let ((result (first (sx-parse "nil")))) (assert-nil result)))) ;; -------------------------------------------------------------------------- ;; JIT regression: mutable pos shared via upvalues across recursive calls ;; -------------------------------------------------------------------------- (defsuite "parser-jit-regression" (deftest "letrec parser with mutable pos — recursive sublists" ;; Minimal reproducer for the sx-parse JIT bug. ;; Uses define inside fn (like sx-parse's read-list-loop pattern). (let ((parse-fn (fn (src) (let ((pos 0)) (letrec ((read-list (fn () (let ((result (list)) (done false)) (define go (fn () (when (and (not done) (< pos (len src))) (let ((ch (nth src pos))) (set! pos (inc pos)) (cond (= ch ")") (set! done true) (= ch "(") (do (append! result (read-list)) (go)) :else (do (append! result ch) (go))))))) (go) result)))) (set! pos 1) (read-list)))))) (let ((r (parse-fn "(a(b)(c))"))) (assert (list? r) (str "result should be list, got type=" (type-of r))) (assert-equal 3 (len r)) (assert-equal (list "a" (list "b") (list "c")) r)))) ) (defsuite "define-as-local" (deftest "define inside fn creates local, not global" ;; When define is inside a fn body, recursive calls must each ;; get their own copy. If define writes to global, recursive ;; calls overwrite each other. (let ((result (let ((counter 0)) (letrec ((make-counter (fn () (define my-val counter) (set! counter (inc counter)) my-val))) (list (make-counter) (make-counter) (make-counter)))))) (assert-equal (list 0 1 2) result))) (deftest "define inside fn with self-recursion via define" ;; read-list-loop pattern: define a function that calls itself (let ((result (let ((items (list))) (define go (fn (n) (when (< n 3) (append! items n) (go (inc n))))) (go 0) items))) (assert-equal (list 0 1 2) result))) (deftest "recursive define inside letrec fn doesn't overwrite" ;; Each call to make-list creates its own 'loop' local (let ((make-list (fn (items) (let ((result (list))) (define loop (fn (i) (when (< i (len items)) (append! result (nth items i)) (loop (inc i))))) (loop 0) result)))) (assert-equal (list "a" "b") (make-list (list "a" "b"))) (assert-equal (list 1 2 3) (make-list (list 1 2 3))))))