tamer: asg::graph::object::rel: Extract object relationships

DEV-13708
main
Mike Gerwitz 2023-02-08 15:15:30 -05:00
parent 065dca88fc
commit 2b2776f4e1
2 changed files with 297 additions and 268 deletions

View File

@ -120,11 +120,16 @@ use std::{convert::Infallible, fmt::Display, marker::PhantomData};
pub mod expr;
pub mod ident;
pub mod pkg;
mod rel;
pub mod root;
pub use expr::Expr;
pub use ident::Ident;
pub use pkg::Pkg;
pub use rel::{
is_dyn_cross_edge, ObjectRel, ObjectRelFrom, ObjectRelTo, ObjectRelTy,
ObjectRelatable,
};
pub use root::Root;
/// An object on the ASG.
@ -152,69 +157,6 @@ pub enum Object {
Expr(Expr),
}
/// 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: `pub(super)` 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(super) 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)
}
impl Display for Object {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
@ -713,211 +655,6 @@ impl<O: ObjectKind> From<ObjectIndex<O>> for Span {
}
}
/// 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;
}
/// A container for an [`Object`] allowing for owned borrowing of data.
///
/// The purpose of allowing this owned borrowing is to permit a functional

View File

@ -0,0 +1,292 @@
// 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;
}