Fix scopes page: rename &key code param that shadowed HTML <code> tag
The ~geography/scopes-demo-example component had (&key demo code), and its body used (code code) — the parameter shadowed the HTML tag, causing "Undefined symbol: code" at render time. Renamed to (&key demo src). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
422
sx/sx/scopes.sx
422
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).")))))
|
||||
|
||||
Reference in New Issue
Block a user