tamer: nir: Desugar shorthand template params and yield AIR

I had intended for this to be a full vertical slice initially, but AIR's
parser is going to need enough work that it'll muddy this patch a bit too
much.

This keeps the desugaring simple, which is what I was hoping for.

The next step is to load it into the graph and emit regenerated longhand
sources.

I also don't like how the namespace prefix is just being ignored for
shorthand param desugaring.  This is also the case in the XSLT-based
compiler, but this violates TAMER's principle that it should parse every bit
of information; nothing should be ignored.  If something does not contribute
useful information, then it is not a useful construct and ought to be
rejected.

DEV-13708
Mike Gerwitz 2023-03-22 10:07:16 -04:00
parent d3f8f76904
commit 91b787d367
7 changed files with 202 additions and 26 deletions

View File

@ -210,6 +210,9 @@ pub enum NirEntity {
/// then the param is shorthand,
/// with the non-`@`-padded name as the first of the pair and the
/// value as the second.
///
/// A shorthand entity is implicitly closed and should not have a
/// matching [`Nir::Close`] token.
TplParam(Option<(SPair, SPair)>),
/// Full application and expansion a template.

View File

@ -124,7 +124,13 @@ impl ParseState for NirToAir {
// Short-hand template application must be handled through
// desugaring as part of the lowering pipeline,
// so that it is converted to long form before getting here.
(Ready, Nir::Open(NirEntity::TplApply(Some(_)), span)) => {
(
Ready,
Nir::Open(
NirEntity::TplApply(Some(_)) | NirEntity::TplParam(Some(_)),
span,
),
) => {
// TODO: In the future maybe TAMER will have evolved its
// abstractions enough that there's an ROI for prohibiting
// this at the type level.
@ -147,6 +153,16 @@ impl ParseState for NirToAir {
Transition(Ready).ok(Air::TplEndRef(span))
}
(Ready, Nir::Open(NirEntity::TplParam(None), span)) => {
Transition(Ready).ok(Air::TplMetaStart(span))
}
(Ready, Nir::Close(NirEntity::TplParam(_), span)) => {
Transition(Ready).ok(Air::TplMetaEnd(span))
}
(Ready, Nir::Text(lexeme)) => {
Transition(Ready).ok(Air::TplLexeme(lexeme))
}
(
Ready,
Nir::Close(
@ -169,15 +185,9 @@ impl ParseState for NirToAir {
Transition(Ready).ok(Air::RefIdent(spair))
}
(
Ready,
Nir::Todo
| Nir::TodoAttr(..)
| Nir::Desc(..)
| Nir::Text(_)
| Nir::Open(NirEntity::TplParam(_), _)
| Nir::Close(NirEntity::TplParam(_), _),
) => Transition(Ready).ok(Air::Todo(UNKNOWN_SPAN)),
(Ready, Nir::Todo | Nir::TodoAttr(..) | Nir::Desc(..)) => {
Transition(Ready).ok(Air::Todo(UNKNOWN_SPAN))
}
}
}

View File

@ -141,7 +141,7 @@ fn tpl_with_name() {
// Long form takes the actual underscore-padded template name without any
// additional processing.
#[test]
fn apply_template_long_form() {
fn apply_template_long_form_nullary() {
let name = SPair("_tpl-name_".into(), S2);
#[rustfmt::skip]
@ -161,3 +161,49 @@ fn apply_template_long_form() {
Sut::parse(toks.into_iter()).collect(),
);
}
#[test]
fn apply_template_long_form_args() {
let name = SPair("_tpl-name_".into(), S2);
let p1 = SPair("@p1@".into(), S4);
let v1 = SPair("value1".into(), S5);
let p2 = SPair("@p2@".into(), S8);
let v2 = SPair("value2".into(), S9);
#[rustfmt::skip]
let toks = vec![
Nir::Open(NirEntity::TplApply(None), S1),
Nir::Ref(name),
Nir::Open(NirEntity::TplParam(None), S3),
Nir::BindIdent(p1),
Nir::Text(v1),
Nir::Close(NirEntity::TplParam(None), S6),
Nir::Open(NirEntity::TplParam(None), S7),
Nir::BindIdent(p2),
Nir::Text(v2),
Nir::Close(NirEntity::TplParam(None), S10),
Nir::Close(NirEntity::TplApply(None), S11),
];
#[rustfmt::skip]
assert_eq!(
Ok(vec![
O(Air::TplStart(S1)),
O(Air::RefIdent(name)),
O(Air::TplMetaStart(S3)),
O(Air::BindIdent(p1)),
O(Air::TplLexeme(v1)),
O(Air::TplMetaEnd(S6)),
O(Air::TplMetaStart(S7)),
O(Air::BindIdent(p2)),
O(Air::TplLexeme(v2)),
O(Air::TplMetaEnd(S10)),
O(Air::TplEndRef(S11)),
]),
Sut::parse(toks.into_iter()).collect(),
);
}

View File

@ -1695,6 +1695,13 @@ ele_parse! {
/// and where the body of this application is the `@values@`
/// template argument.
/// See [`ApplyTemplate`] for more information.
///
/// The name of the template omits the surrounding `_`s;
/// `t:foo` will desugar into the template name `_foo_`.
/// Params similarly omit `@` and are derived from the _local name
/// only_;
/// so `bar="baz"` will be desugared into a param `@bar@` with a
/// text value `baz`.
TplApplyShort := NS_T(qname, ospan) {
@ {} => Nir::Open(NirEntity::TplApply(Some(qname)), ospan.into()),
/(cspan) => Nir::Close(NirEntity::TplApply(None), cspan.into()),
@ -1702,7 +1709,18 @@ ele_parse! {
// Streaming attribute parsing;
// this takes precedence over any attribute parsing above
// (which is used only for emitting the opening object).
[attr](_attr) => Todo,
[attr](Attr(name, value, AttrSpan(name_span, value_span))) => {
Nir::Open(
// TODO: This simply _ignores_ the namespace prefix.
// If it's not a useful construct,
// it ought to be rejected.
NirEntity::TplParam(Some((
SPair(*name.local_name(), name_span),
SPair(value, value_span),
))),
name_span,
)
},
// Template bodies depend on context,
// so we have to just accept everything and defer to a future

View File

@ -124,8 +124,24 @@ impl ParseState for TplShortDesugar {
.ok(Nir::Open(NirEntity::TplApply(None), span))
}
// Shorthand template params' names do not contain the
// surrounding `@`s.
(
Scanning,
Nir::Open(NirEntity::TplParam(Some((name, val))), span),
) => {
let pname = format!("@{name}@").intern();
stack.push(Nir::Close(NirEntity::TplParam(None), span));
stack.push(Nir::Text(val));
stack.push(Nir::BindIdent(SPair(pname, name.span())));
Transition(Scanning)
.ok(Nir::Open(NirEntity::TplParam(None), span))
}
// Any tokens that we don't recognize will be passed on unchanged.
(Scanning, tok) => Transition(Scanning).ok(tok),
(st, tok) => Transition(st).ok(tok),
}
}
@ -134,7 +150,7 @@ impl ParseState for TplShortDesugar {
}
}
type Stack = ArrayVec<Nir, 2>;
type Stack = ArrayVec<Nir, 3>;
#[cfg(test)]
mod test;

View File

@ -35,20 +35,103 @@ fn desugars_nullary() {
Nir::Close(NirEntity::TplApply(None), S2),
];
#[rustfmt::skip]
assert_eq!(
Ok(vec![
O(Nir::Open(NirEntity::TplApply(None), S1)),
// The span associated with the name of the template to be
// applied is the span of the entire QName from NIR.
// The reason for this is that `t:foo` is transformed into
// `_foo_`,
// and so the `t:` is a necessary part of the
// representation of the name of the template;
// `foo` is not in itself a valid template name at the
// time of writing.
O(Nir::Ref(SPair(tpl, S1))),
// The span associated with the name of the template to be
// applied is the span of the entire QName from NIR.
// The reason for this is that `t:foo` is transformed into
// `_foo_`,
// and so the `t:` is a necessary part of the
// representation of the name of the template;
// `foo` is not in itself a valid template name at the
// time of writing.
O(Nir::Ref(SPair(tpl, S1))),
O(Nir::Close(NirEntity::TplApply(None), S2)),
]),
Sut::parse(toks.into_iter()).collect(),
);
}
#[test]
fn desugars_unary() {
// Shorthand converts `t:tpl-name` into `_tpl-name_`.
let qname = ("t", "tpl-name").unwrap_into();
let name = SPair("_tpl-name_".into(), S1);
let aname = SPair("foo".into(), S3);
let pval = SPair("foo value".into(), S4);
// The attribute name gets padded with '@',
// much like the template does with underscores.
let pname = SPair("@foo@".into(), S3);
#[rustfmt::skip]
let toks = vec![
// <t:qname
Nir::Open(NirEntity::TplApply(Some(qname)), S1),
// foo="foo value"
Nir::Open(NirEntity::TplParam(Some((aname, pval))), S2),
// Implicit close.
// />
Nir::Close(NirEntity::TplApply(None), S6),
];
#[rustfmt::skip]
assert_eq!(
Ok(vec![
O(Nir::Open(NirEntity::TplApply(None), S1)),
O(Nir::Ref(name)),
O(Nir::Open(NirEntity::TplParam(None), S2)),
// Derived from `aname` (by padding)
O(Nir::BindIdent(pname)),
// The value is left untouched.
O(Nir::Text(pval)),
// Close is derived from open.
O(Nir::Close(NirEntity::TplParam(None), S2)),
O(Nir::Close(NirEntity::TplApply(None), S6)),
]),
Sut::parse(toks.into_iter()).collect(),
);
}
// Don't parse what we desugar into!
#[test]
fn does_not_desugar_long_form() {
let name = SPair("_tpl-name_".into(), S2);
let pname = SPair("@param".into(), S4);
let pval = SPair("value".into(), S5);
#[rustfmt::skip]
let toks = [
Nir::Open(NirEntity::TplApply(None), S1),
Nir::BindIdent(name),
Nir::Open(NirEntity::TplParam(None), S3),
Nir::BindIdent(pname),
Nir::Text(pval),
Nir::Close(NirEntity::TplParam(None), S6),
Nir::Close(NirEntity::TplApply(None), S7),
];
#[rustfmt::skip]
assert_eq!(
// We avoid #[derive(Clone)] on Nir so that we have confidence that
// we're not doing anything suspicious with tokens.
// So this is a duplicate of the above,
// mapped over `O`.
Ok(vec![
O(Nir::Open(NirEntity::TplApply(None), S1)),
O(Nir::BindIdent(name)),
O(Nir::Open(NirEntity::TplParam(None), S3)),
O(Nir::BindIdent(pname)),
O(Nir::Text(pval)),
O(Nir::Close(NirEntity::TplParam(None), S6)),
O(Nir::Close(NirEntity::TplApply(None), S7)),
]),
Sut::parse(toks.into_iter()).collect(),
);
}

View File

@ -320,7 +320,7 @@ macro_rules! ele_parse {
// Special forms (`[sp](args) => expr`).
$(
[$special:ident]$(($($special_arg:ident),*))?
[$special:ident]$(($($special_arg:tt)*))?
=> $special_map:expr,
)?
@ -338,7 +338,7 @@ macro_rules! ele_parse {
@ { $($attrbody)* } => $attrmap,
/$($($close_span)?)? => ele_parse!(@!ele_close $($closemap)?),
$([$special]$(($($special_arg),*))? => $special_map,)?
$([$special]$(($($special_arg)*))? => $special_map,)?
<> {
$(
@ -422,7 +422,7 @@ macro_rules! ele_parse {
/$($close_span:ident)? => $closemap:expr,
// Streaming (as opposed to aggregate) attribute parsing.
$([attr]($attr_stream_binding:ident) => $attr_stream_map:expr,)?
$([attr]($attr_stream_binding:pat) => $attr_stream_map:expr,)?
// Nonterminal references.
<> {