mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 11:38:12 +08:00
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:
@@ -3,6 +3,11 @@
|
|||||||
//! collects every declaration so name resolution, defaults, and diagnostics
|
//! collects every declaration so name resolution, defaults, and diagnostics
|
||||||
//! run at graph compile time. One name belongs to one marker, so a name can
|
//! run at graph compile time. One name belongs to one marker, so a name can
|
||||||
//! never mean two different types.
|
//! 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 crate::list::AnyAttributeValue;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
@@ -18,11 +23,15 @@ use std::sync::{LazyLock, Mutex};
|
|||||||
pub trait Attribute: 'static {
|
pub trait Attribute: 'static {
|
||||||
/// The name as it appears in documents and diagnostics.
|
/// The name as it appears in documents and diagnostics.
|
||||||
const NAME: &'static str;
|
const NAME: &'static str;
|
||||||
/// The value type every read and write of this name shares.
|
/// The value type every read and write of this name shares. The lifetime
|
||||||
type Value: AnyAttributeValue + Clone + Default + std::fmt::Debug;
|
/// 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.
|
/// The name-specific default, filled where an item lacks the attribute.
|
||||||
fn default() -> Self::Value {
|
/// Producing a value for any `'e` from no inputs, reference defaults can
|
||||||
Self::Value::default()
|
/// 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
|
/// (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
|
/// `Attr<A>` in the return tuple is a write, and the same marker on both
|
||||||
/// sides is a modify.
|
/// 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> {
|
impl<'e, A: Attribute> Deref for Attr<'e, A> {
|
||||||
type Target = A::Value;
|
type Target = A::Value<'e>;
|
||||||
|
|
||||||
fn deref(&self) -> &A::Value {
|
fn deref(&self) -> &A::Value<'e> {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: Attribute> Clone for Attr<A> {
|
impl<'e, A: Attribute> Clone for Attr<'e, A> {
|
||||||
fn clone(&self) -> Self {
|
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 {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_tuple(A::NAME).field(&self.0).finish()
|
f.debug_tuple(A::NAME).field(&self.0).finish()
|
||||||
}
|
}
|
||||||
@@ -61,20 +72,14 @@ pub struct AttributeInfo {
|
|||||||
pub default: fn() -> Box<dyn AnyAttributeValue>,
|
pub default: fn() -> Box<dyn AnyAttributeValue>,
|
||||||
pub size: usize,
|
pub size: usize,
|
||||||
pub align: 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.
|
/// Writes the declared default's bytes into a `size`-long slice.
|
||||||
/// Meaningful only when `packable`.
|
|
||||||
pub write_default_bytes: fn(&mut [u8]),
|
pub write_default_bytes: fn(&mut [u8]),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_default_bytes<A: Attribute>(out: &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<'static>>());
|
||||||
assert_eq!(out.len(), size_of::<A::Value>());
|
let value: A::Value<'static> = A::default();
|
||||||
let value = A::default();
|
unsafe { std::ptr::copy_nonoverlapping((&raw const value).cast::<u8>(), out.as_mut_ptr(), size_of::<A::Value<'static>>()) };
|
||||||
unsafe { std::ptr::copy_nonoverlapping((&raw const value).cast::<u8>(), out.as_mut_ptr(), size_of::<A::Value>()) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All declared attribute names, keyed by name.
|
/// 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).
|
/// startup (ctor natively, a `__node_registry_attribute_*` export on wasm).
|
||||||
/// Re-registration at the same value type is idempotent; a second marker
|
/// Re-registration at the same value type is idempotent; a second marker
|
||||||
/// claiming the name at a different value type panics.
|
/// 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 {
|
let info = AttributeInfo {
|
||||||
name: A::NAME,
|
name: A::NAME,
|
||||||
value_type: TypeId::of::<A::Value>(),
|
value_type: TypeId::of::<A::Value<'static>>(),
|
||||||
value_type_name: std::any::type_name::<A::Value>(),
|
value_type_name: std::any::type_name::<A::Value<'static>>(),
|
||||||
default: || Box::new(A::default()),
|
default: || Box::new(A::default()),
|
||||||
size: size_of::<A::Value>(),
|
size: size_of::<A::Value<'static>>(),
|
||||||
align: align_of::<A::Value>(),
|
align: align_of::<A::Value<'static>>(),
|
||||||
packable: !std::mem::needs_drop::<A::Value>(),
|
|
||||||
write_default_bytes: write_default_bytes::<A>,
|
write_default_bytes: write_default_bytes::<A>,
|
||||||
};
|
};
|
||||||
let conflict = match ATTRIBUTE_REGISTRY.lock().unwrap().entry(A::NAME) {
|
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! {
|
/// core_types::attribute! {
|
||||||
/// /// How visible the content is.
|
/// /// How visible the content is.
|
||||||
/// pub Opacity("opacity"): f64 = 1.;
|
/// pub Opacity("opacity"): f64 = 1.;
|
||||||
/// /// The item's transformation.
|
/// /// The item's label, parked in the arena by the writer.
|
||||||
/// pub Transform("transform"): glam::DAffine2;
|
/// pub Label("label"): &str;
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// The trailing `= expr` is the name-specific default; without it the value
|
/// 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_export]
|
||||||
macro_rules! attribute {
|
macro_rules! attribute {
|
||||||
($($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): $value:ty $(= $default:expr)?;)+) => {
|
() => {};
|
||||||
$(
|
($(#[$meta:meta])* $vis:vis $marker:ident($name:literal): &$value:ty $(= $default:expr)?; $($rest:tt)*) => {
|
||||||
$(#[$meta])*
|
$(#[$meta])*
|
||||||
$vis struct $marker;
|
$vis struct $marker;
|
||||||
|
|
||||||
impl $crate::attribute::Attribute for $marker {
|
impl $crate::attribute::Attribute for $marker {
|
||||||
const NAME: &'static str = $name;
|
const NAME: &'static str = $name;
|
||||||
type Value = $value;
|
type Value<'e> = &'e $value;
|
||||||
$(
|
$(
|
||||||
fn default() -> $value {
|
fn default<'e>() -> Self::Value<'e> {
|
||||||
$default
|
$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(target_family = "wasm")]
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[unsafe(export_name = concat!("__node_registry_attribute_", stringify!($marker)))]
|
||||||
#[$crate::ctor::ctor]
|
extern "C" fn register() {
|
||||||
fn register() {
|
$crate::attribute::register::<$marker>();
|
||||||
$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.
|
/// Artboard's top-left corner in document coordinates.
|
||||||
pub Location("location"): DVec2;
|
pub Location("location"): DVec2;
|
||||||
/// A regex named-capture-group's name, or empty for unnamed groups.
|
/// A regex named-capture-group's name, or empty for unnamed groups.
|
||||||
pub Name("name"): String;
|
pub Name("name"): &str;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -198,7 +226,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn name_specific_default_overrides_the_type_default() {
|
fn name_specific_default_overrides_the_type_default() {
|
||||||
assert_eq!(<Opacity as Attribute>::default(), 1.);
|
assert_eq!(<Opacity as Attribute>::default(), 1.);
|
||||||
assert_eq!(<Name as Attribute>::default(), String::new());
|
assert_eq!(<Name as Attribute>::default(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -207,6 +235,13 @@ mod tests {
|
|||||||
assert_eq!(*value.as_any().downcast_ref::<f64>().unwrap(), 1.);
|
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]
|
#[test]
|
||||||
fn reregistration_at_the_same_type_is_idempotent() {
|
fn reregistration_at_the_same_type_is_idempotent() {
|
||||||
register::<Opacity>();
|
register::<Opacity>();
|
||||||
@@ -220,7 +255,7 @@ mod tests {
|
|||||||
struct Conflict;
|
struct Conflict;
|
||||||
impl Attribute for Conflict {
|
impl Attribute for Conflict {
|
||||||
const NAME: &'static str = "opacity";
|
const NAME: &'static str = "opacity";
|
||||||
type Value = bool;
|
type Value<'e> = bool;
|
||||||
}
|
}
|
||||||
register::<Conflict>();
|
register::<Conflict>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -624,7 +624,6 @@ pub struct EvalScope<'a> {
|
|||||||
pointer_position: Option<DVec2>,
|
pointer_position: Option<DVec2>,
|
||||||
generations: &'a [(SourceId, u64)],
|
generations: &'a [(SourceId, u64)],
|
||||||
arena: &'a Arena,
|
arena: &'a Arena,
|
||||||
frame: Option<&'a crate::record::Frame>,
|
|
||||||
hash: u64,
|
hash: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,19 +635,12 @@ impl<'a> EvalScope<'a> {
|
|||||||
pointer_position,
|
pointer_position,
|
||||||
generations,
|
generations,
|
||||||
arena,
|
arena,
|
||||||
frame: None,
|
|
||||||
hash: 0,
|
hash: 0,
|
||||||
};
|
};
|
||||||
scope.hash = scope.compute_hash(|_| true);
|
scope.hash = scope.compute_hash(|_| true);
|
||||||
scope
|
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> {
|
pub fn with_real_time(&self, real_time: Option<f64>) -> EvalScope<'a> {
|
||||||
let mut scope = EvalScope { real_time, ..*self };
|
let mut scope = EvalScope { real_time, ..*self };
|
||||||
scope.hash = scope.compute_hash(|_| true);
|
scope.hash = scope.compute_hash(|_| true);
|
||||||
@@ -708,23 +700,6 @@ impl<'a> EvalScope<'a> {
|
|||||||
pub fn arena(&self) -> &'a Arena {
|
pub fn arena(&self) -> &'a Arena {
|
||||||
self.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 {
|
pub trait ExtractArena {
|
||||||
|
|||||||
@@ -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.
|
/// Creates a list containing a single item from the given [`Item`], preserving its attributes.
|
||||||
pub fn new_from_item(item: Item<T>) -> Self {
|
pub fn new_from_item(item: Item<T>) -> Self {
|
||||||
let mut attributes = Attributes::new();
|
let mut attributes = Attributes::new();
|
||||||
@@ -1009,11 +1000,6 @@ impl<T> List<T> {
|
|||||||
self.element.iter()
|
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.
|
/// Returns an iterator over mutable references to all element values.
|
||||||
pub fn iter_element_values_mut(&mut self) -> std::slice::IterMut<'_, T> {
|
pub fn iter_element_values_mut(&mut self) -> std::slice::IterMut<'_, T> {
|
||||||
self.element.iter_mut()
|
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.
|
/// Removes the entire attribute for the given key, if present.
|
||||||
pub fn remove_attribute(&mut self, key: &str) {
|
pub fn remove_attribute(&mut self, key: &str) {
|
||||||
self.attributes.remove_attribute(key);
|
self.attributes.remove_attribute(key);
|
||||||
|
|||||||
@@ -71,6 +71,14 @@ pub trait Node<Input> {
|
|||||||
None
|
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>
|
fn eval_batch<'a>(&self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'a, Self::Output>
|
||||||
where
|
where
|
||||||
Input: InjectIndex + Copy,
|
Input: InjectIndex + Copy,
|
||||||
@@ -133,6 +141,10 @@ where
|
|||||||
(**self).serialize()
|
(**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>
|
fn eval_batch<'a>(&self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'a, Self::Output>
|
||||||
where
|
where
|
||||||
Input: InjectIndex + Copy,
|
Input: InjectIndex + Copy,
|
||||||
@@ -159,6 +171,10 @@ where
|
|||||||
(**self).serialize()
|
(**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>
|
fn eval_batch<'a>(&self, input: &'a Input, range: Range<u64>, scratch: Option<&'a mut [MaybeUninit<Self::Output>]>) -> BatchStatus<'a, Self::Output>
|
||||||
where
|
where
|
||||||
Input: InjectIndex + Copy,
|
Input: InjectIndex + Copy,
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
//! The packed-record tier at rank 0. A record is the element at offset 0
|
//! 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
|
//! plus one field per written attribute; its [`Layout`] is computed at
|
||||||
//! wiring from the upstream write set and never serialized. Records live as
|
//! 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
|
//! per-lane views on the per-thread record [`stack`], claimed per
|
||||||
//! wiring, and kernels route them as opaque [`RecordValue`]s that carry
|
//! evaluation, and kernels route them as opaque [`RecordValue`]s that carry
|
||||||
//! their provenance. Only generated or wiring code touches offsets, so a
|
//! their provenance. Only generated or wiring code touches offsets, so a
|
||||||
//! safe kernel cannot misalign a field.
|
//! safe kernel cannot misalign a field.
|
||||||
|
|
||||||
use crate::attribute;
|
use crate::attribute;
|
||||||
use crate::gpoll::GPoll;
|
use crate::gpoll::GPoll;
|
||||||
use crate::node::Node;
|
use crate::node::Node;
|
||||||
use std::cell::UnsafeCell;
|
|
||||||
|
|
||||||
/// One field of a [`Layout`]: a (name, level) key resolved to an offset.
|
/// 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.
|
/// 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.
|
/// A view of one record: a pointer whose layout is proven at wiring.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Rec(*const u8);
|
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
|
/// The per-thread record stack: every record evaluation claims its activation
|
||||||
/// to at most 8 (record layouts never exceed word alignment).
|
/// frame at the stack pointer and evaluates its carrier beyond it, so slot
|
||||||
#[derive(Debug, Default)]
|
/// addresses are a property of the evaluating thread and no global assignment
|
||||||
pub struct FrameLayout {
|
/// exists. Thread-local by construction, so access is single-threaded without
|
||||||
size: usize,
|
/// claims or gates. Records are overwritten per lane and never touch the
|
||||||
}
|
/// arena.
|
||||||
|
pub mod stack {
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
impl FrameLayout {
|
struct Stack {
|
||||||
pub fn slot(&mut self, layout: &Layout) -> usize {
|
base: Cell<*mut u8>,
|
||||||
assert!(layout.align <= 8, "record layouts align to at most 8");
|
capacity: Cell<usize>,
|
||||||
self.size = self.size.next_multiple_of(layout.align.max(1));
|
sp: Cell<usize>,
|
||||||
let offset = self.size;
|
|
||||||
self.size += layout.size;
|
|
||||||
offset
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn size(&self) -> usize {
|
impl Stack {
|
||||||
self.size
|
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) });
|
||||||
/// 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(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Safety
|
impl Drop for Stack {
|
||||||
/// `offset` must be a slot offset assigned by [`FrameLayout`] for this
|
fn drop(&mut self) {
|
||||||
/// frame, and the caller must be the slot's owning node evaluation.
|
self.free();
|
||||||
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 std::fmt::Debug for Frame {
|
thread_local! {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
static STACK: Stack = const {
|
||||||
f.debug_struct("Frame").field("bytes", &(self.words.len() * 8)).finish()
|
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
|
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
|
/// # Safety
|
||||||
/// `src` must be a record of the plan's source layout and `dst` a buffer of
|
/// `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.
|
/// 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]> {
|
fn default_fill_bytes(name: &str, size: usize) -> Box<[u8]> {
|
||||||
let mut bytes = vec![0u8; size].into_boxed_slice();
|
let mut bytes = vec![0u8; size].into_boxed_slice();
|
||||||
if let Some(info) = attribute::info(name)
|
if let Some(info) = attribute::info(name)
|
||||||
&& info.packable
|
|
||||||
&& info.size == size
|
&& info.size == size
|
||||||
{
|
{
|
||||||
(info.write_default_bytes)(&mut bytes);
|
(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
|
/// A routing source's wiring-resolved translation: field moves into the
|
||||||
/// consumer's frame buffer plus census default fill for union fields the
|
/// union layout plus census default fill for union fields the source lacks.
|
||||||
/// source lacks. Absent when the source's layout already equals the union,
|
/// Absent when the source's layout already equals the union, in which case
|
||||||
/// in which case the record pointer forwards untouched.
|
/// the record pointer forwards untouched.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SourcePlan {
|
pub struct SourcePlan {
|
||||||
moves: Vec<(usize, usize, usize)>,
|
moves: Vec<(usize, usize, usize)>,
|
||||||
fills: Vec<(usize, Box<[u8]>)>,
|
fills: Vec<(usize, Box<[u8]>)>,
|
||||||
slot: usize,
|
union_bytes: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SourcePlan {
|
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 {
|
if source == union {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -266,15 +309,18 @@ impl SourcePlan {
|
|||||||
.filter(|field| source.offset_of(field.name, field.level).is_none())
|
.filter(|field| source.offset_of(field.name, field.level).is_none())
|
||||||
.map(|field| (field.offset, default_fill_bytes(field.name, field.size)))
|
.map(|field| (field.offset, default_fill_bytes(field.name, field.size)))
|
||||||
.collect();
|
.collect();
|
||||||
Some(SourcePlan { moves, fills, slot })
|
Some(SourcePlan {
|
||||||
|
moves,
|
||||||
|
fills,
|
||||||
|
union_bytes: union.size,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Safety
|
/// # Safety
|
||||||
/// `src` must be a record of this plan's source layout and `frame` the
|
/// `src` must be a record of this plan's source layout and `dst` a
|
||||||
/// frame whose slot was assigned to this plan at wiring.
|
/// buffer of the plan's union layout.
|
||||||
pub unsafe fn translate(&self, src: Rec, frame: &Frame) -> Rec {
|
pub unsafe fn translate(&self, src: Rec, dst: *mut u8) -> Rec {
|
||||||
unsafe {
|
unsafe {
|
||||||
let dst = frame.slot(self.slot);
|
|
||||||
apply_plan(src, dst, &self.moves);
|
apply_plan(src, dst, &self.moves);
|
||||||
for (offset, bytes) in &self.fills {
|
for (offset, bytes) in &self.fills {
|
||||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst.add(*offset), bytes.len());
|
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
|
/// Evaluating it yields the source's record translated to the union layout
|
||||||
/// (or forwarded untouched when the layouts already agree), so the kernel
|
/// (or forwarded untouched when the layouts already agree), so the kernel
|
||||||
/// holds and returns record values without ever seeing the representation.
|
/// 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> {
|
pub struct RecordSource<N> {
|
||||||
edge: N,
|
edge: N,
|
||||||
plan: Option<SourcePlan>,
|
plan: Option<SourcePlan>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<N> RecordSource<N> {
|
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 {
|
Self {
|
||||||
edge,
|
edge,
|
||||||
plan: SourcePlan::new(source, union, slot),
|
plan: SourcePlan::new(source, union),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'e, C, N> Node<C> for RecordSource<N>
|
impl<'e, C, N> Node<C> for RecordSource<N>
|
||||||
where
|
where
|
||||||
C: crate::context::ExtractFrame<'e>,
|
|
||||||
N: Node<C, Output = RecordValue<'e>>,
|
N: Node<C, Output = RecordValue<'e>>,
|
||||||
{
|
{
|
||||||
type Output = RecordValue<'e>;
|
type Output = RecordValue<'e>;
|
||||||
|
|
||||||
fn eval(&self, input: &C) -> GPoll<RecordValue<'e>> {
|
fn eval(&self, input: &C) -> GPoll<RecordValue<'e>> {
|
||||||
let value = self.edge.eval(input);
|
|
||||||
match &self.plan {
|
match &self.plan {
|
||||||
None => value,
|
None => self.edge.eval(input),
|
||||||
Some(plan) => {
|
Some(plan) => {
|
||||||
let Some(frame) = crate::context::ExtractFrame::frame(input) else {
|
let dst = stack::push(plan.union_bytes);
|
||||||
return GPoll::error("record frame missing");
|
let value = self.edge.eval(input);
|
||||||
};
|
value.map(|value| RecordValue::from_rec(unsafe { plan.translate(value.rec(), dst) }))
|
||||||
value.map(|value| RecordValue::from_rec(unsafe { plan.translate(value.rec(), frame) }))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,27 +403,15 @@ mod tests {
|
|||||||
assert!(Layout::union(&[&a, &b]).offset_of("length", 0).is_some());
|
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]
|
#[test]
|
||||||
fn translation_moves_fields_and_fills_census_defaults() {
|
fn translation_moves_fields_and_fills_census_defaults() {
|
||||||
let source = Layout::default().with_writes(0, (8, 8), &[f64_field("length")]);
|
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 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 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.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("length", 0).unwrap()) }, 7.);
|
||||||
assert_eq!(unsafe { translated.read::<f64>(union.offset_of("opacity", 0).unwrap()) }, 1.);
|
assert_eq!(unsafe { translated.read::<f64>(union.offset_of("opacity", 0).unwrap()) }, 1.);
|
||||||
@@ -385,6 +420,45 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn identity_layouts_forward() {
|
fn identity_layouts_forward() {
|
||||||
let layout = Layout::default().with_writes(0, (8, 8), &[f64_field("opacity")]);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,11 @@ where
|
|||||||
unsafe { self.ptr.as_ref() }.serialize()
|
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>
|
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
|
where
|
||||||
Input: crate::context::InjectIndex + Copy,
|
Input: crate::context::InjectIndex + Copy,
|
||||||
@@ -167,6 +172,7 @@ pub struct EdgeHandle {
|
|||||||
node: Box<DynEdge>,
|
node: Box<DynEdge>,
|
||||||
share: fn(&DynEdge) -> Box<DynEdge>,
|
share: fn(&DynEdge) -> Box<DynEdge>,
|
||||||
serialize: fn(&DynEdge) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
|
serialize: fn(&DynEdge) -> Option<std::sync::Arc<dyn std::any::Any + Send + Sync>>,
|
||||||
|
layout: fn(&DynEdge) -> Option<&crate::record::Layout>,
|
||||||
ty: Type,
|
ty: Type,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +207,7 @@ impl EdgeHandle {
|
|||||||
node: Box::new(SharedEdge::new(node)),
|
node: Box::new(SharedEdge::new(node)),
|
||||||
share: |edge| Box::new(edge.downcast_ref::<SharedEdge<N>>().expect("share hook matches the stored edge type").share()),
|
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")),
|
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,
|
ty,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,6 +221,7 @@ impl EdgeHandle {
|
|||||||
node: (self.share)(&*self.node),
|
node: (self.share)(&*self.node),
|
||||||
share: self.share,
|
share: self.share,
|
||||||
serialize: self.serialize,
|
serialize: self.serialize,
|
||||||
|
layout: self.layout,
|
||||||
ty: self.ty.clone(),
|
ty: self.ty.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,6 +230,10 @@ impl EdgeHandle {
|
|||||||
(self.serialize)(&*self.node)
|
(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> {
|
pub fn downcast<T: 'static>(self) -> Result<SharedEdge<ErasedNode<T>>, ConstructionError> {
|
||||||
self.downcast_erased(edge_type::<T>())
|
self.downcast_erased(edge_type::<T>())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
// Separate data fields from regular fields
|
// Separate data fields from regular fields
|
||||||
let (data_fields, regular_fields): (Vec<_>, Vec<_>) = fields.iter().partition(|f| f.is_data_field);
|
let (data_fields, regular_fields): (Vec<_>, Vec<_>) = fields.iter().partition(|f| f.is_data_field);
|
||||||
|
|
||||||
|
let record = record_shape(parsed);
|
||||||
|
let record_skips_carrier = record.as_ref().is_some_and(|shape| shape.skips_carrier());
|
||||||
|
// Record nodes with a `_: ()` primary input have no carrier edge; the unit
|
||||||
|
// field stays visible in the metadata but claims no struct field.
|
||||||
|
let struct_regular_fields: Vec<_> = regular_fields.iter().skip(record_skips_carrier as usize).copied().collect();
|
||||||
|
let struct_regular_field_names: Vec<_> = struct_regular_fields.iter().map(|f| &f.pat_ident.ident).collect();
|
||||||
|
|
||||||
// Extract function generics used by data fields
|
// Extract function generics used by data fields
|
||||||
let data_field_generics: Vec<_> = fn_generics
|
let data_field_generics: Vec<_> = fn_generics
|
||||||
.iter()
|
.iter()
|
||||||
@@ -58,7 +65,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Node generics for regular fields (Node0, Node1, ...)
|
// Node generics for regular fields (Node0, Node1, ...)
|
||||||
let node_generics: Vec<Ident> = regular_fields.iter().enumerate().map(|(i, _)| format_ident!("Node{}", i)).collect();
|
let node_generics: Vec<Ident> = struct_regular_fields.iter().enumerate().map(|(i, _)| format_ident!("Node{}", i)).collect();
|
||||||
|
|
||||||
// Extract just the idents from data_field_generics for struct type parameters
|
// Extract just the idents from data_field_generics for struct type parameters
|
||||||
let data_field_generic_idents: Vec<Ident> = data_field_generics
|
let data_field_generic_idents: Vec<Ident> = data_field_generics
|
||||||
@@ -108,16 +115,36 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
quote! { pub(super) #name: #ty }
|
quote! { pub(super) #name: #ty }
|
||||||
});
|
});
|
||||||
|
|
||||||
let regular_field_defs = regular_field_names.iter().zip(node_generics.iter()).map(|(name, r#gen)| {
|
let regular_field_defs = struct_regular_field_names.iter().zip(node_generics.iter()).map(|(name, r#gen)| {
|
||||||
quote! { pub(super) #name: #r#gen }
|
quote! { pub(super) #name: #r#gen }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let record_state_fields: Vec<TokenStream2> = match &record {
|
||||||
|
Some(shape) => {
|
||||||
|
let mut state = vec![quote!(pub(super) __layout: gcore::record::Layout)];
|
||||||
|
if !shape.skips_carrier() {
|
||||||
|
state.push(quote!(pub(super) __plan: ::std::vec::Vec<(usize, usize, usize)>));
|
||||||
|
}
|
||||||
|
state.push(quote!(pub(super) __frame_bytes: usize));
|
||||||
|
state.extend((0..parsed.attribute_reads.len()).map(|index| {
|
||||||
|
let slot = format_ident!("__read_{index}");
|
||||||
|
quote!(pub(super) #slot: Option<usize>)
|
||||||
|
}));
|
||||||
|
state.extend((0..shape.write_markers.len()).map(|index| {
|
||||||
|
let slot = format_ident!("__write_{index}");
|
||||||
|
quote!(pub(super) #slot: usize)
|
||||||
|
}));
|
||||||
|
state
|
||||||
|
}
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
let async_source = parsed.injects_async_source_fields();
|
let async_source = parsed.injects_async_source_fields();
|
||||||
let slot_value_type = slot_value_type(output_type);
|
let slot_value_type = slot_value_type(output_type);
|
||||||
let slot_field = async_source
|
let slot_field = async_source
|
||||||
.then(|| quote! { pub(super) slot: std::sync::Arc<std::sync::Mutex<std::collections::HashMap<u64, Option<gcore::gpoll::GPoll<#slot_value_type>>>>> })
|
.then(|| quote! { pub(super) slot: std::sync::Arc<std::sync::Mutex<std::collections::HashMap<u64, Option<gcore::gpoll::GPoll<#slot_value_type>>>>> })
|
||||||
.into_iter();
|
.into_iter();
|
||||||
let struct_fields = data_field_defs.chain(regular_field_defs).chain(slot_field);
|
let struct_fields = data_field_defs.chain(regular_field_defs).chain(record_state_fields.iter().cloned()).chain(slot_field);
|
||||||
|
|
||||||
// Only regular fields have UI metadata (data fields are internal state)
|
// Only regular fields have UI metadata (data fields are internal state)
|
||||||
let widget_override: Vec<_> = regular_fields
|
let widget_override: Vec<_> = regular_fields
|
||||||
@@ -215,7 +242,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let all_implementation_types = all_implementation_types.chain(input.implementations.iter().cloned());
|
let all_implementation_types = all_implementation_types.chain(input.implementations.iter().cloned());
|
||||||
|
|
||||||
// Only regular fields are parameters to new()
|
// Only regular fields are parameters to new()
|
||||||
let new_args = node_generics.iter().zip(regular_field_names.iter()).map(|(r#gen, name)| {
|
let new_args = node_generics.iter().zip(struct_regular_field_names.iter()).map(|(r#gen, name)| {
|
||||||
quote! { #name: #r#gen }
|
quote! { #name: #r#gen }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -223,14 +250,16 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let data_inits = data_field_names.iter().map(|name| {
|
let data_inits = data_field_names.iter().map(|name| {
|
||||||
quote! { #name: Default::default() }
|
quote! { #name: Default::default() }
|
||||||
});
|
});
|
||||||
let regular_inits = regular_field_names.iter().map(|name| {
|
let regular_inits = struct_regular_field_names.iter().map(|name| {
|
||||||
quote! { #name }
|
quote! { #name }
|
||||||
});
|
});
|
||||||
let slot_init = async_source.then(|| quote! { slot: Default::default() }).into_iter();
|
let slot_init = async_source.then(|| quote! { slot: Default::default() }).into_iter();
|
||||||
let all_field_inits = data_inits.chain(regular_inits).chain(slot_init);
|
let all_field_inits = data_inits.chain(regular_inits).chain(slot_init);
|
||||||
|
|
||||||
// Data fields may not implement Copy, PartialEq, etc., so only derive Debug and Clone
|
// Data fields may not implement Copy, PartialEq, etc., so only derive Debug and Clone
|
||||||
let struct_derives = if data_fields.is_empty() && !async_source {
|
let struct_derives = if record.is_some() {
|
||||||
|
quote!(#[derive(Debug, Clone)])
|
||||||
|
} else if data_fields.is_empty() && !async_source {
|
||||||
quote!(#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)])
|
quote!(#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)])
|
||||||
} else {
|
} else {
|
||||||
quote!(#[derive(Debug, Clone)])
|
quote!(#[derive(Debug, Clone)])
|
||||||
@@ -253,6 +282,24 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
register_metadata();
|
register_metadata();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Record nodes construct through the generated `wire` fn, which resolves
|
||||||
|
// offsets from the carrier layout; `new` cannot fill that state.
|
||||||
|
let new_impl = match record.is_none() {
|
||||||
|
true => quote! {
|
||||||
|
#[automatically_derived]
|
||||||
|
impl<'n, #(#struct_generic_params,)*> #struct_name<#(#struct_type_params,)*>
|
||||||
|
{
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new(#(#new_args,)*) -> Self {
|
||||||
|
Self {
|
||||||
|
#(#all_field_inits,)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false => quote!(),
|
||||||
|
};
|
||||||
|
|
||||||
let import_name = format_ident!("_IMPORT_STUB_{}", mod_name.to_string().to_case(Case::UpperSnake));
|
let import_name = format_ident!("_IMPORT_STUB_{}", mod_name.to_string().to_case(Case::UpperSnake));
|
||||||
let node = generate_node_impl(crate_ident, parsed)?;
|
let node = generate_node_impl(crate_ident, parsed)?;
|
||||||
let node_in_mod = node.in_mod;
|
let node_in_mod = node.in_mod;
|
||||||
@@ -330,16 +377,7 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
#(#struct_fields,)*
|
#(#struct_fields,)*
|
||||||
}
|
}
|
||||||
|
|
||||||
#[automatically_derived]
|
#new_impl
|
||||||
impl<'n, #(#struct_generic_params,)*> #struct_name<#(#struct_type_params,)*>
|
|
||||||
{
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(#(#new_args,)*) -> Self {
|
|
||||||
Self {
|
|
||||||
#(#all_field_inits,)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#node_in_mod
|
#node_in_mod
|
||||||
|
|
||||||
@@ -612,6 +650,11 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
top_level: quote!(),
|
top_level: quote!(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
let record_token = match record.as_ref().map(|shape| &shape.carrier) {
|
||||||
|
Some(RecordCarrier::Token(token)) => Some(token.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let skips_carrier = record.as_ref().is_some_and(|shape| shape.skips_carrier());
|
||||||
let routing = routing_io(parsed);
|
let routing = routing_io(parsed);
|
||||||
let snapshot_ctx = async_fn && matches!(&parsed.input.ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "CtxSnapshot"));
|
let snapshot_ctx = async_fn && matches!(&parsed.input.ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "CtxSnapshot"));
|
||||||
|
|
||||||
@@ -681,8 +724,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let mut impl_generics: Vec<TokenStream2> = parsed
|
let mut impl_generics: Vec<TokenStream2> = parsed
|
||||||
.fn_generics
|
.fn_generics
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|param| match (param, &routing) {
|
.filter(|param| match param {
|
||||||
(GenericParam::Type(type_param), Some(routing)) => type_param.ident != routing.generic,
|
GenericParam::Type(type_param) => {
|
||||||
|
Some(&type_param.ident) != routing.as_ref().map(|routing| &routing.generic) && Some(&type_param.ident) != record_token.as_ref()
|
||||||
|
}
|
||||||
_ => true,
|
_ => true,
|
||||||
})
|
})
|
||||||
.map(&generic_tokens)
|
.map(&generic_tokens)
|
||||||
@@ -695,7 +740,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
generics.insert(0, quote!(#lifetime));
|
generics.insert(0, quote!(#lifetime));
|
||||||
impl_generics.insert(0, quote!(#lifetime));
|
impl_generics.insert(0, quote!(#lifetime));
|
||||||
}
|
}
|
||||||
if routing.is_some() {
|
if routing.is_some() || record.is_some() {
|
||||||
impl_generics.insert(0, quote!('__record));
|
impl_generics.insert(0, quote!('__record));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,11 +749,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let struct_name = format_ident!("{}Node", parsed.struct_name);
|
let struct_name = format_ident!("{}Node", parsed.struct_name);
|
||||||
let output_type = &parsed.output_type;
|
let output_type = &parsed.output_type;
|
||||||
let trait_output = match (&record, &routing) {
|
let trait_output = match (&record, &routing) {
|
||||||
(Some(shape), _) => {
|
(Some(_), _) | (None, Some(_)) => syn::parse_quote!(#core_types::record::RecordValue<'__record>),
|
||||||
let element_out = &shape.element_out;
|
|
||||||
syn::parse_quote!(#core_types::list::List<#element_out>)
|
|
||||||
}
|
|
||||||
(None, Some(_)) => syn::parse_quote!(#core_types::record::RecordValue<'__record>),
|
|
||||||
(None, None) => slot_value_type(&parsed.output_type),
|
(None, None) => slot_value_type(&parsed.output_type),
|
||||||
};
|
};
|
||||||
let raw_lazy = matches!(kernel_kind(&parsed.output_type), KernelKind::Poll(_));
|
let raw_lazy = matches!(kernel_kind(&parsed.output_type), KernelKind::Poll(_));
|
||||||
@@ -716,6 +757,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let where_predicates: Vec<TokenStream2> = parsed.where_clause.iter().flat_map(|clause| clause.predicates.iter()).map(|predicate| quote!(#predicate)).collect();
|
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_fields, regular_fields): (Vec<_>, Vec<_>) = parsed.fields.iter().partition(|field| field.is_data_field);
|
||||||
|
let regular_fields: Vec<_> = regular_fields.into_iter().skip(skips_carrier as usize).collect();
|
||||||
|
|
||||||
let data_field_generic_idents: Vec<Ident> = parsed
|
let data_field_generic_idents: Vec<Ident> = parsed
|
||||||
.fn_generics
|
.fn_generics
|
||||||
@@ -781,8 +823,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let lifetime = lend_lifetime.as_ref().expect("lend fields imply the lend lifetime");
|
let lifetime = lend_lifetime.as_ref().expect("lend fields imply the lend lifetime");
|
||||||
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = &#lifetime #ty>)
|
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = &#lifetime #ty>)
|
||||||
}
|
}
|
||||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) if record.is_some() && index == 0 => {
|
ParsedFieldType::Regular(_) if record.is_some() && !skips_carrier && index == 0 => {
|
||||||
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #core_types::list::List<#ty>>)
|
quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>)
|
||||||
}
|
}
|
||||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) if routing_source(ty) => {
|
ParsedFieldType::Regular(RegularParsedField { ty, .. }) if routing_source(ty) => {
|
||||||
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>)
|
||||||
@@ -848,12 +890,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let eval_values = regular_fields.iter().enumerate().map(|(index, field)| {
|
let eval_values = regular_fields.iter().enumerate().map(|(index, field)| {
|
||||||
let name = &field.pat_ident.ident;
|
let name = &field.pat_ident.ident;
|
||||||
match &field.ty {
|
match &field.ty {
|
||||||
ParsedFieldType::Regular(_) if record.is_some() && index == 0 => quote! {
|
ParsedFieldType::Regular(_) if record.is_some() && !skips_carrier && index == 0 => quote!(),
|
||||||
let mut __record_list = match __cell.eval_input(#index, &self.#name, __input) {
|
|
||||||
Ok(value) => value,
|
|
||||||
Err(interrupt) => return interrupt.into(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
ParsedFieldType::Regular(_) => quote! {
|
ParsedFieldType::Regular(_) => quote! {
|
||||||
let #name = match __cell.eval_input(#index, &self.#name, __input) {
|
let #name = match __cell.eval_input(#index, &self.#name, __input) {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
@@ -951,10 +988,16 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
let body = &parsed.body;
|
let body = &parsed.body;
|
||||||
let vis = &parsed.vis;
|
let vis = &parsed.vis;
|
||||||
let kernel_fields: Vec<&&ParsedField> = regular_fields.iter().filter(|field| !injected_name(&field.pat_ident.ident)).collect();
|
let kernel_fields: Vec<&&ParsedField> = regular_fields.iter().filter(|field| !injected_name(&field.pat_ident.ident)).collect();
|
||||||
|
// A bare `Attr<M>` in the return type cannot elide its lifetime, so the
|
||||||
|
// kernel gets a fresh one; reference-valued writes name their real
|
||||||
|
// lifetime explicitly and pass through untouched.
|
||||||
|
let kernel_output = record.as_ref().and_then(|_| inject_attr_lifetimes(&parsed.output_type));
|
||||||
|
let attr_lifetime = kernel_output.is_some().then(|| quote!('__attr,));
|
||||||
|
let kernel_output = kernel_output.map(|ty| quote!(#ty)).unwrap_or_else(|| quote!(#output_type));
|
||||||
let kernel = match async_fn {
|
let kernel = match async_fn {
|
||||||
false => quote! {
|
false => quote! {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
#vis fn #fn_name<#(#generics,)*>(#ctx_pat: &#ctx_ident #(, #data_params)* #(, #kernel_params)*) -> #output_type #fn_where #body
|
#vis fn #fn_name<#attr_lifetime #(#generics,)*>(#ctx_pat: &#ctx_ident #(, #data_params)* #(, #kernel_params)*) -> #kernel_output #fn_where #body
|
||||||
},
|
},
|
||||||
true => {
|
true => {
|
||||||
let kernel_generics = parsed.fn_generics.iter().filter(|param| match param {
|
let kernel_generics = parsed.fn_generics.iter().filter(|param| match param {
|
||||||
@@ -1048,61 +1091,43 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let record_tail = record.as_ref().map(|shape| {
|
let record_tail = record.as_ref().map(|shape| {
|
||||||
let record_call_args = regular_fields.iter().enumerate().map(|(index, field)| {
|
let carrier_arg = match &shape.carrier {
|
||||||
|
RecordCarrier::None => None,
|
||||||
|
RecordCarrier::Token(_) => Some(quote!(#core_types::record::ElToken)),
|
||||||
|
RecordCarrier::Read(ty) => Some(quote!(unsafe { __src_rec.element::<#ty>() })),
|
||||||
|
}
|
||||||
|
.into_iter();
|
||||||
|
let value_args = regular_fields.iter().skip(if shape.skips_carrier() { 0 } else { 1 }).map(|field| {
|
||||||
let name = &field.pat_ident.ident;
|
let name = &field.pat_ident.ident;
|
||||||
match (index, &field.ty) {
|
quote!(#name)
|
||||||
(0, _) => quote!(__element),
|
|
||||||
(_, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. })) => quote!(#name),
|
|
||||||
_ => quote!(#name.clone()),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
let attr_call_args = parsed.attribute_reads.iter().map(|read| {
|
let attr_args = parsed.attribute_reads.iter().map(|read| {
|
||||||
let pat = &read.pat_ident.ident;
|
let pat = &read.pat_ident.ident;
|
||||||
quote!(#pat)
|
quote!(#pat)
|
||||||
});
|
});
|
||||||
let record_kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #record_call_args)* #(, #attr_call_args)*));
|
let record_kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #carrier_arg)* #(, #value_args)* #(, #attr_args)*));
|
||||||
let read_columns = parsed.attribute_reads.iter().enumerate().map(|(index, read)| {
|
let carrier_eval = (!shape.skips_carrier()).then(|| {
|
||||||
let marker = &read.marker;
|
let name = ®ular_fields[0].pat_ident.ident;
|
||||||
let column = format_ident!("__read_{index}");
|
|
||||||
quote! {
|
quote! {
|
||||||
let #column = __record_list
|
let __src = match __cell.eval_input(0, &self.#name, __input) {
|
||||||
.iter_attribute_values::<<#marker as #core_types::attribute::Attribute>::Value>(<#marker as #core_types::attribute::Attribute>::NAME)
|
Ok(value) => value,
|
||||||
.map(|__values| __values.cloned().collect::<::std::vec::Vec<_>>());
|
Err(interrupt) => return interrupt.into(),
|
||||||
|
};
|
||||||
|
let __src_rec = #core_types::record::RecordValue::rec(__src);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let carry = (!shape.skips_carrier()).then(|| quote!(unsafe { #core_types::record::apply_plan(__src_rec, __dst, &self.__plan) };));
|
||||||
let read_bindings = parsed.attribute_reads.iter().enumerate().map(|(index, read)| {
|
let read_bindings = parsed.attribute_reads.iter().enumerate().map(|(index, read)| {
|
||||||
let pat = &read.pat_ident;
|
let pat = &read.pat_ident;
|
||||||
let marker = &read.marker;
|
let marker = &read.marker;
|
||||||
let column = format_ident!("__read_{index}");
|
let slot = format_ident!("__read_{index}");
|
||||||
quote! {
|
quote! {
|
||||||
let #pat = #core_types::attribute::Attr::<#marker>(match &#column {
|
let #pat = #core_types::attribute::Attr::<#marker>(match self.#slot {
|
||||||
Some(__values) => __values[__index].clone(),
|
Some(__offset) => unsafe { __src_rec.read(__offset) },
|
||||||
None => <#marker as #core_types::attribute::Attribute>::default(),
|
None => <#marker as #core_types::attribute::Attribute>::default(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let write_markers = &shape.write_markers;
|
|
||||||
let write_columns: Vec<Ident> = (0..write_markers.len()).map(|index| format_ident!("__write_{index}")).collect();
|
|
||||||
let write_column_decls = write_markers.iter().zip(&write_columns).map(|(marker, column)| {
|
|
||||||
quote! {
|
|
||||||
let mut #column: ::std::vec::Vec<<#marker as #core_types::attribute::Attribute>::Value> = ::std::vec::Vec::with_capacity(__len);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let write_pats: Vec<Ident> = (0..write_markers.len()).map(|index| format_ident!("__written_{index}")).collect();
|
|
||||||
let destructure = match write_markers.is_empty() {
|
|
||||||
true => quote!(let __element_out = __kernel_value;),
|
|
||||||
false => quote!(let (__element_out #(, #core_types::attribute::Attr(#write_pats))*) = __kernel_value;),
|
|
||||||
};
|
|
||||||
let write_pushes = write_columns.iter().zip(&write_pats).map(|(column, pat)| quote!(#column.push(#pat);));
|
|
||||||
let written_names = write_markers.iter().map(|marker| quote!(<#marker as #core_types::attribute::Attribute>::NAME));
|
|
||||||
let write_inserts = write_markers.iter().zip(&write_columns).map(|(marker, column)| {
|
|
||||||
quote! {
|
|
||||||
__out.insert_attribute_dyn(
|
|
||||||
<#marker as #core_types::attribute::Attribute>::NAME,
|
|
||||||
#core_types::list::AttributeDyn(::std::boxed::Box::new(#core_types::list::Attribute(#column))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let kernel_value = match shape.dialect {
|
let kernel_value = match shape.dialect {
|
||||||
RecordDialect::Plain => quote!(#record_kernel_call),
|
RecordDialect::Plain => quote!(#record_kernel_call),
|
||||||
RecordDialect::Interrupt => quote! {
|
RecordDialect::Interrupt => quote! {
|
||||||
@@ -1112,37 +1137,31 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let element_out = &shape.element_out;
|
let attr_binders: Vec<Ident> = (0..shape.write_markers.len()).map(|index| format_ident!("__attr_{index}")).collect();
|
||||||
|
let element_binder = match &shape.element_write {
|
||||||
|
Some(_) => quote!(__element),
|
||||||
|
None => quote!(_),
|
||||||
|
};
|
||||||
|
let destructure = match attr_binders.is_empty() {
|
||||||
|
true => quote!(let #element_binder = __kernel_value;),
|
||||||
|
false => quote!(let (#element_binder #(, #core_types::attribute::Attr(#attr_binders))*) = __kernel_value;),
|
||||||
|
};
|
||||||
|
let element_store = shape.element_write.as_ref().map(|ty| quote!(unsafe { #core_types::record::write_field::<#ty>(__dst, 0, __element) };));
|
||||||
|
let attr_stores = attr_binders.iter().enumerate().map(|(index, binder)| {
|
||||||
|
let slot = format_ident!("__write_{index}");
|
||||||
|
quote!(unsafe { #core_types::record::write_field(__dst, self.#slot, #binder) };)
|
||||||
|
});
|
||||||
quote! {
|
quote! {
|
||||||
let __len = __record_list.len();
|
let __dst = #core_types::record::stack::push(self.__frame_bytes);
|
||||||
#(#read_columns)*
|
#carrier_eval
|
||||||
let __written_names: &[&str] = &[#(#written_names),*];
|
#carry
|
||||||
let __carried_keys: ::std::vec::Vec<::std::string::String> = __record_list
|
#(#read_bindings)*
|
||||||
.attribute_keys()
|
let __kernel_value = #kernel_value;
|
||||||
.filter(|__key| !__written_names.contains(__key))
|
#destructure
|
||||||
.map(::std::string::String::from)
|
#element_store
|
||||||
.collect();
|
#(#attr_stores)*
|
||||||
let mut __carried: ::std::vec::Vec<(::std::string::String, #core_types::list::AttributeDyn)> = ::std::vec::Vec::with_capacity(__carried_keys.len());
|
#core_types::record::stack::pop(__dst);
|
||||||
for __key in __carried_keys {
|
__cell.finish(#core_types::record::RecordValue::from_rec(unsafe { #core_types::record::Rec::new(__dst.cast_const()) }))
|
||||||
if let Some(__column) = __record_list.take_attribute_dyn(&__key) {
|
|
||||||
__carried.push((__key, __column));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#(#write_column_decls)*
|
|
||||||
let mut __out_elements: ::std::vec::Vec<#element_out> = ::std::vec::Vec::with_capacity(__len);
|
|
||||||
for (__index, __element) in __record_list.into_element_values().into_iter().enumerate() {
|
|
||||||
#(#read_bindings)*
|
|
||||||
let __kernel_value = #kernel_value;
|
|
||||||
#destructure
|
|
||||||
__out_elements.push(__element_out);
|
|
||||||
#(#write_pushes)*
|
|
||||||
}
|
|
||||||
let mut __out = #core_types::list::List::from_element_values(__out_elements);
|
|
||||||
for (__key, __column) in __carried {
|
|
||||||
__out.insert_attribute_dyn(__key, __column);
|
|
||||||
}
|
|
||||||
#(#write_inserts)*
|
|
||||||
__cell.finish(__out)
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let eval_tail = match (async_fn, future_kernel) {
|
let eval_tail = match (async_fn, future_kernel) {
|
||||||
@@ -1199,21 +1218,114 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
};
|
};
|
||||||
|
|
||||||
let record_bounds: Vec<TokenStream2> = match &record {
|
let record_bounds: Vec<TokenStream2> = match &record {
|
||||||
Some(_) => regular_fields
|
Some(shape) if shape.skips_carrier() => {
|
||||||
.iter()
|
vec![quote!(#ctx_ident: #core_types::context::ExtractArena<ArenaRef = &'__record #core_types::arena::Arena>)]
|
||||||
.skip(1)
|
}
|
||||||
.filter_map(|field| match &field.ty {
|
_ => Vec::new(),
|
||||||
ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }) => None,
|
};
|
||||||
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => Some(quote!(#ty: Clone)),
|
|
||||||
_ => None,
|
let record_layout_impl = match &record {
|
||||||
})
|
Some(_) => quote! {
|
||||||
.collect(),
|
fn layout(&self) -> Option<&#core_types::record::Layout> {
|
||||||
None => Vec::new(),
|
Some(&self.__layout)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => quote!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let entries = entries_tokens(parsed, &struct_name, &data_field_generic_idents, ®ular_fields);
|
let entries = entries_tokens(parsed, &struct_name, &data_field_generic_idents, ®ular_fields);
|
||||||
let cfg = crate::shader_nodes::modify_cfg(&parsed.attributes);
|
let cfg = crate::shader_nodes::modify_cfg(&parsed.attributes);
|
||||||
|
|
||||||
|
let record_wiring = record.as_ref().map(|shape| {
|
||||||
|
let layout_fn = format_ident!("{}_layout", fn_name);
|
||||||
|
let write_descs: Vec<TokenStream2> = shape
|
||||||
|
.write_markers
|
||||||
|
.iter()
|
||||||
|
.map(|marker| {
|
||||||
|
quote! {
|
||||||
|
(
|
||||||
|
<#marker as #core_types::attribute::Attribute>::NAME,
|
||||||
|
0u8,
|
||||||
|
::core::mem::size_of::<<#marker as #core_types::attribute::Attribute>::Value<'static>>(),
|
||||||
|
::core::mem::align_of::<<#marker as #core_types::attribute::Attribute>::Value<'static>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let element_dims = match &shape.element_write {
|
||||||
|
Some(ty) => quote!((::core::mem::size_of::<#ty>(), ::core::mem::align_of::<#ty>())),
|
||||||
|
None => quote!((__carrier.element_size, __carrier.element_align)),
|
||||||
|
};
|
||||||
|
let layout_def = match shape.skips_carrier() {
|
||||||
|
true => quote! {
|
||||||
|
#vis fn #layout_fn() -> #core_types::record::Layout {
|
||||||
|
#core_types::record::Layout::default().with_writes(0, #element_dims, &[#(#write_descs),*])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false => quote! {
|
||||||
|
#vis fn #layout_fn(__carrier: &#core_types::record::Layout) -> #core_types::record::Layout {
|
||||||
|
__carrier.with_writes(__carrier.depth, #element_dims, &[#(#write_descs),*])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let edge_args = regular_fields.iter().zip(&node_generics).map(|(field, generic)| {
|
||||||
|
let name = &field.pat_ident.ident;
|
||||||
|
quote!(#name: #generic)
|
||||||
|
});
|
||||||
|
let carrier_layout_param = (!shape.skips_carrier()).then(|| quote!(__carrier_layout: &#core_types::record::Layout,)).into_iter();
|
||||||
|
let layout_binding = match shape.skips_carrier() {
|
||||||
|
true => quote!(let __layout = self::#layout_fn();),
|
||||||
|
false => quote!(let __layout = self::#layout_fn(__carrier_layout);),
|
||||||
|
};
|
||||||
|
let carry_element = shape.carries_element();
|
||||||
|
let plan_binding = (!shape.skips_carrier()).then(|| quote!(let __plan = #core_types::record::copy_plan(__carrier_layout, &__layout, #carry_element);));
|
||||||
|
let read_inits = parsed.attribute_reads.iter().enumerate().map(|(index, read)| {
|
||||||
|
let marker = &read.marker;
|
||||||
|
let slot = format_ident!("__read_{index}");
|
||||||
|
quote!(let #slot = __carrier_layout.offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0);)
|
||||||
|
});
|
||||||
|
let write_inits = shape.write_markers.iter().enumerate().map(|(index, marker)| {
|
||||||
|
let slot = format_ident!("__write_{index}");
|
||||||
|
quote! {
|
||||||
|
let #slot = __layout
|
||||||
|
.offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0)
|
||||||
|
.expect("a written attribute is always part of the wired layout");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let data_inits = data_names.iter().map(|name| quote!(#name: ::core::default::Default::default(),));
|
||||||
|
let edge_inits = regular_fields.iter().map(|field| {
|
||||||
|
let name = &field.pat_ident.ident;
|
||||||
|
quote!(#name,)
|
||||||
|
});
|
||||||
|
let plan_init = (!shape.skips_carrier()).then(|| quote!(__plan,)).into_iter();
|
||||||
|
let read_names = (0..parsed.attribute_reads.len()).map(|index| format_ident!("__read_{index}")).map(|slot| quote!(#slot,));
|
||||||
|
let write_names = (0..shape.write_markers.len()).map(|index| format_ident!("__write_{index}")).map(|slot| quote!(#slot,));
|
||||||
|
quote! {
|
||||||
|
#layout_def
|
||||||
|
|
||||||
|
#[automatically_derived]
|
||||||
|
impl<#(#data_field_generic_idents,)* #(#node_generics,)*> #mod_name::#struct_name<#(#struct_type_params,)*> {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#vis fn wire(#(#edge_args,)* #(#carrier_layout_param)*) -> Self {
|
||||||
|
#layout_binding
|
||||||
|
#plan_binding
|
||||||
|
#(#read_inits)*
|
||||||
|
#(#write_inits)*
|
||||||
|
let __frame_bytes = __layout.size.next_multiple_of(8);
|
||||||
|
Self {
|
||||||
|
#(#data_inits)*
|
||||||
|
#(#edge_inits)*
|
||||||
|
__layout,
|
||||||
|
#(#plan_init)*
|
||||||
|
__frame_bytes,
|
||||||
|
#(#read_names)*
|
||||||
|
#(#write_names)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let top_level = quote! {
|
let top_level = quote! {
|
||||||
#cfg
|
#cfg
|
||||||
#[automatically_derived]
|
#[automatically_derived]
|
||||||
@@ -1239,6 +1351,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
|
|
||||||
#serialize_impl
|
#serialize_impl
|
||||||
|
|
||||||
|
#record_layout_impl
|
||||||
|
|
||||||
#batch_impl
|
#batch_impl
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1248,6 +1362,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
|||||||
top_level: quote! {
|
top_level: quote! {
|
||||||
#kernel
|
#kernel
|
||||||
|
|
||||||
|
#record_wiring
|
||||||
|
|
||||||
#top_level
|
#top_level
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1258,20 +1374,90 @@ pub(crate) enum RecordDialect {
|
|||||||
Interrupt,
|
Interrupt,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The record io of a node fn: the output element type and the written
|
/// How a record node's primary input lowers.
|
||||||
/// markers. Present exactly when the signature declares attribute reads or
|
pub(crate) enum RecordCarrier {
|
||||||
/// writes in a shape the driver supports (the carrier is the first field);
|
/// `_: ()`: no carrier edge, the kernel writes a fresh record.
|
||||||
/// malformed record io is reported by validation and generates no node impl.
|
None,
|
||||||
|
/// An unbounded generic returned in the element position: the element
|
||||||
|
/// bytes carry through the copy plan and the kernel sees `ElToken`.
|
||||||
|
Token(Ident),
|
||||||
|
/// An element type read at offset 0, monomorphized per its
|
||||||
|
/// implementations list where generic.
|
||||||
|
Read(Type),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The record io of a node fn: how the carrier lowers, the element write,
|
||||||
|
/// and the written markers. Present exactly when the signature declares
|
||||||
|
/// attribute reads or writes in a shape the record tier supports; malformed
|
||||||
|
/// record io is reported by validation and generates no node impl.
|
||||||
pub(crate) struct RecordShape {
|
pub(crate) struct RecordShape {
|
||||||
pub(crate) element_out: Type,
|
pub(crate) carrier: RecordCarrier,
|
||||||
|
pub(crate) element_write: Option<Type>,
|
||||||
pub(crate) write_markers: Vec<Type>,
|
pub(crate) write_markers: Vec<Type>,
|
||||||
pub(crate) dialect: RecordDialect,
|
pub(crate) dialect: RecordDialect,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RecordShape {
|
||||||
|
pub(crate) fn skips_carrier(&self) -> bool {
|
||||||
|
matches!(self.carrier, RecordCarrier::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn carries_element(&self) -> bool {
|
||||||
|
self.element_write.is_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn has_record_io(parsed: &ParsedNodeFn) -> bool {
|
pub(crate) fn has_record_io(parsed: &ParsedNodeFn) -> bool {
|
||||||
!parsed.attribute_reads.is_empty() || record_writes(&slot_value_type(&parsed.output_type)).is_some()
|
!parsed.attribute_reads.is_empty() || record_writes(&slot_value_type(&parsed.output_type)).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inject_attr_lifetimes(output: &Type) -> Option<Type> {
|
||||||
|
struct Injector {
|
||||||
|
changed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VisitMut for Injector {
|
||||||
|
fn visit_path_segment_mut(&mut self, segment: &mut syn::PathSegment) {
|
||||||
|
if segment.ident == "Attr"
|
||||||
|
&& let PathArguments::AngleBracketed(args) = &mut segment.arguments
|
||||||
|
&& !args.args.iter().any(|arg| matches!(arg, GenericArgument::Lifetime(_)))
|
||||||
|
{
|
||||||
|
args.args.insert(0, GenericArgument::Lifetime(Lifetime::new("'__attr", proc_macro2::Span::call_site())));
|
||||||
|
self.changed = true;
|
||||||
|
}
|
||||||
|
syn::visit_mut::visit_path_segment_mut(self, segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ty = output.clone();
|
||||||
|
let mut injector = Injector { changed: false };
|
||||||
|
injector.visit_type_mut(&mut ty);
|
||||||
|
injector.changed.then_some(ty)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn contains_open_generic(parsed: &ParsedNodeFn, ty: &Type) -> bool {
|
||||||
|
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||||
|
parsed
|
||||||
|
.fn_generics
|
||||||
|
.iter()
|
||||||
|
.any(|param| matches!(param, GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() && type_contains_ident(ty, &type_param.ident)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unbounded_generic(parsed: &ParsedNodeFn, ty: &Type) -> Option<Ident> {
|
||||||
|
let ident = bare_ident(ty)?.clone();
|
||||||
|
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
|
||||||
|
parsed
|
||||||
|
.fn_generics
|
||||||
|
.iter()
|
||||||
|
.find(|param| matches!(param, GenericParam::Type(type_param) if type_param.ident == ident && type_param.bounds.is_empty() && Some(&type_param.ident) != ctx_ident.as_ref()))?;
|
||||||
|
if let Some(where_clause) = &parsed.where_clause
|
||||||
|
&& tokens_contain_ident(where_clause.to_token_stream(), &ident)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(ident)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
|
pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
|
||||||
let (value, dialect) = match kernel_kind(&parsed.output_type) {
|
let (value, dialect) = match kernel_kind(&parsed.output_type) {
|
||||||
KernelKind::Plain => (parsed.output_type.clone(), RecordDialect::Plain),
|
KernelKind::Plain => (parsed.output_type.clone(), RecordDialect::Plain),
|
||||||
@@ -1282,28 +1468,50 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
|
|||||||
if parsed.attribute_reads.is_empty() && writes.is_none() {
|
if parsed.attribute_reads.is_empty() && writes.is_none() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if parsed.is_async {
|
if parsed.is_async || parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let carrier = parsed.fields.first()?;
|
let carrier_field = parsed.fields.first()?;
|
||||||
if carrier.is_data_field {
|
if carrier_field.is_data_field {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let ParsedFieldType::Regular(RegularParsedField { ty, lend: None, .. }) = &carrier.ty else {
|
let ParsedFieldType::Regular(RegularParsedField { ty, lend: None, implementations, .. }) = &carrier_field.ty else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
if matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty()) {
|
let carrier = match ty {
|
||||||
return None;
|
Type::Tuple(tuple) if tuple.elems.is_empty() => RecordCarrier::None,
|
||||||
}
|
ty => match implementations.is_empty().then(|| unbounded_generic(parsed, ty)).flatten() {
|
||||||
if parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) {
|
Some(token) => RecordCarrier::Token(token),
|
||||||
return None;
|
None => {
|
||||||
}
|
if contains_open_generic(parsed, ty) {
|
||||||
let (element_out, write_markers) = match writes {
|
return None;
|
||||||
|
}
|
||||||
|
RecordCarrier::Read(ty.clone())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let (element, write_markers) = match writes {
|
||||||
Some(RecordWrites { element, markers }) => (element, markers),
|
Some(RecordWrites { element, markers }) => (element, markers),
|
||||||
None => (value, Vec::new()),
|
None => (value, Vec::new()),
|
||||||
};
|
};
|
||||||
|
let element_write = match &carrier {
|
||||||
|
RecordCarrier::Token(token) => match bare_ident(&element) {
|
||||||
|
Some(ident) if ident == token => None,
|
||||||
|
_ => return None,
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
if contains_open_generic(parsed, &element) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(element)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if matches!(carrier, RecordCarrier::None) && !parsed.attribute_reads.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
Some(RecordShape {
|
Some(RecordShape {
|
||||||
element_out,
|
carrier,
|
||||||
|
element_write,
|
||||||
write_markers,
|
write_markers,
|
||||||
dialect,
|
dialect,
|
||||||
})
|
})
|
||||||
@@ -1374,7 +1582,7 @@ pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option<RoutingIo> {
|
|||||||
(sources > 0).then(|| RoutingIo { generic: ident })
|
(sources > 0).then(|| RoutingIo { generic: ident })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bare_ident(ty: &Type) -> Option<&Ident> {
|
pub(crate) fn bare_ident(ty: &Type) -> Option<&Ident> {
|
||||||
let Type::Path(path) = ty else { return None };
|
let Type::Path(path) = ty else { return None };
|
||||||
path.path.get_ident()
|
path.path.get_ident()
|
||||||
}
|
}
|
||||||
@@ -1522,6 +1730,9 @@ fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic
|
|||||||
if !data_field_generic_idents.is_empty() {
|
if !data_field_generic_idents.is_empty() {
|
||||||
return quote!();
|
return quote!();
|
||||||
}
|
}
|
||||||
|
if has_record_io(parsed) {
|
||||||
|
return quote!();
|
||||||
|
}
|
||||||
let Some(rows) = implementation_rows(parsed, regular_fields) else {
|
let Some(rows) = implementation_rows(parsed, regular_fields) else {
|
||||||
return quote!();
|
return quote!();
|
||||||
};
|
};
|
||||||
@@ -1554,17 +1765,14 @@ fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic
|
|||||||
.map(|field| matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. })))
|
.map(|field| matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. })))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let record_carrier = record_shape(parsed).is_some();
|
|
||||||
let entries = rows.iter().map(|row| {
|
let entries = rows.iter().map(|row| {
|
||||||
let input_types = row.iter().enumerate().zip(&lend_flags).map(|((index, ty), lend)| match (lend, record_carrier && index == 0) {
|
let input_types = row.iter().zip(&lend_flags).map(|(ty, lend)| match lend {
|
||||||
(true, _) => quote!(gcore::registry::lend_edge_type::<#ty>()),
|
true => quote!(gcore::registry::lend_edge_type::<#ty>()),
|
||||||
(false, true) => quote!(gcore::registry::edge_type::<gcore::list::List<#ty>>()),
|
false => quote!(gcore::registry::edge_type::<#ty>()),
|
||||||
(false, false) => quote!(gcore::registry::edge_type::<#ty>()),
|
|
||||||
});
|
});
|
||||||
let edge_types = row.iter().enumerate().zip(&lend_flags).map(|((index, ty), lend)| match (lend, record_carrier && index == 0) {
|
let edge_types = row.iter().zip(&lend_flags).map(|(ty, lend)| match lend {
|
||||||
(true, _) => quote!(gcore::registry::SharedEdge<gcore::registry::ErasedLendNode<#ty>>),
|
true => quote!(gcore::registry::SharedEdge<gcore::registry::ErasedLendNode<#ty>>),
|
||||||
(false, true) => quote!(gcore::registry::SharedEdge<gcore::registry::ErasedNode<gcore::list::List<#ty>>>),
|
false => quote!(gcore::registry::SharedEdge<gcore::registry::ErasedNode<#ty>>),
|
||||||
(false, false) => quote!(gcore::registry::SharedEdge<gcore::registry::ErasedNode<#ty>>),
|
|
||||||
});
|
});
|
||||||
let output = quote!(<#struct_name<#(#edge_types),*> as gcore::node::Node<gcore::context::ContextImpl<'static>>>::Output);
|
let output = quote!(<#struct_name<#(#edge_types),*> as gcore::node::Node<gcore::context::ContextImpl<'static>>>::Output);
|
||||||
let (io_output, construct) = match &ref_output_inner {
|
let (io_output, construct) = match &ref_output_inner {
|
||||||
@@ -1577,10 +1785,9 @@ fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic
|
|||||||
quote!(Ok(gcore::registry::EdgeHandle::new(::std::sync::Arc::new(#struct_name::new(#(#names),*)) as ::std::sync::Arc<gcore::registry::ErasedNode<#output>>))),
|
quote!(Ok(gcore::registry::EdgeHandle::new(::std::sync::Arc::new(#struct_name::new(#(#names),*)) as ::std::sync::Arc<gcore::registry::ErasedNode<#output>>))),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
let downcasts = names.iter().zip(row.iter().enumerate()).zip(&lend_flags).map(|((name, (index, ty)), lend)| match (lend, record_carrier && index == 0) {
|
let downcasts = names.iter().zip(row.iter()).zip(&lend_flags).map(|((name, ty), lend)| match lend {
|
||||||
(true, _) => quote!(let #name = inputs.next().unwrap().downcast_lend::<#ty>()?;),
|
true => quote!(let #name = inputs.next().unwrap().downcast_lend::<#ty>()?;),
|
||||||
(false, true) => quote!(let #name = inputs.next().unwrap().downcast::<gcore::list::List<#ty>>()?;),
|
false => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;),
|
||||||
(false, false) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;),
|
|
||||||
});
|
});
|
||||||
quote! {
|
quote! {
|
||||||
gcore::registry::RegistryEntry {
|
gcore::registry::RegistryEntry {
|
||||||
|
|||||||
@@ -716,6 +716,16 @@ fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<Pa
|
|||||||
let field = parse_field(pat_ident.clone(), (**ty).clone(), attrs).map_err(|e| Error::new_spanned(pat_ident, format!("Failed to parse argument '{}': {}", pat_ident.ident, e)))?;
|
let field = parse_field(pat_ident.clone(), (**ty).clone(), attrs).map_err(|e| Error::new_spanned(pat_ident, format!("Failed to parse argument '{}': {}", pat_ident.ident, e)))?;
|
||||||
fields.push(field);
|
fields.push(field);
|
||||||
}
|
}
|
||||||
|
} else if let Pat::Wild(wild) = &**pat {
|
||||||
|
let pat_ident = PatIdent {
|
||||||
|
attrs: wild.attrs.clone(),
|
||||||
|
by_ref: None,
|
||||||
|
mutability: None,
|
||||||
|
ident: format_ident!("_unit{}", index, span = wild.underscore_token.span),
|
||||||
|
subpat: None,
|
||||||
|
};
|
||||||
|
let field = parse_field(pat_ident, (**ty).clone(), attrs).map_err(|e| Error::new_spanned(pat, format!("Failed to parse argument: {e}")))?;
|
||||||
|
fields.push(field);
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::new_spanned(pat, "Expected a simple identifier for the field name"));
|
return Err(Error::new_spanned(pat, "Expected a simple identifier for the field name"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,28 +49,64 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
|
|||||||
emit_error!(parsed.output_type.span(), "attribute io needs a plain or `Result<_, Interrupt>` kernel, not a `GPoll` one");
|
emit_error!(parsed.output_type.span(), "attribute io needs a plain or `Result<_, Interrupt>` kernel, not a `GPoll` one");
|
||||||
}
|
}
|
||||||
|
|
||||||
match parsed.fields.first() {
|
|
||||||
None => emit_error!(
|
|
||||||
parsed.fn_name.span(),
|
|
||||||
"attribute io needs a value carrier as the first parameter after the context"
|
|
||||||
),
|
|
||||||
Some(carrier) => {
|
|
||||||
let valid = !carrier.is_data_field
|
|
||||||
&& matches!(&carrier.ty, ParsedFieldType::Regular(RegularParsedField { ty, lend: None, .. }) if !matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty()));
|
|
||||||
if !valid {
|
|
||||||
emit_error!(
|
|
||||||
carrier.pat_ident.span(),
|
|
||||||
"attribute io needs a value carrier as the first parameter after the context: an owned element type, not `()`, `#[data]`, `&T`, or `impl Node`"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for field in parsed.fields.iter().skip(1) {
|
for field in parsed.fields.iter().skip(1) {
|
||||||
if matches!(field.ty, ParsedFieldType::Node(_)) {
|
if matches!(field.ty, ParsedFieldType::Node(_)) {
|
||||||
emit_error!(field.pat_ident.span(), "record nodes take no lazy inputs yet");
|
emit_error!(field.pat_ident.span(), "record nodes take no lazy inputs yet");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some(carrier) = parsed.fields.first() else {
|
||||||
|
emit_error!(
|
||||||
|
parsed.fn_name.span(),
|
||||||
|
"attribute io needs a primary input as the first parameter after the context (`_: ()` for none)"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let carrier_ty = match &carrier.ty {
|
||||||
|
ParsedFieldType::Regular(RegularParsedField { ty, lend: None, .. }) if !carrier.is_data_field => Some(ty),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let Some(carrier_ty) = carrier_ty else {
|
||||||
|
emit_error!(
|
||||||
|
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`"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let no_carrier = matches!(carrier_ty, Type::Tuple(tuple) if tuple.elems.is_empty());
|
||||||
|
if no_carrier && !parsed.attribute_reads.is_empty() {
|
||||||
|
emit_error!(carrier.pat_ident.span(), "a node without a primary input has no attributes to read");
|
||||||
|
}
|
||||||
|
let token = match (no_carrier, &carrier.ty) {
|
||||||
|
(false, ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. })) if implementations.is_empty() => crate::codegen::unbounded_generic(parsed, ty),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let element = writes.as_ref().map(|writes| &writes.element).unwrap_or(&value);
|
||||||
|
match &token {
|
||||||
|
Some(token) => {
|
||||||
|
if !matches!(crate::codegen::bare_ident(element), Some(ident) if ident == token) {
|
||||||
|
emit_error!(
|
||||||
|
parsed.output_type.span(),
|
||||||
|
"a generic element passes through unchanged: return `{}` in the first tuple position",
|
||||||
|
token
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if let Some(ident) = crate::codegen::unbounded_generic(parsed, element) {
|
||||||
|
emit_error!(parsed.output_type.span(), "the returned generic element `{}` has no matching input", ident);
|
||||||
|
} else if !no_carrier && crate::codegen::contains_open_generic(parsed, carrier_ty) {
|
||||||
|
emit_error!(
|
||||||
|
carrier.pat_ident.span(),
|
||||||
|
"record element reads are monomorphic for now; use a concrete element type or an unbounded passthrough generic"
|
||||||
|
);
|
||||||
|
} else if crate::codegen::contains_open_generic(parsed, element) {
|
||||||
|
emit_error!(parsed.output_type.span(), "a written element must be a concrete type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut seen_reads: Vec<String> = Vec::new();
|
let mut seen_reads: Vec<String> = Vec::new();
|
||||||
for read in &parsed.attribute_reads {
|
for read in &parsed.attribute_reads {
|
||||||
let marker = read.marker.to_token_stream().to_string();
|
let marker = read.marker.to_token_stream().to_string();
|
||||||
@@ -276,7 +312,17 @@ fn validate_primary_input_expose(parsed: &ParsedNodeFn) {
|
|||||||
fn validate_implementations_for_generics(parsed: &ParsedNodeFn) {
|
fn validate_implementations_for_generics(parsed: &ParsedNodeFn) {
|
||||||
let has_skip_impl = parsed.attributes.skip_impl;
|
let has_skip_impl = parsed.attributes.skip_impl;
|
||||||
let routing = crate::codegen::routing_io(parsed);
|
let routing = crate::codegen::routing_io(parsed);
|
||||||
let routing_source = |ty: &Type| matches!((&routing, ty), (Some(routing), Type::Path(path)) if path.path.get_ident() == Some(&routing.generic));
|
let record_token = crate::codegen::record_shape(parsed).and_then(|shape| match shape.carrier {
|
||||||
|
crate::codegen::RecordCarrier::Token(token) => Some(token),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
let opaque_record_generic = |ty: &Type| {
|
||||||
|
let ident = match ty {
|
||||||
|
Type::Path(path) => path.path.get_ident(),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
ident.is_some() && (ident == routing.as_ref().map(|routing| &routing.generic) || ident == record_token.as_ref())
|
||||||
|
};
|
||||||
|
|
||||||
if !has_skip_impl && !parsed.fn_generics.is_empty() {
|
if !has_skip_impl && !parsed.fn_generics.is_empty() {
|
||||||
for field in &parsed.fields {
|
for field in &parsed.fields {
|
||||||
@@ -288,7 +334,7 @@ fn validate_implementations_for_generics(parsed: &ParsedNodeFn) {
|
|||||||
let pat_ident = &field.pat_ident;
|
let pat_ident = &field.pat_ident;
|
||||||
match &field.ty {
|
match &field.ty {
|
||||||
ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. }) => {
|
ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. }) => {
|
||||||
if routing_source(ty) {
|
if opaque_record_generic(ty) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if contains_generic_param(ty, &parsed.fn_generics) && implementations.is_empty() {
|
if contains_generic_param(ty, &parsed.fn_generics) && implementations.is_empty() {
|
||||||
@@ -308,7 +354,7 @@ fn validate_implementations_for_generics(parsed: &ParsedNodeFn) {
|
|||||||
implementations,
|
implementations,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
if routing_source(output_type) {
|
if opaque_record_generic(output_type) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (contains_generic_param(input_type, &parsed.fn_generics) || contains_generic_param(output_type, &parsed.fn_generics)) && implementations.is_empty() {
|
if (contains_generic_param(input_type, &parsed.fn_generics) || contains_generic_param(output_type, &parsed.fn_generics)) && implementations.is_empty() {
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
//! Pilot record nodes exercising the macro's attribute io over materialized
|
//! Pilot record nodes exercising the macro's record-tier attribute io:
|
||||||
//! lists: `Attr<A>` reads, tuple writes, column carry, and the census
|
//! offset reads and writes against record edges, the ElToken byte-carry for
|
||||||
//! defaults. These are the flat-wave law tests; the node forms are the
|
//! passthrough elements, and the `_: ()` no-carrier form. These are the
|
||||||
//! production authoring surface, the list driver behind them is interim.
|
//! flat-wave law tests; the node forms are the production authoring surface,
|
||||||
|
//! and the wiring is by hand until the compiler pass constructs layouts.
|
||||||
|
|
||||||
use core_types::attribute::{Attr, Opacity};
|
use core_types::attribute::{Attr, Opacity};
|
||||||
use core_types::gpoll::{GraphError, Interrupt};
|
use core_types::context::ExtractArena;
|
||||||
|
use core_types::gpoll::{ErrorKind, GraphError, Interrupt};
|
||||||
use core_types::{Context, Ctx};
|
use core_types::{Context, Ctx};
|
||||||
|
|
||||||
core_types::attribute! {
|
core_types::attribute! {
|
||||||
/// Test-only measured length of an element.
|
/// Test-only measured length of an element.
|
||||||
pub Length("length"): f64;
|
pub Length("length"): f64;
|
||||||
|
/// Test-only label parked in the arena by its writer.
|
||||||
|
pub Label("label"): &str;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Test"))]
|
#[node_macro::node(category("Test"))]
|
||||||
@@ -40,6 +44,26 @@ fn scale(_: impl Ctx, element: f64, factor: &f64, opacity: Attr<Opacity>) -> (f6
|
|||||||
(element * *factor, Attr(*opacity))
|
(element * *factor, Attr(*opacity))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[node_macro::node(category("Test"))]
|
||||||
|
fn fade<T>(_: impl Ctx, element: T, factor: f64, opacity: Attr<Opacity>) -> (T, Attr<Opacity>) {
|
||||||
|
(element, Attr(*opacity * factor))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[node_macro::node(category("Test"))]
|
||||||
|
fn source_opacity(_: impl Ctx, _: (), element: f64, opacity: f64) -> (f64, Attr<Opacity>) {
|
||||||
|
(element, Attr(opacity))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[node_macro::node(category("Test"))]
|
||||||
|
fn label<'e>(ctx: impl Ctx + ExtractArena<'e>, element: f64, text: String, label: Attr<Label>) -> Result<(f64, Attr<'e, Label>), Interrupt> {
|
||||||
|
let joined = format!("{}{text}", *label);
|
||||||
|
let (parked, _) = ctx.arena().alloc(joined).ok_or(GraphError {
|
||||||
|
kind: ErrorKind::ArenaExhausted,
|
||||||
|
trace: Vec::new(),
|
||||||
|
})?;
|
||||||
|
Ok((element, Attr(parked.as_str())))
|
||||||
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Test"))]
|
#[node_macro::node(category("Test"))]
|
||||||
fn pick<T>(ctx: impl Ctx, take_second: bool, first: impl Node<Context<'_>, Output = T>, second: impl Node<Context<'_>, Output = T>) -> Result<T, Interrupt> {
|
fn pick<T>(ctx: impl Ctx, take_second: bool, first: impl Node<Context<'_>, Output = T>, second: impl Node<Context<'_>, Output = T>) -> Result<T, Interrupt> {
|
||||||
if take_second { second.eval(ctx) } else { first.eval(ctx) }
|
if take_second { second.eval(ctx) } else { first.eval(ctx) }
|
||||||
@@ -57,6 +81,7 @@ fn forward_record<T>(_: impl Ctx, element: T) -> T {
|
|||||||
element
|
element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -65,8 +90,8 @@ mod tests {
|
|||||||
use core_types::attribute::Attribute as AttributeMarker;
|
use core_types::attribute::Attribute as AttributeMarker;
|
||||||
use core_types::context::{ContextImpl, EvalScope};
|
use core_types::context::{ContextImpl, EvalScope};
|
||||||
use core_types::gpoll::GPoll;
|
use core_types::gpoll::GPoll;
|
||||||
use core_types::list::List;
|
|
||||||
use core_types::node::Node;
|
use core_types::node::Node;
|
||||||
|
use core_types::record::{Layout, Rec, RecordSource, RecordValue, stack};
|
||||||
|
|
||||||
struct ValueNode<T>(T);
|
struct ValueNode<T>(T);
|
||||||
|
|
||||||
@@ -78,13 +103,30 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PartialListNode(List<f64>);
|
struct RecordSourceNode<E> {
|
||||||
|
frame_bytes: usize,
|
||||||
|
element: E,
|
||||||
|
fields: Vec<(usize, f64)>,
|
||||||
|
partial: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl<Input> Node<Input> for PartialListNode {
|
impl<'e, E: Copy> Node<ContextImpl<'e>> for RecordSourceNode<E> {
|
||||||
type Output = List<f64>;
|
type Output = RecordValue<'e>;
|
||||||
|
|
||||||
fn eval(&self, _input: &Input) -> GPoll<List<f64>> {
|
fn eval(&self, _input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
|
||||||
GPoll::Partial(self.0.clone())
|
let dst = stack::push(self.frame_bytes);
|
||||||
|
let value = unsafe {
|
||||||
|
dst.cast::<E>().write(self.element);
|
||||||
|
for (offset, value) in &self.fields {
|
||||||
|
dst.add(*offset).cast::<f64>().write(*value);
|
||||||
|
}
|
||||||
|
RecordValue::from_rec(Rec::new(dst))
|
||||||
|
};
|
||||||
|
stack::pop(dst);
|
||||||
|
match self.partial {
|
||||||
|
true => GPoll::Partial(value),
|
||||||
|
false => GPoll::Final(value),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,12 +134,26 @@ mod tests {
|
|||||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn elements(list: &List<f64>) -> Vec<f64> {
|
fn f64_layout(names: &[&'static str]) -> Layout {
|
||||||
list.iter_element_values().copied().collect()
|
let writes: Vec<(&'static str, u8, usize, usize)> = names.iter().map(|name| (*name, 0, 8, 8)).collect();
|
||||||
|
Layout::default().with_writes(0, (8, 8), &writes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn column(list: &List<f64>, key: &str) -> Vec<f64> {
|
fn frame_bytes(layout: &Layout) -> usize {
|
||||||
list.iter_attribute_values::<f64>(key).unwrap().copied().collect()
|
layout.size.next_multiple_of(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reserve_for(layouts: &[&Layout]) {
|
||||||
|
stack::reserve(layouts.iter().map(|layout| frame_bytes(layout)).sum());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bare_source(layout: &Layout, element: f64) -> RecordSourceNode<f64> {
|
||||||
|
RecordSourceNode {
|
||||||
|
frame_bytes: frame_bytes(layout),
|
||||||
|
element,
|
||||||
|
fields: vec![],
|
||||||
|
partial: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -107,15 +163,23 @@ mod tests {
|
|||||||
let scope = scope_fixture(&generations, &arena);
|
let scope = scope_fixture(&generations, &arena);
|
||||||
let ctx = ContextImpl::root(&scope);
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
let chain = MultiplyOpacityNode::new(
|
let source_layout = f64_layout(&[]);
|
||||||
MultiplyOpacityNode::new(ValueNode(List::from_element_values(vec![1., 2.])), ValueNode(0.5)),
|
let modified = multiply_opacity_layout(&source_layout);
|
||||||
|
let stacked = multiply_opacity_layout(&modified);
|
||||||
|
reserve_for(&[&source_layout, &modified, &stacked]);
|
||||||
|
|
||||||
|
let chain = MultiplyOpacityNode::wire(
|
||||||
|
MultiplyOpacityNode::wire(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout),
|
||||||
ValueNode(0.5),
|
ValueNode(0.5),
|
||||||
|
&modified,
|
||||||
);
|
);
|
||||||
let GPoll::Final(list) = chain.eval(&ctx) else {
|
assert_eq!(chain.layout(), Some(&stacked));
|
||||||
panic!("expected a final list");
|
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||||
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
assert_eq!(elements(&list), vec![1., 2.]);
|
let rec = value.rec();
|
||||||
assert_eq!(column(&list, Opacity::NAME), vec![0.25, 0.25]);
|
assert_eq!(unsafe { rec.element::<f64>() }, 2.);
|
||||||
|
assert_eq!(unsafe { rec.read::<f64>(stacked.offset_of(Opacity::NAME, 0).unwrap()) }, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -125,27 +189,38 @@ mod tests {
|
|||||||
let scope = scope_fixture(&generations, &arena);
|
let scope = scope_fixture(&generations, &arena);
|
||||||
let ctx = ContextImpl::root(&scope);
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
let chain = MeasureNode::new(ValueNode(List::from_element_values(vec![-2., 3.])));
|
let source_layout = f64_layout(&[]);
|
||||||
let GPoll::Final(list) = chain.eval(&ctx) else {
|
let measured = measure_layout(&source_layout);
|
||||||
panic!("expected a final list");
|
reserve_for(&[&source_layout, &measured]);
|
||||||
|
|
||||||
|
let chain = MeasureNode::wire(bare_source(&source_layout, -2.), &source_layout);
|
||||||
|
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||||
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
assert_eq!(elements(&list), vec![-2., 3.]);
|
let rec = value.rec();
|
||||||
assert_eq!(column(&list, Length::NAME), vec![2., 3.]);
|
assert_eq!(unsafe { rec.element::<f64>() }, -2.);
|
||||||
|
assert_eq!(unsafe { rec.read::<f64>(measured.offset_of(Length::NAME, 0).unwrap()) }, 2.);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn elementwise_write_carries_unrelated_columns() {
|
fn elementwise_write_carries_unrelated_fields() {
|
||||||
let arena = Arena::new(1024).unwrap();
|
let arena = Arena::new(1024).unwrap();
|
||||||
let generations = [];
|
let generations = [];
|
||||||
let scope = scope_fixture(&generations, &arena);
|
let scope = scope_fixture(&generations, &arena);
|
||||||
let ctx = ContextImpl::root(&scope);
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
let chain = MeasureNode::new(MultiplyOpacityNode::new(ValueNode(List::from_element_values(vec![-2., 3.])), ValueNode(0.5)));
|
let source_layout = f64_layout(&[]);
|
||||||
let GPoll::Final(list) = chain.eval(&ctx) else {
|
let modified = multiply_opacity_layout(&source_layout);
|
||||||
panic!("expected a final list");
|
let measured = measure_layout(&modified);
|
||||||
|
reserve_for(&[&source_layout, &modified, &measured]);
|
||||||
|
|
||||||
|
let chain = MeasureNode::wire(MultiplyOpacityNode::wire(bare_source(&source_layout, -2.), ValueNode(0.5), &source_layout), &modified);
|
||||||
|
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||||
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
assert_eq!(column(&list, Opacity::NAME), vec![0.5, 0.5]);
|
let rec = value.rec();
|
||||||
assert_eq!(column(&list, Length::NAME), vec![2., 3.]);
|
assert_eq!(unsafe { rec.read::<f64>(measured.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
|
||||||
|
assert_eq!(unsafe { rec.read::<f64>(measured.offset_of(Length::NAME, 0).unwrap()) }, 2.);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -155,18 +230,83 @@ mod tests {
|
|||||||
let scope = scope_fixture(&generations, &arena);
|
let scope = scope_fixture(&generations, &arena);
|
||||||
let ctx = ContextImpl::root(&scope);
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
let bare = ShadeNode::new(ValueNode(List::from_element_values(vec![4., 6.])));
|
let source_layout = f64_layout(&[]);
|
||||||
let GPoll::Final(list) = bare.eval(&ctx) else {
|
let modified = multiply_opacity_layout(&source_layout);
|
||||||
panic!("expected a final list");
|
let shaded = shade_layout(&modified);
|
||||||
};
|
reserve_for(&[&source_layout, &modified, &shaded]);
|
||||||
assert_eq!(elements(&list), vec![4., 6.]);
|
|
||||||
|
|
||||||
let shaded = ShadeNode::new(MultiplyOpacityNode::new(ValueNode(List::from_element_values(vec![4., 6.])), ValueNode(0.5)));
|
let bare = ShadeNode::wire(bare_source(&source_layout, 4.), &source_layout);
|
||||||
let GPoll::Final(list) = shaded.eval(&ctx) else {
|
let GPoll::Final(value) = bare.eval(&ctx) else {
|
||||||
panic!("expected a final list");
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
assert_eq!(elements(&list), vec![2., 3.]);
|
assert_eq!(unsafe { value.rec().element::<f64>() }, 4.);
|
||||||
assert_eq!(column(&list, Opacity::NAME), vec![0.5, 0.5]);
|
|
||||||
|
let chain = ShadeNode::wire(MultiplyOpacityNode::wire(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), &modified);
|
||||||
|
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||||
|
panic!("expected a final record");
|
||||||
|
};
|
||||||
|
let rec = value.rec();
|
||||||
|
assert_eq!(unsafe { rec.element::<f64>() }, 2.);
|
||||||
|
assert_eq!(unsafe { rec.read::<f64>(shaded.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_passthrough_carries_any_element_type() {
|
||||||
|
let arena = Arena::new(1024).unwrap();
|
||||||
|
let generations = [];
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
|
let f64_source = f64_layout(&[]);
|
||||||
|
let f64_faded = fade_layout(&f64_source);
|
||||||
|
let u32_source = Layout::default().with_writes(0, (4, 4), &[]);
|
||||||
|
let u32_faded = fade_layout(&u32_source);
|
||||||
|
reserve_for(&[&f64_source, &f64_faded, &u32_source, &u32_faded]);
|
||||||
|
|
||||||
|
let wide = FadeNode::wire(bare_source(&f64_source, 8.), ValueNode(0.5), &f64_source);
|
||||||
|
let GPoll::Final(value) = wide.eval(&ctx) else {
|
||||||
|
panic!("expected a final record");
|
||||||
|
};
|
||||||
|
let rec = value.rec();
|
||||||
|
assert_eq!(unsafe { rec.element::<f64>() }, 8.);
|
||||||
|
assert_eq!(unsafe { rec.read::<f64>(f64_faded.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
|
||||||
|
|
||||||
|
let narrow = FadeNode::wire(
|
||||||
|
RecordSourceNode {
|
||||||
|
frame_bytes: frame_bytes(&u32_source),
|
||||||
|
element: 7u32,
|
||||||
|
fields: vec![],
|
||||||
|
partial: false,
|
||||||
|
},
|
||||||
|
ValueNode(0.25),
|
||||||
|
&u32_source,
|
||||||
|
);
|
||||||
|
let GPoll::Final(value) = narrow.eval(&ctx) else {
|
||||||
|
panic!("expected a final record");
|
||||||
|
};
|
||||||
|
let rec = value.rec();
|
||||||
|
assert_eq!(unsafe { rec.element::<u32>() }, 7);
|
||||||
|
assert_eq!(unsafe { rec.read::<f64>(u32_faded.offset_of(Opacity::NAME, 0).unwrap()) }, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_carrier_form_writes_a_fresh_record() {
|
||||||
|
let arena = Arena::new(1024).unwrap();
|
||||||
|
let generations = [];
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
|
let layout = source_opacity_layout();
|
||||||
|
reserve_for(&[&layout]);
|
||||||
|
|
||||||
|
let node = SourceOpacityNode::wire(ValueNode(3.), ValueNode(0.25));
|
||||||
|
assert_eq!(Node::<ContextImpl>::layout(&node), Some(&layout));
|
||||||
|
let GPoll::Final(value) = node.eval(&ctx) else {
|
||||||
|
panic!("expected a final record");
|
||||||
|
};
|
||||||
|
let rec = value.rec();
|
||||||
|
assert_eq!(unsafe { rec.element::<f64>() }, 3.);
|
||||||
|
assert_eq!(unsafe { rec.read::<f64>(layout.offset_of(Opacity::NAME, 0).unwrap()) }, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -176,46 +316,48 @@ mod tests {
|
|||||||
let scope = scope_fixture(&generations, &arena);
|
let scope = scope_fixture(&generations, &arena);
|
||||||
let ctx = ContextImpl::root(&scope);
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
let chain = MultiplyOpacityNode::new(PartialListNode(List::from_element_values(vec![1.])), ValueNode(0.5));
|
let source_layout = f64_layout(&[]);
|
||||||
let GPoll::Partial(list) = chain.eval(&ctx) else {
|
let modified = multiply_opacity_layout(&source_layout);
|
||||||
panic!("expected a partial list");
|
reserve_for(&[&source_layout, &modified]);
|
||||||
|
|
||||||
|
let chain = MultiplyOpacityNode::wire(
|
||||||
|
RecordSourceNode {
|
||||||
|
frame_bytes: frame_bytes(&source_layout),
|
||||||
|
element: 1.,
|
||||||
|
fields: vec![],
|
||||||
|
partial: true,
|
||||||
|
},
|
||||||
|
ValueNode(0.5),
|
||||||
|
&source_layout,
|
||||||
|
);
|
||||||
|
let GPoll::Partial(value) = chain.eval(&ctx) else {
|
||||||
|
panic!("expected a partial record");
|
||||||
};
|
};
|
||||||
assert_eq!(column(&list, Opacity::NAME), vec![0.5]);
|
assert_eq!(unsafe { value.rec().read::<f64>(modified.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
use core_types::context::ExtractFrame;
|
#[test]
|
||||||
use core_types::record::{Frame, FrameLayout, Layout, Rec, RecordSource, RecordValue};
|
fn interrupt_kernel_errors_stop_the_eval() {
|
||||||
|
let arena = Arena::new(1024).unwrap();
|
||||||
|
let generations = [];
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
struct RecordSourceNode {
|
let source_layout = f64_layout(&[]);
|
||||||
slot: usize,
|
let modified = checked_multiply_opacity_layout(&source_layout);
|
||||||
element: f64,
|
reserve_for(&[&source_layout, &modified]);
|
||||||
fields: Vec<(usize, f64)>,
|
|
||||||
partial: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'e> Node<ContextImpl<'e>> for RecordSourceNode {
|
let ok = CheckedMultiplyOpacityNode::wire(bare_source(&source_layout, 1.), ValueNode(0.5), &source_layout);
|
||||||
type Output = RecordValue<'e>;
|
let GPoll::Final(value) = ok.eval(&ctx) else {
|
||||||
|
panic!("expected a final record");
|
||||||
|
};
|
||||||
|
assert_eq!(unsafe { value.rec().read::<f64>(modified.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
|
||||||
|
|
||||||
fn eval(&self, input: &ContextImpl<'e>) -> GPoll<RecordValue<'e>> {
|
let failing = CheckedMultiplyOpacityNode::wire(bare_source(&source_layout, 1.), ValueNode(-1.), &source_layout);
|
||||||
let frame = ExtractFrame::frame(input).unwrap();
|
let GPoll::Error(error) = failing.eval(&ctx) else {
|
||||||
let value = unsafe {
|
panic!("expected an error");
|
||||||
let dst = frame.slot(self.slot);
|
};
|
||||||
dst.cast::<f64>().write(self.element);
|
assert!(error.kind == "negative factor");
|
||||||
for (offset, value) in &self.fields {
|
|
||||||
dst.add(*offset).cast::<f64>().write(*value);
|
|
||||||
}
|
|
||||||
RecordValue::from_rec(Rec::new(dst))
|
|
||||||
};
|
|
||||||
match self.partial {
|
|
||||||
true => GPoll::Partial(value),
|
|
||||||
false => GPoll::Final(value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn f64_layout(names: &[&'static str]) -> Layout {
|
|
||||||
let writes: Vec<(&'static str, u8, usize, usize)> = names.iter().map(|name| (*name, 0, 8, 8)).collect();
|
|
||||||
Layout::default().with_writes(0, (8, 8), &writes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static FACTOR: f64 = 3.;
|
static FACTOR: f64 = 3.;
|
||||||
@@ -237,61 +379,88 @@ mod tests {
|
|||||||
let scope = scope_fixture(&generations, &arena);
|
let scope = scope_fixture(&generations, &arena);
|
||||||
let ctx = ContextImpl::root(&scope);
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
let chain = ScaleNode::new(
|
let source_layout = f64_layout(&[]);
|
||||||
MultiplyOpacityNode::new(ValueNode(List::from_element_values(vec![1., 2.])), ValueNode(0.5)),
|
let modified = multiply_opacity_layout(&source_layout);
|
||||||
|
let scaled = scale_layout(&modified);
|
||||||
|
reserve_for(&[&source_layout, &modified, &scaled]);
|
||||||
|
|
||||||
|
let chain = ScaleNode::wire(
|
||||||
|
MultiplyOpacityNode::wire(bare_source(&source_layout, 2.), ValueNode(0.5), &source_layout),
|
||||||
StaticLendNode(&FACTOR),
|
StaticLendNode(&FACTOR),
|
||||||
|
&modified,
|
||||||
);
|
);
|
||||||
let GPoll::Final(list) = chain.eval(&ctx) else {
|
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||||
panic!("expected a final list");
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
assert_eq!(elements(&list), vec![3., 6.]);
|
let rec = value.rec();
|
||||||
assert_eq!(column(&list, Opacity::NAME), vec![0.5, 0.5]);
|
assert_eq!(unsafe { rec.element::<f64>() }, 6.);
|
||||||
|
assert_eq!(unsafe { rec.read::<f64>(scaled.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parked_reference_attributes_write_and_carry() {
|
||||||
|
let arena = Arena::new(1024).unwrap();
|
||||||
|
let generations = [];
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
|
let source_layout = f64_layout(&[]);
|
||||||
|
let labeled = label_layout(&source_layout);
|
||||||
|
let relabeled = label_layout(&labeled);
|
||||||
|
reserve_for(&[&source_layout, &labeled, &relabeled]);
|
||||||
|
|
||||||
|
let chain = LabelNode::wire(
|
||||||
|
LabelNode::wire(bare_source(&source_layout, 1.), ValueNode(String::from("a")), &source_layout),
|
||||||
|
ValueNode(String::from("b")),
|
||||||
|
&labeled,
|
||||||
|
);
|
||||||
|
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||||
|
panic!("expected a final record");
|
||||||
|
};
|
||||||
|
let rec = value.rec();
|
||||||
|
assert_eq!(unsafe { rec.element::<f64>() }, 1.);
|
||||||
|
assert_eq!(unsafe { rec.read::<&str>(relabeled.offset_of(Label::NAME, 0).unwrap()) }, "ab");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn census_fills_reference_defaults_from_static_data() {
|
||||||
|
let source = f64_layout(&[]);
|
||||||
|
let labeled = Layout::default().with_writes(0, (8, 8), &[(Label::NAME, 0, 16, 8)]);
|
||||||
|
|
||||||
|
let plan = core_types::record::SourcePlan::new(&source, &labeled).unwrap();
|
||||||
|
let record = [5f64];
|
||||||
|
let mut buffer = vec![0u64; labeled.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::<&str>(labeled.offset_of(Label::NAME, 0).unwrap()) }, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn f64_record_source(layout: &Layout, element: f64, fields: Vec<(usize, f64)>) -> RecordSourceNode<f64> {
|
||||||
|
RecordSourceNode {
|
||||||
|
frame_bytes: frame_bytes(layout),
|
||||||
|
element,
|
||||||
|
fields,
|
||||||
|
partial: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn routing_unions_branch_layouts_and_fills_census_defaults() {
|
fn routing_unions_branch_layouts_and_fills_census_defaults() {
|
||||||
let arena = Arena::new(1024).unwrap();
|
let arena = Arena::new(1024).unwrap();
|
||||||
let generations = [];
|
let generations = [];
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
let layout_a = f64_layout(&["opacity"]);
|
let layout_a = f64_layout(&["opacity"]);
|
||||||
let layout_b = f64_layout(&["length"]);
|
let layout_b = f64_layout(&["length"]);
|
||||||
let union = Layout::union(&[&layout_a, &layout_b]);
|
let union = Layout::union(&[&layout_a, &layout_b]);
|
||||||
|
reserve_for(&[&layout_a, &layout_b, &union, &union]);
|
||||||
let mut frame_layout = FrameLayout::default();
|
|
||||||
let slot_a = frame_layout.slot(&layout_a);
|
|
||||||
let slot_b = frame_layout.slot(&layout_b);
|
|
||||||
let translate_a = frame_layout.slot(&union);
|
|
||||||
let translate_b = frame_layout.slot(&union);
|
|
||||||
let frame = Frame::new(frame_layout.size());
|
|
||||||
|
|
||||||
let scope = scope_fixture(&generations, &arena).with_frame(&frame);
|
|
||||||
let ctx = ContextImpl::root(&scope);
|
|
||||||
|
|
||||||
let taken = |second: bool| {
|
let taken = |second: bool| {
|
||||||
PickNode::new(
|
PickNode::new(
|
||||||
ValueNode(second),
|
ValueNode(second),
|
||||||
RecordSource::wire(
|
RecordSource::wire(f64_record_source(&layout_a, 1., vec![(layout_a.offset_of("opacity", 0).unwrap(), 0.5)]), &layout_a, &union),
|
||||||
RecordSourceNode {
|
RecordSource::wire(f64_record_source(&layout_b, 3., vec![(layout_b.offset_of("length", 0).unwrap(), 3.)]), &layout_b, &union),
|
||||||
slot: slot_a,
|
|
||||||
element: 1.,
|
|
||||||
fields: vec![(layout_a.offset_of("opacity", 0).unwrap(), 0.5)],
|
|
||||||
partial: false,
|
|
||||||
},
|
|
||||||
&layout_a,
|
|
||||||
&union,
|
|
||||||
translate_a,
|
|
||||||
),
|
|
||||||
RecordSource::wire(
|
|
||||||
RecordSourceNode {
|
|
||||||
slot: slot_b,
|
|
||||||
element: 3.,
|
|
||||||
fields: vec![(layout_b.offset_of("length", 0).unwrap(), 3.)],
|
|
||||||
partial: false,
|
|
||||||
},
|
|
||||||
&layout_b,
|
|
||||||
&union,
|
|
||||||
translate_b,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -316,45 +485,18 @@ mod tests {
|
|||||||
fn routing_provenance_survives_later_evaluations() {
|
fn routing_provenance_survives_later_evaluations() {
|
||||||
let arena = Arena::new(1024).unwrap();
|
let arena = Arena::new(1024).unwrap();
|
||||||
let generations = [];
|
let generations = [];
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
let layout_a = f64_layout(&["opacity"]);
|
let layout_a = f64_layout(&["opacity"]);
|
||||||
let layout_b = f64_layout(&["length"]);
|
let layout_b = f64_layout(&["length"]);
|
||||||
let union = Layout::union(&[&layout_a, &layout_b]);
|
let union = Layout::union(&[&layout_a, &layout_b]);
|
||||||
|
reserve_for(&[&layout_a, &layout_b, &union, &union]);
|
||||||
let mut frame_layout = FrameLayout::default();
|
|
||||||
let slot_a = frame_layout.slot(&layout_a);
|
|
||||||
let slot_b = frame_layout.slot(&layout_b);
|
|
||||||
let translate_a = frame_layout.slot(&union);
|
|
||||||
let translate_b = frame_layout.slot(&union);
|
|
||||||
let frame = Frame::new(frame_layout.size());
|
|
||||||
|
|
||||||
let scope = scope_fixture(&generations, &arena).with_frame(&frame);
|
|
||||||
let ctx = ContextImpl::root(&scope);
|
|
||||||
|
|
||||||
let chain = HoldFirstNode::new(
|
let chain = HoldFirstNode::new(
|
||||||
ValueNode(false),
|
ValueNode(false),
|
||||||
RecordSource::wire(
|
RecordSource::wire(f64_record_source(&layout_a, 1., vec![(layout_a.offset_of("opacity", 0).unwrap(), 0.5)]), &layout_a, &union),
|
||||||
RecordSourceNode {
|
RecordSource::wire(f64_record_source(&layout_b, 3., vec![(layout_b.offset_of("length", 0).unwrap(), 3.)]), &layout_b, &union),
|
||||||
slot: slot_a,
|
|
||||||
element: 1.,
|
|
||||||
fields: vec![(layout_a.offset_of("opacity", 0).unwrap(), 0.5)],
|
|
||||||
partial: false,
|
|
||||||
},
|
|
||||||
&layout_a,
|
|
||||||
&union,
|
|
||||||
translate_a,
|
|
||||||
),
|
|
||||||
RecordSource::wire(
|
|
||||||
RecordSourceNode {
|
|
||||||
slot: slot_b,
|
|
||||||
element: 3.,
|
|
||||||
fields: vec![(layout_b.offset_of("length", 0).unwrap(), 3.)],
|
|
||||||
partial: false,
|
|
||||||
},
|
|
||||||
&layout_b,
|
|
||||||
&union,
|
|
||||||
translate_b,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let GPoll::Final(value) = chain.eval(&ctx) else {
|
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||||
@@ -370,32 +512,25 @@ mod tests {
|
|||||||
fn identity_layouts_forward_the_record_pointer() {
|
fn identity_layouts_forward_the_record_pointer() {
|
||||||
let arena = Arena::new(1024).unwrap();
|
let arena = Arena::new(1024).unwrap();
|
||||||
let generations = [];
|
let generations = [];
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
let layout = f64_layout(&["opacity"]);
|
|
||||||
let mut frame_layout = FrameLayout::default();
|
|
||||||
let slot = frame_layout.slot(&layout);
|
|
||||||
let frame = Frame::new(frame_layout.size());
|
|
||||||
|
|
||||||
let scope = scope_fixture(&generations, &arena).with_frame(&frame);
|
|
||||||
let ctx = ContextImpl::root(&scope);
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
|
let layout = f64_layout(&["opacity"]);
|
||||||
|
reserve_for(&[&layout]);
|
||||||
|
let base = stack::push(0);
|
||||||
|
stack::pop(base);
|
||||||
|
|
||||||
let chain = ForwardRecordNode::new(RecordSource::wire(
|
let chain = ForwardRecordNode::new(RecordSource::wire(
|
||||||
RecordSourceNode {
|
f64_record_source(&layout, 4., vec![(layout.offset_of("opacity", 0).unwrap(), 0.25)]),
|
||||||
slot,
|
|
||||||
element: 4.,
|
|
||||||
fields: vec![(layout.offset_of("opacity", 0).unwrap(), 0.25)],
|
|
||||||
partial: false,
|
|
||||||
},
|
|
||||||
&layout,
|
&layout,
|
||||||
&layout.clone(),
|
&layout.clone(),
|
||||||
0,
|
|
||||||
));
|
));
|
||||||
|
|
||||||
let GPoll::Final(value) = chain.eval(&ctx) else {
|
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||||
panic!("expected a final record");
|
panic!("expected a final record");
|
||||||
};
|
};
|
||||||
let rec = value.rec();
|
let rec = value.rec();
|
||||||
assert_eq!(rec.ptr(), unsafe { frame.slot(slot) }.cast_const());
|
assert_eq!(rec.ptr(), base.cast_const());
|
||||||
assert_eq!(unsafe { rec.element::<f64>() }, 4.);
|
assert_eq!(unsafe { rec.element::<f64>() }, 4.);
|
||||||
assert_eq!(unsafe { rec.read::<f64>(layout.offset_of("opacity", 0).unwrap()) }, 0.25);
|
assert_eq!(unsafe { rec.read::<f64>(layout.offset_of("opacity", 0).unwrap()) }, 0.25);
|
||||||
}
|
}
|
||||||
@@ -404,25 +539,21 @@ mod tests {
|
|||||||
fn partial_routing_sources_downgrade_the_output() {
|
fn partial_routing_sources_downgrade_the_output() {
|
||||||
let arena = Arena::new(1024).unwrap();
|
let arena = Arena::new(1024).unwrap();
|
||||||
let generations = [];
|
let generations = [];
|
||||||
|
let scope = scope_fixture(&generations, &arena);
|
||||||
|
let ctx = ContextImpl::root(&scope);
|
||||||
|
|
||||||
let layout = f64_layout(&["opacity"]);
|
let layout = f64_layout(&["opacity"]);
|
||||||
let mut frame_layout = FrameLayout::default();
|
reserve_for(&[&layout]);
|
||||||
let slot = frame_layout.slot(&layout);
|
|
||||||
let frame = Frame::new(frame_layout.size());
|
|
||||||
|
|
||||||
let scope = scope_fixture(&generations, &arena).with_frame(&frame);
|
|
||||||
let ctx = ContextImpl::root(&scope);
|
|
||||||
|
|
||||||
let chain = ForwardRecordNode::new(RecordSource::wire(
|
let chain = ForwardRecordNode::new(RecordSource::wire(
|
||||||
RecordSourceNode {
|
RecordSourceNode {
|
||||||
slot,
|
frame_bytes: frame_bytes(&layout),
|
||||||
element: 4.,
|
element: 4.,
|
||||||
fields: vec![],
|
fields: vec![],
|
||||||
partial: true,
|
partial: true,
|
||||||
},
|
},
|
||||||
&layout,
|
&layout,
|
||||||
&layout.clone(),
|
&layout.clone(),
|
||||||
0,
|
|
||||||
));
|
));
|
||||||
|
|
||||||
let GPoll::Partial(value) = chain.eval(&ctx) else {
|
let GPoll::Partial(value) = chain.eval(&ctx) else {
|
||||||
@@ -430,24 +561,4 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert_eq!(unsafe { value.rec().element::<f64>() }, 4.);
|
assert_eq!(unsafe { value.rec().element::<f64>() }, 4.);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn interrupt_kernel_errors_stop_the_eval() {
|
|
||||||
let arena = Arena::new(1024).unwrap();
|
|
||||||
let generations = [];
|
|
||||||
let scope = scope_fixture(&generations, &arena);
|
|
||||||
let ctx = ContextImpl::root(&scope);
|
|
||||||
|
|
||||||
let ok = CheckedMultiplyOpacityNode::new(ValueNode(List::from_element_values(vec![1.])), ValueNode(0.5));
|
|
||||||
let GPoll::Final(list) = ok.eval(&ctx) else {
|
|
||||||
panic!("expected a final list");
|
|
||||||
};
|
|
||||||
assert_eq!(column(&list, Opacity::NAME), vec![0.5]);
|
|
||||||
|
|
||||||
let failing = CheckedMultiplyOpacityNode::new(ValueNode(List::from_element_values(vec![1.])), ValueNode(-1.));
|
|
||||||
let GPoll::Error(error) = failing.eval(&ctx) else {
|
|
||||||
panic!("expected an error");
|
|
||||||
};
|
|
||||||
assert!(error.kind == "negative factor");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user