Drop the list driver, make attribute values Copy over arena references, and land record-tier offset io on a per-thread record stack

This commit is contained in:
Dennis Kobert
2026-08-05 09:47:03 +00:00
parent 6154e4d219
commit abe2a565d1
10 changed files with 1003 additions and 545 deletions

View File

@@ -3,6 +3,11 @@
//! collects every declaration so name resolution, defaults, and diagnostics
//! run at graph compile time. One name belongs to one marker, so a name can
//! never mean two different types.
//!
//! Values are `Copy` and pack directly into record fields. Data with drop
//! glue rides the arena instead: the marker declares a reference value
//! (`&str`), the writing kernel parks the payload in the arena, and the
//! record field carries the eval-lifetime reference.
use crate::list::AnyAttributeValue;
use glam::{DAffine2, DVec2};
@@ -18,11 +23,15 @@ use std::sync::{LazyLock, Mutex};
pub trait Attribute: 'static {
/// The name as it appears in documents and diagnostics.
const NAME: &'static str;
/// The value type every read and write of this name shares.
type Value: AnyAttributeValue + Clone + Default + std::fmt::Debug;
/// The value type every read and write of this name shares. The lifetime
/// is the evaluation the value flows in; non-reference values ignore it.
type Value<'e>: Copy + Default + std::fmt::Debug;
/// The name-specific default, filled where an item lacks the attribute.
fn default() -> Self::Value {
Self::Value::default()
/// Producing a value for any `'e` from no inputs, reference defaults can
/// only point at `'static` data, which is what lets the census fill them
/// as plain bytes.
fn default<'e>() -> Self::Value<'e> {
Default::default()
}
}
@@ -30,23 +39,25 @@ pub trait Attribute: 'static {
/// (yielding the declared default where the attribute is absent upstream), an
/// `Attr<A>` in the return tuple is a write, and the same marker on both
/// sides is a modify.
pub struct Attr<A: Attribute>(pub A::Value);
pub struct Attr<'e, A: Attribute>(pub A::Value<'e>);
impl<A: Attribute> Deref for Attr<A> {
type Target = A::Value;
impl<'e, A: Attribute> Deref for Attr<'e, A> {
type Target = A::Value<'e>;
fn deref(&self) -> &A::Value {
fn deref(&self) -> &A::Value<'e> {
&self.0
}
}
impl<A: Attribute> Clone for Attr<A> {
impl<'e, A: Attribute> Clone for Attr<'e, A> {
fn clone(&self) -> Self {
Attr(self.0.clone())
*self
}
}
impl<A: Attribute> std::fmt::Debug for Attr<A> {
impl<'e, A: Attribute> Copy for Attr<'e, A> {}
impl<'e, A: Attribute> std::fmt::Debug for Attr<'e, A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple(A::NAME).field(&self.0).finish()
}
@@ -61,20 +72,14 @@ pub struct AttributeInfo {
pub default: fn() -> Box<dyn AnyAttributeValue>,
pub size: usize,
pub align: usize,
/// Whether the value is eligible for packed-record fields: no drop glue,
/// so its bytes copy freely. Droppable values stay on the erased path
/// until the per-type clone/drop glue lands.
pub packable: bool,
/// Writes the declared default's bytes into a `size`-long slice.
/// Meaningful only when `packable`.
pub write_default_bytes: fn(&mut [u8]),
}
fn write_default_bytes<A: Attribute>(out: &mut [u8]) {
assert!(!std::mem::needs_drop::<A::Value>(), "default bytes exist only for packable values");
assert_eq!(out.len(), size_of::<A::Value>());
let value = A::default();
unsafe { std::ptr::copy_nonoverlapping((&raw const value).cast::<u8>(), out.as_mut_ptr(), size_of::<A::Value>()) };
assert_eq!(out.len(), size_of::<A::Value<'static>>());
let value: A::Value<'static> = A::default();
unsafe { std::ptr::copy_nonoverlapping((&raw const value).cast::<u8>(), out.as_mut_ptr(), size_of::<A::Value<'static>>()) };
}
/// All declared attribute names, keyed by name.
@@ -84,15 +89,17 @@ pub static ATTRIBUTE_REGISTRY: LazyLock<Mutex<HashMap<&'static str, AttributeInf
/// startup (ctor natively, a `__node_registry_attribute_*` export on wasm).
/// Re-registration at the same value type is idempotent; a second marker
/// claiming the name at a different value type panics.
pub fn register<A: Attribute>() {
pub fn register<A: Attribute>()
where
A::Value<'static>: AnyAttributeValue,
{
let info = AttributeInfo {
name: A::NAME,
value_type: TypeId::of::<A::Value>(),
value_type_name: std::any::type_name::<A::Value>(),
value_type: TypeId::of::<A::Value<'static>>(),
value_type_name: std::any::type_name::<A::Value<'static>>(),
default: || Box::new(A::default()),
size: size_of::<A::Value>(),
align: align_of::<A::Value>(),
packable: !std::mem::needs_drop::<A::Value>(),
size: size_of::<A::Value<'static>>(),
align: align_of::<A::Value<'static>>(),
write_default_bytes: write_default_bytes::<A>,
};
let conflict = match ATTRIBUTE_REGISTRY.lock().unwrap().entry(A::NAME) {
@@ -124,44 +131,65 @@ pub fn default_value(name: &str) -> Option<Box<dyn AnyAttributeValue>> {
/// core_types::attribute! {
/// /// How visible the content is.
/// pub Opacity("opacity"): f64 = 1.;
/// /// The item's transformation.
/// pub Transform("transform"): glam::DAffine2;
/// /// The item's label, parked in the arena by the writer.
/// pub Label("label"): &str;
/// }
/// ```
///
/// The trailing `= expr` is the name-specific default; without it the value
/// type's `Default` applies.
/// type's `Default` applies. A `&T` value carries the eval lifetime, so its
/// default must be `'static` data.
#[macro_export]
macro_rules! attribute {
($($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): $value:ty $(= $default:expr)?;)+) => {
$(
$(#[$meta])*
$vis struct $marker;
() => {};
($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): &$value:ty $(= $default:expr)?; $($rest:tt)*) => {
$(#[$meta])*
$vis struct $marker;
impl $crate::attribute::Attribute for $marker {
const NAME: &'static str = $name;
type Value = $value;
$(
fn default() -> $value {
$default
}
)?
impl $crate::attribute::Attribute for $marker {
const NAME: &'static str = $name;
type Value<'e> = &'e $value;
$(
fn default<'e>() -> Self::Value<'e> {
$default
}
)?
}
$crate::attribute!(@register $marker);
$crate::attribute!($($rest)*);
};
($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): $value:ty $(= $default:expr)?; $($rest:tt)*) => {
$(#[$meta])*
$vis struct $marker;
impl $crate::attribute::Attribute for $marker {
const NAME: &'static str = $name;
type Value<'e> = $value;
$(
fn default<'e>() -> Self::Value<'e> {
$default
}
)?
}
$crate::attribute!(@register $marker);
$crate::attribute!($($rest)*);
};
(@register $marker:ident) => {
const _: () = {
#[cfg(not(target_family = "wasm"))]
#[$crate::ctor::ctor]
fn register() {
$crate::attribute::register::<$marker>();
}
const _: () = {
#[cfg(not(target_family = "wasm"))]
#[$crate::ctor::ctor]
fn register() {
$crate::attribute::register::<$marker>();
}
#[cfg(target_family = "wasm")]
#[unsafe(export_name = concat!("__node_registry_attribute_", stringify!($marker)))]
extern "C" fn register() {
$crate::attribute::register::<$marker>();
}
};
)+
#[cfg(target_family = "wasm")]
#[unsafe(export_name = concat!("__node_registry_attribute_", stringify!($marker)))]
extern "C" fn register() {
$crate::attribute::register::<$marker>();
}
};
};
}
@@ -180,7 +208,7 @@ attribute! {
/// Artboard's top-left corner in document coordinates.
pub Location("location"): DVec2;
/// A regex named-capture-group's name, or empty for unnamed groups.
pub Name("name"): String;
pub Name("name"): &str;
}
#[cfg(test)]
@@ -198,7 +226,7 @@ mod tests {
#[test]
fn name_specific_default_overrides_the_type_default() {
assert_eq!(<Opacity as Attribute>::default(), 1.);
assert_eq!(<Name as Attribute>::default(), String::new());
assert_eq!(<Name as Attribute>::default(), "");
}
#[test]
@@ -207,6 +235,13 @@ mod tests {
assert_eq!(*value.as_any().downcast_ref::<f64>().unwrap(), 1.);
}
#[test]
fn reference_values_register_at_the_static_instantiation() {
let row = info("name").unwrap();
assert_eq!(row.value_type, TypeId::of::<&'static str>());
assert_eq!(row.size, size_of::<&str>());
}
#[test]
fn reregistration_at_the_same_type_is_idempotent() {
register::<Opacity>();
@@ -220,7 +255,7 @@ mod tests {
struct Conflict;
impl Attribute for Conflict {
const NAME: &'static str = "opacity";
type Value = bool;
type Value<'e> = bool;
}
register::<Conflict>();
}

View File

@@ -624,7 +624,6 @@ pub struct EvalScope<'a> {
pointer_position: Option<DVec2>,
generations: &'a [(SourceId, u64)],
arena: &'a Arena,
frame: Option<&'a crate::record::Frame>,
hash: u64,
}
@@ -636,19 +635,12 @@ impl<'a> EvalScope<'a> {
pointer_position,
generations,
arena,
frame: None,
hash: 0,
};
scope.hash = scope.compute_hash(|_| true);
scope
}
/// Attaches the record frame. Operational like the arena: not part of the
/// scope hash.
pub fn with_frame(&self, frame: &'a crate::record::Frame) -> EvalScope<'a> {
EvalScope { frame: Some(frame), ..*self }
}
pub fn with_real_time(&self, real_time: Option<f64>) -> EvalScope<'a> {
let mut scope = EvalScope { real_time, ..*self };
scope.hash = scope.compute_hash(|_| true);
@@ -708,23 +700,6 @@ impl<'a> EvalScope<'a> {
pub fn arena(&self) -> &'a Arena {
self.arena
}
pub fn frame(&self) -> Option<&'a crate::record::Frame> {
self.frame
}
}
/// Read access to the record frame, the operational sibling of
/// [`ExtractArena`]. `None` on contexts whose scope carries no frame (a
/// graph without record edges allocates none).
pub trait ExtractFrame<'e> {
fn frame(&self) -> Option<&'e crate::record::Frame>;
}
impl<'a> ExtractFrame<'a> for ContextImpl<'a> {
fn frame(&self) -> Option<&'a crate::record::Frame> {
self.scope.frame()
}
}
pub trait ExtractArena {

View File

@@ -954,15 +954,6 @@ impl<T> List<T> {
}
}
/// Creates a list from element values with no attributes.
pub fn from_element_values(element: Vec<T>) -> Self {
let len = element.len();
Self {
element,
attributes: Attributes::with_len(len),
}
}
/// Creates a list containing a single item from the given [`Item`], preserving its attributes.
pub fn new_from_item(item: Item<T>) -> Self {
let mut attributes = Attributes::new();
@@ -1009,11 +1000,6 @@ impl<T> List<T> {
self.element.iter()
}
/// Consumes the list, returning its element values and dropping its attributes.
pub fn into_element_values(self) -> Vec<T> {
self.element
}
/// Returns an iterator over mutable references to all element values.
pub fn iter_element_values_mut(&mut self) -> std::slice::IterMut<'_, T> {
self.element.iter_mut()
@@ -1101,20 +1087,6 @@ impl<T> List<T> {
}
}
/// Removes and returns the attribute column for the given key, if present.
pub fn take_attribute_dyn(&mut self, key: &str) -> Option<AttributeDyn> {
let position = self.attributes.attributes.iter().position(|(k, _)| k == key)?;
Some(AttributeDyn(self.attributes.attributes.remove(position).1))
}
/// Replaces (or adds) an attribute, taking ownership of the column, whose length must equal this list's item count.
pub fn insert_attribute_dyn(&mut self, key: impl Into<String>, column: AttributeDyn) {
assert_eq!(column.len(), self.element.len(), "attribute column length must match the list's item count");
let key = key.into();
self.attributes.attributes.retain(|(k, _)| k != &key);
self.attributes.attributes.push((key, column.0));
}
/// Removes the entire attribute for the given key, if present.
pub fn remove_attribute(&mut self, key: &str) {
self.attributes.remove_attribute(key);

View File

@@ -71,6 +71,14 @@ pub trait Node<Input> {
None
}
/// The record layout of this node's output; `None` for element-only
/// producers. Consumers read their carrier's layout through this at
/// wiring, and the wiring layer derives stack sizing from the same
/// layouts, in the dynamic executor and exported source alike.
fn layout(&self) -> Option<&crate::record::Layout> {
None
}
fn eval_batch<'a>(&self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'a, Self::Output>
where
Input: InjectIndex + Copy,
@@ -133,6 +141,10 @@ where
(**self).serialize()
}
fn layout(&self) -> Option<&crate::record::Layout> {
(**self).layout()
}
fn eval_batch<'a>(&self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'a, Self::Output>
where
Input: InjectIndex + Copy,
@@ -159,6 +171,10 @@ where
(**self).serialize()
}
fn layout(&self) -> Option<&crate::record::Layout> {
(**self).layout()
}
fn eval_batch<'a>(&self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'a, Self::Output>
where
Input: InjectIndex + Copy,

View File

@@ -1,15 +1,14 @@
//! The packed-record tier at rank 0. A record is the element at offset 0
//! plus one field per written attribute; its [`Layout`] is computed at
//! wiring from the upstream write set and never serialized. Records live as
//! per-lane views in a per-worker [`Frame`] whose slots are assigned at
//! wiring, and kernels route them as opaque [`RecordValue`]s that carry
//! per-lane views on the per-thread record [`stack`], claimed per
//! evaluation, and kernels route them as opaque [`RecordValue`]s that carry
//! their provenance. Only generated or wiring code touches offsets, so a
//! safe kernel cannot misalign a field.
use crate::attribute;
use crate::gpoll::GPoll;
use crate::node::Node;
use std::cell::UnsafeCell;
/// One field of a [`Layout`]: a (name, level) key resolved to an offset.
/// Levels are numbered innermost-out; only level 0 exists at rank 0.
@@ -97,6 +96,13 @@ impl Layout {
}
}
/// The stand-in fed to a kernel's unbounded `element: T` parameter. The type
/// system forces the kernel to route it to the element position of its return
/// tuple, so the passthrough is explicit in the signature while the lowering
/// carries the element bytes untyped through the copy plan.
#[derive(Clone, Copy, Debug, Default)]
pub struct ElToken;
/// A view of one record: a pointer whose layout is proven at wiring.
#[derive(Clone, Copy, Debug)]
pub struct Rec(*const u8);
@@ -148,58 +154,89 @@ impl<'e> RecordValue<'e> {
}
}
/// Assigns frame slots at wiring: a bump allocator over slot sizes, aligned
/// to at most 8 (record layouts never exceed word alignment).
#[derive(Debug, Default)]
pub struct FrameLayout {
size: usize,
}
/// The per-thread record stack: every record evaluation claims its activation
/// 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
/// exists. Thread-local by construction, so access is single-threaded without
/// claims or gates. Records are overwritten per lane and never touch the
/// arena.
pub mod stack {
use std::cell::Cell;
impl FrameLayout {
pub fn slot(&mut self, layout: &Layout) -> usize {
assert!(layout.align <= 8, "record layouts align to at most 8");
self.size = self.size.next_multiple_of(layout.align.max(1));
let offset = self.size;
self.size += layout.size;
offset
struct Stack {
base: Cell<*mut u8>,
capacity: Cell<usize>,
sp: Cell<usize>,
}
pub fn size(&self) -> usize {
self.size
}
}
/// The per-worker record frame: every record-producing slot lives at a
/// wiring-assigned offset. Slots are overwritten per lane and never touch
/// the arena.
pub struct Frame {
words: Box<[UnsafeCell<u64>]>,
}
// SAFETY: a frame belongs to one worker; slot writes happen only inside that
// worker's evaluation, and wiring assigns disjoint offsets per slot.
unsafe impl Send for Frame {}
unsafe impl Sync for Frame {}
impl Frame {
pub fn new(size: usize) -> Self {
Self {
words: (0..size.div_ceil(8).max(1)).map(|_| UnsafeCell::new(0)).collect(),
impl Stack {
fn free(&self) {
let base = self.base.get();
if !base.is_null() {
drop(unsafe { Vec::from_raw_parts(base.cast::<u64>(), 0, self.capacity.get() / 8) });
}
}
}
/// # Safety
/// `offset` must be a slot offset assigned by [`FrameLayout`] for this
/// frame, and the caller must be the slot's owning node evaluation.
pub unsafe fn slot(&self, offset: usize) -> *mut u8 {
debug_assert!(offset <= self.words.len() * 8);
unsafe { self.words.as_ptr().cast::<u8>().cast_mut().add(offset) }
impl Drop for Stack {
fn drop(&mut self) {
self.free();
}
}
}
impl std::fmt::Debug for Frame {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Frame").field("bytes", &(self.words.len() * 8)).finish()
thread_local! {
static STACK: Stack = const {
Stack {
base: Cell::new(std::ptr::null_mut()),
capacity: Cell::new(0),
sp: Cell::new(0),
}
};
}
/// Ensures the calling thread's stack holds `bytes`, the root's wiring-
/// derived stack need, and resets the stack pointer. Called only between
/// evaluations, like the arena reset: nothing survives it, so frames
/// leaked by an interrupted evaluation are reclaimed here.
pub fn reserve(bytes: usize) {
STACK.with(|stack| {
stack.sp.set(0);
if stack.capacity.get() >= bytes {
return;
}
let words = bytes.div_ceil(8).max(1);
let mut memory = vec![0u64; words];
let base = memory.as_mut_ptr().cast::<u8>();
std::mem::forget(memory);
stack.free();
stack.base.set(base);
stack.capacity.set(words * 8);
});
}
/// Claims `bytes` (rounded to word alignment) at the stack pointer and
/// advances past them. The region stays claimed until [`pop`], and stays
/// readable until the next `push`. `reserve` derived from the root's
/// stack need makes the capacity bound exact, so overflow is a debug
/// assertion, not a hot-path branch.
pub fn push(bytes: usize) -> *mut u8 {
STACK.with(|stack| {
let sp = stack.sp.get();
let next = sp + bytes.next_multiple_of(8);
debug_assert!(next <= stack.capacity.get(), "record stack overflow: reserve() must cover the root's stack need");
stack.sp.set(next);
unsafe { stack.base.get().add(sp) }
})
}
/// Returns the stack pointer to `frame`, a pointer earlier returned by
/// [`push`] on this thread, releasing everything above it.
pub fn pop(frame: *mut u8) {
STACK.with(|stack| {
let offset = frame as usize - stack.base.get() as usize;
debug_assert!(offset <= stack.sp.get(), "pop target must lie within the claimed stack");
stack.sp.set(offset);
});
}
}
@@ -221,6 +258,13 @@ pub fn copy_plan(from: &Layout, to: &Layout, carry_element: bool) -> Vec<(usize,
plan
}
/// # Safety
/// `offset` must be a field offset of the layout of the record under
/// construction at `dst` and `T` the field's type; both are proven at wiring.
pub unsafe fn write_field<T>(dst: *mut u8, offset: usize, value: T) {
unsafe { dst.add(offset).cast::<T>().write(value) }
}
/// # Safety
/// `src` must be a record of the plan's source layout and `dst` a buffer of
/// the plan's target layout; both are proven at wiring.
@@ -235,7 +279,6 @@ pub unsafe fn apply_plan(src: Rec, dst: *mut u8, plan: &[(usize, usize, usize)])
fn default_fill_bytes(name: &str, size: usize) -> Box<[u8]> {
let mut bytes = vec![0u8; size].into_boxed_slice();
if let Some(info) = attribute::info(name)
&& info.packable
&& info.size == size
{
(info.write_default_bytes)(&mut bytes);
@@ -244,18 +287,18 @@ fn default_fill_bytes(name: &str, size: usize) -> Box<[u8]> {
}
/// A routing source's wiring-resolved translation: field moves into the
/// consumer's frame buffer plus census default fill for union fields the
/// source lacks. Absent when the source's layout already equals the union,
/// in which case the record pointer forwards untouched.
/// union layout plus census default fill for union fields the source lacks.
/// Absent when the source's layout already equals the union, in which case
/// the record pointer forwards untouched.
#[derive(Debug)]
pub struct SourcePlan {
moves: Vec<(usize, usize, usize)>,
fills: Vec<(usize, Box<[u8]>)>,
slot: usize,
union_bytes: usize,
}
impl SourcePlan {
pub fn new(source: &Layout, union: &Layout, slot: usize) -> Option<SourcePlan> {
pub fn new(source: &Layout, union: &Layout) -> Option<SourcePlan> {
if source == union {
return None;
}
@@ -266,15 +309,18 @@ impl SourcePlan {
.filter(|field| source.offset_of(field.name, field.level).is_none())
.map(|field| (field.offset, default_fill_bytes(field.name, field.size)))
.collect();
Some(SourcePlan { moves, fills, slot })
Some(SourcePlan {
moves,
fills,
union_bytes: union.size,
})
}
/// # Safety
/// `src` must be a record of this plan's source layout and `frame` the
/// frame whose slot was assigned to this plan at wiring.
pub unsafe fn translate(&self, src: Rec, frame: &Frame) -> Rec {
/// `src` must be a record of this plan's source layout and `dst` a
/// buffer of the plan's union layout.
pub unsafe fn translate(&self, src: Rec, dst: *mut u8) -> Rec {
unsafe {
let dst = frame.slot(self.slot);
apply_plan(src, dst, &self.moves);
for (offset, bytes) in &self.fills {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst.add(*offset), bytes.len());
@@ -288,36 +334,37 @@ impl SourcePlan {
/// Evaluating it yields the source's record translated to the union layout
/// (or forwarded untouched when the layouts already agree), so the kernel
/// holds and returns record values without ever seeing the representation.
/// A translation claims its landing region from the record stack without
/// popping, so the value survives sibling evaluations; the region is
/// released with the enclosing frame, which bounds claims at one per source
/// evaluation the kernel performs.
pub struct RecordSource<N> {
edge: N,
plan: Option<SourcePlan>,
}
impl<N> RecordSource<N> {
pub fn wire(edge: N, source: &Layout, union: &Layout, slot: usize) -> Self {
pub fn wire(edge: N, source: &Layout, union: &Layout) -> Self {
Self {
edge,
plan: SourcePlan::new(source, union, slot),
plan: SourcePlan::new(source, union),
}
}
}
impl<'e, C, N> Node<C> for RecordSource<N>
where
C: crate::context::ExtractFrame<'e>,
N: Node<C, Output = RecordValue<'e>>,
{
type Output = RecordValue<'e>;
fn eval(&self, input: &C) -> GPoll<RecordValue<'e>> {
let value = self.edge.eval(input);
match &self.plan {
None => value,
None => self.edge.eval(input),
Some(plan) => {
let Some(frame) = crate::context::ExtractFrame::frame(input) else {
return GPoll::error("record frame missing");
};
value.map(|value| RecordValue::from_rec(unsafe { plan.translate(value.rec(), frame) }))
let dst = stack::push(plan.union_bytes);
let value = self.edge.eval(input);
value.map(|value| RecordValue::from_rec(unsafe { plan.translate(value.rec(), dst) }))
}
}
}
@@ -356,27 +403,15 @@ mod tests {
assert!(Layout::union(&[&a, &b]).offset_of("length", 0).is_some());
}
#[test]
fn frame_slots_bump_aligned() {
let flag = Layout::default().with_writes(0, (1, 1), &[]);
let wide = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]);
let mut frame = FrameLayout::default();
assert_eq!(frame.slot(&flag), 0);
assert_eq!(frame.slot(&wide), 8);
assert_eq!(frame.size(), 24);
}
#[test]
fn translation_moves_fields_and_fills_census_defaults() {
let source = Layout::default().with_writes(0, (8, 8), &[f64_field("length")]);
let union = Layout::union(&[&source, &Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")])]);
let mut frame_layout = FrameLayout::default();
let slot = frame_layout.slot(&union);
let frame = Frame::new(frame_layout.size());
let plan = SourcePlan::new(&source, &union, slot).unwrap();
let plan = SourcePlan::new(&source, &union).unwrap();
let record = [5f64, 7f64];
let translated = unsafe { plan.translate(Rec::new(record.as_ptr().cast()), &frame) };
let mut buffer = vec![0u64; union.size.div_ceil(8)];
let translated = unsafe { plan.translate(Rec::new(record.as_ptr().cast()), buffer.as_mut_ptr().cast()) };
assert_eq!(unsafe { translated.element::<f64>() }, 5.);
assert_eq!(unsafe { translated.read::<f64>(union.offset_of("length", 0).unwrap()) }, 7.);
assert_eq!(unsafe { translated.read::<f64>(union.offset_of("opacity", 0).unwrap()) }, 1.);
@@ -385,6 +420,45 @@ mod tests {
#[test]
fn identity_layouts_forward() {
let layout = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]);
assert!(SourcePlan::new(&layout, &layout.clone(), 0).is_none());
assert!(SourcePlan::new(&layout, &layout.clone()).is_none());
}
#[test]
fn stack_frames_nest_and_release() {
stack::reserve(64);
let outer = stack::push(24);
let inner = stack::push(8);
assert_eq!(inner as usize - outer as usize, 24);
stack::pop(outer);
assert_eq!(stack::push(8), outer);
stack::pop(outer);
}
#[test]
fn stack_rounds_frames_to_word_alignment() {
stack::reserve(64);
let first = stack::push(21);
let second = stack::push(8);
assert_eq!(second as usize - first as usize, 24);
stack::pop(first);
}
#[test]
fn each_thread_gets_its_own_stack() {
stack::reserve(64);
let here = stack::push(8);
let here_address = here as usize;
std::thread::scope(|scope| {
scope
.spawn(move || {
stack::reserve(64);
let there = stack::push(8);
assert_ne!(here_address, there as usize, "stacks are per thread");
stack::pop(there);
})
.join()
.unwrap();
});
stack::pop(here);
}
}

View File

@@ -154,6 +154,11 @@ where
unsafe { self.ptr.as_ref() }.serialize()
}
fn layout(&self) -> Option<&crate::record::Layout> {
// SAFETY: as in eval.
unsafe { self.ptr.as_ref() }.layout()
}
fn eval_batch<'a>(&self, input: &'a Input, range: std::ops::Range<u64>, scratch: Option<&'a mut [std::mem::MaybeUninit<Self::Output>]>) -> crate::node::BatchStatus<'a, Self::Output>
where
Input: crate::context::InjectIndex + Copy,
@@ -167,6 +172,7 @@ pub struct EdgeHandle {
node: Box<DynEdge>,
share: fn(&DynEdge) -> Box<DynEdge>,
serialize: fn(&DynEdge) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
layout: fn(&DynEdge) -> Option<&crate::record::Layout>,
ty: Type,
}
@@ -201,6 +207,7 @@ impl EdgeHandle {
node: Box::new(SharedEdge::new(node)),
share: |edge| Box::new(edge.downcast_ref::<SharedEdge<N>>().expect("share hook matches the stored edge type").share()),
serialize: |edge| Node::<ContextImpl>::serialize(edge.downcast_ref::<SharedEdge<N>>().expect("serialize hook matches the stored edge type")),
layout: |edge| Node::<ContextImpl>::layout(edge.downcast_ref::<SharedEdge<N>>().expect("layout hook matches the stored edge type")),
ty,
}
}
@@ -214,6 +221,7 @@ impl EdgeHandle {
node: (self.share)(&*self.node),
share: self.share,
serialize: self.serialize,
layout: self.layout,
ty: self.ty.clone(),
}
}
@@ -222,6 +230,10 @@ impl EdgeHandle {
(self.serialize)(&*self.node)
}
pub fn layout(&self) -> Option<&crate::record::Layout> {
(self.layout)(&*self.node)
}
pub fn downcast<T: 'static>(self) -> Result<SharedEdge<ErasedNode<T>>, ConstructionError> {
self.downcast_erased(edge_type::<T>())
}