// XML representation of graph objects // // 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 . //! XML representation of graph objects via [XIR](crate::xir). //! //! Attempts to produce a representation of graph [`Object`]s that is //! familiar to those writing TAME's XML-based source language. //! //! _This representation will change over time_ as TAME's source language //! evolves. //! There is no guarantee that this representation will stay over time; //! it was written for transitional purposes, //! but may be useful in the future for concrete code suggestions/fixes, //! or observing template expansions. use super::object::{ Doc, DynObjectRel, Expr, Meta, Object, ObjectIndex, ObjectRelTy, OiPairObjectInner, Pkg, Tpl, }; use crate::{ asg::{ visit::{Depth, TreeWalkRel}, Asg, ExprOp, Ident, }, diagnose::{panic::DiagnosticPanic, Annotate}, diagnostic_panic, diagnostic_todo, diagnostic_unreachable, fmt::{DisplayWrapper, TtQuote}, parse::{prelude::*, util::SPair, Transitionable}, span::{Span, UNKNOWN_SPAN}, sym::{ st::{URI_LV_CALC, URI_LV_RATER, URI_LV_TPL}, UriStaticSymbolId, }, xir::{ flat::{Text, XirfToken}, st::qname::*, CloseSpan, OpenSpan, QName, }, }; use arrayvec::ArrayVec; use std::{convert::Infallible, fmt::Display, marker::PhantomData}; #[derive(Debug, PartialEq, Eq)] pub enum AsgTreeToXirf<'a> { Ready(PhantomData<&'a ()>), } impl<'a> Default for AsgTreeToXirf<'a> { fn default() -> Self { Self::Ready(PhantomData::default()) } } impl<'a> Display for AsgTreeToXirf<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "generating XIRF sources from ASG tree") } } type Xirf = XirfToken; impl<'a> ParseState for AsgTreeToXirf<'a> { type Token = TreeWalkRel; type Object = Xirf; type Error = Infallible; type Context = TreeContext<'a>; fn parse_token( self, TreeWalkRel(dyn_rel, depth): Self::Token, ctx: &mut TreeContext, ) -> TransitionResult { match (ctx.pop(), depth) { // Empty the token stack before processing any further. // Note that we must yield the token as lookahead to ensure that // we do eventually process it. (Some(emit), _) => Transition(self) .ok(emit) .with_lookahead(TreeWalkRel(dyn_rel, depth)), // Used by `eof_tok` only to empty the token stack, // which we're now done with. // We consume the token by not yielding any lookahead. (None, Depth(0)) => Transition(self).incomplete(), // The stack is empty, // so proceed with processing the provided relation. (None, depth) => ctx.derive_src(dyn_rel, depth).transition(self), } } fn is_accepting(&self, ctx: &Self::Context) -> bool { ctx.stack_is_empty() } fn eof_tok(&self, ctx: &Self::Context) -> Option { // If the stack is not empty on EOF, // yield a dummy token just to invoke `parse_token` to finish // emptying it. (!ctx.stack_is_empty()).then_some(TreeWalkRel( DynObjectRel::new( ObjectRelTy::Root, ObjectRelTy::Root, ObjectIndex::new(0.into(), UNKNOWN_SPAN), ObjectIndex::new(0.into(), UNKNOWN_SPAN), None, ), // This is the only part that really matters; // the tree walk will never yield a depth of 0. Depth(0), )) } } /// Size of the token stack. /// /// See [`TokenStack`] for more information. const TOK_STACK_SIZE: usize = 8; /// Token stack to hold generated tokens between [`AsgTreeToXirf`] /// iterations. /// /// The token stack is used to avoid having to create separate states for /// emitting each individual token. /// It is populated by [`AsgTreeToXirf`] if more than a single [`XirfToken`] /// needs to be emitted, /// and tokens are removed on each subsequent iteration until empty. /// /// This need only be big enough to accommodate [`AsgTreeToXirf`]'s /// implementation; /// the size is independent of user input. type TokenStack = ArrayVec; pub struct TreeContext<'a> { stack: TokenStack, asg: &'a Asg, } impl<'a> TreeContext<'a> { /// Given a [`DynObjectRel`], /// derive a legacy TAME representation that will faithfully represent /// an equivalent program when compiled by the XSLT-based TAME /// compiler. /// /// The [`TokenStack`] may be used to pre-generate [XIRF](Xirf) to be /// yielded on subsequent iterations rather than having to introduce /// [`AsgTreeToXirf`] states for each individual token. /// Adjust [`TOK_STACK_SIZE`] as necessary. /// /// The provided [`Depth`] represent the depth of the tree at the /// position of the provided [`DynObjectRel`]. /// See [`TreeWalkRel`] for more information. fn derive_src( &mut self, dyn_rel: DynObjectRel, depth: Depth, ) -> Option { // TODO: Verify that the binary does not perform unnecessary // resolution in branches that do not utilize the source. let paired_rel = dyn_rel.resolve_oi_pairs(self.asg); match paired_rel.target() { Object::Pkg((pkg, oi_pkg)) => match paired_rel.source() { Object::Root(_) => self.emit_package(pkg, depth), Object::Pkg(_) => self.emit_import(pkg, depth), _ => diagnostic_panic!( vec![oi_pkg.error("package was not expected here")], "invalid context for package object during xmli derivation", ), }, // Identifiers will be considered in context; // pass over it for now. // But we must not skip over its depth, // otherwise we parent a following sibling at a matching // depth; // this close will force the auto-closing system to close // any siblings in preparation for the object to follow. Object::Ident((ident, _)) => Some(Xirf::Close( None, CloseSpan::without_name_span(ident.span()), depth, )), Object::Expr((expr, oi_expr)) => { self.emit_expr(expr, *oi_expr, paired_rel.source(), depth) } Object::Tpl((tpl, oi_tpl)) => { self.emit_template(tpl, *oi_tpl, paired_rel.source(), depth) } Object::Meta((meta, oi_meta)) => { self.emit_tpl_arg(meta, *oi_meta, depth) } Object::Doc((doc, oi_doc)) => { self.emit_doc(doc, *oi_doc, paired_rel.source(), depth) } Object::Root(..) => diagnostic_unreachable!( vec![], "tree walk is not expected to emit Root", ), } } /// Emit tokens representing the root package element. fn emit_package(&mut self, pkg: &Pkg, depth: Depth) -> Option { let span = pkg.span(); self.push_all([ ns(QN_XMLNS_T, URI_LV_TPL, span), ns(QN_XMLNS_C, URI_LV_CALC, span), ns(QN_XMLNS, URI_LV_RATER, span), ]); Some(package(pkg, depth)) } /// Emit a package import statement. /// /// The import will have its path canonicalized. fn emit_import(&mut self, pkg: &Pkg, depth: Depth) -> Option { let ps = pkg.canonical_name(); self.push(Xirf::attr(QN_PACKAGE, ps.symbol(), (ps.span(), ps.span()))); Some(Xirf::open( QN_IMPORT, OpenSpan::without_name_span(pkg.span()), depth, )) } /// Emit an expression as a legacy TAME statement or expression. /// /// Identified expressions must be represented using statements in /// legacy TAME, /// such as ``. /// Anonymous expressions are nested within statements. /// /// This system will emit statements and expressions that are compatible /// with the information on the [ASG](crate::asg) and recognized by the /// downstream XSLT-based compiler. /// There is no guarantee, /// however, /// that what is emitted is exactly representative of what the user /// originally entered. /// /// Please ensure that the system matches your expectations using the system /// tests in `:tamer/tests/xmli`. fn emit_expr( &mut self, expr: &Expr, oi_expr: ObjectIndex, src: &Object, depth: Depth, ) -> Option { match src { Object::Ident((ident, _)) => { self.emit_expr_ident(expr, ident, depth) } Object::Expr((pexpr, _)) => match (pexpr.op(), expr.op()) { (ExprOp::Conj | ExprOp::Disj, ExprOp::Eq) => { Some(self.emit_match(expr, oi_expr, depth)) } _ => Some(expr_ele(expr, oi_expr, depth)), }, // TODO: See `:tamer/tests/xmli/template` regarding `match` and // `when`/`c:*`; // this is not an ambiguity that can be resolved without // adding more information to the graph, // but is hopefully one that we can avoid. Object::Tpl(..) => match expr.op() { ExprOp::Eq => Some(self.emit_match(expr, oi_expr, depth)), _ => Some(expr_ele(expr, oi_expr, depth)), }, // TODO: Perhaps errors for Root, Meta, and Doc? Object::Root(_) | Object::Pkg(_) | Object::Meta(_) | Object::Doc(_) => Some(expr_ele(expr, oi_expr, depth)), } } /// Emit an identified expression. /// /// Legacy TAME is only able to bind certain identifiers via statements /// such as `rate` and `classify`. fn emit_expr_ident( &mut self, expr: &Expr, ident: &Ident, depth: Depth, ) -> Option { let (qname, ident_qname) = match expr.op() { ExprOp::Sum => (QN_RATE, QN_YIELDS), ExprOp::Conj => (QN_CLASSIFY, QN_AS), ExprOp::Product | ExprOp::Ceil | ExprOp::Floor | ExprOp::Disj | ExprOp::Eq => { todo!("stmt: {expr:?}") } }; let ispan = ident.span(); self.push(Xirf::attr(ident_qname, ident.name(), (ispan, ispan))); Some(Xirf::open( qname, OpenSpan::without_name_span(expr.span()), depth, )) } /// Emit a match expression. /// /// This is intended as a classify/any/all child. fn emit_match( &mut self, expr: &Expr, oi_expr: ObjectIndex, depth: Depth, ) -> Xirf { let mut edges = oi_expr.edges_filtered::(self.asg); // note: the edges are reversed (TODO?) let value = edges .next() .diagnostic_expect( || vec![oi_expr.note("for this match")], "missing @value ref", ) .resolve(self.asg); let on = edges .next() .diagnostic_expect( || vec![oi_expr.note("for this match")], "missing @on ref", ) .resolve(self.asg); if let Some(unexpected) = edges.next() { diagnostic_panic!( vec![unexpected.error("a third ref was unexpected")], "unexpected third ref during match generation", ); } self.push(attr_value(value.name())); self.push(attr_on(on.name())); Xirf::open(QN_MATCH, OpenSpan::without_name_span(expr.span()), depth) } /// Emit a template definition or application. fn emit_template( &mut self, tpl: &Tpl, oi_tpl: ObjectIndex, src: &Object, depth: Depth, ) -> Option { match src { Object::Ident((ident, _)) => { self.push(attr_name(ident.name())); Some(Xirf::open( QN_TEMPLATE, OpenSpan::without_name_span(tpl.span()), depth, )) } // 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(..) | Object::Tpl(..) | Object::Expr(..) => { // [`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. // TODO: Not checking that it's a Tpl ident because we need // to be able to accommodate Missing identifiers until we // both properly handle scoping rules and support package // imports; // this is making some dangerous assumptions, // though they are tested. let ident = oi_tpl.edges_filtered::(self.asg).last(); let apply_tpl = ident.diagnostic_expect( || { vec![tpl .span() .internal_error("missing target Tpl Ident")] }, "cannot derive name of template for application", ); self.push(attr_name(apply_tpl.resolve(self.asg).name())); Some(Xirf::open( QN_APPLY_TEMPLATE, OpenSpan::without_name_span(tpl.span()), depth, )) } _ => diagnostic_todo!( vec![ oi_tpl.note("interpreting this as a template application"), ], "emit_template: {src:?}" ), } } /// 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, )) } /// Emit short documentation strings. /// /// This derives e.g. `@desc`. fn emit_doc( &mut self, doc: &Doc, oi_doc: ObjectIndex, src: &Object, _depth: Depth, ) -> Option { match (src, doc) { // TODO: Non-stmt exprs should use `@label` instead. (Object::Expr(..) | Object::Tpl(..), Doc::IndepClause(desc)) => { Some(attr_desc(*desc)) } (_, Doc::Text(_text)) => { // TODO: This isn't utilized by the XSLT parser and // `xmllint` for system tests does not format with mixed // data present, // so let's just omit for now. // Some(Xirf::text(Text(text.symbol(), text.span()), depth)) None } _ => { diagnostic_todo!( vec![oi_doc.internal_error( "this documentation is not supported in XIRF output" )], "unsupported documentation", ) } } } fn push(&mut self, tok: Xirf) { if self.stack.is_full() { diagnostic_panic!( vec![tok .internal_error("while emitting a token for this object")], "token stack exhausted (increase TOK_STACK_SIZE)", ) } self.stack.push(tok) } fn pop(&mut self) -> Option { self.stack.pop() } fn stack_is_empty(&self) -> bool { self.stack.is_empty() } fn push_all(&mut self, toks: impl IntoIterator) { toks.into_iter().for_each(|x| self.push(x)) } } // Custom `Debug` impl to omit ASG rendering, // since it's large and already included while rendering other parts of // the lowering pipeline. // Of course, // that's assuming this is part of the lowering pipeline. impl<'a> std::fmt::Debug for TreeContext<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_struct("TreeContext") .field("stack", &self.stack) .finish_non_exhaustive() } } impl<'a> From<&'a Asg> for TreeContext<'a> { fn from(asg: &'a Asg) -> Self { TreeContext { stack: Default::default(), asg, } } } fn package(pkg: &Pkg, depth: Depth) -> Xirf { Xirf::open(QN_PACKAGE, OpenSpan::without_name_span(pkg.span()), depth) } fn ns(qname: QName, uri: UriStaticSymbolId, span: Span) -> Xirf { Xirf::attr(qname, uri, (span, span)) } fn attr_name(name: SPair) -> Xirf { Xirf::attr(QN_NAME, name, (name.span(), name.span())) } fn attr_on(on: SPair) -> Xirf { Xirf::attr(QN_ON, on, (on.span(), on.span())) } fn attr_value(val: SPair) -> Xirf { Xirf::attr(QN_VALUE, val, (val.span(), val.span())) } fn attr_desc(desc: SPair) -> Xirf { Xirf::attr(QN_DESC, desc, (desc.span(), desc.span())) } fn expr_ele(expr: &Expr, oi_expr: ObjectIndex, depth: Depth) -> Xirf { use ExprOp::*; let qname = match expr.op() { Sum => QN_C_SUM, Product => QN_C_PRODUCT, Ceil => QN_C_CEIL, Floor => QN_C_FLOOR, Conj => QN_ALL, Disj => QN_ANY, Eq => diagnostic_panic!( vec![oi_expr.error("unsupported expression type in this context")], "cannot derive expression of this type in this context", ), }; Xirf::open(qname, OpenSpan::without_name_span(expr.span()), depth) } // System tests covering this functionality can be found in // `:tamer/tests/xir/`.