mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 03:58:12 +08:00
Make the data model use Item and List types universally, with nodes authored as rank-polymorphic kernels (#4335)
* Add rank polymorphism node audit classifying all 271 nodes
* Implement StaticType for Item<T>
* Generate Item and mapped List wire variants for nodes declaring an Item<T> primary input
* Migrate nine nodes to Item element-wise kernels, dissolving the blending trait boilerplate
* Document the Item kernel implementation and staging plan
* Route Item<Vector> through TaggedValue::TypeDefault
* Add executor integration tests covering the Item and List wire variants
* Collapse element-wise Item/List wire pairs to the List form for conversion insertion
* Migrate sixteen vector modifier nodes to Item element-wise kernels
* Migrate Sample Image, Extend Image to Bounds, and Dehaze to Item element-wise kernels
* Fix bevel_with_transform test to actually exercise the transform attribute
* Implement From<T> for Item<T>
* Register PromoteNode rank adapters wrapping bare values into Item wires
* Insert PromoteNode adapters for Item/List wire pair fields in the preprocessor
* Define a real promote node backing the PromoteNode registry identifiers
* Zip ranked Item connectors by frame slot in the mapped element-wise variant
* Register ItemToListNode singleton raise adapters
* Resolve Item wires against List connectors by inserting promotion adapters at construction
* Rank the Offset Points distance connector and prove mixed-rank resolution end-to-end
* Implement Clampable for Item and List wires with per-variant clamp bounds
* Rank the Round Corners radius connector, exercising hard bounds on a ranked wire
* Implement ApplyTransform for Item
* Add Item wire implementations to the Transform node, keeping rank-0 chains rank 0
* Detect element-wise nodes by lazy primary connectors declaring Output = Item
* Convert Transform to an Item kernel with ranked parameters, delivering the broadcast milestone
* Rename Apply Transform to Bake Transform, baking item transforms on Vector, DAffine2, and DVec2
* Promote bare wires onto Item connectors at resolution via WrapItemNode adapters
* Rank the numeric, vector, and boolean parameters across the migrated element-wise nodes
* Rank the enum, integer, and seed parameters, registering their rank adapters via a consolidated macro
* Amend the audit with the DashPattern value type resolution
* Migrate the string family to Item element-wise kernels
* Unwrap Item wires into bare legacy connectors at resolution via UnwrapItemNode adapters
* Shadow owned node parameters in bodies instead of mut in signatures
* Migrate the math family and string measure nodes to Item element-wise kernels
* Convert the comparison and clamp nodes to Item kernels, dropping unreachable &str rows
* Flat-map expander kernels returning List under the mapped variant's frame
* Migrate the expander nodes to Item kernels flat-mapping under the frame
* Remove the unused peel_list helper
* Rank the raster adjustment and blending kernels, recontextualizing shader nodes onto an Item stand-in
Migrate the 16 adjustment nodes, Mix, Color Overlay, and Gradient Map from whole-List kernels to rank-0 Item kernels, letting the macro derive the List-mapped (zip) variants. Move the Adjust and Blend per-element seams off List onto the element types (add the Raster<CPU> impls, drop the now-dead List impls).
Shader nodes keep their bodies verbatim: PerPixelAdjust re-emits the identical kernel against a transparent no_std Item stand-in, so every Item<T> connector and .element() call resolves to a zero-cost identity on the GPU while the uniform buffer stays bare repr(C). The macro peels Item off ranked uniform params, wraps the fetched texel and uniforms at the entry point, and unwraps the result. This drops the shader_node/Item incompatibility guard. Register rank adapters for the adjustment enums.
* Update the rank polymorphism roadmap for the landed shader-node and adjustments chunk
* Rename the GPU Item stand-in to ShaderItem, aliased as Item at its shader-node import sites
* Flip the vector shape generators to emit rank-0 Item<Vector>
The shape generators (Rectangle, Circle, Ellipse, Arc, Spiral, Polygon, Star, Arrow, Line, Grid, QR Code) each produced exactly one shape wrapped in a singleton List<Vector>. Emit Item<Vector> directly so they connect to the rank-0 content connector of the migrated Transform node. Downstream List consumers receive the value through the existing Item to List promotion.
Relax the element-wise validation so a `()` (generator) primary may return Item<T> without being element-wise. Adapt the Repeat on Points test, which still takes a List content connector, by raising the generator's Item output through a singleton wrapper node.
* Parse ranked Item<T> parameter defaults against the bare element type
A ranked `Item<T>` parameter's default value is a bare, unranked `T` (promoted to the wire at resolution), but the preprocessor was handed the wrapped `Item<T>` type and could not parse the literal, flooding the console with warnings and dropping the defaults. Key the field's default_type metadata off the peeled element type for concrete ranked parameters, leaving generic `Item<T>` primaries and skip_impl nodes untouched.
* Parse an element-wise primary's scalar default against the bare element type
An element-wise node's primary reports its default_type as the List wire form so an unconnected primary defaults to an empty list. But when the primary carries a scalar `#[default]` (such as Root's radicand), that literal must parse as a bare element, not a List. Key the primary's default_type off the bare element type when it has a Default value source, keeping the List form otherwise.
* Add the DashPattern value type for stroke dash sequences
Introduce a rank-0 DashPattern value type (a Vec<f64> of alternating dash and gap lengths) so a stroke's dash pattern is a single frameable value rather than a rank-1 List<f64>. Register it as an auto-generated TaggedValue variant, parse its default from a comma or space separated string, and register its rank adapters. Not yet wired into the Stroke node.
* Rank the Fill and Stroke nodes element-wise and give Stroke a DashPattern connector
Migrate Fill and Stroke to element-wise Item<V> primaries (over Vector and Graphic element types) via a new element-level VectorItemMut trait, so styling one shape yields one shape and rank is preserved instead of promoting the input to a singleton List and emitting a List. The macro derives the List-mapped variant for genuine collections.
Wire the Stroke dash sequence to the new rank-0 DashPattern value type, collapsing the old content x paint x dash cartesian and dropping the IntoF64Vec trait. Update the stroke properties dash widget, the drawing tool, and graph-operation plumbing to read and write DashPattern, and migrate legacy F64Array, F64, and String dash inputs on document open.
Assign Colors stays a whole-collection node: each element's gradient position depends on its index among all siblings, which the element frame does not expose, so it keeps its List primary and the VectorListIterMut trait.
* Register rank adapters for the ranked Stroke enum parameters
The element-wise Stroke node ranks its align, cap, and paint order parameters as Item<StrokeAlign>, Item<StrokeCap>, and Item<PaintOrder>, but those enums lacked promotion adapters, so a bare default enum value could not be promoted to its Item wire and no Stroke variant resolved ("No construct found for node"). Register their rank adapters alongside StrokeJoin.
* Display Item wires in the Data panel without a List's ID column
Add a TableItemLayout impl for Item<T> and recognize Item wire types when introspecting graph data. An Item holds a single element, so it renders as a one-row table of the element plus its attributes with no leading index column, and it labels as its element type T rather than a List's T[]. Add ItemAttributeValues::get_any for the attribute widget dispatch.
* Register MonitorNode for Item wire types so the Data panel introspects them directly
Graph introspection wraps the inspected output in a generic MonitorNode typed to the wire. Without Item<T> monitor registrations, an Item<Vector> output could only be monitored after an Item to List promotion, so the Data panel captured and displayed a List<Vector> despite the connector being Item<Vector>. Register monitors for the Item types the element-wise nodes emit, and add the matching Data panel downcast entries.
* Color and double Item/List wires and cleave layer-stack connectors in the node graph
* Route wire color and rank through hidden nodes and refresh them on type changes
* Rework the DashPattern connector conversions with element-wise promotion and an explicit reducer node
* Rank the remaining value, context, aggregation, and transform nodes onto Item<T> wires
* Back DashPattern with a List<f64> so the Data panel can introspect its lengths
* Carry a single Item<T> through varargs so the Read context nodes emit Item<T> not List<T>
* Relax rank validation for aggregation shapes, add element adapters, and match variants by fewest promotions
* Rank the remaining bare and unnecessarily-List connectors across the node catalog
* Add Graphic::None and the FillChoice paint value, making colors and gradients plain values
* Rename GradientStops to Gradient and the legacy Gradient/Fill structs to LegacyGradient/LegacyFill
* Restore generator frame-from-params ranking to the roadmap as a planned stage
* Rename the ranked-field adapter identifier from PromoteNode to FieldAdapterNode to reflect its full contract
* Unload only the wires whose displayed style changed when types update
* Peel wire rank in the editor's semantic type checks so rank-0 layers are recognized
* Restore the whole-List Transform variant so rank-1 content wires resolve again
* Register the Item wire forms for the Memoize and Context Modification infrastructure nodes
* Give every ranked connector a field adapter and add numeric cast variants for legacy wires
* Key a ranked param's type default off its Item wire form when no literal default exists
* Inherit the layer's content value when splicing a node into an empty chain
* Migrate stale List-form TypeDefault inputs to the definition's current default
* Generate the mapped wire variant only when the element-wise node has a frame source
* Let a bare wire feed a List connector via a wrap-raise adapter, costed as two rank steps
* Add a zip companion to the whole-List Transform so ranked List parameters pair per slot
* Add the Sum, Average, Minimum, Maximum, Any, and All list reducers
* Convert the measure family to element-wise Item kernels per the audit classification
* Prefer the bare element value over the Item type default so ranked params keep their widgets
* Rename GradientStopsUI to GradientUI
* Split Fill's optional transform into a _has_transform bool and a ranked _transform matrix
* Rename the migration-only OptionalDAffine2 TaggedValue to LegacyOptionalDAffine2
* Flow byte buffers as Item<Resource> instead of List<u8> across the byte nodes
* Macro-generate the list-content wire variant, retiring the hand-written Transform-zip, Area, and Centroid companions
* Let ()-primary generators take ranked params and frame over them via the mapped variant, ranking Circle's radius
* Rank the vector shape generators' params to Item, adding a rank-aware input grab to the introspection harness
* Rank the value, color, and text generator params to Item
* Rank the raster, web-request, and context-reader generator params to Item
* Fix the repeat and brush test wirings left behind by the param-ranking sweeps
* Delete the vestigial Some, Unwrap Option, and Size Of debug nodes
* Delete the Attach Attribute node, folding its role into Write Attribute
* Add the Filter and Sort list companion nodes
* Guard the removed-definition migration swap target with a test
* Add the Box Corners value type in place of the rectangle corner radius list
* Split Text to Vector's per-glyph mode into a Text to Vector Glyphs node
* Rank the Combine Channels node's channel connectors to Item
* Make Map Points an element-wise node
* Delete the deprecated Upload Texture node
* Update the implementation roadmap to reflect the landed stages
* Let monitor introspection read rank-0 wires, locking in the layer coercion promotion path
* Prefer the rank-0 default when disconnecting a rank-capable input
* Make Path Modify an element-wise node
* Wrap node paths in a NodeIdPath newtype so they flow as a single Item
* Give Item<Raster<CPU>> a default so an unconnected Brush background resolves
* Stop the Brush node from setting layer attributes its paint operation doesn't produce
* Present-gate Flatten Path's adopted layer path like its fill and stroke
* Gate carried layer attributes on static column presence, not runtime values
* Give the remaining graphic Item<T> types a default so unconnected primaries resolve
* Dispatch a ranked param's Properties widget from its rank-0 element type
* Make Extract Transform an element-wise node, restoring the Origins to Polyline body
* Rename Flatten Path to Combine Paths
* Stamp Legacy Layer Extend's adopted layer path as a readable NodeIdPath
* Drop the dead List<u8> and List<NodeId> wire rows
* Rank Flatten Graphic's Fully Flatten toggle to Item
* Update the implementation roadmap with the endgame scope
* Make Combine Paths a reducer that collapses the whole frame into one path
* Stop type-converter nodes from carrying the source's unrelated attributes
* Format the Origins to Polyline regression test
* Wrap the Brush node's trace in a BrushTrace newtype so it flows as one value
* Make Switch a framed element-wise select, bundling whole collections
* Widen and align element-type coverage across the list and graphic nodes
* Register the compiler's cache chain pair for every ranked enum and newtype wire
* Fix wire colors for Passthrough outputs, bundled lists, and bools, and widen list wires
* Represent List wire types structurally with Type::List, replacing name-parsed rank promotion
* Treat scope and data fields as environment, rank scope wires as Item, and feed the render boundary through a context vararg
* Delete the vestigial Clone debug node
* Reinstate Upload Texture as an element-wise node and fix the GPU variants' scope executor and rank adapters
* Rename Combine Paths back to Flatten Path, deferring that rename to its own PR
* Deduplicate the promotion adapter registrations into the field adapter macro
* Rank Write Attribute's value connector to Item<AttributeValueDyn>, retiring the UnwrapItem bridge
* Vertical wire styling
* Store the editor layer path attribute as a bare NodeIdPath, not an Item<NodeIdPath>
* Rank Context Modification's features connector to Item<ContextFeatures>, dropping the dead memoize row
* Rank Path Modify's modification parameter to Item<Box<VectorModification>>
* Rename the field adapter node family to input adapter
* Drop the dead bare scalar rows from Context Modification's implementations list
* Move the dynamic executor's test module into its own file
* Drop the registry's unreachable bare rows for Memoize, the cache chain, and ConvertNode
* Materialize stored TaggedValues as ranked Item wires at the source
* Remove the bare-wire promotion and adapter machinery made dead by ranked value materialization
* Plant the input adapter for List-only inputs, composing position conversion from standard rows
* Consolidate Into/Convert conversions into the input adapter umbrella and rename the rank adapter identifiers
* Fix grouped layers gaining a phantom None stack element from the FillChoice default hijacking every List<Graphic> disconnect
* Enforce ranked node inputs in the macro, rejecting bare wire declarations
* Remove the unit Context => () machinery rows, leaving () purely as the no-primary sentinel
* Add a --signatures rank-audit mode to node-docs for the ranked-wire migration
* Remove the node-docs --signatures rank-audit mode now that ranked wires are enforced
* Migrate legacy no-color values on the Black & White, Color Overlay, and Empty Image color inputs
* Rewrite the element-wise accessor wire type at the primary input, not raw index 0
* Register the cache chain for Resource wires, replacing the lone hand-written Monitor row
* Gate the remaining Raster<GPU> registry rows behind the gpu feature
* Let List<DVec2> wires erase to ListDyn for the attribute reader and element counter
* Rename Extract Element to Item at Index, Count Elements to List Length, and Omit Element to Remove at Index
* Store paint picks as plain color/gradient values, removing the FillChoice value type
* Code review restructuring
* Sort by the consumed sort_key attribute or natural element order, adding the Sort Key node
* Remove the new list-combinator and reducer nodes to defer them to a follow-up PR
* Parse Fill and Stroke color defaults through the paint wire's Graphic element
* Emit ranked implementation-row default types structurally so their element TypeIds survive to default-literal parsing
* Exempt the deliberate no-paint choice from the stale List-form TypeDefault migration
* Migrate the legacy 4-input Fill directly to the split has-transform shape
* Upgrade the demo artwork
* Fix the valid AI review findings: Item eq/hash contract, table-era no-paint migration, quantize List rows, and other smaller issues
* Remove the rank polymorphism working documents
* Hash Item attribute values directly instead of debug-formatting them, speeding up cached evaluation
* Replace the data panel's dead bare-wire downcast arms with full coverage of the ranked monitor row types
* Derive PartialEq for Item now that attributes participate in equality
* Extend the data panel's attribute dispatchers with the newly supported scalar and choice enum types
* Add List monitor rows for the framed numeric conversion outputs so inspecting them resolves, with matching data panel arms
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "node-macro"
|
||||
publish = false
|
||||
version = "0.0.0"
|
||||
rust-version = "1.88"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
edition = "2024"
|
||||
readme = "../../README.md"
|
||||
homepage = "https://graphite.art"
|
||||
repository = "https://github.com/GraphiteEditor/Graphite"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
# Workspace dependencies
|
||||
syn = { workspace = true }
|
||||
proc-macro2 = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
convert_case = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
indoc = { workspace = true }
|
||||
|
||||
proc-macro-crate = "3.1.0"
|
||||
proc-macro-error2 = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
core-types = { workspace = true }
|
||||
no-std-types = { workspace = true }
|
||||
@@ -0,0 +1,263 @@
|
||||
use crate::crate_ident::CrateIdent;
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{ToTokens, format_ident, quote};
|
||||
use std::collections::HashSet;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::visit_mut::VisitMut;
|
||||
use syn::{Fields, GenericParam, Generics, Item, ItemEnum, ItemStruct, Meta, MetaList, Path, PathSegment, Result, Token, TypeParam, TypeParamBound, visit_mut};
|
||||
|
||||
pub fn derive_buffer_struct(crate_ident: &CrateIdent, content: proc_macro::TokenStream) -> Result<TokenStream> {
|
||||
let item = syn::parse::<Item>(content)?;
|
||||
match &item {
|
||||
Item::Enum(item) => derive_buffer_struct_enum(crate_ident, item),
|
||||
Item::Struct(item) => derive_buffer_struct_struct(crate_ident, item),
|
||||
_ => Err(syn::Error::new_spanned(&item, "Expected a struct or an enum")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_buffer_struct_enum(crate_ident: &CrateIdent, item: &ItemEnum) -> Result<TokenStream> {
|
||||
let gcore_shaders = crate_ident.gcore_shaders()?;
|
||||
let mod_buffer_struct = quote!(#gcore_shaders::shaders::buffer_struct);
|
||||
let reexport = quote!(#gcore_shaders::shaders::__private);
|
||||
|
||||
if !item.generics.params.is_empty() {
|
||||
return Err(syn::Error::new_spanned(&item.generics, "enum must not have any generics"));
|
||||
}
|
||||
|
||||
let enum_requirements_error = || {
|
||||
syn::Error::new(
|
||||
Span::call_site(),
|
||||
"deriving `BufferStruct` on an enum requires `#[repr(u32)]` and `#[derive(num_enum::FromPrimitive, num_enum::IntoPrimitive)]`",
|
||||
)
|
||||
};
|
||||
let repr_path = Path::from(format_ident!("repr"));
|
||||
let repr = item
|
||||
.attrs
|
||||
.iter()
|
||||
.filter_map(|a| match &a.meta {
|
||||
Meta::List(MetaList { path, tokens, .. }) if *path == repr_path => Some(tokens),
|
||||
_ => None,
|
||||
})
|
||||
.next()
|
||||
.ok_or_else(enum_requirements_error)?;
|
||||
|
||||
let ident = &item.ident;
|
||||
Ok(quote! {
|
||||
unsafe impl #mod_buffer_struct::BufferStruct for #ident
|
||||
{
|
||||
type Buffer = #repr;
|
||||
|
||||
fn write(from: Self) -> Self::Buffer {
|
||||
<#repr as From<Self>>::from(from)
|
||||
}
|
||||
|
||||
fn read(from: Self::Buffer) -> Self {
|
||||
<Self as #reexport::num_enum::FromPrimitive>::from_primitive(from)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// see [`BufferStruct`] docs
|
||||
///
|
||||
/// This is also largely copied from my (@firestar99) project and adjusted
|
||||
///
|
||||
/// [`BufferStruct`]: `no_std_types::shaders::buffer_struct::BufferStruct`
|
||||
pub fn derive_buffer_struct_struct(crate_ident: &CrateIdent, item: &ItemStruct) -> Result<TokenStream> {
|
||||
let gcore_shaders = crate_ident.gcore_shaders()?;
|
||||
let mod_buffer_struct = quote!(#gcore_shaders::shaders::buffer_struct);
|
||||
let reexport = quote!(#gcore_shaders::shaders::__private);
|
||||
|
||||
let generics = item
|
||||
.generics
|
||||
.params
|
||||
.iter()
|
||||
.filter_map(|g| match g {
|
||||
GenericParam::Lifetime(_) => None,
|
||||
GenericParam::Type(t) => Some(t.ident.clone()),
|
||||
GenericParam::Const(c) => Some(c.ident.clone()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut members_buffer = Punctuated::<TokenStream, Token![,]>::new();
|
||||
let mut write = Punctuated::<TokenStream, Token![,]>::new();
|
||||
let mut read = Punctuated::<TokenStream, Token![,]>::new();
|
||||
let mut gen_name_gen = GenericNameGen::new();
|
||||
let mut gen_ref_tys = Vec::new();
|
||||
let (members_buffer, write, read) = match &item.fields {
|
||||
Fields::Named(named) => {
|
||||
for f in &named.named {
|
||||
let name = f.ident.as_ref().unwrap();
|
||||
let mut ty = f.ty.clone();
|
||||
let mut visitor = GenericsVisitor::new(&item.ident, &generics);
|
||||
visit_mut::visit_type_mut(&mut visitor, &mut ty);
|
||||
if visitor.found_generics {
|
||||
gen_ref_tys.push(f.ty.clone());
|
||||
let gen_ident = gen_name_gen.next();
|
||||
members_buffer.push(quote!(#name: #gen_ident));
|
||||
} else {
|
||||
members_buffer.push(quote! {
|
||||
#name: <#ty as #mod_buffer_struct::BufferStruct>::Buffer
|
||||
});
|
||||
}
|
||||
|
||||
write.push(quote! {
|
||||
#name: <#ty as #mod_buffer_struct::BufferStruct>::write(from.#name)
|
||||
});
|
||||
read.push(quote! {
|
||||
#name: <#ty as #mod_buffer_struct::BufferStruct>::read(from.#name)
|
||||
});
|
||||
}
|
||||
(quote!({#members_buffer}), quote!(Self::Buffer {#write}), quote!(Self {#read}))
|
||||
}
|
||||
Fields::Unnamed(unnamed) => {
|
||||
for (i, f) in unnamed.unnamed.iter().enumerate() {
|
||||
let mut ty = f.ty.clone();
|
||||
let mut visitor = GenericsVisitor::new(&item.ident, &generics);
|
||||
visit_mut::visit_type_mut(&mut visitor, &mut ty);
|
||||
if visitor.found_generics {
|
||||
gen_ref_tys.push(f.ty.clone());
|
||||
members_buffer.push(gen_name_gen.next().into_token_stream());
|
||||
} else {
|
||||
members_buffer.push(quote! {
|
||||
<#ty as #mod_buffer_struct::BufferStruct>::Buffer
|
||||
});
|
||||
}
|
||||
|
||||
let index = syn::Index::from(i);
|
||||
write.push(quote! {
|
||||
<#ty as #mod_buffer_struct::BufferStruct>::write(from.#index)
|
||||
});
|
||||
read.push(quote! {
|
||||
<#ty as #mod_buffer_struct::BufferStruct>::read(from.#index)
|
||||
});
|
||||
}
|
||||
(quote!((#members_buffer);), quote!(Self::Buffer(#write)), quote!(Self(#read)))
|
||||
}
|
||||
Fields::Unit => (quote!(;), quote!(let _ = from; Self::Buffer {}), quote!(let _ = from; Self::Shader {})),
|
||||
};
|
||||
|
||||
let generics_decl = &item.generics;
|
||||
let generics_ref = decl_to_ref(item.generics.params.iter());
|
||||
let generics_where = gen_ref_tys
|
||||
.iter()
|
||||
.map(|ty| quote!(#ty: #mod_buffer_struct::BufferStruct))
|
||||
.collect::<Punctuated<TokenStream, Token![,]>>()
|
||||
.into_token_stream();
|
||||
|
||||
let generics_decl_any = gen_name_gen.decl(quote! {
|
||||
#reexport::bytemuck::Pod + Send + Sync
|
||||
});
|
||||
let generics_ref_buffer = gen_ref_tys
|
||||
.iter()
|
||||
.map(|ty| quote!(<#ty as #mod_buffer_struct::BufferStruct>::Buffer))
|
||||
.collect::<Punctuated<TokenStream, Token![,]>>()
|
||||
.into_token_stream();
|
||||
|
||||
let vis = &item.vis;
|
||||
let ident = &item.ident;
|
||||
let buffer_ident = format_ident!("{}Buffer", ident);
|
||||
Ok(quote! {
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, #reexport::bytemuck::Zeroable, #reexport::bytemuck::Pod)]
|
||||
#vis struct #buffer_ident #generics_decl_any #members_buffer
|
||||
|
||||
unsafe impl #generics_decl #mod_buffer_struct::BufferStruct for #ident #generics_ref
|
||||
where
|
||||
#ident #generics_ref: Copy,
|
||||
#generics_where
|
||||
{
|
||||
type Buffer = #buffer_ident <#generics_ref_buffer>;
|
||||
|
||||
fn write(from: Self) -> Self::Buffer {
|
||||
#write
|
||||
}
|
||||
|
||||
fn read(from: Self::Buffer) -> Self {
|
||||
#read
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
struct GenericsVisitor<'a> {
|
||||
self_ident: &'a Ident,
|
||||
generics: &'a HashSet<Ident>,
|
||||
found_generics: bool,
|
||||
}
|
||||
|
||||
impl<'a> GenericsVisitor<'a> {
|
||||
pub fn new(self_ident: &'a Ident, generics: &'a HashSet<Ident>) -> Self {
|
||||
Self {
|
||||
self_ident,
|
||||
generics,
|
||||
found_generics: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VisitMut for GenericsVisitor<'_> {
|
||||
fn visit_ident_mut(&mut self, i: &mut Ident) {
|
||||
if self.generics.contains(i) {
|
||||
self.found_generics = true;
|
||||
}
|
||||
visit_mut::visit_ident_mut(self, i);
|
||||
}
|
||||
|
||||
fn visit_path_segment_mut(&mut self, i: &mut PathSegment) {
|
||||
if i.ident == "Self" {
|
||||
i.ident = self.self_ident.clone();
|
||||
}
|
||||
visit_mut::visit_path_segment_mut(self, i);
|
||||
}
|
||||
}
|
||||
|
||||
struct GenericNameGen(u32);
|
||||
|
||||
impl GenericNameGen {
|
||||
pub fn new() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Ident {
|
||||
let i = self.0;
|
||||
self.0 += 1;
|
||||
format_ident!("T{}", i)
|
||||
}
|
||||
|
||||
pub fn decl(self, ty: TokenStream) -> Generics {
|
||||
let params: Punctuated<GenericParam, Token![,]> = (0..self.0)
|
||||
.map(|i| {
|
||||
GenericParam::Type(TypeParam {
|
||||
attrs: Vec::new(),
|
||||
ident: format_ident!("T{}", i),
|
||||
colon_token: Some(Default::default()),
|
||||
bounds: Punctuated::from_iter([TypeParamBound::Verbatim(ty.clone())]),
|
||||
eq_token: None,
|
||||
default: None,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if !params.is_empty() {
|
||||
Generics {
|
||||
lt_token: Some(Default::default()),
|
||||
params,
|
||||
gt_token: Some(Default::default()),
|
||||
where_clause: None,
|
||||
}
|
||||
} else {
|
||||
Generics::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decl_to_ref<'a>(generics: impl Iterator<Item = &'a GenericParam>) -> TokenStream {
|
||||
let out = generics
|
||||
.map(|generic| match generic {
|
||||
GenericParam::Lifetime(l) => l.lifetime.to_token_stream(),
|
||||
GenericParam::Type(t) => t.ident.to_token_stream(),
|
||||
GenericParam::Const(c) => c.ident.to_token_stream(),
|
||||
})
|
||||
.collect::<Punctuated<TokenStream, Token![,]>>();
|
||||
if out.is_empty() { TokenStream::new() } else { quote!(<#out>) }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,804 @@
|
||||
use super::*;
|
||||
|
||||
/// How a record node's primary input lowers: `None` writes a fresh record,
|
||||
/// `Token` carries the element bytes through as `ElToken`, `Read` reads a
|
||||
/// concrete element at offset 0, and `LazyToken` is a derive-routing carrier
|
||||
/// the kernel evaluates itself, returning the row token it received. The
|
||||
/// element and write set fold from the IR.
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum RecordCarrier {
|
||||
None,
|
||||
Token,
|
||||
Read,
|
||||
LazyToken,
|
||||
}
|
||||
|
||||
/// A well-formed record-io node: only the carrier form is retained, so
|
||||
/// [`skips_carrier`] can gate the fresh-record path. Malformed record io yields
|
||||
/// `None` from [`record_shape`] and generates no node impl.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RecordShape {
|
||||
pub(crate) carrier: RecordCarrier,
|
||||
}
|
||||
|
||||
impl RecordShape {
|
||||
pub(crate) fn skips_carrier(&self) -> bool {
|
||||
matches!(self.carrier, RecordCarrier::None)
|
||||
}
|
||||
}
|
||||
|
||||
/// The effect/return axis of a node's kernel, resolved once from the signature.
|
||||
/// It selects the eval tail (finish / merge / spawn) and the kernel signature
|
||||
/// wrapping across every node kind.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) enum Dialect {
|
||||
Sync,
|
||||
Interrupt,
|
||||
Poll,
|
||||
AsyncFn,
|
||||
Future,
|
||||
FutureInterrupt,
|
||||
}
|
||||
|
||||
pub(crate) fn dialect(parsed: &ParsedNodeFn) -> Dialect {
|
||||
if parsed.is_async {
|
||||
return Dialect::AsyncFn;
|
||||
}
|
||||
match kernel_kind(&parsed.output_type) {
|
||||
KernelKind::Plain => Dialect::Sync,
|
||||
KernelKind::Interrupt(_) => Dialect::Interrupt,
|
||||
KernelKind::Poll(_) => Dialect::Poll,
|
||||
KernelKind::Future(_) => Dialect::Future,
|
||||
KernelKind::FutureInterrupt(_) => Dialect::FutureInterrupt,
|
||||
}
|
||||
}
|
||||
|
||||
/// The dialect of a node fn that lowers to a `Node` impl, or `None` when no
|
||||
/// lowering supports the signature (an async node with lazy inputs, malformed
|
||||
/// record io, or a shape no kind accepts). The kind itself is derived from the
|
||||
/// intent IR ([`crate::codegen::ir::node_kind`]); this only gates support.
|
||||
pub(crate) fn analyze(parsed: &ParsedNodeFn) -> Option<Dialect> {
|
||||
if parsed.is_async && parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) {
|
||||
return None;
|
||||
}
|
||||
let supported = if record_shape(parsed).is_some() {
|
||||
true
|
||||
} else if has_record_io(parsed) {
|
||||
return None;
|
||||
} else {
|
||||
routing_io(parsed).is_some() || record_flip(parsed) || record_opaque(parsed) || has_materialized_input(parsed)
|
||||
};
|
||||
supported.then(|| dialect(parsed))
|
||||
}
|
||||
|
||||
/// The tail form of a node's eval, selected from its class and dialect: forward
|
||||
/// the kernel's own record, assemble a record (io or flip carrier), or spawn a
|
||||
/// source and lift its completion.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum Tail {
|
||||
Forward,
|
||||
Record,
|
||||
Flip,
|
||||
SpawnAsyncFn,
|
||||
SpawnFuture,
|
||||
}
|
||||
|
||||
/// One statement group of a node's `eval` body, lowered in order: the input
|
||||
/// binds first (one per input), then the numeric clamps, then the tail that
|
||||
/// assembles the output record and closes the dialect.
|
||||
pub(crate) enum EvalStep<'a> {
|
||||
Bind(usize, &'a ParsedField),
|
||||
Clamp(&'a ParsedField),
|
||||
Tail(Tail),
|
||||
}
|
||||
|
||||
/// Whether the signature declares record-tier attribute io: value-input reads
|
||||
/// or return-tuple writes. Reads on lazy inputs belong to the record lowering
|
||||
/// of the flip class instead.
|
||||
pub(crate) fn has_record_io(parsed: &ParsedNodeFn) -> bool {
|
||||
let value_reads = parsed.fields.iter().any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Regular(_)));
|
||||
value_reads || record_writes(&slot_value_type(&parsed.output_type)).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn has_lazy_reads(parsed: &ParsedNodeFn) -> bool {
|
||||
parsed.fields.iter().any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Node(_)))
|
||||
}
|
||||
|
||||
/// The value inputs of a routing node (every regular field that is neither a
|
||||
/// routing source nor a ranked whole-list input), with their indices into the
|
||||
/// regular fields.
|
||||
pub(crate) fn routing_value_indices(regular_fields: &[&ParsedField], generic: &Ident) -> Vec<usize> {
|
||||
regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, field)| match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, list_levels, .. }) => *list_levels == 0 && !matches!(ty, Type::Path(path) if path.path.get_ident() == Some(generic)),
|
||||
ParsedFieldType::Node(_) => false,
|
||||
})
|
||||
.map(|(index, _)| index)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The lazy inputs declaring attribute reads, with their indices into the
|
||||
/// regular fields.
|
||||
pub(crate) fn lazy_read_fields<'a>(regular_fields: &[&'a ParsedField]) -> Vec<(usize, &'a ParsedField)> {
|
||||
regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, field)| matches!(field.ty, ParsedFieldType::Node(_)) && !field.attribute_reads.is_empty())
|
||||
.map(|(index, field)| (index, *field))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The indices (into the regular fields) of value inputs whose
|
||||
/// reads resolve against their own input rather than the carrier's.
|
||||
pub(crate) fn reading_secondary_indices(regular_fields: &[&ParsedField], skips_carrier: bool) -> Vec<usize> {
|
||||
regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, field)| !field.attribute_reads.is_empty() && (skips_carrier || *index != 0))
|
||||
.map(|(index, _)| index)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Every attribute read in field order with the owning field's index, flat so
|
||||
/// read slots are numbered across inputs.
|
||||
pub(crate) fn field_reads<'a>(regular_fields: &[&'a ParsedField]) -> Vec<(usize, &'a AttributeRead)> {
|
||||
regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, field)| field.attribute_reads.iter().map(move |read| (index, read)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Substitutes bare generic idents with their row-assigned types.
|
||||
pub(crate) fn substitute_ident_types(ty: &Type, assignments: &[(Ident, Type)]) -> Type {
|
||||
struct Subst<'a> {
|
||||
assignments: &'a [(Ident, Type)],
|
||||
}
|
||||
|
||||
impl VisitMut for Subst<'_> {
|
||||
fn visit_type_mut(&mut self, ty: &mut Type) {
|
||||
if let Type::Path(path) = ty
|
||||
&& path.qself.is_none()
|
||||
&& let Some(ident) = path.path.get_ident()
|
||||
&& let Some((_, replacement)) = self.assignments.iter().find(|(generic, _)| generic == ident)
|
||||
{
|
||||
*ty = replacement.clone();
|
||||
return;
|
||||
}
|
||||
syn::visit_mut::visit_type_mut(self, ty);
|
||||
}
|
||||
}
|
||||
|
||||
let mut ty = ty.clone();
|
||||
Subst { assignments }.visit_type_mut(&mut ty);
|
||||
ty
|
||||
}
|
||||
|
||||
/// Replaces the routing generic in a derive-routing kernel's return type with
|
||||
/// the routing record value, since the kernel's inputs rebind to '__record.
|
||||
pub(crate) fn substitute_routing_record(output: &Type, generic: &Ident, core_types: &TokenStream2) -> Type {
|
||||
struct Subst<'a> {
|
||||
generic: &'a Ident,
|
||||
replacement: Type,
|
||||
}
|
||||
|
||||
impl VisitMut for Subst<'_> {
|
||||
fn visit_type_mut(&mut self, ty: &mut Type) {
|
||||
if let Type::Path(path) = ty
|
||||
&& path.qself.is_none()
|
||||
&& path.path.get_ident() == Some(self.generic)
|
||||
{
|
||||
*ty = self.replacement.clone();
|
||||
return;
|
||||
}
|
||||
syn::visit_mut::visit_type_mut(self, ty);
|
||||
}
|
||||
}
|
||||
|
||||
let mut ty = output.clone();
|
||||
let mut subst = Subst {
|
||||
generic,
|
||||
replacement: syn::parse_quote!(#core_types::record::RecordValue<'__record>),
|
||||
};
|
||||
subst.visit_type_mut(&mut ty);
|
||||
ty
|
||||
}
|
||||
|
||||
pub(crate) fn inject_attr_lifetimes(output: &Type, lifetime: &str) -> Option<Type> {
|
||||
struct Injector<'a> {
|
||||
changed: bool,
|
||||
lifetime: &'a str,
|
||||
}
|
||||
|
||||
impl VisitMut for Injector<'_> {
|
||||
fn visit_path_segment_mut(&mut self, segment: &mut syn::PathSegment) {
|
||||
if segment.ident == "Attr"
|
||||
&& let PathArguments::AngleBracketed(args) = &mut segment.arguments
|
||||
&& !args.args.iter().any(|arg| matches!(arg, GenericArgument::Lifetime(_)))
|
||||
{
|
||||
args.args.insert(0, GenericArgument::Lifetime(Lifetime::new(self.lifetime, proc_macro2::Span::call_site())));
|
||||
self.changed = true;
|
||||
}
|
||||
syn::visit_mut::visit_path_segment_mut(self, segment);
|
||||
}
|
||||
}
|
||||
|
||||
let mut ty = output.clone();
|
||||
let mut injector = Injector { changed: false, lifetime };
|
||||
injector.visit_type_mut(&mut ty);
|
||||
injector.changed.then_some(ty)
|
||||
}
|
||||
|
||||
/// Binds a `Lane` output to the kernel's subject lifetime, replacing whatever
|
||||
/// the author spelled: the lane borrows the materialized subject, not the arena.
|
||||
pub(crate) fn inject_lane_lifetime(output: &Type) -> Option<Type> {
|
||||
struct Injector {
|
||||
changed: bool,
|
||||
}
|
||||
|
||||
impl VisitMut for Injector {
|
||||
fn visit_path_segment_mut(&mut self, segment: &mut syn::PathSegment) {
|
||||
if segment.ident == "Lane"
|
||||
&& let PathArguments::AngleBracketed(args) = &mut segment.arguments
|
||||
{
|
||||
let kept: Vec<GenericArgument> = args.args.iter().filter(|arg| !matches!(arg, GenericArgument::Lifetime(_))).cloned().collect();
|
||||
args.args = kept.into_iter().collect();
|
||||
args.args.insert(0, GenericArgument::Lifetime(Lifetime::new("'__lane", proc_macro2::Span::call_site())));
|
||||
self.changed = true;
|
||||
}
|
||||
syn::visit_mut::visit_path_segment_mut(self, segment);
|
||||
}
|
||||
}
|
||||
|
||||
let mut ty = output.clone();
|
||||
let mut injector = Injector { changed: false };
|
||||
injector.visit_type_mut(&mut ty);
|
||||
injector.changed.then_some(ty)
|
||||
}
|
||||
|
||||
pub(crate) fn contains_open_generic(parsed: &ParsedNodeFn, ty: &Type) -> bool {
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.any(|param| matches!(param, GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() && type_contains_ident(ty, &type_param.ident)))
|
||||
}
|
||||
|
||||
pub(crate) fn unbounded_generic(parsed: &ParsedNodeFn, ty: &Type) -> Option<Ident> {
|
||||
let ident = bare_ident(ty)?.clone();
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.find(|param| matches!(param, GenericParam::Type(type_param) if type_param.ident == ident && type_param.bounds.is_empty() && Some(&type_param.ident) != ctx_ident.as_ref()))?;
|
||||
if let Some(where_clause) = &parsed.where_clause
|
||||
&& tokens_contain_ident(where_clause.to_token_stream(), &ident)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(ident)
|
||||
}
|
||||
|
||||
pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
|
||||
let source = is_async_source(parsed);
|
||||
let value = match kernel_kind(&parsed.output_type) {
|
||||
KernelKind::Plain => parsed.output_type.clone(),
|
||||
KernelKind::Interrupt(inner) => inner,
|
||||
_ if source => slot_value_type(&parsed.output_type),
|
||||
_ => return None,
|
||||
};
|
||||
let writes = record_writes(&value);
|
||||
let has_reads = parsed.fields.iter().any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Regular(_)));
|
||||
if !has_reads && writes.is_none() {
|
||||
return None;
|
||||
}
|
||||
// An async source's slot stores the kernel's plain tuple; the per-eval lift
|
||||
// writes it through the claim, and the reads have no input to bind against.
|
||||
if source && (has_reads || writes.is_none()) {
|
||||
return None;
|
||||
}
|
||||
let carrier_field = parsed.fields.first()?;
|
||||
if carrier_field.is_data_field {
|
||||
return None;
|
||||
}
|
||||
// A first-field lazy carrier: the kernel evaluates the derived content
|
||||
// itself and returns its opaque row token beside the write set.
|
||||
let lazy_carrier = matches!(&carrier_field.ty, ParsedFieldType::Node(_));
|
||||
// Lazy secondaries are consumed as plain elements; raw record inputs and
|
||||
// ranked outputs have no element binding here.
|
||||
let unsupported_lazy_secondary = |field: &ParsedField| match &field.ty {
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => is_served(output_type) || crate::codegen::ir::strip_ilist(output_type).1 > 0 || !field.attribute_reads.is_empty(),
|
||||
ParsedFieldType::Regular(_) => false,
|
||||
};
|
||||
if parsed.fields.iter().skip(lazy_carrier as usize).any(unsupported_lazy_secondary) {
|
||||
return None;
|
||||
}
|
||||
let reads_well_placed = parsed.fields.iter().enumerate().all(|(index, field)| {
|
||||
field.attribute_reads.is_empty() || (lazy_carrier && index == 0) || (!field.is_data_field && matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: None, .. })))
|
||||
});
|
||||
if !reads_well_placed {
|
||||
return None;
|
||||
}
|
||||
if lazy_carrier {
|
||||
let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &carrier_field.ty else {
|
||||
unreachable!("guarded by the lazy_carrier match");
|
||||
};
|
||||
let token = unbounded_generic(parsed, output_type)?;
|
||||
let element = match writes {
|
||||
Some(RecordWrites { element, .. }) => element,
|
||||
None => value,
|
||||
};
|
||||
if !matches!(bare_ident(&element), Some(ident) if ident == &token) {
|
||||
return None;
|
||||
}
|
||||
return Some(RecordShape { carrier: RecordCarrier::LazyToken });
|
||||
}
|
||||
let ParsedFieldType::Regular(RegularParsedField { ty, lend: None, implementations, .. }) = &carrier_field.ty else {
|
||||
return None;
|
||||
};
|
||||
// A gathered subject is never read as an element, so its generic stays open.
|
||||
let gathers = crate::codegen::ir::gathers_lane(parsed);
|
||||
let token = match ty {
|
||||
Type::Tuple(tuple) if tuple.elems.is_empty() => None,
|
||||
ty => match implementations.is_empty().then(|| unbounded_generic(parsed, ty)).flatten() {
|
||||
Some(token) => Some(token),
|
||||
None => {
|
||||
if !gathers && contains_open_generic(parsed, ty) {
|
||||
return None;
|
||||
}
|
||||
None
|
||||
}
|
||||
},
|
||||
};
|
||||
let carrier = match ty {
|
||||
Type::Tuple(tuple) if tuple.elems.is_empty() => RecordCarrier::None,
|
||||
_ if token.is_some() => RecordCarrier::Token,
|
||||
_ => RecordCarrier::Read,
|
||||
};
|
||||
let (element, _, removes) = match writes {
|
||||
Some(RecordWrites { element, markers, removes }) => (element, markers, removes),
|
||||
None => (value, Vec::new(), Vec::new()),
|
||||
};
|
||||
match &token {
|
||||
Some(token) => {
|
||||
if !matches!(bare_ident(&element), Some(ident) if ident == token) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !gathers && contains_open_generic(parsed, &element) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches!(carrier, RecordCarrier::None) && !removes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// The byte-carried token never becomes a value, so it cannot cross a
|
||||
// future boundary.
|
||||
if source && matches!(carrier, RecordCarrier::Token) {
|
||||
return None;
|
||||
}
|
||||
Some(RecordShape { carrier })
|
||||
}
|
||||
|
||||
pub(crate) fn is_poll_kernel(output: &Type) -> bool {
|
||||
matches!(kernel_kind(output), KernelKind::Poll(_))
|
||||
}
|
||||
|
||||
/// A routing family: an unbounded generic shared by lazy inputs (and
|
||||
/// optionally the first parameter) and returned whole, instantiated at
|
||||
/// `RecordValue` so opaque records flow through the kernel. Detected only
|
||||
/// when the family's fields carry no implementations lists, so the existing
|
||||
/// per-type row spelling keeps its meaning.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RoutingIo {
|
||||
pub(crate) generic: Ident,
|
||||
}
|
||||
|
||||
/// Whether a flipped node's primary input is a carrier: the first parameter
|
||||
/// after the context, when it is an owned or lent value input. A carrier's
|
||||
/// fields pass through to the output; every production layout is element-only
|
||||
/// until attribute adoption, so the copy plan is empty and behavior is
|
||||
/// unchanged. Async kernels carry fields per eval around the slot (only the
|
||||
/// element crosses the future boundary), so their carrier must be owned: the
|
||||
/// future captures the element by value.
|
||||
pub(crate) fn flip_carrier(parsed: &ParsedNodeFn) -> bool {
|
||||
if !record_flip(parsed) {
|
||||
return false;
|
||||
}
|
||||
let Some(first) = parsed.fields.first() else { return false };
|
||||
if first.is_data_field {
|
||||
return false;
|
||||
}
|
||||
let ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) = &first.ty else {
|
||||
return false;
|
||||
};
|
||||
if matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty()) {
|
||||
return false;
|
||||
}
|
||||
let async_kernel = parsed.is_async || matches!(kernel_kind(&parsed.output_type), KernelKind::Future(_) | KernelKind::FutureInterrupt(_));
|
||||
!(async_kernel && lend.is_some())
|
||||
}
|
||||
|
||||
/// Whether any value input declares `IList` nesting, so the node materializes a ranked input.
|
||||
pub(crate) fn has_materialized_input(parsed: &ParsedNodeFn) -> bool {
|
||||
parsed
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { list_levels, .. }) if *list_levels > 0))
|
||||
}
|
||||
|
||||
/// Whether a plain node's lowering flips onto record inputs.
|
||||
pub(crate) fn record_flip(parsed: &ParsedNodeFn) -> bool {
|
||||
if record_shape(parsed).is_some() || has_record_io(parsed) || routing_io(parsed).is_some() || record_opaque(parsed) {
|
||||
return false;
|
||||
}
|
||||
// Shader nodes flip like any value node: the kernel doubles as the
|
||||
// shader body on the spirv target, but the struct and Node impl are
|
||||
// std-gated, so the record machinery never reaches the shader build.
|
||||
if parsed.attributes.batch.is_some() || parsed.attributes.plain {
|
||||
return false;
|
||||
}
|
||||
if type_disqualifies(&slot_value_type(&parsed.output_type)) {
|
||||
return false;
|
||||
}
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
for param in &parsed.fn_generics {
|
||||
match param {
|
||||
GenericParam::Type(type_param) if Some(&type_param.ident) == ctx_ident.as_ref() => {}
|
||||
// Registry rows assign a generic by unifying a field's type with
|
||||
// the row's, so a generic without an extractable position keeps
|
||||
// the plain lowering. A `skip_impl` node's rows are hand-written
|
||||
// with explicit types, so no extractable position is needed.
|
||||
GenericParam::Type(type_param) => {
|
||||
let extractable = parsed.fields.iter().filter(|field| !field.is_data_field).any(|field| {
|
||||
let ty = match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty,
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type,
|
||||
};
|
||||
generic_extractable(ty, &type_param.ident)
|
||||
});
|
||||
if !extractable && !parsed.attributes.skip_impl {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// A named lifetime is the serving lifetime: input types substitute
|
||||
// its erased projection in registry and layout contexts.
|
||||
GenericParam::Lifetime(_) => {}
|
||||
GenericParam::Const(_) => return false,
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether unifying a value of `field_ty`'s shape can bind `generic`: the
|
||||
/// generic sits bare or under path type arguments, the shapes
|
||||
/// [`generic_assignment`] walks.
|
||||
pub(crate) fn generic_extractable(field_ty: &Type, generic: &Ident) -> bool {
|
||||
match field_ty {
|
||||
Type::Path(path) if path.qself.is_none() && path.path.get_ident() == Some(generic) => true,
|
||||
Type::Path(path) => path.path.segments.iter().any(|segment| match &segment.arguments {
|
||||
PathArguments::AngleBracketed(args) => args.args.iter().any(|argument| match argument {
|
||||
GenericArgument::Type(inner) => generic_extractable(inner, generic),
|
||||
_ => false,
|
||||
}),
|
||||
_ => false,
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds `generic` by unifying `field_ty` against `row_ty`: where the field
|
||||
/// names the generic, the row's corresponding subtree is the assignment.
|
||||
pub(crate) fn generic_assignment(field_ty: &Type, row_ty: &Type, generic: &Ident) -> Option<Type> {
|
||||
if matches!(field_ty, Type::Path(path) if path.qself.is_none() && path.path.get_ident() == Some(generic)) {
|
||||
return Some(row_ty.clone());
|
||||
}
|
||||
let (Type::Path(field_path), Type::Path(row_path)) = (field_ty, row_ty) else {
|
||||
return None;
|
||||
};
|
||||
let field_segment = field_path.path.segments.last()?;
|
||||
let row_segment = row_path.path.segments.last()?;
|
||||
let (PathArguments::AngleBracketed(field_args), PathArguments::AngleBracketed(row_args)) = (&field_segment.arguments, &row_segment.arguments) else {
|
||||
return None;
|
||||
};
|
||||
field_args.args.iter().zip(row_args.args.iter()).find_map(|(field_arg, row_arg)| match (field_arg, row_arg) {
|
||||
(GenericArgument::Type(field_inner), GenericArgument::Type(row_inner)) => generic_assignment(field_inner, row_inner, generic),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A whole-record position: the served proof a record-opaque kernel hands
|
||||
/// back, or the subject it receives without naming an element.
|
||||
pub(crate) fn is_served(ty: &Type) -> bool {
|
||||
matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "Served"))
|
||||
}
|
||||
|
||||
/// Whether a kernel operates on whole records: it serves through the claim it
|
||||
/// was handed, receives raw record inputs paired with the node's layout, and
|
||||
/// takes on the record APIs' unsafe contracts itself.
|
||||
pub(crate) fn record_opaque(parsed: &ParsedNodeFn) -> bool {
|
||||
is_served(&slot_value_type(&parsed.output_type))
|
||||
}
|
||||
|
||||
pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option<RoutingIo> {
|
||||
if has_record_io(parsed) || parsed.is_async {
|
||||
return None;
|
||||
}
|
||||
if !matches!(kernel_kind(&parsed.output_type), KernelKind::Plain | KernelKind::Interrupt(_)) {
|
||||
return None;
|
||||
}
|
||||
let value = slot_value_type(&parsed.output_type);
|
||||
let Type::Path(path) = &value else { return None };
|
||||
let ident = path.path.get_ident()?.clone();
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.find(|param| matches!(param, GenericParam::Type(type_param) if type_param.ident == ident && type_param.bounds.is_empty() && Some(&type_param.ident) != ctx_ident.as_ref()))?;
|
||||
if let Some(where_clause) = &parsed.where_clause
|
||||
&& tokens_contain_ident(where_clause.to_token_stream(), &ident)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut sources = 0;
|
||||
for (index, field) in parsed.fields.iter().enumerate() {
|
||||
match &field.ty {
|
||||
ParsedFieldType::Node(NodeParsedField {
|
||||
output_type,
|
||||
input_type,
|
||||
implementations,
|
||||
}) => {
|
||||
if routing_source_output(output_type, &ident) {
|
||||
// A source forwards its whole record opaquely; declared
|
||||
// reads contradict that and are rejected by validation.
|
||||
if !implementations.is_empty() || type_contains_ident(input_type, &ident) || !field.attribute_reads.is_empty() {
|
||||
return None;
|
||||
}
|
||||
sources += 1;
|
||||
} else if type_contains_ident(output_type, &ident) || type_contains_ident(input_type, &ident) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, implementations, lend, .. }) => {
|
||||
if bare_ident(ty) == Some(&ident) {
|
||||
if index != 0 || field.is_data_field || !implementations.is_empty() || lend.is_some() {
|
||||
return None;
|
||||
}
|
||||
sources += 1;
|
||||
} else if type_contains_ident(ty, &ident) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(sources > 0).then_some(RoutingIo { generic: ident })
|
||||
}
|
||||
|
||||
pub(crate) fn bare_ident(ty: &Type) -> Option<&Ident> {
|
||||
let Type::Path(path) = ty else { return None };
|
||||
path.path.get_ident()
|
||||
}
|
||||
|
||||
/// Whether a lazy input's declared output is the routing generic, at any
|
||||
/// `IList` nesting: the nesting is rank depth, not a distinct row type.
|
||||
pub(crate) fn routing_source_output(output_type: &Type, generic: &Ident) -> bool {
|
||||
let (stripped, _) = crate::codegen::ir::strip_ilist(output_type);
|
||||
bare_ident(&stripped) == Some(generic)
|
||||
}
|
||||
|
||||
pub(crate) fn tokens_contain_ident(tokens: TokenStream2, ident: &Ident) -> bool {
|
||||
tokens.into_iter().any(|token| match token {
|
||||
proc_macro2::TokenTree::Ident(candidate) => &candidate == ident,
|
||||
proc_macro2::TokenTree::Group(group) => tokens_contain_ident(group.stream(), ident),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn slot_value_type(output: &Type) -> Type {
|
||||
match kernel_kind(output) {
|
||||
KernelKind::Plain => output.clone(),
|
||||
KernelKind::Poll(inner) | KernelKind::Interrupt(inner) => inner,
|
||||
KernelKind::Future(payload) | KernelKind::FutureInterrupt(payload) => match kernel_kind(&payload) {
|
||||
KernelKind::Poll(inner) | KernelKind::Interrupt(inner) => inner,
|
||||
_ => payload,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_source_kernel(output: &Type) -> bool {
|
||||
matches!(kernel_kind(output), KernelKind::Future(_) | KernelKind::FutureInterrupt(_))
|
||||
}
|
||||
|
||||
/// A kernel whose value completes off the evaluation: an `async fn` or a
|
||||
/// `SourceFuture` return. Its slot persists a plain value across evaluations,
|
||||
/// so nothing it returns may borrow the arena.
|
||||
pub(crate) fn is_async_source(parsed: &ParsedNodeFn) -> bool {
|
||||
parsed.is_async || is_source_kernel(&parsed.output_type)
|
||||
}
|
||||
|
||||
/// The type an async source's slot persists. A writing source's value outlives
|
||||
/// the evaluation, so every lifetime it names, including a bare `Attr<M>`'s
|
||||
/// elided one, is `'static`.
|
||||
pub(crate) fn slot_static_type(output: &Type) -> Type {
|
||||
let value = slot_value_type(output);
|
||||
match record_writes(&value).is_some() {
|
||||
true => substitute_lifetimes(&inject_attr_lifetimes(&value, "'static").unwrap_or_else(|| value.clone()), "'static"),
|
||||
false => value,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum KernelKind {
|
||||
Plain,
|
||||
Interrupt(Type),
|
||||
Poll(Type),
|
||||
Future(Type),
|
||||
FutureInterrupt(Type),
|
||||
}
|
||||
|
||||
pub(crate) fn source_future_payload(segment: &syn::PathSegment) -> Type {
|
||||
let PathArguments::AngleBracketed(args) = &segment.arguments else {
|
||||
return syn::parse_quote!(());
|
||||
};
|
||||
args.args
|
||||
.iter()
|
||||
.find_map(|argument| match argument {
|
||||
GenericArgument::Type(ty) => Some(ty.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| syn::parse_quote!(()))
|
||||
}
|
||||
|
||||
pub(crate) fn kernel_kind(output: &Type) -> KernelKind {
|
||||
let plain = || KernelKind::Plain;
|
||||
let Type::Path(path) = output else { return plain() };
|
||||
let Some(segment) = path.path.segments.last() else { return plain() };
|
||||
match segment.ident.to_string().as_str() {
|
||||
"GPoll" => {
|
||||
let PathArguments::AngleBracketed(args) = &segment.arguments else { return plain() };
|
||||
let inner = args.args.iter().find_map(|argument| match argument {
|
||||
GenericArgument::Type(ty) => Some(ty.clone()),
|
||||
_ => None,
|
||||
});
|
||||
inner.map(KernelKind::Poll).unwrap_or_else(plain)
|
||||
}
|
||||
"SourceFuture" => KernelKind::Future(source_future_payload(segment)),
|
||||
"Result" => {
|
||||
let PathArguments::AngleBracketed(args) = &segment.arguments else { return plain() };
|
||||
let mut types = args.args.iter().filter_map(|argument| match argument {
|
||||
GenericArgument::Type(ty) => Some(ty),
|
||||
_ => None,
|
||||
});
|
||||
let (Some(inner), Some(Type::Path(error_path))) = (types.next(), types.next()) else {
|
||||
return plain();
|
||||
};
|
||||
if error_path.path.segments.last().is_none_or(|segment| segment.ident != "Interrupt") {
|
||||
return plain();
|
||||
}
|
||||
if let Type::Path(inner_path) = inner
|
||||
&& let Some(inner_segment) = inner_path.path.segments.last()
|
||||
&& inner_segment.ident == "SourceFuture"
|
||||
{
|
||||
return KernelKind::FutureInterrupt(source_future_payload(inner_segment));
|
||||
}
|
||||
KernelKind::Interrupt(inner.clone())
|
||||
}
|
||||
_ => plain(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn context_param(parsed: &ParsedNodeFn) -> Option<&TypeParam> {
|
||||
let Type::Path(path) = &parsed.input.ty else {
|
||||
return None;
|
||||
};
|
||||
let ident = path.path.get_ident()?;
|
||||
parsed.fn_generics.iter().find_map(|param| match param {
|
||||
GenericParam::Type(type_param) if &type_param.ident == ident => Some(type_param),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn type_disqualifies(ty: &Type) -> bool {
|
||||
struct Disqualifier {
|
||||
found: bool,
|
||||
}
|
||||
|
||||
impl<'ast> Visit<'ast> for Disqualifier {
|
||||
fn visit_type_reference(&mut self, _: &'ast syn::TypeReference) {
|
||||
self.found = true;
|
||||
}
|
||||
|
||||
fn visit_type_impl_trait(&mut self, _: &'ast syn::TypeImplTrait) {
|
||||
self.found = true;
|
||||
}
|
||||
}
|
||||
|
||||
let mut visitor = Disqualifier { found: false };
|
||||
visitor.visit_type(ty);
|
||||
visitor.found
|
||||
}
|
||||
|
||||
/// The input type with every named serving lifetime replaced: `'static` for
|
||||
/// registry, layout, and declaration contexts (the erased projection shares
|
||||
/// its type id and layout), `'_` for eval bindings, where inference recovers
|
||||
/// the serving lifetime.
|
||||
pub(crate) fn substitute_lifetimes(ty: &Type, replacement: &str) -> Type {
|
||||
struct Subst {
|
||||
replacement: &'static str,
|
||||
}
|
||||
|
||||
impl VisitMut for Subst {
|
||||
fn visit_lifetime_mut(&mut self, lifetime: &mut Lifetime) {
|
||||
if lifetime.ident != "static" {
|
||||
*lifetime = Lifetime::new(self.replacement, lifetime.span());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let replacement = match replacement {
|
||||
"'static" => "'static",
|
||||
_ => "'_",
|
||||
};
|
||||
let mut ty = ty.clone();
|
||||
Subst { replacement }.visit_type_mut(&mut ty);
|
||||
ty
|
||||
}
|
||||
|
||||
/// The serving lifetime an input type names, so a materialized binding can tie
|
||||
/// the list view to the element's own region.
|
||||
pub(crate) fn named_serving_lifetime(ty: &Type) -> Option<Lifetime> {
|
||||
struct Find {
|
||||
found: Option<Lifetime>,
|
||||
}
|
||||
|
||||
impl<'ast> Visit<'ast> for Find {
|
||||
fn visit_lifetime(&mut self, lifetime: &'ast Lifetime) {
|
||||
if self.found.is_none() && lifetime.ident != "static" {
|
||||
self.found = Some(lifetime.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut visitor = Find { found: None };
|
||||
visitor.visit_type(ty);
|
||||
visitor.found
|
||||
}
|
||||
|
||||
/// Rewrites a kernel-declared `ExtractArena<'e>` bound into the equality the
|
||||
/// trait names.
|
||||
pub(crate) fn desugar_extract_lifetime(bound: &TypeParamBound, core_types: &TokenStream2) -> TokenStream2 {
|
||||
let TypeParamBound::Trait(trait_bound) = bound else {
|
||||
return quote!(#bound);
|
||||
};
|
||||
let Some(segment) = trait_bound.path.segments.last() else {
|
||||
return quote!(#bound);
|
||||
};
|
||||
if segment.ident != "ExtractArena" {
|
||||
return quote!(#bound);
|
||||
}
|
||||
let PathArguments::AngleBracketed(args) = &segment.arguments else {
|
||||
return quote!(#bound);
|
||||
};
|
||||
if args.args.len() != 1 {
|
||||
return quote!(#bound);
|
||||
}
|
||||
let Some(GenericArgument::Lifetime(lifetime)) = args.args.first() else {
|
||||
return quote!(#bound);
|
||||
};
|
||||
quote!(#core_types::context::ExtractArena<ArenaRef = &#lifetime #core_types::arena::Arena>)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod lifetime_subst_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn named_lifetimes_erase_to_static() {
|
||||
let ty: Type = syn::parse_quote!(Graphic<'e>);
|
||||
let erased = substitute_lifetimes(&ty, "'static");
|
||||
assert_eq!(quote::quote!(#erased).to_string(), "Graphic < 'static >");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
use super::*;
|
||||
use proc_macro_error2::emit_error;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{GenericParam, Ident, Type};
|
||||
|
||||
pub(crate) fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic_idents: &[Ident], regular_fields: &[&ParsedField]) -> TokenStream2 {
|
||||
// A data-field generic is monomorphized by hand, so those nodes carry their own rows.
|
||||
if !data_field_generic_idents.is_empty() {
|
||||
return quote!();
|
||||
}
|
||||
let entries = match crate::codegen::ir::node_kind(&crate::codegen::ir::build(parsed)) {
|
||||
crate::codegen::ir::NodeKind::Flip => flip_entries_tokens(parsed, struct_name, regular_fields),
|
||||
_ => single_row_entries(parsed, struct_name, regular_fields),
|
||||
};
|
||||
// Validation already rejects a generic input without implementations, so an
|
||||
// empty row set here is an emitter gap, not an unregistered shape.
|
||||
if entries.is_empty() && !parsed.attributes.skip_impl {
|
||||
emit_error!(
|
||||
parsed.fn_name.span(),
|
||||
"no registry rows were generated for `{}`, so a document cannot resolve it",
|
||||
parsed.fn_name;
|
||||
help = "give every generic input an #[implementations(...)] list, or mark the node skip_impl if it registers its rows by hand"
|
||||
);
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
/// The registry rows of a flipped plain node: every input is a record input,
|
||||
/// inputs resolve their layouts off the claimed handles, and the output is an
|
||||
/// element-only record of the kernel's return type.
|
||||
fn flip_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 {
|
||||
let Some(rows) = implementation_rows(parsed, regular_fields) else {
|
||||
return quote!();
|
||||
};
|
||||
let rows: Vec<&Vec<Type>> = rows.iter().filter(|row| row.iter().all(|ty| !type_disqualifies(ty))).collect();
|
||||
if rows.is_empty() {
|
||||
return quote!();
|
||||
}
|
||||
let output = substitute_lifetimes(&slot_value_type(&parsed.output_type), "'static");
|
||||
|
||||
let field_type = |field: &ParsedField| match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(),
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type.clone(),
|
||||
};
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
let generic_positions: Option<Vec<(Ident, usize)>> = parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.filter_map(|param| match param {
|
||||
GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(&type_param.ident),
|
||||
_ => None,
|
||||
})
|
||||
.map(|generic| {
|
||||
regular_fields
|
||||
.iter()
|
||||
.position(|field| generic_extractable(&field_type(field), generic))
|
||||
.map(|index| (generic.clone(), index))
|
||||
})
|
||||
.collect();
|
||||
let Some(generic_positions) = generic_positions else {
|
||||
return quote!();
|
||||
};
|
||||
|
||||
let fn_name = &parsed.fn_name;
|
||||
let entries_name = format_ident!("{}_entries", fn_name);
|
||||
let arity = regular_fields.len();
|
||||
let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect();
|
||||
let node_underscores: Vec<TokenStream2> = regular_fields.iter().map(|_| quote!(_)).collect();
|
||||
let node = crate::codegen::ir::build(parsed);
|
||||
let core_types = quote!(gcore);
|
||||
|
||||
// Shorthand associated types in the output only resolve against the
|
||||
// generics' bounds, so rows name the output through a bounded alias. Only
|
||||
// output-reaching generics (directly or through a kept bound) may appear:
|
||||
// an unused alias parameter is an error.
|
||||
let candidate_params: Vec<&GenericParam> = parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.filter(|param| matches!(param, GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref()))
|
||||
.collect();
|
||||
let param_ident = |param: &&GenericParam| match param {
|
||||
GenericParam::Type(type_param) => type_param.ident.clone(),
|
||||
_ => unreachable!("candidates are type parameters"),
|
||||
};
|
||||
let mut kept: Vec<bool> = candidate_params.iter().map(|param| type_contains_ident(&output, ¶m_ident(param))).collect();
|
||||
loop {
|
||||
let mut grew = false;
|
||||
for index in 0..candidate_params.len() {
|
||||
if kept[index] {
|
||||
continue;
|
||||
}
|
||||
let ident = param_ident(&candidate_params[index]);
|
||||
let mentioned = candidate_params.iter().zip(&kept).any(|(param, kept)| {
|
||||
*kept
|
||||
&& match param {
|
||||
GenericParam::Type(type_param) => type_param.bounds.iter().any(|bound| {
|
||||
let bound: Type = syn::parse_quote!(dyn #bound);
|
||||
type_contains_ident(&bound, &ident)
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
if mentioned {
|
||||
kept[index] = true;
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
if !grew {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let alias_params: Vec<&GenericParam> = candidate_params.iter().zip(&kept).filter(|(_, kept)| **kept).map(|(param, _)| *param).collect();
|
||||
let alias_param_idents: Vec<Ident> = alias_params.iter().map(param_ident).collect();
|
||||
let alias_param_tokens: Vec<TokenStream2> = alias_params.iter().map(|param| quote!(#param)).collect();
|
||||
let output_alias = format_ident!("__{}_output", fn_name);
|
||||
let alias_def = match alias_param_tokens.is_empty() {
|
||||
true => quote!(#[allow(non_camel_case_types)] type #output_alias = #output;),
|
||||
false => quote!(#[allow(non_camel_case_types, type_alias_bounds)] type #output_alias<#(#alias_param_tokens,)*> = #output;),
|
||||
};
|
||||
|
||||
let entries = rows.iter().filter_map(|row| {
|
||||
let assignments: Vec<(Ident, Type)> = generic_positions
|
||||
.iter()
|
||||
.map(|(generic, index)| generic_assignment(&field_type(regular_fields[*index]), &row[*index], generic).map(|assigned| (generic.clone(), assigned)))
|
||||
.collect::<Option<_>>()?;
|
||||
let row: Vec<Type> = row.iter().map(|ty| substitute_lifetimes(ty, "'static")).collect();
|
||||
let assignments: Vec<(Ident, Type)> = assignments.into_iter().map(|(generic, ty)| (generic, substitute_lifetimes(&ty, "'static"))).collect();
|
||||
if type_disqualifies(&substitute_ident_types(&output, &assignments)) {
|
||||
return None;
|
||||
}
|
||||
let assignment_types: Vec<TokenStream2> = assignments.iter().map(|(_, ty)| quote!(#ty)).collect();
|
||||
let alias_arguments: Vec<TokenStream2> = assignments
|
||||
.iter()
|
||||
.filter(|(generic, _)| alias_param_idents.contains(generic))
|
||||
.map(|(_, ty)| quote!(#ty))
|
||||
.collect();
|
||||
let row_output = match alias_arguments.is_empty() {
|
||||
true => quote!(#output_alias),
|
||||
false => quote!(#output_alias<#(#alias_arguments),*>),
|
||||
};
|
||||
let assignment_types = assignment_types.iter();
|
||||
let turbofish = quote!(::<#(#node_underscores,)* #(#assignment_types,)*>);
|
||||
let input_types = row.iter().map(|ty| quote!(gcore::registry::record_source_type::<#ty>()));
|
||||
let downcasts = names.iter().zip(row.iter()).enumerate().map(|(index, (name, ty))| {
|
||||
let handle = format_ident!("__handle_{index}");
|
||||
let layout = format_ident!("__layout_{index}");
|
||||
quote! {
|
||||
let #handle = inputs.next().unwrap();
|
||||
let #layout = #handle.layout().clone();
|
||||
let #name = #handle.downcast_record::<#ty>()?;
|
||||
}
|
||||
});
|
||||
let layout_args = (0..arity).map(|index| {
|
||||
let layout = format_ident!("__layout_{index}");
|
||||
quote!(&#layout,)
|
||||
});
|
||||
let element_spec = quote!(gcore::record::ElementSpec::Concrete({ use gcore::record::{ElementWritePickHashed as _, ElementWritePickPlain as _}; (&gcore::record::ElementWritePick::<#row_output>(::core::marker::PhantomData)).element_write() }));
|
||||
let layout_meta = crate::codegen::ir::layout_meta_tokens(&node, element_spec, &core_types);
|
||||
Some(quote! {
|
||||
gcore::registry::RegistryEntry {
|
||||
layout_meta: Some(#layout_meta),
|
||||
io: gcore::registry::NodeIOTypes::new(
|
||||
gcore::concrete!(gcore::context::ContextImpl<'static>),
|
||||
gcore::registry::record_type::<#row_output>(),
|
||||
vec![#(#input_types),*],
|
||||
),
|
||||
constructor: |inputs| {
|
||||
if inputs.len() != #arity {
|
||||
return Err(gcore::registry::ConstructionError::Arity { expected: #arity, got: inputs.len() });
|
||||
}
|
||||
let mut inputs = inputs.into_iter();
|
||||
#(#downcasts)*
|
||||
let __node = #struct_name #turbofish::new(#(#names,)* #(#layout_args)*);
|
||||
Ok(gcore::registry::SourceHandle::new_record::<#row_output>(::std::sync::Arc::new(__node) as ::std::sync::Arc<gcore::registry::ErasedRecordNode>))
|
||||
},
|
||||
}
|
||||
})
|
||||
});
|
||||
let entries: Vec<TokenStream2> = entries.collect();
|
||||
if entries.is_empty() {
|
||||
return quote!();
|
||||
}
|
||||
|
||||
quote! {
|
||||
pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> {
|
||||
#alias_def
|
||||
vec![#(#entries),*]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which record an input claims and how its value is recovered. Base slots
|
||||
/// are the record inputs whose layouts form the output; value slots are record
|
||||
/// inputs read for their layout.
|
||||
enum SlotKind {
|
||||
/// A generic record input whose element is only known at runtime; the runtime
|
||||
/// type is captured for the output wrap or the union.
|
||||
BaseGeneric(String),
|
||||
/// A concrete record carrier read for its layout.
|
||||
BaseConcrete(Type),
|
||||
/// A concrete record input read for its layout only.
|
||||
Value(Type),
|
||||
/// A record input whose element extracts to the node's plain value input.
|
||||
Extracted(Type),
|
||||
/// A ranked record input consumed whole; no layout rides to the constructor.
|
||||
Ranked(Type),
|
||||
}
|
||||
|
||||
impl SlotKind {
|
||||
fn is_base(&self) -> bool {
|
||||
matches!(self, SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_))
|
||||
}
|
||||
}
|
||||
|
||||
/// The single registry row shared by record-io, routing, and opaque nodes: one
|
||||
/// instance covers the input, each input's type and downcast follow its
|
||||
/// slot, and the output layout folds from the base slots.
|
||||
fn single_row_entries(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fields: &[&ParsedField]) -> TokenStream2 {
|
||||
use crate::codegen::ir;
|
||||
let fn_name = &parsed.fn_name;
|
||||
let node = ir::build(parsed);
|
||||
let core_types = quote!(gcore);
|
||||
let lend = |field: &ParsedField| matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }));
|
||||
|
||||
// A subject is its record input (concrete carrier or erased generic); a
|
||||
// non-subject value rides a record input when it reads its layout.
|
||||
let slots: Option<Vec<SlotKind>> = regular_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, field)| {
|
||||
let input = &node.inputs[index];
|
||||
if input.subject {
|
||||
return Some(match &input.shape.element {
|
||||
ir::Element::Concrete(ty) => SlotKind::BaseConcrete(ty.clone()),
|
||||
ir::Element::Generic(ident) => SlotKind::BaseGeneric(ident.to_string()),
|
||||
ir::Element::Opaque => SlotKind::BaseGeneric("T".to_string()),
|
||||
});
|
||||
}
|
||||
match &field.ty {
|
||||
// An element-consuming lazy secondary of a record node rides a
|
||||
// record input with a layout slot, like a reading secondary.
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. })
|
||||
if matches!(ir::node_kind(&node), ir::NodeKind::RecordIo) && matches!(ir::lazy_binding(&node, index), ir::LazyBinding::Element) =>
|
||||
{
|
||||
Some(SlotKind::Value(output_type.clone()))
|
||||
}
|
||||
ParsedFieldType::Node(_) => {
|
||||
emit_error!(field.pat_ident.span(), "plain (non-record) io is unsupported: this lazy input needs a record edge");
|
||||
None
|
||||
}
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => match ir::value_binding(&node, index) {
|
||||
ir::ValueBinding::Materialized => Some(SlotKind::Ranked(ty.clone())),
|
||||
ir::ValueBinding::ReadingSecondary | ir::ValueBinding::RecordElement => Some(SlotKind::Value(ty.clone())),
|
||||
// One input kind: a record node's plain value still rides a
|
||||
// record input, extracted to its element at construction.
|
||||
_ if matches!(ir::node_kind(&node), ir::NodeKind::RecordIo) => Some(SlotKind::Extracted(ty.clone())),
|
||||
_ => {
|
||||
emit_error!(field.pat_ident.span(), "plain (non-record) io is unsupported: this value input needs a record edge");
|
||||
None
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let Some(slots) = slots else {
|
||||
return quote!();
|
||||
};
|
||||
|
||||
// A ranked input's element generic monomorphizes the kernel, so its
|
||||
// implementations expand to one registry row each; every other slot
|
||||
// (erased routing generics included) is row-invariant. The carried list
|
||||
// mirrors the struct's carried generic parameters in declaration order.
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
let ranked = |index: usize| matches!(®ular_fields[index].ty, ParsedFieldType::Regular(RegularParsedField { list_levels, .. }) if *list_levels > 0);
|
||||
// A record-io node's plain secondary reaches the constructor concrete, so its generic monomorphizes the row like a ranked element does.
|
||||
let record_secondary =
|
||||
|index: usize| matches!(ir::node_kind(&node), ir::NodeKind::RecordIo) && index > 0 && !node.inputs[index].subject && matches!(®ular_fields[index].ty, ParsedFieldType::Regular(_));
|
||||
let names_generic = |index: usize, generic: &Ident| match ®ular_fields[index].ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => crate::codegen::type_contains_ident(ty, generic),
|
||||
_ => false,
|
||||
};
|
||||
let solves_generic = |index: usize, generic: &Ident| match ®ular_fields[index].ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. }) => !implementations.is_empty() && generic_extractable(ty, generic),
|
||||
_ => false,
|
||||
};
|
||||
let carried_generic_idents: Vec<Ident> = parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.filter_map(|param| match param {
|
||||
GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(type_param.ident.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|ident| (0..regular_fields.len()).any(|index| (ranked(index) || record_secondary(index)) && names_generic(index, ident)))
|
||||
.collect();
|
||||
// Ranked sources come first, so a generic a ranked input already carries keeps sourcing its rows from that input.
|
||||
let carried_source = |generic: &Ident| {
|
||||
(0..regular_fields.len())
|
||||
.find(|&index| ranked(index) && solves_generic(index, generic))
|
||||
.or_else(|| (0..regular_fields.len()).find(|&index| record_secondary(index) && solves_generic(index, generic)))
|
||||
};
|
||||
let carried: Option<Vec<(Ident, usize)>> = carried_generic_idents.iter().map(|ident| carried_source(ident).map(|index| (ident.clone(), index))).collect();
|
||||
let Some(carried) = carried else {
|
||||
return quote!();
|
||||
};
|
||||
let impls_of = |index: usize| match ®ular_fields[index].ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { implementations, .. }) => implementations.iter().cloned().collect::<Vec<Type>>(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let row_count = carried.iter().map(|(_, index)| impls_of(*index).len()).max().unwrap_or(1).max(1);
|
||||
let row_assignments: Vec<Vec<(Ident, Type)>> = (0..row_count)
|
||||
.map(|row| {
|
||||
carried
|
||||
.iter()
|
||||
.filter_map(|(generic, index)| {
|
||||
let impls = impls_of(*index);
|
||||
let row_ty = ir::strip_ilist(&impls[row.min(impls.len() - 1)]).0;
|
||||
let field_ty = match ®ular_fields[*index].ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(),
|
||||
_ => unreachable!("carried sources are regular fields"),
|
||||
};
|
||||
generic_assignment(&field_ty, &row_ty, generic).map(|ty| (generic.clone(), ty))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let entries_name = format_ident!("{}_entries", fn_name);
|
||||
let arity = regular_fields.len();
|
||||
let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect();
|
||||
|
||||
let entries: Vec<TokenStream2> = row_assignments
|
||||
.iter()
|
||||
.filter_map(|assignments| {
|
||||
// A row whose assignments did not all solve cannot instantiate the struct.
|
||||
if assignments.len() != carried.len() {
|
||||
return None;
|
||||
}
|
||||
let slots: Vec<SlotKind> = slots
|
||||
.iter()
|
||||
.map(|slot| match slot {
|
||||
SlotKind::BaseGeneric(name) => SlotKind::BaseGeneric(name.clone()),
|
||||
SlotKind::BaseConcrete(ty) => SlotKind::BaseConcrete(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")),
|
||||
SlotKind::Value(ty) => SlotKind::Value(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")),
|
||||
SlotKind::Extracted(ty) => SlotKind::Extracted(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")),
|
||||
SlotKind::Ranked(ty) => SlotKind::Ranked(substitute_lifetimes(&substitute_ident_types(ty, assignments), "'static")),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Every non-base value input must be concrete.
|
||||
let values_concrete = regular_fields.iter().zip(&slots).all(|(field, slot)| match slot {
|
||||
SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_) => true,
|
||||
SlotKind::Value(ty) | SlotKind::Extracted(ty) | SlotKind::Ranked(ty) => !contains_open_generic(parsed, ty) && (lend(field) || !type_disqualifies(ty)),
|
||||
});
|
||||
if !values_concrete {
|
||||
return None;
|
||||
}
|
||||
|
||||
let input_types = slots.iter().map(|slot| match slot {
|
||||
SlotKind::BaseGeneric(name) => quote!(gcore::registry::generic_record_source_type(#name)),
|
||||
SlotKind::BaseConcrete(ty) | SlotKind::Value(ty) | SlotKind::Extracted(ty) | SlotKind::Ranked(ty) => quote!(gcore::registry::record_source_type::<#ty>()),
|
||||
});
|
||||
|
||||
let downcasts = names.iter().zip(&slots).enumerate().map(|(index, (name, slot))| {
|
||||
let handle = format_ident!("__handle_{index}");
|
||||
let layout = format_ident!("__layout_{index}");
|
||||
let ty = format_ident!("__ty_{index}");
|
||||
match slot {
|
||||
SlotKind::BaseGeneric(_) | SlotKind::BaseConcrete(_) => quote! {
|
||||
let #handle = inputs.next().unwrap();
|
||||
let #ty = #handle.ty().clone();
|
||||
let #layout = #handle.layout().clone();
|
||||
let #name = #handle.downcast_erased::<gcore::registry::ErasedRecordNode>(#ty.clone())?;
|
||||
},
|
||||
SlotKind::Value(value_ty) => quote! {
|
||||
let #handle = inputs.next().unwrap();
|
||||
let #layout = #handle.layout().clone();
|
||||
let #name = #handle.downcast_record::<#value_ty>()?;
|
||||
},
|
||||
// The node reads the element off the input's own layout, so
|
||||
// neither slot rides a layout to the constructor.
|
||||
SlotKind::Extracted(value_ty) | SlotKind::Ranked(value_ty) => quote! {
|
||||
let #name = inputs.next().unwrap().downcast_record::<#value_ty>()?;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
let base_indices: Vec<usize> = slots.iter().enumerate().filter(|(_, slot)| slot.is_base()).map(|(index, _)| index).collect();
|
||||
let value_indices: Vec<usize> = slots.iter().enumerate().filter(|(_, slot)| matches!(slot, SlotKind::Value(_))).map(|(index, _)| index).collect();
|
||||
let value_layout_args: Vec<TokenStream2> = value_indices
|
||||
.iter()
|
||||
.map(|index| {
|
||||
let layout = format_ident!("__layout_{index}");
|
||||
quote!(&#layout,)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let carried_meta = || {
|
||||
let meta = ir::layout_meta_tokens(&node, quote!(gcore::record::ElementSpec::Carried), &core_types);
|
||||
quote!(Some(#meta))
|
||||
};
|
||||
|
||||
// The output type and node wrap follow the output element: a concrete (or
|
||||
// row-assigned) element is a typed record; a generic or opaque element is
|
||||
// an erased record carrying the first base slot's runtime type.
|
||||
let output_element = match &node.output.shape.element {
|
||||
ir::Element::Concrete(element) => Some(substitute_ident_types(element, assignments)),
|
||||
ir::Element::Generic(ident) => assignments.iter().find(|(generic, _)| generic == ident).map(|(_, ty)| ty.clone()),
|
||||
ir::Element::Opaque => None,
|
||||
};
|
||||
let output_element = output_element.map(|element| substitute_lifetimes(&element, "'static"));
|
||||
let (io_output, wrap) = match &output_element {
|
||||
Some(element) => (
|
||||
quote!(gcore::registry::record_type::<#element>()),
|
||||
quote!(Ok(gcore::registry::SourceHandle::new_record::<#element>(::std::sync::Arc::new(__node)))),
|
||||
),
|
||||
None => {
|
||||
let name = match &node.output.shape.element {
|
||||
ir::Element::Generic(ident) => ident.to_string(),
|
||||
_ => "T".to_string(),
|
||||
};
|
||||
let base_ty = format_ident!("__ty_{}", base_indices[0]);
|
||||
(
|
||||
quote!(gcore::Type::Record(Box::new(gcore::Type::Generic(::std::borrow::Cow::Borrowed(#name))))),
|
||||
quote!(Ok(gcore::registry::SourceHandle::new_erased(::std::sync::Arc::new(__node) as ::std::sync::Arc<gcore::registry::ErasedRecordNode>, #base_ty))),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let (prelude, new_layout_args, layout_meta) = match ir::node_kind(&node) {
|
||||
ir::NodeKind::RecordIo => {
|
||||
let carrier_arg = (node.inputs.first().is_some_and(|input| input.subject) && ir::materialized_levels(&node, 0) == 0).then(|| quote!(&__layout_0,));
|
||||
let layout_meta_fn = format_ident!("{}_layout_meta", fn_name);
|
||||
(quote!(), quote!(#carrier_arg #(#value_layout_args)*), quote!(Some(self::#layout_meta_fn())))
|
||||
}
|
||||
ir::NodeKind::Routing => {
|
||||
let source_layouts = base_indices.iter().map(|index| format_ident!("__layout_{index}"));
|
||||
let source_wraps = base_indices.iter().map(|index| {
|
||||
let name = names[*index];
|
||||
let layout = format_ident!("__layout_{index}");
|
||||
quote!(let #name = gcore::record::RecordSource::new(#name, &#layout, &__union);)
|
||||
});
|
||||
let prelude = quote! {
|
||||
let __union = gcore::record::Layout::union(&[#(&#source_layouts),*]);
|
||||
#(#source_wraps)*
|
||||
};
|
||||
(prelude, quote!(&__union, #(#value_layout_args)*), carried_meta())
|
||||
}
|
||||
ir::NodeKind::Opaque => {
|
||||
let record_layout = format_ident!("__layout_{}", base_indices[0]);
|
||||
(quote!(), quote!(&#record_layout), carried_meta())
|
||||
}
|
||||
ir::NodeKind::Flip => unreachable!("flip has its own multi-row emitter"),
|
||||
};
|
||||
|
||||
// A carried generic instantiates through the struct's trailing phantom
|
||||
// parameters, so the constructor names the row's types after one inferred
|
||||
// slot per input field.
|
||||
let turbofish = (!carried.is_empty()).then(|| {
|
||||
let underscores = (0..arity).map(|_| quote!(_));
|
||||
let carried_types = carried
|
||||
.iter()
|
||||
.filter_map(|(generic, _)| assignments.iter().find(|(ident, _)| ident == generic).map(|(_, ty)| quote!(#ty)));
|
||||
quote!(::<#(#underscores,)* #(#carried_types,)*>)
|
||||
});
|
||||
|
||||
Some(quote! {
|
||||
gcore::registry::RegistryEntry {
|
||||
layout_meta: #layout_meta,
|
||||
io: gcore::registry::NodeIOTypes::new(
|
||||
gcore::concrete!(gcore::context::ContextImpl<'static>),
|
||||
#io_output,
|
||||
vec![#(#input_types),*],
|
||||
),
|
||||
constructor: |inputs| {
|
||||
if inputs.len() != #arity {
|
||||
return Err(gcore::registry::ConstructionError::Arity { expected: #arity, got: inputs.len() });
|
||||
}
|
||||
let mut inputs = inputs.into_iter();
|
||||
#(#downcasts)*
|
||||
#prelude
|
||||
let __node = #struct_name #turbofish::new(#(#names,)* #new_layout_args);
|
||||
#wrap
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
if entries.is_empty() {
|
||||
return quote!();
|
||||
}
|
||||
quote! {
|
||||
pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> {
|
||||
vec![#(#entries),*]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn implementation_rows(parsed: &ParsedNodeFn, regular_fields: &[&ParsedField]) -> Option<Vec<Vec<Type>>> {
|
||||
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||
let open_generics: Vec<&Ident> = parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.filter_map(|param| match param {
|
||||
GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() => Some(&type_param.ident),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let candidates: Vec<Vec<Type>> = regular_fields
|
||||
.iter()
|
||||
.map(|field| match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. }) => match implementations.is_empty() {
|
||||
false => Some(implementations.iter().cloned().collect()),
|
||||
true => open_generics.iter().all(|generic| !crate::codegen::type_contains_ident(ty, generic)).then(|| vec![ty.clone()]),
|
||||
},
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, implementations, .. }) => match implementations.is_empty() {
|
||||
false => Some(implementations.iter().map(|implementation| implementation.output.clone()).collect()),
|
||||
true => open_generics
|
||||
.iter()
|
||||
.all(|generic| !crate::codegen::type_contains_ident(output_type, generic))
|
||||
.then(|| vec![output_type.clone()]),
|
||||
},
|
||||
})
|
||||
.collect::<Option<_>>()?;
|
||||
|
||||
let row_count = candidates.iter().map(|types| types.len()).max().unwrap_or(1).max(1);
|
||||
Some((0..row_count).map(|row| candidates.iter().map(|types| types[row.min(types.len() - 1)].clone()).collect()).collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::parsing::parse_node_fn;
|
||||
|
||||
/// The macro's own pipeline up to the entries emitter, so a test node's rows
|
||||
/// match what `#[node]` would generate.
|
||||
fn entries_of(attr: TokenStream2, item: TokenStream2) -> String {
|
||||
let mut parsed = parse_node_fn(attr, item).unwrap();
|
||||
parsed.replace_impl_trait_in_input();
|
||||
if parsed.injects_async_source_fields() {
|
||||
parsed.inject_async_source_fields("e!(gcore));
|
||||
}
|
||||
let regular_fields: Vec<&ParsedField> = parsed.fields.iter().filter(|field| !field.is_data_field).collect();
|
||||
let data_field_generic_idents: Vec<Ident> = parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.filter_map(|param| match param {
|
||||
GenericParam::Type(type_param) => Some(type_param.ident.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|ident| {
|
||||
parsed
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| field.is_data_field && matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { ty, .. }) if crate::codegen::type_contains_ident(ty, ident)))
|
||||
})
|
||||
.collect();
|
||||
entries_tokens(&parsed, &format_ident!("TestNode"), &data_field_generic_idents, ®ular_fields).to_string()
|
||||
}
|
||||
|
||||
/// A record-io async source whose only generic sits on a plain secondary:
|
||||
/// the shape the rasterize node has. One row per implementation, or the
|
||||
/// node never reaches the registry.
|
||||
#[test]
|
||||
fn record_io_source_rows_an_implementations_generic_secondary() {
|
||||
let entries = entries_of(
|
||||
quote!(category("")),
|
||||
quote!(
|
||||
async fn rasterize<T: Send + Clone>(
|
||||
_: impl Ctx,
|
||||
_: (),
|
||||
#[implementations(List<Vector>, List<Raster<CPU>>, List<Graphic>, List<Color>, List<GradientStops>)] data: List<T>,
|
||||
footprint: Footprint,
|
||||
canvas: CanvasHandle,
|
||||
) -> (Raster<CPU>, Attr<Transform>, OwnedAttr<EditorMergedLayers>) {
|
||||
todo!()
|
||||
}
|
||||
),
|
||||
);
|
||||
assert!(entries.contains("fn rasterize_entries"), "a registrable record-io source must emit its entries fn");
|
||||
for element in ["Vector", "Raster < CPU >", "Graphic", "Color", "GradientStops"] {
|
||||
let row = format!("record_source_type :: < List < {element} > > ()");
|
||||
assert!(entries.contains(&row), "the implementations row {element} is missing: {entries}");
|
||||
}
|
||||
assert_eq!(entries.matches("constructor :").count(), 5, "one row per implementation");
|
||||
assert!(!entries.contains("< T >"), "every row instantiates the carried generic");
|
||||
}
|
||||
|
||||
/// The same shape without implementations stays unregistered: nothing names
|
||||
/// the rows, so a silent empty emission is the intended answer.
|
||||
#[test]
|
||||
fn record_io_source_without_implementations_stays_unregistered() {
|
||||
let entries = entries_of(
|
||||
quote!(category(""), skip_impl),
|
||||
quote!(
|
||||
async fn rasterize_open<T: Send + Clone>(_: impl Ctx, _: (), data: List<T>) -> (Raster<CPU>, Attr<Transform>) {
|
||||
todo!()
|
||||
}
|
||||
),
|
||||
);
|
||||
assert!(entries.is_empty(), "an unsourced generic secondary registers nothing: {entries}");
|
||||
}
|
||||
|
||||
/// A ranked element generic keeps sourcing its rows from the ranked input.
|
||||
#[test]
|
||||
fn ranked_generic_still_rows_from_its_ranked_input() {
|
||||
let entries = entries_of(
|
||||
quote!(category("")),
|
||||
quote!(
|
||||
fn count<T: Send + Clone>(_: impl Ctx, _: (), #[implementations(IList<f64>, IList<u32>)] items: IList<T>) -> (u32, Attr<Opacity>) {
|
||||
todo!()
|
||||
}
|
||||
),
|
||||
);
|
||||
assert!(entries.contains("fn count_entries"), "a ranked record-io node still emits: {entries}");
|
||||
assert_eq!(entries.matches("constructor :").count(), 2, "one row per ranked implementation");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,173 @@
|
||||
use super::*;
|
||||
|
||||
/// Generates strongly typed utilites to access inputs
|
||||
pub(crate) fn generate_node_input_references(
|
||||
parsed: &ParsedNodeFn,
|
||||
fn_generics: &[crate::GenericParam],
|
||||
field_idents: &[&PatIdent],
|
||||
core_types: &TokenStream2,
|
||||
identifier: &Ident,
|
||||
cfg: &TokenStream2,
|
||||
) -> TokenStream2 {
|
||||
let inputs_module_name = format_ident!("{}", parsed.struct_name.to_string().to_case(Case::Snake));
|
||||
|
||||
let mut generated_input_accessor = Vec::new();
|
||||
if !parsed.attributes.skip_impl {
|
||||
let (mut modified, mut generic_collector) = FilterUsedGenerics::new(fn_generics);
|
||||
|
||||
for (input_index, (parsed_input, input_ident)) in parsed.fields.iter().zip(field_idents).enumerate() {
|
||||
// `IList` nesting is rank metadata, not part of the value type.
|
||||
let ty = match &parsed_input.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(),
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => crate::codegen::ir::strip_ilist(output_type).0,
|
||||
};
|
||||
let mut ty = crate::codegen::classify::substitute_lifetimes(&ty, "'static");
|
||||
|
||||
// We only want the necessary generics.
|
||||
let used = generic_collector.filter_unnecessary_generics(&mut modified, &mut ty);
|
||||
// TODO: figure out a better name that doesn't conflict with so many types
|
||||
let struct_name = format_ident!("{}Input", input_ident.ident.to_string().to_case(Case::Pascal));
|
||||
let (fn_generic_params, phantom_data_declerations) = generate_phantom_data(used.iter());
|
||||
|
||||
// Only create structs with phantom data where necessary.
|
||||
generated_input_accessor.push(if phantom_data_declerations.is_empty() {
|
||||
quote! {
|
||||
pub struct #struct_name;
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
pub struct #struct_name <#(#used),*>{
|
||||
#(#phantom_data_declerations,)*
|
||||
}
|
||||
}
|
||||
});
|
||||
generated_input_accessor.push(quote! {
|
||||
impl <#(#used),*> #core_types::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> {
|
||||
const INDEX: usize = #input_index;
|
||||
fn identifier() -> #core_types::ProtoNodeIdentifier {
|
||||
#inputs_module_name::IDENTIFIER.clone()
|
||||
}
|
||||
type Result = #ty;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
#cfg
|
||||
pub mod #inputs_module_name {
|
||||
use super::*;
|
||||
|
||||
/// The `ProtoNodeIdentifier` of this node without any generics attached to it
|
||||
pub const IDENTIFIER: #core_types::ProtoNodeIdentifier = #identifier();
|
||||
#(#generated_input_accessor)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// It is necessary to generate PhantomData for each fn generic to avoid compiler errors.
|
||||
pub(crate) fn generate_phantom_data<'a>(fn_generics: impl Iterator<Item = &'a crate::GenericParam>) -> (Vec<TokenStream2>, Vec<TokenStream2>) {
|
||||
let mut phantom_data_declerations = Vec::new();
|
||||
let mut fn_generic_params = Vec::new();
|
||||
|
||||
for fn_generic_param in fn_generics {
|
||||
let field_name = format_ident!("phantom_{}", phantom_data_declerations.len());
|
||||
|
||||
match fn_generic_param {
|
||||
crate::GenericParam::Lifetime(lifetime_param) => {
|
||||
let lifetime = &lifetime_param.lifetime;
|
||||
|
||||
fn_generic_params.push(quote! {#lifetime});
|
||||
phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<&#lifetime ()>})
|
||||
}
|
||||
crate::GenericParam::Type(type_param) => {
|
||||
let generic_name = &type_param.ident;
|
||||
|
||||
fn_generic_params.push(quote! {#generic_name});
|
||||
phantom_data_declerations.push(quote! {#field_name: core::marker::PhantomData<#generic_name>});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(fn_generic_params, phantom_data_declerations)
|
||||
}
|
||||
|
||||
/// Get only the necessary generics.
|
||||
struct FilterUsedGenerics {
|
||||
all: Vec<crate::GenericParam>,
|
||||
used: Vec<bool>,
|
||||
}
|
||||
|
||||
impl VisitMut for FilterUsedGenerics {
|
||||
fn visit_lifetime_mut(&mut self, used_lifetime: &mut Lifetime) {
|
||||
for (generic, used) in self.all.iter().zip(self.used.iter_mut()) {
|
||||
let crate::GenericParam::Lifetime(lifetime_param) = generic else { continue };
|
||||
if used_lifetime == &lifetime_param.lifetime {
|
||||
*used = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_path_mut(&mut self, path: &mut syn::Path) {
|
||||
for (index, (generic, used)) in self.all.iter().zip(self.used.iter_mut()).enumerate() {
|
||||
let crate::GenericParam::Type(type_param) = generic else { continue };
|
||||
if path.leading_colon.is_none() && !path.segments.is_empty() && path.segments[0].arguments.is_none() && path.segments[0].ident == type_param.ident {
|
||||
*used = true;
|
||||
// Sometimes the generics conflict with the type name so we rename the generics.
|
||||
path.segments[0].ident = format_ident!("G{index}");
|
||||
}
|
||||
}
|
||||
for mut el in Punctuated::pairs_mut(&mut path.segments) {
|
||||
self.visit_path_segment_mut(el.value_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FilterUsedGenerics {
|
||||
fn new(fn_generics: &[crate::GenericParam]) -> (Vec<crate::GenericParam>, Self) {
|
||||
let mut all_possible_generics = fn_generics.to_vec();
|
||||
// The 'n lifetime may also be needed; we must add it in
|
||||
all_possible_generics.insert(0, syn::GenericParam::Lifetime(syn::LifetimeParam::new(Lifetime::new("'n", proc_macro2::Span::call_site()))));
|
||||
|
||||
let modified = all_possible_generics
|
||||
.iter()
|
||||
.cloned()
|
||||
.enumerate()
|
||||
.map(|(index, mut generic)| {
|
||||
let crate::GenericParam::Type(type_param) = &mut generic else { return generic };
|
||||
// Sometimes the generics conflict with the type name so we rename the generics.
|
||||
type_param.ident = format_ident!("G{index}");
|
||||
generic
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let generic_collector = Self {
|
||||
used: vec![false; all_possible_generics.len()],
|
||||
all: all_possible_generics,
|
||||
};
|
||||
|
||||
(modified, generic_collector)
|
||||
}
|
||||
|
||||
fn used<'a>(&'a self, modified: &'a [crate::GenericParam]) -> impl Iterator<Item = &'a crate::GenericParam> {
|
||||
modified.iter().zip(&self.used).filter(|(_, used)| **used).map(move |(value, _)| value)
|
||||
}
|
||||
|
||||
fn filter_unnecessary_generics(&mut self, modified: &mut Vec<syn::GenericParam>, ty: &mut Type) -> Vec<syn::GenericParam> {
|
||||
self.used.fill(false);
|
||||
|
||||
// Find out which generics are necessary to support the node input
|
||||
self.visit_type_mut(ty);
|
||||
|
||||
// Sometimes generics may reference other generics. This is a non-optimal way of dealing with that.
|
||||
for _ in 0..=self.all.len() {
|
||||
for (index, item) in modified.iter_mut().enumerate() {
|
||||
if self.used[index] {
|
||||
self.visit_generic_param_mut(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.used(&*modified).cloned().collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use proc_macro_crate::{FoundCrate, crate_name};
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
pub struct CrateIdent {
|
||||
gcore: syn::Result<TokenStream>,
|
||||
gcore_shaders: syn::Result<TokenStream>,
|
||||
raster_types: syn::Result<TokenStream>,
|
||||
wgpu_executor: syn::Result<TokenStream>,
|
||||
}
|
||||
|
||||
impl CrateIdent {
|
||||
pub fn gcore(&self) -> syn::Result<&TokenStream> {
|
||||
self.gcore.as_ref().map_err(Clone::clone)
|
||||
}
|
||||
|
||||
pub fn gcore_shaders(&self) -> syn::Result<&TokenStream> {
|
||||
self.gcore_shaders.as_ref().map_err(Clone::clone)
|
||||
}
|
||||
|
||||
pub fn raster_types(&self) -> syn::Result<&TokenStream> {
|
||||
self.raster_types.as_ref().map_err(Clone::clone)
|
||||
}
|
||||
|
||||
pub fn wgpu_executor(&self) -> syn::Result<&TokenStream> {
|
||||
self.wgpu_executor.as_ref().map_err(Clone::clone)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CrateIdent {
|
||||
fn default() -> Self {
|
||||
let find_crate = |orig_name| match crate_name(orig_name) {
|
||||
Ok(FoundCrate::Itself) => Ok(quote!(crate)),
|
||||
Ok(FoundCrate::Name(name)) => {
|
||||
let name = format_ident!("{}", name);
|
||||
Ok(quote!(::#name))
|
||||
}
|
||||
Err(e) => Err(syn::Error::new(Span::call_site(), format!("Could not find dependency on `{orig_name}`:\n{e}"))),
|
||||
};
|
||||
|
||||
let gcore = find_crate("core-types");
|
||||
let gcore_shaders = find_crate("no-std-types").or_else(|eshaders| gcore.clone().map_err(|ecore| syn::Error::new(Span::call_site(), format!("{ecore}\n\nFallback: {eshaders}"))));
|
||||
let raster_types = find_crate("raster-types");
|
||||
let wgpu_executor = find_crate("wgpu-executor");
|
||||
Self {
|
||||
gcore,
|
||||
gcore_shaders,
|
||||
raster_types,
|
||||
wgpu_executor,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::parse::Parse;
|
||||
use syn::{Attribute, DeriveInput, Expr, LitStr, Meta};
|
||||
|
||||
pub fn derive_choice_type_impl(input_item: TokenStream) -> syn::Result<TokenStream> {
|
||||
let input = syn::parse2::<DeriveInput>(input_item).unwrap();
|
||||
|
||||
match input.data {
|
||||
syn::Data::Enum(data_enum) => derive_enum(&input.attrs, input.ident, data_enum),
|
||||
_ => Err(syn::Error::new(input.ident.span(), "Only enums are supported at the moment")),
|
||||
}
|
||||
}
|
||||
|
||||
struct Type {
|
||||
basic_item: BasicItem,
|
||||
widget_hint: WidgetHint,
|
||||
}
|
||||
|
||||
enum WidgetHint {
|
||||
Radio,
|
||||
Dropdown,
|
||||
}
|
||||
impl Parse for WidgetHint {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let tokens: Ident = input.parse()?;
|
||||
if tokens == "Radio" {
|
||||
Ok(Self::Radio)
|
||||
} else if tokens == "Dropdown" {
|
||||
Ok(Self::Dropdown)
|
||||
} else {
|
||||
Err(syn::Error::new_spanned(tokens, "Widget must be either Radio or Dropdown"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct BasicItem {
|
||||
label: String,
|
||||
description: Option<String>,
|
||||
icon: Option<String>,
|
||||
}
|
||||
impl BasicItem {
|
||||
fn read_attribute(&mut self, attribute: &Attribute) -> syn::Result<()> {
|
||||
if attribute.path().is_ident("label") {
|
||||
let token: LitStr = attribute.parse_args()?;
|
||||
self.label = token.value();
|
||||
}
|
||||
if attribute.path().is_ident("icon") {
|
||||
let token: LitStr = attribute.parse_args()?;
|
||||
self.icon = Some(token.value());
|
||||
}
|
||||
if attribute.path().is_ident("doc")
|
||||
&& let Meta::NameValue(meta_name_value) = &attribute.meta
|
||||
&& let Expr::Lit(el) = &meta_name_value.value
|
||||
&& let syn::Lit::Str(token) = &el.lit
|
||||
{
|
||||
self.description = Some(token.value());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct Variant {
|
||||
name: Ident,
|
||||
basic_item: BasicItem,
|
||||
}
|
||||
|
||||
fn derive_enum(enum_attributes: &[Attribute], name: Ident, input: syn::DataEnum) -> syn::Result<TokenStream> {
|
||||
let mut enum_info = Type {
|
||||
basic_item: BasicItem::default(),
|
||||
widget_hint: WidgetHint::Dropdown,
|
||||
};
|
||||
for attribute in enum_attributes {
|
||||
enum_info.basic_item.read_attribute(attribute)?;
|
||||
if attribute.path().is_ident("widget") {
|
||||
enum_info.widget_hint = attribute.parse_args()?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut variants = vec![Vec::new()];
|
||||
for variant in &input.variants {
|
||||
let mut basic_item = BasicItem::default();
|
||||
|
||||
for attribute in &variant.attrs {
|
||||
if attribute.path().is_ident("menu_separator") {
|
||||
attribute.meta.require_path_only()?;
|
||||
variants.push(Vec::new());
|
||||
}
|
||||
basic_item.read_attribute(attribute)?;
|
||||
}
|
||||
|
||||
if basic_item.label.is_empty() {
|
||||
basic_item.label = ident_to_label(&variant.ident);
|
||||
}
|
||||
|
||||
variants.last_mut().unwrap().push(Variant {
|
||||
name: variant.ident.clone(),
|
||||
basic_item,
|
||||
})
|
||||
}
|
||||
let display_arm: Vec<_> = variants
|
||||
.iter()
|
||||
.flat_map(|variants| variants.iter())
|
||||
.map(|variant| {
|
||||
let variant_name = &variant.name;
|
||||
let variant_label = &variant.basic_item.label;
|
||||
quote! { #name::#variant_name => write!(f, #variant_label), }
|
||||
})
|
||||
.collect();
|
||||
|
||||
let crate_name = {
|
||||
let crate_name = proc_macro_crate::crate_name("no-std-types").or_else(|_e| proc_macro_crate::crate_name("core-types")).map_err(|e| {
|
||||
syn::Error::new(
|
||||
Span::call_site(),
|
||||
format!("Failed to find location of 'no-std-types' or 'core-types'. Make sure it is imported as a dependency: {e}"),
|
||||
)
|
||||
})?;
|
||||
match crate_name {
|
||||
proc_macro_crate::FoundCrate::Itself => quote!(crate),
|
||||
proc_macro_crate::FoundCrate::Name(name) => {
|
||||
let identifier = Ident::new(&name, Span::call_site());
|
||||
quote! { #identifier }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let enum_description = match &enum_info.basic_item.description {
|
||||
Some(s) => {
|
||||
let s = s.trim();
|
||||
quote! { Some(#s) }
|
||||
}
|
||||
None => quote! { None },
|
||||
};
|
||||
let group: Vec<_> = variants
|
||||
.iter()
|
||||
.map(|variants| {
|
||||
let items = variants
|
||||
.iter()
|
||||
.map(|variant| {
|
||||
let vname = &variant.name;
|
||||
let vname_str = variant.name.to_string();
|
||||
let label = &variant.basic_item.label;
|
||||
let description = match &variant.basic_item.description {
|
||||
Some(s) => {
|
||||
let s = s.trim();
|
||||
quote! { Some(#s) }
|
||||
}
|
||||
None => quote! { None },
|
||||
};
|
||||
let icon = match &variant.basic_item.icon {
|
||||
Some(s) => quote! { Some(#s) },
|
||||
None => quote! { None },
|
||||
};
|
||||
quote! {
|
||||
(
|
||||
#name::#vname, #crate_name::choice_type::VariantMetadata {
|
||||
name: #vname_str,
|
||||
label: #label,
|
||||
description: #description,
|
||||
icon: #icon,
|
||||
}
|
||||
),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
quote! { &[ #(#items)* ], }
|
||||
})
|
||||
.collect();
|
||||
let widget_hint = match enum_info.widget_hint {
|
||||
WidgetHint::Radio => quote! { RadioButtons },
|
||||
WidgetHint::Dropdown => quote! { Dropdown },
|
||||
};
|
||||
Ok(quote! {
|
||||
impl #crate_name::AsU32 for #name {
|
||||
fn as_u32(&self) -> u32 {
|
||||
*self as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl #crate_name::choice_type::ChoiceTypeStatic for #name {
|
||||
const WIDGET_HINT: #crate_name::choice_type::ChoiceWidgetHint = #crate_name::choice_type::ChoiceWidgetHint::#widget_hint;
|
||||
const DESCRIPTION: Option<&'static str> = #enum_description;
|
||||
fn list() -> &'static [&'static [(Self, #crate_name::choice_type::VariantMetadata)]] {
|
||||
&[ #(#group)* ]
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for #name {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
#( #display_arm )*
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn ident_to_label(id: &Ident) -> String {
|
||||
use convert_case::{Case, Casing};
|
||||
id.to_string().from_case(Case::Pascal).to_case(Case::Title)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use crate::crate_ident::CrateIdent;
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro_error2::proc_macro_error;
|
||||
use syn::GenericParam;
|
||||
|
||||
mod buffer_struct;
|
||||
mod codegen;
|
||||
mod crate_ident;
|
||||
mod derive_choice_type;
|
||||
mod parsing;
|
||||
mod shader_nodes;
|
||||
mod validation;
|
||||
|
||||
/// Used to create a node definition.
|
||||
#[proc_macro_error]
|
||||
#[proc_macro_attribute]
|
||||
pub fn node(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// Performs the `node_impl` macro's functionality of attaching an `impl Node for TheGivenStruct` block to the node struct
|
||||
parsing::new_node_fn(attr.into(), item.into()).unwrap_or_else(|err| err.to_compile_error()).into()
|
||||
}
|
||||
|
||||
/// Generate meta-information for an enum.
|
||||
///
|
||||
/// `#[widget(F)]` on a type indicates the type of widget to use to display/edit the type, currently `Radio` and `Dropdown` are supported.
|
||||
///
|
||||
/// `#[label("Foo")]` on a variant overrides the default UI label (which is otherwise the name converted to title case). All labels are collected into a [`core::fmt::Display`] impl.
|
||||
///
|
||||
/// `#[icon("tag"))]` sets the icon to use when a variant is shown in a menu or radio button.
|
||||
///
|
||||
/// Doc comments on a variant become tooltip description text.
|
||||
#[proc_macro_derive(ChoiceType, attributes(widget, menu_separator, label, icon))]
|
||||
pub fn derive_choice_type(input_item: TokenStream) -> TokenStream {
|
||||
derive_choice_type::derive_choice_type_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()).into()
|
||||
}
|
||||
|
||||
/// Derive a struct to implement `ShaderStruct`, see that for docs.
|
||||
#[proc_macro_derive(BufferStruct)]
|
||||
pub fn derive_buffer_struct(input_item: TokenStream) -> TokenStream {
|
||||
let crate_ident = CrateIdent::default();
|
||||
TokenStream::from(buffer_struct::derive_buffer_struct(&crate_ident, input_item).unwrap_or_else(|err| err.to_compile_error()))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
use crate::crate_ident::CrateIdent;
|
||||
use crate::parsing::{NodeFnAttributes, ParsedNodeFn};
|
||||
use crate::shader_nodes::per_pixel_adjust::PerPixelAdjust;
|
||||
use proc_macro2::{Ident, TokenStream};
|
||||
use quote::quote;
|
||||
use strum::VariantNames;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::{Error, Token};
|
||||
|
||||
pub mod per_pixel_adjust;
|
||||
|
||||
pub const STD_FEATURE_GATE: &str = "std";
|
||||
pub const SHADER_NODES_FEATURE_GATE: &str = "shader-nodes";
|
||||
|
||||
pub fn modify_cfg(attributes: &NodeFnAttributes) -> TokenStream {
|
||||
let feature_gate = match &attributes.shader_node {
|
||||
// shader node cfg is done on the mod
|
||||
Some(ShaderNodeType::ShaderNode) => quote!(),
|
||||
Some(_) => quote!(feature = #STD_FEATURE_GATE),
|
||||
None => quote!(),
|
||||
};
|
||||
let cfgs: Punctuated<_, Token![,]> = match &attributes.cfg {
|
||||
None => [&feature_gate].into_iter().collect(),
|
||||
Some(cfg) => [cfg, &feature_gate].into_iter().collect(),
|
||||
};
|
||||
quote!(#[cfg(all(#cfgs))])
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, VariantNames)]
|
||||
pub(crate) enum ShaderNodeType {
|
||||
/// Marker for this node being in a gpu node crate, but not having a gpu implementation. This is distinct from not
|
||||
/// declaring `shader_node` at all, as it will wrap the CPU node with a `#[cfg(feature = "std")]` feature gate.
|
||||
None,
|
||||
/// Marker for this node being a generated gpu node implementation, that should not emit anything to prevent
|
||||
/// recursively generating more gpu nodes. But it still counts as a gpu node and will get the
|
||||
/// `#[cfg(feature = "std")]` feature gate around it's impl.
|
||||
ShaderNode,
|
||||
PerPixelAdjust(PerPixelAdjust),
|
||||
}
|
||||
|
||||
impl Parse for ShaderNodeType {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let ident: Ident = input.parse()?;
|
||||
Ok(match ident.to_string().as_str() {
|
||||
"None" => ShaderNodeType::None,
|
||||
"PerPixelAdjust" => ShaderNodeType::PerPixelAdjust(PerPixelAdjust::parse(input)?),
|
||||
_ => return Err(Error::new_spanned(&ident, format!("attr 'shader_node' must be one of {:?}", Self::VARIANTS))),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ShaderCodegen {
|
||||
fn codegen(&self, crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result<ShaderTokens>;
|
||||
}
|
||||
|
||||
impl ShaderCodegen for ShaderNodeType {
|
||||
fn codegen(&self, crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result<ShaderTokens> {
|
||||
match self {
|
||||
ShaderNodeType::None | ShaderNodeType::ShaderNode => (),
|
||||
_ => {
|
||||
if parsed.is_async {
|
||||
return Err(Error::new_spanned(&parsed.fn_name, "Shader nodes must not be async"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match self {
|
||||
ShaderNodeType::None | ShaderNodeType::ShaderNode => Ok(ShaderTokens::default()),
|
||||
ShaderNodeType::PerPixelAdjust(x) => x.codegen(crate_ident, parsed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ShaderTokens {
|
||||
pub shader_entry_point: TokenStream,
|
||||
pub gpu_node: TokenStream,
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
use crate::crate_ident::CrateIdent;
|
||||
use crate::parsing::{Input, NodeFnAttributes, ParsedField, ParsedFieldType, ParsedNodeFn, ParsedValueSource, RegularParsedField};
|
||||
use crate::shader_nodes::{SHADER_NODES_FEATURE_GATE, ShaderCodegen, ShaderNodeType, ShaderTokens};
|
||||
use convert_case::{Case, Casing};
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::{ToTokens, format_ident, quote};
|
||||
use std::borrow::Cow;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::{LitStr, PatIdent, Type, parse_quote};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PerPixelAdjust {}
|
||||
|
||||
impl Parse for PerPixelAdjust {
|
||||
fn parse(_input: ParseStream) -> syn::Result<Self> {
|
||||
Ok(Self {})
|
||||
}
|
||||
}
|
||||
|
||||
impl ShaderCodegen for PerPixelAdjust {
|
||||
fn codegen(&self, crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result<ShaderTokens> {
|
||||
let fn_name = &parsed.fn_name;
|
||||
|
||||
let mut params;
|
||||
let has_uniform;
|
||||
{
|
||||
// categorize params
|
||||
params = parsed
|
||||
.fields
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let ident = &f.pat_ident;
|
||||
if let ParsedFieldType::Node { .. } = &f.ty {
|
||||
return Err(syn::Error::new_spanned(ident, "PerPixelAdjust shader nodes cannot accept other nodes as generics"));
|
||||
}
|
||||
let regular = f.ty.regular().expect("a non-node shader field is a value field");
|
||||
// Ranked connectors carry their bare element type in the uniform buffer and get rewrapped in the entry point
|
||||
let element_ty = f.ty.item_element();
|
||||
if regular.gpu_image {
|
||||
Ok(Param {
|
||||
ident: Cow::Owned(format_ident!("image_{}", &ident.ident)),
|
||||
ty: quote!(Image2d),
|
||||
item_wrapped: element_ty.is_some(),
|
||||
param_type: ParamType::Image { binding: 0 },
|
||||
})
|
||||
} else {
|
||||
Ok(Param {
|
||||
ident: Cow::Borrowed(&ident.ident),
|
||||
item_wrapped: element_ty.is_some(),
|
||||
ty: element_ty.map(|element_ty| element_ty.to_token_stream()).unwrap_or_else(|| regular.ty.to_token_stream()),
|
||||
param_type: ParamType::Uniform,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<syn::Result<Vec<_>>>()?;
|
||||
|
||||
has_uniform = params.iter().any(|p| matches!(p.param_type, ParamType::Uniform));
|
||||
|
||||
// assign image bindings
|
||||
// if an arg_buffer exists, bindings for images start at 1 to leave 0 for arg buffer
|
||||
let mut binding_cnt = if has_uniform { 1 } else { 0 };
|
||||
for p in params.iter_mut() {
|
||||
match &mut p.param_type {
|
||||
ParamType::Image { binding } => {
|
||||
*binding = binding_cnt;
|
||||
binding_cnt += 1;
|
||||
}
|
||||
ParamType::Uniform => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let entry_point_mod = format_ident!("{}_gpu", fn_name);
|
||||
let entry_point_name_ident = format_ident!("ENTRY_POINT_NAME");
|
||||
let entry_point_name = quote!(#entry_point_mod::#entry_point_name_ident);
|
||||
let uniform_struct_ident = format_ident!("Uniform");
|
||||
let uniform_struct = quote!(#entry_point_mod::#uniform_struct_ident);
|
||||
let shader_node_mod = format_ident!("{}_shader_node", fn_name);
|
||||
|
||||
let codegen = PerPixelAdjustCodegen {
|
||||
crate_ident,
|
||||
parsed,
|
||||
params,
|
||||
has_uniform,
|
||||
entry_point_mod,
|
||||
entry_point_name_ident,
|
||||
entry_point_name,
|
||||
uniform_struct_ident,
|
||||
uniform_struct,
|
||||
shader_node_mod,
|
||||
};
|
||||
|
||||
Ok(ShaderTokens {
|
||||
shader_entry_point: codegen.codegen_shader_entry_point()?,
|
||||
gpu_node: codegen.codegen_gpu_node()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PerPixelAdjustCodegen<'a> {
|
||||
crate_ident: &'a CrateIdent,
|
||||
parsed: &'a ParsedNodeFn,
|
||||
params: Vec<Param<'a>>,
|
||||
has_uniform: bool,
|
||||
entry_point_mod: Ident,
|
||||
entry_point_name_ident: Ident,
|
||||
entry_point_name: TokenStream,
|
||||
uniform_struct_ident: Ident,
|
||||
uniform_struct: TokenStream,
|
||||
shader_node_mod: Ident,
|
||||
}
|
||||
|
||||
impl PerPixelAdjustCodegen<'_> {
|
||||
fn codegen_shader_entry_point(&self) -> syn::Result<TokenStream> {
|
||||
let fn_name = &self.parsed.fn_name;
|
||||
let gcore_shaders = self.crate_ident.gcore_shaders()?;
|
||||
let reexport = quote!(#gcore_shaders::shaders::__private);
|
||||
|
||||
let uniform_members = self
|
||||
.params
|
||||
.iter()
|
||||
.filter_map(|Param { ident, ty, param_type, .. }| match param_type {
|
||||
ParamType::Image { .. } => None,
|
||||
ParamType::Uniform => Some(quote! {#ident: #ty}),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let uniform_struct_ident = &self.uniform_struct_ident;
|
||||
let uniform_struct = parse_quote! {
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct #uniform_struct_ident {
|
||||
#(pub #uniform_members),*
|
||||
}
|
||||
};
|
||||
let uniform_struct_shader_struct_derive = crate::buffer_struct::derive_buffer_struct_struct(self.crate_ident, &uniform_struct)?;
|
||||
|
||||
let image_params = self
|
||||
.params
|
||||
.iter()
|
||||
.filter_map(|Param { ident, ty, param_type, .. }| match param_type {
|
||||
ParamType::Image { binding } => Some(quote! {#[spirv(descriptor_set = 0, binding = #binding)] #ident: &#ty}),
|
||||
ParamType::Uniform => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// Ranked connectors compile against the no_std `Item` stand-in on the GPU, so wrapping and unwrapping here is free
|
||||
let call_args = self
|
||||
.params
|
||||
.iter()
|
||||
.map(|Param { ident, param_type, item_wrapped, .. }| {
|
||||
let bare_value = match param_type {
|
||||
ParamType::Image { .. } => quote!(Color::from_vec4(#ident.fetch_with(texel_coord, lod(0)))),
|
||||
ParamType::Uniform => quote!(uniform.#ident),
|
||||
};
|
||||
if *item_wrapped { quote!(Item::new_from_element(#bare_value)) } else { bare_value }
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let unwrap_result = self.parsed.output_element.as_ref().map(|_| quote!(.into_element()));
|
||||
let context = quote!(());
|
||||
|
||||
let entry_point_mod = &self.entry_point_mod;
|
||||
let entry_point_name = &self.entry_point_name_ident;
|
||||
Ok(quote! {
|
||||
pub mod #entry_point_mod {
|
||||
use super::*;
|
||||
use #gcore_shaders::color::Color;
|
||||
use #reexport::glam::{Vec4, Vec4Swizzles};
|
||||
use #reexport::spirv_std::spirv;
|
||||
use #reexport::spirv_std::image::{Image2d, ImageWithMethods};
|
||||
use #reexport::spirv_std::image::sample_with::lod;
|
||||
|
||||
pub const #entry_point_name: &str = core::concat!(core::module_path!(), "::entry_point");
|
||||
|
||||
#uniform_struct
|
||||
#uniform_struct_shader_struct_derive
|
||||
|
||||
#[spirv(fragment)]
|
||||
pub fn entry_point(
|
||||
#[spirv(frag_coord)] frag_coord: Vec4,
|
||||
color_out: &mut Vec4,
|
||||
#[spirv(descriptor_set = 0, binding = 0, storage_buffer)] uniform: &UniformBuffer,
|
||||
#(#image_params),*
|
||||
) {
|
||||
let uniform = <Uniform as #gcore_shaders::shaders::buffer_struct::BufferStruct>::read(*uniform);
|
||||
let texel_coord = frag_coord.xy().as_uvec2();
|
||||
let color: Color = #fn_name(#context, #(#call_args),*)#unwrap_result;
|
||||
*color_out = color.to_vec4();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn codegen_gpu_node(&self) -> syn::Result<TokenStream> {
|
||||
let gcore = self.crate_ident.gcore()?;
|
||||
let raster_types = self.crate_ident.raster_types()?;
|
||||
let wgpu_executor = self.crate_ident.wgpu_executor()?;
|
||||
|
||||
// adapt fields for gpu node
|
||||
let raster_gpu: Type = parse_quote!(#gcore::list::List<#raster_types::Raster<#raster_types::GPU>>);
|
||||
let mut fields = self
|
||||
.parsed
|
||||
.fields
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if let ParsedFieldType::Node { .. } = &f.ty {
|
||||
return Err(syn::Error::new_spanned(&f.pat_ident, "PerPixelAdjust shader nodes cannot accept other nodes as generics"));
|
||||
}
|
||||
let regular = f.ty.regular().expect("a non-node shader field is a value field");
|
||||
let pat_ident = PatIdent {
|
||||
mutability: None,
|
||||
by_ref: None,
|
||||
..f.pat_ident.clone()
|
||||
};
|
||||
// A gpu_image parameter is fed the whole GPU raster list, so its wire is re-typed and re-classified
|
||||
let ty = if regular.gpu_image {
|
||||
ParsedFieldType::classify(RegularParsedField {
|
||||
ty: raster_gpu.clone(),
|
||||
implementations: Punctuated::default(),
|
||||
..regular.clone()
|
||||
})
|
||||
} else {
|
||||
f.ty.clone()
|
||||
};
|
||||
Ok(ParsedField { pat_ident, ty, ..f.clone() })
|
||||
})
|
||||
.collect::<syn::Result<Vec<_>>>()?;
|
||||
|
||||
// insert wgpu_executor field
|
||||
let executor = format_ident!("__wgpu_executor");
|
||||
fields.push(ParsedField {
|
||||
pat_ident: PatIdent {
|
||||
attrs: vec![],
|
||||
by_ref: None,
|
||||
mutability: None,
|
||||
ident: parse_quote!(#executor),
|
||||
subpat: None,
|
||||
},
|
||||
name: None,
|
||||
description: "".to_string(),
|
||||
widget_override: Default::default(),
|
||||
ty: ParsedFieldType::classify(RegularParsedField {
|
||||
ty: parse_quote!(#gcore::list::Item<&'a WgpuExecutor>),
|
||||
exposed: true,
|
||||
value_source: ParsedValueSource::Scope(parse_quote!("graphene_std::platform_application_io::WgpuExecutorNode")),
|
||||
number_soft_min: None,
|
||||
number_soft_max: None,
|
||||
number_hard_min: None,
|
||||
number_hard_max: None,
|
||||
number_mode_range: false,
|
||||
implementations: Default::default(),
|
||||
gpu_image: false,
|
||||
}),
|
||||
number_display_decimal_places: None,
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
});
|
||||
|
||||
// find exactly one gpu_image field, runtime doesn't support more than 1 atm
|
||||
let gpu_image_field = {
|
||||
let mut iter = fields.iter().filter(|f| f.ty.regular().is_some_and(|regular| regular.gpu_image));
|
||||
match (iter.next(), iter.next()) {
|
||||
(Some(v), None) => Ok(v),
|
||||
(Some(_), Some(more)) => Err(syn::Error::new_spanned(&more.pat_ident, "No more than one parameter must be annotated with `#[gpu_image]`")),
|
||||
(None, _) => Err(syn::Error::new_spanned(&self.parsed.fn_name, "At least one parameter must be annotated with `#[gpu_image]`")),
|
||||
}?
|
||||
};
|
||||
let gpu_image = &gpu_image_field.pat_ident.ident;
|
||||
|
||||
// uniform buffer struct construction
|
||||
let has_uniform = self.has_uniform;
|
||||
let uniform_buffer = if has_uniform {
|
||||
let uniform_struct = &self.uniform_struct;
|
||||
let uniform_members = self
|
||||
.params
|
||||
.iter()
|
||||
.filter_map(|p| match p.param_type {
|
||||
ParamType::Image { .. } => None,
|
||||
ParamType::Uniform => {
|
||||
let ident = p.ident.as_ref();
|
||||
Some(if p.item_wrapped { quote!(#ident: #ident.into_element()) } else { quote!(#ident) })
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
quote!(Some(&super::#uniform_struct {
|
||||
#(#uniform_members),*
|
||||
}))
|
||||
} else {
|
||||
// explicit generics placed here cause it's easier than explicitly writing `run_per_pixel_adjust::<()>`
|
||||
quote!(Option::<&()>::None)
|
||||
};
|
||||
|
||||
// node function body
|
||||
let entry_point_name = &self.entry_point_name;
|
||||
let body = quote! {
|
||||
{
|
||||
#executor.into_element().shader_runtime().run_per_pixel_adjust(&::wgpu_executor::shader_runtime::per_pixel_adjust_runtime::Shaders {
|
||||
wgsl_shader: crate::WGSL_SHADER,
|
||||
fragment_shader_name: super::#entry_point_name,
|
||||
has_uniform: #has_uniform,
|
||||
}, #gpu_image, #uniform_buffer).await
|
||||
}
|
||||
};
|
||||
|
||||
// call node codegen
|
||||
let display_name = self.parsed.attributes.display_name.clone();
|
||||
let display_name = display_name.unwrap_or_else(|| LitStr::new(&self.shader_node_mod.to_string().strip_suffix("_shader_node").unwrap().to_case(Case::Title), Span::call_site()));
|
||||
let display_name = LitStr::new(&format!("{} GPU", display_name.value()), display_name.span());
|
||||
let mut parsed_node_fn = ParsedNodeFn {
|
||||
vis: self.parsed.vis.clone(),
|
||||
attributes: NodeFnAttributes {
|
||||
display_name: Some(display_name),
|
||||
shader_node: Some(ShaderNodeType::ShaderNode),
|
||||
..self.parsed.attributes.clone()
|
||||
},
|
||||
fn_name: self.shader_node_mod.clone(),
|
||||
struct_name: format_ident!("{}", self.shader_node_mod.to_string().to_case(Case::Pascal)),
|
||||
mod_name: self.shader_node_mod.clone(),
|
||||
fn_generics: vec![parse_quote!('a: 'n)],
|
||||
where_clause: None,
|
||||
input: Input {
|
||||
pat_ident: self.parsed.input.pat_ident.clone(),
|
||||
ty: parse_quote!(impl #gcore::context::Ctx),
|
||||
implementations: Default::default(),
|
||||
context_features: self.parsed.input.context_features.clone(),
|
||||
},
|
||||
output_type: raster_gpu,
|
||||
output_element: None,
|
||||
is_async: true,
|
||||
fields,
|
||||
body,
|
||||
description: self.parsed.description.clone(),
|
||||
};
|
||||
parsed_node_fn.replace_impl_trait_in_input();
|
||||
let gpu_node_impl = crate::codegen::generate_node_code(self.crate_ident, &parsed_node_fn)?;
|
||||
|
||||
// wrap node in `mod #gpu_node_mod`
|
||||
let shader_node_mod = &self.shader_node_mod;
|
||||
Ok(quote! {
|
||||
#[cfg(feature = #SHADER_NODES_FEATURE_GATE)]
|
||||
mod #shader_node_mod {
|
||||
use super::*;
|
||||
use #wgpu_executor::WgpuExecutor;
|
||||
|
||||
#gpu_node_impl
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Param<'a> {
|
||||
ident: Cow<'a, Ident>,
|
||||
ty: TokenStream,
|
||||
item_wrapped: bool,
|
||||
param_type: ParamType,
|
||||
}
|
||||
|
||||
enum ParamType {
|
||||
Image { binding: u32 },
|
||||
Uniform,
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
use crate::parsing::{Implementation, NodeParsedField, ParsedFieldType, ParsedNodeFn, RegularParsedField};
|
||||
use proc_macro_error2::emit_error;
|
||||
use quote::quote;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{GenericParam, Type};
|
||||
|
||||
pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> {
|
||||
let validators: &[fn(&ParsedNodeFn)] = &[
|
||||
// Add more validators here as needed
|
||||
validate_implementations_for_generics,
|
||||
validate_primary_input_expose,
|
||||
validate_min_max,
|
||||
validate_range_slider_bounds,
|
||||
validate_no_item_parameters,
|
||||
validate_element_wise,
|
||||
validate_ranked_inputs,
|
||||
];
|
||||
|
||||
for validator in validators {
|
||||
validator(parsed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_no_item_parameters(parsed: &ParsedNodeFn) {
|
||||
if parsed.attributes.skip_impl {
|
||||
return;
|
||||
}
|
||||
|
||||
// An `Item` primary shares its element-wise frame with ranked parameters; a `List`/`ListDyn` aggregation primary accepts
|
||||
// them as fixed ranked inputs; a `()` generator has no primary and draws its frame from the ranked parameters themselves.
|
||||
let ranked = |ty: &Type| outer_wrapper_is(ty, "Item") || outer_wrapper_is(ty, "List") || outer_wrapper_is(ty, "ListDyn");
|
||||
let primary = parsed.primary_input_field();
|
||||
let primary_permits_item_params = match primary.map(|(_, field)| &field.ty) {
|
||||
Some(ParsedFieldType::Node(NodeParsedField { output_type, implementations, .. })) => ranked(output_type) || implementations.iter().any(|implementation| ranked(&implementation.output)),
|
||||
Some(value) => {
|
||||
let regular = value.regular().expect("a non-node primary is a value field");
|
||||
is_unit_type(®ular.ty) || ranked(®ular.ty) || regular.implementations.iter().any(ranked)
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
let primary_index = primary.map(|(index, _)| index);
|
||||
|
||||
for (index, field) in parsed.fields.iter().enumerate() {
|
||||
if Some(index) == primary_index || field.is_environment() {
|
||||
continue;
|
||||
}
|
||||
let Some(RegularParsedField { ty, implementations, .. }) = field.ty.regular() else {
|
||||
continue;
|
||||
};
|
||||
let pat_ident = &field.pat_ident;
|
||||
|
||||
// A ranked parameter requires a ranked primary: `Item<T>` (element-wise frame) or `List<T>`/`ListDyn` (aggregation)
|
||||
if outer_wrapper_is(ty, "Item") && !primary_permits_item_params {
|
||||
emit_error!(
|
||||
pat_ident.span(),
|
||||
"The `Item<T>` parameter `{}` requires the primary input to be ranked (`Item<T>`, `List<T>`, or `ListDyn`)",
|
||||
pat_ident.ident
|
||||
);
|
||||
}
|
||||
|
||||
if outer_wrapper_is(ty, "Item")
|
||||
&& implementations
|
||||
.iter()
|
||||
.any(|ty| outer_wrapper_is(ty, "Item") || outer_wrapper_is(ty, "List") || outer_wrapper_is(ty, "ListDyn"))
|
||||
{
|
||||
emit_error!(pat_ident.span(), "The #[implementations(...)] of the ranked parameter `{}` must be bare element types", pat_ident.ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_element_wise(parsed: &ParsedNodeFn) {
|
||||
if parsed.attributes.skip_impl {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some((_, primary)) = parsed.primary_input_field() else { return };
|
||||
let Some(RegularParsedField { ty, implementations, .. }) = primary.ty.regular() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !outer_wrapper_is(ty, "Item") {
|
||||
// A non-`Item` primary may still emit a rank-0 `Item<T>`: a `()` generator has no input, and a `List<T>` or
|
||||
// `ListDyn` aggregation (declared directly or via a generic primary's implementation rows) reduces a whole list down to a single item.
|
||||
let primary_reduces_or_generates = is_unit_type(ty)
|
||||
|| primary.ty.list_element().is_some()
|
||||
|| outer_wrapper_is(ty, "ListDyn")
|
||||
|| implementations.iter().any(|ty| outer_wrapper_is(ty, "List") || outer_wrapper_is(ty, "ListDyn"));
|
||||
if outer_wrapper_is(&parsed.output_type, "Item") && !primary_reduces_or_generates {
|
||||
emit_error!(
|
||||
parsed.output_type.span(),
|
||||
"Returning `Item<T>` requires the primary input to be `Item<T>` (element-wise), `List<T>`/`ListDyn` (aggregation), or `()` (generator)"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if implementations
|
||||
.iter()
|
||||
.any(|ty| outer_wrapper_is(ty, "List") || outer_wrapper_is(ty, "Item") || outer_wrapper_is(ty, "ListDyn"))
|
||||
{
|
||||
emit_error!(
|
||||
primary.pat_ident.span(),
|
||||
"The #[implementations(...)] of `{}` must be bare element types; the macro derives the Item and List wire forms",
|
||||
primary.pat_ident.ident
|
||||
);
|
||||
}
|
||||
|
||||
if !outer_wrapper_is(&parsed.output_type, "Item") && !outer_wrapper_is(&parsed.output_type, "List") {
|
||||
emit_error!(
|
||||
parsed.output_type.span(),
|
||||
"An element-wise node (declared by its `Item<T>` primary input) must return `Item<U>`, or `List<U>` for an expander"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every input must ride a ranked wire: `Item<T>`, `List<T>`, or `ListDyn`, declared directly or substituted per
|
||||
/// implementations row when the field's type is a bare generic. The () type stays legal as the no-primary-input
|
||||
/// sentinel for generator nodes, and `#[data]` fields are exempt as internal state rather than wires.
|
||||
fn validate_ranked_inputs(parsed: &ParsedNodeFn) {
|
||||
for (span, message) in ranked_input_violations(parsed) {
|
||||
emit_error!(span, "{}", message);
|
||||
}
|
||||
}
|
||||
|
||||
fn ranked_input_violations(parsed: &ParsedNodeFn) -> Vec<(proc_macro2::Span, String)> {
|
||||
let mut violations = Vec::new();
|
||||
if parsed.attributes.skip_impl {
|
||||
return violations;
|
||||
}
|
||||
|
||||
let ranked = |ty: &Type| outer_wrapper_is(ty, "Item") || outer_wrapper_is(ty, "List") || outer_wrapper_is(ty, "ListDyn");
|
||||
let primary_index = parsed.primary_input_field().map(|(index, _)| index);
|
||||
|
||||
for (index, field) in parsed.fields.iter().enumerate() {
|
||||
if field.is_data_field {
|
||||
continue;
|
||||
}
|
||||
let pat_ident = &field.pat_ident;
|
||||
|
||||
match &field.ty {
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, implementations, .. }) => {
|
||||
if ranked(output_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_bare_generic(output_type, &parsed.fn_generics) {
|
||||
for row in implementations {
|
||||
let output = &row.output;
|
||||
if !ranked(output) {
|
||||
violations.push((
|
||||
output.span(),
|
||||
format!(
|
||||
"Implementations row output `{ty}` of the lazy input `{name}` must be ranked: produce `Item<{ty}>` for one cell or `List<{ty}>` for a whole list",
|
||||
name = pat_ident.ident,
|
||||
ty = quote!(#output)
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
violations.push((
|
||||
pat_ident.span(),
|
||||
format!(
|
||||
"Lazy input `{name}` with output type `{ty}` must be ranked: declare its `Output` as `Item<{ty}>` for one cell or `List<{ty}>` for a whole list",
|
||||
name = pat_ident.ident,
|
||||
ty = quote!(#output_type)
|
||||
),
|
||||
));
|
||||
}
|
||||
value => {
|
||||
let RegularParsedField { ty, implementations, .. } = value.regular().expect("a non-node field is a value field");
|
||||
|
||||
if is_unit_type(ty) {
|
||||
if Some(index) != primary_index {
|
||||
violations.push((
|
||||
pat_ident.span(),
|
||||
format!(
|
||||
"Parameter `{}` cannot be typed `()`: the unit type is only the no-primary-input sentinel for generator nodes",
|
||||
pat_ident.ident
|
||||
),
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ranked(ty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// A bare-generic field takes each implementations row as its whole wire type, so the rows carry the rank
|
||||
if is_bare_generic(ty, &parsed.fn_generics) {
|
||||
for row in implementations {
|
||||
if !ranked(row) && !is_bare_generic(row, &parsed.fn_generics) {
|
||||
violations.push((
|
||||
row.span(),
|
||||
format!(
|
||||
"Implementations row `{row}` of `{name}` must be ranked: wrap it as `Item<{row}>` to consume one cell or `List<{row}>` to consume a whole list",
|
||||
name = pat_ident.ident,
|
||||
row = quote!(#row)
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
violations.push((
|
||||
pat_ident.span(),
|
||||
format!(
|
||||
"Parameter `{name}` of type `{ty}` must be ranked: wrap it as `Item<{ty}>` to consume one cell or `List<{ty}>` to consume a whole list",
|
||||
name = pat_ident.ident,
|
||||
ty = quote!(#ty)
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
violations
|
||||
}
|
||||
|
||||
/// Returns whether the type is exactly one of the function's generic parameters, like `T`.
|
||||
fn is_bare_generic(ty: &Type, fn_generics: &[GenericParam]) -> bool {
|
||||
let Type::Path(type_path) = ty else { return false };
|
||||
let Some(ident) = type_path.path.get_ident() else { return false };
|
||||
fn_generics.iter().any(|param| matches!(param, GenericParam::Type(type_param) if type_param.ident == *ident))
|
||||
}
|
||||
|
||||
/// Returns whether the type's outermost path segment is the given wrapper name, like `Item` in `Item<T>`.
|
||||
fn outer_wrapper_is(ty: &Type, wrapper: &str) -> bool {
|
||||
let Type::Path(type_path) = ty else { return false };
|
||||
type_path.path.segments.last().is_some_and(|segment| segment.ident == wrapper)
|
||||
}
|
||||
|
||||
/// Returns whether the type is the unit type `()`, which marks a generator with no primary input.
|
||||
fn is_unit_type(ty: &Type) -> bool {
|
||||
matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty())
|
||||
}
|
||||
|
||||
fn validate_min_max(parsed: &ParsedNodeFn) {
|
||||
for field in &parsed.fields {
|
||||
if let Some(RegularParsedField {
|
||||
number_hard_max,
|
||||
number_hard_min,
|
||||
number_soft_max,
|
||||
number_soft_min,
|
||||
..
|
||||
}) = field.ty.regular()
|
||||
{
|
||||
let pat_ident = &field.pat_ident;
|
||||
if let (Some(soft_min), Some(hard_min)) = (number_soft_min, number_hard_min) {
|
||||
let soft_min_value: f64 = soft_min.to_f64();
|
||||
let hard_min_value: f64 = hard_min.to_f64();
|
||||
if soft_min_value == hard_min_value {
|
||||
emit_error!(
|
||||
pat_ident.span(),
|
||||
"Redundant lower bound on `{}`: the #[soft] and #[hard] lower bounds are equal.",
|
||||
pat_ident.ident;
|
||||
help = "Drop the lower bound from #[soft] and let the slider fall back to #[hard].";
|
||||
note = "A soft bound only matters when it sits inside the corresponding hard bound.",
|
||||
);
|
||||
} else if soft_min_value < hard_min_value {
|
||||
emit_error!(
|
||||
pat_ident.span(),
|
||||
"The #[soft] lower bound on `{}` is below the #[hard] lower bound.",
|
||||
pat_ident.ident;
|
||||
help = "The soft (slider) range must stay within the hard (clamped) range.";
|
||||
note = "Letting the slider range precede #[hard]'s lower bound doesn't make sense.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(soft_max), Some(hard_max)) = (number_soft_max, number_hard_max) {
|
||||
let soft_max_value: f64 = soft_max.to_f64();
|
||||
let hard_max_value: f64 = hard_max.to_f64();
|
||||
if soft_max_value == hard_max_value {
|
||||
emit_error!(
|
||||
pat_ident.span(),
|
||||
"Redundant upper bound on `{}`: the #[soft] and #[hard] upper bounds are equal.",
|
||||
pat_ident.ident;
|
||||
help = "Drop the upper bound from #[soft] and let the slider fall back to #[hard].";
|
||||
note = "A soft bound only matters when it sits inside the corresponding hard bound.",
|
||||
);
|
||||
} else if soft_max_value > hard_max_value {
|
||||
emit_error!(
|
||||
pat_ident.span(),
|
||||
"The #[soft] upper bound on `{}` is above the #[hard] upper bound.",
|
||||
pat_ident.ident;
|
||||
help = "The soft (slider) range must stay within the hard (clamped) range.";
|
||||
note = "Letting the slider range exceed #[hard]'s upper bound doesn't make sense.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `#[range]` slider needs a defined extent on both ends. The extent comes from `#[soft]` when present,
|
||||
/// otherwise it falls back to `#[hard]`, so each end must be covered by at least one of the two attributes.
|
||||
fn validate_range_slider_bounds(parsed: &ParsedNodeFn) {
|
||||
for field in &parsed.fields {
|
||||
if let Some(RegularParsedField {
|
||||
number_mode_range: true,
|
||||
number_soft_min,
|
||||
number_soft_max,
|
||||
number_hard_min,
|
||||
number_hard_max,
|
||||
..
|
||||
}) = field.ty.regular()
|
||||
{
|
||||
let pat_ident = &field.pat_ident;
|
||||
let min_bounded = number_soft_min.is_some() || number_hard_min.is_some();
|
||||
let max_bounded = number_soft_max.is_some() || number_hard_max.is_some();
|
||||
|
||||
let missing = match (min_bounded, max_bounded) {
|
||||
(true, true) => continue,
|
||||
(false, false) => "lower and upper bounds",
|
||||
(false, true) => "a lower bound",
|
||||
(true, false) => "an upper bound",
|
||||
};
|
||||
|
||||
emit_error!(
|
||||
pat_ident.span(),
|
||||
"The #[range] slider on `{}` is missing {}.",
|
||||
pat_ident.ident, missing;
|
||||
help = "A slider needs both ends defined; add the missing bound via #[soft(..)] or #[hard(..)], e.g. #[soft(0..100)].";
|
||||
note = "The slider's extent comes from #[soft] if present, otherwise #[hard].",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_primary_input_expose(parsed: &ParsedNodeFn) {
|
||||
if let Some(field) = parsed.fields.first()
|
||||
&& let Some(RegularParsedField { exposed: true, .. }) = field.ty.regular()
|
||||
{
|
||||
let pat_ident = &field.pat_ident;
|
||||
emit_error!(
|
||||
pat_ident.span(),
|
||||
"Unnecessary #[expose] attribute on primary input `{}`. Primary inputs are always exposed.",
|
||||
pat_ident.ident;
|
||||
help = "You can safely remove the #[expose] attribute from this field.";
|
||||
note = "The function's second argument, `{}`, is the node's primary input and it's always exposed by default", pat_ident.ident
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_implementations_for_generics(parsed: &ParsedNodeFn) {
|
||||
let has_skip_impl = parsed.attributes.skip_impl;
|
||||
|
||||
if !has_skip_impl && !parsed.fn_generics.is_empty() {
|
||||
for field in &parsed.fields {
|
||||
// Skip validation for data fields - they're internal state and can be generic
|
||||
if field.is_data_field {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pat_ident = &field.pat_ident;
|
||||
match &field.ty {
|
||||
ParsedFieldType::Node(NodeParsedField {
|
||||
input_type,
|
||||
output_type,
|
||||
implementations,
|
||||
..
|
||||
}) => {
|
||||
if (contains_generic_param(input_type, &parsed.fn_generics) || contains_generic_param(output_type, &parsed.fn_generics)) && implementations.is_empty() {
|
||||
emit_error!(
|
||||
pat_ident.span(),
|
||||
"Generic types in Node field `{}` require an #[implementations(...)] attribute",
|
||||
pat_ident.ident;
|
||||
help = "Add #[implementations(InputType1 -> OutputType1, InputType2 -> OutputType2)] to field '{}'", pat_ident.ident;
|
||||
help = "Or use #[node_macro::node(category(...), skip_impl)] if you want to manually implement the node"
|
||||
);
|
||||
}
|
||||
// Additional check for Node implementations
|
||||
for impl_ in implementations {
|
||||
validate_node_implementation(impl_, input_type, output_type, &parsed.fn_generics);
|
||||
}
|
||||
}
|
||||
value => {
|
||||
let RegularParsedField { ty, implementations, .. } = value.regular().expect("a non-node field is a value field");
|
||||
if contains_generic_param(ty, &parsed.fn_generics) && implementations.is_empty() {
|
||||
emit_error!(
|
||||
ty.span(),
|
||||
"Generic type `{}` in field `{}` requires an #[implementations(...)] attribute",
|
||||
quote!(#ty),
|
||||
pat_ident.ident;
|
||||
help = "Add #[implementations(ConcreteType1, ConcreteType2)] to field '{}'", pat_ident.ident;
|
||||
help = "Or use #[node_macro::node(category(...), skip_impl)] if you want to manually implement the node"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_node_implementation(impl_: &Implementation, input_type: &Type, output_type: &Type, fn_generics: &[GenericParam]) {
|
||||
if contains_generic_param(&impl_.input, fn_generics) || contains_generic_param(&impl_.output, fn_generics) {
|
||||
emit_error!(
|
||||
impl_.input.span(),
|
||||
"Implementation types `{}` and `{}` must be concrete, not generic",
|
||||
quote!(#input_type), quote!(#output_type);
|
||||
help = "Replace generic types with concrete types in the implementation"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_generic_param(ty: &Type, fn_generics: &[GenericParam]) -> bool {
|
||||
struct GenericParamChecker<'a> {
|
||||
fn_generics: &'a [GenericParam],
|
||||
found: bool,
|
||||
}
|
||||
|
||||
impl<'a> syn::visit::Visit<'a> for GenericParamChecker<'a> {
|
||||
fn visit_ident(&mut self, ident: &'a syn::Ident) {
|
||||
if self
|
||||
.fn_generics
|
||||
.iter()
|
||||
.any(|param| if let GenericParam::Type(type_param) = param { type_param.ident == *ident } else { false })
|
||||
{
|
||||
self.found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut checker = GenericParamChecker { fn_generics, found: false };
|
||||
syn::visit::visit_type(&mut checker, ty);
|
||||
checker.found
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::parsing::parse_node_fn;
|
||||
use proc_macro2::TokenStream;
|
||||
|
||||
fn violations(attr: TokenStream, input: TokenStream) -> Vec<String> {
|
||||
let parsed = parse_node_fn(attr, input).expect("The test node fn should parse");
|
||||
ranked_input_violations(&parsed).into_iter().map(|(_, message)| message).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bare_concrete_parameter_is_rejected() {
|
||||
let messages = violations(
|
||||
quote::quote!(category("Test")),
|
||||
quote::quote!(
|
||||
fn scale(_: impl Ctx, content: Item<Vector>, factor: f64) -> Item<Vector> {
|
||||
content
|
||||
}
|
||||
),
|
||||
);
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(messages[0].contains("of type `f64` must be ranked"), "{messages:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ranked_parameters_and_the_unit_primary_sentinel_pass() {
|
||||
let messages = violations(
|
||||
quote::quote!(category("Test")),
|
||||
quote::quote!(
|
||||
fn circle(_: impl Ctx, _primary: (), radius: Item<f64>, points: List<DVec2>, erased: ListDyn) -> Item<Vector> {
|
||||
Item::default()
|
||||
}
|
||||
),
|
||||
);
|
||||
assert_eq!(messages, Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_unit_typed_non_primary_parameter_is_rejected() {
|
||||
let messages = violations(
|
||||
quote::quote!(category("Test")),
|
||||
quote::quote!(
|
||||
fn weird(_: impl Ctx, content: Item<Vector>, marker: ()) -> Item<Vector> {
|
||||
content
|
||||
}
|
||||
),
|
||||
);
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(messages[0].contains("Parameter `marker` cannot be typed `()`"), "{messages:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bare_generic_parameter_with_ranked_rows_passes() {
|
||||
let messages = violations(
|
||||
quote::quote!(category("Test")),
|
||||
quote::quote!(
|
||||
fn to_thing<T>(_: impl Ctx, #[implementations(List<Graphic>, List<Vector>, ListDyn)] content: T) -> Item<Graphic> {
|
||||
Item::default()
|
||||
}
|
||||
),
|
||||
);
|
||||
assert_eq!(messages, Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bare_generic_parameter_with_a_bare_row_is_rejected() {
|
||||
let messages = violations(
|
||||
quote::quote!(category("Test")),
|
||||
quote::quote!(
|
||||
fn to_thing<T>(_: impl Ctx, #[implementations(List<Graphic>, f64)] content: T) -> Item<Graphic> {
|
||||
Item::default()
|
||||
}
|
||||
),
|
||||
);
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(messages[0].contains("row `f64`"), "{messages:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_item_declared_parameter_with_bare_element_rows_passes() {
|
||||
let messages = violations(
|
||||
quote::quote!(category("Test")),
|
||||
quote::quote!(
|
||||
fn blend<T>(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item<T>, mode: Item<f64>) -> Item<T> {
|
||||
content
|
||||
}
|
||||
),
|
||||
);
|
||||
assert_eq!(messages, Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lazy_input_with_a_bare_output_is_rejected() {
|
||||
let messages = violations(
|
||||
quote::quote!(category("Test")),
|
||||
quote::quote!(
|
||||
fn lazy_thing(_: impl Ctx, content: Item<Vector>, source: impl Node<Context, Output = f64>) -> Item<Vector> {
|
||||
content
|
||||
}
|
||||
),
|
||||
);
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(messages[0].contains("Lazy input `source` with output type `f64` must be ranked"), "{messages:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lazy_generic_output_with_ranked_rows_passes() {
|
||||
let messages = violations(
|
||||
quote::quote!(category("Test")),
|
||||
quote::quote!(
|
||||
fn cache<T>(_: impl Ctx, #[implementations(Context -> Item<f64>, Context -> ListDyn)] value: impl Node<Context, Output = T>) -> T {
|
||||
T::default()
|
||||
}
|
||||
),
|
||||
);
|
||||
assert_eq!(messages, Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lazy_unit_row_is_rejected() {
|
||||
let messages = violations(
|
||||
quote::quote!(category("Test")),
|
||||
quote::quote!(
|
||||
fn cache<T>(_: impl Ctx, #[implementations(Context -> (), Context -> Item<f64>)] value: impl Node<Context, Output = T>) -> T {
|
||||
T::default()
|
||||
}
|
||||
),
|
||||
);
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(messages[0].contains("row output `()` of the lazy input `value` must be ranked"), "{messages:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lazy_generic_output_with_a_bare_row_is_rejected() {
|
||||
let messages = violations(
|
||||
quote::quote!(category("Test")),
|
||||
quote::quote!(
|
||||
fn cache<T>(_: impl Ctx, #[implementations(Context -> Item<f64>, Context -> f64)] value: impl Node<Context, Output = T>) -> T {
|
||||
T::default()
|
||||
}
|
||||
),
|
||||
);
|
||||
assert_eq!(messages.len(), 1, "{messages:?}");
|
||||
assert!(messages[0].contains("row output `f64` of the lazy input `value` must be ranked"), "{messages:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_impl_nodes_and_data_fields_are_exempt() {
|
||||
let skip_impl_messages = violations(
|
||||
quote::quote!(category(""), skip_impl),
|
||||
quote::quote!(
|
||||
fn passthrough<T>(_: impl Ctx, content: T) -> T {
|
||||
content
|
||||
}
|
||||
),
|
||||
);
|
||||
assert_eq!(skip_impl_messages, Vec::<String>::new());
|
||||
|
||||
let data_field_messages = violations(
|
||||
quote::quote!(category("Test")),
|
||||
quote::quote!(
|
||||
fn stateful(_: impl Ctx, content: Item<Vector>, #[data] cache: f64) -> Item<Vector> {
|
||||
content
|
||||
}
|
||||
),
|
||||
);
|
||||
assert_eq!(data_field_messages, Vec::<String>::new());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user