diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index e9aaf3f7f3..fde3d44f62 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -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::>()); - entries.extend(core_types::registry::record_bridge_rows::>()); - entries.extend(core_types::registry::record_bridge_rows::>()); - entries.extend(core_types::registry::record_bridge_rows::>()); - entries.extend(core_types::registry::record_bridge_rows::()); - entries.extend(core_types::registry::record_bridge_rows::>()); - entries.extend(core_types::registry::record_bridge_rows::()); - entries.extend(core_types::registry::record_bridge_rows::()); - entries.extend(core_types::registry::record_bridge_rows::>()); - entries.extend(core_types::registry::record_bridge_rows::()); - $( - 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 { match self { diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 1e6afbfa84..25361c3219 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -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) { - 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) { 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> { - let ConstructionArgs::Nodes(args) = &node.construction_args else { return None }; - let inputs = args.iter().map(|id| self.inferred.get(id).map(NodeIOTypes::ty)).collect::>>()?; - 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 { 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 { - 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> { - 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> { 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> { - static LOOKUP: std::sync::LazyLock>> = 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::()]), - constructor: unused, - }], - ), - ( - ProtoNodeIdentifier::new("wants_owned_ambiguously"), - vec![ - RegistryEntry { - io: NodeIOTypes::new(concrete!(Context), concrete!(String), vec![edge_type::()]), - constructor: unused, - }, - RegistryEntry { - io: NodeIOTypes::new(concrete!(Context), concrete!(f64), vec![edge_type::()]), - constructor: unused, - }, - ], - ), - ( - ProtoNodeIdentifier::new("core_types::record::RecordExtractNode"), - vec![RegistryEntry { - io: NodeIOTypes::new(concrete!(Context), concrete!(String), vec![record_edge_type::()]), - 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 = 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> = 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"); - } -} diff --git a/node-graph/interpreted-executor/src/dynamic_executor.rs b/node-graph/interpreted-executor/src/dynamic_executor.rs index 60f146bd72..43a91450e3 100644 --- a/node-graph/interpreted-executor/src/dynamic_executor.rs +++ b/node-graph/interpreted-executor/src/dynamic_executor.rs @@ -586,11 +586,10 @@ mod test { let raster_list = TaggedValue::from_type(&core_types::concrete!(graphene_std::list::List>)).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>>> = 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::>>() + .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>)).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>> = 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::>() + .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::()); - assert!(lift.layout().is_some()); + let value = executor.tree().get(NodeId(0)).unwrap(); + assert_eq!(value.ty(), &core_types::registry::record_edge_type::()); + 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)])), ], }; diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 17c8e01eee..4758d66c40 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -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> { // ========== // 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), - record_extract_node!(Option), - 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), - record_extract_node!(List), - record_lift_node!(List), - record_extract_node!(List), - record_lift_node!(List), - record_extract_node!(List), - record_lift_node!(List), - record_extract_node!(List), - record_lift_node!(List), - record_extract_node!(List), - record_lift_node!(List), - record_extract_node!(List), - record_lift_node!(List>), - record_extract_node!(List>), - #[cfg(feature = "gpu")] - record_lift_node!(List>), - #[cfg(feature = "gpu")] - record_extract_node!(List>), - record_lift_node!(List), - record_extract_node!(List), - record_lift_node!(List), - record_extract_node!(List), - record_lift_node!(List), - record_extract_node!(List), - 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), - record_extract_node!(std::sync::Arc), - 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), - #[cfg(feature = "gpu")] - record_extract_node!(Option), - #[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> { .flatten(), ); - node_types.extend(graph_craft::document::value::TaggedValue::record_bridge_entries()); - node_types.extend(core_types::registry::record_bridge_rows::()); let mut map: HashMap> = HashMap::new(); let insert = |map: &mut HashMap>, 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>)) + 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)) }, }, ) @@ -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::(), + core_types::registry::record_edge_type::(), + ], ), 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::()?, - inputs.next().unwrap().downcast::()?, + 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::()?, + source.downcast_record::()?, + &value_layout, + &converter_layout, + &runtime_layout, + &source_layout, ); - Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc>)) + Ok(EdgeHandle::new_record::<$to>(std::sync::Arc::new(node) as std::sync::Arc)) }, }, ) @@ -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>)) + 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)) }, }, ) }; } - 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)) - }, - }, - ) - }; - } - - 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>)) - }, - }, - ) - }; - } 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 = 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 = 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")); + } } diff --git a/node-graph/libraries/core-types/src/attribute.rs b/node-graph/libraries/core-types/src/attribute.rs index 2e7533d745..f49b834a88 100644 --- a/node-graph/libraries/core-types/src/attribute.rs +++ b/node-graph/libraries/core-types/src/attribute.rs @@ -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(PhantomData); + +impl RemoveAttr { + pub const fn new() -> Self { + RemoveAttr(PhantomData) + } +} + +impl Default for RemoveAttr { + fn default() -> Self { + Self::new() + } +} + /// A census row: what is known about one declared attribute name. #[derive(Clone, Copy, Debug)] pub struct AttributeInfo { diff --git a/node-graph/libraries/core-types/src/record.rs b/node-graph/libraries/core-types/src/record.rs index cc25c82816..4ff57d06d4 100644 --- a/node-graph/libraries/core-types/src/record.rs +++ b/node-graph/libraries/core-types/src/record.rs @@ -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 = 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(rec: Rec, _reads: &[Option]) -> El { + unsafe { read_element::(rec) } +} + +pub struct ElementEdge<'a, Out, N> { node: &'a N, layout: &'a Layout, - _marker: std::marker::PhantomData El>, + reads: &'a [Option], + read: unsafe fn(Rec, &[Option]) -> 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::, } } +} - pub fn eval<'d, C>(&self, ctx: &C) -> GPoll +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], read: unsafe fn(Rec, &[Option]) -> Out) -> Self { + Self { node, layout, reads, read } + } + + pub fn eval<'d, C>(&self, ctx: &C) -> GPoll where N: Node>, { - self.node.eval(ctx).map(|value| unsafe { read_element::(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 El>, + reads: &'a [Option], + read: unsafe fn(Rec, &[Option]) -> 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::, + } + } +} + +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], + read: unsafe fn(Rec, &[Option]) -> Out, + ) -> Self { + Self { + node, + cell, + input_index, + layout, + reads, + read, } } - pub fn eval<'d, C>(&self, ctx: &C) -> Result + pub fn eval<'d, C>(&self, ctx: &C) -> Result where N: Node>, { let value = self.cell.eval_input(self.input_index, self.node, ctx)?; - Ok(unsafe { read_element::(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(dst: *mut u8, offset: usize, value: T) { unsafe { dst.add(offset).cast::().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, dst: *mut u8, frame_bytes: usize, arena: &'e crate::arena::Arena) -> GPoll> { + 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::().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(rec: Rec) -> T { unsafe { borrow_element::(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::() { + return Some(unsafe { rec.element::<&T>() }); + } + if layout.is_inline() { + return Some(unsafe { &*rec.ptr().cast::() }); + } + // A bitwise read duplicates soundly: byte-carried elements have no drop + // glue. + let value = unsafe { rec.ptr().cast::().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 { 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 { edge: N, layout: Layout, diff --git a/node-graph/libraries/core-types/src/registry.rs b/node-graph/libraries/core-types/src/registry.rs index e8ec9bf07e..35418b4238 100644 --- a/node-graph/libraries/core-types/src/registry.rs +++ b/node-graph/libraries/core-types/src/registry.rs @@ -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() -> [(crate::ProtoNodeIdentifier, RegistryEntry); 2] { - [ - (crate::ProtoNodeIdentifier::new("core_types::record::RecordLiftNode"), record_lift_entry::()), - (crate::ProtoNodeIdentifier::new("core_types::record::RecordExtractNode"), record_extract_entry::()), - ] -} - -/// 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() -> RegistryEntry { - RegistryEntry { - io: NodeIOTypes::new(concrete!(Context), record_type::(), vec![edge_type::()]), - 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::::new(inputs.next().unwrap().downcast::()?); - Ok(EdgeHandle::new_record::(std::sync::Arc::new(node) as std::sync::Arc)) - }, - } -} - -/// The extract bridge row for `T`: a record wire into a plain consumer. -pub fn record_extract_entry() -> RegistryEntry { - RegistryEntry { - io: NodeIOTypes::new(concrete!(Context), concrete!(T), vec![record_edge_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 = crate::record::RecordExtract::::new(edge.downcast_record::()?, &layout); - Ok(EdgeHandle::new(std::sync::Arc::new(node) as std::sync::Arc>)) - }, - } -} - pub fn construct(entry: &RegistryEntry, inputs: Vec) -> Result { if inputs.len() != entry.io.inputs.len() { return Err(ConstructionError::Arity { diff --git a/node-graph/libraries/core-types/src/value.rs b/node-graph/libraries/core-types/src/value.rs index 191e9f8092..c2c4f9f4c5 100644 --- a/node-graph/libraries/core-types/src/value.rs +++ b/node-graph/libraries/core-types/src/value.rs @@ -13,11 +13,41 @@ pub fn value_edge( crate::registry::EdgeHandle::new(std::sync::Arc::new(ClonedNode(value)) as std::sync::Arc>) } -/// 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 { + value: T, + layout: crate::record::Layout, +} + +impl ValueSource { + pub fn new(value: T) -> Self { + Self { + value, + layout: crate::record::Layout::default().with_writes(0, crate::record::element_write::(), &[]), + } + } +} + +impl<'e, C, T> crate::node::Node for ValueSource +where + C: crate::context::ExtractArena, + T: Clone + Send + Sync + 'static, +{ + type Output = crate::record::RecordValue<'e>; + + fn eval(&self, input: &C) -> crate::gpoll::GPoll> { + 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(value: T) -> crate::registry::EdgeHandle { - let node = crate::record::RecordLift::::new(ClonedNode(value)); - crate::registry::EdgeHandle::new_record::(std::sync::Arc::new(node) as std::sync::Arc) + crate::registry::EdgeHandle::new_record::(std::sync::Arc::new(ValueSource::new(value)) as std::sync::Arc) } impl ClonedNode { diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index ca40e157c1..5af9852911 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -162,7 +162,12 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn state.push(quote!(pub(super) __plan: ::std::vec::Vec<(usize, usize, usize)>)); } state.push(quote!(pub(super) __frame_bytes: usize)); - state.extend((0..parsed.attribute_reads.len()).map(|index| { + state.extend(reading_secondary_indices(&struct_regular_fields, shape).into_iter().map(|index| { + let slot = format_ident!("__in_{index}"); + quote!(pub(super) #slot: gcore::record::Layout) + })); + let total_reads: usize = struct_regular_fields.iter().map(|field| field.attribute_reads.len()).sum(); + state.extend((0..total_reads).map(|index| { let slot = format_ident!("__read_{index}"); quote!(pub(super) #slot: Option) })); @@ -172,13 +177,29 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn })); state } - None if routing.is_some() || opaque => vec![quote!(pub(super) __layout: gcore::record::Layout)], + None if routing.is_some() => { + let mut state = vec![quote!(pub(super) __layout: gcore::record::Layout)]; + state.extend(routing_value_indices(&struct_regular_fields, routing.as_ref().expect("guarded by the arm")).into_iter().map(|index| { + let slot = format_ident!("__in_{index}"); + quote!(pub(super) #slot: gcore::record::Layout) + })); + state + } + None if opaque => vec![quote!(pub(super) __layout: gcore::record::Layout)], None if flip => { let mut state = vec![quote!(pub(super) __layout: gcore::record::Layout), quote!(pub(super) __frame_bytes: usize)]; + if flip_carrier(parsed) { + state.push(quote!(pub(super) __plan: ::std::vec::Vec<(usize, usize, usize)>)); + } state.extend((0..struct_regular_fields.len()).map(|index| { let slot = format_ident!("__in_{index}"); quote!(pub(super) #slot: gcore::record::Layout) })); + state.extend(lazy_read_fields(&struct_regular_fields).into_iter().map(|(index, field)| { + let slot = format_ident!("__reads_{index}"); + let arity = field.attribute_reads.len(); + quote!(pub(super) #slot: [Option; #arity]) + })); if !flip_generic_idents.is_empty() { state.push(quote!(pub(super) __marker: ::core::marker::PhantomData (#(#flip_generic_idents,)*)>)); } @@ -334,6 +355,18 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // offsets from the carrier layout; `new` cannot fill that state. let routing_layout_param = (routing.is_some() || opaque).then(|| quote!(__layout: &gcore::record::Layout,)).into_iter(); let routing_layout_init = (routing.is_some() || opaque).then(|| quote!(__layout: __layout.clone(),)).into_iter(); + let routing_value_layouts: Vec = routing + .as_ref() + .map(|routing| routing_value_indices(&struct_regular_fields, routing)) + .unwrap_or_default(); + let routing_in_params = routing_value_layouts.iter().map(|index| { + let slot = format_ident!("__in_{index}"); + quote!(#slot: &gcore::record::Layout,) + }); + let routing_in_inits = routing_value_layouts.iter().map(|index| { + let slot = format_ident!("__in_{index}"); + quote!(#slot: #slot.clone(),) + }); let flip_layout_params = flip .then(|| { (0..struct_regular_fields.len()).map(|index| { @@ -353,17 +386,48 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .into_iter() .flatten(); let flip_prelude = flip - .then(|| { - quote! { + .then(|| match flip_carrier(parsed) { + true => quote! { + let __layout = __in_0.with_writes(__in_0.depth, gcore::record::element_write::<#slot_value_type>(), &[]); + let __plan = gcore::record::copy_plan(__in_0, &__layout, false, &[]); + let __frame_bytes = __layout.frame_bytes(); + }, + false => quote! { let __layout = gcore::record::Layout::default().with_writes(0, gcore::record::element_write::<#slot_value_type>(), &[]); let __frame_bytes = __layout.frame_bytes(); - } + }, }) .into_iter(); + let flip_read_bindings = flip + .then(|| { + lazy_read_fields(&struct_regular_fields).into_iter().map(|(index, field)| { + let arr = format_ident!("__reads_{index}"); + let slot = format_ident!("__in_{index}"); + let offsets = field.attribute_reads.iter().map(|read| { + let marker = &read.marker; + quote!(#slot.offset_of(<#marker as gcore::attribute::Attribute>::NAME, 0)) + }); + quote!(let #arr = [#(#offsets),*];) + }) + }) + .into_iter() + .flatten(); + let flip_read_inits = flip + .then(|| { + lazy_read_fields(&struct_regular_fields).into_iter().map(|(index, _)| { + let arr = format_ident!("__reads_{index}"); + quote!(#arr,) + }) + }) + .into_iter() + .flatten(); let flip_output_inits = flip - .then(|| match flip_generic_idents.is_empty() { - true => quote!(__layout, __frame_bytes,), - false => quote!(__layout, __frame_bytes, __marker: ::core::marker::PhantomData,), + .then(|| { + let plan = flip_carrier(parsed).then(|| quote!(__plan,)); + match flip_generic_idents.is_empty() { + true => quote!(__layout, __frame_bytes, #plan), + false => quote!(__layout, __frame_bytes, #plan __marker: ::core::marker::PhantomData,), + } }) .into_iter(); // The flip prelude's `element_write` instantiates the erased glue at the @@ -380,12 +444,15 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn impl<'n, #(#struct_generic_params,)*> #struct_name<#(#struct_type_params,)*> #(#new_where)* { #[allow(clippy::too_many_arguments)] - pub fn new(#(#new_args,)* #(#routing_layout_param)* #(#flip_layout_params)*) -> Self { + pub fn new(#(#new_args,)* #(#routing_layout_param)* #(#routing_in_params)* #(#flip_layout_params)*) -> Self { #(#flip_prelude)* + #(#flip_read_bindings)* Self { #(#all_field_inits,)* #(#routing_layout_init)* + #(#routing_in_inits)* #(#flip_layout_inits)* + #(#flip_read_inits)* #(#flip_output_inits)* } } @@ -751,6 +818,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let skips_carrier = record.as_ref().is_some_and(|shape| shape.skips_carrier()); let routing = routing_io(parsed); let flip = record_flip(parsed); + let carrier_flip = flip_carrier(parsed); let opaque = record_opaque(parsed); let snapshot_ctx = async_fn && matches!(&parsed.input.ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "CtxSnapshot")); @@ -958,11 +1026,24 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let routing_source = |ty: &Type| matches!((&routing, ty), (Some(routing), Type::Path(path)) if path.path.get_ident() == Some(&routing.generic)); - let attr_kernel_params = parsed.attribute_reads.iter().map(|read| { - let pat = &read.pat_ident; - let marker = &read.marker; - quote!(#pat: #core_types::attribute::Attr<#marker>) - }); + let lazy_read_out = |field: &ParsedField, output_type: &Type| { + let attr_tys = field.attribute_reads.iter().map(|read| { + let marker = &read.marker; + quote!(#core_types::attribute::Attr<#marker>) + }); + match field.attribute_reads.is_empty() { + true => quote!(#output_type), + false => quote!((#output_type #(, #attr_tys)*)), + } + }; + let read_tuple_param = |field: &ParsedField, value_param: TokenStream2, value_ty: TokenStream2| { + let read_pats = field.attribute_reads.iter().map(|read| &read.pat_ident); + let read_tys = field.attribute_reads.iter().map(|read| { + let marker = &read.marker; + quote!(#core_types::attribute::Attr<#marker>) + }); + quote!((#value_param #(, #read_pats)*): (#value_ty #(, #read_tys)*)) + }; let kernel_params = regular_fields .iter() .enumerate() @@ -971,6 +1052,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let pat = &field.pat_ident; match &field.ty { ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(#pat: &#ty), + ParsedFieldType::Regular(RegularParsedField { ty, .. }) if !field.attribute_reads.is_empty() => read_tuple_param(field, quote!(#pat), quote!(#ty)), ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#pat: #ty), ParsedFieldType::Node(NodeParsedField { output_type, .. }) if derive_routing && routing_source(output_type) => { let source_generic = format_ident!("__Source{index}"); @@ -982,11 +1064,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } ParsedFieldType::Node(NodeParsedField { output_type, .. }) if flip && raw_lazy => { let source_generic = format_ident!("__Source{index}"); - quote!(#pat: &#core_types::record::ElementEdge<'_, #output_type, #source_generic>) + let out = lazy_read_out(field, output_type); + quote!(#pat: &#core_types::record::ElementEdge<'_, #out, #source_generic>) } ParsedFieldType::Node(NodeParsedField { output_type, .. }) if flip => { let source_generic = format_ident!("__Source{index}"); - quote!(#pat: #core_types::record::ElementLazyInput<'_, #output_type, #source_generic>) + let out = lazy_read_out(field, output_type); + quote!(#pat: #core_types::record::ElementLazyInput<'_, #out, #source_generic>) } ParsedFieldType::Node(NodeParsedField { output_type, .. }) if raw_lazy => { let bound = lazy_bound(output_type); @@ -997,8 +1081,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(#pat: #core_types::node::LazyInput<'_, impl #bound>) } } - }) - .chain(attr_kernel_params); + }); let record_value_ty: Type = syn::parse_quote!(#core_types::record::RecordValue<'__record>); let node_bounds = regular_fields.iter().enumerate().zip(&node_generics).map(|((index, field), node_generic)| match &field.ty { @@ -1013,9 +1096,15 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn ParsedFieldType::Regular(_) if record.is_some() && !skips_carrier && index == 0 => { quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>) } + ParsedFieldType::Regular(_) if record.is_some() && !field.attribute_reads.is_empty() => { + quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>) + } ParsedFieldType::Regular(RegularParsedField { ty, .. }) if routing_source(ty) => { quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>) } + ParsedFieldType::Regular(_) if routing.is_some() => { + quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #record_value_ty>) + } ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#node_generic: #core_types::node::Node<#ctx_ident, Output = #ty>), ParsedFieldType::Node(NodeParsedField { output_type, .. }) if routing_source(output_type) => match derives { true => quote!(#node_generic: for<'__derived> #core_types::record::DerivedRecordEdge<'__derived, #core_types::context::Derived<'__derived, #ctx_ident>>), @@ -1041,11 +1130,14 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn lend_outlives.push(quote!(#inner: #lifetime)); } + // The slot persists the plain value even on record wires, so the Clone + // bound targets the slot type, not the (possibly lifted) trait output. + let slot_ty = slot_value_type(&parsed.output_type); let mut async_bounds = match (async_fn, future_kernel) { (false, false) => Vec::new(), - (false, true) => vec![quote!(#trait_output: Clone)], + (false, true) => vec![quote!(#slot_ty: Clone)], (true, _) => { - let output_clone = std::iter::once(quote!(#trait_output: Clone)); + let output_clone = std::iter::once(quote!(#slot_ty: Clone)); let value_clones = regular_fields.iter().filter_map(|field| match &field.ty { ParsedFieldType::Regular(RegularParsedField { ty, .. }) => Some(quote!(#ty: Clone)), _ => None, @@ -1071,10 +1163,54 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn (number_hard_min.is_some() || number_hard_max.is_some()).then(|| quote!(#ty: #core_types::misc::Clampable)) }); + let flat_reads = field_reads(®ular_fields); + let read_binding = |slot: usize, read: &AttributeRead, rec: TokenStream2| { + let pat = &read.pat_ident; + let marker = &read.marker; + let slot = format_ident!("__read_{slot}"); + quote! { + let #pat = #core_types::attribute::Attr::<#marker>(match self.#slot { + Some(__offset) => unsafe { #rec.read(__offset) }, + None => <#marker as #core_types::attribute::Attribute>::default(), + }); + } + }; + let reads_of = |field_index: usize| { + flat_reads + .iter() + .enumerate() + .filter(move |(_, (owner, _))| *owner == field_index) + .map(|(slot, (_, read))| (slot, *read)) + .collect::>() + }; + let eval_values = regular_fields.iter().enumerate().map(|(index, field)| { let name = &field.pat_ident.ident; match &field.ty { ParsedFieldType::Regular(_) if record.is_some() && !skips_carrier && index == 0 => quote!(), + // A carrier primary evaluates beyond the node's own frame in the + // flip tail, so its fields survive until the carry. + ParsedFieldType::Regular(_) if carrier_flip && index == 0 => quote!(), + // A reading secondary input claims a record edge: the element and + // the declared reads copy out right after its eval, before any + // later sibling eval can reuse the record stack. + ParsedFieldType::Regular(RegularParsedField { ty, .. }) if record.is_some() && !field.attribute_reads.is_empty() => { + let slot = format_ident!("__in_{index}"); + let rec_local = format_ident!("__rec_{index}"); + let bindings: Vec = reads_of(index).into_iter().map(|(slot, read)| read_binding(slot, read, quote!(#rec_local))).collect(); + quote! { + let #name = match __cell.eval_input(#index, &self.#name, __input) { + Ok(value) => value, + Err(interrupt) => return interrupt.into(), + }; + let #rec_local = self.#slot.rec(&#name); + #(#bindings)* + let #name: #ty = unsafe { #core_types::record::read_element(#rec_local) }; + } + } + // A borrow taken before the node's own frame push parks a + // byte-carried spilled element into the arena; parked and inline + // elements borrow directly. ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) if flip => { let slot = format_ident!("__in_{index}"); let record_local = format_ident!("__record_{index}"); @@ -1083,7 +1219,14 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn Ok(value) => value, Err(interrupt) => return interrupt.into(), }; - let #name: &#ty = unsafe { #core_types::record::borrow_element(self.#slot.rec(&#record_local)) }; + let Some(#name) = (unsafe { + #core_types::record::borrow_or_park::<#ty>(self.#slot.rec(&#record_local), &self.#slot, #core_types::context::ExtractArena::arena(__input)) + }) else { + return #core_types::gpoll::GPoll::Error(::std::boxed::Box::new(#core_types::gpoll::GraphError { + kind: #core_types::gpoll::ErrorKind::ArenaExhausted, + trace: ::std::vec::Vec::new(), + })); + }; } } ParsedFieldType::Regular(RegularParsedField { ty, .. }) if flip => { @@ -1096,6 +1239,19 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let #name: #ty = unsafe { #core_types::record::read_element(self.#slot.rec(&#name)) }; } } + // A routing node's value input rides a record edge; the element + // copies out right after its eval, before any source evaluation + // can reuse the record stack. + ParsedFieldType::Regular(RegularParsedField { ty, .. }) if routing.is_some() && !routing_source(ty) => { + let slot = format_ident!("__in_{index}"); + quote! { + let #name = match __cell.eval_input(#index, &self.#name, __input) { + Ok(value) => value, + Err(interrupt) => return interrupt.into(), + }; + let #name: #ty = unsafe { #core_types::record::read_element(self.#slot.rec(&#name)) }; + } + } ParsedFieldType::Regular(_) => quote! { let #name = match __cell.eval_input(#index, &self.#name, __input) { Ok(value) => value, @@ -1107,14 +1263,32 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }, ParsedFieldType::Node(NodeParsedField { output_type, .. }) if flip && raw_lazy => { let slot = format_ident!("__in_{index}"); - quote! { - let #name = #core_types::record::ElementEdge::<#output_type, _>::new(&self.#name, &self.#slot); + match field.attribute_reads.is_empty() { + true => quote! { + let #name = #core_types::record::ElementEdge::<#output_type, _>::new(&self.#name, &self.#slot); + }, + false => { + let arr = format_ident!("__reads_{index}"); + let read_fn = format_ident!("__{}_read_{}", fn_name, index); + quote! { + let #name = #core_types::record::ElementEdge::with_reads(&self.#name, &self.#slot, &self.#arr, self::#read_fn); + } + } } } ParsedFieldType::Node(NodeParsedField { output_type, .. }) if flip => { let slot = format_ident!("__in_{index}"); - quote! { - let #name = #core_types::record::ElementLazyInput::<#output_type, _>::new(&self.#name, &__cell, #index, &self.#slot); + match field.attribute_reads.is_empty() { + true => quote! { + let #name = #core_types::record::ElementLazyInput::<#output_type, _>::new(&self.#name, &__cell, #index, &self.#slot); + }, + false => { + let arr = format_ident!("__reads_{index}"); + let read_fn = format_ident!("__{}_read_{}", fn_name, index); + quote! { + let #name = #core_types::record::ElementLazyInput::with_reads(&self.#name, &__cell, #index, &self.#slot, &self.#arr, self::#read_fn); + } + } } } ParsedFieldType::Node(NodeParsedField { output_type, .. }) if opaque && raw_lazy && is_record_value(output_type) => quote! { @@ -1127,7 +1301,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } }); - let clamps = regular_fields.iter().filter_map(|field| { + let clamp_tokens = |field: &ParsedField| { let ParsedFieldType::Regular(RegularParsedField { number_hard_min, number_hard_max, .. }) = &field.ty else { return None; }; @@ -1140,7 +1314,13 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn tokens.extend(quote!(let #name = #core_types::misc::Clampable::clamp_hard_max(#name, #max);)); } (!tokens.is_empty()).then_some(tokens) - }); + }; + // A carrier primary binds in the flip tail; its clamp runs there too. + let clamps = regular_fields + .iter() + .enumerate() + .filter(|(index, _)| !(carrier_flip && *index == 0)) + .filter_map(|(_, field)| clamp_tokens(field)); let call_args = regular_fields.iter().filter(|field| !injected_name(&field.pat_ident.ident)).map(|field| { let name = &field.pat_ident.ident; @@ -1282,15 +1462,55 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .filter(|field| matches!(field.ty, ParsedFieldType::Regular(_))) .map(|field| &field.pat_ident.ident) .collect(); + // A carried tail claims the node's frame first, evaluates the carrier + // beyond it, and carries its fields; every exit closes the frame through + // `lift_poll_into`. + let carried_prelude = carrier_flip.then(|| { + let field = regular_fields[0]; + let name = &field.pat_ident.ident; + let read = match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => { + quote!(let #name: &#ty = unsafe { #core_types::record::borrow_element(__src_rec) };) + } + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(let #name: #ty = unsafe { #core_types::record::read_element(__src_rec) };), + _ => unreachable!("a flip carrier is a regular value input"), + }; + let clamp = clamp_tokens(field); + quote! { + let mut __carried = #core_types::record::RecordValue::zeroed(); + let __dst = match self.__frame_bytes { + 0 => __carried.as_mut_ptr(), + __bytes => #core_types::record::stack::push(__bytes), + }; + let __src = match __cell.eval_input(0, &self.#name, __input) { + Ok(value) => value, + Err(interrupt) => return interrupt.into(), + }; + let __src_rec = self.__in_0.rec(&__src); + unsafe { #core_types::record::apply_plan(__src_rec, __dst, &self.__plan) }; + #read + #clamp + } + }); // Async slots persist plain values across evaluations; a flipped source - // lifts the slot value onto its record wire at every merge point. - let merge_lifted = |poll: TokenStream2| match flip { - true => quote!(__cell.merge(#core_types::record::lift_poll(#poll, &self.__layout, #core_types::context::ExtractArena::arena(__input)))), - false => quote!(__cell.merge(#poll)), + // lifts the slot value onto its record wire at every merge point, into + // the carried frame when the node has a carrier. + let merge_lifted = |poll: TokenStream2| match (flip, carrier_flip) { + (true, true) => { + quote!(__cell.merge(unsafe { #core_types::record::lift_poll_into(#poll, __dst, self.__frame_bytes, #core_types::context::ExtractArena::arena(__input)) })) + } + (true, false) => quote!(__cell.merge(#core_types::record::lift_poll(#poll, &self.__layout, #core_types::context::ExtractArena::arena(__input)))), + (false, _) => quote!(__cell.merge(#poll)), + }; + let pending_return = match flip && carrier_flip { + true => quote! { + unsafe { #core_types::record::lift_poll_into::<#slot_ty>(#core_types::gpoll::GPoll::Pending, __dst, self.__frame_bytes, #core_types::context::ExtractArena::arena(__input)) } + }, + false => quote!(#core_types::gpoll::GPoll::Pending), }; let inflight = match &parsed.attributes.placeholder { Some(path) => merge_lifted(quote!(#core_types::gpoll::GPoll::Partial(#path(#(&#placeholder_value_names),*)))), - None => quote!(#core_types::gpoll::GPoll::Pending), + None => pending_return.clone(), }; let slot_hit = merge_lifted(quote!(value.clone())); let slot_check = quote! { @@ -1335,10 +1555,17 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } }; let record_tail = record.as_ref().map(|shape| { + let tuple_arg = |field: &ParsedField, value: TokenStream2| match field.attribute_reads.is_empty() { + true => value, + false => { + let read_pats = field.attribute_reads.iter().map(|read| &read.pat_ident.ident); + quote!((#value #(, #read_pats)*)) + } + }; let carrier_arg = match &shape.carrier { RecordCarrier::None => None, - RecordCarrier::Token(_) => Some(quote!(#core_types::record::ElToken)), - RecordCarrier::Read(ty) => Some(quote!(unsafe { __src_rec.element::<#ty>() })), + RecordCarrier::Token(_) => Some(tuple_arg(regular_fields[0], quote!(#core_types::record::ElToken))), + RecordCarrier::Read(ty) => Some(tuple_arg(regular_fields[0], quote!(unsafe { #core_types::record::read_element::<#ty>(__src_rec) }))), } .into_iter(); let value_args = regular_fields.iter().skip(if shape.skips_carrier() { 0 } else { 1 }).map(|field| { @@ -1347,14 +1574,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // A lend param binds an owned edge; the kernel borrows the // evaluated value. ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }) => quote!(&#name), - _ => quote!(#name), + _ => tuple_arg(field, quote!(#name)), } }); - let attr_args = parsed.attribute_reads.iter().map(|read| { - let pat = &read.pat_ident.ident; - quote!(#pat) - }); - let record_kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #carrier_arg)* #(, #value_args)* #(, #attr_args)*)); + let record_kernel_call = quote!(self::#fn_name(__input #(, &self.#data_names)* #(, #carrier_arg)* #(, #value_args)*)); let carrier_eval = (!shape.skips_carrier()).then(|| { let name = ®ular_fields[0].pat_ident.ident; quote! { @@ -1366,17 +1589,10 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } }); let carry = (!shape.skips_carrier()).then(|| quote!(unsafe { #core_types::record::apply_plan(__src_rec, __dst, &self.__plan) };)); - let read_bindings = parsed.attribute_reads.iter().enumerate().map(|(index, read)| { - let pat = &read.pat_ident; - let marker = &read.marker; - let slot = format_ident!("__read_{index}"); - quote! { - let #pat = #core_types::attribute::Attr::<#marker>(match self.#slot { - Some(__offset) => unsafe { __src_rec.read(__offset) }, - None => <#marker as #core_types::attribute::Attribute>::default(), - }); - } - }); + let carrier_read_bindings: Vec = match shape.skips_carrier() { + true => Vec::new(), + false => reads_of(0).into_iter().map(|(slot, read)| read_binding(slot, read, quote!(__src_rec))).collect(), + }; let kernel_value = match shape.dialect { RecordDialect::Plain => quote!(#record_kernel_call), RecordDialect::Interrupt => quote! { @@ -1391,9 +1607,29 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn Some(_) => quote!(__element), None => quote!(_), }; - let destructure = match attr_binders.is_empty() { + // Slot binders in the return tuple's own order: an `Attr` binds the + // next write binder, a `RemoveAttr` binds nothing. + let slot_binders: Vec = { + let mut binders = attr_binders.iter(); + match slot_value_type(&parsed.output_type) { + Type::Tuple(tuple) => tuple + .elems + .iter() + .skip(1) + .map(|slot| match attr_marker(slot) { + Some(_) => { + let binder = binders.next().expect("write binders match the Attr slots"); + quote!(#core_types::attribute::Attr(#binder)) + } + None => quote!(_), + }) + .collect(), + _ => Vec::new(), + } + }; + let destructure = match slot_binders.is_empty() { true => quote!(let #element_binder = __kernel_value;), - false => quote!(let (#element_binder #(, #core_types::attribute::Attr(#attr_binders))*) = __kernel_value;), + false => quote!(let (#element_binder #(, #slot_binders)*) = __kernel_value;), }; let element_store = shape.element_write.as_ref().map(|ty| quote!(unsafe { #core_types::record::write_field::<#ty>(__dst, 0, __element) };)); let attr_stores = attr_binders.iter().enumerate().map(|(index, binder)| { @@ -1408,7 +1644,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }; #carrier_eval #carry - #(#read_bindings)* + #(#carrier_read_bindings)* let __kernel_value = #kernel_value; #destructure #element_store @@ -1422,8 +1658,14 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }); let flip_tail = flip.then(|| { if matches!(kernel_kind(&parsed.output_type), KernelKind::Poll(_)) { - return quote! { - __cell.merge(#core_types::record::lift_poll(#kernel_call, &self.__layout, #core_types::context::ExtractArena::arena(__input))) + return match &carried_prelude { + Some(prelude) => quote! { + #prelude + __cell.merge(unsafe { #core_types::record::lift_poll_into(#kernel_call, __dst, self.__frame_bytes, #core_types::context::ExtractArena::arena(__input)) }) + }, + None => quote! { + __cell.merge(#core_types::record::lift_poll(#kernel_call, &self.__layout, #core_types::context::ExtractArena::arena(__input))) + }, }; } let kernel_value = match kernel_kind(&parsed.output_type) { @@ -1435,25 +1677,20 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }, _ => quote!(#kernel_call), }; - quote! { - let __kernel_value = #kernel_value; - let mut __value = #core_types::record::RecordValue::zeroed(); - let __dst = match self.__frame_bytes { - 0 => __value.as_mut_ptr(), - __bytes => #core_types::record::stack::push(__bytes), - }; - let __written = unsafe { #core_types::record::write_element(__dst, __kernel_value, #core_types::context::ExtractArena::arena(__input)) }; - if self.__frame_bytes != 0 { - #core_types::record::stack::pop(__dst); - __value = #core_types::record::RecordValue::spilled(unsafe { #core_types::record::Rec::new(__dst.cast_const()) }); - } - match __written { - Some(()) => __cell.finish(__value), - None => #core_types::gpoll::GPoll::Error(::std::boxed::Box::new(#core_types::gpoll::GraphError { - kind: #core_types::gpoll::ErrorKind::ArenaExhausted, - trace: ::std::vec::Vec::new(), - })), - } + match &carried_prelude { + // The carrier evaluates beyond the claimed frame, so the kernel + // runs after the push. + Some(prelude) => quote! { + #prelude + let __kernel_value = #kernel_value; + __cell.merge(unsafe { + #core_types::record::lift_poll_into(#core_types::gpoll::GPoll::Final(__kernel_value), __dst, self.__frame_bytes, #core_types::context::ExtractArena::arena(__input)) + }) + }, + None => quote! { + let __kernel_value = #kernel_value; + __cell.merge(#core_types::record::lift_poll(#core_types::gpoll::GPoll::Final(__kernel_value), &self.__layout, #core_types::context::ExtractArena::arena(__input))) + }, } }); let eval_tail = match (async_fn, future_kernel) { @@ -1470,7 +1707,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .chain(kernel_value_names.iter().map(|name| quote!(#name.clone()))); let completion = future_completion(&parsed.output_type); let tail = spawn_tail(completion, inflight.clone()); + let prelude = carried_prelude.iter(); quote! { + #(#prelude)* #slot_check #(#snapshot_binding)* let __future = self::#fn_name(#(#future_args),*); @@ -1483,7 +1722,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(let __placeholder = #path(#(&#placeholder_value_names),*);), merge_lifted(quote!(#core_types::gpoll::GPoll::Partial(__placeholder))), ), - None => (quote!(), quote!(#core_types::gpoll::GPoll::Pending)), + None => (quote!(), pending_return.clone()), }; let acquire = match kernel_kind(&parsed.output_type) { KernelKind::FutureInterrupt(_) => quote! { @@ -1500,7 +1739,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }; let completion = future_completion(&payload); let tail = spawn_tail(completion, spawn_return); + let prelude = carried_prelude.iter(); quote! { + #(#prelude)* #slot_check #placeholder_binding #acquire @@ -1509,14 +1750,35 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } }; - let record_bounds: Vec = match &record { - Some(shape) if shape.skips_carrier() => { - vec![quote!(#ctx_ident: #core_types::context::ExtractArena)] + let record_bounds: Vec = { + let mut bounds = match &record { + Some(shape) if shape.skips_carrier() => { + vec![quote!(#ctx_ident: #core_types::context::ExtractArena)] + } + None if derive_routing || flip => { + vec![quote!(#ctx_ident: #core_types::context::ExtractArena)] + } + _ => Vec::new(), + }; + // A reading secondary input's element copies out of its record, as + // does a concrete carrier read. + if let Some(shape) = &record { + bounds.extend(reading_secondary_indices(®ular_fields, shape).into_iter().filter_map(|index| match ®ular_fields[index].ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => Some(quote!(#ty: ::core::clone::Clone)), + _ => None, + })); + if let RecordCarrier::Read(ty) = &shape.carrier { + bounds.push(quote!(#ty: ::core::clone::Clone)); + } } - None if derive_routing || flip => { - vec![quote!(#ctx_ident: #core_types::context::ExtractArena)] + // A routing node's value elements copy out of their records. + if let Some(routing) = &routing { + bounds.extend(routing_value_indices(®ular_fields, routing).into_iter().filter_map(|index| match ®ular_fields[index].ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => Some(quote!(#ty: ::core::clone::Clone)), + _ => None, + })); } - _ => Vec::new(), + bounds }; let flip_bounds: Vec = match flip { @@ -1524,7 +1786,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let mut bounds: Vec = regular_fields .iter() .filter_map(|field| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }) => None, + // The conditional arena-park moves a lend element once. + ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => Some(quote!(#ty: ::core::marker::Send + ::core::marker::Sync + 'static)), ParsedFieldType::Regular(RegularParsedField { ty, .. }) => Some(quote!(#ty: ::core::clone::Clone)), ParsedFieldType::Node(NodeParsedField { output_type, .. }) => Some(quote!(#output_type: ::core::clone::Clone)), }) @@ -1555,6 +1818,12 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn .iter() .map(|marker| quote!(#core_types::record::FieldWrite::of::<#marker>(0))) .collect(); + let remove_pairs: Vec = shape + .removes + .iter() + .map(|marker| quote!((<#marker as #core_types::attribute::Attribute>::NAME, 0))) + .collect(); + let subtraction = (!remove_pairs.is_empty()).then(|| quote!(.without(&[#(#remove_pairs),*]))); let element = match &shape.element_write { Some(ty) => quote!(#core_types::record::element_write::<#ty>()), None => quote!(__carrier.element), @@ -1567,25 +1836,35 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn }, false => quote! { #vis fn #layout_fn(__carrier: &#core_types::record::Layout) -> #core_types::record::Layout { - __carrier.with_writes(__carrier.depth, #element, &[#(#write_descs),*]) + __carrier #subtraction.with_writes(__carrier.depth, #element, &[#(#write_descs),*]) } }, }; + let reading_secondaries = reading_secondary_indices(®ular_fields, shape); let edge_args = regular_fields.iter().zip(&node_generics).map(|(field, generic)| { let name = &field.pat_ident.ident; quote!(#name: #generic) }); let carrier_layout_param = (!shape.skips_carrier()).then(|| quote!(__carrier_layout: &#core_types::record::Layout,)).into_iter(); + let input_layout_params = reading_secondaries.iter().map(|index| { + let slot = format_ident!("__in_{index}"); + quote!(#slot: &#core_types::record::Layout,) + }); let layout_binding = match shape.skips_carrier() { true => quote!(let __layout = self::#layout_fn();), false => quote!(let __layout = self::#layout_fn(__carrier_layout);), }; let carry_element = shape.carries_element(); - let plan_binding = (!shape.skips_carrier()).then(|| quote!(let __plan = #core_types::record::copy_plan(__carrier_layout, &__layout, #carry_element);)); - let read_inits = parsed.attribute_reads.iter().enumerate().map(|(index, read)| { + let plan_binding = + (!shape.skips_carrier()).then(|| quote!(let __plan = #core_types::record::copy_plan(__carrier_layout, &__layout, #carry_element, &[#(#remove_pairs),*]);)); + let read_inits = flat_reads.iter().enumerate().map(|(slot, (owner, read))| { let marker = &read.marker; - let slot = format_ident!("__read_{index}"); - quote!(let #slot = __carrier_layout.offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0);) + let slot = format_ident!("__read_{slot}"); + let source = match !shape.skips_carrier() && *owner == 0 { + true => quote!(__carrier_layout), + false => format_ident!("__in_{owner}").to_token_stream(), + }; + quote!(let #slot = #source.offset_of(<#marker as #core_types::attribute::Attribute>::NAME, 0);) }); let write_inits = shape.write_markers.iter().enumerate().map(|(index, marker)| { let slot = format_ident!("__write_{index}"); @@ -1601,8 +1880,12 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn quote!(#name,) }); let carrier_init = (!shape.skips_carrier()).then(|| quote!(__carrier: __carrier_layout.clone(),)).into_iter(); + let input_layout_inits = reading_secondaries.iter().map(|index| { + let slot = format_ident!("__in_{index}"); + quote!(#slot: #slot.clone(),) + }); let plan_init = (!shape.skips_carrier()).then(|| quote!(__plan,)).into_iter(); - let read_names = (0..parsed.attribute_reads.len()).map(|index| format_ident!("__read_{index}")).map(|slot| quote!(#slot,)); + let read_names = (0..flat_reads.len()).map(|index| format_ident!("__read_{index}")).map(|slot| quote!(#slot,)); let write_names = (0..shape.write_markers.len()).map(|index| format_ident!("__write_{index}")).map(|slot| quote!(#slot,)); quote! { #layout_def @@ -1610,7 +1893,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #[automatically_derived] impl<#(#data_field_generic_idents,)* #(#node_generics,)*> #mod_name::#struct_name<#(#struct_type_params,)*> { #[allow(clippy::too_many_arguments)] - #vis fn new(#(#edge_args,)* #(#carrier_layout_param)*) -> Self { + #vis fn new(#(#edge_args,)* #(#carrier_layout_param)* #(#input_layout_params)*) -> Self { #layout_binding #plan_binding #(#read_inits)* @@ -1620,6 +1903,7 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn #(#data_inits)* #(#edge_inits)* #(#carrier_init)* + #(#input_layout_inits)* __layout, #(#plan_init)* __frame_bytes, @@ -1663,11 +1947,55 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn } }; + let lazy_read_fns: Vec = lazy_read_fields(®ular_fields) + .into_iter() + .map(|(index, field)| { + let ParsedFieldType::Node(NodeParsedField { output_type, .. }) = &field.ty else { + unreachable!("lazy read fields are Node fields"); + }; + let read_fn = format_ident!("__{}_read_{}", fn_name, index); + let generics: Vec<&Ident> = parsed + .fn_generics + .iter() + .filter_map(|param| match param { + GenericParam::Type(type_param) if type_contains_ident(output_type, &type_param.ident) => Some(&type_param.ident), + _ => None, + }) + .collect(); + let attr_slots = field.attribute_reads.iter().enumerate().map(|(slot, read)| { + let marker = &read.marker; + quote! { + #core_types::attribute::Attr::<#marker>(match __reads[#slot] { + Some(__offset) => unsafe { __rec.read(__offset) }, + None => <#marker as #core_types::attribute::Attribute>::default(), + }) + } + }); + let attr_tys = field.attribute_reads.iter().map(|read| { + let marker = &read.marker; + quote!(#core_types::attribute::Attr<'__read, #marker>) + }); + quote! { + /// # Safety + /// `__rec` must be a record whose element is the declared output + /// type, of the layout `__reads` was resolved against. + unsafe fn #read_fn<'__read #(, #generics)*>(__rec: #core_types::record::Rec, __reads: &[Option]) -> (#output_type #(, #attr_tys)*) + where + #output_type: ::core::clone::Clone, + { + (unsafe { #core_types::record::read_element::<#output_type>(__rec) } #(, #attr_slots)*) + } + } + }) + .collect(); + Ok(NodeImplTokens { in_mod: entries, top_level: quote! { #kernel + #(#lazy_read_fns)* + #record_wiring #top_level @@ -1693,13 +2021,14 @@ pub(crate) enum RecordCarrier { } /// The record io of a node fn: how the carrier lowers, the element write, -/// and the written markers. Present exactly when the signature declares -/// attribute reads or writes in a shape the record tier supports; malformed -/// record io is reported by validation and generates no node impl. +/// and the markers written and removed. Present exactly when the signature +/// declares attribute reads or writes in a shape the record tier supports; +/// malformed record io is reported by validation and generates no node impl. pub(crate) struct RecordShape { pub(crate) carrier: RecordCarrier, pub(crate) element_write: Option, pub(crate) write_markers: Vec, + pub(crate) removes: Vec, pub(crate) dialect: RecordDialect, } @@ -1713,8 +2042,68 @@ impl RecordShape { } } +/// Whether the signature declares record-tier attribute io: value-input reads +/// or return-tuple writes. Reads on lazy inputs belong to the record lowering +/// of the flip class instead. pub(crate) fn has_record_io(parsed: &ParsedNodeFn) -> bool { - !parsed.attribute_reads.is_empty() || record_writes(&slot_value_type(&parsed.output_type)).is_some() + let value_reads = parsed + .fields + .iter() + .any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Regular(_))); + value_reads || record_writes(&slot_value_type(&parsed.output_type)).is_some() +} + +pub(crate) fn has_lazy_reads(parsed: &ParsedNodeFn) -> bool { + parsed + .fields + .iter() + .any(|field| !field.attribute_reads.is_empty() && matches!(field.ty, ParsedFieldType::Node(_))) +} + +/// The value inputs of a routing node (every regular field that is not a +/// routing source), with their indices into the regular fields. +pub(crate) fn routing_value_indices(regular_fields: &[&ParsedField], routing: &RoutingIo) -> Vec { + regular_fields + .iter() + .enumerate() + .filter(|(_, field)| match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => !matches!(ty, Type::Path(path) if path.path.get_ident() == Some(&routing.generic)), + ParsedFieldType::Node(_) => false, + }) + .map(|(index, _)| index) + .collect() +} + +/// The lazy inputs declaring attribute reads, with their indices into the +/// unit-skipped regular fields. +pub(crate) fn lazy_read_fields<'a>(regular_fields: &[&'a ParsedField]) -> Vec<(usize, &'a ParsedField)> { + regular_fields + .iter() + .enumerate() + .filter(|(_, field)| matches!(field.ty, ParsedFieldType::Node(_)) && !field.attribute_reads.is_empty()) + .map(|(index, field)| (index, *field)) + .collect() +} + +/// The indices (into the unit-skipped regular fields) of value inputs whose +/// reads resolve against their own wire rather than the carrier's. +pub(crate) fn reading_secondary_indices(regular_fields: &[&ParsedField], shape: &RecordShape) -> Vec { + regular_fields + .iter() + .enumerate() + .filter(|(index, field)| !field.attribute_reads.is_empty() && (shape.skips_carrier() || *index != 0)) + .map(|(index, _)| index) + .collect() +} + +/// Every attribute read in field order with the owning field's index, flat so +/// read slots are numbered across inputs. +pub(crate) fn field_reads<'a>(regular_fields: &[&'a ParsedField]) -> Vec<(usize, &'a AttributeRead)> { + regular_fields + .iter() + .enumerate() + .flat_map(|(index, field)| field.attribute_reads.iter().map(move |read| (index, read))) + .collect() } /// Substitutes bare generic idents with their row-assigned types. @@ -1826,12 +2215,22 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option { _ => return None, }; 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 None; } if parsed.is_async || parsed.fields.iter().any(|field| matches!(field.ty, ParsedFieldType::Node(_))) { return None; } + let reads_well_placed = parsed.fields.iter().all(|field| { + field.attribute_reads.is_empty() || (!field.is_data_field && matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: None, .. }))) + }); + if !reads_well_placed { + return None; + } let carrier_field = parsed.fields.first()?; if carrier_field.is_data_field { return None; @@ -1851,9 +2250,9 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option { } }, }; - let (element, write_markers) = match writes { - Some(RecordWrites { element, markers }) => (element, markers), - None => (value, Vec::new()), + let (element, write_markers, removes) = match writes { + Some(RecordWrites { element, markers, removes }) => (element, markers, removes), + None => (value, Vec::new(), Vec::new()), }; let element_write = match &carrier { RecordCarrier::Token(token) => match bare_ident(&element) { @@ -1867,13 +2266,14 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option { Some(element) } }; - if matches!(carrier, RecordCarrier::None) && !parsed.attribute_reads.is_empty() { + if matches!(carrier, RecordCarrier::None) && !removes.is_empty() { return None; } Some(RecordShape { carrier, element_write, write_markers, + removes, dialect, }) } @@ -1891,6 +2291,31 @@ pub(crate) struct RoutingIo { pub(crate) generic: Ident, } +/// Whether a flipped node's primary input is a carrier: the first parameter +/// after the context, when it is an owned or lent value input. A carrier's +/// fields pass through to the output; every production layout is element-only +/// until attribute adoption, so the copy plan is empty and behavior is +/// unchanged. Async kernels carry fields per eval around the slot (only the +/// element crosses the future boundary), so their carrier must be owned: the +/// future captures the element by value. +pub(crate) fn flip_carrier(parsed: &ParsedNodeFn) -> bool { + if !record_flip(parsed) { + return false; + } + let Some(first) = parsed.fields.first() else { return false }; + if first.is_data_field { + return false; + } + let ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) = &first.ty else { + return false; + }; + if matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty()) { + return false; + } + let async_kernel = parsed.is_async || matches!(kernel_kind(&parsed.output_type), KernelKind::Future(_) | KernelKind::FutureInterrupt(_)); + !(async_kernel && lend.is_some()) +} + /// Whether a plain node's lowering flips onto record wires: sync, /// fully-concrete value-input nodes in this cut; batch, shader, async, lend, /// lazy, and generic nodes keep the plain lowering until their record forms @@ -1899,7 +2324,10 @@ pub(crate) fn record_flip(parsed: &ParsedNodeFn) -> bool { if record_shape(parsed).is_some() || has_record_io(parsed) || routing_io(parsed).is_some() { return false; } - if parsed.attributes.batch.is_some() || parsed.attributes.shader_node.is_some() || parsed.attributes.plain { + // Shader nodes flip like any value node: the kernel doubles as the + // shader body on the spirv target, but the struct and Node impl are + // std-gated, so the record machinery never reaches the shader build. + if parsed.attributes.batch.is_some() || parsed.attributes.plain { return false; } if type_disqualifies(&slot_value_type(&parsed.output_type)) { @@ -1911,7 +2339,8 @@ pub(crate) fn record_flip(parsed: &ParsedNodeFn) -> bool { GenericParam::Type(type_param) if Some(&type_param.ident) == ctx_ident.as_ref() => {} // Registry rows assign a generic by unifying a field's type with // the row's, so a generic without an extractable position keeps - // the plain lowering. + // the plain lowering. A `skip_impl` node's rows are hand-written + // with explicit types, so no extractable position is needed. GenericParam::Type(type_param) => { let extractable = parsed.fields.iter().filter(|field| !field.is_data_field).any(|field| { let ty = match &field.ty { @@ -1920,7 +2349,7 @@ pub(crate) fn record_flip(parsed: &ParsedNodeFn) -> bool { }; generic_extractable(ty, &type_param.ident) }); - if !extractable { + if !extractable && !parsed.attributes.skip_impl { return false; } } @@ -2007,7 +2436,9 @@ pub(crate) fn routing_io(parsed: &ParsedNodeFn) -> Option { implementations, }) => { if bare_ident(output_type) == Some(&ident) { - if !implementations.is_empty() || type_contains_ident(input_type, &ident) { + // A source forwards its whole record opaquely; declared + // reads contradict that and are rejected by validation. + if !implementations.is_empty() || type_contains_ident(input_type, &ident) || !field.attribute_reads.is_empty() { return None; } sources += 1; @@ -2437,8 +2868,7 @@ fn routing_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fi return quote!(gcore::registry::generic_record_edge_type(#token_name)); } match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(gcore::registry::edge_type::<#ty>()), - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(gcore::registry::edge_type::<#ty>()), + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(gcore::registry::record_edge_type::<#ty>()), ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(gcore::registry::edge_type::<#output_type>()), } }); @@ -2464,11 +2894,28 @@ fn routing_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fi }; } match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, lend: Some(_), .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;), - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;), + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => { + let handle = format_ident!("__handle_{index}"); + let layout = format_ident!("__in_layout_{index}"); + quote! { + let #handle = inputs.next().unwrap(); + let Some(#layout) = #handle.layout().cloned() else { + return Err(gcore::registry::ConstructionError::MissingLayout); + }; + let #name = #handle.downcast_record::<#ty>()?; + } + } ParsedFieldType::Node(NodeParsedField { output_type, .. }) => quote!(let #name = inputs.next().unwrap().downcast::<#output_type>()?;), } }); + let value_layout_args = regular_fields + .iter() + .enumerate() + .filter(|(_, field)| !is_source(field) && matches!(field.ty, ParsedFieldType::Regular(_))) + .map(|(index, _)| { + let layout = format_ident!("__in_layout_{index}"); + quote!(&#layout,) + }); let source_wraps = regular_fields.iter().enumerate().filter(|(_, field)| is_source(field)).map(|(index, field)| { let name = &field.pat_ident.ident; let layout = format_ident!("__layout_{index}"); @@ -2497,7 +2944,7 @@ fn routing_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fi #(#downcasts)* let __union = gcore::record::Layout::union(&[#(&#source_layouts),*]); #(#source_wraps)* - let __node = #struct_name::new(#(#names,)* &__union); + let __node = #struct_name::new(#(#names,)* &__union, #(#value_layout_args)*); Ok(gcore::registry::EdgeHandle::new_erased( ::std::sync::Arc::new(__node) as ::std::sync::Arc, #first_source_ty, @@ -2603,6 +3050,7 @@ fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fie let entries_name = format_ident!("{}_entries", fn_name); let arity = regular_fields.len(); let names: Vec<&Ident> = regular_fields.iter().map(|field| &field.pat_ident.ident).collect(); + let reading_secondaries = reading_secondary_indices(regular_fields, &shape); let input_types = regular_fields.iter().enumerate().map(|(index, field)| { let ParsedFieldType::Regular(RegularParsedField { ty, .. }) = &field.ty else { @@ -2618,7 +3066,10 @@ fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fie RecordCarrier::None => unreachable!(), }; } - quote!(gcore::registry::edge_type::<#ty>()) + match field.attribute_reads.is_empty() { + true => quote!(gcore::registry::edge_type::<#ty>()), + false => quote!(gcore::registry::record_edge_type::<#ty>()), + } }); let downcasts = regular_fields.iter().enumerate().map(|(index, field)| { let name = &field.pat_ident.ident; @@ -2635,9 +3086,24 @@ fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fie let #name = __carrier_handle.downcast_erased::(__carrier_ty.clone())?; }; } + if !field.attribute_reads.is_empty() { + let layout_local = format_ident!("__in_layout_{index}"); + return quote! { + let __in_handle = inputs.next().unwrap(); + let __in_ty = __in_handle.ty().clone(); + let Some(#layout_local) = __in_handle.layout().cloned() else { + return Err(gcore::registry::ConstructionError::MissingLayout); + }; + let #name = __in_handle.downcast_erased::(__in_ty)?; + }; + } quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;) }); let wire_layout_arg = carrier_in_fields.then(|| quote!(&__carrier_layout,)).into_iter(); + let input_layout_args = reading_secondaries.iter().map(|index| { + let layout_local = format_ident!("__in_layout_{index}"); + quote!(&#layout_local,) + }); let (io_output, construct_output) = match (&shape.carrier, &shape.element_write) { (RecordCarrier::Token(token), _) => { let name = token.to_string(); @@ -2670,7 +3136,7 @@ fn record_entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, regular_fie } let mut inputs = inputs.into_iter(); #(#downcasts)* - let __node = #struct_name::new(#(#names,)* #(#wire_layout_arg)*); + let __node = #struct_name::new(#(#names,)* #(#wire_layout_arg)* #(#input_layout_args)*); #construct_output }, }] diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index 36c3a6996d..4564962655 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -35,13 +35,12 @@ pub(crate) struct ParsedNodeFn { pub(crate) output_type: Type, pub(crate) is_async: bool, pub(crate) fields: Vec, - pub(crate) attribute_reads: Vec, pub(crate) body: TokenStream2, pub(crate) description: String, } -/// An `Attr` parameter: a declared attribute read on the carrier's -/// items, not a wired input. +/// An `Attr` 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, + pub(crate) removes: Vec, } pub(crate) fn record_writes(value: &Type) -> Option { 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> = 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` type, if `ty` is one. pub(crate) fn attr_marker(ty: &Type) -> Option { + marker_of(ty, "Attr") +} + +/// Returns the marker type of a `RemoveAttr` type, if `ty` is one. +pub(crate) fn remove_attr_marker(ty: &Type) -> Option { + marker_of(ty, "RemoveAttr") +} + +fn marker_of(ty: &Type, wrapper: &str) -> Option { 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, pub unit: Option, 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, } #[derive(Clone, Debug)] @@ -637,7 +659,7 @@ fn parse_node_fn(attr: TokenStream2, item: TokenStream2) -> syn::Result syn::Result) -> syn::Result<(Input, Vec, Vec)> { +fn parse_inputs(inputs: &Punctuated) -> syn::Result<(Input, Vec)> { 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) -> syn::Result<(Input, Vec)`")); } + 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) -> syn::Result<(Input, Vec..)` 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)> { + 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 = 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::>()?; + 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 { + 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 = 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::>()?; + 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>), 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>), 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(), diff --git a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs index 180dbae75f..23a4b78a61 100644 --- a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs +++ b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs @@ -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(), }; diff --git a/node-graph/node-macro/src/validation.rs b/node-graph/node-macro/src/validation.rs index 763e82e176..a712733316 100644 --- a/node-graph/node-macro/src/validation.rs +++ b/node-graph/node-macro/src/validation.rs @@ -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 = 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 = 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 = Vec::new(); @@ -124,6 +157,54 @@ fn validate_record_io(parsed: &ParsedNodeFn) { } seen_writes.push(written); } + let mut seen_removes: Vec = 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 = 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); + } } } diff --git a/node-graph/nodes/gcore/src/record.rs b/node-graph/nodes/gcore/src/record.rs index 7bc313b6e0..dc7912ab31 100644 --- a/node-graph/nodes/gcore/src/record.rs +++ b/node-graph/nodes/gcore/src/record.rs @@ -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) -> (f64, Attr) { +fn multiply_opacity(_: impl Ctx, (element, opacity): (f64, Attr), factor: f64) -> (f64, Attr) { (element, Attr(*opacity * factor)) } @@ -27,12 +28,12 @@ fn measure(_: impl Ctx, element: f64) -> (f64, Attr) { } #[node_macro::node(category("Test"))] -fn shade(_: impl Ctx, element: f64, opacity: Attr) -> f64 { +fn shade(_: impl Ctx, (element, opacity): (f64, Attr)) -> f64 { element * *opacity } #[node_macro::node(category("Test"))] -fn checked_multiply_opacity(_: impl Ctx, element: f64, factor: f64, opacity: Attr) -> Result<(f64, Attr), Interrupt> { +fn checked_multiply_opacity(_: impl Ctx, (element, opacity): (f64, Attr), factor: f64) -> Result<(f64, Attr), 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) -> (f64, Attr) { +fn scale(_: impl Ctx, (element, opacity): (f64, Attr), factor: &f64) -> (f64, Attr) { (element * *factor, Attr(*opacity)) } #[node_macro::node(category("Test"))] -fn fade(_: impl Ctx, element: T, factor: f64, opacity: Attr) -> (T, Attr) { +fn fade(_: impl Ctx, (element, opacity): (T, Attr), factor: f64) -> (T, Attr) { (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