mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Update node macro to new syntax
This commit is contained in:
@@ -36,6 +36,9 @@ pub trait ExtractPosition {
|
||||
}
|
||||
pub trait ExtractIndex {
|
||||
fn try_index(&self) -> Option<impl Iterator<Item = usize>>;
|
||||
fn innermost_index(&self) -> u64 {
|
||||
self.try_index().and_then(|mut indices| indices.next()).unwrap_or(0) as u64
|
||||
}
|
||||
}
|
||||
pub trait ExtractVarArgs {
|
||||
// TODO: Consider returning a slice or something like that
|
||||
@@ -466,7 +469,8 @@ impl CloneVarArgs for Arc<OwnedContextImpl> {
|
||||
// TYPES `Context` AND `OwnedContextImpl`
|
||||
// ======================================
|
||||
|
||||
pub type Context<'a> = Option<Arc<OwnedContextImpl>>;
|
||||
pub type OwnedContext = Option<Arc<OwnedContextImpl>>;
|
||||
pub type Context<'a> = ContextImpl<'a>;
|
||||
type DynRef<'a> = &'a (dyn Any + Send + Sync);
|
||||
type DynBox = Box<dyn AnyHash + Send + Sync>;
|
||||
|
||||
@@ -655,9 +659,39 @@ pub struct PositionLink<'a> {
|
||||
|
||||
pub type DynSlot<'a> = &'a (dyn AnyHash + Send + Sync);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum VarArgSlots<'a> {
|
||||
Single(DynSlot<'a>),
|
||||
Slice(&'a [DynSlot<'a>]),
|
||||
}
|
||||
|
||||
impl<'a> VarArgSlots<'a> {
|
||||
pub fn get(&self, index: usize) -> Option<DynSlot<'a>> {
|
||||
match self {
|
||||
VarArgSlots::Single(slot) => (index == 0).then_some(*slot),
|
||||
VarArgSlots::Slice(slots) => slots.get(index).copied(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
VarArgSlots::Single(_) => 1,
|
||||
VarArgSlots::Slice(slots) => slots.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = DynSlot<'a>> + '_ {
|
||||
(0..self.len()).filter_map(move |index| self.get(index))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct VarArgLink<'a> {
|
||||
pub args: &'a [DynSlot<'a>],
|
||||
pub args: VarArgSlots<'a>,
|
||||
pub outer: Option<&'a VarArgLink<'a>>,
|
||||
}
|
||||
|
||||
@@ -704,6 +738,17 @@ impl<'a> EvalScope<'a> {
|
||||
scope
|
||||
}
|
||||
|
||||
pub fn nullified(&self, keep: ContextFeatures) -> EvalScope<'a> {
|
||||
let mut scope = EvalScope {
|
||||
real_time: self.real_time.filter(|_| keep.contains(ContextFeatures::REAL_TIME)),
|
||||
animation_time: self.animation_time.filter(|_| keep.contains(ContextFeatures::ANIMATION_TIME)),
|
||||
pointer_position: self.pointer_position.filter(|_| keep.contains(ContextFeatures::POINTER_POSITION)),
|
||||
..*self
|
||||
};
|
||||
scope.hash = scope.compute_hash(None);
|
||||
scope
|
||||
}
|
||||
|
||||
fn compute_hash(&self, retain: Option<&[SourceId]>) -> u64 {
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
self.real_time.map(f64::to_bits).hash(&mut hasher);
|
||||
@@ -735,6 +780,106 @@ pub trait ExtractArena {
|
||||
fn arena(&self) -> Self::ArenaRef;
|
||||
}
|
||||
|
||||
pub trait CtxFamily {
|
||||
type Ctx<'s>: Ctx + DeriveCtx<Family = Self>;
|
||||
}
|
||||
|
||||
pub type Derived<'s, C> = <<C as DeriveCtx>::Family as CtxFamily>::Ctx<'s>;
|
||||
|
||||
pub trait DeriveCtx {
|
||||
type Family: CtxFamily;
|
||||
fn derived(&self) -> Derived<'_, Self>;
|
||||
fn index_head(&self) -> IndexLink<'_>;
|
||||
fn scope(&self) -> &EvalScope<'_>;
|
||||
fn position_head(&self) -> Option<&PositionLink<'_>>;
|
||||
fn varargs_head(&self) -> Option<&VarArgLink<'_>>;
|
||||
fn promoted<'s>(&'s self, spilled_head: &'s IndexLink<'s>, inner_index: u64) -> Derived<'s, Self>;
|
||||
fn with_footprint<'s>(&'s self, footprint: &'s Footprint) -> Derived<'s, Self>;
|
||||
fn with_varargs<'s>(&'s self, varargs: &'s VarArgLink<'s>) -> Derived<'s, Self>;
|
||||
fn with_position<'s>(&'s self, position: &'s PositionLink<'s>) -> Derived<'s, Self>;
|
||||
fn with_scope<'s>(&'s self, scope: &'s EvalScope<'s>) -> Derived<'s, Self>;
|
||||
fn nullified<'s>(&'s self, keep: ContextFeatures, scope: &'s EvalScope<'s>) -> Derived<'s, Self>;
|
||||
|
||||
fn modify_footprint(&self, modify: impl FnOnce(&mut Footprint)) -> ModifiedFootprint<'_, Self>
|
||||
where
|
||||
Self: ExtractFootprint + Sized,
|
||||
{
|
||||
let mut footprint = self.try_footprint().copied();
|
||||
if let Some(footprint) = &mut footprint {
|
||||
modify(footprint);
|
||||
}
|
||||
ModifiedFootprint { ctx: self, footprint }
|
||||
}
|
||||
|
||||
fn push_vararg<'s>(&'s self, arg: DynSlot<'s>) -> VarArgScope<'s, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
VarArgScope {
|
||||
ctx: self,
|
||||
link: VarArgLink {
|
||||
args: VarArgSlots::Single(arg),
|
||||
outer: self.varargs_head(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn push_position(&self, position: DVec2) -> PositionScope<'_, Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
PositionScope {
|
||||
ctx: self,
|
||||
link: PositionLink {
|
||||
position,
|
||||
outer: self.position_head(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PositionScope<'c, C> {
|
||||
ctx: &'c C,
|
||||
link: PositionLink<'c>,
|
||||
}
|
||||
|
||||
impl<C: DeriveCtx> PositionScope<'_, C> {
|
||||
pub fn ctx(&self) -> Derived<'_, C> {
|
||||
self.ctx.with_position(&self.link)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VarArgScope<'c, C> {
|
||||
ctx: &'c C,
|
||||
link: VarArgLink<'c>,
|
||||
}
|
||||
|
||||
impl<C: DeriveCtx> VarArgScope<'_, C> {
|
||||
pub fn ctx(&self) -> Derived<'_, C> {
|
||||
self.ctx.with_varargs(&self.link)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModifiedFootprint<'c, C> {
|
||||
ctx: &'c C,
|
||||
footprint: Option<Footprint>,
|
||||
}
|
||||
|
||||
impl<C: DeriveCtx> ModifiedFootprint<'_, C> {
|
||||
pub fn ctx(&self) -> Derived<'_, C> {
|
||||
match &self.footprint {
|
||||
Some(footprint) => self.ctx.with_footprint(footprint),
|
||||
None => self.ctx.derived(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContextImplFamily;
|
||||
|
||||
impl CtxFamily for ContextImplFamily {
|
||||
type Ctx<'s> = ContextImpl<'s>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ContextImpl<'a> {
|
||||
index: IndexLink<'a>,
|
||||
@@ -791,6 +936,22 @@ impl<'a> ContextImpl<'a> {
|
||||
ContextImpl { position: Some(position), ..*self }
|
||||
}
|
||||
|
||||
pub fn nullified<'s>(&self, keep: ContextFeatures, scope: &'s EvalScope<'s>) -> ContextImpl<'s>
|
||||
where
|
||||
'a: 's,
|
||||
{
|
||||
ContextImpl {
|
||||
index: match keep.contains(ContextFeatures::INDEX) {
|
||||
true => self.index,
|
||||
false => IndexLink { index: 0, outer: None },
|
||||
},
|
||||
position: self.position.filter(|_| keep.contains(ContextFeatures::POSITION)),
|
||||
varargs: self.varargs.filter(|_| keep.contains(ContextFeatures::VARARGS)),
|
||||
footprint: self.footprint.filter(|_| keep.contains(ContextFeatures::FOOTPRINT)),
|
||||
scope,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn promoted<'s>(&self, spilled_head: &'s IndexLink<'s>, inner_index: u64) -> ContextImpl<'s>
|
||||
where
|
||||
'a: 's,
|
||||
@@ -849,7 +1010,7 @@ impl ExtractVarArgs for ContextImpl<'_> {
|
||||
let mut remaining = index;
|
||||
loop {
|
||||
match link.args.get(remaining) {
|
||||
Some(arg) => return Ok(*arg as DynRef<'_>),
|
||||
Some(arg) => return Ok(arg as DynRef<'_>),
|
||||
None => {
|
||||
remaining -= link.args.len();
|
||||
link = link.outer.ok_or(VarArgsResult::IndexOutOfBounds)?;
|
||||
@@ -867,7 +1028,7 @@ impl ExtractVarArgs for ContextImpl<'_> {
|
||||
let mut count = 0u64;
|
||||
let mut link = self.varargs;
|
||||
while let Some(current) = link {
|
||||
for arg in current.args {
|
||||
for arg in current.args.iter() {
|
||||
arg.dyn_hash(&mut *hasher);
|
||||
count += 1;
|
||||
}
|
||||
@@ -883,6 +1044,54 @@ impl<'a> ExtractArena for ContextImpl<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DeriveCtx for ContextImpl<'a> {
|
||||
type Family = ContextImplFamily;
|
||||
|
||||
fn derived(&self) -> ContextImpl<'_> {
|
||||
*self
|
||||
}
|
||||
|
||||
fn index_head(&self) -> IndexLink<'_> {
|
||||
self.index
|
||||
}
|
||||
|
||||
fn scope(&self) -> &EvalScope<'_> {
|
||||
self.scope
|
||||
}
|
||||
|
||||
fn position_head(&self) -> Option<&PositionLink<'_>> {
|
||||
self.position
|
||||
}
|
||||
|
||||
fn varargs_head(&self) -> Option<&VarArgLink<'_>> {
|
||||
self.varargs
|
||||
}
|
||||
|
||||
fn promoted<'s>(&'s self, spilled_head: &'s IndexLink<'s>, inner_index: u64) -> ContextImpl<'s> {
|
||||
ContextImpl::promoted(self, spilled_head, inner_index)
|
||||
}
|
||||
|
||||
fn with_footprint<'s>(&'s self, footprint: &'s Footprint) -> ContextImpl<'s> {
|
||||
ContextImpl::with_footprint(self, footprint)
|
||||
}
|
||||
|
||||
fn with_varargs<'s>(&'s self, varargs: &'s VarArgLink<'s>) -> ContextImpl<'s> {
|
||||
ContextImpl::with_varargs(self, varargs)
|
||||
}
|
||||
|
||||
fn with_position<'s>(&'s self, position: &'s PositionLink<'s>) -> ContextImpl<'s> {
|
||||
ContextImpl::with_position(self, position)
|
||||
}
|
||||
|
||||
fn with_scope<'s>(&'s self, scope: &'s EvalScope<'s>) -> ContextImpl<'s> {
|
||||
ContextImpl::with_scope(self, scope)
|
||||
}
|
||||
|
||||
fn nullified<'s>(&'s self, keep: ContextFeatures, scope: &'s EvalScope<'s>) -> ContextImpl<'s> {
|
||||
ContextImpl::nullified(self, keep, scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl graphene_hash::CacheHash for ContextImpl<'_> {
|
||||
fn cache_hash<H: Hasher>(&self, state: &mut H) {
|
||||
match self.footprint {
|
||||
@@ -1053,13 +1262,15 @@ mod context_impl_tests {
|
||||
|
||||
let outer_value = 7u32;
|
||||
let outer_args: [DynSlot; 1] = [&outer_value];
|
||||
let outer_link = VarArgLink { args: &outer_args, outer: None };
|
||||
let outer_link = VarArgLink {
|
||||
args: VarArgSlots::Slice(&outer_args),
|
||||
outer: None,
|
||||
};
|
||||
let outer_ctx = root.with_varargs(&outer_link);
|
||||
|
||||
let inner_value = String::from("inner");
|
||||
let inner_args: [DynSlot; 1] = [&inner_value];
|
||||
let inner_link = VarArgLink {
|
||||
args: &inner_args,
|
||||
args: VarArgSlots::Single(&inner_value),
|
||||
outer: Some(&outer_link),
|
||||
};
|
||||
let inner_ctx = root.with_varargs(&inner_link);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::context::InjectIndex;
|
||||
use crate::gpoll::{Extent, Finality, GPoll, GraphError};
|
||||
use crate::gpoll::{Extent, Finality, GPoll, GraphError, Interrupt};
|
||||
use std::cell::Cell;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::ops::Range;
|
||||
|
||||
@@ -118,6 +119,123 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StatusCell {
|
||||
finality: Cell<Finality>,
|
||||
error: Cell<Option<GraphError>>,
|
||||
no_partial: bool,
|
||||
}
|
||||
|
||||
impl Default for StatusCell {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusCell {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
finality: Cell::new(Finality::AllFinal),
|
||||
error: Cell::new(None),
|
||||
no_partial: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_partial() -> Self {
|
||||
Self {
|
||||
no_partial: true,
|
||||
..Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval_input<Input, N: GNode<Input>>(&self, input_index: usize, node: &N, input: &Input) -> Result<N::Output, Interrupt> {
|
||||
match node.eval(input) {
|
||||
GPoll::Final(value) => Ok(value),
|
||||
GPoll::Partial(_) if self.no_partial => Err(Interrupt::Pending),
|
||||
GPoll::Partial(value) => {
|
||||
self.finality.set(Finality::Partial);
|
||||
Ok(value)
|
||||
}
|
||||
GPoll::Fallback(boxed) => {
|
||||
let (value, error) = *boxed;
|
||||
let first = self.error.take();
|
||||
self.error.set(first.or(Some(error.traced(input_index))));
|
||||
Ok(value)
|
||||
}
|
||||
GPoll::Pending => Err(Interrupt::Pending),
|
||||
GPoll::Error(mut error) => {
|
||||
error.trace.push(input_index);
|
||||
Err(Interrupt::Error(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish<T>(self, value: T) -> GPoll<T> {
|
||||
match (self.error.take(), self.finality.get()) {
|
||||
(Some(error), _) => GPoll::Fallback(Box::new((value, error))),
|
||||
(None, Finality::AllFinal) => GPoll::Final(value),
|
||||
(None, Finality::Partial) => GPoll::Partial(value),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge<T>(self, poll: GPoll<T>) -> GPoll<T> {
|
||||
match poll {
|
||||
GPoll::Final(value) => self.finish(value),
|
||||
GPoll::Partial(value) => match self.finish(value) {
|
||||
GPoll::Final(value) => GPoll::Partial(value),
|
||||
other => other,
|
||||
},
|
||||
GPoll::Fallback(boxed) => {
|
||||
let (value, error) = *boxed;
|
||||
let first = self.error.take().unwrap_or(error);
|
||||
GPoll::Fallback(Box::new((value, first)))
|
||||
}
|
||||
interrupted => interrupted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct LazyInput<'a, N> {
|
||||
node: &'a N,
|
||||
cell: &'a StatusCell,
|
||||
input_index: usize,
|
||||
}
|
||||
|
||||
impl<'a, N> LazyInput<'a, N> {
|
||||
pub fn new(node: &'a N, cell: &'a StatusCell, input_index: usize) -> Self {
|
||||
Self { node, cell, input_index }
|
||||
}
|
||||
|
||||
pub fn eval<Input>(&self, ctx: &Input) -> Result<N::Output, Interrupt>
|
||||
where
|
||||
N: GNode<Input>,
|
||||
{
|
||||
self.cell.eval_input(self.input_index, self.node, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Input, N> GNode<Input> for LazyInput<'a, N>
|
||||
where
|
||||
N: GNode<Input>,
|
||||
{
|
||||
type Output = N::Output;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<Self::Output> {
|
||||
self.node.eval(input)
|
||||
}
|
||||
|
||||
fn extent(&self, input: &Input) -> GPoll<Extent> {
|
||||
self.node.extent(input)
|
||||
}
|
||||
|
||||
fn eval_batch<'b>(&self, input: &'b Input, range: Range<u64>, scratch: Option<&'b mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'b, Self::Output>
|
||||
where
|
||||
Input: InjectIndex + Copy,
|
||||
{
|
||||
self.node.eval_batch(input, range, scratch)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -23,6 +23,11 @@ impl GraphError {
|
||||
trace: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn traced(mut self, input_index: usize) -> Self {
|
||||
self.trace.push(input_index);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
||||
@@ -18,6 +18,7 @@ pub mod render_complexity;
|
||||
pub mod transform;
|
||||
pub mod uuid;
|
||||
pub mod value;
|
||||
pub mod wire;
|
||||
|
||||
pub use crate as core_types;
|
||||
pub use blending::*;
|
||||
|
||||
@@ -44,13 +44,13 @@ impl<'i, N: for<'a> Node<'a, I> + Copy, I: 'i> Copy for TypeNode<N, I, <N as Nod
|
||||
pub trait Convert<T, C>: Sized {
|
||||
/// Converts this type into the (usually inferred) output type.
|
||||
#[must_use]
|
||||
fn convert(self, footprint: Footprint, converter: C) -> impl Future<Output = T> + Send;
|
||||
fn convert(self, footprint: Footprint, converter: C) -> T;
|
||||
}
|
||||
|
||||
impl<T: ToString + Send> Convert<String, ()> for T {
|
||||
/// Converts this type into a `String` using its `ToString` implementation.
|
||||
#[inline]
|
||||
async fn convert(self, _: Footprint, _converter: ()) -> String {
|
||||
fn convert(self, _: Footprint, _converter: ()) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ pub trait ListConvert<U> {
|
||||
}
|
||||
|
||||
impl<U, T: ListConvert<U> + Send> Convert<List<U>, ()> for List<T> {
|
||||
async fn convert(self, _: Footprint, _: ()) -> List<U> {
|
||||
fn convert(self, _: Footprint, _: ()) -> List<U> {
|
||||
let list: List<U> = self
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
@@ -76,7 +76,7 @@ impl<U, T: ListConvert<U> + Send> Convert<List<U>, ()> for List<T> {
|
||||
/// from any `List<U>` express their signature as `AttributeDyn` and avoid monomorphizing
|
||||
/// over `U`; the compiler inserts this convert to bridge concrete-typed graph wires to the dyn input.
|
||||
impl<T: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static> Convert<AttributeDyn, ()> for List<T> {
|
||||
async fn convert(self, _: Footprint, _: ()) -> AttributeDyn {
|
||||
fn convert(self, _: Footprint, _: ()) -> AttributeDyn {
|
||||
let values: Vec<T> = self.into_iter().map(|row| row.into_element()).collect();
|
||||
AttributeDyn(Box::new(Attribute(values)))
|
||||
}
|
||||
@@ -86,7 +86,7 @@ impl<T: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash
|
||||
/// (such as `write_attribute`'s value-producing input) be generic over the destination list type
|
||||
/// alone, with the compiler-inserted convert handling each concrete value type at the wire level.
|
||||
impl<T: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash + 'static> Convert<AttributeValueDyn, ()> for T {
|
||||
async fn convert(self, _: Footprint, _: ()) -> AttributeValueDyn {
|
||||
fn convert(self, _: Footprint, _: ()) -> AttributeValueDyn {
|
||||
AttributeValueDyn(Box::new(self))
|
||||
}
|
||||
}
|
||||
@@ -95,13 +95,13 @@ impl<T: Clone + Send + Sync + Default + std::fmt::Debug + PartialEq + CacheHash
|
||||
/// only need attribute access (such as the `read_attribute_*` family) take a single `ListDyn` input
|
||||
/// instead of monomorphizing over every possible carrier list type.
|
||||
impl<T: Send> Convert<ListDyn, ()> for List<T> {
|
||||
async fn convert(self, _: Footprint, _: ()) -> ListDyn {
|
||||
fn convert(self, _: Footprint, _: ()) -> ListDyn {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Convert<DVec2, ()> for DVec2 {
|
||||
async fn convert(self, _: Footprint, _: ()) -> DVec2 {
|
||||
fn convert(self, _: Footprint, _: ()) -> DVec2 {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ pub trait FromAnchorPosition {
|
||||
|
||||
// Converts a position into a vector path composed of a single anchor point
|
||||
impl<T: FromAnchorPosition + Send> Convert<List<T>, ()> for DVec2 {
|
||||
async fn convert(self, _: Footprint, _: ()) -> List<T> {
|
||||
fn convert(self, _: Footprint, _: ()) -> List<T> {
|
||||
List::new_from_item(Item::new_from_element(T::from_anchor_position(self)))
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ impl<T: FromAnchorPosition + Send> Convert<List<T>, ()> for DVec2 {
|
||||
macro_rules! impl_convert {
|
||||
($from:ty, $to:ty) => {
|
||||
impl Convert<$to, ()> for $from {
|
||||
async fn convert(self, _: Footprint, _: ()) -> $to {
|
||||
fn convert(self, _: Footprint, _: ()) -> $to {
|
||||
self as $to
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ macro_rules! impl_convert {
|
||||
impl_convert!(usize, $to);
|
||||
|
||||
impl Convert<DVec2, ()> for $to {
|
||||
async fn convert(self, _: Footprint, _: ()) -> DVec2 {
|
||||
fn convert(self, _: Footprint, _: ()) -> DVec2 {
|
||||
DVec2::splat(self as f64)
|
||||
}
|
||||
}
|
||||
|
||||
560
node-graph/libraries/core-types/src/wire.rs
Normal file
560
node-graph/libraries/core-types/src/wire.rs
Normal file
@@ -0,0 +1,560 @@
|
||||
use crate::Type;
|
||||
use crate::arena::{Arena, ArenaCell};
|
||||
use crate::concrete;
|
||||
use crate::context::{ContextImpl, Ctx, ExtractArena};
|
||||
use crate::frame_table::{FrameTable, Lookup};
|
||||
use crate::gnode::GNode;
|
||||
use crate::gpoll::{Extent, Finality, GPoll};
|
||||
use graphene_hash::CacheHash;
|
||||
use std::any::Any;
|
||||
use std::hash::Hasher;
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub type ErasedGNode<T> = dyn for<'c> GNode<ContextImpl<'c>, Output = T>;
|
||||
pub type ErasedLendGNode<T> = dyn for<'c> GNode<ContextImpl<'c>, Output = &'c T>;
|
||||
|
||||
pub fn cache_key<C: CacheHash + ?Sized>(ctx: &C) -> u64 {
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
ctx.cache_hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum WireError {
|
||||
Arity { expected: usize, got: usize },
|
||||
Type { expected: Type, found: Type },
|
||||
MissingCapability { ty: Type },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct WireCapabilities {
|
||||
pub memoize: Option<fn(EdgeHandle) -> Result<EdgeHandle, WireError>>,
|
||||
pub lend: Option<fn(EdgeHandle) -> Result<EdgeHandle, WireError>>,
|
||||
}
|
||||
|
||||
fn memoize_edge<T: Clone + 'static>(edge: EdgeHandle) -> Result<EdgeHandle, WireError> {
|
||||
let content = edge.downcast::<T>()?;
|
||||
Ok(EdgeHandle::new(Box::new(MemoizeNode::new(content)) as Box<ErasedGNode<T>>))
|
||||
}
|
||||
|
||||
fn lend_edge<T: Clone + 'static>(edge: EdgeHandle) -> Result<EdgeHandle, WireError> {
|
||||
let content = edge.downcast::<T>()?;
|
||||
Ok(EdgeHandle::new_ref(Box::new(FrameMemoNode::new(content)) as Box<ErasedLendGNode<T>>))
|
||||
}
|
||||
|
||||
pub struct EdgeHandle {
|
||||
node: Box<dyn Any>,
|
||||
ty: Type,
|
||||
capabilities: WireCapabilities,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EdgeHandle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("EdgeHandle").field("ty", &self.ty).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl EdgeHandle {
|
||||
pub fn new<T: Clone + 'static>(node: Box<ErasedGNode<T>>) -> Self {
|
||||
Self::new_erased(
|
||||
node,
|
||||
concrete!(T),
|
||||
WireCapabilities {
|
||||
memoize: Some(memoize_edge::<T>),
|
||||
lend: Some(lend_edge::<T>),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_ref<T: 'static>(node: Box<ErasedLendGNode<T>>) -> Self {
|
||||
Self::new_erased(node, Type::Ref(Box::new(concrete!(T))), WireCapabilities::default())
|
||||
}
|
||||
|
||||
pub fn new_erased<N: ?Sized>(node: Box<N>, ty: Type, capabilities: WireCapabilities) -> Self
|
||||
where
|
||||
Box<N>: Any,
|
||||
{
|
||||
Self {
|
||||
node: Box::new(node),
|
||||
ty,
|
||||
capabilities,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wire_type(&self) -> &Type {
|
||||
&self.ty
|
||||
}
|
||||
|
||||
pub fn memoized(self) -> Result<EdgeHandle, WireError> {
|
||||
match self.capabilities.memoize {
|
||||
Some(wrap) => wrap(self),
|
||||
None => Err(WireError::MissingCapability { ty: self.ty }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lent(self) -> Result<EdgeHandle, WireError> {
|
||||
match self.capabilities.lend {
|
||||
Some(wrap) => wrap(self),
|
||||
None => Err(WireError::MissingCapability { ty: self.ty }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn downcast<T: 'static>(self) -> Result<Box<ErasedGNode<T>>, WireError> {
|
||||
self.downcast_erased(concrete!(T))
|
||||
}
|
||||
|
||||
pub fn downcast_lend<T: 'static>(self) -> Result<Box<ErasedLendGNode<T>>, WireError> {
|
||||
self.downcast_erased(Type::Ref(Box::new(concrete!(T))))
|
||||
}
|
||||
|
||||
pub fn downcast_erased<N: ?Sized>(self, expected: Type) -> Result<Box<N>, WireError>
|
||||
where
|
||||
Box<N>: Any,
|
||||
{
|
||||
let found = self.ty;
|
||||
self.node.downcast::<Box<N>>().map(|node| *node).map_err(|_| WireError::Type { expected, found })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NodeIoRecord {
|
||||
pub inputs: Vec<Type>,
|
||||
pub output: Type,
|
||||
}
|
||||
|
||||
pub struct RegistryEntry {
|
||||
pub io: NodeIoRecord,
|
||||
pub wire: fn(Vec<EdgeHandle>) -> Result<EdgeHandle, WireError>,
|
||||
}
|
||||
|
||||
pub fn resolve_and_wire(entry: &RegistryEntry, inputs: Vec<EdgeHandle>) -> Result<EdgeHandle, WireError> {
|
||||
if inputs.len() != entry.io.inputs.len() {
|
||||
return Err(WireError::Arity {
|
||||
expected: entry.io.inputs.len(),
|
||||
got: inputs.len(),
|
||||
});
|
||||
}
|
||||
for (handle, expected) in inputs.iter().zip(&entry.io.inputs) {
|
||||
if handle.wire_type() != expected {
|
||||
return Err(WireError::Type {
|
||||
expected: expected.clone(),
|
||||
found: handle.wire_type().clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
(entry.wire)(inputs)
|
||||
}
|
||||
|
||||
pub struct MemoizeNode<T, NodeContent> {
|
||||
cache: Mutex<Option<(u64, T, Finality)>>,
|
||||
content: NodeContent,
|
||||
}
|
||||
|
||||
impl<T, NodeContent> MemoizeNode<T, NodeContent> {
|
||||
pub fn new(content: NodeContent) -> Self {
|
||||
Self {
|
||||
cache: Mutex::new(None),
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Input, NodeContent> GNode<Input> for MemoizeNode<T, NodeContent>
|
||||
where
|
||||
T: Clone,
|
||||
Input: Ctx + CacheHash,
|
||||
NodeContent: GNode<Input, Output = T>,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<T> {
|
||||
let key = cache_key(input);
|
||||
if let Some((hash, value, finality)) = self.cache.lock().unwrap().as_ref() {
|
||||
if *hash == key {
|
||||
return match finality {
|
||||
Finality::AllFinal => GPoll::Final(value.clone()),
|
||||
Finality::Partial => GPoll::Partial(value.clone()),
|
||||
};
|
||||
}
|
||||
}
|
||||
let result = self.content.eval(input);
|
||||
match &result {
|
||||
GPoll::Final(value) => *self.cache.lock().unwrap() = Some((key, value.clone(), Finality::AllFinal)),
|
||||
GPoll::Partial(value) => *self.cache.lock().unwrap() = Some((key, value.clone(), Finality::Partial)),
|
||||
GPoll::Pending | GPoll::Fallback(_) | GPoll::Error(_) => {}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn extent(&self, input: &Input) -> GPoll<Extent> {
|
||||
self.content.extent(input)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FrameMemoNode<T, NodeContent> {
|
||||
cell: ArenaCell<FrameTable<T, 32>>,
|
||||
content: NodeContent,
|
||||
}
|
||||
|
||||
impl<T, NodeContent> FrameMemoNode<T, NodeContent> {
|
||||
pub fn new(content: NodeContent) -> Self {
|
||||
Self {
|
||||
cell: ArenaCell::new(),
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'e, T, Input, NodeContent> GNode<Input> for FrameMemoNode<T, NodeContent>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
Input: Ctx + CacheHash + ExtractArena<ArenaRef = &'e Arena>,
|
||||
NodeContent: GNode<Input, Output = T>,
|
||||
{
|
||||
type Output = &'e T;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<&'e T> {
|
||||
let arena = input.arena();
|
||||
let table = match self.cell.load(arena) {
|
||||
Some(table) => table,
|
||||
None => match arena.alloc(FrameTable::new()) {
|
||||
Some((table, weak)) => {
|
||||
self.cell.store(weak);
|
||||
table
|
||||
}
|
||||
None => return park(arena, self.content.eval(input)),
|
||||
},
|
||||
};
|
||||
match table.lookup(cache_key(input)) {
|
||||
Lookup::Hit(Finality::AllFinal, value) => GPoll::Final(value),
|
||||
Lookup::Hit(Finality::Partial, value) => GPoll::Partial(value),
|
||||
Lookup::Vacant(slot) => match self.content.eval(input) {
|
||||
GPoll::Final(value) => GPoll::Final(slot.publish(value, Finality::AllFinal)),
|
||||
GPoll::Partial(value) => GPoll::Partial(slot.publish(value, Finality::Partial)),
|
||||
unpublishable => {
|
||||
slot.release();
|
||||
park(arena, unpublishable)
|
||||
}
|
||||
},
|
||||
Lookup::Full => park(arena, self.content.eval(input)),
|
||||
}
|
||||
}
|
||||
|
||||
fn extent(&self, input: &Input) -> GPoll<Extent> {
|
||||
self.content.extent(input)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn park<'e, T>(arena: &'e Arena, result: GPoll<T>) -> GPoll<&'e T> {
|
||||
match result {
|
||||
GPoll::Final(value) => match arena.alloc(value) {
|
||||
Some((parked, _)) => GPoll::Final(parked),
|
||||
None => GPoll::arena_exhausted(),
|
||||
},
|
||||
GPoll::Partial(value) => match arena.alloc(value) {
|
||||
Some((parked, _)) => GPoll::Partial(parked),
|
||||
None => GPoll::arena_exhausted(),
|
||||
},
|
||||
GPoll::Fallback(boxed) => {
|
||||
let (value, error) = *boxed;
|
||||
match arena.alloc(value) {
|
||||
Some((parked, _)) => GPoll::Fallback(Box::new((parked, error))),
|
||||
None => GPoll::arena_exhausted(),
|
||||
}
|
||||
}
|
||||
GPoll::Pending => GPoll::Pending,
|
||||
GPoll::Error(error) => GPoll::Error(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::context::EvalScope;
|
||||
use crate::SourceId;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
struct CountingNode(AtomicU32);
|
||||
|
||||
impl<Input> GNode<Input> for CountingNode {
|
||||
type Output = u32;
|
||||
|
||||
fn eval(&self, _input: &Input) -> GPoll<u32> {
|
||||
GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
struct ValueNode<T>(T);
|
||||
|
||||
impl<T: Clone, Input> GNode<Input> for ValueNode<T> {
|
||||
type Output = T;
|
||||
|
||||
fn eval(&self, _input: &Input) -> GPoll<T> {
|
||||
GPoll::Final(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memo_capability_wraps_edges_type_blind() {
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let edge = EdgeHandle::new(Box::new(CountingNode(AtomicU32::new(0))) as Box<ErasedGNode<u32>>);
|
||||
let memoized = edge.memoized().unwrap().downcast::<u32>().unwrap();
|
||||
|
||||
assert_eq!(memoized.eval(&ctx), GPoll::Final(1));
|
||||
assert_eq!(memoized.eval(&ctx), GPoll::Final(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memo_invalidates_on_generation_bump() {
|
||||
let arena = Arena::new(1024);
|
||||
let source: SourceId = 7;
|
||||
let before = [(source, 1)];
|
||||
let after = [(source, 2)];
|
||||
let scope_before = scope_fixture(&before, &arena);
|
||||
let scope_after = scope_fixture(&after, &arena);
|
||||
|
||||
let edge = EdgeHandle::new(Box::new(CountingNode(AtomicU32::new(0))) as Box<ErasedGNode<u32>>);
|
||||
let memoized = edge.memoized().unwrap().downcast::<u32>().unwrap();
|
||||
|
||||
assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1));
|
||||
assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1));
|
||||
assert_eq!(memoized.eval(&ContextImpl::root(&scope_after)), GPoll::Final(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memoized_edges_stack_and_rewire() {
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let edge = EdgeHandle::new(Box::new(CountingNode(AtomicU32::new(0))) as Box<ErasedGNode<u32>>);
|
||||
let stacked = edge.memoized().unwrap().memoized().unwrap().downcast::<u32>().unwrap();
|
||||
|
||||
assert_eq!(stacked.eval(&ctx), GPoll::Final(1));
|
||||
assert_eq!(stacked.eval(&ctx), GPoll::Final(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lend_capability_turns_an_owned_edge_into_a_lending_edge() {
|
||||
let arena = Arena::new(4096);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let edge = EdgeHandle::new(Box::new(ValueNode("lent out".to_string())) as Box<ErasedGNode<String>>);
|
||||
let lending = edge.lent().unwrap();
|
||||
assert_eq!(*lending.wire_type(), Type::Ref(Box::new(concrete!(String))));
|
||||
|
||||
let node = lending.downcast_lend::<String>().unwrap();
|
||||
let GPoll::Final(first) = node.eval(&ctx) else {
|
||||
panic!("lend must fill the frame table and lend");
|
||||
};
|
||||
let GPoll::Final(second) = node.eval(&ctx) else {
|
||||
panic!("second eval must lend the published value");
|
||||
};
|
||||
assert_eq!(first, "lent out");
|
||||
assert!(std::ptr::eq(first, second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ref_edges_report_missing_capabilities() {
|
||||
let edge = EdgeHandle::new(Box::new(ValueNode(5u32)) as Box<ErasedGNode<u32>>);
|
||||
let lending = edge.lent().unwrap();
|
||||
|
||||
match lending.memoized() {
|
||||
Err(WireError::MissingCapability { ty }) => assert_eq!(ty, Type::Ref(Box::new(concrete!(u32)))),
|
||||
other => panic!("expected missing capability, got {:?}", other.map(|handle| handle.ty)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn borrow_carrying_value_types_wire_through_the_general_constructor() {
|
||||
struct SplitBorrow<'c>(&'c str, usize);
|
||||
|
||||
struct SplitNode<Node0> {
|
||||
content: Node0,
|
||||
}
|
||||
|
||||
impl<'e, Input, Node0> GNode<Input> for SplitNode<Node0>
|
||||
where
|
||||
Input: Ctx,
|
||||
Node0: GNode<Input, Output = &'e String>,
|
||||
{
|
||||
type Output = SplitBorrow<'e>;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<SplitBorrow<'e>> {
|
||||
self.content.eval(input).map(|value| SplitBorrow(value, value.len()))
|
||||
}
|
||||
}
|
||||
|
||||
type ErasedSplitEdge = dyn for<'c> GNode<ContextImpl<'c>, Output = SplitBorrow<'c>>;
|
||||
|
||||
let arena = Arena::new(4096);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let lending = EdgeHandle::new(Box::new(ValueNode("held".to_string())) as Box<ErasedGNode<String>>).lent().unwrap();
|
||||
let upstream = lending.downcast_lend::<String>().unwrap();
|
||||
let node: Box<ErasedSplitEdge> = Box::new(SplitNode { content: upstream });
|
||||
let handle = EdgeHandle::new_erased(node, concrete!(SplitBorrow<'static>), WireCapabilities::default());
|
||||
assert_eq!(*handle.wire_type(), concrete!(SplitBorrow<'static>));
|
||||
|
||||
let wired = handle.downcast_erased::<ErasedSplitEdge>(concrete!(SplitBorrow<'static>)).unwrap();
|
||||
let GPoll::Final(split) = wired.eval(&ctx) else {
|
||||
panic!("borrow-carrying output must eval through the erased edge");
|
||||
};
|
||||
assert_eq!(split.0, "held");
|
||||
assert_eq!(split.1, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_ctx_repeat_pushes_index_levels_through_the_erased_edge() {
|
||||
use crate::context::{Derived, DeriveCtx, ExtractIndex};
|
||||
|
||||
struct RepeatNode<Node0> {
|
||||
content: Node0,
|
||||
}
|
||||
|
||||
impl<C, T, Node0> GNode<C> for RepeatNode<Node0>
|
||||
where
|
||||
C: Ctx + DeriveCtx,
|
||||
Node0: for<'x> GNode<Derived<'x, C>, Output = T>,
|
||||
{
|
||||
type Output = Vec<T>;
|
||||
|
||||
fn eval(&self, input: &C) -> GPoll<Vec<T>> {
|
||||
let spilled = input.index_head();
|
||||
let mut result = Vec::new();
|
||||
for index in 0..3 {
|
||||
let derived = input.promoted(&spilled, index);
|
||||
match self.content.eval(&derived) {
|
||||
GPoll::Final(value) => result.push(value),
|
||||
other => return other.map(|_| Vec::new()),
|
||||
}
|
||||
}
|
||||
GPoll::Final(result)
|
||||
}
|
||||
}
|
||||
|
||||
struct LevelsNode;
|
||||
|
||||
impl<Input: ExtractIndex> GNode<Input> for LevelsNode {
|
||||
type Output = Vec<usize>;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<Vec<usize>> {
|
||||
GPoll::Final(input.try_index().map(|levels| levels.collect()).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let nested = RepeatNode {
|
||||
content: RepeatNode { content: LevelsNode },
|
||||
};
|
||||
let erased: Box<ErasedGNode<Vec<Vec<Vec<usize>>>>> = Box::new(nested);
|
||||
|
||||
let GPoll::Final(outer) = erased.eval(&ctx) else {
|
||||
panic!("nested repeat must evaluate");
|
||||
};
|
||||
assert_eq!(outer.len(), 3);
|
||||
assert_eq!(outer[2][1], vec![1, 2, 0]);
|
||||
assert_eq!(outer[0][0], vec![0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_ctx_footprint_replace_reaches_the_content() {
|
||||
use crate::context::{Derived, DeriveCtx, ExtractFootprint};
|
||||
use crate::transform::Footprint;
|
||||
|
||||
struct ShiftFootprintNode<Node0> {
|
||||
content: Node0,
|
||||
}
|
||||
|
||||
impl<C, T, Node0> GNode<C> for ShiftFootprintNode<Node0>
|
||||
where
|
||||
C: Ctx + DeriveCtx + ExtractFootprint,
|
||||
Node0: for<'x> GNode<Derived<'x, C>, Output = T>,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
fn eval(&self, input: &C) -> GPoll<T> {
|
||||
let mut footprint = input.try_footprint().copied().unwrap_or(Footprint::DEFAULT);
|
||||
footprint.resolution.x += 7;
|
||||
let derived = input.with_footprint(&footprint);
|
||||
self.content.eval(&derived)
|
||||
}
|
||||
}
|
||||
|
||||
struct ResolutionNode;
|
||||
|
||||
impl<Input: ExtractFootprint> GNode<Input> for ResolutionNode {
|
||||
type Output = u32;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<u32> {
|
||||
GPoll::Final(input.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0))
|
||||
}
|
||||
}
|
||||
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let graph: Box<ErasedGNode<u32>> = Box::new(ShiftFootprintNode {
|
||||
content: ShiftFootprintNode { content: ResolutionNode },
|
||||
});
|
||||
assert_eq!(graph.eval(&ctx), GPoll::Final(Footprint::DEFAULT.resolution.x + 14));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_and_wire_checks_arity_and_types() {
|
||||
fn wire_strlen(args: Vec<EdgeHandle>) -> Result<EdgeHandle, WireError> {
|
||||
let mut args = args.into_iter();
|
||||
let value = args.next().ok_or(WireError::Arity { expected: 1, got: 0 })?.downcast::<String>()?;
|
||||
drop(value);
|
||||
Ok(EdgeHandle::new(Box::new(ValueNode(0u32)) as Box<ErasedGNode<u32>>))
|
||||
}
|
||||
let entry = RegistryEntry {
|
||||
io: NodeIoRecord {
|
||||
inputs: vec![concrete!(String)],
|
||||
output: concrete!(u32),
|
||||
},
|
||||
wire: wire_strlen,
|
||||
};
|
||||
|
||||
let owned = EdgeHandle::new(Box::new(ValueNode("typed".to_string())) as Box<ErasedGNode<String>>);
|
||||
assert!(resolve_and_wire(&entry, vec![owned]).is_ok());
|
||||
|
||||
assert_eq!(resolve_and_wire(&entry, vec![]).unwrap_err(), WireError::Arity { expected: 1, got: 0 });
|
||||
|
||||
let mistyped = EdgeHandle::new(Box::new(ValueNode(1.0f64)) as Box<ErasedGNode<f64>>);
|
||||
assert_eq!(
|
||||
resolve_and_wire(&entry, vec![mistyped]).unwrap_err(),
|
||||
WireError::Type {
|
||||
expected: concrete!(String),
|
||||
found: concrete!(f64),
|
||||
}
|
||||
);
|
||||
|
||||
let lent = EdgeHandle::new(Box::new(ValueNode("typed".to_string())) as Box<ErasedGNode<String>>).lent().unwrap();
|
||||
assert_eq!(
|
||||
resolve_and_wire(&entry, vec![lent]).unwrap_err(),
|
||||
WireError::Type {
|
||||
expected: concrete!(String),
|
||||
found: Type::Ref(Box::new(concrete!(String))),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,21 @@
|
||||
use crate::parsing::*;
|
||||
use convert_case::{Case, Casing};
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{ToTokens, format_ident, quote, quote_spanned};
|
||||
use quote::{ToTokens, format_ident, quote};
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::token::Comma;
|
||||
use syn::{Error, Ident, PatIdent, Token, WhereClause, WherePredicate, parse_quote};
|
||||
use syn::{Ident, PatIdent};
|
||||
static NODE_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result<TokenStream2> {
|
||||
let ParsedNodeFn {
|
||||
vis,
|
||||
attributes,
|
||||
fn_name,
|
||||
struct_name,
|
||||
mod_name,
|
||||
fn_generics,
|
||||
where_clause,
|
||||
input,
|
||||
output_type,
|
||||
is_async,
|
||||
fields,
|
||||
body,
|
||||
description,
|
||||
..
|
||||
} = parsed;
|
||||
@@ -80,13 +73,10 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
// Combined struct generic parameters with bounds for struct definition
|
||||
// struct MemoizeNode<T: Clone, Node0>
|
||||
let struct_generic_params: Vec<TokenStream2> = data_field_generics.iter().map(|gp| quote!(#gp)).chain(node_generics.iter().map(|id| quote!(#id))).collect();
|
||||
let input_ident = &input.pat_ident;
|
||||
|
||||
let context_features = &input.context_features;
|
||||
|
||||
// Regular field idents and names (for function parameters)
|
||||
let field_idents: Vec<_> = regular_fields.iter().map(|f| &f.pat_ident).collect();
|
||||
let field_names: Vec<_> = field_idents.iter().map(|pat_ident| &pat_ident.ident).collect();
|
||||
let regular_field_names: Vec<_> = regular_fields.iter().map(|f| &f.pat_ident.ident).collect();
|
||||
let data_field_names: Vec<_> = data_fields.iter().map(|f| &f.pat_ident.ident).collect();
|
||||
|
||||
@@ -121,33 +111,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
|
||||
let struct_fields = data_field_defs.chain(regular_field_defs);
|
||||
|
||||
let mut future_idents = Vec::new();
|
||||
|
||||
// Data fields get passed as references to the underlying function
|
||||
let data_field_idents: Vec<_> = data_fields.iter().map(|f| &f.pat_ident).collect();
|
||||
let data_field_types: Vec<_> = data_fields
|
||||
.iter()
|
||||
.map(|field| match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => {
|
||||
let ty = ty.clone();
|
||||
quote!(&#ty)
|
||||
}
|
||||
_ => unreachable!("Data fields must be Regular types, not Node types"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Regular fields have types passed to the function
|
||||
let field_types: Vec<_> = regular_fields
|
||||
.iter()
|
||||
.map(|field| match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(),
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, input_type, .. }) => match parsed.is_async {
|
||||
true => parse_quote!(&'n impl #core_types::Node<'n, #input_type, Output = impl core::future::Future<Output=#output_type>>),
|
||||
false => parse_quote!(&'n impl #core_types::Node<'n, #input_type, Output = #output_type>),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Only regular fields have UI metadata (data fields are internal state)
|
||||
let widget_override: Vec<_> = regular_fields
|
||||
.iter()
|
||||
@@ -233,39 +196,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
.collect();
|
||||
|
||||
// Only eval regular fields (data fields are accessed directly as self.field_name)
|
||||
let eval_args = regular_fields.iter().map(|field| {
|
||||
let name = &field.pat_ident.ident;
|
||||
match &field.ty {
|
||||
ParsedFieldType::Regular { .. } => {
|
||||
quote! { let #name = self.#name.eval(__input.clone()).await; }
|
||||
}
|
||||
ParsedFieldType::Node { .. } => {
|
||||
quote! { let #name = &self.#name; }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Only regular fields can have min/max constraints
|
||||
let min_max_args = regular_fields.iter().map(|field| match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { number_hard_min, number_hard_max, .. }) => {
|
||||
let name = &field.pat_ident.ident;
|
||||
let mut tokens = quote!();
|
||||
if let Some(min) = number_hard_min {
|
||||
tokens.extend(quote_spanned! {min.span()=>
|
||||
let #name = #core_types::misc::Clampable::clamp_hard_min(#name, #min);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(max) = number_hard_max {
|
||||
tokens.extend(quote_spanned! {max.span()=>
|
||||
let #name = #core_types::misc::Clampable::clamp_hard_max(#name, #max);
|
||||
});
|
||||
}
|
||||
tokens
|
||||
}
|
||||
ParsedFieldType::Node { .. } => quote!(),
|
||||
});
|
||||
|
||||
let all_implementation_types = fields.iter().flat_map(|field| match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { implementations, .. }) => implementations.iter().cloned().collect::<Vec<_>>(),
|
||||
ParsedFieldType::Node(NodeParsedField { implementations, .. }) => implementations
|
||||
@@ -275,61 +205,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
});
|
||||
let all_implementation_types = all_implementation_types.chain(input.implementations.iter().cloned());
|
||||
|
||||
let input_type = &parsed.input.ty;
|
||||
let mut clauses = Vec::new();
|
||||
let mut clampable_clauses = Vec::new();
|
||||
|
||||
for (field, name) in regular_fields.iter().zip(node_generics.iter()) {
|
||||
clauses.push(match (&field.ty, *is_async) {
|
||||
(
|
||||
ParsedFieldType::Regular(RegularParsedField {
|
||||
ty, number_hard_min, number_hard_max, ..
|
||||
}),
|
||||
_,
|
||||
) => {
|
||||
let all_lifetime_ty = substitute_lifetimes(ty.clone(), "all");
|
||||
let id = future_idents.len();
|
||||
let fut_ident = format_ident!("F{}", id);
|
||||
future_idents.push(fut_ident.clone());
|
||||
|
||||
// Add Clampable bound if this field uses hard_min or hard_max
|
||||
if number_hard_min.is_some() || number_hard_max.is_some() {
|
||||
// The bound applies to the Output type of the future, which is #ty
|
||||
clampable_clauses.push(quote!(#ty: #core_types::misc::Clampable));
|
||||
}
|
||||
|
||||
quote!(
|
||||
#fut_ident: core::future::Future<Output = #ty> + #core_types::WasmNotSend + 'n,
|
||||
for<'all> #all_lifetime_ty: #core_types::WasmNotSend,
|
||||
#name: #core_types::Node<'n, #input_type, Output = #fut_ident> + #core_types::WasmNotSync
|
||||
)
|
||||
}
|
||||
(ParsedFieldType::Node(NodeParsedField { input_type, output_type, .. }), true) => {
|
||||
let id = future_idents.len();
|
||||
let fut_ident = format_ident!("F{}", id);
|
||||
future_idents.push(fut_ident.clone());
|
||||
|
||||
quote!(
|
||||
#fut_ident: core::future::Future<Output = #output_type> + #core_types::WasmNotSend + 'n,
|
||||
#name: #core_types::Node<'n, #input_type, Output = #fut_ident > + #core_types::WasmNotSync
|
||||
)
|
||||
}
|
||||
(ParsedFieldType::Node { .. }, false) => unreachable!("Found node which takes an impl Node<> input but is not async"),
|
||||
});
|
||||
}
|
||||
let where_clause = where_clause.clone().unwrap_or(WhereClause {
|
||||
where_token: Token),
|
||||
predicates: Default::default(),
|
||||
});
|
||||
|
||||
let mut struct_where_clause = where_clause.clone();
|
||||
let extra_where: Punctuated<WherePredicate, Comma> = parse_quote!(
|
||||
#(#clauses,)*
|
||||
#(#clampable_clauses,)*
|
||||
#output_type: 'n,
|
||||
);
|
||||
struct_where_clause.predicates.extend(extra_where);
|
||||
|
||||
// Only regular fields are parameters to new()
|
||||
let new_args = node_generics.iter().zip(regular_field_names.iter()).map(|(r#gen, name)| {
|
||||
quote! { #name: #r#gen }
|
||||
@@ -344,9 +219,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
});
|
||||
let all_field_inits = data_inits.chain(regular_inits);
|
||||
|
||||
let async_keyword = is_async.then(|| quote!(async));
|
||||
let await_keyword = is_async.then(|| quote!(.await));
|
||||
|
||||
// Data fields may not implement Copy, PartialEq, etc., so only derive Debug and Clone
|
||||
let struct_derives = if data_fields.is_empty() {
|
||||
quote!(#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)])
|
||||
@@ -354,34 +226,6 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
quote!(#[derive(Debug, Clone)])
|
||||
};
|
||||
|
||||
// Generate serialize method if serialize attribute is specified
|
||||
let serialize_impl = if let Some(serialize_fn) = &parsed.attributes.serialize {
|
||||
let data_field_refs = data_field_names.iter().map(|name| quote!(&self.#name));
|
||||
quote! {
|
||||
fn serialize(&self) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
|
||||
#serialize_fn(#(#data_field_refs),*)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote!()
|
||||
};
|
||||
|
||||
let eval_impl = quote! {
|
||||
type Output = #core_types::registry::DynFuture<'n, #output_type>;
|
||||
#[inline]
|
||||
fn eval(&'n self, __input: #input_type) -> Self::Output {
|
||||
Box::pin(async move {
|
||||
use #core_types::misc::Clampable;
|
||||
|
||||
#(#eval_args)*
|
||||
#(#min_max_args)*
|
||||
self::#fn_name(__input #(, &self.#data_field_names)* #(, #regular_field_names)*) #await_keyword
|
||||
})
|
||||
}
|
||||
|
||||
#serialize_impl
|
||||
};
|
||||
|
||||
let identifier = format_ident!("{}_proto_ident", fn_name);
|
||||
let identifier_path = match parsed.attributes.path.as_ref() {
|
||||
Some(path) => {
|
||||
@@ -391,8 +235,18 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
None => quote!(std::module_path!()),
|
||||
};
|
||||
|
||||
let register_node_impl = generate_register_node_impl(parsed, &field_names, &struct_name, &identifier)?;
|
||||
let registry_name = format_ident!("__node_registry_{}_{}", NODE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst), struct_name);
|
||||
let register_node_impl = quote! {
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn #registry_name() {
|
||||
register_metadata();
|
||||
}
|
||||
};
|
||||
let import_name = format_ident!("_IMPORT_STUB_{}", mod_name.to_string().to_case(Case::UpperSnake));
|
||||
let gnode = crate::gcodegen::generate_gnode_code(crate_ident, parsed)?;
|
||||
let gnode_in_mod = gnode.in_mod;
|
||||
let gnode_top_level = gnode.top_level;
|
||||
|
||||
let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None));
|
||||
let memoize_flag = attributes.memoize;
|
||||
@@ -428,17 +282,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
|
||||
Ok(quote! {
|
||||
#(#description_doc_attrs)*
|
||||
#[inline]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#vis #async_keyword fn #fn_name <'n, #(#fn_generics,)*> (#input_ident: #input_type #(, #data_field_idents: #data_field_types)* #(, #field_idents: #field_types)*) -> #output_type #where_clause #body
|
||||
|
||||
#cfg
|
||||
#[automatically_derived]
|
||||
impl<'n, #(#fn_generics,)* #(#node_generics,)* #(#future_idents,)*> #core_types::Node<'n, #input_type> for #mod_name::#struct_name<#(#struct_type_params,)*>
|
||||
#struct_where_clause
|
||||
{
|
||||
#eval_impl
|
||||
}
|
||||
#gnode_top_level
|
||||
|
||||
#cfg
|
||||
const fn #identifier() -> #core_types::ProtoNodeIdentifier {
|
||||
@@ -458,10 +302,8 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
mod #mod_name {
|
||||
use super::*;
|
||||
use #core_types as gcore;
|
||||
use gcore::{Node, NodeIOTypes, concrete, fn_type, fn_type_fut, future, ProtoNodeIdentifier, WasmNotSync, NodeIO, ContextFeature};
|
||||
use gcore::value::ClonedNode;
|
||||
use gcore::ops::TypeNode;
|
||||
use gcore::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, DynFuture, TypeErasedBox, PanicNode, RegistryValueSource, RegistryWidgetOverride};
|
||||
use gcore::{ContextFeature, concrete};
|
||||
use gcore::registry::{NodeMetadata, FieldMetadata, NODE_METADATA, RegistryValueSource, RegistryWidgetOverride};
|
||||
use gcore::ctor::ctor;
|
||||
|
||||
// Use the types specified in the implementation
|
||||
@@ -484,6 +326,8 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
}
|
||||
}
|
||||
|
||||
#gnode_in_mod
|
||||
|
||||
#register_node_impl
|
||||
|
||||
#[cfg_attr(not(target_family = "wasm"), ctor)]
|
||||
@@ -619,161 +463,10 @@ fn generate_phantom_data<'a>(fn_generics: impl Iterator<Item = &'a crate::Generi
|
||||
(fn_generic_params, phantom_data_declerations)
|
||||
}
|
||||
|
||||
fn generate_register_node_impl(parsed: &ParsedNodeFn, field_names: &[&Ident], struct_name: &Ident, identifier: &Ident) -> Result<TokenStream2, Error> {
|
||||
// On native, `register_node` and `register_metadata` run automatically via `#[ctor]`.
|
||||
// On Wasm, `ctor` isn't available, so this `extern "C"` fn is invoked from JS to register the same way.
|
||||
// `skip_impl` nodes don't generate a `register_node`, so the shim calls only `register_metadata` for them.
|
||||
let registry_name = format_ident!("__node_registry_{}_{}", NODE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst), struct_name);
|
||||
let register_node_call = if parsed.attributes.skip_impl { quote!() } else { quote!(register_node();) };
|
||||
let wasm_shim = quote! {
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn #registry_name() {
|
||||
#register_node_call
|
||||
register_metadata();
|
||||
}
|
||||
};
|
||||
|
||||
if parsed.attributes.skip_impl {
|
||||
return Ok(wasm_shim);
|
||||
}
|
||||
|
||||
let mut constructors = Vec::new();
|
||||
let unit = parse_quote!(gcore::Context);
|
||||
|
||||
let regular_fields: Vec<_> = parsed.fields.iter().filter(|f| !f.is_data_field).collect();
|
||||
|
||||
let parameter_types: Vec<_> = regular_fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { implementations, ty, .. }) => {
|
||||
if !implementations.is_empty() {
|
||||
implementations.iter().map(|ty| (&unit, ty)).collect()
|
||||
} else {
|
||||
vec![(&unit, ty)]
|
||||
}
|
||||
}
|
||||
ParsedFieldType::Node(NodeParsedField {
|
||||
implementations,
|
||||
input_type,
|
||||
output_type,
|
||||
..
|
||||
}) => {
|
||||
if !implementations.is_empty() {
|
||||
implementations.iter().map(|impl_| (&impl_.input, &impl_.output)).collect()
|
||||
} else {
|
||||
vec![(input_type, output_type)]
|
||||
}
|
||||
}
|
||||
}
|
||||
.into_iter()
|
||||
.map(|(input, out)| (substitute_lifetimes(input.clone(), "_"), substitute_lifetimes(out.clone(), "_")))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let max_implementations = parameter_types.iter().map(|x| x.len()).chain([parsed.input.implementations.len().max(1)]).max();
|
||||
|
||||
for i in 0..max_implementations.unwrap_or(0) {
|
||||
let mut temp_constructors = Vec::new();
|
||||
let mut temp_node_io = Vec::new();
|
||||
let mut panic_node_types = Vec::new();
|
||||
|
||||
for (j, types) in parameter_types.iter().enumerate() {
|
||||
let field_name = field_names[j];
|
||||
let (input_type, output_type) = &types[i.min(types.len() - 1)];
|
||||
|
||||
let node = matches!(regular_fields[j].ty, ParsedFieldType::Node { .. });
|
||||
|
||||
let downcast_node = quote!(
|
||||
let #field_name: DowncastBothNode<#input_type, #output_type> = DowncastBothNode::new(args[#j].clone());
|
||||
);
|
||||
if node && !parsed.is_async {
|
||||
return Err(Error::new_spanned(&parsed.fn_name, "Node needs to be async if you want to use lambda parameters"));
|
||||
}
|
||||
temp_constructors.push(downcast_node);
|
||||
temp_node_io.push(quote!(fn_type_fut!(#input_type, #output_type, alias: #output_type)));
|
||||
panic_node_types.push(quote!(#input_type, DynFuture<'static, #output_type>));
|
||||
}
|
||||
let input_type = match parsed.input.implementations.is_empty() {
|
||||
true => parsed.input.ty.clone(),
|
||||
false => parsed.input.implementations[i.min(parsed.input.implementations.len() - 1)].clone(),
|
||||
};
|
||||
constructors.push(quote!(
|
||||
(
|
||||
|args| {
|
||||
Box::pin(async move {
|
||||
#(#temp_constructors;)*
|
||||
let node = #struct_name::new(#(#field_names,)*);
|
||||
// try polling futures
|
||||
let any: DynAnyNode<#input_type, _, _> = DynAnyNode::new(node);
|
||||
Box::new(any) as TypeErasedBox<'_>
|
||||
})
|
||||
}, {
|
||||
let node = #struct_name::new(#(PanicNode::<#panic_node_types>::new(),)*);
|
||||
let params = vec![#(#temp_node_io,)*];
|
||||
let mut node_io = NodeIO::<'_, #input_type>::to_async_node_io(&node, params);
|
||||
node_io
|
||||
|
||||
}
|
||||
)
|
||||
));
|
||||
}
|
||||
Ok(quote! {
|
||||
#[cfg_attr(not(target_family = "wasm"), ctor)]
|
||||
fn register_node() {
|
||||
let mut registry = NODE_REGISTRY.lock().unwrap();
|
||||
registry.insert(
|
||||
#identifier(),
|
||||
vec![
|
||||
#(#constructors,)*
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#wasm_shim
|
||||
})
|
||||
}
|
||||
|
||||
use crate::crate_ident::CrateIdent;
|
||||
use crate::shader_nodes::{ShaderCodegen, ShaderTokens};
|
||||
use syn::visit_mut::VisitMut;
|
||||
use syn::{GenericArgument, Lifetime, Type};
|
||||
|
||||
struct LifetimeReplacer(&'static str);
|
||||
|
||||
impl VisitMut for LifetimeReplacer {
|
||||
fn visit_lifetime_mut(&mut self, lifetime: &mut Lifetime) {
|
||||
lifetime.ident = Ident::new(self.0, lifetime.ident.span());
|
||||
}
|
||||
|
||||
fn visit_type_mut(&mut self, ty: &mut Type) {
|
||||
match ty {
|
||||
Type::Reference(type_reference) => {
|
||||
if let Some(lifetime) = &mut type_reference.lifetime {
|
||||
self.visit_lifetime_mut(lifetime);
|
||||
}
|
||||
self.visit_type_mut(&mut type_reference.elem);
|
||||
}
|
||||
_ => syn::visit_mut::visit_type_mut(self, ty),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_generic_argument_mut(&mut self, arg: &mut GenericArgument) {
|
||||
if let GenericArgument::Lifetime(lifetime) = arg {
|
||||
self.visit_lifetime_mut(lifetime);
|
||||
} else {
|
||||
syn::visit_mut::visit_generic_argument_mut(self, arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn substitute_lifetimes(mut ty: Type, lifetime: &'static str) -> Type {
|
||||
LifetimeReplacer(lifetime).visit_type_mut(&mut ty);
|
||||
ty
|
||||
}
|
||||
use syn::{Lifetime, Type};
|
||||
|
||||
/// Get only the necessary generics.
|
||||
struct FilterUsedGenerics {
|
||||
@@ -856,7 +549,7 @@ impl FilterUsedGenerics {
|
||||
}
|
||||
|
||||
/// Check if a type contains a reference to a specific identifier (e.g., a generic type parameter)
|
||||
fn type_contains_ident(ty: &Type, ident: &Ident) -> bool {
|
||||
pub(crate) fn type_contains_ident(ty: &Type, ident: &Ident) -> bool {
|
||||
struct IdentChecker<'a> {
|
||||
target: &'a Ident,
|
||||
found: bool,
|
||||
|
||||
462
node-graph/node-macro/src/gcodegen.rs
Normal file
462
node-graph/node-macro/src/gcodegen.rs
Normal file
@@ -0,0 +1,462 @@
|
||||
use crate::crate_ident::CrateIdent;
|
||||
use crate::parsing::*;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::visit::Visit;
|
||||
use syn::{GenericArgument, GenericParam, Ident, Lifetime, PathArguments, Type, TypeParam, TypeParamBound};
|
||||
|
||||
pub(crate) struct GNodeTokens {
|
||||
pub(crate) in_mod: TokenStream2,
|
||||
pub(crate) top_level: TokenStream2,
|
||||
}
|
||||
|
||||
pub(crate) fn generate_gnode_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn) -> syn::Result<GNodeTokens> {
|
||||
let core_types = crate_ident.gcore()?;
|
||||
|
||||
let ctx_param = context_param(parsed);
|
||||
let ctx_ident = match ctx_param {
|
||||
Some(ctx_param) => ctx_param.ident.clone(),
|
||||
None => format_ident!("__Ctx"),
|
||||
};
|
||||
let ctx_bounds: Vec<TokenStream2> = match ctx_param {
|
||||
Some(ctx_param) => ctx_param
|
||||
.bounds
|
||||
.iter()
|
||||
.filter_map(|bound| match bound {
|
||||
TypeParamBound::Lifetime(_) => None,
|
||||
bound => Some(desugar_extract_lifetime(bound, core_types)),
|
||||
})
|
||||
.collect(),
|
||||
None => vec![quote!(#core_types::Ctx)],
|
||||
};
|
||||
|
||||
let derives = ctx_param.is_some_and(|ctx_param| {
|
||||
ctx_param.bounds.iter().any(|bound| match bound {
|
||||
TypeParamBound::Trait(trait_bound) => trait_bound.path.segments.last().is_some_and(|segment| segment.ident == "DeriveCtx"),
|
||||
_ => false,
|
||||
})
|
||||
});
|
||||
|
||||
let ctx_generic = match ctx_bounds.is_empty() {
|
||||
true => quote!(#ctx_ident),
|
||||
false => quote!(#ctx_ident: #(#ctx_bounds)+*),
|
||||
};
|
||||
let mut generics: Vec<TokenStream2> = parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.map(|param| match param {
|
||||
GenericParam::Type(type_param) if Some(&type_param.ident) == ctx_param.map(|ctx_param| &ctx_param.ident) => ctx_generic.clone(),
|
||||
param => quote!(#param),
|
||||
})
|
||||
.collect();
|
||||
if ctx_param.is_none() {
|
||||
generics.push(ctx_generic);
|
||||
}
|
||||
|
||||
let fn_name = &parsed.fn_name;
|
||||
let mod_name = format_ident!("_{}_mod", parsed.mod_name);
|
||||
let struct_name = format_ident!("{}Node", parsed.struct_name);
|
||||
let output_type = &parsed.output_type;
|
||||
let trait_output = match kernel_kind(&parsed.output_type) {
|
||||
KernelKind::Interrupt(inner) | KernelKind::Poll(inner) => inner,
|
||||
KernelKind::Plain => parsed.output_type.clone(),
|
||||
};
|
||||
let raw_lazy = matches!(kernel_kind(&parsed.output_type), KernelKind::Poll(_));
|
||||
let where_predicates: Vec<TokenStream2> = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter()).map(|predicate| quote!(#predicate)).collect();
|
||||
|
||||
let (data_fields, regular_fields): (Vec<_>, Vec<_>) = parsed.fields.iter().partition(|field| field.is_data_field);
|
||||
|
||||
let data_field_generic_idents: Vec<Ident> = parsed
|
||||
.fn_generics
|
||||
.iter()
|
||||
.filter_map(|generic| match generic {
|
||||
GenericParam::Type(type_param) => Some(type_param.ident.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|ident| {
|
||||
data_fields.iter().any(|field| match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => crate::codegen::type_contains_ident(ty, ident),
|
||||
_ => false,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let node_generics: Vec<Ident> = regular_fields.iter().enumerate().map(|(index, _)| format_ident!("Node{}", index)).collect();
|
||||
let struct_type_params: Vec<Ident> = data_field_generic_idents.iter().cloned().chain(node_generics.iter().cloned()).collect();
|
||||
|
||||
let data_names: Vec<&Ident> = data_fields.iter().map(|field| &field.pat_ident.ident).collect();
|
||||
let data_params = data_fields.iter().map(|field| {
|
||||
let pat = &field.pat_ident;
|
||||
let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else {
|
||||
unreachable!("data fields are regular types");
|
||||
};
|
||||
quote!(#pat: &#ty)
|
||||
});
|
||||
|
||||
let lazy_bound = |output_type: &Type| match derives {
|
||||
true => quote!(for<'__derived> #core_types::gnode::GNode<#core_types::context::Derived<'__derived, #ctx_ident>, Output = #output_type>),
|
||||
false => quote!(#core_types::gnode::GNode<#ctx_ident, Output = #output_type>),
|
||||
};
|
||||
|
||||
let kernel_params = regular_fields.iter().map(|field| {
|
||||
let pat = &field.pat_ident;
|
||||
match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#pat: #ty),
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) if raw_lazy => {
|
||||
let bound = lazy_bound(output_type);
|
||||
quote!(#pat: &impl #bound)
|
||||
}
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => {
|
||||
let bound = lazy_bound(output_type);
|
||||
quote!(#pat: #core_types::gnode::LazyInput<'_, impl #bound>)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let node_bounds = regular_fields.iter().zip(&node_generics).map(|(field, node_generic)| match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#node_generic: #core_types::gnode::GNode<#ctx_ident, Output = #ty>),
|
||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => {
|
||||
let bound = lazy_bound(output_type);
|
||||
quote!(#node_generic: #bound)
|
||||
}
|
||||
});
|
||||
|
||||
let clampable_bounds = regular_fields.iter().filter_map(|field| {
|
||||
let ParsedFieldType::Regular(RegularParsedField { ty, number_hard_min, number_hard_max, .. }) = &field.ty else {
|
||||
return None;
|
||||
};
|
||||
(number_hard_min.is_some() || number_hard_max.is_some()).then(|| quote!(#ty: #core_types::misc::Clampable))
|
||||
});
|
||||
|
||||
let eval_values = regular_fields.iter().enumerate().map(|(index, field)| {
|
||||
let name = &field.pat_ident.ident;
|
||||
match &field.ty {
|
||||
ParsedFieldType::Regular(_) => quote! {
|
||||
let #name = match __cell.eval_input(#index, &self.#name, __input) {
|
||||
Ok(value) => value,
|
||||
Err(interrupt) => return interrupt.into(),
|
||||
};
|
||||
},
|
||||
ParsedFieldType::Node(_) if raw_lazy => quote!(),
|
||||
ParsedFieldType::Node(_) => quote! {
|
||||
let #name = #core_types::gnode::LazyInput::new(&self.#name, &__cell, #index);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
let clamps = regular_fields.iter().filter_map(|field| {
|
||||
let ParsedFieldType::Regular(RegularParsedField { number_hard_min, number_hard_max, .. }) = &field.ty else {
|
||||
return None;
|
||||
};
|
||||
let name = &field.pat_ident.ident;
|
||||
let mut tokens = quote!();
|
||||
if let Some(min) = number_hard_min {
|
||||
tokens.extend(quote!(let #name = #core_types::misc::Clampable::clamp_hard_min(#name, #min);));
|
||||
}
|
||||
if let Some(max) = number_hard_max {
|
||||
tokens.extend(quote!(let #name = #core_types::misc::Clampable::clamp_hard_max(#name, #max);));
|
||||
}
|
||||
(!tokens.is_empty()).then_some(tokens)
|
||||
});
|
||||
|
||||
let call_args = regular_fields.iter().map(|field| {
|
||||
let name = &field.pat_ident.ident;
|
||||
match &field.ty {
|
||||
ParsedFieldType::Node(_) if raw_lazy => quote!(&self.#name),
|
||||
_ => quote!(#name),
|
||||
}
|
||||
});
|
||||
|
||||
let value_field_names: Vec<&Ident> = regular_fields
|
||||
.iter()
|
||||
.filter(|field| matches!(field.ty, ParsedFieldType::Regular(_)))
|
||||
.map(|field| &field.pat_ident.ident)
|
||||
.collect();
|
||||
|
||||
let extent_impl = match &parsed.attributes.extent {
|
||||
Some(path) => quote! {
|
||||
fn extent(&self, __input: &#ctx_ident) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> {
|
||||
#path(self, __input)
|
||||
}
|
||||
},
|
||||
None if value_field_names.is_empty() => quote!(),
|
||||
None => {
|
||||
let first = value_field_names[0];
|
||||
let mut meet = quote!(self.#first.extent(__input));
|
||||
for name in &value_field_names[1..] {
|
||||
meet = quote!(#core_types::gpoll::Extent::meet(#meet, self.#name.extent(__input)));
|
||||
}
|
||||
quote! {
|
||||
fn extent(&self, __input: &#ctx_ident) -> #core_types::gpoll::GPoll<#core_types::gpoll::Extent> {
|
||||
#meet
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let batch_impl = match &parsed.attributes.batch {
|
||||
Some(path) => quote! {
|
||||
fn eval_batch<'__batch>(
|
||||
&self,
|
||||
__input: &'__batch #ctx_ident,
|
||||
__range: ::std::ops::Range<u64>,
|
||||
__scratch: Option<&'__batch mut [::std::mem::MaybeUninit<Self::Output>]>,
|
||||
) -> #core_types::gnode::BatchStatus<'__batch, Self::Output>
|
||||
where
|
||||
#ctx_ident: #core_types::context::InjectIndex + Copy,
|
||||
{
|
||||
#path(self, __input, __range, __scratch)
|
||||
}
|
||||
},
|
||||
None => quote!(),
|
||||
};
|
||||
|
||||
let ctx_pat = &parsed.input.pat_ident;
|
||||
let fn_where = &parsed.where_clause;
|
||||
let body = &parsed.body;
|
||||
let vis = &parsed.vis;
|
||||
let kernel = quote! {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#vis fn #fn_name<#(#generics,)*>(#ctx_pat: &#ctx_ident #(, #data_params)* #(, #kernel_params)*) -> #output_type #fn_where #body
|
||||
};
|
||||
let cell_constructor = match parsed.attributes.no_partial {
|
||||
true => quote!(#core_types::gnode::StatusCell::no_partial()),
|
||||
false => quote!(#core_types::gnode::StatusCell::new()),
|
||||
};
|
||||
let kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #call_args)*));
|
||||
let lift = match kernel_kind(&parsed.output_type) {
|
||||
KernelKind::Interrupt(_) => quote! {
|
||||
match #kernel_call {
|
||||
Ok(value) => __cell.finish(value),
|
||||
Err(interrupt) => interrupt.into(),
|
||||
}
|
||||
},
|
||||
KernelKind::Poll(_) => quote!(__cell.merge(#kernel_call)),
|
||||
KernelKind::Plain => quote!(__cell.finish(#kernel_call)),
|
||||
};
|
||||
|
||||
let wire = entries_tokens(parsed, &struct_name, &data_field_generic_idents, ®ular_fields);
|
||||
let cfg = crate::shader_nodes::modify_cfg(&parsed.attributes);
|
||||
let wire_reexport = match wire.is_empty() {
|
||||
true => quote!(),
|
||||
false => {
|
||||
let entries_name = format_ident!("{}_entries", fn_name);
|
||||
quote! {
|
||||
#cfg
|
||||
#[doc(hidden)]
|
||||
pub use #mod_name::#entries_name;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let top_level = quote! {
|
||||
#wire_reexport
|
||||
|
||||
#cfg
|
||||
#[automatically_derived]
|
||||
impl<#(#generics,)* #(#node_generics,)*> #core_types::gnode::GNode<#ctx_ident> for #mod_name::#struct_name<#(#struct_type_params,)*>
|
||||
where
|
||||
#(#node_bounds,)*
|
||||
#(#clampable_bounds,)*
|
||||
#(#where_predicates,)*
|
||||
{
|
||||
type Output = #trait_output;
|
||||
|
||||
fn eval(&self, __input: &#ctx_ident) -> #core_types::gpoll::GPoll<Self::Output> {
|
||||
let __cell = #cell_constructor;
|
||||
#(#eval_values)*
|
||||
#(#clamps)*
|
||||
#lift
|
||||
}
|
||||
|
||||
#extent_impl
|
||||
|
||||
#batch_impl
|
||||
}
|
||||
};
|
||||
|
||||
Ok(GNodeTokens {
|
||||
in_mod: wire,
|
||||
top_level: quote! {
|
||||
#kernel
|
||||
|
||||
#top_level
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
enum KernelKind {
|
||||
Plain,
|
||||
Interrupt(Type),
|
||||
Poll(Type),
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
"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();
|
||||
};
|
||||
match error_path.path.segments.last().is_some_and(|segment| segment.ident == "Interrupt") {
|
||||
true => KernelKind::Interrupt(inner.clone()),
|
||||
false => plain(),
|
||||
}
|
||||
}
|
||||
_ => plain(),
|
||||
}
|
||||
}
|
||||
|
||||
fn context_param<'a>(parsed: &'a ParsedNodeFn) -> Option<&'a 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,
|
||||
})
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
fn visit_lifetime(&mut self, _: &'ast Lifetime) {
|
||||
self.found = true;
|
||||
}
|
||||
}
|
||||
|
||||
let mut visitor = Disqualifier { found: false };
|
||||
visitor.visit_type(ty);
|
||||
visitor.found
|
||||
}
|
||||
|
||||
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>)
|
||||
}
|
||||
|
||||
fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic_idents: &[Ident], regular_fields: &[&ParsedField]) -> TokenStream2 {
|
||||
if !data_field_generic_idents.is_empty() {
|
||||
return quote!();
|
||||
}
|
||||
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 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 entries = rows.iter().map(|row| {
|
||||
let types = row.iter();
|
||||
let boxed_types = row.iter().map(|ty| quote!(::std::boxed::Box<gcore::wire::ErasedGNode<#ty>>));
|
||||
let output = quote!(<#struct_name<#(#boxed_types),*> as gcore::gnode::GNode<gcore::context::ContextImpl<'static>>>::Output);
|
||||
let downcasts = names.iter().zip(row.iter()).map(|(name, ty)| {
|
||||
quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;)
|
||||
});
|
||||
quote! {
|
||||
gcore::wire::RegistryEntry {
|
||||
io: gcore::wire::NodeIoRecord {
|
||||
inputs: vec![#(gcore::concrete!(#types)),*],
|
||||
output: gcore::concrete!(#output),
|
||||
},
|
||||
wire: |inputs| {
|
||||
if inputs.len() != #arity {
|
||||
return Err(gcore::wire::WireError::Arity { expected: #arity, got: inputs.len() });
|
||||
}
|
||||
let mut inputs = inputs.into_iter();
|
||||
#(#downcasts)*
|
||||
Ok(gcore::wire::EdgeHandle::new(::std::boxed::Box::new(#struct_name::new(#(#names),*)) as ::std::boxed::Box<gcore::wire::ErasedGNode<#output>>))
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
pub fn #entries_name() -> ::std::vec::Vec<gcore::wire::RegistryEntry> {
|
||||
vec![#(#entries),*]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ mod buffer_struct;
|
||||
mod codegen;
|
||||
mod crate_ident;
|
||||
mod derive_choice_type;
|
||||
mod gcodegen;
|
||||
mod parsing;
|
||||
mod shader_nodes;
|
||||
mod validation;
|
||||
|
||||
@@ -56,6 +56,14 @@ pub(crate) struct NodeFnAttributes {
|
||||
pub(crate) memoize: bool,
|
||||
/// Whether this node provides a scope
|
||||
pub(crate) inject_scope: bool,
|
||||
/// Function producing a stand-in value while an async source node's real value is in flight
|
||||
pub(crate) placeholder: Option<Path>,
|
||||
/// Function overriding the generated `extent` method
|
||||
pub(crate) extent: Option<Path>,
|
||||
/// Function overriding the generated `eval_batch` method
|
||||
pub(crate) batch: Option<Path>,
|
||||
/// Whether partial upstream values are mapped to `Pending` instead of flowing into this node
|
||||
pub(crate) no_partial: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -311,6 +319,10 @@ impl Parse for NodeFnAttributes {
|
||||
let mut serialize = None;
|
||||
let mut memoize = false;
|
||||
let mut inject_scope = false;
|
||||
let mut placeholder = None;
|
||||
let mut extent = None;
|
||||
let mut batch = None;
|
||||
let mut no_partial = false;
|
||||
|
||||
let content = input;
|
||||
// let content;
|
||||
@@ -453,13 +465,63 @@ impl Parse for NodeFnAttributes {
|
||||
}
|
||||
inject_scope = true;
|
||||
}
|
||||
// Function producing a stand-in value for an async source node while the spawned future is in flight.
|
||||
// The node reports `Partial` with the stand-in until the real value lands; without a placeholder it reports `Pending`.
|
||||
//
|
||||
// Example usage:
|
||||
// #[node_macro::node(..., placeholder(empty_image), ...)]
|
||||
"placeholder" => {
|
||||
let meta = meta.require_list()?;
|
||||
if placeholder.is_some() {
|
||||
return Err(Error::new_spanned(meta, "Multiple 'placeholder' attributes are not allowed"));
|
||||
}
|
||||
let parsed_path: Path = meta
|
||||
.parse_args()
|
||||
.map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'placeholder', e.g., placeholder(empty_image)"))?;
|
||||
placeholder = Some(parsed_path);
|
||||
}
|
||||
// Function overriding the generated `extent` method, replacing the default meet over the node's inputs.
|
||||
//
|
||||
// Example usage:
|
||||
// #[node_macro::node(..., extent(my_extent), ...)]
|
||||
"extent" => {
|
||||
let meta = meta.require_list()?;
|
||||
if extent.is_some() {
|
||||
return Err(Error::new_spanned(meta, "Multiple 'extent' attributes are not allowed"));
|
||||
}
|
||||
let parsed_path: Path = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'extent', e.g., extent(my_extent)"))?;
|
||||
extent = Some(parsed_path);
|
||||
}
|
||||
// Function overriding the generated `eval_batch` method, replacing the trait's per-lane spec loop.
|
||||
//
|
||||
// Example usage:
|
||||
// #[node_macro::node(..., batch(my_batch), ...)]
|
||||
"batch" => {
|
||||
let meta = meta.require_list()?;
|
||||
if batch.is_some() {
|
||||
return Err(Error::new_spanned(meta, "Multiple 'batch' attributes are not allowed"));
|
||||
}
|
||||
let parsed_path: Path = meta.parse_args().map_err(|_| Error::new_spanned(meta, "Expected a valid path for 'batch', e.g., batch(my_batch)"))?;
|
||||
batch = Some(parsed_path);
|
||||
}
|
||||
// Instructs the generated eval to report `Pending` instead of passing partial upstream values into this node.
|
||||
//
|
||||
// Example usage:
|
||||
// #[node_macro::node(..., no_partial, ...)]
|
||||
"no_partial" => {
|
||||
let path = meta.require_path_only()?;
|
||||
if no_partial {
|
||||
return Err(Error::new_spanned(path, "Multiple 'no_partial' attributes are not allowed"));
|
||||
}
|
||||
no_partial = true;
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::new_spanned(
|
||||
meta,
|
||||
indoc!(
|
||||
r#"
|
||||
Unsupported attribute in `node`.
|
||||
Supported attributes are 'category', 'name', 'path', 'skip_impl', 'properties', 'cfg', 'shader_node', 'serialize', 'memoize', and 'inject_scope'.
|
||||
Supported attributes are 'category', 'name', 'path', 'skip_impl', 'properties', 'cfg', 'shader_node', 'serialize', 'memoize', 'inject_scope', 'placeholder', 'extent', 'batch', and 'no_partial'.
|
||||
Example usage:
|
||||
#[node_macro::node(..., name("Test Node"), ...)]
|
||||
"#
|
||||
@@ -493,6 +555,10 @@ impl Parse for NodeFnAttributes {
|
||||
serialize,
|
||||
memoize,
|
||||
inject_scope,
|
||||
placeholder,
|
||||
extent,
|
||||
batch,
|
||||
no_partial,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1082,6 +1148,10 @@ mod tests {
|
||||
serialize: None,
|
||||
memoize: false,
|
||||
inject_scope: false,
|
||||
placeholder: None,
|
||||
extent: None,
|
||||
batch: None,
|
||||
no_partial: false,
|
||||
},
|
||||
fn_name: Ident::new("add", Span::call_site()),
|
||||
struct_name: Ident::new("Add", Span::call_site()),
|
||||
@@ -1152,6 +1222,10 @@ mod tests {
|
||||
serialize: None,
|
||||
memoize: false,
|
||||
inject_scope: false,
|
||||
placeholder: None,
|
||||
extent: None,
|
||||
batch: None,
|
||||
no_partial: false,
|
||||
},
|
||||
fn_name: Ident::new("transform", Span::call_site()),
|
||||
struct_name: Ident::new("Transform", Span::call_site()),
|
||||
@@ -1236,6 +1310,10 @@ mod tests {
|
||||
serialize: None,
|
||||
memoize: false,
|
||||
inject_scope: false,
|
||||
placeholder: None,
|
||||
extent: None,
|
||||
batch: None,
|
||||
no_partial: false,
|
||||
},
|
||||
fn_name: Ident::new("circle", Span::call_site()),
|
||||
struct_name: Ident::new("Circle", Span::call_site()),
|
||||
@@ -1302,6 +1380,10 @@ mod tests {
|
||||
serialize: None,
|
||||
memoize: false,
|
||||
inject_scope: false,
|
||||
placeholder: None,
|
||||
extent: None,
|
||||
batch: None,
|
||||
no_partial: false,
|
||||
},
|
||||
fn_name: Ident::new("levels", Span::call_site()),
|
||||
struct_name: Ident::new("Levels", Span::call_site()),
|
||||
@@ -1380,6 +1462,10 @@ mod tests {
|
||||
serialize: None,
|
||||
memoize: false,
|
||||
inject_scope: false,
|
||||
placeholder: None,
|
||||
extent: None,
|
||||
batch: None,
|
||||
no_partial: false,
|
||||
},
|
||||
fn_name: Ident::new("add", Span::call_site()),
|
||||
struct_name: Ident::new("Add", Span::call_site()),
|
||||
@@ -1461,6 +1547,10 @@ mod tests {
|
||||
serialize: None,
|
||||
memoize: false,
|
||||
inject_scope: false,
|
||||
placeholder: None,
|
||||
extent: None,
|
||||
batch: None,
|
||||
no_partial: false,
|
||||
},
|
||||
fn_name: Ident::new("load_image", Span::call_site()),
|
||||
struct_name: Ident::new("LoadImage", Span::call_site()),
|
||||
@@ -1527,6 +1617,10 @@ mod tests {
|
||||
serialize: None,
|
||||
memoize: false,
|
||||
inject_scope: false,
|
||||
placeholder: None,
|
||||
extent: None,
|
||||
batch: None,
|
||||
no_partial: false,
|
||||
},
|
||||
fn_name: Ident::new("custom_node", Span::call_site()),
|
||||
struct_name: Ident::new("CustomNode", Span::call_site()),
|
||||
|
||||
@@ -146,7 +146,7 @@ impl PerPixelAdjustCodegen<'_> {
|
||||
ParamType::Uniform => quote!(uniform.#ident),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let context = quote!(());
|
||||
let context = quote!(&());
|
||||
|
||||
let entry_point_mod = &self.entry_point_mod;
|
||||
let entry_point_name = &self.entry_point_name_ident;
|
||||
|
||||
Reference in New Issue
Block a user