tamer: asg: Initial nested template supported

I had hoped this would be considerably easier to implement, but there are
some confounding factors.

First of all: this accomplishes the initial task of getting nested template
applications and definitions re-output in the `xmli` file.  But to do so
successfully, some assumptions had to be made.

The primary issue is that of scope.  The old (XSLT-based) TAME relied on the
output JS to handle lexical scope for it at runtime in most situations.  In
the case of the template system, when scoping/shadowing were needed, complex
and buggy XPaths were used to make a best effort.  The equivalent here would
be a graph traversal, which is not ideal.

I had begun going down the rabbit hole of formalizing lexical scope for
TAMER with environments, but I want to get this committed and working first;
I've been holding onto this and breaking off changes for some time now.

DEV-13708
main
Mike Gerwitz 2023-04-05 10:45:54 -04:00
parent a738a05461
commit daa8c6967b
9 changed files with 449 additions and 94 deletions

View File

@ -36,7 +36,7 @@
//! air such cringeworthy dad jokes here.
use super::{
graph::object::{ObjectIndexRelTo, ObjectIndexTo, Pkg, Tpl},
graph::object::{ObjectIndexTo, ObjectIndexToTree, Pkg, Tpl},
Asg, AsgError, Expr, Ident, ObjectIndex,
};
use crate::{
@ -262,6 +262,34 @@ impl AirAggregate {
PkgTpl(st) => st.is_accepting(ctx),
}
}
/// The rooting context for [`Ident`]s for the active parser.
///
/// A value of [`None`] indicates that the current parser does not
/// support direct bindings,
/// but a parent context may
/// (see [`AirAggregateCtx::rooting_oi`]).
fn active_rooting_oi(&self) -> Option<ObjectIndexToTree<Ident>> {
match self {
AirAggregate::Empty => None,
AirAggregate::Toplevel(pkg_oi) => Some((*pkg_oi).into()),
// Expressions never serve as roots for identifiers;
// this will always fall through to the parent context.
// Since the parent context is a package or a template,
// the next frame should succeed.
AirAggregate::PkgExpr(_) => None,
// Identifiers bound while within a template definition context
// must bind to the eventual _expansion_ site,
// as if the body were pasted there.
// Templates must therefore serve as containers for identifiers
// bound therein.
AirAggregate::PkgTpl(tplst) => {
tplst.active_tpl_oi().map(Into::into)
}
}
}
}
/// Additional parser context,
@ -337,18 +365,20 @@ impl AirAggregateCtx {
/// control to the child `to`.
///
/// See also [`Self::proxy`].
fn ret_or_transfer(
fn ret_or_transfer<S: Into<AirAggregate>, SB: Into<AirAggregate>>(
&mut self,
st: AirAggregate,
st: S,
tok: impl Token + Into<Air>,
to: impl Into<AirAggregate>,
to: SB,
) -> TransitionResult<AirAggregate> {
if st.active_is_accepting(self) {
let st_super = st.into();
if st_super.active_is_accepting(self) {
// TODO: dead state or error
self.stack().ret_or_dead(AirAggregate::Empty, tok)
} else {
self.stack().transfer_with_ret(
Transition(st),
Transition(st_super),
Transition(to.into()).incomplete().with_lookahead(tok),
)
}
@ -393,28 +423,10 @@ impl AirAggregateCtx {
///
/// A value of [`None`] indicates that no bindings are permitted in the
/// current context.
fn rooting_oi(&self) -> Option<(usize, ObjectIndexTo<Ident>)> {
fn rooting_oi(&self) -> Option<ObjectIndexToTree<Ident>> {
let Self(_, stack, _) = self;
stack.iter().enumerate().rev().find_map(|(i, st)| match st {
AirAggregate::Empty => None,
AirAggregate::Toplevel(pkg_oi) => Some((i, (*pkg_oi).into())),
// Expressions never serve as roots for identifiers;
// this will always fall through to the parent context.
// Since the parent context is a package or a template,
// the next frame should succeed.
AirAggregate::PkgExpr(_) => None,
// Identifiers bound while within a template definition context
// must bind to the eventual _expansion_ site,
// as if the body were pasted there.
// Templates must therefore serve as containers for identifiers
// bound therein.
AirAggregate::PkgTpl(tplst) => {
tplst.active_tpl_oi().map(|oi| (i, oi.into()))
}
})
stack.iter().rev().find_map(|st| st.active_rooting_oi())
}
/// The active dangling expression context for [`Expr`]s.
@ -457,24 +469,38 @@ impl AirAggregateCtx {
fn expansion_oi(&self) -> Option<ObjectIndexTo<Tpl>> {
let Self(_, stack, _) = self;
stack.iter().rev().find_map(|st| match *st {
stack.iter().rev().find_map(|st| match st {
AirAggregate::Empty => None,
AirAggregate::Toplevel(pkg_oi) => Some(pkg_oi.into()),
AirAggregate::Toplevel(pkg_oi) => Some((*pkg_oi).into()),
AirAggregate::PkgExpr(_) => {
diagnostic_todo!(vec![], "PkgExpr expansion_oi")
}
AirAggregate::PkgTpl(_) => {
diagnostic_todo!(vec![], "PkgTpl expansion_oi")
AirAggregate::PkgTpl(tplst) => {
tplst.active_tpl_oi().map(Into::into)
}
})
}
/// Root an identifier using the [`Self::rooting_oi`] atop of the stack.
fn defines(&mut self, name: SPair) -> Result<ObjectIndex<Ident>, AsgError> {
let oi_root = self
.rooting_oi()
.ok_or(AsgError::InvalidBindContext(name))?;
Ok(self.lookup_lexical_or_missing(name).add_edge_from(
self.asg_mut(),
oi_root,
None,
))
}
/// Attempt to locate a lexically scoped identifier,
/// or create a new one if missing.
///
/// Until [`Asg`] can be further generalized,
/// there are unfortunately two rooting strategies employed:
///
/// 1. If the stack has only a single held frame,
/// 1. If the stack has only a single held frame at a scope boundary,
/// then it is assumed to be the package representing the active
/// compilation unit and the identifier is indexed in the global
/// scope.
@ -482,20 +508,45 @@ impl AirAggregateCtx {
/// the identifier is defined locally and does not undergo
/// indexing.
///
/// TODO: Generalize this.
fn defines(&mut self, name: SPair) -> Result<ObjectIndex<Ident>, AsgError> {
let (stacki, oi_root) = self
.rooting_oi()
.ok_or(AsgError::InvalidBindContext(name))?;
/// TODO: This is very informal and just starts to get things working.
fn lookup_lexical_or_missing(&mut self, name: SPair) -> ObjectIndex<Ident> {
let Self(asg, stack, _) = self;
let asg = self.asg_mut();
let found = stack
.iter()
.skip(1)
.rev()
.filter_map(|st| st.active_rooting_oi())
.find_map(|oi| asg.lookup(oi, name));
Ok(match stacki {
0 => asg
.lookup_global_or_missing(name)
.add_edge_from(asg, oi_root, None),
_ => oi_root.declare_local(asg, name),
})
// Rust's borrow checker won't allow us to use unwrap_or_else above
// as of 2023-04.
if let Some(oi) = found {
oi
} else {
// TODO: This special case can be removed once we generalize
// indexing/scope.
// TODO: This filtering is a quick kludge to get things working!
match stack.iter().filter_map(|st| st.active_rooting_oi()).nth(1) {
None => asg.lookup_global_or_missing(name),
_ => self.create_env_indexed_ident(name),
}
}
}
/// Index an identifier within its environment.
///
/// TODO: More information as this is formalized.
fn create_env_indexed_ident(&mut self, name: SPair) -> ObjectIndex<Ident> {
let Self(asg, stack, _) = self;
let oi_ident = asg.create(Ident::declare(name));
stack
.iter()
.filter_map(|st| st.active_rooting_oi())
.for_each(|oi| asg.index_identifier(oi, name, oi_ident));
oi_ident
}
}

View File

@ -69,6 +69,13 @@ pub enum AirTplAggregate {
/// Defining a template metavariable.
TplMeta(TplState, ObjectIndex<Meta>),
/// A template has been completed.
///
/// This is used to determine whether the next token of input ought to
/// result in a dead state transition or a transition back to
/// [`Self::Ready`].
Done,
}
impl Display for AirTplAggregate {
@ -81,6 +88,7 @@ impl Display for AirTplAggregate {
Self::TplMeta(tpl, _) => {
write!(f, "building {tpl} metavariable")
}
Self::Done => write!(f, "completed building template"),
}
}
}
@ -171,16 +179,15 @@ impl ParseState for AirTplAggregate {
use AirTplAggregate::*;
match (self, tok) {
(Ready, AirTpl(TplStart(span))) => {
(Ready | Done, AirTpl(TplStart(span))) => {
let oi_tpl = ctx.asg_mut().create(Tpl::new(span));
Transition(Toplevel(TplState::Dangling(oi_tpl))).incomplete()
}
(Toplevel(..), AirTpl(TplStart(span))) => diagnostic_todo!(
vec![span.note("for this template")],
"nested tpl open"
),
(st @ Toplevel(..), tok @ AirTpl(TplStart(_))) => {
ctx.ret_or_transfer(st, tok, AirTplAggregate::new())
}
(Toplevel(tpl), AirBind(BindIdent(id))) => ctx
.defines(id)
@ -190,8 +197,12 @@ impl ParseState for AirTplAggregate {
.map(|_| ())
.transition(Toplevel(tpl.identify(id))),
(Toplevel(tpl), AirBind(RefIdent(id))) => {
tpl.oi().apply_named_tpl(ctx.asg_mut(), id);
(Toplevel(tpl), AirBind(RefIdent(name))) => {
let tpl_oi = tpl.oi();
let ref_oi = ctx.lookup_lexical_or_missing(name);
tpl_oi.apply_named_tpl(ctx.asg_mut(), ref_oi, name.span());
Transition(Toplevel(tpl)).incomplete()
}
@ -246,7 +257,7 @@ impl ParseState for AirTplAggregate {
}
(Toplevel(tpl), AirTpl(TplEnd(span))) => {
tpl.close(ctx.asg_mut(), span).transition(Ready)
tpl.close(ctx.asg_mut(), span).transition(Done)
}
(Toplevel(tpl), AirTpl(TplEndRef(span))) => {
@ -268,7 +279,7 @@ impl ParseState for AirTplAggregate {
}
(
Ready,
Ready | Done,
tok @ AirTpl(TplMetaStart(..) | TplLexeme(..) | TplMetaEnd(..)),
) => {
diagnostic_todo!(
@ -277,16 +288,25 @@ impl ParseState for AirTplAggregate {
)
}
(st @ Ready, AirTpl(TplEnd(span) | TplEndRef(span))) => {
Transition(st).err(AsgError::UnbalancedTpl(span))
// If we just finished a template then this end may represent
// the closing of a parent template.
(Done, tok @ AirTpl(TplEnd(_) | TplEndRef(_))) => {
Transition(Done).dead(tok)
}
// Otherwise we have an unbalanced close
// (we just started parsing to receive an end token).
(Ready, AirTpl(TplEnd(span) | TplEndRef(span))) => {
Transition(Ready).err(AsgError::UnbalancedTpl(span))
}
(st @ Ready, tok @ AirBind(..)) => Transition(st).dead(tok),
(st @ (Ready | Done), tok @ AirBind(..)) => {
Transition(st).dead(tok)
}
}
}
fn is_accepting(&self, _: &Self::Context) -> bool {
matches!(self, Self::Ready)
matches!(self, Self::Ready | Self::Done)
}
}
@ -299,7 +319,7 @@ impl AirTplAggregate {
use AirTplAggregate::*;
match self {
Ready => None,
Ready | Done => None,
Toplevel(tplst) | TplMeta(tplst, _) => Some(tplst.oi()),
}
}

View File

@ -448,3 +448,156 @@ fn tpl_with_param() {
assert_eq!(params[0], Some(&Meta::Lexeme(S3.merge(S6).unwrap(), pval1)));
assert_eq!(params[1], Some(&Meta::Required(S7.merge(S9).unwrap())));
}
// 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_".into(), S2);
let id_tpl_inner = SPair("_tpl-inner_".into(), S4);
#[rustfmt::skip]
let toks = vec![
Air::TplStart(S1),
Air::BindIdent(id_tpl_outer),
// Inner template
Air::TplStart(S3),
Air::BindIdent(id_tpl_inner),
Air::TplEnd(S5),
Air::TplEnd(S6),
];
let asg = asg_from_toks(toks);
// The outer template should be defined globally,
// but not the inner,
// since it hasn't been expanded yet.
let oi_tpl_outer = asg.expect_ident_oi::<Tpl>(id_tpl_outer);
assert_eq!(None, asg.lookup_global(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 = oi_tpl_outer.lookup_local_linear(&asg, id_tpl_inner);
assert_eq!(
S3.merge(S5),
oi_tpl_inner
.and_then(|oi| oi.definition::<Tpl>(&asg))
.map(|oi| oi.resolve(&asg).span())
);
}
// 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_".into(), S2);
#[rustfmt::skip]
let toks = vec![
Air::TplStart(S1),
Air::BindIdent(id_tpl_outer),
// Inner template application
Air::TplStart(S3),
Air::TplEndRef(S4),
Air::TplEnd(S5),
];
let asg = asg_from_toks(toks);
let oi_tpl_outer = asg.expect_ident_oi::<Tpl>(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<_>>(),);
}
// 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_".into(), S2);
let tpl_inner = "_tpl-inner_".into();
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 = vec![
Air::TplStart(S1),
Air::BindIdent(id_tpl_outer),
// Inner template application (Missing)
Air::TplStart(S3),
Air::RefIdent(ref_tpl_inner_pre),
Air::TplEndRef(S5),
// Define the template above
Air::TplStart(S6),
Air::BindIdent(id_tpl_inner),
Air::TplEnd(S8),
// Apply again,
// this time _after_ having been defined.
Air::TplStart(S9),
Air::RefIdent(ref_tpl_inner_post),
Air::TplEndRef(S11),
Air::TplEnd(S12),
];
let asg = asg_from_toks(toks);
let oi_tpl_outer = asg.expect_ident_oi::<Tpl>(id_tpl_outer);
assert_eq!(S1.merge(S12).unwrap(), oi_tpl_outer.resolve(&asg).span());
// The inner template should be contained within the outer and so not
// globally resolvable.
assert!(asg.lookup_global(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<_>>(),
);
}

View File

@ -1046,6 +1046,17 @@ impl ObjectIndex<Ident> {
.map(|ident_oi| ident_oi.add_edge_to(asg, definition, None))
}
/// Look up the definition that this identifier binds to,
/// if any.
///
/// See [`Self::bind_definition`].
pub fn definition<O: ObjectRelFrom<Ident> + ObjectRelatable>(
&self,
asg: &Asg,
) -> Option<ObjectIndex<O>> {
self.edges(asg).find_map(ObjectRel::narrow)
}
/// Whether this identifier is bound to the object represented by `oi`.
///
/// To bind an identifier,

View File

@ -25,12 +25,7 @@ use super::{
Expr, Ident, Object, ObjectIndex, ObjectIndexRelTo, ObjectRel,
ObjectRelFrom, ObjectRelTy, ObjectRelatable, ObjectTreeRelTo,
};
use crate::{
asg::Asg,
f::Functor,
parse::{util::SPair, Token},
span::Span,
};
use crate::{asg::Asg, f::Functor, span::Span};
/// Template with associated name.
#[derive(Debug, PartialEq, Eq)]
@ -73,6 +68,9 @@ object_rel! {
// Identifiers are used for both references and identifiers that
// will expand into an application site.
dyn Ident,
// Template application.
tree Tpl,
}
}
@ -94,9 +92,13 @@ impl ObjectIndex<Tpl> {
/// During evaluation,
/// this application will expand the template in place,
/// re-binding metavariables to the context of `self`.
pub fn apply_named_tpl(self, asg: &mut Asg, id: SPair) -> Self {
let oi_apply = asg.lookup_global_or_missing(id);
self.add_edge_to(asg, oi_apply, Some(id.span()))
pub fn apply_named_tpl(
self,
asg: &mut Asg,
oi_apply: ObjectIndex<Ident>,
ref_span: Span,
) -> Self {
self.add_edge_to(asg, oi_apply, Some(ref_span))
}
/// Directly reference this template from another object

View File

@ -241,3 +241,90 @@ fn traverses_ontological_tree_tpl_apply() {
tree_reconstruction_report(toks),
);
}
// A template acts as a container for anything defined therein,
// to be expanded into an application site.
// This means that identifiers that might otherwise be bound to the package
// need to be contained by the template,
// and further that identifier _resolution_ must be able to occur within
// the template,
// e.g. to apply templates defined therein.
#[test]
fn traverses_ontological_tree_tpl_within_template() {
let name_outer = "_tpl-outer_".into();
let id_tpl_outer = SPair(name_outer, S3);
let name_inner = "_tpl-inner_".into();
let id_tpl_inner = SPair(name_inner, S10);
let ref_inner_before = SPair(name_inner, S7);
let ref_inner_after = SPair(name_inner, S13);
#[rustfmt::skip]
let toks = vec![
PkgStart(S1),
TplStart(S2),
BindIdent(id_tpl_outer),
// Anonymous inner template application.
TplStart(S4),
TplEndRef(S5), // notice the `Ref` at the end
// Apply above inner template,
// _before_ definition,
// which will begin as Missing and must be later resolved when
// the template is defined.
TplStart(S6),
RefIdent(ref_inner_before), // --.
TplEndRef(S8), // |
// |
// Named inner template. // |
TplStart(S9), // /
BindIdent(id_tpl_inner), //<-:
TplEnd(S11), // \
// |
// Apply above inner template, // |
// _after_ definition. // |
TplStart(S12), // |
RefIdent(ref_inner_after), // __/
TplEndRef(S14),
TplEnd(S15),
PkgEnd(S16),
];
// We need more concise expressions for the below table of values.
let d = DynObjectRel::new;
let m = |a: Span, b: Span| a.merge(b).unwrap();
#[rustfmt::skip]
assert_eq!(
// A -|-> B | A span -|-> B span | espan | depth
vec![//-----|-------|-----------|-----------|---------|-----------------
(d(Root, Pkg, SU, m(S1, S16), None ), Depth(1)),
(d(Pkg, Ident, m(S1, S16), S3, None ), Depth(2)),
(d(Ident, Tpl, S3, m(S2, S15), None ), Depth(3)),
(d(Tpl, Tpl, m(S2, S15), m(S4, S5), None ), Depth(4)),
(d(Tpl, Tpl, m(S2, S15), m(S6, S8), None ), Depth(4)),
/*cross*/ (d(Tpl, Ident, m(S6, S8), S10, Some(S7) ), Depth(5)),
// ,--------------------------------^^^
/* | */ (d(Tpl, Ident, m(S2, S15), S10, None ), Depth(4)),
/* | */ (d(Ident, Tpl, S10, m(S9, S11), None ), Depth(5)),
/* | */ (d(Tpl, Tpl, m(S2, S15), m(S12,S14), None ), Depth(4)),
/*cross*/ (d(Tpl, Ident, m(S12,S14), S10, Some(S13)), Depth(5)),
// | // ^^^
// | // Note that successfully /
// | // resolving this span --`
// | // as S10 (Ident::Transparent) instead of S13 (which would mean
// | // a new Ident::Missing was created) requires that we resolve
// | // a local identifier that is rooted in the _template_ rather
// | // than the global scope.
// `----> Similarly,
// resolving the former as S10 instead of S7 means that a
// local identifier that was originally Missing is properly
// resolved to Transparent once it was defined;
// which asserts consistency in identifier scope regardless
// of reference/definition order.
// This lexical analysis is something that the XSLT-based TAME
// was not capable of doing.
],
tree_reconstruction_report(toks),
);
}

View File

@ -309,19 +309,21 @@ impl<'a> TreeContext<'a> {
// Convert this into a long-hand template expansion so that we
// do not have to deal with converting underscore-padded
// template names back into short-hand form.
Object::Pkg(..) => {
Object::Pkg(..) | Object::Tpl(..) => {
// [`Ident`]s are skipped during traversal,
// so we'll handle it ourselves here.
// This also gives us the opportunity to make sure that
// we're deriving something that's actually supported by the
// XSLT-based compiler.
// TODO: This doesn't handle `Missing` or invalid (non-`Tpl`)
// identifiers.
let mut idents = oi_tpl
.edges_filtered::<Ident>(self.asg)
.filter(|oi| oi.is_bound_to_kind::<Tpl>(self.asg));
// TODO: Not checking that it's a Tpl ident because we need
// to be able to accommodate Missing identifiers until we
// both properly handle scoping rules and support package
// imports;
// this is making some dangerous assumptions,
// though they are tested.
let ident = oi_tpl.edges_filtered::<Ident>(self.asg).last();
let apply_tpl = idents.next().diagnostic_expect(
let apply_tpl = ident.diagnostic_expect(
|| {
vec![tpl
.span()
@ -330,19 +332,6 @@ impl<'a> TreeContext<'a> {
"cannot derive name of template for application",
);
if let Some(bad_ident) = idents.next() {
diagnostic_panic!(
vec![
tpl.span().note(
"while processing this template application"
),
bad_ident
.internal_error("unexpected second identifier"),
],
"expected only one Ident->Tpl for template application",
);
}
self.push(attr_name(apply_tpl.resolve(self.asg).name()));
Some(Xirf::open(

View File

@ -125,5 +125,32 @@
<c:product />
</c:sum>
</template>
<template name="_short-hand-nullary-outer_" />
<apply-template name="_short-hand-nullary-outer_">
<with-param name="@values@" value="___dsgr-d72___" />
</apply-template>
<template name="___dsgr-d72___">
<template name="_short-hand-nullary-inner-dfn-inner_" />
<apply-template name="_short-hand-nullary-inner-dfn-inner_" />
</template>
<template name="_short-hand-nullary-inner-dfn-outer_" />
<apply-template name="_short-hand-nullary-outer_">
<with-param name="@values@" value="___dsgr-e59___" />
</apply-template>
<template name="___dsgr-e59___">
<apply-template name="_short-hand-nullary-inner-dfn-outer_" />
</template>
<template name="_short-hand-unary-with-values_" />
<apply-template name="_short-hand-unary-with-values_">
<with-param name="@foo@" value="bar" />
<with-param name="@values@" value="___dsgr-eff___" />
</apply-template>
<template name="___dsgr-eff___">
<template name="_short-hand-unary-with-values-inner_" />
<apply-template name="_short-hand-unary-with-values-inner_" />
</template>
</package>

View File

@ -125,16 +125,31 @@
<template name="_short-hand-nullary-outer_" />
<t:short-hand-nullary-outer>
<template name="_short-hand-nullary-inner-dfn-inner_" />
<t:short-hand-nullary-inner-dfn-inner />
</t:short-hand-nullary-outer>
<template name="_short-hand-nullary-inner-dfn-outer_" />
<t:short-hand-nullary-outer>
<t:short-hand-nullary-inner-dfn-outer />
</t:short-hand-nullary-outer>
<template name="_short-hand-unary-with-values_" />
<t:short-hand-unary-with-values foo="bar">
<template name="_short-hand-unary-with-values-inner_" />
<t:short-hand-unary-with-values-inner />
</t:short-hand-unary-with-values>
<!-- TODO
<t:short-hand-nullary-inner>
<t:inner-short />
</t:short-hand-nullary-inner>
<t:short-hand foo="bar">
<t:inner-short />
</t:short-hand>
<rate yields="shortHandTplInExpr">
<t:short-hand in="rate" />
</rate>