tamer: asg: Dynamically determined cross edges

Previous to this commit, ontological cross edges were declared
statically.  But this doesn't fare well with the decided implementation for
template application.

The documentation details it, but we have Tpl->Ident which could mean "I
define this Ident once expanded", or it could mean "this is a reference to a
template I will be applying".  The former is a tree edge, the latter is a
cross edge, and that determination can only be made by inspecting edge data
at runtime.

It could have been resolved by introducing new Object types, but that is a
lot of work for little benefit, especially given that only (right now) the
visitor uses this information.

DEV-13708
main
Mike Gerwitz 2023-03-17 12:53:53 -04:00
parent e132f108e8
commit 893da0ed20
5 changed files with 148 additions and 56 deletions

View File

@ -114,9 +114,23 @@ block_src && $NF ~ /\w+,$/ {
# Edge type (cross, tree)
ty = $(NF-1)
# Dashed is visually in-between solid and dotted,
# and `dyn` is either `tree` or `cross`,
# determined at runtime.
# But we may need some other representation if `dotted` is too visually
# sparse and difficult/annoying to see;
# let's see where the ASG visualization ends up first.
attrs = ""
if (ty == "cross") {
attrs="[style=dashed]"
switch (ty) {
case "tree":
attrs="[style=solid]";
break;
case "cross":
attrs="[style=dotted]";
break;
case "dyn":
attrs="[style=dashed]";
break;
}
gsub(/,$/, "")

View File

@ -178,12 +178,6 @@ fn expr_non_empty_ident_root() {
// Identifiers should reference the same expression.
let expr_b = asg.expect_ident_obj::<Expr>(id_b);
assert_eq!(expr_a, expr_b);
// Ontological sanity check:
// Child expressions must not be considered cross edges since they are
// part of the same tree.
let oi_expr_a = asg.expect_ident_oi::<Expr>(id_a);
assert!(!oi_expr_a.edges(&asg).any(|rel| rel.is_cross_edge()));
}
// Binding an identifier after a child expression means that the parser is
@ -746,12 +740,6 @@ fn expr_ref_to_ident() {
// We should have only a single reference (to `id_bar`).
assert_eq!(foo_rels.len(), 1);
// Ontological sanity check:
// references to identifiers should count as cross edges.
// This is very important to ensure that certain graph traversals work
// correctly between trees.
assert!(foo_rels.iter().all(|rel| rel.is_cross_edge()));
let oi_ident_bar =
foo_rels.pop().and_then(ExprRel::narrow::<Ident>).unwrap();
let ident_bar = oi_ident_bar.resolve(&asg);
@ -852,12 +840,6 @@ fn idents_share_defining_pkg() {
let oi_baz = asg.lookup(id_baz).unwrap();
assert_eq!(None, oi_baz.src_pkg(&asg));
// Ontological sanity check:
// edges from the package to identifiers defined by it should not be
// considered cross edges.
let oi_pkg = oi_foo.src_pkg(&asg).unwrap();
assert!(oi_pkg.edges(&asg).all(|rel| !rel.is_cross_edge()));
// The package span should encompass the entire definition.
assert_eq!(
S1.merge(S9),

View File

@ -80,10 +80,14 @@ macro_rules! object_rel {
}
/// The root of the graph by definition has no cross edges.
fn is_cross_edge(&self) -> bool {
fn is_cross_edge<S, T>(
&self,
#[allow(unused_variables)] // used only for `dyn` edges
rel: &$crate::asg::graph::object::rel::DynObjectRel<S, T>
) -> bool {
match self {
$(
Self::$kind(..) => object_rel!(@is_cross_edge $ety),
Self::$kind(..) => object_rel!(@is_cross_edge $ety rel),
)*
#[allow(unreachable_patterns)] // for empty Rel types
@ -126,8 +130,21 @@ macro_rules! object_rel {
)*
}};
(@is_cross_edge cross) => { true };
(@is_cross_edge tree) => { false };
// Static edge types.
(@is_cross_edge cross $_:ident) => { true };
(@is_cross_edge tree $_:ident) => { false };
// Dynamic edge type.
//
// We consider an edge to be a cross edge iff it contains a context
// span.
// If it were _not_ a cross edge,
// then the edge would represent ownership,
// and the span information on the target would be sufficient context
// on its own;
// an edge only needs supplemental span information if there is
// another context in which that object is referenced.
(@is_cross_edge dyn $rel:ident) => { $rel.ctx_span().is_some() };
}
/// A dynamic relationship (edge) from one object to another before it has
@ -248,7 +265,7 @@ impl<S> DynObjectRel<S, ObjectIndex<Object>> {
$(
ObjectRelTy::$ty => {
self.narrow_target::<$ty>().is_some_and(
|rel| rel.is_cross_edge()
|rel| rel.is_cross_edge(self)
)
},
)*
@ -509,5 +526,38 @@ pub trait ObjectRel<OA: ObjectKind + ObjectRelatable>: Sized {
/// then do _not_ assume that it is a cross edge without further
/// analysis,
/// which may require introducing more context to this method.
fn is_cross_edge(&self) -> bool;
///
/// Dynamic Cross Edges
/// ==================
/// Sometimes a cross edge cannot be determined statically due to
/// ontological compromises.
/// For example,
/// a [`Tpl`]->[`Ident`] edge could be a reference to another tree,
/// as in the case of template application,
/// or the template could be serving as a container for that
/// definition.
/// The former case is a cross edge,
/// but the ladder is not.
///
/// We could introduce a layer of indirection on the graph to
/// disambiguate,
/// but that adds complexity to the graph that many different
/// subsystems need to concern themselves with.
/// But cross edge determination is fairly isolated,
/// needed only by traversals.
///
/// Therefore,
/// this method also receives a [`DynObjectRel`] reference,
/// which contains edge information.
/// The edge can be augmented with data that helps to determine,
/// at runtime,
/// whether that particular edge is a cross edge.
///
/// The use of [`DynObjectRel`] is heavy and effectively makes this
/// method useless for callers that do not deal directly with raw
/// [`Asg`] data;
/// it may be useful to refine this further in the future to correct
/// that,
/// once all use cases are clear.
fn is_cross_edge<S, T>(&self, rel: &DynObjectRel<S, T>) -> bool;
}

View File

@ -25,7 +25,12 @@ use super::{
Expr, Ident, Meta, Object, ObjectIndex, ObjectRel, ObjectRelFrom,
ObjectRelTo, ObjectRelTy, ObjectRelatable,
};
use crate::{asg::Asg, f::Functor, parse::util::SPair, span::Span};
use crate::{
asg::Asg,
f::Functor,
parse::{util::SPair, Token},
span::Span,
};
/// Template with associated name.
#[derive(Debug, PartialEq, Eq)]
@ -61,9 +66,10 @@ object_rel! {
/// Templates may expand into nearly any context,
/// and must therefore be able to contain just about anything.
Tpl -> {
tree Ident,
tree Expr,
tree Meta,
dyn Ident,
}
}
@ -87,8 +93,7 @@ impl ObjectIndex<Tpl> {
/// re-binding metavariables to the context of `self`.
pub fn apply_named_tpl(self, asg: &mut Asg, id: SPair) -> Self {
let oi_apply = asg.lookup_or_missing(id);
// TODO: span
self.add_edge_to(asg, oi_apply, None)
self.add_edge_to(asg, oi_apply, Some(id.span()))
}
/// Directly reference this template from another object

View File

@ -36,13 +36,27 @@ use Air::*;
use ObjectRelTy::*;
const SU: Span = UNKNOWN_SPAN;
fn asg_from_toks<I: IntoIterator<Item = Air>>(toks: I) -> Asg
fn tree_reconstruction_report<I: IntoIterator<Item = Air>>(
toks: I,
) -> Vec<(DynObjectRel<Span, Span>, Depth)>
where
I::IntoIter: Debug,
{
let mut parser = AirAggregate::parse(toks.into_iter());
assert!(parser.all(|x| x.is_ok()));
parser.finalize().unwrap().into_context()
let asg = &parser.finalize().unwrap().into_context();
tree_reconstruction(asg)
.map(|TreeWalkRel(rel, depth)| {
(
rel.map(|(soi, toi)| {
(soi.resolve(asg).span(), toi.resolve(asg).span())
}),
depth,
)
})
.collect()
}
// Note that this is an integration test beginning at AIR.
@ -79,13 +93,6 @@ fn traverses_ontological_tree() {
PkgEnd(S11),
];
let asg = asg_from_toks(toks);
// From the above graph,
// we're going to traverse in such a way as to reconstruct the source
// tree.
let sut = tree_reconstruction(&asg);
// We need more concise expressions for the below table of values.
let d = DynObjectRel::new;
let m = |a: Span, b: Span| a.merge(b).unwrap();
@ -107,13 +114,7 @@ fn traverses_ontological_tree() {
(d(Pkg, Ident, m(S1, S11), S9, None ), Depth(2)),
(d(Ident, Expr, S9, m(S8, S10), None ), Depth(3)),
],
sut.map(|TreeWalkRel(rel, depth)| (
rel.map(|(soi, toi)| (
soi.resolve(&asg).span(),
toi.resolve(&asg).span()
)),
depth
)).collect::<Vec<_>>(),
tree_reconstruction_report(toks),
);
}
@ -156,9 +157,6 @@ fn traverses_ontological_tree_tpl_with_sibling_at_increasing_depth() {
PkgEnd(S10),
];
let asg = asg_from_toks(toks);
let sut = tree_reconstruction(&asg);
// We need more concise expressions for the below table of values.
let d = DynObjectRel::new;
let m = |a: Span, b: Span| a.merge(b).unwrap();
@ -184,12 +182,55 @@ fn traverses_ontological_tree_tpl_with_sibling_at_increasing_depth() {
(d(Tpl, Ident, m(S2, S9), S7, None), Depth(4)), // |
(d(Ident, Expr, S7, m(S6, S8), None), Depth(5)), // <'
],
sut.map(|TreeWalkRel(rel, depth)| (
rel.map(|(soi, toi)| (
soi.resolve(&asg).span(),
toi.resolve(&asg).span()
)),
depth
)).collect::<Vec<_>>(),
tree_reconstruction_report(toks),
);
}
// The way template applications are handled on the ASG differs from NIR.
// This test ensure that the representation on the ASG is precise;
// it's far easier to catch those problems here than it is to catch them
// in an implementation utilizing these data that's failing in some
// unexpected way.
#[test]
fn traverses_ontological_tree_tpl_apply() {
let name_tpl = "_tpl-to-apply_".into();
let id_tpl = SPair(name_tpl, S3);
let ref_tpl = SPair(name_tpl, S6);
#[rustfmt::skip]
let toks = vec![
PkgStart(S1),
// The template that will be applied.
TplStart(S2),
BindIdent(id_tpl),
// This test is light for now,
// until we develop the ASG further.
TplEnd(S4),
// Apply the above template.
// This seems like an odd construction until we introduce
// metavariables.
TplStart(S5),
RefIdent(ref_tpl),
TplEndRef(S7), // notice the `Ref` at the end
PkgEnd(S8),
];
// We need more concise expressions for the below table of values.
let d = DynObjectRel::new;
let m = |a: Span, b: Span| a.merge(b).unwrap();
#[rustfmt::skip]
assert_eq!(
// A -|-> B | A span -|-> B span | espan | depth
vec![//-----|-------|-----------|-----------|--------|-----------------
(d(Root, Pkg, SU, m(S1, S8), None ), Depth(1)),
(d(Pkg, Ident, m(S1, S8), S3, None ), Depth(2)),
(d(Ident, Tpl, S3, m(S2, S4), None ), Depth(3)),
(d(Pkg, Tpl, m(S1, S8), m(S5, S7), None ), Depth(2)),
(d(Tpl, Ident, m(S5, S7), S3, Some(S6)), Depth(3)),
],
tree_reconstruction_report(toks),
);
}