diff --git a/tamer/src/asg/air/tpl.rs b/tamer/src/asg/air/tpl.rs index 9bfffd08..b40c28ed 100644 --- a/tamer/src/asg/air/tpl.rs +++ b/tamer/src/asg/air/tpl.rs @@ -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))) } ( diff --git a/tamer/src/asg/air/tpl/test.rs b/tamer/src/asg/air/tpl/test.rs index 478a5387..d6e6009c 100644 --- a/tamer/src/asg/air/tpl/test.rs +++ b/tamer/src/asg/air/tpl/test.rs @@ -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::>(), ); } + +// 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::>(), + ); + + let asg = sut.finalize().unwrap().into_context(); + + // Let's make sure that the template created after recovery succeeded. + asg.expect_ident_obj::(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. +} diff --git a/tamer/src/asg/error.rs b/tamer/src/asg/error.rs index 015788dd..bab120a6 100644 --- a/tamer/src/asg/error.rs +++ b/tamer/src/asg/error.rs @@ -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")] } diff --git a/tamer/src/asg/graph/object/pkg.rs b/tamer/src/asg/graph/object/pkg.rs index 7f522257..0ae189ad 100644 --- a/tamer/src/asg/graph/object/pkg.rs +++ b/tamer/src/asg/graph/object/pkg.rs @@ -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, } } diff --git a/tamer/src/asg/graph/object/tpl.rs b/tamer/src/asg/graph/object/tpl.rs index 41bf68c3..5c11ddf6 100644 --- a/tamer/src/asg/graph/object/tpl.rs +++ b/tamer/src/asg/graph/object/tpl.rs @@ -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 { - /// 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 { }) }) } + + /// 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>( + self, + asg: &mut Asg, + oi_target_parent: ObjectIndex, + ) -> Self { + self.add_edge_from(asg, oi_target_parent, None) + } } diff --git a/tamer/src/asg/graph/xmli.rs b/tamer/src/asg/graph/xmli.rs index 8f36fb5d..3f85ad47 100644 --- a/tamer/src/asg/graph/xmli.rs +++ b/tamer/src/asg/graph/xmli.rs @@ -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, src: &Object, depth: Depth, ) -> Option { @@ -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::(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:?}"), } } diff --git a/tamer/src/nir.rs b/tamer/src/nir.rs index ce251469..e8383bea 100644 --- a/tamer/src/nir.rs +++ b/tamer/src/nir.rs @@ -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), } 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()) + ), } } } diff --git a/tamer/src/nir/air.rs b/tamer/src/nir/air.rs index 54947235..896ed804 100644 --- a/tamer/src/nir/air.rs +++ b/tamer/src/nir/air.rs @@ -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; + 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 { 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 + return 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 { + 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, _) diff --git a/tamer/src/nir/air/test.rs b/tamer/src/nir/air/test.rs index ffd26a17..34a114a8 100644 --- a/tamer/src/nir/air/test.rs +++ b/tamer/src/nir/air/test.rs @@ -18,7 +18,7 @@ // along with this program. If not, see . 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(), + ); +} diff --git a/tamer/src/nir/parse.rs b/tamer/src/nir/parse.rs index 5174949d..d97d8dff 100644 --- a/tamer/src/nir/parse.rs +++ b/tamer/src/nir/parse.rs @@ -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 diff --git a/tamer/tests/xmli/template/expected.xml b/tamer/tests/xmli/template/expected.xml index 67fb1958..6e646237 100644 --- a/tamer/tests/xmli/template/expected.xml +++ b/tamer/tests/xmli/template/expected.xml @@ -50,11 +50,22 @@ + + + + + + + + + + + diff --git a/tamer/tests/xmli/template/src.xml b/tamer/tests/xmli/template/src.xml index b1c5a757..833dc76a 100644 --- a/tamer/tests/xmli/template/src.xml +++ b/tamer/tests/xmli/template/src.xml @@ -58,5 +58,46 @@ + + 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. + + + +