[fine] Start on a pretty printer
ala https://justinpombrio.net/2024/02/23/a-twist-on-Wadlers-printer.html
This commit is contained in:
parent
0b0b5d72d0
commit
228ca719f0
7 changed files with 225 additions and 4 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -732,6 +732,7 @@ dependencies = [
|
||||||
"salsa-2022",
|
"salsa-2022",
|
||||||
"syn 2.0.52",
|
"syn 2.0.52",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,5 @@ syn = "2.0.47"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1.0.56"
|
thiserror = "1.0.56"
|
||||||
|
unicode-width = "=0.1.11"
|
||||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", package = "salsa-2022" }
|
salsa = { git = "https://github.com/salsa-rs/salsa.git", package = "salsa-2022" }
|
||||||
5
fine/src/format/mod.rs
Normal file
5
fine/src/format/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
mod notation;
|
||||||
|
mod print;
|
||||||
|
|
||||||
|
pub use notation::{flat, indent, nl, txt, Notation};
|
||||||
|
pub use print::pretty_print;
|
||||||
65
fine/src/format/notation.rs
Normal file
65
fine/src/format/notation.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
use std::ops::{BitAnd, BitOr};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Notation(pub(crate) Rc<NotationInner>);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum NotationInner {
|
||||||
|
Newline,
|
||||||
|
Text(String, u32),
|
||||||
|
Flat(Notation),
|
||||||
|
Indent(u32, Notation),
|
||||||
|
Concat(Notation, Notation),
|
||||||
|
Choice(Notation, Notation),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display a newline
|
||||||
|
pub fn nl() -> Notation {
|
||||||
|
Notation(Rc::new(NotationInner::Newline))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display text exactly as-is. The text should not contain a newline!
|
||||||
|
pub fn txt(s: impl ToString) -> Notation {
|
||||||
|
let string = s.to_string();
|
||||||
|
let width = unicode_width::UnicodeWidthStr::width(&string as &str) as u32;
|
||||||
|
Notation(Rc::new(NotationInner::Text(string, width)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use the leftmost option of every choice in the contained Notation.
|
||||||
|
/// If the contained Notation follows the recommendation of not
|
||||||
|
/// putting newlines in the left-most options of choices, then this
|
||||||
|
/// `flat` will be displayed all on one line.
|
||||||
|
pub fn flat(notation: Notation) -> Notation {
|
||||||
|
Notation(Rc::new(NotationInner::Flat(notation)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Increase the indentation level of the contained notation by the
|
||||||
|
/// given width. The indentation level determines the number of spaces
|
||||||
|
/// put after `Newline`s. (It therefore doesn't affect the first line
|
||||||
|
/// of a notation.)
|
||||||
|
pub fn indent(indent: u32, notation: Notation) -> Notation {
|
||||||
|
Notation(Rc::new(NotationInner::Indent(indent, notation)))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BitAnd<Notation> for Notation {
|
||||||
|
type Output = Notation;
|
||||||
|
|
||||||
|
/// Display both notations. The first character of the right
|
||||||
|
/// notation immediately follows the last character of the
|
||||||
|
/// left notation.
|
||||||
|
fn bitand(self, other: Notation) -> Notation {
|
||||||
|
Notation(Rc::new(NotationInner::Concat(self, other)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BitOr<Notation> for Notation {
|
||||||
|
type Output = Notation;
|
||||||
|
|
||||||
|
/// If inside a `flat`, _or_ the first line of the left notation
|
||||||
|
/// fits within the required width, then display the left
|
||||||
|
/// notation. Otherwise, display the right notation.
|
||||||
|
fn bitor(self, other: Notation) -> Notation {
|
||||||
|
Notation(Rc::new(NotationInner::Choice(self, other)))
|
||||||
|
}
|
||||||
|
}
|
||||||
152
fine/src/format/print.rs
Normal file
152
fine/src/format/print.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
use super::notation::{Notation, NotationInner};
|
||||||
|
|
||||||
|
pub fn pretty_print(notation: &Notation, printing_width: u32) -> String {
|
||||||
|
let mut printer = PrettyPrinter::new(notation, printing_width);
|
||||||
|
printer.print()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct Chunk<'a> {
|
||||||
|
notation: &'a Notation,
|
||||||
|
indent: u32,
|
||||||
|
flat: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Chunk<'a> {
|
||||||
|
fn with_notation(self, notation: &'a Notation) -> Chunk<'a> {
|
||||||
|
Chunk {
|
||||||
|
notation,
|
||||||
|
indent: self.indent,
|
||||||
|
flat: self.flat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indented(self, indent: u32) -> Chunk<'a> {
|
||||||
|
Chunk {
|
||||||
|
notation: self.notation,
|
||||||
|
indent: self.indent + indent,
|
||||||
|
flat: self.flat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flat(self) -> Chunk<'a> {
|
||||||
|
Chunk {
|
||||||
|
notation: self.notation,
|
||||||
|
indent: self.indent,
|
||||||
|
flat: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PrettyPrinter<'a> {
|
||||||
|
/// Maximum line width that we'll try to stay within
|
||||||
|
width: u32,
|
||||||
|
/// Current column position
|
||||||
|
col: u32,
|
||||||
|
/// A stack of chunks to print. The _top_ of the stack is the
|
||||||
|
/// _end_ of the vector, which represents the _earliest_ part
|
||||||
|
/// of the document to print.
|
||||||
|
chunks: Vec<Chunk<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PrettyPrinter<'a> {
|
||||||
|
fn new(notation: &'a Notation, width: u32) -> PrettyPrinter<'a> {
|
||||||
|
let chunk = Chunk {
|
||||||
|
notation,
|
||||||
|
indent: 0,
|
||||||
|
flat: false,
|
||||||
|
};
|
||||||
|
PrettyPrinter {
|
||||||
|
width,
|
||||||
|
col: 0,
|
||||||
|
chunks: vec![chunk],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print(&mut self) -> String {
|
||||||
|
use NotationInner::*;
|
||||||
|
|
||||||
|
let mut output = String::new();
|
||||||
|
while let Some(chunk) = self.chunks.pop() {
|
||||||
|
match chunk.notation.0.as_ref() {
|
||||||
|
Text(text, width) => {
|
||||||
|
output.push_str(text);
|
||||||
|
self.col += width;
|
||||||
|
}
|
||||||
|
Newline => {
|
||||||
|
output.push('\n');
|
||||||
|
for _ in 0..chunk.indent {
|
||||||
|
output.push(' ');
|
||||||
|
}
|
||||||
|
self.col = chunk.indent;
|
||||||
|
}
|
||||||
|
Flat(x) => self.chunks.push(chunk.with_notation(x).flat()),
|
||||||
|
Indent(i, x) => self.chunks.push(chunk.with_notation(x).indented(*i)),
|
||||||
|
Concat(x, y) => {
|
||||||
|
self.chunks.push(chunk.with_notation(y));
|
||||||
|
self.chunks.push(chunk.with_notation(x));
|
||||||
|
}
|
||||||
|
Choice(x, y) => {
|
||||||
|
if chunk.flat || self.fits(chunk.with_notation(x)) {
|
||||||
|
self.chunks.push(chunk.with_notation(x));
|
||||||
|
} else {
|
||||||
|
self.chunks.push(chunk.with_notation(y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fits(&self, chunk: Chunk<'a>) -> bool {
|
||||||
|
use NotationInner::*;
|
||||||
|
|
||||||
|
let mut remaining = if self.col <= self.width {
|
||||||
|
self.width - self.col
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut stack = vec![chunk];
|
||||||
|
let mut chunks = &self.chunks as &[Chunk];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let chunk = match stack.pop() {
|
||||||
|
Some(chunk) => chunk,
|
||||||
|
None => match chunks.split_last() {
|
||||||
|
None => return true,
|
||||||
|
Some((chunk, more_chunks)) => {
|
||||||
|
chunks = more_chunks;
|
||||||
|
*chunk
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match chunk.notation.0.as_ref() {
|
||||||
|
Newline => return true,
|
||||||
|
Text(_text, text_width) => {
|
||||||
|
if *text_width <= remaining {
|
||||||
|
remaining -= *text_width;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Flat(x) => stack.push(chunk.with_notation(x).flat()),
|
||||||
|
Indent(i, x) => stack.push(chunk.with_notation(x).indented(*i)),
|
||||||
|
Concat(x, y) => {
|
||||||
|
stack.push(chunk.with_notation(y));
|
||||||
|
stack.push(chunk.with_notation(x));
|
||||||
|
}
|
||||||
|
Choice(x, y) => {
|
||||||
|
if chunk.flat {
|
||||||
|
stack.push(chunk.with_notation(x));
|
||||||
|
} else {
|
||||||
|
// Relies on the rule that for every choice
|
||||||
|
// `x | y`, the first line of `y` is no longer
|
||||||
|
// than the first line of `x`.
|
||||||
|
stack.push(chunk.with_notation(y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ use program::{Module, Program, StandardModuleLoader};
|
||||||
use vm::{eval, Context};
|
use vm::{eval, Context};
|
||||||
|
|
||||||
pub mod compiler;
|
pub mod compiler;
|
||||||
|
pub mod format;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod program;
|
pub mod program;
|
||||||
pub mod semantics;
|
pub mod semantics;
|
||||||
|
|
|
||||||
|
|
@ -1494,10 +1494,6 @@ mod tests {
|
||||||
// them to key function definitions and the type checker and use them
|
// them to key function definitions and the type checker and use them
|
||||||
// to link classes to their definitions, etc. It's important that an
|
// to link classes to their definitions, etc. It's important that an
|
||||||
// Option<TreeRef> be *extremely* cheap to manipulate.
|
// Option<TreeRef> be *extremely* cheap to manipulate.
|
||||||
//
|
|
||||||
// TODO: This optimization isn't as good as it might be because tokens are
|
|
||||||
// huge so Child is huge no matter what we do. If we retain
|
|
||||||
// tokens out of line then we can take full advantage of this.
|
|
||||||
assert_eq!(4, std::mem::size_of::<Option<TreeRef>>());
|
assert_eq!(4, std::mem::size_of::<Option<TreeRef>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue