- inject_path_name: strip _islands/ convention dirs from path-derived names - page-functions.sx: fix geography (→ ~geography) and isomorphism (→ ~etc/plan/isomorphic) - request-handler.sx: rewrite sx-eval-page to call page functions explicitly via env-get+apply, avoiding provide special form intercepting (provide) calls - sx_server.ml: set expand-components? on AJAX aser paths so server-side components expand for the browser (islands stay unexpanded for hydration) - Rename 19 component references in geography/spreads, geography/provide, geography/scopes to use path-qualified names matching inject_path_name output Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
223 lines
8.1 KiB
Plaintext
223 lines
8.1 KiB
Plaintext
(defcomp
|
|
()
|
|
(~docs/page
|
|
:title "Scopes"
|
|
(p
|
|
(~tw :tokens "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.")
|
|
(~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/scopes-demo-example
|
|
:demo (~geography/scopes/demo-scope-basic)
|
|
: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.")
|
|
(~geography/scopes/scopes-demo-example
|
|
:demo (~geography/scopes/demo-scope-dedup)
|
|
: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))"
|
|
(~tw :tokens "text-violet-600 hover:underline")
|
|
"spreads article")
|
|
" for the full mechanism.")))
|
|
(~docs/section
|
|
:title "Upward data flow"
|
|
:id "upward"
|
|
(~geography/scopes/scopes-demo-example
|
|
:demo (~geography/scopes/demo-scope-emit)
|
|
: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-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 "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."))
|
|
(~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"))
|
|
(p "The unification is not just code cleanup. It means:")
|
|
(ul
|
|
(~tw :tokens "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))"
|
|
(~tw :tokens "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)))"
|
|
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
|
|
"eval.sx")
|
|
". Platform primitives are declared in "
|
|
(a
|
|
:href "/sx/(language.(spec.(explore.boundary)))"
|
|
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
|
|
"boundary.sx")
|
|
" (Tier 5: Scoped effects).")))))
|