tame/tamer/src/asg/graph/visit/topo/test.rs

466 lines
16 KiB
Rust

// Test topological sort ASG traversal
//
// 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/>.
use super::*;
use crate::{
asg::{
air::{Air, AirAggregate},
graph::object::{
self, ObjectKind, ObjectRelFrom, ObjectRelatable, ObjectTy, Root,
},
ExprOp, IdentKind,
},
num::{Dim, Dtype},
parse::{util::SPair, ParseState},
span::{dummy::*, Span, UNKNOWN_SPAN},
};
use std::fmt::Debug;
use Air::*;
fn topo_report_only(
asg: &Asg,
edges: impl Iterator<Item = ObjectIndex<Object>>,
) -> Vec<Result<(ObjectTy, Span), Vec<(ObjectTy, Span)>>> {
topo_sort(asg, edges)
.map(|result| {
result
.map(|oi| oi.resolve(asg))
.map(|obj| (obj.ty(), obj.span()))
.map_err(|cycle| {
cycle
.path_rev()
.iter()
// Retain the resolved span from Cycle so that our
// assertions verify that it is being resolved
// correctly.
.map(|oirs| (oirs.oi().resolve(asg).ty(), oirs.span()))
.collect()
})
})
.collect()
}
fn topo_report<O: ObjectKind + ObjectRelatable, I: IntoIterator<Item = Air>>(
toks: I,
) -> Vec<Result<(ObjectTy, Span), Vec<(ObjectTy, Span)>>>
where
I::IntoIter: Debug,
O: ObjectRelFrom<Root>,
{
let mut parser = AirAggregate::parse(toks.into_iter());
assert!(parser.all(|x| x.is_ok()));
let asg = &parser.finalize().unwrap().into_context().finish();
let oi_root = asg.root(UNKNOWN_SPAN);
topo_report_only(
asg,
oi_root.edges_filtered::<O>(asg).map(ObjectIndex::widen),
)
}
#[test]
fn sorts_objects_given_single_root() {
let id_a = SPair("expr_a".into(), S3);
let id_b = SPair("expr_b".into(), S9);
let id_c = SPair("expr_c".into(), S12);
#[rustfmt::skip]
let toks = vec![
// Packages are auto-rooted as part of the graph's ontology.
// There is only one for this test.
PkgStart(S1, SPair("/pkg".into(), S1)),
// Before this can be computed,
// its dependencies must be.
ExprStart(ExprOp::Sum, S2), // -.
BindIdent(id_a), // |
// |
// This is a dependency, // |
// but it is owned by this Expr // |
// and so would have been emitted // |
// first anyway. // |
ExprStart(ExprOp::Sum, S4), // |
ExprEnd(S5), // |
// v
// But this is a reference to another
// Expr that appears later.
RefIdent(SPair(id_b.symbol(), S6)), // --.
ExprEnd(S7), // |
// |
// This will have to be emitted // |
// _before_ the above Expr that // |
// depends on its value having been // |
// computed. // /
ExprStart(ExprOp::Sum, S8), // <`
BindIdent(id_b),
ExprEnd(S10),
// A sibling expression with no dependency on
// other expressions.
ExprStart(ExprOp::Sum, S11),
BindIdent(id_c),
ExprEnd(S13),
PkgEnd(S14),
];
use ObjectTy::*;
let m = |a: Span, b: Span| a.merge(b).unwrap();
#[rustfmt::skip]
assert_eq!(
Ok(vec![
// The first leaf is this anonymous child expression,
// which has no dependencies.
(Expr, m(S4, S5) ), // child of id_a
// The sibling of the above expression is a reference to the
// value of `id_b`.
// `id_a` cannot be computed before it.
(Expr, m(S8, S10) ), // id_b
(Ident, S9, ), // id_b
// With `id_b` emitted,
// `id_a` has no more dependencies,
// and so itself can be emitted.
(Expr, m(S2, S7) ), // id_a
(Ident, S3, ), // id_a
// `id_a` has a sibling `id_c`.
// Its ordering is undefined relative to `id_a`
// (it could also be ordered before it),
// but the implementation of the traversal causes it to be
// output in the same order as it appeared in the source
// token stream.
(Expr, m(S11, S13)), // id_c
(Ident, S12 ), // id_c
// We end with the root that was explicitly provided to
// `topo_sort` via `topo_report`.
(Pkg, m(S1, S14) ),
]),
topo_report::<object::Pkg, _>(toks).into_iter().collect(),
);
}
// Like the above test,
// but the path is deeper to emphasize that the topological sort applies
// recursively to dependencies.
// Multiple expressions depending on the same dependency have an arbitrary
// order that is deterministic between runs.
#[test]
fn sorts_objects_given_single_root_more_complex() {
let id_a = SPair("expr_a".into(), S3);
let id_b = SPair("expr_b".into(), S7);
let id_c = SPair("expr_c".into(), S11);
let id_d = SPair("expr_d".into(), S15);
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, SPair("/pkg".into(), S1)),
ExprStart(ExprOp::Sum, S2),
BindIdent(id_a),
RefIdent(SPair(id_b.symbol(), S4)), // ---.
ExprEnd(S5), // )
// /
ExprStart(ExprOp::Sum, S6), // /
BindIdent(id_b), // <'
RefIdent(SPair(id_d.symbol(), S8)), // -------.
ExprEnd(S9), // <. |
// \ |
ExprStart(ExprOp::Sum, S10), // \ |
BindIdent(id_c), // ) |
RefIdent(SPair(id_b.symbol(), S12)), // ---' /
ExprEnd(S13), // /
// /
ExprStart(ExprOp::Sum, S14), // /
BindIdent(id_d), // <--'
ExprEnd(S16),
PkgEnd(S17),
];
use ObjectTy::*;
let m = |a: Span, b: Span| a.merge(b).unwrap();
#[rustfmt::skip]
assert_eq!(
Ok(vec![
(Expr, m(S14, S16)), // id_d
(Ident, S15 ), // id_d
(Expr, m(S6, S9) ), // id_b
(Ident, S7, ), // id_b
(Expr, m(S2, S5) ), // id_a
(Ident, S3, ), // id_a
(Expr, m(S10, S13)), // id_c
(Ident, S11 ), // id_c
(Pkg, m(S1, S17) ),
]),
topo_report::<object::Pkg, _>(toks).into_iter().collect(),
);
}
// This tests what the linker (tameld) does:
// topologically sorts explicitly rooted objects and ignores everything
// else.
// This also gives us dead code elimination.
#[test]
fn omits_unreachable() {
let id_a = SPair("expr_a".into(), S3);
let id_b = SPair("expr_b".into(), S7);
let id_c = SPair("expr_c".into(), S11);
let id_d = SPair("expr_d".into(), S15);
// We will only use a portion of this graph.
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, SPair("/pkg".into(), S1)),
ExprStart(ExprOp::Sum, S2),
BindIdent(id_a),
RefIdent(SPair(id_b.symbol(), S4)), // ---.
ExprEnd(S5), // )
// /
ExprStart(ExprOp::Sum, S6), // /
BindIdent(id_b), // <'
RefIdent(SPair(id_d.symbol(), S8)), // -------.
ExprEnd(S9), // <. |
// \ |
ExprStart(ExprOp::Sum, S10), // \ |
BindIdent(id_c), // ) |
RefIdent(SPair(id_b.symbol(), S12)), // ---' /
ExprEnd(S13), // /
// /
ExprStart(ExprOp::Sum, S14), // /
BindIdent(id_d), // <--'
ExprEnd(S16),
PkgEnd(S17),
];
use ObjectTy::*;
let m = |a: Span, b: Span| a.merge(b).unwrap();
let mut parser = AirAggregate::parse(toks.into_iter());
assert!(parser.all(|x| x.is_ok()));
let asg = &parser.finalize().unwrap().into_context().finish();
let oi_pkg = asg
.root(UNKNOWN_SPAN)
.edges_filtered::<object::Pkg>(&asg)
.next()
.expect("cannot find Pkg on graph");
let oi_b = oi_pkg.lookup_local_linear(asg, id_b).expect("missing oi_b");
// We'll use only `oi_b` as the root,
// which will include it and its (only) dependency.
// The rest of the graph must be ignored.
let report = topo_report_only(&asg, [oi_b.widen()].into_iter());
#[rustfmt::skip]
assert_eq!(
Ok(vec![
(Expr, m(S14, S16)), // id_d
(Ident, S15 ), // id_d
(Expr, m(S6, S9) ), // id_b
(Ident, S7, ), // id_b
]),
report.into_iter().collect(),
);
}
// If multiple roots are given,
// and they have entirely independent subgraphs,
// then their ordering is deterministic between runs of the same graph,
// but undefined.
//
// This is no different than the ordering of siblings above;
// this simply provides an explicit example for the behavior of provided
// roots since that is the entry point for this API.
#[test]
fn sorts_objects_given_multiple_roots() {
let pkg_a_name = SPair("/pkg/a".into(), S2);
let pkg_b_name = SPair("/pkg/b".into(), S8);
let id_a = SPair("expr_a".into(), S4);
let id_b = SPair("expr_b".into(), S10);
#[rustfmt::skip]
let toks = vec![
// First root
PkgStart(S1, pkg_a_name),
ExprStart(ExprOp::Sum, S3),
BindIdent(id_a),
ExprEnd(S5),
PkgEnd(S6),
// Second root,
// independent of the first.
PkgStart(S7, pkg_b_name),
ExprStart(ExprOp::Sum, S9),
BindIdent(id_b),
ExprEnd(S11),
PkgEnd(S12),
];
use ObjectTy::*;
let m = |a: Span, b: Span| a.merge(b).unwrap();
#[rustfmt::skip]
assert_eq!(
Ok(vec![
// First root.
(Expr, m(S3, S5) ),
(Ident, S4),
(Pkg, m(S1, S6) ),
// Second root,
// but the fact that it is emitted after the first is not
// behavior that should be relied upon.
(Expr, m(S9, S11) ),
(Ident, S10),
(Pkg, m(S7, S12)),
]),
topo_report::<object::Pkg, _>(toks).into_iter().collect(),
);
}
// Most cycles are unsupported by TAME.
// Recovery allows compilation/linking to continue so that additional errors
// can be discovered and reported.
#[test]
fn unsupported_cycles_with_recovery() {
let id_a = SPair("expr_a".into(), S3);
let id_b = SPair("expr_b".into(), S8);
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, SPair("/pkg-a".into(), S1)),
ExprStart(ExprOp::Sum, S2),
BindIdent(id_a), // <----. self-cycle
RefIdent(SPair(id_a.symbol(), S4)), // ____/ \
RefIdent(SPair(id_b.symbol(), S5)), // ---. \ a->b->a
ExprEnd(S6), // ) ) cycle
// / /
ExprStart(ExprOp::Sum, S7), // / /
BindIdent(id_b), // <' /
RefIdent(SPair(id_a.symbol(), S9)), // ----'
ExprEnd(S10),
PkgEnd(S11),
];
use ObjectTy::*;
let m = |a: Span, b: Span| a.merge(b).unwrap();
assert_eq!(
#[rustfmt::skip]
vec![
// Pkg -> Ident (id_a) -> Ref (id_a) gives us a self-cycle.
Err(vec![
(Expr, m(S2, S6)), // -.
(Ident, S3 ), // <' id_a
]),
// RECOVERY: We do not traverse into the cycle and continue as
// if the edge causing the cycle was not taken.
// ...which unfortunately lands us on another cycle caused by
// a->b->a before we can emit the parent Expr.
// TODO: In the future we ought to represent the reference here
// as well.
Err(vec![
(Expr, m(S7, S10)), // -.
(Ident, S8 ), // | id_b
(Expr, m(S2, S6)), // |
(Ident, S3 ), // <' id_a
]),
// RECOVERY: We ignore the edge leading to the cycle,
// which means that id_b Expr has no more dependencies.
Ok((Expr, m(S7, S10))),
Ok((Ident, S8 )),
// And id_a is now also complete,
// since the cycle was the last dependency.
Ok((Expr, m(S2, S6 ))),
Ok((Ident, S3 )),
Ok((Pkg, m(S1, S11))),
],
topo_report::<object::Pkg, _>(toks)
.into_iter()
.collect::<Vec<_>>(),
);
}
// TAME supports cycles in certain contexts,
// as a component of the graph's ontology.
// A topological sort of a graph containing permitted cycles should be
// viewed as sorting a graph that first "cuts" those cycles,
// filtering out the edge that would have caused the cycle to occur.
// It is the responsibility of the ontology to ensure that all such cuts
// will result in a topological sort.
#[test]
fn supported_cycles() {
let id_a = SPair("func_a".into(), S3);
let id_b = SPair("func_b".into(), S8);
let kind = IdentKind::Func(Dim::Scalar, Dtype::Integer);
#[rustfmt::skip]
let toks = vec![
PkgStart(S1, SPair("/pkg-a".into(), S1)),
// Two mutually recursive functions.
IdentDecl(id_a, kind.clone(), Default::default()), // <--.
IdentDep(id_a, id_b), // -. |
// | |
IdentDecl(id_b, kind.clone(), Default::default()), // <' |
IdentDep(id_b, id_a), // ---'
// Root so that `topo_report` will find them.
IdentRoot(id_a),
IdentRoot(id_b),
PkgEnd(S11),
];
// TODO: Template recursion was not part of the ontology at the time of
// writing.
use ObjectTy::*;
assert_eq!(
#[rustfmt::skip]
Ok(vec![
// The order in which the above functions will be visited is
// undefined;
// this is the ordering that happens to be taken by the
// implementation based on the definition and stack
// ordering.
(Ident, S8),
(Ident, S3),
]),
topo_report::<object::Ident, _>(toks)
.into_iter()
.collect::<Result<Vec<_>, _>>(),
);
}