Adopt tuple attribute io and the carrier lowering, deleting the bridge adapters for record-only wires

This commit is contained in:
Dennis Kobert
2026-08-06 18:48:16 +00:00
parent 166241b0e8
commit 9c7a633d5b
14 changed files with 1481 additions and 718 deletions

View File

@@ -14,6 +14,7 @@ use glam::{DAffine2, DVec2};
use std::any::TypeId;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::marker::PhantomData;
use std::ops::Deref;
use std::sync::{LazyLock, Mutex};
@@ -71,6 +72,23 @@ impl<'e, A: Attribute> std::fmt::Debug for Attr<'e, A> {
}
}
/// A deletion of `A` in a node's return tuple: the name leaves the output
/// layout, so downstream reads yield the declared default again. Functionally
/// a write of the default; the value carries nothing.
pub struct RemoveAttr<A: Attribute>(PhantomData<A>);
impl<A: Attribute> RemoveAttr<A> {
pub const fn new() -> Self {
RemoveAttr(PhantomData)
}
}
impl<A: Attribute> Default for RemoveAttr<A> {
fn default() -> Self {
Self::new()
}
}
/// A census row: what is known about one declared attribute name.
#[derive(Clone, Copy, Debug)]
pub struct AttributeInfo {

View File

@@ -186,6 +186,25 @@ impl Layout {
}
}
/// This layout minus the named fields, offsets recomputed. Removing an
/// absent name is a no-op: downstream reads yield the default either way.
pub fn without(&self, removes: &[(&str, u8)]) -> Layout {
let retained: Vec<FieldWrite> = self
.fields
.iter()
.filter(|field| !removes.contains(&(field.name, field.level)))
.map(|field| FieldWrite {
name: field.name,
level: field.level,
size: field.size,
align: field.align,
read_erased: field.read_erased,
repark: field.repark,
})
.collect();
Layout::default().with_writes(self.depth, self.element, &retained)
}
/// The union of several layouts over the same element and depth.
pub fn union(layouts: &[&Layout]) -> Layout {
let first = layouts.first().expect("a union needs at least one layout");
@@ -392,10 +411,18 @@ impl<'a, N> RecordEdgeInput<'a, N> {
/// The raw lazy edge handed to a poll kernel whose wire rides records while
/// the kernel consumes the plain element.
pub struct ElementEdge<'a, El, N> {
/// # Safety
/// `rec` must be a record of the layout the offsets were resolved against
/// and `El` its element type; both are proven at wiring.
unsafe fn element_only<El: Clone>(rec: Rec, _reads: &[Option<usize>]) -> El {
unsafe { read_element::<El>(rec) }
}
pub struct ElementEdge<'a, Out, N> {
node: &'a N,
layout: &'a Layout,
_marker: std::marker::PhantomData<fn() -> El>,
reads: &'a [Option<usize>],
read: unsafe fn(Rec, &[Option<usize>]) -> Out,
}
impl<'a, El: Clone, N> ElementEdge<'a, El, N> {
@@ -403,27 +430,38 @@ impl<'a, El: Clone, N> ElementEdge<'a, El, N> {
Self {
node,
layout,
_marker: std::marker::PhantomData,
reads: &[],
read: element_only::<El>,
}
}
}
pub fn eval<'d, C>(&self, ctx: &C) -> GPoll<El>
impl<'a, Out, N> ElementEdge<'a, Out, N> {
/// `read` must be sound against the layout the offsets in `reads` were
/// resolved from; the macro proves both at wiring.
pub fn with_reads(node: &'a N, layout: &'a Layout, reads: &'a [Option<usize>], read: unsafe fn(Rec, &[Option<usize>]) -> Out) -> Self {
Self { node, layout, reads, read }
}
pub fn eval<'d, C>(&self, ctx: &C) -> GPoll<Out>
where
N: Node<C, Output = RecordValue<'d>>,
{
self.node.eval(ctx).map(|value| unsafe { read_element::<El>(self.layout.rec(&value)) })
self.node.eval(ctx).map(|value| unsafe { (self.read)(self.layout.rec(&value), self.reads) })
}
}
/// The lazy input handed to a kernel whose edge rides a record wire while
/// the kernel consumes the plain element.
/// the kernel consumes the plain element, or the element beside its declared
/// attribute reads.
#[derive(Clone, Copy)]
pub struct ElementLazyInput<'a, El, N> {
pub struct ElementLazyInput<'a, Out, N> {
node: &'a N,
cell: &'a crate::node::StatusCell,
input_index: usize,
layout: &'a Layout,
_marker: std::marker::PhantomData<fn() -> El>,
reads: &'a [Option<usize>],
read: unsafe fn(Rec, &[Option<usize>]) -> Out,
}
impl<'a, El: Clone, N> ElementLazyInput<'a, El, N> {
@@ -433,16 +471,39 @@ impl<'a, El: Clone, N> ElementLazyInput<'a, El, N> {
cell,
input_index,
layout,
_marker: std::marker::PhantomData,
reads: &[],
read: element_only::<El>,
}
}
}
impl<'a, Out, N> ElementLazyInput<'a, Out, N> {
/// `read` must be sound against the layout the offsets in `reads` were
/// resolved from; the macro proves both at wiring.
pub fn with_reads(
node: &'a N,
cell: &'a crate::node::StatusCell,
input_index: usize,
layout: &'a Layout,
reads: &'a [Option<usize>],
read: unsafe fn(Rec, &[Option<usize>]) -> Out,
) -> Self {
Self {
node,
cell,
input_index,
layout,
reads,
read,
}
}
pub fn eval<'d, C>(&self, ctx: &C) -> Result<El, crate::gpoll::Interrupt>
pub fn eval<'d, C>(&self, ctx: &C) -> Result<Out, crate::gpoll::Interrupt>
where
N: Node<C, Output = RecordValue<'d>>,
{
let value = self.cell.eval_input(self.input_index, self.node, ctx)?;
Ok(unsafe { read_element::<El>(self.layout.rec(&value)) })
Ok(unsafe { (self.read)(self.layout.rec(&value), self.reads) })
}
}
@@ -563,8 +624,10 @@ pub mod stack {
/// Field-by-field carry from `from`'s layout into `to`'s, computed at
/// wiring. The element copy is included when `carry_element` holds, which is
/// exactly when the node does not write a concrete element itself.
pub fn copy_plan(from: &Layout, to: &Layout, carry_element: bool) -> Vec<(usize, usize, usize)> {
/// exactly when the node does not write a concrete element itself. `removes`
/// names the fields the node deletes, which are exactly the ones allowed to
/// be absent from `to`.
pub fn copy_plan(from: &Layout, to: &Layout, carry_element: bool, removes: &[(&str, u8)]) -> Vec<(usize, usize, usize)> {
let mut plan = Vec::new();
if carry_element {
assert_eq!(from.element.size, to.element.size, "a carried element must keep its size");
@@ -573,6 +636,9 @@ pub fn copy_plan(from: &Layout, to: &Layout, carry_element: bool) -> Vec<(usize,
}
}
for field in &from.fields {
if removes.contains(&(field.name, field.level)) {
continue;
}
let target = to.offset_of(field.name, field.level).expect("carried field missing from the output layout");
plan.push((field.offset, target, field.size));
}
@@ -586,6 +652,53 @@ pub unsafe fn write_field<T>(dst: *mut u8, offset: usize, value: T) {
unsafe { dst.add(offset).cast::<T>().write(value) }
}
/// Finishes a carried record frame: the element lands beside the fields
/// already carried into `dst`, inline frames copy out of the scratch bytes,
/// and the frame releases in every branch, so the frame lifecycle closes
/// here. Arena exhaustion of a parked element reports as an error poll.
///
/// # Safety
/// `dst` must be the claimed frame (or inline scratch when `frame_bytes` is
/// 0) of a record whose element is `T` and whose frame size is `frame_bytes`,
/// with every carried field already written.
pub unsafe fn lift_poll_into<'e, T: Send + Sync + 'static>(poll: GPoll<T>, dst: *mut u8, frame_bytes: usize, arena: &'e crate::arena::Arena) -> GPoll<RecordValue<'e>> {
let release = || {
if frame_bytes != 0 {
stack::pop(dst);
}
};
let build = |element: T| {
let written = unsafe { write_element(dst, element, arena) };
release();
written.map(|()| match frame_bytes {
0 => unsafe { dst.cast::<RecordValue>().read() },
_ => RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) }),
})
};
let exhausted = || {
GPoll::Error(Box::new(crate::gpoll::GraphError {
kind: crate::gpoll::ErrorKind::ArenaExhausted,
trace: Vec::new(),
}))
};
match poll {
GPoll::Final(element) => build(element).map_or_else(exhausted, GPoll::Final),
GPoll::Partial(element) => build(element).map_or_else(exhausted, GPoll::Partial),
GPoll::Fallback(boxed) => {
let (element, error) = *boxed;
build(element).map_or_else(exhausted, |value| GPoll::Fallback(Box::new((value, error))))
}
GPoll::Pending => {
release();
GPoll::Pending
}
GPoll::Error(error) => {
release();
GPoll::Error(error)
}
}
}
/// Whether elements of `T` move once into the arena and ride as references:
/// records byte-copy their contents and never run drop glue, so a type is
/// byte-carried exactly when it has none.
@@ -637,6 +750,28 @@ pub unsafe fn read_element<T: Clone>(rec: Rec) -> T {
unsafe { borrow_element::<T>(rec) }.clone()
}
/// Borrows a record's element for the rest of the evaluation. Parked elements
/// borrow the arena, inline elements borrow their record value in place, and
/// a byte-carried spilled element copies into the arena first: its stack
/// region dies with the next push, and a borrow taken before the consumer's
/// own frame push always has one coming. `None` reports arena exhaustion.
///
/// # Safety
/// The record's element must be a `T` in the form [`element_parked`] picks,
/// and for inline layouts the record value must outlive the borrow.
pub unsafe fn borrow_or_park<'e, T: Send + Sync + 'static>(rec: Rec, layout: &Layout, arena: &'e crate::arena::Arena) -> Option<&'e T> {
if element_parked::<T>() {
return Some(unsafe { rec.element::<&T>() });
}
if layout.is_inline() {
return Some(unsafe { &*rec.ptr().cast::<T>() });
}
// A bitwise read duplicates soundly: byte-carried elements have no drop
// glue.
let value = unsafe { rec.ptr().cast::<T>().read() };
arena.alloc(value).map(|(parked, _)| parked)
}
/// # Safety
/// `dst` must be fresh element storage of a record whose element is `T`.
/// `None` reports arena exhaustion for a parked element.
@@ -692,7 +827,7 @@ impl SourcePlan {
if source == union {
return None;
}
let moves = copy_plan(source, union, true);
let moves = copy_plan(source, union, true, &[]);
let fills = union
.fields
.iter()
@@ -870,8 +1005,10 @@ impl OwnedRecord {
}
}
/// Lifts a plain producer onto a record wire: the element lands at offset 0
/// of a fresh element-only record, parked when it carries drop glue.
/// Law-test scaffolding: wraps an arbitrary plain node onto a record wire
/// (the element lands at offset 0 of a fresh element-only record, parked when
/// it carries drop glue). No production path constructs one; value edges are
/// [`crate::value::ValueSource`].
pub struct RecordLift<El, N> {
edge: N,
layout: Layout,
@@ -905,8 +1042,9 @@ where
}
}
/// Extracts the element from a record wire for a plain consumer, cloning out
/// of the parked reference when the element carries drop glue.
/// Law-test scaffolding: a plain probe over a record wire, cloning the
/// element out of the parked reference when it carries drop glue. No
/// production path constructs one.
pub struct RecordExtract<El, N> {
edge: N,
layout: Layout,

View File

@@ -267,49 +267,6 @@ pub struct RegistryEntry {
pub constructor: NodeConstructor,
}
/// The bridge rows of `T`: a plain producer onto a record wire and a record
/// wire into a plain consumer. One pair exists per wire type while the
/// deferred plain classes (shader, batch) coexist with record wires.
pub fn record_bridge_rows<T: Clone + Send + Sync + 'static>() -> [(crate::ProtoNodeIdentifier, RegistryEntry); 2] {
[
(crate::ProtoNodeIdentifier::new("core_types::record::RecordLiftNode"), record_lift_entry::<T>()),
(crate::ProtoNodeIdentifier::new("core_types::record::RecordExtractNode"), record_extract_entry::<T>()),
]
}
/// The lift bridge row for `T`: a plain producer onto a record wire. One
/// exists per wire type while plain and record worlds coexist.
pub fn record_lift_entry<T: Clone + Send + Sync + 'static>() -> RegistryEntry {
RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), record_type::<T>(), vec![edge_type::<T>()]),
constructor: |inputs| {
if inputs.len() != 1 {
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
}
let mut inputs = inputs.into_iter();
let node = crate::record::RecordLift::<T, _>::new(inputs.next().unwrap().downcast::<T>()?);
Ok(EdgeHandle::new_record::<T>(std::sync::Arc::new(node) as std::sync::Arc<ErasedRecordNode>))
},
}
}
/// The extract bridge row for `T`: a record wire into a plain consumer.
pub fn record_extract_entry<T: Clone + Send + Sync + 'static>() -> RegistryEntry {
RegistryEntry {
io: NodeIOTypes::new(concrete!(Context), concrete!(T), vec![record_edge_type::<T>()]),
constructor: |inputs| {
if inputs.len() != 1 {
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
}
let mut inputs = inputs.into_iter();
let edge = inputs.next().unwrap();
let layout = edge.layout().ok_or(ConstructionError::MissingLayout)?.clone();
let node = crate::record::RecordExtract::<T, _>::new(edge.downcast_record::<T>()?, &layout);
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<T>>))
},
}
}
pub fn construct(entry: &RegistryEntry, inputs: Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError> {
if inputs.len() != entry.io.inputs.len() {
return Err(ConstructionError::Arity {

View File

@@ -13,11 +13,41 @@ pub fn value_edge<T: Clone + crate::WasmNotSend + crate::WasmNotSync + 'static>(
crate::registry::EdgeHandle::new(std::sync::Arc::new(ClonedNode(value)) as std::sync::Arc<crate::registry::ErasedNode<T>>)
}
/// The native record edge of a constant: the element lifts onto the record
/// wire per evaluation, so value sources need no spliced adapter.
/// The node behind every value edge: clones its constant onto the record
/// wire per evaluation.
pub struct ValueSource<T> {
value: T,
layout: crate::record::Layout,
}
impl<T: Clone + Send + Sync + 'static> ValueSource<T> {
pub fn new(value: T) -> Self {
Self {
value,
layout: crate::record::Layout::default().with_writes(0, crate::record::element_write::<T>(), &[]),
}
}
}
impl<'e, C, T> crate::node::Node<C> for ValueSource<T>
where
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
T: Clone + Send + Sync + 'static,
{
type Output = crate::record::RecordValue<'e>;
fn eval(&self, input: &C) -> crate::gpoll::GPoll<crate::record::RecordValue<'e>> {
crate::record::lift_poll(crate::gpoll::GPoll::Final(self.value.clone()), &self.layout, input.arena())
}
fn layout(&self) -> Option<&crate::record::Layout> {
Some(&self.layout)
}
}
/// The native record edge of a constant.
pub fn record_value_edge<T: Clone + Send + Sync + 'static>(value: T) -> crate::registry::EdgeHandle {
let node = crate::record::RecordLift::<T, _>::new(ClonedNode(value));
crate::registry::EdgeHandle::new_record::<T>(std::sync::Arc::new(node) as std::sync::Arc<crate::registry::ErasedRecordNode>)
crate::registry::EdgeHandle::new_record::<T>(std::sync::Arc::new(ValueSource::new(value)) as std::sync::Arc<crate::registry::ErasedRecordNode>)
}
impl<T: Clone> ClonedNode<T> {