Accept any record wire on an input read only for its attributes

An element generic declared on a reading input, absent from the output
and from every other input, has nowhere to go and nothing to be: the
node reads its declared attributes and never the element. That is an
attr-to-element map, so it accepts any upstream record wire.

The blessing is inferred rather than marked, since the signature already
says it. The two substrate pieces were both in place: the element rides
as the byte-carried token, so the registry takes one generic row instead
of a row per element type, and the reads resolve against the input's own
layout as they always have. Only the passthrough rule stood in the way,
in the three places that enforced it.

A generic the output carries, or one on an input with no reads, is
unchanged and still owes an implementations list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dennis Kobert
2026-09-09 12:15:40 +00:00
parent f04d439b33
commit 5baeb8fd46
6 changed files with 138 additions and 7 deletions

View File

@@ -808,7 +808,9 @@ pub(crate) fn generate_node_impl(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
// its generic stays a struct parameter.
let record_token = match (kind, &node.output.shape.element) {
(crate::codegen::ir::NodeKind::RecordIo, crate::codegen::ir::Element::Generic(ident)) if !gather_carrier => Some(ident.clone()),
_ => None,
// An opaque reading input's element is byte-carried the same way, even
// though the output replaces it rather than carrying it through.
_ => crate::codegen::classify::opaque_reading_carrier(parsed),
};
// The record-io write set, resolved from the output item and carrier input.
let write_markers: Vec<&Type> = node.output.shape.attrs.iter().map(|attr| &attr.marker).collect();

View File

@@ -266,6 +266,36 @@ pub(crate) fn contains_open_generic(parsed: &ParsedNodeFn, ty: &Type) -> bool {
.any(|param| matches!(param, GenericParam::Type(type_param) if Some(&type_param.ident) != ctx_ident.as_ref() && type_contains_ident(ty, &type_param.ident)))
}
/// The element generic of a primary input that is read but never looked at:
/// declared as an unbounded passthrough, carrying attribute reads, and absent
/// from everywhere else in the signature. Such a node maps its declared
/// attributes onto a fresh element, so it accepts any upstream record wire and
/// registers one generic row rather than a row per element type.
///
/// Inferred rather than marked: a generic with nowhere to go and nothing to be
/// is opaque by construction, and the signature already says so.
pub(crate) fn opaque_reading_carrier(parsed: &ParsedNodeFn) -> Option<Ident> {
let carrier = parsed.fields.first()?;
if carrier.is_data_field || carrier.attribute_reads.is_empty() {
return None;
}
let ParsedFieldType::Regular(RegularParsedField { ty, lend: None, implementations, .. }) = &carrier.ty else {
return None;
};
if !implementations.is_empty() {
return None;
}
let token = unbounded_generic(parsed, ty)?;
// A token that reaches the output or another input is a passthrough
// element, which the node does carry; only one going nowhere is opaque.
let escapes = type_contains_ident(&parsed.output_type, &token)
|| parsed.fields.iter().skip(1).any(|field| match &field.ty {
ParsedFieldType::Regular(RegularParsedField { ty, .. }) => type_contains_ident(ty, &token),
ParsedFieldType::Node(NodeParsedField { input_type, output_type, .. }) => type_contains_ident(input_type, &token) || type_contains_ident(output_type, &token),
});
(!escapes).then_some(token)
}
pub(crate) fn unbounded_generic(parsed: &ParsedNodeFn, ty: &Type) -> Option<Ident> {
let ident = bare_ident(ty)?.clone();
let ctx_ident = context_param(parsed).map(|ctx| ctx.ident.clone());
@@ -362,6 +392,13 @@ pub(crate) fn record_shape(parsed: &ParsedNodeFn) -> Option<RecordShape> {
None => (value, Vec::new(), Vec::new()),
};
match &token {
// An opaque reading carrier never looks at its element, so it writes a
// fresh one instead of passing the token through.
Some(_) if opaque_reading_carrier(parsed).is_some() => {
if contains_open_generic(parsed, &element) {
return None;
}
}
Some(token) => {
if !matches!(bare_ident(&element), Some(ident) if ident == token) {
return None;

View File

@@ -912,6 +912,68 @@ mod tests {
assert_eq!(generics, vec!["V".to_string()], "only the value generic rides the node");
}
#[test]
fn a_reading_input_whose_element_goes_nowhere_is_opaque() {
let mut parsed = crate::parsing::parse_node_fn(
quote!(category("")),
quote!(
fn peek<'e, T>(_: impl Ctx, (content, opacity): (T, Attr<'e, Opacity>)) -> f64 {
let _ = content;
*opacity
}
),
)
.unwrap();
parsed.replace_impl_trait_in_input();
let token = crate::codegen::classify::opaque_reading_carrier(&parsed).expect("the element generic is opaque");
assert_eq!(token.to_string(), "T");
// The node still reads, so it keeps the record-io tail, and the element
// rides it as the byte-carried token: one row covers every element type.
let node = build(&parsed);
assert!(matches!(node_kind(&node), NodeKind::RecordIo), "an opaque reading input still reads attributes");
assert!(
matches!(crate::codegen::record_shape(&parsed), Some(shape) if matches!(shape.carrier, crate::codegen::RecordCarrier::Token)),
"the element is carried as a token rather than read"
);
assert_eq!(node.inputs[0].shape.attrs.len(), 1, "the declared read survives the blessing");
}
#[test]
fn an_element_generic_the_node_uses_is_not_opaque() {
// Returned rather than dropped, so the element is carried, not opaque:
// the usual passthrough rule still governs it.
let mut carried = crate::parsing::parse_node_fn(
quote!(category("")),
quote!(
fn keep<'e, T>(_: impl Ctx, (content, opacity): (T, Attr<'e, Opacity>)) -> (T, Attr<'e, Opacity>) {
(content, opacity)
}
),
)
.unwrap();
carried.replace_impl_trait_in_input();
assert!(crate::codegen::classify::opaque_reading_carrier(&carried).is_none(), "an element the output carries is not opaque");
// No reads at all, so the generic is an ordinary element and still owes
// an implementations list.
let mut plain = crate::parsing::parse_node_fn(
quote!(category("")),
quote!(
fn plain<T>(_: impl Ctx, content: T) -> f64 {
let _ = content;
0.
}
),
)
.unwrap();
plain.replace_impl_trait_in_input();
assert!(
crate::codegen::classify::opaque_reading_carrier(&plain).is_none(),
"a generic element without reads still needs implementations"
);
}
#[test]
fn bridge_record_remove() {
assert_bridge(

View File

@@ -135,7 +135,14 @@ fn validate_record_io(parsed: &ParsedNodeFn) {
let element = writes.as_ref().map(|writes| &writes.element).unwrap_or(&value);
match &token {
Some(token) => {
if !matches!(crate::codegen::bare_ident(element), Some(ident) if ident == token) {
// An opaque reading input never looks at its element, so it writes
// a fresh one rather than carrying the input's through.
let opaque_reading = crate::codegen::classify::opaque_reading_carrier(parsed).is_some();
if opaque_reading {
if crate::codegen::contains_open_generic(parsed, element) {
emit_error!(parsed.output_type.span(), "an opaque reading input writes a concrete element, since `{}` is never read", token);
}
} else if !matches!(crate::codegen::bare_ident(element), Some(ident) if ident == token) {
emit_error!(parsed.output_type.span(), "a generic element passes through unchanged: return `{}` in the first tuple position", token);
}
}
@@ -430,13 +437,16 @@ fn validate_implementations_for_generics(parsed: &ParsedNodeFn) {
(crate::codegen::ir::NodeKind::RecordIo, crate::codegen::ir::Element::Generic(ident)) => Some(ident.clone()),
_ => None,
};
// An opaque reading carrier's element is never looked at, so it needs no
// rows: the node registers one generic row and accepts any record wire.
let opaque_reading = crate::codegen::classify::opaque_reading_carrier(parsed);
let opaque_record_generic = |ty: &Type| {
let (stripped, _) = crate::codegen::ir::strip_ilist(ty);
let ident = match &stripped {
Type::Path(path) => path.path.get_ident(),
_ => None,
};
ident.is_some() && (ident == routing.as_ref().map(|routing| &routing.generic) || ident == record_token.as_ref())
ident.is_some() && (ident == routing.as_ref().map(|routing| &routing.generic) || ident == record_token.as_ref() || ident == opaque_reading.as_ref())
};
if !has_skip_impl && !parsed.fn_generics.is_empty() {