tame/tamer/src/fmt.rs

655 lines
20 KiB
Rust
Raw Normal View History

// TAMER formatting helpers
//
// 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/>.
//! Typed formatting helpers.
//!
//! These types create composable formatters for use with [`Display`].
//! Whereas [`Display`] operates on data owned by the type implementing it,
//! these formatters compose into functions that operate on data provided
//! _to_ it.
//! Consequently,
//! formatters are simply types,
//! and writes can be streamed just as they are with [`Display`].
//!
//! There are two types of wrappers:
//!
//! - [`DisplayWrapper`] formats objects as atoms; and
//! - [`ListDisplayWrapper`] maps a [`DisplayWrapper`] to each of its
//! items as atoms,
//! where the specific wrapper used depends on the position of the
//! item within the list and the properties of the list itself.
//!
//! [`DisplayWrapper::wrap`] and [`ListDisplayWrapper::wrap`] can be used to
//! associate wrappers with data,
//! effectively creating a custom [`Display`] implementation controlled
//! by the caller.
//! This is _not_ a substitute for canonical object representations that own
//! their own [`Display`] implementation,
//! but is suitable where display is context-dependent.
//!
//! For example:
//!
//! ```
//! # use tamer::{fmt::*, xir::fmt::*};
//! let attrs = ["foo", "bar", "baz"];
//!
//! assert_eq!(
//! AndQualConjList::<"attribute", "attributes", Tt<XmlAttr>>::wrap(&attrs)
//! .to_string(),
//! "attributes `@foo`, `@bar`, and `@baz`",
//! );
//!
//! assert_eq!(
//! AndQualConjList::<"attribute", "attributes", Tt<XmlAttr>>::wrap(&attrs[0..1])
//! .to_string(),
//! "attribute `@foo`",
//! );
//!
//! assert_eq!(
//! AndConjList::<Raw>::wrap(&["toil", "trouble"]).to_string(),
//! "toil and trouble",
//! );
//! ```
//!
//! This is in contrast to the approach taken by (for example)
//! the [diagnostic system](crate::diagnose),
//! which is to produce a data structure representing the data to be
//! formatted.
//! This system is not useful as,
//! and is not intended to be,
//! an IR for more sophisticated manipulation of data prior to output.
use std::{
fmt::{Display, Formatter, Result},
marker::PhantomData,
};
/// Wrapper for a [`Display`]-able type.
///
/// See the [module-level documentation](super) for more information.
pub trait DisplayWrapper {
/// Transform inner data and output using the provided [`Formatter`].
///
/// If a [`Formatter`] is not available,
/// [`ListDisplayWrapper::wrap`] may be used to produce a
/// [`Display`]-able object instead.
fn fmt<T: Display>(inner: T, f: &mut Formatter) -> Result;
/// Associate data with a [`DisplayWrapper`] for rendering using
/// [`Display`].
///
/// This has the effect of creating an arbitrary [`Display`]
/// implementation for the wrapped object,
/// which will work well with [`format!`] and anything else that
/// does not have access to an explicit [`Formatter`].
fn wrap<T: Display>(inner: T) -> Wrap<Self, T> {
Wrap {
inner,
_phantom: Default::default(),
}
}
}
/// Wrapper with associated data.
///
/// This has the effect of creating an arbitrary [`Display`] implementation
/// for the wrapped data,
/// which will work well with [`format!`] and anything else that does
/// not have access to an explicit [`Formatter`].
pub struct Wrap<W: DisplayWrapper + ?Sized, T: Display> {
inner: T,
_phantom: PhantomData<W>,
}
impl<W: DisplayWrapper, T: Display> Display for Wrap<W, T> {
fn fmt(&self, f: &mut Formatter) -> Result {
W::fmt(&self.inner, f)
}
}
/// Echo data as-is without any wrapping.
///
/// This is primarily used at the root of a wrapper composition.
pub struct Raw;
impl DisplayWrapper for Raw {
fn fmt<T: Display>(inner: T, f: &mut Formatter) -> Result {
inner.fmt(f)
}
}
/// Prefix data with a static [`str`].
///
/// See also [`Suffix`] and [`Delim`].
pub struct Prefix<const PREFIX: &'static str, W: DisplayWrapper>(
PhantomData<W>,
);
impl<const PREFIX: &'static str, W: DisplayWrapper> DisplayWrapper
for Prefix<PREFIX, W>
{
fn fmt<T: Display>(inner: T, f: &mut Formatter) -> Result {
f.write_str(PREFIX)?;
W::fmt(inner, f)
}
}
/// Suffix data with a static [`str`].
///
/// See also [`Prefix`] and [`Delim`].
pub struct Suffix<const SUFFIX: &'static str, W: DisplayWrapper>(
PhantomData<W>,
);
impl<const SUFFIX: &'static str, W: DisplayWrapper> DisplayWrapper
for Suffix<SUFFIX, W>
{
fn fmt<T: Display>(inner: T, f: &mut Formatter) -> Result {
W::fmt(inner, f)?;
f.write_str(SUFFIX)
}
}
/// Surround a value in delimiters.
///
/// See also [`Prefix`] and [`Suffix`].
pub type Delim<const LEFT: &'static str, const RIGHT: &'static str, W> =
Prefix<LEFT, Suffix<RIGHT, W>>;
/// Denote text that would conventionally be delimited in a teletypewriter
/// font.
///
/// This produces a markdown-style quote using backticks.
///
/// NB: This does not defend against nested quotes,
/// so this is _not_ safe against format escapes.
pub type Tt<W> = Delim<"`", "`", W>;
tamer: xir::parse::ele: Initial element parser generator concept This begins generating parsers that are capable of parsing elements. I need to move on, so this abstraction isn't going to go as far as it could, but let's see where it takes me. This was the work that required the recent lookahead changes, which has been detailed in previous commits. This initial support is basic, but robust. It supports parsing elements with attributes and children, but it does not yet support the equivalent of the Kleene star (`*`). Such support will likely be added by supporting parsers that are able to recurse on their own definition in tail position, which will also require supporting parsers that do not add to the stack. This generates parsers that, like all the other parsers, use enums to provide a typed stack. Stitched parsers produce a nested stack that is always bounded in size. Fortunately, expressions---which can nest deeply---do not need to maintain ancestor context on the stack, and so this should work fine; we can get away with this because XIRF ensures proper nesting for us. Statements that _do_ need to maintain such context are not nested. This also does not yet support emitting an object on closing tag, which will be necessary for NIR, which will be a streaming IR that is "near" to the source XML in structure. This will then be used to lower into AIR for the ASG, which gives structure needed for further analysis. More information to come; I just want to get this committed to serve as a mental synchronization point and clear my head, since I've been sitting on these changes for so long and have to keep stashing them as I tumble down rabbit holes covered in yak hair. DEV-7145
2022-07-13 13:55:32 -04:00
/// Quote text that would conventionally be delimited in a teletypewriter
/// font.
///
/// This is a more terse alternative to [`Tt`] when formatter composition is
/// unneeded.
pub type TtQuote = Tt<Raw>;
/// Prefix with a single space.
pub type Sp<W> = Prefix<" ", W>;
/// Wrapper for a list that maps each element to a context-specific
/// [`DisplayWrapper`].
///
/// This uses the slice API for wrapping since [`Display`] takes objects by
/// non-mutable reference,
/// and so we cannot consume an iterator.
///
/// The associated types define the wrappers to use for items in various
/// positions depending on the length of the list.
/// They were chosen to be somewhat intuitive given the necessary use cases.
///
/// See the [module-level documentation](super) for examples.
pub trait ListDisplayWrapper {
/// Wrapper to use when the list contains only a single item.
type Single: DisplayWrapper = Raw;
/// Wrapper for the first item in a multi-item list.
type First: DisplayWrapper = Raw;
/// Wrapper for all but the first and last items in a multi-item list.
type Middle: DisplayWrapper = Raw;
/// Wrapper for the last item of a list containing a pair of items.
type LastOfPair: DisplayWrapper = Raw;
/// Wrapper for the last item of a list containing more than two items.
type LastOfMany: DisplayWrapper = Raw;
/// Format a slice using the provided wrappers.
///
/// If a [`Formatter`] is not available,
/// [`ListDisplayWrapper::wrap`] may be used to produce a
/// [`Display`]-able object instead.
fn fmt<T: Display>(list: &[T], f: &mut Formatter) -> Result {
let lasti = list.len().saturating_sub(1);
// This can be further abstracted away using the above primitives,
// if ever we have a use.
tamer: Integrate clippy This invokes clippy as part of `make check` now, which I had previously avoided doing (I'll elaborate on that below). This commit represents the changes needed to resolve all the warnings presented by clippy. Many changes have been made where I find the lints to be useful and agreeable, but there are a number of lints, rationalized in `src/lib.rs`, where I found the lints to be disagreeable. I have provided rationale, primarily for those wondering why I desire to deviate from the default lints, though it does feel backward to rationalize why certain lints ought to be applied (the reverse should be true). With that said, this did catch some legitimage issues, and it was also helpful in getting some older code up-to-date with new language additions that perhaps I used in new code but hadn't gone back and updated old code for. My goal was to get clippy working without errors so that, in the future, when others get into TAMER and are still getting used to Rust, clippy is able to help guide them in the right direction. One of the reasons I went without clippy for so long (though I admittedly forgot I wasn't using it for a period of time) was because there were a number of suggestions that I found disagreeable, and I didn't take the time to go through them and determine what I wanted to follow. Furthermore, it was hard to make that judgment when I was new to the language and lacked the necessary experience to do so. One thing I would like to comment further on is the use of `format!` with `expect`, which is also what the diagnostic system convenience methods do (which clippy does not cover). Because of all the work I've done trying to understand Rust and looking at disassemblies and seeing what it optimizes, I falsely assumed that Rust would convert such things into conditionals in my otherwise-pure code...but apparently that's not the case, when `format!` is involved. I noticed that, after making the suggested fix with `get_ident`, Rust proceeded to then inline it into each call site and then apply further optimizations. It was also previously invoking the thread lock (for the interner) unconditionally and invoking the `Display` implementation. That is not at all what I intended for, despite knowing the eager semantics of function calls in Rust. Anyway, possibly more to come on that, I'm just tired of typing and need to move on. I'll be returning to investigate further diagnostic messages soon.
2023-01-12 10:46:48 -05:00
for next in list.iter().enumerate() {
match next {
(i, x) => Self::fmt_nth(lasti, i, x, f)?,
};
}
Ok(())
}
/// Format an item as if it were the `i`th value of a list of length
/// `lasti+1`.
///
/// This allows for generating list-like output without the expense of
/// actually producing a list.
/// This may be useful when values are stored in different memory
/// location,
/// so that the displaying of those values is a problem of invoking
/// this method on them in the right order,
/// rather than collecting them just for the sake of display.
/// If Rust supports `const` array/Vec functions in the future,
/// this may not be necessary anymore,
/// unless we also don't want the space cost of such a
/// precomputation
/// (but it may come with performance benefits from locality).
#[inline]
fn fmt_nth<T: Display>(
lasti: usize,
i: usize,
item: &T,
f: &mut Formatter,
) -> Result {
match (i, item) {
(0, x) if lasti == 0 => {
Self::Single::fmt(x, f)?;
}
(0, x) => {
Self::First::fmt(x, f)?;
}
(i, x) if lasti == i => {
if i == 1 {
Self::LastOfPair::fmt(x, f)?;
} else {
Self::LastOfMany::fmt(x, f)?;
}
}
(_, x) => {
Self::Middle::fmt(x, f)?;
}
}
Ok(())
}
/// Associate data with a [`ListDisplayWrapper`] for rendering using
/// [`Display`].
///
/// This has the effect of creating an arbitrary [`Display`]
/// implementation for the wrapped slice,
/// which will work well with [`format!`] and anything else that
/// does not have access to an explicit [`Formatter`].
fn wrap<T: Display>(list: &[T]) -> ListWrap<Self, T> {
ListWrap {
list,
_phantom: Default::default(),
}
}
}
/// Format each item of a slice using a [`DisplayWrapper`] formatter,
/// outputting an English list with a serial comma and conjunctive term.
///
/// No formatting is done to a single item,
/// and the serial comma is omitted for only two items.
/// It is assumed that the items are not complete sentences,
/// and nested lists are not expected
/// (in such a case you'd want to replace the inner list with
/// semicolons and a more robust abstraction is needed).
///
/// The use of the serial comma (also known as the Oxford comma) is the
/// preference of the author.
///
/// For example:
/// If we have a slice `[1, 2, 3]`,
/// this will output "1, 2, and 3".
/// If we have a slice `[1, 2]`,
/// it will omit the serial comma and output "1 and 2",
/// since no person would write "1, and 2" unless they wished to
/// place particularly dramatic emphasis on the first item.
///
/// To output a qualifier before the list,
/// see [`QualConjList`].
pub struct ConjList<const CONJ: &'static str, W: DisplayWrapper>(
PhantomData<W>,
);
impl<const CONJ: &'static str, W: DisplayWrapper> ListDisplayWrapper
for ConjList<CONJ, W>
{
type Single = W;
type First = W;
type Middle = Prefix<", ", W>;
type LastOfPair = Sp<Prefix<CONJ, Sp<W>>>;
// Comma after the penultimate term (serial/Oxford comma).
type LastOfMany = Prefix<", ", Prefix<CONJ, Sp<W>>>;
}
/// A list of values with a serial comma and the term "and" as a
/// conjunction between the penultimate and final items.
pub type AndConjList<W> = ConjList<"and", W>;
/// A list of values with a serial comma and the term "or" as a
/// conjunction between the penultimate and final items.
///
/// Terminology note:
/// English refers to the term "or" here as a conjunction between words,
/// which differs from "or" in logic as a disjunction.
pub type OrConjList<W> = ConjList<"or", W>;
/// Format each item of a slice using a [`DisplayWrapper`] formatter,
/// outputting an English list with a serial comma and conjunctive term,
/// qualified by a term that is singular or plural depending on the length
/// of the list.
///
/// See [`ConjList`] for more information;
/// this operates identically but with the addition of the qualifier.
pub struct QualConjList<
const QUAL_ONE: &'static str,
const QUAL_MANY: &'static str,
const CONJ: &'static str,
W: DisplayWrapper,
>(PhantomData<W>);
impl<
const QUAL_ONE: &'static str,
const QUAL_MANY: &'static str,
const CONJ: &'static str,
W: DisplayWrapper,
> ListDisplayWrapper for QualConjList<QUAL_ONE, QUAL_MANY, CONJ, W>
{
type Single =
Prefix<QUAL_ONE, Sp<<ConjList<CONJ, W> as ListDisplayWrapper>::Single>>;
type First =
Prefix<QUAL_MANY, Sp<<ConjList<CONJ, W> as ListDisplayWrapper>::First>>;
type Middle = <ConjList<CONJ, W> as ListDisplayWrapper>::Middle;
type LastOfPair = <ConjList<CONJ, W> as ListDisplayWrapper>::LastOfPair;
type LastOfMany = <ConjList<CONJ, W> as ListDisplayWrapper>::LastOfMany;
}
/// A list of values with a serial comma and the term "and" as a
/// conjunction between the penultimate and final items.
/// prefixed with a singular or plural qualifier term depending on the
/// length of the list.
pub type AndQualConjList<
const QUAL_ONE: &'static str,
const QUAL_MANY: &'static str,
W,
> = QualConjList<QUAL_ONE, QUAL_MANY, "and", W>;
/// A list of values with a serial comma and the term "or" as a
/// conjunction between the penultimate and final items.
/// prefixed with a singular or plural qualifier term depending on the
/// length of the list.
///
/// Terminology note:
/// English refers to the term "or" here as a conjunction between words,
/// which differs from "or" in logic as a disjunction.
pub type OrQualConjList<
const QUAL_ONE: &'static str,
const QUAL_MANY: &'static str,
W,
> = QualConjList<QUAL_ONE, QUAL_MANY, "or", W>;
/// A list of values separated by a delimiter.
///
/// This is analogous to the typical `join` operation on lists.
/// Single-element lists will have no delimiter,
/// but lists of multiple elements will have each element delimited by the
/// same delimiter.
/// This is in contrast with wrappers that follow more sophisticated
/// typographical conventions,
/// such as [`ConjList`] and [`QualConjList`].
pub struct JoinListWrap<const DELIM: &'static str, W: DisplayWrapper>(
PhantomData<W>,
);
impl<const DELIM: &'static str, W: DisplayWrapper> ListDisplayWrapper
for JoinListWrap<DELIM, W>
{
type Single = W;
type First = W;
type Middle = Prefix<DELIM, W>;
type LastOfPair = Prefix<DELIM, W>;
type LastOfMany = Prefix<DELIM, W>;
}
/// List wrapper with associated data.
///
/// This has the effect of creating an arbitrary [`Display`] implementation
/// for the wrapped list,
/// which will work well with [`format!`] and anything else that does
/// not have access to an explicit [`Formatter`].
pub struct ListWrap<'a, W: ListDisplayWrapper + ?Sized, T: Display> {
list: &'a [T],
_phantom: PhantomData<W>,
}
impl<'a, W: ListDisplayWrapper + ?Sized, T: Display> Display
for ListWrap<'a, W, T>
{
fn fmt(&self, f: &mut Formatter) -> Result {
W::fmt(self.list, f)
}
}
/// Wrap a `fmt`-like function to be used as [`Display::fmt`] for this
/// object.
///
/// This works around the problem of having a function expecting a
/// [`Formatter`],
/// but not having a [`Formatter`] to call it with.
/// It also allows for arbitrary (compatible) functions to be used as
/// [`Display`].
pub struct DisplayFn<F: Fn(&mut Formatter) -> Result>(pub F);
impl<F: Fn(&mut Formatter) -> Result> Display for DisplayFn<F> {
fn fmt(&self, f: &mut Formatter) -> Result {
(self.0)(f)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn prefix() {
assert_eq!(Prefix::<"@!", Raw>::wrap("foo").to_string(), "@!foo",);
assert_eq!(
Prefix::<"1", Prefix::<"2", Raw>>::wrap("bar").to_string(),
"12bar",
);
}
#[test]
fn suffix() {
assert_eq!(Suffix::<"!@", Raw>::wrap("foo").to_string(), "foo!@",);
assert_eq!(
Suffix::<"1", Suffix::<"2", Raw>>::wrap("bar").to_string(),
"bar21",
);
}
#[test]
fn delimit() {
assert_eq!(Delim::<"[", "]", Raw>::wrap("foo").to_string(), "[foo]",);
assert_eq!(
Delim::<"(", ")", Delim::<"|", "|", Raw>>::wrap("bar").to_string(),
"(|bar|)",
);
}
// Certain types are trivially verifiable by their definition,
// and so have no tests.
#[test]
fn conj_list_single() {
assert_eq!(
ConjList::<"unused", Raw>::wrap(&["single"]).to_string(),
"single",
);
// Ensure that we're actually mapping.
assert_eq!(
ConjList::<"unused", Delim<"(", ")", Raw>>::wrap(&["single"])
.to_string(),
"(single)",
);
}
#[test]
fn conj_list_double() {
assert_eq!(
ConjList::<"and", Raw>::wrap(&["first", "second"]).to_string(),
"first and second",
);
// Ensure that we're actually mapping.
assert_eq!(
ConjList::<"or", Delim<"(", ")", Raw>>::wrap(&["first", "second"])
.to_string(),
"(first) or (second)",
);
}
#[test]
fn conj_list_many() {
assert_eq!(
ConjList::<"and", Raw>::wrap(&["first", "second", "third"])
.to_string(),
"first, second, and third",
);
// Ensure that we're actually mapping.
assert_eq!(
ConjList::<"or", Delim<"[", "]", Raw>>::wrap(&[
"first", "second", "third"
])
.to_string(),
"[first], [second], or [third]",
);
}
#[test]
fn qualified_conj_list_single() {
assert_eq!(
QualConjList::<"thing", "things", "unused", Raw>::wrap(&["a"])
.to_string(),
"thing a",
);
// Ensure that we're actually mapping.
assert_eq!(
QualConjList::<"thing", "things", "unused", Delim<"(", ")", Raw>>::wrap(&["a"])
.to_string(),
"thing (a)",
);
}
#[test]
fn qualified_conj_list_double() {
assert_eq!(
QualConjList::<"thing", "things", "and", Raw>::wrap(&["a", "b"])
.to_string(),
"things a and b",
);
// Ensure that we're actually mapping.
assert_eq!(
QualConjList::<"thing", "things", "or", Delim<"(", ")", Raw>>::wrap(&["a", "b"])
.to_string(),
"things (a) or (b)",
);
}
#[test]
fn qualified_conj_list_many() {
assert_eq!(
QualConjList::<"thing", "things", "and", Raw>::wrap(&[
"a", "b", "c"
])
.to_string(),
"things a, b, and c",
);
// Ensure that we're actually mapping.
assert_eq!(
QualConjList::<"thing", "things", "or", Delim<"(", ")", Raw>>::wrap(&["a", "b", "c"])
.to_string(),
"things (a), (b), or (c)",
);
}
#[test]
fn display_fn() {
assert_eq!(
DisplayFn(|f| write!(f, "test fmt")).to_string(),
"test fmt",
);
}
// `fmt_nth` is used by the above tests,
// but that's an implementation detail;
// we expose it as a public API so it ought to be tested too.
#[test]
fn fmt_nth() {
type Sut = QualConjList<"thing", "things", "or", Raw>;
assert_eq!(
DisplayFn(|f| Sut::fmt_nth(0, 0, &"foo", f)).to_string(),
"thing foo",
);
assert_eq!(
DisplayFn(|f| Sut::fmt_nth(1, 0, &"foo", f)).to_string(),
"things foo",
);
assert_eq!(
DisplayFn(|f| Sut::fmt_nth(1, 1, &"foo", f)).to_string(),
" or foo",
);
assert_eq!(
DisplayFn(|f| Sut::fmt_nth(2, 0, &"foo", f)).to_string(),
"things foo",
);
assert_eq!(
DisplayFn(|f| Sut::fmt_nth(2, 1, &"foo", f)).to_string(),
", foo",
);
assert_eq!(
DisplayFn(|f| Sut::fmt_nth(2, 2, &"foo", f)).to_string(),
", or foo",
);
}
#[test]
fn delim_list() {
assert_eq!(
JoinListWrap::<"::", Raw>::wrap(&["one", "two", "three"])
.to_string(),
"one::two::three",
);
assert_eq!(
JoinListWrap::<" -> ", TtQuote>::wrap(&["foo", "bar"]).to_string(),
"`foo` -> `bar`",
);
}
}