roc/crates/compiler/fmt/src/expr.rs
2025-01-04 14:26:20 -06:00

2217 lines
73 KiB
Rust

use crate::annotation::{except_last, is_collection_multiline, Formattable, Newlines, Parens};
use crate::collection::{fmt_collection, Braces};
use crate::def::{fmt_defs, valdef_lift_spaces_before};
use crate::pattern::{
fmt_pattern, pattern_lift_spaces, snakify_camel_ident, starts_with_inline_comment,
};
use crate::spaces::{
count_leading_newlines, fmt_comments_only, fmt_spaces, fmt_spaces_no_blank_lines,
fmt_spaces_with_newline_mode, NewlineAt, SpacesNewlineMode, INDENT,
};
use crate::Buf;
use bumpalo::collections::Vec;
use bumpalo::Bump;
use roc_module::called_via::{self, BinOp, CalledVia, UnaryOp};
use roc_parse::ast::{
AssignedField, Base, Collection, CommentOrNewline, Expr, ExtractSpaces, Pattern, Spaceable,
Spaces, SpacesAfter, SpacesBefore, TryTarget, WhenBranch,
};
use roc_parse::ast::{StrLiteral, StrSegment};
use roc_parse::expr::merge_spaces;
use roc_parse::ident::Accessor;
use roc_parse::keyword;
use roc_region::all::Loc;
use soa::Slice;
impl<'a> Formattable for Expr<'a> {
fn is_multiline(&self) -> bool {
// TODO cache these answers using a Map<Pointer, bool>, so
// we don't have to traverse subexpressions repeatedly
expr_is_multiline(self, false)
}
fn format_with_options(&self, buf: &mut Buf, parens: Parens, newlines: Newlines, indent: u16) {
let me = expr_lift_spaces(parens, buf.text.bump(), self);
if !me.before.is_empty() {
format_spaces(buf, me.before, newlines, indent);
}
format_expr_only(&me.item, buf, parens, newlines, indent);
if !me.after.is_empty() {
format_spaces(buf, me.after, newlines, indent);
}
}
}
fn format_expr_only(
item: &Expr<'_>,
buf: &mut Buf,
parens: Parens,
newlines: Newlines,
indent: u16,
) {
match &item {
Expr::SpaceBefore(_sub_expr, _spaces) | Expr::SpaceAfter(_sub_expr, _spaces) => {
unreachable!()
}
Expr::ParensAround(sub_expr) => {
if parens == Parens::NotNeeded && !sub_expr_requests_parens(sub_expr) {
sub_expr.format_with_options(buf, Parens::NotNeeded, newlines, indent);
} else {
fmt_parens(sub_expr, buf, indent);
}
}
Expr::Str(literal) => {
fmt_str_literal(buf, *literal, indent);
}
Expr::Var { module_name, ident } => {
buf.indent(indent);
if !module_name.is_empty() {
buf.push_str(module_name);
buf.push('.');
}
if buf.flags().snakify {
snakify_camel_ident(buf, ident);
} else {
buf.push_str(ident);
}
}
Expr::Underscore(name) => {
buf.indent(indent);
buf.push('_');
buf.push_str(name);
}
Expr::Crash => {
buf.indent(indent);
buf.push_str("crash");
}
Expr::Try => {
buf.indent(indent);
buf.push_str("try");
}
Expr::Apply(loc_expr, loc_args, called_via::CalledVia::ParensAndCommas) => {
fmt_apply(loc_expr, loc_args, indent, buf, true);
}
Expr::Apply(loc_expr, loc_args, _) => {
let apply_needs_parens = parens == Parens::InApply || parens == Parens::InApplyLastArg;
if buf.flags().parens_and_commas || !apply_needs_parens || loc_args.is_empty() {
fmt_apply(loc_expr, loc_args, indent, buf, false);
} else {
fmt_parens(item, buf, indent);
}
}
&Expr::Num(string) => {
buf.indent(indent);
buf.push_str(string);
}
&Expr::Float(string) => {
buf.indent(indent);
buf.push_str(string);
}
Expr::Tag(string) | Expr::OpaqueRef(string) => {
buf.indent(indent);
buf.push_str(string)
}
Expr::SingleQuote(string) => {
buf.indent(indent);
format_sq_literal(buf, string);
}
Expr::NonBase10Int {
base,
string,
is_negative,
} => {
buf.indent(indent);
if *is_negative {
buf.push('-');
}
match base {
Base::Hex => buf.push_str("0x"),
Base::Octal => buf.push_str("0o"),
Base::Binary => buf.push_str("0b"),
Base::Decimal => { /* nothing */ }
}
buf.push_str(string);
}
Expr::Record(fields) => {
fmt_record_like(
buf,
None,
prepare_expr_field_collection(buf.text.bump(), *fields),
indent,
assigned_field_to_spaces,
);
}
Expr::RecordUpdate { update, fields } => {
fmt_record_like(
buf,
Some(RecordPrefix::Update(update)),
prepare_expr_field_collection(buf.text.bump(), *fields),
indent,
assigned_field_to_spaces,
);
}
Expr::RecordBuilder { mapper, fields } => {
fmt_record_like(
buf,
Some(RecordPrefix::Mapper(mapper)),
prepare_expr_field_collection(buf.text.bump(), *fields),
indent,
assigned_field_to_spaces,
);
}
Expr::Closure(loc_patterns, loc_ret) => {
fmt_closure(buf, loc_patterns, loc_ret, indent);
}
Expr::Defs(defs, ret) => {
let defs_needs_parens = parens == Parens::InOperator || parens == Parens::InApply;
if defs_needs_parens {
fmt_parens(item, buf, indent)
} else {
// It should theoretically be impossible to *parse* an empty defs list.
// (Canonicalization can remove defs later, but that hasn't happened yet!)
debug_assert!(!defs.is_empty());
fmt_defs(buf, defs, indent);
match &ret.value {
Expr::SpaceBefore(sub_expr, spaces) => {
buf.spaces(1);
fmt_spaces(buf, spaces.iter(), indent);
buf.indent(indent);
sub_expr.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent);
}
_ => {
buf.ensure_ends_with_newline();
buf.indent(indent);
// Even if there were no defs, which theoretically should never happen,
// still print the return value.
ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent);
}
}
}
}
Expr::Dbg => {
buf.indent(indent);
buf.push_str("dbg");
}
Expr::DbgStmt {
first: condition,
extra_args,
continuation,
} => {
fmt_dbg_stmt(buf, condition, extra_args, continuation, parens, indent);
}
Expr::LowLevelDbg(_, _, _) => {
unreachable!("LowLevelDbg should only exist after desugaring, not during formatting")
}
Expr::LowLevelTry(..) => {
unreachable!("LowLevelTry should only exist after desugaring, not during formatting")
}
Expr::Return(return_value, after_return) => {
fmt_return(buf, return_value, after_return, parens, newlines, indent);
}
Expr::If {
if_thens: branches,
final_else,
indented_else,
} => {
fmt_if(
buf,
branches,
final_else,
item.is_multiline(),
*indented_else,
indent,
);
}
Expr::When(loc_condition, branches) => fmt_when(buf, loc_condition, branches, indent),
Expr::Tuple(items) => fmt_expr_collection(buf, indent, Braces::Round, *items, Newlines::No),
Expr::List(items) => fmt_expr_collection(buf, indent, Braces::Square, *items, Newlines::No),
Expr::BinOps(lefts, right) => fmt_binops(buf, lefts, right, indent),
Expr::UnaryOp(sub_expr, unary_op) => {
buf.indent(indent);
match &unary_op.value {
called_via::UnaryOp::Negate => {
buf.push('-');
}
called_via::UnaryOp::Not => {
buf.push('!');
}
}
let lifted = expr_lift_spaces(Parens::InOperator, buf.text.bump(), &sub_expr.value);
let before_all_newlines = lifted.before.iter().all(|s| s.is_newline());
let needs_newline = !before_all_newlines
|| match &lifted.item {
Expr::Str(text) => is_str_multiline(text),
_ => false,
};
let needs_parens = (needs_newline
&& matches!(unary_op.value, called_via::UnaryOp::Negate))
|| matches!(
lifted.item,
Expr::Apply(..) | Expr::BinOps(..) | Expr::Defs(..)
)
|| (matches!(unary_op.value, called_via::UnaryOp::Negate)
&& requires_space_after_unary(&lifted.item))
|| ends_with_closure(&lifted.item);
if needs_parens {
// Unary negation can't be followed by whitespace (which is what a newline is) - so
// we need to wrap the negated value in parens.
fmt_parens(&sub_expr.value, buf, indent);
} else {
if matches!(unary_op.value, called_via::UnaryOp::Not)
&& requires_space_after_unary(&lifted.item)
{
// If the subexpression is an accessor function, we need to add a space,
// since `!.foo` doesn't parse. Yes, this wouldn't be valid anyway,
// but the formatter needs to be able to format invalid code.
buf.spaces(1);
}
let inner_indent = if needs_newline {
indent + INDENT
} else {
indent
};
let inner_parens = if needs_parens {
Parens::NotNeeded
} else {
Parens::InApplyLastArg
};
if !before_all_newlines {
format_spaces(buf, lifted.before, newlines, inner_indent);
}
lifted
.item
.format_with_options(buf, inner_parens, newlines, inner_indent);
format_spaces(buf, lifted.after, newlines, inner_indent);
}
}
Expr::AccessorFunction(key) => {
buf.indent(indent);
buf.push('.');
match key {
Accessor::RecordField(key) => {
if buf.flags().snakify {
snakify_camel_ident(buf, key);
} else {
buf.push_str(key);
}
}
Accessor::TupleIndex(key) => buf.push_str(key),
}
}
Expr::RecordUpdater(key) => {
buf.indent(indent);
buf.push('&');
if buf.flags().snakify {
snakify_camel_ident(buf, key);
} else {
buf.push_str(key);
}
}
Expr::RecordAccess(expr, key) => {
expr.format_with_options(buf, Parens::InApply, Newlines::Yes, indent);
buf.push('.');
if buf.flags().snakify {
snakify_camel_ident(buf, key);
} else {
buf.push_str(key);
}
}
Expr::TupleAccess(expr, key) => {
expr.format_with_options(buf, Parens::InApply, Newlines::Yes, indent);
buf.push('.');
buf.push_str(key);
}
Expr::TrySuffix { expr, target } => {
expr.format_with_options(buf, Parens::InApply, Newlines::Yes, indent);
match target {
TryTarget::Task => buf.push('!'),
TryTarget::Result => buf.push('?'),
}
}
Expr::MalformedIdent(str, _) => {
buf.indent(indent);
if buf.flags().snakify {
snakify_camel_ident(buf, str);
} else {
buf.push_str(str);
}
}
Expr::MalformedSuffixed(loc_expr) => {
buf.indent(indent);
loc_expr.format_with_options(buf, parens, newlines, indent);
}
Expr::PrecedenceConflict { .. } => {}
Expr::EmptyRecordBuilder { .. } => {}
Expr::SingleFieldRecordBuilder { .. } => {}
Expr::OptionalFieldInRecordBuilder(_, _) => {}
}
}
fn prepare_expr_field_collection<'a>(
arena: &'a Bump,
items: Collection<'a, Loc<AssignedField<'a, Expr<'a>>>>,
) -> Collection<'a, Loc<AssignedField<'a, Expr<'a>>>> {
let mut new_items: Vec<'_, Loc<AssignedField<'a, Expr<'a>>>> =
Vec::with_capacity_in(items.len(), arena);
let mut last_after: &[CommentOrNewline<'_>] = &[];
for (i, item) in items.items.iter().enumerate() {
let mut lifted = assigned_field_lift_spaces(arena, item.value);
if i == items.items.len() - 1 {
last_after = lifted.after;
lifted.after = &[];
}
new_items.push(Loc::at(item.region, lower_assigned_field(arena, lifted)));
}
let final_comments = merge_spaces_conservative(arena, last_after, items.final_comments());
Collection::with_items_and_comments(arena, new_items.into_bump_slice(), final_comments)
}
fn lower_assigned_field<'a>(
arena: &'a Bump,
lifted: Spaces<'a, AssignedField<'a, Expr<'a>>>,
) -> AssignedField<'a, Expr<'a>> {
if lifted.before.is_empty() && lifted.after.is_empty() {
return lifted.item;
}
if lifted.before.is_empty() {
return AssignedField::SpaceAfter(arena.alloc(lifted.item), lifted.after);
}
if lifted.after.is_empty() {
return AssignedField::SpaceBefore(arena.alloc(lifted.item), lifted.before);
}
AssignedField::SpaceBefore(
arena.alloc(AssignedField::SpaceAfter(
arena.alloc(lifted.item),
lifted.after,
)),
lifted.before,
)
}
fn assigned_field_lift_spaces<'a, 'b: 'a>(
arena: &'a Bump,
value: AssignedField<'b, Expr<'b>>,
) -> Spaces<'a, AssignedField<'a, Expr<'a>>> {
match value {
AssignedField::RequiredValue(name, sp, value) => {
let new_value = expr_lift_spaces_after(Parens::NotNeeded, arena, &value.value);
Spaces {
before: &[],
item: AssignedField::RequiredValue(
name,
sp,
arena.alloc(Loc::at(value.region, new_value.item)),
),
after: new_value.after,
}
}
AssignedField::OptionalValue(name, sp, value) => {
let new_value = expr_lift_spaces_after(Parens::NotNeeded, arena, &value.value);
Spaces {
before: &[],
item: AssignedField::OptionalValue(
name,
sp,
arena.alloc(Loc::at(value.region, new_value.item)),
),
after: new_value.after,
}
}
AssignedField::IgnoredValue(name, sp, value) => {
let new_value = expr_lift_spaces_after(Parens::NotNeeded, arena, &value.value);
Spaces {
before: &[],
item: AssignedField::IgnoredValue(
name,
sp,
arena.alloc(Loc::at(value.region, new_value.item)),
),
after: new_value.after,
}
}
AssignedField::LabelOnly(name) => Spaces {
before: &[],
item: AssignedField::LabelOnly(name),
after: &[],
},
AssignedField::SpaceBefore(inner, sp) => {
let mut inner = assigned_field_lift_spaces(arena, *inner);
inner.before = merge_spaces_conservative(arena, sp, inner.before);
inner
}
AssignedField::SpaceAfter(inner, sp) => {
let mut inner = assigned_field_lift_spaces(arena, *inner);
inner.after = merge_spaces_conservative(arena, inner.after, sp);
inner
}
}
}
pub fn expr_is_multiline(me: &Expr<'_>, comments_only: bool) -> bool {
match me {
// Return whether these spaces contain any Newlines
Expr::SpaceBefore(sub_expr, spaces) | Expr::SpaceAfter(sub_expr, spaces) => {
debug_assert!(!spaces.is_empty());
if comments_only {
spaces.iter().any(|s| s.is_comment()) || expr_is_multiline(sub_expr, comments_only)
} else {
true
}
}
Expr::MalformedSuffixed(loc_expr) => expr_is_multiline(&loc_expr.value, comments_only),
// These expressions never have newlines
Expr::Float(..)
| Expr::Num(..)
| Expr::NonBase10Int { .. }
| Expr::SingleQuote(_)
| Expr::AccessorFunction(_)
| Expr::RecordUpdater(_)
| Expr::Var { .. }
| Expr::Underscore { .. }
| Expr::MalformedIdent(_, _)
| Expr::Tag(_)
| Expr::OpaqueRef(_)
| Expr::Crash
| Expr::Dbg
| Expr::Try => false,
Expr::LowLevelTry(_, _) => {
unreachable!("LowLevelTry should only exist after desugaring, not during formatting")
}
Expr::RecordAccess(inner, _)
| Expr::TupleAccess(inner, _)
| Expr::TrySuffix { expr: inner, .. } => expr_is_multiline(inner, comments_only),
// These expressions always have newlines
Expr::Defs(_, _) | Expr::When(_, _) => true,
Expr::List(items) => is_collection_multiline(items),
Expr::Str(literal) => is_str_multiline(literal),
Expr::Apply(loc_expr, args, _) => {
expr_is_multiline(&loc_expr.value, comments_only)
|| args
.iter()
.any(|loc_arg| expr_is_multiline(&loc_arg.value, comments_only))
}
Expr::DbgStmt { .. } => true,
Expr::LowLevelDbg(_, _, _) => {
unreachable!("LowLevelDbg should only exist after desugaring, not during formatting")
}
Expr::Return(_return_value, _after_return) => true,
Expr::If {
if_thens: branches,
final_else,
..
} => {
expr_is_multiline(&final_else.value, comments_only)
|| branches.iter().any(|(c, t)| {
expr_is_multiline(&c.value, comments_only)
|| expr_is_multiline(&t.value, comments_only)
})
}
Expr::BinOps(lefts, loc_right) => {
lefts
.iter()
.any(|(loc_expr, _)| expr_is_multiline(&loc_expr.value, comments_only))
|| expr_is_multiline(&loc_right.value, comments_only)
}
Expr::UnaryOp(loc_subexpr, _)
| Expr::PrecedenceConflict(roc_parse::ast::PrecedenceConflict {
expr: loc_subexpr, ..
})
| Expr::EmptyRecordBuilder(loc_subexpr)
| Expr::SingleFieldRecordBuilder(loc_subexpr)
| Expr::OptionalFieldInRecordBuilder(_, loc_subexpr) => {
expr_is_multiline(&loc_subexpr.value, comments_only)
}
Expr::ParensAround(subexpr) => expr_is_multiline(subexpr, comments_only),
Expr::Closure(loc_patterns, loc_body) => {
// check the body first because it's more likely to be multiline
expr_is_multiline(&loc_body.value, comments_only)
|| loc_patterns
.iter()
.any(|loc_pattern| loc_pattern.value.is_multiline())
}
Expr::Record(fields) => is_collection_multiline(fields),
Expr::Tuple(fields) => is_collection_multiline(fields),
Expr::RecordUpdate { fields, .. } => is_collection_multiline(fields),
Expr::RecordBuilder { fields, .. } => is_collection_multiline(fields),
}
}
fn lower<'a, 'b: 'a>(arena: &'b Bump, lifted: Spaces<'b, Expr<'b>>) -> Expr<'b> {
if lifted.before.is_empty() && lifted.after.is_empty() {
return lifted.item;
}
if lifted.before.is_empty() {
return Expr::SpaceAfter(arena.alloc(lifted.item), lifted.after);
}
if lifted.after.is_empty() {
return Expr::SpaceBefore(arena.alloc(lifted.item), lifted.before);
}
Expr::SpaceBefore(
arena.alloc(Expr::SpaceAfter(arena.alloc(lifted.item), lifted.after)),
lifted.before,
)
}
fn fmt_expr_collection(
buf: &mut Buf<'_>,
indent: u16,
braces: Braces,
items: Collection<'_, &Loc<Expr<'_>>>,
newlines: Newlines,
) {
let arena = buf.text.bump();
let mut new_items: Vec<'_, &Expr<'_>> = Vec::with_capacity_in(items.len(), arena);
let mut last_after: &[CommentOrNewline<'_>] = &[];
for item in items.items {
let mut lifted = expr_lift_spaces(Parens::InCollection, arena, &item.value);
lifted.before = merge_spaces_conservative(arena, last_after, lifted.before);
last_after = lifted.after;
lifted.after = &[];
new_items.push(arena.alloc(lower(arena, lifted)));
}
let final_comments = merge_spaces_conservative(arena, last_after, items.final_comments());
let new_items =
Collection::with_items_and_comments(arena, new_items.into_bump_slice(), final_comments);
fmt_collection(buf, indent, braces, new_items, newlines)
}
fn requires_space_after_unary(item: &Expr<'_>) -> bool {
match item {
Expr::AccessorFunction(_) | Expr::UnaryOp(..) => true,
Expr::Num(text) | Expr::Float(text) => text.starts_with('-'),
Expr::NonBase10Int {
string: _,
base: _,
is_negative,
} => *is_negative,
Expr::RecordUpdater(..) => true,
Expr::RecordAccess(inner, _field) | Expr::TupleAccess(inner, _field) => {
requires_space_after_unary(inner)
}
Expr::Apply(inner, _, _) => requires_space_after_unary(&inner.value),
Expr::TrySuffix { target: _, expr } => requires_space_after_unary(expr),
Expr::SpaceAfter(inner, _) | Expr::SpaceBefore(inner, _) => {
requires_space_after_unary(inner)
}
_ => false,
}
}
fn fmt_apply(
loc_expr: &Loc<Expr<'_>>,
loc_args: &[&Loc<Expr<'_>>],
indent: u16,
buf: &mut Buf<'_>,
expr_used_commas_and_parens: bool,
) {
// should_reflow_outdentable, aka should we transform this:
//
// ```
// foo bar
// [
// 1,
// 2,
// ]
// ```
//
// Into this:
//
// ```
// foo bar [
// 1,
// 2,
// ]
// ```
let use_commas_and_parens = expr_used_commas_and_parens || buf.flags().parens_and_commas;
let should_reflow_outdentable = loc_expr.extract_spaces().after.is_empty()
&& except_last(loc_args).all(|a| !a.is_multiline())
&& loc_args
.last()
.map(|a| {
a.extract_spaces().item.is_multiline()
&& is_outdentable_collection(&a.value.extract_spaces().item)
&& (a.extract_spaces().before == [CommentOrNewline::Newline]
|| a.extract_spaces().before.is_empty())
})
.unwrap_or_default();
let needs_indent = !should_reflow_outdentable
&& (!loc_expr.extract_spaces().after.is_empty()
|| except_last(loc_args).any(|a| a.is_multiline())
|| loc_expr.is_multiline()
|| loc_args
.last()
.map(|a| {
a.is_multiline()
&& (!a.extract_spaces().before.is_empty() || !is_outdentable(&a.value))
})
.unwrap_or_default());
let arg_indent = if needs_indent {
indent + INDENT
} else {
indent
};
let expr = expr_lift_spaces(Parens::InApply, buf.text.bump(), &loc_expr.value);
if !expr.before.is_empty() {
format_spaces(buf, expr.before, Newlines::Yes, indent);
}
expr.item
.format_with_options(buf, Parens::InApply, Newlines::Yes, indent);
if use_commas_and_parens {
buf.push('(');
}
let mut last_after = expr.after;
for (i, loc_arg) in loc_args.iter().enumerate() {
let is_last_arg = i == loc_args.len() - 1;
let is_first_arg = i == 0;
let arg = expr_lift_spaces(
if use_commas_and_parens {
Parens::NotNeeded
} else if is_last_arg {
Parens::InApplyLastArg
} else {
Parens::InApply
},
buf.text.bump(),
&loc_arg.value,
);
if !should_reflow_outdentable {
if !last_after.is_empty() {
format_spaces(buf, last_after, Newlines::Yes, arg_indent);
}
if !arg.before.is_empty() {
format_spaces(buf, arg.before, Newlines::Yes, arg_indent);
}
}
last_after = arg.after;
if needs_indent {
buf.ensure_ends_with_newline();
} else if !(is_first_arg && use_commas_and_parens) {
buf.spaces(1);
}
if matches!(arg.item, Expr::Var { module_name, ident } if module_name.is_empty() && ident == "implements")
{
fmt_parens(&arg.item, buf, arg_indent);
} else {
format_expr_only(
&arg.item,
buf,
if use_commas_and_parens {
Parens::NotNeeded
} else {
Parens::InApply
},
Newlines::Yes,
arg_indent,
);
}
if use_commas_and_parens && (!is_last_arg || needs_indent) {
buf.push(',');
}
}
if !last_after.is_empty() {
format_spaces(buf, last_after, Newlines::Yes, arg_indent);
}
if use_commas_and_parens {
if needs_indent {
buf.ensure_ends_with_newline();
buf.indent(indent);
}
buf.push(')');
}
}
fn is_outdentable_collection(expr: &Expr<'_>) -> bool {
match expr {
Expr::Tuple(items) => is_collection_multiline(items),
Expr::List(items) => is_collection_multiline(items),
Expr::Record(items) => is_collection_multiline(items),
_ => false,
}
}
fn fmt_parens(sub_expr: &Expr<'_>, buf: &mut Buf<'_>, indent: u16) {
let should_add_newlines = match sub_expr {
Expr::Closure(..)
| Expr::SpaceBefore(..)
| Expr::SpaceAfter(Expr::Closure(..), ..)
| Expr::DbgStmt { .. } => false,
_ => sub_expr.is_multiline(),
};
buf.indent(indent);
buf.push('(');
if should_add_newlines {
buf.newline();
}
let next_indent = if starts_with_newline(sub_expr) || should_add_newlines {
match sub_expr {
Expr::Closure(..) | Expr::SpaceAfter(Expr::Closure(..), ..) => indent,
_ => indent + INDENT,
}
} else {
indent
};
sub_expr.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, next_indent);
if !matches!(sub_expr, Expr::SpaceAfter(..)) && should_add_newlines {
buf.ensure_ends_with_newline();
}
buf.indent(indent);
buf.push(')');
}
pub fn is_str_multiline(literal: &StrLiteral) -> bool {
use roc_parse::ast::StrLiteral::*;
match literal {
PlainLine(string) => {
// When a PlainLine contains '\n' or '"', format as a block string
string.contains('"') || string.contains('\n')
}
Line(_) => {
// If this had any newlines, it'd have parsed as Block.
false
}
Block(_) => {
// Block strings are always formatted on multiple lines,
// even if the string is only a single line.
true
}
}
}
fn needs_unicode_escape(ch: char) -> bool {
matches!(ch, '\u{0000}'..='\u{001f}' | '\u{007f}'..='\u{009f}')
}
pub(crate) fn format_sq_literal(buf: &mut Buf, s: &str) {
buf.push('\'');
for c in s.chars() {
if c == '"' {
buf.push_char_literal('"')
} else {
match c {
'"' => buf.push_str("\""),
'\'' => buf.push_str("\\\'"),
'\t' => buf.push_str("\\t"),
'\r' => buf.push_str("\\r"),
'\n' => buf.push_str("\\n"),
'\\' => buf.push_str("\\\\"),
_ => {
if needs_unicode_escape(c) {
buf.push_str(&format!("\\u({:x})", c as u32))
} else {
buf.push_char_literal(c)
}
}
}
}
}
buf.push('\'');
}
fn is_outdentable(expr: &Expr) -> bool {
matches!(
expr.extract_spaces().item,
Expr::Tuple(_) | Expr::List(_) | Expr::Record(_) | Expr::Closure(..)
)
}
fn starts_with_newline(expr: &Expr) -> bool {
use roc_parse::ast::Expr::*;
match expr {
SpaceBefore(_, comment_or_newline) => {
matches!(comment_or_newline.first(), Some(CommentOrNewline::Newline))
}
DbgStmt { .. } => true,
_ => false,
}
}
fn fmt_str_body(body: &str, buf: &mut Buf) {
for c in body.chars() {
match c {
// Format blank characters as unicode escapes
'\u{200a}' => buf.push_str("\\u(200a)"),
'\u{200b}' => buf.push_str("\\u(200b)"),
'\u{200c}' => buf.push_str("\\u(200c)"),
'\u{feff}' => buf.push_str("\\u(feff)"),
// Don't change anything else in the string
' ' => buf.push_str_allow_spaces(" "),
'\n' => buf.push_str_allow_spaces("\n"),
_ => buf.push(c),
}
}
}
fn format_str_segment(seg: &StrSegment, buf: &mut Buf) {
use StrSegment::*;
match seg {
Plaintext(string) => {
// Lines in block strings will end with Plaintext ending in "\n" to indicate
// a line break in the input string
match string.strip_suffix('\n') {
Some(string_without_newline) => {
fmt_str_body(string_without_newline, buf);
buf.newline();
}
None => fmt_str_body(string, buf),
}
}
Unicode(loc_str) => {
buf.push_str("\\u(");
buf.push_str(loc_str.value); // e.g. "00A0" in "\u(00A0)"
buf.push(')');
}
EscapedChar(escaped) => {
buf.push('\\');
buf.push(escaped.to_parsed_char());
}
Interpolated(loc_expr) => {
buf.push_str("$(");
// e.g. (name) in "Hi, $(name)!"
let min_indent = buf.cur_line_indent() + INDENT;
loc_expr.value.format_with_options(
buf,
Parens::NotNeeded, // We already printed parens!
Newlines::No, // Interpolations can never have newlines
min_indent,
);
buf.indent(min_indent);
buf.push(')');
}
}
}
fn push_op(buf: &mut Buf, op: BinOp) {
match op {
called_via::BinOp::Caret => buf.push('^'),
called_via::BinOp::Star => buf.push('*'),
called_via::BinOp::Slash => buf.push('/'),
called_via::BinOp::DoubleSlash => buf.push_str("//"),
called_via::BinOp::Percent => buf.push('%'),
called_via::BinOp::Plus => buf.push('+'),
called_via::BinOp::Minus => buf.push('-'),
called_via::BinOp::Equals => buf.push_str("=="),
called_via::BinOp::NotEquals => buf.push_str("!="),
called_via::BinOp::LessThan => buf.push('<'),
called_via::BinOp::GreaterThan => buf.push('>'),
called_via::BinOp::LessThanOrEq => buf.push_str("<="),
called_via::BinOp::GreaterThanOrEq => buf.push_str(">="),
called_via::BinOp::And => buf.push_str("&&"),
called_via::BinOp::Or => buf.push_str("||"),
called_via::BinOp::Pizza => buf.push_str("|>"),
called_via::BinOp::DoubleQuestion => buf.push_str("??"),
}
}
pub fn fmt_str_literal(buf: &mut Buf, literal: StrLiteral, indent: u16) {
use roc_parse::ast::StrLiteral::*;
match literal {
PlainLine(string) => {
// When a PlainLine contains '\n' or '"', format as a block string
if string.contains('"') || string.contains('\n') {
buf.ensure_ends_with_newline();
buf.indent(indent);
buf.push_str("\"\"\"");
buf.push_newline_literal();
for line in string.split('\n') {
buf.indent(indent);
fmt_str_body(line, buf);
buf.push_newline_literal();
}
buf.indent(indent);
buf.push_str("\"\"\"");
} else {
buf.indent(indent);
buf.push('"');
fmt_str_body(string, buf);
buf.push('"');
};
}
Line(segments) => {
buf.indent(indent);
buf.push('"');
for seg in segments.iter() {
format_str_segment(seg, buf)
}
buf.push('"');
}
Block(lines) => {
// Block strings will always be formatted with """ on new lines
buf.ensure_ends_with_newline();
buf.indent(indent);
buf.push_str("\"\"\"");
buf.push_newline_literal();
for segments in lines.iter() {
for seg in segments.iter() {
// only add indent if the line isn't empty
if *seg != StrSegment::Plaintext("\n") {
buf.indent(indent);
format_str_segment(seg, buf);
} else {
buf.push_newline_literal();
}
}
buf.push_newline_literal();
}
buf.indent(indent);
buf.push_str("\"\"\"");
}
}
}
pub fn expr_lift_and_lower<'a, 'b: 'a>(
_parens: Parens,
arena: &'a Bump,
expr: &Expr<'b>,
) -> Expr<'a> {
lower(arena, expr_lift_spaces(Parens::NotNeeded, arena, expr))
}
pub fn expr_lift_spaces<'a, 'b: 'a>(
parens: Parens,
arena: &'a Bump,
expr: &Expr<'b>,
) -> Spaces<'a, Expr<'a>> {
match expr {
Expr::Apply(func, args, CalledVia::ParensAndCommas) => {
let lifted = expr_lift_spaces_before(Parens::NotNeeded, arena, &func.value);
Spaces {
before: lifted.before,
item: Expr::Apply(
arena.alloc(Loc::at(func.region, lifted.item)),
args,
CalledVia::ParensAndCommas,
),
after: arena.alloc([]),
}
}
Expr::Apply(func, args, called_via) => {
if args.is_empty() {
return expr_lift_spaces(Parens::NotNeeded, arena, &func.value);
}
let func_lifted = expr_lift_spaces(Parens::InApply, arena, &func.value);
let args = arena.alloc_slice_copy(args);
let mut res = if let Some(last) = args.last_mut() {
let last_lifted = expr_lift_spaces(Parens::InApplyLastArg, arena, &last.value);
if last_lifted.before.is_empty() {
*last = arena.alloc(Loc::at(last.region, last_lifted.item));
} else {
*last = arena.alloc(Loc::at(
last.region,
Expr::SpaceBefore(arena.alloc(last_lifted.item), last_lifted.before),
));
}
let func_fixed = if func_lifted.after.is_empty() {
func_lifted.item
} else {
Expr::SpaceAfter(arena.alloc(func_lifted.item), func_lifted.after)
};
Spaces {
before: func_lifted.before,
item: Expr::Apply(
arena.alloc(Loc::at(func.region, func_fixed)),
args,
*called_via,
),
after: last_lifted.after,
}
} else {
Spaces {
before: func_lifted.before,
item: Expr::Apply(
arena.alloc(Loc::at(func.region, func_lifted.item)),
args,
*called_via,
),
after: func_lifted.after,
}
};
if parens == Parens::InApply || parens == Parens::InApplyLastArg {
res = Spaces {
before: &[],
item: Expr::ParensAround(arena.alloc(lower(arena, res))),
after: &[],
};
}
res
}
Expr::Defs(defs, final_expr) => {
let mut defs = (*defs).clone();
let mut before: &[CommentOrNewline] = &[];
if let Some(spaces_range) = defs.space_before.first_mut() {
if !spaces_range.is_empty() {
before = &defs.spaces[spaces_range.indices()];
*spaces_range = Slice::empty();
}
}
let inner_before = match defs.tags[0].split() {
Ok(_td) => &[],
Err(vd) => {
let lifted = valdef_lift_spaces_before(arena, defs.value_defs[vd.index()]);
defs.value_defs[vd.index()] = lifted.item;
lifted.before
}
};
let final_expr_lifted = expr_lift_spaces(Parens::NotNeeded, arena, &final_expr.value);
let new_final_expr = if final_expr_lifted.before.is_empty() {
final_expr_lifted.item
} else {
Expr::SpaceBefore(
arena.alloc(final_expr_lifted.item),
final_expr_lifted.before,
)
};
let before = merge_spaces(arena, arena.alloc_slice_copy(before), inner_before);
let mut item = Expr::Defs(
arena.alloc(defs),
arena.alloc(Loc::at(final_expr.region, new_final_expr)),
);
if parens == Parens::InCollection {
item = Expr::ParensAround(arena.alloc(item));
}
Spaces {
before,
item,
after: final_expr_lifted.after,
}
}
Expr::Closure(pats, body) => {
if parens == Parens::InApply {
return Spaces {
before: &[],
item: Expr::ParensAround(arena.alloc(*expr)),
after: &[],
};
}
let body_lifted = expr_lift_spaces_after(Parens::NotNeeded, arena, &body.value);
Spaces {
before: &[],
item: Expr::Closure(pats, arena.alloc(Loc::at(body.region, body_lifted.item))),
after: body_lifted.after,
}
}
Expr::If {
if_thens,
final_else,
indented_else,
} => {
if parens == Parens::InApply || parens == Parens::InApplyLastArg {
Spaces {
before: &[],
item: Expr::ParensAround(arena.alloc(*expr)),
after: &[],
}
} else {
let else_lifted =
expr_lift_spaces_after(Parens::NotNeeded, arena, &final_else.value);
Spaces {
before: &[],
item: Expr::If {
if_thens,
final_else: arena.alloc(Loc::at(final_else.region, else_lifted.item)),
indented_else: *indented_else,
},
after: else_lifted.after,
}
}
}
Expr::When(cond, branches) => {
if parens == Parens::InApply || parens == Parens::InApplyLastArg {
Spaces {
before: &[],
item: Expr::ParensAround(arena.alloc(*expr)),
after: &[],
}
} else {
let new_branches = arena.alloc_slice_copy(branches);
if let Some(last) = new_branches.last_mut() {
let last_value_lifted =
expr_lift_spaces_after(Parens::NotNeeded, arena, &last.value.value);
*last = arena.alloc(WhenBranch {
patterns: last.patterns,
value: Loc::at(last.value.region, last_value_lifted.item),
guard: last.guard,
});
Spaces {
before: &[],
item: Expr::When(
arena.alloc(Loc::at(cond.region, cond.value)),
new_branches,
),
after: last_value_lifted.after,
}
} else {
Spaces {
before: &[],
item: *expr,
after: &[],
}
}
}
}
Expr::Return(val, opt_after) => {
if parens == Parens::InApply || parens == Parens::InApplyLastArg {
Spaces {
before: &[],
item: Expr::ParensAround(arena.alloc(*expr)),
after: &[],
}
} else if let Some(after) = opt_after {
let after_lifted = expr_lift_spaces_after(Parens::NotNeeded, arena, &after.value);
Spaces {
before: &[],
item: Expr::Return(
val,
Some(arena.alloc(Loc::at(after.region, after_lifted.item))),
),
after: after_lifted.after,
}
} else {
let val_lifted = expr_lift_spaces_after(Parens::NotNeeded, arena, &val.value);
Spaces {
before: &[],
item: Expr::Return(arena.alloc(Loc::at(val.region, val_lifted.item)), None),
after: val_lifted.after,
}
}
}
Expr::SpaceBefore(expr, spaces) => {
let mut inner = expr_lift_spaces(parens, arena, expr);
inner.before = merge_spaces_conservative(arena, spaces, inner.before);
inner
}
Expr::SpaceAfter(expr, spaces) => {
let mut inner = expr_lift_spaces(parens, arena, expr);
inner.after = merge_spaces_conservative(arena, inner.after, spaces);
inner
}
Expr::ParensAround(inner) => {
if (parens == Parens::NotNeeded || parens == Parens::InCollection)
&& !sub_expr_requests_parens(inner)
{
expr_lift_spaces(Parens::NotNeeded, arena, inner)
} else {
Spaces {
before: &[],
item: *expr,
after: &[],
}
}
}
Expr::Float(_)
| Expr::Num(_)
| Expr::NonBase10Int { .. }
| Expr::Str(_)
| Expr::SingleQuote(_)
| Expr::AccessorFunction(_)
| Expr::RecordUpdater(_)
| Expr::RecordAccess(_, _)
| Expr::TupleAccess(_, _)
| Expr::Var { .. }
| Expr::Underscore(_)
| Expr::Crash
| Expr::Tag(_)
| Expr::OpaqueRef(_)
| Expr::Dbg
| Expr::Try
| Expr::List(_)
| Expr::Record(_)
| Expr::Tuple(_)
| Expr::RecordBuilder { .. }
| Expr::RecordUpdate { .. } => Spaces {
before: &[],
item: *expr,
after: &[],
},
Expr::TrySuffix { target, expr } => {
let expr_lifted = expr_lift_spaces_after(Parens::InApply, arena, expr);
Spaces {
before: &[],
item: Expr::TrySuffix {
target: *target,
expr: arena.alloc(expr_lifted.item),
},
after: expr_lifted.after,
}
}
Expr::DbgStmt {
first,
extra_args,
continuation,
} => {
let continuation_lifted =
expr_lift_spaces_after(Parens::NotNeeded, arena, &continuation.value);
Spaces {
before: &[],
item: Expr::DbgStmt {
first,
extra_args,
continuation: arena
.alloc(Loc::at(continuation.region, continuation_lifted.item)),
},
after: continuation_lifted.after,
}
}
Expr::LowLevelDbg(_, _, _) => {
unreachable!("LowLevelDbg should only exist after desugaring, not during formatting")
}
Expr::LowLevelTry(..) => {
unreachable!("LowLevelTry should only exist after desugaring, not during formatting")
}
Expr::BinOps(lefts, right) => {
let lefts = arena.alloc_slice_copy(lefts);
let before = if let Some(first) = lefts.first_mut() {
let lifted = expr_lift_spaces_before(Parens::InOperator, arena, &first.0.value);
*first = (Loc::at(first.0.region, lifted.item), first.1);
lifted.before
} else {
&[]
};
let right_lifted = expr_lift_spaces_after(Parens::InOperator, arena, &right.value);
Spaces {
before,
item: Expr::BinOps(lefts, arena.alloc(Loc::at(right.region, right_lifted.item))),
after: right_lifted.after,
}
}
Expr::UnaryOp(inner, op) => {
if parens == Parens::InApply && matches!(inner.without_spaces(), Expr::Closure(..)) {
return Spaces {
before: &[],
item: Expr::ParensAround(arena.alloc(*expr)),
after: &[],
};
}
let inner_lifted = expr_lift_spaces_after(Parens::InOperator, arena, &inner.value);
Spaces {
before: &[],
item: Expr::UnaryOp(arena.alloc(Loc::at(inner.region, inner_lifted.item)), *op),
after: inner_lifted.after,
}
}
Expr::MalformedIdent(_, _)
| Expr::MalformedSuffixed(_)
| Expr::PrecedenceConflict(_)
| Expr::EmptyRecordBuilder(_)
| Expr::SingleFieldRecordBuilder(_)
| Expr::OptionalFieldInRecordBuilder(_, _) => Spaces {
before: &[],
item: *expr,
after: &[],
}, // _ => Spaces {
// before: &[],
// item: *expr,
// after: &[],
// },
}
}
pub fn expr_lift_spaces_before<'a, 'b: 'a>(
parens: Parens,
arena: &'a Bump,
expr: &Expr<'b>,
) -> SpacesBefore<'a, Expr<'a>> {
let lifted = expr_lift_spaces(parens, arena, expr);
SpacesBefore {
before: lifted.before,
item: lifted.item.maybe_after(arena, lifted.after),
}
}
pub fn expr_lift_spaces_after<'a, 'b: 'a>(
parens: Parens,
arena: &'a Bump,
expr: &Expr<'b>,
) -> SpacesAfter<'a, Expr<'a>> {
let lifted = expr_lift_spaces(parens, arena, expr);
SpacesAfter {
item: lifted.item.maybe_before(arena, lifted.before),
after: lifted.after,
}
}
pub fn merge_spaces_conservative<'a>(
arena: &'a Bump,
a: &'a [CommentOrNewline<'a>],
b: &'a [CommentOrNewline<'a>],
) -> &'a [CommentOrNewline<'a>] {
if a.is_empty() {
b
} else if b.is_empty() {
a
} else {
let mut merged = Vec::with_capacity_in(a.len() + b.len(), arena);
merged.extend_from_slice(a);
let mut it = b.iter();
for item in it.by_ref() {
if item.is_comment() {
merged.push(*item);
break;
}
}
merged.extend(it);
merged.into_bump_slice()
}
}
fn fmt_binops<'a>(
buf: &mut Buf,
lefts: &'a [(Loc<Expr<'a>>, Loc<BinOp>)],
loc_right_side: &'a Loc<Expr<'a>>,
indent: u16,
) {
let is_multiline = loc_right_side.value.is_multiline()
|| lefts.iter().any(|(expr, _)| expr.value.is_multiline());
for (loc_left_side, loc_binop) in lefts {
let binop = loc_binop.value;
let lifted_left_side =
expr_lift_spaces(Parens::InOperator, buf.text.bump(), &loc_left_side.value);
format_spaces(buf, lifted_left_side.before, Newlines::Yes, indent);
buf.indent(indent);
let line_indent = buf.cur_line_indent();
let need_parens = matches!(lifted_left_side.item, Expr::BinOps(..))
|| starts_with_unary_minus(lifted_left_side.item)
|| (ends_with_closure(&lifted_left_side.item) && line_indent < indent);
if need_parens {
fmt_parens(&lifted_left_side.item, buf, indent);
} else {
lifted_left_side.item.format_with_options(
buf,
Parens::InOperator,
Newlines::Yes,
indent,
);
}
format_spaces(buf, lifted_left_side.after, Newlines::Yes, indent);
if is_multiline {
buf.ensure_ends_with_newline();
buf.indent(indent);
} else {
buf.spaces(1);
}
push_op(buf, binop);
buf.spaces(1);
}
let lifted_right_side =
expr_lift_spaces(Parens::InOperator, buf.text.bump(), &loc_right_side.value);
format_spaces(buf, lifted_right_side.before, Newlines::Yes, indent);
let need_parens = matches!(lifted_right_side.item, Expr::BinOps(..))
|| starts_with_unary_minus(lifted_right_side.item);
if need_parens {
fmt_parens(&lifted_right_side.item, buf, indent);
} else {
lifted_right_side
.item
.format_with_options(buf, Parens::InOperator, Newlines::Yes, indent);
}
format_spaces(buf, lifted_right_side.after, Newlines::Yes, indent);
}
fn ends_with_closure(item: &Expr<'_>) -> bool {
match item {
Expr::Closure(..) => true,
Expr::UnaryOp(inner, _) => ends_with_closure(&inner.value),
Expr::Apply(expr, args, _) => args
.last()
.map(|a| ends_with_closure(&a.value))
.unwrap_or_else(|| ends_with_closure(&expr.value)),
_ => false,
}
}
fn starts_with_unary_minus(item: Expr<'_>) -> bool {
match item {
Expr::UnaryOp(
_,
Loc {
value: UnaryOp::Negate,
..
},
) => true,
Expr::SpaceAfter(expr, _) | Expr::SpaceBefore(expr, _) => starts_with_unary_minus(*expr),
Expr::Apply(expr, _args, _) => starts_with_unary_minus(expr.value),
Expr::BinOps(lefts, _right) => lefts
.first()
.map_or(false, |(expr, _)| starts_with_unary_minus(expr.value)),
_ => false,
}
}
pub fn format_spaces(buf: &mut Buf, spaces: &[CommentOrNewline], newlines: Newlines, indent: u16) {
match newlines {
Newlines::Yes => {
fmt_spaces(buf, spaces.iter(), indent);
}
Newlines::No => {
fmt_comments_only(buf, spaces.iter(), NewlineAt::Bottom, indent);
}
}
}
fn is_when_patterns_multiline(when_branch: &WhenBranch) -> bool {
let patterns = when_branch.patterns;
let (first_pattern, rest) = patterns.split_first().unwrap();
let is_multiline_patterns = if let Some((last_pattern, inner_patterns)) = rest.split_last() {
!first_pattern.value.extract_spaces().after.is_empty()
|| !last_pattern.value.extract_spaces().before.is_empty()
|| inner_patterns.iter().any(|p| {
let spaces = p.value.extract_spaces();
!spaces.before.is_empty() || !spaces.after.is_empty()
})
} else {
false
};
is_multiline_patterns
}
fn fmt_if_or_when_condition<'a>(buf: &mut Buf, loc_condition: &'a Loc<Expr<'a>>, indent: u16) {
let is_multiline_condition = loc_condition.is_multiline();
if is_multiline_condition {
let condition = expr_lift_spaces(Parens::NotNeeded, buf.text.bump(), &loc_condition.value);
fmt_comments_only(
buf,
condition.before.iter(),
NewlineAt::Both,
indent + INDENT,
);
buf.ensure_ends_with_newline();
condition.item.format(buf, indent + INDENT);
if condition.after.iter().any(|s| s.is_newline()) {
buf.ensure_ends_with_newline();
}
fmt_comments_only(
buf,
condition.after.iter(),
NewlineAt::Bottom,
indent + INDENT,
);
buf.ensure_ends_with_newline();
buf.indent(indent);
} else {
buf.spaces(1);
loc_condition.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent);
buf.spaces(1);
}
}
fn fmt_when<'a>(
buf: &mut Buf,
loc_condition: &'a Loc<Expr<'a>>,
branches: &[&'a WhenBranch<'a>],
indent: u16,
) {
buf.ensure_ends_with_newline();
buf.indent(indent);
buf.push_str("when");
fmt_if_or_when_condition(buf, loc_condition, indent);
buf.push_str("is");
buf.newline();
let mut last_after: &[CommentOrNewline] = &[];
let mut prev_branch_was_multiline = false;
for (branch_index, branch) in branches.iter().enumerate() {
let expr = &branch.value;
let patterns = &branch.patterns;
let is_multiline_expr = expr.is_multiline();
let is_multiline_patterns = is_when_patterns_multiline(branch);
for (pattern_index, pattern) in patterns.iter().enumerate() {
if pattern_index == 0 {
let pattern_lifted = pattern_lift_spaces(buf.text.bump(), &pattern.value);
let before = merge_spaces(buf.text.bump(), last_after, pattern_lifted.before);
if !before.is_empty() {
let added_blank_line;
if branch_index > 0 // Never render newlines before the first branch.
&& matches!(before.first(), Some(CommentOrNewline::Newline))
{
if prev_branch_was_multiline {
// Multiline branches always get a full blank line after them.
buf.ensure_ends_with_blank_line();
added_blank_line = true;
} else {
buf.ensure_ends_with_newline();
added_blank_line = false;
}
} else {
added_blank_line = false;
}
// Write comments (which may have been attached to the previous
// branch's expr, if there was a previous branch).
fmt_comments_only(buf, before.iter(), NewlineAt::Bottom, indent + INDENT);
if branch_index > 0 {
if prev_branch_was_multiline && !added_blank_line {
// Multiline branches always get a full blank line after them
// (which we may already have added before a comment).
buf.ensure_ends_with_blank_line();
} else {
buf.ensure_ends_with_newline();
}
}
fmt_pattern(
buf,
&pattern_lifted.item,
indent + INDENT,
Parens::NotNeeded,
);
} else {
if branch_index > 0 {
if prev_branch_was_multiline {
// Multiline branches always get a full blank line after them.
buf.ensure_ends_with_blank_line();
} else {
buf.ensure_ends_with_newline();
}
}
fmt_pattern(
buf,
&pattern_lifted.item,
indent + INDENT,
Parens::NotNeeded,
);
}
if !pattern_lifted.after.is_empty() {
if starts_with_inline_comment(pattern_lifted.after.iter()) {
buf.spaces(1);
}
if !pattern_lifted.item.is_multiline()
&& pattern_lifted.after.iter().all(|s| s.is_newline())
{
fmt_comments_only(
buf,
pattern_lifted.after.iter(),
NewlineAt::Bottom,
indent,
)
} else {
fmt_spaces(buf, pattern_lifted.after.iter(), indent);
}
}
} else {
if is_multiline_patterns {
buf.ensure_ends_with_newline();
buf.indent(indent + INDENT);
buf.push('|');
} else {
buf.push_str(" |");
}
buf.spaces(1);
fmt_pattern(buf, &pattern.value, indent + INDENT, Parens::NotNeeded);
}
}
if let Some(guard_expr) = &branch.guard {
buf.indent(indent + INDENT);
buf.push_str(" if");
buf.spaces(1);
guard_expr.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent + INDENT);
}
buf.indent(indent + INDENT);
let line_indent = buf.cur_line_indent();
buf.push_str(" ->");
let inner_indent = line_indent + INDENT;
let expr = expr_lift_spaces(Parens::NotNeeded, buf.text.bump(), &expr.value);
fmt_spaces_no_blank_lines(buf, expr.before.iter(), inner_indent);
if is_multiline_expr {
buf.ensure_ends_with_newline();
} else {
buf.spaces(1);
}
// expr.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, inner_indent);
format_expr_only(
&expr.item,
buf,
Parens::NotNeeded,
Newlines::Yes,
inner_indent,
);
last_after = expr.after;
prev_branch_was_multiline = is_multiline_expr || is_multiline_patterns;
}
if !last_after.is_empty() {
format_spaces(buf, last_after, Newlines::Yes, indent);
}
}
fn fmt_dbg_stmt<'a>(
buf: &mut Buf,
condition: &'a Loc<Expr<'a>>,
extra_args: &'a [&'a Loc<Expr<'a>>],
continuation: &'a Loc<Expr<'a>>,
parens: Parens,
indent: u16,
) {
buf.ensure_ends_with_newline();
let mut args = Vec::with_capacity_in(extra_args.len() + 1, buf.text.bump());
args.push(condition);
args.extend_from_slice(extra_args);
Expr::Apply(
&Loc::at_zero(Expr::Dbg),
args.into_bump_slice(),
called_via::CalledVia::Space,
)
.format_with_options(buf, parens, Newlines::Yes, indent);
let cont_lifted = expr_lift_spaces(Parens::NotNeeded, buf.text.bump(), &continuation.value);
if !cont_lifted.before.is_empty() {
format_spaces(buf, cont_lifted.before, Newlines::Yes, indent);
}
// Always put a newline after the `dbg` line(s)
buf.ensure_ends_with_newline();
format_expr_only(
&cont_lifted.item,
buf,
Parens::NotNeeded,
Newlines::Yes,
indent,
);
if !cont_lifted.after.is_empty() {
format_spaces(buf, cont_lifted.after, Newlines::Yes, indent);
}
}
fn fmt_return<'a>(
buf: &mut Buf,
return_value: &'a Loc<Expr<'a>>,
after_return: &Option<&'a Loc<Expr<'a>>>,
parens: Parens,
newlines: Newlines,
indent: u16,
) {
buf.ensure_ends_with_newline();
buf.indent(indent);
buf.push_str(keyword::RETURN);
let return_indent = if return_value.is_multiline() {
indent + INDENT
} else {
indent
};
let value = expr_lift_spaces(parens, buf.text.bump(), &return_value.value);
if !value.before.is_empty() {
format_spaces(buf, value.before, newlines, return_indent);
}
if matches!(value.item, Expr::Defs(..)) {
buf.ensure_ends_with_newline();
} else {
buf.spaces(1);
}
format_expr_only(&value.item, buf, parens, newlines, return_indent);
if !value.after.is_empty() {
format_spaces(buf, value.after, newlines, indent);
}
if let Some(after_return) = after_return {
let lifted = expr_lift_spaces(Parens::NotNeeded, buf.text.bump(), &after_return.value);
if lifted.before.is_empty() {
buf.ensure_ends_with_newline();
} else {
fmt_spaces(buf, lifted.before.iter(), indent);
}
lifted
.item
.format_with_options(buf, parens, newlines, indent);
fmt_spaces(buf, lifted.after.iter(), indent);
} else if parens != Parens::NotNeeded {
buf.ensure_ends_with_newline();
}
}
fn fmt_if<'a>(
buf: &mut Buf,
branches: &'a [(Loc<Expr<'a>>, Loc<Expr<'a>>)],
final_else: &'a Loc<Expr<'a>>,
is_multiline: bool,
indented_else: bool,
indent: u16,
) {
// let is_multiline_then = loc_then.is_multiline();
// let is_multiline_else = final_else.is_multiline();
// let is_multiline_condition = loc_condition.is_multiline();
// let is_multiline = is_multiline_then || is_multiline_else || is_multiline_condition;
let return_indent = if is_multiline {
indent + INDENT
} else {
indent
};
for (i, (loc_condition, loc_then)) in branches.iter().enumerate() {
buf.indent(indent);
if i > 0 {
buf.push_str("else");
buf.spaces(1);
}
buf.push_str("if");
fmt_if_or_when_condition(buf, loc_condition, indent);
buf.push_str("then");
if is_multiline {
let then = expr_lift_spaces(Parens::NotNeeded, buf.text.bump(), &loc_then.value);
fmt_comments_only(buf, then.before.iter(), NewlineAt::Both, return_indent);
buf.ensure_ends_with_newline();
then.item.format(buf, return_indent);
if then.after.iter().any(|s| s.is_newline()) {
buf.ensure_ends_with_newline();
}
fmt_comments_only(buf, then.after.iter(), NewlineAt::Bottom, return_indent);
buf.ensure_ends_with_newline();
} else {
buf.push_str("");
buf.spaces(1);
loc_then.format(buf, return_indent);
}
}
if indented_else {
buf.indent(indent + INDENT);
buf.push_str("else");
buf.newline();
buf.newline();
} else if is_multiline {
buf.indent(indent);
buf.push_str("else");
buf.newline();
} else {
buf.indent(indent);
buf.push_str(" else");
buf.spaces(1);
}
let indent = if indented_else { indent } else { return_indent };
final_else.format(buf, indent);
}
fn fmt_closure<'a>(
buf: &mut Buf,
loc_patterns: &'a [Loc<Pattern<'a>>],
loc_ret: &'a Loc<Expr<'a>>,
indent: u16,
) {
use self::Expr::*;
buf.indent(indent);
buf.push('\\');
let arguments_are_multiline = loc_patterns
.iter()
.any(|loc_pattern| loc_pattern.is_multiline());
// If the arguments are multiline, go down a line and indent.
let indent = if arguments_are_multiline {
indent + INDENT
} else {
indent
};
let mut first = true;
for loc_pattern in loc_patterns.iter() {
if !first {
buf.indent(indent);
if arguments_are_multiline {
buf.push(',');
buf.newline();
} else {
buf.push_str(",");
buf.spaces(1);
}
}
first = false;
let arg = pattern_lift_spaces(buf.text.bump(), &loc_pattern.value);
if !arg.before.is_empty() {
fmt_comments_only(buf, arg.before.iter(), NewlineAt::Bottom, indent)
}
arg.item
.format_with_options(buf, Parens::InClosurePattern, Newlines::No, indent);
if !arg.after.is_empty() {
if starts_with_inline_comment(arg.after.iter()) {
buf.spaces(1);
}
fmt_comments_only(buf, arg.after.iter(), NewlineAt::Bottom, indent)
}
}
if arguments_are_multiline {
buf.ensure_ends_with_newline();
buf.indent(indent);
} else {
buf.spaces(1);
}
let arrow_line_indent = buf.cur_line_indent();
buf.push_str("->");
buf.spaces(1);
let is_multiline = loc_ret.value.is_multiline();
// If the body is multiline, go down a line and indent.
let body_indent = if is_multiline {
arrow_line_indent + INDENT
} else {
indent
};
if is_multiline {
match &loc_ret.value {
SpaceBefore(sub_expr, spaces) => {
let should_outdent = match sub_expr {
Record { .. } | List { .. } => {
let is_only_newlines = spaces.iter().all(|s| s.is_newline());
is_only_newlines && sub_expr.is_multiline()
}
_ => false,
};
if should_outdent {
sub_expr.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent);
} else {
loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, body_indent);
}
}
Record { .. } | List { .. } => {
loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, indent);
}
_ => {
buf.ensure_ends_with_newline();
loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, body_indent);
}
}
} else {
loc_ret.format_with_options(buf, Parens::NotNeeded, Newlines::Yes, body_indent);
}
}
enum RecordPrefix<'a> {
Update(&'a Loc<Expr<'a>>),
Mapper(&'a Loc<Expr<'a>>),
}
fn fmt_record_like<'a, 'b: 'a, Field, ToSpacesAround>(
buf: &'a mut Buf,
prefix: Option<RecordPrefix<'b>>,
fields: Collection<'b, Loc<Field>>,
indent: u16,
to_space_around: ToSpacesAround,
) where
Field: Formattable + std::fmt::Debug,
ToSpacesAround: Fn(&'a Bump, &'b Field) -> Spaces<'a, Field>,
{
let loc_fields = fields.items;
let final_comments = fields.final_comments();
buf.indent(indent);
if loc_fields.is_empty() && final_comments.is_empty() && prefix.is_none() {
buf.push_str("{}");
} else {
buf.push('{');
match prefix {
None => {}
// We are presuming this to be a Var()
// If it wasnt a Var() we would not have made
// it this far. For example "{ 4 & hello = 9 }"
// doesnt make sense.
Some(RecordPrefix::Update(record_var)) => {
buf.spaces(1);
record_var.format(buf, indent);
buf.indent(indent);
buf.push_str(" &");
}
Some(RecordPrefix::Mapper(mapper_var)) => {
buf.spaces(1);
mapper_var.format(buf, indent);
buf.indent(indent);
buf.push_str(" <-");
}
}
let is_multiline = loc_fields.iter().any(|loc_field| loc_field.is_multiline())
|| !final_comments.is_empty();
if is_multiline {
let field_indent = indent + INDENT;
let mut last_after: &[CommentOrNewline<'_>] = &[];
for (iter, field) in loc_fields.iter().enumerate() {
// comma addition is handled by the `format_field_multiline` function
// since we can have stuff like:
// { x # comment
// , y
// }
// In this case, we have to move the comma before the comment.
let field_lifted = to_space_around(buf.text.bump(), &field.value);
let before = merge_spaces(buf.text.bump(), last_after, field_lifted.before);
if iter == 0 || count_leading_newlines(before.iter()) == 0 {
buf.ensure_ends_with_newline();
}
let newline_mode = if iter == 0 {
if loc_fields.len() == 1 {
SpacesNewlineMode::SkipNewlinesAtBoth
} else {
SpacesNewlineMode::SkipNewlinesAtStart
}
} else {
SpacesNewlineMode::Normal
};
fmt_spaces_with_newline_mode(buf, before, field_indent, newline_mode);
field_lifted.item.format_with_options(
buf,
Parens::NotNeeded,
Newlines::No,
field_indent,
);
buf.indent(field_indent);
buf.push_str(",");
last_after = field_lifted.after;
}
let after = merge_spaces(buf.text.bump(), last_after, final_comments);
if count_leading_newlines(after.iter()) == 0 {
buf.ensure_ends_with_newline();
}
fmt_spaces_with_newline_mode(
buf,
after,
field_indent,
SpacesNewlineMode::SkipNewlinesAtEnd,
);
buf.ensure_ends_with_newline();
} else {
// is_multiline == false
buf.spaces(1);
let field_indent = indent;
let mut iter = loc_fields.iter().peekable();
while let Some(field) = iter.next() {
field.format_with_options(buf, Parens::NotNeeded, Newlines::No, field_indent);
if iter.peek().is_some() {
buf.push_str(",");
buf.spaces(1);
}
}
buf.spaces(1);
// if we are here, that means that `final_comments` is empty, thus we don't have
// to add a comment. Anyway, it is not possible to have a single line record with
// a comment in it.
};
// closes the initial bracket
buf.indent(indent);
buf.push('}');
}
}
fn assigned_field_to_spaces<'a, 'b: 'a, T: Copy>(
arena: &'a Bump,
field: &'b AssignedField<'b, T>,
) -> Spaces<'a, AssignedField<'a, T>> {
match field {
AssignedField::SpaceBefore(sub_field, spaces) => {
let mut inner = assigned_field_to_spaces(arena, sub_field);
inner.before = merge_spaces(arena, spaces, inner.before);
inner
}
AssignedField::SpaceAfter(sub_field, spaces) => {
let mut inner = assigned_field_to_spaces(arena, sub_field);
inner.after = merge_spaces(arena, inner.after, spaces);
inner
}
_ => Spaces {
before: &[],
item: *field,
after: &[],
},
}
}
pub fn sub_expr_requests_parens(expr: &Expr<'_>) -> bool {
match expr {
Expr::BinOps(left_side, _) => {
left_side
.iter()
.any(|(_, loc_binop)| match loc_binop.value {
BinOp::Caret
| BinOp::Star
| BinOp::Slash
| BinOp::DoubleSlash
| BinOp::Percent
| BinOp::Plus
| BinOp::Minus
| BinOp::Equals
| BinOp::NotEquals
| BinOp::LessThan
| BinOp::GreaterThan
| BinOp::LessThanOrEq
| BinOp::GreaterThanOrEq
| BinOp::And
| BinOp::Or
| BinOp::Pizza
| BinOp::DoubleQuestion => true,
})
}
Expr::If { .. } => true,
Expr::Defs(_, _) => true,
Expr::Return(..) | Expr::DbgStmt { .. } => {
// This is because e.g. (return x)\nfoo would be de-parenthesized and cause the `after_return` to be `foo`.
// That _is_ a semantic change technically right now, because that transform is done in the parser.
// When that's moved to `can`, we can remove this
true
}
Expr::SpaceBefore(e, _) => sub_expr_requests_parens(e),
Expr::SpaceAfter(e, _) => sub_expr_requests_parens(e),
_ => false,
}
}