From b58d2ab470b13fe95dae536f82f1c628decea368 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sun, 6 Sep 2026 16:58:15 +0000 Subject: [PATCH] Record the settled level and generic-name decisions --- node-graph/rfcs/attribute-model.md | 97 +++++++++++++----------------- 1 file changed, 41 insertions(+), 56 deletions(-) diff --git a/node-graph/rfcs/attribute-model.md b/node-graph/rfcs/attribute-model.md index f51d9ed609..e960cd6679 100644 --- a/node-graph/rfcs/attribute-model.md +++ b/node-graph/rfcs/attribute-model.md @@ -102,37 +102,21 @@ table at graph compile time, which is where every resolution happens anyway, and two user-supplied names colliding at different value types is a graph compile error naming both nodes. -A write can also be generic over both the name and the value type. The -attribute then arrives on its own wire, as an input whose element is -`()`: - -```rs -/// Attaches the attribute to the content. -#[node_macro::node(category("Attributes"))] -fn set_attribute( - _: impl Ctx, - element: T, - (_, attr): ((), Attr>), -) -> (T, Attr>) { - (element, attr) -} -``` - -A unit value component means the edge exists and carries only its -attributes (`_: ()` still means no edge at all). The name enters the -graph at a source node holding the constant text input, whose output -type is filled at graph compile time, where user-supplied names join -the name table anyway; the compiler pairs `A` and `Y` through the wire -types, so the write set is derived from types alone, and the -one-name-one-type check covers the binding, making a declared name -targeted at a different type a graph compile error. A generic read -resolves only when the input wire's type determines the binding -uniquely, and anything else is a validation error. The node is one -compiled instance: `A` and `Y` instantiate with tokens and the value -rides the copy plan as a byte move, parked in the arena when its type -has drop glue, so no implementations list exists. A kernel that -computes on the value uses a bound and monomorphizes per its -implementations list as usual. +A write can also be generic over the name. The shape is settled but not +yet built: the marker is `Generic` with the value type declared +concretely in the signature, so `Attr>` and +`Attr>` are two independently named f64 writes and no +runtime type dispatch exists. The name itself arrives as a constant +text input on the document node and joins the name table at graph +compile time, which is where every resolution happens anyway; two +user-supplied names colliding at different value types is a graph +compile error naming both nodes, the same one-name-one-type check that +covers declared markers. A generic read resolves only when the binding +is unique, and anything else is a validation error. The node is one +compiled instance per concrete value type; the name never enters +monomorphization. The build is sequenced after the current cleanup, +and its editor-facing access has to be design-checked against the +typed attribute key API (#4352) before it lands. ## Reading and writing attributes @@ -387,7 +371,7 @@ A name's type is unique by construction. For declared markers the census admits one marker per name, checked when the registry is built. For user-supplied names the binding forms at graph compile time, carrying the marker's declared value type, and two names colliding at different types -is a graph compile error that names both nodes. Generic-typed writes +is a graph compile error that names both nodes. Generic-named writes join the same table, carrying the name and value type their bindings resolve to, so the check runs over declared markers, user-supplied names, and generic instantiations together. We do not attempt @@ -401,7 +385,9 @@ out, which keeps them stable when a structure node pushes a level level 0 is populated today: the packed-record tier is flat, so the binding rules below and the residency analysis that follows them are the intended design rather than the implemented one. The level in the key is -what leaves room for both. +what leaves room for both. Keying is frozen at level 0 for the parity +landing: no re-leveling exists, a level other than 0 is unsupported, +and catalog nodes assume level 0. The binding rules are: @@ -842,34 +828,33 @@ from shading languages for residency. # Unresolved questions - Leveled attributes, the largest open area. Attributes at more than one - nesting level are designed but not built: the layout key carries a - level, and nothing populates a level above 0. Open within it are the - binding rules as stated (does a read really bind to the top level of - the wire it is destructured from, and is pinning element-reading nodes - to level 0 the right rule), how a structure node's per-copy write - lands in the former top row, whether residency is worth its analysis - or whether the extent machinery already answers it (an attribute whose - index function ignores the index is `Free`), and what the storage for - a level above 0 looks like given that the record tier is flat. The - UX half of the same question is the map/enter construct: how "set on - the parent" and "map over the children" read differently in the graph. - Until this is settled, the Repeat-around-Opacity requirement is - unmet. + nesting level are designed but not built, and the keying is frozen at + level 0 for this landing, which parity does not need. Open within the + area are the binding rules as stated (does a read really bind to the + top level of the wire it is destructured from, and is pinning + element-reading nodes to level 0 the right rule), how a structure + node's per-copy write lands in the former top row, whether residency + is worth its analysis or whether the extent machinery already answers + it (an attribute whose index function ignores the index is `Free`), + and what the storage for a level above 0 looks like given that the + record tier is flat. The UX half of the same question is the + map/enter construct: how "set on the parent" and "map over the + children" read differently in the graph. Until this is settled, the + Repeat-around-Opacity requirement is unmet. - Where and how the combine rule is declared on the attribute marker. Merge push-down and flatten both consume it, and inner-wins is the intended fallback. - The spelling of the push-down marker on the merge node, the one part of the additive shape the extent override cannot express. -- Naming: `Attribute` trait vs. `Attr` wrapper. The authoring list type - is spelled `IList` here, as in the implementation, to keep it clear of - the legacy wire type's `List`; a rename to `List` is planned once that - type retires. -- Generic-typed writes: where the default for a generically written - name comes from (a `Default` bound on the value vs. an input on the - name source), what `A` instantiates to at the Rust level, whether - attribute-only wires carry exactly one attribute by construction or - uniqueness is checked per read, and the graph UX of the name source - node. +- Naming: the authoring list type is spelled `IList` here, as in the + implementation, to keep it clear of the legacy wire type's `List`; a + rename to `List` is planned once that type retires. +- Generic-named writes: the shape is settled (`Generic`, concrete + value type, name as a constant text input resolved at graph compile + time), and what remains open is where the default for a generically + written name comes from (a `Default` bound on the value vs. an input + on the name source) and the graph UX of the name source node, to be + design-checked against the typed attribute key API (#4352). - Whether evaluating at a lane outside the input's extent is clamped, wrapped, or a debug assertion. - `IList>` outputs, i.e. one node pushing two levels.