feat!: pass component children by reference, with ref/move markers to choose#223
Open
XX wants to merge 2 commits into
Open
feat!: pass component children by reference, with ref/move markers to choose#223XX wants to merge 2 commits into
XX wants to merge 2 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
This change grew out of a practical problem: shrinking the wasm bundle when
hypertextis used on the client.Every component generic over its children type is monomorphized once per nested
block at the call site. In an app with a tree of nested components this grows
combinatorially, and the wasm binary ends up carrying a separate copy of the
rendering code for close to every combination. Moving to
children: &dyn Renderablereplaces monomorphization with dynamic dispatch: onecopy of a component's code serves all call sites. In practice this cut the binary
size by several times.
For
&dyn Renderableto be usable as achildrenparameter type at all, themacro has to pass the block by reference — which is what the rest of this PR is
about.
Problem
When a component is invoked with a nested block,
maud!/rsx!compile that blockinto a
Lazy<_>and hand it to the component'schildrensetter. How it is handedover is hardcoded in the code generator, so it fits some components and not others:
children: &'a dyn Renderableneeds it by reference — that is the only waythe
&Lazy<_> → &dyn Renderableunsizing coercion applies;children: Lazy<fn(&mut Buffer)>needs it by value — the component owns theblock and can outlive the calling statement.
Both cannot be supported as things stand: any fixed choice rules out half of the
useful
childrensignatures. And dropping by-value passing is not an optioneither — it is what components that must own their children rely on, and on the
server the binary-size win does not matter.
Solution
The call site chooses, with a
reformovemarker given as the lastattribute of the component:
The markers behave identically in both syntaxes,
maud!andrsx!.With no marker, children are passed by reference. The new
children-movefeature flips the default to by value; explicit markers always override the
default, so both styles stay available in either configuration. A wasm build can
keep the by-reference default while server-side code turns on
children-move,with no change to the markup itself.
refandmoveare Rust keywords, so they can never be confused with anattribute name — a struct field cannot be called
reformoveeither.maud!already relies on the same property.
Diagnostics
Two errors, both with precise spans:
`ref` must be the last attribute of the component— when other attributesfollow the marker;
`move` requires a children block to apply to— when the marker is put on acomponent with no nested block (
<Component move />).Backward compatibility
The default changes from by-value to by-reference. Components that store children
by value (e.g.
Lazy<fn(&mut Buffer)>) will need an explicitmoveat the callsite after this change — or the
children-movefeature, which restores theprevious default across the dependency graph. Components taking
children: &Ror&dyn Renderablekeep working unchanged and without markers.Testing
crates/hypertext/tests/components.rs: tests forrefandmovein bothsyntaxes, for a component that only accepts by value (
Lazy<fn(&mut Buffer)>),and for one that only accepts by reference (
&dyn Renderable); the default iscovered by two tests made mutually exclusive with
#[cfg(feature = "children-move")].cargo test --test componentsandcargo test --test components --features children-move— 46 tests each, all green.#[renderable]gained a "childrenparameter type → required passingmode" table plus two working examples.