mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-26 11:28:11 +08:00
Support lazy record carriers so a creator can read and rewrite attrs per copy
This commit is contained in:
@@ -750,6 +750,53 @@ impl<'a, 'e, N> RecordLazyInput<'a, 'e, N> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The derive-routing carrier beside its declared attribute reads: evaluating
|
||||||
|
/// at a derived context yields the opaque row token and the read values in one
|
||||||
|
/// step, so the kernel drives the per-copy eval while reads stay resolved
|
||||||
|
/// against the source's wired layout.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct DerivedLazyInput<'a, 'e, Out, N> {
|
||||||
|
node: &'a N,
|
||||||
|
cell: &'a crate::node::StatusCell,
|
||||||
|
input_index: usize,
|
||||||
|
reads: &'a [Option<usize>],
|
||||||
|
read: unsafe fn(Rec, &[Option<usize>]) -> Out,
|
||||||
|
_lifetime: std::marker::PhantomData<fn() -> RecordValue<'e>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'e, Out, N> DerivedLazyInput<'a, 'e, Out, N> {
|
||||||
|
/// `read` must be sound against the layout the offsets in `reads` were
|
||||||
|
/// resolved from; the macro proves both at wiring.
|
||||||
|
pub fn new(node: &'a N, cell: &'a crate::node::StatusCell, input_index: usize, reads: &'a [Option<usize>], read: unsafe fn(Rec, &[Option<usize>]) -> Out) -> Self {
|
||||||
|
Self {
|
||||||
|
node,
|
||||||
|
cell,
|
||||||
|
input_index,
|
||||||
|
reads,
|
||||||
|
read,
|
||||||
|
_lifetime: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval<'d, C>(&self, ctx: &C) -> Result<Out, crate::gpoll::Interrupt>
|
||||||
|
where
|
||||||
|
N: DerivedRecordEdge<'d, C>,
|
||||||
|
{
|
||||||
|
let value: RecordValue<'e> = self.node.eval_derived(self.cell, self.input_index, ctx)?.rebind();
|
||||||
|
// SAFETY: declared reads imply a non-empty layout, so the record is
|
||||||
|
// spilled and its pointer is the frame the offsets index into.
|
||||||
|
Ok(unsafe { (self.read)(Rec::new(value.ptr), self.reads) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The read-less [`DerivedLazyInput`] glue: the token alone.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `rec` must be a spilled record's frame.
|
||||||
|
pub unsafe fn token_only<'e>(rec: Rec, _reads: &[Option<usize>]) -> RecordValue<'e> {
|
||||||
|
RecordValue::spilled(rec)
|
||||||
|
}
|
||||||
|
|
||||||
/// The per-thread record stack: every record evaluation claims its activation
|
/// The per-thread record stack: every record evaluation claims its activation
|
||||||
/// frame at the stack pointer and evaluates its carrier beyond it, so slot
|
/// frame at the stack pointer and evaluates its carrier beyond it, so slot
|
||||||
/// addresses are a property of the evaluating thread and no global assignment
|
/// addresses are a property of the evaluating thread and no global assignment
|
||||||
|
|||||||
@@ -826,7 +826,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
if routing_generic.is_some() || record_io || flip {
|
if routing_generic.is_some() || record_io || flip {
|
||||||
impl_generics.insert(0, quote!('__record));
|
impl_generics.insert(0, quote!('__record));
|
||||||
}
|
}
|
||||||
if derive_routing {
|
let lazy_carrier = record_io && carrier_present && matches!(parsed.fields.iter().find(|field| !field.is_data_field).map(|field| &field.ty), Some(ParsedFieldType::Node(_)));
|
||||||
|
if derive_routing || (lazy_carrier && derives) {
|
||||||
generics.insert(0, quote!('__record));
|
generics.insert(0, quote!('__record));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,6 +866,12 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if lazy_carrier && derives {
|
||||||
|
let source_generic = format_ident!("__Source0");
|
||||||
|
generics.push(quote! {
|
||||||
|
#source_generic: for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>
|
||||||
|
});
|
||||||
|
}
|
||||||
if flip {
|
if flip {
|
||||||
let mut kernel_lazy = false;
|
let mut kernel_lazy = false;
|
||||||
for (index, field) in regular_fields.iter().enumerate() {
|
for (index, field) in regular_fields.iter().enumerate() {
|
||||||
@@ -943,6 +950,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let source_generic = format_ident!("__Source{index}");
|
let source_generic = format_ident!("__Source{index}");
|
||||||
match (ir::lazy_binding(&node, index), raw_lazy) {
|
match (ir::lazy_binding(&node, index), raw_lazy) {
|
||||||
(LazyBinding::DeriveRouting, _) => quote!(#pat: #core_types::record::RecordLazyInput<'_, '__record, #source_generic>),
|
(LazyBinding::DeriveRouting, _) => quote!(#pat: #core_types::record::RecordLazyInput<'_, '__record, #source_generic>),
|
||||||
|
(LazyBinding::DeriveCarrier, _) => {
|
||||||
|
let out = lazy_read_out(field, output_type);
|
||||||
|
quote!(#pat: #core_types::record::DerivedLazyInput<'_, '__record, #out, #source_generic>)
|
||||||
|
}
|
||||||
(LazyBinding::OpaqueRecord, _) => quote!(#pat: &#core_types::record::RecordEdgeInput<'_, #source_generic>),
|
(LazyBinding::OpaqueRecord, _) => quote!(#pat: &#core_types::record::RecordEdgeInput<'_, #source_generic>),
|
||||||
(LazyBinding::Element, true) => {
|
(LazyBinding::Element, true) => {
|
||||||
let out = lazy_read_out(field, output_type);
|
let out = lazy_read_out(field, output_type);
|
||||||
@@ -975,6 +986,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
},
|
},
|
||||||
false => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>),
|
false => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>),
|
||||||
},
|
},
|
||||||
|
ParsedFieldType::Node(_) if record_io && !skips_carrier && index == 0 => match derives {
|
||||||
|
true => quote!(#node_generic: for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>),
|
||||||
|
false => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>),
|
||||||
|
},
|
||||||
ParsedFieldType::Regular(_) if record_io && !skips_carrier && index == 0 => {
|
ParsedFieldType::Regular(_) if record_io && !skips_carrier && index == 0 => {
|
||||||
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>)
|
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>)
|
||||||
}
|
}
|
||||||
@@ -1148,6 +1163,22 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
(LazyBinding::DeriveRouting, _) => quote! {
|
(LazyBinding::DeriveRouting, _) => quote! {
|
||||||
let #name = #core_types::record::RecordLazyInput::new(&self.#name, &__cell, #index);
|
let #name = #core_types::record::RecordLazyInput::new(&self.#name, &__cell, #index);
|
||||||
},
|
},
|
||||||
|
(LazyBinding::DeriveCarrier, _) => {
|
||||||
|
let reads = reads_of(index);
|
||||||
|
let read_fn = format_ident!("__{}_read_{}", fn_name, index);
|
||||||
|
match reads.is_empty() {
|
||||||
|
true => quote! {
|
||||||
|
let #name = #core_types::record::DerivedLazyInput::new(&self.#name, &__cell, #index, &[], #core_types::record::token_only);
|
||||||
|
},
|
||||||
|
false => {
|
||||||
|
let slot_idents: Vec<Ident> = reads.iter().map(|(slot, _)| format_ident!("__read_{slot}")).collect();
|
||||||
|
quote! {
|
||||||
|
let __carrier_reads = [#(self.#slot_idents),*];
|
||||||
|
let #name = #core_types::record::DerivedLazyInput::new(&self.#name, &__cell, #index, &__carrier_reads, self::#read_fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
(LazyBinding::Element, true) => {
|
(LazyBinding::Element, true) => {
|
||||||
let slot = format_ident!("__in_{index}");
|
let slot = format_ident!("__in_{index}");
|
||||||
match field.attribute_reads.is_empty() {
|
match field.attribute_reads.is_empty() {
|
||||||
@@ -1242,7 +1273,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
};
|
};
|
||||||
let decl = match &field.ty {
|
let decl = match &field.ty {
|
||||||
ParsedFieldType::Node(_) => match ir::lazy_binding(&node, index) {
|
ParsedFieldType::Node(_) => match ir::lazy_binding(&node, index) {
|
||||||
ir::LazyBinding::DeriveRouting => quote! {
|
ir::LazyBinding::DeriveRouting | ir::LazyBinding::DeriveCarrier => quote! {
|
||||||
let #query = |__copy: u64, __lvl: u8| {
|
let #query = |__copy: u64, __lvl: u8| {
|
||||||
let __head = #core_types::context::DeriveCtx::index_head(__input);
|
let __head = #core_types::context::DeriveCtx::index_head(__input);
|
||||||
#core_types::record::DerivedRecordEdge::extent_at_derived(&self.#name, &#core_types::context::DeriveCtx::promoted(__input, &__head, __copy), __lvl)
|
#core_types::record::DerivedRecordEdge::extent_at_derived(&self.#name, &#core_types::context::DeriveCtx::promoted(__input, &__head, __copy), __lvl)
|
||||||
@@ -1505,6 +1536,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
};
|
};
|
||||||
let carrier_arg = if skips_carrier {
|
let carrier_arg = if skips_carrier {
|
||||||
None
|
None
|
||||||
|
} else if lazy_carrier {
|
||||||
|
// The kernel drives the derived carrier itself through its handle.
|
||||||
|
let name = ®ular_fields[0].pat_ident.ident;
|
||||||
|
Some(quote!(#name))
|
||||||
} else if let Some(ty) = carrier_read_ty {
|
} else if let Some(ty) = carrier_read_ty {
|
||||||
Some(tuple_arg(regular_fields[0], quote!(unsafe { #core_types::record::read_element::<#ty>(__src_rec) })))
|
Some(tuple_arg(regular_fields[0], quote!(unsafe { #core_types::record::read_element::<#ty>(__src_rec) })))
|
||||||
} else {
|
} else {
|
||||||
@@ -1521,7 +1556,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let record_kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #carrier_arg)* #(, #value_args)*));
|
let record_kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #carrier_arg)* #(, #value_args)*));
|
||||||
let carrier_eval = (!skips_carrier).then(|| {
|
let carrier_eval = (!skips_carrier && !lazy_carrier).then(|| {
|
||||||
let name = ®ular_fields[0].pat_ident.ident;
|
let name = ®ular_fields[0].pat_ident.ident;
|
||||||
quote! {
|
quote! {
|
||||||
let __src = match __cell.eval_input(0, &self.#name, __input) {
|
let __src = match __cell.eval_input(0, &self.#name, __input) {
|
||||||
@@ -1531,8 +1566,18 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let __src_rec = self.__carrier.rec(&__src);
|
let __src_rec = self.__carrier.rec(&__src);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let carry = (!skips_carrier).then(|| quote!(unsafe { #core_types::record::apply_plan(__src_rec, __dst, &self.__plan) };));
|
let carry = (!skips_carrier && !lazy_carrier).then(|| quote!(unsafe { #core_types::record::apply_plan(__src_rec, __dst, &self.__plan) };));
|
||||||
let carrier_read_bindings: Vec<TokenStream2> = match skips_carrier {
|
// A lazy carrier's source record is the token the kernel returned; its
|
||||||
|
// content frames sit above `__dst` and stay readable until the truncate.
|
||||||
|
let lazy_carry = lazy_carrier
|
||||||
|
.then(|| {
|
||||||
|
quote! {
|
||||||
|
let __src_rec = self.__carrier.rec(&__element);
|
||||||
|
unsafe { #core_types::record::apply_plan(__src_rec, __dst, &self.__plan) };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let carrier_read_bindings: Vec<TokenStream2> = match skips_carrier || lazy_carrier {
|
||||||
true => Vec::new(),
|
true => Vec::new(),
|
||||||
false => reads_of(0).into_iter().map(|(slot, read)| read_binding(slot, read, quote!(__src_rec))).collect(),
|
false => reads_of(0).into_iter().map(|(slot, read)| read_binding(slot, read, quote!(__src_rec))).collect(),
|
||||||
};
|
};
|
||||||
@@ -1546,9 +1591,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
_ => quote!(#record_kernel_call),
|
_ => quote!(#record_kernel_call),
|
||||||
};
|
};
|
||||||
let attr_binders: Vec<Ident> = (0..write_markers.len()).map(|index| format_ident!("__attr_{index}")).collect();
|
let attr_binders: Vec<Ident> = (0..write_markers.len()).map(|index| format_ident!("__attr_{index}")).collect();
|
||||||
let element_binder = match element_write {
|
let element_binder = match (element_write, lazy_carrier) {
|
||||||
Some(_) => quote!(__element),
|
(Some(_), _) | (None, true) => quote!(__element),
|
||||||
None => quote!(_),
|
(None, false) => quote!(_),
|
||||||
};
|
};
|
||||||
// Slot binders in the return tuple's own order: an `Attr` binds the
|
// Slot binders in the return tuple's own order: an `Attr` binds the
|
||||||
// next write binder, a `RemoveAttr` binds nothing.
|
// next write binder, a `RemoveAttr` binds nothing.
|
||||||
@@ -1590,6 +1635,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
#(#carrier_read_bindings)*
|
#(#carrier_read_bindings)*
|
||||||
let __kernel_value = #kernel_value;
|
let __kernel_value = #kernel_value;
|
||||||
#destructure
|
#destructure
|
||||||
|
#lazy_carry
|
||||||
#element_store
|
#element_store
|
||||||
#(#attr_stores)*
|
#(#attr_stores)*
|
||||||
if self.__frame_bytes != 0 {
|
if self.__frame_bytes != 0 {
|
||||||
@@ -1714,7 +1760,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
};
|
};
|
||||||
|
|
||||||
let record_bounds: Vec<TokenStream2> = {
|
let record_bounds: Vec<TokenStream2> = {
|
||||||
let arena_bound = (record_io && skips_carrier) || (!record_io && (derive_routing || flip));
|
let arena_bound = (record_io && skips_carrier) || (record_io && lazy_carrier) || (!record_io && (derive_routing || flip));
|
||||||
let mut bounds = if arena_bound {
|
let mut bounds = if arena_bound {
|
||||||
vec![quote!(#ctx_ident: #core_types::context::ExtractArena<ArenaRef = &'__record #core_types::arena::Arena>)]
|
vec![quote!(#ctx_ident: #core_types::context::ExtractArena<ArenaRef = &'__record #core_types::arena::Arena>)]
|
||||||
} else {
|
} else {
|
||||||
@@ -2037,6 +2083,16 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let marker = &read.marker;
|
let marker = &read.marker;
|
||||||
quote!(#core_types::attribute::Attr<'__read, #marker>)
|
quote!(#core_types::attribute::Attr<'__read, #marker>)
|
||||||
});
|
});
|
||||||
|
if matches!(ir::lazy_binding(&node, index), LazyBinding::DeriveCarrier) {
|
||||||
|
return quote! {
|
||||||
|
/// # Safety
|
||||||
|
/// `__rec` must be a spilled record's frame, of the layout
|
||||||
|
/// `__reads` was resolved against; the token rebinds it.
|
||||||
|
unsafe fn #read_fn<'__read>(__rec: #core_types::record::Rec, __reads: &[Option<usize>]) -> (#core_types::record::RecordValue<'__read> #(, #attr_tys)*) {
|
||||||
|
(#core_types::record::RecordValue::spilled(__rec) #(, #attr_slots)*)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
quote! {
|
quote! {
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// `__rec` must be a record whose element is the declared output
|
/// `__rec` must be a record whose element is the declared output
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ use super::*;
|
|||||||
|
|
||||||
/// How a record node's primary input lowers: `None` writes a fresh record,
|
/// How a record node's primary input lowers: `None` writes a fresh record,
|
||||||
/// `Token` carries the element bytes through as `ElToken`, `Read` reads a
|
/// `Token` carries the element bytes through as `ElToken`, `Read` reads a
|
||||||
/// concrete element at offset 0. The element and write set fold from the IR.
|
/// 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)]
|
#[derive(Clone)]
|
||||||
pub(crate) enum RecordCarrier {
|
pub(crate) enum RecordCarrier {
|
||||||
None,
|
None,
|
||||||
Token,
|
Token,
|
||||||
Read,
|
Read,
|
||||||
|
LazyToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A well-formed record-io node: only the carrier form is retained, so
|
/// A well-formed record-io node: only the carrier form is retained, so
|
||||||
@@ -269,19 +272,41 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
|
|||||||
if !has_reads && writes.is_none() {
|
if !has_reads && writes.is_none() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if parsed.is_async || parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) {
|
if parsed.is_async {
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let reads_well_placed = parsed.fields.iter().all(|field| {
|
|
||||||
field.attribute_reads.is_empty() || (!field.is_data_field && matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: None, .. })))
|
|
||||||
});
|
|
||||||
if !reads_well_placed {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let carrier_field = parsed.fields.first()?;
|
let carrier_field = parsed.fields.first()?;
|
||||||
if carrier_field.is_data_field {
|
if carrier_field.is_data_field {
|
||||||
return None;
|
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(_));
|
||||||
|
if parsed.fields.iter().skip(lazy_carrier as usize).any(|field| matches!(field.ty, ParsedFieldType::Node(_))) {
|
||||||
|
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 {
|
let ParsedFieldType::Regular(RegularParsedField { ty, lend: None, implementations, .. }) = &carrier_field.ty else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ fn inputs(parsed: &ParsedNodeFn, fields: &[&ParsedField], generics: &[Ident]) ->
|
|||||||
|
|
||||||
fn subject(index: usize, field: &ParsedField, carrier_subject: bool, routing: Option<&RoutingIo>) -> bool {
|
fn subject(index: usize, field: &ParsedField, carrier_subject: bool, routing: Option<&RoutingIo>) -> bool {
|
||||||
match &field.ty {
|
match &field.ty {
|
||||||
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => is_record_value(output_type) || routing.is_some_and(|routing| bare_ident(output_type) == Some(&routing.generic)),
|
ParsedFieldType::Node(NodeParsedField { output_type, .. }) => {
|
||||||
|
is_record_value(output_type) || routing.is_some_and(|routing| bare_ident(output_type) == Some(&routing.generic)) || (index == 0 && carrier_subject)
|
||||||
|
}
|
||||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => routing.is_some_and(|routing| bare_ident(ty) == Some(&routing.generic)) || (index == 0 && carrier_subject),
|
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => routing.is_some_and(|routing| bare_ident(ty) == Some(&routing.generic)) || (index == 0 && carrier_subject),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,6 +274,7 @@ pub(crate) enum LazyBinding {
|
|||||||
Element,
|
Element,
|
||||||
Plain,
|
Plain,
|
||||||
DeriveRouting,
|
DeriveRouting,
|
||||||
|
DeriveCarrier,
|
||||||
OpaqueRecord,
|
OpaqueRecord,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,6 +348,8 @@ pub(crate) fn lazy_binding(node: &Node, index: usize) -> LazyBinding {
|
|||||||
let kind = node_kind(node);
|
let kind = node_kind(node);
|
||||||
if node.derives && matches!(kind, NodeKind::Routing) && input.subject {
|
if node.derives && matches!(kind, NodeKind::Routing) && input.subject {
|
||||||
LazyBinding::DeriveRouting
|
LazyBinding::DeriveRouting
|
||||||
|
} else if node.derives && matches!(kind, NodeKind::RecordIo) && input.subject {
|
||||||
|
LazyBinding::DeriveCarrier
|
||||||
} else if matches!(kind, NodeKind::Flip) {
|
} else if matches!(kind, NodeKind::Flip) {
|
||||||
LazyBinding::Element
|
LazyBinding::Element
|
||||||
} else if matches!(input.shape.element, Element::Opaque) {
|
} else if matches!(input.shape.element, Element::Opaque) {
|
||||||
@@ -670,6 +675,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
ParsedFieldType::Node(_) => match (lazy_binding(node, index), raw) {
|
ParsedFieldType::Node(_) => match (lazy_binding(node, index), raw) {
|
||||||
(LazyBinding::DeriveRouting, _) => "derive-routing",
|
(LazyBinding::DeriveRouting, _) => "derive-routing",
|
||||||
|
(LazyBinding::DeriveCarrier, _) => "derive-carrier",
|
||||||
(LazyBinding::OpaqueRecord, _) => "opaque-record",
|
(LazyBinding::OpaqueRecord, _) => "opaque-record",
|
||||||
(LazyBinding::Element, true) => "flip-raw",
|
(LazyBinding::Element, true) => "flip-raw",
|
||||||
(LazyBinding::Element, false) => "flip-lazy",
|
(LazyBinding::Element, false) => "flip-lazy",
|
||||||
|
|||||||
@@ -97,21 +97,31 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let lazy_carrier = matches!(&crate::codegen::record_shape(parsed), Some(shape) if matches!(shape.carrier, crate::codegen::RecordCarrier::LazyToken));
|
||||||
let carrier_ty = match &carrier.ty {
|
let carrier_ty = match &carrier.ty {
|
||||||
ParsedFieldType::Regular(RegularParsedField { ty, lend: None, .. }) if !carrier.is_data_field => Some(ty),
|
ParsedFieldType::Regular(RegularParsedField { ty, lend: None, .. }) if !carrier.is_data_field => Some(ty),
|
||||||
|
ParsedFieldType::Node(NodeParsedField { output_type, .. }) if lazy_carrier => Some(output_type),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let Some(carrier_ty) = carrier_ty else {
|
let Some(carrier_ty) = carrier_ty else {
|
||||||
emit_error!(
|
emit_error!(
|
||||||
carrier.pat_ident.span(),
|
carrier.pat_ident.span(),
|
||||||
"a record node's primary input is an owned element, an unbounded passthrough generic, or `_: ()`; not `#[data]`, `&T`, or `impl Node`"
|
"a record node's primary input is an owned element, an unbounded passthrough generic, a lazy passthrough source, or `_: ()`; not `#[data]` or `&T`"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if lazy_carrier && !crate::codegen::ir::build(parsed).derives {
|
||||||
|
emit_error!(
|
||||||
|
parsed.input.pat_ident.span(),
|
||||||
|
"a lazy record carrier evaluates at derived contexts; spell `impl Ctx + DeriveCtx`"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let no_carrier = matches!(carrier_ty, Type::Tuple(tuple) if tuple.elems.is_empty());
|
let no_carrier = matches!(carrier_ty, Type::Tuple(tuple) if tuple.elems.is_empty());
|
||||||
let token = match (no_carrier, &carrier.ty) {
|
let token = match (no_carrier, &carrier.ty) {
|
||||||
(false, ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. })) if implementations.is_empty() => crate::codegen::unbounded_generic(parsed, ty),
|
(false, ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. })) if implementations.is_empty() => crate::codegen::unbounded_generic(parsed, ty),
|
||||||
|
(false, ParsedFieldType::Node(NodeParsedField { output_type, .. })) if lazy_carrier => crate::codegen::unbounded_generic(parsed, output_type),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let element = writes.as_ref().map(|writes| &writes.element).unwrap_or(&value);
|
let element = writes.as_ref().map(|writes| &writes.element).unwrap_or(&value);
|
||||||
@@ -179,20 +189,23 @@ fn validate_lazy_reads(parsed: &ParsedNodeFn) {
|
|||||||
if !crate::codegen::has_lazy_reads(parsed) {
|
if !crate::codegen::has_lazy_reads(parsed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if !crate::codegen::record_flip(parsed) {
|
let lazy_carrier = matches!(&crate::codegen::record_shape(parsed), Some(shape) if matches!(shape.carrier, crate::codegen::RecordCarrier::LazyToken));
|
||||||
|
if !crate::codegen::record_flip(parsed) && !lazy_carrier {
|
||||||
emit_error!(
|
emit_error!(
|
||||||
parsed.fn_name.span(),
|
parsed.fn_name.span(),
|
||||||
"attribute reads on a lazy input need the record lowering; routing, `plain`, shader, batch, and non-row-assignable generic nodes keep the plain one"
|
"attribute reads on a lazy input need the record lowering; routing, `plain`, shader, batch, and non-row-assignable generic nodes keep the plain one"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for field in &parsed.fields {
|
for (index, field) in parsed.fields.iter().enumerate() {
|
||||||
let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &field.ty else {
|
let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &field.ty else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if field.attribute_reads.is_empty() {
|
if field.attribute_reads.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if crate::codegen::unbounded_generic(parsed, output_type).is_some() {
|
// The lazy carrier forwards its token AND reads: the reads resolve
|
||||||
|
// against its wired layout, not the row type.
|
||||||
|
if crate::codegen::unbounded_generic(parsed, output_type).is_some() && !(lazy_carrier && index == 0) {
|
||||||
emit_error!(
|
emit_error!(
|
||||||
field.pat_ident.span(),
|
field.pat_ident.span(),
|
||||||
"an unbounded generic source forwards its whole record; attribute reads need a concrete output type"
|
"an unbounded generic source forwards its whole record; attribute reads need a concrete output type"
|
||||||
|
|||||||
@@ -100,6 +100,29 @@ fn repeat_extent(content: ExtentIn<'_>, count: ValueIn<'_, u32>, _reverse: Value
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test-only lazy-carrier creator: each copy evaluates the content at its own
|
||||||
|
/// index and re-scales the row's opacity by the copy number.
|
||||||
|
#[node_macro::node(category("Test"), extent(repeat_faded_extent))]
|
||||||
|
fn repeat_faded<T>(
|
||||||
|
ctx: impl Ctx + DeriveCtx + ExtractIndex,
|
||||||
|
content: impl Node<Context<'_>, Output = (T, Attr<Opacity>)>,
|
||||||
|
count: u32,
|
||||||
|
) -> Result<IList<(T, Attr<Opacity>)>, Interrupt> {
|
||||||
|
let spilled = ctx.index_head();
|
||||||
|
let copy = ctx.innermost_index() % count as u64;
|
||||||
|
let (element, opacity) = content.eval(&ctx.promoted(&spilled, copy))?;
|
||||||
|
Ok(emit(element, Attr(*opacity * (copy + 1) as f64)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pushed level's extent is the copy count; inner levels forward to the
|
||||||
|
/// content, whose extent is taken uniform across copies (queried at copy 0).
|
||||||
|
fn repeat_faded_extent(content: ExtentIn<'_>, count: ValueIn<'_, u32>, level: LevelIn) -> GPoll<Extent> {
|
||||||
|
match level.pushed() {
|
||||||
|
true => count.get().map(|count| Extent::Exactly(count as usize)),
|
||||||
|
false => content.at(level),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Test"))]
|
#[node_macro::node(category("Test"))]
|
||||||
fn source_opacity(_: impl Ctx, _: (), element: f64, opacity: f64) -> (f64, Attr<Opacity>) {
|
fn source_opacity(_: impl Ctx, _: (), element: f64, opacity: f64) -> (f64, Attr<Opacity>) {
|
||||||
(element, Attr(opacity))
|
(element, Attr(opacity))
|
||||||
@@ -472,6 +495,48 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lazy_carrier_reads_and_rewrites_the_attr_per_copy() {
|
||||||
|
let arena = Arena::new(1024).unwrap();
|
||||||
|
let generations = [];
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
|
let base = f64_layout(&[Opacity::NAME]);
|
||||||
|
let opacity_offset = base.offset_of(Opacity::NAME, 0).unwrap();
|
||||||
|
let content = RecordSourceNode {
|
||||||
|
layout: base.clone(),
|
||||||
|
element: 7.,
|
||||||
|
fields: vec![(opacity_offset, 0.5)],
|
||||||
|
partial: false,
|
||||||
|
};
|
||||||
|
reserve_for(&[&base]);
|
||||||
|
|
||||||
|
let node = install(
|
||||||
|
RepeatFadedNode::new(RecordSource::new(content, &base, &base), ValueNode(4u32), &base),
|
||||||
|
repeat_faded_layout_meta(),
|
||||||
|
&[Some(&base)],
|
||||||
|
);
|
||||||
|
let leveled = Node::<ContextImpl>::layout(&node).clone();
|
||||||
|
assert_eq!(leveled.depth, 1, "the IList return pushed one rank level above the content");
|
||||||
|
assert_eq!(node.extent_at(&ctx, 0), GPoll::Final(Extent::Exactly(4)));
|
||||||
|
|
||||||
|
let head = ctx.index_head();
|
||||||
|
for copy in 0..4u64 {
|
||||||
|
let mark = stack::sp();
|
||||||
|
let lane = ctx.promoted(&head, copy);
|
||||||
|
let GPoll::Final(value) = node.eval(&lane) else {
|
||||||
|
panic!("expected a final record");
|
||||||
|
};
|
||||||
|
let rec = leveled.rec(&value);
|
||||||
|
// The content row's element forwards; its opacity re-scales per copy.
|
||||||
|
assert_eq!(unsafe { rec.element::<f64>() }, 7.);
|
||||||
|
assert_eq!(unsafe { rec.read::<f64>(leveled.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5 * (copy + 1) as f64);
|
||||||
|
// SAFETY: the element and attr were read out above, so no borrow into this lane's frames remains.
|
||||||
|
unsafe { stack::rewind(mark) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reducer_folds_a_repeated_level() {
|
fn reducer_folds_a_repeated_level() {
|
||||||
let arena = Arena::new(1024).unwrap();
|
let arena = Arena::new(1024).unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user