293 lines
12 KiB
Rust
293 lines
12 KiB
Rust
|
// Relationship between objects represented on ASG //
|
|||
|
// Copyright (C) 2014-2023 Ryan Specialty, LLC.
|
|||
|
//
|
|||
|
// This file is part of TAME.
|
|||
|
//
|
|||
|
// This program is free software: you can redistribute it and/or modify
|
|||
|
// it under the terms of the GNU General Public License as published by
|
|||
|
// the Free Software Foundation, either version 3 of the License, or
|
|||
|
// (at your option) any later version.
|
|||
|
//
|
|||
|
// This program is distributed in the hope that it will be useful,
|
|||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|||
|
// GNU General Public License for more details.
|
|||
|
//
|
|||
|
// You should have received a copy of the GNU General Public License
|
|||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
|
|||
|
//! Relationship betwen objects on the ASG.
|
|||
|
//!
|
|||
|
//! See (parent module)[super] for more information.
|
|||
|
|
|||
|
use super::{Expr, Ident, Object, ObjectIndex, ObjectKind, Pkg, Root};
|
|||
|
|
|||
|
/// Object types corresponding to variants in [`Object`].
|
|||
|
///
|
|||
|
/// These are used as small tags for [`ObjectRelatable`].
|
|||
|
/// Rust unfortunately makes working with its internal tags difficult,
|
|||
|
/// despite their efforts with [`std::mem::Discriminant`],
|
|||
|
/// which requires a _value_ to produce.
|
|||
|
///
|
|||
|
/// TODO: Encapsulate within `crate::asg` when the graph can be better
|
|||
|
/// encapsulated.
|
|||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|||
|
pub enum ObjectRelTy {
|
|||
|
Root,
|
|||
|
Pkg,
|
|||
|
Ident,
|
|||
|
Expr,
|
|||
|
}
|
|||
|
|
|||
|
/// Determine whether an edge from `from_ty` to `to_ty` is a cross edge.
|
|||
|
///
|
|||
|
/// This function is intended for _dynamic_ edge types,
|
|||
|
/// which cannot be determined statically;
|
|||
|
/// it should be used only in situations where the potential edge types
|
|||
|
/// are unbounded,
|
|||
|
/// e.g. on an iterator yielding generalized [`ObjectIndex`]es during
|
|||
|
/// a full graph traversal.
|
|||
|
/// You should otherwise use [`ObjectRel::is_cross_edge`].
|
|||
|
///
|
|||
|
/// The [`ObjectIndex`] `oi_to` represents the target object.
|
|||
|
/// It is not utilized at the time of writing,
|
|||
|
/// but is needed for internal data structures.
|
|||
|
///
|
|||
|
/// For more information on cross edges,
|
|||
|
/// see [`ObjectRel::is_cross_edge`].
|
|||
|
pub fn is_dyn_cross_edge(
|
|||
|
from_ty: ObjectRelTy,
|
|||
|
to_ty: ObjectRelTy,
|
|||
|
oi_to: ObjectIndex<Object>,
|
|||
|
) -> bool {
|
|||
|
/// Generate cross-edge mappings between ObjectRelTy and the associated
|
|||
|
/// ObjectRel.
|
|||
|
///
|
|||
|
/// This is intended to both reduce boilerplate and to eliminate typos.
|
|||
|
///
|
|||
|
/// This mess will be optimized away,
|
|||
|
/// but exists so that cross edge definitions can exist alongside
|
|||
|
/// other relationship definitions for each individual object type,
|
|||
|
/// rather than having to maintain them in aggregate here.
|
|||
|
macro_rules! ty_cross_edge {
|
|||
|
($($ty:ident),*) => {
|
|||
|
match from_ty {
|
|||
|
$(
|
|||
|
ObjectRelTy::$ty => {
|
|||
|
$ty::new_rel_dyn(to_ty, oi_to).is_some_and(
|
|||
|
|rel| rel.is_cross_edge()
|
|||
|
)
|
|||
|
},
|
|||
|
)*
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
ty_cross_edge!(Root, Pkg, Ident, Expr)
|
|||
|
}
|
|||
|
|
|||
|
/// Indicate that an [`ObjectKind`] `Self` can be related to
|
|||
|
/// [`ObjectKind`] `OB` by creating an edge from `Self` to `OB`.
|
|||
|
///
|
|||
|
/// This trait defines a portion of the graph ontology,
|
|||
|
/// allowing [`Self`] to be related to `OB` by creating a directed edge
|
|||
|
/// from [`Self`] _to_ `OB`, as in:
|
|||
|
///
|
|||
|
/// ```text
|
|||
|
/// (Self) -> (OB)
|
|||
|
/// ```
|
|||
|
///
|
|||
|
/// While the data on the graph itself is dynamic and provided at runtime,
|
|||
|
/// the systems that _construct_ the graph using the runtime data can be
|
|||
|
/// statically analyzed by the type system to ensure that they only
|
|||
|
/// construct graphs that adhere to this schema.
|
|||
|
pub trait ObjectRelTo<OB: ObjectKind + ObjectRelatable> =
|
|||
|
ObjectRelatable where <Self as ObjectRelatable>::Rel: From<ObjectIndex<OB>>;
|
|||
|
|
|||
|
/// Reverse of [`ObjectRelTo`].
|
|||
|
///
|
|||
|
/// This is primarily useful for avoiding `where` clauses,
|
|||
|
/// or for use in `impl Trait` specifications.
|
|||
|
pub trait ObjectRelFrom<OA: ObjectKind + ObjectRelatable> =
|
|||
|
ObjectRelatable where <OA as ObjectRelatable>::Rel: From<ObjectIndex<Self>>;
|
|||
|
|
|||
|
/// Identify [`Self::Rel`] as a sum type consisting of the subset of
|
|||
|
/// [`Object`] variants representing the valid _target_ edges of
|
|||
|
/// [`Self`].
|
|||
|
///
|
|||
|
/// This is used to derive [`ObjectRelTo``],
|
|||
|
/// which can be used as a trait bound to assert a valid relationship
|
|||
|
/// between two [`Object`]s.
|
|||
|
pub trait ObjectRelatable: ObjectKind {
|
|||
|
/// Sum type representing a subset of [`Object`] variants that are valid
|
|||
|
/// targets for edges from [`Self`].
|
|||
|
///
|
|||
|
/// See [`ObjectRel`] for more information.
|
|||
|
type Rel: ObjectRel<Self>;
|
|||
|
|
|||
|
/// The [`ObjectRelTy`] tag used to identify this [`ObjectKind`] as a
|
|||
|
/// target of a relation.
|
|||
|
fn rel_ty() -> ObjectRelTy;
|
|||
|
|
|||
|
/// Represent a relation to another [`ObjectKind`] that cannot be
|
|||
|
/// statically known and must be handled at runtime.
|
|||
|
///
|
|||
|
/// A value of [`None`] means that the provided [`ObjectRelTy`] is not
|
|||
|
/// valid for [`Self`].
|
|||
|
/// If the caller is utilizing edge data that is already present on the graph,
|
|||
|
/// then this means that the system is not properly upholding edge
|
|||
|
/// invariants
|
|||
|
/// (the graph's ontology)
|
|||
|
/// and the system ought to panic;
|
|||
|
/// this is a significant bug representing a problem with the
|
|||
|
/// correctness of the system.
|
|||
|
///
|
|||
|
/// See [`ObjectRel`] for more information.
|
|||
|
fn new_rel_dyn(
|
|||
|
ty: ObjectRelTy,
|
|||
|
oi: ObjectIndex<Object>,
|
|||
|
) -> Option<Self::Rel>;
|
|||
|
}
|
|||
|
|
|||
|
impl<O: ObjectKind + ObjectRelatable> ObjectIndex<O> {
|
|||
|
pub fn rel_ty(&self) -> ObjectRelTy {
|
|||
|
O::rel_ty()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// A relationship to another [`ObjectKind`].
|
|||
|
///
|
|||
|
/// This trait is intended to be implemented by enums that represent the
|
|||
|
/// subset of [`ObjectKind`]s that are able to serve as edge targets for
|
|||
|
/// the [`ObjectRelatable`] that utilizes it as its
|
|||
|
/// [`ObjectRelatable::Rel`].
|
|||
|
///
|
|||
|
/// As described in the [module-level documentation](super),
|
|||
|
/// the concrete [`ObjectKind`] of an edge is generally not able to be
|
|||
|
/// determined statically outside of code paths that created the
|
|||
|
/// [`Object`] anew.
|
|||
|
/// But we _can_ at least narrow the types of [`ObjectKind`]s to those
|
|||
|
/// [`ObjectRelTo`]s that we know are valid,
|
|||
|
/// since the system is restricted (statically) to those edges when
|
|||
|
/// performing operations on the graph.
|
|||
|
///
|
|||
|
/// This [`ObjectRel`] represents that subset of [`ObjectKind`]s.
|
|||
|
/// A caller may decide to dispatch based on the type of edge it receives,
|
|||
|
/// or it may filter edges with [`Self::narrow`] in conjunction with
|
|||
|
/// [`Iterator::filter_map`]
|
|||
|
/// (for example).
|
|||
|
/// Since the wrapped value is an [`ObjectIndex`],
|
|||
|
/// the system will eventually panic if it attempts to reference a node
|
|||
|
/// that is not of the type expected by the edge,
|
|||
|
/// which can only happen if the edge has an incorrect [`ObjectRelTy`],
|
|||
|
/// meaning the graph is somehow corrupt
|
|||
|
/// (because system invariants were not upheld).
|
|||
|
///
|
|||
|
/// This affords us both runtime memory safety and static guarantees that
|
|||
|
/// the system is not able to generate an invalid graph that does not
|
|||
|
/// adhere to the prescribed ontology,
|
|||
|
/// provided that invariants are properly upheld by the
|
|||
|
/// [`asg`](crate::asg) module.
|
|||
|
pub trait ObjectRel<OA: ObjectKind + ObjectRelatable>: Sized {
|
|||
|
/// Attempt to narrow into the [`ObjectKind`] `OB`.
|
|||
|
///
|
|||
|
/// Unlike [`Object`] nodes,
|
|||
|
/// _this operation does not panic_,
|
|||
|
/// instead returning an [`Option`].
|
|||
|
/// If the relationship is of type `OB`,
|
|||
|
/// then [`Some`] will be returned with an inner
|
|||
|
/// [`ObjectIndex<OB>`](ObjectIndex).
|
|||
|
/// If the narrowing fails,
|
|||
|
/// [`None`] will be returned instead.
|
|||
|
///
|
|||
|
/// This return value is well-suited for [`Iterator::filter_map`] to
|
|||
|
/// query for edges of particular kinds.
|
|||
|
fn narrow<OB: ObjectRelFrom<OA> + ObjectRelatable>(
|
|||
|
self,
|
|||
|
) -> Option<ObjectIndex<OB>>;
|
|||
|
|
|||
|
/// Attempt to narrow into the [`ObjectKind`] `OB`,
|
|||
|
/// but rather than returning the narrowed type,
|
|||
|
/// return `Option<Self>`.
|
|||
|
///
|
|||
|
/// This can be used with [`Iterator::filter_map`].
|
|||
|
/// By not being a [`bool`] predicate,
|
|||
|
/// we're able to provide a default trait implementation based on
|
|||
|
/// [`Self::narrow`] without requiring that [`Self`] implement
|
|||
|
/// [`Copy`].
|
|||
|
fn narrows_into<OB: ObjectRelFrom<OA> + ObjectRelatable>(
|
|||
|
self,
|
|||
|
) -> Option<Self>
|
|||
|
where
|
|||
|
Self: From<ObjectIndex<OB>>,
|
|||
|
{
|
|||
|
self.narrow::<OB>().map(Into::into)
|
|||
|
}
|
|||
|
|
|||
|
/// Whether this relationship represents an ontological cross edge.
|
|||
|
///
|
|||
|
/// A _cross edge_ is an edge between two trees as described by the
|
|||
|
/// graph's ontology.
|
|||
|
/// Many objects on the graph represent trees,
|
|||
|
/// but contain references to other trees.
|
|||
|
/// Recognizing cross edges allows the system to understand when it is
|
|||
|
/// following an edge between two trees,
|
|||
|
/// which may require different behavior.
|
|||
|
///
|
|||
|
/// This contrasts to cross edges in the context of a graph traversal,
|
|||
|
/// where a tree is determined by a walk of the graph and may not take
|
|||
|
/// into consideration the meaning of edges.
|
|||
|
///
|
|||
|
/// _Because this is a property of the ontology rather than a structural
|
|||
|
/// interpretation of the graph,
|
|||
|
/// it must be manually verified by a human._
|
|||
|
/// An incorrect flagging of cross edges here will result in certain
|
|||
|
/// traversals being incorrect.
|
|||
|
///
|
|||
|
/// Implementation Context
|
|||
|
/// ======================
|
|||
|
/// It is important to understand why this method exists and how it may
|
|||
|
/// be used so that implementations of this trait do the right thing
|
|||
|
/// with regards to determining whether an edge ought to represent a
|
|||
|
/// cross edge.
|
|||
|
///
|
|||
|
/// For example,
|
|||
|
/// when generating a representation of an [`Expr`],
|
|||
|
/// a walk of the graph ought not consider an [`Ident`] reference to
|
|||
|
/// be part of the expression tree,
|
|||
|
/// otherwise the referenced expression would be inlined.
|
|||
|
/// Furthermore,
|
|||
|
/// visiting the referenced [`Ident`] ought not inhibit a later walk,
|
|||
|
/// since the walk must later traverse the [`Ident`] to reach the
|
|||
|
/// [`Object`] that it represents.
|
|||
|
/// Similarly,
|
|||
|
/// if the [`Ident`] has already been visited by a previous walk,
|
|||
|
/// we want to _re-visit_ it to output a reference as part of the
|
|||
|
/// referencing [`Expr`].
|
|||
|
///
|
|||
|
/// However,
|
|||
|
/// this behavior is not always desirable.
|
|||
|
/// In the case of a topological sort of the graph for linking,
|
|||
|
/// cross edges ought to count as visitations since that dependency
|
|||
|
/// must be calculated before the expression that needs it,
|
|||
|
/// and we don't want to re-calculate it again later on.
|
|||
|
///
|
|||
|
/// The cross-edge is therefore an ontological fact,
|
|||
|
/// but its _interpretation_ is context-dependent.
|
|||
|
///
|
|||
|
/// Note that the ontology is not intended to support back edges,
|
|||
|
/// since they produce cycles,
|
|||
|
/// except for exceptional situations
|
|||
|
/// (e.g. function recursion which will hopefully be removed from
|
|||
|
/// the language in the future).
|
|||
|
/// With that said,
|
|||
|
/// if an edge could conceivably be a back edge and not be rejected
|
|||
|
/// from circular dependency checks,
|
|||
|
/// then do _not_ assume that it is a cross edge without further
|
|||
|
/// analysis,
|
|||
|
/// which may require adding the [`Asg`] or other data
|
|||
|
/// (like a path)
|
|||
|
/// as another parameter to this function.
|
|||
|
fn is_cross_edge(&self) -> bool;
|
|||
|
}
|