Fold attribute names out of their constant inputs when the graph compiles

`compute_layouts` resolves every name-from-input write against the text
its name input carries, so a folded meta is indistinguishable from a
marker node's and the layout holds a `&'static str` from there on. That
leaves nowhere for a name to be computed, which is the whole of the
rule: a name input that is not a constant is refused right here, per
node, with the path the editor pins its diagnostic to.

One name carries one value type, checked across the census and the
names a node folds together, so no graph can declare one field at two
widths. `compute_layouts` returns those refusals rather than panicking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dennis Kobert
2026-09-09 08:51:05 +00:00
parent 0001cdf04d
commit 348e9022a6
5 changed files with 157 additions and 3 deletions

View File

@@ -110,6 +110,42 @@ pub(crate) fn remove_attr_marker(ty: &Type) -> Option<Type> {
marker_of(ty, "RemoveAttr")
}
/// Splits a `Named<X, V>` marker into its placeholder and value type. A write
/// of one takes its name from the input the placeholder is declared at rather
/// than from the marker, so the name folds at graph compile time.
pub(crate) fn named_marker(ty: &Type) -> Option<(Type, Type)> {
let mut args = named_arguments(ty)?.into_iter();
let (placeholder, value) = (args.next()?, args.next()?);
args.next().is_none().then_some((placeholder, value))
}
/// The placeholder a `Named<X>` parameter declares. Such a parameter is the
/// name source for every `Attr<Named<X, _>>` the signature writes, and crosses
/// the wire as constant text.
pub(crate) fn named_source(ty: &Type) -> Option<Type> {
let mut args = named_arguments(ty)?.into_iter();
let placeholder = args.next()?;
args.next().is_none().then_some(placeholder)
}
fn named_arguments(ty: &Type) -> Option<Vec<Type>> {
let Type::Path(path) = ty else { return None };
let segment = path.path.segments.last()?;
if segment.ident != "Named" {
return None;
}
let PathArguments::AngleBracketed(args) = &segment.arguments else { return None };
Some(
args.args
.iter()
.filter_map(|argument| match argument {
GenericArgument::Type(ty) => Some(ty.clone()),
_ => None,
})
.collect(),
)
}
fn marker_of(ty: &Type, wrapper: &str) -> Option<Type> {
let Type::Path(path) = ty else { return None };
let segment = path.path.segments.last()?;