feat: codegen section, surface/component macros, proc-macro boundary rules

- build: harden build.rs vs proc-macro responsibilities
- codegen: specify generated slots/handles/forms/atoms APIs
- component: add #[slhx::surface] bridge + optional #[slhx::component]
- enforce: proc-macros are local, side-effect free, never parse .heml
This commit is contained in:
2026-05-10 11:42:56 +02:00
parent bd13d0fa0d
commit 705ca3c72b
+180 -6
View File
@@ -10,14 +10,111 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
## pitch
### req: pitch/001
001 slhx is checked hypermedia for Rust. Authors write HTML templates and Rust handlers. The compiler lowers every cross-file reference to a stable numeric id. The browser runtime only sees ids and effect bytes. [north_star]
001 slhx is checked hypermedia for Rust. Write `.heml`, write `#[slhx::handler]`, return commands. The compiler checks every cross-file reference. The browser runtime only sees ids and effect bytes. No JS app code required. [north_star]
### req: pitch/002
002 No CSS selectors. No hx-* strings. No virtual DOM. No client framework. No hidden global proxy magic. No hydration. [north_star]
002 No CSS selectors. No hx-* strings. No virtual DOM. No client framework. No hidden global proxy magic. No hydration. No user-authored JavaScript for standard forms, lists, navigation, or server push. [north_star]
### req: pitch/003
003 slhx replaces React/Vue not with a UI framework, but with a compiler contract: hemplate knows the surface, Rust knows the types, slhx knows the effects, the browser only executes commands. [north_star]
### req: pitch/004
004 The north-star feel: **Svelte at the call site, Rust at the boundary.** Short handler bodies, compile-checked HTML contracts, one language for server and client logic. [north_star]
---
## dx
### req: dx/001
001 The common case must feel like writing a Svelte/Vue component: template, state, handlers, and targeted updates. Users should not need to understand Surface IR, ResourceId, EffectWriter, postcard, or runtime opcodes for basic apps. [north_star]
### req: dx/002
002 The happy path is: write `.heml`, write `#[slhx::handler]`, return generated slot/atom commands. No manual ids, no manual registry, no manual serialization, no manual JavaScript. [north_star]
### req: dx/003
003 Public APIs are generated around the user's names. If the template declares `data-slhx-slot="todo_list"`, the user gets `slots::todo_list`, not `SlotId(12)`.
### req: dx/004
004 Common handlers must fit in a small function. Advanced contexts (`EffectWriter`, raw ops, custom encoders) exist but are not part of the beginner path.
### req: dx/005
005 Error messages must explain fixes in author language, not internal language. Say “add `key todo.id` to this loop”, not “missing ScopeKey for ResourceRef”.
### req: dx/006
006 Generated resource methods are the preferred authoring API: `slots::todo_list.render(view)`, `slots::card.replace(key, view)`, `slots::count.text(42)`, `atoms::user.set(user)`. These return `impl IntoEffect`. Raw `Effect::render`, `Effect::set`, and `EffectWriter` remain low-level. [north_star]
### req: dx/007
007 Tuple composition of `IntoEffect` is the canonical batch syntax: `(a, b, c)` implements `IntoEffect` up to arity 12. `Effect::batch((...))` is available but not required for the happy path.
### req: dx/008
008 User-authored JavaScript is never required for standard forms, lists, navigation, optimistic actions, or server push. Custom JS is only needed at opaque leaf boundaries such as charts, maps, editors, and Web Components.
---
## ceremony
### req: ceremony/001
001 A minimal counter app requires one `.heml` file, one Rust state struct, and one handler function. No manual registry, no manual route table, no manual JS. Under 50 lines of user-authored Rust plus one template.
### req: ceremony/002
002 Generated modules are imported through a prelude or component namespace. Users should not manually include `$OUT_DIR` files in normal apps.
### req: ceremony/003
003 `build.rs` must be a one-liner for the common case: `fn main() { slhx_build::app().run().unwrap(); }`
### req: ceremony/004
004 No API may require users to write numeric ids, raw ResourceIds, raw opcodes, or serialized payloads in normal code.
---
## progressive_disclosure
### req: pd/001
001 A beginner can build CRUD with only: `.heml`, `#[slhx::handler]`, `Form<T>`, generated `slots::*` methods, and `impl IntoEffect`.
### req: pd/002
002 Atoms are not required for basic server-first apps. They appear only when client-local state, SSR bootstrapped state, or WASM handlers are used.
### req: pd/003
003 Sync, transitions, resources/queries, islands, capabilities, and raw EffectWriter are advanced layers. They must not appear in starter examples.
### req: pd/004
004 Documentation must present three levels: server-first, client-local, hybrid-sync. Each level introduces only the new primitive it needs.
---
## component
### req: component/001
001 The primary authoring unit is a hemplate component plus adjacent Rust handlers. A component owns a template root, generated slots, generated handles, generated form checks, and source spans.
### req: component/002
002 slhx supports colocated layout: `todo_list.heml` beside `todo_list.rs`, with generated APIs namespaced by component to avoid global symbol soup.
### req: component/004
004 `#[slhx::surface]` bridges generated code into a user module. Users write `#[slhx::surface] mod ui {}` instead of `include!(concat!(env!("OUT_DIR"), ...))`. slhx-build emits `slhx.generated.rs` which the macro expands in place. No direct `$OUT_DIR` includes in user-authored source.
### req: component/005
005 An optional `#[slhx::component]` macro may validate that every handle declared in the template Surface has a corresponding `#[slhx::handler]` within the annotated module. This is the only macro with cross-handler visibility inside a single module; it remains strictly local. Missing handlers without `#[slhx::component]` are caught at app mount or test time, not `cargo check`.
---
## codegen
### req: codegen/001
001 `slhx_build` generates three artifacts from the generic Surface IR: (a) `slhx.generated.rs` containing ergonomic resource modules (`slots`, `handles`, `forms`, `atoms`), (b) `slhx.syms` for proc-macro validation, (c) runtime id-lowering tables. `slhx_build` interprets tool-specific conventions (`data-slhx-*`, `@for key`, form controls) from the Surface. [north_star]
### req: codegen/002
002 Generated module `slots` exposes ergonomic methods: `Slot<T>::render(value)`, `Slot<T>::text(value)`, `KeyedSlot<K,T>::append(key, value)`, `KeyedSlot<K,T>::replace(key, value)`, `KeyedSlot<K,T>::remove(key)`. Methods return `impl IntoEffect`.
### req: codegen/003
003 Generated module `handles` exports typed constants: `Handle<I>` where `I` is `Form<T>`, a param type, or `()`. Users rarely reference handles directly; they are consumed by `#[slhx::handler]` for validation.
### req: codegen/004
004 Generated module `forms` exports `FormContract` metadata (field names, HTML control kinds, required). `#[slhx::handler]` compares the `Form<T>` type against the contract. Domain types (`Email`, `TodoId`) remain user-authored; no auto-generated structs.
### req: codegen/005
005 Generated module `atoms` exports `Atom<T>` for values that must be addressable, hydrated, or synced. Ordinary Rust fields on app/components are not automatically atoms.
---
## invariant
@@ -80,13 +177,22 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
## build
### req: build/001
001 Build order: `.heml``hemplate_build``hemplate.surface.postcard``slhx_build``slhx.syms` + generated Rust constants.
001 Build order: `.heml``hemplate_build``hemplate.surface.postcard``slhx_build``slhx.generated.rs` + `slhx.syms` + diagnostics.
### req: build/002
002 `slhx-derive` (`#[slhx::handler]`) reads `slhx.syms` at expansion time to validate handle names, slot names, and form signatures.
002 Proc-macros (`#[slhx::handler]`, `#[slhx::surface]`) are side-effect free. They read generated artifacts (`slhx.syms`, `slhx.generated.rs`) but never parse `.heml`, never process generic Surface IR, and never write files. Global codegen lives only in `build.rs` invoked by `slhx_build`. [north_star]
### req: build/003
003 A `build.rs` failure (missing Surface, version mismatch, stale hash) is a hard error before proc-macro expansion.
003 `slhx-derive` (`#[slhx::handler]`) reads `slhx.syms` at expansion time to validate handle names, slot names, and form signatures. It generates only local glue (static fn-table entry) plus compile errors.
### req: build/004
004 `#slhx::surface]` reads `slhx.generated.rs` from `$OUT_DIR` and expands it into the annotated module. It is a pure include/bridge macro with no semantic analysis of its own.
### req: build/005
005 A `build.rs` failure (missing Surface, version mismatch, stale hash) is a hard error before proc-macro expansion.
### req: build/006
006 Proc-macros are considered local: they have knowledge of the item they annotate, plus pre-generated symbol tables. They do not have global knowledge of all handlers across the crate. Global checks (e.g. every declared handle has an implementation) are either deferred to app-mount tests or enabled by an optional `#[slhx::component]` macro.
---
@@ -99,7 +205,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
002 `EffectWriter` has a fixed canonical op set: `Set`, `Patch`, `Insert`, `Remove`, `Move`, `Focus`, `Navigate`, `Emit`. The DOM is one backend; core does not hardcode DOM operations. Wire format is canonical postcard opcodes; Rust API is flexible.
### req: effect/003
003 Multiple effects are combined with `Effect::batch((...))` or a method chain on `EffectWriter`. No `!` call-syntax macros.
003 Multiple effects are combined with tuple composition or a method chain on `EffectWriter`. No `!` call-syntax macros.
### req: effect/004
004 `Effect::render(slot, value)` is sugar for `Effect::set` on a Slot resource. `Effect::text(slot, value)` is sugar for `Effect::set` with a text payload.
@@ -110,6 +216,12 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
### req: effect/006
006 `Effect::event(name, payload)` dispatches a native `CustomEvent` on the root element. Core interprets the payload as opaque bytes. Web Components, charts, editors, or legacy JS may listen without slhx knowing about them. [north_star]
### req: effect/007
007 `Effect::event` is the raw escape hatch. Common UI events such as toast, dialog close, focus, and clipboard may have typed helper wrappers (`toast("Saved")`, `nav::push("/dashboard")`) in optional crates. Helpers compile to `Effect::event` or canonical ops.
### req: effect/008
008 Small semantic helper types may implement `IntoEffect`: `Toast`, `Nav`, `FormError`, generated slot commands, generated atom commands. Users compose them by returning tuples.
---
## state
@@ -126,6 +238,9 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
### req: state/004
004 SSR pages carry a `data-slhx-st` base64url-encoded postcard blob on the document root. Runtime decodes it into the client atom store. Atoms computed from server state are immediately available to client-side handlers without a round-trip.
### req: state/005
005 Not all state is an Atom. Ordinary Rust fields are preferred unless the value must be independently addressed, bootstrapped, synced, or subscribed. Atoms are explicit resources, not the default state container.
---
## form
@@ -297,6 +412,19 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
---
## client_local
### req: client_local/001
001 Client-local handlers use the same function shape as server handlers. Adding `client` changes the execution backend, not the authoring model.
### req: client_local/002
002 Local UI state may live as ordinary fields on the app/component state. `Atom<T>` is required only when the value must be addressed by effects, synced, hydrated, or subscribed.
### req: client_local/003
003 High-frequency UI handlers (drag, pointermove, animation tick) must not require server round-trips or handwritten JS.
---
## escape_hatch
### req: escape_hatch/001
@@ -304,6 +432,29 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
---
## view_model
### req: view/001
001 Slots render view types, not necessarily domain types. Domain-to-view conversion is explicit Rust (`From`, `Into`, or constructor). slhx never assumes a domain object is its own view.
### req: view/002
002 Generated slot types are allowed to target `Display`, `Hemplate`, or explicit view wrappers. Type errors should suggest the expected renderable view type.
---
## diagnostics
### req: diag/001
001 Every compile-time error must point to both sides of the mismatch when possible: the Rust handler span and the template Surface span.
### req: diag/002
002 Diagnostics must include a suggested fix for common cases: missing key, unknown slot, missing form field, optionality mismatch, handler param mismatch, wrong keyed slot type.
### req: diag/003
003 Internal terms (`ResourceId`, `ScopeKey`, `EffectBatch`) must not appear in beginner-facing diagnostics unless `--verbose` is enabled.
---
## derive_handler
### req: derive_handler/001
@@ -312,6 +463,16 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
### req: derive_handler/002
002 Handler param inference from `data-*` attributes: when a template declares `data-card-id="{card.id}"` on a node with `data-slhx-handle`, the handler may declare `card_id: CardId` as a parameter. slhx-build checks attribute → param name and type mapping.
### req: derive_handler/003
003 Handler parameters are inferred from four sources: `Form<T>`, `data-*` attributes on the triggering node, route params supplied by integration crates, and explicit app/context parameters. Missing or incompatible params are compile-time errors with source spans.
### req: derive_handler/004
004 The common handler signature forms are:
`fn h(form: Form<T>, app: &mut App) -> impl IntoEffect`
`fn h(id: Id, app: &mut App) -> impl IntoEffect`
`fn h(event: Event<T>, app: &mut App) -> impl IntoEffect`
`fn h(ctx: Ctx, ...) -> Result<impl IntoEffect, Error>`
---
## derive_app
@@ -328,6 +489,19 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
---
## examples
### req: examples/001
001 The repository must contain canonical examples that act as API tests: counter, todo CRUD, form wizard, realtime dashboard, local-first kanban.
### req: examples/002
002 Each example must have a maximum ceremony budget. The counter example must fit in under 50 lines of user-authored Rust plus one template. Todo CRUD must fit in under 150 lines excluding model definitions.
### req: examples/003
003 If an example requires raw EffectWriter, manual ids, manual JS, or manual registry setup, the API is considered too complex.
---
## milestone
### req: ms/001