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
27 changes: 14 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ lists every document in one page, which is the fastest way to find the few a tas
`#[cgp_component]`/`#[cgp_impl]`/`#[cgp_fn]`, `delegate_components!`, `HasField`, `UseDelegate`,
check traits, and so on). Re-invoke it whenever you move into an unfamiliar construct — the macros
and core traits here are the ground truth the skill describes, so read the two together.
- **Always read [cgp-knowledge-base/cgp/README.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/README.md)** to orient in the knowledge base, then follow it
into the README of whichever section covers your task.
- **Read [cgp-knowledge-base/cgp/reference/README.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/reference/README.md) and the relevant reference documents
whenever the task requires understanding a CGP construct** — what it means, what syntax it accepts,
and what code it expands to.
- **Read [cgp-knowledge-base/cgp/implementation/README.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/implementation/README.md) and the relevant
- **Always read the knowledge base's [`cgp` section README](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/README.md)** to orient,
then follow it into the README of whichever part covers your task.
- **Read the [construct reference](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/reference/README.md) and the relevant reference
documents whenever the task requires understanding a CGP construct** — what it means, what syntax
it accepts, and what code it expands to.
- **Read the [implementation reference](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/implementation/README.md) and the relevant
implementation documents whenever the task involves reading or modifying the CGP source code** —
they map each macro to its `cgp-macro-core`/`cgp-macro-lib` internals, corner cases, and tests.
- **Load the `/dual-reader-prose` skill whenever the task involves editing markdown documentation or
Expand Down Expand Up @@ -187,13 +187,14 @@ as the behavior allows.
### Orient before touching anything

Perform the standing steps in [Orient before any task](#orient-before-any-task) first, every
iteration. Then read the documentation specific to the macro under review, in the [knowledge base](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/cgp): its
reference document under [cgp-knowledge-base/cgp/reference/](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/cgp/reference), its implementation documents under
[cgp-knowledge-base/cgp/implementation/](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/cgp/implementation) (the `entrypoints/` document, the `asts/` stack it
drives, and any `functions/` helpers it relies on), and the governing `AGENTS.md` files that define
how those documents stay in sync with the code: [cgp-knowledge-base/cgp/AGENTS.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/AGENTS.md),
[cgp-knowledge-base/cgp/implementation/AGENTS.md](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/implementation/AGENTS.md), and
[crates/macros/cgp-macro-core/AGENTS.md](crates/macros/cgp-macro-core/AGENTS.md). These establish
iteration. Then read the knowledge base's documentation for the macro under review: its
[reference document](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/cgp/reference), its
[implementation documents](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/cgp/implementation) — the `entrypoints/` document, the
`asts/` stack it drives, and any `functions/` helpers it relies on — and the `AGENTS.md` files that
define how those documents stay in sync with the code
([the `cgp` section's](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/AGENTS.md),
[the implementation tree's](https://github.com/contextgeneric/cgp-knowledge-base/blob/main/cgp/implementation/AGENTS.md), and
[cgp-macro-core's](crates/macros/cgp-macro-core/AGENTS.md)). These establish
that the source is the single source of truth and that reference, implementation, snapshot, and
skill are four views of it that must never drift.

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use syn::{Ident, Type};

use crate::parse_internal;
use crate::types::attributes::UseTypeIdent;
use crate::types::ident::PathWithTypeArgs;
use crate::types::ident::{PathWithTypeArgs, TypeArg};

/// 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
Expand All @@ -17,6 +17,43 @@ pub struct UseTypeAttribute {
}

impl UseTypeAttribute {
/// The type positions in this spec that *grounding* resolves: the context the
/// imported types are projected against, and the generic arguments of the
/// owning trait path.
///
/// Both positions end up inside the emitted `<Context as Trait<Args…>>::Assoc`
/// path, so an alias left bare in either is an identifier that resolves to
/// nothing. Naming the set once — rather than at each of the three places that
/// walk it — is what keeps the grounding pass, the emitted bounds, and the
/// cycle check in agreement about what grounding reaches.
pub fn groundable_types(&self) -> impl Iterator<Item = &Type> {
core::iter::once(&self.context_type).chain(
self.trait_path
.type_args
.args
.iter()
.filter_map(|arg| match arg {
TypeArg::Type(ty) => Some(ty),
// A lifetime or const argument can never name an abstract type.
TypeArg::Lifetime(_) | TypeArg::Const(_) => None,
}),
)
}

/// [`Self::groundable_types`] by mutable reference, for the grounding pass.
pub fn groundable_types_mut(&mut self) -> impl Iterator<Item = &mut Type> {
core::iter::once(&mut self.context_type).chain(
self.trait_path
.type_args
.args
.iter_mut()
.filter_map(|arg| match arg {
TypeArg::Type(ty) => Some(ty),
TypeArg::Lifetime(_) | TypeArg::Const(_) => None,
}),
)
}

pub fn replace_ident(&self, ident: &Ident) -> Option<Ident> {
for type_ident in &self.type_idents {
if type_ident.alias_ident() == ident {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use syn::{ItemImpl, ItemTrait, Type};

use crate::functions::parse_internal;
use crate::types::attributes::UseTypeAttribute;
use crate::types::attributes::use_type::grounding::ground_specs;
use crate::types::attributes::use_type::type_predicates::{
derive_use_type_predicates, forbid_duplicate_aliases,
};
Expand All @@ -15,47 +16,24 @@ pub struct UseTypeAttributes {
}

impl UseTypeAttributes {
/// 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.
/// Validate the import list and resolve every spec into fully-qualified form,
/// the two steps that must precede any substitution.
///
/// 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;
}

if !changed {
break;
}
}
/// Aliases are checked for uniqueness first, because [`ground_specs`] resolves a
/// reference through the spec that owns the alias and so needs one owner per
/// name; it then grounds each spec against its dependencies and rejects a cycle.
fn resolved_specs(&self) -> syn::Result<Vec<UseTypeAttribute>> {
forbid_duplicate_aliases(&self.attributes)?;

grounded
ground_specs(&self.attributes)
}

pub fn transform_item_trait(&self, item_trait: &mut ItemTrait) -> syn::Result<()> {
if self.attributes.is_empty() {
return Ok(());
}

forbid_duplicate_aliases(&self.attributes)?;

let grounded = self.grounded_specs();
let grounded = self.resolved_specs()?;

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

Expand Down Expand Up @@ -98,9 +76,7 @@ impl UseTypeAttributes {
return Ok(());
}

forbid_duplicate_aliases(&self.attributes)?;

let grounded = self.grounded_specs();
let grounded = self.resolved_specs()?;

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
use std::collections::BTreeMap;

use syn::Ident;
use syn::visit_mut::VisitMut;

use crate::types::attributes::UseTypeAttribute;
use crate::visitors::{SubstituteAbstractTypes, collect_bare_aliases};

/// Resolve every `#[use_type]` spec's groundable positions into fully-qualified
/// form, rejecting an import list whose positions resolve through one another in a
/// cycle.
///
/// Grounding reaches the positions named by
/// [`UseTypeAttribute::groundable_types`] — the context and the trait path's own
/// generic arguments — because an alias left bare in either ends up inside an
/// emitted `<Context as Trait<Args…>>::Assoc` path, resolving to nothing. So
/// `HasTypes.Types, HasScalarType.Scalar in Types` rewrites the second context to
/// `<Self as HasTypes>::Types`, and `HasDbType.Db, HasPoolType<Db>.Pool` projects
/// against `HasPoolType<<Self as HasDbType>::Db>`. A position naming a real generic
/// parameter or `Self` matches no alias and is left untouched.
///
/// **Each spec is grounded against only the specs that are already grounded**, in
/// dependency order, which is what makes one pass per spec sufficient and makes a
/// half-resolved substitution impossible. A single depth-first walk of the
/// dependency graph gives that order and detects a cycle in the same traversal: a
/// spec is grounded once its dependencies return, and an edge back into a spec
/// still on the walk is a cycle. Both properties are therefore structural rather
/// than resting on an earlier validation pass.
///
/// Aliases must already be unique (see `forbid_duplicate_aliases`), since the graph
/// assumes one owning spec per alias.
///
/// The returned specs keep their **source order**, so the supertraits and `where`
/// predicates derived from them read in the order the author wrote the imports.
pub fn ground_specs(specs: &[UseTypeAttribute]) -> syn::Result<Vec<UseTypeAttribute>> {
// Which spec imports each alias, so a reference can be resolved to the spec it
// depends on.
let mut owner_of_alias: BTreeMap<String, usize> = BTreeMap::new();

for (index, spec) in specs.iter().enumerate() {
for type_ident in spec.type_idents.iter() {
owner_of_alias.insert(type_ident.alias_ident().to_string(), index);
}
}

// The dependency edges out of each spec: for every alias its groundable
// positions reference, the spec owning that alias, together with the referencing
// token so a rejected cycle can point at what the user wrote.
//
// Held outside `Grounding` so that indexing it inside the walk borrows the slice
// rather than `self`, leaving `self` free to mutate as the walk descends.
let edges: Vec<Vec<(usize, &Ident)>> = specs
.iter()
.map(|spec| {
spec.groundable_types()
.flat_map(collect_bare_aliases)
.filter_map(|ident| Some((*owner_of_alias.get(&ident.to_string())?, ident)))
.collect()
})
.collect();

let mut grounding = Grounding {
specs,
edges: &edges,
state: vec![VisitState::Unvisited; specs.len()],
grounded: vec![None; specs.len()],
labels: Vec::new(),
path: Vec::new(),
};

for index in 0..specs.len() {
grounding.resolve(index)?;
}

Ok(grounding
.grounded
.into_iter()
.map(|spec| spec.expect("every spec is grounded once the walk has visited all of them"))
.collect())
}

#[derive(Clone, Copy, PartialEq)]
enum VisitState {
Unvisited,
/// On the current walk, so an edge back into it closes a cycle.
InProgress,
/// Grounded, along with everything it depends on.
Done,
}

/// The depth-first walk that grounds each spec after its dependencies.
///
/// `path` holds the specs on the current walk and `labels` the referencing tokens
/// that led between them, so a cycle can be reported in the alias names the author
/// wrote rather than as spec indices.
struct Grounding<'a, 'edges> {
specs: &'a [UseTypeAttribute],
edges: &'edges [Vec<(usize, &'a Ident)>],
state: Vec<VisitState>,
grounded: Vec<Option<UseTypeAttribute>>,
labels: Vec<&'a Ident>,
path: Vec<usize>,
}

impl Grounding<'_, '_> {
fn resolve(&mut self, index: usize) -> syn::Result<()> {
// `Done` is the ordinary case: the spec and its dependencies are grounded, so
// there is nothing left to do. `InProgress` cannot arrive here, because the
// guard at the call site below reports a cycle before recursing — but
// descending on it anyway would recurse forever, so the test is written to
// stop on any state but `Unvisited`. A call site that ever skipped the guard
// then fails as an ungrounded spec at the end of the walk, which is
// diagnosable, rather than as a stack overflow inside the compiler.
if self.state[index] != VisitState::Unvisited {
return Ok(());
}

self.state[index] = VisitState::InProgress;
self.path.push(index);

for (target, reference) in self.edges[index].iter() {
if self.state[*target] == VisitState::InProgress {
return Err(self.cycle_error(*target, reference));
}

self.labels.push(reference);
self.resolve(*target)?;
self.labels.pop();
}

// Every dependency has returned, so all of them are grounded and this spec
// can be resolved against them.
self.ground(index);

self.path.pop();
self.state[index] = VisitState::Done;

Ok(())
}

/// Substitute one spec's groundable positions against the already-grounded
/// specs, and record the result.
fn ground(&mut self, index: usize) {
// Only grounded specs take part, so a replacement can never carry a bare
// alias of its own — which is what lets one pass per spec suffice.
let resolved: Vec<UseTypeAttribute> = self.grounded.iter().flatten().cloned().collect();

let mut spec = self.specs[index].clone();
let mut visitor = SubstituteAbstractTypes::new(&resolved);

for ty in spec.groundable_types_mut() {
visitor.visit_type_mut(ty);
}

self.grounded[index] = Some(spec);
}

/// The rejection for an edge back into `target`, labelled `closing`.
///
/// The cycle runs from wherever `target` sits on the current walk back around to
/// it, so its hops are the labels from that point on plus the closing reference —
/// which for a self-reference is the single `A` -> `A` hop.
fn cycle_error(&self, target: usize, closing: &Ident) -> syn::Error {
let entry = self
.path
.iter()
.position(|index| *index == target)
.unwrap_or(0);

let mut hops: Vec<String> = self.labels[entry..]
.iter()
.map(|ident| format!("`{ident}`"))
.collect();

hops.push(format!("`{closing}`"));
// Close the loop by restating where it started.
hops.push(hops[0].clone());

syn::Error::new_spanned(
closing,
format!(
"cannot ground `#[use_type]` imports: they resolve through one another \
in a cycle {}. An `in Context` clause or a trait argument may name another \
import's alias only if the resulting chain is acyclic, since a cycle has \
no valid grounding order.",
hops.join(" -> "),
),
)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod attribute;
mod attributes;
mod grounding;
mod ident;
mod type_predicates;

Expand Down
Loading
Loading