tamer: Very basic support for template application NIR -> xmli

This this a big change that's difficult to break up, and I don't have the
energy after it.

This introduces nullary template application, short- and long-form.  Note
that a body of the short form is a `@values@` argument, so that's not
supported yet.

This continues to formalize the idea of what "template application" and
"template expansion" mean in TAMER.  It makes a separate `TplApply`
unnecessary, because now application is simply a reference to a
template.  Expansion and application are one and the same: when a template
expands, it'll re-bind metavariables to the parent context.  So in a
template context, this amounts to application.

But applying a closed template will have nothing to bind, and so is
equivalent to expansion.  And since `Meta` objects are not valid outside of
a `Tpl` context, applying a non-closed template outside of another template
will be invalid.

So we get all of this with a single primitive (getting the "value" of a
template).

The expansion is conceptually like `,@` in Lisp, where we're splicing trees.

It's a mess in some spots, but I want to get this committed before I do a
little bit of cleanup.
main
Mike Gerwitz 2023-03-17 10:25:56 -04:00
parent aa229b827c
commit 9d50157f8e
12 changed files with 444 additions and 49 deletions

View File

@ -33,6 +33,7 @@ use super::{
use crate::{
fmt::{DisplayWrapper, TtQuote},
parse::prelude::*,
span::Span,
};
/// Template parser and token aggregator.
@ -116,6 +117,29 @@ impl TplState {
fn identify(self, id: SPair) -> Self {
Self::Identified(self.oi(), id)
}
fn anonymous_reachable(self) -> Self {
Self::AnonymousReachable(self.oi())
}
/// Attempt to complete a template definition.
///
/// If `self` is [`Self::Dangling`],
/// then an [`AsgError::DanglingTpl`] will be returned.
///
/// This updates the span of the template to encompass the entire
/// definition,
/// even if an error occurs.
fn close(self, asg: &mut Asg, close_span: Span) -> Result<(), AsgError> {
let oi = self.oi().close(asg, close_span);
match self {
Self::Dangling(_) => {
Err(AsgError::DanglingTpl(oi.resolve(asg).span()))
}
Self::AnonymousReachable(..) | Self::Identified(..) => Ok(()),
}
}
}
impl Display for TplState {
@ -168,8 +192,9 @@ impl ParseState for AirTplAggregate {
.map(|_| ())
.transition(Toplevel(oi_pkg, tpl.identify(id), expr)),
(Toplevel(..), AirBind(RefIdent(_))) => {
todo!("tpl Toplevel RefIdent")
(Toplevel(oi_pkg, tpl, expr), AirBind(RefIdent(id))) => {
tpl.oi().apply_named_tpl(asg, id);
Transition(Toplevel(oi_pkg, tpl, expr)).incomplete()
}
(
@ -184,25 +209,41 @@ impl ParseState for AirTplAggregate {
}
(Toplevel(oi_pkg, tpl, _expr_done), AirTpl(TplEnd(span))) => {
tpl.oi().close(asg, span);
Transition(Ready(oi_pkg)).incomplete()
tpl.close(asg, span).transition(Ready(oi_pkg))
}
(TplExpr(oi_pkg, tpl, expr), AirTpl(TplEnd(span))) => {
// TODO: duplicated with AirAggregate
match expr.is_accepting(asg) {
true => {
// TODO: this is duplicated with the above
tpl.oi().close(asg, span);
Transition(Ready(oi_pkg)).incomplete()
}
false => Transition(TplExpr(oi_pkg, tpl, expr))
.err(AsgError::InvalidTplEndContext(span)),
if expr.is_accepting(asg) {
tpl.close(asg, span).transition(Ready(oi_pkg))
} else {
Transition(TplExpr(oi_pkg, tpl, expr))
.err(AsgError::InvalidTplEndContext(span))
}
}
(Toplevel(..) | TplExpr(..), AirTpl(TplEndRef(..))) => {
todo!("TplEndRef")
(Toplevel(oi_pkg, tpl, expr_done), AirTpl(TplEndRef(span))) => {
tpl.oi().expand_into(asg, oi_pkg);
Transition(Toplevel(
oi_pkg,
tpl.anonymous_reachable(),
expr_done,
))
.incomplete()
.with_lookahead(AirTpl(TplEnd(span)))
}
(TplExpr(oi_pkg, tpl, expr_done), AirTpl(TplEndRef(span))) => {
tpl.oi().expand_into(asg, oi_pkg);
Transition(TplExpr(
oi_pkg,
tpl.anonymous_reachable(),
expr_done,
))
.incomplete()
.with_lookahead(AirTpl(TplEnd(span)))
}
(

View File

@ -158,11 +158,14 @@ fn tpl_within_expr() {
#[test]
fn close_tpl_without_open() {
let id_tpl = SPair("_tpl_".into(), S3);
let toks = vec![
Air::TplEnd(S1),
// RECOVERY: Try again.
Air::TplStart(S2),
Air::TplEnd(S3),
Air::BindIdent(id_tpl),
Air::TplEnd(S4),
];
assert_eq!(
@ -171,6 +174,7 @@ fn close_tpl_without_open() {
Err(ParseError::StateError(AsgError::UnbalancedTpl(S1))),
// RECOVERY
Ok(Parsed::Incomplete), // TplStart
Ok(Parsed::Incomplete), // BindIdent
Ok(Parsed::Incomplete), // TplEnd
Ok(Parsed::Incomplete), // PkgEnd
],
@ -268,18 +272,21 @@ fn tpl_holds_dangling_expressions() {
#[test]
fn close_tpl_mid_open() {
let id_expr = SPair("expr".into(), S3);
let id_tpl = SPair("_tpl_".into(), S2);
let id_expr = SPair("expr".into(), S4);
#[rustfmt::skip]
let toks = vec![
Air::TplStart(S1),
Air::ExprStart(ExprOp::Sum, S2),
Air::BindIdent(id_expr),
Air::BindIdent(id_tpl),
Air::ExprStart(ExprOp::Sum, S3),
Air::BindIdent(id_expr),
// This is misplaced.
Air::TplEnd(S4),
Air::TplEnd(S5),
// RECOVERY: Close the expression and try again.
Air::ExprEnd(S5),
Air::TplEnd(S6),
Air::ExprEnd(S6),
Air::TplEnd(S7),
];
assert_eq!(
@ -287,10 +294,11 @@ fn close_tpl_mid_open() {
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::InvalidTplEndContext(S4))
AsgError::InvalidTplEndContext(S5))
),
// RECOVERY
Ok(Parsed::Incomplete), // ExprEnd
@ -300,3 +308,75 @@ fn close_tpl_mid_open() {
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_".into(), S4);
#[rustfmt::skip]
let toks = vec![
Air::TplStart(S1),
// No BindIdent
Air::TplEnd(S2),
// Recovery should ignore the above template
// (it's lost to the void)
// and allow continuing.
Air::TplStart(S3),
Air::BindIdent(id_ok),
Air::TplEnd(S5),
];
let mut sut = parse_as_pkg_body(toks);
assert_eq!(
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 asg = sut.finalize().unwrap().into_context();
// Let's make sure that the template created after recovery succeeded.
asg.expect_ident_obj::<Tpl>(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 = vec![
Air::TplStart(S1),
// No BindIdent
// But ended with `TplEndRef`,
// so the missing identifier is okay.
// This would fail if it were `TplEnd`.
Air::TplEndRef(S2),
];
let mut sut = parse_as_pkg_body(toks);
assert!(sut.all(|x| x.is_ok()));
// TODO: More to come.
}

View File

@ -88,6 +88,12 @@ pub enum AsgError {
/// The span should encompass the entirety of the expression.
DanglingExpr(Span),
/// A template is not reachable by any other object.
///
/// See [`Self::DanglingExpr`] for more information on the concept of
/// dangling objects.
DanglingTpl(Span),
/// Attempted to close an expression with no corresponding opening
/// delimiter.
UnbalancedExpr(Span),
@ -132,6 +138,10 @@ impl Display for AsgError {
f,
"dangling expression (anonymous expression has no parent)"
),
DanglingTpl(_) => write!(
f,
"dangling template (anonymous template cannot be referenced)"
),
UnbalancedExpr(_) => write!(f, "unbalanced expression"),
UnbalancedTpl(_) => write!(f, "unbalanced template definition"),
InvalidExprBindContext(_) => {
@ -221,6 +231,17 @@ impl Diagnostic for AsgError {
span.help(" its value cannot referenced."),
],
DanglingTpl(span) => vec![
span.error(
"this template is unreachable and can never be used",
),
span.help(
"a template may only be anonymous if it is ephemeral ",
),
span.help(" (immediately expanded)."),
span.help("alternatively, assign this template an identifier."),
],
UnbalancedExpr(span) => {
vec![span.error("there is no open expression to close here")]
}

View File

@ -21,7 +21,7 @@
use super::{
Ident, Object, ObjectIndex, ObjectRel, ObjectRelFrom, ObjectRelTy,
ObjectRelatable,
ObjectRelatable, Tpl,
};
use crate::{asg::Asg, f::Functor, span::Span};
use std::fmt::Display;
@ -65,6 +65,9 @@ object_rel! {
/// Imported [`Ident`]s do not have edges from this package.
Pkg -> {
tree Ident,
// Anonymous templates are used for expansion.
tree Tpl,
}
}

View File

@ -23,9 +23,9 @@ use std::fmt::Display;
use super::{
Expr, Ident, Meta, Object, ObjectIndex, ObjectRel, ObjectRelFrom,
ObjectRelTy, ObjectRelatable,
ObjectRelTo, ObjectRelTy, ObjectRelatable,
};
use crate::{asg::Asg, f::Functor, span::Span};
use crate::{asg::Asg, f::Functor, parse::util::SPair, span::Span};
/// Template with associated name.
#[derive(Debug, PartialEq, Eq)]
@ -68,9 +68,9 @@ object_rel! {
}
impl ObjectIndex<Tpl> {
/// Complete a template definition.
/// Attempt to complete a template definition.
///
/// This simply updates the span of the template to encompass the entire
/// This updates the span of the template to encompass the entire
/// definition.
pub fn close(self, asg: &mut Asg, close_span: Span) -> Self {
self.map_obj(asg, |tpl| {
@ -79,4 +79,33 @@ impl ObjectIndex<Tpl> {
})
})
}
/// Apply a named template `id` to the context of `self`.
///
/// 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_or_missing(id);
// TODO: span
self.add_edge_to(asg, oi_apply, None)
}
/// Directly reference this template from another object
/// `oi_target_parent`,
/// indicating the intent to expand the template in place.
///
/// This direct reference allows applying anonymous templates.
///
/// The term "expansion" is equivalent to the application of a closed
/// template.
/// If this template is _not_ closed,
/// it will result in an error during evaluation.
pub fn expand_into<O: ObjectRelTo<Tpl>>(
self,
asg: &mut Asg,
oi_target_parent: ObjectIndex<O>,
) -> Self {
self.add_edge_from(asg, oi_target_parent, None)
}
}

View File

@ -38,7 +38,7 @@ use crate::{
visit::{Depth, TreeWalkRel},
Asg, ExprOp, Ident,
},
diagnose::Annotate,
diagnose::{panic::DiagnosticPanic, Annotate},
diagnostic_panic, diagnostic_unreachable,
parse::{prelude::*, util::SPair, Transitionable},
span::{Span, UNKNOWN_SPAN},
@ -194,8 +194,8 @@ impl<'a> TreeContext<'a> {
self.emit_expr(expr, paired_rel.source(), depth)
}
Object::Tpl((tpl, _)) => {
self.emit_template(tpl, paired_rel.source(), depth)
Object::Tpl((tpl, oi_tpl)) => {
self.emit_template(tpl, *oi_tpl, paired_rel.source(), depth)
}
target @ Object::Meta(..) => todo!("Object::Meta: {target:?}"),
@ -280,10 +280,11 @@ impl<'a> TreeContext<'a> {
))
}
/// Emit a template definition.
/// Emit a template definition or application.
fn emit_template(
&mut self,
tpl: &Tpl,
oi_tpl: ObjectIndex<Tpl>,
src: &Object<OiPairObjectInner>,
depth: Depth,
) -> Option<Xirf> {
@ -298,6 +299,52 @@ impl<'a> TreeContext<'a> {
))
}
// If we're not behind an Ident,
// then this is a direct template reference,
// which indicates application of a closed template
// (template expansion).
// 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(..) => {
// [`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.
let mut idents = oi_tpl.edges_filtered::<Ident>(self.asg);
let apply_tpl = idents.next().diagnostic_expect(
|| {
vec![tpl
.span()
.internal_error("missing target Tpl Ident")]
},
"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 for template application",
);
}
self.push(attr_name(apply_tpl.resolve(self.asg).name()));
Some(Xirf::open(
QN_APPLY_TEMPLATE,
OpenSpan::without_name_span(tpl.span()),
depth,
))
}
_ => todo!("emit_template: {src:?}"),
}
}

View File

@ -203,6 +203,10 @@ pub enum NirEntity {
Tpl,
/// Template parameter (metavariable).
TplParam,
/// Full application and expansion of the template identified by the
/// provided name.
TplApply(Option<QName>),
}
impl NirEntity {
@ -234,6 +238,14 @@ impl Display for NirEntity {
Tpl => write!(f, "template"),
TplParam => write!(f, "template param (metavariable)"),
TplApply(None) => {
write!(f, "full template application and expansion")
}
TplApply(Some(qname)) => write!(
f,
"full template application and expansion of {}",
TtQuote::wrap(qname.local_name())
),
}
}
}

View File

@ -22,11 +22,15 @@
use std::{error::Error, fmt::Display};
use crate::{
asg::{air::Air, ExprOp},
diagnose::Diagnostic,
asg::air::Air, diagnose::Diagnostic, parse::prelude::*, span::UNKNOWN_SPAN,
};
// These are also used by the `test` module which imports `super`.
#[cfg(feature = "wip-nir-to-air")]
use crate::{
asg::ExprOp,
nir::NirEntity,
parse::prelude::*,
span::UNKNOWN_SPAN,
sym::{GlobalSymbolIntern, GlobalSymbolResolve},
};
use super::Nir;
@ -47,25 +51,39 @@ impl Display for NirToAir {
}
}
type QueuedObj = Option<Air>;
impl ParseState for NirToAir {
type Token = Nir;
type Object = Air;
type Error = NirToAirError;
type Context = QueuedObj;
#[cfg(not(feature = "wip-nir-to-air"))]
fn parse_token(
self,
tok: Self::Token,
_: NoContext,
_queue: &mut Self::Context,
) -> TransitionResult<Self::Super> {
use NirToAir::*;
#[cfg(not(feature = "wip-nir-to-air"))]
{
let _ = tok; // prevent `unused_variables` warning
return Transition(Ready).ok(Air::Todo(UNKNOWN_SPAN));
let _ = tok; // prevent `unused_variables` warning
Transition(Ready).ok(Air::Todo(UNKNOWN_SPAN))
}
#[cfg(feature = "wip-nir-to-air")]
fn parse_token(
self,
tok: Self::Token,
queue: &mut Self::Context,
) -> TransitionResult<Self::Super> {
use NirToAir::*;
// Single-item "queue".
if let Some(obj) = queue.take() {
return Transition(Ready).ok(obj).with_lookahead(tok);
}
#[allow(unreachable_code)] // due to wip-nir-to-air
match (self, tok) {
(Ready, Nir::Open(NirEntity::Package, span)) => {
Transition(Ready).ok(Air::PkgStart(span))
@ -97,11 +115,37 @@ impl ParseState for NirToAir {
(Ready, Nir::Open(NirEntity::Tpl, span)) => {
Transition(Ready).ok(Air::TplStart(span))
}
(Ready, Nir::Close(NirEntity::Tpl, span)) => {
Transition(Ready).ok(Air::TplEnd(span))
}
(Ready, Nir::Open(NirEntity::TplApply(None), span)) => {
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))
}
(Ready, Nir::Close(NirEntity::TplApply(_), span)) => {
Transition(Ready).ok(Air::TplEndRef(span))
}
(
Ready,
Nir::Close(
@ -120,12 +164,14 @@ impl ParseState for NirToAir {
(Ready, Nir::BindIdent(spair)) => {
Transition(Ready).ok(Air::BindIdent(spair))
}
(Ready, Nir::Ref(spair)) => {
Transition(Ready).ok(Air::RefIdent(spair))
}
(
Ready,
Nir::Todo
| Nir::TodoAttr(..)
| Nir::Ref(..)
| Nir::Desc(..)
| Nir::Text(_)
| Nir::Open(NirEntity::TplParam, _)

View File

@ -18,7 +18,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use crate::{parse::util::SPair, span::dummy::*};
use crate::{convert::ExpectInto, parse::util::SPair, span::dummy::*};
type Sut = NirToAir;
@ -137,3 +137,62 @@ fn tpl_with_name() {
Sut::parse(toks.into_iter()).collect(),
);
}
// 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]
fn apply_template_long_form() {
let name = SPair("_tpl-name_".into(), S2);
#[rustfmt::skip]
let toks = vec![
Nir::Open(NirEntity::TplApply(None), S1),
Nir::Ref(name),
Nir::Close(NirEntity::TplApply(None), S3),
];
#[rustfmt::skip]
assert_eq!(
Ok(vec![
O(Air::TplStart(S1)),
O(Air::RefIdent(name)),
O(Air::TplEndRef(S3)),
]),
Sut::parse(toks.into_iter()).collect(),
);
}

View File

@ -1678,10 +1678,14 @@ ele_parse! {
/// in favor of a transition to [`TplApplyShort`],
/// but this is still needed to support dynamic template application
/// (templates whose names are derived from other template inputs).
ApplyTemplate := QN_APPLY_TEMPLATE {
@ {} => Todo,
ApplyTemplate := QN_APPLY_TEMPLATE(_, ospan) {
@ {
QN_NAME => Ref,
} => Nir::Open(NirEntity::TplApply(None), ospan.into()),
/(cspan) => Nir::Close(NirEntity::TplApply(None), cspan.into()),
// TODO
// TODO: This is wrong, we just need something here for now.
AnyStmtOrExpr,
};
/// Short-hand template application.
@ -1691,8 +1695,9 @@ ele_parse! {
/// and where the body of this application is the `@values@`
/// template argument.
/// See [`ApplyTemplate`] for more information.
TplApplyShort := NS_T {
@ {} => Todo,
TplApplyShort := NS_T(qname, ospan) {
@ {} => Nir::Open(NirEntity::TplApply(Some(qname)), ospan.into()),
/(cspan) => Nir::Close(NirEntity::TplApply(None), cspan.into()),
// Streaming attribute parsing;
// this takes precedence over any attribute parsing above

View File

@ -50,11 +50,22 @@
<rate yields="tplStaticMix" />
<c:sum>
<c:product />
</c:sum>
</template>
<apply-template name="_short-hand-nullary_" />
</package>

View File

@ -58,5 +58,46 @@
<c:product />
</c:sum>
</template>
Short-hand template application.
These get expanding into the long form so that we don't have to translate
back and forth between the underscore-padded strings.
The fixpoint test will further verify that TAMER also recognizes the long
`apply-template` form,
asserting their equivalency.
<t:short-hand-nullary />
<!-- TODO
<t:short-hand-nullary-body>
<c:sum />
</t:short-hand-nullary-body>
<t:short-hand-nullary-inner>
<t:inner-short />
</t:short-hand-nullary-inner>
<t:short-hand foo="bar" />
<t:short-hand foo="bar">
<c:sum />
</t:short-hand>
<t:short-hand foo="bar">
<t:inner-short />
</t:short-hand>
<rate yields="shortHandTplInExpr">
<t:short-hand in="rate" />
</rate>
<template name="_tpl-with-short-hand-inner_">
<t:short-hand />
<c:sum>
<t:short-hand in="sum" />
</c:sum>
</template>
-->
</package>