mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Adopt tuple attribute io and the carrier lowering, deleting the bridge adapters for record-only wires
This commit is contained in:
@@ -263,27 +263,6 @@ macro_rules! tagged_value {
|
||||
}
|
||||
}
|
||||
|
||||
/// The bridge rows for every wire type a value can carry, spliced
|
||||
/// while plain and record worlds coexist.
|
||||
pub fn record_bridge_entries() -> Vec<(core_types::ProtoNodeIdentifier, core_types::registry::RegistryEntry)> {
|
||||
let mut entries = Vec::new();
|
||||
entries.extend(core_types::registry::record_bridge_rows::<()>());
|
||||
entries.extend(core_types::registry::record_bridge_rows::<List<f64>>());
|
||||
entries.extend(core_types::registry::record_bridge_rows::<List<Color>>());
|
||||
entries.extend(core_types::registry::record_bridge_rows::<List<GradientStops>>());
|
||||
entries.extend(core_types::registry::record_bridge_rows::<List<BrushStroke>>());
|
||||
entries.extend(core_types::registry::record_bridge_rows::<RenderOutput>());
|
||||
entries.extend(core_types::registry::record_bridge_rows::<List<NodeId>>());
|
||||
entries.extend(core_types::registry::record_bridge_rows::<DocumentNode>());
|
||||
entries.extend(core_types::registry::record_bridge_rows::<ContextModification>());
|
||||
entries.extend(core_types::registry::record_bridge_rows::<Arc<PlatformEditorApi>>());
|
||||
entries.extend(core_types::registry::record_bridge_rows::<ResourceHash>());
|
||||
$(
|
||||
entries.extend(core_types::registry::record_bridge_rows::<$ty>());
|
||||
)*
|
||||
entries
|
||||
}
|
||||
|
||||
/// Materializes the value as [`Self::to_dynany`] does, wrapped in a `ClonedNode` edge typed by [`Self::ty`].
|
||||
pub fn to_edge(self) -> Result<EdgeHandle, String> {
|
||||
match self {
|
||||
|
||||
@@ -374,40 +374,6 @@ impl ProtoNetwork {
|
||||
nullification_node_id
|
||||
}
|
||||
|
||||
/// Splices the selected `Ref` adapters between the consumer at `consumer_index` and its
|
||||
/// producers, keeping the node vec topologically sorted and ids stable. `materialized` carries
|
||||
/// the adapter ids already spliced during this update so shared producers get one adapter.
|
||||
fn insert_ref_adapters(&mut self, consumer_index: usize, adapters: &[RefAdapter], materialized: &mut HashSet<NodeId>) {
|
||||
let mut consumer_index = consumer_index;
|
||||
for adapter in adapters {
|
||||
let (_, consumer) = &self.nodes[consumer_index];
|
||||
let producer = consumer.unwrap_construction_nodes()[adapter.input_index];
|
||||
let mut path = consumer.original_location.path.clone();
|
||||
|
||||
// A path extension with a placeholder value which should not conflict with existing paths
|
||||
if let Some(path) = path.as_mut() {
|
||||
path.push(NodeId(11));
|
||||
}
|
||||
|
||||
let node = ProtoNode {
|
||||
construction_args: ConstructionArgs::Nodes(vec![producer]),
|
||||
call_argument: concrete!(Context),
|
||||
identifier: adapter.identifier.clone(),
|
||||
original_location: OriginalLocation { path, ..Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
let id = node.stable_node_id().expect("adapter nodes always produce a stable id");
|
||||
if materialized.insert(id) {
|
||||
self.nodes.insert(consumer_index, (id, node));
|
||||
consumer_index += 1;
|
||||
}
|
||||
let (_, consumer) = &mut self.nodes[consumer_index];
|
||||
if let ConstructionArgs::Nodes(args) = &mut consumer.construction_args {
|
||||
args[adapter.input_index] = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_context_dependencies(&mut self, id: NodeId) -> (ContextModification, Option<NodeId>) {
|
||||
let mut branch_dependencies = Vec::new();
|
||||
let mut combined_deps = ContextModification::default();
|
||||
@@ -696,39 +662,15 @@ impl TypingContext {
|
||||
|
||||
/// Updates the `TypingContext` with a given proto network. This will infer the types of the nodes
|
||||
/// and store them in the `inferred` field. The proto network has to be topologically sorted
|
||||
/// and contain fully resolved stable node ids. When a node's inputs disagree with every
|
||||
/// implementation only on `Ref`-ness, the matching lend/clone_out adapter is spliced into the
|
||||
/// network and inference resumes with the adapter in place.
|
||||
/// and contain fully resolved stable node ids.
|
||||
pub fn update(&mut self, network: &mut ProtoNetwork) -> Result<(), GraphErrors> {
|
||||
let mut materialized_adapters = HashSet::new();
|
||||
let mut index = 0;
|
||||
while index < network.nodes.len() {
|
||||
let (id, node) = &network.nodes[index];
|
||||
match self.infer(*id, node) {
|
||||
Ok(_) => index += 1,
|
||||
Err(errors) => match self.select_ref_adapters(node) {
|
||||
Some(adapters) => network.insert_ref_adapters(index, &adapters, &mut materialized_adapters),
|
||||
None => return Err(errors),
|
||||
},
|
||||
}
|
||||
for (id, node) in &network.nodes {
|
||||
self.infer(*id, node)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn select_ref_adapters(&self, node: &ProtoNode) -> Option<Vec<RefAdapter>> {
|
||||
let ConstructionArgs::Nodes(args) = &node.construction_args else { return None };
|
||||
let inputs = args.iter().map(|id| self.inferred.get(id).map(NodeIOTypes::ty)).collect::<Option<Vec<_>>>()?;
|
||||
let candidates: Vec<&NodeIOTypes> = self.lookup.get(&node.identifier)?.iter().map(|entry| &entry.io).collect();
|
||||
let adapters = select_ref_adapters_over(&node.call_argument, &inputs, &candidates)?;
|
||||
let constructible = adapters.iter().all(|adapter| {
|
||||
self.lookup
|
||||
.get(&adapter.identifier)
|
||||
.is_some_and(|entries| entries.iter().any(|entry| entry.io.inputs.len() == 1 && valid_type(&inputs[adapter.input_index], &entry.io.inputs[0])))
|
||||
});
|
||||
constructible.then_some(adapters)
|
||||
}
|
||||
|
||||
pub fn remove_inference(&mut self, node_id: NodeId) -> Option<NodeIOTypes> {
|
||||
self.constructor.remove(&node_id);
|
||||
self.inferred.remove(&node_id)
|
||||
@@ -916,58 +858,6 @@ fn valid_type(from: &Type, to: &Type) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct RefAdapter {
|
||||
input_index: usize,
|
||||
identifier: ProtoNodeIdentifier,
|
||||
}
|
||||
|
||||
/// Picks the lend or clone_out adapter that reconciles a proposed edge with a wanted edge when they
|
||||
/// differ only by one `Ref` layer around a concrete output.
|
||||
fn ref_adapter(proposed: &Type, wanted: &Type) -> Option<ProtoNodeIdentifier> {
|
||||
let (Type::Fn(_, proposed_output), Type::Fn(_, wanted_output)) = (proposed, wanted) else {
|
||||
return None;
|
||||
};
|
||||
match (proposed_output.as_ref(), wanted_output.as_ref()) {
|
||||
(Type::Record(inner), wanted_output @ Type::Concrete(_)) if valid_type(inner, wanted_output) => Some(ProtoNodeIdentifier::new("core_types::record::RecordExtractNode")),
|
||||
(proposed_output @ Type::Concrete(_), Type::Record(inner)) if valid_type(proposed_output, inner) => Some(ProtoNodeIdentifier::new("core_types::record::RecordLiftNode")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects adapters for the single implementation the inputs match modulo `Ref`-ness, or `None`
|
||||
/// when no or several implementations do.
|
||||
fn select_ref_adapters_over(call_argument: &Type, inputs: &[Type], candidates: &[&NodeIOTypes]) -> Option<Vec<RefAdapter>> {
|
||||
let mut selected = None;
|
||||
for candidate in candidates {
|
||||
if !valid_type(&candidate.call_argument, call_argument) || candidate.inputs.len() != inputs.len() {
|
||||
continue;
|
||||
}
|
||||
let mut adapters = Vec::new();
|
||||
let mut fits = true;
|
||||
for (input_index, (proposed, wanted)) in inputs.iter().zip(&candidate.inputs).enumerate() {
|
||||
if valid_type(proposed, wanted) {
|
||||
continue;
|
||||
}
|
||||
match ref_adapter(proposed, wanted) {
|
||||
Some(identifier) => adapters.push(RefAdapter { input_index, identifier }),
|
||||
None => {
|
||||
fits = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !fits || adapters.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if selected.is_some() {
|
||||
return None;
|
||||
}
|
||||
selected = Some(adapters);
|
||||
}
|
||||
selected
|
||||
}
|
||||
|
||||
/// Returns a list of all generic types used in the node
|
||||
fn collect_generics(types: &NodeIOTypes) -> Vec<Cow<'static, str>> {
|
||||
let inputs = [&types.call_argument].into_iter().chain(types.inputs.iter().map(|x| x.nested_type()));
|
||||
@@ -1292,126 +1182,3 @@ mod test {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod adapter_splice_test {
|
||||
use super::*;
|
||||
|
||||
fn adapter_lookup() -> &'static HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
|
||||
static LOOKUP: std::sync::LazyLock<HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>> = std::sync::LazyLock::new(|| {
|
||||
let unused: NodeConstructor = |_| Err(ConstructionError::Arity { expected: 0, got: 0 });
|
||||
[
|
||||
(
|
||||
ProtoNodeIdentifier::new("wants_owned"),
|
||||
vec![RegistryEntry {
|
||||
io: NodeIOTypes::new(concrete!(Context), concrete!(String), vec![edge_type::<String>()]),
|
||||
constructor: unused,
|
||||
}],
|
||||
),
|
||||
(
|
||||
ProtoNodeIdentifier::new("wants_owned_ambiguously"),
|
||||
vec![
|
||||
RegistryEntry {
|
||||
io: NodeIOTypes::new(concrete!(Context), concrete!(String), vec![edge_type::<String>()]),
|
||||
constructor: unused,
|
||||
},
|
||||
RegistryEntry {
|
||||
io: NodeIOTypes::new(concrete!(Context), concrete!(f64), vec![edge_type::<String>()]),
|
||||
constructor: unused,
|
||||
},
|
||||
],
|
||||
),
|
||||
(
|
||||
ProtoNodeIdentifier::new("core_types::record::RecordExtractNode"),
|
||||
vec![RegistryEntry {
|
||||
io: NodeIOTypes::new(concrete!(Context), concrete!(String), vec![record_edge_type::<String>()]),
|
||||
constructor: unused,
|
||||
}],
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
});
|
||||
&LOOKUP
|
||||
}
|
||||
|
||||
fn string_value() -> ProtoNode {
|
||||
ProtoNode::value(ConstructionArgs::Value(TaggedValue::String("landed".to_string()).into()), vec![])
|
||||
}
|
||||
|
||||
fn consumer(identifier: &str, producer: NodeId) -> ProtoNode {
|
||||
ProtoNode {
|
||||
identifier: ProtoNodeIdentifier::with_owned_string(identifier.to_string()),
|
||||
call_argument: concrete!(Context),
|
||||
construction_args: ConstructionArgs::Nodes(vec![producer]),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_record_only_mismatch_splices_the_extract_adapter() {
|
||||
let mut network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(1),
|
||||
nodes: vec![(NodeId(0), string_value()), (NodeId(1), consumer("wants_owned", NodeId(0)))],
|
||||
};
|
||||
|
||||
let mut typing = TypingContext::new(adapter_lookup());
|
||||
typing.update(&mut network).unwrap();
|
||||
|
||||
assert_eq!(network.nodes.len(), 3);
|
||||
let (adapter_id, adapter) = &network.nodes[1];
|
||||
assert_eq!(adapter.identifier.as_str(), "core_types::record::RecordExtractNode");
|
||||
assert_eq!(adapter.unwrap_construction_nodes(), vec![NodeId(0)]);
|
||||
assert_eq!(network.nodes[2].1.unwrap_construction_nodes(), vec![*adapter_id]);
|
||||
assert_eq!(typing.type_of(*adapter_id).unwrap().return_value, concrete!(String));
|
||||
assert_eq!(typing.type_of(NodeId(1)).unwrap().return_value, concrete!(String));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consumers_sharing_a_producer_share_one_adapter() {
|
||||
let mut network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(2),
|
||||
nodes: vec![
|
||||
(NodeId(0), string_value()),
|
||||
(NodeId(1), consumer("wants_owned", NodeId(0))),
|
||||
(NodeId(2), consumer("wants_owned", NodeId(0))),
|
||||
],
|
||||
};
|
||||
|
||||
let mut typing = TypingContext::new(adapter_lookup());
|
||||
typing.update(&mut network).unwrap();
|
||||
|
||||
assert_eq!(network.nodes.len(), 4);
|
||||
let adapters: Vec<NodeId> = network
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|(_, node)| node.identifier.as_str() == "core_types::record::RecordExtractNode")
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
let [adapter_id] = adapters.as_slice() else {
|
||||
panic!("expected exactly one shared adapter, got {adapters:?}");
|
||||
};
|
||||
let consumers: Vec<Vec<NodeId>> = network
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|(_, node)| node.identifier.as_str() == "wants_owned")
|
||||
.map(|(_, node)| node.unwrap_construction_nodes())
|
||||
.collect();
|
||||
assert_eq!(consumers, vec![vec![*adapter_id], vec![*adapter_id]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ambiguous_record_mismatch_stays_an_error() {
|
||||
let mut network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(1),
|
||||
nodes: vec![(NodeId(0), string_value()), (NodeId(1), consumer("wants_owned_ambiguously", NodeId(0)))],
|
||||
};
|
||||
|
||||
let mut typing = TypingContext::new(adapter_lookup());
|
||||
assert!(typing.update(&mut network).is_err());
|
||||
assert_eq!(network.nodes.len(), 2, "an ambiguous mismatch must not materialize adapters");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,11 +586,10 @@ mod test {
|
||||
let raster_list = TaggedValue::from_type(&core_types::concrete!(graphene_std::list::List<graphene_std::raster_types::Raster<graphene_std::raster_types::CPU>>)).unwrap();
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(2),
|
||||
output: NodeId(1),
|
||||
nodes: vec![
|
||||
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(raster_list.into()), vec![])),
|
||||
(NodeId(1), proto_node("graphene_core::debug::CloneNode", vec![NodeId(0)])),
|
||||
(NodeId(2), proto_node("core_types::record::RecordExtractNode", vec![NodeId(1)])),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -599,8 +598,14 @@ mod test {
|
||||
let generations = [];
|
||||
let scope = EvalScope::new(None, None, None, &generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
let result: Option<GPoll<graphene_std::list::List<graphene_std::raster_types::Raster<graphene_std::raster_types::CPU>>>> = executor.tree().eval(NodeId(2), &ctx);
|
||||
assert!(matches!(result, Some(GPoll::Final(_))), "the flipped clone must evaluate over record wires, got {result:?}");
|
||||
let edge = executor
|
||||
.tree()
|
||||
.get(NodeId(1))
|
||||
.unwrap()
|
||||
.downcast_record::<graphene_std::list::List<graphene_std::raster_types::Raster<graphene_std::raster_types::CPU>>>()
|
||||
.unwrap();
|
||||
let result = edge.eval(&ctx);
|
||||
assert!(matches!(result, GPoll::Final(_)), "the flipped clone must evaluate over record wires, got a non-final poll");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -608,12 +613,11 @@ mod test {
|
||||
let raster_list = TaggedValue::from_type(&core_types::concrete!(graphene_std::list::List<graphene_std::raster_types::Raster<graphene_std::raster_types::CPU>>)).unwrap();
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(3),
|
||||
output: NodeId(2),
|
||||
nodes: vec![
|
||||
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(raster_list.into()), vec![])),
|
||||
(NodeId(1), ProtoNode::value(ConstructionArgs::Value(TaggedValue::U32(4).into()), vec![])),
|
||||
(NodeId(2), proto_node("raster_nodes::image_color_palette::ImageColorPaletteNode", vec![NodeId(0), NodeId(1)])),
|
||||
(NodeId(3), proto_node("core_types::record::RecordExtractNode", vec![NodeId(2)])),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -622,42 +626,39 @@ mod test {
|
||||
let generations = [];
|
||||
let scope = EvalScope::new(None, None, None, &generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
let result: Option<GPoll<graphene_std::list::List<graphene_std::raster::color::Color>>> = executor.tree().eval(NodeId(3), &ctx);
|
||||
assert!(matches!(result, Some(GPoll::Final(_))), "the palette must evaluate through its record wires, got {result:?}");
|
||||
let edge = executor
|
||||
.tree()
|
||||
.get(NodeId(2))
|
||||
.unwrap()
|
||||
.downcast_record::<graphene_std::list::List<graphene_std::raster::color::Color>>()
|
||||
.unwrap();
|
||||
let result = edge.eval(&ctx);
|
||||
assert!(matches!(result, GPoll::Final(_)), "the palette must evaluate through its record wires, got a non-final poll");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_record_wire_types_wires_and_evaluates_through_the_registry() {
|
||||
fn a_value_edge_is_a_record_wire_end_to_end() {
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(2),
|
||||
nodes: vec![
|
||||
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(7.).into()), vec![])),
|
||||
(NodeId(1), proto_node("core_types::record::RecordLiftNode", vec![NodeId(0)])),
|
||||
(NodeId(2), proto_node("core_types::record::RecordExtractNode", vec![NodeId(1)])),
|
||||
],
|
||||
output: NodeId(0),
|
||||
nodes: vec![(NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(7.).into()), vec![]))],
|
||||
};
|
||||
|
||||
let executor = DynamicExecutor::new(network).unwrap();
|
||||
let lift = executor.tree().get(NodeId(1)).unwrap();
|
||||
assert_eq!(lift.ty(), &core_types::registry::record_edge_type::<f64>());
|
||||
assert!(lift.layout().is_some());
|
||||
let value = executor.tree().get(NodeId(0)).unwrap();
|
||||
assert_eq!(value.ty(), &core_types::registry::record_edge_type::<f64>());
|
||||
assert!(value.layout().is_some());
|
||||
assert_eq!((&executor).execute(()).unwrap(), GPoll::Final(TaggedValue::F64(7.)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_record_monitor_row_forwards_and_introspects_through_the_executor() {
|
||||
let mut monitor = proto_node("graphene_core::memo::MonitorNode", vec![NodeId(1)]);
|
||||
let mut monitor = proto_node("graphene_core::memo::MonitorNode", vec![NodeId(0)]);
|
||||
monitor.original_location.path = Some(vec![NodeId(9)]);
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(3),
|
||||
nodes: vec![
|
||||
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(7.).into()), vec![])),
|
||||
(NodeId(1), proto_node("core_types::record::RecordLiftNode", vec![NodeId(0)])),
|
||||
(NodeId(2), monitor),
|
||||
(NodeId(3), proto_node("core_types::record::RecordExtractNode", vec![NodeId(2)])),
|
||||
],
|
||||
output: NodeId(1),
|
||||
nodes: vec![(NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(7.).into()), vec![])), (NodeId(1), monitor)],
|
||||
};
|
||||
|
||||
let executor = DynamicExecutor::new(network).unwrap();
|
||||
@@ -671,11 +672,10 @@ mod test {
|
||||
fn a_memoize_row_wires_generically_and_replays_over_record_wires() {
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(2),
|
||||
output: NodeId(1),
|
||||
nodes: vec![
|
||||
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::String(String::from("cached")).into()), vec![])),
|
||||
(NodeId(1), proto_node("graphene_core::memo::MemoizeNode", vec![NodeId(0)])),
|
||||
(NodeId(2), proto_node("core_types::record::RecordExtractNode", vec![NodeId(1)])),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -694,15 +694,14 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_context_modification_row_wires_over_a_plain_value_wire() {
|
||||
fn a_context_modification_row_wires_over_a_value_wire() {
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(3),
|
||||
output: NodeId(2),
|
||||
nodes: vec![
|
||||
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(7.).into()), vec![])),
|
||||
(NodeId(1), modification_value()),
|
||||
(NodeId(2), proto_node("graphene_core::context_modification::ContextModificationNode", vec![NodeId(0), NodeId(1)])),
|
||||
(NodeId(3), proto_node("core_types::record::RecordExtractNode", vec![NodeId(2)])),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -710,51 +709,17 @@ mod test {
|
||||
assert_eq!((&executor).execute(()).unwrap(), GPoll::Final(TaggedValue::F64(7.)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_context_modification_over_a_wire_without_a_lift_row_reports_at_typing() {
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(3),
|
||||
nodes: vec![
|
||||
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::BrushStrokes(vec![]).into()), vec![])),
|
||||
(NodeId(1), modification_value()),
|
||||
(NodeId(2), proto_node("graphene_core::context_modification::ContextModificationNode", vec![NodeId(0), NodeId(1)])),
|
||||
(NodeId(3), proto_node("core_types::record::RecordExtractNode", vec![NodeId(2)])),
|
||||
],
|
||||
};
|
||||
|
||||
let result = DynamicExecutor::new(network);
|
||||
let error = format!("{:?}", result.err());
|
||||
assert!(!error.contains("MissingLayout"), "an absent lift row must be a typing error, not a construction failure: {error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_context_modifications_forward_the_layout() {
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(5),
|
||||
output: NodeId(4),
|
||||
nodes: vec![
|
||||
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(7.).into()), vec![])),
|
||||
(NodeId(1), modification_value()),
|
||||
(NodeId(2), proto_node("graphene_core::context_modification::ContextModificationNode", vec![NodeId(0), NodeId(1)])),
|
||||
(NodeId(3), modification_value()),
|
||||
(NodeId(4), proto_node("graphene_core::context_modification::ContextModificationNode", vec![NodeId(2), NodeId(3)])),
|
||||
(NodeId(5), proto_node("core_types::record::RecordExtractNode", vec![NodeId(4)])),
|
||||
],
|
||||
};
|
||||
|
||||
let executor = DynamicExecutor::new(network).unwrap();
|
||||
assert_eq!((&executor).execute(()).unwrap(), GPoll::Final(TaggedValue::F64(7.)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lift_adapter_is_spliced_between_a_plain_producer_and_a_record_consumer() {
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(1),
|
||||
nodes: vec![
|
||||
(NodeId(0), ProtoNode::value(ConstructionArgs::Value(TaggedValue::F64(7.).into()), vec![])),
|
||||
(NodeId(1), proto_node("core_types::record::RecordExtractNode", vec![NodeId(0)])),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -766,12 +731,11 @@ mod test {
|
||||
fn stacked_frame_memos_replay_over_record_wires() {
|
||||
let network = ProtoNetwork {
|
||||
inputs: vec![],
|
||||
output: NodeId(3),
|
||||
output: NodeId(2),
|
||||
nodes: vec![
|
||||
(NodeId(0), string_value("memoized")),
|
||||
(NodeId(1), proto_node("graphene_core::memo::FrameMemoNode", vec![NodeId(0)])),
|
||||
(NodeId(2), proto_node("graphene_core::memo::FrameMemoNode", vec![NodeId(1)])),
|
||||
(NodeId(3), proto_node("core_types::record::RecordExtractNode", vec![NodeId(2)])),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ use graphene_std::transform::Footprint;
|
||||
use graphene_std::uuid::NodeId;
|
||||
use graphene_std::vector::Vector;
|
||||
use graphene_std::{Artboard, Context, Graphic, ProtoNodeIdentifier, SourceId, concrete, fn_type};
|
||||
use node_registry_macros::{convert_node, into_node, record_extract_node, record_lift_node};
|
||||
use node_registry_macros::{convert_node, into_node};
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "gpu")]
|
||||
use wgpu_executor::WgpuExecutorHandle;
|
||||
@@ -109,98 +109,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
|
||||
// ==========
|
||||
// MEMO NODES
|
||||
// ==========
|
||||
// ============
|
||||
// REF ADAPTERS
|
||||
// ============
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[cfg(target_family = "wasm")]
|
||||
record_lift_node!(f64),
|
||||
record_extract_node!(f64),
|
||||
record_lift_node!(()),
|
||||
record_extract_node!(()),
|
||||
record_lift_node!(bool),
|
||||
record_extract_node!(bool),
|
||||
record_lift_node!(u32),
|
||||
record_extract_node!(u32),
|
||||
record_lift_node!(u64),
|
||||
record_extract_node!(u64),
|
||||
record_lift_node!(f32),
|
||||
record_extract_node!(f32),
|
||||
record_lift_node!(DVec2),
|
||||
record_extract_node!(DVec2),
|
||||
record_lift_node!(IVec2),
|
||||
record_extract_node!(IVec2),
|
||||
record_lift_node!(DAffine2),
|
||||
record_extract_node!(DAffine2),
|
||||
record_lift_node!(Option<DAffine2>),
|
||||
record_extract_node!(Option<DAffine2>),
|
||||
record_lift_node!(Footprint),
|
||||
record_extract_node!(Footprint),
|
||||
record_lift_node!(SourceId),
|
||||
record_extract_node!(SourceId),
|
||||
record_lift_node!(BlendMode),
|
||||
record_extract_node!(BlendMode),
|
||||
record_lift_node!(graphene_std::vector::style::GradientType),
|
||||
record_extract_node!(graphene_std::vector::style::GradientType),
|
||||
record_lift_node!(graphene_std::vector::style::GradientSpreadMethod),
|
||||
record_extract_node!(graphene_std::vector::style::GradientSpreadMethod),
|
||||
record_lift_node!(String),
|
||||
record_extract_node!(String),
|
||||
record_lift_node!(List<String>),
|
||||
record_extract_node!(List<String>),
|
||||
record_lift_node!(List<NodeId>),
|
||||
record_extract_node!(List<NodeId>),
|
||||
record_lift_node!(List<f64>),
|
||||
record_extract_node!(List<f64>),
|
||||
record_lift_node!(List<u8>),
|
||||
record_extract_node!(List<u8>),
|
||||
record_lift_node!(List<Vector>),
|
||||
record_extract_node!(List<Vector>),
|
||||
record_lift_node!(List<Graphic>),
|
||||
record_extract_node!(List<Graphic>),
|
||||
record_lift_node!(List<Raster<CPU>>),
|
||||
record_extract_node!(List<Raster<CPU>>),
|
||||
#[cfg(feature = "gpu")]
|
||||
record_lift_node!(List<Raster<GPU>>),
|
||||
#[cfg(feature = "gpu")]
|
||||
record_extract_node!(List<Raster<GPU>>),
|
||||
record_lift_node!(List<Color>),
|
||||
record_extract_node!(List<Color>),
|
||||
record_lift_node!(List<Artboard>),
|
||||
record_extract_node!(List<Artboard>),
|
||||
record_lift_node!(List<GradientStops>),
|
||||
record_extract_node!(List<GradientStops>),
|
||||
record_lift_node!(AttributeDyn),
|
||||
record_extract_node!(AttributeDyn),
|
||||
record_lift_node!(AttributeValueDyn),
|
||||
record_extract_node!(AttributeValueDyn),
|
||||
record_lift_node!(ListDyn),
|
||||
record_extract_node!(ListDyn),
|
||||
record_lift_node!(std::sync::Arc<PlatformEditorApi>),
|
||||
record_extract_node!(std::sync::Arc<PlatformEditorApi>),
|
||||
record_lift_node!(RuntimeHandle),
|
||||
record_extract_node!(RuntimeHandle),
|
||||
record_lift_node!(RenderIntermediate),
|
||||
record_extract_node!(RenderIntermediate),
|
||||
record_lift_node!(RenderOutput),
|
||||
record_extract_node!(RenderOutput),
|
||||
#[cfg(target_family = "wasm")]
|
||||
record_lift_node!(CanvasHandle),
|
||||
#[cfg(target_family = "wasm")]
|
||||
record_extract_node!(CanvasHandle),
|
||||
#[cfg(feature = "gpu")]
|
||||
record_lift_node!(WgpuExecutorHandle),
|
||||
#[cfg(feature = "gpu")]
|
||||
record_extract_node!(WgpuExecutorHandle),
|
||||
#[cfg(feature = "gpu")]
|
||||
record_lift_node!(Option<WgpuExecutorHandle>),
|
||||
#[cfg(feature = "gpu")]
|
||||
record_extract_node!(Option<WgpuExecutorHandle>),
|
||||
#[cfg(feature = "gpu")]
|
||||
record_lift_node!(wgpu_executor::WgpuPipelineCache),
|
||||
#[cfg(feature = "gpu")]
|
||||
record_extract_node!(wgpu_executor::WgpuPipelineCache),
|
||||
];
|
||||
// =============
|
||||
// CONVERT NODES
|
||||
@@ -228,8 +136,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> {
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
node_types.extend(graph_craft::document::value::TaggedValue::record_bridge_entries());
|
||||
node_types.extend(core_types::registry::record_bridge_rows::<graphene_std::application_io::resource::Resource>());
|
||||
|
||||
let mut map: HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>> = HashMap::new();
|
||||
let insert = |map: &mut HashMap<ProtoNodeIdentifier, Vec<RegistryEntry>>, id: ProtoNodeIdentifier, entry: RegistryEntry| {
|
||||
@@ -273,14 +179,22 @@ mod node_registry_macros {
|
||||
(
|
||||
ProtoNodeIdentifier::new(concat!["graphene_core::ops::IntoNode<", stringify!($to), ">"]),
|
||||
RegistryEntry {
|
||||
io: NodeIOTypes::new(concrete!(Context), concrete!($to), vec![fn_type!(Context, $from)]),
|
||||
io: NodeIOTypes::new(
|
||||
concrete!(Context),
|
||||
core_types::registry::record_type::<$to>(),
|
||||
vec![core_types::registry::record_edge_type::<$from>()],
|
||||
),
|
||||
constructor: |inputs| {
|
||||
if inputs.len() != 1 {
|
||||
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
|
||||
}
|
||||
let mut inputs = inputs.into_iter();
|
||||
let node = graphene_std::ops::IntoNode::<$to, _>::new(inputs.next().unwrap().downcast::<$from>()?);
|
||||
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$to>>))
|
||||
let handle = inputs.next().unwrap();
|
||||
let Some(layout) = handle.layout().cloned() else {
|
||||
return Err(ConstructionError::MissingLayout);
|
||||
};
|
||||
let node = graphene_std::ops::IntoNode::<$to, _, $from>::new(handle.downcast_record::<$from>()?, &layout);
|
||||
Ok(EdgeHandle::new_record::<$to>(std::sync::Arc::new(node) as std::sync::Arc<core_types::registry::ErasedRecordNode>))
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -334,21 +248,41 @@ mod node_registry_macros {
|
||||
RegistryEntry {
|
||||
io: NodeIOTypes::new(
|
||||
concrete!(Context),
|
||||
concrete!($to),
|
||||
vec![fn_type!(Context, $from), fn_type!(Context, $convert), fn_type!(Context, RuntimeHandle), fn_type!(Context, SourceId)],
|
||||
core_types::registry::record_type::<$to>(),
|
||||
vec![
|
||||
core_types::registry::record_edge_type::<$from>(),
|
||||
core_types::registry::record_edge_type::<$convert>(),
|
||||
core_types::registry::record_edge_type::<RuntimeHandle>(),
|
||||
core_types::registry::record_edge_type::<SourceId>(),
|
||||
],
|
||||
),
|
||||
constructor: |inputs| {
|
||||
if inputs.len() != 4 {
|
||||
return Err(ConstructionError::Arity { expected: 4, got: inputs.len() });
|
||||
}
|
||||
let mut inputs = inputs.into_iter();
|
||||
let node = graphene_std::ops::ConvertAsyncNode::<$to, _, _, _, _>::new(
|
||||
inputs.next().unwrap().downcast::<$from>()?,
|
||||
inputs.next().unwrap().downcast::<$convert>()?,
|
||||
inputs.next().unwrap().downcast::<RuntimeHandle>()?,
|
||||
inputs.next().unwrap().downcast::<SourceId>()?,
|
||||
let mut claim = || {
|
||||
let handle = inputs.next().unwrap();
|
||||
let Some(layout) = handle.layout().cloned() else {
|
||||
return Err(ConstructionError::MissingLayout);
|
||||
};
|
||||
Ok((handle, layout))
|
||||
};
|
||||
let (value, value_layout) = claim()?;
|
||||
let (converter, converter_layout) = claim()?;
|
||||
let (runtime, runtime_layout) = claim()?;
|
||||
let (source, source_layout) = claim()?;
|
||||
let node = graphene_std::ops::ConvertAsyncNode::<$to, _, _, _, _, $from, $convert>::new(
|
||||
value.downcast_record::<$from>()?,
|
||||
converter.downcast_record::<$convert>()?,
|
||||
runtime.downcast_record::<RuntimeHandle>()?,
|
||||
source.downcast_record::<SourceId>()?,
|
||||
&value_layout,
|
||||
&converter_layout,
|
||||
&runtime_layout,
|
||||
&source_layout,
|
||||
);
|
||||
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$to>>))
|
||||
Ok(EdgeHandle::new_record::<$to>(std::sync::Arc::new(node) as std::sync::Arc<core_types::registry::ErasedRecordNode>))
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -357,62 +291,69 @@ mod node_registry_macros {
|
||||
(
|
||||
ProtoNodeIdentifier::new(concat!["graphene_core::ops::ConvertNode<", stringify!($to), ">"]),
|
||||
RegistryEntry {
|
||||
io: NodeIOTypes::new(concrete!(Context), concrete!($to), vec![fn_type!(Context, $from), fn_type!(Context, $convert)]),
|
||||
io: NodeIOTypes::new(
|
||||
concrete!(Context),
|
||||
core_types::registry::record_type::<$to>(),
|
||||
vec![core_types::registry::record_edge_type::<$from>(), core_types::registry::record_edge_type::<$convert>()],
|
||||
),
|
||||
constructor: |inputs| {
|
||||
if inputs.len() != 2 {
|
||||
return Err(ConstructionError::Arity { expected: 2, got: inputs.len() });
|
||||
}
|
||||
let mut inputs = inputs.into_iter();
|
||||
let node = graphene_std::ops::ConvertNode::<$to, _, _>::new(inputs.next().unwrap().downcast::<$from>()?, inputs.next().unwrap().downcast::<$convert>()?);
|
||||
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$to>>))
|
||||
let value = inputs.next().unwrap();
|
||||
let Some(value_layout) = value.layout().cloned() else {
|
||||
return Err(ConstructionError::MissingLayout);
|
||||
};
|
||||
let converter = inputs.next().unwrap();
|
||||
let Some(converter_layout) = converter.layout().cloned() else {
|
||||
return Err(ConstructionError::MissingLayout);
|
||||
};
|
||||
let node = graphene_std::ops::ConvertNode::<$to, _, _, $from, $convert>::new(
|
||||
value.downcast_record::<$from>()?,
|
||||
converter.downcast_record::<$convert>()?,
|
||||
&value_layout,
|
||||
&converter_layout,
|
||||
);
|
||||
Ok(EdgeHandle::new_record::<$to>(std::sync::Arc::new(node) as std::sync::Arc<core_types::registry::ErasedRecordNode>))
|
||||
},
|
||||
},
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! record_lift_node {
|
||||
($type:ty) => {
|
||||
(
|
||||
ProtoNodeIdentifier::new("core_types::record::RecordLiftNode"),
|
||||
RegistryEntry {
|
||||
io: NodeIOTypes::new(concrete!(Context), core_types::registry::record_type::<$type>(), vec![fn_type!(Context, $type)]),
|
||||
constructor: |inputs| {
|
||||
if inputs.len() != 1 {
|
||||
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
|
||||
}
|
||||
let mut inputs = inputs.into_iter();
|
||||
let node = core_types::record::RecordLift::<$type, _>::new(inputs.next().unwrap().downcast::<$type>()?);
|
||||
Ok(EdgeHandle::new_record::<$type>(std::sync::Arc::new(node) as std::sync::Arc<core_types::registry::ErasedRecordNode>))
|
||||
},
|
||||
},
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! record_extract_node {
|
||||
($type:ty) => {
|
||||
(
|
||||
ProtoNodeIdentifier::new("core_types::record::RecordExtractNode"),
|
||||
RegistryEntry {
|
||||
io: NodeIOTypes::new(concrete!(Context), concrete!($type), vec![core_types::registry::record_edge_type::<$type>()]),
|
||||
constructor: |inputs| {
|
||||
if inputs.len() != 1 {
|
||||
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
|
||||
}
|
||||
let mut inputs = inputs.into_iter();
|
||||
let edge = inputs.next().unwrap();
|
||||
let layout = edge.layout().ok_or(ConstructionError::MissingLayout)?.clone();
|
||||
let node = core_types::record::RecordExtract::<$type, _>::new(edge.downcast_record::<$type>()?, &layout);
|
||||
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<$type>>))
|
||||
},
|
||||
},
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use convert_node;
|
||||
pub(crate) use into_node;
|
||||
pub(crate) use record_extract_node;
|
||||
pub(crate) use record_lift_node;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use graphene_std::Type;
|
||||
|
||||
fn is_record_edge(ty: &Type) -> bool {
|
||||
match ty {
|
||||
Type::Fn(_, output) => matches!(&**output, Type::Record(_)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// One wire kind: every row consumes and produces record wires. A plain
|
||||
/// io type here would need a bridge adapter, and those are gone.
|
||||
#[test]
|
||||
fn every_registry_row_is_record_typed() {
|
||||
let mut plain: Vec<String> = Vec::new();
|
||||
for (id, entries) in NODE_REGISTRY.iter() {
|
||||
for entry in entries {
|
||||
let plain_output = !matches!(entry.io.return_value, Type::Record(_));
|
||||
let plain_inputs: Vec<usize> = entry.io.inputs.iter().enumerate().filter(|(_, ty)| !is_record_edge(ty)).map(|(index, _)| index).collect();
|
||||
if plain_output || !plain_inputs.is_empty() {
|
||||
plain.push(format!("{} plain_output={plain_output} plain_inputs={plain_inputs:?}", id.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
plain.sort();
|
||||
assert!(plain.is_empty(), "plain io remains in the registry:\n{}", plain.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use glam::{DAffine2, DVec2};
|
||||
use std::any::TypeId;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
@@ -71,6 +72,23 @@ impl<'e, A: Attribute> std::fmt::Debug for Attr<'e, A> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A deletion of `A` in a node's return tuple: the name leaves the output
|
||||
/// layout, so downstream reads yield the declared default again. Functionally
|
||||
/// a write of the default; the value carries nothing.
|
||||
pub struct RemoveAttr<A: Attribute>(PhantomData<A>);
|
||||
|
||||
impl<A: Attribute> RemoveAttr<A> {
|
||||
pub const fn new() -> Self {
|
||||
RemoveAttr(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Attribute> Default for RemoveAttr<A> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// A census row: what is known about one declared attribute name.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct AttributeInfo {
|
||||
|
||||
@@ -186,6 +186,25 @@ impl Layout {
|
||||
}
|
||||
}
|
||||
|
||||
/// This layout minus the named fields, offsets recomputed. Removing an
|
||||
/// absent name is a no-op: downstream reads yield the default either way.
|
||||
pub fn without(&self, removes: &[(&str, u8)]) -> Layout {
|
||||
let retained: Vec<FieldWrite> = self
|
||||
.fields
|
||||
.iter()
|
||||
.filter(|field| !removes.contains(&(field.name, field.level)))
|
||||
.map(|field| FieldWrite {
|
||||
name: field.name,
|
||||
level: field.level,
|
||||
size: field.size,
|
||||
align: field.align,
|
||||
read_erased: field.read_erased,
|
||||
repark: field.repark,
|
||||
})
|
||||
.collect();
|
||||
Layout::default().with_writes(self.depth, self.element, &retained)
|
||||
}
|
||||
|
||||
/// The union of several layouts over the same element and depth.
|
||||
pub fn union(layouts: &[&Layout]) -> Layout {
|
||||
let first = layouts.first().expect("a union needs at least one layout");
|
||||
@@ -392,10 +411,18 @@ impl<'a, N> RecordEdgeInput<'a, N> {
|
||||
|
||||
/// The raw lazy edge handed to a poll kernel whose wire rides records while
|
||||
/// the kernel consumes the plain element.
|
||||
pub struct ElementEdge<'a, El, N> {
|
||||
/// # Safety
|
||||
/// `rec` must be a record of the layout the offsets were resolved against
|
||||
/// and `El` its element type; both are proven at wiring.
|
||||
unsafe fn element_only<El: Clone>(rec: Rec, _reads: &[Option<usize>]) -> El {
|
||||
unsafe { read_element::<El>(rec) }
|
||||
}
|
||||
|
||||
pub struct ElementEdge<'a, Out, N> {
|
||||
node: &'a N,
|
||||
layout: &'a Layout,
|
||||
_marker: std::marker::PhantomData<fn() -> El>,
|
||||
reads: &'a [Option<usize>],
|
||||
read: unsafe fn(Rec, &[Option<usize>]) -> Out,
|
||||
}
|
||||
|
||||
impl<'a, El: Clone, N> ElementEdge<'a, El, N> {
|
||||
@@ -403,27 +430,38 @@ impl<'a, El: Clone, N> ElementEdge<'a, El, N> {
|
||||
Self {
|
||||
node,
|
||||
layout,
|
||||
_marker: std::marker::PhantomData,
|
||||
reads: &[],
|
||||
read: element_only::<El>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval<'d, C>(&self, ctx: &C) -> GPoll<El>
|
||||
impl<'a, Out, N> ElementEdge<'a, Out, N> {
|
||||
/// `read` must be sound against the layout the offsets in `reads` were
|
||||
/// resolved from; the macro proves both at wiring.
|
||||
pub fn with_reads(node: &'a N, layout: &'a Layout, reads: &'a [Option<usize>], read: unsafe fn(Rec, &[Option<usize>]) -> Out) -> Self {
|
||||
Self { node, layout, reads, read }
|
||||
}
|
||||
|
||||
pub fn eval<'d, C>(&self, ctx: &C) -> GPoll<Out>
|
||||
where
|
||||
N: Node<C, Output = RecordValue<'d>>,
|
||||
{
|
||||
self.node.eval(ctx).map(|value| unsafe { read_element::<El>(self.layout.rec(&value)) })
|
||||
self.node.eval(ctx).map(|value| unsafe { (self.read)(self.layout.rec(&value), self.reads) })
|
||||
}
|
||||
}
|
||||
|
||||
/// The lazy input handed to a kernel whose edge rides a record wire while
|
||||
/// the kernel consumes the plain element.
|
||||
/// the kernel consumes the plain element, or the element beside its declared
|
||||
/// attribute reads.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ElementLazyInput<'a, El, N> {
|
||||
pub struct ElementLazyInput<'a, Out, N> {
|
||||
node: &'a N,
|
||||
cell: &'a crate::node::StatusCell,
|
||||
input_index: usize,
|
||||
layout: &'a Layout,
|
||||
_marker: std::marker::PhantomData<fn() -> El>,
|
||||
reads: &'a [Option<usize>],
|
||||
read: unsafe fn(Rec, &[Option<usize>]) -> Out,
|
||||
}
|
||||
|
||||
impl<'a, El: Clone, N> ElementLazyInput<'a, El, N> {
|
||||
@@ -433,16 +471,39 @@ impl<'a, El: Clone, N> ElementLazyInput<'a, El, N> {
|
||||
cell,
|
||||
input_index,
|
||||
layout,
|
||||
_marker: std::marker::PhantomData,
|
||||
reads: &[],
|
||||
read: element_only::<El>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Out, N> ElementLazyInput<'a, Out, N> {
|
||||
/// `read` must be sound against the layout the offsets in `reads` were
|
||||
/// resolved from; the macro proves both at wiring.
|
||||
pub fn with_reads(
|
||||
node: &'a N,
|
||||
cell: &'a crate::node::StatusCell,
|
||||
input_index: usize,
|
||||
layout: &'a Layout,
|
||||
reads: &'a [Option<usize>],
|
||||
read: unsafe fn(Rec, &[Option<usize>]) -> Out,
|
||||
) -> Self {
|
||||
Self {
|
||||
node,
|
||||
cell,
|
||||
input_index,
|
||||
layout,
|
||||
reads,
|
||||
read,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval<'d, C>(&self, ctx: &C) -> Result<El, crate::gpoll::Interrupt>
|
||||
pub fn eval<'d, C>(&self, ctx: &C) -> Result<Out, crate::gpoll::Interrupt>
|
||||
where
|
||||
N: Node<C, Output = RecordValue<'d>>,
|
||||
{
|
||||
let value = self.cell.eval_input(self.input_index, self.node, ctx)?;
|
||||
Ok(unsafe { read_element::<El>(self.layout.rec(&value)) })
|
||||
Ok(unsafe { (self.read)(self.layout.rec(&value), self.reads) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,8 +624,10 @@ pub mod stack {
|
||||
|
||||
/// Field-by-field carry from `from`'s layout into `to`'s, computed at
|
||||
/// wiring. The element copy is included when `carry_element` holds, which is
|
||||
/// exactly when the node does not write a concrete element itself.
|
||||
pub fn copy_plan(from: &Layout, to: &Layout, carry_element: bool) -> Vec<(usize, usize, usize)> {
|
||||
/// exactly when the node does not write a concrete element itself. `removes`
|
||||
/// names the fields the node deletes, which are exactly the ones allowed to
|
||||
/// be absent from `to`.
|
||||
pub fn copy_plan(from: &Layout, to: &Layout, carry_element: bool, removes: &[(&str, u8)]) -> Vec<(usize, usize, usize)> {
|
||||
let mut plan = Vec::new();
|
||||
if carry_element {
|
||||
assert_eq!(from.element.size, to.element.size, "a carried element must keep its size");
|
||||
@@ -573,6 +636,9 @@ pub fn copy_plan(from: &Layout, to: &Layout, carry_element: bool) -> Vec<(usize,
|
||||
}
|
||||
}
|
||||
for field in &from.fields {
|
||||
if removes.contains(&(field.name, field.level)) {
|
||||
continue;
|
||||
}
|
||||
let target = to.offset_of(field.name, field.level).expect("carried field missing from the output layout");
|
||||
plan.push((field.offset, target, field.size));
|
||||
}
|
||||
@@ -586,6 +652,53 @@ pub unsafe fn write_field<T>(dst: *mut u8, offset: usize, value: T) {
|
||||
unsafe { dst.add(offset).cast::<T>().write(value) }
|
||||
}
|
||||
|
||||
/// Finishes a carried record frame: the element lands beside the fields
|
||||
/// already carried into `dst`, inline frames copy out of the scratch bytes,
|
||||
/// and the frame releases in every branch, so the frame lifecycle closes
|
||||
/// here. Arena exhaustion of a parked element reports as an error poll.
|
||||
///
|
||||
/// # Safety
|
||||
/// `dst` must be the claimed frame (or inline scratch when `frame_bytes` is
|
||||
/// 0) of a record whose element is `T` and whose frame size is `frame_bytes`,
|
||||
/// with every carried field already written.
|
||||
pub unsafe fn lift_poll_into<'e, T: Send + Sync + 'static>(poll: GPoll<T>, dst: *mut u8, frame_bytes: usize, arena: &'e crate::arena::Arena) -> GPoll<RecordValue<'e>> {
|
||||
let release = || {
|
||||
if frame_bytes != 0 {
|
||||
stack::pop(dst);
|
||||
}
|
||||
};
|
||||
let build = |element: T| {
|
||||
let written = unsafe { write_element(dst, element, arena) };
|
||||
release();
|
||||
written.map(|()| match frame_bytes {
|
||||
0 => unsafe { dst.cast::<RecordValue>().read() },
|
||||
_ => RecordValue::spilled(unsafe { Rec::new(dst.cast_const()) }),
|
||||
})
|
||||
};
|
||||
let exhausted = || {
|
||||
GPoll::Error(Box::new(crate::gpoll::GraphError {
|
||||
kind: crate::gpoll::ErrorKind::ArenaExhausted,
|
||||
trace: Vec::new(),
|
||||
}))
|
||||
};
|
||||
match poll {
|
||||
GPoll::Final(element) => build(element).map_or_else(exhausted, GPoll::Final),
|
||||
GPoll::Partial(element) => build(element).map_or_else(exhausted, GPoll::Partial),
|
||||
GPoll::Fallback(boxed) => {
|
||||
let (element, error) = *boxed;
|
||||
build(element).map_or_else(exhausted, |value| GPoll::Fallback(Box::new((value, error))))
|
||||
}
|
||||
GPoll::Pending => {
|
||||
release();
|
||||
GPoll::Pending
|
||||
}
|
||||
GPoll::Error(error) => {
|
||||
release();
|
||||
GPoll::Error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether elements of `T` move once into the arena and ride as references:
|
||||
/// records byte-copy their contents and never run drop glue, so a type is
|
||||
/// byte-carried exactly when it has none.
|
||||
@@ -637,6 +750,28 @@ pub unsafe fn read_element<T: Clone>(rec: Rec) -> T {
|
||||
unsafe { borrow_element::<T>(rec) }.clone()
|
||||
}
|
||||
|
||||
/// Borrows a record's element for the rest of the evaluation. Parked elements
|
||||
/// borrow the arena, inline elements borrow their record value in place, and
|
||||
/// a byte-carried spilled element copies into the arena first: its stack
|
||||
/// region dies with the next push, and a borrow taken before the consumer's
|
||||
/// own frame push always has one coming. `None` reports arena exhaustion.
|
||||
///
|
||||
/// # Safety
|
||||
/// The record's element must be a `T` in the form [`element_parked`] picks,
|
||||
/// and for inline layouts the record value must outlive the borrow.
|
||||
pub unsafe fn borrow_or_park<'e, T: Send + Sync + 'static>(rec: Rec, layout: &Layout, arena: &'e crate::arena::Arena) -> Option<&'e T> {
|
||||
if element_parked::<T>() {
|
||||
return Some(unsafe { rec.element::<&T>() });
|
||||
}
|
||||
if layout.is_inline() {
|
||||
return Some(unsafe { &*rec.ptr().cast::<T>() });
|
||||
}
|
||||
// A bitwise read duplicates soundly: byte-carried elements have no drop
|
||||
// glue.
|
||||
let value = unsafe { rec.ptr().cast::<T>().read() };
|
||||
arena.alloc(value).map(|(parked, _)| parked)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `dst` must be fresh element storage of a record whose element is `T`.
|
||||
/// `None` reports arena exhaustion for a parked element.
|
||||
@@ -692,7 +827,7 @@ impl SourcePlan {
|
||||
if source == union {
|
||||
return None;
|
||||
}
|
||||
let moves = copy_plan(source, union, true);
|
||||
let moves = copy_plan(source, union, true, &[]);
|
||||
let fills = union
|
||||
.fields
|
||||
.iter()
|
||||
@@ -870,8 +1005,10 @@ impl OwnedRecord {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lifts a plain producer onto a record wire: the element lands at offset 0
|
||||
/// of a fresh element-only record, parked when it carries drop glue.
|
||||
/// Law-test scaffolding: wraps an arbitrary plain node onto a record wire
|
||||
/// (the element lands at offset 0 of a fresh element-only record, parked when
|
||||
/// it carries drop glue). No production path constructs one; value edges are
|
||||
/// [`crate::value::ValueSource`].
|
||||
pub struct RecordLift<El, N> {
|
||||
edge: N,
|
||||
layout: Layout,
|
||||
@@ -905,8 +1042,9 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the element from a record wire for a plain consumer, cloning out
|
||||
/// of the parked reference when the element carries drop glue.
|
||||
/// Law-test scaffolding: a plain probe over a record wire, cloning the
|
||||
/// element out of the parked reference when it carries drop glue. No
|
||||
/// production path constructs one.
|
||||
pub struct RecordExtract<El, N> {
|
||||
edge: N,
|
||||
layout: Layout,
|
||||
|
||||
@@ -267,49 +267,6 @@ pub struct RegistryEntry {
|
||||
pub constructor: NodeConstructor,
|
||||
}
|
||||
|
||||
/// The bridge rows of `T`: a plain producer onto a record wire and a record
|
||||
/// wire into a plain consumer. One pair exists per wire type while the
|
||||
/// deferred plain classes (shader, batch) coexist with record wires.
|
||||
pub fn record_bridge_rows<T: Clone + Send + Sync + 'static>() -> [(crate::ProtoNodeIdentifier, RegistryEntry); 2] {
|
||||
[
|
||||
(crate::ProtoNodeIdentifier::new("core_types::record::RecordLiftNode"), record_lift_entry::<T>()),
|
||||
(crate::ProtoNodeIdentifier::new("core_types::record::RecordExtractNode"), record_extract_entry::<T>()),
|
||||
]
|
||||
}
|
||||
|
||||
/// The lift bridge row for `T`: a plain producer onto a record wire. One
|
||||
/// exists per wire type while plain and record worlds coexist.
|
||||
pub fn record_lift_entry<T: Clone + Send + Sync + 'static>() -> RegistryEntry {
|
||||
RegistryEntry {
|
||||
io: NodeIOTypes::new(concrete!(Context), record_type::<T>(), vec![edge_type::<T>()]),
|
||||
constructor: |inputs| {
|
||||
if inputs.len() != 1 {
|
||||
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
|
||||
}
|
||||
let mut inputs = inputs.into_iter();
|
||||
let node = crate::record::RecordLift::<T, _>::new(inputs.next().unwrap().downcast::<T>()?);
|
||||
Ok(EdgeHandle::new_record::<T>(std::sync::Arc::new(node) as std::sync::Arc<ErasedRecordNode>))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The extract bridge row for `T`: a record wire into a plain consumer.
|
||||
pub fn record_extract_entry<T: Clone + Send + Sync + 'static>() -> RegistryEntry {
|
||||
RegistryEntry {
|
||||
io: NodeIOTypes::new(concrete!(Context), concrete!(T), vec![record_edge_type::<T>()]),
|
||||
constructor: |inputs| {
|
||||
if inputs.len() != 1 {
|
||||
return Err(ConstructionError::Arity { expected: 1, got: inputs.len() });
|
||||
}
|
||||
let mut inputs = inputs.into_iter();
|
||||
let edge = inputs.next().unwrap();
|
||||
let layout = edge.layout().ok_or(ConstructionError::MissingLayout)?.clone();
|
||||
let node = crate::record::RecordExtract::<T, _>::new(edge.downcast_record::<T>()?, &layout);
|
||||
Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc<ErasedNode<T>>))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn construct(entry: &RegistryEntry, inputs: Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError> {
|
||||
if inputs.len() != entry.io.inputs.len() {
|
||||
return Err(ConstructionError::Arity {
|
||||
|
||||
@@ -13,11 +13,41 @@ pub fn value_edge<T: Clone + crate::WasmNotSend + crate::WasmNotSync + 'static>(
|
||||
crate::registry::EdgeHandle::new(std::sync::Arc::new(ClonedNode(value)) as std::sync::Arc<crate::registry::ErasedNode<T>>)
|
||||
}
|
||||
|
||||
/// The native record edge of a constant: the element lifts onto the record
|
||||
/// wire per evaluation, so value sources need no spliced adapter.
|
||||
/// The node behind every value edge: clones its constant onto the record
|
||||
/// wire per evaluation.
|
||||
pub struct ValueSource<T> {
|
||||
value: T,
|
||||
layout: crate::record::Layout,
|
||||
}
|
||||
|
||||
impl<T: Clone + Send + Sync + 'static> ValueSource<T> {
|
||||
pub fn new(value: T) -> Self {
|
||||
Self {
|
||||
value,
|
||||
layout: crate::record::Layout::default().with_writes(0, crate::record::element_write::<T>(), &[]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'e, C, T> crate::node::Node<C> for ValueSource<T>
|
||||
where
|
||||
C: crate::context::ExtractArena<ArenaRef = &'e crate::arena::Arena>,
|
||||
T: Clone + Send + Sync + 'static,
|
||||
{
|
||||
type Output = crate::record::RecordValue<'e>;
|
||||
|
||||
fn eval(&self, input: &C) -> crate::gpoll::GPoll<crate::record::RecordValue<'e>> {
|
||||
crate::record::lift_poll(crate::gpoll::GPoll::Final(self.value.clone()), &self.layout, input.arena())
|
||||
}
|
||||
|
||||
fn layout(&self) -> Option<&crate::record::Layout> {
|
||||
Some(&self.layout)
|
||||
}
|
||||
}
|
||||
|
||||
/// The native record edge of a constant.
|
||||
pub fn record_value_edge<T: Clone + Send + Sync + 'static>(value: T) -> crate::registry::EdgeHandle {
|
||||
let node = crate::record::RecordLift::<T, _>::new(ClonedNode(value));
|
||||
crate::registry::EdgeHandle::new_record::<T>(std::sync::Arc::new(node) as std::sync::Arc<crate::registry::ErasedRecordNode>)
|
||||
crate::registry::EdgeHandle::new_record::<T>(std::sync::Arc::new(ValueSource::new(value)) as std::sync::Arc<crate::registry::ErasedRecordNode>)
|
||||
}
|
||||
|
||||
impl<T: Clone> ClonedNode<T> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,13 +35,12 @@ pub(crate) struct ParsedNodeFn {
|
||||
pub(crate) output_type: Type,
|
||||
pub(crate) is_async: bool,
|
||||
pub(crate) fields: Vec<ParsedField>,
|
||||
pub(crate) attribute_reads: Vec<AttributeRead>,
|
||||
pub(crate) body: TokenStream2,
|
||||
pub(crate) description: String,
|
||||
}
|
||||
|
||||
/// An `Attr<Marker>` parameter: a declared attribute read on the carrier's
|
||||
/// items, not a wired input.
|
||||
/// An `Attr<Marker>` slot in a parameter's read tuple: a declared attribute
|
||||
/// read on that input's wire, not a wired input of its own.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct AttributeRead {
|
||||
pub(crate) pat_ident: PatIdent,
|
||||
@@ -49,34 +48,54 @@ pub(crate) struct AttributeRead {
|
||||
}
|
||||
|
||||
/// The write half of a record kernel's return: the element type in the first
|
||||
/// tuple slot and the attribute markers written after it. `None` unless the
|
||||
/// value is a well-formed write tuple (a non-`Attr` element first, then only
|
||||
/// `Attr` slots, at least one).
|
||||
/// tuple slot, then the attribute markers written and the ones removed. `None`
|
||||
/// unless the value is a well-formed write tuple (a non-marker element first,
|
||||
/// then only `Attr` and `RemoveAttr` slots, at least one).
|
||||
pub(crate) struct RecordWrites {
|
||||
pub(crate) element: Type,
|
||||
pub(crate) markers: Vec<Type>,
|
||||
pub(crate) removes: Vec<Type>,
|
||||
}
|
||||
|
||||
pub(crate) fn record_writes(value: &Type) -> Option<RecordWrites> {
|
||||
let Type::Tuple(tuple) = value else { return None };
|
||||
let mut slots = tuple.elems.iter();
|
||||
let element = slots.next()?;
|
||||
if attr_marker(element).is_some() {
|
||||
if attr_marker(element).is_some() || remove_attr_marker(element).is_some() {
|
||||
return None;
|
||||
}
|
||||
let markers: Option<Vec<Type>> = slots.map(attr_marker).collect();
|
||||
let markers = markers?;
|
||||
(!markers.is_empty()).then(|| RecordWrites {
|
||||
let mut markers = Vec::new();
|
||||
let mut removes = Vec::new();
|
||||
for slot in slots {
|
||||
if let Some(marker) = attr_marker(slot) {
|
||||
markers.push(marker);
|
||||
} else if let Some(marker) = remove_attr_marker(slot) {
|
||||
removes.push(marker);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
(!markers.is_empty() || !removes.is_empty()).then(|| RecordWrites {
|
||||
element: element.clone(),
|
||||
markers,
|
||||
removes,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the marker type of an `Attr<Marker>` type, if `ty` is one.
|
||||
pub(crate) fn attr_marker(ty: &Type) -> Option<Type> {
|
||||
marker_of(ty, "Attr")
|
||||
}
|
||||
|
||||
/// Returns the marker type of a `RemoveAttr<Marker>` type, if `ty` is one.
|
||||
pub(crate) fn remove_attr_marker(ty: &Type) -> Option<Type> {
|
||||
marker_of(ty, "RemoveAttr")
|
||||
}
|
||||
|
||||
fn marker_of(ty: &Type, wrapper: &str) -> Option<Type> {
|
||||
let Type::Path(path) = ty else { return None };
|
||||
let segment = path.path.segments.last()?;
|
||||
if segment.ident != "Attr" {
|
||||
if segment.ident != wrapper {
|
||||
return None;
|
||||
}
|
||||
let PathArguments::AngleBracketed(args) = &segment.arguments else { return None };
|
||||
@@ -178,6 +197,9 @@ pub struct ParsedField {
|
||||
pub number_step: Option<LitFloat>,
|
||||
pub unit: Option<LitStr>,
|
||||
pub is_data_field: bool,
|
||||
/// The attribute reads destructured from this input's tuple, resolved
|
||||
/// against this input's wire.
|
||||
pub(crate) attribute_reads: Vec<AttributeRead>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -637,7 +659,7 @@ fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNo
|
||||
let fn_generics = input_fn.sig.generics.params.into_iter().collect();
|
||||
let is_async = input_fn.sig.asyncness.is_some();
|
||||
|
||||
let (input, fields, attribute_reads) = parse_inputs(&input_fn.sig.inputs)?;
|
||||
let (input, fields) = parse_inputs(&input_fn.sig.inputs)?;
|
||||
let output_type = parse_output(&input_fn.sig.output)?;
|
||||
let where_clause = input_fn.sig.generics.where_clause;
|
||||
let body = input_fn.block.to_token_stream();
|
||||
@@ -669,16 +691,14 @@ fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result<ParsedNo
|
||||
output_type,
|
||||
is_async,
|
||||
fields,
|
||||
attribute_reads,
|
||||
where_clause,
|
||||
body,
|
||||
description,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<ParsedField>, Vec<AttributeRead>)> {
|
||||
fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<ParsedField>)> {
|
||||
let mut fields = Vec::new();
|
||||
let mut attribute_reads = Vec::new();
|
||||
let mut input = None;
|
||||
|
||||
for (index, arg) in inputs.iter().enumerate() {
|
||||
@@ -715,18 +735,14 @@ fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<Pa
|
||||
context_features,
|
||||
});
|
||||
} else if let Pat::Ident(pat_ident) = &**pat {
|
||||
if let Some(marker) = attr_marker(ty) {
|
||||
if !attrs.iter().all(|attr| attr.path().is_ident("doc")) {
|
||||
return Err(Error::new_spanned(pat_ident, "attribute parameters take no field attributes"));
|
||||
}
|
||||
attribute_reads.push(AttributeRead {
|
||||
pat_ident: pat_ident.clone(),
|
||||
marker,
|
||||
});
|
||||
} else {
|
||||
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);
|
||||
if attr_marker(ty).is_some() {
|
||||
return Err(Error::new_spanned(pat_ident, "an attribute read binds to an input: destructure it as `(value, Attr<..>)`"));
|
||||
}
|
||||
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);
|
||||
} else if let Pat::Tuple(pat_tuple) = &**pat {
|
||||
let field = parse_read_tuple(pat_tuple, ty, attrs, index)?;
|
||||
fields.push(field);
|
||||
} else if let Pat::Wild(wild) = &**pat {
|
||||
let pat_ident = PatIdent {
|
||||
attrs: wild.attrs.clone(),
|
||||
@@ -746,7 +762,86 @@ fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<Pa
|
||||
}
|
||||
|
||||
let input = input.ok_or_else(|| Error::new_spanned(inputs, "Expected at least one input argument. The first argument should be the node input type."))?;
|
||||
Ok((input, fields, attribute_reads))
|
||||
Ok((input, fields))
|
||||
}
|
||||
|
||||
/// Splits a lazy input's `Output = (T, Attr<..>..)` tuple into the element
|
||||
/// type (the wire type) and the declared reads on that edge. A tuple without
|
||||
/// `Attr` slots is an ordinary tuple output and passes through untouched.
|
||||
fn split_lazy_reads(output_type: Type) -> syn::Result<(Type, Vec<AttributeRead>)> {
|
||||
let Type::Tuple(tuple) = &output_type else {
|
||||
return Ok((output_type, Vec::new()));
|
||||
};
|
||||
if !tuple.elems.iter().any(|slot| attr_marker(slot).is_some()) {
|
||||
return Ok((output_type, Vec::new()));
|
||||
}
|
||||
let spelling = "a lazy input with attribute reads declares `Output = (T, Attr<..>)`";
|
||||
let mut slots = tuple.elems.iter();
|
||||
let element = slots.next().ok_or_else(|| Error::new_spanned(tuple, spelling))?;
|
||||
if attr_marker(element).is_some() {
|
||||
return Err(Error::new_spanned(element, spelling));
|
||||
}
|
||||
let attribute_reads: Vec<AttributeRead> = slots
|
||||
.enumerate()
|
||||
.map(|(index, slot)| {
|
||||
let marker = attr_marker(slot).ok_or_else(|| Error::new_spanned(slot, spelling))?;
|
||||
Ok(AttributeRead {
|
||||
pat_ident: PatIdent {
|
||||
attrs: Vec::new(),
|
||||
by_ref: None,
|
||||
mutability: None,
|
||||
ident: format_ident!("__lazy_read_{}", index, span = slot.span()),
|
||||
subpat: None,
|
||||
},
|
||||
marker,
|
||||
})
|
||||
})
|
||||
.collect::<syn::Result<_>>()?;
|
||||
Ok((element.clone(), attribute_reads))
|
||||
}
|
||||
|
||||
/// Parses a `(value, reads..): (T, Attr<..>..)` parameter: the value component
|
||||
/// is an ordinary field of the value type, each `Attr` component a read bound
|
||||
/// to this input's wire.
|
||||
fn parse_read_tuple(pat_tuple: &syn::PatTuple, ty: &Type, attrs: &[Attribute], index: usize) -> syn::Result<ParsedField> {
|
||||
let spelling = "an input with attribute reads destructures as `(value, Attr<..>)` over `(T, Attr<..>)`";
|
||||
let Type::Tuple(ty_tuple) = ty else {
|
||||
return Err(Error::new_spanned(ty, spelling));
|
||||
};
|
||||
if pat_tuple.elems.len() != ty_tuple.elems.len() || ty_tuple.elems.len() < 2 {
|
||||
return Err(Error::new_spanned(pat_tuple, spelling));
|
||||
}
|
||||
let mut slots = pat_tuple.elems.iter().zip(ty_tuple.elems.iter());
|
||||
let (value_pat, value_ty) = slots.next().expect("length checked above");
|
||||
if attr_marker(value_ty).is_some() {
|
||||
return Err(Error::new_spanned(value_ty, spelling));
|
||||
}
|
||||
let value_ident = match value_pat {
|
||||
Pat::Ident(pat_ident) => pat_ident.clone(),
|
||||
Pat::Wild(wild) => PatIdent {
|
||||
attrs: wild.attrs.clone(),
|
||||
by_ref: None,
|
||||
mutability: None,
|
||||
ident: format_ident!("_value{}", index, span = wild.underscore_token.span),
|
||||
subpat: None,
|
||||
},
|
||||
_ => return Err(Error::new_spanned(value_pat, "Expected a simple identifier for the value component")),
|
||||
};
|
||||
let attribute_reads: Vec<AttributeRead> = slots
|
||||
.map(|(pat, ty)| {
|
||||
let marker = attr_marker(ty).ok_or_else(|| Error::new_spanned(ty, spelling))?;
|
||||
let Pat::Ident(pat_ident) = pat else {
|
||||
return Err(Error::new_spanned(pat, "Expected a simple identifier for the attribute read"));
|
||||
};
|
||||
Ok(AttributeRead {
|
||||
pat_ident: pat_ident.clone(),
|
||||
marker,
|
||||
})
|
||||
})
|
||||
.collect::<syn::Result<_>>()?;
|
||||
let mut field = parse_field(value_ident.clone(), value_ty.clone(), attrs).map_err(|e| Error::new_spanned(&value_ident, format!("Failed to parse argument '{}': {}", value_ident.ident, e)))?;
|
||||
field.attribute_reads = attribute_reads;
|
||||
Ok(field)
|
||||
}
|
||||
|
||||
/// Parse context feature identifiers from the trait bounds of a context parameter.
|
||||
@@ -967,6 +1062,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
let (output_type, attribute_reads) = split_lazy_reads(output_type)?;
|
||||
Ok(ParsedField {
|
||||
pat_ident,
|
||||
ty: ParsedFieldType::Node(NodeParsedField {
|
||||
@@ -981,6 +1077,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
||||
number_step,
|
||||
unit,
|
||||
is_data_field,
|
||||
attribute_reads,
|
||||
})
|
||||
} else {
|
||||
let implementations = extract_attribute(attrs, "implementations")
|
||||
@@ -1037,6 +1134,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
||||
number_step,
|
||||
unit,
|
||||
is_data_field,
|
||||
attribute_reads: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1153,6 +1251,7 @@ impl ParsedNodeFn {
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
attribute_reads: Vec::new(),
|
||||
};
|
||||
self.fields.push(hidden_field(
|
||||
"_runtime",
|
||||
@@ -1304,7 +1403,6 @@ mod tests {
|
||||
},
|
||||
output_type: parse_quote!(f64),
|
||||
is_async: false,
|
||||
attribute_reads: vec![],
|
||||
fields: vec![ParsedField {
|
||||
pat_ident: pat_ident("b"),
|
||||
name: None,
|
||||
@@ -1327,6 +1425,7 @@ mod tests {
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
attribute_reads: Vec::new(),
|
||||
}],
|
||||
body: TokenStream2::new(),
|
||||
description: String::from("Multi\nLine\n"),
|
||||
@@ -1381,7 +1480,6 @@ mod tests {
|
||||
},
|
||||
output_type: parse_quote!(T),
|
||||
is_async: false,
|
||||
attribute_reads: vec![],
|
||||
fields: vec![
|
||||
ParsedField {
|
||||
pat_ident: pat_ident("transform_target"),
|
||||
@@ -1397,6 +1495,7 @@ mod tests {
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
attribute_reads: Vec::new(),
|
||||
},
|
||||
ParsedField {
|
||||
pat_ident: pat_ident("translate"),
|
||||
@@ -1420,6 +1519,7 @@ mod tests {
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
attribute_reads: Vec::new(),
|
||||
},
|
||||
],
|
||||
body: TokenStream2::new(),
|
||||
@@ -1472,7 +1572,6 @@ mod tests {
|
||||
},
|
||||
output_type: parse_quote!(Vector),
|
||||
is_async: false,
|
||||
attribute_reads: vec![],
|
||||
fields: vec![ParsedField {
|
||||
pat_ident: pat_ident("radius"),
|
||||
name: None,
|
||||
@@ -1495,6 +1594,7 @@ mod tests {
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
attribute_reads: Vec::new(),
|
||||
}],
|
||||
body: TokenStream2::new(),
|
||||
description: "Test\n".into(),
|
||||
@@ -1545,7 +1645,6 @@ mod tests {
|
||||
},
|
||||
output_type: parse_quote!(List<Raster<P>>),
|
||||
is_async: false,
|
||||
attribute_reads: vec![],
|
||||
fields: vec![ParsedField {
|
||||
pat_ident: pat_ident("shadows"),
|
||||
name: None,
|
||||
@@ -1573,6 +1672,7 @@ mod tests {
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
attribute_reads: Vec::new(),
|
||||
}],
|
||||
body: TokenStream2::new(),
|
||||
description: String::new(),
|
||||
@@ -1630,7 +1730,6 @@ mod tests {
|
||||
},
|
||||
output_type: parse_quote!(f64),
|
||||
is_async: false,
|
||||
attribute_reads: vec![],
|
||||
fields: vec![ParsedField {
|
||||
pat_ident: pat_ident("b"),
|
||||
name: None,
|
||||
@@ -1653,6 +1752,7 @@ mod tests {
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
attribute_reads: Vec::new(),
|
||||
}],
|
||||
body: TokenStream2::new(),
|
||||
description: String::new(),
|
||||
@@ -1718,7 +1818,6 @@ mod tests {
|
||||
},
|
||||
output_type: parse_quote!(List<Raster<CPU>>),
|
||||
is_async: true,
|
||||
attribute_reads: vec![],
|
||||
fields: vec![ParsedField {
|
||||
pat_ident: pat_ident("path"),
|
||||
name: None,
|
||||
@@ -1741,6 +1840,7 @@ mod tests {
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
attribute_reads: Vec::new(),
|
||||
}],
|
||||
body: TokenStream2::new(),
|
||||
description: String::new(),
|
||||
@@ -1791,7 +1891,6 @@ mod tests {
|
||||
},
|
||||
output_type: parse_quote!(i32),
|
||||
is_async: false,
|
||||
attribute_reads: vec![],
|
||||
fields: vec![],
|
||||
body: TokenStream2::new(),
|
||||
description: String::new(),
|
||||
|
||||
@@ -247,6 +247,7 @@ impl PerPixelAdjustCodegen<'_> {
|
||||
number_step: None,
|
||||
unit: None,
|
||||
is_data_field: false,
|
||||
attribute_reads: Vec::new(),
|
||||
});
|
||||
|
||||
// find exactly one gpu_image field, runtime doesn't support more than 1 atm
|
||||
@@ -317,7 +318,6 @@ impl PerPixelAdjustCodegen<'_> {
|
||||
output_type: raster_gpu,
|
||||
is_async: false,
|
||||
fields,
|
||||
attribute_reads: Vec::new(),
|
||||
body,
|
||||
description: self.parsed.description.clone(),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::parsing::{Implementation, NodeParsedField, ParsedField, ParsedFieldType, ParsedNodeFn, RegularParsedField, attr_marker, record_writes};
|
||||
use crate::parsing::{Implementation, NodeParsedField, ParsedField, ParsedFieldType, ParsedNodeFn, RegularParsedField, attr_marker, record_writes, remove_attr_marker};
|
||||
use proc_macro_error2::emit_error;
|
||||
use quote::{ToTokens, quote};
|
||||
use syn::spanned::Spanned;
|
||||
@@ -14,6 +14,7 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> {
|
||||
validate_async_source,
|
||||
validate_lend_fields,
|
||||
validate_record_io,
|
||||
validate_lazy_reads,
|
||||
];
|
||||
|
||||
for validator in validators {
|
||||
@@ -26,19 +27,23 @@ pub fn validate_node_fn(parsed: &ParsedNodeFn) -> syn::Result<()> {
|
||||
fn validate_record_io(parsed: &ParsedNodeFn) {
|
||||
let value = crate::codegen::slot_value_type(&parsed.output_type);
|
||||
if let Type::Tuple(tuple) = &value {
|
||||
let has_attr_slot = tuple.elems.iter().any(|slot| attr_marker(slot).is_some());
|
||||
if has_attr_slot && record_writes(&value).is_none() {
|
||||
let has_marker_slot = tuple.elems.iter().any(|slot| attr_marker(slot).is_some() || remove_attr_marker(slot).is_some());
|
||||
if has_marker_slot && record_writes(&value).is_none() {
|
||||
emit_error!(
|
||||
parsed.output_type.span(),
|
||||
"a record return tuple is the element first, then only `Attr<..>` writes"
|
||||
"a record return tuple is the element first, then only `Attr<..>` writes and `RemoveAttr<..>` deletions"
|
||||
);
|
||||
}
|
||||
} else if attr_marker(&value).is_some() {
|
||||
emit_error!(parsed.output_type.span(), "an `Attr<..>` write needs an element in the first tuple slot, e.g. `(T, Attr<..>)`");
|
||||
} else if attr_marker(&value).is_some() || remove_attr_marker(&value).is_some() {
|
||||
emit_error!(parsed.output_type.span(), "an attribute write needs an element in the first tuple slot, e.g. `(T, Attr<..>)`");
|
||||
}
|
||||
|
||||
let writes = record_writes(&value);
|
||||
if parsed.attribute_reads.is_empty() && writes.is_none() {
|
||||
let has_reads = parsed
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Regular(_)));
|
||||
if !has_reads && writes.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,6 +59,35 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
|
||||
emit_error!(field.pat_ident.span(), "record nodes take no lazy inputs yet");
|
||||
}
|
||||
}
|
||||
for (index, field) in parsed.fields.iter().enumerate() {
|
||||
if field.attribute_reads.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if field.is_data_field {
|
||||
emit_error!(field.pat_ident.span(), "a `#[data]` field has no wire to read attributes from");
|
||||
continue;
|
||||
}
|
||||
match &field.ty {
|
||||
ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }) => {
|
||||
emit_error!(field.pat_ident.span(), "attribute reads need an owned value; take `T` instead of `&T`");
|
||||
}
|
||||
ParsedFieldType::Regular(RegularParsedField { ty, implementations, .. }) => {
|
||||
if matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty()) {
|
||||
emit_error!(field.pat_ident.span(), "attribute-only inputs are not supported yet; the value component cannot be `()`");
|
||||
}
|
||||
let is_token_carrier = index == 0 && implementations.is_empty() && crate::codegen::unbounded_generic(parsed, ty).is_some();
|
||||
if !is_token_carrier && crate::codegen::contains_open_generic(parsed, ty) {
|
||||
emit_error!(
|
||||
field.pat_ident.span(),
|
||||
"a reading input's value is monomorphic for now; use a concrete type or an unbounded passthrough generic in the primary input"
|
||||
);
|
||||
}
|
||||
}
|
||||
// Lazy-input reads are validated by `validate_lazy_reads`; a
|
||||
// record-io node already rejects lazy inputs above.
|
||||
ParsedFieldType::Node(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(carrier) = parsed.fields.first() else {
|
||||
emit_error!(
|
||||
@@ -75,9 +109,6 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
|
||||
};
|
||||
|
||||
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,
|
||||
@@ -107,13 +138,15 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
|
||||
}
|
||||
}
|
||||
|
||||
let mut seen_reads: Vec<String> = Vec::new();
|
||||
for read in &parsed.attribute_reads {
|
||||
let marker = read.marker.to_token_stream().to_string();
|
||||
if seen_reads.contains(&marker) {
|
||||
emit_error!(read.pat_ident.span(), "attribute `{}` is read twice", marker);
|
||||
for field in &parsed.fields {
|
||||
let mut seen_reads: Vec<String> = Vec::new();
|
||||
for read in &field.attribute_reads {
|
||||
let marker = read.marker.to_token_stream().to_string();
|
||||
if seen_reads.contains(&marker) {
|
||||
emit_error!(read.pat_ident.span(), "attribute `{}` is read twice from `{}`", marker, field.pat_ident.ident);
|
||||
}
|
||||
seen_reads.push(marker);
|
||||
}
|
||||
seen_reads.push(marker);
|
||||
}
|
||||
if let Some(writes) = &writes {
|
||||
let mut seen_writes: Vec<String> = Vec::new();
|
||||
@@ -124,6 +157,54 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
|
||||
}
|
||||
seen_writes.push(written);
|
||||
}
|
||||
let mut seen_removes: Vec<String> = Vec::new();
|
||||
for marker in &writes.removes {
|
||||
let removed = marker.to_token_stream().to_string();
|
||||
if seen_removes.contains(&removed) {
|
||||
emit_error!(parsed.output_type.span(), "attribute `{}` is removed twice", removed);
|
||||
}
|
||||
if seen_writes.contains(&removed) {
|
||||
emit_error!(parsed.output_type.span(), "attribute `{}` is both written and removed", removed);
|
||||
}
|
||||
seen_removes.push(removed);
|
||||
}
|
||||
if no_carrier && !writes.removes.is_empty() {
|
||||
emit_error!(parsed.output_type.span(), "a node without a primary input writes a fresh record; there is nothing to remove");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_lazy_reads(parsed: &ParsedNodeFn) {
|
||||
if !crate::codegen::has_lazy_reads(parsed) {
|
||||
return;
|
||||
}
|
||||
if !crate::codegen::record_flip(parsed) {
|
||||
emit_error!(
|
||||
parsed.fn_name.span(),
|
||||
"attribute reads on a lazy input need the record lowering; routing, `plain`, shader, batch, and non-row-assignable generic nodes keep the plain one"
|
||||
);
|
||||
}
|
||||
for field in &parsed.fields {
|
||||
let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &field.ty else {
|
||||
continue;
|
||||
};
|
||||
if field.attribute_reads.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if crate::codegen::unbounded_generic(parsed, output_type).is_some() {
|
||||
emit_error!(
|
||||
field.pat_ident.span(),
|
||||
"an unbounded generic source forwards its whole record; attribute reads need a concrete output type"
|
||||
);
|
||||
}
|
||||
let mut seen: Vec<String> = Vec::new();
|
||||
for read in &field.attribute_reads {
|
||||
let marker = read.marker.to_token_stream().to_string();
|
||||
if seen.contains(&marker) {
|
||||
emit_error!(read.marker.span(), "attribute `{}` is read twice from `{}`", marker, field.pat_ident.ident);
|
||||
}
|
||||
seen.push(marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! Pilot record nodes exercising the macro's record-tier attribute io:
|
||||
//! offset reads and writes against record edges, the ElToken byte-carry for
|
||||
//! passthrough elements, and the `_: ()` no-carrier form. These are the
|
||||
//! flat-wave law tests; the node forms are the production authoring surface,
|
||||
//! and the wiring is by hand until the compiler pass constructs layouts.
|
||||
//! per-input tuple reads resolved against each input's wire, offset writes,
|
||||
//! `RemoveAttr` layout subtraction, the ElToken byte-carry for passthrough
|
||||
//! elements, and the `_: ()` no-carrier form. These are the 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, RemoveAttr};
|
||||
use core_types::context::ExtractArena;
|
||||
use core_types::gpoll::{ErrorKind, GraphError, Interrupt};
|
||||
use core_types::{Context, Ctx};
|
||||
@@ -17,7 +18,7 @@ core_types::attribute! {
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn multiply_opacity(_: impl Ctx, element: f64, factor: f64, opacity: Attr<Opacity>) -> (f64, Attr<Opacity>) {
|
||||
fn multiply_opacity(_: impl Ctx, (element, opacity): (f64, Attr<Opacity>), factor: f64) -> (f64, Attr<Opacity>) {
|
||||
(element, Attr(*opacity * factor))
|
||||
}
|
||||
|
||||
@@ -27,12 +28,12 @@ fn measure(_: impl Ctx, element: f64) -> (f64, Attr<Length>) {
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn shade(_: impl Ctx, element: f64, opacity: Attr<Opacity>) -> f64 {
|
||||
fn shade(_: impl Ctx, (element, opacity): (f64, Attr<Opacity>)) -> f64 {
|
||||
element * *opacity
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn checked_multiply_opacity(_: impl Ctx, element: f64, factor: f64, opacity: Attr<Opacity>) -> Result<(f64, Attr<Opacity>), Interrupt> {
|
||||
fn checked_multiply_opacity(_: impl Ctx, (element, opacity): (f64, Attr<Opacity>), factor: f64) -> Result<(f64, Attr<Opacity>), Interrupt> {
|
||||
if factor < 0. {
|
||||
return Err(GraphError::new("negative factor").into());
|
||||
}
|
||||
@@ -40,12 +41,12 @@ fn checked_multiply_opacity(_: impl Ctx, element: f64, factor: f64, opacity: Att
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn scale(_: impl Ctx, element: f64, factor: &f64, opacity: Attr<Opacity>) -> (f64, Attr<Opacity>) {
|
||||
fn scale(_: impl Ctx, (element, opacity): (f64, Attr<Opacity>), factor: &f64) -> (f64, 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>) {
|
||||
fn fade<T>(_: impl Ctx, (element, opacity): (T, Attr<Opacity>), factor: f64) -> (T, Attr<Opacity>) {
|
||||
(element, Attr(*opacity * factor))
|
||||
}
|
||||
|
||||
@@ -55,7 +56,7 @@ fn source_opacity(_: impl Ctx, _: (), element: f64, opacity: f64) -> (f64, Attr<
|
||||
}
|
||||
|
||||
#[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> {
|
||||
fn label<'e>(ctx: impl Ctx + ExtractArena<'e>, (element, label): (f64, Attr<Label>), text: String) -> Result<(f64, Attr<'e, Label>), Interrupt> {
|
||||
let joined = format!("{}{text}", *label);
|
||||
let (parked, _) = ctx.arena().alloc(joined).ok_or(GraphError {
|
||||
kind: ErrorKind::ArenaExhausted,
|
||||
@@ -64,6 +65,52 @@ fn label<'e>(ctx: impl Ctx + ExtractArena<'e>, element: f64, text: String, label
|
||||
Ok((element, Attr(parked.as_str())))
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn transfer_opacity(_: impl Ctx, (element, opacity): (f64, Attr<Opacity>), (other, other_opacity): (f64, Attr<Opacity>)) -> (f64, Attr<Opacity>) {
|
||||
(element + other, Attr(*opacity * *other_opacity))
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn strip_opacity<T>(_: impl Ctx, element: T) -> (T, RemoveAttr<Opacity>) {
|
||||
(element, RemoveAttr::new())
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn relength(_: impl Ctx, element: f64) -> (f64, RemoveAttr<Opacity>, Attr<Length>) {
|
||||
(element, RemoveAttr::new(), Attr(element * 2.))
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn boost(_: impl Ctx, element: f64, factor: f64) -> f64 {
|
||||
element * factor
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn boost_poll(_: impl Ctx, element: f64, factor: f64) -> core_types::gpoll::GPoll<f64> {
|
||||
core_types::gpoll::GPoll::Final(element * factor)
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn offset(_: impl Ctx, element: f64, by: &f64) -> f64 {
|
||||
element + *by
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
async fn double_async(_: impl Ctx, element: f64) -> f64 {
|
||||
element * 2.
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Test"))]
|
||||
fn fallback(
|
||||
ctx: impl Ctx,
|
||||
_: (),
|
||||
#[expose] content: impl Node<Context<'_>, Output = (f64, Attr<Opacity>)>,
|
||||
#[expose] alternate: impl Node<Context<'_>, Output = f64>,
|
||||
) -> Result<f64, Interrupt> {
|
||||
let (element, opacity) = content.eval(ctx)?;
|
||||
Ok(if *opacity > 0. { element } else { alternate.eval(ctx)? })
|
||||
}
|
||||
|
||||
#[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> {
|
||||
if take_second { second.eval(ctx) } else { first.eval(ctx) }
|
||||
@@ -158,6 +205,12 @@ mod tests {
|
||||
stack::reserve(layouts.iter().map(|layout| layout.frame_bytes()).sum());
|
||||
}
|
||||
|
||||
fn lifted_value<T: Clone + Send + Sync + 'static>(value: T) -> (core_types::record::RecordLift<T, ValueNode<T>>, Layout) {
|
||||
let lift = core_types::record::RecordLift::<T, _>::new(ValueNode(value));
|
||||
let layout = Node::<ContextImpl>::layout(&lift).unwrap().clone();
|
||||
(lift, layout)
|
||||
}
|
||||
|
||||
fn bare_source(layout: &Layout, element: f64) -> RecordSourceNode<f64> {
|
||||
RecordSourceNode {
|
||||
layout: layout.clone(),
|
||||
@@ -388,6 +441,267 @@ mod tests {
|
||||
assert_eq!(unsafe { rec.read::<f64>(scaled.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secondary_input_reads_bind_to_their_own_wire() {
|
||||
let arena = Arena::new(1024).unwrap();
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let carrier_layout = f64_layout(&["opacity"]);
|
||||
let secondary_layout = f64_layout(&["opacity"]);
|
||||
let transferred = transfer_opacity_layout(&carrier_layout);
|
||||
reserve_for(&[&carrier_layout, &secondary_layout, &transferred]);
|
||||
|
||||
let chain = TransferOpacityNode::new(
|
||||
f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.5)]),
|
||||
f64_record_source(&secondary_layout, 3., vec![(secondary_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
|
||||
&carrier_layout,
|
||||
&secondary_layout,
|
||||
);
|
||||
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
let rec = transferred.rec(&value);
|
||||
assert_eq!(unsafe { rec.element::<f64>() }, 5.);
|
||||
assert_eq!(unsafe { rec.read::<f64>(transferred.offset_of(Opacity::NAME, 0).unwrap()) }, 0.125);
|
||||
|
||||
let bare_secondary = f64_layout(&[]);
|
||||
let defaulted = TransferOpacityNode::new(
|
||||
f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.5)]),
|
||||
bare_source(&bare_secondary, 3.),
|
||||
&carrier_layout,
|
||||
&bare_secondary,
|
||||
);
|
||||
let GPoll::Final(value) = defaulted.eval(&ctx) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
let rec = transferred.rec(&value);
|
||||
assert_eq!(unsafe { rec.read::<f64>(transferred.offset_of(Opacity::NAME, 0).unwrap()) }, 0.5, "an absent secondary attribute reads its default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_flipped_node_carries_its_primary_inputs_fields() {
|
||||
let arena = Arena::new(1024).unwrap();
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let source_layout = f64_layout(&["opacity"]);
|
||||
let factor = core_types::record::RecordLift::<f64, _>::new(ValueNode(3.));
|
||||
let factor_layout = Node::<ContextImpl>::layout(&factor).unwrap().clone();
|
||||
reserve_for(&[&source_layout]);
|
||||
|
||||
let node = BoostNode::new(
|
||||
f64_record_source(&source_layout, 2., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
|
||||
factor,
|
||||
&source_layout,
|
||||
&factor_layout,
|
||||
);
|
||||
let out_layout = Node::<ContextImpl>::layout(&node).unwrap().clone();
|
||||
let opacity_offset = out_layout.offset_of(Opacity::NAME, 0).expect("the primary input's fields pass through to the output");
|
||||
let GPoll::Final(value) = node.eval(&ctx) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
let rec = out_layout.rec(&value);
|
||||
assert_eq!(unsafe { rec.element::<f64>() }, 6.);
|
||||
assert_eq!(unsafe { rec.read::<f64>(opacity_offset) }, 0.25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_poll_kernel_carries_its_primary_inputs_fields() {
|
||||
let arena = Arena::new(1024).unwrap();
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let source_layout = f64_layout(&["opacity"]);
|
||||
let (factor, factor_layout) = lifted_value(3.);
|
||||
reserve_for(&[&source_layout]);
|
||||
|
||||
let node = BoostPollNode::new(
|
||||
f64_record_source(&source_layout, 2., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
|
||||
factor,
|
||||
&source_layout,
|
||||
&factor_layout,
|
||||
);
|
||||
let out_layout = Node::<ContextImpl>::layout(&node).unwrap().clone();
|
||||
let opacity_offset = out_layout.offset_of(Opacity::NAME, 0).expect("the primary input's fields pass through the poll kernel");
|
||||
let GPoll::Final(value) = node.eval(&ctx) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
let rec = out_layout.rec(&value);
|
||||
assert_eq!(unsafe { rec.element::<f64>() }, 6.);
|
||||
assert_eq!(unsafe { rec.read::<f64>(opacity_offset) }, 0.25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_byte_carried_spilled_borrow_parks_and_survives_the_carrier_eval() {
|
||||
let arena = Arena::new(1024).unwrap();
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let carrier_layout = f64_layout(&["opacity"]);
|
||||
let by_layout = f64_layout(&["opacity", "length"]);
|
||||
assert!(!by_layout.is_inline(), "the borrow must point into a spilled frame to exercise the park");
|
||||
reserve_for(&[&carrier_layout, &by_layout]);
|
||||
|
||||
let node = OffsetNode::new(
|
||||
f64_record_source(&carrier_layout, 2., vec![(carrier_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
|
||||
f64_record_source(&by_layout, 40., vec![]),
|
||||
&carrier_layout,
|
||||
&by_layout,
|
||||
);
|
||||
let out_layout = Node::<ContextImpl>::layout(&node).unwrap().clone();
|
||||
let GPoll::Final(value) = node.eval(&ctx) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
let rec = out_layout.rec(&value);
|
||||
assert_eq!(unsafe { rec.element::<f64>() }, 42., "the parked borrow survives the carrier evaluation reusing its frame");
|
||||
assert_eq!(unsafe { rec.read::<f64>(out_layout.offset_of(Opacity::NAME, 0).unwrap()) }, 0.25);
|
||||
}
|
||||
|
||||
struct InlineRuntime;
|
||||
|
||||
impl core_types::runtime::Runtime for InlineRuntime {
|
||||
fn spawn(&self, _source: SourceId, mut future: core_types::runtime::SourceFuture) -> bool {
|
||||
let mut task_ctx = std::task::Context::from_waker(std::task::Waker::noop());
|
||||
assert!(future.as_mut().poll(&mut task_ctx).is_ready(), "the inline runtime completes tasks at spawn");
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_async_source_carries_its_primary_inputs_fields_around_the_slot() {
|
||||
let arena = Arena::new(1024).unwrap();
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let source_layout = f64_layout(&["opacity"]);
|
||||
let (runtime, runtime_layout) = lifted_value(core_types::runtime::RuntimeHandle(std::sync::Arc::new(InlineRuntime)));
|
||||
let (source_id, source_id_layout) = lifted_value(7 as SourceId);
|
||||
reserve_for(&[&source_layout]);
|
||||
|
||||
let node = DoubleAsyncNode::new(
|
||||
f64_record_source(&source_layout, 3., vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25)]),
|
||||
runtime,
|
||||
source_id,
|
||||
&source_layout,
|
||||
&runtime_layout,
|
||||
&source_id_layout,
|
||||
);
|
||||
let out_layout = Node::<ContextImpl>::layout(&node).unwrap().clone();
|
||||
let opacity_offset = out_layout.offset_of(Opacity::NAME, 0).expect("the carrier's fields pass through the async source");
|
||||
|
||||
let GPoll::Final(value) = node.eval(&ctx) else {
|
||||
panic!("an inline completion is final on the spawning eval");
|
||||
};
|
||||
let rec = out_layout.rec(&value);
|
||||
assert_eq!(unsafe { rec.element::<f64>() }, 6.);
|
||||
assert_eq!(unsafe { rec.read::<f64>(opacity_offset) }, 0.25);
|
||||
|
||||
let GPoll::Final(value) = node.eval(&ctx) else {
|
||||
panic!("a slot hit is final");
|
||||
};
|
||||
let rec = out_layout.rec(&value);
|
||||
assert_eq!(unsafe { rec.element::<f64>() }, 6., "the slot hit replays the element");
|
||||
assert_eq!(unsafe { rec.read::<f64>(opacity_offset) }, 0.25, "the fields re-carry on every eval");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lazy_reads_bind_to_their_edge_and_leave_the_untaken_branch_unevaluated() {
|
||||
let arena = Arena::new(1024).unwrap();
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let unit = core_types::record::RecordLift::<(), _>::new(ValueNode(()));
|
||||
let unit_layout = Node::<ContextImpl>::layout(&unit).unwrap().clone();
|
||||
let content_layout = f64_layout(&["opacity"]);
|
||||
reserve_for(&[&content_layout]);
|
||||
|
||||
let run = |opacity: Option<f64>| {
|
||||
let evals = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
|
||||
let alternate = core_types::record::RecordLift::<f64, _>::new(CountingValue(evals.clone()));
|
||||
let alternate_layout = Node::<ContextImpl>::layout(&alternate).unwrap().clone();
|
||||
let (content_layout, fields) = match opacity {
|
||||
Some(value) => (content_layout.clone(), vec![(content_layout.offset_of("opacity", 0).unwrap(), value)]),
|
||||
None => (f64_layout(&[]), vec![]),
|
||||
};
|
||||
let node = FallbackNode::new(
|
||||
core_types::record::RecordLift::<(), _>::new(ValueNode(())),
|
||||
f64_record_source(&content_layout, 7., fields),
|
||||
alternate,
|
||||
&unit_layout,
|
||||
&content_layout,
|
||||
&alternate_layout,
|
||||
);
|
||||
let GPoll::Final(value) = node.eval(&ctx) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
let element = unsafe { Node::<ContextImpl>::layout(&node).unwrap().rec(&value).element::<f64>() };
|
||||
(element, evals.load(std::sync::atomic::Ordering::Relaxed))
|
||||
};
|
||||
|
||||
assert_eq!(run(Some(0.5)), (7., 0), "a visible content skips the alternate branch entirely");
|
||||
assert_eq!(run(Some(0.)), (21., 1), "a transparent content evaluates the alternate branch");
|
||||
assert_eq!(run(None), (7., 0), "an absent attribute reads its declared default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_attr_leaves_the_layout_and_downstream_reads_the_default() {
|
||||
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 modified = multiply_opacity_layout(&source_layout);
|
||||
let stripped = strip_opacity_layout(&modified);
|
||||
assert!(stripped.offset_of(Opacity::NAME, 0).is_none(), "the removed name leaves the output layout");
|
||||
let shaded = shade_layout(&stripped);
|
||||
reserve_for(&[&source_layout, &modified, &stripped, &shaded]);
|
||||
|
||||
let chain = ShadeNode::new(
|
||||
StripOpacityNode::new(MultiplyOpacityNode::new(bare_source(&source_layout, 4.), ValueNode(0.5), &source_layout), &modified),
|
||||
&stripped,
|
||||
);
|
||||
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
assert_eq!(unsafe { shaded.rec(&value).element::<f64>() }, 4., "a read after the removal yields the declared default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_writes_and_removes_destructure_in_tuple_order() {
|
||||
let arena = Arena::new(1024).unwrap();
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let source_layout = f64_layout(&["opacity", "length"]);
|
||||
let relengthed = relength_layout(&source_layout);
|
||||
assert!(relengthed.offset_of(Opacity::NAME, 0).is_none());
|
||||
reserve_for(&[&source_layout, &relengthed]);
|
||||
|
||||
let chain = RelengthNode::new(
|
||||
f64_record_source(
|
||||
&source_layout,
|
||||
3.,
|
||||
vec![(source_layout.offset_of("opacity", 0).unwrap(), 0.25), (source_layout.offset_of("length", 0).unwrap(), 9.)],
|
||||
),
|
||||
&source_layout,
|
||||
);
|
||||
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||
panic!("expected a final record");
|
||||
};
|
||||
let rec = relengthed.rec(&value);
|
||||
assert_eq!(unsafe { rec.element::<f64>() }, 3.);
|
||||
assert_eq!(unsafe { rec.read::<f64>(relengthed.offset_of(Length::NAME, 0).unwrap()) }, 6.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parked_reference_attributes_write_and_carry() {
|
||||
let arena = Arena::new(1024).unwrap();
|
||||
@@ -479,11 +793,13 @@ mod tests {
|
||||
reserve_for(&[&layout_a, &layout_b, &union, &union]);
|
||||
|
||||
let taken = |second: bool| {
|
||||
let (condition, condition_layout) = lifted_value(second);
|
||||
PickNode::new(
|
||||
ValueNode(second),
|
||||
condition,
|
||||
RecordSource::new(f64_record_source(&layout_a, 1., vec![(layout_a.offset_of("opacity", 0).unwrap(), 0.5)]), &layout_a, &union),
|
||||
RecordSource::new(f64_record_source(&layout_b, 3., vec![(layout_b.offset_of("length", 0).unwrap(), 3.)]), &layout_b, &union),
|
||||
&union,
|
||||
&condition_layout,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -516,11 +832,13 @@ mod tests {
|
||||
let union = Layout::union(&[&layout_a, &layout_b]);
|
||||
reserve_for(&[&layout_a, &layout_b, &union, &union]);
|
||||
|
||||
let (condition, condition_layout) = lifted_value(false);
|
||||
let chain = HoldFirstNode::new(
|
||||
ValueNode(false),
|
||||
condition,
|
||||
RecordSource::new(f64_record_source(&layout_a, 1., vec![(layout_a.offset_of("opacity", 0).unwrap(), 0.5)]), &layout_a, &union),
|
||||
RecordSource::new(f64_record_source(&layout_b, 3., vec![(layout_b.offset_of("length", 0).unwrap(), 3.)]), &layout_b, &union),
|
||||
&union,
|
||||
&condition_layout,
|
||||
);
|
||||
|
||||
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||
@@ -564,7 +882,8 @@ mod tests {
|
||||
reserve_for(&[&layout]);
|
||||
|
||||
let probed = |features: ContextFeatures| {
|
||||
let node = crate::context_modification::ContextModificationNode::new(RealTimeProbe { layout: layout.clone() }, ValueNode(ContextModification::from_sources(features, &[])), &layout);
|
||||
let (modification, modification_layout) = lifted_value(ContextModification::from_sources(features, &[]));
|
||||
let node = crate::context_modification::ContextModificationNode::new(RealTimeProbe { layout: layout.clone() }, modification, &layout, &modification_layout);
|
||||
assert_eq!(Node::<ContextImpl>::layout(&node), Some(&layout));
|
||||
let GPoll::Final(value) = node.eval(&ctx) else {
|
||||
panic!("expected a final record");
|
||||
@@ -588,6 +907,7 @@ mod tests {
|
||||
let layout = f64_layout(&["opacity"]);
|
||||
reserve_for(&[&layout]);
|
||||
|
||||
let (modification, modification_layout) = lifted_value(ContextModification::from_sources(ContextFeatures::all(), &[]));
|
||||
let node = crate::context_modification::ContextModificationNode::new(
|
||||
RecordSourceNode {
|
||||
layout: layout.clone(),
|
||||
@@ -595,8 +915,9 @@ mod tests {
|
||||
fields: vec![(layout.offset_of("opacity", 0).unwrap(), 0.25)],
|
||||
partial: true,
|
||||
},
|
||||
ValueNode(ContextModification::from_sources(ContextFeatures::all(), &[])),
|
||||
modification,
|
||||
&layout,
|
||||
&modification_layout,
|
||||
);
|
||||
|
||||
let GPoll::Partial(value) = node.eval(&ctx) else {
|
||||
@@ -637,11 +958,13 @@ mod tests {
|
||||
assert!(union.is_inline());
|
||||
reserve_for(&[&layout_a, &layout_b, &union, &union]);
|
||||
|
||||
let (condition, condition_layout) = lifted_value(false);
|
||||
let chain = HoldFirstNode::new(
|
||||
ValueNode(false),
|
||||
condition,
|
||||
RecordSource::new(f64_record_source(&layout_a, 1., vec![(layout_a.offset_of("opacity", 0).unwrap(), 0.5)]), &layout_a, &union),
|
||||
RecordSource::new(f64_record_source(&layout_b, 3., vec![]), &layout_b, &union),
|
||||
&union,
|
||||
&condition_layout,
|
||||
);
|
||||
|
||||
let GPoll::Final(value) = chain.eval(&ctx) else {
|
||||
|
||||
@@ -12,7 +12,7 @@ use wgpu::util::DeviceExt;
|
||||
use wgpu_executor::{WgpuExecutor, WgpuPipeline, WgpuPipelineCache};
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
fn render_background<'a>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, #[scope(composite_background_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, data: RenderOutput) -> RenderOutput {
|
||||
fn render_background(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, #[scope(composite_background_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, data: RenderOutput) -> RenderOutput {
|
||||
let footprint = ctx.footprint();
|
||||
let render_params = ctx
|
||||
.vararg(0)
|
||||
|
||||
Reference in New Issue
Block a user