Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use syn::parse::{Parse, ParseStream};
use syn::token::{At, Brace, Comma, Dot};
use syn::token::{Brace, Comma, Dot, In};
use syn::{Ident, Type};

use crate::parse_internal;
use crate::types::attributes::UseTypeIdent;
use crate::types::ident::PathWithTypeArgs;

/// One `#[use_type(...)]` import spec: a rewrite target (`Self` or an `@Context`),
/// the owning trait path, and one or more associated types to import from it.
/// One `#[use_type(...)]` import spec: the owning trait path, one or more
/// associated types to import from it, and a rewrite target (`Self`, or a named
/// type set by a trailing `in Context`).
#[derive(Clone)]
pub struct UseTypeAttribute {
pub context_type: Type,
Expand All @@ -31,19 +32,10 @@ impl UseTypeAttribute {

impl Parse for UseTypeAttribute {
fn parse(input: ParseStream) -> syn::Result<Self> {
// A `.` (not `::`) separates the context, trait, and associated type. This
// A `.` (not `::`) separates the trait from the associated type. This
// keeps the trait unambiguous even when it is a full path such as
// `foo::bar::HasScalarType`: `::` stays inside the path, and the trailing
// `.` marks where the associated type begins.
let context_type: Type = if input.peek(At) {
let _: At = input.parse()?;
let context: PathWithTypeArgs = input.parse()?;
let _: Dot = input.parse()?;
context.into()
} else {
parse_internal! { Self }
};

let trait_path: PathWithTypeArgs = input.parse()?;

let _: Dot = input.parse()?;
Expand All @@ -59,6 +51,18 @@ impl Parse for UseTypeAttribute {
vec![input.parse()?]
};

// An optional `in Context` suffix sets the rewrite target to a named
// type; `in` is a reserved keyword, so it can never be confused with a
// trait, type, or associated-type name and reads as a clean delimiter.
// Without the suffix, the target defaults to `Self`.
let context_type: Type = if input.peek(In) {
let _: In = input.parse()?;
let context: PathWithTypeArgs = input.parse()?;
context.into()
} else {
parse_internal! { Self }
};

Ok(Self {
context_type,
trait_path,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,51 @@
use quote::ToTokens;
use syn::visit_mut::VisitMut;
use syn::{ItemImpl, ItemTrait};
use syn::{ItemImpl, ItemTrait, Type};

use crate::functions::parse_internal;
use crate::types::attributes::UseTypeAttribute;
use crate::types::attributes::use_type::type_predicates::{
derive_use_type_predicates, forbid_duplicate_aliases,
};
use crate::visitors::SubstituteAbstractType;
use crate::visitors::SubstituteAbstractTypes;

#[derive(Default, Clone)]
pub struct UseTypeAttributes {
pub attributes: Vec<UseTypeAttribute>,
}

impl UseTypeAttributes {
pub fn substitute_abstract_types_in_item_trait(&self, item_trait: &mut ItemTrait) {
for type_spec in self.attributes.iter().rev() {
SubstituteAbstractType { type_spec }.visit_item_trait_mut(item_trait);
}
}
/// Resolve every spec's context type into fully-qualified form before it is
/// used, so that both the body substitution and the appended bounds agree on
/// one grounded context.
///
/// An `in Context` suffix whose `Context` is itself imported by another spec —
/// as in `#[use_type(HasTypes.Types, HasScalarType.Scalar in Types)]` — is
/// rewritten from the bare alias `Types` to `<Self as HasTypes>::Types`.
/// Contexts that name a real generic parameter or `Self` are left untouched.
/// The pass iterates to a fixpoint so a chain of links resolves fully; each
/// pass grounds one more level, so `attributes.len()` passes cover any
/// acyclic chain, and a cyclic reference simply stops making progress and
/// surfaces later as an ordinary unresolved-type error rather than looping.
fn grounded_specs(&self) -> Vec<UseTypeAttribute> {
let mut grounded = self.attributes.clone();

for _ in 0..grounded.len() {
let snapshot = grounded.clone();
let mut changed = false;

for spec in grounded.iter_mut() {
let mut visitor = SubstituteAbstractTypes::new(&snapshot);
visitor.visit_type_mut(&mut spec.context_type);
changed |= visitor.is_changed;
}

pub fn substitute_abstract_types_in_item_impl(&self, item_impl: &mut ItemImpl) {
for type_spec in self.attributes.iter().rev() {
SubstituteAbstractType { type_spec }.visit_item_impl_mut(item_impl);
if !changed {
break;
}
}

grounded
}

pub fn transform_item_trait(&self, item_trait: &mut ItemTrait) -> syn::Result<()> {
Expand All @@ -34,16 +55,39 @@ impl UseTypeAttributes {

forbid_duplicate_aliases(&self.attributes)?;

self.substitute_abstract_types_in_item_trait(item_trait);

for use_type in self.attributes.iter() {
if use_type.context_type != parse_internal! { Self } {
continue;
let grounded = self.grounded_specs();

SubstituteAbstractTypes::new(&grounded).visit_item_trait_mut(item_trait);

let self_type: Type = parse_internal! { Self };

for use_type in grounded.iter() {
let trait_path = &use_type.trait_path;

if use_type.context_type == self_type {
// A `Self`-context import becomes a supertrait of the generated
// trait, so the abstract type is available to every signature.
item_trait
.supertraits
.push(parse_internal(trait_path.to_token_stream())?);
} else {
// A foreign `in Context` import rewrites signatures to name
// `<Context as Trait>::Assoc`, so the trait must require
// `Context: Trait` for those paths to be well-formed. Without
// this bound the constraint would be silently dropped, leaving a
// signature that only compiles when `Context`'s bound happens to
// be supplied elsewhere. The type-equality (`= T`) form is an
// impl-side pin and is deliberately *not* added here.
let context_type = &use_type.context_type;

item_trait
.generics
.make_where_clause()
.predicates
.push(parse_internal! {
#context_type: #trait_path
});
}

item_trait
.supertraits
.push(parse_internal(use_type.trait_path.to_token_stream())?);
}

Ok(())
Expand All @@ -56,9 +100,11 @@ impl UseTypeAttributes {

forbid_duplicate_aliases(&self.attributes)?;

self.substitute_abstract_types_in_item_impl(item_impl);
let grounded = self.grounded_specs();

SubstituteAbstractTypes::new(&grounded).visit_item_impl_mut(item_impl);

let predicates = derive_use_type_predicates(&self.attributes)?;
let predicates = derive_use_type_predicates(&grounded)?;

item_impl
.generics
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@ use syn::{Ident, Type, WherePredicate};
use crate::functions::parse_internal;
use crate::types::attributes::{UseTypeAttribute, UseTypeIdent};

/// Derive the impl-side `where` predicates a set of `#[use_type]` specs
/// contributes: one `Context: Trait` bound per spec, carrying any type-equality
/// (`= T`) pins as associated-type bindings. The specs' contexts must already be
/// grounded (see [`UseTypeAttributes::grounded_specs`]), so this reads
/// `use_type.context_type` directly rather than re-resolving aliases.
pub fn derive_use_type_predicates(specs: &[UseTypeAttribute]) -> syn::Result<Vec<WherePredicate>> {
let mut predicates = Vec::new();

for use_type in specs.iter() {
let type_equalities = find_type_equalities(use_type, specs)?;

let trait_path = &use_type.trait_path;
let mut context_type = use_type.context_type.clone();

if context_type != parse_internal!(Self)
&& let Some(new_context_type) = find_type_alias(specs, &context_type)?
{
context_type = new_context_type;
}
let context_type = &use_type.context_type;

if type_equalities.is_empty() {
predicates.push(parse_internal! {
Expand All @@ -44,30 +43,6 @@ pub fn derive_use_type_predicates(specs: &[UseTypeAttribute]) -> syn::Result<Vec
Ok(predicates)
}

fn find_type_alias(specs: &[UseTypeAttribute], context_type: &Type) -> syn::Result<Option<Type>> {
let Ok(context_ident) = parse_internal::<Ident>(context_type.to_token_stream()) else {
return Ok(None);
};

for spec in specs {
for ident in spec.type_idents.iter() {
if ident.alias_ident() == &context_ident {
let new_context_type = &spec.context_type;
let type_ident = &ident.type_ident;
let trait_path = &spec.trait_path;

let new_type = parse_internal! {
<#new_context_type as #trait_path>::#type_ident
};

return Ok(Some(new_type));
}
}
}

Ok(None)
}

/// Reject two imports that resolve to the same bare identifier or alias, across
/// every spec *and within a single braced list*. A shared alias would make the
/// substitution silently pick the first match and drop the rest, so it is an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,56 @@ use syn::{PathArguments, Type, TypePath, parse_quote, visit_mut};

use crate::types::attributes::UseTypeAttribute;

pub struct SubstituteAbstractType<'a> {
pub type_spec: &'a UseTypeAttribute,
/// A single-pass `VisitMut` that rewrites every bare, single-segment,
/// argument-free type path matching an imported alias into its fully-qualified
/// `<Context as Trait>::AssocType` form.
///
/// Unlike a per-spec visitor, this holds *every* `#[use_type]` spec at once, so
/// one traversal of the item handles all imports regardless of the order they
/// were written. Because the imported aliases are guaranteed unique (see
/// `forbid_duplicate_aliases`), at most one spec can match a given identifier,
/// so the match order among specs is irrelevant.
///
/// Each spec's `context_type` must already be *grounded* — resolved to a fully
/// qualified path (`<Self as HasTypes>::Types`) with no remaining bare alias —
/// before the visitor runs. Grounding is what lets a single traversal suffice:
/// the replacement a spec emits contains no bare alias, so the visitor never has
/// to revisit its own output to finish a nested import.
///
/// `is_changed` records whether any replacement was made during the traversal,
/// which the grounding fixpoint reads to decide when a further pass would be a
/// no-op.
pub struct SubstituteAbstractTypes<'a> {
pub specs: &'a [UseTypeAttribute],
pub is_changed: bool,
}

impl VisitMut for SubstituteAbstractType<'_> {
impl<'a> SubstituteAbstractTypes<'a> {
pub fn new(specs: &'a [UseTypeAttribute]) -> Self {
Self {
specs,
is_changed: false,
}
}
}

impl VisitMut for SubstituteAbstractTypes<'_> {
fn visit_type_mut(&mut self, ty: &mut Type) {
if let Type::Path(TypePath { qself: None, path }) = ty
&& path.leading_colon.is_none()
&& path.segments.len() == 1
{
let segment = &path.segments[0];
if matches!(segment.arguments, PathArguments::None)
&& let Some(replacement_ident) = self.type_spec.replace_ident(&segment.ident)
{
let trait_path = &self.type_spec.trait_path;
let context_type = &self.type_spec.context_type;
*ty = parse_quote! { <#context_type as #trait_path>::#replacement_ident };
return;
if matches!(segment.arguments, PathArguments::None) {
for spec in self.specs {
if let Some(replacement_ident) = spec.replace_ident(&segment.ident) {
let trait_path = &spec.trait_path;
let context_type = &spec.context_type;
*ty = parse_quote! { <#context_type as #trait_path>::#replacement_ident };
self.is_changed = true;
return;
}
}
}
}
visit_mut::visit_type_mut(self, ty);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
//! given a bespoke rule. The same boundary holds for `#[cgp_getter]` and for a
//! `#[cgp_fn]` `#[implicit]` argument, since all three share `parse_field_type`.
//!
//! See docs/implementation/entrypoints/cgp_auto_getter.md (Behavior and corner
//! cases).
//! See docs/errors/lowering/ill-formed-generated-type.md; the parser detail is in
//! docs/implementation/entrypoints/cgp_auto_getter.md (Behavior and corner cases).

use cgp::prelude::*;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! Acceptable failure: a foreign `#[use_type(HasScalarType.Scalar in Types)]` import
//! adds `Types: HasScalarType` to the generated trait, so naming the component for
//! a `Types` that does not implement `HasScalarType` is rejected by the compiler.
//!
//! This is the constraint that used to be *silently dropped* — before the trait
//! carried the foreign bound, `NoScalar` would have slipped through here and only
//! failed much later (or not at all, if the abstract type went unused). CGP is now
//! working as designed: it emits the bound and defers the actual check to `rustc`,
//! which reports the missing `NoScalar: HasScalarType` at the use site.
//!
//! See docs/reference/attributes/use_type.md and docs/errors/checks/check-trait-failure.md.

use cgp::prelude::*;

#[cgp_type]
pub trait HasScalarType {
type Scalar;
}

#[cgp_component(AreaCalculator)]
#[use_type(HasScalarType.Scalar in Types)]
pub trait CanCalculateArea<Types> {
fn area(&self) -> Scalar;
}

// `NoScalar` deliberately does not implement `HasScalarType`.
pub struct NoScalar;

// Asserting the component for `NoScalar` requires `NoScalar: HasScalarType`, which
// the foreign import now demands — so this fails to compile.
pub trait CheckMissingScalar: CanCalculateArea<NoScalar> {}

fn main() {}
Loading
Loading