diff --git a/node-graph/node-macro/src/codegen.rs b/node-graph/node-macro/src/codegen.rs index 5dbc1dda24..5bdda3cdb9 100644 --- a/node-graph/node-macro/src/codegen.rs +++ b/node-graph/node-macro/src/codegen.rs @@ -16,6 +16,7 @@ mod ir; mod metadata; pub(crate) use classify::*; use entries::entries_tokens; +use ir::{LazyBinding, ValueBinding}; use metadata::generate_node_input_references; static NODE_ID: AtomicU64 = AtomicU64::new(0); @@ -886,39 +887,7 @@ 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 field_role = |index: usize, field: &ParsedField| match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => { - if record.is_some() && !skips_carrier && index == 0 { - InputRole::RecordCarrier - } else if carrier_flip && index == 0 { - InputRole::FlipCarrier - } else if flip && lend.is_some() { - InputRole::LendBorrow - } else if record.is_some() && !field.attribute_reads.is_empty() { - InputRole::ReadingSecondary - } else if flip || (routing.is_some() && !routing_source(ty)) { - InputRole::RecordValue - } else { - InputRole::PlainValue - } - } - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { - if derive_routing && routing_source(output_type) { - InputRole::DeriveRoutingSource - } else if flip && raw_lazy { - InputRole::FlipRawLazyEdge - } else if flip { - InputRole::FlipLazy - } else if opaque && raw_lazy && is_record_value(output_type) { - InputRole::OpaqueRecordEdge - } else if raw_lazy { - InputRole::RawLazy - } else { - InputRole::Lazy - } - } - }; - let roles: Vec = regular_fields.iter().enumerate().map(|(index, field)| field_role(index, field)).collect(); + let node = crate::codegen::ir::build(parsed); let lazy_read_out = |field: &ParsedField, output_type: &Type| { let attr_tys = field.attribute_reads.iter().map(|read| { @@ -950,26 +919,25 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn ParsedFieldType::Regular(RegularParsedField { ty, .. }) => quote!(#pat: #ty), ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { let source_generic = format_ident!("__Source{index}"); - match roles[index] { - InputRole::DeriveRoutingSource => quote!(#pat: #core_types::record::RecordLazyInput<'_, '__record, #source_generic>), - InputRole::OpaqueRecordEdge => quote!(#pat: &#core_types::record::RecordEdgeInput<'_, #source_generic>), - InputRole::FlipRawLazyEdge => { + match (ir::lazy_binding(&node, index), raw_lazy) { + (LazyBinding::DeriveRouting, _) => quote!(#pat: #core_types::record::RecordLazyInput<'_, '__record, #source_generic>), + (LazyBinding::OpaqueRecord, _) => quote!(#pat: &#core_types::record::RecordEdgeInput<'_, #source_generic>), + (LazyBinding::Element, true) => { let out = lazy_read_out(field, output_type); quote!(#pat: &#core_types::record::ElementEdge<'_, #out, #source_generic>) } - InputRole::FlipLazy => { + (LazyBinding::Element, false) => { let out = lazy_read_out(field, output_type); quote!(#pat: #core_types::record::ElementLazyInput<'_, #out, #source_generic>) } - InputRole::RawLazy => { + (LazyBinding::Plain, true) => { let bound = lazy_bound(output_type); quote!(#pat: &impl #bound) } - InputRole::Lazy => { + (LazyBinding::Plain, false) => { let bound = lazy_bound(output_type); quote!(#pat: #core_types::node::LazyInput<'_, impl #bound>) } - _ => unreachable!("value role on a lazy input"), } } } @@ -1078,111 +1046,103 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let bind_body = |index: usize, field: &ParsedField| { let name = &field.pat_ident.ident; - let regular_ty = || match &field.ty { - ParsedFieldType::Regular(RegularParsedField { ty, .. }) => ty.clone(), - _ => unreachable!("value role on a lazy input"), - }; - let node_output = || match &field.ty { - ParsedFieldType::Node(NodeParsedField { output_type, .. }) => output_type.clone(), - _ => unreachable!("lazy role on a value input"), - }; - match roles[index] { - // A carrier primary evaluates beyond the node's own frame (in the - // record/flip tail), and a raw poll edge is threaded straight through, - // so none bind here. - InputRole::RecordCarrier | InputRole::FlipCarrier | InputRole::RawLazy => 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. - InputRole::ReadingSecondary => { - let ty = regular_ty(); - 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! { + match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, .. }) => match ir::value_binding(&node, index) { + // A carrier primary evaluates beyond the node's own frame (in the + // record/flip tail), so it does not bind here. + ValueBinding::Carrier => 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. + ValueBinding::ReadingSecondary => { + 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) }; + } + } + // The lend input's frame survives on the record stack until this + // node's frame is reclaimed, so the borrow stays valid in place. + ValueBinding::Lend => { + let slot = format_ident!("__in_{index}"); + let record_local = format_ident!("__record_{index}"); + quote! { + let #record_local = match __cell.eval_input(#index, &self.#name, __input) { + Ok(value) => value, + Err(interrupt) => return interrupt.into(), + }; + let #name = unsafe { #core_types::record::borrow_element::<#ty>(self.#slot.rec(&#record_local)) }; + } + } + // A flip value or a routing non-source value rides a record edge; the + // element copies out into `name`. The mark/rewind that reclaims the + // record's frame is applied by the step lowering (see `reads_out`). + ValueBinding::RecordElement => { + 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)) }; + } + } + ValueBinding::Plain => 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) }; - } - } - // The lend input's frame survives on the record stack until this - // node's frame is reclaimed, so the borrow stays valid in place. - InputRole::LendBorrow => { - let ty = regular_ty(); - let slot = format_ident!("__in_{index}"); - let record_local = format_ident!("__record_{index}"); - quote! { - let #record_local = match __cell.eval_input(#index, &self.#name, __input) { - Ok(value) => value, - Err(interrupt) => return interrupt.into(), - }; - let #name = unsafe { #core_types::record::borrow_element::<#ty>(self.#slot.rec(&#record_local)) }; - } - } - // A flip value or a routing non-source value rides a record edge; the - // element copies out into `name`. The mark/rewind that reclaims the - // record's frame is applied by the step lowering (see `reads_out`). - InputRole::RecordValue => { - let ty = regular_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)) }; - } - } - InputRole::PlainValue => quote! { - let #name = match __cell.eval_input(#index, &self.#name, __input) { - Ok(value) => value, - Err(interrupt) => return interrupt.into(), - }; + }, }, - InputRole::DeriveRoutingSource => quote! { - let #name = #core_types::record::RecordLazyInput::new(&self.#name, &__cell, #index); - }, - InputRole::FlipRawLazyEdge => { - let output_type = node_output(); - let slot = format_ident!("__in_{index}"); - 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, .. }) => match (ir::lazy_binding(&node, index), raw_lazy) { + // A raw poll edge is threaded straight through, so it does not bind here. + (LazyBinding::Plain, true) => quote!(), + (LazyBinding::DeriveRouting, _) => quote! { + let #name = #core_types::record::RecordLazyInput::new(&self.#name, &__cell, #index); + }, + (LazyBinding::Element, true) => { + let slot = format_ident!("__in_{index}"); + 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); + } } } } - } - InputRole::FlipLazy => { - let output_type = node_output(); - let slot = format_ident!("__in_{index}"); - 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); + (LazyBinding::Element, false) => { + let slot = format_ident!("__in_{index}"); + 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); + } } } } - } - InputRole::OpaqueRecordEdge => quote! { - let #name = #core_types::record::RecordEdgeInput::new(&self.#name, &self.__layout); - }, - InputRole::Lazy => quote! { - let #name = #core_types::node::LazyInput::new(&self.#name, &__cell, #index); + (LazyBinding::OpaqueRecord, _) => quote! { + let #name = #core_types::record::RecordEdgeInput::new(&self.#name, &self.__layout); + }, + (LazyBinding::Plain, false) => quote! { + let #name = #core_types::node::LazyInput::new(&self.#name, &__cell, #index); + }, }, } }; @@ -1209,9 +1169,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn // evaluated value. ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. }) if !flip => quote!(&#name), ParsedFieldType::Regular(_) => quote!(#name), - ParsedFieldType::Node(_) => match roles[index] { - InputRole::FlipRawLazyEdge | InputRole::OpaqueRecordEdge => quote!(&#name), - InputRole::RawLazy => quote!(&self.#name), + ParsedFieldType::Node(_) => match (ir::lazy_binding(&node, index), raw_lazy) { + (LazyBinding::Element, true) | (LazyBinding::OpaqueRecord, _) => quote!(&#name), + (LazyBinding::Plain, true) => quote!(&self.#name), _ => quote!(#name), }, } @@ -1869,7 +1829,8 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn let eval_body = eval_steps.iter().map(|step| match step { EvalStep::Bind(index, field) => { let body = bind_body(*index, field); - match roles[*index].reads_out() { + let reads_out = matches!(&field.ty, ParsedFieldType::Regular(_)) && ir::value_binding(&node, *index).reads_out(); + match reads_out { false => body, true => { let mark = format_ident!("__mark_{index}"); diff --git a/node-graph/node-macro/src/codegen/classify.rs b/node-graph/node-macro/src/codegen/classify.rs index d1c3ca97ad..1f6e559daf 100644 --- a/node-graph/node-macro/src/codegen/classify.rs +++ b/node-graph/node-macro/src/codegen/classify.rs @@ -100,36 +100,6 @@ pub(crate) fn analyze(parsed: &ParsedNodeFn) -> Option { Some(NodeModel { class, dialect: dialect(parsed) }) } -/// The per-field binding role, resolved once per regular field from the node -/// class and field shape. Drives the eval bindings and the lazy-edge input -/// types. The `lend` and `reads` axes stay field properties the value arms of -/// `kernel_params`/`call_args`/`value_args` consult, since they cross roles. -#[derive(Clone, Copy)] -pub(crate) enum InputRole { - RecordCarrier, - FlipCarrier, - ReadingSecondary, - LendBorrow, - RecordValue, - PlainValue, - DeriveRoutingSource, - OpaqueRecordEdge, - FlipRawLazyEdge, - FlipLazy, - RawLazy, - Lazy, -} - -impl InputRole { - /// A role that copies an element out of a record edge, so its record frame - /// must be reclaimed after the read. The step lowering wraps such a bind in - /// `mark`/`rewind`, making the stack discipline structural rather than - /// hand-threaded through each read-out arm. - pub(crate) fn reads_out(self) -> bool { - matches!(self, InputRole::ReadingSecondary | InputRole::RecordValue) - } -} - /// The tail form of a node's eval, selected from its class and dialect: forward /// the kernel's own record, assemble a record (io or flip carrier), or spawn a /// source and lift its completion. diff --git a/node-graph/node-macro/src/codegen/ir.rs b/node-graph/node-macro/src/codegen/ir.rs index d79172bea3..7e19aa91c2 100644 --- a/node-graph/node-macro/src/codegen/ir.rs +++ b/node-graph/node-macro/src/codegen/ir.rs @@ -21,9 +21,18 @@ pub(crate) fn build(parsed: &ParsedNodeFn) -> Node { output: output(parsed, &generic_idents), generics, effect: effect(parsed), + derives: derives(parsed), } } +fn derives(parsed: &ParsedNodeFn) -> bool { + context_param(parsed).is_some_and(|ctx| { + ctx.bounds + .iter() + .any(|bound| matches!(bound, TypeParamBound::Trait(trait_bound) if trait_bound.path.segments.last().is_some_and(|segment| segment.ident == "DeriveCtx"))) + }) +} + fn generics(parsed: &ParsedNodeFn) -> Vec { let ctx = context_param(parsed).map(|param| param.ident.clone()); parsed @@ -55,6 +64,7 @@ fn inputs(parsed: &ParsedNodeFn, fields: &[&ParsedField], generics: &[Ident]) -> evaluation, shape: item_shape(field_element_type(field), &field.attribute_reads, generics), subject: subject(index, field, carrier_subject, routing.as_ref()), + lend: matches!(&field.ty, ParsedFieldType::Regular(RegularParsedField { lend: Some(_), .. })), } }) .collect() @@ -172,7 +182,7 @@ fn ilist_inner(ty: &Type) -> Option { pub(crate) fn layout_meta_tokens(node: &Node, element_spec: TokenStream2, core_types: &TokenStream2) -> TokenStream2 { let sources = node.inputs.iter().enumerate().filter(|(_, input)| input.subject).map(|(index, _)| index as u8); let reads = node.inputs.iter().enumerate().filter_map(|(index, input)| { - (!input.shape.attrs.is_empty()).then(|| { + (matches!(input.evaluation, Evaluation::Eager) && !input.shape.attrs.is_empty()).then(|| { let descs = field_writes(&input.shape.attrs, core_types); let index = index as u8; quote!(#core_types::record::InputReads { input: #index, reads: ::std::vec![#(#descs),*] }) @@ -213,6 +223,94 @@ fn level_delta(node: &Node) -> i8 { node.output.shape.depth as i8 - subject_depth } +/// How an eager value input binds in eval. +pub(crate) enum ValueBinding { + Carrier, + Lend, + ReadingSecondary, + RecordElement, + Plain, +} + +/// How a lazy (`impl Node`) input binds in eval. The `Poll` effect further +/// selects the borrowed vs `__cell`-driven form within `Element`/`Plain`. +pub(crate) enum LazyBinding { + Element, + Plain, + DeriveRouting, + OpaqueRecord, +} + +impl ValueBinding { + /// Copies an element out of a record edge, so the frame is reclaimed after. + pub(crate) fn reads_out(&self) -> bool { + matches!(self, ValueBinding::ReadingSecondary | ValueBinding::RecordElement) + } +} + +enum NodeKind { + Flip, + RecordIo, + Routing, + Opaque, +} + +fn node_kind(node: &Node) -> NodeKind { + if matches!(node.output.shape.element, Element::Opaque) { + NodeKind::Opaque + } else if has_attr_io(node) { + NodeKind::RecordIo + } else if is_routing(node) { + NodeKind::Routing + } else { + NodeKind::Flip + } +} + +/// Routing forwards an unbounded generic from a source whole; a bounded generic +/// or one transformed into a different output type works on the element and flips. +fn is_routing(node: &Node) -> bool { + let Element::Generic(output) = &node.output.shape.element else { return false }; + node.monomorphizations.is_empty() + && node.generics.iter().any(|generic| &generic.ident == output && generic.bounds.is_empty()) + && node.inputs.iter().any(|input| input.subject && matches!(&input.shape.element, Element::Generic(generic) if generic == output)) +} + +fn has_attr_io(node: &Node) -> bool { + // Reads on lazy inputs ride the flip; only eager reads make a record-io node. + node.inputs.iter().any(|input| matches!(input.evaluation, Evaluation::Eager) && !input.shape.attrs.is_empty()) || !node.output.shape.attrs.is_empty() || !node.output.removes.is_empty() +} + +pub(crate) fn value_binding(node: &Node, index: usize) -> ValueBinding { + let input = &node.inputs[index]; + let kind = node_kind(node); + if matches!(kind, NodeKind::RecordIo | NodeKind::Flip) && index == 0 && input.subject { + ValueBinding::Carrier + } else if matches!(kind, NodeKind::Flip) && input.lend { + ValueBinding::Lend + } else if matches!(kind, NodeKind::RecordIo) && !input.shape.attrs.is_empty() { + ValueBinding::ReadingSecondary + } else if matches!(kind, NodeKind::Flip) || (matches!(kind, NodeKind::Routing) && !input.subject) { + ValueBinding::RecordElement + } else { + ValueBinding::Plain + } +} + +pub(crate) fn lazy_binding(node: &Node, index: usize) -> LazyBinding { + let input = &node.inputs[index]; + let kind = node_kind(node); + if node.derives && matches!(kind, NodeKind::Routing) && input.subject { + LazyBinding::DeriveRouting + } else if matches!(kind, NodeKind::Flip) { + LazyBinding::Element + } else if matches!(input.shape.element, Element::Opaque) { + LazyBinding::OpaqueRecord + } else { + LazyBinding::Plain + } +} + pub(crate) struct Node { pub(crate) kernel: Kernel, pub(crate) generics: Vec, @@ -221,6 +319,8 @@ pub(crate) struct Node { pub(crate) inputs: Vec, pub(crate) output: Output, pub(crate) effect: Effect, + /// The context is derived (a `DeriveCtx` bound), so routing sources rebind it. + pub(crate) derives: bool, } /// The kernel fn the node wraps. @@ -244,6 +344,8 @@ pub(crate) struct Input { pub(crate) shape: ItemShape, /// This input's layout folds into the output. pub(crate) subject: bool, + /// Written `&T`; the kernel borrows the evaluated element. + pub(crate) lend: bool, } /// `Lazy` = `impl Node<..>`, the kernel drives it. @@ -288,7 +390,7 @@ pub(crate) enum Effect { #[cfg(test)] mod tests { use super::*; - use crate::codegen::classify::{Class, analyze}; + use crate::codegen::classify::{Class, Dialect, analyze, context_param, dialect}; use crate::parsing::parse_node_fn; use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, quote}; @@ -424,6 +526,161 @@ mod tests { ); } + fn ctx_derives(parsed: &ParsedNodeFn) -> bool { + context_param(parsed).is_some_and(|ctx| { + ctx.bounds + .iter() + .any(|bound| matches!(bound, TypeParamBound::Trait(trait_bound) if trait_bound.path.segments.last().is_some_and(|segment| segment.ident == "DeriveCtx"))) + }) + } + + /// The frozen `field_role` classification the IR bindings must reproduce. + fn reference_label(parsed: &ParsedNodeFn, class: &Class, raw: bool, index: usize, field: &ParsedField) -> &'static str { + let record = matches!(class, Class::RecordIo(_)); + let skips_carrier = matches!(class, Class::RecordIo(shape) if shape.skips_carrier()); + let carrier_flip = matches!(class, Class::Flip { carrier: true }); + let flip = matches!(class, Class::Flip { .. }); + let opaque = matches!(class, Class::Opaque); + let routing = matches!(class, Class::Routing(_)); + let derives = ctx_derives(parsed); + let routing_source = |ty: &Type| matches!(class, Class::Routing(routing) if bare_ident(ty) == Some(&routing.generic)); + match &field.ty { + ParsedFieldType::Regular(RegularParsedField { ty, lend, .. }) => { + if record && !skips_carrier && index == 0 { + "carrier" + } else if carrier_flip && index == 0 { + "carrier" + } else if flip && lend.is_some() { + "lend" + } else if record && !field.attribute_reads.is_empty() { + "reading" + } else if flip || (routing && !routing_source(ty)) { + "record" + } else { + "plain" + } + } + ParsedFieldType::Node(NodeParsedField { output_type, .. }) => { + if derives && routing && routing_source(output_type) { + "derive-routing" + } else if flip && raw { + "flip-raw" + } else if flip { + "flip-lazy" + } else if opaque && raw && is_record_value(output_type) { + "opaque-record" + } else if raw { + "raw-lazy" + } else { + "lazy" + } + } + } + } + + fn ir_label(node: &Node, index: usize, field: &ParsedField, raw: bool) -> &'static str { + match &field.ty { + ParsedFieldType::Regular(_) => match value_binding(node, index) { + ValueBinding::Carrier => "carrier", + ValueBinding::Lend => "lend", + ValueBinding::ReadingSecondary => "reading", + ValueBinding::RecordElement => "record", + ValueBinding::Plain => "plain", + }, + ParsedFieldType::Node(_) => match (lazy_binding(node, index), raw) { + (LazyBinding::DeriveRouting, _) => "derive-routing", + (LazyBinding::OpaqueRecord, _) => "opaque-record", + (LazyBinding::Element, true) => "flip-raw", + (LazyBinding::Element, false) => "flip-lazy", + (LazyBinding::Plain, true) => "raw-lazy", + (LazyBinding::Plain, false) => "lazy", + }, + } + } + + fn assert_bindings(attr: TokenStream2, item: TokenStream2) { + let mut parsed = parse_node_fn(attr, item).unwrap(); + parsed.replace_impl_trait_in_input(); + let model = analyze(&parsed).expect("representative resolves to a class"); + let raw = matches!(dialect(&parsed), Dialect::Poll); + let node = build(&parsed); + let fields: Vec<&ParsedField> = parsed.fields.iter().filter(|field| !field.is_data_field).collect(); + for (index, field) in fields.iter().enumerate() { + assert_eq!( + ir_label(&node, index, field, raw), + reference_label(&parsed, &model.class, raw, index, field), + "field {index} of {}", + parsed.fn_name + ); + } + } + + #[test] + fn bindings_flip() { + assert_bindings(quote!(category("")), quote!(fn negate(_: impl Ctx, x: f64) -> f64 { -x })); + assert_bindings(quote!(category("")), quote!(fn add2(_: impl Ctx, a: f64, b: f64) -> f64 { a + b })); + } + + #[test] + fn bindings_lend() { + assert_bindings(quote!(category("")), quote!(fn borrow(_: impl Ctx, prim: f64, other: &f64) -> f64 { prim + *other })); + } + + #[test] + fn bindings_reading_secondary() { + assert_bindings(quote!(category("")), quote!(fn read_op(_: impl Ctx, carrier: f64, (other, op): (f64, Attr)) -> f64 { carrier + other })); + } + + #[test] + fn bindings_flip_lazy() { + assert_bindings(quote!(category("")), quote!(fn apply(_: impl Ctx, inner: impl Node<(), Output = f64>) -> f64 { inner.eval(()) })); + } + + #[test] + fn bindings_flip_lazy_reads() { + assert_bindings( + quote!(category("")), + quote!(fn apply_reads(_: impl Ctx, carrier: f64, inner: impl Node<(), Output = (f64, Attr)>) -> f64 { carrier + inner.eval(()).0 }), + ); + } + + #[test] + fn bindings_flip_raw() { + assert_bindings(quote!(category("")), quote!(fn poll_apply(_: impl Ctx, inner: impl Node<(), Output = f64>) -> GPoll { inner.eval(()) })); + } + + #[test] + fn bindings_skip_impl_generic() { + // A bounded generic forwarded whole (passthrough) flips, not routes. + assert_bindings(quote!(category(""), skip_impl), quote!(fn passthrough(_: impl Ctx, content: T) -> T { content })); + // A generic transformed into a different output type flips. + assert_bindings( + quote!(category(""), skip_impl), + quote!(fn into_ty, O: Send>(_: impl Ctx, value: T, #[data] _out: PhantomData) -> O { value.into() }), + ); + } + + #[test] + fn bindings_routing() { + assert_bindings( + quote!(category("")), + quote!(fn switch(_: impl Ctx, condition: bool, off: impl Node<(), Output = T>, on: impl Node<(), Output = T>) -> T { if condition { on.eval(()) } else { off.eval(()) } }), + ); + } + + #[test] + fn bindings_derive_routing() { + assert_bindings(quote!(category("")), quote!(fn ctx_mod(_: impl Ctx + DeriveCtx, inner: impl Node<(), Output = T>) -> T { inner.eval(()) })); + } + + #[test] + fn bindings_opaque() { + assert_bindings( + quote!(category("")), + quote!(fn memo<'e>(_: impl Ctx, #[data] cache: Store, content: impl Node, Output = RecordValue<'e>>) -> GPoll> { content.eval(()) }), + ); + } + #[test] fn monomorphizations_key_by_generic() { let node = assert_bridge(