;; GraphQL parser — tokenizer + recursive descent → SX AST ;; ;; Parses the GraphQL query language (queries, mutations, subscriptions, ;; fragments, variables, directives) into s-expression AST. ;; ;; Usage: ;; (gql-parse "{ user(id: 1) { name email } }") ;; ;; AST node types: ;; (gql-doc definitions...) ;; (gql-query name vars directives selections) ;; (gql-mutation name vars directives selections) ;; (gql-subscription name vars directives selections) ;; (gql-field name args directives selections [alias]) ;; (gql-fragment name on-type directives selections) ;; (gql-fragment-spread name directives) ;; (gql-inline-fragment on-type directives selections) ;; (gql-var name) — $variableName reference ;; (gql-var-def name type default) — variable definition ;; (gql-type name) — named type ;; (gql-list-type inner) — [Type] ;; (gql-non-null inner) — Type! ;; (gql-directive name args) — @directive(args) ;; ── Character helpers (shared) ──────────────────────────────────── (define gql-ws? (fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r") (= c ",")))) (define gql-digit? (fn (c) (and c (>= c "0") (<= c "9")))) (define gql-letter? (fn (c) (and c (or (and (>= c "a") (<= c "z")) (and (>= c "A") (<= c "Z")))))) (define gql-name-start? (fn (c) (or (gql-letter? c) (= c "_")))) (define gql-name-char? (fn (c) (or (gql-name-start? c) (gql-digit? c)))) ;; ── Tokenizer ───────────────────────────────────────────────────── ;; Returns {:tokens list :pos int} — state-passing style to avoid ;; multiple define closures over the same mutable variable. (define gql-char-at (fn (src len i) (if (< i len) (substring src i (+ i 1)) nil))) (define gql-skip-ws (fn (src len pos) "Skip whitespace, commas, and # comments. Returns new pos." (if (>= pos len) pos (let ((c (gql-char-at src len pos))) (cond ((gql-ws? c) (gql-skip-ws src len (+ pos 1))) ((= c "#") (let ((eol-pos (gql-skip-to-eol src len (+ pos 1)))) (gql-skip-ws src len eol-pos))) (true pos)))))) (define gql-skip-to-eol (fn (src len pos) (if (>= pos len) pos (if (= (gql-char-at src len pos) "\n") pos (gql-skip-to-eol src len (+ pos 1)))))) (define gql-read-name (fn (src len pos) "Read [_A-Za-z][_A-Za-z0-9]*. Returns {:value name :pos new-pos}." (let ((start pos)) (define loop (fn (p) (if (and (< p len) (gql-name-char? (gql-char-at src len p))) (loop (+ p 1)) {:pos p :value (substring src start p)}))) (loop pos)))) (define gql-read-number (fn (src len pos) "Read number. Returns {:value num :pos new-pos}." (let ((start pos) (p pos)) (when (= (gql-char-at src len p) "-") (set! p (+ p 1))) (define dloop (fn (p has-dot) (if (>= p len) {:pos p :value (parse-number (substring src start p))} (let ((c (gql-char-at src len p))) (cond ((gql-digit? c) (dloop (+ p 1) has-dot)) ((and (= c ".") (not has-dot)) (dloop (+ p 1) true)) ((or (= c "e") (= c "E")) (let ((p2 (+ p 1))) (when (or (= (gql-char-at src len p2) "+") (= (gql-char-at src len p2) "-")) (set! p2 (+ p2 1))) (dloop p2 has-dot))) (true {:pos p :value (parse-number (substring src start p))})))))) (dloop p false)))) (define gql-read-string (fn (src len pos) "Read double-quoted string. pos is ON the opening quote. Returns {:value str :pos new-pos}." (let ((p (+ pos 1))) (if (and (< (+ p 1) len) (= (gql-char-at src len p) "\"") (= (gql-char-at src len (+ p 1)) "\"")) (let ((p2 (+ p 2))) (define bloop (fn (bp) (if (and (< (+ bp 2) len) (= (gql-char-at src len bp) "\"") (= (gql-char-at src len (+ bp 1)) "\"") (= (gql-char-at src len (+ bp 2)) "\"")) {:pos (+ bp 3) :value (substring src p2 bp)} (bloop (+ bp 1))))) (bloop p2)) (do (define sloop (fn (sp parts) (if (>= sp len) {:pos sp :value (join "" parts)} (let ((c (gql-char-at src len sp))) (cond ((= c "\"") {:pos (+ sp 1) :value (join "" parts)}) ((= c "\\") (let ((esc (gql-char-at src len (+ sp 1))) (sp2 (+ sp 2))) (sloop sp2 (append parts (list (cond ((= esc "n") "\n") ((= esc "t") "\t") ((= esc "r") "\r") ((= esc "\\") "\\") ((= esc "\"") "\"") ((= esc "/") "/") (true (str "\\" esc)))))))) (true (sloop (+ sp 1) (append parts (list c))))))))) (sloop p (list))))))) (define gql-tokenize (fn (src) (let ((len (string-length src))) (define tok-loop (fn (pos acc) (let ((pos (gql-skip-ws src len pos))) (if (>= pos len) (append acc (list {:pos pos :value nil :type "eof"})) (let ((c (gql-char-at src len pos))) (cond ((= c "{") (tok-loop (+ pos 1) (append acc (list {:pos pos :value "{" :type "brace-open"})))) ((= c "}") (tok-loop (+ pos 1) (append acc (list {:pos pos :value "}" :type "brace-close"})))) ((= c "(") (tok-loop (+ pos 1) (append acc (list {:pos pos :value "(" :type "paren-open"})))) ((= c ")") (tok-loop (+ pos 1) (append acc (list {:pos pos :value ")" :type "paren-close"})))) ((= c "[") (tok-loop (+ pos 1) (append acc (list {:pos pos :value "[" :type "bracket-open"})))) ((= c "]") (tok-loop (+ pos 1) (append acc (list {:pos pos :value "]" :type "bracket-close"})))) ((= c ":") (tok-loop (+ pos 1) (append acc (list {:pos pos :value ":" :type "colon"})))) ((= c "!") (tok-loop (+ pos 1) (append acc (list {:pos pos :value "!" :type "bang"})))) ((= c "$") (tok-loop (+ pos 1) (append acc (list {:pos pos :value "$" :type "dollar"})))) ((= c "@") (tok-loop (+ pos 1) (append acc (list {:pos pos :value "@" :type "at"})))) ((= c "=") (tok-loop (+ pos 1) (append acc (list {:pos pos :value "=" :type "equals"})))) ((= c "|") (tok-loop (+ pos 1) (append acc (list {:pos pos :value "|" :type "pipe"})))) ((and (= c ".") (< (+ pos 2) len) (= (gql-char-at src len (+ pos 1)) ".") (= (gql-char-at src len (+ pos 2)) ".")) (tok-loop (+ pos 3) (append acc (list {:pos pos :value "..." :type "spread"})))) ((= c "\"") (let ((r (gql-read-string src len pos))) (tok-loop (get r :pos) (append acc (list {:pos pos :value (get r :value) :type "string"}))))) ((or (gql-digit? c) (and (= c "-") (< (+ pos 1) len) (gql-digit? (gql-char-at src len (+ pos 1))))) (let ((r (gql-read-number src len pos))) (tok-loop (get r :pos) (append acc (list {:pos pos :value (get r :value) :type "number"}))))) ((gql-name-start? c) (let ((r (gql-read-name src len pos))) (tok-loop (get r :pos) (append acc (list {:pos pos :value (get r :value) :type "name"}))))) (true (tok-loop (+ pos 1) acc)))))))) (tok-loop 0 (list))))) ;; ── Parser ──────────────────────────────────────────────────────── (define gql-parse-tokens (fn (tokens) (let ((p 0) (tlen (length tokens))) (define cur (fn () (if (< p tlen) (nth tokens p) {:value nil :type "eof"}))) (define cur-type (fn () (get (cur) :type))) (define cur-val (fn () (get (cur) :value))) (define adv! (fn () (set! p (+ p 1)))) (define at-end? (fn () (= (cur-type) "eof"))) (define expect! (fn (type) (if (= (cur-type) type) (let ((v (cur-val))) (adv!) v) (error (str "GraphQL parse error: expected " type " got " (cur-type)))))) (define expect-name! (fn () (expect! "name"))) (define parse-value (fn () (let ((typ (cur-type)) (val (cur-val))) (cond ((= typ "dollar") (do (adv!) (list (quote gql-var) (expect-name!)))) ((= typ "number") (do (adv!) val)) ((= typ "string") (do (adv!) val)) ((and (= typ "name") (= val "true")) (do (adv!) true)) ((and (= typ "name") (= val "false")) (do (adv!) false)) ((and (= typ "name") (= val "null")) (do (adv!) nil)) ((= typ "bracket-open") (parse-list-value)) ((= typ "brace-open") (parse-object-value)) ((= typ "name") (do (adv!) val)) (true (error (str "GraphQL parse error: unexpected " typ " in value"))))))) (define parse-list-value (fn () (do (expect! "bracket-open") (define collect (fn (acc) (if (or (at-end?) (= (cur-type) "bracket-close")) (do (expect! "bracket-close") acc) (collect (append acc (list (parse-value))))))) (collect (list))))) (define parse-object-value (fn () (do (expect! "brace-open") (define collect (fn (acc) (if (or (at-end?) (= (cur-type) "brace-close")) (do (expect! "brace-close") acc) (let ((k (expect-name!))) (expect! "colon") (let ((v (parse-value))) (collect (assoc acc (make-keyword k) v))))))) (collect {})))) (define parse-arguments (fn () (if (not (= (cur-type) "paren-open")) (list) (do (adv!) (define collect (fn (acc) (if (or (at-end?) (= (cur-type) "paren-close")) (do (adv!) acc) (let ((name (expect-name!))) (expect! "colon") (let ((val (parse-value))) (collect (append acc (list (list name val))))))))) (collect (list)))))) (define parse-directives (fn () (define collect (fn (acc) (if (and (= (cur-type) "at") (not (at-end?))) (do (adv!) (let ((name (expect-name!)) (args (parse-arguments))) (collect (append acc (list (list (quote gql-directive) name args)))))) acc))) (collect (list)))) (define parse-type (fn () (let ((base (cond ((= (cur-type) "bracket-open") (do (adv!) (let ((inner (parse-type))) (expect! "bracket-close") (list (quote gql-list-type) inner)))) (true (list (quote gql-type) (expect-name!)))))) (if (= (cur-type) "bang") (do (adv!) (list (quote gql-non-null) base)) base)))) (define parse-variable-defs (fn () (if (not (= (cur-type) "paren-open")) (list) (do (adv!) (define collect (fn (acc) (if (or (at-end?) (= (cur-type) "paren-close")) (do (adv!) acc) (do (expect! "dollar") (let ((name (expect-name!))) (expect! "colon") (let ((typ (parse-type)) (default (if (= (cur-type) "equals") (do (adv!) (parse-value)) nil))) (collect (append acc (list (list (quote gql-var-def) name typ default)))))))))) (collect (list)))))) (define parse-selection-set (fn () (if (not (= (cur-type) "brace-open")) (list) (do (adv!) (define collect (fn (acc) (if (or (at-end?) (= (cur-type) "brace-close")) (do (adv!) acc) (collect (append acc (list (parse-selection))))))) (collect (list)))))) (define parse-selection (fn () (cond ((= (cur-type) "spread") (do (adv!) (if (and (= (cur-type) "name") (not (= (cur-val) "on"))) (let ((name (expect-name!)) (dirs (parse-directives))) (list (quote gql-fragment-spread) name dirs)) (let ((on-type (if (and (= (cur-type) "name") (= (cur-val) "on")) (do (adv!) (expect-name!)) nil)) (dirs (parse-directives)) (sels (parse-selection-set))) (list (quote gql-inline-fragment) on-type dirs sels))))) (true (parse-field))))) (define parse-field (fn () (let ((name1 (expect-name!))) (let ((actual-name (if (= (cur-type) "colon") (do (adv!) (expect-name!)) nil)) (alias (if actual-name name1 nil)) (field-name (if actual-name actual-name name1))) (let ((args (parse-arguments)) (dirs (parse-directives)) (sels (parse-selection-set))) (if alias (list (quote gql-field) field-name args dirs sels alias) (list (quote gql-field) field-name args dirs sels))))))) (define parse-operation (fn (op-type) (let ((name (if (and (= (cur-type) "name") (not (= (cur-val) "query")) (not (= (cur-val) "mutation")) (not (= (cur-val) "subscription")) (not (= (cur-val) "fragment"))) (expect-name!) nil)) (vars (parse-variable-defs)) (dirs (parse-directives)) (sels (parse-selection-set))) (list op-type name vars dirs sels)))) (define parse-fragment-def (fn () (let ((name (expect-name!))) (when (and (= (cur-type) "name") (= (cur-val) "on")) (adv!)) (let ((on-type (expect-name!)) (dirs (parse-directives)) (sels (parse-selection-set))) (list (quote gql-fragment) name on-type dirs sels))))) (define parse-definition (fn () (let ((typ (cur-type)) (val (cur-val))) (cond ((= typ "brace-open") (let ((sels (parse-selection-set))) (list (quote gql-query) nil (list) (list) sels))) ((and (= typ "name") (= val "query")) (do (adv!) (parse-operation (quote gql-query)))) ((and (= typ "name") (= val "mutation")) (do (adv!) (parse-operation (quote gql-mutation)))) ((and (= typ "name") (= val "subscription")) (do (adv!) (parse-operation (quote gql-subscription)))) ((and (= typ "name") (= val "fragment")) (do (adv!) (parse-fragment-def))) (true (error (str "GraphQL parse error: unexpected " typ " " (if val val "")))))))) (define parse-document (fn () (define collect (fn (acc) (if (at-end?) (cons (quote gql-doc) acc) (collect (append acc (list (parse-definition))))))) (collect (list)))) (parse-document)))) ;; ── Convenience: source → AST ───────────────────────────────────── (define gql-parse (fn (source) (gql-parse-tokens (gql-tokenize source)))) ;; ── AST accessors ───────────────────────────────────────────────── (define gql-node-type (fn (node) (if (list? node) (first node) nil))) (define gql-doc? (fn (node) (= (gql-node-type node) (quote gql-doc)))) (define gql-query? (fn (node) (= (gql-node-type node) (quote gql-query)))) (define gql-mutation? (fn (node) (= (gql-node-type node) (quote gql-mutation)))) (define gql-subscription? (fn (node) (= (gql-node-type node) (quote gql-subscription)))) (define gql-field? (fn (node) (= (gql-node-type node) (quote gql-field)))) (define gql-fragment? (fn (node) (= (gql-node-type node) (quote gql-fragment)))) (define gql-fragment-spread? (fn (node) (= (gql-node-type node) (quote gql-fragment-spread)))) (define gql-var? (fn (node) (= (gql-node-type node) (quote gql-var)))) ;; Field accessors: (gql-field name args directives selections [alias]) (define gql-field-name (fn (f) (nth f 1))) (define gql-field-args (fn (f) (nth f 2))) (define gql-field-directives (fn (f) (nth f 3))) (define gql-field-selections (fn (f) (nth f 4))) (define gql-field-alias (fn (f) (if (> (length f) 5) (nth f 5) nil))) ;; Operation accessors: (gql-query/mutation/subscription name vars directives selections) (define gql-op-name (fn (op) (nth op 1))) (define gql-op-vars (fn (op) (nth op 2))) (define gql-op-directives (fn (op) (nth op 3))) (define gql-op-selections (fn (op) (nth op 4))) ;; Fragment accessors: (gql-fragment name on-type directives selections) (define gql-frag-name (fn (f) (nth f 1))) (define gql-frag-type (fn (f) (nth f 2))) (define gql-frag-directives (fn (f) (nth f 3))) (define gql-frag-selections (fn (f) (nth f 4))) ;; Document: (gql-doc def1 def2 ...) (define gql-doc-definitions (fn (doc) (rest doc))) ;; ── Serializer: AST → GraphQL source ───────────────────────────── (define serialize-selection-set (fn (sels) (str "{ " (join " " (map gql-serialize sels)) " }"))) (define serialize-args (fn (args) (str "(" (join ", " (map (fn (a) (str (first a) ": " (gql-serialize (nth a 1)))) args)) ")"))) (define serialize-var-defs (fn (vars) (str "(" (join ", " (map (fn (v) (let ((name (nth v 1)) (typ (serialize-type (nth v 2))) (default (nth v 3))) (str "$" name ": " typ (if default (str " = " (gql-serialize default)) "")))) vars)) ")"))) (define serialize-type (fn (t) (let ((typ (first t))) (cond ((= typ (quote gql-type)) (nth t 1)) ((= typ (quote gql-list-type)) (str "[" (serialize-type (nth t 1)) "]")) ((= typ (quote gql-non-null)) (str (serialize-type (nth t 1)) "!")) (true "Unknown"))))) (define gql-serialize (fn (node) (cond ((not (list? node)) (cond ((string? node) (str "\"" node "\"")) ((number? node) (str node)) ((= node true) "true") ((= node false) "false") ((nil? node) "null") (true (str node)))) (true (let ((typ (gql-node-type node))) (cond ((= typ (quote gql-doc)) (join "\n\n" (map gql-serialize (gql-doc-definitions node)))) ((or (= typ (quote gql-query)) (= typ (quote gql-mutation)) (= typ (quote gql-subscription))) (let ((op-word (cond ((= typ (quote gql-query)) "query") ((= typ (quote gql-mutation)) "mutation") ((= typ (quote gql-subscription)) "subscription"))) (name (gql-op-name node)) (vars (gql-op-vars node)) (sels (gql-op-selections node))) (str op-word (if name (str " " name) "") (if (> (length vars) 0) (serialize-var-defs vars) "") " " (serialize-selection-set sels)))) ((= typ (quote gql-field)) (let ((name (gql-field-name node)) (alias (gql-field-alias node)) (args (gql-field-args node)) (sels (gql-field-selections node))) (str (if alias (str alias ": ") "") name (if (> (length args) 0) (serialize-args args) "") (if (> (length sels) 0) (str " " (serialize-selection-set sels)) "")))) ((= typ (quote gql-fragment)) (str "fragment " (gql-frag-name node) " on " (gql-frag-type node) " " (serialize-selection-set (gql-frag-selections node)))) ((= typ (quote gql-fragment-spread)) (str "..." (nth node 1))) ((= typ (quote gql-var)) (str "$" (nth node 1))) (true "")))))))