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

370 lines
12 KiB
Rust

// Tests for ASG IR metavariable parsing
//
// 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/>.
/// Metavariable parsing tests.
///
/// Note that some metavariable-related tests are in
/// [`super::super::tpl::test`],
/// where the focus is on how they are integrated as template
/// parameters.
/// The tests here focus instead on the particulars of metavariables
/// themselves,
/// with templates being only incidental.
use super::*;
use crate::{
asg::{
air::{
test::{parse_as_pkg_body, pkg_lookup},
Air::{self, *},
},
graph::object::{meta::MetaRel, Meta, Tpl},
},
parse::{util::spair, Parser},
span::{dummy::*, Span},
sym::SymbolId,
};
type Sut = AirAggregate;
// Metavariables can reference the lexical value of other metavariables.
// This does not actually check that the target reference is a metavariable,
// at least not at the time of writing.
// TODO: But the system ought to,
// since if we defer that to expansion-time,
// then we don't have confidence that the template definition is actually
// valid as a part of this compilation unit.
#[test]
fn metavar_ref_only() {
let name_meta = "@foo@";
let name_other = "@other@";
#[rustfmt::skip]
assert_concat_list(
[
MetaStart(S1),
BindIdent(spair(name_meta, S2)),
// Reference to another metavariable,
// effectively creating an alias.
RefIdent(spair(name_other, S3)), // --.
MetaEnd(S4), // |
// |
MetaStart(S5), // |
BindIdent(spair(name_other, S6)), // <-'
MetaEnd(S7),
],
name_meta,
S1.merge(S4),
[
&Meta::Required(S5.merge(S7).unwrap()),
],
);
}
// Similar to above,
// but multiple references.
#[test]
fn metavar_ref_multiple() {
let name_meta = "@foo@";
let name_other_a = "@other-a@";
let name_other_b = "@other-b@";
#[rustfmt::skip]
assert_concat_list(
// Def/ref is commutative,
// so this defines with both orderings to demonstrate that.
[
MetaStart(S1),
BindIdent(spair(name_other_a, S2)), // <-.
MetaEnd(S3), // |
// |
MetaStart(S3), // |
BindIdent(spair(name_meta, S4)), // |
// |
RefIdent(spair(name_other_a, S5)), // --'
RefIdent(spair(name_other_b, S6)), // --.
MetaEnd(S7), // |
// |
MetaStart(S8), // |
BindIdent(spair(name_other_b, S9)), // <-'
MetaEnd(S10),
],
name_meta,
S3.merge(S7),
[
&Meta::Required(S1.merge(S3).unwrap()),
&Meta::Required(S8.merge(S10).unwrap()),
],
);
}
// If a metavariable already has a concrete lexical value,
// then appending a reference to it should convert it into a list.
#[test]
fn metavar_ref_after_lexeme() {
let name_meta = "@foo@";
let value = "foo value";
let name_other = "@other@";
#[rustfmt::skip]
assert_concat_list(
[
MetaStart(S1),
BindIdent(spair(name_meta, S2)),
// At this point,
// we have only a concrete lexeme,
// and so we are not a list.
MetaLexeme(spair(value, S3)),
// Upon encountering this token,
// we should convert into a list.
RefIdent(spair(name_other, S4)), // --.
MetaEnd(S5), // |
// |
MetaStart(S6), // |
BindIdent(spair(name_other, S7)), // <-'
MetaEnd(S8),
],
name_meta,
S1.merge(S5),
// Having been converted into a list,
// we should have a reference to _two_ metavariables:
// one holding the original lexeme and the reference.
[
&Meta::Lexeme(S3, spair(value, S3)),
&Meta::Required(S6.merge(S8).unwrap()),
],
);
}
// If we have a reference,
// then that means we already have a `ConcatList`,
// and therefore should just have to append to it.
// This is the same as the above test,
// with operations reversed.
// It is not commutative,
// though,
// since concatenation is ordered.
#[test]
fn lexeme_after_metavar_ref() {
let name_meta = "@foo@";
let value = "foo value";
let name_other = "@other@";
#[rustfmt::skip]
assert_concat_list(
[
MetaStart(S1),
BindIdent(spair(name_meta, S2)),
// We produce a list here...
RefIdent(spair(name_other, S3)), // --.
// |
// ...and should just append a // |
// `Lexeme` here. // |
MetaLexeme(spair(value, S4)), // |
MetaEnd(S5), // |
// |
MetaStart(S6), // |
BindIdent(spair(name_other, S7)), // <-'
MetaEnd(S8),
],
name_meta,
S1.merge(S5),
// Same as the previous test,
// but concatenation order is reversed.
[
&Meta::Required(S6.merge(S8).unwrap()),
// Note the first span here is not that of the parent
// metavariable,
// since we do not want diagnostics to suggest that this
// object represents the entirety of the parent.
&Meta::Lexeme(S4, spair(value, S4)),
],
);
}
// Multiple lexemes should _also_ produce a list.
// While this could have been replaced by a single lexeme,
// there are still uses:
// maybe this was the result of template expansion,
// or simply a multi-line formatting choice in the provided sources.
#[test]
fn lexeme_after_lexeme() {
let name_meta = "@foo@";
let value_a = "foo value a";
let value_b = "foo value b";
#[rustfmt::skip]
assert_concat_list(
[
MetaStart(S1),
BindIdent(spair(name_meta, S2)),
MetaLexeme(spair(value_a, S3)),
MetaLexeme(spair(value_b, S4)),
MetaEnd(S5),
],
name_meta,
S1.merge(S5),
[
// Since these are Meta objects derived from the original,
// our spans
// (the first value in the tuple)
// are not that of the containing metavariable;
// we don't want diagnostics implying that this represents
// the entirety of that the parent metavariable.
&Meta::Lexeme(S3, spair(value_a, S3)),
&Meta::Lexeme(S4, spair(value_b, S4)),
],
);
}
/////// ///////
/////// Tests above; plumbing begins below ///////
/////// ///////
fn assert_concat_list<'a, IT, IE: 'a>(
toks: IT,
meta_name: impl Into<SymbolId>,
expect_span: Option<Span>,
expect: IE,
) where
IT: IntoIterator<Item = Air>,
IT::IntoIter: Debug,
IE: IntoIterator<Item = &'a Meta>,
IE::IntoIter: Debug + DoubleEndedIterator,
{
let (ctx, oi_tpl) = air_ctx_from_tpl_body_toks(toks);
let asg = ctx.asg_ref();
let oi_meta = ctx
.env_scope_lookup_ident_dfn::<Meta>(
oi_tpl,
spair(meta_name.into(), DUMMY_SPAN),
)
.unwrap();
// Meta references are only supported through lexical concatenation.
assert_eq!(
&Meta::ConcatList(expect_span.expect("expected metavar span is None")),
oi_meta.resolve(asg),
"expected metavariable to be a ConcatList with the provided span",
);
// We `collect()` rather than using `Iterator::eq` so that the failure
// message includes the data.
// If we do this too often,
// we can consider a crate like `itertools` or write our own
// comparison,
// but it's not worth doing for these tests where the cost of
// collection is so low and insignificant.
assert_eq!(
// TODO: We need to modify edge methods to return in the proper
// order (not reversed) without a performance hit,
// which will involve investigating Petgraph further.
expect.into_iter().rev().collect::<Vec<_>>(),
// Each reference is to an Ident whose definition is the other
// metavariable.
oi_meta
.edges(asg)
.filter_map(|rel| match rel {
MetaRel::Meta(oi) => Some(oi),
MetaRel::Ident(oi) => oi.definition::<Meta>(asg),
MetaRel::Doc(_) => None,
})
.map(ObjectIndex::cresolve(asg))
.collect::<Vec<_>>(),
"note: expected tokens are in reverse order in this error message",
);
}
const STUB_TPL_NAME: &str = "__incidental-tpl__";
/// Parse the provided tokens within the context of a template body.
///
/// This allows tests to focus on testing of metavariables instead of
/// mudding tests with setup.
///
/// This creates a package and template that are purely incidental and serve
/// only as scaffolding to put the parser into the necessary state and
/// context.
/// This is an alternative to providing methods to force the parse into a
/// certain context,
/// since we're acting as users of the SUT's public API and are
/// therefore testing real-world situations.
fn parse_as_tpl_body<I: IntoIterator<Item = Air>>(
toks: I,
) -> Parser<Sut, impl Iterator<Item = Air> + Debug>
where
<I as IntoIterator>::IntoIter: Debug,
{
#[rustfmt::skip]
let head = [
TplStart(S1),
BindIdent(spair(STUB_TPL_NAME, S1)),
];
#[rustfmt::skip]
let tail = [
TplEnd(S1),
];
#[rustfmt::skip]
parse_as_pkg_body(
head.into_iter()
.chain(toks.into_iter())
.chain(tail.into_iter())
)
}
fn air_ctx_from_tpl_body_toks<I: IntoIterator<Item = Air>>(
toks: I,
) -> (<Sut as ParseState>::Context, ObjectIndex<Tpl>)
where
I::IntoIter: Debug,
{
let mut sut = parse_as_tpl_body(toks);
assert!(sut.all(|x| x.is_ok()));
let ctx = sut.finalize().unwrap().into_private_context();
let oi_tpl = pkg_lookup(&ctx, spair(STUB_TPL_NAME, S1))
.expect(
"could not locate stub template (did you call \
air_ctx_from_tpl_body_toks without parse_as_tpl_body?)",
)
.definition(ctx.asg_ref())
.expect("missing stub template definition (test setup bug?)");
(ctx, oi_tpl)
}