;; --------------------------------------------------------------------------- ;; Provide / Context / Emit! — render-time dynamic scope ;; --------------------------------------------------------------------------- ;; ---- Demo components ---- (defcomp ~geography/demo-provide-basic () (div (~tw :tokens "space-y-2") (provide "theme" {:primary "violet" :accent "rose"} (div (~tw :tokens "rounded-lg p-3 bg-violet-50 border border-violet-200") (p (~tw :tokens "text-sm text-violet-800 font-semibold") "Inside provider: theme.primary = violet") (p (~tw :tokens "text-xs text-stone-500") "Child reads context value without prop threading."))) (div (~tw :tokens "rounded-lg p-3 bg-stone-50 border border-stone-200") (p (~tw :tokens "text-sm text-stone-600") "Outside provider: no theme context.")))) (defcomp ~geography/demo-emit-collect () (div (~tw :tokens "space-y-2") (provide "scripts" nil (div (~tw :tokens "rounded-lg p-3 bg-stone-50 border border-stone-200") (p (~tw :tokens "text-sm text-stone-700") (emit! "scripts" "analytics.js") (emit! "scripts" "charts.js") "Page content renders here. Scripts emitted silently.")) (div (~tw :tokens "rounded-lg p-3 bg-violet-50 border border-violet-200") (p (~tw :tokens "text-sm text-violet-800 font-semibold") "Emitted scripts:") (ul (~tw :tokens "text-xs text-stone-600 list-disc pl-5") (map (fn (s) (li (code s))) (emitted "scripts"))))))) (defcomp ~geography/demo-spread-mechanism () (div (~tw :tokens "space-y-2") (div (make-spread (~tw :tokens "rounded-lg p-3 bg-rose-50 border border-rose-200")) (p (~tw :tokens "text-sm text-rose-800 font-semibold") "Spread child styled this div") (p (~tw :tokens "text-xs text-stone-500") "The spread emitted into the element-attrs provider.")) (let ((card (make-spread (~tw :tokens "rounded-lg p-3 bg-amber-50 border border-amber-200")))) (div card (p (~tw :tokens "text-sm text-amber-800 font-semibold") "Stored spread, same mechanism") (p (~tw :tokens "text-xs text-stone-500") "Bound to a let variable, applied when rendered as child."))))) (defcomp ~geography/demo-nested-provide () (div (~tw :tokens "space-y-2") (provide "level" "outer" (div (~tw :tokens "rounded-lg p-3 bg-stone-50 border border-stone-200") (p (~tw :tokens "text-sm text-stone-700") (str "Level: " (context "level"))) (provide "level" "inner" (div (~tw :tokens "rounded-lg p-3 bg-violet-50 border border-violet-200 ml-4") (p (~tw :tokens "text-sm text-violet-700") (str "Level: " (context "level"))))) (p (~tw :tokens "text-sm text-stone-500 mt-1") (str "Back to: " (context "level"))))))) ;; ---- Layout helper (reuse from spreads article) ---- (defcomp ~geography/provide-demo-example (&key demo code) (div (~tw :tokens "grid grid-cols-1 lg:grid-cols-2 gap-4 my-6 items-start") (div (~tw :tokens "border border-dashed border-stone-300 rounded-lg p-4 bg-stone-50 min-h-[80px]") demo) (div (~tw :tokens "not-prose bg-stone-100 rounded-lg p-4 overflow-x-auto") (pre (~tw :tokens "text-sm leading-relaxed whitespace-pre-wrap break-words") (code code))))) ;; ---- Page content ---- (defcomp ~geography/provide-content () (~docs/page :title "Provide / Context / Emit!" (p (~tw :tokens "text-stone-500 text-sm italic mb-8") "Sugar for " (code "scope") " with a value. " (code "provide") " creates a named scope " "with a value and an accumulator. " (code "context") " reads the value downward. " (code "emit!") " appends to the accumulator upward. " (code "emitted") " retrieves what was emitted. " "See " (a :href "/sx/(geography.(scopes))" (~tw :tokens "text-violet-600 hover:underline") "scopes") " for the unified primitive.") ;; ===================================================================== ;; I. The four primitives ;; ===================================================================== (~docs/section :title "Four primitives" :id "primitives" (~docs/subsection :title "provide (special form)" (p (code "provide") " creates a named scope with a value and an empty accumulator. " "The body expressions execute with the scope active. When the body completes, " "the scope is popped.") (~docs/code :src (highlight "(provide name value\n body...)\n\n;; Example: theme context\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"Title\") ;; can read (context \"theme\")\n (p \"Body\")) ;; scope active for all children" "lisp")) (p (code "provide") " is a special form, not a function — the body is evaluated " "inside the scope, not before it.")) (~docs/subsection :title "context" (p "Reads the value from the nearest enclosing " (code "provide") " with the given name. " "Errors if no provider and no default given.") (~docs/code :src (highlight "(provide \"theme\" {:primary \"violet\" :font \"serif\"}\n (get (context \"theme\") :primary)) ;; → \"violet\"\n\n;; With default (no error when missing):\n(context \"theme\" {:primary \"stone\"}) ;; → {:primary \"stone\"}" "lisp"))) (~docs/subsection :title "emit!" (p "Appends a value to the nearest enclosing provider's accumulator. " "Tolerant: returns nil silently when no provider exists.") (~docs/code :src (highlight "(provide \"scripts\" nil\n (emit! \"scripts\" \"analytics.js\")\n (emit! \"scripts\" \"charts.js\")\n ;; accumulator now has both scripts\n )\n\n;; Outside any provider — silently does nothing:\n(emit! \"scripts\" \"orphan.js\") ;; → nil, no error" "lisp")) (p "Tolerance is critical. Spreads emit into " (code "\"element-attrs\"") " — but a spread might be evaluated in a fragment, a " (code "begin") " block, or a " (code "map") " call where no element provider exists. " "Tolerant " (code "emit!") " means these cases silently vanish instead of crashing.")) (~docs/subsection :title "emitted" (p "Returns the list of values emitted into the nearest provider with the given name. " "Empty list if no provider.") (~docs/code :src (highlight "(provide \"scripts\" nil\n (emit! \"scripts\" \"a.js\")\n (emit! \"scripts\" \"b.js\")\n (emitted \"scripts\")) ;; → (\"a.js\" \"b.js\")" "lisp")))) ;; ===================================================================== ;; II. Two directions, one mechanism ;; ===================================================================== (~docs/section :title "Two directions, one mechanism" :id "directions" (p (code "provide") " serves both downward and upward communication through a single scope.") (~docs/table :headers (list "Direction" "Read with" "Write with" "Example") :rows (list (list "Downward (scope → child)" "context" "provide value" "Theme, config, locale") (list "Upward (child → scope)" "emitted" "emit!" "Script collection, spread attrs"))) (~geography/provide-demo-example :demo (~geography/demo-provide-basic) :code (highlight ";; Downward: theme context\n(provide \"theme\"\n {:primary \"violet\" :accent \"rose\"}\n (h1 :style (str \"color:\"\n (get (context \"theme\") :primary))\n \"Themed heading\")\n (p \"inherits theme context\"))" "lisp")) (~geography/provide-demo-example :demo (~geography/demo-emit-collect) :code (highlight ";; Upward: script accumulation\n(provide \"scripts\" nil\n (div\n (emit! \"scripts\" \"analytics.js\")\n (div\n (emit! \"scripts\" \"charts.js\")\n \"chart\"))\n ;; Collect at the boundary:\n (for-each (fn (s)\n (script :src s))\n (emitted \"scripts\")))" "lisp"))) ;; ===================================================================== ;; III. How spreads use it ;; ===================================================================== (~docs/section :title "How spreads use provide/emit!" :id "spreads" (p "Every element rendering function wraps its children in a provider scope " "named " (code "\"element-attrs\"") ". When the adapter encounters a spread child, " "it emits the spread's attrs into this scope. After all children render, the " "element collects and merges the emitted attrs.") (~geography/provide-demo-example :demo (~geography/demo-spread-mechanism) :code (highlight ";; Spread = emit! into element-attrs\n(div (make-spread {:class \"card\"})\n \"hello\")\n\n;; Internally:\n;; 1. div opens provider:\n;; (provide-push! \"element-attrs\" nil)\n;; 2. spread child emits:\n;; (emit! \"element-attrs\"\n;; {:class \"card\"})\n;; 3. div collects + merges:\n;; (emitted \"element-attrs\")\n;; → ({:class \"card\"})\n;; 4. (provide-pop! \"element-attrs\")\n;; Result:
hello
" "lisp")) (~docs/subsection :title "Why this matters" (p "Before the refactor, every intermediate form in the render pipeline — " "fragments, " (code "let") ", " (code "begin") ", " (code "map") ", " (code "for-each") ", " (code "when") ", " (code "cond") ", component children — " "needed an explicit " (code "(filter (fn (r) (not (spread? r))) ...)") " to strip " "spread values from rendered output. Over 25 such filters existed across the four adapters.") (p "With provide/emit!, all of these disappear. Spreads emit into the nearest element's " "scope regardless of how many layers of control flow they pass through. Non-element " "contexts have no provider, so " (code "emit!") " is a silent no-op."))) ;; ===================================================================== ;; IV. Nested scoping ;; ===================================================================== (~docs/section :title "Nested scoping" :id "nesting" (p "Providers stack. Each " (code "provide") " pushes onto a per-name stack; " "the closest one wins. This gives lexical-style scoping at render time.") (~geography/provide-demo-example :demo (~geography/demo-nested-provide) :code (highlight "(provide \"level\" \"outer\"\n (context \"level\") ;; → \"outer\"\n (provide \"level\" \"inner\"\n (context \"level\")) ;; → \"inner\"\n (context \"level\")) ;; → \"outer\" again" "lisp")) (p "For " (code "emit!") ", this means emissions go to the " (em "nearest") " provider. " "A spread inside a nested element emits to that element, not an ancestor.") (~docs/code :src (highlight ";; Nested elements = nested providers\n(div ;; provider A\n (span ;; provider B\n (make-spread {:class \"inner\"})) ;; emits to B\n (make-spread {:class \"outer\"})) ;; emits to A\n;; →
" "lisp"))) ;; ===================================================================== ;; V. Across all adapters ;; ===================================================================== (~docs/section :title "Across all adapters" :id "adapters" (p "The provide/emit! mechanism works identically across all four rendering adapters. " "The element rendering pattern is the same; only the output format differs.") (~docs/table :headers (list "Adapter" "Element render" "Spread dispatch") :rows (list (list "HTML (server)" "provide-push! → render children → merge emitted → provide-pop!" "(emit! \"element-attrs\" (spread-attrs expr)) → \"\"") (list "Async (server)" "Same pattern, with await on child rendering" "Same dispatch") (list "SX wire (aser)" "provide-push! → serialize children → merge emitted as :key attrs → provide-pop!" "(emit! \"element-attrs\" (spread-attrs expr)) → nil") (list "DOM (browser)" "provide-push! → reduce children → merge emitted onto DOM element → provide-pop!" "emit! + keep value for reactive-spread detection"))) (~docs/subsection :title "DOM adapter: reactive-spread preserved" (p "In the DOM adapter, spread children inside islands are still checked individually " "for signal dependencies. " (code "reactive-spread") " tracks signal deps and " "surgically updates attributes when signals change. The static path uses provide/emit!; " "the reactive path wraps it in an effect.") (p "See the " (a :href "/sx/(geography.(spreads))" (~tw :tokens "text-violet-600 hover:underline") "spreads article") " for reactive-spread details."))) ;; ===================================================================== ;; VI. Comparison with collect! ;; ===================================================================== (~docs/section :title "Comparison with collect! / collected" :id "comparison" (~docs/table :headers (list "" "provide / emit!" "collect! / collected") :rows (list (list "Scope" "Lexical (nearest enclosing provide)" "Global (render-wide)") (list "Deduplication" "None — every emit! appends" "Automatic (same value skipped)") (list "Multiple scopes" "Yes — nested provides shadow" "No — single global bucket per name") (list "Downward data" "Yes (context)" "No") (list "Used by" "Spreads (element-attrs)" "CSSX rule accumulation"))) (p (code "collect!") " remains the right tool for CSS rule accumulation — deduplication " "matters there, and rules need to reach the layout root regardless of nesting depth. " (code "emit!") " is right for spread attrs — no dedup needed, and each element only " "wants attrs from its direct children.")) ;; ===================================================================== ;; VII. Platform implementation ;; ===================================================================== (~docs/section :title "Platform implementation" :id "platform" (p (code "provide") " is sugar for " (code "scope") ". At the platform level, " (code "provide-push!") " and " (code "provide-pop!") " are aliases for " (code "scope-push!") " and " (code "scope-pop!") ". All operations work on a unified " (code "_scope_stacks") " data structure.") (~docs/table :headers (list "Platform primitive" "Purpose") :rows (list (list "scope-push!(name, value)" "Push a new scope with value and empty accumulator") (list "scope-pop!(name)" "Pop the most recent scope") (list "context(name, ...default)" "Read value from nearest scope (error if missing and no default)") (list "emit!(name, value)" "Append to nearest scope's accumulator (tolerant: no-op if missing)") (list "emitted(name)" "Return accumulated values from nearest scope"))) (p (code "provide") " is a special form in " (a :href "/sx/(language.(spec.(explore.evaluator)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "eval.sx") " — it calls " (code "scope-push!") ", evaluates the body, " "then calls " (code "scope-pop!") ". See " (a :href "/sx/(geography.(scopes))" (~tw :tokens "text-violet-600 hover:underline") "scopes") " for the full unified platform.") (~docs/note (p (strong "Spec explorer: ") "See the provide/emit! primitives in " (a :href "/sx/(language.(spec.(explore.boundary)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "boundary.sx explorer") ". The " (code "provide") " special form is in " (a :href "/sx/(language.(spec.(explore.evaluator)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "eval.sx explorer") ". Element rendering with provide/emit! is visible in " (a :href "/sx/(language.(spec.(explore.adapter-html)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "adapter-html") " and " (a :href "/sx/(language.(spec.(explore.adapter-async)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "adapter-async") ".")))))