;; ---------------------------------------------------------------------------
;; Typed SX — Gradual Type System
;; ---------------------------------------------------------------------------

(defcomp ()
  (~docs/page :title "Typed SX"

    (~docs/section :title "The Opportunity" :id "opportunity"
      (p "SX already has types. Every primitive in " (code "primitives.sx") " declares " (code ":returns \"number\"") " or " (code ":returns \"boolean\"") ". Every IO primitive in " (code "boundary.sx") " declares " (code ":returns \"dict?\"") " or " (code ":returns \"any\"") ". Component params are named. The information exists — nobody checks it.")
      (p "A gradual type system makes this information useful. Annotations are optional. Unannotated code works exactly as before. Annotated code gets checked at registration time — zero runtime cost, errors before any request is served. The checker is a spec module (" (code "types.sx") "), bootstrapped to every host.")
      (p "This is not Haskell. SX doesn't need a type system to be correct — " (a :href "/sx/(etc.(plan.theorem-prover))" (~tw :tokens "text-violet-700 underline") "prove.sx") " already verifies primitive properties by exhaustive search. Types serve a different purpose: they catch " (strong "composition errors") " — wrong argument passed to a component, mismatched return type piped into another function, missing keyword arg. The kind of bug you find by reading the stack trace and slapping your forehead."))

    ;; -----------------------------------------------------------------------
    ;; What already exists
    ;; -----------------------------------------------------------------------

    (~docs/section :title "What Already Exists" :id "existing"
      (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 mb-4")
        (table (~tw :tokens "w-full text-left text-sm")
          (thead (tr (~tw :tokens "border-b border-stone-200 bg-stone-100")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Feature")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Where")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Types today")))
          (tbody
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Primitive return types")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "primitives.sx :returns")
              (td (~tw :tokens "px-3 py-2 text-stone-600") "\"number\", \"string\", \"boolean\", \"list\", \"dict\", \"any\""))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "IO primitive return types")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "boundary.sx :returns")
              (td (~tw :tokens "px-3 py-2 text-stone-600") "Same + \"dict?\", \"string?\", \"element\" — nullable types already appear"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Primitive param names")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "primitives.sx :params")
              (td (~tw :tokens "px-3 py-2 text-stone-600") "Named but untyped: " (code "(a b)") ", " (code "(&rest args)")))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Component param names")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "eval.sx parse-comp-params")
              (td (~tw :tokens "px-3 py-2 text-stone-600") (code "&key") " params, " (code "&rest") " children — named, untyped"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Runtime type predicates")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "primitives.sx")
              (td (~tw :tokens "px-3 py-2 text-stone-600") (code "number?") ", " (code "string?") ", " (code "list?") ", " (code "dict?") ", " (code "nil?") ", " (code "symbol?") " — all exist"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Purity classification")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "deps.sx + boundary.py")
              (td (~tw :tokens "px-3 py-2 text-stone-600") "Pure vs IO — a binary type at the component level"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Property verification")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "prove.sx")
              (td (~tw :tokens "px-3 py-2 text-stone-600") "Algebraic properties (commutativity, transitivity) verified by bounded model checking")))))

      (p "The foundation is solid. Primitives already have return types. Params already have names. Boundary enforcement already does structural analysis. Types extend this — they don't replace it."))

    ;; -----------------------------------------------------------------------
    ;; Type language
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Type Language" :id "type-language"
      (p "Small, practical, no type theory PhD required.")

      (~docs/subsection :title "Base types"
        (~docs/code :src (highlight ";; Atomic types\nnumber string boolean nil symbol keyword element\n\n;; Nullable (already used in boundary.sx)\nstring?    ;; (or string nil)\ndict?      ;; (or dict nil)\nnumber?    ;; (or number nil)\n\n;; The top type — anything goes\nany\n\n;; The bottom type — never returns (e.g. abort)\nnever" "lisp")))

      (~docs/subsection :title "Compound types"
        (~docs/code :src (highlight ";; Collections with element types\n(list-of number)           ;; list where every element is a number\n(list-of string)           ;; list of strings\n(list-of any)              ;; list (same as untyped)\n(dict-of string number)    ;; dict with string keys, number values\n(dict-of string any)       ;; dict with string keys (typical kwargs)\n\n;; Union types\n(or string number)         ;; either string or number\n(or string nil)            ;; same as string?\n\n;; Function types\n(-> number number)              ;; number → number\n(-> string string boolean)      ;; (string, string) → boolean\n(-> (list-of any) number)       ;; list → number\n(-> &rest any number)           ;; variadic → number" "lisp")))

      (~docs/subsection :title "Component types"
        (~docs/code :src (highlight ";; Component type is its keyword signature\n;; Derived automatically from defcomp — no annotation needed\n\n(comp :title string :price number &rest element)\n;;     keyword args               children type\n\n;; This is NOT a new syntax for defcomp.\n;; It's the TYPE that a defcomp declaration produces.\n;; The checker infers it from parse-comp-params + annotations." "lisp")))

      (p "That's the core. No higher-kinded types, no dependent types, no type classes. Just: what goes in, what comes out, can it be nil.")

      (~docs/subsection :title "User-defined types"
        (~docs/code :src (highlight ";; Type alias — a name for an existing type\n(deftype price number)\n(deftype html-string string)\n\n;; Union — one of several types\n(deftype renderable (union string number nil))\n(deftype key-type (union string keyword))\n\n;; Record — typed dict shape\n(deftype card-props\n  {:title string\n   :subtitle string?\n   :price number})\n\n;; Parameterized — generic over a type variable\n(deftype (maybe a) (union nil a))\n(deftype (list-of-pairs a b) (list-of (dict-of a b)))\n(deftype (result a e) (union (ok a) (err e)))" "lisp"))
        (p (code "deftype") " is a declaration form — zero runtime cost, purely for the checker. The type registry resolves user-defined type names during " (code "subtype?") " and " (code "infer-type") ". Records enable typed keyword args for components:")
        (~docs/code :src (highlight ";; Define a prop shape\n(deftype card-props\n  {:title string\n   :subtitle string?\n   :price number\n   :on-click (-> any nil)?})\n\n;; Use it in a component\n(defcomp ~plans/typed-sx/product-card (&key (props :as card-props) &rest children)\n  (div :class \"card\"\n    (h2 (get props :title))\n    (span (format-decimal (get props :price) 2))\n    children))\n\n;; Checker validates dict literals against record shape:\n(~plans/typed-sx/product-card :props {:title \"Widget\" :price \"oops\"})\n;;                                       ^^^^^^\n;; ERROR: :price expects number, got string" "lisp"))))

    ;; -----------------------------------------------------------------------
    ;; Annotation syntax
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Annotation Syntax" :id "syntax"
      (p "Annotations are optional. Three places they can appear:")

      (~docs/subsection :title "1. Component params"
        (~docs/code :src (highlight ";; Current (unchanged, still works)\n(defcomp ~plans/typed-sx/product-card (&key title price image-url &rest children)\n  (div ...))\n\n;; Annotated — colon after param name\n(defcomp ~plans/typed-sx/product-card (&key (title : string)\n                             (price : number)\n                             (image-url : string?)\n                        &rest children)\n  (div ...))\n\n;; Parenthesized pairs: (name : type)\n;; Unannotated params default to `any`\n;; &rest children is always (list-of element)" "lisp"))
        (p "The " (code "(name : type)") " syntax is unambiguous — a 3-element list where the second element is the symbol " (code ":") ". The parser already handles lists inside parameter lists. " (code "parse-comp-params") " gains a branch: if a param is a list of length 3 with " (code ":") " in the middle, extract name and type."))

      (~docs/subsection :title "2. Define/lambda return types"
        (~docs/code :src (highlight ";; Current (unchanged)\n(define total-price\n  (fn (items)\n    (reduce + 0 (map (fn (i) (get i \"price\")) items))))\n\n;; Annotated — :returns after params\n(define total-price\n  (fn ((items : (list-of dict)) :returns number)\n    (reduce + 0 (map (fn (i) (get i \"price\")) items))))" "lisp"))
        (p (code ":returns") " is already the convention in " (code "primitives.sx") " and " (code "boundary.sx") ". Same keyword, same position (after params), same meaning."))

      (~docs/subsection :title "3. Let bindings"
        (~docs/code :src (highlight ";; Current (unchanged)\n(let ((x (compute-value)))\n  (+ x 1))\n\n;; Annotated\n(let (((x : number) (compute-value)))\n  (+ x 1))\n\n;; Usually unnecessary — the checker infers let binding\n;; types from the right-hand side. Only annotate when\n;; the inference is ambiguous (e.g. the RHS returns `any`)." "lisp"))

      (p "All annotations are syntactically backward-compatible. Unannotated code parses and runs identically. The annotations are simply ignored by evaluators that don't have the type checker loaded."))

    ;; -----------------------------------------------------------------------
    ;; Type checking
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Type Checking" :id "checking"
      (p "The checker runs at registration time — after " (code "compute_all_deps") ", before serving. It walks every component's body AST and verifies that call sites match declared signatures.")

      (~docs/subsection :title "What it checks"
        (ol (~tw :tokens "list-decimal pl-5 text-stone-700 space-y-2")
          (li (strong "Primitive calls:") " " (code "(+ \"hello\" 3)") " — " (code "+") " expects numbers, got a string. Error.")
          (li (strong "Component calls:") " " (code "(~plans/typed-sx/product-card :title 42)") " — " (code ":title") " declared as " (code "string") ", got " (code "number") ". Error.")
          (li (strong "Missing required params:") " " (code "(~plans/typed-sx/product-card :price 29.99)") " — " (code ":title") " not provided, no default. Error.")
          (li (strong "Unknown keyword args:") " " (code "(~plans/typed-sx/product-card :title \"Hi\" :colour \"red\")") " — " (code ":colour") " not in param list. Warning.")
          (li (strong "Nil safety:") " " (code "(+ 1 (get user \"age\"))") " — " (code "get") " returns " (code "any") " (might be nil). " (code "+") " expects " (code "number") ". Warning: possible nil.")
          (li (strong "Thread-first type flow:") " " (code "(-> items (filter active?) (map name) (join \", \"))") " — checks each step's output matches the next step's input.")))

      (~docs/subsection :title "What it does NOT check"
        (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
          (li (strong "Runtime values.") " " (code "(if condition 42 \"hello\")") " — the type is " (code "(or number string)") ". The checker doesn't know which branch executes.")
          (li (strong "Dict key presence (yet).") " " (code "(get user \"name\")") " — the checker knows " (code "get") " returns " (code "any") " but doesn't track which keys a dict has. Phase 6 (" (code "deftype") " records) will enable this.")
          (li (strong "Termination.") " That's " (a :href "/sx/(etc.(plan.theorem-prover))" (~tw :tokens "text-violet-700 underline") "prove.sx") "'s domain.")
          (li (strong "Full algebraic effects.") " The effect system (Phase 7) checks static effect annotations — it does not provide algebraic effect handlers, effect polymorphism, or continuation-based effect dispatch. That door remains open for the future.")))


      (~docs/subsection :title "Inference"
        (p "Most types are inferred, not annotated. The checker knows:")
        (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
          (li "Literal types: " (code "42") " → " (code "number") ", " (code "\"hi\"") " → " (code "string") ", " (code "true") " → " (code "boolean") ", " (code "nil") " → " (code "nil"))
          (li "Primitive return types: " (code "(+ a b)") " → " (code "number") ", " (code "(str x)") " → " (code "string") ", " (code "(empty? x)") " → " (code "boolean"))
          (li "Let bindings: " (code "(let ((x 42)) ...)") " → " (code "x : number"))
          (li "If/cond narrowing: " (code "(if (nil? x) \"default\" (str x))") " — in the else branch, " (code "x") " is not nil")
          (li "Component return: always " (code "element"))
          (li "Map/filter propagation: " (code "(map name items)") " → " (code "(list-of string)") " if " (code "name") " returns " (code "string")))
        (p "In practice, most component bodies need zero annotations. The checker infers types from literals and primitive return declarations. Annotations are for the edges: params coming from outside, and ambiguous flows.")))

    ;; -----------------------------------------------------------------------
    ;; Gradual semantics
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Gradual Semantics" :id "gradual"
      (p "The type " (code "any") " is the escape hatch. It's compatible with everything — passes every check, accepts every value. Unannotated params are " (code "any") ". The return type of " (code "get") " is " (code "any") ". This means:")

      (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-2")
        (li (strong "Fully untyped code:") " All params are " (code "any") ", all returns are " (code "any") ". The checker has nothing to verify. No errors, no warnings. Exactly the same as today.")
        (li (strong "Partially typed:") " Some components annotate params, others don't. The checker verifies annotated call sites and skips untyped ones. You get value proportional to effort.")
        (li (strong "Fully typed:") " Every component, every lambda, every let binding. The checker catches every composition error. Maximum value, maximum annotation cost."))

      (p "The practical sweet spot: " (strong "annotate component params, nothing else.") " Components are the public API — the boundary between independent pieces of code. Their params are the contract. Internal lambdas and let bindings benefit less from annotations because the checker can infer their types from context.")

      (~docs/code :src (highlight ";; Sweet spot: annotate the interface, infer the rest\n(defcomp ~plans/typed-sx/price-display (&key (price : number)\n                              (currency : string)\n                              (sale-price : number?))\n  ;; Everything below is inferred:\n  ;; formatted → string (str returns string)\n  ;; discount → number (- returns number)\n  ;; has-sale → boolean (and returns boolean)\n  (let ((formatted (str currency (format-number price 2)))\n        (has-sale (and sale-price (< sale-price price)))\n        (discount (if has-sale\n                   (round (* 100 (/ (- price sale-price) price)))\n                   0)))\n    (div :class \"price\"\n      (span :class (if has-sale \"line-through text-stone-400\" \"font-bold\")\n        formatted)\n      (when has-sale\n        (span :class \"text-green-700 font-bold ml-2\"\n          (str currency (format-number sale-price 2))\n          (span :class \"text-xs ml-1\" (str \"(-\" discount \"%)\")))))))" "lisp")))

    ;; -----------------------------------------------------------------------
    ;; Error reporting
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Error Reporting" :id "errors"
      (p "Type errors are reported at registration time with source location, expected type, actual type, and the full call chain.")

      (~docs/code :src (highlight ";; Example error output:\n;;\n;; TYPE ERROR in ~checkout-summary (checkout.sx:34)\n;;\n;;   (str \"Total: \" (compute-total items))\n;;                    ^^^^^^^^^^^^^^^^^\n;;   Argument 2 of `str` expects: string\n;;   Got: number (from compute-total :returns number)\n;;\n;;   Fix: (str \"Total: \" (str (compute-total items)))\n;;\n;;\n;; TYPE ERROR in ~reactive-islands/event-bridge/product-page (products.sx:12)\n;;\n;;   (~plans/typed-sx/product-card :title product-name :price \"29.99\")\n;;                                             ^^^^^^\n;;   Keyword :price of ~plans/typed-sx/product-card expects: number\n;;   Got: string (literal \"29.99\")\n;;\n;;   Fix: (~plans/typed-sx/product-card :title product-name :price 29.99)" "lisp"))

      (p "Severity levels:")
      (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 mb-4")
        (table (~tw :tokens "w-full text-left text-sm")
          (thead (tr (~tw :tokens "border-b border-stone-200 bg-stone-100")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Level")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "When")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Behavior")))
          (tbody
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-semibold text-red-700") "Error")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Definite type mismatch: " (code "number") " where " (code "string") " expected")
              (td (~tw :tokens "px-3 py-2 text-stone-600") "Strict mode: startup crash. Permissive: logged warning."))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-semibold text-amber-700") "Warning")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Possible mismatch: " (code "any") " where " (code "number") " expected, unknown kwarg")
              (td (~tw :tokens "px-3 py-2 text-stone-600") "Logged, never crashes"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-semibold text-stone-500") "Info")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Annotation suggestion: frequently-called untyped component")
              (td (~tw :tokens "px-3 py-2 text-stone-600") "Dev mode only")))))

      (p (code "SX_TYPE_STRICT=1") " (env var, like " (code "SX_BOUNDARY_STRICT") ") makes type errors fatal at startup. Absent = permissive. Same pattern as boundary enforcement."))

    ;; -----------------------------------------------------------------------
    ;; Nil narrowing
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Nil Narrowing" :id "nil"
      (p "The most common real-world type error in SX: passing a possibly-nil value where a non-nil is required. " (code "get") " returns " (code "any") " (might be nil). " (code "current-user") " returns " (code "dict?") " (explicitly nullable). Piping these into " (code "str") " or arithmetic without checking is the #1 source of runtime errors.")

      (~docs/code :src (highlight ";; Before: runtime error if user is nil\n(defcomp ~plans/typed-sx/greeting (&key (user : dict?))\n  (h1 (str \"Hello, \" (get user \"name\"))))\n  ;; ^^^ TYPE WARNING: user is dict?, get needs non-nil first arg\n\n;; After: checker enforces nil handling\n(defcomp ~plans/typed-sx/greeting (&key (user : dict?))\n  (if user\n    (h1 (str \"Hello, \" (get user \"name\")))\n    ;; In this branch, checker narrows user to `dict` (not nil)\n    (h1 \"Hello, guest\")))\n    ;; No warning — nil case handled" "lisp"))

      (p "Narrowing rules:")
      (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
        (li (code "(if x then else)") " — in " (code "then") ", " (code "x") " is narrowed to exclude " (code "nil") " and " (code "false"))
        (li (code "(when x body)") " — in " (code "body") ", " (code "x") " is narrowed")
        (li (code "(nil? x)") " in an if test — " (code "then") " branch: " (code "x") " is " (code "nil") ", " (code "else") " branch: " (code "x") " is non-nil")
        (li (code "(string? x)") " in an if test — " (code "then") " branch: " (code "x") " is " (code "string")))
      (p "This is standard flow typing, nothing exotic. TypeScript does the same thing with " (code "if (x !== null)") " narrowing."))

    ;; -----------------------------------------------------------------------
    ;; Component signature verification
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Component Signature Verification" :id "signatures"
      (p "The highest-value check: verifying that component call sites match declared signatures. This is where most bugs live.")

      (~docs/code :src (highlight ";; Definition\n(defcomp ~plans/typed-sx/product-card (&key (title : string)\n                             (price : number)\n                             (image-url : string?)\n                        &rest children)\n  ...)\n\n;; Call site checks:\n(~plans/typed-sx/product-card :title \"Widget\" :price 29.99)         ;; OK\n(~plans/typed-sx/product-card :title \"Widget\")                       ;; ERROR: :price required\n(~plans/typed-sx/product-card :title 42 :price 29.99)                ;; ERROR: :title expects string\n(~plans/typed-sx/product-card :title \"Widget\" :price 29.99\n  (p \"Description\") (p \"Details\"))                    ;; OK: children\n(~plans/typed-sx/product-card :titel \"Widget\" :price 29.99)          ;; WARNING: :titel unknown\n                                                       ;; (did you mean :title?)" "lisp"))

      (p "The checker walks every component call in every component body. For each call:")
      (ol (~tw :tokens "list-decimal pl-5 text-stone-700 space-y-1")
        (li "Look up the callee component in env")
        (li "Match provided keyword args against declared params")
        (li "Check each arg's inferred type against the param's declared type")
        (li "Report missing required params (those without defaults)")
        (li "Report unknown keyword args (with Levenshtein suggestion)"))

      (p "This catches the majority of composition bugs. A renamed param, a swapped argument, a missing required field — all caught before serving."))

    ;; -----------------------------------------------------------------------
    ;; Thread-first type flow
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Thread-First Type Flow" :id "thread-first"
      (p "The " (code "->") " (thread-first) form is SX's primary composition operator. Type checking it means verifying each step's output matches the next step's input:")

      (~docs/code :src (highlight ";; (-> items\n;;     (filter active?)     ;; (list-of dict) → (list-of dict)\n;;     (map name)           ;; (list-of dict) → (list-of string)\n;;     (join \", \"))         ;; (list-of string) → string\n;;\n;; Type flow: (list-of dict) → (list-of dict) → (list-of string) → string\n;; Each step's output is the next step's first argument.\n\n;; ERROR example:\n;; (-> items\n;;     (filter active?)\n;;     (join \", \")          ;; join expects (list-of string),\n;;     (map name))           ;; got string — wrong order!\n;;\n;; TYPE ERROR: step 3 (map) expects (list-of any) as first arg\n;;            got: string (from join)" "lisp"))

      (p "The checker threads the inferred type through each step. If any step's input type doesn't match the previous step's output type, it reports the exact point where the pipeline breaks."))

    ;; -----------------------------------------------------------------------
    ;; Relationship to prove.sx
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Types vs Proofs" :id "types-vs-proofs"
      (p (a :href "/sx/(etc.(plan.theorem-prover))" (~tw :tokens "text-violet-700 underline") "prove.sx") " and types.sx are complementary, not competing:")

      (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 mb-4")
        (table (~tw :tokens "w-full text-left text-sm")
          (thead (tr (~tw :tokens "border-b border-stone-200 bg-stone-100")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "types.sx")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "prove.sx")))
          (tbody
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-semibold text-stone-700") "Checks")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Composition: does A's output fit B's input?")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Properties: is + commutative? Is sort stable?"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-semibold text-stone-700") "Scope")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "All component bodies, every call site")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Primitives only (declared in primitives.sx)"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-semibold text-stone-700") "Method")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Type inference + checking (fast, O(n) AST walk)")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Bounded model checking (exhaustive, slower)"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-semibold text-stone-700") "When")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Registration time (every startup)")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "CI / on-demand (not every startup)"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-semibold text-stone-700") "Catches")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Wrong arg type, missing param, nil misuse")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Algebraic law violations, edge case failures")))))

      (p "Types answer: " (em "\"does this code fit together?\"") " Proofs answer: " (em "\"does this code do the right thing?\"") " Both are spec modules, both bootstrapped, both run without external tools."))

    ;; -----------------------------------------------------------------------
    ;; User-defined types
    ;; -----------------------------------------------------------------------

    (~docs/section :title "User-Defined Types" :id "deftype"
      (p (code "deftype") " introduces named types — aliases, unions, records, and parameterized types. All are declaration-only, zero runtime cost, resolved at check time.")

      (~docs/subsection :title "Type aliases"
        (~docs/code :src (highlight ";; Simple name for an existing type\n(deftype price number)\n(deftype html-string string)\n(deftype user-id (or string number))\n\n;; Use anywhere a type is expected\n(defcomp ~plans/typed-sx/price-tag (&key (amount :as price) (label :as string))\n  (span :class \"price\" (str label \": $\" (format-decimal amount 2))))" "lisp"))
        (p "Aliases are transparent — " (code "price") " IS " (code "number") " for all checking purposes. They exist for documentation and domain semantics."))

      (~docs/subsection :title "Union types"
        (~docs/code :src (highlight ";; Named unions\n(deftype renderable (union string number nil component))\n(deftype key-type (union string keyword))\n(deftype falsy (union nil false))\n\n;; The checker narrows unions in branches:\n(define handle-input\n  (fn ((val :as (union string number)))\n    (if (string? val)\n      (upper val)          ;; narrowed to string — upper is valid\n      (+ val 1))))         ;; narrowed to number — + is valid" "lisp"))
        (p "Union types compose with narrowing — " (code "if (string? x)") " in the then-branch narrows " (code "(union string number)") " to " (code "string") ". Same flow typing that already works for nullable."))

      (~docs/subsection :title "Record types (typed dicts)"
        (~docs/code :src (highlight ";; Typed dict shape\n(deftype card-props\n  {:title string\n   :subtitle string?\n   :price number\n   :tags (list-of string)})\n\n;; Checker validates dict literals against shape:\n{:title \"Widget\" :price \"oops\"}\n;; ERROR: :price expects number, got string\n\n{:title \"Widget\" :price 29.99}\n;; WARNING: missing :tags (required field)\n\n;; Record types enable typed component props:\n(defcomp ~plans/typed-sx/product-card (&key (props :as card-props) &rest children)\n  (div :class \"card\"\n    (h2 (get props :title))\n    children))" "lisp"))
        (p "Records are the big win. Components pass dicts everywhere — config, props, context. A record type makes " (code "get") " on a known-shape dict return the field's type instead of " (code "any") ". This is where " (code "deftype") " pays for itself."))

      (~docs/subsection :title "Parameterized types"
        (~docs/code :src (highlight ";; Generic over type variables\n(deftype (maybe a) (union nil a))\n(deftype (result a e) (union {:ok a} {:err e}))\n(deftype (pair a b) {:fst a :snd b})\n\n;; Used in signatures:\n(define find-user : (-> number (maybe user-record))\n  (fn (id) ...))\n\n;; Checker instantiates: (maybe user-record) = (union nil user-record)\n;; So the caller must handle nil." "lisp"))
        (p "Parameterized types are substitution-based — " (code "(maybe string)") " expands to " (code "(union nil string)") " at check time. No inference of type parameters (that would require Hindley-Milner). You write " (code "(maybe string)") " explicitly, the checker substitutes and verifies.")))

    ;; -----------------------------------------------------------------------
    ;; Effect system
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Effect System" :id "effects"
      (p "The pragmatic middle: static effect " (em "checking") " without algebraic effect " (em "handlers") ". Functions declare what side effects they use. The checker enforces that effects don't leak across boundaries. No continuations, no runtime cost.")

      (~docs/subsection :title "Effect declarations"
        (~docs/code :src (highlight ";; Declare named effects\n(defeffect io)      ;; Database, HTTP, file system\n(defeffect dom)     ;; Browser DOM manipulation\n(defeffect async)   ;; Asynchronous operations\n(defeffect state)   ;; Mutable state (set!, dict-set!, append!)\n\n;; Functions declare their effects in brackets\n(define fetch-user : (-> number user) [io async]\n  (fn (id) (query \"SELECT * FROM users WHERE id = $1\" id)))\n\n(define toggle-class : (-> element string nil) [dom]\n  (fn (el cls) (set-attr! el :class cls)))\n\n;; Pure by default — no annotation means no effects\n(define add-prices : (-> (list-of number) number)\n  (fn (prices) (reduce + 0 prices)))" "lisp")))

      (~docs/subsection :title "What it checks"
        (ol (~tw :tokens "list-decimal pl-5 text-stone-700 space-y-2")
          (li (strong "Pure functions can't call effectful ones.") " A function with no effect annotation calling " (code "fetch-user") " (which has " (code "[io async]") ") is an error. The IO leaked into pure code.")
          (li (strong "Components declare their effect ceiling.") " A " (code "[pure]") " component can only call pure functions. A " (code "[io]") " component can call IO but not DOM. This is the render-mode safety guarantee.")
          (li (strong "Render modes enforce effect sets.") " " (code "render-to-html") " (server) allows " (code "[io]") " but not " (code "[dom]") ". " (code "render-to-dom") " (browser) allows " (code "[dom]") " but not " (code "[io]") ". " (code "aser") " (wire format) allows " (code "[io]") " for evaluation but serializes the result.")
          (li (strong "Islands are the effect boundary.") " Server effects (" (code "io") ") can't cross into client island code. Client effects (" (code "dom") ") can't leak into server rendering. Currently this is convention — effects make it a proof.")))

      (~docs/subsection :title "Three effect sets match three render modes"
        (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 mb-4")
          (table (~tw :tokens "w-full text-left text-sm")
            (thead (tr (~tw :tokens "border-b border-stone-200 bg-stone-100")
              (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Render mode")
              (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Allowed effects")
              (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Forbidden")))
            (tbody
              (tr (~tw :tokens "border-b border-stone-100")
                (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "render-to-html")
                (td (~tw :tokens "px-3 py-2 text-stone-700") (code "pure") ", " (code "io") ", " (code "async") ", " (code "state"))
                (td (~tw :tokens "px-3 py-2 text-red-600") (code "dom")))
              (tr (~tw :tokens "border-b border-stone-100")
                (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "render-to-dom")
                (td (~tw :tokens "px-3 py-2 text-stone-700") (code "pure") ", " (code "dom") ", " (code "state"))
                (td (~tw :tokens "px-3 py-2 text-red-600") (code "io") ", " (code "async")))
              (tr (~tw :tokens "border-b border-stone-100")
                (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "aser (wire)")
                (td (~tw :tokens "px-3 py-2 text-stone-700") (code "pure") ", " (code "io") ", " (code "async") ", " (code "state"))
                (td (~tw :tokens "px-3 py-2 text-red-600") (code "dom"))))))

        (p "This is exactly the information " (code "deps.sx") " already computes — which components have IO refs. Effects promote it from a runtime classification to a static type-level property. Pure components get an ironclad guarantee: memoize, cache, SSR anywhere, serialize for client — provably safe."))

      (~docs/subsection :title "Effect propagation"
        (~docs/code :src (highlight ";; Effects propagate through calls:\n(define fetch-prices : (-> (list-of number)) [io async]\n  (fn () (query \"SELECT price FROM products\")))\n\n(define render-total : (-> element) [io async]  ;; must declare, calls fetch-prices\n  (fn ()\n    (let ((prices (fetch-prices)))\n      (span (str \"$\" (reduce + 0 prices))))))\n\n;; ERROR if you forget:\n(define render-total : (-> element)  ;; no effects declared\n  (fn ()\n    (let ((prices (fetch-prices)))   ;; ERROR: calls [io async] from pure context\n      (span (str \"$\" (reduce + 0 prices))))))" "lisp"))
        (p "The checker walks call graphs and verifies that every function's declared effects are a superset of its callees' effects. This is transitive — if A calls B calls C, and C has " (code "[io]") ", then A must also declare " (code "[io]") "."))

      (~docs/subsection :title "Gradual effects"
        (p "Like gradual types, effects are opt-in. Unannotated functions are assumed to have " (em "all") " effects — they can call anything, and anything can call them. This is safe (no false positives) but provides no guarantees. As you annotate more functions, the checker catches more violations.")
        (p "The practical sweet spot: annotate " (code "defcomp") " declarations (they're the public API) and let the checker verify that pure components don't accidentally depend on IO. Internal helpers can stay unannotated until they matter.")

        (~docs/code :src (highlight ";; Annotated component — checker enforces purity\n(defcomp ~plans/typed-sx/price-display [pure] (&key (price :as number))\n  (span :class \"price\" (str \"$\" (format-decimal price 2))))\n\n;; ERROR: pure component calls IO\n(defcomp ~plans/typed-sx/price-display [pure] (&key (product-id :as number))\n  (let ((product (fetch-product product-id)))  ;; ERROR: [io] in [pure] context\n    (span :class \"price\" (str \"$\" (get product \"price\")))))" "lisp")))

      (~docs/subsection :title "Relationship to deps.sx and boundary.sx"
        (p "Effects don't replace the existing systems — they formalize them:")
        (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
          (li (code "boundary.sx") " declares which primitives are IO. Effects declare which " (em "functions") " use IO.")
          (li (code "deps.sx") " computes transitive IO refs at registration time. Effects check them at type-check time — earlier, with better error messages.")
          (li "The boundary is still the source of truth for " (em "what is IO") ". Effects are the enforcement mechanism for " (em "who can use it") "."))
        (p "Long term, " (code "deps.sx") "'s IO classification can be derived from effect annotations. In the short term, both coexist — effects are checked, deps are computed, both must agree."))

      (~docs/subsection :title "What it does NOT do"
        (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
          (li (strong "No algebraic effect handlers.") " You can't intercept and resume effects. This would require delimited continuations in every bootstrapper target — massive complexity for marginal UI benefit.")
          (li (strong "No effect polymorphism.") " You can't write a function generic over effects (" (code "forall e. (-> a [e] b)") "). This needs higher-kinded effect types — the same complexity as type classes.")
          (li (strong "No effect inference.") " Effects are declared, not inferred. Inference would require whole-program analysis and produce confusing error messages.")
          (li (strong "No runtime cost.") " Effects are erased after checking. The bootstrapped code has zero overhead. This is purely a static guarantee."))

        (div (~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 mt-4")
          (p (~tw :tokens "text-violet-900 font-medium") "The door stays open.")
          (p (~tw :tokens "text-violet-800") "Static effect checking is the pragmatic middle — 80% of the benefit of a full effect system, 20% of the complexity. If SX ever needs algebraic handlers (e.g. for suspense, or cooperative scheduling beyond what " (code "shift") "/" (code "reset") " provides), the annotation syntax is already in place. Add handlers later without changing any existing code."))))

    ;; -----------------------------------------------------------------------
    ;; Implementation
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Implementation" :id "implementation"

      (~docs/subsection :title "Phase 1: Type Registry (done)"
        (p "Build the type registry from existing declarations.")
        (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
          (li "Parse " (code ":returns") " from " (code "primitives.sx") " and " (code "boundary.sx") " into a type map: " (code "primitive-name → return-type"))
          (li "Parse " (code ":params") " declarations into param type maps (currently untyped — default to " (code "any") ")")
          (li "Compute component signatures from " (code "parse-comp-params") " + any type annotations")
          (li "Store in env as metadata alongside existing component/primitive objects")))

      (~docs/subsection :title "Phase 2: Type Inference Engine (done)"
        (p "Walk AST, infer types bottom-up.")
        (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
          (li "Literals → concrete types")
          (li "Primitive calls → look up return type in registry")
          (li "Component calls → " (code "element"))
          (li "Let bindings → RHS inferred type")
          (li "If/when/cond → union of branch types, with narrowing in branches")
          (li "Lambda → " (code "(-> param-types return-type)") " from body inference")
          (li "Map/filter → propagate element types through the transform")))

      (~docs/subsection :title "Phase 3: Type Checker (done)"
        (p "Compare inferred types at call sites against declared types.")
        (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
          (li "Subtype check: " (code "number") " <: " (code "any") ", " (code "string") " <: " (code "string?") ", " (code "nil") " <: " (code "string?"))
          (li "Error on definite mismatch: " (code "number") " vs " (code "string"))
          (li "Warn on possible mismatch: " (code "any") " vs " (code "number") " (might work, might not)")
          (li "Component kwarg checking: required params, unknown kwargs, type mismatches")))

      (~docs/subsection :title "Phase 4: Annotation Parsing (done)"
        (p "Extend " (code "parse-comp-params") " and " (code "sf-defcomp") " to recognize type annotations.")
        (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
          (li (code "(name : type)") " in param lists → extract type, store in component metadata")
          (li (code ":returns type") " in lambda/fn bodies → store as declared return type")
          (li "Backward compatible: unannotated params remain " (code "any"))))

      (~docs/subsection :title "Phase 5: Typed Primitives (done)"
        (p "Add param types to " (code "primitives.sx") " declarations.")
        (~docs/code :src (highlight ";; Current\n(define-primitive \"+\"\n  :params (&rest args)\n  :returns \"number\"\n  :doc \"Sum all arguments.\")\n\n;; Extended\n(define-primitive \"+\"\n  :params (&rest (args : number))\n  :returns \"number\"\n  :doc \"Sum all arguments.\")" "lisp"))
        (p "This is the biggest payoff for effort: ~80 primitives gain param types, enabling the checker to catch every mistyped primitive call across the entire codebase."))

      (~docs/subsection :title "Phase 6: User-Defined Types (deftype)"
        (p "Extend the type system with named user-defined types.")
        (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
          (li (code "deftype") " special form — parsed by evaluator, stored in type registry")
          (li "Type aliases: " (code "(deftype price number)") " → transparent substitution")
          (li "Union types: " (code "(deftype renderable (union string number nil))") " → checked via " (code "subtype?"))
          (li "Record types: " (code "(deftype card-props {:title string :price number})") " → dict shape validation")
          (li "Parameterized types: " (code "(deftype (maybe a) (union nil a))") " → substitution-based instantiation")
          (li "Extend " (code "infer-type") " — " (code "(get d :title)") " on a known record returns the field type instead of " (code "any"))
          (li "Extend " (code "subtype?") " — resolve named types through the registry before comparing")
          (li "Test: dict literal against record shape, parameterized type instantiation, field-typed " (code "get"))))

      (~docs/subsection :title "Phase 7: Static Effect System"
        (p "Add effect annotations and static checking. No handlers, no runtime cost.")
        (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
          (li (code "defeffect") " declaration form — registers named effects: " (code "io") ", " (code "dom") ", " (code "async") ", " (code "state"))
          (li "Effect annotation syntax: " (code "[io async]") " after function type or on " (code "defcomp"))
          (li "Extend " (code "check-body-walk") " — verify called functions' effects are subset of caller's declared effects")
          (li "Transitive propagation: if A calls B calls C[io], A must declare [io]")
          (li "Render mode enforcement: " (code "render-to-html") " forbids " (code "[dom]") ", " (code "render-to-dom") " forbids " (code "[io]"))
          (li "Gradual: unannotated functions assumed " (code "[all]") " — safe but unchecked. Annotations opt in.")
          (li "Integration with " (code "deps.sx") " — IO classification derived from effect annotations when available")
          (li "Integration with " (code "boundary.sx") " — IO primitives automatically carry " (code "[io]") " effect")
          (li "Test: pure calling IO = error, DOM in server render = error, island boundary enforcement"))))

    ;; -----------------------------------------------------------------------
    ;; Spec module
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Spec Module" :id "spec-module"
      (p (code "types.sx") " — the type checker, written in SX, bootstrapped to every host.")

      (div (~tw :tokens "overflow-x-auto rounded border border-stone-200 mb-4")
        (table (~tw :tokens "w-full text-left text-sm")
          (thead (tr (~tw :tokens "border-b border-stone-200 bg-stone-100")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Function")
            (th (~tw :tokens "px-3 py-2 font-medium text-stone-600") "Purpose")))
          (tbody
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "infer-type")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Infer the type of an AST node in a given type environment"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "check-call")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Check a function/component call against declared signature"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "check-component")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Type-check an entire component body"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "check-all")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Check all registered components, return error/warning list"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "subtype?")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Is type A a subtype of type B?"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "narrow-type")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Narrow a type based on a predicate test (nil?, string?, etc)"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "type-union")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Compute the union of two types"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "parse-type-annotation")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Parse a type expression from annotation syntax"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "build-type-registry")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Build type map from primitives.sx + boundary.sx declarations"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "check-primitive-call")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Check a primitive call against declared param types (positional + rest)"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "resolve-type")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Resolve a named type through the deftype registry (Phase 6)"))
            (tr (~tw :tokens "border-b border-stone-100")
              (td (~tw :tokens "px-3 py-2 font-mono text-sm text-violet-700") "check-effects")
              (td (~tw :tokens "px-3 py-2 text-stone-700") "Verify effect annotations: callee effects ⊆ caller effects (Phase 7)")))))

      (p "The checker is a growing spec module — currently ~650 lines of SX. It's an AST walk with a type environment, structurally similar to " (code "deps.sx") " (which walks ASTs to find IO refs) and " (code "prove.sx") " (which walks ASTs to generate verification conditions). Same pattern, different question."))

    ;; -----------------------------------------------------------------------
    ;; Relationships
    ;; -----------------------------------------------------------------------

    (~docs/section :title "Relationships" :id "relationships"
      (ul (~tw :tokens "list-disc pl-5 text-stone-700 space-y-1")
        (li (a :href "/sx/(etc.(plan.theorem-prover))" (~tw :tokens "text-violet-700 underline") "Theorem Prover") " — prove.sx verifies primitive properties; types.sx verifies composition. Complementary.")
        (li (a :href "/sx/(etc.(plan.content-addressed-components))" (~tw :tokens "text-violet-700 underline") "Content-Addressed Components") " — component manifests gain type signatures. A consumer knows param types before fetching the source.")
        (li (a :href "/sx/(etc.(plan.environment-images))" (~tw :tokens "text-violet-700 underline") "Environment Images") " — the type registry serializes into the image. Type checking happens once at image build time, not on every startup.")
        (li (a :href "/sx/(etc.(plan.runtime-slicing))" (~tw :tokens "text-violet-700 underline") "Runtime Slicing") " — types.sx is a registration-time module, not a runtime module. It doesn't ship to the client. Zero impact on bundle size."))

      (div (~tw :tokens "rounded border border-amber-200 bg-amber-50 p-3 mt-2")
        (p (~tw :tokens "text-amber-800 text-sm") (strong "Depends on: ") "primitives.sx (return + param types), boundary.sx (IO return types + IO classification), eval.sx (defcomp/deftype/defeffect parsing), deps.sx (IO detection — effects formalize this). " (strong "New: ") (code "types.sx") " spec module, type annotations in " (code "parse-comp-params") ", " (code "deftype") " declaration form, " (code "defeffect") " declaration form."))

      (div (~tw :tokens "rounded border border-violet-200 bg-violet-50 p-4 mt-4")
        (p (~tw :tokens "text-violet-900 font-medium") "Why not a Haskell host?")
        (p (~tw :tokens "text-violet-800") "A Haskell SX host would type-check the " (em "host") " code (the evaluator, renderer, parser). But it can't type-check " (em "SX programs") " — those are dynamically typed values passing through " (code "SxVal") ". " (code "types.sx") " checks SX programs directly, on every host. One spec, all hosts benefit. The type system lives where it matters — in the language, not in any particular implementation of it."))))))
