tamer: asg: Shorthand and long-form template arguments

This applies to template application only; there's still some work to do for
template parameters in definitions (well, for deriving them in `xmli` at
least).  And, as you can see, there's still a lot of TODO items here.

I ended up backtracking on tree edges to Meta, and even on cross edges to
Meta, because it complicated xmli derivation with no benefit right now;
maybe a cross edge will be re-added in the future, but I need to move on and
see where this takes me.

But, it works.

DEV-13708
Mike Gerwitz 2023-03-23 00:04:53 -04:00
parent b2c6b7f073
commit c26444666f
16 changed files with 381 additions and 54 deletions

View File

@ -31,6 +31,7 @@ use super::{
AirExprAggregate,
};
use crate::{
asg::graph::object::Meta,
diagnose::Annotate,
diagnostic_todo,
fmt::{DisplayWrapper, TtQuote},
@ -63,12 +64,26 @@ pub enum AirTplAggregate {
/// which simplifies AIR generation.
Ready(ObjectIndex<Pkg>),
/// Toplevel template context.
///
/// Conceptually,
/// tokens that are received in this state are interpreted as directly
/// applying to the template itself,
/// or creating an object directly owned by the template.
Toplevel(
ObjectIndex<Pkg>,
TplState,
AirExprAggregateStoreDangling<Tpl>,
),
/// Defining a template metavariable.
TplMeta(
ObjectIndex<Pkg>,
TplState,
AirExprAggregateStoreDangling<Tpl>,
ObjectIndex<Meta>,
),
/// Aggregating tokens into a template.
TplExpr(
ObjectIndex<Pkg>,
@ -85,6 +100,10 @@ impl Display for AirTplAggregate {
Self::Toplevel(_, tpl, expr) | Self::TplExpr(_, tpl, expr) => {
write!(f, "building {tpl} with {expr}")
}
Self::TplMeta(_, tpl, _, _) => {
write!(f, "building {tpl} metavariable")
}
}
}
}
@ -202,17 +221,69 @@ impl ParseState for AirTplAggregate {
Transition(Toplevel(oi_pkg, tpl, expr)).incomplete()
}
(Toplevel(..), tok @ AirTpl(TplMetaStart(..) | TplMetaEnd(..))) => {
(Toplevel(oi_pkg, tpl, expr), AirTpl(TplMetaStart(span))) => {
let oi_meta = asg.create(Meta::new_required(span));
Transition(TplMeta(oi_pkg, tpl, expr, oi_meta)).incomplete()
}
(
TplMeta(oi_pkg, tpl, expr, oi_meta),
AirTpl(TplMetaEnd(cspan)),
) => {
oi_meta.close(asg, cspan);
Transition(Toplevel(oi_pkg, tpl, expr)).incomplete()
}
(
TplMeta(oi_pkg, tpl, expr, oi_meta),
AirTpl(TplLexeme(lexeme)),
) => Transition(TplMeta(
oi_pkg,
tpl,
expr,
oi_meta.assign_lexeme(asg, lexeme),
))
.incomplete(),
(TplMeta(oi_pkg, tpl, expr, oi_meta), AirBind(BindIdent(name))) => {
oi_meta.identify_as_tpl_param(asg, tpl.oi(), name);
Transition(TplMeta(oi_pkg, tpl, expr, oi_meta)).incomplete()
}
(TplMeta(..), tok @ AirBind(RefIdent(..))) => {
diagnostic_todo!(
vec![tok.note("for this token")],
"Toplevel meta"
vec![tok.note("this token")],
"AirBind in metavar context (param-value)"
)
}
(TplMeta(..), tok @ AirExpr(..)) => {
diagnostic_todo!(
vec![tok.note("this token")],
"AirExpr in metavar context (e.g. @values@)"
)
}
(
TplMeta(..),
tok @ AirTpl(
TplStart(..) | TplMetaStart(..) | TplEnd(..)
| TplEndRef(..),
),
) => {
diagnostic_todo!(vec![tok.note("this token")], "AirTpl variant")
}
(Toplevel(..), tok @ AirTpl(TplMetaEnd(..))) => {
diagnostic_todo!(
vec![tok.note("this token")],
"unbalanced meta"
)
}
(Toplevel(..), tok @ AirTpl(TplLexeme(..))) => {
diagnostic_todo!(
vec![tok.note("for this token")],
"err: Toplevel lexeme {tok:?} (must be within metavar)"
vec![tok.note("this token")],
"err: TplLexeme outside of metavar"
)
}

View File

@ -24,6 +24,7 @@ use crate::asg::{
test::{asg_from_toks, parse_as_pkg_body},
Air, AirAggregate,
},
graph::object::Meta,
Expr, ExprOp, Ident,
};
use crate::span::dummy::*;
@ -402,3 +403,48 @@ fn anonymous_tpl_immediate_ref() {
// TODO: More to come.
}
#[test]
fn tpl_with_param() {
let id_tpl = SPair("_tpl_".into(), S2);
let id_param1 = SPair("@param1@".into(), S4);
let pval1 = SPair("value1".into(), S5);
let id_param2 = SPair("@param2@".into(), S8);
#[rustfmt::skip]
let toks = vec![
Air::TplStart(S1),
Air::BindIdent(id_tpl),
// Metavariable with a value.
Air::TplMetaStart(S3),
Air::BindIdent(id_param1),
Air::TplLexeme(pval1),
Air::TplMetaEnd(S6),
// Required metavariable (no value).
Air::TplMetaStart(S7),
Air::BindIdent(id_param2),
Air::TplMetaEnd(S9),
Air::TplEnd(S10),
];
let asg = asg_from_toks(toks);
let oi_tpl = asg.expect_ident_oi::<Tpl>(id_tpl);
// 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(ObjectIndex::cresolve(&asg))
})
.collect::<Vec<_>>();
assert_eq!(params[0], Some(&Meta::Lexeme(S3.merge(S6).unwrap(), pval1)));
assert_eq!(params[1], Some(&Meta::Required(S7.merge(S9).unwrap())));
}

View File

@ -640,13 +640,8 @@ impl Asg {
ident: SPair,
) -> ObjectIndex<O> {
self.get_ident_oi(ident).diagnostic_expect(
|| diagnostic_opaque_ident_desc(ident),
|| {
format!(
"opaque identifier: {} has no object binding",
TtQuote::wrap(ident),
)
},
|| diagnostic_unknown_ident_desc(ident),
|| format!("unknown identifier {}", TtQuote::wrap(ident),),
)
}
@ -828,5 +823,15 @@ fn diagnostic_opaque_ident_desc(ident: SPair) -> Vec<AnnotatedSpan<'static>> {
]
}
fn diagnostic_unknown_ident_desc(ident: SPair) -> Vec<AnnotatedSpan<'static>> {
vec![
ident.internal_error("reference to an unknown identifier"),
ident.help(
"the system expects this identifier to be known, \
but it could not be found.",
),
]
}
#[cfg(test)]
mod test;

View File

@ -768,6 +768,25 @@ impl<O: ObjectKind> ObjectIndex<O> {
asg.create(Ident::declare(name))
.add_edge_from(asg, *self, None)
}
/// Retrieve the identifier for this object,
/// if any.
///
/// If there is more than one identifier,
/// only one will be returned,
/// and the result of the operation is undefined.
/// This can be problematic if certain optimizations have been performed
/// on the graph,
/// like common subexpression elimination,
/// in which case it's best not to rely on following edges in reverse.
pub fn ident<'a>(&self, asg: &'a Asg) -> Option<&'a Ident>
where
O: ObjectRelFrom<Ident>,
{
self.incoming_edges_filtered(asg)
.map(ObjectIndex::cresolve(asg))
.next()
}
}
impl ObjectIndex<Object> {

View File

@ -249,12 +249,4 @@ impl ObjectIndex<Expr> {
let identi = asg.lookup_global_or_missing(ident);
self.add_edge_to(asg, identi, Some(ident.span()))
}
/// The [`Ident`] bound to this expression,
/// if any.
pub fn ident(self, asg: &Asg) -> Option<&Ident> {
self.incoming_edges_filtered(asg)
.map(ObjectIndex::cresolve(asg))
.next()
}
}

View File

@ -982,9 +982,7 @@ object_rel! {
tree Ident,
tree Expr,
tree Tpl,
// A metavariable is directly referenced by a template.
cross Meta,
tree Meta,
}
}
@ -1060,6 +1058,14 @@ impl ObjectIndex<Ident> {
self.edges(asg).find_map(ObjectRel::narrow) == Some(oi)
}
/// Whether this identifier is bound to an object of kind `O`.
///
/// To bind an identifier,
/// see [`Self::bind_definition`].
pub fn is_bound_to_kind<O: ObjectRelFrom<Ident>>(&self, asg: &Asg) -> bool {
self.edges_filtered::<O>(asg).next().is_some()
}
/// The source package defining this identifier,
/// if known.
pub fn src_pkg(&self, asg: &Asg) -> Option<ObjectIndex<Pkg>> {

View File

@ -26,11 +26,15 @@
use super::{
Ident, Object, ObjectIndex, ObjectRel, ObjectRelFrom, ObjectRelTy,
ObjectRelatable,
ObjectRelatable, Tpl,
};
use crate::{
asg::Asg,
diagnose::Annotate,
diagnostic_todo,
f::Functor,
fmt::{DisplayWrapper, TtQuote},
parse::{util::SPair, Token},
parse::util::SPair,
span::Span,
};
use std::fmt::Display;
@ -51,16 +55,54 @@ use std::fmt::Display;
pub enum Meta {
Required(Span),
ConcatList(Span),
Lexeme(SPair),
Lexeme(Span, SPair),
}
impl Meta {
/// Create a new metavariable without a value.
///
/// Metavariables with no value cannot be used in an expansion context.
/// Intuitively,
/// they act as required parameters.
pub fn new_required(span: Span) -> Self {
Self::Required(span)
}
pub fn span(&self) -> Span {
match self {
Self::Required(span) | Self::ConcatList(span) => *span,
Self::Lexeme(spair) => spair.span(),
Self::Required(span)
| Self::ConcatList(span)
| Self::Lexeme(span, _) => *span,
}
}
/// Assign a lexeme to a metavariable.
///
/// In a template definition context,
/// this acts as a default value for this metavariable.
/// In an application context,
/// this has the effect of binding a value to this metavariable.
pub fn assign_lexeme(self, lexeme: SPair) -> Self {
match self {
Self::Required(span) => Self::Lexeme(span, lexeme),
Self::ConcatList(_) => diagnostic_todo!(
vec![lexeme.note("while parsing this lexeme")],
"append to ConcatList",
),
Self::Lexeme(_, _) => diagnostic_todo!(
vec![lexeme.note("while parsing this lexeme")],
"Lexeme => ConcatList",
),
}
}
}
impl From<&Meta> for Span {
fn from(meta: &Meta) -> Self {
meta.span()
}
}
impl Display for Meta {
@ -72,7 +114,19 @@ impl Display for Meta {
Self::ConcatList(_) => {
write!(f, "metasyntactic concatenation list")
}
Self::Lexeme(spair) => write!(f, "lexeme {}", TtQuote::wrap(spair)),
Self::Lexeme(_, spair) => {
write!(f, "lexeme {}", TtQuote::wrap(spair))
}
}
}
}
impl Functor<Span> for Meta {
fn map(self, f: impl FnOnce(Span) -> Span) -> Self::Target {
match self {
Self::Required(span) => Self::Required(f(span)),
Self::ConcatList(span) => Self::ConcatList(f(span)),
Self::Lexeme(span, spair) => Self::Lexeme(f(span), spair),
}
}
}
@ -85,3 +139,28 @@ object_rel! {
cross Ident,
}
}
impl ObjectIndex<Meta> {
pub fn identify_as_tpl_param(
&self,
asg: &mut Asg,
oi_tpl: ObjectIndex<Tpl>,
name: SPair,
) -> ObjectIndex<Ident> {
oi_tpl
.declare_local(asg, name)
.add_edge_to(asg, *self, None)
}
pub fn assign_lexeme(self, asg: &mut Asg, lexeme: SPair) -> Self {
self.map_obj(asg, |meta| meta.assign_lexeme(lexeme))
}
pub fn close(self, asg: &mut Asg, close_span: Span) -> Self {
self.map_obj(asg, |meta| {
meta.map(|open_span| {
open_span.merge(close_span).unwrap_or(open_span)
})
})
}
}

View File

@ -22,8 +22,8 @@
use std::fmt::Display;
use super::{
Expr, Ident, Meta, Object, ObjectIndex, ObjectRel, ObjectRelFrom,
ObjectRelTo, ObjectRelTy, ObjectRelatable,
Expr, Ident, Object, ObjectIndex, ObjectRel, ObjectRelFrom, ObjectRelTo,
ObjectRelTy, ObjectRelatable,
};
use crate::{
asg::Asg,
@ -66,10 +66,13 @@ object_rel! {
/// Templates may expand into nearly any context,
/// and must therefore be able to contain just about anything.
Tpl -> {
// Expressions must be able to be anonymous to allow templates in
// any `Expr` context.
tree Expr,
tree Meta,
dyn Ident,
// Identifiers are used for both references and identifiers that
// will expand into an application site.
dyn Ident,
}
}

View File

@ -110,6 +110,13 @@ use super::object::ObjectRel;
/// may modify the graph beyond recognition,
/// though they should retain ordering where it is important.
///
/// _Objects that do not have a path from the root will not be visited by
/// this traversal._
/// These objects are expected to act as additional metadata,
/// and must be queried for explicitly.
/// Such querying can be done during the traversal since this visitor holds
/// only a shared immutable reference to the [`Asg`].
///
/// For more information,
/// see [`ObjectRel::is_cross_edge`].
///

View File

@ -196,6 +196,8 @@ fn traverses_ontological_tree_tpl_apply() {
let name_tpl = "_tpl-to-apply_".into();
let id_tpl = SPair(name_tpl, S3);
let ref_tpl = SPair(name_tpl, S6);
let id_param = SPair("@param@".into(), S8);
let value_param = SPair("value".into(), S9);
#[rustfmt::skip]
let toks = vec![
@ -213,8 +215,13 @@ fn traverses_ontological_tree_tpl_apply() {
// metavariables.
TplStart(S5),
RefIdent(ref_tpl),
TplEndRef(S7), // notice the `Ref` at the end
PkgEnd(S8),
TplMetaStart(S7),
BindIdent(id_param),
TplLexeme(value_param),
TplMetaEnd(S10),
TplEndRef(S11), // notice the `Ref` at the end
PkgEnd(S12),
];
// We need more concise expressions for the below table of values.
@ -225,11 +232,13 @@ fn traverses_ontological_tree_tpl_apply() {
assert_eq!(
// A -|-> B | A span -|-> B span | espan | depth
vec![//-----|-------|-----------|-----------|--------|-----------------
(d(Root, Pkg, SU, m(S1, S8), None ), Depth(1)),
(d(Pkg, Ident, m(S1, S8), S3, None ), Depth(2)),
(d(Root, Pkg, SU, m(S1, S12), None ), Depth(1)),
(d(Pkg, Ident, m(S1, S12), S3, None ), Depth(2)),
(d(Ident, Tpl, S3, m(S2, S4), None ), Depth(3)),
(d(Pkg, Tpl, m(S1, S8), m(S5, S7), None ), Depth(2)),
(d(Tpl, Ident, m(S5, S7), S3, Some(S6)), Depth(3)),
(d(Pkg, Tpl, m(S1, S12), m(S5, S11), None ), Depth(2)),
/*cross*/ (d(Tpl, Ident, m(S5, S11), S3, Some(S6)), Depth(3)),
(d(Tpl, Ident, m(S5, S11), S8, None ), Depth(3)),
(d(Ident, Meta, S8, m(S7, S10), None ), Depth(4)),
],
tree_reconstruction_report(toks),
);

View File

@ -30,8 +30,8 @@
//! or observing template expansions.
use super::object::{
DynObjectRel, Expr, Object, ObjectIndex, ObjectRelTy, OiPairObjectInner,
Pkg, Tpl,
DynObjectRel, Expr, Meta, Object, ObjectIndex, ObjectRelTy,
OiPairObjectInner, Pkg, Tpl,
};
use crate::{
asg::{
@ -39,7 +39,8 @@ use crate::{
Asg, ExprOp, Ident,
},
diagnose::{panic::DiagnosticPanic, Annotate},
diagnostic_panic, diagnostic_unreachable,
diagnostic_panic, diagnostic_todo, diagnostic_unreachable,
fmt::{DisplayWrapper, TtQuote},
parse::{prelude::*, util::SPair, Transitionable},
span::{Span, UNKNOWN_SPAN},
sym::{
@ -198,7 +199,9 @@ impl<'a> TreeContext<'a> {
self.emit_template(tpl, *oi_tpl, paired_rel.source(), depth)
}
target @ Object::Meta(..) => todo!("Object::Meta: {target:?}"),
Object::Meta((meta, oi_meta)) => {
self.emit_tpl_arg(meta, *oi_meta, depth)
}
Object::Root(..) => diagnostic_unreachable!(
vec![],
@ -312,7 +315,11 @@ impl<'a> TreeContext<'a> {
// 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);
// 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));
let apply_tpl = idents.next().diagnostic_expect(
|| {
@ -332,7 +339,7 @@ impl<'a> TreeContext<'a> {
bad_ident
.internal_error("unexpected second identifier"),
],
"expected only one Ident for template application",
"expected only one Ident->Tpl for template application",
);
}
@ -349,6 +356,47 @@ impl<'a> TreeContext<'a> {
}
}
/// Emit a long-form template argument.
///
/// For the parent template application,
/// see [`Self::emit_template`].
fn emit_tpl_arg(
&mut self,
meta: &Meta,
oi_meta: ObjectIndex<Meta>,
depth: Depth,
) -> Option<Xirf> {
let pname = oi_meta.ident(self.asg).map(Ident::name)
.diagnostic_unwrap(|| vec![meta.internal_error(
"anonymous metavariables are not supported as template arguments"
)]);
let pval = match meta {
Meta::Required(span) => diagnostic_todo!(
vec![span.error("value expected for this param")],
"value missing for param {}",
TtQuote::wrap(pname)
),
Meta::ConcatList(span) => diagnostic_todo!(
vec![span.error("concatenation occurs here")],
"concatenation not yet supported in xmli for param {}",
TtQuote::wrap(pname)
),
Meta::Lexeme(_, value) => *value,
};
self.push(attr_value(pval));
self.push(attr_name(pname));
Some(Xirf::open(
QN_WITH_PARAM,
OpenSpan::without_name_span(meta.span()),
depth,
))
}
fn push(&mut self, tok: Xirf) {
if self.stack.is_full() {
diagnostic_panic!(
@ -408,6 +456,10 @@ fn attr_name(name: SPair) -> Xirf {
Xirf::attr(QN_NAME, name, (name.span(), name.span()))
}
fn attr_value(val: SPair) -> Xirf {
Xirf::attr(QN_VALUE, val, (val.span(), val.span()))
}
fn expr_ele(expr: &Expr, depth: Depth) -> Xirf {
use ExprOp::*;

View File

@ -1674,21 +1674,34 @@ ele_parse! {
/// or even a mix of the two
/// (with statements hoisted out of expressions).
///
/// TODO: This is apparently unused by the current system,
/// 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).
/// See also [`TplApplyShort`],
/// which gets desugared into this via [`super::tplshort`].
ApplyTemplate := QN_APPLY_TEMPLATE(_, ospan) {
@ {
QN_NAME => Ref,
} => Nir::Open(NirEntity::TplApply(None), ospan.into()),
/(cspan) => Nir::Close(NirEntity::TplApply(None), cspan.into()),
// TODO: This is wrong, we just need something here for now.
AnyStmtOrExpr,
ApplyTemplateParam,
};
/// Short-hand template application.
/// Long-form template argument.
///
/// Template arguments are lexical.
///
/// See also [`TplApplyShort`],
/// which gets desugared into this via [`super::tplshort`].
ApplyTemplateParam := QN_WITH_PARAM(_, ospan) {
@ {
QN_NAME => BindIdent,
QN_VALUE => Text,
} => Nir::Open(NirEntity::TplParam(None), ospan.into()),
/(cspan) => Nir::Close(NirEntity::TplParam(None), cspan.into()),
// TODO: Need to support children, e.g. @values@
};
/// Shorthand template application.
///
/// This expands into an equivalent [`ApplyTemplate`] form where each
/// attribute is a template argument,

View File

@ -710,6 +710,7 @@ pub mod st {
L_VIRTUAL: cid "virtual",
L_WARNING: cid "warning",
L_WHEN: cid "when",
L_WITH_PARAM: tid "with-param",
L_WORKSHEET: cid "worksheet",
L_XMLNS: cid "xmlns",
L_YIELD: cid "yield",

View File

@ -225,6 +225,7 @@ pub mod qname {
QN_VALUES: :L_VALUES,
QN_VIRTUAL: :L_VIRTUAL,
QN_WARNING: :L_WARNING,
QN_WITH_PARAM: :L_WITH_PARAM,
QN_WORKSHEET: :L_WORKSHEET,
QN_YIELD: :L_YIELD,
QN_YIELDS: :L_YIELDS,

View File

@ -66,6 +66,19 @@
<template name="_short-hand-nullary_" />
<apply-template name="_short-hand-nullary_" />
<template name="_short-hand-unary_" />
<apply-template name="_short-hand-unary_">
<with-param name="@foo@" value="bar" />
</apply-template>
<template name="_short-hand-nary_" />
<apply-template name="_short-hand-nary_">
<with-param name="@foo@" value="bar" />
<with-param name="@bar@" value="baz" />
<with-param name="@baz@" value="quux" />
</apply-template>
</package>

View File

@ -66,8 +66,20 @@
`apply-template` form,
asserting their equivalency.
<template name="_short-hand-nullary_" />
<t:short-hand-nullary />
<template name="_short-hand-unary_" />
<t:short-hand-unary foo="bar" />
<template name="_short-hand-nary_" />
<t:short-hand-nary foo="bar" bar="baz" baz="quux" />
<!-- TODO
<t:short-hand-nullary-body>
<c:sum />
@ -77,8 +89,6 @@
<t:inner-short />
</t:short-hand-nullary-inner>
<t:short-hand foo="bar" />
<t:short-hand foo="bar">
<c:sum />
</t:short-hand>