tamer: asg::graph: Formalize dynamic relationships (edges)

The `TreePreOrderDfs` iterator needed to expose additional edge context to
the caller (specifically, the `Span`).  This was getting a bit messy, so
this consolodates everything into a new `DynObjectRel`, which also
emphasizes that it is in need of narrowing.

Packing everything up like that also allows us to return more information to
the caller without complicating the API, since the caller does not need to
be concerned with all of those values individually.

Depth is kept separate, since that is a property of the traversal and is not
stored on the graph.  (Rather, it _is_ a property of the graph, but it's not
calculated until traversal.  But, depth will also vary for a given node
because of cross edges, and so we cannot store any concrete depth on the
graph for a given node.  Not even a canonical one, because once we start
doing inlining and common subexpression elimination, there will be shared
edges that are _not_ cross edges (the node is conceptually part of _both_
trees).  Okay, enough of this rambling parenthetical.)

DEV-13708
main
Mike Gerwitz 2023-02-09 13:11:27 -05:00
parent 2b2776f4e1
commit 7f3ce44481
8 changed files with 218 additions and 122 deletions

View File

@ -19,7 +19,9 @@
//! Abstract semantic graph.
use self::object::{ObjectRelFrom, ObjectRelTy, ObjectRelatable, Root};
use self::object::{
DynObjectRel, ObjectRelFrom, ObjectRelTy, ObjectRelatable, Root,
};
use super::{
AsgError, FragmentText, Ident, IdentKind, Object, ObjectIndex, ObjectKind,
@ -61,7 +63,12 @@ pub type AsgResult<T> = Result<T, AsgError>;
/// This small memory expense allows for bidirectional edge filtering
/// and [`ObjectIndex`] [`ObjectKind`] resolution without an extra layer
/// of indirection to look up the source/target [`Node`].
type AsgEdge = (ObjectRelTy, ObjectRelTy);
///
/// The edge may also optionally contain a [`Span`] that provides additional
/// context in situations where the distinction between the span of the
/// target object and the span of the _reference_ to that object is
/// important.
type AsgEdge = (ObjectRelTy, ObjectRelTy, Option<Span>);
/// Each node of the graph.
type Node = ObjectContainer;
@ -305,7 +312,7 @@ impl Asg {
self.graph.add_edge(
self.root_node,
identi.into(),
(ObjectRelTy::Root, ObjectRelTy::Ident),
(ObjectRelTy::Root, ObjectRelTy::Ident, None),
);
}
@ -420,19 +427,25 @@ impl Asg {
/// Add an edge from the [`Object`] represented by the
/// [`ObjectIndex`] `from_oi` to the object represented by `to_oi`.
///
/// The edge may optionally contain a _contextual [`Span`]_,
/// in cases where it is important to distinguish between the span
/// associated with the target and the span associated with the
/// _reference_ to the target.
///
/// For more information on how the ASG's ontology is enforced statically,
/// see [`ObjectRelTo`].
fn add_edge<OA: ObjectKind, OB: ObjectKind>(
&mut self,
from_oi: ObjectIndex<OA>,
to_oi: ObjectIndex<OB>,
ctx_span: Option<Span>,
) where
OA: ObjectRelTo<OB>,
{
self.graph.add_edge(
from_oi.into(),
to_oi.into(),
(OA::rel_ty(), OB::rel_ty()),
(OA::rel_ty(), OB::rel_ty(), ctx_span),
);
}
@ -516,12 +529,14 @@ impl Asg {
&'a self,
oi: ObjectIndex<O>,
) -> impl Iterator<Item = O::Rel> + 'a {
self.edges_dyn(oi.widen()).map(move |(rel_ty, oi_b)| {
O::new_rel_dyn(rel_ty, oi_b).diagnostic_unwrap(|| {
self.edges_dyn(oi.widen()).map(move |dyn_rel| {
let target_ty = dyn_rel.target_ty();
dyn_rel.narrow::<O>().diagnostic_unwrap(|| {
vec![
oi.internal_error(format!(
"encountered invalid outgoing edge type {:?}",
rel_ty,
target_ty,
)),
oi.help(
"this means that Asg did not enforce edge invariants \
@ -545,11 +560,15 @@ impl Asg {
fn edges_dyn<'a>(
&'a self,
oi: ObjectIndex<Object>,
) -> impl Iterator<Item = (ObjectRelTy, ObjectIndex<Object>)> + 'a {
) -> impl Iterator<Item = DynObjectRel> + 'a {
self.graph.edges(oi.into()).map(move |edge| {
(
edge.weight().1,
let (src_ty, target_ty, ctx_span) = edge.weight();
DynObjectRel::new(
*src_ty,
*target_ty,
ObjectIndex::<Object>::new(edge.target(), oi),
*ctx_span,
)
})
}
@ -727,7 +746,7 @@ impl Asg {
self.graph.update_edge(
identi.into(),
depi.into(),
(Ident::rel_ty(), O::rel_ty()),
(Ident::rel_ty(), O::rel_ty(), None),
);
}
@ -765,7 +784,7 @@ impl Asg {
self.graph.update_edge(
identi.into(),
depi.into(),
(Ident::rel_ty(), Ident::rel_ty()),
(Ident::rel_ty(), Ident::rel_ty(), None),
);
(identi, depi)

View File

@ -127,7 +127,7 @@ pub use expr::Expr;
pub use ident::Ident;
pub use pkg::Pkg;
pub use rel::{
is_dyn_cross_edge, ObjectRel, ObjectRelFrom, ObjectRelTo, ObjectRelTy,
DynObjectRel, ObjectRel, ObjectRelFrom, ObjectRelTo, ObjectRelTy,
ObjectRelatable,
};
pub use root::Root;
@ -419,16 +419,24 @@ impl<O: ObjectKind> ObjectIndex<O> {
/// An edge can only be added if ontologically valid;
/// see [`ObjectRelTo`] for more information.
///
/// Edges may contain _contextual [`Span`]s_ if there is an important
/// distinction to be made between a the span of a _reference_ to the
/// target and the span of the target itself.
/// This is of particular benefit to cross edges
/// (see [`ObjectRel::is_cross_edge`]),
/// which reference nodes from other trees in different contexts.
///
/// See also [`Self::add_edge_to`].
pub fn add_edge_to<OB: ObjectKind>(
self,
asg: &mut Asg,
to_oi: ObjectIndex<OB>,
ctx_span: Option<Span>,
) -> Self
where
O: ObjectRelTo<OB>,
{
asg.add_edge(self, to_oi);
asg.add_edge(self, to_oi, ctx_span);
self
}
@ -442,11 +450,12 @@ impl<O: ObjectKind> ObjectIndex<O> {
self,
asg: &mut Asg,
from_oi: ObjectIndex<OB>,
ctx_span: Option<Span>,
) -> Self
where
OB: ObjectRelTo<O>,
{
from_oi.add_edge_to(asg, self);
from_oi.add_edge_to(asg, self, ctx_span);
self
}
@ -587,7 +596,7 @@ impl<O: ObjectKind> ObjectIndex<O> {
where
Root: ObjectRelTo<O>,
{
asg.root(self.span()).add_edge_to(asg, self);
asg.root(self.span()).add_edge_to(asg, self, None);
self
}

View File

@ -25,7 +25,12 @@ use super::{
Asg, Ident, Object, ObjectIndex, ObjectRel, ObjectRelFrom, ObjectRelTy,
ObjectRelatable,
};
use crate::{f::Functor, num::Dim, parse::util::SPair, span::Span};
use crate::{
f::Functor,
num::Dim,
parse::{util::SPair, Token},
span::Span,
};
#[cfg(doc)]
use super::ObjectKind;
@ -266,7 +271,7 @@ impl ObjectIndex<Expr> {
expr: Expr,
) -> ObjectIndex<Expr> {
let oi_subexpr = asg.create(expr);
oi_subexpr.add_edge_from(asg, self)
oi_subexpr.add_edge_from(asg, self, None)
}
/// Reference the value of the expression identified by `ident` as if it
@ -277,6 +282,6 @@ impl ObjectIndex<Expr> {
/// it becomes available.
pub fn ref_expr(self, asg: &mut Asg, ident: SPair) -> Self {
let identi = asg.lookup_or_missing(ident);
self.add_edge_to(asg, identi)
self.add_edge_to(asg, identi, Some(ident.span()))
}
}

View File

@ -1095,7 +1095,7 @@ impl ObjectIndex<Ident> {
// and use the newly provided `id` and its span.
Missing(_) => Ok(Transparent(id)),
})
.map(|ident_oi| ident_oi.add_edge_to(asg, definition))
.map(|ident_oi| ident_oi.add_edge_to(asg, definition, None))
}
/// Whether this identifier is bound to the object represented by `oi`.

View File

@ -113,7 +113,7 @@ impl From<ObjectIndex<Ident>> for PkgRel {
impl ObjectIndex<Pkg> {
/// Indicate that the given identifier `oi` is defined in this package.
pub fn defines(self, asg: &mut Asg, oi: ObjectIndex<Ident>) -> Self {
self.add_edge_to(asg, oi)
self.add_edge_to(asg, oi, None)
}
/// Complete the definition of a package.

View File

@ -20,6 +20,8 @@
//!
//! See (parent module)[super] for more information.
use crate::{f::Functor, span::Span};
use super::{Expr, Ident, Object, ObjectIndex, ObjectKind, Pkg, Root};
/// Object types corresponding to variants in [`Object`].
@ -39,51 +41,127 @@ pub enum ObjectRelTy {
Expr,
}
/// Determine whether an edge from `from_ty` to `to_ty` is a cross edge.
/// A dynamic relationship (edge) from one object to another before it has
/// been narrowed.
///
/// 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()
)
},
)*
}
/// The target of this edge is usually an [`ObjectIndex`],
/// but it is made generic (`T`) to support mapping while retaining useful
/// metadata,
/// e.g. to resolve an object while retaining the edge information.
#[derive(Debug, PartialEq)]
pub struct DynObjectRel<T = ObjectIndex<Object>>(
(ObjectRelTy, ObjectRelTy),
T,
Option<Span>,
);
impl<T> DynObjectRel<T> {
pub(in super::super) fn new(
from_ty: ObjectRelTy,
to_ty: ObjectRelTy,
x: T,
ctx_span: Option<Span>,
) -> Self {
Self((from_ty, to_ty), x, ctx_span)
}
/// The type of the source edge.
pub fn source_ty(&self) -> ObjectRelTy {
match self {
Self((ty, _), ..) => *ty,
}
}
ty_cross_edge!(Root, Pkg, Ident, Expr)
/// The type of the target edge.
pub fn target_ty(&self) -> ObjectRelTy {
match self {
Self((_, ty), ..) => *ty,
}
}
/// The target of this relationship.
///
/// This type generally originates as [`ObjectIndex`] but can be mapped
/// over to retain the structured edge data.
pub fn target(&self) -> &T {
match self {
Self(_, oi, _) => oi,
}
}
/// A [`Span`] associated with the _relationship_ between the source and
/// target objects,
/// if any.
pub fn ctx_span(&self) -> Option<Span> {
match self {
Self(_, _, ctx_span) => *ctx_span,
}
}
}
impl DynObjectRel<ObjectIndex<Object>> {
/// See [`ObjectIndex::must_narrow_into`].
pub fn must_narrow_into<O: ObjectKind>(&self) -> ObjectIndex<O> {
match self {
Self(_, oi, _) => oi.must_narrow_into(),
}
}
/// Attempt to narrow into the [`ObjectRel`] of `O`.
///
/// See [`ObjectRelatable::new_rel_dyn`] for more information.
pub fn narrow<O: ObjectKind + ObjectRelatable>(&self) -> Option<O::Rel> {
O::new_rel_dyn(self.target_ty(), *self.target())
}
/// Dynamically determine whether this edge represents 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`].
///
/// For more information on cross edges,
/// see [`ObjectRel::is_cross_edge`].
pub fn is_cross_edge(&self) -> 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 self.source_ty() {
$(
ObjectRelTy::$ty => {
self.narrow::<$ty>().is_some_and(
|rel| rel.is_cross_edge()
)
},
)*
}
}
}
ty_cross_edge!(Root, Pkg, Ident, Expr)
}
}
impl<T, U> Functor<T, U> for DynObjectRel<T> {
type Target = DynObjectRel<U>;
fn map(self, f: impl FnOnce(T) -> U) -> Self::Target {
match self {
Self(tys, x, ctx_span) => DynObjectRel(tys, f(x), ctx_span),
}
}
}
/// Indicate that an [`ObjectKind`] `Self` can be related to
@ -132,7 +210,7 @@ pub trait ObjectRelatable: ObjectKind {
/// 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
/// A value of [`None`] means that the provided [`DynObjectRel`] 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
@ -285,8 +363,6 @@ pub trait ObjectRel<OA: ObjectKind + ObjectRelatable>: Sized {
/// 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.
/// which may require introducing more context to this method.
fn is_cross_edge(&self) -> bool;
}

View File

@ -23,10 +23,7 @@
//! reconstruct a source representation of the program from the current
//! state of [`Asg`].
use super::{
object::{is_dyn_cross_edge, ObjectRelTy},
Asg, Object, ObjectIndex,
};
use super::{object::DynObjectRel, Asg, Object, ObjectIndex};
use crate::span::UNKNOWN_SPAN;
// Re-export so that users of this API can avoid an awkward import from a
@ -170,15 +167,12 @@ pub struct TreePreOrderDfs<'a> {
/// DFS stack.
///
/// The tuple represents the source and target edge [`ObjectRelTy`]s
/// respectively,
/// along with the [`ObjectIndex`] to be visited.
/// As nodes are visited,
/// its edges are pushed onto the stack.
/// Each iterator pops a tuple off the stack and visits that node.
/// As objects (nodes/vertices) are visited,
/// its relationships (edges) are pushed onto the stack.
/// Each iterator pops a relationship off the stack and visits it.
///
/// The traversal ends once the stack becomes empty.
stack: Vec<(ObjectRelTy, ObjectRelTy, ObjectIndex<Object>, Depth)>,
stack: Vec<(DynObjectRel, Depth)>,
}
/// Initial size of the DFS stack for [`TreePreOrderDfs`].
@ -196,25 +190,20 @@ impl<'a> TreePreOrderDfs<'a> {
};
let root = asg.root(span);
dfs.push_edges_of(root.rel_ty(), root.widen(), Depth::root());
dfs.push_edges_of(root.widen(), Depth::root());
dfs
}
fn push_edges_of(
&mut self,
from_ty: ObjectRelTy,
oi: ObjectIndex<Object>,
depth: Depth,
) {
fn push_edges_of(&mut self, oi: ObjectIndex<Object>, depth: Depth) {
self.asg
.edges_dyn(oi)
.map(|(rel_ty, oi)| (from_ty, rel_ty, oi, depth.child_depth()))
.map(|rel| (rel, depth.child_depth()))
.collect_into(&mut self.stack);
}
}
impl<'a> Iterator for TreePreOrderDfs<'a> {
type Item = (ObjectIndex<Object>, Depth);
type Item = (DynObjectRel, Depth);
/// Produce the next [`ObjectIndex`] from the traversal in pre-order.
///
@ -228,15 +217,15 @@ impl<'a> Iterator for TreePreOrderDfs<'a> {
/// This depth is the only way to derive the tree structure from this
/// iterator.
fn next(&mut self) -> Option<Self::Item> {
let (from_ty, next_ty, next, next_depth) = self.stack.pop()?;
let (rel, depth) = self.stack.pop()?;
// We want to output information about references to other trees,
// but we must not traverse into them.
if !is_dyn_cross_edge(from_ty, next_ty, next) {
self.push_edges_of(next_ty, next, next_depth);
if !rel.is_cross_edge() {
self.push_edges_of(*rel.target(), depth);
}
Some((next, next_depth))
Some((rel, depth))
}
}

View File

@ -21,10 +21,12 @@ use super::*;
use crate::{
asg::{
air::{Air, AirAggregate},
graph::object::ObjectRelTy,
ExprOp,
},
f::Functor,
parse::{util::SPair, ParseState},
span::dummy::*,
span::{dummy::*, Span},
};
use std::fmt::Debug;
@ -51,25 +53,21 @@ fn traverses_ontological_tree() {
let id_a = SPair("expr_a".into(), S3);
let id_b = SPair("expr_b".into(), S9);
#[rustfmt::skip]
let toks = vec![
// <package>
PkgOpen(S1),
// <expr>
ExprOpen(ExprOp::Sum, S2),
ExprIdent(id_a),
// <expr>
ExprOpen(ExprOp::Sum, S4),
ExprClose(S5),
// </expr>
ExprRef(SPair(id_b.symbol(), S6)),
ExprClose(S7),
// </expr>
// <expr>
ExprOpen(ExprOp::Sum, S8),
ExprIdent(id_b),
ExprClose(S10),
// </expr>
// </package>
ExprOpen(ExprOp::Sum, S2),
ExprIdent(id_a),
ExprOpen(ExprOp::Sum, S4),
ExprClose(S5),
ExprRef(SPair(id_b.symbol(), S6)),
ExprClose(S7),
ExprOpen(ExprOp::Sum, S8),
ExprIdent(id_b),
ExprClose(S10),
PkgClose(S11),
];
@ -80,28 +78,28 @@ fn traverses_ontological_tree() {
// tree.
let sut = tree_reconstruction(&asg);
// We need more concise expressions for the below table of values.
use ObjectRelTy as Ty;
let d = DynObjectRel::new;
let m = |a: Span, b: Span| a.merge(b).unwrap();
// Note that the `Depth` beings at 1 because the actual root of the
// graph is not emitted.
// Further note that the depth is the depth of the _path_,
// and so identifiers contribute to the depth even though the source
// language doesn't have such nesting.
#[rustfmt::skip]
assert_eq!(
vec![
(S1.merge(S11).unwrap(), Depth(1)), // Pkg
(S3, Depth(2)), // Ident (id_a)
(S2.merge(S7).unwrap(), Depth(3)), // Expr
(S4.merge(S5).unwrap(), Depth(4)), // Expr
(S9, Depth(4)), // Ident (ExpRef)¹
(S9, Depth(2)), // Ident (id_b)
(S8.merge(S10).unwrap(), Depth(3)), // Expr
(d(Ty::Root, Ty::Pkg, m(S1, S11), None ), Depth(1)),
(d(Ty::Pkg, Ty::Ident, S3, None ), Depth(2)),
(d(Ty::Ident, Ty::Expr, m(S2, S7), None ), Depth(3)),
(d(Ty::Expr, Ty::Expr, m(S4, S5), None ), Depth(4)),
(d(Ty::Expr, Ty::Ident, S9, Some(S6)), Depth(4)),
(d(Ty::Pkg, Ty::Ident, S9, None ), Depth(2)),
(d(Ty::Ident, Ty::Expr, m(S8, S10), None ), Depth(3)),
],
sut.map(|(oi, depth)| (oi.resolve(&asg).span(), depth))
sut.map(|(rel, depth)| (rel.map(|oi| oi.resolve(&asg).span()), depth))
.collect::<Vec<_>>(),
);
// ¹ We have lost the reference context (S6),
// which is probably the more appropriate one to be output here,
// given that this is a source reconstruction and ought to be mapped
// back to what the user entered at the equivalent point in the tree.
// TODO: Figure out how to best expose this,
// which probably also involves the introduction of edge spans.
}