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

1169 lines
36 KiB
Rust

// Tests for ASG IR template 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/>.
use super::*;
use crate::asg::air::test::{as_pkg_body, Sut};
use crate::span::dummy::*;
use crate::{
asg::{
air::{
expr::test::collect_subexprs,
test::{
air_ctx_from_pkg_body_toks, air_ctx_from_toks,
parse_as_pkg_body, pkg_expect_ident_obj, pkg_expect_ident_oi,
pkg_lookup,
},
Air::*,
},
graph::object::{tpl::TplShape, Doc, Meta, ObjectRel},
Expr, ExprOp, Ident,
},
parse::util::spair,
};
// A template is defined by the package containing it,
// like an expression.
#[test]
fn tpl_defining_pkg() {
let id_tpl = spair("_tpl_", S3);
#[rustfmt::skip]
let toks = [
PkgStart(S1, spair("/pkg", S1)),
// This also tests tpl as a transition away from the package header.
TplStart(S2),
BindIdent(id_tpl),
TplEnd(S4),
PkgEnd(S5),
];
let ctx = air_ctx_from_toks(toks);
let asg = ctx.asg_ref();
let tpl = pkg_expect_ident_obj::<Tpl>(&ctx, id_tpl);
assert_eq!(S2.merge(S4).unwrap(), tpl.span());
assert_eq!(TplShape::Empty, tpl.shape());
let oi_id_tpl = pkg_lookup(&ctx, id_tpl).unwrap();
assert_eq!(
S1.merge(S5),
oi_id_tpl.src_pkg(&asg).map(|pkg| pkg.resolve(&asg).span()),
);
}
#[test]
fn tpl_after_expr() {
let id_expr = spair("expr", S3);
let id_tpl = spair("_tpl_", S6);
#[rustfmt::skip]
let toks = [
PkgStart(S1, spair("/pkg", S1)),
// This expression is incidental to this test;
// it need only parse.
ExprStart(ExprOp::Sum, S2),
BindIdent(id_expr),
ExprEnd(S4),
// Open after an expression.
TplStart(S5),
BindIdent(id_tpl),
TplEnd(S7),
PkgEnd(S8),
];
let ctx = air_ctx_from_toks(toks);
let tpl = pkg_expect_ident_obj::<Tpl>(&ctx, id_tpl);
assert_eq!(S5.merge(S7).unwrap(), tpl.span());
assert_eq!(TplShape::Empty, tpl.shape());
}
// Templates within expressions are permitted by NIR at the time of writing
// (and so cannot be observed in system tests using TAME's source
// language),
// but it _is_ permitted by AIR,
// to simplify lowering, desugaring, and template expansion.
//
// This test is a rather important one,
// since it ensures that expression context is properly restored
// regardless of whether a template is encountered.
// This context includes the entire active expression stack.
#[test]
fn tpl_within_expr() {
let id_expr = spair("expr", S3);
let id_tpl = spair("_tpl_", S7);
#[rustfmt::skip]
let toks = [
PkgStart(S1, spair("/pkg", S1)),
ExprStart(ExprOp::Sum, S2),
BindIdent(id_expr),
// Child expression before the template to ensure that the
// context is properly restored after template parsing.
ExprStart(ExprOp::Sum, S4),
ExprEnd(S5),
// Template _within_ an expression.
// This will not be present in the final expression,
// as if it were hoisted out.
TplStart(S6),
BindIdent(id_tpl),
TplEnd(S8),
// Child expression _after_ the template for the same reason.
ExprStart(ExprOp::Sum, S9),
ExprEnd(S10),
ExprEnd(S11),
PkgEnd(S12),
];
let ctx = air_ctx_from_toks(toks);
let asg = ctx.asg_ref();
// The inner template.
let tpl = pkg_expect_ident_obj::<Tpl>(&ctx, id_tpl);
assert_eq!(S6.merge(S8).unwrap(), tpl.span());
assert_eq!(TplShape::Empty, tpl.shape());
// The expression that was produced on the graph ought to be equivalent
// to the expression without the template being present at all
// (noting that the spans are of course not adjusted).
let oi_expr = pkg_expect_ident_oi::<Expr>(&ctx, id_expr);
let expr = oi_expr.resolve(&asg);
assert_eq!(S2.merge(S11).unwrap(), expr.span());
assert_eq!(
#[rustfmt::skip]
vec![
S4.merge(S5).unwrap(),
S9.merge(S10).unwrap(),
],
collect_subexprs(&asg, oi_expr)
.iter()
.map(|(_, expr)| expr.span())
.rev()
.collect::<Vec<_>>(),
);
}
// Like the above test,
// but now we're _applying_ a template.
#[test]
fn tpl_apply_within_expr() {
let id_expr = spair("expr", S3);
let id_tpl = spair("_tpl_", S5);
let ref_tpl = spair("_tpl_", S8);
#[rustfmt::skip]
let toks = [
ExprStart(ExprOp::Sum, S2),
BindIdent(id_expr),
// This will not be present in the final expression,
// as if it were hoisted out.
TplStart(S4),
BindIdent(id_tpl),
TplEnd(S6),
// But the application will remain.
TplStart(S7),
RefIdent(ref_tpl),
TplEndRef(S9),
ExprEnd(S10),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
let asg = ctx.asg_ref();
let tpl = pkg_expect_ident_obj::<Tpl>(&ctx, id_tpl);
assert_eq!(S4.merge(S6).unwrap(), tpl.span());
assert_eq!(TplShape::Empty, tpl.shape());
// The expression that was produced on the graph ought to be equivalent
// to the expression without the template being present at all,
// but retaining the _application_.
let oi_expr = pkg_expect_ident_oi::<Expr>(&ctx, id_expr);
let expr = oi_expr.resolve(&asg);
assert_eq!(S2.merge(S10).unwrap(), expr.span());
assert_eq!(
#[rustfmt::skip]
vec![
S7.merge(S9).unwrap(),
],
oi_expr
.edges(&asg)
.map(|rel| rel.widen().resolve(&asg).span())
.collect::<Vec<_>>()
);
}
#[test]
fn close_tpl_without_open() {
let id_tpl = spair("_tpl_", S3);
#[rustfmt::skip]
let toks = [
TplEnd(S1),
// RECOVERY: Try again.
TplStart(S2),
BindIdent(id_tpl),
TplEnd(S4),
];
assert_eq!(
#[rustfmt::skip]
vec![
Ok(Parsed::Incomplete), // PkgStart
Err(ParseError::StateError(AsgError::UnbalancedTpl(S1))),
// RECOVERY
Ok(Parsed::Incomplete), // TplStart
Ok(Parsed::Incomplete), // BindIdent
Ok(Parsed::Incomplete), // TplEnd
Ok(Parsed::Incomplete), // PkgEnd
],
parse_as_pkg_body(toks).collect::<Vec<_>>(),
);
}
#[test]
fn tpl_with_reachable_expression() {
let id_tpl = spair("_tpl_", S2);
let id_expr_a = spair("expra", S4);
let id_expr_b = spair("exprb", S7);
#[rustfmt::skip]
let toks = [
TplStart(S1),
BindIdent(id_tpl),
ExprStart(ExprOp::Sum, S3),
// Must not be cached in the global env.
BindIdent(id_expr_a),
ExprEnd(S5),
ExprStart(ExprOp::Sum, S6),
// Must not be cached in the global env.
BindIdent(id_expr_b),
ExprEnd(S8),
TplEnd(S9),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
let asg = ctx.asg_ref();
let oi_tpl = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl);
let tpl = oi_tpl.resolve(&asg);
assert_eq!(S1.merge(S9).unwrap(), tpl.span());
// Because the above expressions were bound to identifiers,
// they will not be inlined into the application site
// (they'll be hoisted to the nearest container,
// which might be the same as the application site,
// but it's still not inlining an expression).
assert_eq!(TplShape::Empty, tpl.shape());
// The inner expressions are reachable,
// but the intent is to expand them into the template's eventual
// application site.
// They have identifiers,
// but those identifiers _must not_ be cached in the global
// environment;
// such a determination will be made at expansion-time.
// Given that,
// they should be defined by the template...
assert_eq!(
vec![
// At the time of writing,
// this is implemented using the same `edges_filtered`,
// but the point is that we want to ensure that the
// identifiers bound to this template are only these.
oi_tpl.lookup_local_linear(&asg, id_expr_b),
oi_tpl.lookup_local_linear(&asg, id_expr_a),
],
oi_tpl
.edges_filtered::<Ident>(&asg)
.map(Some)
.collect::<Vec<_>>()
);
// ...but not by the package containing the template.
let oi_pkg = pkg_lookup(&ctx, id_tpl).unwrap().src_pkg(&asg).unwrap();
assert_eq!(
vec![
// The only identifier on the package should be the template itself.
pkg_lookup(&ctx, id_tpl).unwrap(),
],
oi_pkg.edges_filtered::<Ident>(&asg).collect::<Vec<_>>()
);
// Verify the above claim that these are not cached in the global
// environment.
assert_eq!(None, pkg_lookup(&ctx, id_expr_a));
assert_eq!(None, pkg_lookup(&ctx, id_expr_b));
}
// Templates can expand into many contexts,
// including other expressions,
// and so must be able to contain expressions that,
// while dangling now,
// will become reachable in its expansion context.
#[test]
fn tpl_holds_dangling_expressions() {
let id_tpl = spair("_tpl_", S2);
#[rustfmt::skip]
let toks = [
TplStart(S1),
BindIdent(id_tpl),
// Dangling expression.
ExprStart(ExprOp::Sum, S3),
ExprEnd(S4),
TplEnd(S5),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
let asg = ctx.asg_ref();
let oi_tpl = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl);
let tpl = oi_tpl.resolve(&asg);
// The above `Expr` would be inlined at an application site,
// and so this changes the shape of the template.
assert_eq!(TplShape::Expr(S3.merge(S4).unwrap()), tpl.shape());
assert_eq!(
vec![S3.merge(S4).unwrap()],
oi_tpl
.edges_filtered::<Expr>(&asg)
.map(ObjectIndex::cresolve(&asg))
.map(Expr::span)
.collect::<Vec<_>>()
);
}
// As of TAMER,
// a new restriction is that templates can't just inline whatever they
// want into their expression expansion context.
#[test]
fn multi_dangling_invalid_expr_shape() {
let id_tpl = spair("_tpl_", S2);
#[rustfmt::skip]
let toks = [
TplStart(S1),
BindIdent(id_tpl),
// We have an expression to be expanded inline.
// We cannot have another.
ExprStart(ExprOp::Sum, S3),
ExprEnd(S4),
// ...but we're provided another!
// This should error.
ExprStart(ExprOp::Sum, S5),
ExprEnd(S6),
TplEnd(S7),
];
let mut sut = Sut::parse(as_pkg_body(toks));
assert_eq!(
#[rustfmt::skip]
vec![
Ok(Parsed::Incomplete), // PkgStart
Ok(Parsed::Incomplete), // TplStart
Ok(Parsed::Incomplete), // BindIdent
// This one's okay.
Ok(Parsed::Incomplete), // ExprStart
Ok(Parsed::Incomplete), // ExprEnd
// We start out okay,
// because an edge for a dangling expression is not added
// until after the expression ends
// (we have until then to bind an identifier).
Ok(Parsed::Incomplete), // ExprStart
// But the ending token will trigger the error.
Err(ParseError::StateError(
AsgError::TplShapeExprMulti(
Some(id_tpl),
S5.merge(S6).unwrap(),
S3.merge(S4).unwrap()
)
)),
// RECOVERY: We do not add the edge,
// but the object otherwise continues to exist on the
// graph,
// unreachable.
Ok(Parsed::Incomplete), // TplEnd
Ok(Parsed::Incomplete), // PkgEnd
],
sut.by_ref().collect::<Vec<_>>(),
);
let ctx = sut.finalize().unwrap().into_private_context();
let asg = ctx.asg_ref();
let oi_tpl = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl);
let tpl = oi_tpl.resolve(&asg);
// The error above should be evidence for this,
// but let's make sure the error didn't somehow cause the shape to
// change.
assert_eq!(TplShape::Expr(S3.merge(S4).unwrap()), tpl.shape());
// The template should have only one inline expression;
// it should have discarded the other.
assert_eq!(
vec![S3.merge(S4).unwrap()],
oi_tpl
.edges_filtered::<Expr>(&asg)
.map(ObjectIndex::cresolve(&asg))
.map(Expr::span)
.collect::<Vec<_>>()
);
}
#[test]
fn close_tpl_mid_open() {
let id_tpl = spair("_tpl_", S2);
let id_expr = spair("expr", S4);
#[rustfmt::skip]
let toks = [
TplStart(S1),
BindIdent(id_tpl),
ExprStart(ExprOp::Sum, S3),
BindIdent(id_expr),
// This is misplaced.
TplEnd(S5),
// RECOVERY: Close the expression and try again.
ExprEnd(S6),
TplEnd(S7),
];
assert_eq!(
#[rustfmt::skip]
vec![
Ok(Parsed::Incomplete), // PkgStart
Ok(Parsed::Incomplete), // TplStart
Ok(Parsed::Incomplete), // BindIdent
Ok(Parsed::Incomplete), // ExprStart
Ok(Parsed::Incomplete), // BindIdent
Err(ParseError::StateError(
AsgError::UnbalancedTpl(S5))
),
// RECOVERY
Ok(Parsed::Incomplete), // ExprEnd
Ok(Parsed::Incomplete), // TplEnd
Ok(Parsed::Incomplete), // PkgEnd
],
parse_as_pkg_body(toks).collect::<Vec<_>>(),
);
}
// If a template is ended with `TplEnd` and was not assigned a name,
// then it isn't reachable on the graph.
//
// ...that's technically not entirely true in a traversal sense
// (see following test),
// but the context would be all wrong.
// It _is_ true from a practical sense,
// with how NIR and AIR have been constructed at the time of writing,
// but may not be true in the future.
#[test]
fn unreachable_anonymous_tpl() {
let id_ok = spair("_tpl_", S4);
#[rustfmt::skip]
let toks = [
TplStart(S1),
// No BindIdent
TplEnd(S2),
// Recovery should ignore the above template
// (it's lost to the void)
// and allow continuing.
TplStart(S3),
BindIdent(id_ok),
TplEnd(S5),
];
let mut sut = parse_as_pkg_body(toks);
assert_eq!(
#[rustfmt::skip]
vec![
Ok(Parsed::Incomplete), // PkgStart
Ok(Parsed::Incomplete), // TplStart
Err(ParseError::StateError(AsgError::DanglingTpl(
S1.merge(S2).unwrap()
))),
// RECOVERY
Ok(Parsed::Incomplete), // TplStart
Ok(Parsed::Incomplete), // TplBindIdent
Ok(Parsed::Incomplete), // TplEnd
Ok(Parsed::Incomplete), // PkgEnd
],
sut.by_ref().collect::<Vec<_>>(),
);
let ctx = sut.finalize().unwrap().into_private_context();
// Let's make sure that the template created after recovery succeeded.
pkg_expect_ident_obj::<Tpl>(&ctx, id_ok);
}
// Normally we cannot reference objects without an identifier using AIR
// (at the time of writing at least),
// but `TplEndRef` is an exception.
#[test]
fn anonymous_tpl_immediate_ref() {
#[rustfmt::skip]
let toks = [
TplStart(S1),
// No BindIdent
// But ended with `TplEndRef`,
// so the missing identifier is okay.
// This would fail if it were `TplEnd`.
TplEndRef(S2),
];
let mut sut = parse_as_pkg_body(toks);
assert!(sut.all(|x| x.is_ok()));
// TODO: More to come.
}
#[test]
fn tpl_with_param() {
let id_tpl = spair("_tpl_", S2);
let id_param1 = spair("@param1@", S4);
let pval1 = spair("value1", S5);
let id_param2 = spair("@param2@", S8);
let param_desc = spair("param desc", S9);
#[rustfmt::skip]
let toks = [
TplStart(S1),
BindIdent(id_tpl),
// Metavariable with a value.
MetaStart(S3),
BindIdent(id_param1),
MetaLexeme(pval1),
MetaEnd(S6),
// Required metavariable (no value).
MetaStart(S7),
BindIdent(id_param2),
DocIndepClause(param_desc),
MetaEnd(S10),
TplEnd(S11),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
let asg = ctx.asg_ref();
let oi_tpl = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl);
let tpl = oi_tpl.resolve(&asg);
// The template contains no body
// (only metavariables / params).
assert_eq!(TplShape::Empty, tpl.shape());
// The template should have an edge to each identifier for each
// metavariable.
let params = [id_param1, id_param2]
.iter()
.map(|id| {
oi_tpl
.lookup_local_linear(&asg, *id)
.and_then(|oi| oi.edges_filtered::<Meta>(&asg).next())
.map(|oi| (oi, oi.resolve(&asg)))
})
.collect::<Vec<_>>();
assert_eq!(
params[0].unwrap().1,
&Meta::Lexeme(S3.merge(S6).unwrap(), pval1)
);
assert_eq!(
params[1].unwrap().1,
&Meta::Required(S7.merge(S10).unwrap())
);
// The second param should have a description.
let doc = params[1]
.unwrap()
.0
.edges_filtered::<Doc>(&asg)
.last()
.map(ObjectIndex::cresolve(&asg));
assert_eq!(doc, Some(&Doc::new_indep_clause(param_desc)));
}
// A template definition nested within another creates a
// template-defining-template.
// The inner template and its identifier should be entirely contained within
// the outer (parent) template so that it will expand into a template
// definition in the context of the expansion site.
#[test]
fn tpl_nested() {
let id_tpl_outer = spair("_tpl-outer_", S2);
let id_tpl_inner = spair("_tpl-inner_", S4);
#[rustfmt::skip]
let toks = vec![
TplStart(S1),
BindIdent(id_tpl_outer),
// Inner template
TplStart(S3),
BindIdent(id_tpl_inner),
TplEnd(S5),
TplEnd(S6),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
let asg = ctx.asg_ref();
// The outer template should be defined globally,
// but not the inner,
// since it hasn't been expanded yet.
let oi_tpl_outer = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl_outer);
assert_eq!(None, pkg_lookup(&ctx, id_tpl_inner));
assert_eq!(S1.merge(S6).unwrap(), oi_tpl_outer.resolve(&asg).span());
// The identifier for the inner template should be local to the outer
// template.
let oi_tpl_inner_ident =
oi_tpl_outer.lookup_local_linear(&asg, id_tpl_inner);
let tpl_inner = oi_tpl_inner_ident
.and_then(|oi| oi.definition::<Tpl>(&asg))
.map(ObjectIndex::cresolve(&asg));
assert_eq!(S3.merge(S5), tpl_inner.map(Tpl::span));
let tpl_outer = oi_tpl_outer.resolve(&asg);
// The inner template has no body and so is empty.
assert_eq!(TplShape::Empty, tpl_inner.unwrap().shape());
// The outer template defines an inner template but has nothing to
// inline,
// and so its shape is also empty.
assert_eq!(TplShape::Empty, tpl_outer.shape());
}
// A template application within another template can be interpreted as
// either applying the template as much as possible into the body of the
// template definition,
// or as expanding the template application into the expansion site and
// _then_ expanding the inner template at the expansion site.
// Both will yield equivalent results,
// but in either case,
// it all starts the same.
#[test]
fn tpl_apply_nested() {
let id_tpl_outer = spair("_tpl-outer_", S2);
#[rustfmt::skip]
let toks = [
TplStart(S1),
BindIdent(id_tpl_outer),
// Inner template application
TplStart(S3),
TplEndRef(S4),
TplEnd(S5),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
let asg = ctx.asg_ref();
let oi_tpl_outer = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl_outer);
assert_eq!(S1.merge(S5).unwrap(), oi_tpl_outer.resolve(&asg).span());
// The inner template,
// being a template application,
// should be a direct child of the outer template.
let inners = oi_tpl_outer
.edges_filtered::<Tpl>(&asg)
.map(|oi| oi.resolve(&asg).span());
assert_eq!(vec![S3.merge(S4).unwrap()], inners.collect::<Vec<_>>());
// Since the inner template is empty,
// so too should the outer.
let tpl_outer = oi_tpl_outer.resolve(&asg);
assert_eq!(TplShape::Empty, tpl_outer.shape());
}
// Template application should resolve all the same regardless of order of
// ref/def.
#[test]
fn tpl_apply_nested_missing() {
let id_tpl_outer = spair("_tpl-outer_", S2);
let tpl_inner = "_tpl-inner_";
let id_tpl_inner = spair(tpl_inner, S7);
let ref_tpl_inner_pre = spair(tpl_inner, S4);
let ref_tpl_inner_post = spair(tpl_inner, S10);
#[rustfmt::skip]
let toks = [
TplStart(S1),
BindIdent(id_tpl_outer),
// Inner template application (Missing)
TplStart(S3),
RefIdent(ref_tpl_inner_pre),
TplEndRef(S5),
// Define the template above
TplStart(S6),
BindIdent(id_tpl_inner),
TplEnd(S8),
// Apply again,
// this time _after_ having been defined.
TplStart(S9),
RefIdent(ref_tpl_inner_post),
TplEndRef(S11),
TplEnd(S12),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
let asg = ctx.asg_ref();
let oi_tpl_outer = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl_outer);
assert_eq!(S1.merge(S12).unwrap(), oi_tpl_outer.resolve(&asg).span());
// We apply two template,
// both of which are empty,
// and so the outer shape is still empty.
let tpl_outer = oi_tpl_outer.resolve(&asg);
assert_eq!(TplShape::Empty, tpl_outer.shape());
// The inner template should be contained within the outer and so not
// globally resolvable.
assert!(pkg_lookup(&ctx, id_tpl_inner).is_none());
// But it is accessible as a local on the outer template.
let oi_tpl_inner = oi_tpl_outer
.lookup_local_linear(&asg, id_tpl_inner)
.expect("could not locate inner template as a local")
.definition::<Tpl>(&asg)
.expect("could not resolve inner template ref to Tpl");
// We should have two inner template applications.
let inners = oi_tpl_outer.edges_filtered::<Tpl>(&asg).collect::<Vec<_>>();
assert_eq!(
vec![S9.merge(S11).unwrap(), S3.merge(S5).unwrap()],
inners
.iter()
.map(|oi| oi.resolve(&asg).span())
.collect::<Vec<_>>(),
);
// Each of those inner template applications should have resolved to the
// same template,
// despite their varying ref/def ordering.
assert_eq!(
vec![oi_tpl_inner, oi_tpl_inner],
inners
.iter()
.flat_map(|oi| oi.edges_filtered::<Ident>(&asg))
.filter_map(|oi| oi.definition(&asg))
.collect::<Vec<_>>(),
);
}
#[test]
fn tpl_inner_apply_inherit_shape() {
let id_tpl_outer = spair("_tpl-outer_", S2);
#[rustfmt::skip]
let toks = [
TplStart(S1),
BindIdent(id_tpl_outer),
// Inner template application has an Expr shape.
TplStart(S3),
ExprStart(ExprOp::Sum, S4),
ExprEnd(S5),
TplEndRef(S6),
TplEnd(S7),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
let asg = ctx.asg_ref();
let oi_tpl_outer = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl_outer);
let oi_tpl_inner = oi_tpl_outer.edges_filtered::<Tpl>(&asg).next().unwrap();
// The inner template's expression should determine its shape.
assert_eq!(
TplShape::Expr(S4.merge(S5).unwrap()),
oi_tpl_inner.resolve(&asg).shape()
);
// Since the outer template applies the inner,
// it inherits the inner's shape.
// (Imagine pasting the body of the inner template inline.)
assert_eq!(
TplShape::Expr(S3.merge(S6).unwrap()),
oi_tpl_outer.resolve(&asg).shape()
);
}
// It's okay to have sibling template applications that aren't Exprs.
#[test]
fn tpl_inner_apply_expr_alongside_empty() {
let id_tpl_outer = spair("_tpl-outer_", S2);
#[rustfmt::skip]
let toks = [
TplStart(S1),
BindIdent(id_tpl_outer),
// Inner template application has an Expr shape.
TplStart(S3),
ExprStart(ExprOp::Sum, S4),
ExprEnd(S5),
TplEndRef(S6),
// But this is empty.
TplStart(S7),
TplEndRef(S8),
TplEnd(S9),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
let asg = ctx.asg_ref();
let oi_tpl_outer = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl_outer);
// The Expr shape takes precedence.
assert_eq!(
TplShape::Expr(S3.merge(S6).unwrap()),
oi_tpl_outer.resolve(&asg).shape()
);
}
// Template applications with Expr shapes yield the same errors as if the
// bodies were just pasted inline,
// with the exception of the spans that are reported.
// The spans we provide to the user should be in the context of the template
// being defined;
// if we inherit the shape from the template,
// then the span would be for the expression _inside_ that template,
// which would compose to the innermost application's expression,
// recursively.
// And while that would certainly make for an impressive display if you
// understood what was going on,
// that breaks every layer of abstraction that was built and is not what
// we want to provide.
#[test]
fn tpl_inner_apply_expr_alongside_another_apply_expr() {
let id_tpl_outer = spair("_tpl-outer_", S2);
#[rustfmt::skip]
let toks = [
TplStart(S1),
BindIdent(id_tpl_outer),
// Inner template application has an Expr shape.
TplStart(S3),
ExprStart(ExprOp::Sum, S4),
ExprEnd(S5),
TplEndRef(S6),
// As does this one,
// which should produce an error.
TplStart(S7),
ExprStart(ExprOp::Sum, S8),
ExprEnd(S9),
TplEndRef(S10),
TplEnd(S11),
];
let mut sut = Sut::parse(as_pkg_body(toks));
assert_eq!(
#[rustfmt::skip]
vec![
Ok(Parsed::Incomplete), // PkgStart
Ok(Parsed::Incomplete), // TplStart
Ok(Parsed::Incomplete), // BindIdent
// This one's okay and changes the template's shape.
Ok(Parsed::Incomplete), // TplStart
Ok(Parsed::Incomplete), // ExprStart
Ok(Parsed::Incomplete), // ExprEnd
Ok(Parsed::Incomplete), // TplEnd
// But this one runs afoul of that new shape.
Ok(Parsed::Incomplete), // TplStart
Ok(Parsed::Incomplete), // ExprStart
Ok(Parsed::Incomplete), // ExprEnd
Err(ParseError::StateError(
// These spans are relative to the application itself.
// This is important to present the appropriate level of
// abstraction;
// see the comment for this test case.
AsgError::TplShapeExprMulti(
Some(id_tpl_outer),
S7.merge(S10).unwrap(),
S3.merge(S6).unwrap()
)
)),
// RECOVERY: We ignore the template by not adding the edge.
Ok(Parsed::Incomplete), // TplEnd >LA
Ok(Parsed::Incomplete), // TplEnd <LA
Ok(Parsed::Incomplete), // PkgEnd
],
sut.by_ref().collect::<Vec<_>>(),
);
let ctx = sut.finalize().unwrap().into_private_context();
let asg = ctx.asg_ref();
let oi_tpl_outer = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl_outer);
assert_eq!(
TplShape::Expr(S3.merge(S6).unwrap()),
oi_tpl_outer.resolve(&asg).shape()
);
// The second template application should have been omitted.
assert_eq!(
vec![S3.merge(S6).unwrap()],
oi_tpl_outer
.edges_filtered::<Tpl>(&asg)
.map(ObjectIndex::cresolve(&asg))
.map(Tpl::span)
.collect::<Vec<_>>()
);
}
#[test]
fn tpl_doc_short_desc() {
let id_tpl = spair("foo", S2);
let clause = spair("short desc", S3);
#[rustfmt::skip]
let toks = [
TplStart(S1),
BindIdent(id_tpl),
DocIndepClause(clause),
TplEnd(S4),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
let asg = ctx.asg_ref();
let oi_tpl = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl);
let oi_docs = oi_tpl
.edges_filtered::<Doc>(&asg)
.map(ObjectIndex::cresolve(&asg));
assert_eq!(
vec![&Doc::new_indep_clause(clause)],
oi_docs.collect::<Vec<_>>(),
);
// The documentation does not contribute to expansion and therefore does
// not influence the shape of the template.
assert_eq!(TplShape::Empty, oi_tpl.resolve(&asg).shape());
}
// While NIR does not accept metavariables (params) within expressions that
// are the body of a template,
// metavariable interpolation does create them.
// Hoisting them out of the expression and into the template context is a
// fairly simple thing to do for AIR,
// but would be vastly more complicated for NIR,
// especially for a streaming parser,
// as it'd have to hold tokens until it was sure that no
// interpolation would occur.
// We therefore adopt the simple rule that metavariables are hoisted into
// the context of the parent template.
// This also gives much greater flexibility to any other (AIR) code
// generators.
#[test]
fn metavars_within_exprs_hoisted_to_parent_tpl() {
let id_tpl_outer = spair("_tpl-outer_", S2);
let id_tpl_inner = spair("_tpl-inner_", S9);
let id_param_outer = spair("@param_outer@", S5);
let id_param_inner = spair("@param_inner@", S12);
#[rustfmt::skip]
let toks = [
TplStart(S1),
BindIdent(id_tpl_outer),
// This expression begins the body of the template.
// NIR would not allow params past this point,
// but desugaring may produce this.
ExprStart(ExprOp::Sum, S3),
// Expresions are not containers and so this metavariable should
// be hoisted to the parent container context.
// That container must be a valid meta context.
MetaStart(S4),
BindIdent(id_param_outer),
MetaEnd(S6),
ExprEnd(S7),
// Nested template
TplStart(S8),
BindIdent(id_tpl_inner),
ExprStart(ExprOp::Sum, S10),
// Hoisting should be relative to the innermost template.
MetaStart(S11),
BindIdent(id_param_inner),
MetaEnd(S13),
ExprEnd(S14),
TplEnd(S15),
TplEnd(S16),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
let asg = ctx.asg_ref();
let oi_outer = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl_outer);
let span_outer = ctx
.env_scope_lookup::<Ident>(oi_outer, id_param_outer)
.expect("missing id_param_outer Ident")
.definition::<Meta>(asg)
.expect("missing id_param_outer definition")
.resolve(asg)
.span();
assert_eq!(S4.merge(S6).unwrap(), span_outer);
let oi_inner = ctx
.env_scope_lookup::<Ident>(oi_outer, id_tpl_inner)
.expect("could not locate inner Tpl's Ident")
.definition::<Tpl>(asg)
.expect("missing inner Tpl");
let span_inner = ctx
.env_scope_lookup::<Ident>(oi_inner, id_param_inner)
.expect("missing id_param_inner Ident")
.definition::<Meta>(asg)
.expect("missing id_param_inner definition")
.resolve(asg)
.span();
assert_eq!(S11.merge(S13).unwrap(), span_inner);
// The template would expand into an expression,
// since it otherwise dangling.
assert_eq!(
TplShape::Expr(S3.merge(S7).unwrap()),
oi_outer.resolve(&asg).shape(),
);
}
#[test]
fn expr_abstract_bind_produces_cross_edge_from_ident_to_meta() {
let id_tpl = spair("_tpl_", S2);
let id_meta = spair("@foo@", S4);
#[rustfmt::skip]
let toks = [
TplStart(S1),
// This identifier is concrete;
// the abstract identifier will be the _expression_.
BindIdent(id_tpl),
ExprStart(ExprOp::Sum, S3),
// This expression is bound to an _abstract_ identifier,
// which will be expanded at a later time.
// This does _not_ change the dangling status,
// and so can only occur within an expression that acts as a
// container for otherwise-dangling expressions.
BindIdentAbstract(id_meta),
ExprEnd(S5),
TplEnd(S6),
];
let ctx = air_ctx_from_pkg_body_toks(toks);
let asg = ctx.asg_ref();
// The expression cannot be indexed by a named (concrete) identifier
// and so we must find it through a traversal.
// The abstract identifier should be bound to our rooting context,
// which is the template,
// despite not having a name;
// this mirrors the same structure that the template body will
// assume upon instantiation.
let oi_tpl = pkg_expect_ident_oi::<Tpl>(&ctx, id_tpl);
let oi_ident = oi_tpl
.edges_filtered::<Ident>(asg)
.next()
.expect("abstract identifier is not rooted in template");
assert_eq!(
None,
oi_ident.resolve(asg).name(),
"abstract identifier must not have a concrete name",
);
// The metavariable referenced by the abstract identifier
assert_eq!(id_meta, oi_ident.name_or_meta(asg));
// The identifier should be bound to the expression.
let oi_expr = oi_ident
.definition::<Expr>(asg)
.expect("abstract identifier did not bind to Expr");
assert_eq!(S3.merge(S5).unwrap(), oi_expr.resolve(asg).span());
// Finally,
// the expression should not be considered dangling and so we should
// not have an edge directly from the template to the Expr.
// If that were to happen,
// then we'd end up duplicating the expression.
assert!(
oi_tpl.edges_filtered::<Expr>(asg).next().is_none(),
"Tpl must not have an edge directly to Expr \
(is it considered dangling?)",
);
// Because the expression _will be_ bound to an identifier during
// instantiation,
// it'll be hoisted upon expansion,
// and so our shape is still empty.
// This is the same result as if we had a concrete identifier;
// it all ends up expanding into the same thing in the end.
assert_eq!(TplShape::Empty, oi_tpl.resolve(&asg).shape());
}