HS parser: fix number+comparison keyword collision, eval-hs uses hs-compile
Parser: skip unit suffix when next ident is a comparison keyword (starts, ends, contains, matches, is, does, in, precedes, follows). Fixes "123 starts with '12'" returning "123starts" instead of true. eval-hs: use hs-compile directly instead of hs-to-sx-from-source with "return " prefix, which was causing the parser to consume the comparison as a string suffix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
57
sx/sx/applications/graphql/_islands/parse-island.sx
Normal file
57
sx/sx/applications/graphql/_islands/parse-island.sx
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
;; Live GraphQL parse island — parses input and shows AST + serialized output
|
||||
(defisland
|
||||
()
|
||||
(let
|
||||
((input (signal "{\n user(id: 1) {\n name\n email\n }\n}"))
|
||||
(ast-output (signal ""))
|
||||
(ser-output (signal "")))
|
||||
(define
|
||||
do-parse!
|
||||
(fn
|
||||
(e)
|
||||
(let
|
||||
((src (deref input)))
|
||||
(when
|
||||
(not (empty? src))
|
||||
(let
|
||||
((doc (gql-parse src)))
|
||||
(do
|
||||
(set-signal! ast-output (sx-serialize doc))
|
||||
(set-signal! ser-output (gql-serialize doc))))))))
|
||||
(div
|
||||
(~tw :tokens "flex flex-col gap-3")
|
||||
(textarea
|
||||
:rows "5"
|
||||
(~tw
|
||||
:tokens "w-full font-mono text-sm p-3 border border-gray-300 rounded-lg focus:border-violet-500 focus:ring-1 focus:ring-violet-500 outline-none")
|
||||
:placeholder "{ user(id: 1) { name email } }"
|
||||
:value (deref input)
|
||||
:on-input (fn (e) (set-signal! input (get-prop e "target.value"))))
|
||||
(button
|
||||
(~tw
|
||||
:tokens "self-start px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors")
|
||||
:on-click do-parse!
|
||||
"Parse")
|
||||
(when
|
||||
(not (empty? (deref ast-output)))
|
||||
(div
|
||||
(~tw :tokens "space-y-4")
|
||||
(div
|
||||
(h4
|
||||
(~tw
|
||||
:tokens "text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2")
|
||||
"AST")
|
||||
(pre
|
||||
(~tw
|
||||
:tokens "bg-gray-900 text-green-400 p-4 rounded-lg text-sm overflow-x-auto")
|
||||
(deref ast-output)))
|
||||
(div
|
||||
(h4
|
||||
(~tw
|
||||
:tokens "text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2")
|
||||
"Serialized")
|
||||
(pre
|
||||
(~tw
|
||||
:tokens "bg-gray-900 text-amber-400 p-4 rounded-lg text-sm overflow-x-auto")
|
||||
(deref ser-output))))))))
|
||||
274
sx/sx/applications/graphql/index.sx
Normal file
274
sx/sx/applications/graphql/index.sx
Normal file
@@ -0,0 +1,274 @@
|
||||
|
||||
;; GraphQL demo — parser, executor, and hyperscript integration
|
||||
(defcomp
|
||||
()
|
||||
(~docs/page
|
||||
:title "GraphQL"
|
||||
(p
|
||||
(~tw :tokens "text-lg text-gray-600 mb-2")
|
||||
"A pure SX implementation of the "
|
||||
(a
|
||||
:href "https://spec.graphql.org/"
|
||||
:target "_blank"
|
||||
(~tw :tokens "text-violet-600 underline")
|
||||
"GraphQL query language")
|
||||
". The parser, executor, and serializer are all s-expressions "
|
||||
"compiled to bytecode by the same kernel that runs the rest of the site.")
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-6")
|
||||
"GraphQL operations map directly to the existing "
|
||||
(code "defquery")
|
||||
"/"
|
||||
(code "defaction")
|
||||
" system. Queries become IO suspension calls, mutations become actions, "
|
||||
"and field selection is projection over result dicts. The hyperscript "
|
||||
"integration adds "
|
||||
(code "fetch gql { ... }")
|
||||
" as a native command.")
|
||||
(~docs/section
|
||||
:title "Parser"
|
||||
:id "parser"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"The tokenizer and recursive-descent parser live in "
|
||||
(code "lib/graphql.sx")
|
||||
". Feed it any GraphQL source and get an SX AST back.")
|
||||
(~docs/code :src "(gql-parse \"{ user(id: 1) { name email } }\")")
|
||||
(p (~tw :tokens "text-gray-500 text-sm mb-3") "Result:")
|
||||
(~docs/code
|
||||
:src (str
|
||||
"(gql-doc\n"
|
||||
" (gql-query nil () ()\n"
|
||||
" ((gql-field \"user\" ((\"id\" 1)) ()\n"
|
||||
" ((gql-field \"name\" () () ())\n"
|
||||
" (gql-field \"email\" () () ()))))))"))
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mt-3")
|
||||
"Every GraphQL concept has an SX node type: "
|
||||
(code "gql-query")
|
||||
", "
|
||||
(code "gql-mutation")
|
||||
", "
|
||||
(code "gql-field")
|
||||
", "
|
||||
(code "gql-fragment")
|
||||
", "
|
||||
(code "gql-var")
|
||||
", "
|
||||
(code "gql-directive")
|
||||
". "
|
||||
"Accessors like "
|
||||
(code "gql-field-name")
|
||||
" and "
|
||||
(code "gql-op-selections")
|
||||
" make the AST easy to walk."))
|
||||
(~docs/section
|
||||
:title "Variables and Operations"
|
||||
:id "variables"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Named operations, variable definitions with types and defaults, "
|
||||
"and variable references all parse to structured AST nodes.")
|
||||
(~docs/code
|
||||
:src "query GetUser($id: ID!, $limit: Int = 10) {\n user(id: $id) {\n name\n posts(limit: $limit) { title }\n }\n}")
|
||||
(p
|
||||
(~tw :tokens "text-gray-500 text-sm mt-3 mb-3")
|
||||
"Variable definitions become "
|
||||
(code "(gql-var-def name type default)")
|
||||
" nodes. "
|
||||
"References become "
|
||||
(code "(gql-var name)")
|
||||
" nodes that the executor substitutes "
|
||||
"from a provided bindings dict."))
|
||||
(~docs/section
|
||||
:title "Fragments"
|
||||
:id "fragments"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Fragment definitions, fragment spreads, and inline fragments:")
|
||||
(~docs/code
|
||||
:src "query {\n user {\n ...UserFields\n ... on Admin { role }\n }\n}\n\nfragment UserFields on User {\n name\n email\n}")
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mt-3")
|
||||
"The executor collects fragment definitions from the document "
|
||||
"and resolves spreads during projection. Inline fragments apply "
|
||||
"unconditionally (type checking is available via "
|
||||
(code "spec/types.sx")
|
||||
")."))
|
||||
(~docs/section
|
||||
:title "Executor"
|
||||
:id "executor"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"The executor in "
|
||||
(code "lib/graphql-exec.sx")
|
||||
" walks the parsed AST and dispatches each root field through a resolver. "
|
||||
"The default resolver uses "
|
||||
(code "perform io-gql-resolve")
|
||||
" to dispatch via IO suspension, exactly like "
|
||||
(code "defquery")
|
||||
".")
|
||||
(~docs/code
|
||||
:src "(let ((doc (gql-parse \"{ user(id: 1) { name email } }\"))\n (resolver (fn (field-name args op-type)\n (if (= field-name \"user\")\n {:name \"Alice\" :email \"alice@test.com\" :age 30}\n nil))))\n (gql-execute doc {} resolver))")
|
||||
(p (~tw :tokens "text-gray-500 text-sm mb-3") "Result:")
|
||||
(~docs/code
|
||||
:src "{:data {:user {:email \"alice@test.com\" :name \"Alice\"}}}")
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mt-3")
|
||||
"Notice "
|
||||
(code ":age")
|
||||
" is absent from the result. "
|
||||
"The executor projects each resolver result down to only the fields "
|
||||
"the query requested. Nested selections, list results, and aliased "
|
||||
"fields all work."))
|
||||
(~docs/section
|
||||
:title "Aliases"
|
||||
:id "aliases"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Query the same field multiple times with different arguments:")
|
||||
(~docs/code
|
||||
:src "{\n me: user(id: 1) { name }\n them: user(id: 2) { name }\n}")
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mt-3")
|
||||
"Each aliased field calls the resolver independently. "
|
||||
"Results are keyed by the alias name: "
|
||||
(code "{:data {:me {:name ...} :them {:name ...}}}")
|
||||
"."))
|
||||
(~docs/section
|
||||
:title "Serializer"
|
||||
:id "serializer"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"The serializer converts an AST back to GraphQL source. "
|
||||
"Useful for logging, debugging, and wire format.")
|
||||
(~docs/code
|
||||
:src "(gql-serialize (gql-parse \"query GetUser($id: ID!) { user(id: $id) { name } }\"))")
|
||||
(p
|
||||
(~tw :tokens "text-gray-500 text-sm mt-3")
|
||||
"Round-trips cleanly. Parse, transform the AST, serialize back."))
|
||||
(~docs/section
|
||||
:title "Hyperscript: fetch gql"
|
||||
:id "hyperscript"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"The "
|
||||
(code "fetch gql")
|
||||
" command embeds GraphQL queries directly in hyperscript. "
|
||||
"Write queries inline "
|
||||
(em "in the HTML attribute")
|
||||
", compiled to bytecode "
|
||||
"alongside the rest of the hyperscript.")
|
||||
(~docs/code
|
||||
:src "<!-- shorthand query -->\n<button _=\"on click fetch gql { user(id: 1) { name } } then put it.data.user.name into #result\">\n Load User\n</button>\n\n<!-- with explicit endpoint -->\n<button _=\"on click fetch gql { posts { title } } from '/api/graphql' then ...\">\n Load Posts\n</button>\n\n<!-- mutation -->\n<button _=\"on click fetch gql mutation { deletePost(id: 42) { success } } then ...\">\n Delete\n</button>")
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mt-3 mb-3")
|
||||
"The parser collects the GraphQL body between braces, preserving nested "
|
||||
"structure. At runtime, "
|
||||
(code "hs-fetch-gql")
|
||||
" sends a standard "
|
||||
(code "{ \"query\": \"...\", \"variables\": {} }")
|
||||
" JSON POST to the endpoint (defaults to "
|
||||
(code "/graphql")
|
||||
").")
|
||||
(p
|
||||
(~tw :tokens "text-gray-600")
|
||||
"The compilation pipeline: hyperscript source "
|
||||
(span (~tw :tokens "text-gray-400") " → ")
|
||||
"HS tokenizer "
|
||||
(span (~tw :tokens "text-gray-400") " → ")
|
||||
"HS parser ("
|
||||
(code "fetch-gql")
|
||||
" AST) "
|
||||
(span (~tw :tokens "text-gray-400") " → ")
|
||||
"HS compiler ("
|
||||
(code "hs-fetch-gql")
|
||||
" call) "
|
||||
(span (~tw :tokens "text-gray-400") " → ")
|
||||
"bytecode. The GraphQL source string is a constant in the compiled output."))
|
||||
(~docs/section
|
||||
:title "GraphQL → SX Mapping"
|
||||
:id "mapping"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Every GraphQL concept has a direct SX equivalent:")
|
||||
(table
|
||||
(~tw :tokens "w-full text-sm border-collapse mb-4")
|
||||
(thead
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-200")
|
||||
(th
|
||||
(~tw :tokens "text-left py-2 pr-4 text-gray-500 font-medium")
|
||||
"GraphQL")
|
||||
(th
|
||||
(~tw :tokens "text-left py-2 pr-4 text-gray-500 font-medium")
|
||||
"SX")
|
||||
(th
|
||||
(~tw :tokens "text-left py-2 text-gray-500 font-medium")
|
||||
"Mechanism")))
|
||||
(tbody
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-100")
|
||||
(td (~tw :tokens "py-2 pr-4") "Query")
|
||||
(td (~tw :tokens "py-2 pr-4") (code "defquery"))
|
||||
(td (~tw :tokens "py-2") "IO suspension"))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-100")
|
||||
(td (~tw :tokens "py-2 pr-4") "Mutation")
|
||||
(td (~tw :tokens "py-2 pr-4") (code "defaction"))
|
||||
(td (~tw :tokens "py-2") "IO suspension"))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-100")
|
||||
(td (~tw :tokens "py-2 pr-4") "Subscription")
|
||||
(td (~tw :tokens "py-2 pr-4") "SSE + signals")
|
||||
(td (~tw :tokens "py-2") "Reactive islands"))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-100")
|
||||
(td (~tw :tokens "py-2 pr-4") "Fragment")
|
||||
(td (~tw :tokens "py-2 pr-4") (code "defcomp"))
|
||||
(td (~tw :tokens "py-2") "Component composition"))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-100")
|
||||
(td (~tw :tokens "py-2 pr-4") "Schema")
|
||||
(td (~tw :tokens "py-2 pr-4") (code "spec/types.sx"))
|
||||
(td (~tw :tokens "py-2") "Gradual type system"))
|
||||
(tr
|
||||
(~tw :tokens "border-b border-gray-100")
|
||||
(td (~tw :tokens "py-2 pr-4") "Resolver")
|
||||
(td (~tw :tokens "py-2 pr-4") (code "perform"))
|
||||
(td (~tw :tokens "py-2") "CEK IO suspension"))
|
||||
(tr
|
||||
(td (~tw :tokens "py-2 pr-4") "Field selection")
|
||||
(td (~tw :tokens "py-2 pr-4") (code "gql-project"))
|
||||
(td (~tw :tokens "py-2") "Dict projection")))))
|
||||
(~docs/section
|
||||
:title "Live Parse"
|
||||
:id "live-parse"
|
||||
(p
|
||||
(~tw :tokens "text-gray-600 mb-3")
|
||||
"Type a GraphQL query and see the SX AST. "
|
||||
"Parsed by the same "
|
||||
(code "lib/graphql.sx")
|
||||
" that compiles to bytecode.")
|
||||
(div
|
||||
(~tw :tokens "flex flex-col gap-3")
|
||||
(textarea
|
||||
:id "gql-input"
|
||||
:name "source"
|
||||
:rows "5"
|
||||
(~tw
|
||||
:tokens "w-full font-mono text-sm p-3 border border-gray-300 rounded-lg focus:border-violet-500 focus:ring-1 focus:ring-violet-500 outline-none")
|
||||
:placeholder "{ user(id: 1) { name email } }"
|
||||
"{\n user(id: 1) {\n name\n email\n }\n}")
|
||||
(button
|
||||
(~tw
|
||||
:tokens "self-start px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors cursor-pointer")
|
||||
:id "gql-parse-btn"
|
||||
"Parse")
|
||||
(div
|
||||
:id "gql-output"
|
||||
(~tw
|
||||
:tokens "min-h-24 p-3 bg-gray-50 rounded-lg border border-gray-200 font-mono text-sm text-gray-700 whitespace-pre-wrap")
|
||||
(span (~tw :tokens "text-gray-400") "AST will appear here")))
|
||||
(script
|
||||
"document.getElementById('gql-parse-btn').addEventListener('click', function() { var src = document.getElementById('gql-input').value; fetch('/sx/(applications.(graphql.(api.parse-demo)))?source=' + encodeURIComponent(src)).then(function(r) { return r.text(); }).then(function(html) { document.getElementById('gql-output').innerHTML = html; }); });"))))
|
||||
Reference in New Issue
Block a user