// 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 . use super::*; use crate::{ asg::{ air::{Air, AirAggregate}, graph::object::{self, ObjectTy, Pkg}, ExprOp, }, parse::{util::SPair, ParseState}, span::{dummy::*, Span, UNKNOWN_SPAN}, }; use std::fmt::Debug; use Air::*; fn topo_report_only( asg: &Asg, edges: impl Iterator>, ) -> Vec> { topo_sort(asg, edges) .map(|result| { result .map(|oi| oi.resolve(asg)) .map(|obj| (obj.ty(), obj.span())) }) .collect() } fn topo_report>( toks: I, ) -> Vec> where I::IntoIter: Debug, { let mut parser = AirAggregate::parse(toks.into_iter()); assert!(parser.all(|x| x.is_ok())); let asg = &parser.finalize().unwrap().into_context(); let oi_root = asg.root(UNKNOWN_SPAN); topo_report_only( asg, oi_root.edges_filtered::(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), // 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(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), 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(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), 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(); let oi_pkg = asg .root(UNKNOWN_SPAN) .edges_filtered::(&asg) .next() .expect("cannot find Pkg on graph"); let oi_b = asg .lookup::(oi_pkg, 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 id_a = SPair("expr_a".into(), S3); let id_b = SPair("expr_b".into(), S8); #[rustfmt::skip] let toks = vec![ // First root PkgStart(S1), ExprStart(ExprOp::Sum, S2), BindIdent(id_a), ExprEnd(S4), PkgEnd(S5), // Second root, // independent of the first. PkgStart(S6), ExprStart(ExprOp::Sum, S7), BindIdent(id_b), ExprEnd(S9), PkgEnd(S10), ]; use ObjectTy::*; let m = |a: Span, b: Span| a.merge(b).unwrap(); #[rustfmt::skip] assert_eq!( Ok(vec![ // First root. (Expr, m(S2, S4) ), (Ident, S3), (Pkg, m(S1, S5) ), // Second root, // but the fact that it is emitted after the first is not // behavior that should be relied upon. (Expr, m(S7, S9) ), (Ident, S8), (Pkg, m(S6, S10)), ]), topo_report(toks).into_iter().collect(), ); }