tamer: asg: Add basic Doc support (for @desc)

This introduces a new `Doc` object that can be owned by `Expr` (only atm)
and contain what it describes as a concise independent clause.  This
construction is not enforced, and is only really obvious today via the
Summary Pages.

There's a lot of latent and unrealized potential in TAME's documentation
philosophy that was never realized, so this will certainly evolve over
time.  But for now, the primary purpose was to get `@desc` working on things
like classifications so that `xmli` output can compile for certain
packages.

DEV-13708
main
Mike Gerwitz 2023-04-10 12:16:21 -04:00
parent 0163391498
commit 9cb6195046
16 changed files with 315 additions and 36 deletions

View File

@ -73,9 +73,6 @@ BEGINFILE {
found_rels = 0
}
# Skip comments.
/^ *\/+/ { next }
# Predicates will be reset for each line,
# allowing the remainder of the script to be read more declaratively.
{ in_block = in_block_subexpr = 0 }
@ -100,6 +97,15 @@ block_src && /}/ {
print ""
}
# "// empty" means that the lack of edges is intentional.
block_src && /^ *\/\/ empty$/ {
# Suppress error from the edge check below.
found_rels++
}
# Skip comments.
/^ *\/+/ { next }
# For each target object,
# output a relation.
#

View File

@ -40,6 +40,8 @@ use super::{
Asg, AsgError, Expr, Ident, ObjectIndex,
};
use crate::{
diagnose::Annotate,
diagnostic_todo,
parse::{prelude::*, StateStack},
span::{Span, UNKNOWN_SPAN},
sym::SymbolId,
@ -161,18 +163,30 @@ impl ParseState for AirAggregate {
Transition(Empty).incomplete()
}
// TODO: We don't support package ids yet
// Packages are identified by their paths.
(st @ Toplevel(..), AirBind(BindIdent(id))) => {
Transition(st).err(AsgError::InvalidBindContext(id))
}
(Toplevel(oi_pkg), tok @ AirDoc(..)) => {
diagnostic_todo!(
vec![
oi_pkg.note("for this package"),
tok.internal_error(
"this package-level documentation is not yet supported"
)
],
"package-level documentation is not yet supported by TAMER",
)
}
// Package import
(Toplevel(oi_pkg), AirBind(RefIdent(pathspec))) => {
oi_pkg.import(ctx.asg_mut(), pathspec);
Transition(Toplevel(oi_pkg)).incomplete()
}
// Note: We unfortunately can't match on `AirExpr | AirBind`
// Note: We unfortunately can't match on `AirExpr | AirBind | ...`
// and delegate in the same block
// (without having to duplicate type checks and then handle
// unreachable paths)
@ -182,6 +196,7 @@ impl ParseState for AirAggregate {
}
(PkgExpr(expr), AirExpr(etok)) => ctx.proxy(expr, etok),
(PkgExpr(expr), AirBind(etok)) => ctx.proxy(expr, etok),
(PkgExpr(expr), AirDoc(etok)) => ctx.proxy(expr, etok),
// Template parsing.
(st @ (Toplevel(_) | PkgExpr(_)), tok @ AirTpl(..)) => {
@ -189,6 +204,7 @@ impl ParseState for AirAggregate {
}
(PkgTpl(tplst), AirTpl(ttok)) => ctx.proxy(tplst, ttok),
(PkgTpl(tplst), AirBind(ttok)) => ctx.proxy(tplst, ttok),
(PkgTpl(tplst), AirDoc(ttok)) => ctx.proxy(tplst, ttok),
(Empty, AirPkg(PkgEnd(span))) => {
Transition(Empty).err(AsgError::InvalidPkgEndContext(span))
@ -205,9 +221,10 @@ impl ParseState for AirAggregate {
}
}
(Empty, tok @ (AirExpr(..) | AirBind(..) | AirTpl(..))) => {
Transition(Empty).err(AsgError::PkgExpected(tok.span()))
}
(
Empty,
tok @ (AirExpr(..) | AirBind(..) | AirTpl(..) | AirDoc(..)),
) => Transition(Empty).err(AsgError::PkgExpected(tok.span())),
(Empty, AirIdent(IdentDecl(name, kind, src))) => ctx
.asg_mut()

View File

@ -85,7 +85,7 @@ impl ParseState for AirExprAggregate {
tok: Self::Token,
ctx: &mut Self::Context,
) -> crate::parse::TransitionResult<Self::Super> {
use super::ir::{AirBind::*, AirExpr::*};
use super::ir::{AirBind::*, AirDoc::*, AirExpr::*};
use AirBindableExpr::*;
use AirExprAggregate::*;
@ -143,12 +143,19 @@ impl ParseState for AirExprAggregate {
.incomplete()
}
(BuildingExpr(es, oi), AirDoc(DocIndepClause(clause))) => {
oi.desc_short(ctx.asg_mut(), clause);
Transition(BuildingExpr(es, oi)).incomplete()
}
(st @ Ready(..), AirExpr(ExprEnd(span))) => {
Transition(st).err(AsgError::UnbalancedExpr(span))
}
// The binding may refer to a parent context.
(st @ Ready(..), tok @ AirBind(..)) => Transition(st).dead(tok),
// Token may refer to a parent context.
(st @ Ready(..), tok @ (AirBind(..) | AirDoc(..))) => {
Transition(st).dead(tok)
}
}
}

View File

@ -23,7 +23,7 @@ use crate::asg::{
test::{asg_from_toks, parse_as_pkg_body},
Air, AirAggregate,
},
graph::object::{expr::ExprRel, ObjectRel},
graph::object::{expr::ExprRel, Doc, ObjectRel},
ExprOp, Ident,
};
use crate::span::dummy::*;
@ -861,3 +861,29 @@ fn idents_share_defining_pkg() {
oi_foo.src_pkg(&asg).map(|pkg| pkg.resolve(&asg).span())
)
}
#[test]
fn expr_doc_short_desc() {
let id_expr = SPair("foo".into(), S2);
let clause = SPair("short desc".into(), S3);
#[rustfmt::skip]
let toks = vec![
Air::ExprStart(ExprOp::Sum, S1),
Air::BindIdent(id_expr),
Air::DocIndepClause(clause),
Air::ExprEnd(S4),
];
let asg = asg_from_toks(toks);
let oi_expr = asg.expect_ident_oi::<Expr>(id_expr);
let oi_docs = oi_expr
.edges_filtered::<Doc>(&asg)
.map(ObjectIndex::cresolve(&asg));
assert_eq!(
vec![&Doc::new_indep_clause(clause)],
oi_docs.collect::<Vec<_>>(),
);
}

View File

@ -768,14 +768,45 @@ sum_ir! {
display: |f| write!(f, "end template definition and expand it"),
},
}
enum AirDoc {
/// Describe the active object using an independent clause.
///
/// This is like a "subject line",
/// but is intended to be used when generating documentation
/// in various different contexts.
/// Users should think of this as taking place of an identifier
/// name when used in a sentence,
/// and phrase these clauses relative to the semantic
/// properties of the object being described.
/// The description should be able to stand on its own as a
/// simple sentence, and should be able to be used to make
/// compound sentences.
///
/// For example,
/// predicates should make sense when being used to describe
/// other objects,
/// and should make sense when concatenated together using
/// conjunctives and disjunctives.
/// Calculations should make sense with and without those
/// predicates.
DocIndepClause(text: SPair) => {
span: text,
display: |f| write!(
f,
"documentation describing the active object as a subject \
in a sentence",
),
},
}
}
/// Expressions that are able to be bound to identifiers.
///
/// This is the primary token set when parsing packages,
/// since most everything in TAMER is an expression.
pub sum enum AirBindableExpr = AirExpr | AirBind;
pub sum enum AirBindableExpr = AirExpr | AirBind | AirDoc;
/// Tokens that may be used to define or apply templates.
pub sum enum AirBindableTpl = AirTpl | AirBind;
pub sum enum AirBindableTpl = AirTpl | AirBind | AirDoc;
}

View File

@ -206,6 +206,19 @@ impl ParseState for AirTplAggregate {
Transition(Toplevel(tpl)).incomplete()
}
(Toplevel(tpl), tok @ AirDoc(..)) => {
diagnostic_todo!(
vec![
tpl.oi().note("in this template"),
tok.internal_error(
"this template-level documentation is not \
yet supported"
)
],
"template-level documentation is not yet supported by TAMER",
)
}
(Toplevel(tpl), AirTpl(TplMetaStart(span))) => {
let oi_meta = ctx.asg_mut().create(Meta::new_required(span));
Transition(TplMeta(tpl, oi_meta)).incomplete()
@ -242,6 +255,21 @@ impl ParseState for AirTplAggregate {
diagnostic_todo!(vec![tok.note("this token")], "AirTpl variant")
}
(TplMeta(tpl, oi_meta), tok @ AirDoc(..)) => {
diagnostic_todo!(
vec![
tpl.oi().note("in this template"),
oi_meta.note("this metavariable"),
tok.internal_error(
"this metavariable-level documentation is not \
yet supported"
)
],
"metavariable-level documentation is not yet supported \
by TAMER",
)
}
(Toplevel(..), tok @ AirTpl(TplMetaEnd(..))) => {
diagnostic_todo!(
vec![tok.note("this token")],
@ -299,7 +327,7 @@ impl ParseState for AirTplAggregate {
Transition(Ready).err(AsgError::UnbalancedTpl(span))
}
(st @ (Ready | Done), tok @ AirBind(..)) => {
(st @ (Ready | Done), tok @ (AirBind(..) | AirDoc(..))) => {
Transition(st).dead(tok)
}
}

View File

@ -132,6 +132,7 @@ use std::{
#[macro_use]
mod rel;
pub mod doc;
pub mod expr;
pub mod ident;
pub mod meta;
@ -139,6 +140,7 @@ pub mod pkg;
pub mod root;
pub mod tpl;
pub use doc::Doc;
pub use expr::Expr;
pub use ident::Ident;
pub use meta::Meta;
@ -330,6 +332,9 @@ object_gen! {
/// Metasyntactic variable (metavariable).
Meta,
/// Documentation.
Doc,
}
impl Object<OnlyObjectInner> {
@ -341,6 +346,7 @@ impl Object<OnlyObjectInner> {
Self::Expr(expr) => expr.span(),
Self::Tpl(tpl) => tpl.span(),
Self::Meta(meta) => meta.span(),
Self::Doc(doc) => doc.span(),
}
}

View File

@ -0,0 +1,75 @@
// Documentation represented on the ASG
//
// 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 <http://www.gnu.org/licenses/>.
//! Documentation on the ASG.
//!
//! TODO: Document TAME's stance on documentation and literate programming,
//! much of which hasn't been able to be realized over the years.
use super::prelude::*;
use crate::{
parse::{util::SPair, Token},
span::Span,
};
use std::fmt::Display;
/// Documentation string.
///
/// TODO: This presently serves as a subject line,
/// e.g. a description or label,
/// but will evolve in the future.
#[derive(Debug, PartialEq, Eq)]
pub enum Doc {
/// An (ideally) concise independent clause describing an object.
IndepClause(SPair),
}
impl Display for Doc {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "documentation string")
}
}
impl Doc {
/// Document an object using what is ideally a concise independent
/// clause.
pub fn new_indep_clause(clause: SPair) -> Self {
Self::IndepClause(clause)
}
pub fn indep_clause(&self) -> Option<SPair> {
match self {
Self::IndepClause(spair) => Some(*spair),
}
}
pub fn span(&self) -> Span {
match self {
Self::IndepClause(spair) => spair.span(),
}
}
}
object_rel! {
/// Templates may expand into nearly any context,
/// and must therefore be able to contain just about anything.
Doc -> {
// empty
}
}

View File

@ -19,15 +19,14 @@
//! Expressions on the ASG.
use std::fmt::Display;
use super::{prelude::*, Ident, Tpl};
use super::{prelude::*, Doc, Ident, Tpl};
use crate::{
f::Functor,
num::Dim,
parse::{util::SPair, Token},
span::Span,
};
use std::fmt::Display;
#[cfg(doc)]
use super::ObjectKind;
@ -222,6 +221,7 @@ object_rel! {
Expr -> {
cross Ident,
tree Expr,
tree Doc,
// Template application
tree Tpl,
@ -253,4 +253,15 @@ impl ObjectIndex<Expr> {
let identi = asg.lookup_global_or_missing(ident);
self.add_edge_to(asg, identi, Some(ident.span()))
}
/// Describe this expression using a short independent clause.
///
/// This is intended to be a concise description for use either as a
/// simple sentence or as part of a compound sentence.
/// There should only be one such clause for any given object,
/// but that is not enforced here.
pub fn desc_short(&self, asg: &mut Asg, clause: SPair) -> Self {
let oi_doc = asg.create(Doc::new_indep_clause(clause));
self.add_edge_to(asg, oi_doc, None)
}
}

View File

@ -22,8 +22,8 @@
//! See (parent module)[super] for more information.
use super::{
Expr, Ident, Meta, Object, ObjectIndex, ObjectKind, OiPairObjectInner, Pkg,
Root,
Doc, Expr, Ident, Meta, Object, ObjectIndex, ObjectKind, OiPairObjectInner,
Pkg, Root,
};
use crate::{
asg::{graph::object::Tpl, Asg},
@ -317,7 +317,7 @@ impl<S> DynObjectRel<S, ObjectIndex<Object>> {
}
}
ty_cross_edge!(Root, Pkg, Ident, Expr, Tpl, Meta)
ty_cross_edge!(Root, Pkg, Ident, Expr, Tpl, Meta, Doc)
}
}

View File

@ -30,7 +30,7 @@
//! or observing template expansions.
use super::object::{
DynObjectRel, Expr, Meta, Object, ObjectIndex, ObjectRelTy,
Doc, DynObjectRel, Expr, Meta, Object, ObjectIndex, ObjectRelTy,
OiPairObjectInner, Pkg, Tpl,
};
use crate::{
@ -210,6 +210,10 @@ impl<'a> TreeContext<'a> {
self.emit_tpl_arg(meta, *oi_meta, depth)
}
Object::Doc((doc, oi_doc)) => {
self.emit_doc(doc, *oi_doc, paired_rel.source())
}
Object::Root(..) => diagnostic_unreachable!(
vec![],
"tree walk is not expected to emit Root",
@ -285,10 +289,11 @@ impl<'a> TreeContext<'a> {
ExprOp::Eq => Some(self.emit_match(expr, oi_expr, depth)),
_ => Some(expr_ele(expr, oi_expr, depth)),
},
// TODO: Perhaps errors for Root and Meta?
Object::Root(_) | Object::Pkg(_) | Object::Meta(_) => {
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)),
}
}
@ -474,6 +479,30 @@ impl<'a> TreeContext<'a> {
))
}
/// Emit short documentation strings.
///
/// This derives e.g. `@desc`.
fn emit_doc(
&mut self,
doc: &Doc,
oi_doc: ObjectIndex<Doc>,
src: &Object<OiPairObjectInner>,
) -> Option<Xirf> {
match src {
// TODO: Non-stmt exprs should use `@label` instead.
Object::Expr(..) => doc.indep_clause().map(attr_desc),
_ => {
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!(
@ -541,6 +570,10 @@ 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<Expr>, depth: Depth) -> Xirf {
use ExprOp::*;

View File

@ -238,7 +238,11 @@ impl ParseState for NirToAir {
Transition(Ready).ok(Air::RefIdent(spair))
}
(Ready, Todo | TodoAttr(..) | Desc(..)) => {
(Ready, Desc(clause)) => {
Transition(Ready).ok(Air::DocIndepClause(clause))
}
(Ready, Todo | TodoAttr(..)) => {
Transition(Ready).ok(Air::Todo(UNKNOWN_SPAN))
}
}

View File

@ -122,6 +122,33 @@ fn logic_exprs() {
);
}
// @desc becomes an independent clause,
// intended for short summary documentation.
#[test]
fn desc_as_indep_clause() {
let id = SPair("class".into(), S2);
let desc = SPair("class desc".into(), S3);
#[rustfmt::skip]
let toks = vec![
Open(Classify, S1),
BindIdent(id),
Desc(desc),
Close(Classify, S4),
];
assert_eq!(
#[rustfmt::skip]
Ok(vec![
O(Air::ExprStart(ExprOp::Conj, S1)),
O(Air::BindIdent(id)),
O(Air::DocIndepClause(desc)),
O(Air::ExprEnd(S4)),
]),
Sut::parse(toks.into_iter()).collect(),
);
}
#[test]
fn tpl_with_name() {
let name = SPair("_tpl_name_".into(), S2);

View File

@ -392,7 +392,7 @@ ele_parse! {
ClassifyStmt := QN_CLASSIFY(_, ospan) {
@ {
QN_AS => BindIdent,
QN_DESC => TodoAttr,
QN_DESC => Desc,
QN_ANY => TodoAttr,
QN_YIELDS => TodoAttr,
QN_SYM => TodoAttr,

View File

@ -3,9 +3,9 @@
xmlns:c="http://www.lovullo.com/calc"
xmlns:t="http://www.lovullo.com/rater/apply-template">
<classify as="always" />
<classify as="always" desc="Always" />
<classify as="sometimes">
<classify as="sometimes" desc="Sometimes">
<any>
<all />
<any />
@ -14,11 +14,15 @@
<any />
</classify>
<classify as="short-match-implicit-eq-implicit-true">
<classify as="short-match-implicit-eq-implicit-true"
desc="Short match with implicit eq and
an implicit value">
<match on="foo" value="TRUE" />
</classify>
<classify as="short-match-implicit-eq-explicit-value">
<classify as="short-match-implicit-eq-explicit-value"
desc="Short match with an implicit eq and
an explicit value">
<match on="foo" value="bar" />
</classify>
</package>

View File

@ -3,9 +3,9 @@
xmlns:c="http://www.lovullo.com/calc"
xmlns:t="http://www.lovullo.com/rater/apply-template">
<classify as="always" />
<classify as="always" desc="Always" />
<classify as="sometimes">
<classify as="sometimes" desc="Sometimes">
<any>
<all />
<any />
@ -14,11 +14,15 @@
<any />
</classify>
<classify as="short-match-implicit-eq-implicit-true">
<classify as="short-match-implicit-eq-implicit-true"
desc="Short match with implicit eq and
an implicit value">
<match on="foo" />
</classify>
<classify as="short-match-implicit-eq-explicit-value">
<classify as="short-match-implicit-eq-explicit-value"
desc="Short match with an implicit eq and
an explicit value">
<match on="foo" value="bar" />
</classify>
</package>