tamer: Introduce desugaring operation for shorthand template application

This moves translation from NirToAir into TplShortDesugar, and changes the
output from AIR to NIR.

This is going to be much easier to reason about as a desugaring
operation (and indeed that's always how TAME has implemented it, in XSLT);
this keeps the complexity isolated.

Ideally, NirToAir wouldn't even accept tokens that it can't handle, but
that's going to take quite a bit more work and I don't have the time right
now.  Instead, we'll fail at runtime with some hopefully-useful
information.  It shouldn't actually happen in practice.

DEV-13708
Mike Gerwitz 2023-03-21 14:38:07 -04:00
parent c91d175711
commit c42e693b14
6 changed files with 249 additions and 77 deletions

View File

@ -47,8 +47,8 @@ use tamer::{
AnnotatedSpan, Diagnostic, FsSpanResolver, Reporter, VisualReporter,
},
nir::{
InterpError, InterpolateNir, Nir, NirToAir, NirToAirError, XirfToNir,
XirfToNirError,
InterpError, InterpolateNir, Nir, NirToAir, NirToAirError,
TplShortDesugar, XirfToNir, XirfToNirError,
},
parse::{
lowerable, FinalizeError, Lower, ParseError, ParsedObject, Token,
@ -164,21 +164,23 @@ fn compile<R: Reporter>(
_,
>::lower::<_, UnrecoverableError>(src, |toks| {
Lower::<XirToXirf<64, RefinedText>, XirfToNir, _>::lower(toks, |nir| {
Lower::<XirfToNir, InterpolateNir, _>::lower(nir, |nir| {
Lower::<InterpolateNir, NirToAir, _>::lower(nir, |air| {
Lower::<NirToAir, AirAggregate, _>::lower_with_context(
air,
asg,
|end| {
end.fold(Ok(()), |x, result| match result {
Ok(_) => x,
Err(e) => {
report_err(&e, reporter, &mut ebuf)?;
x
}
})
},
)
Lower::<XirfToNir, TplShortDesugar, _>::lower(nir, |nir| {
Lower::<TplShortDesugar, InterpolateNir, _>::lower(nir, |nir| {
Lower::<InterpolateNir, NirToAir, _>::lower(nir, |air| {
Lower::<NirToAir, AirAggregate, _>::lower_with_context(
air,
asg,
|end| {
end.fold(Ok(()), |x, result| match result {
Ok(_) => x,
Err(e) => {
report_err(&e, reporter, &mut ebuf)?;
x
}
})
},
)
})
})
})
})
@ -461,6 +463,14 @@ impl<T: Token> From<ParseError<T, Infallible>> for UnrecoverableError {
}
}
impl<T: Token> From<ParseError<T, Infallible>> for RecoverableError {
fn from(_: ParseError<T, Infallible>) -> Self {
unreachable!(
"<RecoverableError as From<ParseError<T, Infallible>>>::from"
)
}
}
impl From<ParseError<UnknownToken, xir::Error>> for RecoverableError {
fn from(e: ParseError<UnknownToken, xir::Error>) -> Self {
Self::XirParseError(e)

View File

@ -52,6 +52,7 @@
mod air;
mod interp;
mod parse;
mod tplshort;
use crate::{
diagnose::{Annotate, Diagnostic},
@ -77,6 +78,7 @@ pub use interp::{InterpError, InterpState as InterpolateNir};
pub use parse::{
NirParseState as XirfToNir, NirParseStateError_ as XirfToNirError,
};
pub use tplshort::TplShortDesugar;
/// IR that is "near" the source code.
///
@ -252,7 +254,7 @@ impl Display for NirEntity {
impl Token for Nir {
fn ir_name() -> &'static str {
"Plain NIR"
"NIR"
}
/// Identifying span of a token.

View File

@ -27,11 +27,7 @@ use crate::{
// These are also used by the `test` module which imports `super`.
#[cfg(feature = "wip-nir-to-air")]
use crate::{
asg::ExprOp,
nir::NirEntity,
sym::{GlobalSymbolIntern, GlobalSymbolResolve},
};
use crate::{asg::ExprOp, nir::NirEntity};
use super::Nir;
@ -79,6 +75,8 @@ impl ParseState for NirToAir {
) -> TransitionResult<Self::Super> {
use NirToAir::*;
use crate::{diagnose::Annotate, diagnostic_panic};
// Single-item "queue".
if let Some(obj) = queue.take() {
return Transition(Ready).ok(obj).with_lookahead(tok);
@ -123,24 +121,27 @@ impl ParseState for NirToAir {
Transition(Ready).ok(Air::TplStart(span))
}
// Short-hand template application contains the name of the
// template _without_ the underscore padding as the local part
// of the QName.
//
// Template application will create an anonymous template,
// apply it,
// and then expand it.
(Ready, Nir::Open(NirEntity::TplApply(Some(qname)), span)) => {
// TODO: Determine whether caching these has any notable
// benefit over repeated heap allocations,
// comparing packages with very few applications and
// packages with thousands
// (we'd still have to hit the heap for the cache).
let tpl_name =
format!("_{}_", qname.local_name().lookup_str()).intern();
queue.replace(Air::RefIdent(SPair(tpl_name, span)));
Transition(Ready).ok(Air::TplStart(span))
// 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)) => {
// TODO: In the future maybe TAMER will have evolved its
// abstractions enough that there's an ROI for prohibiting
// this at the type level.
diagnostic_panic!(
vec![
span.internal_error(
"attempted shorthand template application"
),
span.help(
"TAMER must be compiled with support for \
shorthand template application by utilizing the \
nir::tplshort module in the lowering pipeline."
)
],
"shortand template application is unsupported in this \
build of TAMER"
)
}
(Ready, Nir::Close(NirEntity::TplApply(_), span)) => {
Transition(Ready).ok(Air::TplEndRef(span))

View File

@ -18,7 +18,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use crate::{convert::ExpectInto, parse::util::SPair, span::dummy::*};
use crate::{parse::util::SPair, span::dummy::*};
type Sut = NirToAir;
@ -138,41 +138,6 @@ fn tpl_with_name() {
);
}
// This is the form everyone uses.
// It applies a template much more concisely and without the underscore
// padding,
// making it look much like a language primitive
// (with the exception of the namespace prefix).
#[test]
fn short_hand_tpl_apply() {
// Shorthand converts `t:tpl-name` into `_tpl-name_`.
let qname = ("t", "tpl-name").unwrap_into();
let name = SPair("_tpl-name_".into(), S1);
let toks = vec![
Nir::Open(NirEntity::TplApply(Some(qname)), S1),
Nir::Close(NirEntity::TplApply(None), S2),
];
#[rustfmt::skip]
assert_eq!(
Ok(vec![
O(Air::TplStart(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(Air::RefIdent(name)),
O(Air::TplEndRef(S2)),
]),
Sut::parse(toks.into_iter()).collect(),
);
}
// Long form takes the actual underscore-padded template name without any
// additional processing.
#[test]

View File

@ -0,0 +1,140 @@
// Shorthand template application desugaring for NIR
//
// 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/>.
//! Shorthand template application desugaring for NIR.
//!
//! A shorthand template application looks something like this,
//! in XML form:
//!
//! ```xml
//! <t:foo bar="baz">
//! <c:sum />
//! </t:foo>
//!
//! <!-- desugars into -->
//! <apply-template name="_foo_">
//! <with-param name="@bar@" value="baz" />
//! <with-param name="@values@">
//! <c:sum />
//! </with-param>
//! </apply-template>
//! ```
//!
//! The shorthand syntax makes templates look like another language
//! primitive,
//! with the exception of the namespace prefix.
//!
//! Note how desugaring also wraps template names in `'_'` and param names
//! in `'@'`.
//! These naming requirements were intended to eliminate conflicts with
//! other types of identifiers and to make it obvious when templates and
//! metavariables were being used,
//! but it works against the goal of making template applications look
//! like language primitives.
//! Shorthand form was added well after the long `apply-template` form.
//!
//! This shorthand version does not permit metavariables for template or
//! param names,
//! so the long form is still a useful language feature for more
//! sophisticated cases.
//!
//! This was originally handled in the XSLT compiler in
//! `:src/current/include/preproc/template.xsl`.
//! You may need to consult the Git history if this file is no longer
//! available or if the XSLT template was since removed.
use arrayvec::ArrayVec;
use super::{Nir, NirEntity};
use crate::{
parse::prelude::*,
sym::{GlobalSymbolIntern, GlobalSymbolResolve},
};
use std::convert::Infallible;
#[derive(Debug, PartialEq, Eq, Default)]
pub enum TplShortDesugar {
/// Waiting for shorthand template application,
/// passing tokens along in the meantime.
#[default]
Scanning,
}
impl Display for TplShortDesugar {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Scanning => {
write!(f, "awaiting shorthand template application")
}
}
}
}
impl ParseState for TplShortDesugar {
type Token = Nir;
type Object = Nir;
type Error = Infallible;
type Context = Stack;
fn parse_token(
self,
tok: Self::Token,
stack: &mut Self::Context,
) -> TransitionResult<Self::Super> {
use TplShortDesugar::*;
if let Some(obj) = stack.pop() {
return Transition(self).ok(obj).with_lookahead(tok);
}
match (self, tok) {
// Shorthand template applications are identified by a `Some`
// QName in the `TplApply` entity.
//
// The name of the template _without_ the underscore padding is
// the local part of the QName.
(Scanning, Nir::Open(NirEntity::TplApply(Some(qname)), span)) => {
// TODO: Determine whether caching these has any notable
// benefit over repeated heap allocations,
// comparing packages with very few applications and
// packages with thousands
// (we'd still have to hit the heap for the cache).
let tpl_name =
format!("_{}_", qname.local_name().lookup_str()).intern();
stack.push(Nir::Ref(SPair(tpl_name, span)));
Transition(Scanning)
.ok(Nir::Open(NirEntity::TplApply(None), span))
}
// Any tokens that we don't recognize will be passed on unchanged.
(Scanning, tok) => Transition(Scanning).ok(tok),
}
}
fn is_accepting(&self, stack: &Self::Context) -> bool {
matches!(self, Self::Scanning) && stack.is_empty()
}
}
type Stack = ArrayVec<Nir, 2>;
#[cfg(test)]
mod test;

View File

@ -0,0 +1,54 @@
// Tests shorthand template application desugaring for NIR
//
// 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::{convert::ExpectInto, span::dummy::*};
use Parsed::Object as O;
type Sut = TplShortDesugar;
#[test]
fn desugars_nullary() {
// Shorthand converts `t:tpl-name` into `_tpl-name_`.
let qname = ("t", "tpl-name").unwrap_into();
let tpl = "_tpl-name_".into();
let toks = [
Nir::Open(NirEntity::TplApply(Some(qname)), S1),
Nir::Close(NirEntity::TplApply(None), S2),
];
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))),
O(Nir::Close(NirEntity::TplApply(None), S2)),
]),
Sut::parse(toks.into_iter()).collect(),
);
}