Improve formatter on long-lines, in particular bin-ops.

This commit is contained in:
KtorZ 2024-08-06 17:29:00 +02:00
parent 91e0e2493a
commit 9d8fdf787c
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
21 changed files with 364 additions and 50 deletions

View File

@ -65,6 +65,10 @@
See also [#978](https://github.com/aiken-lang/aiken/pull/978).
- **aiken-lang**: rework formatter behaviour on long-lines, especially in the presence of binary operators. @KtorZ
- **aiken-lang**: provide better errors for unknown types used in cyclic type definitions. @KtorZ
- **aiken-project**: fix blueprint's apply truncating last character of outputs. See [#987](https://github.com/aiken-lang/aiken/issues/987). @KtorZ
- **aiken-project**: provide better error (include input ref) when inputs are missing during transaction evaluation. See [#974](https://github.com/aiken-lang/aiken/issues/974). @KtorZ

View File

@ -1366,6 +1366,26 @@ impl<A, B> Pattern<A, B> {
}
}
/// Returns true when a Pattern can be displayed in a flex-break manner (i.e. tries to fit as
/// much as possible on a single line). When false, long lines with several of those patterns
/// will be broken down to one pattern per line.
pub fn is_simple_pattern_to_format(&self) -> bool {
match self {
Self::ByteArray { .. } | Self::Int { .. } | Self::Var { .. } | Self::Discard { .. } => {
true
}
Self::Pair { fst, snd, .. } => {
fst.is_simple_pattern_to_format() && snd.is_simple_pattern_to_format()
}
Self::Tuple { elems, .. } => elems.iter().all(|e| e.is_simple_pattern_to_format()),
Self::List { elements, .. } if elements.len() <= 3 => {
elements.iter().all(|e| e.is_simple_pattern_to_format())
}
Self::Constructor { arguments, .. } => arguments.is_empty(),
_ => false,
}
}
pub fn with_spread(&self) -> bool {
match self {
Pattern::Constructor {

View File

@ -1344,11 +1344,23 @@ impl UntypedExpr {
}
}
pub fn is_simple_constant(&self) -> bool {
matches!(
self,
Self::String { .. } | Self::UInt { .. } | Self::ByteArray { .. }
)
/// Returns true when an UntypedExpr can be displayed in a flex-break manner (i.e. tries to fit as
/// much as possible on a single line). When false, long lines with several of those patterns
/// will be broken down to one expr per line.
pub fn is_simple_expr_to_format(&self) -> bool {
match self {
Self::String { .. } | Self::UInt { .. } | Self::ByteArray { .. } | Self::Var { .. } => {
true
}
Self::Pair { fst, snd, .. } => {
fst.is_simple_expr_to_format() && snd.is_simple_expr_to_format()
}
Self::Tuple { elems, .. } => elems.iter().all(|e| e.is_simple_expr_to_format()),
Self::List { elements, .. } if elements.len() <= 3 => {
elements.iter().all(|e| e.is_simple_expr_to_format())
}
_ => false,
}
}
pub fn lambda(

View File

@ -26,7 +26,7 @@ use std::rc::Rc;
use vec1::Vec1;
pub const INDENT: isize = 2;
pub const DOCS_MAX_COLUMNS: isize = 80;
pub const MAX_COLUMNS: isize = 80;
pub fn pretty(writer: &mut String, module: UntypedModule, extra: ModuleExtra, src: &str) {
let intermediate = Intermediate {
@ -50,7 +50,7 @@ pub fn pretty(writer: &mut String, module: UntypedModule, extra: ModuleExtra, sr
Formatter::with_comments(&intermediate)
.module(&module)
.pretty_print(80, writer);
.pretty_print(MAX_COLUMNS, writer);
}
#[derive(Debug)]
@ -1332,8 +1332,15 @@ impl<'comments> Formatter<'comments> {
let left_precedence = left.binop_precedence();
let right_precedence = right.binop_precedence();
let left = self.expr(left, false);
let right = self.expr(right, false);
let mut left = self.expr(left, false);
if left.fits(MAX_COLUMNS) {
left = left.force_unbroken()
}
let mut right = self.expr(right, false);
if right.fits(MAX_COLUMNS) {
right = right.force_unbroken()
}
self.operator_side(
left,
@ -1724,11 +1731,7 @@ impl<'comments> Formatter<'comments> {
let doc = head.append(tail.clone()).group();
// Wrap arguments on multi-lines if they are lengthy.
if doc
.clone()
.to_pretty_string(DOCS_MAX_COLUMNS)
.contains('\n')
{
if doc.clone().to_pretty_string(MAX_COLUMNS).contains('\n') {
let head = name
.to_doc()
.append(self.docs_fn_args(args).force_break())
@ -1862,7 +1865,7 @@ impl<'comments> Formatter<'comments> {
tail: Option<&'a UntypedExpr>,
) -> Document<'a> {
let comma: fn() -> Document<'a> =
if tail.is_none() && elements.iter().all(UntypedExpr::is_simple_constant) {
if elements.iter().all(UntypedExpr::is_simple_expr_to_format) {
|| flex_break(",", ", ")
} else {
|| break_(",", ", ")
@ -1905,8 +1908,14 @@ impl<'comments> Formatter<'comments> {
.group(),
Pattern::List { elements, tail, .. } => {
let break_style: fn() -> Document<'a> =
if elements.iter().all(Pattern::is_simple_pattern_to_format) {
|| flex_break(",", ", ")
} else {
|| break_(",", ", ")
};
let elements_document =
join(elements.iter().map(|e| self.pattern(e)), break_(",", ", "));
join(elements.iter().map(|e| self.pattern(e)), break_style());
let tail = tail.as_ref().map(|e| {
if e.is_discard() {
nil()

View File

@ -12,9 +12,8 @@
//! - `ForcedBreak` from Elixir.
#![allow(clippy::wrong_self_convention)]
use std::collections::VecDeque;
use itertools::Itertools;
use std::collections::VecDeque;
#[macro_export]
macro_rules! docvec {
@ -131,6 +130,9 @@ pub enum Document<'a> {
/// Forces contained groups to break
ForceBroken(Box<Self>),
/// Forces contained group to not break
ForceUnbroken(Box<Self>),
/// Renders `broken` if group is broken, `unbroken` otherwise
Break {
broken: &'a str,
@ -166,12 +168,12 @@ enum Mode {
//
/// Broken and forced to remain broken
ForcedBroken,
// ForcedUnbroken, // Used for next_break_fits. Not yet implemented.
ForcedUnbroken,
}
impl Mode {
fn is_forced(&self) -> bool {
matches!(self, Mode::ForcedBroken)
matches!(self, Mode::ForcedBroken | Mode::ForcedUnbroken)
}
}
@ -199,6 +201,8 @@ fn fits(
Document::Nest(i, doc) => docs.push_front((i + indent, mode, doc)),
Document::ForceUnbroken(doc) => docs.push_front((indent, mode, doc)),
Document::Group(doc) if mode.is_forced() => docs.push_front((indent, mode, doc)),
Document::Group(doc) => docs.push_front((indent, Mode::Unbroken, doc)),
@ -209,7 +213,7 @@ fn fits(
Document::Break { unbroken, .. } => match mode {
Mode::Broken | Mode::ForcedBroken => return true,
Mode::Unbroken => current_width += unbroken.len() as isize,
Mode::Unbroken | Mode::ForcedUnbroken => current_width += unbroken.len() as isize,
},
Document::Vec(vec) => {
@ -254,29 +258,34 @@ fn format(
break_first,
kind: BreakKind::Flex,
} => {
let unbroken_width = width + unbroken.len() as isize;
if fits(limit, unbroken_width, docs.clone()) {
if mode == Mode::ForcedUnbroken {
writer.push_str(unbroken);
width = unbroken_width;
continue;
}
if *break_first {
writer.push('\n');
for _ in 0..indent {
writer.push(' ');
}
writer.push_str(broken);
width += unbroken.len() as isize
} else {
writer.push_str(broken);
writer.push('\n');
for _ in 0..indent {
writer.push(' ');
}
}
let unbroken_width = width + unbroken.len() as isize;
width = indent;
if fits(limit, unbroken_width, docs.clone()) {
writer.push_str(unbroken);
width = unbroken_width;
continue;
}
if *break_first {
writer.push('\n');
for _ in 0..indent {
writer.push(' ');
}
writer.push_str(broken);
} else {
writer.push_str(broken);
writer.push('\n');
for _ in 0..indent {
writer.push(' ');
}
}
width = indent;
}
}
// Strict breaks are conditional to the mode
@ -287,7 +296,7 @@ fn format(
kind: BreakKind::Strict,
} => {
width = match mode {
Mode::Unbroken => {
Mode::Unbroken | Mode::ForcedUnbroken => {
writer.push_str(unbroken);
width + unbroken.len() as isize
@ -344,10 +353,16 @@ fn format(
Document::Group(doc) => {
let mut group_docs = VecDeque::new();
group_docs.push_front((indent, Mode::Unbroken, doc.as_ref()));
let inner_mode = if mode == Mode::ForcedUnbroken {
Mode::ForcedUnbroken
} else {
Mode::Unbroken
};
group_docs.push_front((indent, inner_mode, doc.as_ref()));
if fits(limit, width, group_docs) {
docs.push_front((indent, Mode::Unbroken, doc));
docs.push_front((indent, inner_mode, doc));
} else {
docs.push_front((indent, Mode::Broken, doc));
}
@ -356,6 +371,10 @@ fn format(
Document::ForceBroken(document) => {
docs.push_front((indent, Mode::ForcedBroken, document));
}
Document::ForceUnbroken(document) => {
docs.push_front((indent, Mode::ForcedUnbroken, document));
}
}
}
}
@ -409,6 +428,12 @@ pub fn flex_prebreak<'a>(broken: &'a str, unbroken: &'a str) -> Document<'a> {
}
impl<'a> Document<'a> {
pub fn fits(&self, target: isize) -> bool {
let mut docs = VecDeque::new();
docs.push_front((0, Mode::Unbroken, self));
fits(target, 0, docs)
}
pub fn group(self) -> Self {
Self::Group(Box::new(self))
}
@ -421,6 +446,10 @@ impl<'a> Document<'a> {
Self::ForceBroken(Box::new(self))
}
pub fn force_unbroken(self) -> Self {
Self::ForceUnbroken(Box::new(self))
}
pub fn append(self, second: impl Documentable<'a>) -> Self {
match self {
Self::Vec(mut vec) => {
@ -461,7 +490,7 @@ impl<'a> Document<'a> {
Str(s) => s.is_empty(),
// assuming `broken` and `unbroken` are equivalent
Break { broken, .. } => broken.is_empty(),
ForceBroken(d) | Nest(_, d) | Group(d) => d.is_empty(),
ForceUnbroken(d) | ForceBroken(d) | Nest(_, d) | Group(d) => d.is_empty(),
Vec(docs) => docs.iter().all(|d| d.is_empty()),
}
}

View File

@ -1017,3 +1017,102 @@ fn format_pattern_bytearray() {
"#
);
}
#[test]
fn format_long_bin_op_1() {
assert_format!(
r#"
test foo() {
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}
"#
);
}
#[test]
fn format_long_bin_op_2() {
assert_format!(
r#"
test foo() {
[0, 0, 0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}
"#
);
}
#[test]
fn format_long_bin_op_3() {
assert_format!(
r#"
test foo() {
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] == [0, 0, 0]
}
"#
);
}
#[test]
fn format_long_bin_op_4() {
assert_format!(
r#"
test foo() {
[foo, bar, baz, (2, 3), (4, 5), (6, 7), (8, 9), biz, buz, fizz, fuzz, alice, bob, carole, i, am, out, of, names] == [0, 0, 0]
}
"#
);
}
#[test]
fn format_long_pattern_1() {
assert_format!(
r#"
test foo() {
when x is {
[True, False, True, False, True, False, True, False, True, False, True, False, True, False, True, False, ..] -> todo
_ -> todo
}
}
"#
);
}
#[test]
fn format_long_pattern_2() {
assert_format!(
r#"
test foo() {
when x is {
[(1, 2, 3), (4, 5, 6), (7, 8, 9), (10, 11, 12), (13, 14, 15), (16, 17, 18), (19, 20, 21), (22, 23, 24)] -> todo
_ -> todo
}
}
"#
);
}
#[test]
fn format_long_standalone_literal() {
assert_format!(
r#"
test foo() {
let left =
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let right =
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,
]
left == right
}
"#
);
}
#[test]
fn format_long_imports() {
assert_format!(
r#"
use aiken/list.{foldr, foldl, is_empty, filter, map, find, any, all, flat_map, partition, push, reduce, reverse, repeat}
"#
);
}

View File

@ -0,0 +1,13 @@
---
source: crates/aiken-lang/src/tests/format.rs
description: "Code:\n\ntest foo() {\n [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n}\n"
---
test foo() {
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
] == [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]
}

View File

@ -0,0 +1,10 @@
---
source: crates/aiken-lang/src/tests/format.rs
description: "Code:\n\ntest foo() {\n [0, 0, 0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n}\n"
---
test foo() {
[0, 0, 0] == [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]
}

View File

@ -0,0 +1,10 @@
---
source: crates/aiken-lang/src/tests/format.rs
description: "Code:\n\ntest foo() {\n [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] == [0, 0, 0]\n}\n"
---
test foo() {
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
] == [0, 0, 0]
}

View File

@ -0,0 +1,10 @@
---
source: crates/aiken-lang/src/tests/format.rs
description: "Code:\n\ntest foo() {\n [foo, bar, baz, (2, 3), (4, 5), (6, 7), (8, 9), biz, buz, fizz, fuzz, alice, bob, carole, i, am, out, of, names] == [0, 0, 0]\n}\n"
---
test foo() {
[
foo, bar, baz, (2, 3), (4, 5), (6, 7), (8, 9), biz, buz, fizz, fuzz, alice,
bob, carole, i, am, out, of, names,
] == [0, 0, 0]
}

View File

@ -0,0 +1,14 @@
---
source: crates/aiken-lang/src/tests/format.rs
description: "Code:\n\ntest foo() {\n when x is {\n [True, False, True, False, True, False, True, False, True, False, True, False, True, False, True, False, ..] -> todo\n _ -> todo\n }\n}\n"
---
test foo() {
when x is {
[
True, False, True, False, True, False, True, False, True, False, True,
False, True, False, True, False,
..
] -> todo
_ -> todo
}
}

View File

@ -0,0 +1,8 @@
---
source: crates/aiken-lang/src/tests/format.rs
description: "Code:\n\nuse aiken/list.{foldr, foldl, is_empty, filter, map, find, any, all, flat_map, partition, push, reduce, reverse, repeat}\n"
---
use aiken/list.{
all, any, filter, find, flat_map, foldl, foldr, is_empty, map, partition, push,
reduce, repeat, reverse,
}

View File

@ -0,0 +1,13 @@
---
source: crates/aiken-lang/src/tests/format.rs
description: "Code:\n\ntest foo() {\n [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n}\n"
---
test foo() {
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
] == [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]
}

View File

@ -0,0 +1,12 @@
---
source: crates/aiken-lang/src/tests/format.rs
description: "Code:\n\ntest foo() {\n [0, 0, 0] == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n}\n"
---
test foo() {
[0,
0,
0] == [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]
}

View File

@ -0,0 +1,10 @@
---
source: crates/aiken-lang/src/tests/format.rs
description: "Code:\n\ntest foo() {\n [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] == [0, 0, 0]\n}\n"
---
test foo() {
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
] == [0, 0, 0]
}

View File

@ -0,0 +1,14 @@
---
source: crates/aiken-lang/src/tests/format.rs
description: "Code:\n\ntest foo() {\n when x is {\n [True, False, True, False, True, False, True, False, True, False, True, False, True, False, True, False, ..] -> todo\n _ -> todo\n }\n}\n"
---
test foo() {
when x is {
[
True, False, True, False, True, False, True, False, True, False, True,
False, True, False, True, False,
..
] -> todo
_ -> todo
}
}

View File

@ -0,0 +1,13 @@
---
source: crates/aiken-lang/src/tests/format.rs
description: "Code:\n\ntest foo() {\n when x is {\n [(1, 2, 3), (4, 5, 6), (7, 8, 9), (10, 11, 12), (13, 14, 15), (16, 17, 18), (19, 20, 21), (22, 23, 24)] -> todo\n _ -> todo\n }\n}\n"
---
test foo() {
when x is {
[
(1, 2, 3), (4, 5, 6), (7, 8, 9), (10, 11, 12), (13, 14, 15), (16, 17, 18),
(19, 20, 21), (22, 23, 24),
] -> todo
_ -> todo
}
}

View File

@ -0,0 +1,14 @@
---
source: crates/aiken-lang/src/tests/format.rs
description: "Code:\n\ntest foo() {\n let left =\n [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n let right =\n [\n 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n 0, 0, 0, 0,\n ]\n left == right\n}\n"
---
test foo() {
let left =
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
let right =
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,
]
left == right
}

View File

@ -1,6 +1,6 @@
use super::{Type, TypeVar};
use crate::{
docvec,
docvec, format,
pretty::{nil, *},
tipo::{Annotation, TypeAliasAnnotation},
};
@ -40,7 +40,7 @@ impl Printer {
.to_doc()
.append(self.print(typ))
.nest(initial_indent as isize)
.to_pretty_string(80)
.to_pretty_string(format::MAX_COLUMNS)
}
// TODO: have this function return a Document that borrows from the Type.

View File

@ -556,7 +556,7 @@ impl DocTypeConstructor {
DocTypeConstructor {
definition: format::Formatter::new()
.docs_record_constructor(constructor)
.to_pretty_string(80),
.to_pretty_string(format::MAX_COLUMNS),
documentation: constructor
.doc
.as_deref()

View File

@ -37,7 +37,7 @@ use aiken_lang::{
},
builtins,
expr::UntypedExpr,
format::{Formatter, DOCS_MAX_COLUMNS},
format::{Formatter, MAX_COLUMNS},
gen_uplc::CodeGenerator,
line_numbers::LineNumbers,
plutus_version::PlutusVersion,
@ -676,7 +676,7 @@ where
name: ast::CONFIG_MODULE.to_string(),
code: Formatter::new()
.definitions(&defs[..])
.to_pretty_string(DOCS_MAX_COLUMNS),
.to_pretty_string(MAX_COLUMNS),
},
&root,
ModuleKind::Config,