tame/tamer/src/asg/air/test/scope.rs

545 lines
21 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// Scope tests for ASG IR
//
// 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/>.
//! Scoping tests.
//!
//! These tests verify that identifiers are scoped to the proper
//! environments.
//! These may duplicate portions of other tests,
//! but having concrete examples all in one place helps to develop, debug,
//! and understand a system that can be quite confusing in the abstract.
//!
//! These tests _do not_ assert that identifiers are properly assigned to
//! their corresponding definitions;
//! those tests exist elsewhere.
//!
//! The core abstraction for these tests is [`assert_scope`],
//! which allows for declarative assertions of identifier scope against
//! the graph;
//! it is key to creating tests that can be both easily created and
//! easily understood.
//!
//! If You Are Here Due To A Test Failure
//! =====================================
//! If there are failing tests in parent or sibling modules,
//! check those first;
//! these tests are potentially fragile given that they test only a
//! subset of behavior without first asserting against other system
//! invariants,
//! as described above.
use super::*;
use crate::{
asg::{
graph::object::{self, ObjectTy},
visit::{tree_reconstruction, TreeWalkRel},
ExprOp,
},
span::UNKNOWN_SPAN,
};
use std::iter::once;
use EnvScopeKind::*;
const S0: Span = UNKNOWN_SPAN;
fn m(a: Span, b: Span) -> Span {
a.merge(b).unwrap()
}
/// Assert that the scope of the identifier named `name` is that of the
/// provided environment list `expected`.
///
/// The inner value of `$kind` is the span of the identifier that is
/// expected to have been indexed at that environment.
/// This distinction comes into play for local identifiers that may have
/// overlapping shadows.
macro_rules! test_scopes {
(
setup { $($setup:tt)* }
air $toks:block
#[test]
$name:ident == [
$( ($obj:ident, $span:expr, $kind:expr), )*
];
$( $rest:tt )*
) => {
#[test]
fn $name() {
$($setup)*
let ctx = air_ctx_from_toks($toks);
let given = derive_scopes_from_asg(&ctx, $name);
let expected = [
$( (ObjectTy::$obj, $span, $kind), )*
];
// Collection allows us to see the entire expected and given
// lists on assertion failure.
// Asserting within the macro itself ensures that panics
// will reference the test function rather than a utility
// function.
assert_eq!(
given.collect::<Vec<_>>(),
expected.into_iter().collect::<Vec<_>>(),
"left: given, right: expected",
);
}
test_scopes! {
setup { $($setup)* }
air $toks
$($rest)*
}
};
(
setup { $($setup:tt)* }
air $toks:block
) => {}
}
test_scopes! {
setup {
let pkg_name = SPair("/pkg".into(), S1);
let outer = SPair("outer".into(), S3);
let inner = SPair("inner".into(), S5);
}
air {
[
// ENV: 0 global lexical scoping boundaries (envs)
PkgStart(S1, pkg_name), //- -.
// ENV: 1 pkg // :
ExprStart(ExprOp::Sum, S2), // :
// ENV: 1 pkg // :
BindIdent(outer), // v :v
// :
ExprStart(ExprOp::Sum, S4), // 1: 0
// ENV: 1 pkg // :
BindIdent(inner), // v :v
ExprEnd(S6), // :
ExprEnd(S7), // :
PkgEnd(S8), //- -'
]
}
#[test]
outer == [
// The identifier is not local,
// and so its scope should extend into the global environment.
(Root, S0, Visible(S3)),
// Expr does not introduce a new environment,
// and so the innermost environment in which we should be able to
// find the identifier is the Pkg.
(Pkg, m(S1, S8), Visible(S3)),
];
#[test]
inner == [
// Same as above since the environment is the same;
// `Expr` does not introduce a new environment.
(Root, S0, Visible(S5)),
(Pkg, m(S1, S8), Visible(S5)),
];
}
test_scopes! {
setup {
let pkg_name = SPair("/pkg".into(), S1);
let tpl_outer = SPair("_tpl-outer_".into(), S3);
let meta_outer = SPair("@param_outer@".into(), S5);
let expr_outer = SPair("exprOuter".into(), S8);
let tpl_inner = SPair("_tpl-inner_".into(), S11);
let meta_inner = SPair("@param_inner@".into(), S13);
let expr_inner = SPair("exprInner".into(), S16);
}
air {
[
// ENV: 0 global lexical scoping boundaries (envs)
PkgStart(S1, pkg_name), //- - - - -.
// ENV: 1 pkg // :
TplStart(S2), //-----. :
// ENV: 2 tpl // | :
BindIdent(tpl_outer), // |v :v
// | :
MetaStart(S4), // | :
BindIdent(meta_outer), // vl|s :
MetaEnd(S6), // | :
// | :
ExprStart(ExprOp::Sum, S7), // | :
BindIdent(expr_outer), // vd|s :
ExprEnd(S9), // | :
// | :
TplStart(S10), //---. | :
// ENV: 3 tpl // | | :
BindIdent(tpl_inner), // |v |s :
// | | :
MetaStart(S12), // | | :
BindIdent(meta_inner), // vl|s |s :
MetaEnd(S14), // | | :
// 3| 2| 1: 0
ExprStart(ExprOp::Sum, S15), // | | :
BindIdent(expr_inner), // vd|s |s :
ExprEnd(S17), // | | :
TplEnd(S18), //---' | : v,s = EnvScopeKind
TplEnd(S19), //-----' : |
PkgEnd(S20), //- - - - -' |`- l = local
] // ^ `- d = defer
// observe: - (l)ocal shadows until root
// - (d)efer shadows until root
// - visual >|> shadow
// - visual >:> visual (pool)
// - shadow >|> shadow
// - shadow >:> (no visual/pool)
}
#[test]
tpl_outer == [
// The template is defined at the package level,
// and so is incorporated into the global environment.
(Root, S0, Visible(S3)),
// Definition environment.
(Pkg, m(S1, S20), Visible(S3)),
];
#[test]
meta_outer == [
// The metavariable is local to the template,
// and so is not scoped outside of it.
// It does not contribute to the global scope,
// however we must introduce shadow records so that we're able to
// provide an error if shadowing would occur due to another
// identifier of the same name,
// such as a template within another template.
// Root never contains shadow records since it is not part of a
// hierarchy,
// so it is omitted from the metavariable's scope.
(Pkg, m(S1, S20), Shadow (S5)),
(Tpl, m(S2, S19), Visible(S5)),
];
#[test]
expr_outer == [
// Expressions defined within templates will eventually be scoped to
// their _expansion site_.
// Since the future scope cannot possibly be known until the point
// of expansion,
// we don't know what its parent environment will be.
//
// Why, then, does it shadow?
//
// Templates in TAMER
// (unlike in the original XSLT-based TAME)
// are designed to _close_ over their definition environment.
// If a template references a value defined within the scope of its
// definition
// (e.g. an identifier imported into the package into which the
// template itself was defined),
// the intent is to be able to utilize that identifier at the
// expansion site without having to break encapsulation by
// having to know implementation details of the template;
// this awkward problem is the reason for `import/@export`,
// so that packages templates could re-export their symbols
// to avoid this trap,
// which is far too heavy-handed of an approach and is
// easily forgotten.
// In that sense,
// templates act more like how one would expect functions to
// operate.
//
// Because of that lexical capture,
// it is important that identifiers shadow to ensure that we do
// not rebind an identifier without the user realizing it.
// The intent is that the system should just do the right thing
// unless there happens to be a problem.
// If a user references an identifier from the outer scope,
// the intent is almost certainly to have it be lexically captured
// and available at the expansion site.
// If an identifier is unknown,
// perhaps the intent is to have it defined by another template,
// or to be defined at the expansion site.
// And if the situation changes from the second to the first because
// of the introduction of an import or a duplicate identifier,
// we want to help the user at the earliest possible moment.
(Pkg, m(S1, S20), Shadow (S8)),
(Tpl, m(S2, S19), Visible(S8)),
];
#[test]
tpl_inner == [
// This is similar to `expr_outer` above.
// Even though the template is entirely scoped within the parent
// `tpl_outer` such that it isn't even defined until it is expanded,
// at which point it is defined within its expansion context,
// we still want shadow records so that any _references_ to this
// template can be resolved unambiguously in ways that are
// helpful to the user
// (see `expr_outer` above for more information).
(Pkg, m(S1, S20), Shadow (S11)),
(Tpl, m(S2, S19), Visible(S11)),
];
#[test]
meta_inner == [
// Just as the previous metavariable,
// we need to cast a shadow all the way up to the package level to
// ensure that we do not permit identifier shadowing.
// See `meta_outer` above for more information.
(Pkg, m(S1, S20), Shadow (S13)),
(Tpl, m(S2, S19), Shadow (S13)),
(Tpl, m(S10, S18), Visible(S13)),
];
#[test]
expr_inner == [
// Just the same as the previous expression.
// Note the intended consequence of this:
// if `tpl_outer` contains an identifier,
// it cannot be shadowed by `tpl_inner`.
(Pkg, m(S1, S20), Shadow (S16)),
(Tpl, m(S2, S19), Shadow (S16)),
(Tpl, m(S10, S18), Visible(S16)),
];
}
test_scopes! {
setup {
let pkg_name = SPair("/pkg".into(), S1);
let tpl_outer = SPair("_tpl-outer_".into(), S3);
let tpl_inner = SPair("_tpl-inner_".into(), S9);
// Note how these have the _same name_.
let meta_name = "@param@".into();
let meta_same_a = SPair(meta_name, S5);
let meta_same_b = SPair(meta_name, S11);
// This one will be used for asserting.
let meta_same = SPair(meta_name, S11);
}
air {
// Note that,
// unlike the above set of tests,
// these templates are _siblings_.
[
// ENV: 0 global lexical scoping boundaries (envs)
PkgStart(S1, pkg_name), //- - - -.
// ENV: 1 pkg // :
TplStart(S2), //----. :
// ENV: 2 tpl // | :
BindIdent(tpl_outer), // |~ :
// | :
MetaStart(S4), // | :
BindIdent(meta_same_a), // vl|s : <--.
MetaEnd(S6), // | : |
TplEnd(S7), //----' : |
// : |s
TplStart(S8), //----. : |a
// ENV: 3 tpl // | : |m
BindIdent(tpl_inner), // |~ : |e
// | : |
MetaStart(S10), // | : |
BindIdent(meta_same_b), // vl|s : <--'
MetaEnd(S12), // | :
TplEnd(S13), //----' : ~ = ignored for
PkgEnd(S14), //- - - -' these tests
]
}
// Detailed information on metavariables is present in previous tests.
// We focus here only on the fact that these definitions were permitted
// to occur since identifiers of the same name have overlapping
// shadows.
// Keep in mind that this test is a filtering of an ontological tree,
// so this is ordered as such and does not contain duplicate objects.
#[test]
meta_same == [
// A shadow is cast by both `meta_same_a` and `meta_same_b`.
// When they intersect,
// we must make a choice:
//
// (a) Index both of them; or
// (b) Keep only one of them.
//
// At the time of writing,
// the choice was (b).
// But by keeping only one identifier indexed,
// we do lose information and therefore will only be able to
// present one diagnostic error in the event that we later
// discover that the metavariables shadow another identifier
// that is visible in scope.
// This would present only one error at a time to the user,
// depending on how they chose to resolve it,
// and so is not ideal;
// the solution may eventually move to (a) to retain this
// important information.
//
// Since (b) was chosen,
// which do we keep?
// This choice is not important,
// since in the event of a future error we'll still be providing
// correct
// (albeit incomplete)
// information to the user,
// but it is defined by implementation details,
// and so is subject to change.
// At the time of writing,
// because of how indexing is carried out,
// we retain the shadow of the _first_ encountered identifier.
(Pkg, m(S1, S14), Shadow (S5 )),
// The first identifier of this name is found in the first
// template.
// Its shadow is discussed above.
(Tpl, m(S2, S7), Visible(S5 )),
// And the second identifier in the second template,
// with its shadow also discussed above.
// As noted atop this test,
// we do not have duplicate objects in this test data,
// and so the `Pkg` object above is not duplicated despite two
// shadows being cast.
(Tpl, m(S8, S13), Visible(S11)),
];
}
// From the perspective of the linker (tameld):
test_scopes! {
setup {
let pkg_a = SPair("/pkg/a".into(), S1);
let opaque_a = SPair("opaque_a".into(), S2);
let pkg_b = SPair("/pkg/b".into(), S4);
let opaque_b = SPair("opaque_b".into(), S5);
}
air {
[
// ENV: 0 global lexical scoping boundaries (envs)
PkgStart(S1, pkg_a), //- -.
// ENV: 1 pkg // :
IdentDecl( // v :v
opaque_a, // :
IdentKind::Meta, // :
Default::default(), // :
), // 1:
PkgEnd(S3), //- -'
// 0
PkgStart(S4, pkg_b), //- -.
// ENV: 1 pkg // 1:
IdentDecl( // v :v
opaque_b, // :
IdentKind::Meta, // :
Default::default(), // :
), // :
PkgEnd(S6), //- -'
]
}
#[test]
opaque_a == [
(Root, S0, Visible(S2)),
(Pkg, m(S1, S3), Visible(S2)),
];
#[test]
opaque_b == [
(Root, S0, Visible(S5)),
(Pkg, m(S4, S6), Visible(S5)),
];
}
///// Tests end above this line, plumbing below /////
/// Independently derive identifier scopes from the graph.
///
/// This will search the graph for all environments in which `name` has been
/// indexed,
/// gather information about those environments,
/// and compare them against `expected`.
/// The environment listing is expected to be in ontological order,
/// as by [`tree_reconstruction`].
///
/// This function is essential to providing easily understood,
/// declarative scope test definitions,
/// which make it easy to form and prove hypotheses about the behavior of
/// TAMER's scoping system.
fn derive_scopes_from_asg<'a>(
ctx: &'a <AirAggregate as ParseState>::Context,
name: SPair,
) -> impl Iterator<Item = (ObjectTy, Span, EnvScopeKind<Span>)> + 'a {
// We use what was most convenient at the time of writing to gather
// environments representing the scope of `name`.
// This is not the most efficient,
// but our test graphs are quite small,
// and so that won't matter.
//
// The reason that this works is because the traversal will visit
// objects following the graph's ontology,
// which will produce a tree in the expected order.
// We filter on index lookup,
// which discards the portions of the tree
// (the graph)
// that we are not interested in.
//
// This also means that a failure to extend the scope of `name` to a
// particular environment will cause it to be omitted from this
// iterator,
// but that is okay;
// it'll result in a test failure that should be easy enough to
// understand.
let given_without_root = tree_reconstruction(ctx.asg_ref()).filter_map(
move |TreeWalkRel(dynrel, _)| {
dynrel.target_oi_rel_to_dyn::<object::Ident>().map(|oi_to| {
(
dynrel.target_ty(),
dynrel.target().resolve(ctx.asg_ref()).span(),
ctx.env_scope_lookup_raw(oi_to, name),
)
})
},
);
// `tree_reconstruction` omits root,
// so we'll have to add it ourselves.
let oi_root = ctx.asg_ref().root(name);
once((ObjectTy::Root, S0, ctx.env_scope_lookup_raw(oi_root, name)))
.chain(given_without_root)
.filter_map(|(ty, span, oeoi)| {
oeoi.map(|eoi| {
(ty, span, eoi.map(ObjectIndex::cresolve(ctx.asg_ref())))
})
})
// We discard the inner ObjectIndex since it is not relevant for the
// test assertion.
.map(|(ty, span, eid)| (ty, span, eid.map(|id| id.span())))
}