diff --git a/sx/sx/scopes.sx b/sx/sx/scopes.sx index e10728b0..606fc0aa 100644 --- a/sx/sx/scopes.sx +++ b/sx/sx/scopes.sx @@ -1,194 +1,302 @@ -;; --------------------------------------------------------------------------- -;; Scopes — the unified primitive beneath provide, collect!, and spreads -;; --------------------------------------------------------------------------- - - -;; ---- Demo components ---- - -(defcomp ~geography/demo-scope-basic () - (div :class "space-y-2" - (scope "demo-theme" :value "violet" - (div :class "rounded-lg p-3 bg-violet-50 border border-violet-200" - (p :class "text-sm text-violet-800 font-semibold" +(defcomp + ~geography/demo-scope-basic + () + (div + :class "space-y-2" + (scope + "demo-theme" + :value "violet" + (div + :class "rounded-lg p-3 bg-violet-50 border border-violet-200" + (p + :class "text-sm text-violet-800 font-semibold" (str "Inside scope: theme = " (context "demo-theme"))) - (p :class "text-xs text-stone-500" "scope creates a named scope. context reads it."))) - (div :class "rounded-lg p-3 bg-stone-50 border border-stone-200" - (p :class "text-sm text-stone-600" "Outside scope: no context available.")))) + (p + :class "text-xs text-stone-500" + "scope creates a named scope. context reads it."))) + (div + :class "rounded-lg p-3 bg-stone-50 border border-stone-200" + (p + :class "text-sm text-stone-600" + "Outside scope: no context available.")))) -(defcomp ~geography/demo-scope-emit () - (div :class "space-y-2" - (scope "demo-deps" - (div :class "rounded-lg p-3 bg-stone-50 border border-stone-200" - (p :class "text-sm text-stone-700" +(defcomp + ~geography/demo-scope-emit + () + (div + :class "space-y-2" + (scope + "demo-deps" + (div + :class "rounded-lg p-3 bg-stone-50 border border-stone-200" + (p + :class "text-sm text-stone-700" (emit! "demo-deps" "lodash") (emit! "demo-deps" "react") "Components emit their dependencies upward.")) - (div :class "rounded-lg p-3 bg-violet-50 border border-violet-200" + (div + :class "rounded-lg p-3 bg-violet-50 border border-violet-200" (p :class "text-sm text-violet-800 font-semibold" "Emitted:") - (ul :class "text-xs text-stone-600 list-disc pl-5" + (ul + :class "text-xs text-stone-600 list-disc pl-5" (map (fn (d) (li (code d))) (emitted "demo-deps"))))))) -(defcomp ~geography/demo-scope-dedup () - (div :class "space-y-2" - (div :class "rounded-lg p-3 bg-stone-50 border border-stone-200" - (p :class "text-sm text-stone-700" +(defcomp + ~geography/demo-scope-dedup + () + (div + :class "space-y-2" + (div + :class "rounded-lg p-3 bg-stone-50 border border-stone-200" + (p + :class "text-sm text-stone-700" (collect! "demo-css-dedup" ".card { padding: 1rem }") (collect! "demo-css-dedup" ".card { padding: 1rem }") (collect! "demo-css-dedup" ".btn { color: blue }") "Three collect! calls, two identical. Only unique values kept.")) - (div :class "rounded-lg p-3 bg-violet-50 border border-violet-200" - (p :class "text-sm text-violet-800 font-semibold" + (div + :class "rounded-lg p-3 bg-violet-50 border border-violet-200" + (p + :class "text-sm text-violet-800 font-semibold" (str "Collected: " (len (collected "demo-css-dedup")) " rules")) - (ul :class "text-xs text-stone-600 list-disc pl-5" + (ul + :class "text-xs text-stone-600 list-disc pl-5" (map (fn (r) (li (code r))) (collected "demo-css-dedup")))))) - -;; ---- Layout helper ---- - -(defcomp ~geography/scopes-demo-example (&key demo code) - (div :class "grid grid-cols-1 lg:grid-cols-2 gap-4 my-6 items-start" - (div :class "border border-dashed border-stone-300 rounded-lg p-4 bg-stone-50 min-h-[80px]" +(defcomp + ~geography/scopes-demo-example + (&key demo src) + (div + :class "grid grid-cols-1 lg:grid-cols-2 gap-4 my-6 items-start" + (div + :class "border border-dashed border-stone-300 rounded-lg p-4 bg-stone-50 min-h-[80px]" demo) - (div :class "not-prose bg-stone-100 rounded-lg p-4 overflow-x-auto" - (pre :class "text-sm leading-relaxed whitespace-pre-wrap break-words" (code code))))) + (div + :class "not-prose bg-stone-100 rounded-lg p-4 overflow-x-auto" + (pre + :class "text-sm leading-relaxed whitespace-pre-wrap break-words" + (code src))))) - -;; ---- Page content ---- - -(defcomp ~geography/scopes-content () - (~docs/page :title "Scopes" - - (p :class "text-stone-500 text-sm italic mb-8" - "The unified primitive. " (code "scope") " creates a named scope with an optional value " - "and an accumulator. " (code "provide") ", " (code "collect!") ", spreads, islands — " +(defcomp + ~geography/scopes-content + () + (~docs/page + :title "Scopes" + (p + :class "text-stone-500 text-sm italic mb-8" + "The unified primitive. " + (code "scope") + " creates a named scope with an optional value " + "and an accumulator. " + (code "provide") + ", " + (code "collect!") + ", spreads, islands — " "they all resolve to scope operations at the platform level.") - - ;; ===================================================================== - ;; I. The primitive - ;; ===================================================================== - - (~docs/section :title "The primitive" :id "primitive" - - (p (code "scope") " is a special form that pushes a named scope, evaluates its body, " - "then pops it. The scope has three properties: a name, a downward value, and an " - "upward accumulator.") - - (~docs/code :src (highlight "(scope name body...) ;; scope with no value\n(scope name :value v body...) ;; scope with downward value" "lisp")) - - (p "Within the body, " (code "context") " reads the value, " (code "emit!") " appends " - "to the accumulator, and " (code "emitted") " reads what was accumulated.") - + (~docs/section + :title "The primitive" + :id "primitive" + (p + (code "scope") + " is a special form that pushes a named scope, evaluates its body, " + "then pops it. The scope has three properties: a name, a downward value, and an " + "upward accumulator.") + (~docs/code + :src (highlight + "(scope name body...) ;; scope with no value\n(scope name :value v body...) ;; scope with downward value" + "lisp")) + (p + "Within the body, " + (code "context") + " reads the value, " + (code "emit!") + " appends " + "to the accumulator, and " + (code "emitted") + " reads what was accumulated.") (~geography/scopes-demo-example :demo (~geography/demo-scope-basic) - :code (highlight "(scope \"theme\" :value \"violet\"\n (context \"theme\")) ;; → \"violet\"\n\n;; Nested scopes shadow:\n(scope \"x\" :value \"outer\"\n (scope \"x\" :value \"inner\"\n (context \"x\")) ;; → \"inner\"\n (context \"x\")) ;; → \"outer\"" "lisp"))) - - ;; ===================================================================== - ;; II. Sugar forms - ;; ===================================================================== - - (~docs/section :title "Sugar forms" :id "sugar" - - (p "Nobody writes " (code "scope") " directly. The sugar forms are the API:") - + :src (highlight + "(scope \"theme\" :value \"violet\"\n (context \"theme\")) ;; → \"violet\"\n\n;; Nested scopes shadow:\n(scope \"x\" :value \"outer\"\n (scope \"x\" :value \"inner\"\n (context \"x\")) ;; → \"inner\"\n (context \"x\")) ;; → \"outer\"" + "lisp"))) + (~docs/section + :title "Sugar forms" + :id "sugar" + (p + "Nobody writes " + (code "scope") + " directly. The sugar forms are the API:") (~docs/table :headers (list "Sugar" "Expands to" "Used for") :rows (list - (list "provide" "(scope name :value v body...)" "Downward context passing") - (list "collect!" "Lazy root scope + dedup emit" "CSS rule accumulation") - (list "Spreads" "(scope \"element-attrs\" ...)" "Child-to-parent attrs (implicit)"))) - - (~docs/subsection :title "provide — scope with a value" - (p (code "(provide name value body...)") " is exactly " - (code "(scope name :value value body...)") ". It exists because " - "the two-arg form is the common case.") - (~docs/code :src (highlight ";; These are equivalent:\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"hello\"))\n\n(scope \"theme\" :value {:primary \"violet\"}\n (h1 \"hello\"))" "lisp"))) - - (~docs/subsection :title "collect! — lazy root scope with dedup" - (p (code "collect!") " is the most interesting sugar. When called, if no scope exists " - "for that name, it lazily creates a root scope with deduplication enabled. " - "Then it emits into it.") + (list + "provide" + "(scope name :value v body...)" + "Downward context passing") + (list + "collect!" + "Lazy root scope + dedup emit" + "CSS rule accumulation") + (list + "Spreads" + "(scope \"element-attrs\" ...)" + "Child-to-parent attrs (implicit)"))) + (~docs/subsection + :title "provide — scope with a value" + (p + (code "(provide name value body...)") + " is exactly " + (code "(scope name :value value body...)") + ". It exists because " + "the two-arg form is the common case.") + (~docs/code + :src (highlight + ";; These are equivalent:\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"hello\"))\n\n(scope \"theme\" :value {:primary \"violet\"}\n (h1 \"hello\"))" + "lisp"))) + (~docs/subsection + :title "collect! — lazy root scope with dedup" + (p + (code "collect!") + " is the most interesting sugar. When called, if no scope exists " + "for that name, it lazily creates a root scope with deduplication enabled. " + "Then it emits into it.") (~geography/scopes-demo-example :demo (~geography/demo-scope-dedup) - :code (highlight ";; collect! creates a lazy root scope:\n(collect! \"css\" \".card { pad: 1rem }\")\n(collect! \"css\" \".card { pad: 1rem }\") ;; deduped!\n(collect! \"css\" \".btn { color: blue }\")\n(collected \"css\") ;; → 2 rules\n\n;; Equivalent to:\n(scope \"css\" ;; with dedup\n (emit! \"css\" ...)\n (emitted \"css\"))" "lisp")) - (p (code "collected") " is an alias for " (code "emitted") ". " - (code "clear-collected!") " clears the accumulator.")) - - (~docs/subsection :title "Spreads — implicit element scope" - (p "Every element rendering function wraps its children in " - (code "(scope-push! \"element-attrs\" nil)") ". Spread children " - (code "emit!") " their attrs into this scope. After rendering, the element " - "merges the emitted attrs.") - (p "See the " - (a :href "/sx/(geography.(spreads))" :class "text-violet-600 hover:underline" "spreads article") - " for the full mechanism."))) - - ;; ===================================================================== - ;; III. Accumulator: upward data flow - ;; ===================================================================== - - (~docs/section :title "Upward data flow" :id "upward" - + :src (highlight + ";; collect! creates a lazy root scope:\n(collect! \"css\" \".card { pad: 1rem }\")\n(collect! \"css\" \".card { pad: 1rem }\") ;; deduped!\n(collect! \"css\" \".btn { color: blue }\")\n(collected \"css\") ;; → 2 rules\n\n;; Equivalent to:\n(scope \"css\" ;; with dedup\n (emit! \"css\" ...)\n (emitted \"css\"))" + "lisp")) + (p + (code "collected") + " is an alias for " + (code "emitted") + ". " + (code "clear-collected!") + " clears the accumulator.")) + (~docs/subsection + :title "Spreads — implicit element scope" + (p + "Every element rendering function wraps its children in " + (code "(scope-push! \"element-attrs\" nil)") + ". Spread children " + (code "emit!") + " their attrs into this scope. After rendering, the element " + "merges the emitted attrs.") + (p + "See the " + (a + :href "/sx/(geography.(spreads))" + :class "text-violet-600 hover:underline" + "spreads article") + " for the full mechanism."))) + (~docs/section + :title "Upward data flow" + :id "upward" (~geography/scopes-demo-example :demo (~geography/demo-scope-emit) - :code (highlight "(scope \"deps\"\n (emit! \"deps\" \"lodash\")\n (emit! \"deps\" \"react\")\n (emitted \"deps\")) ;; → (\"lodash\" \"react\")" "lisp")) - - (p "Accumulation always goes to the " (em "nearest") " enclosing scope with that name. " - "This is what makes nested elements work — a spread inside a nested " - (code "span") " emits to the " (code "span") "'s scope, not an outer " - (code "div") "'s scope.")) - - ;; ===================================================================== - ;; IV. Platform implementation - ;; ===================================================================== - - (~docs/section :title "Platform implementation" :id "platform" - - (p "Each platform (Python, JavaScript) maintains a single data structure:") - - (~docs/code :src (highlight "_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python")) - + :src (highlight + "(scope \"deps\"\n (emit! \"deps\" \"lodash\")\n (emit! \"deps\" \"react\")\n (emitted \"deps\")) ;; → (\"lodash\" \"react\")" + "lisp")) + (p + "Accumulation always goes to the " + (em "nearest") + " enclosing scope with that name. " + "This is what makes nested elements work — a spread inside a nested " + (code "span") + " emits to the " + (code "span") + "'s scope, not an outer " + (code "div") + "'s scope.")) + (~docs/section + :title "Platform implementation" + :id "platform" + (p + "Each platform (Python, JavaScript) maintains a single data structure:") + (~docs/code + :src (highlight + "_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" + "python")) (p "Six operations on this structure:") - (~docs/table :headers (list "Operation" "Purpose") :rows (list - (list "scope-push!(name, value)" "Push {value, emitted: [], dedup: false}") + (list + "scope-push!(name, value)" + "Push {value, emitted: [], dedup: false}") (list "scope-pop!(name)" "Pop the most recent scope") (list "context(name, default?)" "Read value from nearest scope") - (list "emit!(name, value)" "Append to nearest scope's accumulator (respects dedup)") + (list + "emit!(name, value)" + "Append to nearest scope's accumulator (respects dedup)") (list "emitted(name)" "Read accumulated values from nearest scope") - (list "collect!(name, value)" "Lazy push root scope with dedup, then emit"))) - - (p (code "provide-push!") " and " (code "provide-pop!") " are aliases for " - (code "scope-push!") " and " (code "scope-pop!") ". " - "All adapter code uses " (code "scope-push!") "/" (code "scope-pop!") " directly.")) - - ;; ===================================================================== - ;; V. Unification - ;; ===================================================================== - - (~docs/section :title "What scope unifies" :id "unification" - + (list + "collect!(name, value)" + "Lazy push root scope with dedup, then emit"))) + (p + (code "provide-push!") + " and " + (code "provide-pop!") + " are aliases for " + (code "scope-push!") + " and " + (code "scope-pop!") + ". " + "All adapter code uses " + (code "scope-push!") + "/" + (code "scope-pop!") + " directly.")) + (~docs/section + :title "What scope unifies" + :id "unification" (p "Before scopes, the platform had two separate mechanisms:") - - (~docs/code :src (highlight ";; Before: two mechanisms\n_provide_stacks = {} ;; {name: [{value, emitted: []}]}\n_collect_buckets = {} ;; {name: [values...]}\n\n;; After: one mechanism\n_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" "python")) - + (~docs/code + :src (highlight + ";; Before: two mechanisms\n_provide_stacks = {} ;; {name: [{value, emitted: []}]}\n_collect_buckets = {} ;; {name: [values...]}\n\n;; After: one mechanism\n_scope_stacks = {} ;; {name: [{value, emitted: [], dedup: bool}]}" + "python")) (p "The unification is not just code cleanup. It means:") - (ul :class "space-y-1" - (li (code "collect!") " can be nested inside " (code "provide") " scopes — " - "they share the same stack.") - (li "A component can " (code "emit!") " and " (code "collect!") " into the same scope — " - "they use the same accumulator.") - (li "The dedup flag is per-scope, not per-mechanism — a " (code "provide") " scope " - "has no dedup, a " (code "collect!") " root scope has dedup.")) - - (p "See the " - (a :href "/sx/(etc.(plan.scoped-effects))" :class "text-violet-600 hover:underline" "scoped effects plan") - " for the full design rationale and future phases (reactive scopes, morph scopes).") - + (ul + :class "space-y-1" + (li + (code "collect!") + " can be nested inside " + (code "provide") + " scopes — " + "they share the same stack.") + (li + "A component can " + (code "emit!") + " and " + (code "collect!") + " into the same scope — " + "they use the same accumulator.") + (li + "The dedup flag is per-scope, not per-mechanism — a " + (code "provide") + " scope " + "has no dedup, a " + (code "collect!") + " root scope has dedup.")) + (p + "See the " + (a + :href "/sx/(etc.(plan.scoped-effects))" + :class "text-violet-600 hover:underline" + "scoped effects plan") + " for the full design rationale and future phases (reactive scopes, morph scopes).") (~docs/note - (p (strong "Spec: ") "The " (code "scope") " special form is in " - (a :href "/sx/(language.(spec.(explore.evaluator)))" :class "font-mono text-violet-600 hover:underline text-sm" "eval.sx") - ". Platform primitives are declared in " - (a :href "/sx/(language.(spec.(explore.boundary)))" :class "font-mono text-violet-600 hover:underline text-sm" "boundary.sx") - " (Tier 5: Scoped effects)."))))) + (p + (strong "Spec: ") + "The " + (code "scope") + " special form is in " + (a + :href "/sx/(language.(spec.(explore.evaluator)))" + :class "font-mono text-violet-600 hover:underline text-sm" + "eval.sx") + ". Platform primitives are declared in " + (a + :href "/sx/(language.(spec.(explore.boundary)))" + :class "font-mono text-violet-600 hover:underline text-sm" + "boundary.sx") + " (Tier 5: Scoped effects).")))))