diff --git a/tamer/src/asg/air/tpl.rs b/tamer/src/asg/air/tpl.rs index 00e2d128..a8268c83 100644 --- a/tamer/src/asg/air/tpl.rs +++ b/tamer/src/asg/air/tpl.rs @@ -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), + /// 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, TplState, AirExprAggregateStoreDangling, ), + /// Defining a template metavariable. + TplMeta( + ObjectIndex, + TplState, + AirExprAggregateStoreDangling, + ObjectIndex, + ), + /// Aggregating tokens into a template. TplExpr( ObjectIndex, @@ -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" ) } diff --git a/tamer/src/asg/air/tpl/test.rs b/tamer/src/asg/air/tpl/test.rs index b1d236d4..90c6e8e8 100644 --- a/tamer/src/asg/air/tpl/test.rs +++ b/tamer/src/asg/air/tpl/test.rs @@ -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::(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::(&asg).next()) + .map(ObjectIndex::cresolve(&asg)) + }) + .collect::>(); + + assert_eq!(params[0], Some(&Meta::Lexeme(S3.merge(S6).unwrap(), pval1))); + assert_eq!(params[1], Some(&Meta::Required(S7.merge(S9).unwrap()))); +} diff --git a/tamer/src/asg/graph.rs b/tamer/src/asg/graph.rs index e3f682b4..faef6dfa 100644 --- a/tamer/src/asg/graph.rs +++ b/tamer/src/asg/graph.rs @@ -640,13 +640,8 @@ impl Asg { ident: SPair, ) -> ObjectIndex { 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> { ] } +fn diagnostic_unknown_ident_desc(ident: SPair) -> Vec> { + 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; diff --git a/tamer/src/asg/graph/object.rs b/tamer/src/asg/graph/object.rs index 814fc175..ac9e3000 100644 --- a/tamer/src/asg/graph/object.rs +++ b/tamer/src/asg/graph/object.rs @@ -768,6 +768,25 @@ impl ObjectIndex { 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, + { + self.incoming_edges_filtered(asg) + .map(ObjectIndex::cresolve(asg)) + .next() + } } impl ObjectIndex { diff --git a/tamer/src/asg/graph/object/expr.rs b/tamer/src/asg/graph/object/expr.rs index fc910187..0f693dcc 100644 --- a/tamer/src/asg/graph/object/expr.rs +++ b/tamer/src/asg/graph/object/expr.rs @@ -249,12 +249,4 @@ impl ObjectIndex { 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() - } } diff --git a/tamer/src/asg/graph/object/ident.rs b/tamer/src/asg/graph/object/ident.rs index 87755f89..5e4c876d 100644 --- a/tamer/src/asg/graph/object/ident.rs +++ b/tamer/src/asg/graph/object/ident.rs @@ -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 { 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>(&self, asg: &Asg) -> bool { + self.edges_filtered::(asg).next().is_some() + } + /// The source package defining this identifier, /// if known. pub fn src_pkg(&self, asg: &Asg) -> Option> { diff --git a/tamer/src/asg/graph/object/meta.rs b/tamer/src/asg/graph/object/meta.rs index 7edb91ba..4de49614 100644 --- a/tamer/src/asg/graph/object/meta.rs +++ b/tamer/src/asg/graph/object/meta.rs @@ -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 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 { + pub fn identify_as_tpl_param( + &self, + asg: &mut Asg, + oi_tpl: ObjectIndex, + name: SPair, + ) -> ObjectIndex { + 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) + }) + }) + } +} diff --git a/tamer/src/asg/graph/object/tpl.rs b/tamer/src/asg/graph/object/tpl.rs index 0486753d..d6466b3c 100644 --- a/tamer/src/asg/graph/object/tpl.rs +++ b/tamer/src/asg/graph/object/tpl.rs @@ -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, } } diff --git a/tamer/src/asg/graph/visit.rs b/tamer/src/asg/graph/visit.rs index 989792d1..8791dcfc 100644 --- a/tamer/src/asg/graph/visit.rs +++ b/tamer/src/asg/graph/visit.rs @@ -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`]. /// diff --git a/tamer/src/asg/graph/visit/test.rs b/tamer/src/asg/graph/visit/test.rs index 99857a29..25a25387 100644 --- a/tamer/src/asg/graph/visit/test.rs +++ b/tamer/src/asg/graph/visit/test.rs @@ -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), ); diff --git a/tamer/src/asg/graph/xmli.rs b/tamer/src/asg/graph/xmli.rs index 3f85ad47..0b2f118c 100644 --- a/tamer/src/asg/graph/xmli.rs +++ b/tamer/src/asg/graph/xmli.rs @@ -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::(self.asg); + // TODO: This doesn't handle `Missing` or invalid (non-`Tpl`) + // identifiers. + let mut idents = oi_tpl + .edges_filtered::(self.asg) + .filter(|oi| oi.is_bound_to_kind::(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, + depth: Depth, + ) -> Option { + 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::*; diff --git a/tamer/src/nir/parse.rs b/tamer/src/nir/parse.rs index 01c0dd0a..07239b49 100644 --- a/tamer/src/nir/parse.rs +++ b/tamer/src/nir/parse.rs @@ -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, diff --git a/tamer/src/sym/prefill.rs b/tamer/src/sym/prefill.rs index 9675f046..4e86fbc0 100644 --- a/tamer/src/sym/prefill.rs +++ b/tamer/src/sym/prefill.rs @@ -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", diff --git a/tamer/src/xir/st.rs b/tamer/src/xir/st.rs index 8b0d6bb1..8f9b7e6a 100644 --- a/tamer/src/xir/st.rs +++ b/tamer/src/xir/st.rs @@ -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, diff --git a/tamer/tests/xmli/template/expected.xml b/tamer/tests/xmli/template/expected.xml index 6e646237..23586d3d 100644 --- a/tamer/tests/xmli/template/expected.xml +++ b/tamer/tests/xmli/template/expected.xml @@ -66,6 +66,19 @@ +