tamer: asg: Basic `Doc::Text` support

This supports arbitrary documentation as sibling text (mixed content, in XML
terms).  The motivation behind this change is to permit existing system
tests to succeed when `Todo | TodoAttr` are both rejected, rather than
having to ignore this.

TAME has always had a philosophy of literate documentation, however it was
never fully realized.  This just maintains the status quo; the text is
unstructured, and maybe will be parsed in the future.

Unfortunately, this does _not_ include the output in the `xmli` file or the
system tests.  The reason has nothing to do with TAMER---`xmllint` does not
format the output when there is mixed content, it seems, and I need to move
on for now; I'll consider my options in the future.  But, it's available on
the graph and ready to go.

DEV-13708
main
Mike Gerwitz 2023-04-12 10:03:37 -04:00
parent 647e0ccbbd
commit e88800af42
13 changed files with 223 additions and 27 deletions

View File

@ -133,7 +133,8 @@ impl ParseState for AirAggregate {
ctx: &mut Self::Context,
) -> crate::parse::TransitionResult<Self> {
use ir::{
AirBind::*, AirIdent::*, AirPkg::*, AirSubsets::*, AirTodo::*,
AirBind::*, AirDoc::*, AirIdent::*, AirPkg::*, AirSubsets::*,
AirTodo::*,
};
use AirAggregate::*;
@ -168,18 +169,23 @@ impl ParseState for AirAggregate {
Transition(st).err(AsgError::InvalidBindContext(id))
}
(Toplevel(oi_pkg), tok @ AirDoc(..)) => {
(Toplevel(oi_pkg), tok @ AirDoc(DocIndepClause(..))) => {
diagnostic_todo!(
vec![
oi_pkg.note("for this package"),
tok.internal_error(
"this package-level documentation is not yet supported"
"this package description is not yet supported"
)
],
"package-level documentation is not yet supported by TAMER",
"package-level short description is not yet supported by TAMER",
)
}
(Toplevel(oi_pkg), AirDoc(DocText(text))) => {
oi_pkg.append_doc_text(ctx.asg_mut(), text);
Transition(Toplevel(oi_pkg)).incomplete()
}
// Package import
(Toplevel(oi_pkg), AirBind(RefIdent(pathspec))) => {
oi_pkg.import(ctx.asg_mut(), pathspec);

View File

@ -148,6 +148,11 @@ impl ParseState for AirExprAggregate {
Transition(BuildingExpr(es, oi)).incomplete()
}
(BuildingExpr(es, oi), AirDoc(DocText(text))) => Transition(
BuildingExpr(es, oi),
)
.err(AsgError::InvalidDocContextExpr(oi.span(), text.span())),
(st @ Ready(..), AirExpr(ExprEnd(span))) => {
Transition(st).err(AsgError::UnbalancedExpr(span))
}

View File

@ -798,6 +798,21 @@ sum_ir! {
in a sentence",
),
},
/// Arbitrary documentation text.
///
/// TAMER hopes to eventually provide structured documentation,
/// but until then,
/// this is just some arbitrary block of text.
/// Historically,
/// the convention was LaTeX,
/// but the approach has fallen out of favor;
/// TAMER should provide its own documentation format that
/// it can reason about.
DocText(text: SPair) => {
span: text,
display: |f| write!(f, "documentation text"),
},
}
}

View File

@ -22,7 +22,7 @@
use super::{super::Ident, *};
use crate::{
asg::{IdentKind, Source},
asg::{graph::object::ObjectRel, IdentKind, Source},
parse::{ParseError, Parsed, Parser},
span::dummy::*,
};
@ -340,6 +340,41 @@ fn pkg_import() {
assert_eq!(pathspec, import.pathspec());
}
// Documentation can be mixed in with objects in a literate style.
#[test]
fn pkg_doc() {
let doc_a = SPair("first".into(), S2);
let id_import = SPair("import".into(), S3);
let doc_b = SPair("first".into(), S4);
#[rustfmt::skip]
let toks = vec![
Air::DocText(doc_a),
// Some object to place in-between the two
// documentation blocks.
Air::RefIdent(id_import),
Air::DocText(doc_b),
];
let asg = asg_from_toks(toks);
let oi_pkg = asg
.root(S1)
.edges_filtered::<Pkg>(&asg)
.next()
.expect("cannot find package from root");
assert_eq!(
vec![S4, S3, S2], // (edges reversed by Petgraph)
oi_pkg
.edges(&asg)
.map(|rel| rel.widen().resolve(&asg).span())
.collect::<Vec<_>>(),
);
}
/// Parse using [`Sut`] when the test does not care about the outer package.
pub fn parse_as_pkg_body<I: IntoIterator<Item = Air>>(
toks: I,

View File

@ -174,7 +174,7 @@ impl ParseState for AirTplAggregate {
tok: Self::Token,
ctx: &mut Self::Context,
) -> TransitionResult<Self::Super> {
use super::ir::{AirBind::*, AirTpl::*};
use super::ir::{AirBind::*, AirDoc::*, AirTpl::*};
use AirBindableTpl::*;
use AirTplAggregate::*;
@ -206,19 +206,23 @@ impl ParseState for AirTplAggregate {
Transition(Toplevel(tpl)).incomplete()
}
(Toplevel(tpl), tok @ AirDoc(..)) => {
(Toplevel(tpl), tok @ AirDoc(DocIndepClause(_))) => {
diagnostic_todo!(
vec![
tpl.oi().note("in this template"),
tok.internal_error(
"this template-level documentation is not \
yet supported"
"this template description is not yet supported"
)
],
"template-level documentation is not yet supported by TAMER",
"template description is not yet supported by TAMER",
)
}
(Toplevel(tpl), AirDoc(DocText(text))) => {
tpl.oi().append_doc_text(ctx.asg_mut(), text);
Transition(Toplevel(tpl)).incomplete()
}
(Toplevel(tpl), AirTpl(TplMetaStart(span))) => {
let oi_meta = ctx.asg_mut().create(Meta::new_required(span));
Transition(TplMeta(tpl, oi_meta)).incomplete()

View File

@ -118,6 +118,17 @@ pub enum AsgError {
/// Attempted to expand a template into a context that does not support
/// expansion.
InvalidExpansionContext(Span),
/// Documentation text is not valid in an expression context.
///
/// This historical limitation existed because the author was unsure how
/// to go about rendering an equation with literate documentation
/// interspersed.
/// The plan is to lift this limitation in the future.
///
/// The spans represent the expression and the documentation text
/// respectively.
InvalidDocContextExpr(Span, Span),
}
impl Display for AsgError {
@ -157,6 +168,9 @@ impl Display for AsgError {
InvalidExpansionContext(_) => {
write!(f, "invalid template expansion context",)
}
InvalidDocContextExpr(_, _) => {
write!(f, "document text is not permitted within expressions")
}
}
}
}
@ -267,6 +281,15 @@ impl Diagnostic for AsgError {
InvalidExpansionContext(span) => {
vec![span.error("cannot expand a template here")]
}
InvalidDocContextExpr(expr_span, span) => vec![
expr_span.note("in this expression"),
span.error("documentation text is not permitted here"),
span.help(
"this is a historical limitation that will \
likely be lifted in the future",
),
],
}
}
}

View File

@ -38,6 +38,17 @@ use std::fmt::Display;
pub enum Doc {
/// An (ideally) concise independent clause describing an object.
IndepClause(SPair),
/// Arbitrary text serving as documentation for sibling objects in a
/// literate style.
///
/// TAMER does not presently ascribe any semantic meaning to this text,
/// and it may even be entirely whitespace.
/// There are plans to improve upon this in the future.
///
/// The intent is for this text to be mixed with sibling objects,
/// in a style similar to that of literate programming.
Text(SPair),
}
impl Display for Doc {
@ -53,15 +64,22 @@ impl Doc {
Self::IndepClause(clause)
}
/// Arbitrary text serving as documentation for sibling objects in a
/// literate style.
pub fn new_text(text: SPair) -> Self {
Self::Text(text)
}
pub fn indep_clause(&self) -> Option<SPair> {
match self {
Self::IndepClause(spair) => Some(*spair),
Self::Text(_) => None,
}
}
pub fn span(&self) -> Span {
match self {
Self::IndepClause(spair) => spair.span(),
Self::IndepClause(spair) | Self::Text(spair) => spair.span(),
}
}
}

View File

@ -19,7 +19,7 @@
//! Package object on the ASG.
use super::{prelude::*, Ident, Tpl};
use super::{prelude::*, Doc, Ident, Tpl};
use crate::{
f::Functor,
parse::{util::SPair, Token},
@ -94,6 +94,9 @@ object_rel! {
// Anonymous templates are used for expansion.
tree Tpl,
// Arbitrary blocks of text serving as documentation.
tree Doc,
}
}
@ -112,4 +115,10 @@ impl ObjectIndex<Pkg> {
let oi_import = asg.create(Pkg::new_imported(pathspec));
self.add_edge_to(asg, oi_import, Some(pathspec.span()))
}
/// Arbitrary text serving as documentation in a literate style.
pub fn append_doc_text(&self, asg: &mut Asg, text: SPair) -> Self {
let oi_doc = asg.create(Doc::new_text(text));
self.add_edge_to(asg, oi_doc, None)
}
}

View File

@ -21,8 +21,8 @@
use std::fmt::Display;
use super::{prelude::*, Expr, Ident};
use crate::{f::Functor, span::Span};
use super::{prelude::*, Doc, Expr, Ident};
use crate::{f::Functor, parse::util::SPair, span::Span};
/// Template with associated name.
#[derive(Debug, PartialEq, Eq)]
@ -68,6 +68,10 @@ object_rel! {
// Template application.
tree Tpl,
// Arbitrary documentation to be expanded into the application
// site.
tree Doc,
}
}
@ -115,4 +119,11 @@ impl ObjectIndex<Tpl> {
) -> Self {
self.add_edge_from(asg, oi_target_parent, None)
}
/// Arbitrary text serving as documentation in a literate style,
/// to be expanded into the application site.
pub fn append_doc_text(&self, asg: &mut Asg, text: SPair) -> Self {
let oi_doc = asg.create(Doc::new_text(text));
self.add_edge_to(asg, oi_doc, None)
}
}

View File

@ -211,7 +211,7 @@ impl<'a> TreeContext<'a> {
}
Object::Doc((doc, oi_doc)) => {
self.emit_doc(doc, *oi_doc, paired_rel.source())
self.emit_doc(doc, *oi_doc, paired_rel.source(), depth)
}
Object::Root(..) => diagnostic_unreachable!(
@ -487,10 +487,22 @@ impl<'a> TreeContext<'a> {
doc: &Doc,
oi_doc: ObjectIndex<Doc>,
src: &Object<OiPairObjectInner>,
_depth: Depth,
) -> Option<Xirf> {
match src {
match (src, doc) {
// TODO: Non-stmt exprs should use `@label` instead.
Object::Expr(..) => doc.indep_clause().map(attr_desc),
(Object::Expr(..), 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!(

View File

@ -49,6 +49,9 @@ pub enum NirToAir {
/// A predicate has been partially applied to its subject,
/// but we do not yet know its function or comparison value.
PredPartial(Span, SPair),
/// Processing a metavariable.
Meta(Span),
}
impl Display for NirToAir {
@ -65,6 +68,7 @@ impl Display for NirToAir {
"waiting to determine type of predicate for identifier {}",
TtQuote::wrap(name),
),
Meta(_) => write!(f, "parsing metavariable definition"),
}
}
}
@ -214,14 +218,23 @@ impl ParseState for NirToAir {
}
(Ready, Open(TplParam, span)) => {
Transition(Ready).ok(Air::TplMetaStart(span))
Transition(Meta(span)).ok(Air::TplMetaStart(span))
}
(Ready, Close(TplParam, span)) => {
(Meta(mspan), Text(lexeme)) => {
Transition(Meta(mspan)).ok(Air::TplLexeme(lexeme))
}
(Ready | Meta(_), Close(TplParam, span)) => {
Transition(Ready).ok(Air::TplMetaEnd(span))
}
(Ready, Text(lexeme)) => {
Transition(Ready).ok(Air::TplLexeme(lexeme))
}
// Some of these will be permitted in the future.
(
Meta(mspan),
tok @ (Open(..) | Close(..) | Ref(..) | RefSubject(..)
| Desc(..)),
) => Transition(Meta(mspan))
.err(NirToAirError::UnexpectedMetaToken(mspan, tok)),
(Ready, Text(text)) => Transition(Ready).ok(Air::DocText(text)),
(
Ready,
@ -231,8 +244,8 @@ impl ParseState for NirToAir {
),
) => Transition(Ready).ok(Air::ExprEnd(span)),
(Ready, BindIdent(spair)) => {
Transition(Ready).ok(Air::BindIdent(spair))
(st @ (Ready | Meta(_)), BindIdent(spair)) => {
Transition(st).ok(Air::BindIdent(spair))
}
(Ready, Ref(spair) | RefSubject(spair)) => {
Transition(Ready).ok(Air::RefIdent(spair))
@ -242,7 +255,7 @@ impl ParseState for NirToAir {
Transition(Ready).ok(Air::DocIndepClause(clause))
}
(Ready, Todo(..) | TodoAttr(..)) => {
(_, Todo(..) | TodoAttr(..)) => {
Transition(Ready).ok(Air::Todo(UNKNOWN_SPAN))
}
@ -267,6 +280,10 @@ pub enum NirToAirError {
/// Match body is not yet supported.
TodoMatchBody(Span, Span),
/// The provided [`Nir`] token of input was unexpected for the body of a
/// metavariable that was opened at the provided [`Span`].
UnexpectedMetaToken(Span, Nir),
}
impl Display for NirToAirError {
@ -281,6 +298,13 @@ impl Display for NirToAirError {
TodoMatchBody(_, _) => {
write!(f, "match body is not yet supported by TAMER")
}
UnexpectedMetaToken(_, tok) => {
write!(
f,
"expected lexical token for metavariable, found {tok}"
)
}
}
}
}
@ -307,6 +331,14 @@ impl Diagnostic for NirToAirError {
"tokens in match body are not yet supported by TAMER",
),
],
// The user should have been preempted by the parent parser
// (e.g. XML->Nir),
// and so shouldn't see this.
UnexpectedMetaToken(mspan, given) => vec![
mspan.note("while parsing the body of this metavariable"),
given.span().error("expected a lexical token here"),
],
}
}
}

View File

@ -370,3 +370,28 @@ fn match_no_args_err() {
Sut::parse(toks.into_iter()).collect::<Vec<Result<_, _>>>(),
);
}
// Sibling text (e.g. mixed content in XML) becomes arbitrary
// documentation,
// intended to be written in a literate style.
#[test]
fn text_as_arbitrary_doc() {
let text = SPair("foo bar baz".into(), S2);
#[rustfmt::skip]
let toks = vec![
Open(Package, S1),
Text(text),
Close(Package, S3),
];
assert_eq!(
#[rustfmt::skip]
Ok(vec![
O(Air::PkgStart(S1)),
O(Air::DocText(text)),
O(Air::PkgEnd(S3)),
]),
Sut::parse(toks.into_iter()).collect(),
);
}

View File

@ -172,7 +172,7 @@ ele_parse! {
// program;
// see [`NirParseState`] for more information.
[super] {
[text](_sym, span) => Todo(span),
[text](sym, span) => Nir::Text(SPair(sym, span)),
TplKw
};
@ -263,7 +263,8 @@ ele_parse! {
@ {
QN_PACKAGE => Ref,
QN_EXPORT => TodoAttr,
} => Todo(ospan.into()),
} => Noop(ospan.into()),
// ^ we only care about the `Ref`
};
/// A statement that is accepted within the body of a package.