Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove syn dependency #8

Merged
merged 7 commits into from
Sep 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ All notable changes to this project will be documented in this file.


## [Unreleased]
### Breaking changes
- Minimal Rust version bumped to 1.46.0

### Changed
- `syn` dependency removed


## [0.1.1] - 2020-09-05
### Fixed
- Minor documentation fixes


## 0.1.0 - 2020-07-30
### Added
- Everything (`write`, `writeln`, `print`, `println`, `style`)
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Bunt

`bunt` offers macros to easily print colored and formatted text to a terminal.
It is just a convenience API on top of [`termcolor`](https://crates.io/crates/termcolor).
`bunt` is implemented using procedural macros, but as it does not depend on `syn`, compilation is fairly quick (≈1.5s on my machine, including all dependencies).

```rust
// Style tags will color/format text between the tags.
Expand All @@ -27,7 +28,8 @@ See [**the documentation**](https://docs.rs/bunt) for more information.

## Status of this project

Very young project. Syntax is by no means final yet.
This is still a young project, but I already use it in two applications of mine.
The syntax is certainly not final yet.
[Seeking feedback from the community!](https://github.com/LukasKalbertodt/bunt/issues/1)


Expand Down
1 change: 0 additions & 1 deletion macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,3 @@ proc-macro = true
[dependencies]
quote = "1"
proc-macro2 = "1.0.19"
syn = { version = "1", features = ["full", "extra-traits"] }
26 changes: 26 additions & 0 deletions macros/src/err.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use proc_macro2::{Span, TokenStream};
use quote::quote_spanned;


/// Helper macro to easily create an error with a span.
macro_rules! err {
($fmt:literal $($t:tt)*) => { Error { span: Span::call_site(), msg: format!($fmt $($t)*) } };
($span:expr, $($t:tt)+) => { Error { span: $span, msg: format!($($t)+) } };
}

/// Simply contains a message and a span. Can be converted to a `compile_error!`
/// via `to_compile_error`.
#[derive(Debug)]
pub(crate) struct Error {
pub(crate) msg: String,
pub(crate) span: Span,
}

impl Error {
pub(crate) fn to_compile_error(&self) -> TokenStream {
let msg = &self.msg;
quote_spanned! {self.span=>
compile_error!(#msg);
}
}
}
229 changes: 229 additions & 0 deletions macros/src/gen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
//! Generating the output tokens from the parsed intermediate representation.

use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use std::{
collections::BTreeSet,
fmt::Write,
};
use crate::{
err::Error,
ir::{WriteInput, FormatStrFragment, ArgRefKind, Style, Color, Expr},
};


impl WriteInput {
pub(crate) fn gen_output(&self) -> Result<TokenStream, Error> {
// Helper functions to create idents for argument bindings
fn pos_arg_ident(id: u32) -> Ident {
Ident::new(&format!("arg_pos_{}", id), Span::mixed_site())
}
fn name_arg_ident(id: &str) -> Ident {
Ident::new(&format!("arg_name_{}", id), Span::mixed_site())
}

// Create a binding for each given argument. This is useful for two
// reasons:
// - The given expression could have side effects or be compuationally
// expensive. The formatting macros from std guarantee that the
// expression is evaluated only once, so we want to guarantee the
// same.
// - We can then very easily refer to all arguments later. Without these
// bindings, we have to do lots of tricky logic to get the right
// arguments in each invidiual `write` call.
let mut arg_bindings = TokenStream::new();
for (i, arg) in self.args.positional.iter().enumerate() {
let ident = pos_arg_ident(i as u32);
arg_bindings.extend(quote! {
let #ident = &#arg;
})
}
for (name, arg) in self.args.named.iter() {
let ident = name_arg_ident(name);
arg_bindings.extend(quote! {
let #ident = &#arg;
})
}

// Prepare the actual process of writing to the target according to the
// format string.
let buf = Ident::new("buf", Span::mixed_site());
let mut style_stack = Vec::new();
let mut writes = TokenStream::new();
let mut next_arg_index = 0;

for segment in &self.format_str.fragments {
match segment {
// A formatting fragment. This is the more tricky one. We have
// to construct a `std::write!` invocation that has the right
// fmt string, the right arguments (and no additional ones!) and
// the correct argument references.
FormatStrFragment::Fmt { fmt_str_parts, args } => {
let mut fmt_str = fmt_str_parts[0].clone();
let mut used_args = BTreeSet::new();

for (i, arg) in args.into_iter().enumerate() {
let ident = match &arg.kind {
ArgRefKind::Next => {
let ident = pos_arg_ident(next_arg_index as u32);
if self.args.positional.get(next_arg_index).is_none() {
return Err(
err!("invalid '{{}}' argument reference \
(too few actual arguments)")
);
}

next_arg_index += 1;
ident
}
ArgRefKind::Position(pos) => {
let ident = pos_arg_ident(*pos);
if self.args.positional.get(*pos as usize).is_none() {
return Err(err!(
"invalid reference to positional argument {} (there are \
not that many arguments)",
pos,
));
}

ident
}
ArgRefKind::Name(name) => {
let ident = name_arg_ident(&name);
if self.args.named.get(name).is_none() {
return Err(err!("there is no argument named `{}`", name));
}

ident
}
};

std::write!(fmt_str, "{{{}{}}}", ident, arg.format_spec).unwrap();
used_args.insert(ident);
fmt_str.push_str(&fmt_str_parts[i + 1]);
}


// Combine everything in `write!` invocation.
writes.extend(quote! {
std::write!(#buf, #fmt_str #(, #used_args = #used_args)* )?;
});
}

// A style start tag: we simply create the `ColorSpec` and call
// `set_color`. The interesting part is how the styles stack and
// merge.
FormatStrFragment::StyleStart(style) => {
let last_style = style_stack.last().copied().unwrap_or(Style::default());
let new_style = style.or(last_style);
let style_def = new_style.to_tokens();
style_stack.push(new_style);
writes.extend(quote! {
::bunt::termcolor::WriteColor::set_color(#buf, &#style_def)?;
});
}

// Revert the last style tag. This means that we pop the topmost
// style from the stack and apply the *then* topmost style
// again.
FormatStrFragment::StyleEnd => {
style_stack.pop().ok_or(err!("unmatched closing style tag"))?;
let style = style_stack.last().copied().unwrap_or(Style::default());
let style_def = style.to_tokens();
writes.extend(quote! {
::bunt::termcolor::WriteColor::set_color(#buf, &#style_def)?;
});
}
}
}

// Check if the style tags are balanced
if !style_stack.is_empty() {
return Err(err!("unclosed style tag"));
}

// Combine everything.
let target = &self.target;
Ok(quote! {
(|| -> Result<(), ::std::io::Error> {
use std::io::Write as _;

#arg_bindings
let #buf = &mut #target;
#writes

Ok(())
})()
})
}
}

impl quote::ToTokens for Expr {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(self.tokens.clone())
}
}

impl Style {
/// Returns a token stream representing an expression constructing the
/// `ColorSpec` value corresponding to `self`.
pub(crate) fn to_tokens(&self) -> TokenStream {
let ident = Ident::new("color_spec", Span::mixed_site());
let mut method_calls = TokenStream::new();

if let Some(fg) = self.fg {
let fg = fg.to_tokens();
method_calls.extend(quote! {
#ident.set_fg(Some(#fg));
})
}
if let Some(bg) = self.bg {
let bg = bg.to_tokens();
method_calls.extend(quote! {
#ident.set_bg(Some(#bg));
})
}

macro_rules! attr {
($field:ident, $method:ident) => {
if let Some(b) = self.$field {
method_calls.extend(quote! {
#ident.$method(#b);
});
}
};
}

attr!(bold, set_bold);
attr!(italic, set_italic);
attr!(underline, set_underline);
attr!(intense, set_intense);

quote! {
{
let mut #ident = ::bunt::termcolor::ColorSpec::new();
#method_calls
#ident
}
}
}
}

impl Color {
/// Returns a token stream representing a value of type `termcolor::Color`.
fn to_tokens(&self) -> TokenStream {
let variant = match self {
Self::Black => Some(quote! { Black }),
Self::Blue => Some(quote! { Blue }),
Self::Green => Some(quote! { Green }),
Self::Red => Some(quote! { Red }),
Self::Cyan => Some(quote! { Cyan }),
Self::Magenta => Some(quote! { Magenta }),
Self::Yellow => Some(quote! { Yellow }),
Self::White => Some(quote! { White }),
Self::Rgb(r, g, b) => Some(quote! { Rgb(#r, #g, #b) }),
};

quote! { ::bunt::termcolor::Color:: #variant }
}
}
Loading